SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
2.5 KB · 55 lines tsx
Raw Blame History
1import { fmtValue, unitLabel } from '@/lib/format';23export interface CompareBarDatum {4  label: string;5  value: number | null; // null = no figure (rendered as "—", never as zero)6  note?: string | null; // e.g. "2024 · observed" or "registry level: Lung cancer"7  href?: string;8  muted?: boolean;9}1011/**12 * Compact horizontal bar list for the compare page (one small chart per metric). Bars are plain13 * divs so the block stays responsive on narrow screens; the number always accompanies the bar and14 * missing values are shown as a dash, never as an empty bar of length zero.15 */16export function CompareBars({ title, unit, data, caption }: { title: string; unit?: string | null; data: CompareBarDatum[]; caption?: React.ReactNode }) {17  const values = data.map((d) => d.value).filter((v): v is number => v != null && Number.isFinite(v));18  const max = Math.max(...values, Number.EPSILON);19  return (20    <figure className="min-w-0 border-t border-rule-strong pt-2">21      <figcaption className="mb-1.5 flex items-baseline justify-between gap-2 text-[12.5px]">22        <span className="font-medium text-ink">{title}</span>23        {unit ? <span className="text-[11px] text-ink-3">{unitLabel(unit)}</span> : null}24      </figcaption>25      <ol className="space-y-1.5" aria-label={`${title} — bar comparison`}>26        {data.map((d, i) => {27          const has = d.value != null && Number.isFinite(d.value);28          const pct = has ? Math.max(1, (Math.abs(d.value!) / max) * 100) : 0;29          return (30            <li key={`${d.label}-${i}`} className="text-[12.5px]">31              <div className="flex items-baseline justify-between gap-2">32                <span className="min-w-0 truncate text-ink-2">33                  {d.href ? (34                    <a className="ci-link" href={d.href}>35                      {d.label}36                    </a>37                  ) : (38                    d.label39                  )}40                </span>41                <span className="ci-num shrink-0 text-ink">{has ? fmtValue(d.value, unit) : '—'}</span>42              </div>43              <div className="mt-0.5 h-[7px] w-full bg-paper-3" role="presentation">44                {has ? <div className="h-full" style={{ width: `${pct}%`, background: d.muted ? 'var(--color-ink-4)' : 'var(--color-accent)' }} /> : null}45              </div>46              {d.note ? <div className="mt-0.5 text-[11px] text-ink-3">{d.note}</div> : null}47            </li>48          );49        })}50      </ol>51      {caption ? <p className="mt-1.5 text-[11.5px] text-ink-3">{caption}</p> : null}52    </figure>53  );54}55