Shared

Data model & shared config

Every visual consumes the same generic x/y/z data model and the same palette, easing and backend options. Learn it once; it applies to bars, lines and every visual to come.

DataSet & Point

A DataSet is a flat array of points plus optional explicit field types. Each Point has x and y (numeric, datetime, or categorical string) and an optional series key z. Omit z for a single series.

type Scalar = number | Date | string;
type FieldType = 'number' | 'time' | 'category';

interface Point {
  x: Scalar;
  y: Scalar;
  z?: Scalar;   // optional series key
}

interface DataSet {
  points: Point[];
  xType?: FieldType;   // inferred from the first non-null value when omitted
  yType?: FieldType;
  zType?: FieldType;
}
Field typeWhenScale used
'number'Values are numbersLinear
'time'Values are DatesTime
'category'Values are stringsBand
Types are inferred from the first non-null value. Declare them explicitly on the DataSet only when inference would guess wrong (e.g. numeric-looking category codes).

Building from arbitrary rows

fromRows(rows, spec) maps any array of objects into a DataSet using accessor functions.

import { fromRows } from '@smalldat/sandycoast';

const data = fromRows(salesRows, {
  x: (r) => r.quarter,
  y: (r) => r.revenue,
  z: (r) => r.region,     // optional
  // xType / yType / zType optional — inferred otherwise
});

Array helpers

The pure functions behind chart.update / add / remove are exported so you can transform point arrays off-chart (e.g. in a store).

FunctionSignatureDescription
patchPoints(points, PointPatch[]) → Point[]Set y on existing points, matched by x (and z if given).
appendPoints(points, Point[]) → Point[]Append new points.
removePoints(points, PointRef[]) → Point[]Remove by index (negative = from end) or { x, z? } match.
seriesKeys(points) → Scalar[]Distinct z values, in first-seen order.
validate / inferType / resolveTypes / toNumericLower-level data utilities; validate throws DataError on malformed input.
type PointPatch = { x: Scalar; z?: Scalar; y: Scalar };
type PointRef   = number | { x: Scalar; z?: Scalar };  // -1 = last point

Mesh data & approximation

The scatter chart (and a future mesh chart) need a z that carries a numeric value, an independent timestamp and an opaque custom payload — for coloring and, later, mesh height/topology. That's a different shape than every other chart's z: Scalar series discriminator, so it's an additive sibling to Point/DataSet above, not a change to them.

interface MeshValue<Custom = unknown> {
  value?: number;      // coloring today, mesh height/weight later
  datetime?: Date;      // independent per-point timestamp, not the x axis
  custom?: Custom;     // opaque caller payload, carried through to hover events
}

interface MeshPoint<X = Scalar, Y = Scalar, Custom = unknown> {
  x: X;
  y: Y;
  z?: MeshValue<Custom>;
}

// z no longer discriminates series membership, so series are explicit:
interface MeshSeries<Custom = unknown> {
  key?: Scalar;         // legend label
  points: MeshPoint<Scalar, Scalar, Custom>[];
}

interface MeshDataSet<Custom = unknown> {
  series: MeshSeries<Custom>[];
  xType?: FieldType;
  yType?: FieldType;
}
MeshValue.value is typed and threaded through to hover metadata today, but does not yet drive marker color — there's no sequential/diverging color-scale utility in the library yet. It exists so a future "color by value" pass and a future mesh chart both consume one shape instead of inventing their own.

resolveMeshTypes / validateMesh / meshPoints are the MeshDataSet counterparts of resolveTypes / validate above (core/data/types.ts itself is untouched by any of this).

Scatter's swappable point-cloud fit — the approximation config's kind — is a small interface, also exported for building a custom strategy or reusing the built-ins elsewhere:

interface FitPoint { x: number; y: number; }

interface Approximation {
  readonly kind: string;
  fit(points: FitPoint[]): FitPoint[];
}

Built-ins: NoneApproximation, StraightApproximation, SplineApproximation, LeastSquaresApproximation. Full behavior table on the scatter chart page.

OHLC data (candlesticks)

The candlestick chart carries four prices per period instead of a single y, so — like the mesh types above — it gets an additive sibling to Point/DataSet rather than bending y into a tuple.

interface Candle<X = Scalar> {
  x: X;                // period start: a Date, a number, or a category label
  open: number;
  high: number;
  low: number;
  close: number;
  actual?: number;      // live / last traded price — orthogonal to close
}

// Two instruments can't share an x-slot legibly, so — like the pie chart's
// series axis — the series array is a *selector*: one is drawn at a time.
interface CandleSeries<X = Scalar> {
  key?: Scalar;          // slider / legend label
  candles: Candle<X>[];
}

interface OhlcDataSet<X = Scalar> {
  series: CandleSeries<X>[];
  xType?: FieldType;
}

resolveOhlcTypes / validateOhlc / allCandles are the OhlcDataSet counterparts of resolveTypes / validate / meshPoints. The array helpers have candle twins too — patchCandles (set only the prices you pass, matched by x), appendCandles and removeCandles (by index or { x }) — which is what update / add / remove call. All of them are pure and exported for use off-chart.

