import { ClaimBadge, Badge } from '@/components/ui/badge'; import { SourceBadge, type ProvenanceInfo } from '@/components/ui/source-badge'; import { ExplorerChart, MAX_PALETTE_SLOTS } from '@/components/charts/explorer-chart'; import { EPI_METRIC_LABEL } from '@/lib/queries/epidemiology'; import { fmtDate, fmtInt, fmtValue, humanize, unitLabel } from '@/lib/format'; import { splitIntoMultiples, shouldUseMultiples, type ComparableGroup, type ExplorerSeries } from '@/lib/explorer-series'; import type { ExplorerView } from '@/lib/explorer-params'; /** * Colour slots follow the entity, never the rank: one slot per (cancer × sex) identity in order of first * appearance across every group of the page. Beyond the palette size the panels are per cancer and the * slot follows the sex inside each panel (the panel title carries the cancer). */ export function assignSlots(groups: readonly ComparableGroup[]): { bySeries: Map | null; bySex: Map } { const ids: string[] = []; const sexes: string[] = []; for (const g of groups) for (const s of g.series) { const id = `${s.cancer_slug}|${s.sex}`; if (!ids.includes(id)) ids.push(id); if (!sexes.includes(s.sex)) sexes.push(s.sex); } const bySex = new Map(sexes.map((s, i) => [s, i % MAX_PALETTE_SLOTS])); if (ids.length > MAX_PALETTE_SLOTS) return { bySeries: null, bySex }; return { bySeries: new Map(ids.map((id, i) => [id, i])), bySex }; } function slotFor(s: ExplorerSeries, slots: ReturnType): number { return slots.bySeries?.get(`${s.cancer_slug}|${s.sex}`) ?? slots.bySex.get(s.sex) ?? 0; } export function groupProvenance(g: ComparableGroup, prov: { dataset?: string | null; dataset_version?: string | null; retrieved_at?: Date | string | null; source_url?: string | null; license?: string | null } | undefined): ProvenanceInfo { return { sourceSlug: g.source_slug, sourceName: g.source_name, dataset: prov?.dataset ?? null, datasetVersion: prov?.dataset_version ?? null, retrievedAt: prov?.retrieved_at ?? null, sourceUrl: prov?.source_url ?? null, license: prov?.license ?? null, layer: 'normalized', evidenceType: 'observed_data', }; } /** * One comparable group → one chart (or small multiples with a shared axis). The caption states every * dimension that defines the group: metric, unit, geography, sex, age group, years, standard population, * source and retrieval date — nothing on the axis is left to be inferred. */ export function ChartGroup({ group, view, slots, provenance, index, total, }: { group: ComparableGroup; view: ExplorerView; slots: ReturnType; provenance: ProvenanceInfo; index: number; total: number; }) { const g = group; const metricLabel = EPI_METRIC_LABEL[g.metric] ?? humanize(g.metric); const multiples = shouldUseMultiples(g, view); const panels = multiples ? splitIntoMultiples(g) : [g]; const sexes = [...new Set(g.series.map((s) => s.sex))]; const hasEstimates = g.series.some((s) => s.dashed); const hasCi = g.series.some((s) => s.points.some((p) => p.lo != null && p.hi != null)); const years = g.year_min === g.year_max ? String(g.year_min) : `${g.year_min}–${g.year_max}`; const toChart = (s: ExplorerSeries) => ({ name: s.name, points: s.points, dashed: s.dashed, slot: slotFor(s, slots) }); return (
1 ? ` (chart ${index + 1} of ${total})` : ''}`} className="min-w-0">

{total > 1 ? Chart {index + 1} of {total} : null} {metricLabel} · {g.geography_name}

{unitLabel(g.unit)} · {sexes.length === 1 ? (sexes[0] === 'all' ? 'both sexes' : humanize(sexes[0]!).toLowerCase()) : 'by sex'} · {g.age_group === 'all' ? 'all ages' : `ages ${g.age_group}`} · {years} {g.standard_population ? ` · standard population: ${g.standard_population}` : ''}
{multiples ? ( <>

Small multiples: one panel per cancer, shared y-axis 0–{fmtValue(g.y_max * 1.08, g.unit)} {unitLabel(g.unit)} across the {panels.length} panels so heights are comparable.

{panels.map((p) => (

{p.series[0]?.cancer_name}

))}
) : ( )} {/* div, not p: the SourceBadge popover contains a
. */}
{g.source_name ?? g.source_slug} {provenance.retrievedAt ? ` (retrieved ${fmtDate(provenance.retrievedAt)})` : ''} · {fmtInt(g.n_obs)} observations · {g.series.length} series {hasCi ? · shaded band = 95% CI as published : null} {hasEstimates ? dashed = estimated by the source : null} {g.unit === 'count' ? · counts are per site group, not summed : null}
); }