Introduction

LLM-first financial charting library for modern web applications.

Try It Live

What is Axon Charts?

Axon Charts is a canvas-based financial charting library built from the ground up for AI agents and developers. It provides:

  • LLM-First Design: Every feature is accessible via simple, predictable APIs that AI agents can discover and use
  • Lightning Fast: Handles 5000+ candles at 60fps with optimized draw calls
  • 164 Configuration Options: Customize every aspect of appearance and behavior
  • TypeScript Support: Full type definitions for excellent developer experience
  • Production Ready: Volume sub-panes, streaming support, market headers, and more

Key Features

Feature Description
Real-time Updates WebSocket support with updateLastBar() and updateLastBarFast() methods
Volume Sub-pane Display volume histogram below main chart with auto precision detection
Crosshair Modes Normal, magnet (snap-to-bar), and disabled modes
Context Export getContext() returns structured JSON for LLM analysis
164 Options Layout, grid, scales, colors, behavior, and more

Browser Support

Works on all modern browsers that support ES6 and Canvas API:

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

Installation

NPM

npm install axon-charts

Yarn

yarn add axon-charts

CDN

<script src="https://cdn.jsdelivr.net/npm/axon-charts/dist/chart.js"></script>
<script src="https://unpkg.com/axon-charts/dist/chart.js"></script>

Bundled Size

  • Minified + IIFE: ~184 KB / 180 KB ESM
  • Gzipped: ~46.2 KB

Quick Start

Try It Live

Basic Chart

// Import the library
import { createChart } from 'axon-charts';

// Create a chart
const chart = createChart('#container', {
  layout: {
    background: '#1a1a1a',
    textColor: '#aaa',
  },
  grid: {
    show: true,
    vertLines: { color: '#2a2a2a' },
    horzLines: { color: '#2a2a2a' },
  },
});

// Load data
chart.setData([
  { time: 1609459200000, open: 100, high: 105, low: 98, close: 103 },
  { time: 1609459260000, open: 103, high: 107, low: 102, close: 105 },
  // ... more data
]);

Real-time Updates

// Update the latest candle
chart.updateLastBar({
  time: Date.now(),
  open: 105,
  high: 108,
  low: 104,
  close: 107,
});

Get Chart State

// Export chart context for LLM analysis
const ctx = chart.getContext();
console.log(ctx.visibleBars);
console.log(ctx.viewport);
console.log(ctx.priceRange);

Settings Overview

164 configuration options across 25 categories

Try It Live

How to Set Options

// Set options during initialization
const chart = createChart('#container', {
  layout: { background: '#1a1a1a' },
  grid: { show: true },
});

// Update options dynamically
chart.setOptions({
  volume: { show: true, heightPercent: 0.25 },
  crosshair: { mode: 'magnet' },
});

Categories

Category Options Description
Layout 10 Dimensions, colors, fonts, padding
Grid 7 Background grid lines
Series 2 Candle colors
Price Scale 15 Y-axis mode, margins, format, current price, and reverse
Time Scale 11 X-axis visibility, spacing, format, and timezone
Crosshair 9 Cursor overlay and tooltip
Menu 2 Right-click context menu
Behavior 6 Drag, zoom, pan, and auto-scroll flags
Data 2 Data management and limits
Volume 6 Volume histogram sub-pane
Attribution 1 Axon.Watch logo at chart bottom-left
Market 6 Trading pair and exchange info
Context (LLM) 3 LLM data exposure, AI agent discovery, chart ID
Watermark 6 Semi-transparent overlay

Layout

Chart dimensions, colors, fonts, and padding — 10 options.

Try It Live

Width & Height

Set to 'auto' (reads container size) or a fixed pixel value. Auto is recommended for responsive layouts.

layout: {
  width: 'auto',    // reads container.clientWidth
  height: 'auto',   // reads container.clientHeight
  background: '#1a1a1a',
  textColor: '#ffffff',
  fontSize: 12,
  fontFamily: 'system-ui'
}

