bonguynvan/tradecanvas

Canvas trading charts for the web: 95 indicators, 69 drawing tools, live exchange feeds, orders on the chart and multi-chart workspaces. Zero dependencies. TypeScript, React, Vue, Svelte.

TypeScript

19

337 commits

updated Oct 4, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built a crypto backtester that tells you when your strategy is just luck (r/SideProject)

**What it is:** tradingdek.com: build a trading strategy from rules (no code), backtest it on years of Binance data with fees included, then run it as a paper bot or follow its signals. **The part I'm proud of:** every strategy in the library carries an honest badge from a monthly audit: 20 pairs,…

1

Oct 4, 2026

README

TradeCanvas, the chart engine for trading apps

npm version npm downloads CI status No third-party dependencies TypeScript MIT license GitHub stars

Live demo · Docs · Examples · Playground · Changelog

English · Tiếng Việt · 简体中文 · 日本語 · 한국어 · Español

A complete trading chart for the web. Candlesticks to Renko, 95 indicators, 69 drawing tools, live exchange feeds and orders on the chart, drawn on Canvas2D with zero dependencies. Drop in the full ChartWidget, or build your own UI on the headless Chart, in plain TypeScript, React, Vue or Svelte.

ChartWidget with live BTCUSDT from Binance: EMA 21 and 55, RSI, a trend line, a long position and a watchlist with live quotes

Quick Start

npm install @tradecanvas/chart     # or: pnpm add / yarn add

ChartWidget is the whole trading UI in one component: toolbar, drawing sidebar, settings dialog and status bar.

import { ChartWidget } from '@tradecanvas/chart/widget'
import { BinanceAdapter } from '@tradecanvas/chart'

const widget = new ChartWidget(document.getElementById('chart')!, {
  symbol: 'BTCUSDT',
  timeframe: '5m',
  theme: 'dark',
  adapter: new BinanceAdapter(), // live data, no API key
  trading: true,
})

That's it. Live data, all 95 indicators, all 69 drawing tools, command palette (Ctrl+K), symbol search (Ctrl+P), hotkey sheet (?), shift-drag measure, alt-click tooltip pin, and drag-drop CSV/JSON loading.

Using a framework? @tradecanvas/react, @tradecanvas/vue and @tradecanvas/svelte wrap the headless Chart as a component, and the widget above mounts the same way in any framework; see Framework Integration. Or fork a StackBlitz sandbox and start from there.

A Look Around

Drawing tools: a Fibonacci retracement with a note, a trend line, an Elliott impulse wave and a long position
69 drawing tools
Fibonacci, Gann, pitchforks, Elliott waves, harmonic patterns, notes and brushes, and a Long/Short position that sizes the trade. Alerts on trend lines, groups, undo and redo.
Trading on the chart: a long position with stop-loss and take-profit, a buy stop and a sell limit, and the account panel
Trading on the chart
Positions with live P&L, orders you drag to a new price, SL and TP, reverse and close buttons, an order ticket and an account panel. A paper broker is built in; connect your own.
A two-by-two workspace of BTC, ETH, SOL and BNB charts on mixed intervals
Multi-chart workspace
Up to six full charts side by side, linked by symbol, interval, crosshair, time or drawings, and saved as one layout.
One chart in three looks: studio, terminal, and capsule on the light theme
Your own look
Three presets (studio, terminal, capsule) or your own corner radius, density, fonts, toolbar and price tags, on light and dark themes.

Why TradeCanvas?

