Framework binding

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)}
    />
  );
}
Container sizing. Same rule as the vanilla charts: the chart fills its container via a 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.

PropTypeDescription
data requiredDataSet | MeshDataSet | OhlcDataSet | WindDataSet That chart's data — the same shape the vanilla constructor takes. See Data model.
optionsOmit<XChartConfig, 'data'> Every other config field that chart's doc page lists (style, animation, chrome, pan/zoom, …).
layersreadonly ChartElement[] Declarative annotations drawn on top of the chart. See below.
className / stylestring / 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.
refRef<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 changeWhat 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.
Memoize 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

Componentdata typeChart-specific event propsConfig reference
BarChartDataSet onHover, onSeriesFocusBar chart →
LineChartDataSet onHover, onSeriesFocusLine chart →
PieChartDataSet onHover, onSeriesChange, onSeriesFocusPie chart →
ScatterChartMeshDataSet onHover, onSeriesFocusScatter chart →
WindRoseChart<Custom>WindDataSet<Custom> onHover, onSelect, onHighlightWind rose →
CandlestickChartOhlcDataSet onHover, onSeriesChange, onSeriesFocusCandlestick 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.

PropPayload fieldsCancel?
onClick / onDblClickmeta, px, buttonyes
onDragphase, target, dx, dy, pxon 'start'
onPanoffset, deltayes
onZoomscale, factor, centeryes
onDataAdd / onDataUpdate / onDataRemove action, itemsyes — fires before the change applies
onDrawFinishednow, frameno
<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)}
/>
Try it live. The playground demo site has a React bindings tab with a runnable example — and its exact source — for every chart above.

© 2026 · License · Data model →