Recipes
Basic chart setup#
import { createChart } from "@hyperprop/charting-library";
const chart = createChart(rootEl, {
width: 900,
height: 520,
backgroundColor: "#101114",
upColor: "#2fb171",
downColor: "#d35a5a",
grid: { opacity: 0.35, xTickCount: 8, yTickCount: 6 }
});Tune axis density when creating the chart:
const chartDense = createChart(rootEl, {
grid: { xTickCount: 10, yTickCount: 8 }
});Streaming updates#
chart.setData(nextData);Use:
autoScaleSmoothingfor smoother scale transitionsautoScaleIgnoreLatestCandleto reduce live-candle jitter
Tick-size aware chart precision#
const chart = createChart(rootEl, {
tickSize: 0.25,
priceDecimals: 2
});Style x-axis and y-axis labels differently#
const chart = createChart(rootEl, {
axis: { lineColor: "#374151" },
xAxis: { textColor: "#9ca3af", fontSize: 11 },
yAxis: { textColor: "#e5e7eb", fontSize: 12 }
});Stabilize candle up/down coloring on tiny deltas#
const chart = createChart(rootEl, {
// default behavior
candleColorMode: "openClose",
// auto epsilon from priceDecimals (recommended)
candleColorEpsilon: -1
});Use previous-close mode if your UX expects green/red by last-close change:
const chart = createChart(rootEl, {
candleColorMode: "prevClose",
candleColorEpsilon: -1
});Add a static alert/level line#
chart.addPriceLine({
price: 6650,
label: "Alert",
style: "dotted",
color: "#f59e0b",
showLabel: true
});Pin out-of-range lines to the chart edge#
By default, horizontal price/order/drawing lines outside the visible price range are hidden instead of being pinned to the top or bottom edge. Opt in globally or per line when you want marker-style behavior for important levels.
const chart = createChart(rootEl, {
pinOutOfRangeLines: true
});
chart.addPriceLine({
price: 6400,
label: "Liquidation",
pinOutOfRange: true
});Add chart drawing tools#
Drawing tools are separate from indicators. Indicators compute/render data series; drawings are user-created objects that can be persisted.
// Single-click tools: the next plot click creates the drawing.
chart.setActiveDrawingTool("horizontal-line");
chart.setActiveDrawingTool("vertical-line");
// Two-click tools: first click starts, second click commits.
chart.setActiveDrawingTool("trendline");
chart.setActiveDrawingTool("ray"); // extends infinitely past the second point
chart.setActiveDrawingTool("fib-retracement");
// Back to normal cursor/pan mode.
chart.setActiveDrawingTool(null);Persist drawings:
chart.onDrawingsChange((drawings) => {
localStorage.setItem("chart-drawings", JSON.stringify(drawings));
});
const saved = localStorage.getItem("chart-drawings");
if (saved) {
chart.setDrawings(JSON.parse(saved));
}Tighten dotted spacing globally#
const chart = createChart(root, {
dashPatterns: {
dotted: [2, 1],
dashed: [8, 5],
connectorDotted: [2, 2],
borderDotted: [2, 1]
}
});Keep price labels from shaking on fast ticks#
const chart = createChart(root, {
priceDecimals: 2,
stabilizePriceLabels: true,
priceLabelMinIntegerDigits: 4,
// If you still see any width movement, pin exact width:
// priceLabelWidthTemplate: "88888.88"
});Add crosshair "+" action button#
const chart = createChart(root, {
crosshair: {
showPriceActionButton: true,
priceActionButtonText: "+",
priceActionButtonGap: 4
}
});
chart.onCrosshairPriceAction((event) => {
// Handle in app/frontend (place order modal, quick ticket, etc.)
console.log("crosshair + clicked at price", event.price);
});Square style example:
const chart = createChart(root, {
crosshair: {
showPriceActionButton: true,
priceActionButtonRounded: false
}
});Add built-in indicators#
const emaId = chart.addIndicator("ema", { length: 34, source: "close" });
const rsiId = chart.addIndicator("rsi", { length: 14 }, { pane: "separate", paneHeightRatio: 0.18 });
const volumeId = chart.addIndicator("volume", { upOpacity: 0.72, downOpacity: 0.72 });Move the main indicator legend away from a HUD#
The main overlay indicator legend defaults to the chart pane's top-left corner. If your app renders a symbol/OHLC HUD over the canvas, move the legend with labels.indicatorLegend*.
const chart = createChart(root, {
labels: {
showIndicatorNames: true,
showIndicatorValues: true,
indicatorLegendPosition: "top-left",
indicatorLegendOffsetX: 10,
indicatorLegendOffsetY: 34
}
});You can also place it in another corner:
chart.updateOptions({
labels: {
indicatorLegendPosition: "top-right",
indicatorLegendOffsetX: 12,
indicatorLegendOffsetY: 10
}
});Hide separate-pane indicator value tags#
RSI and other separate-pane indicators can draw a latest-value tag on the right axis. Hide those tags while keeping the pane scale and legend:
chart.updateOptions({
labels: {
showIndicatorValueLabels: false
}
});Prevent one volume spike from crushing all bars#
const volumeId = chart.addIndicator("volume", {
scaleMode: "visible", // default behavior
scaleType: "sqrt", // default behavior
clampPercentile: 0.95 // clamp scaling reference to p95 of visible bars
});Available built-ins:
volume,sma,ema,rsi,wma,vwma,rma,hma,stddev,atr
Add a custom separate-pane indicator with scale labels#
Separate-pane plugins can return pane metadata from draw() so the chart renders the right-side scale, top-left legend, and latest-value tag.
import { type IndicatorPlugin } from "@hyperprop/charting-library";
const momentumPlugin: IndicatorPlugin<{ length: number; color: string }> = {
id: "momentum",
name: "Momentum",
pane: "separate",
paneHeightRatio: 0.18,
defaultInputs: { length: 10, color: "#38bdf8" },
draw: (ctx, renderContext, inputs) => {
const values = renderContext.data.map((point, index, data) => {
const prior = data[index - inputs.length];
return prior ? point.c - prior.c : null;
});
const visibleValues = values
.slice(renderContext.startIndex, renderContext.endIndex + 1)
.filter((value): value is number => Number.isFinite(value ?? Number.NaN));
if (visibleValues.length === 0) return;
const maxAbs = Math.max(...visibleValues.map((value) => Math.abs(value)), 1);
const min = -maxAbs;
const max = maxAbs;
const yFromValue = (value: number) => {
const ratio = (value - min) / (max - min || 1);
return renderContext.chartBottom - ratio * renderContext.chartHeight;
};
ctx.save();
ctx.strokeStyle = inputs.color;
ctx.lineWidth = 2;
ctx.beginPath();
let drawing = false;
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
const value = values[index];
if (!Number.isFinite(value ?? Number.NaN)) {
drawing = false;
continue;
}
const x = renderContext.xFromIndex(index);
const y = yFromValue(value as number);
if (!drawing) {
ctx.moveTo(x, y);
drawing = true;
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
ctx.restore();
const latest = [...values].reverse().find((value): value is number => Number.isFinite(value ?? Number.NaN));
return {
title: `Momentum ${inputs.length}`,
axis: { min, max, ticks: [min, 0, max], decimals: 2 },
guideLines: [{ value: 0, label: "0", style: "dotted" }],
legendValues: latest === undefined ? [] : [{ value: latest, text: latest.toFixed(2), color: inputs.color }],
valueLabels:
latest === undefined
? []
: [{ value: latest, text: latest.toFixed(2), backgroundColor: inputs.color, textColor: "#0f172a" }]
};
}
};
chart.registerIndicator(momentumPlugin);
chart.addIndicator("momentum", { length: 10 });Resize indicator pane from frontend#
Use paneHeightRatio so your app can wire a drag handle or slider:
const volumeId = chart.addIndicator("volume", {}, { paneHeightRatio: 0.2 });
// e.g. on slider/drag updates:
chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });Recommended range:
0.08to0.45(library clamps to safe bounds)
Use indicator z-order and autoscale influence#
chart.addIndicator("ema", { length: 34 }, { zIndex: 5 });
chart.addIndicator("wma", { length: 55 }, { zIndex: 10 });
chart.addIndicator("sma", { length: 200 }, {
excludeFromAutoscale: false,
overlayScaleWeight: 0.25
});Query built-ins and active indicator instances#
const builtIns = chart.listBuiltInIndicators();
const active = chart.getIndicators();
console.log({ builtIns, active });Remove or hide indicators#
const id = chart.addIndicator("ema", { length: 21 });
chart.updateIndicator(id, { visible: false }); // hide
chart.removeIndicator(id); // removeVolume and VWMA data requirement#
volume and vwma use OhlcDataPoint.v. If v is missing, output can be empty/limited.
const data = [
{ t: "2026-01-01T00:00:00Z", o: 100, h: 103, l: 99, c: 102, v: 125000 }
];
chart.setData(data);Register a custom indicator plugin#
chart.registerIndicator({
id: "my-overlay-line",
name: "My Overlay Line",
pane: "overlay",
defaultInputs: { color: "#22d3ee", width: 2 },
draw: (ctx, rc, inputs) => {
if (!rc.yFromPrice) return;
// draw custom line with rc.xFromIndex + rc.yFromPrice
}
});Add a draggable pending limit line#
chart.addOrderLine({
type: "limit",
side: "buy",
price: 6480,
qty: 1,
draggable: true,
style: "dotted"
});Handle persistence:
chart.onOrderAction((event) => {
if (event.action === "move" && event.price !== undefined) {
// persist new price in your app state
}
});Use custom action buttons#
chart.setOrderLines([
{
type: "limit",
side: "buy",
price: 6488,
qty: 1,
actionButtons: [
{ text: "Buy", action: "execute", backgroundColor: "#3b82f6" },
{ text: "TP", action: "previewTp", draggable: true, borderStyle: "dotted" },
{ text: "SL", action: "previewSl", draggable: true, borderStyle: "dotted" }
]
}
]);Draw connector between two prices#
chart.addOrderLine({
type: "takeProfit",
side: "sell",
price: 6650,
connectorToPrice: 6488,
connectorStyle: "dashed",
connectorAnchorPaddingRight: 34
});Preview fill zone while dragging#
chart.addOrderLine({
type: "takeProfit",
side: "sell",
price: 6650,
fillToPrice: 6488,
fillColor: "rgba(45,212,191,0.16)"
});One-click + double-click limit placement#
chart.setDoubleClickEnabled(true);
chart.setDoubleClickAction("placeLimitOrder");
chart.onChartClick((event) => {
if (event.region === "plot" && event.price !== undefined) {
// create one-click preview state in your app
}
});