# acttrader-charts

A performant, zero-dependency stock charting library built on Canvas 2D.
Dual ESM/CJS output, TypeScript-first.

**Features:** 13 chart series types · 30+ indicators (overlay + sub-pane) · 80+ drawing tools · Trade-From-Chart (TFC) with drag-to-modify levels · Real-time WebSocket streaming · Dark & Light themes · Fully configurable labels for i18n · Fullscreen support

---

## Installation

```bash
npm install acttrader-charts
```

### Beta releases

Pre-release builds are published under the `beta` dist-tag and use prerelease semver (`X.Y.Z-beta.N`). They never move the default `latest` tag, so existing consumers are unaffected.

```bash
npm install acttrader-charts@beta          # opt in to the latest beta
npm install acttrader-charts@1.1.0-beta.1  # pin a specific beta
```

`npm install acttrader-charts` and existing semver ranges (`^1.0.0`, `~1.0.0`) continue to resolve only to stable releases — by spec, semver ranges exclude prerelease versions.

---

## Basic Usage

```ts
import { ChartEngine } from 'acttrader-charts';

const chart = new ChartEngine({
    container: document.getElementById('chart')!,
    theme:     'dark',
    series:    'candlestick',
    symbol:    'XAUUSD',
    timeframe: '1D',
    duration:  '1Y',
});

chart.loadData(ohlcvBars); // OHLCVBar[]
```

Load OHLCV data and the chart is ready. All UI (top bar, bottom bar, drawing toolbar) is rendered automatically unless `showUI: false`.

### Respond to duration / timeframe changes

```ts
chart.on('durationChange', ({ duration, timeframe }) => {
    fetchBars(timeframe, duration).then(bars => chart.loadData(bars));
});
```

### Programmatic data loading via `dataLoader`

```ts
const chart = new ChartEngine({
    container,
    dataLoader: async ({ start, end, interval }) => {
        const res = await fetch(`/api/bars?from=${start.getTime()}&to=${end.getTime()}&tf=${interval}`);
        return res.json(); // OHLCVBar[]
    },
});
// No manual loadData() needed — the engine calls dataLoader on start and on timeframe/duration changes.
```

If the first fetch returns fewer than `minInitialBars` (default `10`), the engine automatically widens the lookback window and calls `dataLoader` again — up to the `maxLookbackMs` ceiling (default 365 days). This keeps the chart useful on weekends, holidays, or for instruments that just listed. If retries still yield zero bars, the engine shows a **"No data available"** overlay (customise the text via `labels.chart.noData`).

---

## Advanced Usage

### Adding Indicators

```ts
import { SMA, EMA, RSI } from 'acttrader-charts';

chart.addIndicator(new SMA(20, '#f59e0b'));      // overlay
chart.addIndicator(new RSI(14, '#818cf8'));      // sub-pane
chart.removeIndicator('SMA');
```

Indicator pills appear inline above the chart (up to 2 visible; beyond that collapses into a dropdown).

### Drawing Tools

```ts
chart.setDrawingTool('trendLine');   // activate
chart.setDrawingTool(null);          // back to pointer mode
```

Keyboard shortcuts: `Escape` cancel / deselect · `Delete`/`Backspace` remove selected · `Ctrl+Z` undo · `Ctrl+Y` redo

### Real-time Streaming

```ts
import { WebSocketAdapter } from 'acttrader-charts';

const adapter = new WebSocketAdapter(
    'wss://feed.example.com/ticks',
    (msg) => ({ time: msg.t, bid: msg.p, ask: msg.p, volume: msg.v }),
);

chart.connectStream(adapter);
// chart.disconnectStream();
```

Auto-reconnects with exponential backoff (2 s → 30 s). Stream status is reflected in the top-bar dot and emitted via `streamStatus`.

To push ticks directly without an adapter (e.g. from your own WebSocket handler):

```ts
chart.pushTick({ time: Date.now(), bid: 1.2055, ask: 1.2057, volume: 120 });
```

### Trade From Chart (TFC) — `setLevels`

TFC renders draggable price levels for open positions and pending orders.
The library is broker-agnostic: it draws the lines and emits typed events after the user confirms; your app owns the API calls.

**Pending order** (dashed line, draggable with optional SL/TP brackets):

```ts
chart.setLevels(
    [{
        label:           'ORD-1',
        price:           1.2050,
        side:            'buy',
        orderType:       'limit',
        lots:            1.0,
        stopLossPrice:   1.1980,
        takeProfitPrice: 1.2150,
        text:            'Buy Limit',
    }],
    'label',   // unique key field
    'price',   // price field
    'pending', // level type
);
```

**Open position** (solid line + live P&L + optional SL/TP brackets):

```ts
chart.setLevels(
    [{
        label:           'POS-1',
        price:           1.2010,
        pnl:             42.5,
        pnlText:         '+$42.50',
        side:            'buy',
        stopLossPrice:   1.1960,
        takeProfitPrice: 1.2120,
    }],
    'label', 'price', 'position',
);
```

**Bracket orders via `ToClose`** — link a separate SL/TP pending order to a parent level:

```ts
chart.setLevels([
    { label: 'POS-1',  price: 1.2010, side: 'buy',  type: 'position' },
    { label: 'ORD-SL', price: 1.1960, side: 'sell', orderType: 'stop', ToClose: 'POS-1' },
], 'label', 'price', 'pending');
```

**Remove levels:**

```ts
chart.removeLevelByLabel('ORD-1');
chart.setLevels([], 'label', 'price', 'pending'); // clear all of one type
```

