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 type | When | Scale used |
|---|---|---|
'number' | Values are numbers | Linear |
'time' | Values are Dates | Time |
'category' | Values are strings | Band |
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).
| Function | Signature | Description |
|---|---|---|
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 / toNumeric | — | Lower-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.
Scales
Charts pick a scale from each field's type automatically. The scale primitives are exported for custom visuals.
| Export | For | Description |
|---|---|---|
LinearScale | numbers | Continuous linear mapping. |
TimeScale | Dates | Continuous time mapping with date-aware ticks. |
BandScale | categories | Discrete band mapping (one slot per category). |
makeScale | — | Factory: 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']
Easing
Used by animation.ease and every reveal.ease.
| Value | Feel |
|---|---|
'linear' | Constant speed. |
'easeOutCubic' default | Fast 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().
| Value | Description |
|---|---|
'auto' default | WebGPU 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
},
},
});
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Master switch for drag / wheel / UI pan & zoom. |
axes | 'x' | 'y' | 'both' | 'both' | Restrict pan/zoom to one axis. |
minZoom / maxZoom | number | 1 / 10 | Zoom clamp. minZoom: 1 keeps the data filling the plot. |
wheel / drag | boolean | true | Enable wheel zoom / drag pan input. |
controls.show | boolean | enabled | Draw the on-chart zoom buttons. |
controls.position | ZoomCorner | 'top-right' | Corner to pin to — anchored inside the plot and nudged clear of the FPS meter, so legend / FPS / zoom never overlap. |
controls.step | number | 1.4 | Zoom multiplier per +/− click. |
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
PanZoomController
and ZoomControls classes are exported too if you're building a custom visual.What's exported
From @smalldat/sandycoast:
- Charts:
BarChart,LineChart,ScatterChart,PieChart,WindRoseChart,CandlestickChart(+ their config & meta types). - Data:
fromRows,patchPoints,appendPoints,removePoints,seriesKeys,validate,inferType,resolveTypes,toNumeric,DataError(+DataSet,Point,Scalar,FieldType,PointPatch,PointReftypes). - Mesh & approximation:
meshPoints,resolveMeshTypes,validateMesh,NoneApproximation,StraightApproximation,SplineApproximation,LeastSquaresApproximation(+MeshValue,MeshPoint,MeshSeries,MeshDataSet,Approximation,FitPointtypes). See Mesh data & approximation. - OHLC (candlesticks):
allCandles,resolveOhlcTypes,validateOhlc,patchCandles,appendCandles,removeCandles,layoutCandles,hitCandle,resolveDirection,resolveCandleStyle,resolveActual(+Candle,CandleSeries,OhlcDataSet,CandlePatch,CandleRef,CandleMeta,CandleDirectiontypes). See OHLC data. - Scales:
LinearScale,TimeScale,BandScale,makeScale. - Color:
DEFAULT_PALETTE,parseColor,cssRGBA. - Pan & zoom:
PanZoomController,ZoomControls,resolvePanZoom,IDENTITY_VIEW(+PanZoomable,PanZoomConfig,ViewTransform,ZoomCornertypes). - Rendering & particles (advanced / custom visuals):
pickRenderer,allocGrains,packBars,packLine,packBlobs,packBoxes,grainCounts,lineGrainCounts,blobGrainCounts,boxGrainCounts,scatterStarts,evalGrain,ease,tracePolylinePath,catmullRomToBezier, and their types.
© 2026 · License · Bar chart →