spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Badge } from '@/components/ui/badge';3import { EPI_METRIC_LABEL } from '@/lib/queries/epidemiology';4import type { CoverageMatrixRow } from '@/lib/queries/explorer';5import { fmtDate, fmtInt, humanize, unitLabel } from '@/lib/format';67/**8 * Coverage matrix: what exists, per metric × geography × sex × age group × source × standard population,9 * with the year span, the number of distinct years, observations and cancers. Each row links to the10 * explorer with the same dimensions so a gap can be checked in one click.11 */12export function CoverageTable({ rows, cancers, compact = false }: { rows: CoverageMatrixRow[]; cancers?: string[]; compact?: boolean }) {13 if (rows.length === 0) return null;14 const hrefFor = (r: CoverageMatrixRow) => {15 const qs = new URLSearchParams({ metric: r.metric, geography: r.geography_slug, sex: r.sex, age: r.age_group, from: String(r.year_from), to: String(r.year_to) });16 if (cancers && cancers.length > 0) qs.set('cancers', cancers.join(','));17 return `/explore?${qs.toString()}`;18 };19 return (20 <div className="ci-table-wrap">21 <table className="ci-table">22 <thead>23 <tr>24 <th scope="col">Metric</th>25 <th scope="col">Geography</th>26 <th scope="col">Sex</th>27 <th scope="col">Age</th>28 <th scope="col">Source</th>29 <th scope="col">Standard population</th>30 <th scope="col">Years</th>31 <th scope="col" className="num">32 Distinct years33 </th>34 <th scope="col" className="num">35 Observations36 </th>37 {compact ? null : (38 <th scope="col" className="num">39 Cancers40 </th>41 )}42 <th scope="col">Type</th>43 {compact ? null : <th scope="col">Updated</th>}44 <th scope="col">45 <span className="sr-only">Open</span>46 </th>47 </tr>48 </thead>49 <tbody>50 {rows.map((r, i) => (51 <tr key={`${r.metric}|${r.geography_id}|${r.sex}|${r.age_group}|${r.source_slug}|${r.standard_population ?? ''}|${i}`}>52 <td className="whitespace-nowrap">53 {EPI_METRIC_LABEL[r.metric] ?? humanize(r.metric)} <span className="text-[11.5px] text-ink-3">({unitLabel(r.unit)})</span>54 </td>55 <td>56 <Link className="ci-link" href={`/country/${r.geography_slug}`}>57 {r.geography_name}58 </Link>59 {r.iso3 ? <span className="ci-mono ml-1 text-[11px] text-ink-3">{r.iso3}</span> : null}60 </td>61 <td>{r.sex === 'all' ? 'Both' : humanize(r.sex)}</td>62 <td>{r.age_group === 'all' ? 'All ages' : r.age_group}</td>63 <td>64 <Link className="ci-src" href={`/source/${r.source_slug}`} title={r.source_name}>65 {r.source_slug}66 </Link>67 </td>68 <td className="max-w-[220px] text-[12px] text-ink-3">{r.standard_population ?? '—'}</td>69 <td className="ci-num whitespace-nowrap">{r.year_from === r.year_to ? r.year_from : `${r.year_from}–${r.year_to}`}</td>70 <td className="num">{fmtInt(r.years)}</td>71 <td className="num">{fmtInt(r.observations)}</td>72 {compact ? null : <td className="num">{fmtInt(r.n_cancers)}</td>}73 <td>74 {r.estimate_types.map((t) => (75 <Badge key={t} tone={t === 'observed' ? 'ok' : 'warn'} className="mr-1">76 {t}77 </Badge>78 ))}79 </td>80 {compact ? null : <td className="whitespace-nowrap text-[12px] text-ink-3">{fmtDate(r.last_updated)}</td>}81 <td>82 <Link className="ci-link whitespace-nowrap text-[12px]" href={hrefFor(r)}>83 Open →84 </Link>85 </td>86 </tr>87 ))}88 </tbody>89 </table>90 </div>91 );92}93