Visual

Bar chart

Sand grains pour in from above and settle into bars. Optionally a solid fill + border resolves as the sand fades out. Supports live update / add / remove with a smooth morph, plus hover interaction.

Constructor

new BarChart(host: HTMLElement, config: BarChartConfig)

Mounts a canvas into host and boots the best available backend (WebGPU → Canvas2D). The only required field is data; everything else has a default. Await chart.whenReady() if you need the backend initialized before your next step.

Config — top level

PropertyTypeDefaultDescription
data requiredDataSet Points to render. See Data model.
grainDensitynumber0.6 Grains per unit area of layout space. Higher = denser sand (and more work).
maxGrainsnumber100000 Hard grain ceiling; the total never exceeds this regardless of bar count (bars share the budget proportionally), so grain cost stays bounded as the data grows.
colorsstring[]DEFAULT_PALETTE Series colors (CSS hex / rgb() / rgba()), cycled per series.
backgroundstringtransparent Canvas clear color (CSS).
grainobject Per-grain appearance. See below.
animationobject Pour-in + morph timing. See below.
interactionobject Hover effects. See below.
axes{ x?, y? }off X and Y axes, each off by default. See below.
legendLegendConfigoff Positionable series legend. See below.
currentValueobjectoff Hover-tied value readout. See below.
barsBarStyleConfigoff Solid fill + border that resolves as sand fades. See below.
fpsFpsConfigoff On-screen FPS meter. See below.
panZoomPanZoomConfigoff Drag to pan, wheel/UI to zoom. See below.
backend'auto' | 'webgpu' | 'webgl2' | 'canvas2d''auto' Force a rendering backend. 'auto' prefers WebGPU, falls back to Canvas2D.

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.6Grid jitter fraction — scatters grains off the packing grid so they look natural.
settleJitternumber0.004Baseline settle wobble amplitude in layout units — the faint shimmer of settled sand.

animation — timing

Units: duration, stagger and morphDuration are given in milliseconds. (The source type comment says "seconds"; the implementation divides by 1000, so pass ms as in the examples.)
PropertyTypeDefaultDescription
durationnumber (ms)900Per-grain travel time from start to settled.
staggernumber (ms)500Spread of pour start times across grains, so they don't all land at once.
ease'linear' | 'easeOutCubic' | 'easeOutQuint''easeOutCubic'Grain travel easing.
morphDurationnumber (ms)duration + staggerTransition window (border tween + grain fade) for update / add / remove.
reflow'translate' | 'reshuffle' | 'withBar''translate' How grains of an unchanged bar move on a data change. translate slides each grain 1:1 (with a settle stagger); reshuffle flows them from random old grains; withBar locks them rigidly to the bar — no delay, no fade flash (quietest).
enter'pour' | 'rise''pour'How an added bar's grains appear. pour falls from above; rise grows up from the base.
exit'fall' | 'vanish''fall'How a removed bar's grains leave. fall drops them off the bottom and fades; vanish removes them instantly.

interaction.hover

PropertyTypeDefaultDescription
effects('highlight' | 'jitter' | 'opacity')[]['highlight','jitter']Which effects apply to the hovered bar's grains.
highlightGainnumber1.6Color multiplier for hovered grains (highlight effect).
jitterAmpnumber0.008Extra motion amplitude for hovered grains (jitter effect), layout units.
opacitynumber (0..1)1Target opacity for hovered grains (opacity effect) — lets them pop even after the reveal has faded the sand.
fadeMsnumber (ms)180Enter/leave transition time; the effect eases in on enter and out on leave (or crossfades between bars).

interaction.dim — legend click-to-isolate tuning

Has no effect until a series is actually isolated — via a click on an legend.interactive entry, or a call to focusSeries(index). Both are the same code path, so a click and a programmatic call always agree and both fire seriesFocus.

