Skip to content

Custom series ​

Add your own chart type without forking the engine. A custom series definition is a plain object; the chart handles data, time alignment, autoscale, the legend, the last-value label and overlay scales.

ts
import { createChart, withAlpha, type Bar, type CustomSeriesDef } from '@chart-on/core';
import type { ExampleContext } from './types';
import { sampleBars } from './sample-data';

interface BandPoint { time: number; upper: number; lower: number }

const band: CustomSeriesDef<BandPoint, { color?: string }> = {
  type: 'band',
  // Price span of one item, for autoscale.
  priceRange: (p) => [p.lower, p.upper],
  draw(ctx, view) {
    const { items } = view;
    if (items.length < 2) return;
    const color = view.options.color ?? view.theme.line;
    ctx.beginPath();
    items.forEach(({ item, x }) => ctx.lineTo(x, view.priceToY(item.upper)));
    for (let i = items.length - 1; i >= 0; i--) ctx.lineTo(items[i]!.x, view.priceToY(items[i]!.item.lower));
    ctx.closePath();
    ctx.fillStyle = withAlpha(color, 0.15);
    ctx.fill();
  },
  legend: (p, o) => (p ? [{ label: 'U', text: o.format(p.upper) }, { label: 'L', text: o.format(p.lower) }] : []),
  lastValue: (p) => ({ value: p.upper }),
};

export default function mount(el: HTMLElement, { theme }: ExampleContext) {
  const chart = createChart(el, { theme });
  const bars = sampleBars();
  chart.addCandles({ title: 'SAMPLE' }).setData(bars);
  const channel = chart.addCustomSeries(band, { title: 'Channel', color: '#38bdf8' });
  channel.setData(bars.map((_, i) => channelAt(bars, i, 20))); // ascending by time, unix seconds
  chart.fitContent();
  return () => chart.destroy();
}

/** Highest high and lowest low of the `length` bars up to bar `i`. */
function channelAt(bars: Bar[], i: number, length: number): BandPoint {
  const span = bars.slice(Math.max(0, i - length + 1), i + 1);
  return {
    time: bars[i]!.time,
    upper: Math.max(...span.map((b) => b.high)),
    lower: Math.min(...span.map((b) => b.low)),
  };
}

Feed it live with update: an item with the same time replaces the last one, a newer one is appended.

ts
channel.update(point);

The demo's price channel (apps/demo/src/band.ts) is a complete example.

Definition ​

FieldRequiredNotes
typeyesId for debugging and saved layouts
defaultOptionsnoYour own options; merged under the ones passed to addCustomSeries
priceRange(item, options)yes[min, max] of one item, or null to leave it out of autoscale
draw(ctx, view)yesCanvas2D drawing, pane-local css px; the context is saved and restored around it
legend(item, options)noLegend parts for the item under the crosshair (undefined when there is none)
lastValue(item, options)no{ value, color? } for the price-axis label of the newest item
step(dt, data, options)noAdvance animations by dt ms each frame; return true while animating
group(items, options)noOne item standing for several consecutive items while the chart groups bars; give it the first item's time. Called once per bucket as it comes into view, and again only when the bucket's items change. Without it a bucket shows its last item

view holds:

  • items: visible items (plus one either side) as { item, index, x }. While the chart groups bars, one item per bucket at the bucket's centre, and barSpacing is the bucket's width.
  • options: your options plus title, lastValueLabel, format and overlay.
  • width, height, barSpacing, pixelRatio, theme.
  • priceToY(price), yToPrice(y) on the series' scale (pane scale or its overlay scale; log and percent modes included), and indexToX(index).

Options ​

Every custom series accepts title, lastValueLabel (default true), axisTitle (default false, a title tag beside the label), format (value formatter for the axis label), priceFormat (a price format that wins over format for the axis label) and overlay (its own scale, like volume) next to its own options. applyOptions, setVisible and chart.removeSeries work as for built-in series.

Items whose time is not a bar of the main series are skipped, like line and histogram points. Drawing is Canvas2D; a GPU hook may come later.

Errors ​

A hook that throws never stops the chart. The series is skipped for that frame: draw draws nothing (any save() it left open is restored), priceRange leaves the series out of autoscale, legend shows no values, lastValue and format show no axis label, step stops animating, and a failing group shows the bucket's last item. Other series, panes and the axes keep drawing.

The chart reports the error through the series:error event, once per hook until that hook succeeds again. Without a listener it goes to console.error.

ts
chart.on('series:error', ({ series, hook, error }) => {
  console.warn(`${series.id}: ${hook} failed`, error);
});