import type { Metadata } from 'next'; import Link from 'next/link'; import { PageHeader, Section, Note } from '@/components/ui/section'; import { EmptyState } from '@/components/ui/empty-state'; import { Freshness } from '@/components/ui/freshness'; import { Badge } from '@/components/ui/badge'; import { ExplorerFilters } from '@/components/explorer/filters'; import { ChartGroup, assignSlots, groupProvenance } from '@/components/explorer/chart-group'; import { ObservationsTable } from '@/components/explorer/observations-table'; import { CoverageTable } from '@/components/explorer/coverage-table'; import { explorerOptions, resolveGeographyRef, resolveCancerRefs, topLevelCancerChoices, topCancersByLatest, yearRangeFor, explorerObservations, coverageMatrix, pendingEpidemiologySources, type ExplorerObsRow } from '@/lib/queries/explorer'; import { EPI_METRIC_LABEL } from '@/lib/queries/epidemiology'; import { parseExplorerParams, serializeExplorerParams, apiQueryFor, yearRangeLabel, sexLabel, ageLabel, SEX_ANY, MAX_CANCERS, type ExplorerState } from '@/lib/explorer-params'; import { groupComparable, explainSplit } from '@/lib/explorer-series'; import { SITE_URL, SITE_NAME } from '@/lib/site'; import type { SP } from '@/lib/search-params'; import { fmtDate, fmtInt, humanize, toDate, unitLabel } from '@/lib/format'; export const revalidate = 600; export const metadata: Metadata = { title: 'Data explorer — cancer statistics by metric, cancer, geography, sex and year', description: 'Explore cancer incidence and mortality observations: choose a metric, up to six cancers, a geography, sex, age group and years; chart, table, sources, CSV and API links, permalink.', alternates: { canonical: '/explore' }, }; const DEFAULT_METRIC = 'mortality_count'; const DEFAULT_GEOGRAPHY = 'united-states'; /** * /explore — "Our World in Data"-style explorer over epidemiology_observations. URL = state = permalink. * Comparable observations (same metric, unit, geography, source, standard population, age group) share * one chart; anything else is a separate chart with the reason stated. Every number is shown with its * unit, geography, years, sex, age group, standard population, source and retrieval date. */ export default async function ExplorePage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const [options, pending] = await Promise.all([explorerOptions(), pendingEpidemiologySources()]); const metricFallback = options.metrics.some((m) => m.metric === DEFAULT_METRIC) ? DEFAULT_METRIC : (options.metrics[0]?.metric ?? DEFAULT_METRIC); const geoFallback = options.geographies.some((g) => g.slug === DEFAULT_GEOGRAPHY) ? DEFAULT_GEOGRAPHY : (options.geographies[0]?.slug ?? DEFAULT_GEOGRAPHY); // Pass 1: metric / geography / sex / age from the URL so the computed defaults (top cancers, years) match them. const prelim = parseExplorerParams(sp, { metric: metricFallback, geography: geoFallback, cancers: [] }); const geo = await resolveGeographyRef(prelim.geography); const [top, range] = geo ? await Promise.all([topCancersByLatest(prelim.metric, geo.id, prelim.sex === SEX_ANY ? 'all' : prelim.sex, prelim.age, 5), yearRangeFor(prelim.metric, geo.id)]) : [null, null]; const state = parseExplorerParams(sp, { metric: metricFallback, geography: geoFallback, cancers: top?.cancers.map((c) => c.slug) ?? [], from: range?.min ?? null, to: range?.max ?? null }); const usingDefaultCancers = !sp.cancers || (Array.isArray(sp.cancers) ? sp.cancers.every((s) => !s.trim()) : !sp.cancers.trim()); const [cancers, choices] = await Promise.all([resolveCancerRefs(state.cancers), topLevelCancerChoices(state.metric, geo?.id ?? null)]); const unresolved = state.cancers.filter((c) => !cancers.some((k) => k.slug === c || k.id === c)); const obs = geo && cancers.length > 0 ? await explorerObservations({ metric: state.metric, cancerIds: cancers.map((c) => c.id), geographyId: geo.id, sex: state.sex, age: state.age, from: state.from, to: state.to }) : []; const groups = groupComparable(obs); const slots = assignSlots(groups); const split = explainSplit(groups); const metricLabel = EPI_METRIC_LABEL[state.metric] ?? humanize(state.metric); const metricOpt = options.metrics.find((m) => m.metric === state.metric); const permalink = `${SITE_URL}/explore${serializeExplorerParams(state, { page: 1 })}`; const csvHref = `/api/export/epidemiology.csv${serializeExplorerParams(state, { page: 1 })}`; const apiHref = `/api/v1/epidemiology${apiQueryFor(state)}`; const hrefFor = (page: number) => `/explore${serializeExplorerParams(state, { page })}`; // Sources actually behind the result, with their latest retrieval date. const sourcesUsed = sourcesIn(obs); const freshest = obs.map((o) => toDate(o.updated_at)).filter((d): d is Date => !!d).sort((a, b) => b.getTime() - a.getTime())[0] ?? null; const coverage = obs.length === 0 ? await coverageMatrix({ cancerIds: cancers.map((c) => c.id), geographyId: geo?.id ?? null }) : []; const ingestedNames = options.sources.map((s) => s.name.split(' — ')[0]!); return (