Padding

Controls chart margins. Maps directly to internal state variables.

layout: {
  padding: {
    top: 40,     // → state.topMargin
    right: 60,   // → state.axisWidth
    bottom: 35,  // → state.bottomMargin
    left: 10     // reserved
  }
}

Dynamic Updates

chart.setOptions({ layout: { background: '#000000', textColor: '#ffffff' } });
chart.setOptions({ layout: { padding: { top: 20, right: 40 } } });

Grid

Background grid lines — 7 options.

Try It Live

Master Toggle

grid.show acts as a master switch. When false, all grid lines are hidden regardless of sub-settings.

Vertical & Horizontal Lines

Each axis has independent show/color/width controls:

grid: {
  show: true,
  vertLines: { show: true, color: '#2a2a2a', width: 1 },
  horzLines: { show: true, color: '#2a2a2a', width: 1 }
}
chart.setOptions({ grid: { show: false } });           // hide all
chart.setOptions({ grid: { vertLines: { color: '#333' } } });  // recolor

Series

Candle and line colors — 2 options.

Try It Live

Up / Down Colors

Controls the fill color of bullish and bearish candles. Also affects the current price line color.

series: {
  upColor: '#10B981',   // green candle body
  downColor: '#E11D48'  // red candle body
}
chart.setOptions({ series: { upColor: '#00ff00', downColor: '#ff0000' } });

Price Scale

Y-axis behavior and formatting — 15 options.

Try It Live

Scale Mode

Linear is standard for most charts. Logarithmic is useful for assets with large price ranges.

priceScale: {
  mode: 'linear',           // 'linear' | 'logarithmic' | 'percentage'
  scaleMargins: {
    top: 0.1,               // 10% padding above max price
    bottom: 0.1             // 10% padding below min price
  }
}

Percentage Mode

Normalizes all prices relative to the close price of the leftmost visible bar. The reference updates dynamically as you scroll or zoom. Display-only — internal data remains in original prices. Switch back to linear mode to restore numeric axis labels.

Axis labels, current price line, and crosshair price label use smart adaptive formatting:

Change Display Range
+2.45+2.45%Small changes (2 decimal places)
+245.8+245.8%Medium changes (1 decimal place)
+8,450+8,450%Large changes (thousands separator, 0 decimals)
+80,500+805.0×Extreme moves ≥10,000% (× multiplier)

× multiplier activates for changes of 100× or more (e.g. Bitcoin from $100 to $80k+). Formatting uses Intl.NumberFormat for proper locale-aware thousands separators.

Price Format

priceScale: {
  priceFormat: {
    type: 'price',           // 'price' | 'volume' | 'percent'
    precision: 2,            // decimal places
    minMove: 0.01            // tick increment (derives precision if not set)
  }
}

Current Price Line

Dashed horizontal line at the latest close price with a label on the Y-axis. Colors cascade: if not set explicitly, they follow series colors and layout.textColor.

priceScale: {
  currentPrice: {
    show: true,              // master toggle for line + label
    showLine: true,          // show/hide only the dashed line
    showCountdown: false,    // candle-close countdown timer (default: false)
    countdownColor: 'rgba(255, 255, 255, 0.8)',
    upColor: null,           // bull line color — falls back to series.upColor
    downColor: null,         // bear line color — falls back to series.downColor
    lineStyle: 'dashed',     // 'dashed' | 'solid'
    textColor: null          // label text — falls back to layout.textColor
  }
}
  • upColor / downColor — when null, follows candle colors. Set explicitly to decouple from candles.
  • textColor — when null, follows layout.textColor. Essential for light backgrounds where white text would be invisible.
  • showLine — hides only the dashed horizontal line while keeping the price label visible.
  • countdownColor — when null, falls back to textColor, then layout.textColor.
  • lineStyle — dashed (default) or solid.

The label background uses the line color at 80% opacity. Line style supports both dashed (default) and solid.