PropertyTypeDefaultDescription
opacitynumber (0..1)0.15Opacity of every dimmed (non-isolated) series — grains, and any solid fill/border/line/marker layer.
fadeMsnumber (ms)200Ease in/out time for the dim transition, mirroring interaction.hover.fadeMs.
new BarChart(el, {
  data,
  legend: { show: true, interactive: true },   // click an entry to isolate it
  interaction: { dim: { opacity: 0.1, fadeMs: 300 } },
});

axes.x / axes.yAxisConfig

Each axis is off until you set show: true. Both axes share the same shape.

PropertyTypeDefaultDescription
showbooleanfalseDraw this axis.
ticksnumber | falseautoApprox tick count, or false for an axis line with no ticks.
tickFormat(v: Scalar) => stringcompact # / locale dateFormat a tick value into its label.
labelstringAxis title, drawn beside the ticks.
gridLinesbooleanfalseExtend ticks across the plot as grid lines.
colorstringsubdued grayLine / label color (CSS).
fontPxnumber11Tick label font size in px.
fontFamilystring'system-ui, sans-serif'Tick / title font-family stack (CSS).
fontWeightstring | number'normal'Tick / title font weight (CSS).
titleDirection'up' | 'down''up'Reading direction of the rotated Y-axis title (ignored on X).

legendLegendConfig

PropertyTypeDefaultDescription
showbooleanfalseShow the legend.
position'left' | 'right' | 'top' | 'bottom''bottom'Which edge to place it on.
align'start' | 'center' | 'end''center'Cross-axis alignment.
swatch'disc' | 'square'follows grain shapeSwatch shape.
interactivebooleanfalseClick an entry to isolate it — every other series dims (see interaction.dim) — and click it again to clear. Off by default; the legend stays purely presentational until you opt in.

currentValue

PropertyTypeDefaultDescription
showbooleanfalseShow the readout.
mode'pointer' | 'axis' | Side'pointer'pointer follows the cursor; a Side pins it to that edge; axis (On axes) drops the floating box and shows the value(s) directly on the axes — a highlighted marker beside the Y axis and/or below the X axis.
format(bar: BarMeta) => stringFormat the hovered bar into a readout string.
guide'none' | 'x' | 'y' | 'both''y'Cursor guide line(s) to the hovered point. y = horizontal to the value axis, x = vertical to the category axis, both = crosshair. Supersedes showGuide.
markersbooleantrueWhen a guide line is on, highlight the matching axis with a value marker (filled tick label at the cursor's row/column).
showGuidebooleantruelegacy Horizontal guide to the value axis. Fallback for guide when it is omitted (true'y', false'none').
colorstringText / line / marker color (CSS).

bars — solid fill + border BarStyleConfig

Off by default; a config without bars renders as pure sand. When present, a solid shape crossfades in as the grains fade out. Fill and border always use the bar's series color — only opacity, width and which sides are configurable.

bars.fill

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

bars.border

PropertyTypeDefaultDescription
left / top / right / bottombooleanall four*Which edges to stroke. *When a border block is present but no side flags are set, all four are drawn.
widthnumber1Stroke width in CSS px.
opacitynumber (0..1)1Stroke opacity.

bars.reveal — particle→solid crossfade

PropertyTypeDefaultDescription
start'afterPour' | number'afterPour'When the fade begins. afterPour = once pour + settle finishes (duration + stagger). A number = absolute seconds from start.
durationnumber (ms)500Fade window length.
easeEasing'easeOutCubic'Fade easing.
grainsTonumber (0..1)0Grain end-opacity after the fade (0 = grains disappear, leaving only the solid bar).

fpsFpsConfig

PropertyTypeDefaultDescription
positionSide | 'off''off'Edge/corner to pin the meter to. left/right sit in the top corner; top/bottom are centered on that edge.
colorstringsubdued light grayText color (CSS).

panZoomPanZoomConfig

Drag to pan, wheel / on-chart buttons to zoom, or drive it from code via the shared PanZoomable interface — one transform moves the grains and all chrome together. Off by default.

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{ show, position, step }shownOn-chart +//reset buttons; position is a corner ('top-right' default), anchored inside the plot and kept clear of the legend and FPS meter.
new BarChart(el, {
  data,
  panZoom: { enabled: true, axes: 'x', maxZoom: 20,
    controls: { position: 'top-right' } },
});

Methods

MethodDescription
whenReady(): Promise<void>Resolves once the backend is initialized and the first frame is scheduled.
get backend'webgpu' | 'webgl2' | 'canvas2d' | null — the active backend, or null before ready.
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 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) / update(patches)Replace the whole dataset, or — given PointPatch[] — set y on existing points in place. Grains morph; the solid layer tweens (no re-pour).
add(points)Append one or more points; new bars grow in and their grains enter per animation.enter.
remove(refs)Remove points by index (negative = from end) or { x, z? } match; bars reflow.
repour()Re-run the pour-in animation with the current data (no morph).
getData(): DataSetDeep-cloned snapshot of the current dataset.
getView() / setView(v)Read / set the pan/zoom transform { scale:[sx,sy], offset:[ox,oy] } (clamped).
panBy(dx,dy) / panTo(x,y)Pan by a plot-local delta / to a data fraction. See PanZoomable.
zoomBy(f,cx?,cy?) / zoomTo(s,cx?,cy?)Zoom by a factor / to an absolute scale, about a plot-local anchor (default center).
resetView() / isPanZoomEnabled()Return to the identity view / query whether pan/zoom is on.
dispose()Cancel the loop, remove listeners, free the backend and DOM. Call on teardown.