**Visual differentiation:** pending orders and ES/EL entry working orders render as **dashed** lines tinted by side (`pendingBuyLine` green / `pendingSellLine` red). True open positions render as **solid** lines: green/red when `pnl` is set (sign-colored), otherwise `positionLine` (purple/indigo). The info-box border matches the line color. Each true open position also gets a small colored price tag on the right-side price axis showing the entry price — same visual language as the Bid/Ask tag — so you can read the entry price without hunting for the info box.

**Brackets follow entry on drag:** when the entry line of a pending order, draft order, or an open position with `entryPriceEditable: true` is dragged, any existing `stopLossPrice` / `takeProfitPrice` brackets translate along with it by the same price delta. The distance between entry and each bracket is whatever the user currently sees — if they manually drag SL or TP to a new price, that new distance becomes the anchor for the next entry drag. Missing brackets aren't auto-created. On confirm, `tradeLevelEdit` carries all translated fields together in one `changes[]` array; on mobile (`hideLevelConfirmCancel`) the three changes are applied as one atomic event.

**Bracket pill auto-offset:** when an SL or TP price sits within about one pill-height of the entry price, the bracket's label pill is pushed vertically away from the entry pill and connected back to its real price line by a dashed leader. The horizontal bracket line itself stays at the true price; only the pill and its `×` button move, and drag targets follow the displaced pill — so grabbing the pill always drags the bracket, grabbing the entry pill always drags the entry, and grabbing the real bracket line still drags the bracket. Works for both buy and sell orders without configuration.

**TFC events:**

```ts
// Confirmed drag of a pending order entry
chart.on('tradeLevelEdit', ({ label, type, data, changes, newLots, isFullscreen }) => {
    for (const c of changes) {
        // `c.newLots` is present on the MAIN change when qty was edited this session.
        // `data.lots` is also overridden with the new qty for convenience.
        if (c.field === 'MAIN') myApi.modifyOrder(label, { price: c.newPrice, qty: c.newLots ?? (data as any).lots });
        if (c.field === 'SL')   myApi.modifyOrder(label, { stopLoss: c.newPrice });
        if (c.field === 'TP')   myApi.modifyOrder(label, { takeProfit: c.newPrice });
    }
    // isFullscreen — show a toast only when the chart is covering the full screen
    if (isFullscreen) showToast(`Order ${label} updated`);
});

// User clicked × on a level
chart.on('tradeLevelClose', ({ label, type, action, isFullscreen }) => {
    if (type === 'pending')  myApi.cancelOrder(label);
    if (type === 'position') myApi.closePosition(label);
    if (isFullscreen) showToast(`${action}: ${label}`);
});

// Live price during drag (fires on every mouse-move — throttle if needed)
chart.on('tradeLevelDrag', ({ label, newPrice, bracketType, isFullscreen }) => {
    updatePricePreview(label, newPrice);
});

// Live qty while the user edits the QTY pill flyout — fires before the level edit is
// confirmed, so host-side Estimated PNL for SL/TP brackets can recompute immediately
// instead of waiting for `tradeLevelEdit` at ✓ confirm. `type` is `'draft'` for draft
// orders, otherwise the parent level's type.
chart.on('tradeLevelQtyChange', ({ label, type, newLots, previousLots }) => {
    updateEstimatedPnl(label, newLots);
});
```

**Off-viewport level indicators:**

When a trade level's entry, SL, or TP price lies outside the visible price range, a small pill (`▲ N` / `▼ N`) appears near the right edge of the chart showing the count of off-screen markers per direction. Clicking the pill smooth-scrolls the nearest off-viewport marker to the vertical center of the chart. This requires no configuration — it activates automatically whenever levels are present.

**Drag constraints (enforced live):**

| Order side | Stop Loss | Take Profit |
|---|---|---|
| `buy`  | must stay ≤ order price | must stay ≥ order price |
| `sell` | must stay ≥ order price | must stay ≤ order price |

**Mobile mode — `hideLevelConfirmCancel: true`:**

When the on-canvas ✓/✗ buttons are hidden (mobile native UI), the chart adapts its interaction model:

- **Tap a trade level line** → `tradeLevelEditOpen` fires immediately (the whole line acts as the edit button; no pencil icon required).
- **Tap empty canvas area while a level is selected** → edit is dismissed and any pending drag changes are reverted (equivalent to pressing ✗).
- **Drag a SL/TP bracket and release** → `tradeLevelEdit` fires automatically without needing a ✓ button tap.

**Market orders — BID/ASK spread zone:**

When `enableTrading` is on and live BID/ASK data is streaming, hovering / activating the trade crosshair button while the cursor price is inside the spread shows **"Buy Market"** and **"Sell Market"** rows instead of the standard Limit/Stop rows. Tapping either row calls `showMarketDraft()` internally and emits `draftInitiated` with `orderType: 'market'`.

---

## Default Configuration

### `ChartConfig`