chart.setOptions({ priceScale: { currentPrice: { textColor: '#000000', lineStyle: 'solid' } } });

Reverse

Inverts the price axis so high prices appear at the bottom:

chart.priceScale().setReverse(true);
chart.execute({ type: 'setReverse', reverse: true });

Time Scale

X-axis behavior, timezone, and formatting — 11 options.

Try It Live

Visibility & Format

timeScale: {
  visible: true,
  timeVisible: true,         // show time on axis (false = date only)
  secondsVisible: false,     // HH:mm:ss format
  showFullDate: true,        // crosshair: "Fri 03 Jan'26 17:55"
  showDayOfWeek: true,       // show "Mon", "Tue" prefix in crosshair
  dateFormat: 'MMM dd, yyyy' // tokens: yyyy, yy, MMM, MM, dd
}

Timezone

Controls how timestamps are displayed on the axis and crosshair. By default, uses the browser's local timezone.

timeScale: {
  timezone: undefined         // browser local (default)
  // timezone: 'UTC'          // universal time
  // timezone: 'America/New_York'   // any IANA name
}

Uses Intl.DateTimeFormat — handles daylight saving automatically. The crosshair tooltip and all axis labels respect the configured timezone.

Bar Spacing

timeScale: {
  barSpacing: 11,            // base bar width in pixels
  minBarSpacing: 4,          // zoom-in boundary
  maxBarSpacing: 1000        // zoom-out boundary
}
chart.timeScale().setBarSpacing(20);

Right Offset

Gap in pixels between the last bar and the right edge of the chart area:

timeScale: {
  rightOffset: 80
}
chart.setOptions({ timeScale: { rightOffset: 40 } });
chart.setOptions({ timeScale: { timezone: 'UTC', dateFormat: 'yyyy-MM-dd' } });  // ISO-style

Crosshair

Cursor overlay and tooltip — 9 options.

Try It Live

Mode

  • 'magnet' (default) — snaps to nearest bar
  • 'normal' — free-floating cursor
  • 'none' — crosshair hidden
chart.crosshairAPI().setMode('magnet');
chart.execute({ type: 'setCrosshair', mode: 'none' });

Labels & Tooltip

crosshair: {
  showLabels: true,          // axis highlight labels
  showTooltip: true          // OHLC tooltip near cursor
}

Line Styling

crosshair: {
  vertLine: { color: '#555555', width: 1, style: 'dashed' },
  horzLine: { color: '#555555', width: 1, style: 'dashed' }
}
chart.crosshairAPI().setVerticalLine({ color: '#ff0000', width: 2 });

Behavior

Interaction controls — 6 options.

Try It Live

All behavior flags are boolean toggles that affect mouse/touch interaction. Changes take effect immediately on the next user action.

behavior: {
  dragToZoom: true,          // time-axis drag-zoom
  scrollToZoom: true,        // mouse wheel zoom
  pinchToZoom: true,         // touch pinch-zoom
  panOnMouseDrag: true,      // chart-area panning
  dragPriceScale: true,      // Y-axis drag zoom
  autoScroll: true           // viewport follows latest bar
}
chart.setOptions({ behavior: { panOnMouseDrag: false } });  // disable pan

Data

Data management and limits — 2 options.

Try It Live

Controls how the chart manages its internal data buffer — useful for long-running streaming sessions.

data: {
  maxBars: 5000,       // max candles kept in memory (1-1,000,000)
  autoCleanup: true    // auto-trim old bars when limit is exceeded
}
chart.setOptions({ data: { maxBars: 10000 } });

maxBars

Maximum number of bars stored in memory. Older bars are trimmed when this limit is exceeded. Default is 5000. Higher values consume more memory but allow further panning into history.

autoCleanup

When true (default), the chart automatically removes bars from the beginning of the dataset when maxBars is exceeded. Set to false if you manage trimming manually.

Attribution

Axon.Watch branding (1 option)

The attribution logo displays a stylized A in a black circle at the bottom-left of the chart area. On hover, it expands to reveal xon.Watch and links to axon.watch.

