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.8 KB · 116 lines tsx
Raw Blame History
1import Link from 'next/link';2import { Section, Note } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { ClaimBadge, ConfidenceBadge } from '@/components/ui/badge';5import { JsonView } from '@/components/ui/json-view';6import { rankingsForCancer } from '@/lib/queries/rankings';7import { fmtDate, fmtInt, fmtValue, scopeLabel, unitLabel } from '@/lib/format';8import { latestYearPerScope } from '@/lib/rankings-util';9import { CancerGapCard } from '@/components/research-gap/cancer-gap-card';10import type { CancerBundle } from '../load';1112function Delta({ rank, prev }: { rank: number; prev: number | null }) {13  if (prev == null) return <span className="text-ink-4" title="No previous snapshot for this scope">new</span>;14  const d = prev - rank;15  if (d === 0) return <span className="text-ink-3">=</span>;16  return (17    <span className={d > 0 ? 'text-ok' : 'text-danger'} title={`Previous rank ${prev}`}>18      {d > 0 ? '▲' : '▼'} {Math.abs(d)}19    </span>20  );21}2223export async function RankingsTab({ b }: { b: CancerBundle }) {24  const allRows = await rankingsForCancer(b.cancer.id);25  // One row per (metric, geography, sex, age, level): the latest year only. Historical years stay on the ranking pages.26  const { rows, hidden: hiddenYears } = latestYearPerScope(allRows);27  if (rows.length === 0) {28    return (29      <Section id="rankings" kicker="Rankings" title="Why this rank?">30        <EmptyState title="Not ranked yet" knows={[{ label: 'All rankings', href: '/rankings' }, { label: 'Methodology', href: '/methodology' }]}>31          This entity appears in no current ranking snapshot. Rankings are computed per metric and scope (geography, sex, age, year, entity level) once the underlying counters or licensed observations exist; an entity with a zero counter is ranked only when the metric admits zeros.32        </EmptyState>33      </Section>34    );35  }36  return (37    <div className="space-y-6">38      <Section id="rankings" kicker="Rankings" title="Why this rank?" description={`${fmtInt(rows.length)} current ranking rows (latest year per scope${hiddenYears > 0 ? `; ${fmtInt(hiddenYears)} earlier yearly snapshots are on the ranking pages` : ''}). Each row lists the scope, the formula version, the value and the exact inputs that produced it.`}>39        <div className="ci-table-wrap">40          <table className="ci-table">41            <thead>42              <tr>43                <th scope="col">Metric</th>44                <th scope="col">Scope</th>45                <th scope="col" className="num">46                  Rank / eligible47                </th>48                <th scope="col" className="num">49                  <abbr title="Change versus the previous snapshot">Δ</abbr>50                </th>51                <th scope="col" className="num">52                  Value53                </th>54                <th scope="col" className="num">55                  Percentile56                </th>57                <th scope="col">Confidence</th>58                <th scope="col">Formula</th>59                <th scope="col">Generated</th>60                <th scope="col">Inputs</th>61              </tr>62            </thead>63            <tbody>64              {rows.map((r) => (65                <tr key={r.id}>66                  <td>67                    <Link className="ci-link" href={`/rankings/${r.metric_slug}?scope=${encodeURIComponent(r.scope_key)}`}>68                      {r.metric_name}69                    </Link>70                  </td>71                  <td className="text-[12.5px] text-ink-2">{scopeLabel(r.scope_key)}</td>72                  <td className="num">73                    <span className="font-medium">#{r.rank}</span> <span className="text-ink-3">/ {fmtInt(r.eligible_entities)}</span>74                  </td>75                  <td className="num">76                    <Delta rank={r.rank} prev={r.previous_rank} />77                  </td>78                  <td className="num">79                    {fmtValue(r.value, r.unit)} <span className="text-[11px] text-ink-3">{unitLabel(r.unit)}</span>80                  </td>81                  <td className="num">{r.percentile}</td>82                  <td>83                    <ConfidenceBadge level={r.confidence} />84                  </td>85                  <td>86                    <span className="ci-mono text-[11.5px]">{r.formula_version}</span>87                  </td>88                  <td className="whitespace-nowrap text-[12.5px]">{fmtDate(r.generated_at)}</td>89                  <td>90                    <details>91                      <summary className="ci-link text-[12.5px]">show</summary>92                      <div className="mt-1 max-w-[420px]">93                        <p className="ci-mono mb-1 text-[11px] text-ink-3">formula: {r.formula}</p>94                        <JsonView data={r.inputs} />95                        <p className="ci-mono mt-1 text-[11px] text-ink-3">inputs hash {r.inputs_hash}</p>96                        <Link href={`/admin/trace?table=rankings&id=${r.id}`} className="ci-link text-[11.5px]">97                          Trace lineage (admin)98                        </Link>99                      </div>100                    </details>101                  </td>102                </tr>103              ))}104            </tbody>105          </table>106        </div>107        <p className="mt-2 flex items-center gap-2 text-[12px] text-ink-3">108          <ClaimBadge kind="computed" /> Ranks are competition ranks (ties share a rank). Percentile = share of eligible entities ranked at or below this one in the ranking direction.109        </p>110      </Section>111      <Note>"Deadliest" is ambiguous: annual deaths, age-standardized mortality rate, mortality-to-incidence ratio and 5-year survival rank cancers differently. CancerIndex publishes each as a separate metric.</Note>112      <CancerGapCard cancerId={b.cancer.id} />113    </div>114  );115}116