| Property | Required | Default | Description |
|---|:---:|---|---|
| `container` | ✓ | — | Host `HTMLElement` |
| `theme` | | `"dark"` | `"dark"` or `"light"` |
| `series` | | `"candlestick"` | Initial chart type |
| `showVolume` | | `true` | Show volume overlay |
| `showUI` | | `true` | Render top / bottom / left bars. When `false`, the loading overlay is also suppressed (mobile wrappers provide their own) |
| `showDrawingTools` | | `true` | Show drawing toolbar and pencil button |
| `showFullscreenButton` | | `true` | Show the fullscreen toggle button in the top bar. Set to `false` to hide it entirely. Mobile wrappers (Android / iOS) default this to `false` |
| `timeframe` | | `"1D"` | Initial timeframe |
| `duration` | | — | Initial active duration button |
| `symbol` | | — | Symbol name shown in the top bar |
| `isins` | | — | Symbol list for the picker modal |
| `onIsinSelect` | | — | Called when user picks a symbol from the picker modal |
| `padding` | | `{top:8,right:0,bottom:0,left:0}` | Canvas padding (px) |
| `minLots` | | `1` | Default lot size in the trade popover |
| `tickActivityMs` | | `30000` | ms the stream dot stays green after last tick |
| `maxCandles` | | `200` | Max bars fetched per data-load request |
| `minInitialBars` | | `10` | If `dataLoader` returns fewer bars, the fetch window auto-widens and retries (handles weekends, market closures, and sparse symbols) |
| `maxLookbackMs` | | `31_536_000_000` | Hard ceiling on auto-widening lookback (ms). Retries stop once the window reaches this. Default: 365 days |
| `prefetchThreshold` | | `80` | Bars from start of data at which historical fetch triggers (min 20) |
| `mobileBarDivisor` | | `2` | Divide desktop visible bar count on touch devices (`2`, `3`, or `4`) |
| `momentumScrollEnabled` | | `true` | Enable momentum (kinetic) scrolling — chart coasts after a fast flick |
| `momentumDecay` | | `0.95` | Per-frame velocity decay, normalised to 60 fps. Clamped `[0.80, 0.99]`. Lower = stops faster |
| `momentumThreshold` | | `0.3` | Min release velocity (px/ms) to launch momentum. Raise to require a faster flick |
| `momentumMaxVelocity` | | `6.0` | Max launch velocity (px/ms) — caps hard-flick speed |
| `targetCandleWidth` | | `10` | Target px width per candle for auto-calculating initial bar count |
| `durationTimeframeMap` | | *(see below)* | Override duration → timeframe pairings |
| `dataLoader` | | — | `(params) => Promise<OHLCVBar[]>` auto-called on load / change |
| `themeOverrides` | | — | Deep-partial color overrides applied on top of the built-in dark/light themes |
| `uiConfig` | | `DEFAULT_UI_CONFIG` | Deep-partial size / font overrides per component |
| `labels` | | `DEFAULT_LABELS` | Deep-partial string overrides for i18n/translation |
| `tickClosePriceSource` | | `"bid"` | Which quote side drives live tick close/high/low (`"bid"` or `"ask"`) |
| `showBidAskLines` | | `false` | Show both bid and ask as dashed lines during a live stream |
| `showActLogo` | | `false` | Show the ACT watermark logo in the bottom-left corner |
| `showCandleCountdown` | | `true` | Show countdown timer on the live candle (time axis) |
| `candleCountdownTimeframes` | | intraday only | Timeframes where the countdown appears; `'all'` or `Timeframe[]` |
| `showPriceAxisCountdown` | | `false` | Show candle countdown timer on the right price axis, just below the live price tag. Honours `candleCountdownTimeframes`. Toggleable from the Settings dialog (Appearance tab). |
| `maxSubPanes` | | `3` | Max simultaneous oscillator sub-panes |
| `tradeDisplayFilter` | | `"all"` | Which TFC levels are visible: `"all"` · `"positions"` · `"orders"` · `"none"` |
| `positionRenderStyle` | | auto | Force position render style: `"line"` or `"dot"` |
| `hideLevelConfirmCancel` | | `false` | Hide on-canvas ✓/✗ confirm-cancel buttons for TFC level edits |
| `deselectActiveOnOutsideClick` | | `false` | When `true`, clicking/tapping anywhere outside a selected trade level dismisses it (reverting any pending edits, mirroring ✗ Cancel). Default `false` keeps the level active so incidental clicks — price-axis resize, taps outside the QTY input — don't drop an in-progress edit. The level can still be dismissed via ✓/✗, tapping the level again, or `setLevels()` removing it |
| `showTradeLevelsAlways` | | `false` | Always render SL/TP bracket lines + price pills, even when the parent level is not hovered or selected. The close (×) button stays hover-only so the chart isn't cluttered. Toggleable from the Settings dialog (Trading tab). Persisted in `localStorage`. |
| `tradeLevelButtonScale` | | `1` | Multiplier for trade-level Confirm/Cancel/Edit/Close button radii and gaps. Scales visuals **and** hit/drag areas together — raise it on touch devices for larger tap targets. Clamped to `[1, 3]`. Also settable at runtime via `chart.setTradeLevelButtonScale(scale)` |
| `tfcEnabled` | | `true` | Enable the TFC toggle button in the top bar. When `false`, TFC is completely disabled — the toggle button is hidden and all trade levels, draft orders, and the floating trade button are suppressed |
| `levelClusteringEnabled` | | `true` | Enable trade-level fan-out clustering; overlapping levels group into expandable badges |
| `clusterThresholdDistance` | | `20` | Pixel proximity threshold for clustering (only when `levelClusteringEnabled` is `true`) |
| `hideSymbolAndTick` | | `false` | Hide the symbol name and tick-activity (streaming) dot in the top-left overlay. Does **not** affect the OHLC(V) strip — use `hideOHLCV` for that |
| `hideOHLCV` | | `false` | Hide the OHLC(V) data strip (`O: H: L: C: V:`) in the top-left overlay. Independent of `hideSymbolAndTick` — set both to `true` to hide the entire overlay |
| `showBottomBar` | | `false` | Show the bottom duration-selector bar |
| `hideQtyButton` | | `false` | Hide the floating Qty input overlay on draft orders |
| `showQuantityField` | | `false` | Render an editable QTY pill at the left of the draft order info box. Clicking the pill opens a flyout input to edit the quantity before submitting |
| `quantityFieldConfig` | | — | Constraints for the draft order QTY field (only relevant when `showQuantityField` is `true`). Object: `{ minLots?: number, maxLots?: number }`. `minLots` also sets the initial quantity and the step value for the flyout input (default: `1`). `maxLots` caps the input (default: `100`) |
| `tradesThresholdForHorizontalLine` | | `2` | Level count above which render auto-switches to `"dot"` mode |
| `timezone` | | `"UTC"` | IANA timezone string for time-axis and crosshair labels. `"UTC"` (default), `"local"` (browser/device timezone), or any IANA string (`"America/New_York"`, `"Europe/London"`, etc.) |
| `canvasColors` | | — | Per-theme canvas background color overrides (persisted from Settings dialog) |
| `aggregateFrom` | | — | Fetch finer-grained data and aggregate client-side per timeframe |
| `onOrderSubmit` | | — | Called when user submits a trade via the floating button |
| `onLevelEdit` | | — | Called when user confirms a TFC level edit |
| `onLotsChange` | | — | Called when user changes qty on a draft order |
| `onLevelDragEnd` | | — | *(deprecated — use `onLevelEdit`)* |

