Shared across every visual

Events & custom drawing

Every chart in the package inherits one event surface and one drawing API from a common base class. The events are cancellable — a listener can veto the chart's built-in reaction — and the drawing API lets you paint your own annotations on top, in the chart's own coordinates, without forking anything.

Listening

chart.on(event, fn) registers a listener and returns a function that removes it. The handler receives one object carrying the payload's own fields plus the cancellation members — there is no detail wrapper, so a handler written against a payload (e.bar, e.index) reads exactly the same as it always did.

const off = chart.on('click', (e) => {
  console.log(e.meta, e.px, e.button);
  console.log(e.type, e.native, e.cancelable, e.defaultPrevented);
});

off(); // unsubscribe
MemberTypeDescription
typestring Event name, so one function can serve several registrations.
nativeEvent | null The originating DOM event, or null for programmatic and internal causes.
cancelableboolean Whether preventDefault() does anything for this event.
defaultPreventedboolean True once any listener has cancelled the built-in reaction.
preventDefault()() => void Suppress the chart's built-in reaction. A no-op when cancelable is false.

Cancelling the default

Cancelling stops the chart from acting, not the other listeners from hearing about it — every registered handler still runs. That matters when one part of an app observes an event while another part governs it.

// Read-only chart: keep the view exactly where it was put.
chart.on('pan', (e) => e.preventDefault());
chart.on('zoom', (e) => e.preventDefault());

// Reject data that fails validation — the chart never rebuilds.
chart.on('dataAdd', (e) => {
  if (e.items.some(isOutOfRange)) e.preventDefault();
});

Event reference

These nine are on every chart. Each visual adds its own on top — hover everywhere, plus seriesFocus, seriesChange, select or highlight depending on the chart; see that chart's Events section.

EventPayload fieldsCancel?Fires
clickmeta, px, buttonyes On pointer-up over the chart. meta is the mark under the pointer, or null for the background.
dblclickmeta, px, buttonyes On a double click, same payload as click.
dragphase, target, dx, dy, pxon 'start' Each phase of a press-move-release. target is 'view' (pan) or 'slider' (the pie/candlestick series slider).
panoffset, deltayes Before the view translates — from a drag, a programmatic panBy()/panTo(), or the zoom controls.
zoomscale, factor, centeryes Before the view scales — wheel, buttons, or zoomBy()/zoomTo().
drawFinishednow, frameno After each rendered frame. See the warning below.
dataAddaction, itemsyes Before add() applies.
dataUpdateaction, itemsyes Before update() / setData() applies. action is 'update' for a patch list, 'replace' for a whole dataset.
dataRemoveaction, itemsyes Before remove() applies; items holds the refs or indices passed in.
drawFinished fires every animation frame — roughly sixty times a second while anything is animating. Keep the handler cheap, and never let it trigger a re-render or a layout read. It is a notification about work already done, so it is not cancellable.
Two chart-specific notes. pan and zoom only fire on charts with panZoom.enabled — the pie and wind rose have no view to move. And the wind rose emits click on pointer-down rather than pointer-up, because that is when its petal selection has always happened; cancelling still suppresses that selection.

Order of precedence

Charts that expose interaction.mouse hooks (the wind rose and candlestick) now have two override layers. They run in a fixed order, and each can stop the ones below it:

#LayerCan
1chart.on('click', …) listenersObserve, and veto everything below.
2interaction.mouse.onPetalClick & friendsReplace or wrap the built-in behavior; call defaultAction() to run it.
3The chart's built-in behaviorSelection, focus, slider seek, pan.

Data events fire first

The three data* events fire before the dataset changes, which is the only point at which a veto can mean anything — a listener that reverted the chart afterwards would still have paid for a rebuild and a visible frame. When a listener cancels, the call returns having done nothing at all.

chart.on('dataAdd', (e) => e.preventDefault());

chart.add({ x: 'F', y: 42 });
chart.getData().points.length; // unchanged

Drawing layers

