spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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