attribution: {
  show: true,                    // Show/hide the attribution logo

}

Configuration

OptionTypeDefaultDescription
showbooleantrueShow/hide the attribution logo

Volume

Volume histogram sub-pane — 6 options.

Try It Live

Volume Sub-Pane

The volume histogram displays below the main chart with an independent Y-axis, draggable separator, and zoom/pan support.

volume: {
  show: false,               // toggle visibility
  upColor: '#10B981',        // bullish volume color
  downColor: '#E11D48',      // bearish volume color
  heightPercent: 0.2,        // 20% of total chart height (0.05-0.5)
  precision: null,           // decimal places (null = auto-detect)
  minMove: null              // min increment (derives precision)
}

Features

  • Independent Y-axis with K/M suffix formatting
  • Draggable separator (6px hit zone, ns-resize cursor)
  • Scroll wheel and drag zoom (1x-10x)
  • Double-click reset on sub-pane axis
  • Crosshair spans entire chart including sub-pane
  • Current value dashed line with axis label
  • Volume tooltip at top-left of sub-pane
  • Bars colored per candle direction (up/down)
  • Precision auto-detected from data (or explicit via options)
chart.setOptions({ volume: { show: true, heightPercent: 0.25 } });

Market Header

Trading pair and exchange info — 6 options.

Try It Live

Displays an in-chart label at the top-left showing the market pair, timeframe, and source.

market: {
  show: false,               // toggle visibility
  baseAsset: 'BTC',          // base asset name
  quoteAsset: 'USDT',        // quote asset name
  timeframe: '1m',           // chart timeframe
  source: '',                // exchange / source name
  fontSize: 15               // header font size in px (default: 15)
}

Format: BTC/USDT | 1m | Binance

The header text is rendered at the configured fontSize (default 20px). The tooltip below it is offset by fontSize + 4 pixels so they don't overlap.

chart.setOptions({ market: { fontSize: 16 } });  // smaller header
chart.setOptions({ market: { fontSize: 24 } });  // larger header
chart.setOptions({ market: { baseAsset: 'ETH', quoteAsset: 'USDT', source: 'Hyperliquid', show: true } });

Context (LLM Exposure)

Control what AI agents can discover and read — 3 options.

Try It Live

These options control the AI agent integration layer. Three settings are available:

exposeData

Controls what getContext() returns. When false (default), only viewport metadata is returned — visible bars, latest bar, and sub-pane data are excluded. When true, full trading data is available to agents.

context: {
  exposeData: false    // metadata only — safe default
  // exposeData: true  // full OHLCV data exposure
}

Runtime-toggleable via chart.setOptions({ context: { exposeData: true } }).

discoverable

Controls whether the chart registers in the global window.__AXON_CHARTS__ registry. When true (default), AI agents with the Axon Charts skill can discover and interact with the chart. Set to false for stealth mode.

context: {
  discoverable: true   // visible to AI agents — default
  // discoverable: false // stealth mode
}

id

Optional identifier for the chart in the AI agent registry. Used to target specific charts in multi-chart environments.

  • If set explicitly: used as-is (e.g. 'Bitcoin-1m')
  • If not set: auto-generated opaque token (e.g. ax-uhq8fz) — no pair inference, no stale associations

Opaque IDs prevent stale instrument associations. Agents must read state.market in getContext() to determine the current instrument.

The ID is used as both the registry key and the data-axon-charts-id DOM attribute on the container element.

// Create with explicit ID
const chart = createChart('#container', {
  context: { id: 'Bitcoin-1m' },
  // ... other options
});

// Agent discovery in multi-chart page
window.__AXON_CHARTS__.charts['Bitcoin-1m'].setOptions({
  context: { exposeData: true }
});

Agent Discovery Flow

  1. Agent checks window.__AXON_CHARTS__ — sees chart ID and version
  2. Agent calls chart.getContext() — gets metadata (exposeData: false by default)
  3. Agent decides to request data access via chart.setOptions({ context: { exposeData: true } })
  4. Full context with bars, latest bar, and sub-panes is now available for analysis

