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
| Property | Type | Default | Description |
data required | DataSet | — |
Points to render. See Data model. |
grainDensity | number | 0.6 |
Grains per unit area of layout space. Higher = denser sand (and more work). |
maxGrains | number | 100000 |
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. |
colors | string[] | DEFAULT_PALETTE |
Series colors (CSS hex / rgb() / rgba()), cycled per series. |
background | string | transparent |
Canvas clear color (CSS). |
grain | object | — |
Per-grain appearance. See below. |
animation | object | — |
Pour-in + morph timing. See below. |
interaction | object | — |
Hover effects. See below. |
axes | { x?, y? } | off |
X and Y axes, each off by default. See below. |
legend | LegendConfig | off |
Positionable series legend. See below. |
currentValue | object | off |
Hover-tied value readout. See below. |
bars | BarStyleConfig | off |
Solid fill + border that resolves as sand fades. See below. |
fps | FpsConfig | off |
On-screen FPS meter. See below. |
panZoom | PanZoomConfig | off |
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
| 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 | Grid jitter fraction — scatters grains off the packing grid so they look natural. |
settleJitter | number | 0.004 | Baseline 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.)
| Property | Type | Default | Description |
duration | number (ms) | 900 | Per-grain travel time from start to settled. |
stagger | number (ms) | 500 | Spread of pour start times across grains, so they don't all land at once. |
ease | 'linear' | 'easeOutCubic' | 'easeOutQuint' | 'easeOutCubic' | Grain travel easing. |
morphDuration | number (ms) | duration + stagger | Transition 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
| Property | Type | Default | Description |
effects | ('highlight' | 'jitter' | 'opacity')[] | ['highlight','jitter'] | Which effects apply to the hovered bar's grains. |
highlightGain | number | 1.6 | Color multiplier for hovered grains (highlight effect). |
jitterAmp | number | 0.008 | Extra motion amplitude for hovered grains (jitter effect), layout units. |
opacity | number (0..1) | 1 | Target opacity for hovered grains (opacity effect) — lets them pop even after the reveal has faded the sand. |
fadeMs | number (ms) | 180 | Enter/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.
| Property | Type | Default | Description |
opacity | number (0..1) | 0.15 | Opacity of every dimmed (non-isolated) series — grains, and any solid fill/border/line/marker layer. |
fadeMs | number (ms) | 200 | Ease 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.y — AxisConfig
Each axis is off until you set show: true. Both axes share the same shape.
| Property | Type | Default | Description |
show | boolean | false | Draw this axis. |
ticks | number | false | auto | Approx tick count, or false for an axis line with no ticks. |
tickFormat | (v: Scalar) => string | compact # / locale date | Format a tick value into its label. |
label | string | — | Axis title, drawn beside the ticks. |
gridLines | boolean | false | Extend ticks across the plot as grid lines. |
color | string | subdued gray | Line / label color (CSS). |
fontPx | number | 11 | Tick label font size in px. |
fontFamily | string | 'system-ui, sans-serif' | Tick / title font-family stack (CSS). |
fontWeight | string | number | 'normal' | Tick / title font weight (CSS). |
titleDirection | 'up' | 'down' | 'up' | Reading direction of the rotated Y-axis title (ignored on X). |
legend — LegendConfig
| Property | Type | Default | Description |
show | boolean | false | Show 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 shape | Swatch shape. |
interactive | boolean | false | Click 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
| Property | Type | Default | Description |
show | boolean | false | Show 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) => string | — | Format 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. |
markers | boolean | true | When a guide line is on, highlight the matching axis with a value marker (filled tick label at the cursor's row/column). |
showGuide | boolean | true | legacy Horizontal guide to the value axis. Fallback for guide when it is omitted (true → 'y', false → 'none'). |
color | string | — | Text / 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
| Property | Type | Default | Description |
opacity | number (0..1) | 1 | Fill opacity. |
bars.border
| Property | Type | Default | Description |
left / top / right / bottom | boolean | all four* | Which edges to stroke. *When a border block is present but no side flags are set, all four are drawn. |
width | number | 1 | Stroke width in CSS px. |
opacity | number (0..1) | 1 | Stroke opacity. |
bars.reveal — particle→solid crossfade
| Property | Type | Default | Description |
start | 'afterPour' | number | 'afterPour' | When the fade begins. afterPour = once pour + settle finishes (duration + stagger). A number = absolute seconds from start. |
duration | number (ms) | 500 | Fade window length. |
ease | Easing | 'easeOutCubic' | Fade easing. |
grainsTo | number (0..1) | 0 | Grain end-opacity after the fade (0 = grains disappear, leaving only the solid bar). |
fps — FpsConfig
| Property | Type | Default | Description |
position | Side | 'off' | 'off' | Edge/corner to pin the meter to. left/right sit in the top corner; top/bottom are centered on that edge. |
color | string | subdued light gray | Text color (CSS). |
panZoom — PanZoomConfig
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.
| 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, position, step } | shown | On-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
| Method | Description |
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(): DataSet | Deep-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));