### `OHLCVBar`

| Field | Required | Type | Description |
|---|:---:|---|---|
| `time` | ✓ | `number` | Unix ms timestamp |
| `open` | ✓ | `number` | Open price |
| `high` | ✓ | `number` | High price |
| `low` | ✓ | `number` | Low price |
| `close` | ✓ | `number` | Close price |
| `volume` | | `number` | Volume (omit to hide volume overlay) |

### Duration → Auto Timeframe Map

| Duration | Default timeframe |
|---|---|
| `1D` | `5m` |
| `5D` | `15m` |
| `1M` | `30m` |
| `3M` | `1h` |
| `6M` | `4h` |
| `1Y` | `1D` |
| `5Y` | `1W` |
| `All` | `1M` |

Override individual entries via `durationTimeframeMap`:

```ts
new ChartEngine({ ..., durationTimeframeMap: { '1Y': '4h', '5Y': '1D' } });
```

### `UiConfig` — sizes & fonts

All values are optional; unset keys fall back to `DEFAULT_UI_CONFIG`.

```ts
import { DEFAULT_UI_CONFIG } from 'acttrader-charts';
```

| Component key | Configurable properties |
|---|---|
| `drawingToolbar` | `iconBtnSize`, `iconFontSize`, `iconFontFamily`, `flyoutLabelFontSize`, `flyoutLabelFontFamily`, `shortcutFontSize`, `soonBadgeFontSize`, `flyoutHeadingFontSize`, `flyoutHeadingLetterSpacing`, `scrollBtnHeight`, `scrollBtnFontSize`, `barPadding`, `btnGap` |
| `topBar` | `height`, `dropBtnFontSize`, `dropBtnFontFamily`, `drawBtnSize`, `drawBtnIconFontSize`, `iconBtnSize`, `mobileIconBtnSize`, `mobileDrawBtnIconSize`, `flyoutRowFontSize`, `flyoutRowFontFamily`, `flyoutCategoryFontSize`, `flyoutCategoryLetterSpacing`, `flyoutCheckFontSize`, `streamDotSize` |
| `bottomBar` | `height`, `btnFontSize`, `btnFontFamily` |
| `priceAxis` | `fontSize`, `fontFamily` |
| `timeAxis` | `fontSize`, `fontFamily` |
| `crosshair` | `labelHeight`, `labelPaddingX`, `fontSize`, `fontFamily` |
| `indicatorOverlay` | `pillFontSize`, `pillFontFamily`, `dotFontSize`, `pillIconFontSize`, `collapseBtnFontSize`, `collapseBtnFontFamily`, `dropdownRowFontSize`, `dropdownRowFontFamily`, `dropdownIconFontSize` |
| `tradeButton` | `size`, `mobileSize`, `iconSize`, `mobileIconSize` |

**Mobile icon sizing:** When the chart container is narrower than `drawingToolbar.mobileBreakpoint` (default `480`), the top-bar icon buttons (settings, fullscreen, drawing toggle) and the floating trade ⊕ button use their mobile sizes automatically. Defaults: top-bar icon buttons grow `26px → 28px` (icon SVG +2px), trade button grows `22px → 24px` (icon `14px → 16px`). Override any of these values via `uiConfig.topBar.mobileIconBtnSize`, `uiConfig.topBar.mobileDrawBtnIconSize`, or `uiConfig.tradeButton.mobileSize` / `mobileIconSize`.

**Example:**

```ts
import { resolveUiConfig } from 'acttrader-charts';

new ChartEngine({
    container,
    uiConfig: {
        topBar:    { height: '40px', dropBtnFontSize: '13px' },
        priceAxis: { fontSize: '12px', fontFamily: 'Inter, sans-serif' },
    },
});
```

You can also import individual section defaults:

```ts
import {
    DEFAULT_UI_CONFIG,
    DEFAULT_TOP_BAR_CONFIG,
    DEFAULT_PRICE_AXIS_CONFIG,
    DEFAULT_CROSSHAIR_CONFIG,
    DEFAULT_TRADE_BUTTON_CONFIG,
    // ...
} from 'acttrader-charts';
```

### `ChartLabels` — i18n / translation

All user-visible strings can be overridden for translation. Only keys you supply are replaced; the rest fall back to `DEFAULT_LABELS` (English).