Most chart libraries make you choose: pretty charts with no trading features, or trading features with an ugly API. TradeCanvas gives you both.

  • 95 built-in indicators — SMA, EMA, TEMA, VWMA, Hull MA, RSI, MACD, Bollinger, Envelope, Ichimoku, Pivot Points, Anchored VWAP, ZigZag, Linear Regression Channel, Awesome / Chaikin Oscillator, and more. Any indicator can read another one's line (an SMA of RSI). No separate calculation library needed.
  • 69 drawing tools — Trendlines (info line, trend angle, cross line), Fibonacci (retracement, extension, channel, time zones, speed resistance fan and arcs, circles, spiral, wedge), horizontal/vertical lines, channels, pitchforks and pitchfan, Gann fan / box / square, cycles, harmonic patterns (XABCD, cypher, ABCD, three drives, head and shoulders), Elliott waves, notes, callouts and marks, brush and path, forecast and projection, Long/Short Position with position sizing, Volume Profile range. Each with its own settings, alerts on trend lines, groups and layers, undo/redo and full serialization.
  • 18 chart types — Candlestick, line, area, bar, hollow candle, baseline, High-Low, Heikin-Ashi, Renko, Kagi, Line Break, Point & Figure, Range Bars, Volume Candles, Equivolume, HLC Area, Step Line, Line+Markers. Renko's box, Kagi's reversal and the like are yours to set.
  • Pro-grade interaction — pan freely past the last bar into empty future space (drawings can go there too), drag the price/time axes to scale, double-click to auto-fit, Ctrl/⌘+drag to select several drawings (then move, restyle or delete them together), Shift+drag to measure (bars × price Δ × %), Alt+click to pin a comparison tooltip, context cursors (crosshair, grabbing hand, resize arrows), axis-following price/time pill labels under the cursor, bar-hover highlight.
  • Trading overlay — Render open positions with entry line, P&L zone, and SL/TP markers. Orders as dashed lines. Drag SL/TP to modify, cancel / close / reverse from the buttons on each line, and see every fill marked on its bar. ChartWidget adds an order ticket that checks the order as you fill it in, and an account panel with positions, working orders and history. Cleanly opt-out via features.trading: false for non-trading projects.
  • Real-time streaming — Built-in Binance, Coinbase, Bybit, and Kraken adapters, plus generic WebSocketAdapter / PollingAdapter bases so any feed plugs in with ~20 lines. Older bars load as you scroll back, any interval (7m, 90m, 2d) is built from the feed's own, tick charts (100T) from its trades, and symbol search and quotes come from the feed.
  • Time zones — any IANA zone with daylight saving time ('America/New_York'), a fixed offset, or the exchange's own zone, for the axis, crosshair, day breaks and session hours.
  • 16 languages — ChartWidget in English, Vietnamese, Simplified and Traditional Chinese, Japanese, Korean, Spanish, Portuguese, French, German, Russian, Turkish, Indonesian, Thai, Arabic and Hebrew; Arabic and Hebrew mirror right to left.
  • Accessible — keyboard navigation, zoom and scroll buttons over the chart, and a screen-reader summary that reads the view and the bars one at a time.
  • Live execution — connect an ExecutionAdapter to turn the trading overlay into a real trading surface, drag on the chart to create orders, and reconcile fills. Ships a PaperExecutionAdapter sandbox.
  • Plugin SDK — register custom indicators, drawing tools, chart types, and overlays — globally or per-chart.
  • Strategy backtester — @tradecanvas/analytics ships a bar-by-bar Backtester with virtual fills, commission/slippage models, portfolio tracking, and risk metrics (Sharpe, Sortino, Calmar, max drawdown). Now with 4 ready-to-use reference strategies + Monte Carlo path-dependence analysis.
  • Replay mode — replay the chart's own bars from any point, in finer steps if you like (an hourly chart forming from 5-minute bars), with play / pause / step / seek / speed, and paper-trade on the replayed prices. The widget has a replay bar for it; ReplayController drives bars headless too.
  • Alerts — on a price level, an indicator line, a drawing, or one line crossing another; on a move of some percent within some bars; only on closed bars; with an expiry. The widget's alerts panel sets all of them.
  • Compare and spread — other symbols in percent on the price scale, on a scale or pane of their own, or as a spread or ratio, lined up with the chart by time.
  • Price formats — prices in your own format or in fractions of a point (a bond in 32nds: 110'165) on every label; times your way; extended hours on or off; data export with the indicator lines.
  • Volume Profile — optional horizontal histogram of traded volume bucketed by price over the visible range, with point-of-control highlighting.
  • Watchlists and symbol info — lists of symbols to switch between, edit and reorder, with live quotes; a symbol panel with price, the market's status, the day's numbers, hours and news.
  • CSV / JSON drag-and-drop — drop a file onto the chart, it parses and loads instantly. Detects header layouts, ISO/unix-s/unix-ms timestamps, and array-vs-object JSON shapes.
  • Named layouts — save the chart under a name (symbol, interval, scale, indicators, drawings, alerts), open, rename, delete, auto-save the open one, Ctrl/⌘+S. Kept in the browser, or on your server through a four-call LayoutStorage. Per-symbol auto-persistence (persistLayouts) is there too.
  • Multi-chart — ChartWidgetGrid puts up to six full widgets side by side, linked by symbol, interval, crosshair, time or drawings as you choose, and saves them as one layout. ChartGrid does the same for bare charts.
  • Signal markers & trade zones — render bot/algorithm output (directional arrows, entry→exit rectangles) as a first-class chart layer.
  • Hotkey sheet — press ? in the widget to open a categorized keyboard-shortcut reference.
  • Extensible widget — add your own toolbar buttons and right-click menu entries (addToolbarButton, chartMenuItems).
  • Save/load chart state — Persist drawings, indicators, theme, and chart type to JSON. Restore with one call.
  • Zero dependencies — The entire library is self-contained. No d3, no chart.js, no fancy-canvas.

Headless Chart

For projects that want to own the surrounding UI (custom toolbar, framework-specific controls), use the lower-level Chart class directly:

import { Chart, BinanceAdapter } from '@tradecanvas/chart'

const chart = new Chart(document.getElementById('chart')!, {
  theme: 'dark',
  autoScale: true,
  features: {
    drawings: true,
    indicators: true,
    trading: true,           // set false to disable orders/positions entirely
    tradingContextMenu: true, // opt-in right-click order menu (off by default)
    volume: true,
  },
})

const adapter = new BinanceAdapter()
chart.connect({ adapter, symbol: 'BTCUSDT', timeframe: '5m', historyLimit: 300 })

Widget Options

OptionTypeDefaultDescription
symbolstring'BTCUSDT'Initial trading symbol
timeframeTimeFrame'5m'Initial timeframe
theme'dark' | 'light' | Theme'dark'Chart theme
adapterDataAdapter—Data source adapter
toolbarbooleantrueShow top toolbar
drawingToolsbooleantrueShow left drawing sidebar
settingsbooleantrueShow settings button
tradingbooleantrueEnable trading overlay
statusBarbooleantrueShow bottom status bar
rangeBarbooleantrueRange presets (1D … All) and go to date (Alt+G) on the status bar
indicatorLegendbooleantrueIndicators listed on the chart (under the OHLCV legend and atop their panes) with show / settings / remove
fullscreenbooleantrueFullscreen button in the toolbar
symbolsstring[]BTC/ETH/SOL/BNBSearchable symbol catalog
timeframesTimeFrame[]1m to 1MTimeframes on offer; pin favourites from the ▾ menu
chartTypesChartType[]18 typesAvailable chart types
watchlistbooleanfalseRight-side watchlist sidebar
dragDropImportbooleantrueDrop CSV / JSON files onto the chart to load data
persistLayoutsboolean | { keyPrefix, debounceMs }falseSave per-symbol indicators / drawings / chart type to localStorage
onSymbolChange(symbol) => void—Symbol change callback
onTimeframeChange(tf) => void—Timeframe change callback
onReady(chart) => void—Fired when chart is ready
localestring'en'UI language — 'en' and 'vi' built in, 12 more from the locales entry, see Widget i18n below
messagesPartial<Record<MessageKey, string>>—Override or add individual UI strings on top of locale

Icons

The widget's icon set is exported for your own UI: createIcon(name), createToolIcon(drawingTool), createChartTypeIcon(chartType) return inline SVG strings drawn in currentColor (24 px grid, 1.75 px strokes).

import { createToolIcon } from '@tradecanvas/chart/widget'
button.innerHTML = createToolIcon('fibRetracement', 16)

Widget i18n

ChartWidget speaks 16 languages: English, Vietnamese, Simplified and Traditional Chinese, Japanese, Korean, Spanish, Portuguese, French, German, Russian, Turkish, Indonesian, Thai, Arabic and Hebrew (the last two right to left; dir sets the direction yourself). Every string it shows is translated: toolbar, settings, drawing tools, alerts, dialogs, the command palette, the hotkey sheet and notices. Indicator names (SMA, RSI…) stay as they are. Set at construction.

English and Vietnamese are built in. The others load from @tradecanvas/chart/widget/locales, so a page ships only the languages it imports:

import { ja } from '@tradecanvas/chart/widget/locales'

new ChartWidget(el, {
  locale: 'ja',
  messages: ja,                               // or registerWidgetLocales() for all of them
  chartOptions: { numberLocale: 'ja-JP' },    // separate: number/date formatting (see below)
});

messages also overrides single keys on top of locale ({ 'watchlist.title': 'Theo dõi' }). A locale with a region falls back to its language (ja-JP → ja; zh-TW → Traditional Chinese).

locale/messages cover the text; chartOptions.numberLocale controls number and date formatting (price axis, legend, watchlist prices, current-price tag, session-break dates) via Intl.

See packages/library/src/widget/locales/en.ts for the full key list (MessageKey).

Widget vs Headless

Chart (headless)ChartWidget
Import@tradecanvas/chart@tradecanvas/chart/widget
UI includedNone — build your ownComplete toolbar, sidebar, settings
Bundle impact~50 KB gzip~65 KB gzip (includes UI)
FrameworkAny (React, Vue, Svelte, vanilla)Vanilla JS DOM (works everywhere)
CustomizationFull controlToggle sections on/off
Advanced accessDirect APIwidget.getChart() for direct API

Widget Look

The widget's shapes and sizes — corners, control heights, type, borders, shadows, how a chosen button shows, bars docked or floating — are a look of their own, apart from the colours. Three presets: Studio (the default), Terminal (dense and square) and Capsule (pills, floating bars). Start from one and change what you like:

const widget = new ChartWidget(host, { ui: 'terminal' });
widget.setUI({ preset: 'studio', radius: { md: 10 }, density: 'compact', toolbar: 'floating', active: 'solid' });

The chart's price tags take the same corners (tagRadius; on a bare Chart, chart.setShapes({ tagRadius })). The widget loads no fonts: load the ones a look names. See Styling.

Widget Theming

ChartWidget's own chrome (toolbar, sidebars, settings panel, watchlist — everything outside the canvas) is styled entirely through CSS custom properties on .tcw-root, the widget's own root element. These are a stable, documented contract: additive-only across minor/patch releases — a property is never renamed or removed without a major version bump. Override them from the host page; no build step or theme object needed.

/* Dark is the default (no attribute needed); light sets data-tcw-theme="light" */
.my-app .tcw-root:not([data-tcw-theme="light"]) {
  --tcw-bg: #0a0a0f;
  --tcw-accent: #7c5cff;
  --tcw-radius: 0px;
  --tcw-radius-lg: 0px;
}
VariableDefault (dark)Purpose
--tcw-bg#080b10Root background
--tcw-bg-surface#0c1016Panel / toolbar surface
--tcw-bg-elevated#141922Popovers, dropdowns, modals
--tcw-bg-overlayrgba(20,25,34,.5)Backdrop behind overlays
--tcw-border#1f2630Default border
--tcw-border-strong#2a323eEmphasized border (focus rings, dividers)
--tcw-text#e7e9eePrimary text
--tcw-text-dim#aab1bdSecondary text
--tcw-text-muted#758091Tertiary / placeholder text
--tcw-accent#f2a93bPrimary accent (active tab, focus, links)
--tcw-accent-ink#1a1204Text and icons on an accent fill
--tcw-accent-hover#f5b95cAccent hover state
--tcw-accent-softrgba(242,169,59,.14)Accent tint (selected row background)
--tcw-accent-glowrgba(242,169,59,.22)Accent glow (focus halo)
--tcw-accent-linergba(242,169,59,.55)Accent border/underline
--tcw-red / --tcw-red-soft#e8505b / tintDown/sell/negative
--tcw-green / --tcw-green-soft#1fa874 / tintUp/buy/positive
--tcw-amber#ff9f43Warning
--tcw-hover-bgrgba(255,255,255,.05)Row/button hover background
--tcw-active-bgrgba(255,255,255,.08)Row/button pressed background
--tcw-dividerrgba(255,255,255,.06)Hairline dividers
--tcw-ease / --tcw-ease-outcubic-bezierTransition easing
--tcw-dur-fast / -normal / -slow120ms / 180ms / 260msTransition durations
--tcw-radius-xs / -sm / --tcw-radius / -lg / -xl3px / 5px / 7px / 11px / 16pxThe corner scale — set to 0 for a square look
--tcw-control-radius / --tcw-input-radius / --tcw-menu-radius / --tcw-dialog-radius / --tcw-panel-radius / --tcw-tooltip-radius / --tcw-tag-radius / --tcw-toast-radiusfrom the scaleEach kind of part's corners
--tcw-toolbar-h / --tcw-control-h / --tcw-control-h-sm / --tcw-icon / --tcw-sidebar-w / --tcw-menu-item-h46px / 30px / 24px / 18px / 48px / 30pxSizes
--tcw-font / --tcw-font-size / --tcw-weight / --tcw-weight-strong'Manrope', 'Inter', … / 13px / 500 / 600Type
--tcw-label-case / --tcw-label-trackingnone / 0emSmall labels (section titles)
--tcw-border-w / --tcw-sep-w1px / 0pxBorder width; rules between toolbar groups
--tcw-menu-shadow / --tcw-dialog-shadow / --tcw-tooltip-shadowthe elevation shadowsShadows of menus, dialogs, tooltips
--tcw-blur / --tcw-surface-opacity0px / 100%Frosted menus
--tcw-shadow-sm / -md / -lg / -xlbox-shadow valuesElevation
--tcw-ring0 0 0 2px rgba(242,169,59,.45)Focus ring
--tcw-font-mono'JetBrains Mono', …Monospace font stack (price ladder, code)

Light theme ([data-tcw-theme="light"]) redefines the color group (--tcw-bg*, --tcw-border*, --tcw-text*, --tcw-accent*, --tcw-hover-bg, --tcw-active-bg, --tcw-divider, --tcw-shadow*) with its own defaults — override both selectors if you support both themes. With the ui option set, the widget writes its look's variables on the element, so they win over your CSS; without it they stay yours to set.

Features

Chart Types

TypeDescription
CandlestickStandard OHLC candles
Hollow CandleOpen/close determines fill
Bar (OHLC)Classic open-high-low-close bars
LineClose price line
AreaFilled area below close
BaselineTwo-tone area split at a reference price
Heikin-AshiSmoothed candles for trend identification
RenkoFixed-size bricks that ignore time
KagiReversal-based line chart
Point & FigureX/O columns for supply/demand analysis
Line BreakThree-line break charts
Range BarsFixed price-range bars — each bar's high − low equals a configured range
Volume CandlesCandlesticks with width proportional to volume
EquivolumeFull-range boxes with width proportional to volume share (Richard Arms style)
HLC AreaHigh-low-close area band with close line
Step LineStaircase/step pattern from close prices
Line with MarkersClose line with circular markers at each data point

Multi-Chart Grid

Display multiple synchronized charts side-by-side with linked crosshairs and time axis:

import { ChartGrid, BinanceAdapter } from '@tradecanvas/chart'

const grid = new ChartGrid(document.getElementById('grid')!, {
  layout: '2x2',
  syncCrosshair: true,
  syncTimeAxis: true,
})

// An adapter keeps one stream: give each chart its own
grid.connectAll(() => new BinanceAdapter(), ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'], '5m')

With the full widget on each chart, a bar to pick the arrangement and sync, and the whole grid saved as a named layout:

import { ChartWidgetGrid } from '@tradecanvas/chart/widget'

const workspace = new ChartWidgetGrid(document.getElementById('grid')!, {
  layout: '1x2',
  adapter: () => new BinanceAdapter(),
  cells: [{ symbol: 'BTCUSDT' }, { symbol: 'ETHUSDT', timeframe: '1h' }],
  sync: { crosshair: true, interval: false, symbol: false, time: false, drawings: false },
})
workspace.setSync({ time: true })

Supported layouts: '1x1', '1x2', '2x1', '2x2', '1x3', '3x1', '2x3', '3x2'.

Command Palette

Press Ctrl+K (or Cmd+K) inside ChartWidget to open a searchable command palette. Quickly find and toggle indicators, change chart types, activate drawing tools, switch timeframes, or trigger actions (screenshot, theme toggle, settings).

Finance Charts

ChartDescription
SparklineChartTiny inline line/area chart from a number array — for dashboards and KPI cards
DepthChartBid/ask order book visualization with cumulative volume areas
EquityCurveChartPortfolio equity line with drawdown shading and benchmark comparison
HeatmapChartColored cell grid with treemap layout — for sector/market performance
WaterfallChartRunning cumulative bars — P&L attribution, revenue bridge, cash flow
GaugeChartSpeedometer-style gauge — KPIs, risk scores, Fear & Greed index
import {
  SparklineChart, DepthChart, EquityCurveChart, HeatmapChart,
  WaterfallChart, GaugeChart,
} from '@tradecanvas/chart'

// Sparkline in a 120x48 container
new SparklineChart(el, { data: [100, 102, 98, 105, 103], mode: 'area', color: '#1fa874' })

// Equity curve with drawdown
new EquityCurveChart(el, { data: equityPoints, drawdown: true, benchmark: spyData })

// Order book depth
new DepthChart(el, { data: { bids, asks }, crosshair: true })

// Market heatmap (treemap weighted by market cap)
new HeatmapChart(el, { data: cells, weighted: true })

// P&L waterfall
new WaterfallChart(el, {
  data: [
    { label: 'Start', value: 10000, type: 'total' },
    { label: 'Gain', value: 1850 },
    { label: 'Loss', value: -620 },
    { label: 'End', value: 11230, type: 'total' },
  ],
})

// Fear & Greed gauge: zones light up to the value, the label shows the current zone
const gauge = new GaugeChart(el, {
  value: 72,
  label: 'Fear & Greed',
  zones: [
    { from: 0, to: 25, color: '#e8505b', label: 'Extreme fear' },
    { from: 25, to: 45, color: '#f2a93b', label: 'Fear' },
    { from: 45, to: 55, color: '#8a93a3', label: 'Neutral' },
    { from: 55, to: 75, color: '#62c895', label: 'Greed' },
    { from: 75, to: 100, color: '#1fa874', label: 'Extreme greed' },
  ],
  // pointer: 'needle',  // classic needle instead of the ring marker
})
gauge.setValue(85) // animates smoothly

Indicators (built-in)

95 indicators — moving averages (SMA, EMA, WMA, Hull, DEMA, TEMA, ALMA, KAMA, LSMA, McGinley, SMMA, MA Cross, MTF MA), bands and channels (Bollinger, Keltner, Donchian, Envelope, Linear Regression), trend and stops (Ichimoku, Supertrend, Parabolic SAR, Chandelier, Chande Kroll Stop, Alligator, ZigZag, Fractals, Pivot Points), VWAPs and Volume Profile on the price pane; RSI, MACD, Stochastic, ATR, ADX, CCI, OBV, MFI, Bollinger %B and BandWidth, Historical Volatility, Ulcer Index and 40 more oscillators, volume and volatility indicators in panes. The indicator catalog lists every id with its inputs, lines and levels.

import { indicatorSource } from '@tradecanvas/chart'

const rsi = chart.addIndicator('rsi', { period: 14 })!
chart.addIndicator('ema', { period: 21, source: 'hlc3' })                     // another price
chart.addIndicator('sma', { period: 9, source: indicatorSource(rsi, 'value') }) // RSI's own average, in RSI's pane
chart.setIndicatorLevels(rsi, [20, 50, 80])
  • Sources: close, open, high, low, hl2, hlc3, ohlc4, hlcc4, or another indicator's line.
  • Panes: one value scale per pane for lines, levels, axis and crosshair; move an indicator into another pane, a new one or back to the price pane; fold, maximise and reorder panes (moveIndicatorToPane, setPaneCollapsed, setMaximizedPane, movePane).
  • Undo and templates: Ctrl/Cmd+Z undoes indicator changes, in the same history as drawings; ChartWidget saves the indicators as named templates (getIndicatorSetup / applyIndicatorSetup).
  • Levels: editable per instance (RSI 30/70, CCI ±100 …), kept in saved layouts.
  • Value tags: each line's latest value on its axis, in its colour.
  • Custom indicators declare their lines (plots), scale, levels and inputs; the chart draws and labels them.

Invalid parameters (NaN, Infinity, non-numeric strings, missing keys) fall back to the defaults instead of reaching the calculations.

Drawing Tools

Trendline, Horizontal Line, Vertical Line, Ray, Extended Line, Parallel Channel, Fibonacci Retracement, Fibonacci Extension, Fibonacci Time Zones, Rectangle, Ellipse, Triangle, Arrow, Pitchfork, Gann Fan, Gann Box, Elliott Wave, Regression Channel, Date Range, Price Range, Measure, Anchored VWAP, Volume Profile Range, Text Annotation

All drawing tools support:

  • Click-to-place with magnet snapping to OHLC values
  • Undo / redo (Ctrl+Z / Ctrl+Y)
  • Serialization for save/load
  • Custom styles (color, width, dash pattern)

Trading Overlay

Render open positions and pending orders directly on the chart, like MT4/MT5.

import type { TradingPosition, TradingOrder } from '@tradecanvas/chart'

chart.setPositions([{
  id: 'pos-1',
  side: 'buy',
  entryPrice: 3500,
  quantity: 1.5,
  closedQuantity: 0.5,   // partial close — visualized as a left-edge dim band
  stopLoss: 3400,
  takeProfit: 3700,
}])

chart.setOrders([{
  id: 'order-1',
  side: 'sell',
  type: 'limit',
  price: 3800,
  quantity: 0.5,
  label: 'TP',
  draggable: true,
}])

// Customize the position zone color via P&L thresholds
chart.setTradingConfig({
  pnlThresholds: [
    { pnl: -Infinity, color: '#b91c1c' },
    { pnl: 0,         color: '#94a3b8' },
    { pnl: 50,        color: '#16a34a' },
    { pnl: 200,       color: '#15803d' },
  ],
  // Custom label template — tokens: {side} {qty} {openQty} {closedQty} {entry} {price} {pnl} {pnlPct} {pnlSign}
  positionLabel: '{side} {openQty}/{qty} @ {entry} | {pnlSign}{pnl} ({pnlPct})',
})

// Listen for user drag-to-modify
chart.on('positionModify', (e) => console.log('SL/TP moved:', e.payload))
chart.on('orderModify', (e) => console.log('Order moved:', e.payload))

// The × and ⇅ buttons on the lines raise these; so can your own UI
chart.cancelOrderIntent('order-1')
chart.reversePositionIntent('pos-1')
chart.on('executionFill', (e) => console.log(e.payload.reason, e.payload.pnl))

Signal Markers

Visualize buy/sell signals from bots, indicators, or manual analysis.

chart.addSignalMarker({
  time: 1715692800000,
  price: 62500,
  direction: 'long',
  confidence: 0.85,
  source: 'ema-crossover',
  label: 'EMA Cross',
})

// Color-code by source
chart.setSignalMarkerStyle({
  sourceColors: {
    'ema-crossover': '#4c8dff',
    'rsi-divergence': '#f2a93b',
    'whale-flow': '#9C27B0',
  },
})

Trade Zones

Render entry→exit rectangles with P&L coloring for executed trades.

const zoneId = chart.addTradeZone({
  entryTime: 1715692800000,
  entryPrice: 62500,
  exitTime: 1715700000000,
  exitPrice: 63200,
  direction: 'long',
  pnl: 140,
  pnlPercent: 1.12,
})

// Update a live trade when it closes
chart.updateTradeZone(zoneId, {
  exitTime: Date.now(),
  exitPrice: 63500,
  pnl: 200,
})

Real-Time Streaming

// Built-in Binance adapter (free, no API key)
chart.connect({
  adapter: new BinanceAdapter(),
  symbol: 'ETHUSDT',
  timeframe: '1m',
  historyLimit: 500,
})

// Or manual data feed
chart.setData(historicalBars)
chart.appendBar(newBar)
chart.updateLastBar(updatedBar)
chart.setCurrentPrice(3500.42)

Built-in adapters (all free, no API key): BinanceAdapter, CoinbaseAdapter, BybitAdapter, KrakenAdapter, plus MockAdapter for offline/testing.

import { BybitAdapter, KrakenAdapter, CoinbaseAdapter } from '@tradecanvas/chart'

chart.connect({ adapter: new BybitAdapter(),    symbol: 'BTCUSDT', timeframe: '1m' })
chart.connect({ adapter: new KrakenAdapter(),   symbol: 'BTC/USD', timeframe: '5m' })
chart.connect({ adapter: new CoinbaseAdapter(), symbol: 'BTC-USD', timeframe: '15m' })

Any feed in ~20 lines. Extend WebSocketAdapter (live + REST history) or PollingAdapter (REST-only feeds) — the base handles the connection lifecycle, reconnect, decoding, and event emission. You supply a URL and a parse function:

import { WebSocketAdapter } from '@tradecanvas/chart'

const myAdapter = new WebSocketAdapter({
  name: 'myexchange',
  wsUrl: (c) => `wss://api.myexchange.com/ws/${c.symbol}@kline_${c.timeframe}`,
  fetchHistory: (symbol, tf, limit) => fetch(`/candles?...`).then((r) => r.json()),
  parseMessage: (raw) => ({ bar: toBar(raw), closed: raw.k.x }),
})

Live Execution

Connect an ExecutionAdapter to turn the display-only trading overlay into a real trading surface. The chart routes its order/position intents into the adapter, and renders the authoritative orders / positions the adapter emits back — the adapter is the single source of truth. With no adapter connected, those intents stay plain events (backward-compatible).

import { PaperExecutionAdapter } from '@tradecanvas/chart'

chart.connectExecution(new PaperExecutionAdapter({ markPrice: 64000 }))

// Drag-to-create an order, then confirm:
chart.startOrderDraft('buy')   // draggable line at the latest close
chart.confirmOrderDraft()      // emits orderPlace → adapter fills → chart renders the position
// chart.cancelOrderDraft()

// One channel for failures (adapter-reported or a failed command):
chart.on('executionError', (e) => toast(e.payload.message))

Implement ExecutionAdapter (it mirrors DataAdapter) to wire a real broker / OMS: placeOrder, modifyOrder, cancelOrder, modifyPosition, closePosition, plus orders / positions / fill / error events. PaperExecutionAdapter is a virtual-fill sandbox for demos and tests. The order type of a drag-to-create order (limit vs stop) is inferred from where you drop the line relative to the current price.

Plugins — extend the chart

Register custom indicators, drawing tools, chart types, and overlays — globally (every chart created afterward inherits) or per-chart.

import { Chart, registerPlugin, IndicatorBase } from '@tradecanvas/chart'

class MyIndicator extends IndicatorBase { /* descriptor, calculate(), render() */ }

// 1) Global — available to every chart created afterward:
registerPlugin({ kind: 'indicator', plugin: new MyIndicator() })

// 2) Per-chart at construction:
const chart = new Chart(el, { plugins: [{ kind: 'overlay', plugin: myHeatmap }] })

// 3) Imperative on an instance:
chart.plugins.register({ kind: 'chartType', plugin: myCustomCandles })
chart.setChartType('my-custom-candles')   // custom chart types render via the plugin
Plugin kindContract
indicatorIndicatorPlugin — calculate() + render()
drawingDrawingPlugin — render() + hitTest()
chartTypeChartTypePlugin — createRenderer() + optional transform()
overlayOverlayPlugin — render(ctx, { viewport, data, theme }) on the main / overlay / ui layer

Indicators outside the chart

IndicatorWorkerHost computes an indicator from bars with the same messages a Web Worker would use. The worker script is not part of the published packages yet; pass null and register the plugins to compute in place (SSR, tests, scripts):

import { IndicatorWorkerHost, RSIIndicator } from '@tradecanvas/core'

const host = new IndicatorWorkerHost(null)
host.registerFallbackPlugin(new RSIIndicator())
const output = await host.calculate('rsi', { id: 'rsi', instanceId: 'rsi-1', params: { period: 14 } }, bars)

Save / Load

const json = chart.saveState()
localStorage.setItem('my-chart', json!)

chart.loadState(localStorage.getItem('my-chart')!)

// Download / upload files
chart.downloadState('my-chart.json')
await chart.loadStateFromFile()

// Or keep a layout saved as it changes (debounced)
chart.setAutoSave('my-chart', 1500)

A saved layout holds the chart type, theme, drawings, indicators (inputs, pane, colours, visibility) and alerts, including alerts on indicator lines.

Themes

import { DARK_THEME, LIGHT_THEME, DARK_TERMINAL, volumeColor } from '@tradecanvas/chart'

// Built-in presets: DARK_THEME, LIGHT_THEME, DARK_TERMINAL
chart.setTheme(DARK_TERMINAL)  // fintech terminal: #0E0E0E bg, #00FF87/#FF3B4D candles, monospace

// Or customize any preset
chart.setTheme({
  ...DARK_THEME,
  candleUp: '#1fa874',
  candleDown: '#e8505b',
  volumeUp: volumeColor('#1fa874'),    // volume bars: the candle colour, see-through
  volumeDown: volumeColor('#e8505b'),
  background: '#0a0a0f',
})

Events

chart.on('crosshairMove', (e) => { /* { point, bar, barIndex, indicatorValues } — also over indicator panes */ })
chart.on('crosshairLeave', () => { /* the pointer left the plot */ })
chart.on('drawingToolChange', (e) => { /* { tool } — null once a drawing is finished or cancelled */ })
chart.on('indicatorUpdate', (e) => { /* { from } — indicator values recomputed from this bar on */ })
chart.on('paneResize', (e) => { /* { instanceId, size } — an indicator pane was resized */ })
chart.on('indicatorChange', (e) => { /* { instanceId, change } — shown/hidden, restyled, levels, inputs or pane changed */ })
chart.on('barClick', (e) => { /* { bar, barIndex, point } */ })
chart.on('visibleRangeChange', (e) => { /* { from, to } — bar indices, not timestamps */ })
chart.on('priceRangeChange', (e) => { /* { min, max } — visible price bounds */ })
chart.on('zoomChange', (e) => { /* { barWidth } — pixels per bar */ })
chart.on('drawingCreate', (e) => { /* ... */ })
chart.on('orderModify', (e) => { /* ... */ })
chart.on('positionModify', (e) => { /* ... */ })

visibleRangeChange, priceRangeChange, and zoomChange fire on every pan, zoom, resize, and data update — but only when that piece of viewport state actually changed. Resolve a visibleRangeChange index to time with chart.getData()[e.payload.from].time.

Replay Mode

ReplayController plays a historical DataSeries forward at controlled speed. Decoupled from Chart — wire it into any sink (chart for UI playback, or a strategy fn for headless backtests).

import { ReplayController } from '@tradecanvas/chart'

const replay = new ReplayController({
  data: historicalBars,
  speed: 10,        // bars per second
  startIndex: 0,
})

// Seed the chart with the prefix before replay starts
chart.setData(replay.getPrefix())

// Each emitted bar drives the chart forward
replay.on('bar', ({ bar }) => chart.appendBar(bar))
replay.on('finished', () => console.log('done'))

replay.start()
// replay.pause(); replay.resume(); replay.step(5); replay.seek(200); replay.setSpeed(20)

Chart Interaction

Every gesture you'd expect from a desktop trading chart is built in:

GestureResult
Drag chart body left/rightPan through time
Drag chart body up/downPan the price scale (freezes auto-scale; double-click price axis to restore)
Drag price axis up/downCompress / expand vertical scale (freezes auto-scale)
Drag time axis left/rightZoom time axis
Double-click price axisRe-enable auto-scale
Double-click time axisFit all data to viewport
WheelZoom around cursor
Drag a pane dividerResize the indicator pane (ns-resize cursor on hover)
Shift + dragMeasure ruler (bars × time × price Δ × %)
Alt + clickPin OHLC tooltip; live crosshair shows Δ to pinned bar
HoverPrice + time pill labels follow on both axes
EscUnpin tooltip / cancel drawing
?Show keyboard-shortcut sheet (widget)
Ctrl/⌘ + KCommand palette (widget)
Ctrl/⌘ + PSymbol search (widget)
Ctrl/⌘ + Z / Shift + ZUndo / redo drawings

Data import — drag-and-drop or programmatic

import { parseOHLCV } from '@tradecanvas/chart'

const { data, rowCount, skipped } = parseOHLCV(csvText)
chart.setData(data)

Drop a CSV or JSON file onto the widget and it loads instantly. Auto-detects delimiter (, / ; / tab / |), header vs. headerless, ISO 8601 timestamps, and array-of-arrays vs. array-of-objects JSON.

Backtesting (@tradecanvas/analytics)

Bar-by-bar strategy backtester with virtual fills, commission/slippage models, and a full risk-metrics report.

import { Backtester, PercentCommission, PercentSlippage } from '@tradecanvas/analytics'

const bt = new Backtester({
  initialCash: 10_000,
  commission: new PercentCommission(0.0005),
  slippage: new PercentSlippage(0.0003),
})

const result = bt.run(historicalBars, (ctx) => {
  // Strategy fn runs at close of each bar; orders fill on the NEXT bar.
  if (!ctx.position && smaFast > smaSlow) {
    ctx.placeOrder({ side: 'long', type: 'market', quantity: 1 })
  } else if (ctx.position && smaFast < smaSlow) {
    ctx.close()
  }
})

console.log(result.metrics.sharpe)         // 1.42
console.log(result.metrics.maxDrawdownPct) // 0.087
console.log(result.equityCurve)            // → feed into the chart via EquityCurveRenderer

Returns: fills, closed trades, equityCurve, metrics (Sharpe, Sortino, Calmar, CAGR, max drawdown, win rate, profit factor, expectancy). See the live backtest demo.

Strategy library

Four drop-in reference strategies — each returns a StrategyFn ready to feed Backtester.run():

import {
  Backtester,
  smaCrossStrategy,
  rsiReversionStrategy,
  donchianBreakoutStrategy,
  bollingerReversionStrategy,
} from '@tradecanvas/analytics'

const bt = new Backtester({ initialCash: 10_000 })
bt.run(bars, smaCrossStrategy({ fastPeriod: 10, slowPeriod: 30 }))
bt.run(bars, donchianBreakoutStrategy({ entryPeriod: 20, exitPeriod: 10 }))

Monte Carlo path-dependence

Shuffle realised trade order N times to expose whether a strategy depends on lucky sequencing. Tight P5/P95 band = robust edge; wide band = path-dependent.

import { runMonteCarlo } from '@tradecanvas/analytics'

const result = bt.run(bars, smaCrossStrategy())
const mc = runMonteCarlo(10_000, result.trades, { simulations: 1000, seed: 42 })

mc.equityBands              // [{ step, p5, p25, p50, p75, p95 }, …]
mc.finalEquityPercentiles   // { p5, p25, p50, p75, p95 }
mc.probabilityProfitable    // 0..1
mc.worstMaxDrawdownPct

Comparison

Feature@tradecanvas/chartlightweight-chartschart.jsHighcharts Stock
Chart types18 + 6 finance48 (non-financial)10+
Finance chartsSparkline, Depth, Equity, Heatmap, Waterfall, GaugeNoneNoneSome
Built-in indicators9500~30
Drawing tools6900Some
Trading overlayFull (pos + orders + drag)NoneNoneNone
Real-time streamingBuilt-in (Binance)ManualManualBuilt-in
Save/load stateYesNoNoYes
Replay modeYes (ReplayController)NoNoNo
BacktesterYes (@tradecanvas/analytics)NoNoNo
Multi-chart gridYes (ChartGrid)NoNoYes
Bundle (gzip)~100 KB core~45 KB~70 KB~200 KB
Dependencies0100
Widget (complete UI)Yes (ChartWidget)NoNoNo
LicenseMITApache 2.0MITCommercial

API Overview

new Chart(container, options)

const chart = new Chart(element, {
  chartType: 'candlestick',
  theme: DARK_THEME,
  autoScale: true,
  rightMargin: 5,
  numberLocale: 'en-US',  // or 'de-DE', 'vi-VN', etc. — BCP 47 locale
  crosshair: { mode: 'magnet' },
  features: { drawings: true, indicators: true, trading: true, volume: true },
})

// Change locale at runtime
chart.setNumberLocale('de-DE')  // 65.234,00

Key Methods

MethodDescription
setData(bars)Load historical OHLCV data
appendBar(bar)Append a new candle
appendBars(bars)Bulk append (reconnect catch-up)
updateLastBar(bar)Update the in-progress candle
setCurrentPrice(price, pulseColor?)Show a live price line
connect(config)Connect to a real-time data source
setTimeframe(tf)Switch timeframe on active stream
setChartType(type)Switch chart type
setTheme(theme)Apply a theme (DARK_THEME, LIGHT_THEME, DARK_TERMINAL)
setNumberLocale(locale)Set number format locale (en-US, de-DE, vi-VN)
setStatusText(text)Show status in legend area ("LIVE · 8ms")
addIndicator(id, params?)Add a technical indicator
removeIndicator(instanceId)Remove an indicator
setDrawingTool(tool)Activate a drawing tool
setPositions(positions)Render trading positions
setOrders(orders)Render pending orders
setVolumeProfileVisible(v)Toggle the horizontal volume-profile overlay
setVolumeProfileConfig({ buckets, widthRatio, opacity, highlightPoC })Tune the volume profile
setAutoScale(v) / setLogScale(v)Lock or change price-scale mode
setInvertScale(v)Turn the price scale upside down
fitContent() / scrollToEnd()Fit all data / jump to live edge
setVisibleRangePreset(p)Show 1D, 5D, 1M, 3M, 6M, YTD, 1Y, 5Y or All
goToTime(time)Centre the bar at a time
setCrosshairTime(time)Mirror another chart's crosshair (vertical line only)
copyDrawings() / pasteDrawings()Copy the selection, paste into this or another chart
setStayInDrawingMode(v)Keep the drawing tool after each drawing
saveState(key?)Serialize chart state
loadState(json)Restore chart state
screenshot()Download chart as image
setRenderer(mode)Draw with 'canvas' (default), 'webgl' or 'auto'; resolves to what draws now
getRenderer()'canvas' or 'webgl'
on(event, handler)Subscribe to events
destroy()Clean up all resources

Data Format

interface OHLCBar {
  time: number    // Unix time in ms or seconds (up to 1e12 is read as seconds); ascending
  open: number
  high: number
  low: number
  close: number
  volume: number
}

Examples

ExampleDescription
Live demoFeature Lab: drawing tools, indicators, trading, ranges, paged history, replay, 16 languages with sub-cent prices, watchlists with live quotes, 200k bars, slow-network switching — each on a live chart. The site and docs are also in Vietnamese, Chinese, Japanese, Korean and Spanish
StackBlitz sandboxesOne-click, forkable: vanilla Chart, ChartWidget, React / Vue / Svelte wrappers, finance charts
@tradecanvas/react · /vue · /svelteFramework components — reactive props, typed, zero boilerplate

AI coding tools

  • llms.txt and llms-full.txt give assistants the docs in one place.
  • An agent skill, skills/tradecanvas, teaches coding agents to build with TradeCanvas: the entry points, the rules that avoid most bugs, and worked examples that CI type-checks against the library. Copy the folder into your project's .claude/skills/ (or your agent's skills folder) to use it.

Browser Support

Chrome 80+, Firefox 80+, Safari 14+, Edge 80+

Framework Integration

Official wrapper components — reactive props, refs, zero boilerplate. Published at 1.x alongside the core:

npm install @tradecanvas/react    # or @tradecanvas/vue · @tradecanvas/svelte
import { TradeCanvas } from '@tradecanvas/react'

<TradeCanvas symbol="BTCUSDT" timeframe="5m" theme="dark" indicators={['rsi', 'macd']} />

All three share the same prop surface and hand you the underlying Chart (for drawings, trading, execution, plugins) via onReady / ref / bind:chart. See the frameworks docs.

Headless (own the lifecycle)

The Chart class also takes a DOM element directly — framework-agnostic:

React:

import { useEffect, useRef } from 'react'
import { Chart, BinanceAdapter } from '@tradecanvas/chart'

function TradingChart() {
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const chart = new Chart(ref.current!, {
      theme: 'dark',
      features: { indicators: true, drawings: true },
    })
    chart.connect({
      adapter: new BinanceAdapter(),
      symbol: 'BTCUSDT',
      timeframe: '5m',
    })
    return () => chart.destroy()
  }, [])

  return <div ref={ref} style={{ width: '100%', height: 500 }} />
}

