/** * Local chart variants for the stats/debris/reentries pages (pure SVG, server-safe). * Same visual system as `@/components/charts/charts` — hairline axes, mono tick labels, token colours. */ import { max as d3max } from 'd3-array'; import { scaleLinear } from 'd3-scale'; import { cn } from '@/lib/cn'; import { fmtCompact } from '@/lib/format'; export interface MultiBin { bin: number; values: number[]; } /** * Grouped histogram: several series drawn side-by-side inside each bin (e.g. active payloads vs debris per 25 km). * `series` = labels + colours in the same order as `values`. */ export function GroupedHistogram({ data, series, className, height = 170, title, unit = '', xTicks = 8 }: { data: MultiBin[]; series: { label: string; color: string }[]; className?: string; height?: number; title: string; unit?: string; xTicks?: number }) { if (!data.length || !series.length) { return (
Unavailable
); } const W = 640; const H = height; const pad = { l: 36, r: 8, t: 6, b: 22 }; const bins = data.map((d) => d.bin); const step = bins.length > 1 ? Math.min(...bins.slice(1).map((b, i) => b - (bins[i] ?? 0)).filter((v) => v > 0)) || 1 : 1; const x = scaleLinear().domain([Math.min(...bins), Math.max(...bins) + step]).range([pad.l, W - pad.r]); const ymaxRaw = d3max(data, (d) => Math.max(...d.values)) || 1; const y = scaleLinear().domain([0, ymaxRaw]).nice().range([H - pad.b, pad.t]); const slot = Math.max(1, x(step) - x(0) - 1); const bw = Math.max(0.8, slot / series.length); return (
{title} {y.ticks(3).map((t) => ( {fmtCompact(t)} ))} {data.map((d) => d.values.map((v, si) => ( )), )} {x.ticks(xTicks).map((t) => ( {fmtCompact(t)}{unit} ))}
); } /** Legend-only helper for charts that need an external key (e.g. two donuts sharing colours). */ export function Legend({ items, className }: { items: { label: string; color: string }[]; className?: string }) { return ( ); }