Histogram A distribution, not a time series with the axis relabelled. The bucket width is the whole argument: too wide and two humps merge, too narrow and noise looks like structure. npx shadcn@latest add toggle-group npm i recharts Tokens this needs: --card, --muted, --muted-foreground, --border, --chart-1, --status-warn The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // Histogram.tsx ──────────────────────────────────────────────────────────────────────── import { Bar, BarChart, ReferenceLine, XAxis, YAxis } from "recharts"; /** * The bucket count is the whole argument, so it is a required prop rather * than a default someone forgot. Too few and two humps merge; too many and * the shape dissolves into noise. Neither failure announces itself. */ export type Bin = { lo: number; hi: number; count: number }; export function bin(samples: number[], buckets: number, min: number, max: number): Bin[] { const width = (max - min) / buckets; const bins: Bin[] = Array.from({ length: buckets }, (_, i) => ({ lo: min + i * width, hi: min + (i + 1) * width, count: 0 })); for (const v of samples) { // The top edge is inclusive, so a sample at exactly `max` lands in the // last bin instead of falling off the chart. const i = Math.min(buckets - 1, Math.floor((v - min) / width)); if (i >= 0) bins[i].count++; } return bins; } /** * A spike against the right edge is a timeout truncating the tail: * everything past it was recorded as exactly `max`. Flagged when the last * bin holds several times what the bins before it do. */ export function wall(bins: Bin[]): Bin | null { const last = bins[bins.length - 1]; const before = bins.slice(-6, -1); const mean = before.reduce((n, b) => n + b.count, 0) / before.length; return last.count > 0 && last.count > 4 * mean ? last : null; } const fmt = (v: number) => (v === 0 ? "0" : v % 1000 === 0 ? `${v / 1000}s` : `${v}ms`); export function Histogram({ samples, buckets, min = 0, max, width = 300, height = 150 }: { /** Raw values, in ms. The component bins them, so changing `buckets` rebins the same population. */ samples: number[]; buckets: number; min?: number; /** The top of the range. Required, because a histogram cut at the data's max hides the wall. */ max: number; width?: number; height?: number; }) { const bins = bin(samples, buckets, min, max); const cut = wall(bins); const step = (max - min) / buckets; const data = bins.map((b) => ({ mid: b.lo + step / 2, count: b.count })); return (
{buckets} buckets of {fmt(Math.round(step))} · {samples.length.toLocaleString("en-GB")} requests {cut && · {cut.count} recorded as exactly {fmt(max)}, which is a timeout, not a mode}