A candle's color is not a series color: it comes from a configurable difference rule (rising vs falling), so the palette is two entries and the legend describes those two groups.

Scales

Charts pick a scale from each field's type automatically. The scale primitives are exported for custom visuals.

ExportForDescription
LinearScalenumbersContinuous linear mapping.
TimeScaleDatesContinuous time mapping with date-aware ticks.
BandScalecategoriesDiscrete band mapping (one slot per category).
makeScaleFactory: returns the right Scale for a FieldType.

Colors

Series colors cycle through colors (or DEFAULT_PALETTE if unset). Any CSS hex (#rgb, #rrggbb, #rrggbbaa) or rgb() / rgba() string works. parseColor and cssRGBA convert to/from the internal RGBA (0..1) tuple.

import { DEFAULT_PALETTE } from '@smalldat/sandycoast';
// ['#e8598b', '#8bc4e8', '#e8c45a', '#5ae89a', '#b98be8', '#e8895a']
#e8598b #8bc4e8 #e8c45a #5ae89a #b98be8 #e8895a

Easing

Used by animation.ease and every reveal.ease.

ValueFeel
'linear'Constant speed.
'easeOutCubic' defaultFast start, gentle stop.
'easeOutQuint'Sharper deceleration than cubic.

Rendering backends

Set backend to force one, or leave it 'auto' to prefer WebGPU and fall back to Canvas2D. All backends implement the same lerp + jitter contract, so visuals look the same regardless of which one runs. Read the active one via chart.backend after whenReady().

ValueDescription
'auto' defaultWebGPU if available, else Canvas2D.
'webgpu'Force WebGPU (best for high grain counts).
'webgl2'Reserved — planned fallback, not yet implemented.
'canvas2d'Force Canvas2D (widest compatibility).

Pan & zoom

Every visual shares one pan/zoom implementation. It's a single affine transform applied in layout space — p' = p · scale + offset — so grains (on the GPU / Canvas2D) and the chrome (axes, grid, bars/line, current value) all move together and stay pixel-aligned. Off by default; add a panZoom block to enable it. When on, grains are clipped to the plot rect so panned content never spills into the axis/legend gutters.

new BarChart(host, {
  data,
  panZoom: {
    enabled: true,           // master switch (default false)
    axes: 'both',           // 'x' | 'y' | 'both' — which axes may pan/zoom
    minZoom: 1,              // fit; can't zoom out past the data extent
    maxZoom: 10,
    wheel: true,             // zoom on wheel / trackpad
    drag: true,              // pan on pointer drag
    controls: {                // on-chart +/-/reset buttons
      show: true,
      position: 'top-right', // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
      step: 1.4,             // zoom factor per +/- click
    },
  },
});
PropertyTypeDefaultDescription
enabledbooleanfalseMaster switch for drag / wheel / UI pan & zoom.
axes'x' | 'y' | 'both''both'Restrict pan/zoom to one axis.
minZoom / maxZoomnumber1 / 10Zoom clamp. minZoom: 1 keeps the data filling the plot.
wheel / dragbooleantrueEnable wheel zoom / drag pan input.
controls.showbooleanenabledDraw the on-chart zoom buttons.
controls.positionZoomCorner'top-right'Corner to pin to — anchored inside the plot and nudged clear of the FPS meter, so legend / FPS / zoom never overlap.
controls.stepnumber1.4Zoom multiplier per +/ click.
The zoom controls are anchored to a plot corner (inside the axes and legend), not the element corner, so they never sit on top of the legend. If the FPS meter shares the same corner the controls drop just below it.

Programmatic API — the PanZoomable interface

Both charts implement the same interface, so pan and zoom can be driven from code identically regardless of chart type. Coordinates are plot-local fractions in [0,1] (y-up): (0,0) is the plot's bottom-left, (1,1) the top-right.

interface PanZoomable {
  getView(): ViewTransform;                       // { scale:[sx,sy], offset:[ox,oy] }
  setView(view: Partial<ViewTransform>): void;   // clamped to the configured limits
  panBy(dx: number, dy: number): void;         // positive dx moves the data right
  panTo(x: number, y: number): void;         // put data fraction (x,y) at the plot origin
  zoomBy(factor: number, cx?: number, cy?: number): void; // about (cx,cy), default center
  zoomTo(scale: number, cx?: number, cy?: number): void;
  resetView(): void;                            // back to identity (fully zoomed out)
  isPanZoomEnabled(): boolean;
}
const chart = new LineChart(host, { data, panZoom: { enabled: true } });

chart.zoomTo(4);              // zoom 4× about the plot center
chart.panTo(0.75, 0);       // scroll to the last quarter of the data
chart.zoomBy(1.4, 1, 0.5);   // zoom in about the right edge
chart.resetView();           // fully zoom out
The same interface backs the on-chart buttons and the drag / wheel handlers, so UI, pointer input and code all share one clamped source of truth. The PanZoomController and ZoomControls classes are exported too if you're building a custom visual.

What's exported

From @smalldat/sandycoast:

© 2026 · License · Bar chart →