Svelte:

<script lang="ts">
  import { onMount, onDestroy } from 'svelte'
  import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'
  import type { TimeFrame } from '@tradecanvas/chart'

  interface Props { symbol?: string; timeframe?: TimeFrame }
  let { symbol = 'BTCUSDT', timeframe = '5m' }: Props = $props()

  let container: HTMLDivElement
  let chart: Chart | null = null

  onMount(() => {
    chart = new Chart(container, {
      chartType: 'candlestick',
      theme: DARK_THEME,
      autoScale: true,
      features: { indicators: true, drawings: true, volume: true },
    })
    chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
  })

  onDestroy(() => chart?.destroy())

  $effect(() => {
    if (!chart) return
    chart.disconnectStream()
    chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
  })
</script>

<div bind:this={container} style="width: 100%; height: 600px" />

Vue:

<template>
  <div ref="chartContainer" style="width: 100%; height: 600px" />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'

const chartContainer = ref<HTMLDivElement>()
let chart: Chart | null = null

onMounted(() => {
  if (!chartContainer.value) return
  chart = new Chart(chartContainer.value, {
    chartType: 'candlestick',
    theme: DARK_THEME,
    autoScale: true,
    features: { indicators: true, drawings: true, volume: true },
  })
  chart.connect({ adapter: new BinanceAdapter(), symbol: 'BTCUSDT', timeframe: '5m' })
})