{fmtInt(options.n_obs)} observations · {options.metrics.length} metrics · {options.geographies.length} {options.geographies.length === 1 ? 'geography' : 'geographies'} · {options.year_min}–{options.year_max} · sources: {options.sources.map((s) => s.slug).join(', ')} ·{' '} full coverage matrix {' '} ·{' '} comparability rules

{unresolved.length > 0 ? ( Unknown cancer reference{unresolved.length === 1 ? '' : 's'} ignored: {unresolved.join(', ')}. Use a taxonomy slug (as in /cancer/<slug>) or a CI-CAN id. ) : null}
} description={ <> {sourcesUsed.length > 0 ? ( <> Source{sourcesUsed.length === 1 ? '' : 's'}: {sourcesUsed.map((s) => `${s.name}${s.retrieved ? ` (retrieved ${fmtDate(s.retrieved)})` : ''}`).join('; ')}.{' '} ) : null} {usingDefaultCancers && top?.cancers.length ? ( <> Default selection: the {top.cancers.length} top-level cancers with the highest {metricLabel.toLowerCase()} in {top.year} ({geo?.name}), computed from the observations — not a curated list. ) : null} } actions={} > {obs.length === 0 ? (
({ label: c.canonical_name, href: `/cancer/${c.slug}/statistics` })), { label: 'Full coverage matrix', href: '/explore/coverage' }, { label: 'Sources and license status', href: '/sources' }, ]} > No observation matches {metricLabel.toLowerCase()} · {geo?.name ?? `geography "${state.geography}"`} · {sexLabel(state.sex)} · {ageLabel(state.age)} · {yearRangeLabel(state.from, state.to)} {cancers.length > 0 ? ` for ${cancers.map((c) => c.canonical_name).join(', ')}` : cancers.length === 0 && state.cancers.length === 0 ? ' — no cancer selected' : ''}. {!geo ? ` "${state.geography}" is not a known geography slug or ISO3 code.` : ''} Nothing is estimated or extrapolated. {coverage.length > 0 ? 'What does exist for this selection is listed below.' : ''} {coverage.length > 0 ? (

What exists for {cancers.length > 0 ? `${cancers.length === 1 ? cancers[0]!.canonical_name : `these ${cancers.length} cancers`}` : 'every cancer'}{geo ? ` in ${geo.name}` : ''}

c.slug)} compact />
) : null}
) : (
{split ? {split} : null} {groups.map((g, i) => ( ))} s.version).filter(Boolean).join(' · ') || null} extra={`${fmtInt(obs.length)} observations · ${groups.length} comparable ${groups.length === 1 ? 'group' : 'groups'}`} />
)}
{obs.length > 0 ? (
) : null}

