spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import { fmtValue } from '@/lib/format';23export interface BarDatum {4 label: string;5 value: number;6 href?: string;7 muted?: boolean;8}910/**11 * Horizontal bar chart in pure SVG (no chart library). Single hue; labels carry the meaning.12 * Values are formatted with the metric unit; bars never start anywhere but zero.13 */14export function BarChart({ data, unit, maxBars = 15, ariaLabel }: { data: BarDatum[]; unit?: string | null; maxBars?: number; ariaLabel: string }) {15 const rows = data.slice(0, maxBars);16 if (rows.length === 0) return null;17 const max = Math.max(...rows.map((r) => Math.abs(r.value)), Number.EPSILON);18 const rowH = 22;19 const labelW = 190;20 const valueW = 74;21 const width = 640;22 const barW = width - labelW - valueW - 8;23 const height = rows.length * rowH + 4;24 return (25 <figure className="w-full overflow-x-auto">26 <svg viewBox={`0 0 ${width} ${height}`} width="100%" role="img" aria-label={ariaLabel} className="block min-w-[480px] text-ink" style={{ maxHeight: height }}>27 <title>{ariaLabel}</title>28 {rows.map((r, i) => {29 const y = i * rowH + 2;30 const w = Math.max(1, (Math.abs(r.value) / max) * barW);31 const fill = r.muted ? 'var(--color-ink-4)' : 'var(--color-accent)';32 return (33 <g key={`${r.label}-${i}`}>34 <text x={labelW - 8} y={y + rowH / 2 + 4} textAnchor="end" fontSize="12" fill="var(--color-ink-2)">35 {r.label.length > 30 ? `${r.label.slice(0, 29)}…` : r.label}36 </text>37 <rect x={labelW} y={y + 4} width={w} height={rowH - 8} fill={fill} />38 <text x={labelW + w + 6} y={y + rowH / 2 + 4} fontSize="12" fill="var(--color-ink)" style={{ fontVariantNumeric: 'tabular-nums' }}>39 {fmtValue(r.value, unit)}40 </text>41 </g>42 );43 })}44 </svg>45 </figure>46 );47}48