onUnmounted(() => chart?.destroy())
</script>

Performance

A two-canvas Canvas2D pipeline: a hover repaints only the thin top canvas, never the scene. Five things keep large data fast:

  • LTTB downsampling — line / area charts automatically downsample the visible range to ~2 points per pixel using Largest-Triangle-Three-Buckets when there are far more bars than pixels. The line stays visually identical while drawing dozens of times fewer points; a no-op at normal zoom. The lttbDownsample utility is exported for your own use.
  • Indicators zoomed out — below a pixel per bar, indicator lines, bands and histograms draw one span per pixel column instead of a stroke through thousands of points: much the same look for a fraction of the raster work (about 54 → 21 ms a frame zoomed out on 200,000 bars with four indicators, on integrated graphics). node scripts/bench-render.mjs measures it on your machine.
  • Visible-range rendering — every renderer iterates only the bars in view, never the whole series. Hover and pan frame cost stays flat from 500 to 100,000 loaded bars.
  • Incremental indicators on live ticks — a tick only changes the forming bar, so built-in indicators that implement update() (SMA, EMA, WMA, VWMA, Bollinger, Envelope, RSI, MACD, ATR, OBV, Stochastic) recompute just that bar instead of the whole history. Others fall back to a full recalculation. Custom plugins can opt in via IndicatorPlugin.update.
  • Cheap full loads — a symbol/timeframe switch recomputes every indicator once. Their per-bar values lookup is an IndicatorValueMap (array-backed while bars arrive in time order, ~3x cheaper to build than a Map keyed by timestamps), and setData reuses already well-formed bars instead of copying each one.

BB + EMA + RSI + MACD (pnpm bench, single core):

HistoryFull recalculation (switch / setData)Incremental update() (live tick)
20,000 bars~5 ms~0.0005 ms
100,000 bars~27 ms~0.001 ms

Downsampling throughput (pnpm bench, single core):

Visible points → 1600Time / frameThroughput
10,000~0.025 ms39,600 / s
100,000~0.32 ms3,100 / s
1,000,000~2.6 ms380 / s

A 100k-bar line chart downsamples in ~0.3 ms — well inside a 16.6 ms frame budget — then draws ~62× fewer points (100k → 1600).

WebGL renderer (preview)

renderer: 'webgl' draws the plot and indicator panes with WebGL 2, on a canvas under the 2D scene: the grid, sessions, candles and volume directly, and the Canvas 2D drawing of indicators, compare lines and most chart types recorded as GPU strokes, fills and rectangles, antialiased at their edges as Canvas 2D is. Text, drawings, orders, axes and the crosshair stay on Canvas 2D, as does anything the GPU wouldn't draw the same (left to Canvas 2D in order, so the stacking doesn't change); custom indicator plugins come along without changes. Candles match Canvas 2D to within 2/255; lines and fills differ only on a few antialiased edge pixels. The WebGL code is a chunk of its own (about 17 KB gzipped), loaded on first use; where WebGL 2 is missing, or its context is lost, the chart carries on with Canvas 2D.

const chart = new Chart(el, { renderer: 'webgl' })   // or 'auto': WebGL on a hardware GPU only

chart.on('rendererChange', (e) => console.log(e.payload))
// { renderer: 'webgl' }, or { renderer: 'canvas', reason: 'unsupported' | 'contextLost' }

await chart.setRenderer('canvas')   // resolves to what draws now

Frame time while panning, on integrated graphics (Intel UHD; 16.7 ms is 60 fps):

ChartPixel ratioCanvas 2DWebGL
1600×900, 500 candles + 4 indicators227.4 ms19.6 ms
1600×900, zoomed out on 200,000 bars + 4 indicators234.5 ms20.2 ms
Six charts, 500 candles and two indicators each223.5 ms17.2 ms
2560×1400, 2,000 candles + 4 indicators141.6 ms17.7 ms
2560×1400, 2,000 candles + 4 indicators1.570.8 ms17.6 ms
2560×1400, 2,000 candles + 4 indicators2114.5 ms29.1 ms
2560×1400, 2,000 candles233.1 ms20.9 ms

Most WebGL frames above land on 16.7 ms; the averages carry a few longer ones. At a pixel ratio of 2 on a 2560×1400 chart, the browser's compositing of the full-size layers alone takes about 23 ms on this GPU. node scripts/bench-render.mjs --renderer=webgl runs these numbers on your own machine.

Architecture

Two stacked canvases — a hover repaints only the thin top one:

  Top canvas    (crosshair + axis pills, legend, countdown, measure)   z=1
  Scene canvas  (grid, candles, indicators, drawings, orders, axes)    z=0
  • bo-grid — tiny, fast Svelte 5 data grid for fintech UIs: canvas sparklines, batched realtime cell updates, virtual scrolling, grouping / pivot / tree data, and Excel export, with a core that gzips to ~32 KB. The table half of the same toolkit — pair it with TradeCanvas for a full trading desk. Live demo

Contributing

Bug reports, ideas and pull requests are welcome. CONTRIBUTING.md covers the setup (pnpm install && pnpm build && pnpm test), how the repo is laid out and what a pull request needs. Found a security issue? Please follow SECURITY.md instead of opening an issue.

If TradeCanvas saves you time, a star on GitHub helps other developers find it.

License

MIT

backtesting
candlestick-chart
canvas
chart
charting-library
crypto
drawing-tools
financial-charts
fintech
indicators
javascript
react
stock-chart
svelte
technical-analysis
trading
trading-chart
typescript
vue
websocket

bonguynvan/tradecanvas

