OpenAlgo Charts skill
openalgo-charts is a from-scratch, dependency-free HTML5-canvas charting engine: one canvas pipeline, no SVG, no DOM per bar, eight lazy-loaded bundle tiers, zero runtime dependencies.
Works the same whether the project is a downstream npm consumer app or an upstream openalgo-charts source checkout. Detect which one you are in and resolve every API name from whatever typings are locally available.
Source lookup order
Do not assume you are inside the upstream source repository.
- In a consumer app, inspect the installed package first:
node_modules/openalgo-charts/package.json for the actual version.
node_modules/openalgo-charts/dist/index.d.ts for the base API surface, and dist/{trade,draw,indicators,transform,profile,webgl,widget}/index.d.ts for each tier.
- In the upstream repo, inspect
dist/index.d.ts first, then src/ if generated output is unavailable.
ARCHITECTURE.md and website/pages/docs/*.mdx are supporting evidence, but local typings win when they disagree.
Verify before answering (copy-paste):
node -p "require('./node_modules/openalgo-charts/package.json').version"
rg -n "createChart|addSeries|addIndicator|DrawingController|OrderEngine" node_modules/openalgo-charts/dist/index.d.ts
# upstream checkout instead of a consumer app:
rg -n "createChart|addSeries|addIndicator" dist/index.d.ts src/index.ts
# which tiers does this project actually load?
rg -n "from 'openalgo-charts" src app
If the relevant file is unavailable, say what could not be verified. Do not invent option names, methods, exports, event names, or indicator ids.
Mental model
Eight layers, in dependency order. Most bugs come from confusing one for another.
- Chart -
createChart(container, options) returns a Chart. One chart per container element. It owns everything below.
- DataLayer - one per chart. Merges every series by time onto a single shared logical index space
0..N-1. This is the load-bearing idea; see rule 2 below.
- Scales -
chart.timeScale maps logical index to x; each pane's PriceScale maps price to y. Panes autoscale independently.
- Panes - vertically stacked drawing regions, each with two canvases (base + overlay) and up to three price scales (
'right', 'left', '' overlay).
- Series -
chart.addSeries(type, options) returns a SeriesApi. The type names an entry in the chart-type registry; the core never switches on type.
- Registries - chart types, indicators, and drawing tools are all descriptors in a Map. Adding one is a registration, never a core change.
- Primitives - the extension point. Anything that draws but is not a series: price lines, markers, legends, profiles, trading pills, drawings.
- Tiers -
indicators, draw, transform, profile, trade, webgl are separate bundles that register into the base engine's registries on import; widget sits above them all and is the one tier that builds DOM.
Install and tiers
npm install openalgo-charts
Import only what you use. Each tier is a separate entry point that registers into the base engine, so a feature you do not load costs zero bytes.
| Import |
Contents |
Brotli limit |
openalgo-charts |
Engine, 13 chart types, panes and scales, primitives, registries, chart state and settings schema, market replay, symbol comparison, chart linking, warm-load bar cache, interval registry, chart timezone, trading visualization, OpenAlgo feeds, EMA/RSI/ATR/Supertrend calculators, vector SVG export, the render backend port |
67 KB |
openalgo-charts/indicators |
102 built-in indicators + the Tier-2 external-data contract |
30 KB |
openalgo-charts/draw |
51 drawing tools + a headless DrawingController with multi-select, z-order, a per-tool settings schema, the 1.9.x migration, the clipboard and the icon builders |
26 KB |
openalgo-charts/transform |
Heikin Ashi, Renko, Range bars, Line Break, Point and Figure, Kagi |
5 KB |
openalgo-charts/profile |
Volume Profile, Market Profile (TPO), Footprint, order flow |
11 KB |
openalgo-charts/trade |
Order engine, state machine, order/position/bracket lines, DOM ladder |
75 KB with base |
openalgo-charts/webgl |
The WebGL2 series backend behind renderer: 'auto' | 'webgl2'; composites into the pane's canvas, falls back to 2D for the session on context loss |
7 KB |
openalgo-charts/widget |
createWidget: the chart with a top bar, drawing rail, status line, settings and indicator dialogs, drawing properties, right-click menu, keymap and optional persistence. The only tier that ships DOM; imports the draw tier itself |
36 KB |
Limits are the CI-enforced budgets in .size-limit.json. Measured on the 2.0.2 build: base engine 66.45 KB against 67 KB, draw tier 25.82 KB against 26 KB, indicator tier 27.27 KB against 30 KB, widget tier 35.56 KB against 36 KB, a widget terminal (base + draw + indicators + widget, what one createWidget call loads) 155.09 KB against 156 KB, and the whole package 182.39 KB against 183 KB. The aggregate budget always sits above the sum of the tier ceilings it contains, or it would fail while every tier it contains passes. Nothing is excluded from these figures because there are no runtime dependencies to exclude; the exact table is in bundling-and-tiers.
The clipboard lives in the draw tier, not the base one, because it needs the drawing-tool registry. DrawingClipboard and friends come from openalgo-charts/draw.
The 60-second chart
import { createChart } from 'openalgo-charts';
const chart = createChart(document.getElementById('chart')!);
const series = chart.addSeries('candlestick');
series.setData([
{ time: 1705286700, open: 100, high: 101, low: 99.5, close: 100.6, volume: 1200 },
{ time: 1705286760, open: 100.6, high: 101.4, low: 100.2, close: 101.1, volume: 900 },
]);
chart.fitContent();
time is UTC seconds. The container must have a non-zero size before the chart can lay out.
Non-negotiable rules
- Time is UTC seconds everywhere, never milliseconds.
Math.floor(Date.now() / 1000), not Date.now(). Feed adapters convert broker formats at the edge. Which wall clock those seconds are labelled in is ChartOptions.timezone, an IANA name defaulting to Asia/Kolkata; never offset the timestamps themselves to fake a zone, or every session anchor and gap moves with them.
- The time axis is gapless and index-based, not timestamp-proportional. x is
logicalIndex * barSpacing, so weekends, holidays and session breaks have no index and collapse to nothing. Never compute an x from a timestamp difference; use chart.timeToCoordinate(t) or chart.timeScale.
- One bar per time per series, ascending. Duplicate times collapse to the last one written.
- Nothing crosses a chart boundary as a logical index. Sync by instant. Because rule 2 makes the index a property of that chart's own bars, index 300 is a different moment on every chart in a grid. Any value passed between charts (a linked crosshair, a shared viewport, a highlighted bar) converts index to time on the sender with
indexToTimeFloat and time back to index on the receiver with timeToIndex / timeToIndexFloat, against that chart's own DataLayer. createLinkGroup, followerIndex and followerRange do this; a hand-rolled b.setVisibleLogicalRange(a.getVisibleLogicalRange()) does not, and it looks correct until the two charts hold different bars. An instant outside the receiver's first or last bar is an absence: draw nothing rather than clamping to an edge bar. See chart-linking.
- Never cache the forming bar. A closed bar is immutable; the last bar is alive until its interval ends. Serving a snapshot of it puts a stale close on the last-price line, the axis tag, the header LTP and every indicator computed off it, with no spinner and no staleness badge.
withBarCache drops trailing unclosed bars for this reason. A fast wrong price is a worse failure than a slow right one on a chart that draws Buy and Sell buttons.
- Never deep-import.
import { X } from 'openalgo-charts' or a published tier specifier only. A deep path into dist/ internals inlines a second copy of the registry Map, and createChart will never see what your tier registered. See rollup.config.js.
- A tier must be imported before its features resolve.
chart.addIndicator('macd') needs import 'openalgo-charts/indicators'; addSeries('kagi') needs import 'openalgo-charts/transform' and throws a specific tier-naming error without it.
- Price-scale margins are fractions of the pane height, not padding on the data span. The data band occupies
1 - marginTop - marginBottom of the pane.
- Drawing anchors are
{ time, price }, never pixels. Pixel anchors slide the moment a gap collapses or the user zooms.
- Canvas drawing happens in bitmap pixels. Multiply media px by
dpr in any custom primitive, or it blurs and misaligns on HiDPI.
chart.trading renders trade state; it does not place orders. The host pushes exchange state in and turns the emitted trading:* events into broker calls. The transactional path is openalgo-charts/trade.
- The engine ships no DOM chrome;
openalgo-charts/widget is the one tier that does. The base and the six engine tiers have no toolbar, no dialogs, no settings forms, no command palette. Drawing tools, indicator settings, replay transports and order menus are the host's UI, driven by descriptors and events. A host that does not want to write that chrome imports the widget tier and calls createWidget (see widget); a host that does gets the description of that UI: chartSettingsSchema(chart) for a settings dialog, the contextmenu event for a right-click menu, and chart.priceAxisState(...) for a menu raised on a price axis. Chrome you write is held to the UI standard in themes-and-styling: styled scrollbars, small square swatches, paired up/down colours on one row, themed form controls, and no control with nothing behind it.
- A control with no data in the current context is rendered disabled, not hidden.
PriceLevels.available(kind) and PriceAxisState.active / scaled / movable exist to be read for exactly this. Hiding it loses the information that the state is off.
- Never use emojis or icons in code, comments, logs, or generated UI text. Project rule.
References
Detailed reference for each topic is in references/. Read the one that matches the task before writing code.
| Reference |
Topic |
| core-api |
createChart, ChartOptions, SeriesApi, lifecycle, coordinate conversion, the render/invalidation model |
| chart-types |
All 13 base series types, their styles and autoscale rules, runtime type switching |
| scales-and-panes |
Price/time scale options, log and inverted modes, left/right/overlay scales, per-axis state and moves, axis chrome (corner clock, bar countdown, tick priority), pane weights and layout |
| themes-and-styling |
ChartTheme keys, dark/light, gradients, SeriesStyle precedence, price formatting, and the UI standard for host chrome |
| data-and-time |
Bar shape, UTC seconds, the chart timezone and the time helpers, setData/update/prependData, the logical-index model, history paging, tick and volume bars |
| feeds-and-live |
DataFeed contract, OpenAlgo REST/WS/live feeds, CandleBuilder, the interval registry, withBarCache warm loading, writing a custom feed |
| events-and-state |
The full event catalogue with payloads, getState/restoreState, saved layouts |
| indicators |
The 102 built-ins with exact ids, placements and input defaults, the settings model, levels/ranges/fills, signal markers, registerIndicator, the Tier-2 external-data contract |
| transforms |
Heikin Ashi, Renko, Range, Line Break, Point and Figure, Kagi |
| drawing-tools |
The 51 tools, DrawingController, anchors, magnet, undo, copy/cut/paste and the clipboard payload, persistence, shortcuts, custom tools |
| primitives-and-plugins |
IPrimitive, z-order, hit-testing, the dpr contract, built-in primitives including the PriceLevels reference-level family, registerChartType |
| replay-and-compare |
ReplayController and its transport events, addComparison, the overlay-scale mechanism, timestamp alignment |
| chart-linking |
createLinkGroup, the sync-by-instant rule, followerIndex / followerRange, the linked crosshair, host-driven symbol sync |
| settings-and-menus |
chartSettingsSchema and its round trip, the five tabs, the colorPair row, the timezone control, canvas options (grid, crosshair, scales, margins), status-line switches, the contextmenu event and the price-axis menu |
| trading |
The data-driven on-chart trading layer, trading:* events, order/position/bracket lines |
| trade-tier |
OrderEngine, order state machine, validation, analyzer mode, DOM ladder, broker adapters |
| profiles-and-orderflow |
Volume Profile, Market Profile (TPO), Footprint, cumulative delta, the trade-data dependency |
| react-integration |
React and Next.js lifecycle, keeping orchestration out of React, SSR, resize |
| bundling-and-tiers |
Entry points, registry identity, tree-shaking, script/ESM/import-map loading, size budget |
| widget |
createWidget and the widget tier: options, the handle, events, the context every dialog is handed, the keymap scopes, the tokens, every exported mount and helper, packaging |
| interactions |
Pan/zoom/pinch, crosshair modes, the keyboard system, touch, accessibility |
| pitfalls |
The verified foot-gun list. Read this when something behaves unexpectedly |
Triage
| User asks about |
First check |
Answer with |
Avoid |
| First chart / blank chart |
container size, dist present |
createChart + addSeries + setData |
assuming a CSS import or web component |
| Bars in the wrong place |
units of time |
UTC seconds |
Date.now() milliseconds |
| Axis shows the wrong hours |
chart.timezone() |
timezone: 'America/New_York' on createChart, or setTimezone |
shifting bar timestamps, or a timeFormatter that only relabels |
| Gaps for weekends |
the gapless-axis rule |
it is intended; whitespace points if you want a gap |
shifting timestamps |
| Realtime ticks |
last-bar vs full replace |
series.update(bar) |
setData on every tick |
| Loading older history |
setHistoryLoader |
prependData + historyLoadComplete |
rebuilding and re-fitting |
| Indicator not found |
is the tier imported |
import 'openalgo-charts/indicators' |
registering it by hand |
| Which indicator id to use |
the catalogue in indicators |
the exact id from the 102-row table, guarded with hasIndicator(id) |
guessing an id from the display name |
| Indicator settings UI |
descriptor inputs + generated style keys |
build the form from the descriptor, apply with setSettings; or openalgo-charts/widget (mountIndicatorSettings, or the whole terminal via createWidget) for a host that does not want to write chrome |
expecting the engine itself to have a dialog |
| Drawing tools |
DrawingController |
headless controller + host toolbar; or createWidget from openalgo-charts/widget, which ships the rail |
expecting the engine itself to have a toolbar |
| Volume in its own pane |
paneIndex and priceScaleId |
addSeries('histogram', { paneIndex: 1 }) |
a second chart |
| Volume inside the price pane |
overlay scale |
priceScaleId: '' plus scale margins |
manual y math |
| Placing real orders |
which layer |
openalgo-charts/trade OrderEngine |
chart.trading (visualization only) |
| Order lines on the chart |
which layer |
chart.trading sync + trading:* events |
drawing your own price lines |
| Custom overlay |
primitive vs chart type |
IPrimitive + addPrimitive |
a custom chart type for decoration |
| Bar-by-bar replay |
ReplayController |
headless controller + host transport bar; indicators rebuild from the prefix |
a second chart, or slicing data by hand |
| Two symbols on one chart |
addComparison |
hidden overlay scale + a rebasing pane mode |
a second series on the same price axis |
| A grid of charts moving together |
createLinkGroup |
one group, three switchable channels; everything syncs by time, never by logical index |
copying getVisibleLogicalRange() between charts |
| Linked crosshair shows the wrong bar |
the index-to-time conversion |
followerIndex(target.dataLayer, time, whenMissing) |
assuming index N is the same instant on both charts |
| Slaving the symbol across charts |
who owns the instrument |
the host emits 'symbol' / calls setSymbol, each member gets an onSymbol loader |
expecting the engine to know what a symbol is |
| Copy and paste a drawing |
draw.copy() / cut() / paste() |
await them; the host owns the key chords |
a synchronous call, or throwing on foreign clipboard text |
| Refetching the same bars on every switch |
withBarCache(feed, opts) |
wrap the feed; key is symbol/exchange/interval, range is sliced |
caching the forming bar, or keying on the range |
| A monthly / tick / volume timeframe |
registerInterval |
a Bucketing rule (calendar, ticks, volume), then bucketStartOf |
intervalToSeconds on a code that has no fixed length |
| Unknown interval code |
isKnownInterval(code) |
validate the picker; resolveInterval throws UnknownIntervalError |
catching the throw and defaulting to 60 seconds |
| Percent / rebased axis |
priceScale.mode |
'percentage' or 'indexed-to-100' plus a baseline |
recomputing the data into percentages |
| Settings dialog |
chartSettingsSchema(chart) |
render the tabs, round-trip with read/applyChartSettings; or mountSettingsDialog / createWidget from openalgo-charts/widget |
hardcoding a control list |
| Right-click menu |
contextmenu event |
preventDefault() + target.kind |
hit-testing the pointer yourself |
| Menu on a price axis |
target.side / target.scaleId |
chart.priceAxisState(pane, scaleId), acted on by the setPriceAxis* calls |
assuming pane 0's right scale |
| Previous close, session high/low, bid/ask lines |
PriceLevels |
one primitive, one options group per level, line and label together |
a PriceLine per level with its own tag bookkeeping |
| A level or axis row with no data |
available(kind), state.active |
render it disabled with its state visible |
hiding the control |
| Corner clock, bar-close countdown |
ChartOptions.axisChrome |
{ sessionClock: true, barCountdown: true }, plus a clock for a delayed feed |
a DOM overlay positioned over the axis |
| Saved layouts |
getState / restoreState |
one JSON payload |
hand-rolled serialisation |
| React lifecycle |
where the chart instance lives |
create in an effect, hold in a ref, chart.destroy() on cleanup |
chart instance in state |
| Bundle size |
which tiers are imported |
drop the unused tier import |
code-splitting the base |
Core API cheat sheet
Verified names. Get these wrong and nothing works.
// chart
const chart = createChart(el, options);
chart.addSeries(type, { paneIndex, style, priceScaleId, priceFormat });
chart.addIndicator(id, settings, { paneIndex }); // needs the indicators tier
chart.addPriceLine(opts, paneIndex);
chart.addPrimitive(primitive, paneIndex);
chart.fitContent();
chart.applyOptions({ theme, grid, canvas, statusLine, priceScale, priceFormatter, timeFormatter, timezone, crosshairMode });
chart.setTheme(theme);
chart.setTimezone('America/New_York') / chart.timezone(); // IANA name, default 'Asia/Kolkata'
chart.panes(); // readonly Pane[]
chart.primarySeries() / chart.primarySeriesInfo();
chart.setCanvasOptions(opts) / chart.setStatusLineOptions(opts) / chart.setPriceScaleOptions(opts);
chart.setAxisChromeOptions({ sessionClock: true, barCountdown: true }) / chart.axisChromeOptions();
// one price axis at a time (what a menu on an axis strip acts on)
chart.priceAxisState(paneIndex, scaleId); // PriceAxisState | null
chart.setPriceAxisOptions(paneIndex, scaleId, patch);
chart.setPriceAxisAutoFit(paneIndex, scaleId, on);
chart.setPriceAxisLockRatio(paneIndex, scaleId, on); // boolean
chart.movePriceAxis(paneIndex, 'right', 'left'); // boolean
PRICE_SCALE_MODES; // ['linear','logarithmic','percentage','indexed-to-100']
chart.getState() / chart.restoreState(state);
chart.on(event, cb); // returns an unsubscribe function
chart.subscribeCrosshairMove(cb);
chart.timeToCoordinate(t) / chart.coordinateToTime(x);
chart.priceToCoordinate(p, paneIndex) / chart.coordinateToPrice(y, paneIndex);
chart.destroy(); // idempotent; emits 'destroy', then drops every listener
chart.isDestroyed; // getter, not a method
// getters, not methods
chart.timeScale; // TimeScale
chart.dataLayer; // DataLayer
chart.trading; // TradingController, created on first access
chart.shortcuts; // ShortcutManager | null
// series handle
series.setData(items);
series.update(item);
series.prependData(items);
series.getData();
series.applyOptions({ visible: false }); // not setStyle
series.priceScale();
series.createMarkers();
series.remove();
// headless controllers, base bundle, no tier import
new ReplayController(chart, { bars, startIndex, barMs }); // seek/step/play/pause/stop/state
addComparison(chart, { symbol, bars }); // setBars/remove/list/alignment
chartSettingsSchema(chart) / readChartSettings(chart) / applyChartSettings(chart, patch);
// linked chart grid (base bundle). Syncs by INSTANT, never by logical index.
const group = createLinkGroup({ crosshair: true, viewport: true, symbol: false, whenMissing: 'nearest' });
group.add(chart, { symbol: 'RELIANCE', onSymbol: (s, c) => loadBars(s, c) });
group.remove(chart) / group.setOptions(patch) / group.setSymbol(chart, s) / group.destroy();
group.members() / group.has(chart) / group.symbol() / group.crosshairIndex(chart);
followerIndex(followerDataLayer, timeSec, 'nearest' | 'hide'); // number | null
followerRange(leaderDataLayer, followerDataLayer, range); // LogicalRange | null
// warm-load bar cache: wraps ANY DataFeed (base bundle)
const feed = withBarCache(sourceFeed, { ttlMs, max, maxBars, storage, now, intervalSeconds });
feed.getBars({ symbol, exchange, interval, from, to, noCache: true });
feed.invalidate({ symbol, exchange, interval }) / feed.clear() / feed.stats() / feed.source;
// interval registry (base bundle): a bucketing rule, not a duration
registerInterval({ code: '1MO', bucketing: { mode: 'calendar', unit: 'month', count: 1 } });
resolveInterval(code) // throws UnknownIntervalError
tryResolveInterval(code) / isKnownInterval(code) / registeredIntervals() / unregisterInterval(code);
bucketStartOf(bucketing, timeSec, zone?) / nextBucketStart(...) / isTimeBucketed(bucketing);
// drawing clipboard (draw tier) - all async
await draw.copy(target?) / await draw.cut(target?) / await draw.paste();
draw.clipboard().lastError();
// reference levels (a primitive: previous close, session high/low, bid/ask, ...)
const levels = new PriceLevels({ levels: { previousClose: { line: true, label: true } } });
chart.addPrimitive(levels, 0);
levels.setLevel(kind, patch) / levels.setQuote(q) / levels.values() / levels.available(kind);
computePriceLevels({ bars, anchorTime }); // the same numbers, pure, no canvas
chart.fitContent() takes no arguments; chart.timeScale.fitContent(barCount) does.
Code-generation rules
- Verify option names against local typings before writing them. Many similarly named options exist at chart, series, pane and scale level. Confirm which level owns the option.
- Minimal snippets. One feature per code block. Combining an indicator, a drawing tool and a trading line in one snippet hides which API does what.
- Import from the package entry or a published tier specifier. Never a deep path.
- Match the user's host. A React user wants the effect lifecycle; a vanilla user does not; a no-bundler user needs the standalone build.
- State which tier a feature needs whenever the answer uses one.
- Do not invent. If a name does not appear in the installed typings or upstream source, it does not exist.
Answer contract
When answering an openalgo-charts question:
- Name the API and the tier it lives in.
- Show one minimal snippet, not a mega-demo.
- Call out the main foot-gun for that task, from pitfalls.
- Say what local source was checked (version, typings), or state that it could not be verified.
1---2name: openalgo-charts3description: Use when working with openalgo-charts - creating canvas charts, adding series (candlestick/bar/line/area/baseline/histogram and 13 more), configuring price and time scales, panes, indicators, drawing tools, primitives and custom renderers, volume/market profile and footprint, on-chart trading and order lines, DOM ladder, OpenAlgo REST history and WebSocket live ticks, chart state persistence, themes, keyboard shortcuts, or React/Next.js integration. Also market replay, multi-symbol comparison, linked chart grids (synced crosshair, viewport and symbol), copy/cut/paste of drawings, warm-load bar caching for any feed, the interval registry for calendar/tick/volume timeframes, rebasing price scales, the chart timezone, reference price levels (previous close, session high/low, bid/ask), axis chrome (session clock, bar-close countdown), the settings schema with its paired up/down colour control, and context menus including one raised on a price axis. Covers the eight-tier bundle model, the one-call widget tier, the4---56# OpenAlgo Charts skill78`openalgo-charts` is a from-scratch, dependency-free HTML5-canvas charting engine: one canvas pipeline, no SVG, no DOM per bar, eight lazy-loaded bundle tiers, zero runtime dependencies.910Works the same whether the project is a downstream npm consumer app or an upstream `openalgo-charts` source checkout. Detect which one you are in and resolve every API name from whatever typings are locally available.1112## Source lookup order1314Do not assume you are inside the upstream source repository.15161. In a consumer app, inspect the installed package first:17 - `node_modules/openalgo-charts/package.json` for the actual version.18 - `node_modules/openalgo-charts/dist/index.d.ts` for the base API surface, and `dist/{trade,draw,indicators,transform,profile,webgl,widget}/index.d.ts` for each tier.192. In the upstream repo, inspect `dist/index.d.ts` first, then `src/` if generated output is unavailable.203. `ARCHITECTURE.md` and `website/pages/docs/*.mdx` are supporting evidence, but local typings win when they disagree.2122Verify before answering (copy-paste):2324```sh25node -p "require('./node_modules/openalgo-charts/package.json').version"26rg -n "createChart|addSeries|addIndicator|DrawingController|OrderEngine" node_modules/openalgo-charts/dist/index.d.ts27# upstream checkout instead of a consumer app:28rg -n "createChart|addSeries|addIndicator" dist/index.d.ts src/index.ts29# which tiers does this project actually load?30rg -n "from 'openalgo-charts" src app31```3233If the relevant file is unavailable, say what could not be verified. Do not invent option names, methods, exports, event names, or indicator ids.3435## Mental model3637Eight layers, in dependency order. Most bugs come from confusing one for another.38391. **Chart** - `createChart(container, options)` returns a `Chart`. One chart per container element. It owns everything below.402. **DataLayer** - one per chart. Merges every series by time onto a single shared **logical index** space `0..N-1`. This is the load-bearing idea; see rule 2 below.413. **Scales** - `chart.timeScale` maps logical index to x; each pane's `PriceScale` maps price to y. Panes autoscale independently.424. **Panes** - vertically stacked drawing regions, each with two canvases (base + overlay) and up to three price scales (`'right'`, `'left'`, `''` overlay).435. **Series** - `chart.addSeries(type, options)` returns a `SeriesApi`. The type names an entry in the chart-type **registry**; the core never switches on type.446. **Registries** - chart types, indicators, and drawing tools are all descriptors in a Map. Adding one is a registration, never a core change.457. **Primitives** - the extension point. Anything that draws but is not a series: price lines, markers, legends, profiles, trading pills, drawings.468. **Tiers** - `indicators`, `draw`, `transform`, `profile`, `trade`, `webgl` are separate bundles that register into the base engine's registries on import; `widget` sits above them all and is the one tier that builds DOM.4748## Install and tiers4950```bash51npm install openalgo-charts52```5354Import only what you use. Each tier is a separate entry point that registers into the base engine, so a feature you do not load costs zero bytes.5556| Import | Contents | Brotli limit |57|---|---|---|58| `openalgo-charts` | Engine, 13 chart types, panes and scales, primitives, registries, chart state and settings schema, market replay, symbol comparison, chart linking, warm-load bar cache, interval registry, chart timezone, trading visualization, OpenAlgo feeds, EMA/RSI/ATR/Supertrend calculators, vector SVG export, the render backend port | 67 KB |59| `openalgo-charts/indicators` | 102 built-in indicators + the Tier-2 external-data contract | 30 KB |60| `openalgo-charts/draw` | 51 drawing tools + a headless `DrawingController` with multi-select, z-order, a per-tool settings schema, the 1.9.x migration, the clipboard and the icon builders | 26 KB |61| `openalgo-charts/transform` | Heikin Ashi, Renko, Range bars, Line Break, Point and Figure, Kagi | 5 KB |62| `openalgo-charts/profile` | Volume Profile, Market Profile (TPO), Footprint, order flow | 11 KB |63| `openalgo-charts/trade` | Order engine, state machine, order/position/bracket lines, DOM ladder | 75 KB with base |64| `openalgo-charts/webgl` | The WebGL2 series backend behind `renderer: 'auto' \| 'webgl2'`; composites into the pane's canvas, falls back to 2D for the session on context loss | 7 KB |65| `openalgo-charts/widget` | `createWidget`: the chart with a top bar, drawing rail, status line, settings and indicator dialogs, drawing properties, right-click menu, keymap and optional persistence. The only tier that ships DOM; imports the draw tier itself | 36 KB |6667Limits are the CI-enforced budgets in `.size-limit.json`. Measured on the 2.0.2 build: base engine 66.45 KB against 67 KB, draw tier 25.82 KB against 26 KB, indicator tier 27.27 KB against 30 KB, widget tier 35.56 KB against 36 KB, a widget terminal (base + draw + indicators + widget, what one `createWidget` call loads) 155.09 KB against 156 KB, and the whole package 182.39 KB against 183 KB. The aggregate budget always sits above the sum of the tier ceilings it contains, or it would fail while every tier it contains passes. Nothing is excluded from these figures because there are no runtime dependencies to exclude; the exact table is in [bundling-and-tiers](references/bundling-and-tiers.md).6869The clipboard lives in the **draw** tier, not the base one, because it needs the drawing-tool registry. `DrawingClipboard` and friends come from `openalgo-charts/draw`.7071## The 60-second chart7273```ts74import { createChart } from 'openalgo-charts';7576const chart = createChart(document.getElementById('chart')!);77const series = chart.addSeries('candlestick');7879series.setData([80 { time: 1705286700, open: 100, high: 101, low: 99.5, close: 100.6, volume: 1200 },81 { time: 1705286760, open: 100.6, high: 101.4, low: 100.2, close: 101.1, volume: 900 },82]);8384chart.fitContent();85```8687`time` is **UTC seconds**. The container must have a non-zero size before the chart can lay out.8889## Non-negotiable rules90911. **Time is UTC seconds everywhere, never milliseconds.** `Math.floor(Date.now() / 1000)`, not `Date.now()`. Feed adapters convert broker formats at the edge. Which wall clock those seconds are *labelled* in is `ChartOptions.timezone`, an IANA name defaulting to `Asia/Kolkata`; never offset the timestamps themselves to fake a zone, or every session anchor and gap moves with them.922. **The time axis is gapless and index-based, not timestamp-proportional.** x is `logicalIndex * barSpacing`, so weekends, holidays and session breaks have no index and collapse to nothing. Never compute an x from a timestamp difference; use `chart.timeToCoordinate(t)` or `chart.timeScale`.933. **One bar per time per series, ascending.** Duplicate times collapse to the last one written.944. **Nothing crosses a chart boundary as a logical index. Sync by instant.** Because rule 2 makes the index a property of *that chart's own bars*, index 300 is a different moment on every chart in a grid. Any value passed between charts (a linked crosshair, a shared viewport, a highlighted bar) converts index to time on the sender with `indexToTimeFloat` and time back to index on the receiver with `timeToIndex` / `timeToIndexFloat`, against that chart's own `DataLayer`. `createLinkGroup`, `followerIndex` and `followerRange` do this; a hand-rolled `b.setVisibleLogicalRange(a.getVisibleLogicalRange())` does not, and it looks correct until the two charts hold different bars. An instant outside the receiver's first or last bar is an **absence**: draw nothing rather than clamping to an edge bar. See [chart-linking](references/chart-linking.md).955. **Never cache the forming bar.** A closed bar is immutable; the last bar is alive until its interval ends. Serving a snapshot of it puts a stale close on the last-price line, the axis tag, the header LTP and every indicator computed off it, with no spinner and no staleness badge. `withBarCache` drops trailing unclosed bars for this reason. A fast wrong price is a worse failure than a slow right one on a chart that draws Buy and Sell buttons.966. **Never deep-import.** `import { X } from 'openalgo-charts'` or a published tier specifier only. A deep path into `dist/` internals inlines a second copy of the registry Map, and `createChart` will never see what your tier registered. See `rollup.config.js`.977. **A tier must be imported before its features resolve.** `chart.addIndicator('macd')` needs `import 'openalgo-charts/indicators'`; `addSeries('kagi')` needs `import 'openalgo-charts/transform'` and throws a specific tier-naming error without it.988. **Price-scale margins are fractions of the pane height, not padding on the data span.** The data band occupies `1 - marginTop - marginBottom` of the pane.999. **Drawing anchors are `{ time, price }`, never pixels.** Pixel anchors slide the moment a gap collapses or the user zooms.10010. **Canvas drawing happens in bitmap pixels.** Multiply media px by `dpr` in any custom primitive, or it blurs and misaligns on HiDPI.10111. **`chart.trading` renders trade state; it does not place orders.** The host pushes exchange state in and turns the emitted `trading:*` events into broker calls. The transactional path is `openalgo-charts/trade`.10212. **The engine ships no DOM chrome; `openalgo-charts/widget` is the one tier that does.** The base and the six engine tiers have no toolbar, no dialogs, no settings forms, no command palette. Drawing tools, indicator settings, replay transports and order menus are the host's UI, driven by descriptors and events. A host that does not want to write that chrome imports the widget tier and calls `createWidget` (see [widget](references/widget.md)); a host that does gets the *description* of that UI: `chartSettingsSchema(chart)` for a settings dialog, the `contextmenu` event for a right-click menu, and `chart.priceAxisState(...)` for a menu raised on a price axis. Chrome you write is held to the UI standard in [themes-and-styling](references/themes-and-styling.md#host-chrome-the-ui-standard): styled scrollbars, small square swatches, paired up/down colours on one row, themed form controls, and no control with nothing behind it.10313. **A control with no data in the current context is rendered disabled, not hidden.** `PriceLevels.available(kind)` and `PriceAxisState.active` / `scaled` / `movable` exist to be read for exactly this. Hiding it loses the information that the state is off.10414. **Never use emojis or icons in code, comments, logs, or generated UI text.** Project rule.105106## References107108Detailed reference for each topic is in `references/`. Read the one that matches the task before writing code.109110| Reference | Topic |111|---|---|112| [core-api](references/core-api.md) | `createChart`, `ChartOptions`, `SeriesApi`, lifecycle, coordinate conversion, the render/invalidation model |113| [chart-types](references/chart-types.md) | All 13 base series types, their styles and autoscale rules, runtime type switching |114| [scales-and-panes](references/scales-and-panes.md) | Price/time scale options, log and inverted modes, left/right/overlay scales, per-axis state and moves, axis chrome (corner clock, bar countdown, tick priority), pane weights and layout |115| [themes-and-styling](references/themes-and-styling.md) | `ChartTheme` keys, dark/light, gradients, `SeriesStyle` precedence, price formatting, and the UI standard for host chrome |116| [data-and-time](references/data-and-time.md) | `Bar` shape, UTC seconds, the chart timezone and the time helpers, setData/update/prependData, the logical-index model, history paging, tick and volume bars |117| [feeds-and-live](references/feeds-and-live.md) | `DataFeed` contract, OpenAlgo REST/WS/live feeds, `CandleBuilder`, the interval registry, `withBarCache` warm loading, writing a custom feed |118| [events-and-state](references/events-and-state.md) | The full event catalogue with payloads, `getState`/`restoreState`, saved layouts |119| [indicators](references/indicators.md) | The 102 built-ins with exact ids, placements and input defaults, the settings model, levels/ranges/fills, signal markers, `registerIndicator`, the Tier-2 external-data contract |120| [transforms](references/transforms.md) | Heikin Ashi, Renko, Range, Line Break, Point and Figure, Kagi |121| [drawing-tools](references/drawing-tools.md) | The 51 tools, `DrawingController`, anchors, magnet, undo, copy/cut/paste and the clipboard payload, persistence, shortcuts, custom tools |122| [primitives-and-plugins](references/primitives-and-plugins.md) | `IPrimitive`, z-order, hit-testing, the dpr contract, built-in primitives including the `PriceLevels` reference-level family, `registerChartType` |123| [replay-and-compare](references/replay-and-compare.md) | `ReplayController` and its transport events, `addComparison`, the overlay-scale mechanism, timestamp alignment |124| [chart-linking](references/chart-linking.md) | `createLinkGroup`, the sync-by-instant rule, `followerIndex` / `followerRange`, the linked crosshair, host-driven symbol sync |125| [settings-and-menus](references/settings-and-menus.md) | `chartSettingsSchema` and its round trip, the five tabs, the `colorPair` row, the timezone control, canvas options (grid, crosshair, scales, margins), status-line switches, the `contextmenu` event and the price-axis menu |126| [trading](references/trading.md) | The data-driven on-chart trading layer, `trading:*` events, order/position/bracket lines |127| [trade-tier](references/trade-tier.md) | `OrderEngine`, order state machine, validation, analyzer mode, DOM ladder, broker adapters |128| [profiles-and-orderflow](references/profiles-and-orderflow.md) | Volume Profile, Market Profile (TPO), Footprint, cumulative delta, the trade-data dependency |129| [react-integration](references/react-integration.md) | React and Next.js lifecycle, keeping orchestration out of React, SSR, resize |130| [bundling-and-tiers](references/bundling-and-tiers.md) | Entry points, registry identity, tree-shaking, script/ESM/import-map loading, size budget |131| [widget](references/widget.md) | `createWidget` and the widget tier: options, the handle, events, the context every dialog is handed, the keymap scopes, the tokens, every exported mount and helper, packaging |132| [interactions](references/interactions.md) | Pan/zoom/pinch, crosshair modes, the keyboard system, touch, accessibility |133| [pitfalls](references/pitfalls.md) | The verified foot-gun list. Read this when something behaves unexpectedly |134135## Triage136137| User asks about | First check | Answer with | Avoid |138|---|---|---|---|139| First chart / blank chart | container size, `dist` present | `createChart` + `addSeries` + `setData` | assuming a CSS import or web component |140| Bars in the wrong place | units of `time` | UTC seconds | `Date.now()` milliseconds |141| Axis shows the wrong hours | `chart.timezone()` | `timezone: 'America/New_York'` on `createChart`, or `setTimezone` | shifting bar timestamps, or a `timeFormatter` that only relabels |142| Gaps for weekends | the gapless-axis rule | it is intended; whitespace points if you want a gap | shifting timestamps |143| Realtime ticks | last-bar vs full replace | `series.update(bar)` | `setData` on every tick |144| Loading older history | `setHistoryLoader` | `prependData` + `historyLoadComplete` | rebuilding and re-fitting |145| Indicator not found | is the tier imported | `import 'openalgo-charts/indicators'` | registering it by hand |146| Which indicator id to use | the catalogue in [indicators](references/indicators.md) | the exact id from the 102-row table, guarded with `hasIndicator(id)` | guessing an id from the display name |147| Indicator settings UI | descriptor `inputs` + generated style keys | build the form from the descriptor, apply with `setSettings`; or `openalgo-charts/widget` (`mountIndicatorSettings`, or the whole terminal via `createWidget`) for a host that does not want to write chrome | expecting the engine itself to have a dialog |148| Drawing tools | `DrawingController` | headless controller + host toolbar; or `createWidget` from `openalgo-charts/widget`, which ships the rail | expecting the engine itself to have a toolbar |149| Volume in its own pane | `paneIndex` and `priceScaleId` | `addSeries('histogram', { paneIndex: 1 })` | a second chart |150| Volume inside the price pane | overlay scale | `priceScaleId: ''` plus scale margins | manual y math |151| Placing real orders | which layer | `openalgo-charts/trade` `OrderEngine` | `chart.trading` (visualization only) |152| Order lines on the chart | which layer | `chart.trading` sync + `trading:*` events | drawing your own price lines |153| Custom overlay | primitive vs chart type | `IPrimitive` + `addPrimitive` | a custom chart type for decoration |154| Bar-by-bar replay | `ReplayController` | headless controller + host transport bar; indicators rebuild from the prefix | a second chart, or slicing data by hand |155| Two symbols on one chart | `addComparison` | hidden overlay scale + a rebasing pane mode | a second series on the same price axis |156| A grid of charts moving together | `createLinkGroup` | one group, three switchable channels; everything syncs by **time**, never by logical index | copying `getVisibleLogicalRange()` between charts |157| Linked crosshair shows the wrong bar | the index-to-time conversion | `followerIndex(target.dataLayer, time, whenMissing)` | assuming index N is the same instant on both charts |158| Slaving the symbol across charts | who owns the instrument | the host emits `'symbol'` / calls `setSymbol`, each member gets an `onSymbol` loader | expecting the engine to know what a symbol is |159| Copy and paste a drawing | `draw.copy()` / `cut()` / `paste()` | await them; the host owns the key chords | a synchronous call, or throwing on foreign clipboard text |160| Refetching the same bars on every switch | `withBarCache(feed, opts)` | wrap the feed; key is symbol/exchange/interval, range is sliced | caching the forming bar, or keying on the range |161| A monthly / tick / volume timeframe | `registerInterval` | a `Bucketing` rule (`calendar`, `ticks`, `volume`), then `bucketStartOf` | `intervalToSeconds` on a code that has no fixed length |162| Unknown interval code | `isKnownInterval(code)` | validate the picker; `resolveInterval` throws `UnknownIntervalError` | catching the throw and defaulting to 60 seconds |163| Percent / rebased axis | `priceScale.mode` | `'percentage'` or `'indexed-to-100'` plus a baseline | recomputing the data into percentages |164| Settings dialog | `chartSettingsSchema(chart)` | render the tabs, round-trip with `read`/`applyChartSettings`; or `mountSettingsDialog` / `createWidget` from `openalgo-charts/widget` | hardcoding a control list |165| Right-click menu | `contextmenu` event | `preventDefault()` + `target.kind` | hit-testing the pointer yourself |166| Menu on a price axis | `target.side` / `target.scaleId` | `chart.priceAxisState(pane, scaleId)`, acted on by the `setPriceAxis*` calls | assuming pane 0's right scale |167| Previous close, session high/low, bid/ask lines | `PriceLevels` | one primitive, one options group per level, `line` and `label` together | a `PriceLine` per level with its own tag bookkeeping |168| A level or axis row with no data | `available(kind)`, `state.active` | render it disabled with its state visible | hiding the control |169| Corner clock, bar-close countdown | `ChartOptions.axisChrome` | `{ sessionClock: true, barCountdown: true }`, plus a `clock` for a delayed feed | a DOM overlay positioned over the axis |170| Saved layouts | `getState` / `restoreState` | one JSON payload | hand-rolled serialisation |171| React lifecycle | where the chart instance lives | create in an effect, hold in a ref, `chart.destroy()` on cleanup | chart instance in state |172| Bundle size | which tiers are imported | drop the unused tier import | code-splitting the base |173174## Core API cheat sheet175176Verified names. Get these wrong and nothing works.177178```ts179// chart180const chart = createChart(el, options);181chart.addSeries(type, { paneIndex, style, priceScaleId, priceFormat });182chart.addIndicator(id, settings, { paneIndex }); // needs the indicators tier183chart.addPriceLine(opts, paneIndex);184chart.addPrimitive(primitive, paneIndex);185chart.fitContent();186chart.applyOptions({ theme, grid, canvas, statusLine, priceScale, priceFormatter, timeFormatter, timezone, crosshairMode });187chart.setTheme(theme);188chart.setTimezone('America/New_York') / chart.timezone(); // IANA name, default 'Asia/Kolkata'189chart.panes(); // readonly Pane[]190chart.primarySeries() / chart.primarySeriesInfo();191chart.setCanvasOptions(opts) / chart.setStatusLineOptions(opts) / chart.setPriceScaleOptions(opts);192chart.setAxisChromeOptions({ sessionClock: true, barCountdown: true }) / chart.axisChromeOptions();193194// one price axis at a time (what a menu on an axis strip acts on)195chart.priceAxisState(paneIndex, scaleId); // PriceAxisState | null196chart.setPriceAxisOptions(paneIndex, scaleId, patch);197chart.setPriceAxisAutoFit(paneIndex, scaleId, on);198chart.setPriceAxisLockRatio(paneIndex, scaleId, on); // boolean199chart.movePriceAxis(paneIndex, 'right', 'left'); // boolean200PRICE_SCALE_MODES; // ['linear','logarithmic','percentage','indexed-to-100']201chart.getState() / chart.restoreState(state);202chart.on(event, cb); // returns an unsubscribe function203chart.subscribeCrosshairMove(cb);204chart.timeToCoordinate(t) / chart.coordinateToTime(x);205chart.priceToCoordinate(p, paneIndex) / chart.coordinateToPrice(y, paneIndex);206chart.destroy(); // idempotent; emits 'destroy', then drops every listener207chart.isDestroyed; // getter, not a method208209// getters, not methods210chart.timeScale; // TimeScale211chart.dataLayer; // DataLayer212chart.trading; // TradingController, created on first access213chart.shortcuts; // ShortcutManager | null214215// series handle216series.setData(items);217series.update(item);218series.prependData(items);219series.getData();220series.applyOptions({ visible: false }); // not setStyle221series.priceScale();222series.createMarkers();223series.remove();224225// headless controllers, base bundle, no tier import226new ReplayController(chart, { bars, startIndex, barMs }); // seek/step/play/pause/stop/state227addComparison(chart, { symbol, bars }); // setBars/remove/list/alignment228chartSettingsSchema(chart) / readChartSettings(chart) / applyChartSettings(chart, patch);229230// linked chart grid (base bundle). Syncs by INSTANT, never by logical index.231const group = createLinkGroup({ crosshair: true, viewport: true, symbol: false, whenMissing: 'nearest' });232group.add(chart, { symbol: 'RELIANCE', onSymbol: (s, c) => loadBars(s, c) });233group.remove(chart) / group.setOptions(patch) / group.setSymbol(chart, s) / group.destroy();234group.members() / group.has(chart) / group.symbol() / group.crosshairIndex(chart);235followerIndex(followerDataLayer, timeSec, 'nearest' | 'hide'); // number | null236followerRange(leaderDataLayer, followerDataLayer, range); // LogicalRange | null237238// warm-load bar cache: wraps ANY DataFeed (base bundle)239const feed = withBarCache(sourceFeed, { ttlMs, max, maxBars, storage, now, intervalSeconds });240feed.getBars({ symbol, exchange, interval, from, to, noCache: true });241feed.invalidate({ symbol, exchange, interval }) / feed.clear() / feed.stats() / feed.source;242243// interval registry (base bundle): a bucketing rule, not a duration244registerInterval({ code: '1MO', bucketing: { mode: 'calendar', unit: 'month', count: 1 } });245resolveInterval(code) // throws UnknownIntervalError246tryResolveInterval(code) / isKnownInterval(code) / registeredIntervals() / unregisterInterval(code);247bucketStartOf(bucketing, timeSec, zone?) / nextBucketStart(...) / isTimeBucketed(bucketing);248249// drawing clipboard (draw tier) - all async250await draw.copy(target?) / await draw.cut(target?) / await draw.paste();251draw.clipboard().lastError();252253// reference levels (a primitive: previous close, session high/low, bid/ask, ...)254const levels = new PriceLevels({ levels: { previousClose: { line: true, label: true } } });255chart.addPrimitive(levels, 0);256levels.setLevel(kind, patch) / levels.setQuote(q) / levels.values() / levels.available(kind);257computePriceLevels({ bars, anchorTime }); // the same numbers, pure, no canvas258```259260`chart.fitContent()` takes no arguments; `chart.timeScale.fitContent(barCount)` does.261262## Code-generation rules263264- **Verify option names against local typings before writing them.** Many similarly named options exist at chart, series, pane and scale level. Confirm which level owns the option.265- **Minimal snippets.** One feature per code block. Combining an indicator, a drawing tool and a trading line in one snippet hides which API does what.266- **Import from the package entry or a published tier specifier.** Never a deep path.267- **Match the user's host.** A React user wants the effect lifecycle; a vanilla user does not; a no-bundler user needs the standalone build.268- **State which tier a feature needs** whenever the answer uses one.269- **Do not invent.** If a name does not appear in the installed typings or upstream source, it does not exist.270271## Answer contract272273When answering an openalgo-charts question:2742751. Name the API and the tier it lives in.2762. Show one minimal snippet, not a mega-demo.2773. Call out the main foot-gun for that task, from [pitfalls](references/pitfalls.md).2784. Say what local source was checked (version, typings), or state that it could not be verified.