Documentation

Recipes

Basic chart setup#

ts
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:

ts
const chartDense = createChart(rootEl, {
  grid: { xTickCount: 10, yTickCount: 8 }
});

Streaming updates#

ts
chart.setData(nextData);

Use:

  • autoScaleSmoothing for smoother scale transitions
  • autoScaleIgnoreLatestCandle to reduce live-candle jitter

Tick-size aware chart precision#

ts
const chart = createChart(rootEl, {
  tickSize: 0.25,
  priceDecimals: 2
});

Style x-axis and y-axis labels differently#

ts
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#

ts
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:

ts
const chart = createChart(rootEl, {
  candleColorMode: "prevClose",
  candleColorEpsilon: -1
});

Add a static alert/level line#

ts
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.

ts
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.

ts
// 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:

ts
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#

ts
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#

ts
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#

ts
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:

ts
const chart = createChart(root, {
  crosshair: {
    showPriceActionButton: true,
    priceActionButtonRounded: false
  }
});

Add built-in indicators#

ts
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*.

ts
const chart = createChart(root, {
  labels: {
    showIndicatorNames: true,
    showIndicatorValues: true,
    indicatorLegendPosition: "top-left",
    indicatorLegendOffsetX: 10,
    indicatorLegendOffsetY: 34
  }
});

You can also place it in another corner:

ts
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:

ts
chart.updateOptions({
  labels: {
    showIndicatorValueLabels: false
  }
});

Prevent one volume spike from crushing all bars#

ts
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.

ts
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:

ts
const volumeId = chart.addIndicator("volume", {}, { paneHeightRatio: 0.2 });

// e.g. on slider/drag updates:
chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });

Recommended range:

  • 0.08 to 0.45 (library clamps to safe bounds)

Use indicator z-order and autoscale influence#

ts
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#

ts
const builtIns = chart.listBuiltInIndicators();
const active = chart.getIndicators();
console.log({ builtIns, active });

Remove or hide indicators#

ts
const id = chart.addIndicator("ema", { length: 21 });
chart.updateIndicator(id, { visible: false }); // hide
chart.removeIndicator(id); // remove

Volume and VWMA data requirement#

volume and vwma use OhlcDataPoint.v. If v is missing, output can be empty/limited.

ts
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#

ts
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#

ts
chart.addOrderLine({
  type: "limit",
  side: "buy",
  price: 6480,
  qty: 1,
  draggable: true,
  style: "dotted"
});

Handle persistence:

ts
chart.onOrderAction((event) => {
  if (event.action === "move" && event.price !== undefined) {
    // persist new price in your app state
  }
});

Use custom action buttons#

ts
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#

ts
chart.addOrderLine({
  type: "takeProfit",
  side: "sell",
  price: 6650,
  connectorToPrice: 6488,
  connectorStyle: "dashed",
  connectorAnchorPaddingRight: 34
});

Preview fill zone while dragging#

ts
chart.addOrderLine({
  type: "takeProfit",
  side: "sell",
  price: 6650,
  fillToPrice: 6488,
  fillColor: "rgba(45,212,191,0.16)"
});

One-click + double-click limit placement#

ts
chart.setDoubleClickEnabled(true);
chart.setDoubleClickAction("placeLimitOrder");

chart.onChartClick((event) => {
  if (event.region === "plot" && event.price !== undefined) {
    // create one-click preview state in your app
  }
});