Plugins
A plugin bundles extensions, such as drawing tools, indicators, custom series and primitives, behind one install step. Everything a plugin adds is removed again when it is uninstalled, so an app can switch a feature pack on and off, and a package on npm can say which core versions it works with.
import type { ChartPlugin } from '@chart-on/core';
export const measureKit: ChartPlugin = {
name: 'acme-measure-kit',
version: '1.2.0',
requires: '^0.1.0', // core versions it works with
install(ctx) {
ctx.registerDrawingTool(rulerTool);
ctx.registerIndicator(rangeIndicator);
const timer = setInterval(refresh, 1000);
return () => clearInterval(timer); // anything else it set up
},
};
const uninstall = chart.install(measureKit);
chart.installedPlugins; // ['acme-measure-kit']
uninstall(); // or chart.uninstall('acme-measure-kit')The context
install(ctx) adds the plugin's contributions through ctx. Each call is the chart's usual one, and the chart remembers it so uninstalling can undo it:
| Method | Does | Undone by uninstall |
|---|---|---|
registerDrawingTool(def) | chart.drawings.register (see Custom drawing tools). Needs drawings on the chart (drawings(), drawingTools() or builtins()); without them it throws not-registered | The tool is removed, or the tool of that type it replaced comes back. Drawings of the type stay in the data, undrawn, until a tool of that type is registered again |
registerIndicator(def) | chart.indicators.register (see Indicators). Needs indicators on the chart (indicators() or builtins()); without them it throws not-registered | The definition is unregistered and its indicators become pending placeholders. A definition the app had registered itself stays. Indicators installed again while the plugin is installed get the definition back |
addCustomSeries(def, options, pane) | chart.addCustomSeries (see Custom series) | The series is removed |
attachPrimitive(primitive, target) | chart.attachPrimitive (see Primitives) | The primitive is detached |
onFrame(observer) | Watches the chart's frames: see Frame observers | The observer stops being called |
onDispose(fn) | Runs fn on uninstall |
ctx.chart is the chart and ctx.coreVersion its version. ctx.host(name) returns what a layer above the chart offers plugins: the UI shell offers 'shell', which shellOf(ctx) from @chart-on/ui wraps so the toolbar items a plugin adds are removed with it:
import { shellOf } from '@chart-on/ui';
install(ctx) {
ctx.registerDrawingTool(rulerTool);
shellOf(ctx)?.addToolbarItem({ type: 'button', id: 'acme-ruler', group: 'analysis', label: 'Ruler', icon: Ruler, onClick: (chart) => chart.drawings.setTool('acme-ruler') });
}shellOf is null when the chart has no shell, so the same plugin works with and without it. A layer of your own can offer plugins something with chart.setPluginHost(name, host); plugins installed afterwards see it. A function returned from install runs first on uninstall, then the contributions are undone, newest first.
Frame observers
ctx.onFrame(observer) lets a plugin watch the frame loop, e.g. to time frames or count them. The built-in statsOverlay() plugin is one: the render statistics are built on it. Every member of the observer is optional and called synchronously:
| Member | Called |
|---|---|
request(reason) | When something asks for a frame; reason is a RedrawReason such as 'series', 'view' or 'host' |
start() | When a frame starts drawing |
lap(phase) | When the frame finishes a phase: 'autoscale', 'drawBottom', 'drawGL', 'drawTop' or 'drawCursor' (see FrameTimes), or null for work between phases. Only the phases the frame draws are reported |
end(kind) | When the frame is drawn: 'scene', 'overlay' or 'cursor', the layers it redrew |
request runs on every redraw request, crosshair moves included, and the others on every drawn frame, so keep them cheap.
import type { ChartPlugin } from '@chart-on/core';
const frameCounter: ChartPlugin = {
name: 'acme-frame-counter',
version: '1.0.0',
install(ctx) {
let frames = 0;
ctx.onFrame({ end: () => frames++ });
},
};Rules
namemust be unique per chart: installing a second plugin of the same name throws.requiresis a semantic-version range checked againstCORE_VERSION: exact versions,>=,>,<=,<,^,~,xranges (0.x,1.2.*), sets separated by spaces (all must match) and alternatives with||. Pre-release tags are ignored. A version outside the range, or a range that cannot be read, throws aChartErrorwith codeplugin-incompatiblebeforeinstallruns.- If
installthrows, everything it added so far is undone and the error is rethrown: a failed install leaves nothing behind. chart.destroy()uninstalls every plugin, newest first.installafterdestroy()throws.
Writing and publishing a plugin
The repository has a starter package, examples/plugin-starter: one drawing tool and one indicator, its own build, and tests that use @chart-on/core/testing. The repository builds and tests it on every change, so a change to the plugin contract shows up there first. Copy it to start a plugin of your own.
- Name the package
charton-plugin-<what-it-does>(or@scope/charton-plugin-<…>) and add thecharton-pluginkeyword, so people can find it. - Prefix ids with the package name: drawing tool types and indicator ids such as
acme-measure-kit/ruler, so they never clash with the built-ins or with other plugins. - Depend on core as a peer:
"peerDependencies": { "@chart-on/core": "^0.1.0" }, and keep it external in your build, so the app's copy of the chart is the one your plugin registers with.@chart-on/uiis an optional peer if you useshellOf. - Version with semantic versioning, and keep
requiresto the core versions you test against; a minor core release can add to the contract, a major one can change it. - License your plugin as you choose. charton is Apache-2.0; a plugin that copies code from this repository keeps its notices.
To have your plugin listed, see Community plugins.