Canvas trading charts for the web: 95 indicators, 69 drawing tools, live exchange feeds, orders on the chart and multi-chart workspaces. Zero dependencies. TypeScript, React, Vue, Svelte.

TypeScript

19

337 commits

updated Oct 4, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built a crypto backtester that tells you when your strategy is just luck (r/SideProject)

**What it is:** tradingdek.com: build a trading strategy from rules (no code), backtest it on years of Binance data with fees included, then run it as a paper bot or follow its signals. **The part I'm proud of:** every strategy in the library carries an honest badge from a monthly audit: 20 pairs,…

1

Oct 4, 2026

README

TradeCanvas, the chart engine for trading apps

npm version npm downloads CI status No third-party dependencies TypeScript MIT license GitHub stars

Live demo · Docs · Examples · Playground · Changelog

English · Tiếng Việt · 简体中文 · 日本語 · 한국어 · Español

A complete trading chart for the web. Candlesticks to Renko, 95 indicators, 69 drawing tools, live exchange feeds and orders on the chart, drawn on Canvas2D with zero dependencies. Drop in the full ChartWidget, or build your own UI on the headless Chart, in plain TypeScript, React, Vue or Svelte.

ChartWidget with live BTCUSDT from Binance: EMA 21 and 55, RSI, a trend line, a long position and a watchlist with live quotes

Quick Start

npm install @tradecanvas/chart     # or: pnpm add / yarn add

ChartWidget is the whole trading UI in one component: toolbar, drawing sidebar, settings dialog and status bar.

import { ChartWidget } from '@tradecanvas/chart/widget'
import { BinanceAdapter } from '@tradecanvas/chart'

const widget = new ChartWidget(document.getElementById('chart')!, {
  symbol: 'BTCUSDT',
  timeframe: '5m',
  theme: 'dark',
  adapter: new BinanceAdapter(), // live data, no API key
  trading: true,
})

That's it. Live data, all 95 indicators, all 69 drawing tools, command palette (Ctrl+K), symbol search (Ctrl+P), hotkey sheet (?), shift-drag measure, alt-click tooltip pin, and drag-drop CSV/JSON loading.

Using a framework? @tradecanvas/react, @tradecanvas/vue and @tradecanvas/svelte wrap the headless Chart as a component, and the widget above mounts the same way in any framework; see Framework Integration. Or fork a StackBlitz sandbox and start from there.

A Look Around

Drawing tools: a Fibonacci retracement with a note, a trend line, an Elliott impulse wave and a long position
69 drawing tools
Fibonacci, Gann, pitchforks, Elliott waves, harmonic patterns, notes and brushes, and a Long/Short position that sizes the trade. Alerts on trend lines, groups, undo and redo.
Trading on the chart: a long position with stop-loss and take-profit, a buy stop and a sell limit, and the account panel
Trading on the chart
Positions with live P&L, orders you drag to a new price, SL and TP, reverse and close buttons, an order ticket and an account panel. A paper broker is built in; connect your own.
A two-by-two workspace of BTC, ETH, SOL and BNB charts on mixed intervals
Multi-chart workspace
Up to six full charts side by side, linked by symbol, interval, crosshair, time or drawings, and saved as one layout.
One chart in three looks: studio, terminal, and capsule on the light theme
Your own look
Three presets (studio, terminal, capsule) or your own corner radius, density, fonts, toolbar and price tags, on light and dark themes.

Why TradeCanvas?

