import { useState } from 'react' import { Tooltip } from './ChartFrame' import { formatNumber, niceTicks } from './scale' import { useWidth } from './useWidth' export interface Series { key: string label: string color: string } interface Props { data: { label: string; values: Record }[] series: Series[] height?: number ariaLabel: string } const PAD = { top: 8, right: 8, bottom: 26, left: 32 } const GAP = 2 // surface gap between stacked segments const RADIUS = 4 /** Rounded top on the data end, square at the baseline. */ function columnPath(x: number, y: number, w: number, h: number, roundTop: boolean) { const r = roundTop ? Math.min(RADIUS, h, w / 2) : 0 return `M${x},${y + h}V${y + r}${r ? `Q${x},${y} ${x + r},${y}` : ''}H${x + w - r}${r ? `Q${x + w},${y} ${x + w},${y + r}` : ''}V${y + h}Z` } export function StackedColumns({ data, series, height = 220, ariaLabel }: Props) { const [ref, width] = useWidth() const [hover, setHover] = useState(null) const plotW = width - PAD.left - PAD.right const plotH = height - PAD.top - PAD.bottom const totals = data.map((d) => series.reduce((sum, s) => sum + (d.values[s.key] ?? 0), 0)) const ticks = niceTicks(Math.max(...totals, 1)) const max = ticks.at(-1)! const band = plotW / Math.max(1, data.length) const barW = Math.min(24, band * 0.6) const y = (v: number) => PAD.top + plotH - (v / max) * plotH const labelEvery = Math.ceil(data.length / Math.max(1, Math.floor(plotW / 56))) return (
setHover(null)}> {ticks.map((t) => ( {formatNumber(t)} ))} {data.map((d, i) => { const x = PAD.left + i * band + (band - barW) / 2 let base = 0 const visible = series.filter((s) => (d.values[s.key] ?? 0) > 0) return ( {visible.map((s, j) => { const v = d.values[s.key] const top = y(base + v) const bottom = y(base) base += v const isTop = j === visible.length - 1 const h = Math.max(0, bottom - top - (j > 0 ? GAP : 0)) return })} {(i % labelEvery === 0 || i === data.length - 1) && ( {d.label} )} `${d.values[s.key] ?? 0} ${s.label}`).join(', ')}`} onPointerEnter={() => setHover(i)} onFocus={() => setHover(i)} onBlur={() => setHover(null)} className="outline-none" /> ) })} {hover !== null && ( ({ label: s.label, value: formatNumber(data[hover].values[s.key] ?? 0), color: s.color }))} /> )}
) }