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