Watermark

Semi-transparent overlay — 6 options.

Try It Live

Displays auto-scaling text over the chart. Falls back to the market pair if text is empty.

watermark: {
  show: false,               // toggle visibility
  text: '',                  // custom text (falls back to pair)
  color: '#ffffff',
  fontSize: null,            // null = auto-scale to ~30% chart width
  opacity: 0.07,             // 0.01 - 1.0
  rotate: false              // true = -45° diagonal
}
chart.setOptions({ watermark: { text: 'DEMO', show: true, opacity: 0.05 } });

API Methods

Try It Live

Chart Methods

Method Description
setData(data) Load OHLCV data array
updateLastBar(bar) Update the latest candle
updateLastBarFast(bar) Fast update for high-frequency streams
setOptions(options) Update chart options
getContext() Get structured chart state
execute(command) Execute universal command
toDataURL() Export chart as image
toBlob() Export chart as Blob (async)
saveState() Serialize chart state (options, data, viewport, drawings, overlays)
loadState(state) Restore chart state (accepts any 1.x schema version)
resetState(opts?) Clear drawings + overlays (single render, preserves options/data/viewport)
registerOverlayType(type, ctor) Register custom overlay type for saveState/loadState
appendBar(bar) Append a completed bar
prependData(bars) Insert historical bars at front (no viewport shift)
getData() Get full data array
getBarAtTime(ts) Lookup bar by timestamp

Component APIs

API Methods
chart.priceScale() setMode(), setMargins(), setReverse(), getReverse()
chart.timeScale() setVisibleRange(), fitContent(), scrollToTime(), setBarSpacing(), zoomIn(), zoomOut()
chart.crosshairAPI() setMode(), getMode(), setVisible(), isVisible(), setShowLabels(), getShowLabels(), setShowTooltip(), getShowTooltip(), setVerticalLine(), setHorizontalLine(), setOptions(), getOptions()

LLM Integration

Try It Live

Read Chart State

const ctx = chart.getContext();

// Visible bars in current viewport
console.log(ctx.visibleBars);

// Price range
console.log(ctx.priceRange);

// Viewport info
console.log(ctx.viewport);

Control Chart

// Universal command interface
chart.execute({ type: 'setPriceScale', mode: 'logarithmic' });
chart.execute({ type: 'fitContent' });
chart.execute({ type: 'zoomIn', factor: 1.5 });

React to Events

Subscribe to chart events for interactive LLM workflows. See Events section for the full list.

Live Streaming

For real-time data integration patterns, see the Streaming Data section.

Export for Vision LLMs

// Get base64 screenshot
const screenshot = chart.toDataURL('image/png');

// Send to vision-capable LLM for analysis

Streaming Data

Try It Live

WebSocket Integration

const ws = new WebSocket('wss://api.example.com/stream');

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  chart.updateLastBar({
    time: data.time,
    open: data.open,
    high: data.high,
    low: data.low,
    close: data.close,
    volume: data.volume,
  });
};

High-Frequency Updates

For tick-by-tick updates (20+ per second), use updateLastBarFast():

// Fast path for every tick during live streaming
chart.updateLastBarFast(currentCandle);

// The current price line and label update atomically with every tick — no extra calls needed.
// The axis range auto-expands when price moves beyond a 1.5% hysteresis threshold.
// A full chart.render() is only needed on candle close or when manually syncing.

Events

React to user interactions and chart state changes.

Crosshair Move

Fired when the user moves the cursor over the chart. Returns the bar under cursor, price, and timestamp.

chart.onCrosshairMove(({ bar, price, time }) => {
  console.log(`Price: $${price} at ${new Date(time)}`);
  console.log(`OHLC: O=${bar.open} H=${bar.high} L=${bar.low} C=${bar.close}`);
});

Bar Click

