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.

Histogram with bins 0, 1, 2, 3, 4, 5 and values 2, 1, 5, 6, 2, 3.201152632435
Customize
Layout
36
140
8
Highlight
1

Installation

npx shadcn@latest add https://craftbits.dev/r/histogram-builder.json

Usage

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

  1. Self-contained SVG. The component renders one <svg> sized to fit every bar plus the value / axis labels. The caller positions it; no top / left / transform: translate is set internally.
  2. Click-to-edit cycles. Tapping a bar adds step to its value and wraps modulo maxValue + 1, so values cycle through every legal height without negative numbers or runaway growth. Pass a negative step for a "decrement on click" feel.
  3. Controlled + uncontrolled. values + onValuesChange is the Radix controlled pattern; defaultValues makes the component own its own state. If neither is provided, the component falls back to bins[i].value as the initial seed.
  4. Highlighted bar. Pass highlightedIndex to 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.
  5. Five tones. default reads as "neutral count"; accent as "currently relevant"; success as "winning value"; warning as "watch this bar"; error as "blown the budget". The tone paints only the highlighted bar.
  6. 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.
  7. 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

PropTypeDefaultDescription
binsHistogramBin[]requiredLabelled bin definitions.
valuesnumber[]Controlled values. Pair with onValuesChange.
defaultValuesnumber[]Uncontrolled initial values.
onValuesChange(next: number[]) => voidFires with the next values array on every click.
maxValuenumber8Maximum bar value. The y-axis scales to this number.
stepnumber1Added to a bar's value on each click. Wraps modulo maxValue + 1.
highlightedIndexnumberMark a single bar with the active tone.
tone"default" | "accent" | "success" | "warning" | "error""accent"Highlight palette.
editablebooleantrueWhen false, bars are not clickable or focusable.
barWidthnumber36Bar width in pixels.
barGapnumber6Gap between bars in pixels.
plotHeightnumber140Plot area height in pixels.
transitionTransitionSPRINGS.smoothOverride bar-value transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the <svg> root via cn().

Accessibility

  • The outer <svg> is role="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" with tabIndex={0}, an explicit aria-label naming 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-editable on the root and data-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.