Pie chart
Sand pours into a ring of annular sectors, then a solid slice fill +
border resolves on top, the same particle→solid crossfade every visual shares. Set
innerRadius above 0 and a pie becomes a donut. Unlike the
other charts a pie shows one series at a time — a slider (styled from
axes.x) picks which, and switching series morphs the wedges to the new values.
data is still a plain DataSet, but
the series axis (z) becomes a selector (the slider) rather than a
visual dimension, and a slice's category is each point's x.Constructor
new PieChart(host: HTMLElement, config: PieChartConfig)
Mounts a canvas into host and boots the best available backend (WebGPU → Canvas2D).
Only data is required.
Config — top level
Mirrors the bar chart's config surface for the shared blocks; the pie-specific additions are the
geometry knobs, seriesIndex/maxSlices/maxSeries/slider,
and slices (the bar chart's bars, renamed).
| Property | Type | Default | Description |
|---|---|---|---|
data required | DataSet | — | Points to render. See Data model. Each series (grouped by z) becomes one slider position; that series' points become slices. |
grainDensity | number | 0.6 | Grain budget multiplier — the chart draws grainDensity * 20 000 grains (capped by maxGrains), independent of slice count. Slices share the budget by area. |
maxGrains | number | 100000 | Hard grain ceiling. |
colors | string[] | DEFAULT_PALETTE | Slice colors (CSS hex / rgb() / rgba()), cycled per slice (not per series — a pie only ever shows one series). |
background | string | transparent | Canvas clear color (CSS). |
grain | object | — | Per-grain appearance. See below. |
animation | object | — | Pour-in + morph timing. See below. |
innerRadius / radius / startAngle / padAngle | number | see below | Disc shape. See below. |
seriesIndex / maxSlices / maxSeries / slider | — | see below | Which series is shown, and the slider that picks it. See below. |
interaction | object | — | Hover effects (per slice) and legend dim tuning — same shape as the bar chart. |
axes | { x? } | off | axes.x styles the slider's ticks; there is no Y axis on a pie. See below. |
legend | LegendConfig | off | Same as bar chart, but entries are the slices of the displayed series, not series. |
title | TitleConfig | off | Chart title; shares the legend's placement vocabulary. |
currentValue | object | off | Hover-tied readout — same shape as bar chart, format typed against SliceMeta. |
slices | PieStyleConfig | off | Solid fill + border that resolves as sand fades (the bar chart's bars, renamed). See below. |
fps | FpsConfig | off | Same as bar chart. |
backend | 'auto' | 'webgpu' | 'webgl2' | 'canvas2d' | 'auto' | Force a rendering backend. |
panZoom — a pie has no scrollable data space to pan or zoom.grain — grain appearance
| Property | Type | Default | Description |
|---|---|---|---|
sizePx | number | 3 | Grain size in CSS px (scaled by device-pixel-ratio). |
shape | 'disc' | 'quad' | 'disc' | Round grain or square grain. |
jitter | number (0..1) | 0.6 | Polar-grid jitter fraction — scatters grains off the packing grid within each wedge. |
settleJitter | number | 0.004 | Baseline settle wobble amplitude in layout units. |
animation — timing
duration, stagger and
morphDuration are given in milliseconds.| 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 (wedge tween + grain fade) for a series change / update / add / remove. |
reflow | 'translate' | 'reshuffle' | 'withSlice' | 'translate' | How grains of an unchanged slice move on a data change. withSlice locks them rigidly to the wedge — no delay, no fade flash. |
enter | 'pour' | 'rise' | 'pour' | How an added slice's grains appear. rise grows out from the disc center (the pie's analogue of the bar chart's rise-from-base). |
exit | 'fall' | 'vanish' | 'fall' | How a removed slice's grains leave. |
Pie geometry — top-level properties
| Property | Type | Default | Description |
|---|---|---|---|
innerRadius | number (0..0.95) | 0 | Hole radius as a fraction of the outer radius. 0 is a pie; anything above cuts the middle out and makes it a donut. |
radius | number (0..1) | 0.92 | Outer radius as a fraction of the disc box's half-extent. |
startAngle | number (degrees) | 0 | Rotation of the first slice's leading edge, clockwise from 12 o'clock. |
padAngle | number (degrees) | 0 | Gap between adjacent slices. |
Series selection & slider
A pie shows exactly one series at a time. The slider — which replaces the bar/line/scatter X axis,
and takes its tick styling from the same axes.x block — lets the viewer pick which;
dragging it (or calling setSeriesIndex) morphs the wedges to the new series' values.
It auto-hides when there is only one series.
| Property | Type | Default | Description |
|---|---|---|---|
seriesIndex | number | 0 | Index of the series shown initially, clamped to the available range. |
maxSlices | number | 10 | Max points of the shown series drawn as slices; extras are dropped. |
maxSeries | number | 1000 | Max series the slider can address; extras are dropped. |
slider — SliderConfig
| Property | Type | Default | Description |
|---|---|---|---|
show | boolean | true | Draw the slider (hidden anyway when there is one series). |
position | 'top' | 'bottom' | 'bottom' | Edge to pin it to. |
interactive | boolean | true | Let the user drag/click the handle. |
color | string | X axis color | Track/handle color (CSS). |
handlePx | number | 7 | Handle radius in CSS px. |
trackPx | number | 3 | Track thickness in CSS px. |
setSeriesIndex call and a dragged
handle are the same code path — both morph the wedges and ease the handle across the track
identically.slices — solid fill + border PieStyleConfig
Off by default; a config without slices renders as pure sand. Fill and border
always use the slice's color — only opacity and stroke width are configurable
(mirrors the bar chart's bars, minus the per-side flags — a wedge outline has no sides
to pick).
slices.fill
| Property | Type | Default | Description |
|---|---|---|---|
opacity | number (0..1) | 1 | Fill opacity. |
slices.border
| Property | Type | Default | Description |
|---|---|---|---|
show | boolean | true when a border block is present | Draw the wedge outline. |
width | number | 1 | Stroke width in CSS px. |
opacity | number (0..1) | 1 | Stroke opacity. |
slices.reveal — particle→solid crossfade
Same shape as the bar chart's bars.reveal:
start, duration, ease, grainsTo.
Shared blocks
interaction.hover, interaction.dim, legend,
currentValue and fps take the same shape the bar chart uses. Full property
tables: interaction.hover ·
interaction.dim ·
legend ·
currentValue ·
fps. axes.x is
the same AxisConfig shape but styles the slider, not
a drawn axis; axes.y is ignored (a pie has no Y axis).
legend and interaction.dim isolate a slice of
the currently displayed series when clicked — focusSeries(index) is named to match the
other charts' public API, but on a pie index is a slice index, not a slider position.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('seriesChange', fn) | Subscribe to slider moves (drag or setSeriesIndex). See Events. |
on('seriesFocus', fn) | Subscribe to legend (slice) isolation changes. See Events. |
getSeriesIndex(): number | Index of the series currently drawn. |
getSeriesKeys(): (Scalar | undefined)[] | Series keys in slider order (after the maxSeries cap). |
get seriesCount | Number of series the slider can address. |
setSeriesIndex(index) | Show another series; slices morph to the new values and the handle eases across, exactly as a drag would. Clamped to the valid range; a no-op if unchanged. |
focusSeries(index) | Isolate one slice by index — full opacity, every other slice dims to interaction.dim.opacity. Pass null to clear. Fires seriesFocus. (See the note under Shared blocks on why this addresses a slice.) |
getFocusedSeries() | number | null — the currently isolated slice index, or null. |
setTitle(text) | Replace the title text; empty string hides it. The plot re-insets around it. |
update(data) | Replace the whole dataset; grains morph to the new targets and the wedge/slice layer tweens (no re-pour). No patch overload — a pie's slices are keyed by x within the shown series, so pass a full DataSet. |
add(points) | Append one or more points; new slices open and their grains enter per animation.enter. |
remove(refs) | Remove points by PointRef (index, negative = from end, or {x, z?} match); remaining slices reflow to fill the circle. |
repour() | Re-run the pour-in animation with the current data (no morph). |
getData(): DataSet | Deep-cloned snapshot of the current dataset. |
dispose() | Cancel the loop, remove listeners, free the backend and DOM. |
Events
hover carries { slice: SliceMeta | null } (null on pointer-leave).
// SliceMeta — metadata for one drawn slice
{
sliceId: number;
xValue: Scalar; // the slice's category
seriesKey: Scalar | undefined;
seriesIndex: number;
value: number; // raw y
fraction: number; // share of the series total, 0..1
a0: number; a1: number; // start/end angle, radians clockwise from 12 o'clock
rInner: number; rOuter: number; // layout-space radii (rInner 0 for a pie)
color: RGBA;
}
seriesChange fires whenever the slider selection changes — from a drag or
a setSeriesIndex() call:
// SeriesChangePayload
{
index: number;
key: Scalar | undefined;
}
seriesFocus carries { index: number | null } — the isolated slice
index (see the note under Shared blocks).
Examples
Donut with solid slices
new PieChart(el, {
data, // one series: points are the slice categories
innerRadius: 0.6,
padAngle: 1.5,
slices: {
fill: { opacity: 0.9 },
border: { show: true, width: 1.5 },
reveal: { start: 'afterPour', duration: 600, grainsTo: 0.1 },
},
});
Multiple series — slider morphs between them
new PieChart(el, {
data, // DataSet: points grouped by z into several series ('2024', '2025', ...)
axes: { x: { tickFormat: (v) => `FY ${v}` } },
slider: { position: 'bottom' },
animation: { morphDuration: 700 },
});
Legend and current-value readout
new PieChart(el, {
data,
legend: { show: true, position: 'right' },
currentValue: {
show: true,
format: (slice) => `${slice.xValue}: ${(slice.fraction * 100).toFixed(1)}%`,
},
});
Live data — update, add, remove
// Replace the dataset; slices morph to the new shares:
chart.update({ points: [{ x: 'EU', y: 180 }, { x: 'US', y: 140 }, { x: 'APAC', y: 90 }] });
// Append a slice; it opens and its grains pour in:
chart.add({ x: 'LATAM', y: 30 });
// Remove by {x} match or by index:
chart.remove([{ x: 'APAC' }]);
Reacting to the slider
chart.on('seriesChange', ({ index, key }) => {
heading.textContent = `Showing: ${key}`;
});
// Drive it from code — same morph, same event, as dragging the slider:
nextButton.addEventListener('click', () => {
chart.setSeriesIndex(chart.getSeriesIndex() + 1);
});
© 2026 · License · Data model →