Speed
Faster than you can see
One million candles, panning on every frame. The numbers are measured live in your browser.
Generating 1,000,000 candles…
WebGL2 · TypeScript · Apache-2.0
Financial charts that stay smooth with a million candles. A headless engine you can drive from any UI, plus an optional toolkit when you want one.
Developer experience
Small, typed and predictable: options are plain objects, every subscription returns its own unsubscribe, and nothing hides in global state. Code that people and coding agents read the same way.
installTestEnvironment.import { createChart } from '@chart-on/core'; const chart = createChart(document.getElementById('chart')!);chart.addCandles({ title: 'BTC/USD' }).setData(bars);chart.fitContent();
import { createChart, createWebSocketDataSource, dataSource } from '@chart-on/core'; const chart = createChart(el, { plugins: [dataSource()] });chart.addCandles();chart.setDataSource(createWebSocketDataSource(feed)); // History pages in on scroll; the forming bar updates live.await chart.setSymbol('BTC/USD', '1m');
import { builtinIndicators, rsiIndicator } from '@chart-on/indicators'; for (const def of builtinIndicators) chart.indicators.register(def);chart.indicators.add('bbands');chart.indicators.add(rsiIndicator, { length: 21 });
const line = chart.drawings.add({ type: 'hline', pane: 0, points: [{ time: 1767225600, price: 104 }], style: { lineStyle: 'dashed' },}); chart.drawings.update(line.id, { text: 'Resistance', locked: true });
import { volumeOf } from '@chart-on/core'; chart.addCandles().setData(bars); // Volume in its own pane, following the main series.const pane = chart.addPane({ stretch: 0.25 });chart.addHistogram({ title: 'Volume', follow: volumeOf }, pane);
import { builtInColorPresets, presetTheme } from '@chart-on/core'; const paper = builtInColorPresets.find((p) => p.id === 'paper-ink')!;chart.applyOptions({ theme: presetTheme(paper, 'light') }); // Only the keys you pass change.chart.applyOptions({ theme: { up: '#3d8bd9', down: '#e07b39' } });
import { describeScene, flushFrames, installTestEnvironment } from '@chart-on/core/testing'; const env = installTestEnvironment({ width: 960, height: 540 });const chart = createChart(env.container());chart.addCandles().setData(bars);flushFrames(); expect(describeScene(chart)).toMatchSnapshot();
CPU per pan frame
0.05ms
With 1,000,000 candles loaded. The rest of a 16.7 ms frame is left to your app.
to load 1M candles
104ms
setData of one million bars. 100,000 bars take 10.5 ms.
gzip for a live chart
92.4KiB
createChart plus a REST data source, tree-shaken. Zero runtime dependencies.
to zoom four linked charts
0.13ms
Four charts of 100,000 candles each, kept in sync on every frame.
Speed
One million candles, panning on every frame. The numbers are measured live in your browser.
Generating 1,000,000 candles…
Agent ready
llms.txtIndex of every pagellms-full.txtAll docs in one file<page>.mdEach page as MarkdownPick what you ship
Every feature beyond a basic chart is an opt-in export.
@chart-on/core@chart-on/ui@chart-on/indicators@chart-on/ta@chart-on/locales@chart-on/react@chart-on/vue@chart-on/svelteDrawings
Trend lines, Fibonacci, shapes and text, plus a plain-object contract for your own.
Indicators
Declared inputs and outputs, O(1) live updates, panes that follow the main series.
Theming
Built-in colour presets, CSS variables for the UI and a theme object for the canvas.
Order flow
Resting size over time as a heatmap, the live book beside the price axis, and footprints and profiles from your trades. Simulated here; feed it any order book.
Liquidity heatmapDepth laneFootprintVolume profileTPO profileLarge tradesMake it yours
Write indicators as pure functions, show your users' trades, mark zones your code finds, draw your own series and restyle every colour. Toggle a layer to see its code.
import { ema } from '@chart-on/ta'; const regime: IndicatorDef<{ length: number }> = { id: 'trend-regime', name: 'Trend regime', placement: 'overlay', inputs: [{ id: 'length', type: 'number', default: 21, min: 2 }], plots: [ // Each bar takes a palette colour from the 'trend' output. { id: 'avg', type: 'line', lineWidth: 2, palette: ['#22a06b', '#e5484d', '#8b93a7'], colorOutput: 'trend', lastValueLabel: false }, { id: 'tint', type: 'background', palette: ['rgba(34, 160, 107, 0.08)', 'rgba(229, 72, 77, 0.08)'] }, ], compute(src, { length }) { const avg = ema(src.close, length); // 0 rising, 1 falling, 2 flat (also the first 8 bars): the change over 8 bars against 0.6% of the price. const trend = avg.map((v, i) => { const change = (v - avg[i - 8]) / v; return Number.isNaN(change) || Math.abs(change) < 0.006 ? 2 : Number(change < 0); }); return { avg, trend, tint: trend }; },}; chart.indicators.add(regime);// Your users' fills, at the price they traded.chart.annotations.setMarkers([ { time: buy.time, shape: 'label', position: 'atPrice', price: buy.price, color: '#3d8bd9', text: 'Buy 2', tooltip: 'Bought 2' }, { time: sell.time, shape: 'label', position: 'atPrice', price: sell.price, color: '#f5a524', text: 'Sell 1', tooltip: 'Sold 1' },]); // The open position's levels, labelled on the price axis.chart.annotations.addPriceLine({ price: buy.price, title: 'Entry', color: '#3d8bd9' });chart.annotations.addPriceLine({ price: stop, title: 'Stop', color: '#e5484d', lineStyle: 'dashed' });chart.annotations.addPriceLine({ price: target, title: 'Target', color: '#22a06b', lineStyle: 'dashed' });// A range your own code found, boxed between its first and last bar.chart.annotations.addPriceZone({ from: range.low, to: range.high, fromTime: range.start, toTime: range.end, color: 'rgba(61, 139, 217, 0.12)', label: 'Range',}); // An event window: the two days after earnings, shaded below the candles.chart.annotations.addTimeBand({ from: earnings, to: earnings + 47 * HOUR, label: 'Earnings' });const bubbles: CustomSeriesDef<{ time: number; price: number; size: number; up: boolean }> = { type: 'bubbles', priceRange: (p) => [p.price, p.price], // Plain Canvas 2D, with the chart's scales and theme. draw(ctx, view) { for (const { item, x } of view.items) { ctx.beginPath(); ctx.arc(x, view.priceToY(item.price), 3 + 9 * item.size, 0, Math.PI * 2); ctx.fillStyle = withAlpha(item.up ? view.theme.up : view.theme.down, 0.3); ctx.fill(); } },}; // Volume spikes: the top tenth of bars by volume.chart.addCustomSeries(bubbles, { title: 'Volume spikes' }).setData( spikes.map((b) => ({ time: b.time, price: b.close, size: Math.sqrt(b.volume / maxVolume), up: b.close >= b.open })),);// Only the keys you pass change; custom series read them from view.theme.chart.applyOptions({ theme: { background: '#0f1729', grid: '#1b2640', text: '#c3cde6', textMuted: '#7b88a8', up: '#2dd4bf', down: '#f472b6', },});Open source
Free and open source under Apache-2.0. Read the code, open an issue or send a pull request: every package, the docs and this site live in one repository.
Attribution
Credit is optional and much appreciated. Keep the small logo the chart shows by default, or put a link near the chart so others can find the project.
GitHub
A star helps other developers find charton and tells us the work is useful.