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%
5.2 KB · 114 lines tsx
Raw Blame History
1import Link from 'next/link';2import { Badge, ClaimBadge } from '@/components/ui/badge';3import { SourceBadge, provenanceTitle, type ProvenanceInfo } from '@/components/ui/source-badge';4import { Pager } from '@/components/ui/pager';5import { EPI_METRIC_LABEL } from '@/lib/queries/epidemiology';6import type { ExplorerObsRow } from '@/lib/queries/explorer';7import { TABLE_PAGE_SIZE } from '@/lib/explorer-params';8import { fmtInt, fmtValue, humanize, unitLabel } from '@/lib/format';910export function obsProvenance(r: ExplorerObsRow): ProvenanceInfo {11  return {12    sourceSlug: r.source_slug,13    sourceName: r.source_name,14    dataset: r.dataset,15    datasetVersion: r.dataset_version,16    retrievedAt: r.retrieved_at,17    sourceUrl: r.source_url,18    license: r.source_license,19    layer: 'normalized',20    evidenceType: 'observed_data',21  };22}2324/**25 * Every observation behind the charts, 50 per page, with the unit, CI, estimate type, standard26 * population, site definition and a per-row provenance popover (datasets differ between rows).27 */28export function ObservationsTable({ rows, page, hrefFor }: { rows: ExplorerObsRow[]; page: number; hrefFor: (page: number) => string }) {29  const total = rows.length;30  const start = (page - 1) * TABLE_PAGE_SIZE;31  const shown = rows.slice(start, start + TABLE_PAGE_SIZE);32  const units = [...new Set(rows.map((r) => r.unit))];33  const observedOnly = rows.every((r) => r.estimate_type === 'observed');34  // One provenance popover per distinct (source, dataset) in the caption; rows carry compact badges whose35  // title repeats dataset · version · retrieved (dense tables: the popover markup is not repeated 50 times).36  const captionProv = new Map<string, ProvenanceInfo>();37  for (const r of rows) {38    const k = `${r.source_slug}|${r.dataset ?? ''}`;39    if (!captionProv.has(k)) captionProv.set(k, obsProvenance(r));40  }41  return (42    <div>43      {/* div, not p: popovers contain a <dl>. */}44      <div className="mb-2 flex flex-wrap items-center gap-2 text-[12px] text-ink-3">45        {[...captionProv.values()].map((p) => (46          <SourceBadge key={`${p.sourceSlug}|${p.dataset ?? ''}`} p={p} />47        ))}48        <ClaimBadge kind="observed" />49        <span>50          {fmtInt(total)} observation{total === 1 ? '' : 's'} · values exactly as published by the source, units harmonized ({units.map(unitLabel).join(', ')}) · hover a row's source badge for its dataset, version and retrieval date51          {observedOnly ? '' : ' · rows labelled estimated/projected are model outputs of the source'}52        </span>53      </div>54      <div className="ci-table-wrap">55        <table className="ci-table">56          <thead>57            <tr>58              <th scope="col">Year</th>59              <th scope="col">Cancer</th>60              <th scope="col">Geography</th>61              <th scope="col">Sex</th>62              <th scope="col">Age</th>63              <th scope="col">Metric</th>64              <th scope="col" className="num">65                Value66              </th>67              <th scope="col">Unit</th>68              <th scope="col" className="num">69                95% CI70              </th>71              <th scope="col">Type</th>72              <th scope="col">Standard population</th>73              <th scope="col">Site definition</th>74              <th scope="col">Source</th>75            </tr>76          </thead>77          <tbody>78            {shown.map((r) => (79              <tr key={r.id}>80                <td className="ci-num">{r.year_end && r.year_end !== r.year ? `${r.year}–${r.year_end}` : r.year}</td>81                <td>82                  <Link className="ci-link" href={`/cancer/${r.cancer_slug}/statistics`}>83                    {r.cancer_name}84                  </Link>85                </td>86                <td>87                  <Link className="ci-link" href={`/country/${r.geography_slug}`}>88                    {r.geography_name}89                  </Link>90                </td>91                <td>{r.sex === 'all' ? 'Both' : humanize(r.sex)}</td>92                <td>{r.age_group === 'all' ? 'All ages' : r.age_group}</td>93                <td className="whitespace-nowrap text-[12.5px]">{EPI_METRIC_LABEL[r.metric] ?? humanize(r.metric)}</td>94                <td className="num font-medium">{fmtValue(r.value, r.unit)}</td>95                <td className="text-[12px] text-ink-3">{unitLabel(r.unit)}</td>96                <td className="num text-ink-3">{r.lower_ci != null && r.upper_ci != null ? `${fmtValue(r.lower_ci, r.unit)}–${fmtValue(r.upper_ci, r.unit)}` : '—'}</td>97                <td>98                  <Badge tone={r.estimate_type === 'observed' ? 'ok' : 'warn'}>{r.estimate_type}</Badge>99                </td>100                <td className="max-w-[200px] text-[12px] text-ink-3">{r.standard_population ?? '—'}</td>101                <td className="max-w-[260px] text-[12px] text-ink-3">{r.site_definition ?? '—'}</td>102                <td>103                  <SourceBadge compact p={obsProvenance(r)} title={provenanceTitle(obsProvenance(r))} />104                </td>105              </tr>106            ))}107          </tbody>108        </table>109      </div>110      <Pager total={total} pageSize={TABLE_PAGE_SIZE} page={page} hrefFor={hrefFor} noun="observations" label="Observations pagination" />111    </div>112  );113}114