Scatter chart
Sand settles into a small cloud around each point, then a solid marker
glyph — circle, triangle, square or asterisk — resolves on top, the scatter analogue of the line
chart's solid line and the bar chart's solid border. Both axes are continuous
(numeric or time), and a swappable approximation strategy can fit a trend/connector
line over each series' cloud.
data is
a MeshDataSet — an explicit array of series, not a
flat Point[] grouped by z — because z here is a per-point
value payload (MeshValue), not a series
key. (2) X is positioned with a real continuous scale
(LinearScale/TimeScale), not slotted into evenly-spaced categories like
the line chart's X axis.Constructor
new ScatterChart(host: HTMLElement, config: ScatterChartConfig)
Mounts a canvas into host and boots the best backend (WebGPU → Canvas2D). Only
data is required.
Config — top level
Mirrors the line chart's config field-for-field except for data (a
MeshDataSet, not a DataSet), pointRadius (new, replaces
lineThickness), line → marker, and the new
approximation block.
| Property | Type | Default | Description |
|---|---|---|---|
data required | MeshDataSet | — | An explicit array of series. See Mesh data model. |
pointRadius | number | 0.02 | New for scatter charts. Sand-cloud radius grains scatter within per point, in layout units. |
grainDensity | number | 0.6 | Grains per unit area of every point's cloud. |
maxGrains | number | 100000 | Hard grain ceiling. Grains scale with total cloud area, not point count, so cost stays bounded as the data grows. |
colors | string[] | DEFAULT_PALETTE | Series colors (CSS), cycled per series. |
background | string | transparent | Canvas clear color. |
grain | object | — | Grain appearance. See below. |
animation | object | — | Timing. See below. |
interaction | object | — | Same as bar chart, but hover targets a single point rather than a whole series; dim (legend click-to-isolate) targets a whole series, same as everywhere else. |
axes | { x?, y? } | off | Same shape as bar chart — both continuous here (see the note above). |
legend | LegendConfig | off | Same as bar chart, including click-to-isolate (interactive). |
title | TitleConfig | off | Chart title; shares the legend's placement vocabulary. |
currentValue | object | off | Same as bar chart. |
marker | MarkerStyleConfig | off | Solid marker glyph per point. See below. |
approximation | ScatterApproximationConfig | off | Trend/connector fit over each series' cloud. See below. |
fps | FpsConfig | off | Same as bar chart. |
panZoom | PanZoomConfig | off | Drag to pan, wheel/UI to zoom. Shared pan & zoom. |
backend | 'auto' | 'webgpu' | 'webgl2' | 'canvas2d' | 'auto' | Force a rendering backend. |
grain
Same shape as the bar chart: sizePx (3), shape ('disc'),
settleJitter (0.004). jitter (default 0.6) means something slightly
different here — it scales how far grains spread across the point's cloud radius
(0 collapses every grain onto the point center, 1 fills the whole
pointRadius), the same way it scales the line chart's ribbon width. See
bar chart → grain for the other properties.
animation
duration, stagger and
morphDuration are in milliseconds.Same as the bar chart, with the line chart's two three-way twists renamed for markers:
| Property | Type | Default | Description |
|---|---|---|---|
duration | number (ms) | 900 | Per-grain travel time. |
stagger | number (ms) | 500 | Spread of pour start times. |
ease | 'linear' | 'easeOutCubic' | 'easeOutQuint' | 'easeOutCubic' | Grain travel easing. |
morphDuration | number (ms) | duration + stagger | Transition window for live changes (marker tween + grain fade). |
reflow | 'translate' | 'reshuffle' | 'withMarker' | 'translate' | How grains of an unchanged point move. withMarker (vs. the line's withLine) locks them rigidly to the marker — no delay, no fade flash. |
morphGrains | boolean | true | Animate the sand during an update/add/remove morph. false snaps every grain straight to its new position so only the solid marker tweens on a data change. Ignored for the initial pour-in. |
enter | 'pour' | 'rise' | 'continue' | 'pour' | How an added point's grains appear. continue skips the emergence entirely — the marker appears already settled at its position. Best for continuous streaming. |
exit | 'fall' | 'vanish' | 'fall' | How a removed point's grains leave. |
marker — solid glyph MarkerStyleConfig
The scatter analogue of the line chart's solid line / bar chart's solid
border. Off by default; a config without marker renders as pure sand.
Shape/size cycle per series exactly like colors; the marker always uses the point's
series color.
| Property | Type | Default | Description |
|---|---|---|---|
shape | 'circle' | 'triangle' | 'square' | 'asterisk' | 'circle' | Default shape for every series. |
shapes | MarkerShape[] | — | Per-series shape cycling, like colors. Overrides shape per series. |
size | number | 6 | Marker size in CSS px. |
sizes | number[] | — | Per-series size cycling. |
opacity | number (0..1) | 1 | Marker fill opacity (stroke opacity for asterisk). |
reveal | RevealConfig | — | Timing of the particle→marker crossfade — same shape as the bar chart's bars.reveal (start, duration, ease, grainsTo). |
Path2D per (shape, size) pair and translated per
point — the shape is never rebuilt per point per frame, so marker count doesn't change per-frame
drawing cost beyond the translate + fill/stroke itself.approximation — trend/connector fit ScatterApproximationConfig
Off by default. When set, each series' point cloud is fit independently and drawn as a stroked
path once the markers resolve, colored by that series unless color overrides it.
| Property | Type | Default | Description |
|---|---|---|---|
kind | 'none' | 'straight' | 'spline' | 'leastSquares' | Approximation | 'none' | Built-in fit, or any object implementing Approximation for a fully custom strategy. See below. |
width | number | 1 | Stroke width in CSS px. |
opacity | number (0..1) | 1 | Stroke opacity. |
color | string | series color | Stroke color override (CSS). |
Built-in kinds
| kind | Behavior |
|---|---|
'none' | No-op; nothing is drawn. |
'straight' | Connect consecutively, in the order the series' points are given. |
'spline' | Smooth with a Catmull-Rom curve through the points in the order given (the same curve math the line chart's spline line style uses, extracted to core/geometry/curve.ts). |
'leastSquares' | Ordinary linear least-squares (y = mx + b) over the whole series (order-independent), drawn as the two endpoints spanning its x-domain. A near-vertical cloud (all points at ~the same x) falls back to the mean vertical instead of dividing by ~0. |
straight and
spline connect the points of each series in the order they appear in
data.series[i].points — they do not sort by x. A function-shaped cloud (one y per x)
typically wants its points pre-sorted by x; a non-monotonic shape like a spiral wants its own
natural order (e.g. the order the points were traced in). Sort your series before handing it to
the chart if you need x-order.Passing an object in place of a kind string plugs in a fully custom fitter without
touching the chart:
interface FitPoint { x: number; y: number; }
interface Approximation {
readonly kind: string;
fit(points: FitPoint[]): FitPoint[]; // unordered cloud -> ordered path to connect with straight segments
}
Shared blocks
axes, legend, currentValue, fps,
interaction.hover and interaction.dim take the same shape the bar chart
uses. Full property tables:
axes ·
legend ·
currentValue ·
fps ·
interaction.hover / interaction.dim.
axes.x
and axes.y are continuous scales built straight from the data's numeric
or time extent, padded ~8% on each side so extreme points don't sit flush against the plot edge.legend.interactive / focusSeries()
dims that series' markers and its approximation
trend/connector line together.Methods
| Method | Description |
|---|---|
whenReady(): Promise<void> | Resolves once the backend is initialized and the first frame is scheduled. |
get backend | 'webgpu' | 'webgl2' | 'canvas2d' | null. |
on('hover', fn) | Subscribe to hover; returns an unsubscribe function. |
on('seriesFocus', fn) | Subscribe to legend isolation changes; returns an unsubscribe function. See Events. |
focusSeries(index) | Isolate one series by index — it stays full opacity, every other series (markers + approximation line) dims to interaction.dim.opacity. Pass null to clear. Equivalent to clicking that series' legend.interactive entry; fires seriesFocus. |
getFocusedSeries() | number | null — the currently isolated series index, or null. |
update(data) | Replace the whole MeshDataSet; grains morph and the marker/approximation layer tweens (no re-pour). There is no point-patch overload — scatter has no z series key to match a patch against. |
add(points, seriesIndex = 0) | Append one or more MeshPoints to a series (created if seriesIndex is past the end); new markers grow in per animation.enter. |
remove(indices, seriesIndex = 0) | Remove points from a series by index within it (negative = from end); the cloud reflows. |
repour() | Re-run the pour-in animation with the current data (no morph). |
getData(): MeshDataSet | Deep-cloned snapshot of the current dataset. |
dispose() | Cancel the loop, remove listeners, free the backend and DOM. |
It also implements the shared PanZoomable
interface: getView(), setView(v), panBy(),
panTo(), zoomBy(), zoomTo(), resetView(),
isPanZoomEnabled().
z key to match points
across a rebuild, so morph identity falls back to a point's (seriesIndex, index within that
series) — same limitation the line chart has for points sharing an x-slot. Inserting or
removing a point inside a series shifts every later point's identity for that morph.Events
The hover event carries { point: ScatterMeta | null }, and — unlike the
line chart, which highlights the whole hovered series — targets a single point.
// ScatterMeta — metadata for one drawn point
{
pointId: number;
seriesIndex: number;
indexInSeries: number; // position within its series — the morph identity key
seriesKey: Scalar | undefined;
xValue: Scalar;
yValue: Scalar;
meshValue: MeshValue | undefined; // the point's z payload, if any
cx: number; cy: number; // layout-space position [0,1]
color: RGBA;
}
seriesFocus carries { index: number | null } — same shape as the bar chart's.
See bar chart → Events.
Examples
Solid markers over a point cloud
new ScatterChart(el, {
data,
pointRadius: 0.02,
marker: { shape: 'circle', size: 7, reveal: { duration: 500, grainsTo: 0.1 } },
axes: { x: { show: true }, y: { show: true, gridLines: true } },
});
Trend line via least-squares
new ScatterChart(el, {
data,
marker: { shape: 'circle', size: 5, opacity: 0.85 },
approximation: { kind: 'leastSquares', width: 2 },
});
Custom approximation strategy
const movingAverage: Approximation = {
kind: 'movingAverage',
fit(points) {
const sorted = [...points].sort((a, b) => a.x - b.x);
return sorted.map((p, i) => {
const window = sorted.slice(Math.max(0, i - 4), i + 5);
return { x: p.x, y: window.reduce((s, w) => s + w.y, 0) / window.length };
});
},
};
new ScatterChart(el, { data, approximation: { kind: movingAverage } });
Per-series marker shapes
new ScatterChart(el, {
data, // MeshDataSet: series: [{ key: 'A', points: [...] }, { key: 'B', points: [...] }]
marker: { shapes: ['circle', 'triangle', 'square'], sizes: [6, 8, 6] },
legend: { show: true },
});
Streaming: appended points settle without a pour
const chart = new ScatterChart(el, {
data,
marker: { shape: 'circle' },
animation: { enter: 'continue' }, // no pour — marker appears settled
});
setInterval(() => {
chart.add({ x: Date.now(), y: nextSample() }, 0); // series 0
}, 1000);
Hover
chart.on('hover', ({ point }) => {
if (point) label.textContent = `(${point.xValue}, ${point.yValue}) · ${point.seriesKey}`;
});
Series dimming — click a legend entry to isolate it
new ScatterChart(el, {
data,
legend: { show: true, interactive: true },
approximation: { kind: 'leastSquares' }, // trend lines dim along with their series' markers
interaction: { dim: { opacity: 0.15 } },
});
© 2026 · License · Data model →