React components
A thin React wrapper around every chart class below — same config, same events, same
imperative methods, exposed as a component instead of new XChart(el, config). Ships in
the same package, under a separate entry point so plain-TS consumers pull in no
React code.
Install
# same package as the vanilla charts — react / react-dom are peer deps
npm install @smalldat/sandycoast react react-dom
React 18 and 19 are both supported (peerDependencies). If your app doesn't already
depend on React, the base @smalldat/sandycoast import stays React-free — only code that
imports from @smalldat/sandycoast/react pulls it in.
Quick start
import { BarChart } from '@smalldat/sandycoast/react';
const data = {
points: [
{ x: 'Q1', y: 120, z: 'EU' },
{ x: 'Q1', y: 90, z: 'US' },
{ x: 'Q2', y: 140, z: 'EU' },
],
};
// hoisted, so its identity is stable across renders — see below
const options = { grain: { sizePx: 2.4 }, legend: { show: true } };
function Revenue() {
return (
<BarChart
data={data}
options={options}
style={{ width: '100%', height: 360 }}
onHover={({ bar }) => console.log(bar?.xValue, bar?.yValue)}
/>
);
}
ResizeObserver, so give it a size — pass style (as above) or a
className with a CSS rule.
Shared prop shape
Every component below takes the same five kinds of prop. Chart-specific pieces are the data type,
the options type (that chart's config, minus data), and the event props —
see Components.
| Prop | Type | Description |
|---|---|---|
data required | DataSet | MeshDataSet | OhlcDataSet | WindDataSet | That chart's data — the same shape the vanilla constructor takes. See Data model. |
options | Omit<XChartConfig, 'data'> | Every other config field that chart's doc page lists (style, animation, chrome, pan/zoom, …). |
layers | readonly ChartElement[] | Declarative annotations drawn on top of the chart. See below. |
className / style | string / CSSProperties | Applied to the chart's container <div>. |
onXxx | (event) => void | One prop per event that chart's .on(event, fn) supports: the nine
standard events every chart has, plus that chart's own — see
Components. |
ref | Ref<XChart> | The underlying vanilla chart instance once mounted. See below. |
data vs. options — what re-renders as what
The two props are deliberately handled differently, matching what the vanilla API can do efficiently:
| Prop change | What happens |
|---|---|
data changes (new reference) |
Calls the chart's update(data) (or setData for the wind rose) — grains morph to the new targets, no re-pour. |
options changes (new reference) |
The chart is disposed and recreated — config like style, animation timing or axes isn't dynamically patchable on the vanilla class either, so this mirrors it exactly. |
options. An inline object literal is a new reference every
render, which would recreate the chart on every re-render. Hoist it to module scope (as in the quick
start above) or wrap it in useMemo. The same goes for data, for a milder
reason: a fresh literal every render restarts the morph tween rather than recreating the chart.
Components
| Component | data type | Chart-specific event props | Config reference |
|---|---|---|---|
BarChart | DataSet | onHover, onSeriesFocus | Bar chart → |
LineChart | DataSet | onHover, onSeriesFocus | Line chart → |
PieChart | DataSet | onHover, onSeriesChange, onSeriesFocus | Pie chart → |
ScatterChart | MeshDataSet | onHover, onSeriesFocus | Scatter chart → |
WindRoseChart<Custom> | WindDataSet<Custom> | onHover, onSelect, onHighlight | Wind rose → |
CandlestickChart | OhlcDataSet | onHover, onSeriesChange, onSeriesFocus | Candlestick chart → |
Those are in addition to the standard event props, which all six
accept. Each payload is exactly what the matching vanilla .on(event, fn) passes — see
that chart's own Events section for the shape. WindRoseChart is generic
over the same Custom per-observation payload type as the vanilla class; it defaults to
unknown.
Standard events
Every component accepts a prop for each of the nine events every chart shares. The handler receives
the same object the vanilla listener does — the payload's fields plus preventDefault()
— and cancelling suppresses the chart's built-in reaction. That is worth calling
out, because React's own synthetic onClick does not work that way: here the veto is the
point.
| Prop | Payload fields | Cancel? |
|---|---|---|
onClick / onDblClick | meta, px, button | yes |
onDrag | phase, target, dx, dy, px | on 'start' |
onPan | offset, delta | yes |
onZoom | scale, factor, center | yes |
onDataAdd / onDataUpdate / onDataRemove |
action, items | yes — fires before the change applies |
onDrawFinished | now, frame | no |
<LineChart
data={data}
options={options}
onClick={({ meta }) => setSelected(meta?.xValue ?? null)}
// A read-only chart: the user can look, but not move the view.
onPan={(e) => e.preventDefault()}
onZoom={(e) => e.preventDefault()}
/>
onDrawFinished fires ~60×/second. A handler that calls
setState will re-render your component on every animation frame. Write to a
ref instead, or don't pass the prop.
CandlestickChart's onClick carries its original
candle and event fields alongside the standard meta,
px and button, so handlers written before the standard events keep
working. Full reference: Events & custom drawing.
Custom drawing — the layers prop
Pass declarative annotations and the component keeps them in sync with the chart. Positions default to data space, so they track the marks under pan and zoom.
// Hoisted: `layers` is compared by reference, exactly like `data`.
const layers: ChartElement[] = [
{ kind: 'rect', at: [0, 0.6], width: 1, height: 0.15, fill: 'rgba(56,189,248,0.12)' },
{ kind: 'line', from: [0, 0.75], to: [1, 0.75], stroke: '#38bdf8', lineDash: [4, 4] },
];
<BarChart data={data} layers={layers} style={{ height: 360 }} />
Only the elements this prop added are removed when it changes, so layers you add imperatively
through the ref (chart.addLayer(…)) are left alone. For the element
kinds, coordinate spaces and the raw per-frame callback, see
Events & custom drawing.
The ref — imperative access
A ref on any of these components resolves to the underlying chart instance — the exact
same object new XChart(el, config) would give you — once it has mounted (null
before that). Use it for anything that isn't a prop: focusSeries(),
getView()/panBy()/zoomTo(), repour(),
getData(), the wind rose's selectObservation() / selectSegment(),
and so on — see each chart's Methods section.
import { useRef } from 'react';
import { BarChart } from '@smalldat/sandycoast/react';
import type { BarChart as BarChartInstance } from '@smalldat/sandycoast';
function Chart() {
const ref = useRef<BarChartInstance>(null);
return (
<>
<button onClick={() => ref.current?.focusSeries(null)}>Clear isolation</button>
<BarChart ref={ref} data={data} style={{ height: 360 }} />
</>
);
}
More examples
Live data — the same morph the vanilla API gives you
function Live() {
const [data, setData] = useState(initialData);
// A new `data` reference morphs the grains in place — no re-pour.
const refresh = () => setData(fetchLatest());
return <BarChart data={data} style={{ height: 360 }} />;
}
Wind rose with a typed Custom payload
JSX doesn't take an explicit generic argument on the tag, so type windData itself —
WindRoseChart infers Custom from it.
interface Reading { stationId: string; }
const windData: WindDataSet<Reading> = { points: [/* … */] };
<WindRoseChart
data={windData}
onSelect={({ segment, observationId }) => console.log(segment?.key, observationId)}
/>
© 2026 · License · Data model →