Histogram Builder
A self-contained <svg> histogram. Pass a bins array of { label, value } pairs and the component lays out one bar per bin, baseline-aligned, with the value label hovering above each bar and the category label below the baseline. Click any bar to cycle its value through 0..maxValue — pair with values + onValuesChange for controlled mode, defaultValues for uncontrolled.
Generic enough to cover three usages from one primitive: a static read-only chart (editable={false}), an edit-the-distribution interaction (the default), or a "largest rectangle in histogram" discovery game where the parent computes the leader index and passes it as highlightedIndex to light up the current best bar.
Installation
npx shadcn@latest add https://craftbits.dev/r/histogram-builder.jsonUsage
import { HistogramBuilder } from "@craft-bits/core";
<HistogramBuilder
bins={[
{ label: "0", value: 2 },
{ label: "1", value: 1 },
{ label: "2", value: 5 },
{ label: "3", value: 6 },
]}
/>Controlled — parent owns the values, click to edit:
const [values, setValues] = useState([2, 1, 5, 6]);
<HistogramBuilder
bins={bins}
values={values}
onValuesChange={setValues}
/>Read-only static chart (no click handlers, no focusable bars):
<HistogramBuilder bins={bins} editable={false} />Highlight the current leader in the accent tone:
<HistogramBuilder
bins={bins}
values={values}
onValuesChange={setValues}
highlightedIndex={leaderIdx}
tone="accent"
/>Understanding the component
- Self-contained SVG. The component renders one
<svg>sized to fit every bar plus the value / axis labels. The caller positions it; notop/left/transform: translateis set internally. - Click-to-edit cycles. Tapping a bar adds
stepto its value and wraps modulomaxValue + 1, so values cycle through every legal height without negative numbers or runaway growth. Pass a negativestepfor a "decrement on click" feel. - Controlled + uncontrolled.
values+onValuesChangeis the Radix controlled pattern;defaultValuesmakes the component own its own state. If neither is provided, the component falls back tobins[i].valueas the initial seed. - Highlighted bar. Pass
highlightedIndexto mark a single bar with the active tone. Every other bar drops to a neutral chip so the chart reads calm even with eight bins. - Five tones.
defaultreads as "neutral count";accentas "currently relevant";successas "winning value";warningas "watch this bar";erroras "blown the budget". The tone paints only the highlighted bar. - Hit target. Each editable bar carries an invisible 44px-wide hit rectangle behind it so narrow bars still satisfy WCAG 2.5.8 — a 12px bar on a 6px gap is still safely tappable on mobile.
- Reduced motion. Bar enter animation, value-change transition, and the highlight stroke all collapse to instant under
prefers-reduced-motion: reduce. The values still update; only the motion drops.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
bins | HistogramBin[] | required | Labelled bin definitions. |
values | number[] | — | Controlled values. Pair with onValuesChange. |
defaultValues | number[] | — | Uncontrolled initial values. |
onValuesChange | (next: number[]) => void | — | Fires with the next values array on every click. |
maxValue | number | 8 | Maximum bar value. The y-axis scales to this number. |
step | number | 1 | Added to a bar's value on each click. Wraps modulo maxValue + 1. |
highlightedIndex | number | — | Mark a single bar with the active tone. |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Highlight palette. |
editable | boolean | true | When false, bars are not clickable or focusable. |
barWidth | number | 36 | Bar width in pixels. |
barGap | number | 6 | Gap between bars in pixels. |
plotHeight | number | 140 | Plot area height in pixels. |
transition | Transition | SPRINGS.smooth | Override bar-value transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the <svg> root via cn(). |
Accessibility
- The outer
<svg>isrole="img"with a<title>summarising the bin labels and current values. Screen readers hear the chart without parsing the SVG geometry. - Every editable bar is
role="button"withtabIndex={0}, an explicitaria-labelnaming the bin and its current value, and Space / Enter keyboard activation that mirrors the click-to-cycle behaviour. - Each bar carries an invisible 44px-wide hit rectangle so narrow bars still satisfy WCAG 2.5.8 AAA on touch screens.
- The component exposes
data-editableon the root anddata-state(highlighted/rest) on every bar group so consumer apps can hook custom styles or assistive tooling. - Tone is never the only signal — the value label renders on every bar regardless of tone, so colourblind users see the magnitude even when the highlight colour is hard to discriminate.
- Motion respects
prefers-reduced-motion: reduce— the enter animation and value-change spring collapse to instant.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/construction/HistogramBuilder.tsx). The source was a 2700-line lesson component bundling a four-phase reducer, audio cues, prediction gates, and width-formula construction UI. The library extract keeps only the visualisation primitive — a labelled bin array, click-to-edit, controlled / uncontrolled, optional highlight index — and lets the caller compose any reducer-driven scoring on top.