Cite

{SITE_NAME} ({new Date().getUTCFullYear()}). Data explorer: {metricLabel}, {geo?.name ?? state.geography}, {sexLabel(state.sex)}, {ageLabel(state.age)}, {yearRangeLabel(state.from, state.to)}. {SITE_URL}/explore (accessed {fmtDate(new Date())}).{' '} {sourcesUsed.length > 0 ? ( <> Underlying observations: {sourcesUsed.map((s) => `${s.name}${s.dataset ? `, ${s.dataset}` : ''}${s.retrieved ? `, retrieved ${fmtDate(s.retrieved)}` : ''}`).join('; ')}. ) : null}

Underlying observations remain under their providers' licenses (see each source page); CancerIndex's harmonization is CC BY 4.0.

Population statistics describe groups defined by geography, period, sex and age. They do not predict any individual's risk or outcome. Values labelled "estimated" or "projected" are model outputs of the source, not registry counts. Geographies are limited to the sources currently ingested: {ingestedNames.join('; ')} ({options.geographies.map((g) => g.name).join(', ')}). {pending.length > 0 ? <> Registered but not yet ingested: {pending.map((p) => `${p.name.split(' — ')[0]} (${pendingReason(p)})`).join('; ')} — their geographies appear only once the licensing gate is passed and a sync has succeeded; nothing is shown from them until then. : null} Up to {MAX_CANCERS} cancers per chart; different standard populations, sources or age groups are never overlaid ( rules ).
); } /** Why a registered epidemiology source has no observation yet: its connector status when not active, else its license status. */ function pendingReason(p: { status: string; license_status: string }): string { const s = p.status !== 'active' ? p.status : p.license_status; return s.replace(/_/g, ' '); } function SelectionSentence({ state, metricLabel, unit, geographyName }: { state: ExplorerState; metricLabel: string; unit: string | null; geographyName: string }) { return ( {metricLabel} {unit ? ({unitLabel(unit)}) : null} · {geographyName} · {sexLabel(state.sex)} · {ageLabel(state.age)} · {yearRangeLabel(state.from, state.to)} {state.view === 'multiples' ? small multiples : null} ); } function Downloads({ csvHref, apiHref, disabled }: { csvHref: string; apiHref: string; disabled: boolean }) { if (disabled) return no rows to download; return ( <> CSV · JSON (API) ); } function sourcesIn(obs: ExplorerObsRow[]): Array<{ slug: string; name: string; retrieved: Date | null; dataset: string | null; version: string | null }> { const m = new Map(); for (const o of obs) { const r = toDate(o.retrieved_at); const cur = m.get(o.source_slug); if (!cur) m.set(o.source_slug, { slug: o.source_slug, name: o.source_name, retrieved: r, dataset: o.dataset, version: o.dataset_version }); else if (r && (!cur.retrieved || r > cur.retrieved)) m.set(o.source_slug, { ...cur, retrieved: r, dataset: o.dataset, version: o.dataset_version }); } return [...m.values()].sort((a, b) => a.slug.localeCompare(b.slug)); } /** Most recently retrieved provenance row of a group (its observations may span several datasets/runs). */ function provenanceFor(sourceSlug: string, provenanceIds: number[], obs: ExplorerObsRow[]) { const ids = new Set(provenanceIds); let best: ExplorerObsRow | undefined; for (const o of obs) { if (o.source_slug !== sourceSlug || !ids.has(o.provenance_id)) continue; if (!best || String(o.retrieved_at ?? '') > String(best.retrieved_at ?? '')) best = o; } return best ? { dataset: best.dataset, dataset_version: best.dataset_version, retrieved_at: best.retrieved_at, source_url: best.source_url, license: best.source_license } : undefined; }