API Reference
Main Entry#
createChart(element: HTMLElement, options?: ChartOptions): ChartInstance
Creates and mounts a chart canvas into element.
Types#
OhlcDataPoint#
type OhlcDataPoint = {
t: string; // ISO time
o: number;
h: number;
l: number;
c: number;
v?: number;
};ChartOptions#
Top-level options:
width(default720)height(default360)backgroundColor(default#101114)axisColor(legacy shorthand for axis line/text color)axis?: AxisOptionsxAxis?: AxisOptions(overrides bottom-axis label text/font)yAxis?: AxisOptions(overrides right-axis label text/font)priceDecimals(default2, used for axis/ticker/line price labels)stabilizePriceLabels(defaulttrue, prevents ticker/crosshair/price-tag width jitter)priceLabelMinIntegerDigits(default3, 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(default60)minVisibleBars(default5, lower clamp for x zoom)maxVisibleBars(default20000, upper clamp for x zoom)maxPanBars(default1000000, max bars allowed to pan beyond data)rightEdgePaddingBars(default2, used by latest-anchored viewport)preserveViewportOnDataUpdate(defaulttrue; setfalseto auto-fit on eachsetData)upColor(default#2fb171)downColor(default#d35a5a)gridColor(default#252932)fontFamilycandleBodyWidthRatio(default0.7)candleMinWidth(default0.5)candleWickWidth(default1)tickSize(default0; when > 0, formatter/pointer prices snap to tick)candleColorMode("openClose" | "prevClose", default"openClose")candleColorEpsilon(default-1= auto frompriceDecimals; set0to 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(default2; stroke width for"line"/"area"/"baseline")areaFillOpacity(default0.12; fill under the line for"area"and the half fills for"baseline")baselinePrice(defaultnull; anchor price for"baseline".nullanchors to the midpoint of the visible price range, TradingView-style)autoScaleSmoothing(default0.16)autoScaleIgnoreLatestCandle(defaulttrue)pinOutOfRangeLines(defaultfalse; whentrue, horizontal price/order/reference lines outside the visible price range are pinned to the top/bottom chart edge)doubleClickEnabled(defaulttrue)doubleClickAction("reset"|"placeLimitOrder", default"reset")crosshair?: CrosshairOptionsgrid?: GridOptionswatermark?: WatermarkOptionspriceLines?: PriceLineOptions[]orderLines?: OrderLineOptions[]tickerLine?: TickerLineOptionslabels?: 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(default12)lineWidth(default1)
GridOptions#
color(default#2b2f38)opacity(default0.38)horizontalLines(defaulttrue)verticalLines(defaulttrue)xTickCount(default8, x-axis label/grid density)yTickCount(default6, y-axis label/grid density)horizontalTickCount(legacy alias foryTickCount)
CrosshairOptions#
visible(defaulttrue)color(default#94a3b8)width(default1)style("solid" | "dotted" | "dashed", default"dotted")mode("cross" | "dot", default"cross";"dot"draws a dot at the cursor instead of crosshair lines)dotRadius(default3; dot radius in px whenmodeis"dot")showHorizontal(defaulttrue)showVertical(defaulttrue)showPriceLabel(defaulttrue)showTimeLabel(defaulttrue)timeLabelFormat("auto" | "date" | "time" | "datetime", default"auto")labelBackgroundColor(default#0b1220)labelTextColor(default#cbd5e1)labelBorderRadius(default3)labelBorderColor(default#94a3b8)labelBorderWidth(default1)labelBorderStyle("solid" | "dotted" | "dashed", default"solid")showPriceActionButton(defaultfalse)priceActionButtonIcon("plus" | "plusThin" | "text", default"plusThin")priceActionButtonText(default"+")priceActionButtonSize(default16)priceActionButtonGap(default4)priceActionButtonRounded(defaulttrue; setfalsefor square corners)priceActionButtonBorderRadius(default8)
Note: the button and its container automatically inherit crosshair label colors/border for a consistent [button-box][label] look.
WatermarkOptions#
visible(defaultfalse)text(default"")color(default#81858d)opacity(default0.14)fontSize(default92)fontWeight(default700)thickness(stroke width, default0)imageSrc(default"", URL/path to watermark logo)imageScale(default1)imageMaxWidthRatio(default0.42)imageMaxHeightRatio(default0.3)imageTintColor(default"", set"#ffffff"for white logo tint)imageTintOpacity(default1)
Example:
watermark: {
visible: true,
imageSrc: "/logo-white.svg",
opacity: 0.14,
imageTintColor: "#ffffff",
imageTintOpacity: 1
}TickerLineOptions#
visible(defaulttrue)style("solid" | "dotted" | "dashed", default"dotted")thickness(default1)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(default0, meaning each extra line uses the same font size as the price)showCountdownInLabel(defaultfalse; appends bar-close countdown as another extra line)labelBorderRadius(default3)
LabelsOptions#
TradingView-style labels can be controlled from a single top-level object:
visible(defaulttrue, master switch for this label layer)symbolName(text for the symbol-name label)showSymbolName(defaultfalse)showLastPrice(defaulttrue; controls the ticker price tag, whiletickerLine.visiblestill controls the line)showPreviousClose(defaultfalse)previousClosePrice(optional; falls back to the prior candle close)showHighLow(defaultfalse; labels visible-range high/low)showBidAsk(defaultfalse)bidPrice,askPrice(optional market data values for bid/ask labels)showIndicatorNames(defaultfalse; draws active indicator names in the chart)showIndicatorValues(defaultfalse; appends simple indicator input values)showIndicatorValueLabels(defaulttrue; 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(default10)indicatorLegendOffsetY(default10; increase this if your frontend overlays a symbol/OHLC HUD in the top-left)showCountdownToBarClose(defaultfalse; draws a bottom-axis countdown based on candle time spacing)noOverlapping(defaulttrue; 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:
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?: stringprice: number(required)label?: stringvisible(defaulttrue)style("solid" | "dotted" | "dashed", default"solid")thickness(default1)color(default#f59e0b)labelBackgroundColor(default#f59e0b)labelTextColor(default#0f172a)labelBorderRadius(default3)showLabel(defaulttrue)pinOutOfRange?: boolean(defaultfalse; overridesChartOptions.pinOutOfRangeLinesfor this line)
OrderActionButton#
text: string(required)action: string(required)draggable?: booleantextColor?: stringbackgroundColor?: stringborderColor?: stringborderStyle?: "solid" | "dotted" | "dashed"borderRadius?: numberminWidth?: numberpaddingX?: numberfullHeight?: booleanfontWeight?: number | string
OrderLineOptions#
Required fields:
type: "market" | "limit" | "stop" | "takeProfit"side: "buy" | "sell"price: number
Common optional fields:
id?: stringbehavior?: "static" | "follow"(default"static")followPrice?: numberqty?: numberpnl?: numberlabel?: stringvisible?: boolean(defaulttrue)style?: "solid" | "dotted" | "dashed"(default"solid")thickness?: number(default1)color?: string(default#f59e0b)labelBackgroundColor?: stringlabelTextColor?: stringlabelBorderRadius?: number(default3)showCloseButton?: boolean(defaulttrue)widgetPosition?: "left" | "center" | "right"(default"left")widgetPaddingRight?: number(default10, extra right margin whenwidgetPositionis"right")draggable?: boolean(defaultfalse)pinOutOfRange?: boolean(defaultfalse; overridesChartOptions.pinOutOfRangeLinesfor this order/position line)
Legacy single action button:
actionButtonText?: stringactionButtonAction?: string(default"execute")actionButtonTextColor?: stringactionButtonBackgroundColor?: stringactionButtonBorderRadius?: numberactionButtonMinWidth?: numberactionButtonPaddingX?: numberactionButtonFullHeight?: booleanactionButtonFontWeight?: number | stringactionButtonBorderColor?: stringactionButtonBorderStyle?: "solid" | "dotted" | "dashed"actionButtonsInnerGap?: number(default6, spacing between action buttons)actionButtonsGroupGap?: number(default8, spacing between action-button group and main order widget)
Multi-button actions:
actionButtons?: OrderActionButton[](default[])
Connector/fill visuals:
connectorToPrice?: numberconnectorColor?: stringconnectorStyle?: "solid" | "dotted" | "dashed"(default"dotted")connectorThickness?: number(default1)connectorAnchorPaddingRight?: number(default10)fillToPrice?: numberfillColor?: string
IndicatorInstanceOptions#
id?: stringtype: string(registered indicator id, e.g."volume")visible?: boolean(defaulttrue)pane?: "overlay" | "separate"paneHeightRatio?: number(for separate panes;0.08to0.45recommended)zIndex?: number(render order; lower first)excludeFromAutoscale?: boolean(defaulttruefor indicator instances)overlayScaleWeight?: number(0..1; only used whenexcludeFromAutoscaleisfalse)inputs?: Record<string, unknown>(plugin-specific parameters)
IndicatorPlugin#
id: stringname: stringpane?: "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)helperyFromPrice(price)helper (available for overlay indicators,nullfor separate-pane indicators)getCandleDirectionByIndex(index)andgetVolumeByIndex(index)helpers- theme colors (
upColor,downColor)
For separate-pane indicators, return IndicatorPaneRenderInfo from draw() when the core should render TradingView-style pane UI:
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 (usesOhlcDataPoint.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, usesOhlcDataPoint.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; inputsfast,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
gridcolor/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 emitonIndicatorPaneActionevents for the host UI; if no handler is registered, X removes the indicator directly. "stochastic": Stochastic %K/%D with 20/80 guides (separate pane; inputskLength,kSmoothing,dLength)"stochrsi": Stochastic RSI %K/%D (separate pane; inputsrsiLength,stochLength,kSmoothing,dSmoothing)"adx": ADX with optional +DI/-DI lines, Wilder smoothing (separate pane; inputslength,showDi)"obv": On-Balance Volume with compact K/M/B axis labels (separate pane)"mfi": Money Flow Index, 20/80 guides (separate pane; inputlength)"cci": Commodity Channel Index, ±100 guides (separate pane; inputlength)"williamsr": Williams %R, -80/-20 guides (separate pane; inputlength)"roc": Rate of Change % with zero line (separate pane; inputlength)"momentum": Momentum (close delta) with zero line (separate pane; inputlength)"psar": Parabolic SAR dots (overlay; inputsstart,increment,maximum,color)"supertrend": SuperTrend line colored by trend direction (overlay; inputsatrLength,multiplier,upColor,downColor)"ichimoku": Ichimoku Cloud — Tenkan, Kijun, Senkou A/B with filled kumo, Chikou (overlay; inputsconversionLength,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; inputsemaLength,atrLength,multiplier)"donchian": Donchian Channels — highest high / lowest low with midline (overlay; inputlength)
Example:
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/vwmawill have limited or no output.
Volume style inputs:
upOpacity,downOpacityupColor,downColorminBarWidthoverlayHeightRatio(when used as overlay)scaleMode("visible"default, or"full")scaleType("sqrt"default, or"log"/"linear")clampPercentile(0..1, default1; e.g.0.95to reduce outlier crush)
DrawingObjectOptions#
Drawings are user-created chart tools, separate from indicators. They are interactive chart objects like horizontal lines and trendlines.
id?: stringtype: "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 pointfib-retracement: two-point retracement with levels/bandsfib-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 movelong-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.colorsets the entry line;colorsis[profitColor, lossColor, labelTextColor](defaultsPOSITION_DEFAULT_COLORS). Sizing inputsaccountSize,lotSize,risk,riskMode("percent"|"amount"),leverage,pointValue,qtyPrecisiondrive the Qty/Amount labels — setpointValue(contract $/point) from your app for real money values. Aprice-rangetool (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?: booleancolor?: stringcolors?: string[](per-level colors; used byfib-retracement. When non-empty, each level/band cycles through these instead ofcolor. New fib drawings default toFIB_DEFAULT_PALETTE; setcolors: []for a monochrome fib that followscolor.)style?: "solid" | "dotted" | "dashed"width?: numberlabel?: string
DrawingPoint:
type DrawingPoint = {
index: number; // fractional candle index
price: number;
time?: string; // ISO timestamp for reference/persistence
};Tool workflow:
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/panNew 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 likerectangle(corner handles, including the derived corners).arrow: atrendlinewith a filled arrowhead at the second point (head size scales withwidth). 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, andonDrawingEditTextfires so the host can show its text editor. Stores content inlabeland size infontSize, liketext/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 throughonDrawingsChange. - 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[]): voidsetChartType(type: ChartType): void— switch the main series style at runtime ("candles" | "hollow-candles" | "heikin-ashi" | "bars" | "line" | "area" | "baseline")getChartType(): ChartTypesetPriceLines(lines: PriceLineOptions[]): voidaddPriceLine(line: PriceLineOptions): stringremovePriceLine(id: string): voidsetOrderLines(lines: OrderLineOptions[]): voidaddOrderLine(line: OrderLineOptions): stringupdateOrderLine(id: string, patch: Partial<OrderLineOptions>): voidremoveOrderLine(id: string): voidonOrderAction(handler: ((event: OrderActionEvent) => void) | null): voidonChartClick(handler: ((event: ChartClickEvent) => void) | null): voidonCrosshairMove(handler: ((event: CrosshairMoveEvent) => void) | null): voidonCrosshairPriceAction(handler: ((event: CrosshairPriceActionEvent) => void) | null): voidonIndicatorPaneAction(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 whendragging === false.zoomInX(factor?: number): void(default factor1.25)zoomOutX(factor?: number): void(default factor1.25)zoomInY(factor?: number): void(default factor1.25)zoomOutY(factor?: number): void(default factor1.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");resetViewportwould 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" belowgetSelectedDrawing(): { drawing: DrawingObjectOptions; bounds: DrawingBounds } | nullonContextMenu(handler: ((event: ChartContextMenuEvent) => void) | null): void— typed right-click with region + hit infoonKeyboardShortcut(handler: ((event: ChartKeyboardShortcutEvent) => void) | null): voidfocus(): void— focus the canvas so keyboard shortcuts applyonDrawingDoubleClick(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 atext/note/callouttool is placed or double-clicked; use it to show an inline text editor at{x, y}and write the result back viaupdateDrawing(id, { label })). These drawings store their content inlabeland size infontSize.setTradeMarkers(markers: TradeMarkerOptions[]): void— draw trade execution markers (arrow +qty @ pricelabel) on the candle of each fill'stime. 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). Returnstrueif a draft was cancelled. Wire it to Escape.- Hold Shift while drawing or dragging an endpoint of a
trendline/ray/arrowto 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 ownlockedflag — 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 insaveState()asdrawingsLocked. Added in 0.1.163.setTheme(theme: ChartThemeName | ChartTheme): void/getTheme(): string | null— apply a whole palette at once; see "Themes" belowsetDatafeed(datafeed: ChartDatafeed | null): Promise<void>— attach a pull-based data source with lazy history; see "Datafeed" belowonLongPress(handler: ((event: ChartLongPressEvent) => void) | null): void— touch press-and-hold inspectionsetCompareSeries(series: CompareSeriesOptions[]): void/addCompareSeries(series)/removeCompareSeries(id)/getCompareSeries()— overlay other instruments on the main pane; see "Compare overlays" belowsetPriceScale(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: trueflips 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 insaveState()aspriceScale.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 | nullonActiveDrawingToolChange(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;measuredoes it after every measurement). Keeps host toolbars in sync without polling.setDrawings(drawings: DrawingObjectOptions[]): voidgetDrawings(): DrawingObjectOptions[]addDrawing(drawing: DrawingObjectOptions): stringupdateDrawing(id: string, patch: Partial<DrawingObjectOptions>): voidremoveDrawing(id: string): voidclearDrawings(): voidonDrawingsChange(handler: ((drawings: DrawingObjectOptions[]) => void) | null): voidsetDoubleClickEnabled(enabled: boolean): voidsetDoubleClickAction(action: "reset" | "placeLimitOrder"): voidregisterIndicator(plugin: IndicatorPlugin): voidunregisterIndicator(type: string): voidlistBuiltInIndicators(): BuiltInIndicatorInfo[]getIndicators(): IndicatorInstanceOptions[]addIndicator(type: string, inputs?: Record<string, unknown>, options?: Partial<IndicatorInstanceOptions>): stringupdateIndicator(id: string, patch: Partial<IndicatorInstanceOptions>): voidremoveIndicator(id: string): voidsetIndicators(indicators: IndicatorInstanceOptions[]): voidsaveState(): 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 throughsetData/upsertBar.loadState(state: Partial<ChartSavedState>): void— restore asaveState()blob. Tolerant of partial blobs (missing sections are left untouched), so it doubles as a bulk setter. Register custom script indicator plugins withregisterIndicatorbefore 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.
// 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(): voidreplayStep(bars?: number): void— negative rewinds.replaySeek(index: number): void— absolute jump, clamped to the series.replayPlay(speed?: number): void/replayPause(): void/setReplaySpeed(speed: number): voidgetReplayState(): 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): voiddestroy(): 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.
chart.addIndicator("volume-profile", { scope: "visible", rows: 40, widthPercent: 28 });
chart.addIndicator("volume-profile", { scope: "session" }); // one per trading dayscope: "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).
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.
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.
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.
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 ETPresets (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:
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.
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.
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 seriesLive 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:
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.
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)#
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:
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:
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.
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.
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-setFive 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.
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 × 3bars, then callssubscribeBarsand upserts every streamed bar. - Lazy history: when the viewport comes within
datafeedOptions.prefetchThresholdBars(default 150) of the oldest loaded bar, the chart requests the nextchunkBars(default 1500) and merges them, re-anchoring on time so the view doesn't jump. AcooldownMs(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/upsertBarkeep 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 withlongPressTooltip: false; subscribe withonLongPress(handler)to drive your own UI (the event carries the bar, price, index and client coords, andscrubbingdistinguishes 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.
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.
scaledefaults 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:
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:
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.
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).showValueLineis 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:
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?)→ numberinput.bool(label, default?)→ booleaninput.color(label, default)→ color stringinput.string(label, default?)→ stringinput.select(label, default, choices)→ string (choices:string[]or{value, label}[])input.source(label?, default?)→ one ofopen/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..100for 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.
