Source: /guide/getting-started.md # Getting started charton ships as eight packages: | Package | Use it for | | --- | --- | | `@chart-on/core` | The chart engine. Canvases only, no UI chrome. Bring your own controls. | | `@chart-on/ui` | Optional shell: top toolbar, drawing-tool rail and styles around a core chart. | | `@chart-on/indicators` | Optional [built-in indicators](./indicators#built-in-indicators). | | `@chart-on/ta` | Optional [technical-analysis helpers](./technical-analysis) for writing indicators. | | `@chart-on/locales` | Optional [translations](./localization#locale-packs) of the built-in text, one entry per language, applied through the `localization()` plugin. | | `@chart-on/react` | Optional [components and hooks](./component-wrapper) for apps built with a component-based UI framework. | | `@chart-on/vue` | Optional [components and composables](./template-wrapper) for apps built with a template-based component framework. | | `@chart-on/svelte` | Optional [components and an action](./compiled-wrapper) for apps built with a compiler-based component framework. | ## Install Install the engine, and the UI shell if you want it: ```bash pnpm add @chart-on/core # optional UI shell pnpm add @chart-on/ui ``` Use the same version for every `@chart-on/*` package ([versioning](./versioning)). Requires a browser with WebGL2. No bundler? Use the [script-tag build](./frameworks#with-a-script-tag). ## Core only The container needs a size. The chart fills it and follows resizes automatically. ```html
``` Every example in these docs is a module whose `mount(el)` draws into an element and returns a clean-up function; in your app, `el` is that `#chart` element. The chart below runs exactly the code under it. `sampleBars` makes up [sample data](https://github.com/chart-on/chart-on/blob/main/docs/examples/sample-data.ts) in the same shape as yours: Example `first-chart` (runs live on the docs page): ```ts import { createChart } from '@chart-on/core'; import type { ExampleContext } from './types'; import { sampleBars } from './sample-data'; export default function mount(el: HTMLElement, { theme }: ExampleContext) { const chart = createChart(el, { theme }); const candles = chart.addCandles({ title: 'SAMPLE' }); // Bars are { time (unix seconds), open, high, low, close, volume? }. candles.setData(sampleBars()); chart.fitContent(); return () => chart.destroy(); } ``` ## With the UI shell The shell adds a small toolbar (interval, chart style, indicators, price scale, settings and fullscreen) and the drawing-tool rail by default; a bottom bar is opt-in (`bottomBar: true`). `theme` picks the `'dark'`, `'light'` or `'system'` scheme (default `'system'`). See [Shell, toolbar and rail](./ui-shell). Example `shell` (runs live on the docs page): ```ts import { createChartShell } from '@chart-on/ui'; import '@chart-on/ui/style.css'; import type { ExampleContext } from './types'; import { sampleBars } from './sample-data'; export default function mount(el: HTMLElement, { dark }: ExampleContext) { // Toolbar, drawing-tool rail and bottom bar around the chart. const shell = createChartShell(el, { theme: dark ? 'dark' : 'light' }); shell.chart.addCandles({ title: 'SAMPLE' }).setData(sampleBars()); shell.chart.fitContent(); return () => shell.destroy(); } ``` ## Live updates ```ts candles.update({ time: 1767229200, open: 103, high: 106, low: 101, close: 105 }); // same time: replace candles.update({ time: 1767232800, open: 105, high: 107, low: 104, close: 106 }); // newer: append ``` ## Clean up ```ts chart.destroy(); // or shell.destroy() when using the UI shell ``` `destroy()` is safe to call twice. Code that still holds the chart afterwards (a live `update`, a late data response) does no harm: nothing is drawn; view changes (`scrollToRealtime`, `fitContent`, `setVisibleTimeRange`, `resetView`), `applyOptions` and removing series do nothing; `chart.data` is `null`; `captureImage` resolves to `null`. Calls that load data fail loudly instead: `setSymbol` rejects and `compare` throws a `ChartError` with code `'destroyed'`, and a `setSymbol` still loading when the chart is destroyed rejects the same way. ## Requirements and failure modes The chart draws its series with WebGL2, which every current desktop and mobile browser supports. There is no fallback renderer. Supported browsers: Chrome and Edge 111+, Firefox 114+, Safari 16.4+ (macOS and iOS). WebGL2 alone needs less (Safari has it from 15), but the packages are compiled for this set, so older browsers fail on the JavaScript before they reach WebGL. The list is the `browserslist` field of each package's `package.json`, so tools that read it (autoprefixers, transpilers) pick it up. Node.js 22+ is needed only for tooling and tests, for example `@chart-on/core/testing`. Check before creating a chart if you want to show your own message: ```ts import { createChart, isWebGL2Supported, ChartError } from '@chart-on/core'; if (!isWebGL2Supported()) showMessage('This chart needs WebGL2.'); try { createChart(el); } catch (error) { if (error instanceof ChartError) console.warn(error.code); // 'webgl2-unavailable' or 'webgl-shader' } ``` - Without WebGL2, `createChart` throws a `ChartError` with code `'webgl2-unavailable'` and leaves the container untouched. A shader that fails to compile or link (usually a driver bug) throws code `'webgl-shader'`, also leaving the container untouched. `createChartShell` removes its markup and rethrows the same error, so wrap it the same way and render your own fallback. - The browser can take the GPU context away, for example after a driver reset or when a page opens too many contexts. The chart emits `context:lost`. The frame loop keeps running: axes, labels, the crosshair and drawings keep drawing and responding, and only the GPU-drawn series are skipped, so they disappear until the browser restores the context. On `context:restored` the chart re-creates its GPU buffers and textures from the series data and redraws. Nothing you set is lost. Both events carry `reason`: `'browser'` here, `'budget'` when the chart gave the context up itself (next point). - Browsers cap live WebGL contexts per page (commonly 16 on desktop and 8 on mobile) and drop the oldest past that, so a page with many charts would blank its first ones. Each chart holds one context, and the charts share a page budget: 12 contexts, or 6 with a coarse pointer. Past it, the chart that has been out of view the longest gives its context up (`context:lost` with `reason: 'budget'`), or the least recently used one when every chart is in view. It takes a context back, evicting another, when it scrolls into view or gets a pointer, wheel or focus event. Set the budget with `setContextBudget(n)`, read it with `getContextBudget()`; lowering it evicts at once. Leave room for other WebGL content on the page, such as maps or 3D views. - If the restored context cannot be set up again, `context:restore-failed` fires with a `ChartError` (code `'webgl-restore-failed'`, the original error in `cause`) and the series stay blank. Destroy the chart and create a new one to recover. - While the context is lost, `captureImage()` resolves to `null`; other methods keep working. ## Using the docs with coding assistants Besides these pages, the docs site publishes plain-text copies that coding assistants read more reliably than HTML: - `/llms.txt`: an index of every guide page, with its URL and a one-line description. - `/llms-full.txt`: every guide page in one Markdown file, in sidebar order. Live examples appear as their source code. - Each page as Markdown at the same path with `.md`, such as `/guide/series.md`. The link sits at the bottom of each page. Point your assistant at the index, or at the full text when it can read a long file. ## Next - [Embedding the chart](./embedding): container size, lifecycle, resizing, several charts, focus and memory. - [Security and strict pages](./security): content security policy, Trusted Types and offline use. - [Core concepts](./concepts): time, panes, the main series. - [Series and data](./series): candles, lines, histograms, heatmaps. - [Events](./events): crosshair, visible range, loading older history. --- Source: /guide/concepts.md # Core concepts A few rules explain almost everything the chart does. ## Time is unix seconds Every `time` in the API (bars, points, drawing anchors, events) is a unix timestamp in **seconds**. Never milliseconds. In development builds the chart warns when times look like milliseconds or are not whole seconds (see [Errors and warnings](./errors)). ## The main series defines the time axis The first candle series you add becomes the **main series**. Its bars define the time axis: one slot per bar, evenly spaced, so gaps such as weekends do not leave empty space. Other series align to those bar times. A point whose time has no matching main bar is not drawn; after the newest bar, the projected bar times count as matches (see [Time after the last bar](./view-and-scale#time-after-the-last-bar)). ```ts const candles = chart.addCandles({ title: 'BTC' }); // main series const ema = chart.addLine({ title: 'EMA 21' }); // aligns to candle times ``` Past the newest bar the time axis keeps going on a projected calendar, so times in the future have slots too (see [Time after the last bar](./view-and-scale#time-after-the-last-bar)). Removing the main series clears the time axis. ## Data is ascending, updates only move forward - `setData` requires ascending times and throws otherwise. `setData(data, { normalize: true })` sorts slightly dirty data instead and keeps the last of equal times. - `update` with the **same** time as the last point replaces it. - `update` with a **newer** time appends. - `update` with an **older** time is dropped, and `data:dropped` reports it. Live bars from a data source follow the same rule. ## Bad values One rule for every way data comes in: - A **time** that is not a finite number throws, on every series, in `setData` (also with `normalize`), `update` and a heatmap's `updateColumn`. - An **invalid candle** throws in `setData` and `update`, with the index and field in the message. Fix the data, or pass `{ normalize: true }` to drop such bars. A candle is invalid when: - its time, open, high, low or close is not a finite number, - it has a `volume` that is not a finite number (leave `volume` out for none), or - its high is below its low. Anything else is drawn as given: an open or close outside low..high, and a negative volume, which volume candles draw at the normal width, like a bar without volume. Data sources and `isValidBar` use the same definition. - A line, histogram or heatmap **value** that is not finite (`NaN`, `Infinity`, `undefined`) is a **gap**: nothing is drawn there, a line with `breakGaps` breaks, and autoscale ignores it. This matches indicators, where `NaN` means no value. An `update` with no value for the last point's time removes that point. - A heatmap `updateColumn` older than the last column is dropped, like an older `update`. - A data source's bars are filtered instead: invalid bars, duplicates and live bars older than the last one are left out. Everything left out is reported once through the `data:dropped` event: ```ts chart.on('data:dropped', ({ reason, bar, series }) => { // reason: 'older' | 'invalid' | 'duplicate' }); ``` ## Panes stack vertically The chart starts with one pane (the main pane). `addPane()` adds another below it. Each pane has its own price scale. Series go into the main pane unless you pass a pane. See [Panes](./panes). ## Listeners return unsubscribe functions ```ts const off = chart.on('crosshair:move', (e) => console.log(e)); off(); // stop listening ``` There is no separate `off` method. ## No hidden state The chart never reads or writes storage on its own. To persist drawings, serialize them and save them yourself. See [Drawing tools](./drawings#save-and-restore). --- Source: /guide/embedding.md # Embedding the chart How the core chart fits into a page without a framework. The [UI shell](./ui-shell) and framework wrappers build on the same rules; [Frameworks and plain pages](./frameworks) covers the script-tag build, server rendering and the contract wrappers follow. ## The container `createChart(el)` adds one element, `chart.root`, inside `el`. It fills `el` (`width: 100%; height: 100%`) and holds the canvases and overlays. `el` must have a size of its own: ```html
``` - A percentage height needs every ancestor up to a sized one to have a height too (`html, body { height: 100% }` and so on), or it resolves to 0. - A flex or grid child needs `min-width: 0` and `min-height: 0`, or the canvases keep it from shrinking. - At 0 × 0 (`display: none`, a collapsed panel, a hidden tab) the chart draws nothing and keeps its state. It draws again as soon as the container has a size. The chart leaves `el` itself alone, apart from appending `chart.root`, so you can style and position `el` as you like. `chart.root` carries the class `pc-chart`. Its size, background, `touch-action`, `user-select` and focus outline come from a `:where(.pc-chart)` rule the chart adds to the page (to the shadow root, when the container is in one), not from inline styles. That rule has no specificity, so any rule of yours that targets the root wins, without `!important`: ```css #chart .pc-chart { background: transparent; /* let the page show through before the first frame */ } ``` Only `position: relative` and `overflow: hidden` stay inline, since the canvases are laid out against them. `touch-action` follows the chart's touch options (it lets vertical swipes scroll the page when touch panning is off); override it only if you handle touch yourself. Browsers that cannot add the rule (no constructed stylesheets), and containers in another document than the one the library loaded in (an iframe or a popup window), get the same styles inline. A container that is not yet in the page when the chart is created gets the rule on the page's document, so create the chart after attaching a container that lives in a shadow root. ## Lifecycle ```ts import { createChart, createMockDataSource, dataSource } from '@chart-on/core'; const el = document.getElementById('chart')!; // 1. Create: options can be set now or later. `dataSource()` lets it load through a source. const chart = createChart(el, { timezone: 'UTC', plugins: [dataSource()] }); // 2. Data: push bars yourself (`addCandles().setData(...)`), or attach a data source. chart.setDataSource(createMockDataSource()); await chart.setSymbol('DEMO', '1h'); // 3. Change options at any time; only the fields you pass change. chart.applyOptions({ crosshair: { mode: 'free' } }); // 4. Destroy when the element goes away. chart.destroy(); ``` `destroy()` stops the frame loop, timers, observers and the data source subscription, releases the WebGL context and removes `chart.root`. Call it once, before or after removing `el`; it is safe to call twice. Afterwards the chart draws nothing and calls that load data reject (see [failure modes](./getting-started#requirements-and-failure-modes)). ## Resizing A `ResizeObserver` on `chart.root` resizes the canvases and redraws in the same frame, so the chart follows its container through window resizes, layout changes and panels opening. It also follows changes of the device pixel ratio (moving to another screen, browser zoom). Call `chart.resize()` yourself only for changes the observer cannot see: - a CSS `transform: scale(...)` on an ancestor (the layout size does not change); - printing, if your print styles give the container another size. `resize()` measures and draws at once, so it is cheap to call when unsure. To keep the same time range on screen when the width changes, instead of the same bar spacing, set `lockVisibleRangeOnResize: true` (see [view and scale](./view-and-scale#resizing-and-new-bars)). ## Several charts on one page Each chart is independent: its own canvases, WebGL context, data and options. Create as many as you need and destroy each one. To keep their crosshair, view, symbol or interval in step, put them in a [link group](./link-groups). ```ts import { createChart, createMockDataSource, dataSource } from '@chart-on/core'; const source = createMockDataSource(); const charts = (['1h', '4h', '1d'] as const).map((interval, i) => { const chart = createChart(document.getElementById(`chart-${i}`)!, { plugins: [dataSource()] }); chart.setDataSource(source); void chart.setSymbol('DEMO', interval); return chart; }); // Later: for (const chart of charts) chart.destroy(); ``` - A chart scrolled out of view (seen through an `IntersectionObserver`), or on a page in a background tab (`document.visibilityState`), draws nothing. Its data, indicators and view keep updating and its events (`view:change`, history requests) keep firing; when it is shown again it draws once, before the browser paints, with any animation at its end rather than replayed. So a page with many streaming charts spends drawing time only on the ones in view. `captureImage()` and `resize()` still draw a hidden chart. Set `renderWhenHidden: true` to keep drawing anyway, e.g. for a host that captures the canvases of hidden charts itself. Where `IntersectionObserver` is missing the chart counts as in view. - Browsers limit the number of live WebGL contexts per page (commonly 16 on desktop and 8 on mobile) and take the oldest away past that; the chart then emits `context:lost`. Destroy charts you no longer show rather than hiding them. For more charts than that, use a shared context (below), or static charts (`interactive: false`) for thumbnails. ### One context for many charts `createChart(el, { gl: 'shared' })` draws through one WebGL context shared by every chart created that way, so a dashboard or a multi-chart layout can hold any number of interactive charts: ```ts import { createChart, sharedContext } from '@chart-on/core'; const charts = cells.map((cell) => createChart(cell, { gl: 'shared', plugins: [sharedContext()] })); ``` - The shared context comes with the [opt-in](./features#opt-in-features) `sharedContext()` plugin, which `builtins()` includes and `createChartGrid` adds to each cell. It is read when the chart is created, so pass it in `plugins` then. Without it a `gl: 'shared'` chart gets its own context (`chart.options.gl` reads `'own'`), and development builds warn once ([`shared-context-missing`](./errors#shared-context-missing)). - Each chart draws its series into a hidden shared canvas, then copies its part into its own canvas in the same frame. The copy costs little: eight charts panning together took about 1.1 ms per frame shared against 0.9 ms with their own contexts in our measurements. - The shared context is made by the first shared chart and released with the last one. It counts as one context in the [context budget](./performance), and stays while any shared chart is in view or used. - A lost context is restored for every shared chart: each emits `context:lost` and `context:restored`. Destroying one chart frees only its own GPU objects. - `gpuMemory()` reports the whole shared context, not one chart's part. - `gl` is fixed at creation; the default is `'own'`, one context per chart. - Keyboard shortcuts go to the chart that has focus by default, so charts do not fight over keys (see below). - A data source can be shared; each chart loads and caches its own bars. ## Keyboard and focus `chart.root` is focusable (`tabIndex = 0`), so it takes part in the page's Tab order, and clicking the chart focuses it. With the default `keyboard: { scope: 'focus' }` the chart handles its [shortcuts](./interaction#keyboard) (arrows and `PageUp` / `PageDown` to pan, `+` and `-` to zoom, `Escape`, `Delete`, copy, paste, undo and redo for drawings) only while it has focus. | You want | Set | | --- | --- | | No keyboard shortcuts at all | `features: { keyboard: false }`. The chart stays in the Tab order | | The chart out of the Tab order (a static or decorative chart) | `focusable: false`. A click still focuses it, so its shortcuts still work once clicked | | Shortcuts also while the pointer is over the chart, without a click | `keyboard: { scope: 'hover' }` | | Shortcuts anywhere on the page (one chart per page). Drawing keys still need the chart focused or hovered | `keyboard: { scope: 'global' }` | Keys typed into inputs, text areas, selects and editable elements, or used inside dialogs and other interactive widgets, never reach the chart. ## Pixel ratio The 2D layers (axes, labels, crosshair, drawings) render at the full device pixel ratio. The WebGL layer is capped at `maxGlDpr` (default 2): on a 3× screen it renders 2× and is scaled up, which fills 4 instead of 9 device pixels per css pixel, with no visible change for candles and lines. Lower it to 1 on weak GPUs with large charts; raise it for pixel-exact heatmap cells on very dense screens. It can be changed with `applyOptions` at any time. ## Memory - A data source keeps the bars of the last `cacheSize` symbol and interval combinations (default 8), so switching back is instant. Lower it with `setDataSource(source, { cacheSize: 2 })` for long histories, or drop entries with `chart.data?.clearCache(symbol?, interval?)`. - Each series keeps a copy of the data you pass to `setData`, plus its GPU form. - A heatmap's grid is capped by its `memoryBudget` (default 256 MiB). - `destroy()` releases everything above; a chart that is only hidden keeps it. ## Production checklist - The container has a size before or right after `createChart`, and the chart is destroyed when the container goes away (route changes, closed panels, removed dashboard tiles). - Check `isWebGL2Supported()` first, or catch the `ChartError` from `createChart`, and show your own fallback (see [failure modes](./getting-started#requirements-and-failure-modes)). - Listen for `data:error` and `context:restore-failed` to show a message instead of a blank chart. - Fewer live charts than the browser's WebGL context limit; destroy rather than hide. - `focusable: false` for charts that are not meant to be used with the keyboard, so Tab does not stop on them. - Pass a `timezone` if your users should see exchange time rather than their own. - On a page with a content security policy or Trusted Types, allow the directives in [Security and strict pages](./security); the chart needs no `unsafe-eval`, no `unsafe-inline` and no network access of its own. --- Source: /guide/frameworks.md # Frameworks and plain pages `@chart-on/core` and `@chart-on/ui` are plain TypeScript with no framework dependency. This page covers three things: - [Plain pages](#plain-pages): a bundler, or a single script tag with no build step. - [Server rendering](#server-rendering): what is safe to import and call on a server. - [The wrapper contract](#the-wrapper-contract): the rules every framework wrapper follows, so components behave the same whichever framework you use. Follow them too when you write your own wrapper. Ready-made wrappers ship as `@chart-on/react`, see [Component wrapper](./component-wrapper), `@chart-on/vue`, see [Template component wrapper](./template-wrapper), and `@chart-on/svelte`, see [Compiled component wrapper](./compiled-wrapper). ## Plain pages ### With a bundler Import the packages as ES modules. Any bundler that understands `package.json` `exports` works, with no plugin or configuration. ```ts import { createChart, createMockDataSource, dataSource } from '@chart-on/core'; const chart = createChart(document.getElementById('chart')!, { plugins: [dataSource()] }); chart.setDataSource(createMockDataSource()); await chart.setSymbol('DEMO', '1h'); ``` With the shell, also import its stylesheet once: ```ts import { createChartShell } from '@chart-on/ui'; import '@chart-on/ui/style.css'; const shell = createChartShell(document.getElementById('chart')!, { theme: 'system' }); ``` ### With a script tag The script-tag build bundles the engine, the shell and its icons into a single file. `pnpm build` writes it to `packages/ui/dist/browser/`: | File | Use | | --- | --- | | `charton.iife.js` | A classic script that defines the global `charton`. Works from any page, also opened straight from disk. | | `charton.js` | The same as one ES module, for ` ``` Or as a module: ```html ``` `charton` holds every export of `@chart-on/core` and `@chart-on/ui`, so the rest of this guide applies unchanged: `createChart(...)` becomes `charton.createChart(...)`. A complete page is in [`examples/script-tag/index.html`](https://github.com/chart-on/chart-on/blob/main/examples/script-tag/index.html). Browsers load module scripts only over `http(s)`, so `charton.js` needs a local server, while `charton.iife.js` also works from `file://`. Use the packages instead of this build when you have a bundler: they share one copy of the engine with `@chart-on/indicators` and plugins, and your bundler leaves out what you do not use. ## Server rendering Frameworks that render on a server import your components there, so importing charton must not need a browser. It doesn't: - Importing `@chart-on/core`, `@chart-on/core/testing`, `@chart-on/core/internal`, `@chart-on/ui`, `@chart-on/indicators` and `@chart-on/ta` reads no `window`, `document`, `navigator`, storage or WebGL. A test checks this for every entry. - Pure helpers (formatting, intervals, indicator math, `parseCsv`, layout migration) work on a server. - `isWebGL2Supported()` returns `false` on a server instead of throwing. - `createChart` and `createChartShell` need a DOM. Call them only in the browser, after the element exists: in the framework's mount hook, never during rendering. Importing `@chart-on/ui/style.css` is a stylesheet import; let your framework's CSS handling take care of it, as for any other package stylesheet. ## The wrapper contract A framework wrapper is a thin component around `createChart` (or `createChartShell`). Every wrapper follows these rules, so apps can move between frameworks and get the same behaviour. ### 1. Create on mount, destroy on unmount - Create the chart or shell when the component's element is in the page: the mount hook, never the render function, and never on a server. - Call `destroy()` when the component unmounts. It stops the frame loop, timers, observers and listeners, releases the WebGL context and removes the chart's element. - `destroy()` is idempotent on both `Chart` and `ChartShell`. Development modes that mount, unmount and mount again leave nothing behind: tests mount and destroy a chart and a shell 100 times each and check that no WebGL context, observer, global listener, timer or animation frame is left. - Do not keep a destroyed chart around for a later mount. Create a new one. ```ts // The shape of any wrapper, written without a framework. function mountChart(el: HTMLElement, props: ChartProps) { const chart = createChart(el, props.options); return { chart, update: (next: ChartProps) => updateChart(chart, props, next), unmount: () => chart.destroy(), }; } ``` ### 2. Update options in place Changing a prop never re-creates the chart. Re-creating would lose the view, drawings, indicators and data, and costs a new WebGL context. - Chart options go through `chart.applyOptions(patch)`. It is a deep-partial update, so pass only what changed. Passing the whole options object again is also fine. - Shell settings go through the shell's runtime methods: `setTheme`, `setPalette`, `applyFeatures`. - Symbol and interval go through `chart.setSymbol(symbol, interval)`; a data source through `chart.setDataSource(source)`. - A few choices are fixed when the chart is created, for example `gl: 'own' | 'shared'`. A wrapper reads them only at mount. If an app needs to change one, it remounts the component on purpose, for example by changing a key. ### 3. Update data through the series Data never travels through a remount. - Data you hold yourself: keep the series handle from `chart.addCandles()` (or `addLine`, `addHistogram`, …) and call `series.setData(bars)` for a new data set, or `series.update(bar)` for a live tick. - Data from a [data source](./data-sources): the chart loads, pages and streams it; the wrapper only passes the source and the symbol. ### 4. Expose the instance The component gives the app the `Chart` (or `ChartShell`) it created, through the framework's usual way of reaching a component instance. The wrapper never hides part of the imperative API: anything the app can do with a chart it created itself, it can do through a wrapper. The instance exists only between mount and unmount, so it is empty during server rendering and before mount. ### 5. Forward events as framework callbacks - Each event the app listens to is subscribed with `chart.on(name, listener)` (or `shell.on`), which returns an unsubscribe function. Call every unsubscribe on unmount; `destroy()` also drops the chart's listeners. - Expose events in the framework's own style (callback props or component events), named after the chart's `noun:verb` event names. The payload is passed on unchanged. - Subscribe only to events the app actually listens to: some, like `crosshair:move` and `frame:render`, fire on every pointer move or frame. - When the app passes a new callback for the same event, call the newest one without re-subscribing, for example by keeping the latest callback in a variable the listener reads. ```ts // Events, written without a framework: the listener always calls the newest handler. function forwardEvents(chart: Chart, handlers: () => Partial void>>) { const names = Object.keys(handlers()) as (keyof ChartEvents)[]; const offs = names.map((name) => chart.on(name, (payload) => handlers()[name]?.(payload as never))); return () => offs.forEach((off) => off()); } ``` ### 6. Keep the engine framework-free A wrapper depends on `@chart-on/core` (and `@chart-on/ui` for the shell) as peer dependencies. The engine and the shell never depend on a framework, so they stay usable from plain pages and from every wrapper at once. ### Checklist for wrapper authors - [ ] The chart is created in the mount hook and destroyed in the unmount hook. - [ ] Mounting, unmounting and mounting again (development double mounting) leaves one chart and one WebGL context. - [ ] Changing any prop except the creation-only ones keeps the same `Chart` instance. - [ ] Data changes call `setData` / `update` on the series, not a remount. - [ ] The instance is reachable by the app. - [ ] Event props subscribe through `on` and unsubscribe on unmount; a new callback is used without re-subscribing. - [ ] The component renders on a server without errors (it outputs only its container element). --- Source: /guide/component-wrapper.md # Component wrapper `@chart-on/react` wraps the engine and the shell as components for apps built with a component-based UI framework, including apps that render on a server first. It follows the [wrapper contract](./frameworks#the-wrapper-contract): the chart is created when the component mounts and destroyed when it unmounts, and changing a prop updates the chart in place instead of creating a new one. | Export | From | What it is | | --- | --- | --- | | `` | `@chart-on/react` | The engine alone, in a `div`. | | `useChart()` | `@chart-on/react` | The `Chart` of the nearest `` or ``. | | `` | `@chart-on/react/shell` | The widget: toolbar, drawing rail, bottom bar and the chart. | | `useChartShell()` | `@chart-on/react/shell` | The `ChartShell` of the nearest ``. | ## Install ```bash pnpm add @chart-on/react @chart-on/core # Only for : pnpm add @chart-on/ui ``` The framework (version 18 or later), `@chart-on/core` and `@chart-on/ui` are peer dependencies: the app installs them once and the wrapper uses the app's copies. `@chart-on/ui` is optional, because `` lives in its own entry, `@chart-on/react/shell`. An app that only imports `@chart-on/react` never loads it. ## The engine Give the component a height; the chart fills its container. ```tsx import { useMemo } from 'react'; import { createMockDataSource, dataSource } from '@chart-on/core'; import { Chart } from '@chart-on/react'; // Plugins are read once, at creation: create the list once. const plugins = [dataSource()]; export function PriceChart({ symbol }: { symbol: string }) { // Keep the same source across renders: a new one reloads the data. const dataSource = useMemo(() => createMockDataSource(), []); return ( ); } ``` | Prop | Applied with | Notes | | --- | --- | --- | | `options` | `createChart` at mount, then `chart.applyOptions` | Compared by value, so an inline object does not apply again on every render. Functions inside it, such as formatters, are compared by identity: create them once (module scope, `useMemo`) or every render applies the options again. Leaving out a key does not reset it; pass the value you want. | | `dataSource` | `chart.setDataSource` | Changing it attaches the new source and loads `symbol` again. Keep it stable (`useMemo`, module scope). Removing the prop keeps the current source; pass `null` to detach it. The chart needs the `dataSource()` plugin in `options.plugins` (or `builtins()`); without it the source is not attached and development builds warn once ([`data-source-missing`](./errors#data-source-missing)). | | `symbol`, `interval` | `chart.setSymbol` | Loads once both are set and a `dataSource` is given; loads again when either changes. A failed load emits `data:error`. | | `on` | `chart.on` | Event callbacks, see [Events](#events). | | `id`, `className`, `style` | | Attributes of the container `div`. | | `children` | | Rendered inside the container. | A few options are fixed when the chart is created, for example `gl`. Changing them in `options` later has no effect; remount the component on purpose with a new `key` instead. One of them is `plugins`, which gives the chart [opt-in features](./features#opt-in-features) such as annotations, drawings and heatmaps: put `plugins: chartPlugins` in `options`, with `const chartPlugins = builtins()` at module scope. Create the list once, since the functions in it are compared by identity. ## Reaching the chart The component gives the app the `Chart` it created, with the whole imperative API. It exists only while the component is mounted: it is null during server rendering and before the first mount. With a ref: ```tsx import { useRef } from 'react'; import type { Chart as ChartInstance } from '@chart-on/core'; import { Chart } from '@chart-on/react'; export function ChartWithButton() { const chart = useRef(null); return ( <> ); } ``` With `useChart()` in a child. The child renders once with null, then again with the chart once it exists, so effects that depend on it run at the right time: ```tsx import { useEffect } from 'react'; import type { Bar } from '@chart-on/core'; import { Chart, useChart } from '@chart-on/react'; function Candles({ bars }: { bars: Bar[] }) { const chart = useChart(); useEffect(() => { if (!chart) return; const series = chart.addCandles(); series.setData(bars); return () => chart.removeSeries(series); }, [chart, bars]); return null; } export function OwnData({ bars }: { bars: Bar[] }) { return ( ); } ``` For live ticks, keep the series handle and call `series.update(bar)`: data never travels through a remount. ## Events The `on` prop takes one callback per event, keyed by the chart's own event names. Only the events listed are subscribed, so leave out the busy ones (`crosshair:move`, `frame:render`) unless you use them. The payload is passed on unchanged. ```tsx setHover(e?.time ?? null), 'data:error': (e) => console.warn(e.symbol, e.error), }} /> ``` A new callback for the same event is used straight away without subscribing again, so inline functions are fine. Removing a key unsubscribes it, and unmounting unsubscribes everything. ## The widget `` mounts the full shell. Import its stylesheet once in the app. ```tsx import { ChartShell } from '@chart-on/react/shell'; import '@chart-on/ui/style.css'; export function Terminal({ dark }: { dark: boolean }) { return ( console.log(e.resolved) }} /> ); } ``` It takes the same `options`, `dataSource`, `symbol`, `interval`, `on` and container props as ``, plus: | Prop | Applied with | Notes | | --- | --- | --- | | `theme` | `shell.setTheme` | `dark`, `light` or `system`. | | `palette` | `shell.setPalette` | | | `features` | `shell.applyFeatures` | Compared by value. | | `shellOptions` | `createChartShell` at mount | Toolbar, rail, dialogs and the rest of the set-up. Read once; remount with a new `key` to change them. | | `shellOn` | `shell.on` | Shell event callbacks, like `on`. | The ref receives the `ChartShell`; its chart is `shell.chart`. Inside the shell, `useChart()` returns that chart and `useChartShell()` the shell. ## Server rendering Both entries import on a server without touching a browser global, and both components render on a server as their empty container `div`. The chart is created only in the browser, after the page has loaded and the component has mounted. Nothing is needed to opt in. ### Frameworks with server components The built files start with the `'use client'` marker, so frameworks that split server and client components treat `` and `` as client components: they render to HTML on the server and hydrate in the browser. You can use them from a server component, but props crossing that boundary must be serializable. A data source, a ref or an event callback is not, so put those in a small component of your own marked as a client component: ```tsx 'use client'; import { useMemo } from 'react'; import { createMockDataSource } from '@chart-on/core'; import { Chart } from '@chart-on/react'; export function LiveChart({ symbol }: { symbol: string }) { const dataSource = useMemo(() => createMockDataSource(), []); return ; } ``` A server component can then render `` and pass it plain values. ### Frameworks that render every page on the server In frameworks without server components, where each page renders on the server and then hydrates, use the components like any other. The server outputs the container; the chart appears once the page hydrates. To skip server rendering for a page section altogether, use the framework's own client-only or lazy-loading helper; the wrapper does not need it. ## Example apps Two small apps in the repository show the components in use: - [`examples/react-bundler`](https://github.com/chart-on/chart-on/tree/main/examples/react-bundler): the widget in a client-rendered app, with a theme switch and a crosshair readout through `on`. - [`examples/react-ssr`](https://github.com/chart-on/chart-on/tree/main/examples/react-ssr): `` rendered to HTML on the server and hydrated in the browser, served by a small Node server. CI builds both and renders the server example in Node with `node scripts/check-examples.mjs`. ## Development double mounting Development modes that mount, unmount and mount again leave one chart and one WebGL context: the first chart is destroyed before the second is created. Tests check this, along with 100 mount and unmount cycles that leave no context or animation frame behind. --- Source: /guide/template-wrapper.md # Template component wrapper `@chart-on/vue` wraps the engine and the shell as components and composables for apps built with a template-based component framework (version 3.5 or later), including apps that render every page on the server first. It follows the [wrapper contract](./frameworks#the-wrapper-contract): the chart is created when the component mounts and destroyed when it unmounts, and changing a prop updates the chart in place instead of creating a new one. | Export | From | What it is | | --- | --- | --- | | `` | `@chart-on/vue` | The engine alone, in a `div`. | | `useChart()` | `@chart-on/vue` | A ref holding the `Chart` of the nearest `` or ``. | | `` | `@chart-on/vue/shell` | The widget: toolbar, drawing rail, bottom bar and the chart. | | `useChartShell()` | `@chart-on/vue/shell` | A ref holding the `ChartShell` of the nearest ``. | ## Install ```bash pnpm add @chart-on/vue @chart-on/core # Only for : pnpm add @chart-on/ui ``` The framework (version 3.5 or later), `@chart-on/core` and `@chart-on/ui` are peer dependencies: the app installs them once and the wrapper uses the app's copies. `@chart-on/ui` is optional, because `` lives in its own entry, `@chart-on/vue/shell`. An app that only imports `@chart-on/vue` never loads it. The components are plain component objects, so they need no compiler plugin. Import them where you use them, or register them globally with `app.component('Chart', Chart)`. ## The engine Give the component a height; the chart fills its container. `class`, `style` and other attributes go to the container `div`. ```vue ``` | Prop | Applied with | Notes | | --- | --- | --- | | `options` | `createChart` at mount, then `chart.applyOptions` | Watched deeply. Plain data in it is compared by value, so an object literal in the template does not apply again on every render, and a change inside a reactive object applies. Functions, dates and class instances inside it are compared by identity: a formatter written inline in the template is new on every render and applies the options again each time, so define callbacks once in the script. The chart gets a plain copy, never a reactive proxy. Leaving out a key does not reset it; pass the value you want. | | `dataSource` | `chart.setDataSource` | Changing it attaches the new source and loads `symbol` again. Keep it in a plain variable, a `shallowRef` or `markRaw`: the wrapper unwraps a reactive one, but a deeply reactive source makes its own work slower. Removing the prop keeps the current source; pass `null` to detach it. The chart needs the `dataSource()` plugin in `options.plugins` (or `builtins()`); without it the source is not attached and development builds warn once ([`data-source-missing`](./errors#data-source-missing)). | | `symbol`, `interval` | `chart.setSymbol` | Loads once both are set and a `dataSource` is given; loads again when either changes. A failed load emits `data:error`. | | `on` | `chart.on` | Event callbacks, see [Events](#events). | A prop left out, or set to `undefined` after having a value, leaves the chart as it is: the wrapper stops controlling it but does not reset it. To detach the data source, pass `null`. A few options are fixed when the chart is created, for example `gl`. Changing them in `options` later has no effect; remount the component on purpose with a new `key` instead. One of them is `plugins`, which gives the chart [opt-in features](./features#opt-in-features) such as annotations, drawings and heatmaps: put `plugins: chartPlugins` in `options`, with `const chartPlugins = builtins()` at module scope. Create the list once, since the functions in it are compared by identity. ## Reaching the chart The component gives the app the `Chart` it created, with the whole imperative API. It exists only while the component is mounted: it is null during server rendering and before the first mount. The wrapper keeps it out of deep reactivity (`shallowRef` and `markRaw`), so calling into it never goes through a proxy. Do the same if you store it yourself: `shallowRef`, not `ref` or `reactive`. With a template ref, which reaches the component's exposed `chart`: ```vue ``` With `useChart()` in a component inside the default slot. It returns a ref that holds null until the chart exists, so a `watch` on it runs at the right time, and its clean-up runs before the chart is destroyed: ```vue ``` ```vue ``` For live ticks, keep the series handle and call `series.update(bar)`: data never travels through a remount. ## Events The `on` prop takes one callback per event, keyed by the chart's own event names. Only the events listed are subscribed, so leave out the busy ones (`crosshair:move`, `frame:render`) unless you use them. The payload is passed on unchanged. ```vue ``` A new callback for the same event is used straight away without subscribing again, so inline functions are fine. Removing a key unsubscribes it, and unmounting unsubscribes everything. Events come through a prop rather than `@event` listeners because the chart and the shell share some event names (`theme:change`): `on` and `shellOn` say which one you mean. ## The widget `` mounts the full shell. Import its stylesheet once in the app. ```vue ``` It takes the same `options`, `dataSource`, `symbol`, `interval` and `on` props and container attributes as ``, plus: | Prop | Applied with | Notes | | --- | --- | --- | | `theme` | `shell.setTheme` | `dark`, `light` or `system`. | | `palette` | `shell.setPalette` | | | `features` | `shell.applyFeatures` | Compared by value. | | `shellOptions` | `createChartShell` at mount | Toolbar, rail, dialogs and the rest of the set-up. Read once; remount with a new `key` to change them. | | `shellOn` | `shell.on` | Shell event callbacks, like `on`. | As with ``, setting `theme`, `palette` or `features` to `undefined` leaves the shell as it is, including a theme the user picked in the shell. A template ref reaches `shell` and its `chart` (type `ChartShellExposed`). Inside the shell, `useChart()` holds that chart and `useChartShell()` the shell. ## Kept-alive components Inside ``, a hidden component is not unmounted: the chart, its data and its view stay. While hidden the chart has no size, so it draws nothing; when shown again it measures its container and draws at the right size straight away. ## Server rendering Both entries import on a server without touching a browser global, and both components render on a server as their container `div` with the slot content inside. The chart is created in the mount hook, which runs only in the browser, after the page has hydrated. Nothing is needed to opt in: meta-frameworks that render every page on the server and then hydrate it can use the components like any other, and `useChart()` holds null until the browser mounts the chart. To skip server rendering for a page section altogether, for example a section that only makes sense with a chart, wrap it in the framework's client-only component (often ``). The wrapper does not need it. ## Tests Tests mount each component, change every prop and check that the same instance received the matching method call, check that events the first load emits while mounting reach the callbacks, hide and show a kept-alive chart, unmount it and check that no WebGL context or animation frame is left (also over 100 mount and unmount cycles), render both components on a server, and hydrate server HTML without mismatches. --- Source: /guide/compiled-wrapper.md # Compiled component wrapper `@chart-on/svelte` wraps the engine and the shell as components, an action and context helpers for apps built with a compiler-based component framework (version 5.18 or later, with runes), including apps whose app framework renders every page on the server first. It follows the [wrapper contract](./frameworks#the-wrapper-contract): the chart is created when the component mounts and destroyed when it unmounts, and changing a prop updates the chart in place instead of creating a new one. | Export | From | What it is | | --- | --- | --- | | `` | `@chart-on/svelte` | The engine alone, in a `div`. | | `use:chart` | `@chart-on/svelte` | The engine in an element of your own, for minimal set-ups. | | `useChart()` | `@chart-on/svelte` | The `Chart` of the nearest `` or ``, as `.current`. | | `` | `@chart-on/svelte/shell` | The widget: toolbar, drawing rail, bottom bar and the chart. | | `useChartShell()` | `@chart-on/svelte/shell` | The `ChartShell` of the nearest ``, as `.current`. | ## Install ```bash pnpm add @chart-on/svelte @chart-on/core # Only for : pnpm add @chart-on/ui ``` The framework (version 5.18 or later), `@chart-on/core` and `@chart-on/ui` are peer dependencies: the app installs them once and the wrapper uses the app's copies. `@chart-on/ui` is optional, because `` lives in its own entry, `@chart-on/svelte/shell`. An app that only imports `@chart-on/svelte` never loads it. The package ships its components as source, with the types already taken out, as the framework's packages usually do: the app's own compiler builds them, for the browser and for the server, with the same version and settings as the rest of the app. The framework's bundler plugin picks them up through the `svelte` export condition; nothing needs configuring. ## The engine Give the component a height; the chart fills its container. `class`, `style` and other attributes go to the container `div`. ```svelte ``` | Prop | Applied with | Notes | | --- | --- | --- | | `options` | `createChart` at mount, then `chart.applyOptions` | Plain data in it is compared by value, so an object literal in the markup does not apply again when it is rebuilt, and a change inside a `$state` object applies. Functions, dates and class instances inside it are compared by identity: a formatter written inline in the markup is new each time and applies the options again, so define callbacks once in the script. The chart gets a plain copy, never a state proxy. Leaving out a key does not reset it; pass the value you want. | | `dataSource` | `chart.setDataSource` | Changing it attaches the new source and loads `symbol` again. Keep it in a `const` or in `$state.raw`. Deep `$state` wraps a source made of plain objects in a proxy, and the wrapper cannot tell a proxy from the object, so the chart gets the proxy and calls through it: it still works, but every call the chart makes on it goes through the state system. Removing the prop keeps the current source, and a new `symbol` or `interval` still loads from it; pass `null` to detach it. The chart needs the `dataSource()` plugin in `options.plugins` (or `builtins()`); without it the source is not attached and development builds warn once ([`data-source-missing`](./errors#data-source-missing)). | | `symbol`, `interval` | `chart.setSymbol` | Loads once both are set and a `dataSource` is given; loads again when either changes. A failed load emits `data:error`. | | `on` | `chart.on` | Event callbacks, see [Events](#events). | A prop left out, or set to `undefined` after having a value, leaves the chart as it is: the wrapper stops controlling it but does not reset it, and keeps using its last value when another prop changes. Setting it back to that same value applies nothing. To detach the data source, pass `null`. A few options are fixed when the chart is created, for example `gl`. Changing them in `options` later has no effect; remount the component on purpose instead, for example inside a `{#key}` block. ## Reaching the chart The component gives the app the `Chart` it created, with the whole imperative API. It exists only while the component is mounted: it is null during server rendering and before the first mount. The chart is a class instance, so `$state` keeps it as it is, without a proxy; `$state.raw` makes that explicit. With `bind:chart`: ```svelte ``` The component sets the binding; the app only reads it. With `useChart()` in a component rendered inside ``. Its `current` is null until the chart exists, so an `$effect` that reads it runs again at the right time, and the effect's clean-up runs before the chart is destroyed: ```svelte ``` ```svelte ``` Call `useChart()` while the component is being set up, as with `getContext`. Outside a chart component its `current` is always null. For live ticks, keep the series handle and call `series.update(bar)`: data never travels through a remount. ## Events The `on` prop takes one callback per event, keyed by the chart's own event names. Only the events listed are subscribed, so leave out the busy ones (`crosshair:move`, `frame:render`) unless you use them. The payload is passed on unchanged. ```svelte (hover = e?.time ?? null), 'data:error': (e) => console.warn(e.symbol, e.error), }} /> ``` A new callback for the same event is used straight away without subscribing again, so inline functions are fine. Removing a key unsubscribes it, and unmounting unsubscribes everything. The events the first load emits while the component mounts, such as `symbol:change`, already reach the callbacks. Events come through one prop rather than one `on…` prop per event because the chart and the shell share some event names (`theme:change`): `on` and `shellOn` say which one you mean. ## The action For a page that needs only the engine and no children, `use:chart` mounts it in an element you already have. It takes the same `options`, `dataSource`, `symbol`, `interval` and `on` as ``, compared the same way, plus `ref`, a callback that receives the chart after it is created and null after it is destroyed. When the parameters bring a different `ref`, the old one receives null and the new one the chart, so an inline arrow function is called again on every update; define it once in the script to avoid that. ```svelte
``` Actions run only in the browser, so the action is as safe in server-rendered pages as the components. There is no `useChart()` inside the element: use `ref`. ## The widget `` mounts the full shell. Import its stylesheet once in the app. ```svelte console.log(e.resolved) }} /> ``` It takes the same `options`, `dataSource`, `symbol`, `interval` and `on` props and container attributes as ``, plus: | Prop | Applied with | Notes | | --- | --- | --- | | `theme` | `shell.setTheme` | `dark`, `light` or `system`. | | `palette` | `shell.setPalette` | | | `features` | `shell.applyFeatures` | Compared by value. | | `shellOptions` | `createChartShell` at mount | Toolbar, rail, dialogs and the rest of the set-up. Read once; remount, for example inside a `{#key}` block, to change them. | | `shellOn` | `shell.on` | Shell event callbacks, like `on`. | As with ``, setting `theme`, `palette` or `features` to `undefined` leaves the shell as it is, including a theme the user picked in the shell. Setting the prop again applies it again. `bind:shell` reaches the shell and `bind:chart` its chart. Inside the shell, `useChart()` holds that chart and `useChartShell()` the shell. ## Server rendering Both entries import on a server without touching a browser global, and both components render on a server as their container `div` with their children inside. The chart is created in `onMount`, which runs only in the browser, after the page has hydrated. Nothing is needed to opt in: an app framework that renders every page on the server and then hydrates it can use the components like any other, and `useChart()` holds null until the browser mounts the chart. To skip server rendering for a page altogether, for example a page that only makes sense with a chart, turn it off for that route in the app framework. The wrapper does not need it. ## Example apps Two small apps in the repository show the components in use: - [`examples/svelte-bundler`](https://github.com/chart-on/chart-on/tree/main/examples/svelte-bundler): the widget in a client-rendered app, with a theme switch, `fitContent()` through `bind:shell` and a crosshair readout through `on`. - [`examples/svelte-ssr`](https://github.com/chart-on/chart-on/tree/main/examples/svelte-ssr): `` rendered to HTML on the server and hydrated in the browser, served by a small Node server. CI builds both and renders the server example in Node with `node scripts/check-examples.mjs`. ## Tests Tests mount each component and the action, change every prop and check that the same instance received the matching method call, check that events the first load emits while mounting reach the callbacks, check that children's effects clean up before the chart is destroyed, unmount and check that no WebGL context or animation frame is left (also over 100 mount and unmount cycles), render both components on a server, and import both entries on a server without touching a browser global. --- Source: /guide/series.md # Series and data Built-in series types plus [custom series](./custom-series), all created from the chart. Each `add*` call takes options and an optional [pane](./panes). | Method | Data | Typical use | | --- | --- | --- | | `addCandles` | `Bar[]` | Price, in [14 chart types](#chart-types) | | `addLine` | `ValuePoint[]` | Moving averages, cumulative delta, any line | | `addHistogram` | `ValuePoint[]` | Volume, liquidations, oscillators | | `addHeatmap` | `HeatmapData` | Order-book or liquidation heatmaps | | `addPoints` | `PointItem[]` | Large trades, liquidations, sampled ticks as dots or bubbles; see [Points](#points) | | `addVolumeProfile` | main series bars | Volume by price, see [Volume profile](./volume-profile) | | `addCustomSeries` | your own `{ time, ... }` items | Chart types you define; see [Custom series](./custom-series) | ```ts interface Bar { time: number; open: number; high: number; low: number; close: number; volume?: number; buyVolume?: number; color?: string; wickColor?: string; borderColor?: string; // this bar's colours, see below } interface ValuePoint { time: number; value: number; color?: string; areaColor?: string } ``` Every series supports: ```ts series.setData(data); // replace all data; times must be ascending series.setData(data, { normalize: true }); // sort, keep the last of equal times, drop invalid candles series.setData(data, { keepVisibleRange: true }); // main series: keep the times in view (see View and scale) series.update(point); // replace last (same time) or append (newer); older is dropped series.applyOptions({ ... }); // change options later series.setVisible(false); // hide without removing chart.removeSeries(series); // also frees its GPU buffers and textures series.zIndex = 1; // draw above the pane's other series (default 0) chart.moveSeriesToFront(series); // or chart.sendToBack(series) ``` Within a pane, z-order applies among series drawn by the same renderer: a higher `zIndex` draws on top. GPU series (candles, histograms, heatmaps, and lines, areas and line-drawn candle styles on the default [GPU renderer](#gpu-lines)) always draw below 2D series (curved lines, lines with per-point colours, custom series, and anything with `renderer: 'canvas'`), so a canvas line stays above candles whatever their `zIndex`; candles in the `columns` style always draw in 2D. Equal values keep the usual order: heatmaps at the bottom, then overlays and volume profiles, then the rest, each in the order added. `moveSeriesToFront` and `sendToBack` set `zIndex` past the pane's other series. Hovering picks the topmost series in the same order. Changing the order only redraws; nothing is uploaded to the GPU again. What `normalize` and an older `update` leave out is reported through the `data:dropped` event. A time that is not a finite number always throws, and a non-finite line or histogram value is a gap. See [bad values](./concepts#bad-values) for the full rules. Candle, line and histogram series also accept `overlay` (see [Overlay scale](#overlay-scale)) and `priceScale`, the id of the price scale they use (default `'right'`; see [several price scales](./view-and-scale#several-price-scales)). ## Candles ```ts const candles = chart.addCandles({ title: 'BTC', style: 'candles', // see chart types below upColor: '#3cc4a0', // defaults to theme.up downColor: '#e8574a', precision: 2, // auto-detected from data when omitted }); candles.setData(bars); candles.update(lastBar); ``` | Option | Default | Notes | | --- | --- | --- | | `title` | `''` | Legend label | | `legend` | `true` | `false` leaves the series out of the legend (every series type takes it, as do volume profiles and `indicators.add(def, inputs, { legend: false })`) | | `style` | `'candles'` | See [chart types](#chart-types) | | `upColor`, `downColor` | theme | | | `upWickColor`, `downWickColor` | body colours | Wick colours of the candle styles | | `upBorderColor`, `downBorderColor` | none | Body outline colours of the candle styles | | `wicks` | `true` | Draw wicks; `false` leaves candle bodies only | | `borders` | `true` | Draw the outlines the border colours set; hollow candles keep theirs | | `colorByPreviousClose` | `false` | Colour bars by change from the previous close instead of from the open | | `hollowDown` | `false` | Draw falling bodies as outlines (`candles`, `heikinAshi`, `volumeCandles`), so up and down differ in shape too | | `precision` | auto | Fixed price decimals | | `priceFormat` | none | Decimals, tick size, fractions and unit; wins over `precision`. See [Price formats](#price-formats) | | `bodyRatio` | `0.7` | Candle body width relative to bar spacing | | `thinBars` | `true` | One-pixel `bars` / `hlcBars` at any zoom; `false` thickens them as bars widen | | `lineWidth` | `2` | For line-based styles | | `lineStyle` | `'solid'` | `'solid'`, `'dashed'` or `'dotted'`, for `line`, `lineMarkers`, `step`, `area`, `baseline` and the close line of `rangeBand` | | `renderer` | `'gpu'` | `'gpu'` draws the lines and fills of the line-drawn styles with WebGL2; `'canvas'` draws them in 2D. See [GPU lines](#gpu-lines) | | `lastValueLabel` | `true` | Price label on the axis | | `priceLine` | main series only, unless its `lastValueLabel` is `false` | Price line across the plot, see [Price line](#price-line) | | `offscreenLastPrice` | unset | `'hollow'` or `'hidden'`: the axis label while the newest bar is panned off screen | | `autoscale` | `true` | `false` leaves the series out of its price scale's fit; a function adjusts the range it adds. See [Per-series autoscale](./view-and-scale#per-series-autoscale) | | `axisTitle` | `false` | Tag with the title (or the symbol name) beside the axis label | | `animate` | `true` | Tween live updates of the forming bar | | `overlay` | none | Own price scale, see [Overlay scale](#overlay-scale) | | `baseLevel` | middle of the pane | Where the `baseline` style splits its fills, see [Baseline base level](#baseline-base-level) | | `lastPointPulse` | `'off'` | A pulse on the newest close of the `line`, `area`, `step` and `baseline` styles (with the `lastPointPulse()` plugin), see [Last-point pulse](#last-point-pulse) | ### Price line The dotted line across the plot at the newest price is the series' price line. It is separate from the axis label, so either can be on without the other, and every series type (candles, lines, histograms, custom series) can have one. It is on by default only for the main candle series. ```ts candles.applyOptions({ priceLine: { color: '#f0a73a', lineStyle: 'dashed', lineWidth: 1 } }); line.applyOptions({ priceLine: true }); // default style: dotted, 1 px, label colour candles.applyOptions({ priceLine: false, lastValueLabel: true }); // label only candles.applyOptions({ priceLine: { source: 'lastVisible' } }); // follow the view while panning ``` | Field | Default | Notes | | --- | --- | --- | | `color` | label colour | | | `lineWidth` | `1` | | | `lineStyle` | `'dotted'` | `'solid'`, `'dashed'` or `'dotted'` | | `source` | `'last'` | `'last'`: the newest value. `'lastVisible'`: the newest value on screen | `features.lastValueLabels` turns price lines and labels off together. While the newest bar is panned off screen, `offscreenLastPrice: 'hollow'` draws the axis label as an outline and `'hidden'` leaves it out. ### Per-bar colours A bar's `color`, `wickColor` and `borderColor` override the series colours for that bar alone, e.g. to highlight signal bars or volume spikes from your own logic: ```ts candles.setData(bars.map((b) => (isSpike(b) ? { ...b, color: '#f59e0b', wickColor: '#f59e0b' } : b))); ``` `color` is the body (and the lines of `bars` and `hlcBars`, and the `highLow` bar). `wickColor` is the wick and `borderColor` the body outline, in the candle styles that have them (`candles`, `hollow`, `heikinAshi`, `volumeCandles`; not `bars`, `hlcBars` or `highLow`). They only recolour: with `wicks: false` or `borders: false` the wick or outline stays hidden. The colours stay with the live bar while it eases, and the last-value label and price line take the last bar's `color`. An `update` replaces the bar, so send the colours again with it to keep them. The line, area, baseline and column styles ignore them, a colour the browser cannot parse falls back to the series colour, and `getDataTable` leaves them out. An indicator's `barColor` plot recolours the main series' bars the same way, body, wick and outline, over these colours (see [plot types](./indicators.md)). ### Chart types Switch any time with `candles.applyOptions({ style })`. | Style | Draws | | --- | --- | | `candles` | Filled candles | | `hollow` | Hollow candles for up bars | | `volumeCandles` | Candles whose body width follows volume, see [Volume-width candles](#volume-width-candles) | | `bars` | OHLC bars | | `heikinAshi` | Heikin-Ashi candles, computed from your bars | | `hlcBars` | Bars with high, low and a close tick (no open) | | `rangeBand` | Close line with a band to the high and the low: the part above the close tinted with the up colour, below with the down colour | | `highLow` | High-low range bars | | `baseline` | Close colored above/below a base level (the middle of the pane by default) | | `columns` | Close as columns, colored up/down | | `area` | Close as a line with gradient fill | | `line` | Close as a line | | `lineMarkers` | Line with a dot per bar | | `step` | Close as a step line | | `kagi` | Kagi line for bars from `PriceBarBuilder` with `type: 'kagi'`, see [Price-based bars](#price-based-bars). Needs the `priceStyles()` plugin | | `pointFigure` | Columns of X and O boxes for bars from `PriceBarBuilder` with `type: 'pointFigure'`, see [Price-based bars](#price-based-bars). Needs the `priceStyles()` plugin | The table lists the styles in the order of the shell's style menu (`chartStyles` in `@chart-on/ui`): grouped by what they draw, all four prices, the range or the close, and within a group the style that shows the most values of a bar first. `rangeBand` was called `hlcArea` before; a saved layout with that name draws `candles` (see [upgrading](../upgrading/)). `heikinAshi(bars)` and `heikinAshiBar(bar, prevHa)` are exported if you need the values yourself. Switching style eases the price scale to the new range (with the chart's `smoothScale` option, on by default), so moving between close-only styles (`line`, `area`, ...) and high-low styles (`candles`, `rangeBand`, ...) animates. `rangeBand` autoscales on high and low like candles. `hollow` always colours by change from the previous close; `colorByPreviousClose` applies the same rule to the other bar and candle styles. Wick and border colours apply to `candles`, `hollow`, `heikinAshi` and `volumeCandles`. The first candle series is the main series and defines the time axis. See [Core concepts](./concepts#the-main-series-defines-the-time-axis). #### Baseline base level The `baseline` style colours and fills the close line with the up colour above a base level and the down colour below it. `baseLevel` sets that level: ```ts candles.applyOptions({ style: 'baseline', baseLevel: { kind: 'price', price: 64000 } }); ``` | `baseLevel` | Base | | --- | --- | | `{ kind: 'height', percent: 50 }` | A height in the pane, in percent from the bottom. The default; the price under it changes as you scroll and zoom | | `{ kind: 'price', price }` | A fixed price. Autoscale keeps it in view. On a log scale a price of zero or below falls back to the middle of the pane | | `{ kind: 'firstVisible' }` | The close of the first visible bar. When this series is the base of a percent scale, this is the 0% line | The fills split exactly at the base price on normal, log and percent scales. `candles.basePrice` is the price the last frame used (null for other styles). While the pointer is over the series, the base price is labelled on the price axis, also with `lastValueLabel: false` (that option hides only the last price label). Drag that level on the price axis to move it: a height level stays a height, a fixed or first-visible level becomes a fixed price (rounded to the tick size). Dragging is an axis drag, so it is off with the `axisDragScale` feature. Each move emits `baseline:change` with `{ series, baseLevel, price }`; store `baseLevel` to restore it later. #### Volume-width candles The body width is the normal body width (`bodyRatio` × bar spacing) scaled by the bar's `volume` relative to the largest volume currently on screen, so the widest body marks the busiest visible bar. Widths are re-measured as you pan and zoom. Bodies never get narrower than a quarter of a normal body, or than three wick widths when there is room, so low-volume bars stay readable. Bars without `volume` keep the normal width. ### Price-based bars Renko, line break, range, kagi and point & figure bars are made from price movement, not from the passage of time, so they have a timeline of their own. `PriceBarBuilder` turns your time bars into them, and an ordinary candle series draws the result: ```ts import { PriceBarBuilder } from '@chart-on/core'; const builder = new PriceBarBuilder({ type: 'renko', size: { kind: 'fixed', size: 5 } }); const candles = chart.addCandles({ title: 'Renko' }); candles.setData(builder.setData(bars)); // Live: send every new or changed source bar, then apply what changed. const change = builder.update(bar); if (change.reset) candles.setData(builder.bars, { keepVisibleRange: true }); else for (const b of change.bars) candles.update(b); ``` | `type` | Draws | | --- | --- | | `renko` | A brick each time price moves `size` past the last brick. A reversal needs `reversal` bricks (default 2) counted from the last brick's close, and starts at the last brick's open. Bricks have no wicks. | | `lineBreak` | A line from the last line's close to each close beyond it. A close beyond the extreme of the last `lines` lines (default 3) the other way draws a reversal line from the last line's open. The first line runs from the first close to the first close that differs. | | `range` | Bars whose high minus low stays within `size`. When price goes past it, the bar closes at its low plus `size` (or its high minus `size`) and the next bar opens there. The newest bar is still forming. | | `kagi` | One bar per vertical stretch of a kagi line, from `open` (where the line turned) to `close` (how far it ran). The line runs on while price keeps its direction, and turns when price moves back `size` from its end. The first stretch starts once price has moved `size` from the first price. Draw it with `style: 'kagi'`. | | `pointFigure` | One bar per column of boxes `size` high, on a grid of multiples of `size`: X boxes from `open` up to `close` while price rises, O boxes from `open` down to `close` while it falls. A move of `reversal` boxes (default 3) back from the last box starts a new column one box beyond it. Prices are box middles. Draw it with `style: 'pointFigure'`. | | Option | Default | Meaning | | --- | --- | --- | | `size` | `{ kind: 'atr', length: 14 }` | Brick size, bar range, kagi reversal amount or point & figure box size: `{ kind: 'fixed', size }`, `{ kind: 'atr', length }` (average true range of the loaded bars) or `{ kind: 'percent', percent }` (of the newest loaded close). ATR and percent sizes are worked out by `setData` and kept for live updates; `builder.size` reads the result. A size that comes out zero (flat or zero prices) is worked out again from each new source bar, and no bars form until it is positive. | | `reversal` | `2` (`renko`), `3` (`pointFigure`) | `renko`: bricks against the trend that draw a reversal brick (at least 2). `pointFigure`: boxes back from the last box that start a new column (at least 1). | | `source` | `'close'` (`renko`, `kagi`, `pointFigure`), `'highLow'` (`range`) | `'close'` follows each close. `'highLow'` follows the high and low, reading a rising bar as touching its low first and a falling bar its high first; range bars also follow the open and close. `lineBreak` always uses closes. | | `lines` | `3` | `lineBreak`: how many lines a reversal must break | | `slotSeconds` | the source interval | Spacing of the bars' `time` (see below) | Each `PriceBar` is a `Bar` with `startTime` and `endTime`: the source bars it covers, from where the previous one ended. Volume of the source bars goes to the bar they complete (renko) or the newest bar (line break, range, kagi, point & figure; source bars before the first kagi stretch or point & figure column count towards it). One source bar can make several price bars, so their `time` is only a slot: the first source time plus `slotSeconds` per bar. When the main series' bars carry `startTime` and `endTime`, the chart shows that span instead of the slot: the crosshair time label and `valuesAt().timeText` (the data window and pointer tooltip) read `start – end` through `formatTime`, and `valuesAt().source` gives it as `{ time, endTime }`. Label the time axis with `builder.barAt(time)`: ```ts const chart = createChart(el, { formatTimeTick: (time) => fmt(builder.barAt(time)?.endTime ?? time), }); ``` Kagi and point & figure bars are not candles, so the candle series has a style for each, drawn on the GPU by its own renderer. Both come with the `priceStyles()` [plugin](./features#opt-in-features), which `builtins()` includes. Without it a series in either style draws nothing, and development builds warn once ([`price-styles-missing`](./errors#price-styles-missing)): ```ts import { PriceBarBuilder, createChart, priceStyles } from '@chart-on/core'; const chart = createChart(el, { plugins: [priceStyles()] }); const builder = new PriceBarBuilder({ type: 'pointFigure', size: { kind: 'percent', percent: 1 }, reversal: 3 }); const columns = chart.addCandles({ style: 'pointFigure' }); columns.setData(builder.setData(source)); ``` - `kagi` draws one line through the bars: each bar a vertical stretch from its open to its close, joined to the next by a horizontal step. The line is thick (yang, in the up colour) from where it rises above the last shoulder and thin (yin, in the down colour) from where it falls below the last waist; it starts thick when the first stretch rises. Every turn makes a shoulder (the top of a rising stretch) or a waist (the bottom of a falling one), whatever the thickness, and the most recent one is the level to break: a thin rally that fails below the old shoulder sets a lower shoulder, so the next rally above it turns the line thick. The thick line is `lineWidth` × 1.5 wide and the thin one `lineWidth` × 0.5 (at least 1 px). - `pointFigure` draws an X for every box of a rising column and an O for every box of a falling one, in the up and down colours (a bar's own `color` wins), each box `bodyRatio` of the bar spacing wide. A one-box column (open equal to close) turns from the column before it. The box height is the `boxSize` each bar from `PriceBarBuilder` carries; the `boxSize` option overrides it, and for bars made elsewhere, without the option, the series reads it from the bars as the step from one column's last box to the next column's first. Autoscale keeps whole boxes in view. Only the boxes in view are drawn, so a very small box size costs no more than the screen holds, and a column whose boxes or width are under 3 px is drawn as one filled span. Both styles draw every bar even when the view groups bars (`grouping`). The legend, crosshair, snapping and price labels read the bars' open, high, low and close as for candles. A live update can take bars back: a forming source bar that retreats removes the bricks it had drawn. `update` reports that as `reset`. One source bar adds at most 10,000 price bars, so a size far too small for the data cannot hang the page. `priceBars(bars, options)` builds the bars in one go when you do not need live updates. Indicators and other series on the same chart line up with the price bars' slots, not with the source times, so compute anything you overlay from the price bars. ## Line ```ts const cvd = chart.addLine({ title: 'CVD', area: true, baseline: 0 }); cvd.setData(points); ``` | Option | Default | Notes | | --- | --- | --- | | `title` | `''` | | | `color` | `theme.line` | | | `lineWidth` | `1.5` | | | `area` | `false` | Gradient fill under the line | | `colorMode` | `'segment'` | How points' own colours join: `'segment'` or `'gradient'`. See [Per-point colours](#per-point-colours) | | `baseline` | none | Switch colors above/below this value | | `aboveColor`, `belowColor` | `theme.line` | Line colors above/below `baseline` | | `format` | pane format | `(value) => string` for legend and axis label. By default a series on the pane's scale uses that scale's [price format](#price-formats); overlays and panes without one use compact numbers | | `breakGaps` | `false` | Break the line over bars where the series has no point, instead of joining across them | | `lineStyle` | `'solid'` | `'solid'`, `'dashed'` or `'dotted'`; dashes grow with `lineWidth` | | `lineType` | `'simple'` | `'simple'` (straight segments), `'step'` (level until the next point, then a vertical jump) or `'curved'` (a smooth curve that never overshoots a point) | | `renderer` | `'gpu'` | `'gpu'` strokes the line with WebGL2; `'canvas'` strokes it in 2D. See [GPU lines](#gpu-lines) | | `pointMarkers` | `false` | `true` or `{ radius }`: a dot on every point, shown while bars are wide enough for the dots not to merge | | `crosshairMarker` | `false` | `true` or `{ radius, color }`: a dot where the crosshair meets the line, in the line colour by default | | `lastPointPulse` | `'off'` | `'on-update'`, `'continuous'` or `{ mode, color, radius, durationMs }`: a pulse on the last point (with the `lastPointPulse()` plugin), see [Last-point pulse](#last-point-pulse) | | `priceFormat` | none | Wins over `format`; also sets the pane's tick size and axis unit when no candle series is on it. See [Price formats](#price-formats) | | `lastValueLabel` | `true` | | | `priceLine`, `offscreenLastPrice` | off, unset | As for [candles](#price-line) | | `autoscale` | `true` | As for [candles](./view-and-scale#per-series-autoscale) | | `axisTitle` | `false` | Title tag beside the axis label | | `overlay` | none | Own price scale | Lines are decimated to first/min/max/last per pixel column, so long histories stay fast. Steps and curves are built from the decimated points, so every `lineType` keeps the extremes of each column. Points without a finite value leave a gap, as do missing bars with `breakGaps`. The same `lineStyle`, `lineType` and `breakGaps` apply in baseline mode. ```ts const signal = chart.addLine({ title: 'Signal', lineStyle: 'dashed', lineType: 'curved', crosshairMarker: true }); ``` ### Last-point pulse On a live chart the newest point is easy to lose. `lastPointPulse` draws a ring that grows out of the last point and fades. It needs the `lastPointPulse()` [plugin](./features#opt-in-features), which `builtins()` includes; without it the option is kept but draws nothing, and development builds warn once ([`pulse-missing`](./errors#pulse-missing)): ```ts import { createChart, lastPointPulse } from '@chart-on/core'; const chart = createChart(el, { plugins: [lastPointPulse()] }); const price = chart.addLine({ title: 'Price', lastPointPulse: 'on-update' }); price.update({ time, value }); // rings once candles.applyOptions({ style: 'area', lastPointPulse: { mode: 'continuous', radius: 14, durationMs: 1200 } }); ``` | Field | Default | Notes | | --- | --- | --- | | `mode` | `'on-update'` | `'on-update'` rings once each time `update` changes or adds the last point; `'continuous'` keeps ringing; `'off'` draws nothing. The string form sets only the mode | | `color` | line colour | Ring and dot colour. By default the colour of the line at the last point (above or below colour in baseline mode) | | `radius` | `10` | Size the ring grows to, in css px | | `durationMs` | `900` | Time one ring takes to grow and fade | The pulse draws on the overlay layer, so it never redraws the series themselves. It asks for frames only while a ring is running: an `'on-update'` ring stops after `durationMs`, and no mode animates while the last point is scrolled off screen, the series is hidden or the chart has no size. With `prefers-reduced-motion` set, and on static charts (`interactive: false`), the last point shows a still dot instead and no frames are requested. Candle series take the same option; it applies to the `line`, `area`, `step` and `baseline` styles. ### GPU lines By default (`renderer: 'gpu'`) the line (and its `area` fill) is drawn on the GPU, like candles: its points upload once, panning and zooming only change uniforms, and appending or updating the last point uploads only the new vertices. It pays off with long lines, many lines, or charts that redraw often: on a pane with ten 3,000-point lines the CPU time per frame drops from about 3 ms to 0.5 ms; dashed lines still measure their length on screen every frame, so they gain less. To stroke a line on the 2D canvas instead, for example so `zIndex` puts it above or below another canvas series, opt out: ```ts const line = chart.addLine({ title: 'Signal', renderer: 'canvas' }); ``` - GPU lines draw below every series drawn on the canvas, like candles, so `zIndex` orders them among GPU series only. - `area` fills and baseline mode (with or without `area`) draw on the GPU too. Curved lines, and lines with [per-point colours](#per-point-colours), always draw on the canvas. - The GPU draws every point on screen rather than decimating per pixel column, so where bars are less than a css pixel apart a GPU line has slightly less colour (about 4%) and its dashes fall elsewhere. With [grouping](./view-and-scale#group-bars-when-zoomed-out) on, zoomed-out views draw the grouped points instead, in the same pixel columns as the canvas. - Joins are round for simple lines and mitred for steps (bevelled where the line turns back on itself), run ends are flat, and dashes carry on through joins, as on the canvas. Candles take the same option, also `'gpu'` by default, for their line-drawn styles (`line`, `lineMarkers`, `step`, `area`, `baseline` and `rangeBand`): the close line, the area and baseline fills, and the range band and its edges draw on the GPU. The dots of `lineMarkers` and the dotted base level of `baseline` stay on the canvas, and the other styles ignore the option (the candle and bar styles always draw on the GPU, `columns` on the canvas). ```ts const main = chart.addCandles({ style: 'area', renderer: 'canvas' }); // opt out ``` Where many bars share a pixel column, the canvas draws a range band from the highest high to the lowest low in it; the GPU draws one bar per column there, so a zoomed-out range band can look slightly thinner. With [grouping](./view-and-scale#group-bars-when-zoomed-out) on, both draw the grouped bars. ### Per-point colours A point's `color` colours the line from that point to the next one, and `areaColor` the fill under that stretch when `area` is on. Use them to colour a line by state, such as green while a trend filter is up and red while it is down, without splitting it into several series: ```ts const trend = chart.addLine({ title: 'Trend', area: true }); trend.setData(points.map((p) => ({ ...p, color: p.up ? '#22c55e' : '#ef4444' }))); // Blend from one point's colour to the next one's instead. trend.applyOptions({ colorMode: 'gradient' }); ``` - With `colorMode: 'segment'` (the default) each segment takes the colour of the point it starts at. With `'gradient'` a segment between two different colours blends from one to the other. - Points without a `color` use the series colour; in baseline mode they keep switching between `aboveColor` and `belowColor`, while a point's own colour wins over both. - The fill under a segment uses the point's `areaColor`, fading downwards; without one it is the line colour faded, as for a plain area. A colour the browser cannot parse falls back to the series colour. - Steps keep the colour of the point they level out from up to the vertical jump. Curves, `lineStyle` and `breakGaps` work as without colours, and dashes run on across colour changes. When several points share a pixel column, the column takes its last point's colour; with grouping on, a bucket takes the colour of its last point. - The legend, the last-value label, point markers and the crosshair marker use the colour at their point. - A line where some point has a colour draws on the canvas, whatever its `renderer`. A line with no colours draws exactly as before, at no extra cost. The series checks for colours on `setData` and on each `update` it keeps (not on one it drops), so once an update brings a colour the line stays on the colour path until the next `setData`. ## Histogram ```ts const volume = chart.addHistogram({ title: 'Volume' }, pane); volume.setData(bars.map((b) => ({ time: b.time, value: b.volume ?? 0, color: b.close >= b.open ? '#3cc4a080' : '#e8574a80', }))); // Or let it follow the candles: no setData or update calls needed. chart.addHistogram({ title: 'Volume', follow: volumeOf }, pane); ``` | Option | Default | Notes | | --- | --- | --- | | `title` | `''` | | | `follow` | none | `(bar, prev, theme) => point \| null`: derive the series from the main bars, see [Series derived from the bars](#series-derived-from-the-bars). Lines and custom series take it too | | `color` | `theme.line` | Color for all columns | | `positiveColor`, `negativeColor` | none | Color by sign relative to `base`; wins over `color` | | `base` | `0` | Columns grow from this value | | `columnRatio` | `0.8` | Column width relative to bar spacing | | `groupBy` | `'sum'` | What a column shows for several points when the chart [groups bars](./view-and-scale#group-bars-when-zoomed-out): `'sum'`, `'max'` or `'last'` | | `format` | compact | | | `priceFormat` | none | As for lines | | `lastValueLabel` | `true` | | | `priceLine`, `offscreenLastPrice` | off, unset | As for [candles](#price-line) | | `autoscale` | `true` | As for [candles](./view-and-scale#per-series-autoscale) | | `axisTitle` | `false` | Title tag beside the axis label | | `overlay` | none | Own price scale | A per-point `color` wins over everything else. ## Series derived from the bars `follow` takes a mapper `(bar, prev, theme) => point | null` on line, histogram and custom series, and the chart keeps the series in step with the main bars: on `setData`, `update`, data source loads, history pages, symbol switches and theme changes. Return `null` to leave a bar out. `volumeOf` is the built-in volume mapper: it colours each bar by whether it closed above the previous close. Example `derived-series` (runs live on the docs page): ```ts import { createChart } from '@chart-on/core'; import type { ExampleContext } from './types'; import { sampleBars } from './sample-data'; export default function mount(el: HTMLElement, { theme }: ExampleContext) { const chart = createChart(el, { theme }); const candles = chart.addCandles({ title: 'SAMPLE' }); // Each bar's range as a line on its own pane; return null to leave a bar out. chart.addLine({ title: 'Range', follow: (bar) => ({ time: bar.time, value: bar.high - bar.low }) }, chart.addPane()); candles.setData(sampleBars()); chart.fitContent(); return () => chart.destroy(); } ``` A new bar is mapped once, and a live update of the forming bar maps only that bar and replaces its point in place. A mapper that needs more than the previous bar (running totals, moving averages) is better written as an [indicator](./indicators). Each point must have its bar's `time`. On a full load (`setData`, attaching the series, setting `follow` with `applyOptions`) a point at another time throws. On a live update the error goes to the `series:error` event with `hook: 'follow'` instead, that series skips the bar and the others carry on. Setting `follow` later maps the current bars; removing it clears the series, and removing the main series empties every follower. ## Price formats `priceFormat` sets how a series' prices are shown, parsed and snapped. The axis, crosshair, legend, last-value labels, indicators on the price pane, drawings, primitives and the line, histogram and custom series on that scale that have no format of their own all use the format of the pane's scale: its first candle series, else the first series with a `priceFormat`. An invalid `priceFormat` throws when the series is created or `applyOptions` is called. ```ts chart.addCandles({ priceFormat: { type: 'decimal', tickSize: 0.25 } }); // 4512.25 chart.addCandles({ priceFormat: { type: 'fraction', denominator: 32 } }); // 101'16 = 101 16/32 chart.addCandles({ priceFormat: { type: 'fraction', denominator: 32, subDenominator: 4 }, // 101'162 = 101 16.25/32 }); chart.addCandles({ priceFormat: { type: 'decimal', unit: 'USD', tickSize: [ { from: 0, tickSize: 0.0001 }, // below 1 { from: 1, tickSize: 0.01 }, { from: 1000, tickSize: 1 }, ], }, }); ``` | Type | Options | Shows | | --- | --- | --- | | `decimal` | `tickSize` (a number, or bands by price; the first band also covers lower prices), `precision`, `zeroFold` | Fixed decimals. Prices round to the tick, and the tick sets the decimals unless `precision` is given (2 without either) | | `fraction` | `denominator`, `subDenominator` (1 to 9), `separator` (default `'`) | Whole units and a numerator padded to the denominator's width. A sub-fraction adds one digit: tenths of a fraction step, rounded down (quarters read 0, 2, 5, 7) | | `volume` | | Compact amounts such as `1.25M` | | `percent` | `precision` | `12.30%` | | `custom` | `format`, `parse`, `tickSize` | Your own formatter | Every type also takes `unit`, shown at the top of the price axis. With a tick size, price-axis labels land on whole ticks and drawing points snap to the nearest tick. `createPriceFormat(format)` returns the resolved formatter (`format`, `parse`, `tickSize`, `round`) for your own UI, such as a price input. `chart.formatPrice(pane, price)` and `chart.roundPrice(pane, price)` apply a pane's format. A data source sets the format per symbol with `priceFormat`, `tickSize` or `precision` in its symbol info. ### Very small prices Prices such as 0.00000412 show long runs of zeros that widen the axis and are hard to count. `zeroFold` on a `decimal` format folds them into a count after the first zero: `0.0₅412` is 0.00000412, with five zeros after the decimal point. ```ts chart.addCandles({ priceFormat: { type: 'decimal', precision: 10, zeroFold: {} } }); // 0.0₅41200 chart.addCandles({ priceFormat: { type: 'decimal', tickSize: 0.00000001, zeroFold: { minZeros: 5, style: 'brace' } }, }); // 0.0{5}412 ``` | Option | Default | Meaning | | --- | --- | --- | | `minZeros` | `4` | Fold only with at least this many zeros after the decimal point; fewer stay as digits | | `style` | `'subscript'` | `'subscript'` writes the count in subscript digits; `'brace'` writes `{5}`, for fonts without subscript digits | Prices round to the tick and precision first, then fold, so the folded text keeps the decimals the format asks for. Zero and prices of 1 or more never fold. Every label that goes through the format folds: axis, crosshair, legend, last-value labels, drawings and annotations. `parse` reads both folded forms (in any decimal format), so price inputs round-trip. `getDataTable()` returns numbers, so exports keep every digit. ## Overlay scale By default a series shares its pane's price scale. With `overlay` it gets its own scale, squeezed between top and bottom margins (fractions of pane height). Typical use: volume along the bottom of the price pane. ```ts const volume = chart.addHistogram({ title: 'Volume', overlay: { top: 0.8, bottom: 0 }, // bottom 20% of the pane }); ``` ## Heatmap A time × price grid, one column per main-series bar. Heatmaps are [opt-in](./features#opt-in-features): create the chart with the `heatmap()` plugin (or `builtins()`). The UI shell adds it for you. `addHeatmap` builds the heatmap's GPU program, so a shader failure throws from that call ([`webgl-shader`](./errors#webgl-shader)). ```ts const heat = chart.addHeatmap({ title: 'Liquidations' }); heat.setData({ times, // one per column, must match consecutive main bars priceStart: 60000, // price of row 0 (lowest bucket) priceStep: 10, // price height of each row rows: 400, values, // Float32Array, column-major: values[col * rows + row] }); heat.updateColumn(time, column); // replace last column (same time) or append next ``` `setData` throws when `times` are not finite and strictly ascending. `updateColumn` follows the `update` rules: the same time as the last column replaces it, a newer time appends, an older one is dropped and reported through `data:dropped` (`bar` is `{ time, column }`), and a time that is not a finite number throws. Non-finite values in a column are empty cells. | Option | Default | Notes | | --- | --- | --- | | `title` | `'Heatmap'` | | | `colors` | `theme.heatmap` | Color ramp, low to high | | `range` | auto | Fixed `{ min, max }`; otherwise percentiles of positive values, recomputed after `setData` or an option change and at most every 250 ms while `updateColumn` changes columns | | `lowPercentile`, `highPercentile` | `40`, `98` | Auto range bounds | | `gap` | `1` | Gap between cells in px, hidden when cells get small | | `opacity` | `0.95` | | | `cellReduce` | `'max'` | Where several cells fall into one device pixel (zoomed out, or many rows in a short pane): `'max'` shows the largest, so a thin level stays visible however the view pans; `'sum'` their total; `'nearest'` only the cell at the pixel centre. Rows are exact at any depth, read from reduction levels (copies of each column with 4, 16, 64… rows combined per value); columns are exact up to 8 per pixel, and past that evenly spaced columns are read and a sum is scaled up for the ones skipped | | `inspector` | `true` | Hover grid showing neighbouring cell values. It shows the cells under the pointer, not the reduced value a pixel is drawn with | | `format` | `v.toFixed(2)` | | | `maxColumns` | none | Most columns kept | | `memoryBudget` | 256 MiB | Bytes the grid may use, at 8 per cell (GPU texture plus CPU copy), plus about a third more for the reduction levels unless `cellReduce` is `'nearest'` | Heatmaps always render beneath other series in their pane. Limits: `rows` is capped by the GPU max texture size. The number of columns kept is the smallest of the GPU max texture size, `maxColumns` and `memoryBudget / ((rows + level values) × 8)`, where the levels add about `rows / 3` values a column with `cellReduce` `'max'` or `'sum'` and none with `'nearest'`; a 2,000-row grid keeps up to about 12,500 columns (16,384 with `'nearest'`), a 16,384-row grid about 1,500 (2,048). Changing `cellReduce` rebuilds the levels and can drop the oldest columns. `setData` keeps the newest columns that fit, an `updateColumn` that appends at the cap drops the oldest eighth, and lowering either option drops the oldest columns over it at once. These trims are silent: they are not bad input, so they do not emit `data:dropped`. An `updateColumn` whose column does not have `rows` values throws and leaves the grid unchanged. ## Points Markers at an exact time and price, drawn on the GPU in one call however many there are: large trades or liquidations as bubbles sized by quantity, sampled ticks, signal clouds. Unlike bar markers they are not tied to a bar: several items may share a time, and each sits at its own time between bars or after the last one. Point series are [opt-in](./features#opt-in-features): create the chart with the `points()` plugin (or `builtins()`). The UI shell adds it for you. Without it, `addPoints` throws [`not-registered`](./errors#not-registered). ```ts const trades = chart.addPoints({ title: 'Large trades', sizeBy: { min: 3, max: 26 }, // marker area grows with `size` opacity: 0.5, }); trades.setData([ { time: 1717200030, value: 67210, size: 12.5, color: '#3cc4a0' }, { time: 1717200030, value: 67190, size: 3.1, color: '#e8574a' }, // same time: kept in order { time: 1717200095, value: 67250, size: 40 }, ]); trades.update({ time: 1717200120, value: 67300, size: 8 }); // appends; never replaces ``` ```ts interface PointItem { time: number; // exact time; x is interpolated between bars value: number; // price; items without a finite value are left out size?: number; // css px diameter, or with `sizeBy` a quantity color?: string; // fill; default the series `color` } ``` Times must be ascending but may repeat, and items with equal times keep their order (later ones draw on top). `setData` throws on a descending time; with `{ normalize: true }` it sorts instead, stably, and drops nothing. `update` always appends: an item older than the newest one is dropped and reported through `data:dropped`. | Option | Default | Notes | | --- | --- | --- | | `title` | `''` | | | `shape` | `'circle'` | `'circle'`, `'square'` or `'diamond'` | | `color` | `theme.line` | Fill of items without their own `color` | | `size` | `6` | Diameter in css px of items without a usable `size` (missing, negative or not finite) | | `sizeBy` | none | `{ min, max, domain?, scale? }`: read item sizes as quantities and map them to diameters from `min` to `max` px. `domain` defaults to the smallest and largest size in the data; quantities outside it clamp. `scale: 'sqrt'` (default) makes the area grow with the quantity, `'linear'` the diameter. Without `sizeBy`, an item's `size` is its diameter in px | | `opacity` | `0.8` | Fill opacity | | `stroke`, `strokeWidth` | none, `1` | Outline colour and width in px | | `format`, `priceFormat` | compact, none | As for lines | | `lastValueLabel` | `false` | Label the newest item's value on the axis | | `priceLine`, `offscreenLastPrice` | off, unset | As for [candles](#price-line) | | `axisTitle`, `overlay`, `priceScale` | | As for lines | Points take part in autoscale (the items whose centres are in view), work on log, percent and overlay scales, and show in the legend as the newest value in the bar under the crosshair with the number of items in that bar (`N`). `chart.valuesAt` gives `{ value, count }` for the same bar. `series:hover` hit-tests the markers themselves, within their radius plus 3 px, and carries the item under the pointer as `item`; it fires again when the pointer moves to another item of the same series. Data is uploaded to the GPU when it changes: `setData` writes every item, `update` one, and a new main bar rewrites only the items from the bar before it on (they were placed by extrapolation). Panning and zooming only change what is drawn, and a widening size domain is a shader uniform, so no data moves. --- Source: /guide/data-sources.md # Data sources Instead of pushing bars with `setData` and `update`, give the chart a data source. The chart then loads the initial bars, pages in older history as the user scrolls back, merges live bars and caches what it loaded per symbol and interval. Loading through a source needs the `dataSource()` plugin, or `builtins()`; the UI shell adds it. Without it `setDataSource` throws a [`not-registered`](./errors#not-registered) `ChartError`, so a chart that only gets bars through `setData` leaves the loading code out of its bundle. ```ts import { createChart, dataSource } from '@chart-on/core'; const chart = createChart(el, { plugins: [dataSource()] }); ``` Example `data-source` (runs live on the docs page): ```ts import { createChart, createMockDataSource, dataSource } from '@chart-on/core'; import type { ExampleContext } from './types'; export default function mount(el: HTMLElement, { theme }: ExampleContext) { // `dataSource()` lets the chart load through a source; it is opt-in so charts fed by `setData` stay small. const chart = createChart(el, { theme, plugins: [dataSource()] }); chart.addCandles(); // The chart loads the first bars, pages in history as you scroll back and // merges live bars. The mock source makes up a market; use your own adapter. chart.setDataSource(createMockDataSource({ seed: 3 })); void chart.setSymbol('DEMO', '1m'); return () => chart.destroy(); } ``` Call `setSymbol` again to switch interval or symbol: the request in flight is cancelled and live updates move to the new stream. `setSymbol` rejects when loading fails. Without a candle series the chart adds one. The push API keeps working when no data source is set. ## Intervals An interval is a count and a unit: `s`, `m`, `h`, `d`, `w`, or the calendar units `M` (month), `Q` (quarter) and `y` (year). A bare unit counts one, so `'d'` is `'1d'`. The `Interval` type spells this out, so a typo in a literal such as `'1x'` does not compile; turn an app's own string into one with `isInterval`: ```ts import { isInterval, intervalSeconds, parseInterval } from '@chart-on/core'; if (isInterval(picked)) await chart.setSymbol('ABC', picked); intervalSeconds('15m'); // 900, the nominal length parseInterval('4h'); // { count: 4, unit: 'h' } ``` `setSymbol` checks the interval before it asks the data source anything: one it cannot parse rejects with a [`ChartError`](./events#errors) whose code is `'invalid-interval'`, and the chart keeps showing what it showed. ### Intervals a symbol supports When `symbol` returns `intervals`, the chart only loads one of them. Asked for another, it loads the supported interval nearest in length (compared as a ratio, so `1h` is as far from `30m` as from `2h`; a tie goes to the shorter one) and says so with `interval:change`: ```ts chart.on('interval:change', ({ interval, previous, requested, reason }) => { // reason 'unsupported': `requested` is not supported, `interval` is shown instead. picker.value = interval; }); chart.data?.supportedIntervals; // the symbol's intervals the chart can parse, or null for any ``` `interval:change` follows `data:loaded` whenever a load lands at another interval than before (`previous` is null on the first load), and whenever the requested interval was replaced, even by the one already shown. An empty `intervals` list, or one without a single valid entry, means any interval. ### Calendar intervals Bars of `M`, `Q` and `y` follow the calendar in the symbol's `timezone` (UTC when it has none): a month bar starts at midnight on the 1st, a quarter in January, April, July or October, and several months (`'6M'`) or years (`'2y'`) are counted from January of year 0. So months are 28 to 31 days long, and DST can make one an hour shorter or longer. Their nominal length, used for comparisons only, is 30, 90 and 365 days. The chart relies on this in two places. A live bar whose time falls inside the open calendar bar updates that bar, instead of adding a new one, so a source that stamps live bars with the trade time still works. And the countdown runs to the next calendar boundary. History requests never derive a time range from the interval's length: older pages ask for at most `limit` bars before `to`. `intervalStart(time, interval, zone, open)` and `nextIntervalStart(start, interval, zone)` do the same calendar math for a data source that builds its own bars. `open` is the session open in seconds after midnight, so a bar opening at 09:30 starts at 09:30 on its first day; the next bar keeps the time of day of the one before. ### Trade and volume intervals Two more units count activity instead of time: `T` makes a bar every `count` trades (`'100T'`) and `V` every `count` units of traded volume (`'5000V'`). They describe the same bars as `createBarBuilder({ ticks: 100 })` and `{ volume: 5000 }` (see [Bars from trades](#bars-from-trades-and-from-shorter-intervals)), so a data source that builds bars from trades can offer them in its symbol's `intervals`. `isActivityInterval` tells them apart: ```ts import { intervalSeconds, isActivityInterval, parseInterval } from '@chart-on/core'; parseInterval('100T'); // { count: 100, unit: 'T' } isActivityInterval('100T'); // true intervalSeconds('100T'); // throws a ChartError 'invalid-interval': no length ``` These bars have no length in seconds, so: - `intervalSeconds` throws for them; anything that tolerates them gets `null` from the chart instead (drawings and indicators asking for the bar length, for example). - The countdown under the last price is hidden: the bar closes on activity, not on time. - The time axis already places bars by index and labels each with its `time` (its first trade), so uneven spacing needs nothing. - They are never part of an interval range (`{ from: '1m', to: '59m' }`), a higher-interval series or grouping steps, and they do not bucket through `barBucket`. An indicator set fits them only when it has no `appliesOn` scope. - When a symbol's `intervals` do not list the activity interval asked for, the chart loads the listed one of the same unit nearest in count, else the shortest listed time interval; looking for the nearest time interval never picks an activity one. It always loads a listed interval. - The built-in mock and exchange data sources make time bars only: asked for an activity interval, they reject with an `invalid-interval` `ChartError`. - A persistent cache keeps their snapshot for 10 minutes. - The `intervals` toolbar preset of `@chart-on/ui` shows them as "100 trades" and "5K volume" under their own headings, and only while the current symbol lists them ([toolbar presets](./ui-shell#presets)). ### The symbol's timezone With `applySymbolTimezone: true` the chart shows each symbol in its `timezone`, and in the chart's own `timezone` when the symbol has none. Setting `timezone` with `applyOptions` while it is on keeps that timezone for later symbols too, until `applySymbolTimezone: true` is set again: ```ts const chart = new Chart(el, { timezone: 'UTC', applySymbolTimezone: true }); await chart.setSymbol('ABC', '1d'); // shown in America/New_York chart.applyOptions({ timezone: 'Asia/Tokyo' }); // the user picked Tokyo: it stays chart.applyOptions({ applySymbolTimezone: true }); // follow the symbol again ``` ## Loading state and events ```ts chart.data?.loading; // true while a load, history page or reset is in flight chart.data?.initialLoad; // the setSymbol load in flight, or null chart.data?.symbol; // SymbolInfo now shown chart.data?.interval; chart.data?.requested; // { symbol, interval } of the last setSymbol, even while it loads chart.data?.supportedIntervals; // the symbol's intervals, or null for any chart.data?.hasMoreHistory; // false once the source reported atStart chart.on('data:loading', ({ symbol, interval, kind }) => showSpinner(kind)); chart.on('data:loaded', ({ symbol, interval, kind, bars }) => hideSpinner()); chart.on('data:error', ({ kind, error }) => console.error(error)); chart.on('data:bar', ({ bar, appended }) => updateHeader(bar)); ``` `kind` is `'initial'`, `'history'` or `'reset'` (bars refetched after a [correction or reconnect](#corrections-and-reconnects)). `data:loaded` carries the whole series now on the chart. Series that are a per-bar mapping of the main bars, such as volume, need no event code: give them a [`follow`](./series#series-derived-from-the-bars) mapper and the chart keeps them in step. Rebuild anything else derived from the bars here. ## Compare symbols Plot other symbols next to the main one. Each compared symbol loads and streams through the same data source, follows the main symbol's interval, and pages in history to match the main series. `chart.compare` is [opt-in](./features#opt-in-features): it needs the `comparisons()` plugin (or `builtins()`), and throws a [`not-registered`](./errors#not-registered) `ChartError` without it. The UI shell adds it for you. ```ts const gold = chart.compare('XAUUSD'); // percent overlay on the main pane const index = chart.compare('IND50', { mode: 'scale' }); // its own left price axis const fx = chart.compare('USDJPY', { mode: 'pane', color: '#22d3ee' }); const oil = chart.compare('OIL', { mode: 'pane', style: 'candles' }); // candles instead of a line await gold.ready; // settles after its first load (which waits for the main symbol) gold.series.applyOptions({ lineWidth: 2 }); gold.serialize(); // { symbol: 'XAUUSD', mode: 'percent', style: 'line', color: '#f59e0b' } gold.setMode('pane'); // moves it to a pane of its own, keeping its bars and this handle gold.remove(); // removes the line and unsubscribes chart.compared; // every compared symbol ``` | Mode | Shows | | --- | --- | | `percent` (default) | A line on the main pane. The main price scale switches to percent mode, and both lines are measured from their first visible bar on one shared percent axis. | | `scale` | A line on the main pane with its own left price axis, in its own units. | | `pane` | A line in a new pane. | - A compared symbol is a line of closes with its own symbol info, price format, colour and legend row. - `style: 'candles'` or `'bars'` draws it as candles or OHLC bars instead (`series` is then a candle series), in `scale` and `pane` modes; `percent` mode takes lines only. In a new pane they use the theme's rising and falling colours. On the main pane, beside the main candles, both directions take the compare `color`, with falling bodies faded, so the two symbols read apart. - It is aligned on the main timeline. A bar whose time falls between main bars moves into the main bar it falls in (the last one wins for a line; candles merge: first open, highest high, lowest low, last close), and main bars it has no data for leave gaps in its line (the `breakGaps` line option). - In `percent` mode the comparison needs the main scale in percent or indexed mode. If you switch the main scale back to normal or log, the compared line takes that mode too and fits its own range, so it only shows its shape. Removing the last percent comparison restores the mode the main scale had before. - A synthetic pair plots two symbols combined bar by bar: `compare({ ratio: ['GOLD', 'SILVER'] })` shows gold in silver, `compare({ spread: ['A', 'B'], weights: [2, 1] })` shows `2 × A − B`. Open and close come from the legs' opens and closes; high and low from their highs and lows, widened to include open and close (an approximation, since the legs' extremes need not happen together). A time where either leg has no bar, or a ratio would divide by zero, is a gap. Both legs load, page and stream like a single compared symbol, and `remove()` unsubscribes both. The handle's `symbol` is the pair's name (`'GOLD/SILVER'`, `'2*A-B'`), `pair` the pair, `info` the first symbol's info, and `serialize()` includes `pair`. - `setMode(mode)` moves a compared symbol between modes without reloading it: the series, its bars, its legend row and the handle stay the same. It fires `panes:change` (a pane is added or removed as needed), never `series:remove` or `series:add`. Leaving the main pane remembers the row's place there, and going back to `pane` mode puts the pane back where it was, at its height. Candles and bars take only `scale` and `pane`. After `remove()` it does nothing. - Load failures emit `compare:error` with the symbol and the error (for a pair, the leg that failed). - Changing the data source removes compared symbols. ## Market and data status With the `marketStatus()` plugin (in `builtins()`), the chart works out the market phase from its `session` option (regular hours) and an optional `extendedSession` (pre- and post-market around them), in the chart's `timezone`. Without the plugin `chart.marketStatus()` throws a [`not-registered`](./errors#not-registered) error and the legend shows no badge; `marketPhase` works without it: ```ts import type { SessionSchedule } from '@chart-on/core'; // The same hours Monday to Friday. const weekdays = (start: string, end: string): SessionSchedule => { const day = [{ start, end }]; return { weekly: { mon: day, tue: day, wed: day, thu: day, fri: day } }; }; chart.applyOptions({ timezone: 'America/New_York', session: weekdays('09:30', '16:00'), extendedSession: weekdays('04:00', '20:00'), }); // Without an argument it uses the current time. At 08:30 in New York on Wednesday 2024-03-06: chart.marketStatus(Date.parse('2024-03-06T13:30:00Z') / 1000); // → { phase: 'pre', next: { phase: 'open', time: 1709735400 }, delay: 0 } (09:30 New York) ``` - `phase` is `pre`, `open`, `post` or `closed`; `next` is the next change within three weeks. Holidays, one-day overrides, overnight sessions and DST changes are taken into account. Without a session the market is always open. - Extended hours are pre-market until regular hours open in the same stretch, and after hours once they have closed in it, so one extended range can cover both (e.g. `weekdays('00:00', '24:00')`). An always-open `extendedSession` (`ALWAYS_OPEN`) means extended hours around the clock. - `delay` comes from the symbol info: `delay: 15` for data 15 minutes behind, `'eod'` for end-of-day data. - The main pane's legend shows both as a badge, e.g. "Pre-market · 15 min delayed" (turn it off with the `marketStatus` option). `marketPhase(regular, extended, zone, now)` is exported for your own UI; `regular` and `extended` take a `SessionSchedule` or a `CompiledSession`. The chart also shows the project mark assembling itself in the centre of the plot area while the first bars load, and a centred message when a symbol has no data, when loading fails, and for an expired contract (`expired: true` in the symbol info). A failed load shows "Could not load data"; the data source's error message is kept in `chart.getStatus().detail` rather than shown. While the live stream is down it shows "Reconnecting…", and after an outage too long to patch "Data may be out of date" (see [Corrections and reconnects](#corrections-and-reconnects)). Apps can set the message themselves with `chart.setStatus({ kind: 'loading' | 'no-data' | 'error' | 'unknown-symbol' | 'expired' | 'reconnecting' | 'stale', message?, detail? })` and clear it with `chart.setStatus(null)`. Every change fires `status:change`; a listener that calls `preventDefault()` hides the chart's text so you can show your own view (the UI shell's [`loading` and `empty` slots](./ui-shell#slots) do this). The chart never sets `unknown-symbol`, since a data source reports every failure the same way: set it from your app when your source knows a symbol does not exist, e.g. on `data:error`. The mark's three steps wipe in, its square grows into place, and it fades out, every 3 seconds (it stays still and complete with reduced motion). It keeps "Loading…" (`statusLoading`) as the status's accessible name, and the watermark and the mark it may carry are left out while it shows; a `loading` status with a `message` of its own shows that text instead. ## Options ```ts chart.setDataSource(source, { initialBars: 1000, // bars requested on the first load pageBars: 500, // bars per page of older history cacheSize: 8, // symbol + interval combinations kept in memory maxGapFillSeconds: 3600, // longest outage patched on reconnect cache: null, // persistent snapshot store, see below }); ``` Switching back to a cached symbol and interval shows the cached bars and fetches only the bars that are newer. If more than a page is missing it reloads. ### Paint ahead from a persistent cache The memory cache dies with the page. With a `cache` adapter, the chart also keeps a snapshot of the newest window of each symbol and interval (the bars of an initial load, live bars included) in persistent storage. The next load of that symbol and interval, after a reload too, paints the snapshot at once while the source is asked as usual: ```ts import { createIndexedDbCache } from '@chart-on/core'; chart.setDataSource(source, { cache: createIndexedDbCache({ namespace: `user-${userId}` }) }); chart.on('data:snapshot', ({ savedAt }) => showBadge(`cached ${ago(savedAt)}`)); chart.on('data:loaded', hideBadge); ``` - The snapshot is never authoritative. The load always runs; its bars replace the snapshot's, and if they are the same nothing is repainted. `data:snapshot` fires when a snapshot is painted, and the loading indicator is left out meanwhile. - If the load fails while a snapshot shows, the snapshot stays: `setSymbol` rejects and `data:error` fires as usual, followed by `data:stale` with `reason: 'reconcile'` and the snapshot's age as `gap`. The chart shows "Data may be out of date" instead of the error. - Snapshots are written after each successful first load and when switching to another symbol or interval, under the symbol and interval `setSymbol` was given. They expire after about 200 bars' worth of time (at least 10 minutes, at most 30 days); expired entries and entries from another storage format version are ignored. - Storage errors (quota, private mode, no IndexedDB) are swallowed: the chart loads as if nothing was cached. - `chart.data.clearCache()` clears the adapter's namespace too, e.g. on logout or a user switch; `clearCache(symbol, interval)` removes that one entry. Use a per-user `namespace` for data that depends on the user's entitlements. - Only data loaded through a data source is cached; series filled with `setData` never are. `createMemoryCache()` keeps snapshots in memory instead (useful in tests), and any object with async `get`, `set`, `delete` and `clear` works as an adapter. A snapshot only helps when the source returns the same bars for the same request. A custom source should be deterministic: the same symbol, interval and range give the same bars, so an unchanged snapshot is not repainted and a changed one really is newer data. ## Corrections and reconnects A live feed is not append-only. Servers republish corrected bars, a socket loses the bars sent while it was down, and a symbol can be relisted with new history. The chart handles all three by refetching through `history`. **Corrections.** Report bars you already sent that changed through the optional `onReset(listener)`. The chart subscribes once, like `onStatus`, and the listener gets a `BarsReset`: `{ symbol, interval?, range? }`. `symbol` is the `SymbolInfo.symbol` of the changed bars, `interval` is omitted when every interval changed (a split, say), and `range` is omitted when the whole series did: ```ts const resets = new Set<(reset: BarsReset) => void>(); const feed = openFeed(); feed.on('corrected', ({ symbol, interval, from, to }) => { for (const listener of resets) listener({ symbol, interval, range: { from, to } }); // just those bars }); feed.on('relisted', ({ symbol }) => { for (const listener of resets) listener({ symbol }); // everything, at every interval }); const source: DataSource = { // symbol, search, history, stream... onReset(listener) { resets.add(listener); return () => resets.delete(listener); }, }; ``` The subscription belongs to the source rather than to `stream` for three reasons: sources without a stream (REST, polling, static) can report corrections too; everything that reads bars (the main series, other symbols' bars in indicators) refetches through one path; and it works like `onStatus`. - The chart refetches only the symbol and interval it shows (or is loading), and an indicator only the other symbols' bars it reads; resets of anything else are ignored. - Without a range, the chart drops its cached series and loads it again: as many bars as it had, and at least the initial count. History paging starts again from the new first bar. - With a range (`from` inclusive, `to` exclusive, `to` omitted for "up to the newest bar"), it asks for one page in that range and replaces only the bars inside it. A range that holds a page or more is treated as a full reset, and so is a malformed one (not finite, or `to` at or before `from`). - The view keeps its right edge. Live bars that arrive during the refetch are merged after it, so the forming bar keeps its live value. - Several resets can be in flight at once. Their responses apply in the order the resets were requested: a slow older response never overwrites bars from a newer reset that landed first. - A reset during the first load runs once that load lands. A reset of a symbol or interval the chart no longer shows (after a switch) is ignored. A full reset drops a history page in flight; scrolling back loads it again. A range reset does not: the page still lands, since it only adds bars older than any the chart has. - Each reset emits `data:loading` and `data:loaded` with `kind: 'reset'`, then `data:reset { symbol, interval, range }`. **Reconnects.** A source with a live stream can report its connection with the optional `onStatus(listener)`. The listener gets `{ state, since }`, where `state` is `'connected'`, `'reconnecting'` or `'disconnected'` and `since` is when that state began, in unix seconds. Use `disconnected` only for a stream closed on purpose, e.g. nothing left to subscribe. The WebSocket and mock adapters implement it. - On `reconnecting` the chart emits `data:reconnecting { since }` and shows "Reconnecting…". A reset that lands while the stream is still down keeps the message. A drop during the first load does not cover the loading indicator; the message appears once the bars land if the stream is still down (a "no data", error or expired message stays instead). - On `connected` it emits `data:reconnect { gap }` with the outage in seconds. It then fetches the bars from the last one it has up to now and merges them. The source wins on equal times; the forming bar keeps any live value that arrived meanwhile. A reconnect during the first load fills its gap once that load lands. - If the outage was longer than `maxGapFillSeconds` (one hour by default), it fetches nothing and emits `data:stale { gap }`. `chart.data.stale` turns true, and the pane shows "Data may be out of date" until a reload, a full reset or a switch loads fresh bars. - The outage is measured in wall-clock time from the `since` of `reconnecting` to that of `connected`, without regard to trading sessions. An outage spanning a closed session (overnight, a weekend) can exceed the limit and show stale although no bars were missed; raise `maxGapFillSeconds` or reload if that matters for your market. **Reloading.** Force a fresh load yourself: ```ts await chart.reload(); // the current symbol and interval, without the cache await chart.reload({ keepView: true }); // ...and keep the visible time range chart.data?.clearCache(); // forget every cached series chart.data?.clearCache('ABC'); // ...or one symbol's, or one symbol and interval's chart.data?.connection; // the source's last { state, since }, or null ``` `reload()` aborts requests in flight like a symbol switch and emits a single `data:loaded` (`kind: 'initial'`). Without `keepView` it shows the newest bars. Compared symbols reload too, without their cache. `clearCache` leaves the series on screen alone; switching back to a cleared series loads it afresh. ## Splits and dividends A stock's history has jumps that are not price moves: after a 4-for-1 split every price is a quarter of what it was, and an ex-dividend day drops by the dividend. To compare old prices with new ones, charts usually back-adjust the history. Send the bars unadjusted, and add `adjustments` to your data source: ```ts const source: DataSource = { // ...symbol, search, history as usual async adjustments(symbol, { from, to, signal }) { const res = await fetch(`${baseUrl}/actions?symbol=${symbol.symbol}&from=${from}&to=${to}`, { signal }); return (await res.json()) as Adjustment[]; }, }; chart.applyOptions({ adjustment: 'splits' }); // or 'all', or 'none' (default) ``` Each adjustment is `{ time, kind: 'split' | 'dividend' | 'other', factor, label? }`. `factor` multiplies the prices of every bar before `time`: `0.25` for a 4-for-1 split, `(close - dividend) / close` of the day before for a dividend. A split also divides earlier volumes by its factor. The chart asks for the adjustments taking effect within each batch of bars it loads (`from` inclusive, `to` exclusive), after the first load and after each history page. | `adjustment` | Applies | | --- | --- | | `'none'` (default) | Nothing: the bars as sent | | `'splits'` | Splits only | | `'all'` | Splits, dividends and other actions | Adjusting and marking need the `adjustments()` plugin (or `builtins()`); without it the bars show as sent. Switching `adjustment` re-scales the loaded bars without fetching anything and keeps the view; indicators recompute on the adjusted bars. A source without `adjustments` is shown as sent whatever the option. For bars you push yourself, `adjustBars(bars, adjustments, mode)` and `adjustBar(bar, adjustments, mode)` from `@chart-on/core` do the same arithmetic. Drawings on the main pane stay on the bars they were drawn on: when the option changes, or loading reveals another adjustment (an older split a history page brings), their points are re-priced with the bars, undo history included. `chart.drawings` reports the prices on screen, while `drawings.serialize()` writes the prices as your source sent them and `drawings.load()` reads them that way, so a saved layout lands on the same bars under any option. Save with `serialize()` rather than from `drawings:change` payloads. Charts in a link group that mirrors drawings put each copy on the same bars, even when their `adjustment` options differ. Annotations pinned to a bar follow it the same way, without changing what you passed: their prices are read as your source sends them, and they draw at the adjusted price of their bar on the main pane. That covers `atPrice` bar markers (at the marker's `time`), trade fills (at their bar), text labels (at their `time`) and price zones that end at a bar (`toTime`). `getMarkers()`, `getTrades()` and the handles' options keep returning your prices, trade events and tooltips keep the traded price, and nothing needs re-sending when the option changes or a split loads. Shape plots of indicators already sit on the adjusted bars and are left alone. Lines without a time stay at their price: alert, price, order and position lines, and price zones that reach the latest bar, are levels of the live price, which no adjustment changes (the bars after the newest action are shown as sent). The adjustments are marked on the main series, in the bottom lane of its pane, whatever `adjustment` applies: a lettered label (`S` split, `D` dividend, `C` other) whose tooltip shows the action's `label` (or its kind) and its factor. Their `kind` is the action's, so `chart.annotations.setMarkerFilter({ kinds: ['split'] })` keeps only the splits, and `marker:hover` and `marker:click` report them with ids `adjustment::