Visual

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.

Structural difference from bar/line/scatter. Every other visual draws all series at once, positioned along an axis. A pie draws one series' points as slicesdata 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).

PropertyTypeDefaultDescription
data requiredDataSet Points to render. See Data model. Each series (grouped by z) becomes one slider position; that series' points become slices.
grainDensitynumber0.6 Grain budget multiplier — the chart draws grainDensity * 20 000 grains (capped by maxGrains), independent of slice count. Slices share the budget by area.
maxGrainsnumber100000 Hard grain ceiling.
colorsstring[]DEFAULT_PALETTE Slice colors (CSS hex / rgb() / rgba()), cycled per slice (not per series — a pie only ever shows one series).
backgroundstringtransparent Canvas clear color (CSS).
grainobject Per-grain appearance. See below.
animationobject Pour-in + morph timing. See below.
innerRadius / radius / startAngle / padAnglenumbersee below Disc shape. See below.
seriesIndex / maxSlices / maxSeries / slidersee below Which series is shown, and the slider that picks it. See below.
interactionobject 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.
legendLegendConfigoff Same as bar chart, but entries are the slices of the displayed series, not series.
titleTitleConfigoff Chart title; shares the legend's placement vocabulary.
currentValueobjectoff Hover-tied readout — same shape as bar chart, format typed against SliceMeta.
slicesPieStyleConfigoff Solid fill + border that resolves as sand fades (the bar chart's bars, renamed). See below.
fpsFpsConfigoff Same as bar chart.
backend'auto' | 'webgpu' | 'webgl2' | 'canvas2d''auto' Force a rendering backend.
No panZoom — a pie has no scrollable data space to pan or zoom.

grain — grain appearance

PropertyTypeDefaultDescription
sizePxnumber3Grain size in CSS px (scaled by device-pixel-ratio).
shape'disc' | 'quad''disc'Round grain or square grain.
jitternumber (0..1)0.6Polar-grid jitter fraction — scatters grains off the packing grid within each wedge.
settleJitternumber0.004Baseline settle wobble amplitude in layout units.

animation — timing

Units: duration, stagger and morphDuration are given in milliseconds.
PropertyTypeDefaultDescription
durationnumber (ms)900Per-grain travel time.
staggernumber (ms)500Spread of pour start times.
ease'linear' | 'easeOutCubic' | 'easeOutQuint''easeOutCubic'Grain travel easing.
morphDurationnumber (ms)duration + staggerTransition 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

PropertyTypeDefaultDescription
innerRadiusnumber (0..0.95)0Hole radius as a fraction of the outer radius. 0 is a pie; anything above cuts the middle out and makes it a donut.
radiusnumber (0..1)0.92Outer radius as a fraction of the disc box's half-extent.
startAnglenumber (degrees)0Rotation of the first slice's leading edge, clockwise from 12 o'clock.
padAnglenumber (degrees)0Gap between adjacent slices.
The disc always sits in a square sub-rect of the plot, centered and re-derived on every resize, so it stays a true circle at any host aspect ratio without repacking grains.

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.

PropertyTypeDefaultDescription
seriesIndexnumber0Index of the series shown initially, clamped to the available range.
maxSlicesnumber10Max points of the shown series drawn as slices; extras are dropped.
maxSeriesnumber1000Max series the slider can address; extras are dropped.

sliderSliderConfig

PropertyTypeDefaultDescription
showbooleantrueDraw the slider (hidden anyway when there is one series).
position'top' | 'bottom''bottom'Edge to pin it to.
interactivebooleantrueLet the user drag/click the handle.
colorstringX axis colorTrack/handle color (CSS).
handlePxnumber7Handle radius in CSS px.
trackPxnumber3Track thickness in CSS px.
A programmatic 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

PropertyTypeDefaultDescription
opacitynumber (0..1)1Fill opacity.

slices.border

PropertyTypeDefaultDescription
showbooleantrue when a border block is presentDraw the wedge outline.
widthnumber1Stroke width in CSS px.
opacitynumber (0..1)1Stroke 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 addresses slices, not series. Because a pie only ever shows one series, 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

MethodDescription
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(): numberIndex of the series currently drawn.
getSeriesKeys(): (Scalar | undefined)[]Series keys in slider order (after the maxSeries cap).
get seriesCountNumber 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(): DataSetDeep-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 →