```ts
import { DEFAULT_LABELS, resolveLabels } from 'acttrader-charts';
```

Pass overrides directly in `ChartConfig`:

```ts
new ChartEngine({
    container,
    labels: {
        trade: { buy: 'Acheter', sell: 'Vendre', limit: 'Limite', stop: 'Stop' },
        ohlc:  { open: 'O', high: 'H', low: 'L', close: 'C', volume: 'V' },
        chart: { loading: 'Chargement…', scrollToLatest: 'Aller à la fin' },
    },
});
```

| Section | Key | Default | Description |
|---|---|---|---|
| `topBar.series` | `candlestick` | `'Candles'` | Series dropdown label |
| `topBar.series` | `hollow` | `'Hollow'` | |
| `topBar.series` | `ohlc` | `'OHLC'` | |
| `topBar.series` | `line` | `'Line'` | |
| `topBar.series` | `area` | `'Area'` | |
| `topBar` | `indicatorsBtn` | `'Indicators'` | Indicators button (no active) |
| `topBar` | `indicatorsBtnActive` | `'Indicators ({count})'` | `{count}` replaced at runtime |
| `topBar` | `toggleDrawingTitle` | `'Toggle Drawing Tools'` | Toolbar toggle tooltip |
| `topBar` | `toggleFullscreenTitle` | `'Toggle Fullscreen'` | Fullscreen button tooltip |
| `topBar` | `exitFullscreenTitle` | `'Exit Fullscreen'` | |
| `topBar` | `settingsTitle` | `'Settings'` | Settings button tooltip |
| `topBar.timeframes` | `'1m'`…`'1Y'` | same as key | Timeframe dropdown labels |
| `bottomBar.durations` | `'1D'`…`'All'` | same as key | Duration button labels |
| `drawingToolbar.categories` | `trendLines` | `'Trend Lines & Channels'` | Category flyout heading |
| `drawingToolbar.categories` | `fibonacci` | `'Fibonacci & Gann Tools'` | |
| `drawingToolbar.categories` | `shapes` | `'Geometric Shapes'` | |
| `drawingToolbar.categories` | `measurements` | `'Measurement Tools'` | |
| `drawingToolbar.categories` | `pitchforks` | `'Pitchfork Tools'` | |
| `drawingToolbar.categories` | `volume` | `'Volume & Statistical Tools'` | |
| `drawingToolbar.categories` | `annotations` | `'Annotation Tools'` | |
| `drawingToolbar.categories` | `patterns` | `'Pattern Recognition'` | |
| `drawingToolbar.categories` | `elliott` | `'Elliott Wave Tools'` | |
| `drawingToolbar.categories` | `brushes` | `'Brush & Highlighter'` | |
| `drawingToolbar.categories` | `advanced` | `'Advanced Tools'` | |
| `drawingToolbar.categories` | `priceProjection` | `'Price Projection Tools'` | |
| `drawingToolbar.tools` | *(DrawingToolType)* | English name per tool | One entry per `DrawingToolType` |
| `drawingToolbar.actions` | `undo` | `'Undo (Ctrl+Z)'` | Action button tooltips |
| `drawingToolbar.actions` | `redo` | `'Redo (Ctrl+Y)'` | |
| `drawingToolbar.actions` | `deleteSelected` | `'Delete Selected (Del)'` | |
| `drawingToolbar.actions` | `clearAll` | `'Clear All'` | |
| `drawingToolbar.actions` | `showAll` | `'Show All'` | |
| `drawingToolbar.actions` | `hideAll` | `'Hide All'` | |
| `drawingToolbar.actions` | `lockAll` | `'Lock All'` | |
| `drawingToolbar.actions` | `unlockAll` | `'Unlock All'` | |
| `drawingToolbar` | `soonBadge` | `'soon'` | Badge on unimplemented tools |
| `drawingToolbar` | `scrollUp` | `'Scroll up'` | Scroll indicator tooltip |
| `drawingToolbar` | `scrollDown` | `'Scroll down'` | |
| `ohlc` | `open` | `'O'` | OHLC strip label |
| `ohlc` | `high` | `'H'` | |
| `ohlc` | `low` | `'L'` | |
| `ohlc` | `close` | `'C'` | |
| `ohlc` | `volume` | `'V'` | |
| `trade` | `buy` | `'Buy'` | Trade popover label |
| `trade` | `sell` | `'Sell'` | |
| `trade` | `limit` | `'Limit'` | Order type label |
| `trade` | `stop` | `'Stop'` | |
| `trade` | `market` | `'Market'` | |
| `trade` | `qty` | `'Qty'` | Draft order qty input label |
| `trade` | `placeOrderTitle` | `'Place order at this price'` | Trade button tooltip |
| `dialogs.settings` | `title` | `'Chart Settings'` | Dialog title |
| `dialogs.settings` | `tfcSection` | `'Trade from Charts'` | Section heading |
| `dialogs.settings` | `showLabel` | `'Show'` | Radio group label |
| `dialogs.settings` | `positionStyleLabel` | `'Position Style'` | |
| `dialogs.settings` | `showAll` | `'All'` | Filter option |
| `dialogs.settings` | `showPositions` | `'Positions Only'` | |
| `dialogs.settings` | `showOrders` | `'Orders Only'` | |
| `dialogs.settings` | `styleLine` | `'Full Line'` | Style option |
| `dialogs.settings` | `styleDot` | `'Dot'` | |
| `dialogs.indicator` | `titleSuffix` | `'Settings'` | Appended to indicator name in dialog title |
| `dialogs.indicator` | `noParams` | `'No configurable parameters.'` | Empty-state message |
| `chart` | `loading` | `'Loading…'` | Loading overlay text |
| `chart` | `scrollToLatest` | `'Scroll to latest'` | Jump-to-end button tooltip |
| `chart` | `nIndicators` | `'{count} indicators'` | Collapse button; `{count}` replaced at runtime |