Most chart libraries make you choose: pretty charts with no trading features, or trading features with an ugly API. TradeCanvas gives you both.

  • 95 built-in indicators — SMA, EMA, TEMA, VWMA, Hull MA, RSI, MACD, Bollinger, Envelope, Ichimoku, Pivot Points, Anchored VWAP, ZigZag, Linear Regression Channel, Awesome / Chaikin Oscillator, and more. Any indicator can read another one's line (an SMA of RSI). No separate calculation library needed.
  • 69 drawing tools — Trendlines (info line, trend angle, cross line), Fibonacci (retracement, extension, channel, time zones, speed resistance fan and arcs, circles, spiral, wedge), horizontal/vertical lines, channels, pitchforks and pitchfan, Gann fan / box / square, cycles, harmonic patterns (XABCD, cypher, ABCD, three drives, head and shoulders), Elliott waves, notes, callouts and marks, brush and path, forecast and projection, Long/Short Position with position sizing, Volume Profile range. Each with its own settings, alerts on trend lines, groups and layers, undo/redo and full serialization.
  • 18 chart types — Candlestick, line, area, bar, hollow candle, baseline, High-Low, Heikin-Ashi, Renko, Kagi, Line Break, Point & Figure, Range Bars, Volume Candles, Equivolume, HLC Area, Step Line, Line+Markers. Renko's box, Kagi's reversal and the like are yours to set.
  • Pro-grade interaction — pan freely past the last bar into empty future space (drawings can go there too), drag the price/time axes to scale, double-click to auto-fit, Ctrl/⌘+drag to select several drawings (then move, restyle or delete them together), Shift+drag to measure (bars × price Δ × %), Alt+click to pin a comparison tooltip, context cursors (crosshair, grabbing hand, resize arrows), axis-following price/time pill labels under the cursor, bar-hover highlight.
  • Trading overlay — Render open positions with entry line, P&L zone, and SL/TP markers. Orders as dashed lines. Drag SL/TP to modify, cancel / close / reverse from the buttons on each line, and see every fill marked on its bar. ChartWidget adds an order ticket that checks the order as you fill it in, and an account panel with positions, working orders and history. Cleanly opt-out via features.trading: false for non-trading projects.
  • Real-time streaming — Built-in Binance, Coinbase, Bybit, and Kraken adapters, plus generic WebSocketAdapter / PollingAdapter bases so any feed plugs in with ~20 lines. Older bars load as you scroll back, any interval (7m, 90m, 2d) is built from the feed's own, tick charts (100T) from its trades, and symbol search and quotes come from the feed.
  • Time zones — any IANA zone with daylight saving time ('America/New_York'), a fixed offset, or the exchange's own zone, for the axis, crosshair, day breaks and session hours.
  • 16 languages — ChartWidget in English, Vietnamese, Simplified and Traditional Chinese, Japanese, Korean, Spanish, Portuguese, French, German, Russian, Turkish, Indonesian, Thai, Arabic and Hebrew; Arabic and Hebrew mirror right to left.
  • Accessible — keyboard navigation, zoom and scroll buttons over the chart, and a screen-reader summary that reads the view and the bars one at a time.
  • Live execution — connect an ExecutionAdapter to turn the trading overlay into a real trading surface, drag on the chart to create orders, and reconcile fills. Ships a PaperExecutionAdapter sandbox.
  • Plugin SDK — register custom indicators, drawing tools, chart types, and overlays — globally or per-chart.
  • Strategy backtester — @tradecanvas/analytics ships a bar-by-bar Backtester with virtual fills, commission/slippage models, portfolio tracking, and risk metrics (Sharpe, Sortino, Calmar, max drawdown). Now with 4 ready-to-use reference strategies + Monte Carlo path-dependence analysis.
  • Replay mode — replay the chart's own bars from any point, in finer steps if you like (an hourly chart forming from 5-minute bars), with play / pause / step / seek / speed, and paper-trade on the replayed prices. The widget has a replay bar for it; ReplayController drives bars headless too.
  • Alerts — on a price level, an indicator line, a drawing, or one line crossing another; on a move of some percent within some bars; only on closed bars; with an expiry. The widget's alerts panel sets all of them.
  • Compare and spread — other symbols in percent on the price scale, on a scale or pane of their own, or as a spread or ratio, lined up with the chart by time.
  • Price formats — prices in your own format or in fractions of a point (a bond in 32nds: 110'165) on every label; times your way; extended hours on or off; data export with the indicator lines.
  • Volume Profile — optional horizontal histogram of traded volume bucketed by price over the visible range, with point-of-control highlighting.
  • Watchlists and symbol info — lists of symbols to switch between, edit and reorder, with live quotes; a symbol panel with price, the market's status, the day's numbers, hours and news.
  • CSV / JSON drag-and-drop — drop a file onto the chart, it parses and loads instantly. Detects header layouts, ISO/unix-s/unix-ms timestamps, and array-vs-object JSON shapes.
  • Named layouts — save the chart under a name (symbol, interval, scale, indicators, drawings, alerts), open, rename, delete, auto-save the open one, Ctrl/⌘+S. Kept in the browser, or on your server through a four-call LayoutStorage. Per-symbol auto-persistence (persistLayouts) is there too.
  • Multi-chart — ChartWidgetGrid puts up to six full widgets side by side, linked by symbol, interval, crosshair, time or drawings as you choose, and saves them as one layout. ChartGrid does the same for bare charts.
  • Signal markers & trade zones — render bot/algorithm output (directional arrows, entry→exit rectangles) as a first-class chart layer.
  • Hotkey sheet — press ? in the widget to open a categorized keyboard-shortcut reference.
  • Extensible widget — add your own toolbar buttons and right-click menu entries (addToolbarButton, chartMenuItems).
  • Save/load chart state — Persist drawings, indicators, theme, and chart type to JSON. Restore with one call.
  • Zero dependencies — The entire library is self-contained. No d3, no chart.js, no fancy-canvas.

Headless Chart

For projects that want to own the surrounding UI (custom toolbar, framework-specific controls), use the lower-level Chart class directly:

import { Chart, BinanceAdapter } from '@tradecanvas/chart'

const chart = new Chart(document.getElementById('chart')!, {
  theme: 'dark',
  autoScale: true,
  features: {
    drawings: true,
    indicators: true,
    trading: true,           // set false to disable orders/positions entirely
    tradingContextMenu: true, // opt-in right-click order menu (off by default)
    volume: true,
  },
})

const adapter = new BinanceAdapter()
chart.connect({ adapter, symbol: 'BTCUSDT', timeframe: '5m', historyLimit: 300 })

Widget Options

OptionTypeDefaultDescription
symbolstring'BTCUSDT'Initial trading symbol
timeframeTimeFrame'5m'Initial timeframe
theme'dark' | 'light' | Theme'dark'Chart theme
adapterDataAdapter—Data source adapter
toolbarbooleantrueShow top toolbar
drawingToolsbooleantrueShow left drawing sidebar
settingsbooleantrueShow settings button
tradingbooleantrueEnable trading overlay
statusBarbooleantrueShow bottom status bar
rangeBarbooleantrueRange presets (1D … All) and go to date (Alt+G) on the status bar
indicatorLegendbooleantrueIndicators listed on the chart (under the OHLCV legend and atop their panes) with show / settings / remove
fullscreenbooleantrueFullscreen button in the toolbar
symbolsstring[]BTC/ETH/SOL/BNBSearchable symbol catalog
timeframesTimeFrame[]1m to 1MTimeframes on offer; pin favourites from the ▾ menu
chartTypesChartType[]18 typesAvailable chart types
watchlistbooleanfalseRight-side watchlist sidebar
dragDropImportbooleantrueDrop CSV / JSON files onto the chart to load data
persistLayoutsboolean | { keyPrefix, debounceMs }falseSave per-symbol indicators / drawings / chart type to localStorage
onSymbolChange(symbol) => void—Symbol change callback
onTimeframeChange(tf) => void—Timeframe change callback
onReady(chart) => void—Fired when chart is ready
localestring'en'UI language — 'en' and 'vi' built in, 12 more from the locales entry, see Widget i18n below
messagesPartial<Record<MessageKey, string>>—Override or add individual UI strings on top of locale

Icons

The widget's icon set is exported for your own UI: createIcon(name), createToolIcon(drawingTool), createChartTypeIcon(chartType) return inline SVG strings drawn in currentColor (24 px grid, 1.75 px strokes).

import { createToolIcon } from '@tradecanvas/chart/widget'
button.innerHTML = createToolIcon('fibRetracement', 16)

Widget i18n

ChartWidget speaks 16 languages: English, Vietnamese, Simplified and Traditional Chinese, Japanese, Korean, Spanish, Portuguese, French, German, Russian, Turkish, Indonesian, Thai, Arabic and Hebrew (the last two right to left; dir sets the direction yourself). Every string it shows is translated: toolbar, settings, drawing tools, alerts, dialogs, the command palette, the hotkey sheet and notices. Indicator names (SMA, RSI…) stay as they are. Set at construction.

English and Vietnamese are built in. The others load from @tradecanvas/chart/widget/locales, so a page ships only the languages it imports:

import { ja } from '@tradecanvas/chart/widget/locales'

new ChartWidget(el, {
  locale: 'ja',
  messages: ja,                               // or registerWidgetLocales() for all of them
  chartOptions: { numberLocale: 'ja-JP' },    // separate: number/date formatting (see below)
});

messages also overrides single keys on top of locale ({ 'watchlist.title': 'Theo dõi' }). A locale with a region falls back to its language (ja-JP → ja; zh-TW → Traditional Chinese).

locale/messages cover the text; chartOptions.numberLocale controls number and date formatting (price axis, legend, watchlist prices, current-price tag, session-break dates) via Intl.

See packages/library/src/widget/locales/en.ts for the full key list (MessageKey).

Widget vs Headless

Chart (headless)ChartWidget
Import@tradecanvas/chart@tradecanvas/chart/widget
UI includedNone — build your ownComplete toolbar, sidebar, settings
Bundle impact~50 KB gzip~65 KB gzip (includes UI)
FrameworkAny (React, Vue, Svelte, vanilla)Vanilla JS DOM (works everywhere)
CustomizationFull controlToggle sections on/off
Advanced accessDirect APIwidget.getChart() for direct API

Widget Look

The widget's shapes and sizes — corners, control heights, type, borders, shadows, how a chosen button shows, bars docked or floating — are a look of their own, apart from the colours. Three presets: Studio (the default), Terminal (dense and square) and Capsule (pills, floating bars). Start from one and change what you like:

const widget = new ChartWidget(host, { ui: 'terminal' });
widget.setUI({ preset: 'studio', radius: { md: 10 }, density: 'compact', toolbar: 'floating', active: 'solid' });

The chart's price tags take the same corners (tagRadius; on a bare Chart, chart.setShapes({ tagRadius })). The widget loads no fonts: load the ones a look names. See Styling.

Widget Theming

ChartWidget's own chrome (toolbar, sidebars, settings panel, watchlist — everything outside the canvas) is styled entirely through CSS custom properties on .tcw-root, the widget's own root element. These are a stable, documented contract: additive-only across minor/patch releases — a property is never renamed or removed without a major version bump. Override them from the host page; no build step or theme object needed.

/* Dark is the default (no attribute needed); light sets data-tcw-theme="light" */
.my-app .tcw-root:not([data-tcw-theme="light"]) {
  --tcw-bg: #0a0a0f;
  --tcw-accent: #7c5cff;
  --tcw-radius: 0px;
  --tcw-radius-lg: 0px;
}
VariableDefault (dark)Purpose
--tcw-bg#080b10Root background
--tcw-bg-surface#0c1016Panel / toolbar surface
--tcw-bg-elevated#141922Popovers, dropdowns, modals
--tcw-bg-overlayrgba(20,25,34,.5)Backdrop behind overlays
--tcw-border#1f2630Default border
--tcw-border-strong#2a323eEmphasized border (focus rings, dividers)
--tcw-text#e7e9eePrimary text
--tcw-text-dim#aab1bdSecondary text
--tcw-text-muted#758091Tertiary / placeholder text
--tcw-accent#f2a93bPrimary accent (active tab, focus, links)
--tcw-accent-ink#1a1204Text and icons on an accent fill
--tcw-accent-hover#f5b95cAccent hover state
--tcw-accent-softrgba(242,169,59,.14)Accent tint (selected row background)
--tcw-accent-glowrgba(242,169,59,.22)Accent glow (focus halo)
--tcw-accent-linergba(242,169,59,.55)Accent border/underline
--tcw-red / --tcw-red-soft#e8505b / tintDown/sell/negative
--tcw-green / --tcw-green-soft#1fa874 / tintUp/buy/positive
--tcw-amber#ff9f43Warning
--tcw-hover-bgrgba(255,255,255,.05)Row/button hover background
--tcw-active-bgrgba(255,255,255,.08)Row/button pressed background
--tcw-dividerrgba(255,255,255,.06)Hairline dividers
--tcw-ease / --tcw-ease-outcubic-bezierTransition easing
--tcw-dur-fast / -normal / -slow120ms / 180ms / 260msTransition durations
--tcw-radius-xs / -sm / --tcw-radius / -lg / -xl3px / 5px / 7px / 11px / 16pxThe corner scale — set to 0 for a square look
--tcw-control-radius / --tcw-input-radius / --tcw-menu-radius / --tcw-dialog-radius / --tcw-panel-radius / --tcw-tooltip-radius / --tcw-tag-radius / --tcw-toast-radiusfrom the scaleEach kind of part's corners
--tcw-toolbar-h / --tcw-control-h / --tcw-control-h-sm / --tcw-icon / --tcw-sidebar-w / --tcw-menu-item-h46px / 30px / 24px / 18px / 48px / 30pxSizes
--tcw-font / --tcw-font-size / --tcw-weight / --tcw-weight-strong'Manrope', 'Inter', … / 13px / 500 / 600Type
--tcw-label-case / --tcw-label-trackingnone / 0emSmall labels (section titles)
--tcw-border-w / --tcw-sep-w1px / 0pxBorder width; rules between toolbar groups
--tcw-menu-shadow / --tcw-dialog-shadow / --tcw-tooltip-shadowthe elevation shadowsShadows of menus, dialogs, tooltips
--tcw-blur / --tcw-surface-opacity0px / 100%Frosted menus
--tcw-shadow-sm / -md / -lg / -xlbox-shadow valuesElevation
--tcw-ring0 0 0 2px rgba(242,169,59,.45)Focus ring
--tcw-font-mono'JetBrains Mono', …Monospace font stack (price ladder, code)

Light theme ([data-tcw-theme="light"]) redefines the color group (--tcw-bg*, --tcw-border*, --tcw-text*, --tcw-accent*, --tcw-hover-bg, --tcw-active-bg, --tcw-divider, --tcw-shadow*) with its own defaults — override both selectors if you support both themes. With the ui option set, the widget writes its look's variables on the element, so they win over your CSS; without it they stay yours to set.

Features

Chart Types

TypeDescription
CandlestickStandard OHLC candles
Hollow CandleOpen/close determines fill
Bar (OHLC)Classic open-high-low-close bars
LineClose price line
AreaFilled area below close
BaselineTwo-tone area split at a reference price
Heikin-AshiSmoothed candles for trend identification
RenkoFixed-size bricks that ignore time
KagiReversal-based line chart
Point & FigureX/O columns for supply/demand analysis
Line BreakThree-line break charts
Range BarsFixed price-range bars — each bar's high − low equals a configured range
Volume CandlesCandlesticks with width proportional to volume
EquivolumeFull-range boxes with width proportional to volume share (Richard Arms style)
HLC AreaHigh-low-close area band with close line
Step LineStaircase/step pattern from close prices
Line with MarkersClose line with circular markers at each data point

Multi-Chart Grid

Display multiple synchronized charts side-by-side with linked crosshairs and time axis:

import { ChartGrid, BinanceAdapter } from '@tradecanvas/chart'

const grid = new ChartGrid(document.getElementById('grid')!, {
  layout: '2x2',
  syncCrosshair: true,
  syncTimeAxis: true,
})

// An adapter keeps one stream: give each chart its own
grid.connectAll(() => new BinanceAdapter(), ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'], '5m')

With the full widget on each chart, a bar to pick the arrangement and sync, and the whole grid saved as a named layout:

import { ChartWidgetGrid } from '@tradecanvas/chart/widget'

const workspace = new ChartWidgetGrid(document.getElementById('grid')!, {
  layout: '1x2',
  adapter: () => new BinanceAdapter(),
  cells: [{ symbol: 'BTCUSDT' }, { symbol: 'ETHUSDT', timeframe: '1h' }],
  sync: { crosshair: true, interval: false, symbol: false, time: false, drawings: false },
})
workspace.setSync({ time: true })

Supported layouts: '1x1', '1x2', '2x1', '2x2', '1x3', '3x1', '2x3', '3x2'.

Command Palette

Press Ctrl+K (or Cmd+K) inside ChartWidget to open a searchable command palette. Quickly find and toggle indicators, change chart types, activate drawing tools, switch timeframes, or trigger actions (screenshot, theme toggle, settings).

Finance Charts

ChartDescription
SparklineChartTiny inline line/area chart from a number array — for dashboards and KPI cards
DepthChartBid/ask order book visualization with cumulative volume areas
EquityCurveChartPortfolio equity line with drawdown shading and benchmark comparison
HeatmapChartColored cell grid with treemap layout — for sector/market performance
WaterfallChartRunning cumulative bars — P&L attribution, revenue bridge, cash flow
GaugeChartSpeedometer-style gauge — KPIs, risk scores, Fear & Greed index
import {
  SparklineChart, DepthChart, EquityCurveChart, HeatmapChart,
  WaterfallChart, GaugeChart,
} from '@tradecanvas/chart'

// Sparkline in a 120x48 container
new SparklineChart(el, { data: [100, 102, 98, 105, 103], mode: 'area', color: '#1fa874' })

// Equity curve with drawdown
new EquityCurveChart(el, { data: equityPoints, drawdown: true, benchmark: spyData })

// Order book depth
new DepthChart(el, { data: { bids, asks }, crosshair: true })

// Market heatmap (treemap weighted by market cap)
new HeatmapChart(el, { data: cells, weighted: true })

// P&L waterfall
new WaterfallChart(el, {
  data: [
    { label: 'Start', value: 10000, type: 'total' },
    { label: 'Gain', value: 1850 },
    { label: 'Loss', value: -620 },
    { label: 'End', value: 11230, type: 'total' },
  ],
})

// Fear & Greed gauge: zones light up to the value, the label shows the current zone
const gauge = new GaugeChart(el, {
  value: 72,
  label: 'Fear & Greed',
  zones: [
    { from: 0, to: 25, color: '#e8505b', label: 'Extreme fear' },
    { from: 25, to: 45, color: '#f2a93b', label: 'Fear' },
    { from: 45, to: 55, color: '#8a93a3', label: 'Neutral' },
    { from: 55, to: 75, color: '#62c895', label: 'Greed' },
    { from: 75, to: 100, color: '#1fa874', label: 'Extreme greed' },
  ],
  // pointer: 'needle',  // classic needle instead of the ring marker
})
gauge.setValue(85) // animates smoothly

Indicators (built-in)

95 indicators — moving averages (SMA, EMA, WMA, Hull, DEMA, TEMA, ALMA, KAMA, LSMA, McGinley, SMMA, MA Cross, MTF MA), bands and channels (Bollinger, Keltner, Donchian, Envelope, Linear Regression), trend and stops (Ichimoku, Supertrend, Parabolic SAR, Chandelier, Chande Kroll Stop, Alligator, ZigZag, Fractals, Pivot Points), VWAPs and Volume Profile on the price pane; RSI, MACD, Stochastic, ATR, ADX, CCI, OBV, MFI, Bollinger %B and BandWidth, Historical Volatility, Ulcer Index and 40 more oscillators, volume and volatility indicators in panes. The indicator catalog lists every id with its inputs, lines and levels.

import { indicatorSource } from '@tradecanvas/chart'

const rsi = chart.addIndicator('rsi', { period: 14 })!
chart.addIndicator('ema', { period: 21, source: 'hlc3' })                     // another price
chart.addIndicator('sma', { period: 9, source: indicatorSource(rsi, 'value') }) // RSI's own average, in RSI's pane
chart.setIndicatorLevels(rsi, [20, 50, 80])
  • Sources: close, open, high, low, hl2, hlc3, ohlc4, hlcc4, or another indicator's line.
  • Panes: one value scale per pane for lines, levels, axis and crosshair; move an indicator into another pane, a new one or back to the price pane; fold, maximise and reorder panes (moveIndicatorToPane, setPaneCollapsed, setMaximizedPane, movePane).
  • Undo and templates: Ctrl/Cmd+Z undoes indicator changes, in the same history as drawings; ChartWidget saves the indicators as named templates (getIndicatorSetup / applyIndicatorSetup).
  • Levels: editable per instance (RSI 30/70, CCI ±100 …), kept in saved layouts.
  • Value tags: each line's latest value on its axis, in its colour.
  • Custom indicators declare their lines (plots), scale, levels and inputs; the chart draws and labels them.

Invalid parameters (NaN, Infinity, non-numeric strings, missing keys) fall back to the defaults instead of reaching the calculations.

Drawing Tools

Trendline, Horizontal Line, Vertical Line, Ray, Extended Line, Parallel Channel, Fibonacci Retracement, Fibonacci Extension, Fibonacci Time Zones, Rectangle, Ellipse, Triangle, Arrow, Pitchfork, Gann Fan, Gann Box, Elliott Wave, Regression Channel, Date Range, Price Range, Measure, Anchored VWAP, Volume Profile Range, Text Annotation

All drawing tools support:

  • Click-to-place with magnet snapping to OHLC values
  • Undo / redo (Ctrl+Z / Ctrl+Y)
  • Serialization for save/load
  • Custom styles (color, width, dash pattern)

Trading Overlay

Render open positions and pending orders directly on the chart, like MT4/MT5.

import type { TradingPosition, TradingOrder } from '@tradecanvas/chart'

chart.setPositions([{
  id: 'pos-1',
  side: 'buy',
  entryPrice: 3500,
  quantity: 1.5,
  closedQuantity: 0.5,   // partial close — visualized as a left-edge dim band
  stopLoss: 3400,
  takeProfit: 3700,
}])

chart.setOrders([{
  id: 'order-1',
  side: 'sell',
  type: 'limit',
  price: 3800,
  quantity: 0.5,
  label: 'TP',
  draggable: true,
}])

// Customize the position zone color via P&L thresholds
chart.setTradingConfig({
  pnlThresholds: [
    { pnl: -Infinity, color: '#b91c1c' },
    { pnl: 0,         color: '#94a3b8' },
    { pnl: 50,        color: '#16a34a' },
    { pnl: 200,       color: '#15803d' },
  ],
  // Custom label template — tokens: {side} {qty} {openQty} {closedQty} {entry} {price} {pnl} {pnlPct} {pnlSign}
  positionLabel: '{side} {openQty}/{qty} @ {entry} | {pnlSign}{pnl} ({pnlPct})',
})

// Listen for user drag-to-modify
chart.on('positionModify', (e) => console.log('SL/TP moved:', e.payload))
chart.on('orderModify', (e) => console.log('Order moved:', e.payload))

// The × and ⇅ buttons on the lines raise these; so can your own UI
chart.cancelOrderIntent('order-1')
chart.reversePositionIntent('pos-1')
chart.on('executionFill', (e) => console.log(e.payload.reason, e.payload.pnl))

Signal Markers

Visualize buy/sell signals from bots, indicators, or manual analysis.

chart.addSignalMarker({
  time: 1715692800000,
  price: 62500,
  direction: 'long',
  confidence: 0.85,
  source: 'ema-crossover',
  label: 'EMA Cross',
})

// Color-code by source
chart.setSignalMarkerStyle({
  sourceColors: {
    'ema-crossover': '#4c8dff',
    'rsi-divergence': '#f2a93b',
    'whale-flow': '#9C27B0',
  },
})

Trade Zones

Render entry→exit rectangles with P&L coloring for executed trades.

const zoneId = chart.addTradeZone({
  entryTime: 1715692800000,
  entryPrice: 62500,
  exitTime: 1715700000000,
  exitPrice: 63200,
  direction: 'long',
  pnl: 140,
  pnlPercent: 1.12,
})

// Update a live trade when it closes
chart.updateTradeZone(zoneId, {
  exitTime: Date.now(),
  exitPrice: 63500,
  pnl: 200,
})

Real-Time Streaming

// Built-in Binance adapter (free, no API key)
chart.connect({
  adapter: new BinanceAdapter(),
  symbol: 'ETHUSDT',
  timeframe: '1m',
  historyLimit: 500,
})

// Or manual data feed
chart.setData(historicalBars)
chart.appendBar(newBar)
chart.updateLastBar(updatedBar)
chart.setCurrentPrice(3500.42)

Built-in adapters (all free, no API key): BinanceAdapter, CoinbaseAdapter, BybitAdapter, KrakenAdapter, plus MockAdapter for offline/testing.

import { BybitAdapter, KrakenAdapter, CoinbaseAdapter } from '@tradecanvas/chart'

chart.connect({ adapter: new BybitAdapter(),    symbol: 'BTCUSDT', timeframe: '1m' })
chart.connect({ adapter: new KrakenAdapter(),   symbol: 'BTC/USD', timeframe: '5m' })
chart.connect({ adapter: new CoinbaseAdapter(), symbol: 'BTC-USD', timeframe: '15m' })

Any feed in ~20 lines. Extend WebSocketAdapter (live + REST history) or PollingAdapter (REST-only feeds) — the base handles the connection lifecycle, reconnect, decoding, and event emission. You supply a URL and a parse function:

import { WebSocketAdapter } from '@tradecanvas/chart'

const myAdapter = new WebSocketAdapter({
  name: 'myexchange',
  wsUrl: (c) => `wss://api.myexchange.com/ws/${c.symbol}@kline_${c.timeframe}`,
  fetchHistory: (symbol, tf, limit) => fetch(`/candles?...`).then((r) => r.json()),
  parseMessage: (raw) => ({ bar: toBar(raw), closed: raw.k.x }),
})

Live Execution

Connect an ExecutionAdapter to turn the display-only trading overlay into a real trading surface. The chart routes its order/position intents into the adapter, and renders the authoritative orders / positions the adapter emits back — the adapter is the single source of truth. With no adapter connected, those intents stay plain events (backward-compatible).

import { PaperExecutionAdapter } from '@tradecanvas/chart'

chart.connectExecution(new PaperExecutionAdapter({ markPrice: 64000 }))

// Drag-to-create an order, then confirm:
chart.startOrderDraft('buy')   // draggable line at the latest close
chart.confirmOrderDraft()      // emits orderPlace → adapter fills → chart renders the position
// chart.cancelOrderDraft()

// One channel for failures (adapter-reported or a failed command):
chart.on('executionError', (e) => toast(e.payload.message))

Implement ExecutionAdapter (it mirrors DataAdapter) to wire a real broker / OMS: placeOrder, modifyOrder, cancelOrder, modifyPosition, closePosition, plus orders / positions / fill / error events. PaperExecutionAdapter is a virtual-fill sandbox for demos and tests. The order type of a drag-to-create order (limit vs stop) is inferred from where you drop the line relative to the current price.

Plugins — extend the chart

Register custom indicators, drawing tools, chart types, and overlays — globally (every chart created afterward inherits) or per-chart.

import { Chart, registerPlugin, IndicatorBase } from '@tradecanvas/chart'

class MyIndicator extends IndicatorBase { /* descriptor, calculate(), render() */ }

// 1) Global — available to every chart created afterward:
registerPlugin({ kind: 'indicator', plugin: new MyIndicator() })

// 2) Per-chart at construction:
const chart = new Chart(el, { plugins: [{ kind: 'overlay', plugin: myHeatmap }] })

// 3) Imperative on an instance:
chart.plugins.register({ kind: 'chartType', plugin: myCustomCandles })
chart.setChartType('my-custom-candles')   // custom chart types render via the plugin
Plugin kindContract
indicatorIndicatorPlugin — calculate() + render()
drawingDrawingPlugin — render() + hitTest()
chartTypeChartTypePlugin — createRenderer() + optional transform()
overlayOverlayPlugin — render(ctx, { viewport, data, theme }) on the main / overlay / ui layer

Indicators outside the chart

IndicatorWorkerHost computes an indicator from bars with the same messages a Web Worker would use. The worker script is not part of the published packages yet; pass null and register the plugins to compute in place (SSR, tests, scripts):

import { IndicatorWorkerHost, RSIIndicator } from '@tradecanvas/core'

const host = new IndicatorWorkerHost(null)
host.registerFallbackPlugin(new RSIIndicator())
const output = await host.calculate('rsi', { id: 'rsi', instanceId: 'rsi-1', params: { period: 14 } }, bars)

Save / Load

const json = chart.saveState()
localStorage.setItem('my-chart', json!)

chart.loadState(localStorage.getItem('my-chart')!)

// Download / upload files
chart.downloadState('my-chart.json')
await chart.loadStateFromFile()

// Or keep a layout saved as it changes (debounced)
chart.setAutoSave('my-chart', 1500)

A saved layout holds the chart type, theme, drawings, indicators (inputs, pane, colours, visibility) and alerts, including alerts on indicator lines.

Themes

import { DARK_THEME, LIGHT_THEME, DARK_TERMINAL, volumeColor } from '@tradecanvas/chart'

// Built-in presets: DARK_THEME, LIGHT_THEME, DARK_TERMINAL
chart.setTheme(DARK_TERMINAL)  // fintech terminal: #0E0E0E bg, #00FF87/#FF3B4D candles, monospace

// Or customize any preset
chart.setTheme({
  ...DARK_THEME,
  candleUp: '#1fa874',
  candleDown: '#e8505b',
  volumeUp: volumeColor('#1fa874'),    // volume bars: the candle colour, see-through
  volumeDown: volumeColor('#e8505b'),
  background: '#0a0a0f',
})

Events

chart.on('crosshairMove', (e) => { /* { point, bar, barIndex, indicatorValues } — also over indicator panes */ })
chart.on('crosshairLeave', () => { /* the pointer left the plot */ })
chart.on('drawingToolChange', (e) => { /* { tool } — null once a drawing is finished or cancelled */ })
chart.on('indicatorUpdate', (e) => { /* { from } — indicator values recomputed from this bar on */ })
chart.on('paneResize', (e) => { /* { instanceId, size } — an indicator pane was resized */ })
chart.on('indicatorChange', (e) => { /* { instanceId, change } — shown/hidden, restyled, levels, inputs or pane changed */ })
chart.on('barClick', (e) => { /* { bar, barIndex, point } */ })
chart.on('visibleRangeChange', (e) => { /* { from, to } — bar indices, not timestamps */ })
chart.on('priceRangeChange', (e) => { /* { min, max } — visible price bounds */ })
chart.on('zoomChange', (e) => { /* { barWidth } — pixels per bar */ })
chart.on('drawingCreate', (e) => { /* ... */ })
chart.on('orderModify', (e) => { /* ... */ })
chart.on('positionModify', (e) => { /* ... */ })

visibleRangeChange, priceRangeChange, and zoomChange fire on every pan, zoom, resize, and data update — but only when that piece of viewport state actually changed. Resolve a visibleRangeChange index to time with chart.getData()[e.payload.from].time.

Replay Mode

ReplayController plays a historical DataSeries forward at controlled speed. Decoupled from Chart — wire it into any sink (chart for UI playback, or a strategy fn for headless backtests).

import { ReplayController } from '@tradecanvas/chart'

const replay = new ReplayController({
  data: historicalBars,
  speed: 10,        // bars per second
  startIndex: 0,
})

// Seed the chart with the prefix before replay starts
chart.setData(replay.getPrefix())

// Each emitted bar drives the chart forward
replay.on('bar', ({ bar }) => chart.appendBar(bar))
replay.on('finished', () => console.log('done'))

replay.start()
// replay.pause(); replay.resume(); replay.step(5); replay.seek(200); replay.setSpeed(20)

Chart Interaction

Every gesture you'd expect from a desktop trading chart is built in:

GestureResult
Drag chart body left/rightPan through time
Drag chart body up/downPan the price scale (freezes auto-scale; double-click price axis to restore)
Drag price axis up/downCompress / expand vertical scale (freezes auto-scale)
Drag time axis left/rightZoom time axis
Double-click price axisRe-enable auto-scale
Double-click time axisFit all data to viewport
WheelZoom around cursor
Drag a pane dividerResize the indicator pane (ns-resize cursor on hover)
Shift + dragMeasure ruler (bars × time × price Δ × %)
Alt + clickPin OHLC tooltip; live crosshair shows Δ to pinned bar
HoverPrice + time pill labels follow on both axes
EscUnpin tooltip / cancel drawing
?Show keyboard-shortcut sheet (widget)
Ctrl/⌘ + KCommand palette (widget)
Ctrl/⌘ + PSymbol search (widget)
Ctrl/⌘ + Z / Shift + ZUndo / redo drawings

Data import — drag-and-drop or programmatic

import { parseOHLCV } from '@tradecanvas/chart'

const { data, rowCount, skipped } = parseOHLCV(csvText)
chart.setData(data)

Drop a CSV or JSON file onto the widget and it loads instantly. Auto-detects delimiter (, / ; / tab / |), header vs. headerless, ISO 8601 timestamps, and array-of-arrays vs. array-of-objects JSON.

Backtesting (@tradecanvas/analytics)

Bar-by-bar strategy backtester with virtual fills, commission/slippage models, and a full risk-metrics report.

import { Backtester, PercentCommission, PercentSlippage } from '@tradecanvas/analytics'

const bt = new Backtester({
  initialCash: 10_000,
  commission: new PercentCommission(0.0005),
  slippage: new PercentSlippage(0.0003),
})

const result = bt.run(historicalBars, (ctx) => {
  // Strategy fn runs at close of each bar; orders fill on the NEXT bar.
  if (!ctx.position && smaFast > smaSlow) {
    ctx.placeOrder({ side: 'long', type: 'market', quantity: 1 })
  } else if (ctx.position && smaFast < smaSlow) {
    ctx.close()
  }
})

console.log(result.metrics.sharpe)         // 1.42
console.log(result.metrics.maxDrawdownPct) // 0.087
console.log(result.equityCurve)            // → feed into the chart via EquityCurveRenderer

Returns: fills, closed trades, equityCurve, metrics (Sharpe, Sortino, Calmar, CAGR, max drawdown, win rate, profit factor, expectancy). See the live backtest demo.

Strategy library

Four drop-in reference strategies — each returns a StrategyFn ready to feed Backtester.run():

import {
  Backtester,
  smaCrossStrategy,
  rsiReversionStrategy,
  donchianBreakoutStrategy,
  bollingerReversionStrategy,
} from '@tradecanvas/analytics'

const bt = new Backtester({ initialCash: 10_000 })
bt.run(bars, smaCrossStrategy({ fastPeriod: 10, slowPeriod: 30 }))
bt.run(bars, donchianBreakoutStrategy({ entryPeriod: 20, exitPeriod: 10 }))

Monte Carlo path-dependence

Shuffle realised trade order N times to expose whether a strategy depends on lucky sequencing. Tight P5/P95 band = robust edge; wide band = path-dependent.

import { runMonteCarlo } from '@tradecanvas/analytics'

const result = bt.run(bars, smaCrossStrategy())
const mc = runMonteCarlo(10_000, result.trades, { simulations: 1000, seed: 42 })

mc.equityBands              // [{ step, p5, p25, p50, p75, p95 }, …]
mc.finalEquityPercentiles   // { p5, p25, p50, p75, p95 }
mc.probabilityProfitable    // 0..1
mc.worstMaxDrawdownPct

Comparison

Feature@tradecanvas/chartlightweight-chartschart.jsHighcharts Stock
Chart types18 + 6 finance48 (non-financial)10+
Finance chartsSparkline, Depth, Equity, Heatmap, Waterfall, GaugeNoneNoneSome
Built-in indicators9500~30
Drawing tools6900Some
Trading overlayFull (pos + orders + drag)NoneNoneNone
Real-time streamingBuilt-in (Binance)ManualManualBuilt-in
Save/load stateYesNoNoYes
Replay modeYes (ReplayController)NoNoNo
BacktesterYes (@tradecanvas/analytics)NoNoNo
Multi-chart gridYes (ChartGrid)NoNoYes
Bundle (gzip)~100 KB core~45 KB~70 KB~200 KB
Dependencies0100
Widget (complete UI)Yes (ChartWidget)NoNoNo
LicenseMITApache 2.0MITCommercial

API Overview

new Chart(container, options)

const chart = new Chart(element, {
  chartType: 'candlestick',
  theme: DARK_THEME,
  autoScale: true,
  rightMargin: 5,
  numberLocale: 'en-US',  // or 'de-DE', 'vi-VN', etc. — BCP 47 locale
  crosshair: { mode: 'magnet' },
  features: { drawings: true, indicators: true, trading: true, volume: true },
})

// Change locale at runtime
chart.setNumberLocale('de-DE')  // 65.234,00

Key Methods

MethodDescription
setData(bars)Load historical OHLCV data
appendBar(bar)Append a new candle
appendBars(bars)Bulk append (reconnect catch-up)
updateLastBar(bar)Update the in-progress candle
setCurrentPrice(price, pulseColor?)Show a live price line
connect(config)Connect to a real-time data source
setTimeframe(tf)Switch timeframe on active stream
setChartType(type)Switch chart type
setTheme(theme)Apply a theme (DARK_THEME, LIGHT_THEME, DARK_TERMINAL)
setNumberLocale(locale)Set number format locale (en-US, de-DE, vi-VN)
setStatusText(text)Show status in legend area ("LIVE · 8ms")
addIndicator(id, params?)Add a technical indicator
removeIndicator(instanceId)Remove an indicator
setDrawingTool(tool)Activate a drawing tool
setPositions(positions)Render trading positions
setOrders(orders)Render pending orders
setVolumeProfileVisible(v)Toggle the horizontal volume-profile overlay
setVolumeProfileConfig({ buckets, widthRatio, opacity, highlightPoC })Tune the volume profile
setAutoScale(v) / setLogScale(v)Lock or change price-scale mode
setInvertScale(v)Turn the price scale upside down
fitContent() / scrollToEnd()Fit all data / jump to live edge
setVisibleRangePreset(p)Show 1D, 5D, 1M, 3M, 6M, YTD, 1Y, 5Y or All
goToTime(time)Centre the bar at a time
setCrosshairTime(time)Mirror another chart's crosshair (vertical line only)
copyDrawings() / pasteDrawings()Copy the selection, paste into this or another chart
setStayInDrawingMode(v)Keep the drawing tool after each drawing
saveState(key?)Serialize chart state
loadState(json)Restore chart state
screenshot()Download chart as image
setRenderer(mode)Draw with 'canvas' (default), 'webgl' or 'auto'; resolves to what draws now
getRenderer()'canvas' or 'webgl'
on(event, handler)Subscribe to events
destroy()Clean up all resources

Data Format

interface OHLCBar {
  time: number    // Unix time in ms or seconds (up to 1e12 is read as seconds); ascending
  open: number
  high: number
  low: number
  close: number
  volume: number
}

Examples

ExampleDescription
Live demoFeature Lab: drawing tools, indicators, trading, ranges, paged history, replay, 16 languages with sub-cent prices, watchlists with live quotes, 200k bars, slow-network switching — each on a live chart. The site and docs are also in Vietnamese, Chinese, Japanese, Korean and Spanish
StackBlitz sandboxesOne-click, forkable: vanilla Chart, ChartWidget, React / Vue / Svelte wrappers, finance charts
@tradecanvas/react · /vue · /svelteFramework components — reactive props, typed, zero boilerplate

AI coding tools

  • llms.txt and llms-full.txt give assistants the docs in one place.
  • An agent skill, skills/tradecanvas, teaches coding agents to build with TradeCanvas: the entry points, the rules that avoid most bugs, and worked examples that CI type-checks against the library. Copy the folder into your project's .claude/skills/ (or your agent's skills folder) to use it.

Browser Support

Chrome 80+, Firefox 80+, Safari 14+, Edge 80+

Framework Integration

Official wrapper components — reactive props, refs, zero boilerplate. Published at 1.x alongside the core:

npm install @tradecanvas/react    # or @tradecanvas/vue · @tradecanvas/svelte
import { TradeCanvas } from '@tradecanvas/react'

<TradeCanvas symbol="BTCUSDT" timeframe="5m" theme="dark" indicators={['rsi', 'macd']} />

All three share the same prop surface and hand you the underlying Chart (for drawings, trading, execution, plugins) via onReady / ref / bind:chart. See the frameworks docs.

Headless (own the lifecycle)

The Chart class also takes a DOM element directly — framework-agnostic:

React:

import { useEffect, useRef } from 'react'
import { Chart, BinanceAdapter } from '@tradecanvas/chart'

function TradingChart() {
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const chart = new Chart(ref.current!, {
      theme: 'dark',
      features: { indicators: true, drawings: true },
    })
    chart.connect({
      adapter: new BinanceAdapter(),
      symbol: 'BTCUSDT',
      timeframe: '5m',
    })
    return () => chart.destroy()
  }, [])

  return <div ref={ref} style={{ width: '100%', height: 500 }} />
}

