Small multiples One chart repeated across a dimension, which only works if every panel shares a y-scale. Free scales make nine panels look alike when one of them is ten times the others. Tokens this needs: --card, --muted-foreground, --border, --ring, --chart-1 ──────────────────────────────────────────────────────────────────────── // SmallMultiples.tsx ──────────────────────────────────────────────────────────────────────── export type Series = { key: string; label: string; values: number[] }; /** Alphabetical is a default, not a decision, so it is not on the list. * `given` means the caller has already ordered them by something. */ export type Order = "magnitude" | "deviation" | "given"; type Props = { series: Series[]; /** The unit, printed once with the shared range. */ unit: string; order: Order; /** See one larger without leaving the grid. */ onSelect?: (key: string) => void; columns?: number; }; const W = 132, H = 40, PAD = 4; const mean = (v: number[]) => v.reduce((a, b) => a + b, 0) / v.length; const sort = (series: Series[], order: Order) => { if (order === "given") return series; const score = order === "magnitude" ? (s: Series) => Math.max(...s.values) : (s: Series) => Math.max(...s.values.map((v) => Math.abs(v - mean(s.values)))); return [...series].sort((a, b) => score(b) - score(a)); }; /** * One chart, repeated. Every panel is drawn against the same domain, computed * here from all of them, so a panel cannot autoscale itself into looking * eventful. The stroke is the same token in every cell for the same reason. */ export function SmallMultiples({ series, unit, order, onSelect, columns = 4 }: Props) { const max = Math.max(1, ...series.flatMap((s) => s.values)); const x = (i: number, n: number) => PAD + (i / (n - 1)) * (W - PAD * 2); const y = (v: number) => H - PAD - (v / max) * (H - PAD * 2); return (

0–{max} {unit}, one scale for every panel

{sort(series, order).map((s) => { const d = s.values.map((v, i) => `${i ? "L" : "M"}${x(i, s.values.length).toFixed(1)} ${y(v).toFixed(1)}`).join(" "); const last = s.values[s.values.length - 1]; return ( ); })}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { SmallMultiples, type Series } from "./SmallMultiples"; /** * Twelve services, 5xx responses per second over the last nine minutes, * already ordered by request volume. Eleven idle between zero and four. * Auth is climbing to twenty-one, and on the shared scale it is the only * panel with a shape. */ const SERVICES: Series[] = [ { key: "checkout", label: "checkout", values: [1, 1, 2, 1, 1, 1, 1, 1, 1] }, { key: "search", label: "search", values: [2, 2, 2, 2, 3, 2, 2, 3, 3] }, { key: "cart", label: "cart", values: [1, 1, 2, 0, 1, 0, 0, 0, 0] }, { key: "user", label: "user", values: [3, 1, 2, 2, 2, 2, 1, 1, 1] }, { key: "feed", label: "feed", values: [1, 0, 0, 0, 0, 0, 1, 1, 0] }, { key: "auth", label: "auth", values: [3, 3, 4, 3, 7, 10, 14, 18, 21] }, { key: "media", label: "media", values: [1, 2, 1, 2, 2, 2, 2, 1, 2] }, { key: "billing", label: "billing", values: [1, 1, 1, 2, 2, 1, 1, 0, 1] }, { key: "email", label: "email", values: [3, 3, 3, 2, 2, 3, 1, 2, 2] }, { key: "push", label: "push", values: [0, 0, 1, 0, 0, 0, 1, 0, 0] }, { key: "jobs", label: "jobs", values: [3, 4, 3, 4, 3, 3, 3, 4, 4] }, { key: "sync", label: "sync", values: [1, 1, 1, 1, 1, 2, 1, 1, 1] }, ]; export default function Demo() { return (
{}} />
); }