A layer is a callback invoked once per frame with a 2D context and the projections needed to place things in the chart's own coordinates. Layers paint onto a dedicated canvas above the chart's axes and legend, so they never contend with the chart's own repaint schedule.

MethodReturnsDescription
addLayer(draw, options?)ChartLayer Register a per-frame paint callback. Options: id, z (paint order, low first), visible.
addElement(element)ChartLayer Register one declarative annotation — see below.
clearLayers()void Remove every layer and element.
getLayerCount()number How many layers are registered.

The returned handle has id, mutable z and visible, setDraw(fn) to swap the callback in place, and remove(). Adding a layer with an id that is already taken replaces it rather than stacking a second one.

const layer = chart.addLayer((c) => {
  // Mark the mean, in the chart's own data coordinates.
  const a = c.toDevice(0, mean);
  const b = c.toDevice(1, mean);
  c.ctx.strokeStyle = '#ff3b6b';
  c.ctx.lineWidth = 2 * c.dpr;
  c.ctx.beginPath();
  c.ctx.moveTo(a.x, a.y);
  c.ctx.lineTo(b.x, b.y);
  c.ctx.stroke();
}, { id: 'mean', z: 10 });

layer.visible = false;
layer.remove();

The context passed to draw:

FieldTypeDescription
ctxCanvasRenderingContext2DAlready cleared, sized in device px.
deviceW / deviceHnumberCanvas size in device px.
dprnumberDevice pixel ratio — multiply CSS px by this.
plotRect[x0,y0,x1,y1]The box the marks are laid out in, as canvas fractions (y-up).
viewViewTransformLive pan/zoom, or identity on charts without it.
nownumberSeconds since the current animation started.
toDevice(x, y, space?){ x, y }Project a point to device px, ready to draw.
toSpace(x, y, space?){ x, y }The inverse, for hit-testing your own marks.

Declarative elements

addElement covers the common annotations without writing canvas code. Every element accepts space, stroke, fill, lineWidth, lineDash, opacity, plus the layer options id, z and visible. Widths and dashes are in CSS px and are scaled for you.

kindGeometryNotes
'line'from, toTwo points.
'rect'at, width, height, radius? at is the lower-left corner in data/plot space. Both corners are projected, so it tracks a zoom correctly.
'circle'center, radius The radius is in CSS px — a data-space radius would go elliptical under a non-uniform zoom.
'text'at, text, font?, align?, baseline? font is a CSS shorthand sized in CSS px.
'path'points, closed?A polyline, optionally closed.
// A target band, plus the rule and label that go with it.
chart.addElement({
  kind: 'rect', at: [0, 0.6], width: 1, height: 0.15,
  fill: 'rgba(56,189,248,0.12)',
});
chart.addElement({
  kind: 'line', from: [0, 0.75], to: [1, 0.75],
  stroke: '#38bdf8', lineDash: [4, 4],
});
chart.addElement({
  kind: 'text', space: 'plot', at: [0.02, 0.78],
  text: 'target', fill: '#38bdf8', font: '12px system-ui',
});

Coordinate spaces

Both addLayer's projections and every element take a space, defaulting to 'data':

spaceUnitsUse for
'data'[0,1] per axis, y-up, follows pan/zoom Anything pinned to the data. This is the space mark metadata is already in (BarMeta.x0, slice angles, …), so an annotation on a mark belongs here.
'plot'[0,1] per axis, y-up, ignores pan/zoom Things anchored to the frame — a corner badge, a legend of your own.
'canvas'CSS px from the top-left, y-down DOM-style placement, when you already have a pixel offset.
Radial charts. On the pie and wind rose, 'data' and 'plot' are the disc box rather than a plot rect — the same space their slice and segment metas use, so the two line up.

In React

Every event above is a prop on the React components, and layers have a layers prop. See React → Standard events.

<BarChart
  data={data}
  layers={layers}
  onClick={(e) => e.preventDefault()}
  onDataAdd={(e) => console.log(e.action, e.items)}
/>

© 2026 · License · React bindings →