Fired when the user clicks on a specific bar/candle.

chart.onBarClick(({ bar, index }) => {
  console.log(`Clicked bar #${index}:`, bar);
});

Visible Range Change

Fired when the viewport changes (pan, zoom, resize). Returns the visible time range.

chart.onVisibleRangeChange(({ from, to }) => {
  console.log(`Viewing: ${new Date(from)} to ${new Date(to)}`);
});

Scroll Lock Change

Fired when auto-scroll locks/unlocks as the user pans through history.

chart.onScrollLockChange((locked) => {
  console.log(locked ? 'Exploring history' : 'Watching live');
});

Event Reference

Event Callback Parameter Description
onCrosshairMove { bar, price, time } Cursor position changed
onBarClick { bar, index } Bar/candle was clicked
onVisibleRangeChange { from, to, fromIndex, toIndex } Viewport changed via pan/zoom/resize
onScrollLockChange locked: boolean Auto-scroll state changed

Types

Complete type definitions for the library.

Bar (OHLCV Data Point)

interface Bar {
  time: number;       // Timestamp in milliseconds
  open: number;       // Opening price
  high: number;       // Highest price
  low: number;        // Lowest price
  close: number;      // Closing price
  volume?: number;    // Optional volume data
}

ChartOptions

The main configuration object. All fields are optional — defaults fill in the rest.

interface ChartOptions {
  layout?: {
    width?: number | 'auto';      // default: 'auto'
    height?: number | 'auto';     // default: 'auto'
    background?: string;          // default: '#1a1a1a'
    textColor?: string;           // default: '#ffffff'
    fontSize?: number;            // default: 12
    fontFamily?: string;          // default: 'system-ui'
    borderVisible?: boolean;      // default: false
    padding?: {
      top?: number;               // default: 40
      right?: number;             // default: 60
      bottom?: number;            // default: 35
      left?: number;              // default: 10
    };
  };
  attribution?: {
    show?: boolean;               // default: true (Axon.Watch attribution at bottom-left)
  };
  grid?: {
    show?: boolean;               // default: true
    vertLines?: { show?, color?, width? };
    horzLines?: { show?, color?, width? };
  };
  series?: {
    type?: 'candlestick' | 'line' | 'area' | 'bar' | 'heiken-ashi' | 'hollow';  // default: 'candlestick'
    upColor?: string;             // default: '#10B981'
    downColor?: string;           // default: '#E11D48'
    lineColor?: string;           // default: '#1E90FF' (line/area only)
    showMarkers?: boolean;        // default: false (dot at every close)
    showLatestPriceMarker?: boolean;  // default: true (highlighted latest dot)
    showLatestPriceAnimation?: boolean; // default: true (pulse on price change)
  };
  priceScale?: {
    mode?: 'linear' | 'logarithmic' | 'percentage';  // default: 'linear'
    scaleMargins?: { top?, bottom? };
    priceFormat?: { type?, precision?, minMove? };
    currentPrice?: {
      show?: boolean;                    // default: true
      showLine?: boolean;                // default: true
      showCountdown?: boolean;          // default: false
      countdownColor?: string;          // default: 'rgba(255, 255, 255, 0.8)'
      upColor?: string;                 // default: null (follows series.upColor)
      downColor?: string;               // default: null (follows series.downColor)
      lineStyle?: 'dashed' | 'solid';   // default: 'dashed'
      textColor?: string;               // default: null (follows layout.textColor)
    };
    reverse?: boolean;            // default: false
  };
  timeScale?: {
    visible?: boolean;            // default: true
    timeVisible?: boolean;        // default: true
    secondsVisible?: boolean;     // default: false
    showFullDate?: boolean;       // default: true
    showDayOfWeek?: boolean;      // default: true
    dateFormat?: string;          // default: 'MMM dd, yyyy'
    timezone?: string;            // default: undefined (browser local)
    rightOffset?: number;         // default: 80
    barSpacing?: number;          // default: 11
    minBarSpacing?: number;       // default: 4
    maxBarSpacing?: number;       // default: 1000
  };
  crosshair?: {
    mode?: 'normal' | 'magnet' | 'none';  // default: 'magnet'
    showLabels?: boolean;         // default: true
    showTooltip?: boolean;        // default: true
    vertLine?: { color?, width?, style? };
    horzLine?: { color?, width?, style? };
  };
  menu?: {
    enabled?: boolean;            // default: true
    items?: string[] | null;      // default: null (all items, auto-dividers)
  };
  behavior?: {
    dragToZoom?: boolean;         // default: true
    scrollToZoom?: boolean;       // default: true
    pinchToZoom?: boolean;        // default: true
    panOnMouseDrag?: boolean;     // default: true
    dragPriceScale?: boolean;     // default: true
    autoScroll?: boolean;         // default: true
  };
  data?: {
    maxBars?: number;             // default: 5000
    autoCleanup?: boolean;        // default: true
  };
  market?: {
    show?: boolean;               // default: false
    baseAsset?: string;           // default: 'BTC'
    quoteAsset?: string;          // default: 'USDT'
    timeframe?: string;           // default: '1m'
    source?: string;              // default: ''
    fontSize?: number;            // default: 15
  };
  watermark?: {
    show?: boolean;               // default: false
    text?: string;                // default: '' (pair fallback)
    color?: string;               // default: '#ffffff'
    fontSize?: number | null;     // default: null (auto-scale)
    opacity?: number;             // default: 0.07
    rotate?: boolean;             // default: false
  };
  volume?: {
    show?: boolean;               // default: false
    upColor?: string;             // default: '#10B981'
    downColor?: string;           // default: '#E11D48'
    heightPercent?: number;       // default: 0.2
    precision?: number | null;    // default: null (auto-detect)
    minMove?: number | null;      // default: null
  };
  context?: {
    exposeData?: boolean;          // default: false (metadata only)
    discoverable?: boolean;        // default: true (visible to AI agents)
    id?: string;                   // default: auto-generated opaque token (ax-xxxxxx)
  };
  // === Callbacks ===
  onVisibleRangeChange?: VisibleRangeChangeCallback;
  onCrosshairMove?: CrosshairMoveCallback;
  onBarClick?: BarClickCallback;
  onScrollLockChange?: ScrollLockChangeCallback;
  onDataUpdate?: ((bars: Bar[]) => void) | null;
  onCandleClose?: (closedBar: Bar) => void;   // fires once per candle close

