ShiftHistogram
ShiftHistogram mirrors before and after distributions on shared bins and states the median shift between them.
- encodes
- mirrored bin heights around a center + median shift
- precision
- medium — shape read; the shift label is the takeaway
- nodes
- 2 per bin + 4
- collection
- Decision
ShiftHistogram mirrors two histograms around a center line: before upward (muted), after downward (accent), on shared bin edges. Bar heights are per-side proportions (each side's counts over its own n) on one shared scale, so a bigger sample on one side cannot fake a shift. The mirror carries identity, not valence — up is "before", not "good". The median shift is the precise takeaway, and the medians are never smoothed or trimmed.
Install
import { ShiftHistogram } from "@microcharts/react/shift-histogram";// before, after — real values under “Sample data” below<ShiftHistogram data={{ before, after }} title="The fix" />Sample data
// latency (ms) before/after a fix — the whole distribution moved leftconst before = Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20);const after = Array.from({ length: 100 }, (_, i) => 96 + (i % 40) - 20);Try it
- hover · rove · click…
Hover or arrow across the bins — each announces the before/after proportions; M jumps to the median bins.
import { ShiftHistogram } from "@microcharts/react/shift-histogram/interactive";// before, after — real values under “Sample data” below<ShiftHistogram data={{ before, after }}/>Sample data
// latency (ms) before/after a fix — the whole distribution moved leftconst before = Array.from({ length: 100 }, (_, i) => 120 + (i % 40) - 20);const after = Array.from({ length: 100 }, (_, i) => 96 + (i % 40) - 20);When to use it
Use it for a "the fix, proven" read in a KPI card, before/after distributions in an experiments table, and showing that a change is more than a mean move. For a single distribution use HistogramStrip; for two labelled arms use ABStrips.
Sizing
Variants
The median shift label and accessible summary follow format/locale. Under de-DE the before median above reads
"12.400" rather than the English "12,400". mode="overlay" draws the after bins above the axis as an outline over the
before fill instead of mirroring them below it; the bins stay shared either way.
Edge cases
With one side empty the chart still renders the populated side's bins alone, and the summary names the gap: "Median 116 ms; no after sample." It never reports a fabricated shift. With both sides empty nothing is plottable and the accessible name says "No data."
Four homes
The fix, proven — latency distribution shifted left after deploy.
| checkout | −20ms | |
| auth | −8ms | |
| search | −12ms |
Preview uses the public chart API only. When an interactive twin exists, it swaps in with the same props and no entrance motion - code samples stay on the static import. Placement recipes: Composition, Sizing.
Accessibility
Side identity rides on position (up is before, down is after), so it survives grayscale and forced-colors. The accessible name states the median shift: "Median fell from 116 ms to 92 ms." It appends "On N / M samples" when the sample sizes differ. The interactive entry steps the bins and announces each bin's before/after proportions; M jumps to the two median bins.
The interactive entry follows the shared interaction contract:
arrow keys rove between units on both axes, Home and End jump to the ends, and a click, tap, Enter or
Space selects a unit — pinning its readout so it survives blur, until you select it again, press Escape, or
press outside the chart. On touch, a tap pins and a drag scrubs.
Props
| Prop | Type | Notes |
|---|---|---|
| data* | { before: number[]; after: number[] } | The two samples — raw observations, shared bin edges are derived. |
| bins | number | Shared bin count (default auto, Sturges capped at 12). |
| mode | "mirror" | "overlay" | Mirror (default) or after-as-outline over before fill. |
| seriesLabels | [string, string] | Side identities for the summary (default ['before', 'after']). |
| label | "shift" | "none" | Signed median shift in a right gutter. |
| animate | boolean | interactiveOpt-in entrance motion when the chart mounts client-side — add import "@microcharts/react/motion" once. Inert on the server, on hydrated server HTML, and under prefers-reduced-motion. |
Plus the shared grammar — data, domain, color, title, summary, format — and the layout props (width, height, className, style) that every chart accepts. Interactive entries also share animate and live, and — wherever a chart has more than one navigable unit — onActive, onSelect, selectedIndex and defaultSelectedIndex; and — wherever the chart shows a hover value — readout. See the shared grammar.