### `ChartTheme` — colors

```ts
import { DARK_THEME, LIGHT_THEME } from 'acttrader-charts';
import type { ChartTheme } from 'acttrader-charts';
```

Pass a full or partial theme object to customize colors:

```ts
const myTheme: ChartTheme = {
    ...DARK_THEME,
    background: '#0a0a0a',
    candle: { up: '#00c853', down: '#ff1744', wickUp: '#00c853', wickDown: '#ff1744' },
};

new ChartEngine({ container, theme: 'dark' });
// Theme override is applied via setTheme() after construction:
chart.setTheme('light');
```

| Top-level key | Type | Description |
|---|---|---|
| `background` | `string` | Canvas background color |
| `grid` | `string` | Grid line color |
| `axisText` | `string` | Axis tick label color |
| `axisBorder` | `string` | Axis border / separator color |
| `crosshair` | `string` | Crosshair line color |
| `tooltip` | `{background, text, border}` | Floating label colors |
| `candle` | `{up, down, wickUp, wickDown}` | Candlestick colors |
| `volume` | `{up, down}` | Volume bar colors |
| `ui` | `ChartThemeUi` | Accent and interactive-state colors |
| `drawingToolbar` | `DrawingToolbarColors` | Icon and flyout colors |
| `topBar` | `TopBarColors` | Top bar button and flyout colors |
| `bottomBar` | `BottomBarColors` | Duration button colors |
| `indicatorOverlay` | `IndicatorOverlayColors` | Pill and dropdown colors |
| `tradeLevels` | `TradeLevelColors` | Level line and info box colors |
| `tradePanel` | `TradePanelColors` | Trade panel side-drawer colors |

---

## API Reference

### Data

```ts
chart.loadData(bars: OHLCVBar[]): this
chart.prependData(bars: OHLCVBar[]): this   // prepend historical bars (infinite scroll back)
chart.correctBar(barTime: number, bar: OHLCVBar): void  // replace a bar with authoritative data after close
chart.setLoading(loading: boolean): this

// Reset — clears all bars, the live price line, any in-flight fetch,
// all user drawings, and all trade/position levels (including pending drafts).
// Call before switching to a new symbol so no previous symbol state bleeds in.
// If you only want to clear bars (e.g. refreshing the same symbol), call
// chart.loadData([]) directly to preserve drawings.
chart.resetData(): this
```

**Symbol switch pattern:**
```ts
chart.setSymbol('GBPUSD').resetData();
// … fetch new bars …
chart.loadData(newBars);
```

### Series & Appearance

```ts
chart.setSeries(series: SeriesType): this
chart.setTheme('dark' | 'light'): this
chart.setTimeframe(tf: Timeframe): this                         // change timeframe and reload data
chart.setThemeOverrides(overrides: ThemeOverrides): this               // update per-theme color overrides at runtime
chart.setTimezone(tz: string): this                                // change display timezone at runtime
chart.setVolume(show: boolean): this
```

`SeriesType` values: `candlestick` · `hollow` · `line` · `area` · `ohlc` · `heikinashi` · `volumecandles` · `linemarkers` · `step` · `hlcarea` · `baseline` · `columns` · `highlow`

### Indicators

```ts
chart.addIndicator(indicator: IIndicator): this
chart.removeIndicator(name: string): this
```

### Drawing Tools

```ts
chart.setDrawingTool(tool: DrawingToolType | null): this
```

### Trade Levels (TFC)

```ts
chart.setLevels(levels, labelField, priceField, type): this
chart.removeLevelByLabel(label: string): this
chart.updateLevelMainPrice(label: string, price: number): this   // move an existing level's entry price; stages the edit so it survives subsequent setLevels refreshes until the server echoes the new price or cancelLevelEdit/cancelCurrentEdit is called
chart.updateLevelBracket(label: string, bracketType: 'sl' | 'tp', price: number | null): this   // update or remove a SL/TP bracket on an existing level; same staging semantics as updateLevelMainPrice — pass null to remove
chart.cancelLevelEdit(label: string): this   // drop any staged edit for this level and revert to last confirmed server state
chart.cancelCurrentEdit(): this   // cancel the active draft order or any in-progress level edit; no-op when nothing is active
chart.addLevelBracket(label: string, bracketType: 'sl' | 'tp'): this   // auto-place a SL or TP bracket at a default offset; emits tradeLevelBracketActivated with the computed price
chart.setDraftBracketPnl(bracketType: 'sl' | 'tp', pnlText: string | null): this   // set estimated P&L text on the active bracket host — the draft order while drafting, or the currently selected existing pending order / position while modifying; pass null to clear
chart.setTfcActive(enabled: boolean): this   // toggle TFC on/off at runtime; hides/shows all trade levels, draft orders, and floating trade button; fires tfcToggle event
```

> **Staging semantics for `updateLevelMainPrice` / `updateLevelBracket`.** Calls to these methods register the change in the chart's pending-edit buffer (the same buffer chart-initiated drags use), so the edit stays visible even when the host app keeps pushing fresh server state via `setLevels` (e.g. per-tick PnL refreshes). When the server echoes back the new price in a later `setLevels` call, the staged edit is auto-released (mobile, i.e. `hideLevelConfirmCancel: true`). If a panel closes without submitting, call `cancelLevelEdit(label)` or `cancelCurrentEdit()` to drop the staged edit — otherwise it will keep overriding server state on the chart.