  devicePixelRatio?: number;      // default: window.devicePixelRatio (init only)
}

ChartCommand

Discriminated union for the execute() method.

type ChartCommand =
  | { type: 'setVisibleRange'; from: number; to: number }
  | { type: 'scrollToTime'; time: number }
  | { type: 'zoomIn'; factor?: number }
  | { type: 'zoomOut'; factor?: number }
  | { type: 'fitContent' }
  | { type: 'setPriceScale'; mode: 'linear' | 'logarithmic' | 'percentage' }
  | { type: 'setCrosshair'; mode: 'normal' | 'magnet' | 'none' }
  | { type: 'setSubPane'; id: string; show: boolean }
  | { type: 'setReverse'; reverse: boolean };

Drawing

Persistent chart drawing (annotation, position, custom). Rendered on the main canvas overlay every frame.

interface Drawing {
  id: string;            // Unique identifier
  type: string;          // 'arrow_up' | 'arrow_down' | 'label' | 'hline' | 'vline' | 'position' | <custom>
  color: string;         // CSS color
  text?: string;

  // Primary anchor (one of barIndex/time required for anchored types)
  barIndex?: number;     // Bar index anchor (prefer time for stability)
  time?: number;         // Anchor timestamp (survives maxBars cleanup)
  price?: number;

  // Secondary anchor (two-point drawings: trendline, box, fib, measure)
  barIndex2?: number;
  time2?: number;
  price2?: number;

