spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { Freshness } from '@/components/ui/freshness';5import { ClaimBadge } from '@/components/ui/badge';6import { SourceBadge } from '@/components/ui/source-badge';7import { ExplorerChart } from '@/components/charts/explorer-chart';8import { resolveGeographyRef, topCancersByLatest, yearRangeFor, explorerObservations, explorerOptions } from '@/lib/queries/explorer';9import { EPI_METRIC_LABEL } from '@/lib/queries/epidemiology';10import { groupComparable } from '@/lib/explorer-series';11import { serializeExplorerParams, type ExplorerState } from '@/lib/explorer-params';12import { fmtDate, fmtInt, humanize, toDate, unitLabel } from '@/lib/format';1314/**15 * Home module: the explorer's default chart — the five top-level cancers with the highest annual deaths16 * in the latest year (computed, not curated), one comparable group only (a single source and standard),17 * with the source line and a link that opens the same selection in /explore.18 */19export async function ExplorerModule({ metric = 'mortality_count', geographySlug = 'united-states', n = 5 }: { metric?: string; geographySlug?: string; n?: number }) {20 const [geo, options] = await Promise.all([resolveGeographyRef(geographySlug), explorerOptions()]);21 const hasMetric = options.metrics.some((m) => m.metric === metric);22 const top = geo && hasMetric ? await topCancersByLatest(metric, geo.id, 'all', 'all', n) : null;23 const range = geo && hasMetric ? await yearRangeFor(metric, geo.id) : null;24 const obs = geo && top && top.cancers.length > 0 ? await explorerObservations({ metric, cancerIds: top.cancers.map((c) => c.id), geographyId: geo.id, sex: 'all', age: 'all', from: range?.min ?? null, to: range?.max ?? null }) : [];25 const groups = groupComparable(obs);26 const g = groups[0];27 const metricLabel = EPI_METRIC_LABEL[metric] ?? humanize(metric);28 const state: ExplorerState | null =29 geo && top30 ? { metric, cancers: top.cancers.map((c) => c.slug), geography: geo.slug, sex: 'all', age: 'all', from: range?.min ?? null, to: range?.max ?? null, view: 'lines', normalize: 'none', page: 1 }31 : null;32 const href = state ? `/explore${serializeExplorerParams(state)}` : '/explore';33 const prov = g ? obs.filter((o) => o.source_slug === g.source_slug).sort((a, b) => String(b.retrieved_at ?? '') .localeCompare(String(a.retrieved_at ?? '')))[0] : undefined;34 const freshest = obs.map((o) => toDate(o.updated_at)).filter((d): d is Date => !!d).sort((a, b) => b.getTime() - a.getTime())[0] ?? null;3536 return (37 <Section38 id="explorer"39 kicker="Data explorer"40 title={g ? `${metricLabel} · ${g.geography_name} · ${g.year_min}–${g.year_max}` : 'Data explorer'}41 description={g && top ? `The ${top.cancers.length} top-level cancers with the highest ${metricLabel.toLowerCase()} in ${top.year}, both sexes, all ages, ${unitLabel(g.unit)} per site group as published by ${g.source_name ?? g.source_slug}. Selection computed from the observations; every metric, cancer, geography, sex, age group and year is one click away.` : 'Chart and download registry observations by metric, cancer, geography, sex, age group and year.'}42 actions={43 <Link href={href} className="ci-link">44 Open in the Data explorer →45 </Link>46 }47 >48 {!g ? (49 <EmptyState title="Data not yet available">50 The explorer chart appears once a licensed registry connector has ingested {metricLabel.toLowerCase()} observations{geo ? ` for ${geo.name}` : ''}.{' '}51 <Link className="ci-link" href="/explore/coverage">52 Coverage matrix53 </Link>54 </EmptyState>55 ) : (56 <>57 <ExplorerChart series={g.series.map((s, i) => ({ name: s.name, points: s.points, dashed: s.dashed, slot: i }))} unit={g.unit} ariaLabel={`${metricLabel} in ${g.geography_name}, ${g.year_min}–${g.year_max}, ${g.series.length} cancers, source ${g.source_slug}`} />58 {/* div, not p: the SourceBadge popover contains a <dl>. */}59 <div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px] text-ink-3">60 <SourceBadge p={{ 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?.source_license ?? null, layer: 'normalized', evidenceType: 'observed_data' }} />61 <ClaimBadge kind="observed" />62 <span>63 {g.source_name ?? g.source_slug}64 {prov?.retrieved_at ? ` (retrieved ${fmtDate(prov.retrieved_at)})` : ''} · {fmtInt(g.n_obs)} observations · {unitLabel(g.unit)} · both sexes · all ages65 {g.standard_population ? ` · standard: ${g.standard_population}` : ''}66 </span>67 {groups.length > 1 ? (68 <span>69 · {groups.length - 1} other source{groups.length > 2 ? 's' : ''} publish{groups.length > 2 ? '' : 'es'} this metric — shown separately in the explorer, never overlaid70 </span>71 ) : null}72 </div>73 <Freshness dataUpdatedAt={freshest} sourceVersion={prov?.dataset_version ?? null} />74 </>75 )}76 </Section>77 );78}79