Svelte:

<script lang="ts">
  import { onMount, onDestroy } from 'svelte'
  import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'
  import type { TimeFrame } from '@tradecanvas/chart'

  interface Props { symbol?: string; timeframe?: TimeFrame }
  let { symbol = 'BTCUSDT', timeframe = '5m' }: Props = $props()

  let container: HTMLDivElement
  let chart: Chart | null = null

  onMount(() => {
    chart = new Chart(container, {
      chartType: 'candlestick',
      theme: DARK_THEME,
      autoScale: true,
      features: { indicators: true, drawings: true, volume: true },
    })
    chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
  })

  onDestroy(() => chart?.destroy())

  $effect(() => {
    if (!chart) return
    chart.disconnectStream()
    chart.connect({ adapter: new BinanceAdapter(), symbol, timeframe })
  })
</script>

<div bind:this={container} style="width: 100%; height: 600px" />

Vue:

<template>
  <div ref="chartContainer" style="width: 100%; height: 600px" />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Chart, BinanceAdapter, DARK_THEME } from '@tradecanvas/chart'

const chartContainer = ref<HTMLDivElement>()
let chart: Chart | null = null

onMounted(() => {
  if (!chartContainer.value) return
  chart = new Chart(chartContainer.value, {
    chartType: 'candlestick',
    theme: DARK_THEME,
    autoScale: true,
    features: { indicators: true, drawings: true, volume: true },
  })
  chart.connect({ adapter: new BinanceAdapter(), symbol: 'BTCUSDT', timeframe: '5m' })
})