  data?: DrawingData;    // Type-specific payload
}

interface DrawingData {
  side?: 'long' | 'short';     // Position
  qty?: number;
  sl?: number;                 // Stop-loss
  tp?: number;                 // Take-profit
  status?: 'open' | 'closed' | 'pending' | 'working' | 'cancelled' | 'filled' | 'rejected';
  kind?: 'limit' | 'stop' | 'stop_limit' | 'market';   // Order
  lineWidth?: number;          // Two-point (trendline, box, fib, measure)
  lineStyle?: 'solid' | 'dashed' | 'dotted';
  extend?: 'none' | 'left' | 'right' | 'both';
  fill?: string;
  lines?: string[];            // Text (multi-line)
  textFill?: string;
  showBackground?: boolean;    // Default true
  showBorder?: boolean;        // Default true
  borderColor?: string;
  textColor?: string;
  maxWidth?: number;           // Wrap width (pixels)
  showAnchorDot?: boolean;     // Default true
  showPrice?: boolean;         // Label: show formatted price
  [key: string]: unknown;      // Custom types
}

Built-in types are registered at module load. Custom types can be registered via chart.registerDrawingType(type, renderer) — the renderer must implement the DrawingRenderer interface (a render(ctx, chart, drawing) method). The resolveAnchor() helper is exported for reuse in custom renderers.

ChartContext (LLM Output)

The structured object returned by getContext().

interface ChartContext {
  viewport: {
    width: number;
    height: number;
    rightGap: number;
    visibleRange: {
      fromIndex: number;
      toIndex: number;
      fromTime: number;
      toTime: number;
    };
    priceRange: { min: number; max: number };
    scales: {
      pricePerPixel: number;
      timePerBar: number;
      barWidth: number;
    };
  };
  state: {
    totalBars: number;
    isAutoScrolling: boolean;
  };
  visibleBars: Bar[];
  latestBar: Bar;
  subPanes: Record<string, any>;
}

Performance

Benchmarks and optimization guidelines.

Benchmark Results

Metric Target Actual
Minified bundle <50 KB gzipped 46.2 KB gzipped ✓
First render (500 bars) <16ms 3.4ms ✓
Live tick update <2ms <1ms ✓
Mousemove overlay <1ms <0.3ms ✓
Memory (5000 bars) <12 MB minimal ✓

Architecture

The library uses a three-layer canvas architecture for maximum performance:

Layer Canvas Renders
Background bgCanvas Fill, grid lines, price/time axes, watermark, volume sub-pane, percentage reference line
Main mainCanvas Candles (offscreen-buffered) + current price line (atomic render pass)
Overlay overlayCanvas Crosshair, tooltip, market header

Offscreen Buffer

The candle buffer is rendered at identity scale (1:1 pixel ratio) into an offscreen canvas. Only the visible portion is composited to the display canvas every frame. When a single candle updates (tick stream), only updateLastCandleInBuffer() is called — no full re-render.

Optimization Tips

  • Streaming: Use updateLastBarFast() for tick-by-tick updates (10-1000/sec). Call chart.render() every 20 ticks to refresh axes/grid.
  • Data Limits: Set data.maxBars to the minimum required for your viewport. 500 bars is sufficient for most charts.
  • Sub-Panes: Only enable volume/indicators when needed — each sub-pane adds render overhead.
  • Overlays: Overlay indicators (SMA, EMA, BB, etc.) re-compute every frame. For large datasets, pre-compute values and inject via SubPane.setData().
  • Persistence: saveState() captures drawings + overlays. Use migrateSnapshot() to upgrade old snapshots before loadState().
  • Resize: The library uses ResizeObserver with a re-entrancy guard — avoid manual resize calls when using auto-sizing containers.
  • Canvas Export: toDataURL() is the most expensive operation — debounce to every 500ms.

Bundle Size

  • Minified + IIFE: ~184 KB / 180 KB ESM
  • Gzipped: ~46.2 KB
  • Zero external dependencies