Documentation

API Reference

Main Entry#

  • createChart(element: HTMLElement, options?: ChartOptions): ChartInstance

Creates and mounts a chart canvas into element.


Types#

OhlcDataPoint#

ts
type OhlcDataPoint = {
  t: string; // ISO time
  o: number;
  h: number;
  l: number;
  c: number;
  v?: number;
};

ChartOptions#

Top-level options:

  • width (default 720)
  • height (default 360)
  • backgroundColor (default #101114)
  • axisColor (legacy shorthand for axis line/text color)
  • axis?: AxisOptions
  • xAxis?: AxisOptions (overrides bottom-axis label text/font)
  • yAxis?: AxisOptions (overrides right-axis label text/font)
  • priceDecimals (default 2, used for axis/ticker/line price labels)
  • stabilizePriceLabels (default true, prevents ticker/crosshair/price-tag width jitter)
  • priceLabelMinIntegerDigits (default 3, baseline integer-digit width for stabilized labels)
  • priceLabelWidthTemplate (default ""; when set, forces label width from this template, e.g. "88888.88")
  • initialViewport ("latest" | "center", default "latest")
  • initialVisibleBars (default 60)
  • minVisibleBars (default 5, lower clamp for x zoom)
  • maxVisibleBars (default 20000, upper clamp for x zoom)
  • maxPanBars (default 1000000, max bars allowed to pan beyond data)
  • rightEdgePaddingBars (default 2, used by latest-anchored viewport)
  • preserveViewportOnDataUpdate (default true; set false to auto-fit on each setData)
  • upColor (default #2fb171)
  • downColor (default #d35a5a)
  • gridColor (default #252932)
  • fontFamily
  • candleBodyWidthRatio (default 0.7)
  • candleMinWidth (default 0.5)
  • candleWickWidth (default 1)
  • tickSize (default 0; when > 0, formatter/pointer prices snap to tick)
  • candleColorMode ("openClose" | "prevClose", default "openClose")
  • candleColorEpsilon (default -1 = auto from priceDecimals; set 0 to disable tolerance)
  • chartType ("candles" | "hollow-candles" | "heikin-ashi" | "bars" | "line" | "area" | "baseline", default "candles"; main series style. Heikin-Ashi renders smoothed HA candles derived from the raw series — indicators, drawings, order lines and the ticker line keep using raw prices)
  • lineColor (default #2962ff; stroke color for "line" / "area" chart types)
  • lineWidth (default 2; stroke width for "line" / "area" / "baseline")
  • areaFillOpacity (default 0.12; fill under the line for "area" and the half fills for "baseline")
  • baselinePrice (default null; anchor price for "baseline". null anchors to the midpoint of the visible price range, TradingView-style)
  • autoScaleSmoothing (default 0.16)
  • autoScaleIgnoreLatestCandle (default true)
  • pinOutOfRangeLines (default false; when true, horizontal price/order/reference lines outside the visible price range are pinned to the top/bottom chart edge)
  • doubleClickEnabled (default true)
  • doubleClickAction ("reset" | "placeLimitOrder", default "reset")
  • crosshair?: CrosshairOptions
  • grid?: GridOptions
  • watermark?: WatermarkOptions
  • priceLines?: PriceLineOptions[]
  • orderLines?: OrderLineOptions[]
  • tickerLine?: TickerLineOptions
  • labels?: LabelsOptions (TradingView-style price-scale/indicator label controls)
  • dashPatterns?: Partial<DashPatternOptions> (controls dotted/dashed spacing)
  • indicators?: IndicatorInstanceOptions[] (initial indicator instances, built-in includes "volume")
  • drawings?: DrawingObjectOptions[] (initial user drawings/tool objects)

AxisOptions#

  • lineColor (default #3b3f47)
  • textColor (default #a9adb6)
  • fontSize (default 12)
  • lineWidth (default 1)

GridOptions#

  • color (default #2b2f38)
  • opacity (default 0.38)
  • horizontalLines (default true)
  • verticalLines (default true)
  • xTickCount (default 8, x-axis label/grid density)
  • yTickCount (default 6, y-axis label/grid density)
  • horizontalTickCount (legacy alias for yTickCount)

CrosshairOptions#

  • visible (default true)
  • color (default #94a3b8)
  • width (default 1)
  • style ("solid" | "dotted" | "dashed", default "dotted")
  • mode ("cross" | "dot", default "cross"; "dot" draws a dot at the cursor instead of crosshair lines)
  • dotRadius (default 3; dot radius in px when mode is "dot")
  • showHorizontal (default true)
  • showVertical (default true)
  • showPriceLabel (default true)
  • showTimeLabel (default true)
  • timeLabelFormat ("auto" | "date" | "time" | "datetime", default "auto")
  • labelBackgroundColor (default #0b1220)
  • labelTextColor (default #cbd5e1)
  • labelBorderRadius (default 3)
  • labelBorderColor (default #94a3b8)
  • labelBorderWidth (default 1)
  • labelBorderStyle ("solid" | "dotted" | "dashed", default "solid")
  • showPriceActionButton (default false)
  • priceActionButtonIcon ("plus" | "plusThin" | "text", default "plusThin")
  • priceActionButtonText (default "+")
  • priceActionButtonSize (default 16)
  • priceActionButtonGap (default 4)
  • priceActionButtonRounded (default true; set false for square corners)
  • priceActionButtonBorderRadius (default 8)

Note: the button and its container automatically inherit crosshair label colors/border for a consistent [button-box][label] look.

WatermarkOptions#

  • visible (default false)
  • text (default "")
  • color (default #81858d)
  • opacity (default 0.14)
  • fontSize (default 92)
  • fontWeight (default 700)
  • thickness (stroke width, default 0)
  • imageSrc (default "", URL/path to watermark logo)
  • imageScale (default 1)
  • imageMaxWidthRatio (default 0.42)
  • imageMaxHeightRatio (default 0.3)
  • imageTintColor (default "", set "#ffffff" for white logo tint)
  • imageTintOpacity (default 1)

Example:

ts
watermark: {
  visible: true,
  imageSrc: "/logo-white.svg",
  opacity: 0.14,
  imageTintColor: "#ffffff",
  imageTintOpacity: 1
}

TickerLineOptions#

  • visible (default true)
  • style ("solid" | "dotted" | "dashed", default "dotted")
  • thickness (default 1)
  • color (default #38bdf8)
  • labelBackgroundColor (default #38bdf8)
  • labelTextColor (default #0b1220)
  • labelSubtext (optional single extra line inside the current-price label, for example "MNQ")
  • labelSubtexts (optional array of extra lines inside the current-price label, for example ["MNQ", "RTH"])
  • labelSubtextColor (defaults to the label text color)
  • labelSubtextFontSize (default 0, meaning each extra line uses the same font size as the price)
  • showCountdownInLabel (default false; appends bar-close countdown as another extra line)
  • labelBorderRadius (default 3)

LabelsOptions#

TradingView-style labels can be controlled from a single top-level object:

  • visible (default true, master switch for this label layer)
  • symbolName (text for the symbol-name label)
  • showSymbolName (default false)
  • showLastPrice (default true; controls the ticker price tag, while tickerLine.visible still controls the line)
  • showPreviousClose (default false)
  • previousClosePrice (optional; falls back to the prior candle close)
  • showHighLow (default false; labels visible-range high/low)
  • showBidAsk (default false)
  • bidPrice, askPrice (optional market data values for bid/ask labels)
  • showIndicatorNames (default false; draws active indicator names in the chart)
  • showIndicatorValues (default false; appends simple indicator input values)
  • showIndicatorValueLabels (default true; controls separate-pane right-side indicator value tags, such as RSI's latest-value label)
  • indicatorLegendPosition ("top-left" | "top-right" | "bottom-left" | "bottom-right", default "top-left")
  • indicatorLegendOffsetX (default 10)
  • indicatorLegendOffsetY (default 10; increase this if your frontend overlays a symbol/OHLC HUD in the top-left)
  • showCountdownToBarClose (default false; draws a bottom-axis countdown based on candle time spacing)
  • noOverlapping (default true; stacks price-scale labels, price-line tags, order/position price tags, and order widgets so they do not cover each other)
  • Style fields: backgroundColor, textColor, mutedTextColor, symbolNameBackgroundColor, symbolNameTextColor, previousCloseColor, highLowColor, bidColor, askColor, indicatorTextColor, borderRadius, labelHeight, labelPaddingX

Example:

ts
createChart(root, {
  labels: {
    symbolName: "ESH6",
    showSymbolName: true,
    showPreviousClose: true,
    previousClosePrice: 5231.25,
    showHighLow: true,
    showBidAsk: true,
    bidPrice: 5234.75,
    askPrice: 5235.0,
    showIndicatorNames: true,
    showIndicatorValues: true,
    showIndicatorValueLabels: true,
    indicatorLegendPosition: "top-left",
    indicatorLegendOffsetY: 34,
    showCountdownToBarClose: true,
    noOverlapping: true
  }
});

DashPatternOptions#

  • dotted (default [2, 2])
  • dashed (default [8, 6])
  • connectorDotted (default [2, 3])
  • connectorDashed (default [6, 5])
  • borderDotted (default [2, 2])
  • borderDashed (default [6, 4])

PriceLineOptions#

  • id?: string
  • price: number (required)
  • label?: string
  • visible (default true)
  • style ("solid" | "dotted" | "dashed", default "solid")
  • thickness (default 1)
  • color (default #f59e0b)
  • labelBackgroundColor (default #f59e0b)
  • labelTextColor (default #0f172a)
  • labelBorderRadius (default 3)
  • showLabel (default true)
  • pinOutOfRange?: boolean (default false; overrides ChartOptions.pinOutOfRangeLines for this line)

OrderActionButton#

  • text: string (required)
  • action: string (required)
  • draggable?: boolean
  • textColor?: string
  • backgroundColor?: string
  • borderColor?: string
  • borderStyle?: "solid" | "dotted" | "dashed"
  • borderRadius?: number
  • minWidth?: number
  • paddingX?: number
  • fullHeight?: boolean
  • fontWeight?: number | string

OrderLineOptions#

Required fields:

  • type: "market" | "limit" | "stop" | "takeProfit"
  • side: "buy" | "sell"
  • price: number

Common optional fields:

  • id?: string
  • behavior?: "static" | "follow" (default "static")
  • followPrice?: number
  • qty?: number
  • pnl?: number
  • label?: string
  • visible?: boolean (default true)
  • style?: "solid" | "dotted" | "dashed" (default "solid")
  • thickness?: number (default 1)
  • color?: string (default #f59e0b)
  • labelBackgroundColor?: string
  • labelTextColor?: string
  • labelBorderRadius?: number (default 3)
  • showCloseButton?: boolean (default true)
  • widgetPosition?: "left" | "center" | "right" (default "left")
  • widgetPaddingRight?: number (default 10, extra right margin when widgetPosition is "right")
  • draggable?: boolean (default false)
  • pinOutOfRange?: boolean (default false; overrides ChartOptions.pinOutOfRangeLines for this order/position line)

Legacy single action button:

  • actionButtonText?: string
  • actionButtonAction?: string (default "execute")
  • actionButtonTextColor?: string
  • actionButtonBackgroundColor?: string
  • actionButtonBorderRadius?: number
  • actionButtonMinWidth?: number
  • actionButtonPaddingX?: number
  • actionButtonFullHeight?: boolean
  • actionButtonFontWeight?: number | string
  • actionButtonBorderColor?: string
  • actionButtonBorderStyle?: "solid" | "dotted" | "dashed"
  • actionButtonsInnerGap?: number (default 6, spacing between action buttons)
  • actionButtonsGroupGap?: number (default 8, spacing between action-button group and main order widget)

Multi-button actions:

  • actionButtons?: OrderActionButton[] (default [])

Connector/fill visuals:

  • connectorToPrice?: number
  • connectorColor?: string
  • connectorStyle?: "solid" | "dotted" | "dashed" (default "dotted")
  • connectorThickness?: number (default 1)
  • connectorAnchorPaddingRight?: number (default 10)
  • fillToPrice?: number
  • fillColor?: string

IndicatorInstanceOptions#

  • id?: string
  • type: string (registered indicator id, e.g. "volume")
  • visible?: boolean (default true)
  • pane?: "overlay" | "separate"
  • paneHeightRatio?: number (for separate panes; 0.08 to 0.45 recommended)
  • zIndex?: number (render order; lower first)
  • excludeFromAutoscale?: boolean (default true for indicator instances)
  • overlayScaleWeight?: number (0..1; only used when excludeFromAutoscale is false)
  • inputs?: Record<string, unknown> (plugin-specific parameters)

IndicatorPlugin#

  • id: string
  • name: string
  • pane?: "overlay" | "separate" (default "overlay")
  • paneHeightRatio?: number (for separate panes)
  • defaultInputs?: Record<string, unknown>
  • draw(ctx, renderContext, inputs): void | IndicatorPaneRenderInfo

IndicatorRenderContext includes:

  • shared x-window and visible index range (startIndex, endIndex, xStart, xSpan)
  • pane bounds (chartLeft, chartRight, chartTop, chartBottom, chartWidth, chartHeight)
  • xFromIndex(index) helper
  • yFromPrice(price) helper (available for overlay indicators, null for separate-pane indicators)
  • getCandleDirectionByIndex(index) and getVolumeByIndex(index) helpers
  • theme colors (upColor, downColor)

For separate-pane indicators, return IndicatorPaneRenderInfo from draw() when the core should render TradingView-style pane UI:

ts
type IndicatorPaneRenderInfo = {
  title?: string;
  axis?: {
    min: number;
    max: number;
    ticks?: number[];
    decimals?: number;
    format?: (value: number) => string;
  };
  guideLines?: Array<{
    value: number;
    label?: string;
    color?: string;
    style?: "solid" | "dotted" | "dashed";
  }>;
  legendValues?: Array<{
    label?: string;
    value?: number;
    text?: string;
    color?: string;
  }>;
  valueLabels?: Array<{
    value: number;
    text?: string;
    color?: string;
    backgroundColor?: string;
    textColor?: string;
  }>;
};

The core uses this metadata to draw separate-pane right-side axis values, top-left pane legends, guide-level labels, and latest-value tags. Existing plugins can keep returning nothing.

Built-in:

  • "volume": overlay histogram by default (uses OhlcDataPoint.v; can be moved to separate pane)
  • "sma": Simple Moving Average (overlay)
  • "ema": Exponential Moving Average (overlay)
  • "rsi": Relative Strength Index (separate pane, 30/50/70 guides, 0/30/50/70/100 axis labels, latest-value tag)
  • "wma": Weighted Moving Average (overlay)
  • "vwma": Volume Weighted Moving Average (overlay, uses OhlcDataPoint.v)
  • "rma": Wilder's Moving Average (overlay)
  • "hma": Hull Moving Average (overlay)
  • "stddev": Standard Deviation (separate pane)
  • "atr": Average True Range (separate pane)
  • "vwap": Session-anchored VWAP with optional sigma bands (overlay)
  • "bollinger": Bollinger Bands (overlay)
  • "macd": MACD with signal line and zero-anchored histogram (separate pane; inputs fast, slow, signal, colors)

All separate-pane indicators (macd, stochastic, stochrsi, adx, obv, mfi, cci, williamsr, roc, momentum, rsi, atr, stddev) also accept showValueLine: boolean (default true): draws a dotted horizontal line across the pane at each line's latest value, ticker-line style, matching the value tag on the right scale. Toggle it per instance, e.g. chart.updateIndicator(id, { inputs: { showValueLine: false } }).

Separate panes also get, automatically:

  • The main chart's grid: vertical time gridlines run through every pane, and each pane draws horizontal gridlines on round values using the same grid color/opacity options.
  • Draggable dividers: the border above each pane resizes it (cursor turns to a row-resize handle). Hosts can persist the new height via onIndicatorPaneHeightChange.
  • TradingView-style legend controls on hover: eye (hide/show), gear (settings), {} (view source), and X (remove). The eye toggles visibility internally — a hidden indicator collapses to a small legend strip instead of disappearing, so it can be un-hidden in place. Gear/source/remove emit onIndicatorPaneAction events for the host UI; if no handler is registered, X removes the indicator directly.
  • "stochastic": Stochastic %K/%D with 20/80 guides (separate pane; inputs kLength, kSmoothing, dLength)
  • "stochrsi": Stochastic RSI %K/%D (separate pane; inputs rsiLength, stochLength, kSmoothing, dSmoothing)
  • "adx": ADX with optional +DI/-DI lines, Wilder smoothing (separate pane; inputs length, showDi)
  • "obv": On-Balance Volume with compact K/M/B axis labels (separate pane)
  • "mfi": Money Flow Index, 20/80 guides (separate pane; input length)
  • "cci": Commodity Channel Index, ±100 guides (separate pane; input length)
  • "williamsr": Williams %R, -80/-20 guides (separate pane; input length)
  • "roc": Rate of Change % with zero line (separate pane; input length)
  • "momentum": Momentum (close delta) with zero line (separate pane; input length)
  • "psar": Parabolic SAR dots (overlay; inputs start, increment, maximum, color)
  • "supertrend": SuperTrend line colored by trend direction (overlay; inputs atrLength, multiplier, upColor, downColor)
  • "ichimoku": Ichimoku Cloud — Tenkan, Kijun, Senkou A/B with filled kumo, Chikou (overlay; inputs conversionLength, baseLength, spanBLength, displacement, cloudOpacity, showChikou. Senkou spans are displaced within the existing bars; the cloud does not extend past the last bar)
  • "keltner": Keltner Channels — EMA basis ± ATR multiple (overlay; inputs emaLength, atrLength, multiplier)
  • "donchian": Donchian Channels — highest high / lowest low with midline (overlay; input length)

Example:

ts
const emaId = chart.addIndicator("ema", { length: 34, source: "close" });
const rsiId = chart.addIndicator("rsi", { length: 14 }, { pane: "separate", paneHeightRatio: 0.18 });

// resize pane later (useful for drag-handle UX in frontend)
chart.updateIndicator(rsiId, { paneHeightRatio: 0.12 });

// remove when needed
chart.removeIndicator(emaId);

Volume/VWMA note:

  • if your data has no v, volume/vwma will have limited or no output.

Volume style inputs:

  • upOpacity, downOpacity
  • upColor, downColor
  • minBarWidth
  • overlayHeightRatio (when used as overlay)
  • scaleMode ("visible" default, or "full")
  • scaleType ("sqrt" default, or "log" / "linear")
  • clampPercentile (0..1, default 1; e.g. 0.95 to reduce outlier crush)

DrawingObjectOptions#

Drawings are user-created chart tools, separate from indicators. They are interactive chart objects like horizontal lines and trendlines.

  • id?: string
  • type: "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension"
    • horizontal-line / vertical-line: single-point, full-width/full-height line (one click to place)
    • trendline: two-point segment (click start, click end)
    • ray: two-point line that extends infinitely past the second point
    • fib-retracement: two-point retracement with levels/bands
    • fib-extension: trend-based three-point extension (click trend start, trend end, then the projection origin); levels project from the third point by the first→second move
    • long-position / short-position: risk/reward forecasting box (single click drops a default box). Points are [entry, target, stop, rightEdge]; anchors are target/entry/stop on the left and a time-width handle on the right. Labels show price, % move, ticks, and risk/reward ratio. color sets the entry line; colors is [profitColor, lossColor, labelTextColor] (defaults POSITION_DEFAULT_COLORS). Sizing inputs accountSize, lotSize, risk, riskMode ("percent"|"amount"), leverage, pointValue, qtyPrecision drive the Qty/Amount labels — set pointValue (contract $/point) from your app for real money values. A price-range tool (single click drops a default box; two diagonal anchors) measures a region and labels the signed price change, % move and ticks with a vertical arrow. The box also runs a trade simulation across the bars it covers: it shades the traversed region and draws a diagonal to the first bar that touches the target or stop, and the center label switches to "Closed P&L" (green if the target was hit first, red if the stop was hit first).
  • points: DrawingPoint[]
  • visible?: boolean
  • color?: string
  • colors?: string[] (per-level colors; used by fib-retracement. When non-empty, each level/band cycles through these instead of color. New fib drawings default to FIB_DEFAULT_PALETTE; set colors: [] for a monochrome fib that follows color.)
  • style?: "solid" | "dotted" | "dashed"
  • width?: number
  • label?: string

DrawingPoint:

ts
type DrawingPoint = {
  index: number;   // fractional candle index
  price: number;
  time?: string;   // ISO timestamp for reference/persistence
};

Tool workflow:

ts
chart.setActiveDrawingTool("horizontal-line"); // next plot click creates a line
chart.setActiveDrawingTool("vertical-line");   // next plot click creates a vertical line
chart.setActiveDrawingTool("trendline");       // first click starts, second click commits
chart.setActiveDrawingTool("ray");             // first click starts, second click commits (extends past p2)
chart.setActiveDrawingTool("fib-retracement"); // first click starts, second click commits
chart.setActiveDrawingTool("fib-extension");   // three clicks: trend start, trend end, projection origin
chart.setActiveDrawingTool("long-position");   // single click drops a long risk/reward box
chart.setActiveDrawingTool("short-position");  // single click drops a short risk/reward box
chart.setActiveDrawingTool("measure");         // next drag (or click-move-click) measures; see below
chart.setActiveDrawingTool("parallel-channel"); // three clicks: base start, base end, parallel offset
chart.setActiveDrawingTool("ellipse");         // two clicks: opposite corners of the bounding box
chart.setActiveDrawingTool("arrow");           // like trendline, with an arrowhead at the second point
chart.setActiveDrawingTool("brush");           // press and drag to draw freehand; release commits
chart.setActiveDrawingTool("callout");         // two clicks: anchor tip, then text box (opens text editor)
chart.setActiveDrawingTool(null);              // back to normal cursor/pan

New shape/annotation tools:

  • parallel-channel: three-point channel, TradingView style. The first two clicks set the base trendline; the third click sets the parallel offset. Renders both lines, a dashed centre line, and a translucent fill. Handles: the two base anchors plus a third on the parallel line. The offset is constant in price, so the channel stays parallel at any zoom.
  • ellipse: two-anchor bounding-box ellipse, created and resized exactly like rectangle (corner handles, including the derived corners).
  • arrow: a trendline with a filled arrowhead at the second point (head size scales with width). Supports the same Shift 0/45/90° constraint.
  • brush: freehand polyline. Press and drag to draw; release commits. Points are appended only when the cursor moves ≥3px, and strokes render smoothed with quadratic curves. No reshape handles — drag the stroke body to move it.
  • callout: anchored text box. First click drops the anchor (arrow tip), second click places the box, and onDrawingEditText fires so the host can show its text editor. Stores content in label and size in fontSize, like text/note. The connector line + arrowhead re-route to the box edge nearest the anchor.

Use getDrawings() / setDrawings() for persistence.

Measure tool (TradingView-style ruler)#

"measure" is transient — the only tool that never creates a drawing. Drag (or click, move, click) across the plot to get a translucent box with direction arrows and a stats pill: signed price change, % move, tick count (when tickSize is configured), bar count, and wall-clock duration. Blue for an up measurement, red for down.

  • The overlay stays pinned to its bars/prices through pans and zooms.
  • It clears on the next click, on Escape, on cancelDrawing(), or when the active tool changes; it is never emitted through onDrawingsChange.
  • After each measurement the chart reverts to the cursor (one-shot, like TradingView) and reports it via onActiveDrawingToolChange.
  • Shortcut: Shift+drag on the plot measures without arming the tool.

ChartInstance Methods#

  • setData(data: OhlcDataPoint[]): void
  • setChartType(type: ChartType): void — switch the main series style at runtime ("candles" | "hollow-candles" | "heikin-ashi" | "bars" | "line" | "area" | "baseline")
  • getChartType(): ChartType
  • setPriceLines(lines: PriceLineOptions[]): void
  • addPriceLine(line: PriceLineOptions): string
  • removePriceLine(id: string): void
  • setOrderLines(lines: OrderLineOptions[]): void
  • addOrderLine(line: OrderLineOptions): string
  • updateOrderLine(id: string, patch: Partial<OrderLineOptions>): void
  • removeOrderLine(id: string): void
  • onOrderAction(handler: ((event: OrderActionEvent) => void) | null): void
  • onChartClick(handler: ((event: ChartClickEvent) => void) | null): void
  • onCrosshairMove(handler: ((event: CrosshairMoveEvent) => void) | null): void
  • onCrosshairPriceAction(handler: ((event: CrosshairPriceActionEvent) => void) | null): void
  • onIndicatorPaneAction(handler: ((event: IndicatorPaneActionEvent) => void) | null): void — pane-legend hover buttons. Event: { id, type, action: "settings" | "remove" | "source" | "visibility", visible? }. Open your settings dialog on "settings", a code/definition view on "source", and remove the indicator from your own state on "remove"; "visibility" is informational (the chart already toggled it).
  • onIndicatorPaneHeightChange(handler: ((event: IndicatorPaneHeightChangeEvent) => void) | null): void — pane divider drag-resize. Event: { id, paneHeightRatio, dragging }; persist the ratio when dragging === false.
  • zoomInX(factor?: number): void (default factor 1.25)
  • zoomOutX(factor?: number): void (default factor 1.25)
  • zoomInY(factor?: number): void (default factor 1.25)
  • zoomOutY(factor?: number): void (default factor 1.25)
  • panX(bars: number): void (negative = left, positive = right)
  • panY(priceDelta: number): void (positive = move viewport up)
  • fitContent(): void (x-only fit, keeps y zoom)
  • resetViewport(): void (fit x + reset y auto-scale)
  • resetPriceScale(): void — reset only the price axis back to autoscale, keeping the horizontal scroll position. Use this from a price-axis context menu ("Reset price scale" / "Auto"); resetViewport would also discard where the trader scrolled to.
  • isPriceScaleAuto(): boolean — true while the price axis follows the data automatically; false once it has been panned/zoomed by hand (until the next reset).
  • setSelectedDrawing(id: string | null): void (marks a drawing selected; only the selected/drafted drawing renders its handles, and position tools render their lines/labels only while selected)
  • onSelectionChange(handler: ((event: DrawingSelectionChangeEvent | null) => void) | null): void — selection geometry stream for a floating toolbar; see "Context menus, selection toolbars and keyboard" below
  • getSelectedDrawing(): { drawing: DrawingObjectOptions; bounds: DrawingBounds } | null
  • onContextMenu(handler: ((event: ChartContextMenuEvent) => void) | null): void — typed right-click with region + hit info
  • onKeyboardShortcut(handler: ((event: ChartKeyboardShortcutEvent) => void) | null): void
  • focus(): void — focus the canvas so keyboard shortcuts apply
  • onDrawingDoubleClick(handler): void (fires when a drawing is double-clicked; use it to open a settings dialog, e.g. for position tools)
  • onDrawingEditText(handler): void (fires when a text/note/callout tool is placed or double-clicked; use it to show an inline text editor at {x, y} and write the result back via updateDrawing(id, { label })). These drawings store their content in label and size in fontSize.
  • setTradeMarkers(markers: TradeMarkerOptions[]): void — draw trade execution markers (arrow + qty @ price label) on the candle of each fill's time. Buy = arrow below the low pointing up; sell = arrow above the high pointing down. Each marker: { time (ms|ISO), price, side: "buy"|"sell", qty?, text?, color?, textColor? } (defaults: buy #2962ff, sell #f23645, text #d1d4dc). Markers resolve to bars by time, so they persist across timeframe switches and scrolling. Pass [] to clear.
  • cancelDrawing(): boolean — abort an in-progress multi-click drawing (between the first click and the final point, e.g. a half-drawn trendline/ray/fib). Returns true if a draft was cancelled. Wire it to Escape.
  • Hold Shift while drawing or dragging an endpoint of a trendline/ray/arrow to constrain it to the nearest 0/45/90° angle (e.g. a perfectly horizontal line).
  • setMagnetMode(mode): void / getMagnetMode() — magnet/snap for all drawing tools. "none" off, "weak" snaps to a candle's OHLC only when the cursor is near a value, "strong" always snaps to the nearest OHLC of the bar under the cursor. Holding Cmd/Ctrl while drawing/dragging forces strong snapping regardless of the mode.
  • setDrawingsLocked(locked: boolean): void / getDrawingsLocked() — chart-wide "Lock all drawings" toggle (TradingView-style padlock). While on, no drawing can be dragged or point-edited and the Delete/Backspace hotkey is ignored, regardless of each drawing's own locked flag — per-drawing flags are left untouched, so unlocking restores their individual behavior. Drawings stay selectable and hover/select events keep firing; explicit host API calls (updateDrawing, removeDrawing, clearDrawings) still work. Included in saveState() as drawingsLocked. Added in 0.1.163.
  • setTheme(theme: ChartThemeName | ChartTheme): void / getTheme(): string | null — apply a whole palette at once; see "Themes" below
  • setDatafeed(datafeed: ChartDatafeed | null): Promise<void> — attach a pull-based data source with lazy history; see "Datafeed" below
  • onLongPress(handler: ((event: ChartLongPressEvent) => void) | null): void — touch press-and-hold inspection
  • setCompareSeries(series: CompareSeriesOptions[]): void / addCompareSeries(series) / removeCompareSeries(id) / getCompareSeries() — overlay other instruments on the main pane; see "Compare overlays" below
  • setPriceScale(options: { mode?: "linear" | "log" | "percent" | "indexed100"; inverted?: boolean }): void / getPriceScale() — price-scale modes for the main pane (TradingView-style). "log" maps prices through log10 (wide ranges get 1/2/5-per-decade axis marks). "percent" keeps the linear mapping but relabels the axis (ticks, last-price/PDC/H/L/bid/ask tags and the crosshair) as % change from the first visible bar's close, updating as you pan. "indexed100" is the same relabeling written as an index (first visible close = 100, so +2.5% reads 102.50). inverted: true flips the axis top-to-bottom. All viewport state stays in plain price space, so switching modes never moves your zoom, and drawings/orders/indicators follow automatically. Indicator panes keep their own scales. Included in saveState() as priceScale.
  • setActiveDrawingTool(tool: DrawingToolType | null): void (DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note" | "parallel-channel" | "ellipse" | "arrow" | "brush" | "callout" | "measure")
  • getActiveDrawingTool(): DrawingToolType | null
  • onActiveDrawingToolChange(handler: ((tool: DrawingToolType | null) => void) | null): void — fires whenever the active tool changes, including the chart auto-reverting to the cursor after a tool completes (all tools do this when their shape is placed; measure does it after every measurement). Keeps host toolbars in sync without polling.
  • setDrawings(drawings: DrawingObjectOptions[]): void
  • getDrawings(): DrawingObjectOptions[]
  • addDrawing(drawing: DrawingObjectOptions): string
  • updateDrawing(id: string, patch: Partial<DrawingObjectOptions>): void
  • removeDrawing(id: string): void
  • clearDrawings(): void
  • onDrawingsChange(handler: ((drawings: DrawingObjectOptions[]) => void) | null): void
  • setDoubleClickEnabled(enabled: boolean): void
  • setDoubleClickAction(action: "reset" | "placeLimitOrder"): void
  • registerIndicator(plugin: IndicatorPlugin): void
  • unregisterIndicator(type: string): void
  • listBuiltInIndicators(): BuiltInIndicatorInfo[]
  • getIndicators(): IndicatorInstanceOptions[]
  • addIndicator(type: string, inputs?: Record<string, unknown>, options?: Partial<IndicatorInstanceOptions>): string
  • updateIndicator(id: string, patch: Partial<IndicatorInstanceOptions>): void
  • removeIndicator(id: string): void
  • setIndicators(indicators: IndicatorInstanceOptions[]): void
  • saveState(): ChartSavedState — one JSON-serializable snapshot of the full user-visible setup: { version: 1, chartType, viewport, drawings, indicators, magnetMode, priceScale, drawingDefaults, alerts, timezone, timeFormat }. Chart data is not included; keep feeding bars through setData/upsertBar.
  • loadState(state: Partial<ChartSavedState>): void — restore a saveState() blob. Tolerant of partial blobs (missing sections are left untouched), so it doubles as a bulk setter. Register custom script indicator plugins with registerIndicator before calling it so their instances resolve. Viewport is applied last so pane changes don't clobber the restore.
  • takeScreenshot(options?: { type?: "image/png" | "image/jpeg"; quality?: number }): string — returns a data URL of the current frame (default PNG) at the chart's device pixel ratio. Everything on the canvas is included: series, indicators, panes, drawings, legends.
ts
// Persist / restore the user's whole chart setup
localStorage.setItem("layout", JSON.stringify(chart.saveState()));
chart.loadState(JSON.parse(localStorage.getItem("layout")!));

// Download a chart snapshot
const link = document.createElement("a");
link.href = chart.takeScreenshot();
link.download = "chart.png";
link.click();

Time zone and sessions#

  • setTimezone(timezone: ChartTimezone): void / getTimezone(): ChartTimezone — an IANA id, "utc", or "local" (default). Unknown ids fall back to local instead of throwing.
  • setTimeFormat(format: "24h" | "12h"): void / getTimeFormat().
  • setSession(session: SessionOptions | SessionSpec | SessionPresetName | null): void / getSession(): SessionOptions.

Undo / redo#

  • undo(): boolean / redo(): boolean — return false when the stack is empty.
  • canUndo(): boolean / canRedo(): boolean / getUndoRedoState(): UndoRedoState.
  • onUndoRedoStateChange(handler) / clearHistory(): void.

Bar replay#

  • startReplay(options?: ReplayStartOptions): void / stopReplay(): void
  • replayStep(bars?: number): void — negative rewinds.
  • replaySeek(index: number): void — absolute jump, clamped to the series.
  • replayPlay(speed?: number): void / replayPause(): void / setReplaySpeed(speed: number): void
  • getReplayState(): ReplayState / onReplayStateChange(handler)

Price alerts#

  • setAlerts(alerts: AlertOptions[]): void / addAlert(alert): string / updateAlert(id, patch) / removeAlert(id) / getAlerts()
  • onAlertTrigger(handler) / onAlertAction(handler)

Marks#

  • setBarMarks(marks: BarMarkOptions[]): void / getBarMarks()

  • setTimescaleMarks(marks: TimescaleMarkOptions[]): void / getTimescaleMarks()

  • onMarkClick(handler) / onMarkHover(handler)

  • resize(width?: number, height?: number): void

  • destroy(): void


Futures desk indicators#

Three built-ins aimed at intraday futures work. All three are overlays on the main pane, and the last two need a session spec (setSession) — without one they have no notion of a trading day and render nothing.

volume-profile#

Volume distribution across price, with point of control and value area.

ts
chart.addIndicator("volume-profile", { scope: "visible", rows: 40, widthPercent: 28 });
chart.addIndicator("volume-profile", { scope: "session" }); // one per trading day

scope: "visible" profiles whatever is on screen and follows pan/zoom, drawn against the price axis; scope: "session" draws a separate profile inside each trading day's bars, skipping any session too narrow to read. Rows are split into buy and sell volume by each bar's own direction, shaded brighter inside the value area. Inputs: rows, widthPercent, placement ("right" | "left", visible scope only), valueAreaPercent, upColor, downColor, valueAreaOpacity, outsideOpacity, showPoc, pocColor, showValueArea, valueAreaColor, showLabels.

OHLCV bars carry no intrabar detail, so each bar's volume is spread evenly over the rows its high–low range touches — the same approximation TradingView makes when building a profile from candles rather than ticks.

session-levels#

The reference prices marked up before the open: prior-day high/low/close (PDH/PDL/PDC), overnight high/low (ONH/ONL), the opening range (ORH/ORL) and optionally the initial balance (IBH/IBL, first hour).

ts
chart.setSession({ spec: "cme-futures" });
chart.addIndicator("session-levels", { openingRangeMinutes: 30, showInitialBalance: true });

Cash-session bounds are inputs (rthStart / rthEnd, default 09:30–16:00) rather than being read from the chart's session spec: a CME futures session runs 18:00–17:00, but the levels traders mark are relative to the 09:30 equity open inside it. Everything in the current session outside those hours counts as overnight.

Fixed-range volume profile (drawing tool)#

The third profile variant is a drawing rather than an indicator, because it needs a range: arm fixed-range-volume-profile and click twice to bracket the bars. Only the anchors' bar indexes matter — the price extent comes from the bars they enclose.

ts
chart.setActiveDrawingTool("fixed-range-volume-profile");
chart.setDrawingDefaults("fixed-range-volume-profile", {
  color: "#5b8def",
  profileRows: 24,
  profileValueAreaPercent: 70,
  profileWidthRatio: 0.5
});

It draws the selected span, the volume rows (buy/sell split, brighter inside the value area) growing right from the range start, and POC/VAH/VAL. Being a drawing, it is draggable, undoable and part of saveState() like any other.

anchored-vwap#

VWAP measured from a bar you choose rather than the session open.

ts
chart.addIndicator("anchored-vwap", {
  anchorTime: "2026-07-14T13:30:00Z", // epoch ms or ISO
  showBands: true,
  bandMultiplier: 2
});

Without an anchorTime it anchors to the most recent session start, which is just the session VWAP. Add one instance per anchor; hosts typically expose this as a "start here" action on the bar under the cursor. Volume-less feeds fall back to an unweighted running average rather than flatlining.


Time zones and trading sessions#

Timestamps are rendered in whatever zone you choose, and the chart can be told the instrument's actual trading hours. Both matter most for futures: a CME day opens at 18:00 ET the previous calendar evening, so calendar-day boundaries draw separators in the wrong places.

ts
const chart = createChart(el, {
  timezone: "America/New_York", // IANA id, "utc", or "local" (default)
  timeFormat: "24h",            // or "12h"
  session: {
    spec: "cme-futures",        // preset name, your own SessionSpec, or null
    separators: true,           // vertical rule at each session open
    highlightOutOfSession: true // tint overnight/extended-hours bars
  }
});

chart.setTimezone("Asia/Tokyo");
chart.setSession({ spec: "cme-rth" }); // 09:30–16:00 ET

Presets (SESSION_PRESETS): cme-futures (Sun–Thu 18:00 → 17:00 ET, including the daily maintenance break), cme-rth, us-equities, us-equities-extended, 24x7. A custom spec is three fields:

ts
chart.setSession({
  spec: {
    timezone: "America/New_York",
    // Days the session OPENS. For a window that runs past midnight this is the
    // evening side, so Sun–Thu means Friday evening is closed for the weekend.
    days: [0, 1, 2, 3, 4],
    segments: [{ start: "18:00", end: "17:00" }]
  },
  highlightOutOfSession: true
});

Session separators only apply to intraday series (on daily bars every bar is its own day). Zone conversion is offset-cached per UTC hour, so DST transitions are exact and per-bar lookups stay arithmetic rather than Intl calls.


Undo / redo#

Covers the drawing document: create, move, restyle, delete, clear. A whole drag collapses into one step, and Ctrl/Cmd+Z (redo: Ctrl/Cmd+Shift+Z or Ctrl+Y) works whenever the chart canvas has focus.

ts
chart.onUndoRedoStateChange((state) => {
  undoButton.disabled = !state.canUndo;
  undoButton.title = state.undoLabel ?? "";   // e.g. "Move trendline"
});
chart.undo();

Deliberately not recorded: viewport changes (panning is navigation, not an edit — TradingView made the same call in v30) and bulk setDrawings syncs, so a host that echoes onDrawingsChange straight back never fills the stack with its own round-trips. Indicators are excluded too: hosts usually own that state, and undoing it here would be overwritten on the next sync. loadState() clears the history, since a restored layout is a starting point rather than a step.


Bar replay#

Hide everything after a chosen bar and step forward through it — the same practice loop TradingView's replay gives you, which the Advanced Charts package itself does not ship. Indicators, drawings and autoscale all see only the revealed bars, so there is no lookahead.

ts
chart.startReplay({ fromTimeMs: Date.parse("2026-07-14T13:30:00Z"), speed: 4 });
chart.replayPlay();          // 4 bars per second
chart.replayStep(1);         // or step by hand
chart.replayStep(-1);        // rewind
chart.replaySeek(1200);      // jump straight to a bar (scrubbers, "replay from here")
chart.onReplayStateChange((s) => {
  label.textContent = `${s.index + 1}/${s.total}${s.atEnd ? " (end)" : ""}`;
});
chart.stopReplay();          // restores the full series

Live bars arriving during replay are buffered rather than shown, and are all present when replay stops. Without fromIndex/fromTimeMs replay starts 70% of the way through the series.

While replay is active the chart also takes the arrow keys and Space: arrows step the tape (Shift steps 10) instead of panning, and Space toggles playback. Both revert to normal as soon as replay stops, and keyboard.replay: false turns them off. Stepping one bar at a time is the wrong tool for covering distance, so pair them with replaySeek for a scrubber:

ts
scrubber.oninput = () => chart.replaySeek(Number(scrubber.value));

Price alerts#

Watch levels drawn as a dashed line plus a bell tag on the price axis. The chart renders them, lets the user drag them and remove them, and tells you when price satisfies the condition; delivery (push, sound, email) stays with the host.

ts
chart.addAlert({ price: 5120.25, condition: "crossing-up", label: "breakout" });

chart.onAlertTrigger(({ alert, price, timeMs }) => {
  notify(`${alert.label} hit ${price}`);
});
chart.onAlertAction(({ alert, action, price, dragging }) => {
  if (action === "move" && !dragging) persist(alert.id, price);
  if (action === "remove") forget(alert.id);
});

Conditions: crossing (either direction), crossing-up, crossing-down, greater, less. Crossings compare against the last price the alert saw, so an alert armed mid-bar fires on the tick that crosses it instead of waiting for a close. once (default true) disarms after firing; the line stays visible but dimmed until you re-arm it with updateAlert(id, { triggered: false }). Dragging a level re-arms it against the current market.


Marks (events on bars and on the time axis)#

ts
chart.setBarMarks([
  { id: "cpi", time: "2026-07-15T12:30:00Z", label: "C", text: "CPI 0.2% m/m", color: "#f7a600" }
]);
chart.setTimescaleMarks([
  { id: "fomc", time: "2026-07-29T18:00:00Z", label: "F", text: "FOMC statement", shape: "diamond" }
]);
chart.onMarkClick(({ mark, clientX, clientY }) => openPopover(mark, clientX, clientY));
chart.onMarkHover((event) => (event ? showTooltip(event) : hideTooltip()));

Bar marks pin under (or above) the bar their timestamp falls on, or at an exact price. Time-axis marks sit at the foot of the plot. Both accept circle | square | diamond | flag, expose hover and click, and are host data — they are not part of saveState().


Multi-chart layouts#

Several charts are just several createChart calls; what the library adds is the piece hosts can't do themselves — mirroring one chart's crosshair onto the others:

ts
primary.onCrosshairMove((event) => {
  const ms = event.time ? Date.parse(event.time) : NaN;
  const at = event.region === "plot" && Number.isFinite(ms) ? { timeMs: ms } : null;
  for (const other of siblings) other.setExternalCrosshair(at);
});

Mirrored crosshairs match by timestamp, not pixels, so charts on different intervals — or different instruments — still line up, and the ghost line is drawn dimmer than the chart's own. It is suppressed while that chart is the one being hovered, so the two never fight.

Time-range sync needs nothing new: feed onViewportChange into the siblings' setViewport({ centerTimeMs, xSpan }).


Overlay indicator legend#

With labels.showIndicatorNames / showIndicatorValues, overlay indicators are listed top-left, one row each. Hovering a row reveals the same four controls the separate panes have — eye, gear, {}, X — and they emit the same onIndicatorPaneAction event, so a host that already handles pane actions gets the overlay legend for free:

ts
chart.updateOptions({ labels: { showIndicatorNames: true, showIndicatorValues: true } });
chart.onIndicatorPaneAction(({ id, action }) => {
  if (action === "remove") store.removeIndicator(id);
  if (action === "settings") store.openSettings(id);
});

The eye also toggles visibility internally so it works without a host, and hidden indicators render their row dimmed. Position and offsets come from labels.indicatorLegendPosition / indicatorLegendOffsetX / OffsetY.


In-chart data line#

The symbol/OHLC line TradingView draws in the top-left of the plot. Hosts have been building this in DOM and re-rendering it on every crosshair move; drawn in-canvas it just follows the frame, inherits the theme's colors, and costs nothing per pointer move.

ts
createChart(el, {
  dataLine: {
    visible: true,
    symbol: "NQU6",
    interval: "1h",
    exchange: "CME",
    showVolume: true,
    statusColor: marketOpen ? "#22c55e" : "#ef4444",
    details: [
      { label: "Session", value: "RTH" },
      { value: "Closed", color: "#fff", background: "rgba(242,54,69,0.85)" },
    ],
  },
});

Values track the hovered bar and fall back to the latest one, the change is colored by direction, and the indicator legend moves down to sit under it. Detail chips are dropped rather than clipped when the row runs out of width. Off by default so hosts with their own overlay don't suddenly render two.

Viewport transitions#

The discrete jumps — resetViewport(), fitContent(), setFollowingLatest(true) and the Home/End/R shortcuts — ease into place over animation.durationMs (default 260) instead of teleporting, so it stays obvious which way the chart moved. Zoom interpolates geometrically, so each frame changes scale by the same ratio rather than appearing to accelerate.

Continuous gestures (drag, wheel, pinch) are never animated — they already track the input — and any pointer interaction cancels a transition in flight. The whole thing is skipped when the OS asks for reduced motion, or with animation: { viewportTransitions: false }.

Indicator recomputation#

Indicator series are memoised per (indicator, inputs, series fingerprint). A live tick changes the fingerprint, which used to mean every indicator recomputed its whole series on every animation frame — measured at 50k bars with 10 indicators, that was ~11ms of the frame budget and dropped the chart to 51fps.

While only the forming bar is ticking, recomputation is now throttled to indicatorUpdate.liveThrottleMs (default 80ms). The values are always exact — this changes how often the live bar's output refreshes, never how it is computed — and anything structural (a bar closing, history prepending, inputs or length changing) recomputes immediately, so closed bars are never stale. Same benchmark after: 8.4ms and 118fps. Set liveThrottleMs: 0 to recompute on every frame.

Themes#

A theme is the chart's whole palette as one object, so hosts stop setting a dozen colors individually and re-pushing them on every light/dark toggle.

ts
import { CHART_THEMES, createChart } from "@hyperprop/charting-library";

chart.setTheme("light");                       // built-in preset
chart.setTheme({ ...CHART_THEMES.dark, upColor: "#00c853" });  // tweak one
chart.getTheme();                              // "light" | null when hand-set

Five presets ship in CHART_THEMES:

Preset Surface Candles For
midnight #171717 #22ab94 / #f23645 the default dark look
light #ffffff #22ab94 / #f23645 same candles on a white surface
brand #0b1220 #22ab94 / #f23645 Hyperprop blue accents (#253cff)
dark #0f0f0f #089981 / #f23645 neutral near-black, no tint
high-contrast #000000 #00e5a0 / #ff4d5e projectors, low vision

setTheme covers background, candles, the line/area series, grid (color and opacity), both axes, the crosshair and its labels, the watermark, and indicator legend text. It deliberately does not touch order-line colors or drawing defaults — those are trading state, not chrome, so switching looks never changes what your orders look like mid-session.

getTheme() returns the active preset name, or null once any themed color is set by hand. Unrelated updateOptions calls (label toggles, sizing, ticker line) don't clear it, so it stays a reliable "am I on a preset" signal.

Datafeed (optional, pull-based)#

By default the host pushes bars in with setData/upsertBar. Attach a datafeed instead and the chart pulls what it needs — including older history as the user scrolls left, which every host otherwise hand-rolls as infinite scroll. The shape follows TradingView's datafeed closely enough to port an existing adapter.

ts
await chart.setDatafeed({
  onReady: async () => { /* optional, awaited once before the first getBars */ },
  getBars: async ({ fromMs, toMs, countBack, firstRequest }) => {
    // countBack is authoritative; fromMs is a hint derived from bar spacing.
    return fetchBars({ symbol, to: toMs, limit: countBack });
  },
  subscribeBars: (onBar) => {
    const socket = openStream(symbol, onBar);
    return () => socket.close();     // teardown, called on replace/destroy
  },
  onError: (error, request) => report(error, request),
});
  • The chart loads initialVisibleBars × 3 bars, then calls subscribeBars and upserts every streamed bar.
  • Lazy history: when the viewport comes within datafeedOptions.prefetchThresholdBars (default 150) of the oldest loaded bar, the chart requests the next chunkBars (default 1500) and merges them, re-anchoring on time so the view doesn't jump. A cooldownMs (default 1200) keeps a fast scroll from stampeding the backend.
  • Return an empty array to mean "no more history" — the chart stops asking until a new datafeed is attached.
  • setData/upsertBar keep working while a datafeed is attached, so hosts can migrate one surface at a time. setDatafeed(null) detaches and unsubscribes.

Touch#

Touch input gets its own ergonomics, configured with touch in ChartOptions:

  • Bigger targets. Every drawing, handle and pane-divider tolerance is multiplied by hitToleranceScale (default 2.2) while the last input was touch or pen, so a line you can grab within 7px with a mouse is grabbable within ~15px with a finger. Mouse precision is untouched, and the mode flips back the moment a mouse moves.
  • Press-and-hold to inspect. Holding still on the plot for longPressMs (default 400) puts the crosshair on that bar and draws an OHLC tooltip (time, O/H/L/C, change with %, volume) offset clear of the fingertip. The finger then scrubs — it moves the reading instead of panning the chart — until it lifts, which clears both. Turn the built-in tooltip off with longPressTooltip: false; subscribe with onLongPress(handler) to drive your own UI (the event carries the bar, price, index and client coords, and scrubbing distinguishes the initial press from the drag that follows).

Compare overlays (multi-series)#

Overlay other instruments on the main pane. The host owns the data (it already has a datafeed); the chart aligns each series to the main bars by timestamp and draws it.

ts
chart.setCompareSeries([
  { id: "NQ", label: "NQ1!", data: nqBars, color: "#22d3ee" },
  { id: "BTC", label: "BTCUSD", data: btcBars, color: "#a855f7", style: "area" },
]);
  • Alignment is by timestamp, computed once per data change (a merge walk, not a search per bar), so different session hours and holidays are fine — the last known value carries forward across gaps, and bars before a compared instrument starts are simply not drawn.
  • scale defaults to "percent": both instruments are normalised to their first visible bar, so a $180 index and a $60,000 coin can be compared by shape. It re-anchors as you pan, exactly like TradingView. Use "price" to plot raw values on the main scale (only sensible for related instruments).
  • Autoscale includes compare series unless includeInAutoScale: false.
  • Each series gets a colored tag in the price gutter showing its own % move (or value in price mode); tags stack instead of overlapping, and drop the label when the gutter is too narrow.
  • Compare data is not in saveState() — it can be megabytes and the host re-fetches it anyway. Persist the symbol list yourself and re-supply data on load.

Deep zoom-out (viewport downsampling)#

Once bars are narrower than downsampling.thresholdPx (default 1.5px), several bars share a pixel column, and drawing each one separately is wasted work. The chart aggregates each column into one min/max bucket, so path operations scale with the chart's width rather than the number of bars. Extremes are preserved exactly — a one-bar spike still reaches full height — because the bucket keeps the true high and low rather than sampling. Candle and bar styles converge to a single colored high-low column (the body is sub-pixel at that density anyway); line/area/baseline keep a min/max envelope so the shape of the move survives.

Measured on the playground benchmark (50,000 bars, 120Hz display):

Scenario Per-bar (before) Downsampled
zoom 100 ↔ 20k bars 21.3ms avg, 49.8ms p95, 47fps, 53 slow frames 8.3ms avg, 10.3ms p95, 120fps, 0 slow frames
deep zoom-out, 50k bars 52.7ms avg, 58.8ms p95, 19fps, 240 slow frames 8.3ms avg, 10.2ms p95, 120fps, 0 slow frames

Turn it off with downsampling: { enabled: false }, or tune when it kicks in with thresholdPx.

Context menus, selection toolbars and keyboard#

Three pieces of chrome that every host previously rebuilt by hand. The chart does the hit-testing and geometry; the host still renders its own menus and toolbar, so it keeps full control of styling and framework.

onContextMenu(handler)#

Right-click anywhere on the chart. The browser's native menu is always suppressed. The event says what was under the pointer, so a host switches on region instead of re-deriving it from coordinates:

ts
chart.onContextMenu((event) => {
  switch (event.region) {
    case "drawing":
      // event.drawing / drawingTarget / pointIndex — the chart has already
      // selected this drawing, exactly like TradingView.
      showDrawingMenu(event.clientX, event.clientY, event.drawing!);
      break;
    case "indicator-pane":
      showIndicatorMenu(event.clientX, event.clientY, event.indicator!); // { id, type }
      break;
    case "y-axis":
      showPriceScaleMenu(event.clientX, event.clientY); // log / percent / invert
      break;
    case "x-axis":
      showTimeScaleMenu(event.clientX, event.clientY);
      break;
    case "plot":
      showChartMenu(event.clientX, event.clientY, event.price); // + index/time/point
      break;
  }
});

clientX/clientY are viewport coordinates, ready to position a DOM menu; x/y are canvas-relative. price, index, time and point describe the bar under the pointer (price only in the main pane).

onSelectionChange(handler) — floating drawing toolbar#

Fires when a drawing is selected or deselected (payload null) and on every frame where the selected drawing's box moves — drag, resize, pan, zoom. That is what lets a host toolbar sit above the shape and track it, instead of floating at a fixed corner:

ts
chart.onSelectionChange((event) => {
  if (!event) return hideToolbar();
  const rect = container.getBoundingClientRect();
  // Above the shape, flipping below when there's no room.
  const above = rect.top + event.bounds.top - toolbarHeight - 10;
  positionToolbar({
    left: rect.left + event.bounds.centerX - toolbarWidth / 2,
    top: above > rect.top + 8 ? above : rect.top + event.bounds.bottom + 10,
  });
  showToolbarFor(event.drawing);
});

bounds is { left, top, right, bottom, centerX, centerY } in canvas pixels, clamped to the plot; plot gives the plot rectangle for keeping the toolbar inside the chart; dragging is true mid-drag (dim or lock the toolbar if you prefer it out of the way). getSelectedDrawing() returns the same { drawing, bounds } on demand, and DrawingSelectEvent now carries bounds too.

Keyboard shortcuts and accessibility#

The canvas is focusable (tabIndex 0, role="application", aria-label, aria-keyshortcuts) and takes focus on pointer-down, so shortcuts apply to the chart the user is actually using and never swallow typing in host inputs. The focus ring only shows for keyboard focus.

Key Action
/ Pan one bar (Shift = 10)
/ Pan price by 5% of the visible range (Shift = 5×)
+ / - Zoom in / out
Delete / Backspace Delete the selected drawing
Escape Cancel a half-drawn shape, then the armed tool, then the selection
Home Fit all bars
End Scroll to realtime
R Reset the viewport

Chords with Cmd/Ctrl/Alt are ignored so host and browser shortcuts still work. Configure with keyboard: { enabled, panBars, deleteSelectedDrawing } and accessibility: { label, description, focusable } in ChartOptions, and subscribe to onKeyboardShortcut(handler) to mirror actions in your own UI (the event includes the removed drawing for "delete-drawing", so hosts can offer undo). focus() puts keyboard focus on the chart programmatically.


Script Indicators (user-authored)#

compileScriptIndicator(definition: ScriptIndicatorDefinition): IndicatorPlugin — compiles a user-authored "Hyperprop Script" into a regular indicator plugin. Register the result with chart.registerIndicator(...) and add it with chart.addIndicator(definition.id). Separate-pane scripts automatically get the shared grid, legend, live values, value lines, drag-resize and the hover controls (eye / gear / source / delete). Overlay scripts participate in price autoscale.

ts
import { createChart, compileScriptIndicator } from "@hyperprop/charting-library";

const plugin = compileScriptIndicator({
  id: "script:my-rsi",
  name: "My RSI",
  pane: "separate", // or "overlay"
  inputs: [
    { key: "length", label: "Length", type: "number", default: 14 },
    { key: "color", label: "Color", type: "color", default: "#ab47bc" }
  ],
  source: `
function compute(bars, inputs, hp) {
  const closes = bars.map(b => b.c);
  const gains = closes.map((c, i) => i === 0 ? null : Math.max(0, c - closes[i - 1]));
  const losses = closes.map((c, i) => i === 0 ? null : Math.max(0, closes[i - 1] - c));
  const avgGain = hp.rma(gains, inputs.length);
  const avgLoss = hp.rma(losses, inputs.length);
  const rsi = avgGain.map((g, i) => {
    const l = avgLoss[i];
    if (g == null || l == null) return null;
    return l === 0 ? 100 : 100 - 100 / (1 + g / l);
  });
  return {
    plots: [{ title: "RSI", values: rsi, color: inputs.color }],
    range: { min: 0, max: 100 },
    guides: [30, 70]
  };
}`
});

chart.registerIndicator(plugin);
chart.addIndicator("script:my-rsi");

Script contract:

  • The source must define function compute(bars, inputs, hp).
  • bars: Array<{ time: Date, o, h, l, c, v? }> — the full series.
  • inputs: resolved input values (defaults merged with per-instance overrides). showValueLine is injected automatically and honored by separate panes.

Pine-style input declarations#

Instead of maintaining a separate JSON inputs schema, scripts can declare their settings inline at the top level — the same idea as TradingView's input.int(14, "Length"). The declared constants always hold the live settings values, and hosts can auto-generate the settings dialog from the declarations:

ts
const length = input.int("Length", 14, { min: 1, max: 500 });
const source = input.source("Source", "close"); // open/high/low/close/hl2/hlc3/ohlc4
const smooth = input.bool("Smooth", true);
const color  = input.color("Line color", "#f59e0b");

function compute(bars, inputs, hp) {
  const values = bars.map(b => source === "open" ? b.o : b.c);
  const line = smooth ? hp.ema(values, length) : hp.sma(values, length);
  return { plots: [{ title: "MA", values: line, color }] };
}

Available declarations (all return the resolved value; opts is { key?, min?, max?, step? }):

  • input.int(label, default, opts?) / input.float(label, default, opts?) → number
  • input.bool(label, default?) → boolean
  • input.color(label, default) → color string
  • input.string(label, default?) → string
  • input.select(label, default, choices) → string (choices: string[] or {value, label}[])
  • input.source(label?, default?) → one of open/high/low/close/hl2/hlc3/ohlc4

Settings keys default to a slug of the label ("Line color"line_color); pass { key: "..." } to pin one explicitly (keeps saved settings stable if you rename the label).

extractScriptInputs(source: string): { inputs: ScriptIndicatorInputDef[], error: string | null } — runs the script's top level with a recording helper and returns the declared schema; hosts use this to auto-generate settings dialogs. The legacy JSON inputs array on ScriptIndicatorDefinition still works; on key collisions the inline declarations win.

  • hp: TA helpers, all (values: Array<number|null>, length) => Array<number|null> unless noted: sma, ema, rma, highest, lowest, stdev, change(values, length = 1), atr(bars, length).
  • Return { plots, range?, guides?, decimals? }:
    • plots: Array<{ title?, values, color?, width?, style?: "line" | "histogram", negativeColor? }>
    • range: { min?, max? } fixes the pane scale (e.g. 0..100 for oscillators).
    • guides: dashed horizontal guide levels (e.g. [30, 70]).
    • decimals: axis/legend decimal places.

Errors never break the chart: compile or runtime errors render as an inline red message in the pane. Scripts are plain JavaScript executed in the page context — treat them like any other code you'd paste into your app (only run scripts you trust).

Execution budget#

Loop conditions are instrumented at compile time, and each compute() invocation gets a 1000 ms wall-clock budget. A script that exceeds it (e.g. while (true) {}) throws "Script exceeded the 1000ms execution budget", which renders as the usual inline pane error instead of freezing the tab. After a budget trip the script is circuit-broken — it is not run again for that compiled instance, so live ticks don't re-trigger the stall. Recompiling (editing the source and calling compileScriptIndicator again) resets the breaker. String/template/comment contents are never instrumented; for...of / for...in loops are left untouched.

validateScriptSource(source: string): string | null — compile-checks a script without registering it, returning an error message or null. It runs under the same execution budget, so validating a script with a top-level infinite loop returns a budget error instead of hanging the caller. Use this for editor save-validation instead of running new Function yourself.