onUnmounted(() => chart?.destroy())
</script>

Performance

A two-canvas Canvas2D pipeline: a hover repaints only the thin top canvas, never the scene. Five things keep large data fast:

  • LTTB downsampling — line / area charts automatically downsample the visible range to ~2 points per pixel using Largest-Triangle-Three-Buckets when there are far more bars than pixels. The line stays visually identical while drawing dozens of times fewer points; a no-op at normal zoom. The lttbDownsample utility is exported for your own use.
  • Indicators zoomed out — below a pixel per bar, indicator lines, bands and histograms draw one span per pixel column instead of a stroke through thousands of points: much the same look for a fraction of the raster work (about 54 → 21 ms a frame zoomed out on 200,000 bars with four indicators, on integrated graphics). node scripts/bench-render.mjs measures it on your machine.
  • Visible-range rendering — every renderer iterates only the bars in view, never the whole series. Hover and pan frame cost stays flat from 500 to 100,000 loaded bars.
  • Incremental indicators on live ticks — a tick only changes the forming bar, so built-in indicators that implement update() (SMA, EMA, WMA, VWMA, Bollinger, Envelope, RSI, MACD, ATR, OBV, Stochastic) recompute just that bar instead of the whole history. Others fall back to a full recalculation. Custom plugins can opt in via IndicatorPlugin.update.
  • Cheap full loads — a symbol/timeframe switch recomputes every indicator once. Their per-bar values lookup is an IndicatorValueMap (array-backed while bars arrive in time order, ~3x cheaper to build than a Map keyed by timestamps), and setData reuses already well-formed bars instead of copying each one.

BB + EMA + RSI + MACD (pnpm bench, single core):

HistoryFull recalculation (switch / setData)Incremental update() (live tick)
20,000 bars~5 ms~0.0005 ms
100,000 bars~27 ms~0.001 ms

Downsampling throughput (pnpm bench, single core):

Visible points → 1600Time / frameThroughput
10,000~0.025 ms39,600 / s
100,000~0.32 ms3,100 / s
1,000,000~2.6 ms380 / s

A 100k-bar line chart downsamples in ~0.3 ms — well inside a 16.6 ms frame budget — then draws ~62× fewer points (100k → 1600).

WebGL renderer (preview)

renderer: 'webgl' draws the plot and indicator panes with WebGL 2, on a canvas under the 2D scene: the grid, sessions, candles and volume directly, and the Canvas 2D drawing of indicators, compare lines and most chart types recorded as GPU strokes, fills and rectangles, antialiased at their edges as Canvas 2D is. Text, drawings, orders, axes and the crosshair stay on Canvas 2D, as does anything the GPU wouldn't draw the same (left to Canvas 2D in order, so the stacking doesn't change); custom indicator plugins come along without changes. Candles match Canvas 2D to within 2/255; lines and fills differ only on a few antialiased edge pixels. The WebGL code is a chunk of its own (about 17 KB gzipped), loaded on first use; where WebGL 2 is missing, or its context is lost, the chart carries on with Canvas 2D.

const chart = new Chart(el, { renderer: 'webgl' })   // or 'auto': WebGL on a hardware GPU only

chart.on('rendererChange', (e) => console.log(e.payload))
// { renderer: 'webgl' }, or { renderer: 'canvas', reason: 'unsupported' | 'contextLost' }

await chart.setRenderer('canvas')   // resolves to what draws now

Frame time while panning, on integrated graphics (Intel UHD; 16.7 ms is 60 fps):

ChartPixel ratioCanvas 2DWebGL
1600×900, 500 candles + 4 indicators227.4 ms19.6 ms
1600×900, zoomed out on 200,000 bars + 4 indicators234.5 ms20.2 ms
Six charts, 500 candles and two indicators each223.5 ms17.2 ms
2560×1400, 2,000 candles + 4 indicators141.6 ms17.7 ms
2560×1400, 2,000 candles + 4 indicators1.570.8 ms17.6 ms
2560×1400, 2,000 candles + 4 indicators2114.5 ms29.1 ms
2560×1400, 2,000 candles233.1 ms20.9 ms

Most WebGL frames above land on 16.7 ms; the averages carry a few longer ones. At a pixel ratio of 2 on a 2560×1400 chart, the browser's compositing of the full-size layers alone takes about 23 ms on this GPU. node scripts/bench-render.mjs --renderer=webgl runs these numbers on your own machine.

Architecture

Two stacked canvases — a hover repaints only the thin top one:

  Top canvas    (crosshair + axis pills, legend, countdown, measure)   z=1
  Scene canvas  (grid, candles, indicators, drawings, orders, axes)    z=0
  • bo-grid — tiny, fast Svelte 5 data grid for fintech UIs: canvas sparklines, batched realtime cell updates, virtual scrolling, grouping / pivot / tree data, and Excel export, with a core that gzips to ~32 KB. The table half of the same toolkit — pair it with TradeCanvas for a full trading desk. Live demo

Contributing

Bug reports, ideas and pull requests are welcome. CONTRIBUTING.md covers the setup (pnpm install && pnpm build && pnpm test), how the repo is laid out and what a pull request needs. Found a security issue? Please follow SECURITY.md instead of opening an issue.

If TradeCanvas saves you time, a star on GitHub helps other developers find it.

License

MIT

backtesting
candlestick-chart
canvas
chart
charting-library
crypto
drawing-tools
financial-charts
fintech
indicators
javascript
react
stock-chart
svelte
technical-analysis
trading
trading-chart
typescript
vue
websocket

Languages

TypeScript

96.6%

CSS

2.4%