### Trade Button

```ts
chart.setOrderLots(lots: number): void   // update default qty at runtime
```

### Viewport

```ts
chart.isAtLiveEdge(): boolean
```

### Streaming

```ts
chart.connectStream(adapter: IWebSocketAdapter): this
chart.disconnectStream(): this
chart.pushTick(tick: { time: number; bid: number; ask: number; volume?: number }): void  // push a tick directly without an adapter
```

### State Persistence

```ts
// Capture the full current configuration as a serializable object
const state: ChartState = chart.getState();
// → { symbol, timeframe, duration, series, showVolume, theme, indicators[], drawings[] }

// Restore a previously saved state (call after construction, before loadData)
chart.setState(state);
```

Use together with the `stateChange` event to persist chart config per symbol:

```ts
import type { ChartState } from 'acttrader-charts';

chart.on('stateChange', ({ symbol, state }) => {
    localStorage.setItem(`chart:${symbol}`, JSON.stringify(state));
});

// On init — restore if available
const saved = localStorage.getItem(`chart:${symbol}`);
if (saved) chart.setState(JSON.parse(saved) as ChartState);
```

### Lifecycle

```ts
chart.destroy(): void
```

### Dismissing open UI

Closes any open flyout/modal/dropdown/popover (quantity edit, symbol picker,
indicator settings, chart settings, trade popover, topbar dropdowns, mobile
drawing flyout) in a single call. Designed for wiring to the system back
button / gesture in PWAs, installed web apps, and native wrapper hosts.

```ts
chart.dismissAllUI(): boolean  // true if anything was dismissed, false if nothing was open
```

Paired with the `uiStateChange` event so you can track whether a back action
should dismiss chart UI or fall through to your own navigation:

```ts
chart.on('uiStateChange', ({ hasOpenUI }) => {
    // hasOpenUI === true when at least one flyout/modal/dropdown is open
});
```

**PWA — proper History API wiring** (back press dismisses flyout without
navigating away from the page):

```ts
let pushedStateForUI = false;

chart.on('uiStateChange', ({ hasOpenUI }) => {
    if (hasOpenUI && !pushedStateForUI) {
        history.pushState({ chartUI: true }, '');
        pushedStateForUI = true;
    } else if (!hasOpenUI) {
        pushedStateForUI = false;
    }
});

window.addEventListener('popstate', () => {
    if (chart.dismissAllUI()) pushedStateForUI = false;
});
```

**Minimum wiring** (back press closes flyout AND advances browser history —
fine for single-screen apps without client routing):

```ts
window.addEventListener('popstate', () => chart.dismissAllUI());
```

---

## Built-in Indicators

### Trend / Price Overlay

| Class | Constructor defaults |
|---|---|
| `SMA(period?, color?)` | period 20 |
| `EMA(period?, color?)` | period 20 |
| `WMA(period?, color?)` | period 20 |
| `HMA(period?, color?)` | period 20 — Hull MA |
| `BollingerBands(period?, mult?, color?)` | 20, 2 |
| `KeltnerChannels(period?, mult?, color?)` | 20, 2 |
| `DonchianChannels(period?, color?)` | 20 |
| `IchimokuCloud(tenkan?, kijun?, senkou?, color?)` | 9, 26, 52 |
| `ParabolicSAR(step?, max?, color?)` | 0.02, 0.2 |
| `Supertrend(period?, mult?, color?)` | 10, 3 |
| `MARibbon(color?)` | 8 EMAs (10–80) |
| `PivotPoints(color?)` | Standard pivot levels |
| `FibRetracementIndicator(color?)` | Auto swing high/low |
| `HeikinAshi(color?)` | Heikin-Ashi candle overlay |
| `LinearRegression(period?, color?)` | Linear regression line overlay |

### Sub-pane Oscillators

| Class | Constructor defaults |
|---|---|
| `RSI(period?, color?)` | 14 |
| `MACD(fast?, slow?, signal?, color?)` | 12, 26, 9 |
| `Stochastic(kPeriod?, dPeriod?, color?)` | 14, 3 |
| `StochRSI(rsiPeriod?, stochPeriod?, kPeriod?, dPeriod?, color?)` | 14, 14, 3, 3 |
| `CCI(period?, color?)` | 20 |
| `WilliamsR(period?, color?)` | 14 |
| `ROC(period?, color?)` | 12 |
| `ATR(period?, color?)` | 14 |
| `ADX(period?, color?)` | 14 — includes +DI / -DI |
| `OBV(color?)` | On Balance Volume |
| `VWAP(color?)` | Intra-day weighted average |
| `AccDistribution(color?)` | Accumulation/Distribution |
| `MFI(period?, color?)` | 14 — Money Flow Index |
| `CMF(period?, color?)` | 20 — Chaikin Money Flow |
| `AwesomeOscillator(color?)` | 5 / 34 SMA difference |
| `VOL(color?)` | Volume oscillator sub-pane |

Sub-pane heights are user-resizable by dragging the separator between panes.

---

## Drawing Tools

### Lines & Channels

| Key | Tool | Placement |
|---|---|---|
| `horizontalLine` | Horizontal Line | Single click |
| `verticalLine` | Vertical Line | Single click |
| `trendLine` | Trend Line | 2 clicks |
| `trendAngle` | Trend Angle | 2 clicks (shows angle) |
| `crossLine` | Cross Line | Single click (H+V) |
| `parallelChannel` | Parallel Channel | 3 clicks |
| `flatChannel` | Flat Channel | 2 clicks |
| `disjointChannel` | Disjoint Channel | 3 clicks |

