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:
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.
| Member | Description |
|---|---|
ctx, pixelRatio | The 2D context and the device pixel ratio, for crisp lines |
layer | below or above |
pane | { id, width, height, valueToY(value), yToValue(y), formatValue(value) } |
bars, source | The displayed bars and the source bars (below) |
visible | Indices { from, to } of the displayed bars intersecting the plot, or null |
barSpacing | Distance 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, font | The 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:
| Member | Description |
|---|---|
length | Number of bars |
revision | Changes 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:
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:
| Member | Description |
|---|---|
chart | The public API: register drawing types, subscribe to events, … |
bars, source | The 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:
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:
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/coreas a peer dependency, at the version you built against. - Validate options when the plugin is created and throw a
ChartErrornaming the bad value, like the chart does. - Never keep state outside the plugin object: one plugin instance belongs to one chart.