Skip to content

Plugins ​

A plugin extends a chart from the outside. It paints in the panes, under or over the series, reads the bars, and uses the public API for everything else: registering drawing types, listening to events, adding indicators. Optional features can ship this way, as packages of their own.

A first plugin ​

This plugin marks the highest high and the lowest low of the visible bars:

ts
import type { ChartPlugin } from '@kumochart-sdk/core';

const visibleExtremes: ChartPlugin = {
  name: 'visible-extremes',
  paint: ({ ctx, layer, pane, bars, visible, theme }) => {
    if (layer !== 'above' || pane.id !== 'main' || !visible) {
      return;
    }

    let high = -Infinity;
    let low = Infinity;

    for (let index = visible.from; index <= visible.to; index += 1) {
      high = Math.max(high, bars.high(index));
      low = Math.min(low, bars.low(index));
    }

    ctx.setLineDash([4, 4]);
    ctx.strokeStyle = theme.textMuted;
    ctx.fillStyle = theme.textMuted;

    for (const value of [high, low]) {
      const y = Math.round(pane.valueToY(value)) + 0.5;
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(pane.width, y);
      ctx.stroke();
      ctx.fillText(pane.formatValue(value), 8, y - 4);
    }
  }
};

chart.plugins.add(visibleExtremes);
chart.plugins.remove('visible-extremes');

paint runs whenever the chart repaints its series: after data changes, scrolling, zooming, resizing and option changes. Painting of the crosshair or of drawings being dragged does not repaint the series, so plugins cost nothing there.

Painting ​

paint(context) is called for every pane, once per layer:

  • below: after the grid, before the series. Use it for backgrounds such as volume profiles or session shading.
  • above: after the series and indicators, under drawings, orders and the crosshair.

The context is clipped to the pane's plot and translated to its top left corner, in CSS pixels; the chart saves and restores the canvas state around each plugin.

MemberDescription
ctx, pixelRatioThe 2D context and the device pixel ratio, for crisp lines
layerbelow or above
pane{ id, width, height, valueToY(value), yToValue(y), formatValue(value) }
bars, sourceThe displayed bars and the source bars (below)
visibleIndices { from, to } of the displayed bars intersecting the plot, or null
barSpacingDistance between bar centers in pixels
indexToX(index), xToIndex(x)Between continuous bar indices and x
timeToX(time)X of any time, interpolated between bars and extrapolated outside them
theme, fontThe theme and the chart font { family, size }

withAlpha(color, alpha) turns a theme color into a translucent one.

Reading data ​

bars are the displayed bars: aggregated to the timeframe, built by the transform and clipped by a running replay. source are the bars the application fed in, at the source interval, up to the replay cursor; they give finer detail, such as one-minute bars inside a daily candle. Without a source interval both hold the same bars.

Both are a BarsView, columnar and read-only; applications reach the same views as chart.data.bars and chart.data.source:

MemberDescription
lengthNumber of bars
revisionChanges whenever the bars change
time(i), open(i), high(i), low(i), close(i), volume(i)Fields of bar i, without allocating
bar(i)The bar as an object, or null outside the bars
lowerBound(time)First index at or after time; length when all are earlier

Key caches by revision and the indices you read, so a plugin computes once per change instead of once per frame:

ts
let key = '';
let cached: Summary | undefined;

function summaryOf(source: BarsView, from: number, to: number): Summary {
  const next = `${source.revision}:${from}:${to}`;

  if (next !== key || !cached) {
    key = next;
    cached = summarize(source, from, to);
  }

  return cached;
}

The source bars of the displayed bars [from, to] start at source.lowerBound(bars.time(from)) and end before source.lowerBound(bars.time(to + 1)), or at source.length for the last bar.

The host ​

attach(host) receives the chart and the same data views, before the first paint:

MemberDescription
chartThe public API: register drawing types, subscribe to events, …
bars, sourceThe data views
repaint()Repaints in the next frame, e.g. after the plugin's settings changed

A plugin with settings keeps the host to repaint when they change. This watermark shows a label behind the bars:

ts
import { withAlpha, type ChartPlugin, type PluginHost } from '@kumochart-sdk/core';

function watermark(initial: string): ChartPlugin & { setLabel(label: string): void } {
  let label = initial;
  let host: PluginHost | null = null;

  return {
    name: 'watermark',
    attach: (given) => {
      host = given;
    },
    detach: () => {
      host = null;
    },
    paint: ({ ctx, layer, pane, theme, font }) => {
      if (layer !== 'below' || pane.id !== 'main') {
        return;
      }

      ctx.font = `600 ${font.size * 4}px ${font.family}`;
      ctx.fillStyle = withAlpha(theme.textMuted, 0.15);
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(label, pane.width / 2, pane.height / 2);
    },
    setLabel: (next) => {
      label = next;
      host?.repaint();
    }
  };
}

const mark = watermark('BTC/USDT · 15m');
chart.plugins.add(mark);
mark.setLabel('ETH/USDT · 1h');

Release in detach() what attach() set up; it runs on chart.plugins.remove() and on chart.destroy(), while the chart still works:

ts
attach: (host) => {
  off = host.chart.on('replay', ({ state }) => updateLabel(state.time));
},
detach: () => {
  off?.();
}

Drawings that read data ​

Custom drawing types receive the same views in their geometry (geometry.bars, geometry.source), so tools that measure the market, such as a fixed-range volume profile or a position, need no plugin at all. A plugin can still register them in attach().

Publishing a plugin ​

  • Export a factory that takes options and returns the plugin, so users can add several with different names.
  • Depend on @kumochart-sdk/core as a peer dependency, at the version you built against.
  • Validate options when the plugin is created and throw a ChartError naming the bad value, like the chart does.
  • Never keep state outside the plugin object: one plugin instance belongs to one chart.

Released under the MIT license.