### Fibonacci & Gann

`fibRetracement` · `fibExtension` · `fibChannel` · `fibTimezone` · `fibCircles` · `fibSpiral` · `fibFan` · `fibProjection` · `gannFan` · `gannSquare` · `gannBox`

### Shapes

`rectangle` · `rotatedRectangle` · `ellipse` · `triangle` · `circle` · `arc` · `polyline` · `path` · `brush` · `highlighter`

### Measurements & Annotations

`priceRange` · `dateRange` · `datePriceRange` · `ruler` · `priceLabel` · `priceNote` · `priceProjection` · `projection` · `arrowUp` · `arrowDown` · `arrowMarker` · `text` · `callout` · `anchoredNote` · `flag` · `sineLine` · `regressionTrend` · `ghostFeed`

### Volume Profile

`volumeProfile` · `anchoredVP` · `fixedRangeVP`

### Cyclic & Time

`cyclicLines` · `timeCycles`

### Harmonic & Elliott Patterns

`abcdPattern` · `headShoulders` · `trianglePattern` · `batPattern` · `butterflyPattern` · `crabPattern` · `gartleyPattern` · `cypherPattern` · `sharkPattern` · `threeDrives` · `elliottImpulse` · `elliottCorrective` · `elliottTriangle` · `elliottCombination` · `elliottWxy`

### Pitchforks

`pitchfork` · `schiffPitchfork` · `modifiedSchiff` · `insidePitchfork`

---

## Events

```ts
chart.on('crosshair',      ({ x, y, barIndex, price, bar }) => {});
chart.on('click',          ({ x, y, barIndex, price, bar }) => {});
chart.on('zoom',           ({ viewport }) => {});
chart.on('pan',            ({ viewport }) => {});
chart.on('timeframeChange',({ timeframe }) => {});
chart.on('durationChange', ({ duration, timeframe }) => {});
chart.on('seriesChange',   ({ series }) => {});
chart.on('streamStatus',   ({ status }) => {}); // 'connected' | 'reconnecting' | 'disconnected'
chart.on('dataLoaded',     ({ timeframe, interval, start, end }) => {});
chart.on('newBar',         ({ completedBar, openingBar, intervalMs }) => {});
chart.on('stateChange',    ({ symbol, state }) => {
    // Fires after every user-driven config change (timeframe, series, theme,
    // indicators, drawings, volume). `state` is a full serializable snapshot.
    localStorage.setItem(`chart:${symbol}`, JSON.stringify(state));
});

// TFC events
// `isFullscreen` is true when the chart is in fullscreen mode (native or CSS) at the time of the action.
// Use it to conditionally show toast notifications only while fullscreen.
chart.on('tradeLevelEdit',     ({ label, type, data, changes, isFullscreen }) => {});
chart.on('tradeLevelClose',    ({ label, type, action, data, isFullscreen }) => {});
chart.on('tradeLevelDrag',     ({ label, newPrice, data, bracketType, isFullscreen }) => {}); // fires on every mouse-move during drag
chart.on('tradeLevelQtyChange', ({ label, type, newLots, previousLots, isFullscreen }) => {}); // live qty edit via QTY pill — use to refresh Estimated PNL on SL/TP brackets
chart.on('tradeLevelEditOpen', ({ label, type, price, side, stopLossPrice, takeProfitPrice, data, isFullscreen }) => {});
chart.on('tradeLevelConfirmed',({ label, type, isFullscreen }) => {});
chart.on('tradeLevelEditCancelled', ({ label, type, isFullscreen }) => {}); // ESC / inline ✕ aborted an edit — reset external modify-order panel
chart.on('draftInitiated',     ({ side, price, orderType, isFullscreen }) => {}); // new draft order shown — open buy/sell form
chart.on('draftCancelled',     ({ label, isFullscreen }) => {}); // draft order dismissed without confirming
chart.on('tfcToggle',         ({ enabled }) => {}); // TFC toggled on or off via top bar button or setTfcActive()
chart.on('tradeLevelDragEnd',  ({ label, type, newPrice, data }) => {}); // deprecated — use tradeLevelEdit
chart.on('tradeLevelBracketDrag', ({ label, bracketType, newPrice, data }) => {}); // deprecated

// Fires after addLevelBracket() auto-places a bracket, delivering the computed price back to the caller
chart.on('tradeLevelBracketActivated', ({ label, bracketType, price, isFullscreen }) => {
    // Use price to pre-populate the SL/TP input field in your native form
    updateBracketPriceInput(bracketType, price);
});
```

---

## Custom Indicator

Implement `IIndicator` and pass it to `addIndicator()`:

```ts
import type { IIndicator, OHLCVBar, IndicatorResult } from 'acttrader-charts';

class MyIndicator implements IIndicator {
    name       = 'MyInd';
    color      = '#e91e63';
    pane       = 'sub' as const;  // 'main' = overlay, 'sub' = dedicated pane
    paneHeight = 80;

    compute(data: OHLCVBar[]): IndicatorResult[] {
        return data.map((bar, i) => ({ index: i, value: bar.close }));
    }

    render(ctx, results, scale, viewport, chartHeight) {
        // draw with ctx (Canvas 2D)
    }
}

chart.addIndicator(new MyIndicator());
```

---

## Build & Dev

```bash
npm run dev        # Vite dev server with demo chart
npm run build      # ESM + CJS via tsup → dist/
npm run test       # Vitest unit tests
npm run typecheck  # tsc --noEmit
npm run lint       # ESLint
```

---

## License

MIT