Events

hover carries { bar: BarMeta | null } (null on pointer-leave).

// BarMeta — metadata for one drawn bar
{
  barId: number;
  seriesIndex: number;
  seriesKey: Scalar | undefined;
  xValue: Scalar;         // number | Date | string
  yValue: number;
  x0: number; x1: number;  // layout-space extents [0,1]
  height: number;
  color: RGBA;            // [r,g,b,a] in 0..1
}

seriesFocus fires whenever the isolated series changes — from a legend click or a focusSeries() call, both the same code path.

// SeriesFocusPayload
{
  index: number | null;   // null = every series shown at full opacity
}

Examples

Solid bars that resolve from sand

new BarChart(el, {
  data,
  bars: {
    fill: { opacity: 0.85 },
    border: { top: true, width: 1.5 },   // only the top edge
    reveal: { start: 'afterPour', duration: 600, grainsTo: 0.12 },
  },
});

Axes, grid, legend and value readout

new BarChart(el, {
  data,
  axes: {
    x: { show: true, label: 'Quarter' },
    y: { show: true, ticks: 5, gridLines: true, label: 'Revenue' },
  },
  legend: { show: true, position: 'top', align: 'end' },
  currentValue: {
    show: true,
    format: (bar) => `${bar.xValue}: €${bar.yValue}k`,
  },
});

Live data — update, add, remove

// Set y on existing points (matched by x, and z if given):
chart.update([{ x: 'Q2', z: 'EU', y: 175 }]);

// Append points; new bars grow in:
chart.add([{ x: 'Q3', y: 60, z: 'EU' }, { x: 'Q3', y: 45, z: 'US' }]);

// Remove by {x,z?} match or by index:
chart.remove([{ x: 'Q1' }]);   // drop every series bar at Q1
chart.remove([-1]);          // drop the last point
Unchanged bars just move to their new position; only added and removed bars get a distinct animation. Tune it with animation.reflow / enter / exit / morphDuration.

Reacting to hover

const off = chart.on('hover', ({ bar }) => {
  if (!bar) return;                // pointer left the plot
  tooltip.textContent = `${bar.seriesKey} · ${bar.yValue}`;
});
// later: off();  // unsubscribe

Series dimming — click a legend entry to isolate it

const chart = new BarChart(el, {
  data,
  legend: { show: true, interactive: true },
  interaction: { dim: { opacity: 0.15 } },
});

chart.on('seriesFocus', ({ index }) => {
  readout.textContent = index === null ? 'all series shown' : `isolated: series ${index}`;
});

// Or drive it from code — same effect, same event, as clicking the legend:
clearButton.addEventListener('click', () => chart.focusSeries(null));

© 2026 · License · Line chart →