// @ts-nocheck — react-simple-maps ships without TypeScript types (same as USMap.tsx) import { startTransition, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type ReactNode, } from 'react' import { createPortal } from 'react-dom' import { motion, useReducedMotion } from 'framer-motion' import { Link, Navigate, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { keepPreviousData, useQuery } from '@tanstack/react-query' import { ComposableMap, Geographies, Geography } from 'react-simple-maps' import { geoCentroid } from 'd3-geo' import { feature } from 'topojson-client' import { MapContainer, GeoJSON, useMap, CircleMarker, Tooltip as LeafletTooltip } from 'react-leaflet' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import { AdjustmentsHorizontalIcon, ArrowLeftIcon, ChartBarSquareIcon, PauseIcon, PlayIcon, SwatchIcon, TableCellsIcon, XMarkIcon, } from '@heroicons/react/24/outline' import { CartesianGrid, ResponsiveContainer, Tooltip as RechartsTooltip, XAxis, YAxis, Line, LineChart } from 'recharts' import { STATE_CODE_TO_NAME } from '../utils/stateMapping' import { CENSUS_CHORO_FILL_TRANSITION, CENSUS_SCALES, bubbleFillFromT, bubbleRadiusPx, colorFromT, formatCensusMapAxisTickForMetric, formatMetricValueCompact, formatMetricValueDisplay, metricToDisplayT, minMaxExtent, quantileExtent, type CensusScaleId, } from '../utils/censusMapTransforms' import { giniLetterSuffix } from '../utils/giniLetterGrade' import { type CensusValueMode, displayValueForMode, nationalBaselineWithFallback, prevVintageInList, trendCell, } from '../utils/censusMapValueMode' import { type CensusChoroLegendSemantics, censusChoroLegendSemantics, censusMetricExploreQuestion, censusMetricFullHelp, censusMetricRankDirection, censusMetricStaleDataNote, censusMetricWinnerCaption, censusMapShowOfficialCensusLabel, compareRankedMetricValues, CENSUS_EXPLORER_HIDDEN_METRIC_SLUGS, CENSUS_MAP_UI_HELP, } from '../utils/censusDataDictionary' import { buildCensusNarrativePack, buildCensusTrendChartTitle, buildPlaceTrendNarrative, type CensusNarrativePack, } from '../utils/censusMapNarrative' import { CensusRaceBarChart } from '../components/CensusRaceBarChart' import { GiniIncomeInequalityLetterLegend } from '../components/GiniInequalityReadout' import { InfoHelpTrigger } from '../components/InfoHelpTrigger' import MapAddressSearch, { type MapAddressResult } from '../components/MapAddressSearch' import CensusMapLeftRail, { type CensusMapRailSection } from '../components/CensusMapLeftRail' import { censusMapPathPrefix, mapPathPlace, mapPathState, mapPathUs } from '../utils/dataExplorerPaths' const STATES_TOPO = 'https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json' const COUNTY_TOPO = 'https://cdn.jsdelivr.net/npm/us-atlas@3/counties-10m.json' /** Truncate long state / place names for chart Y-axis (single line). */ function truncateStateLabel(name: string, maxChars = 18) { if (!name) return '' if (name.length <= maxChars) return name return `${name.slice(0, Math.max(1, maxChars - 1))}…` } /** High-contrast tooltips (default Recharts is pale on transparent). */ const CENSUS_RECHARTS_TOOLTIP = { contentStyle: { backgroundColor: '#0f172a', border: '1px solid #334155', borderRadius: 10, boxShadow: '0 14px 28px rgba(0,0,0,0.35)', padding: '10px 14px', color: '#f8fafc', }, labelStyle: { color: '#e2e8f0', fontWeight: 700, fontSize: 12, marginBottom: 6 }, itemStyle: { color: '#f8fafc', fontSize: 13 }, } /** Lon/lat pair for geography centroids. */ function toLonLatPair(c: unknown): [number, number] | null { if (!Array.isArray(c) || c.length < 2) return null const lon = Number(c[0]) const lat = Number(c[1]) if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null return [lon, lat] } /** Project [lon,lat] to SVG; geoAlbersUsa may return null / non-array — never feed that to r-s-m Marker. */ function safeProjectScreen( projection: ((c: [number, number]) => unknown) | null | undefined, lonLat: [number, number], ): [number, number] | null { if (projection == null || typeof projection !== 'function') return null try { const p = projection(lonLat) if (!Array.isArray(p) || p.length < 2) return null const x = Number(p[0]) const y = Number(p[1]) if (!Number.isFinite(x) || !Number.isFinite(y)) return null return [x, y] } catch { return null } } const FIPS2_TO_USPS: Record = { '01': 'AL', '02': 'AK', '04': 'AZ', '05': 'AR', '06': 'CA', '08': 'CO', '09': 'CT', '10': 'DE', '11': 'DC', '12': 'FL', '13': 'GA', '15': 'HI', '16': 'ID', '17': 'IL', '18': 'IN', '19': 'IA', '20': 'KS', '21': 'KY', '22': 'LA', '23': 'ME', '24': 'MD', '25': 'MA', '26': 'MI', '27': 'MN', '28': 'MS', '29': 'MO', '30': 'MT', '31': 'NE', '32': 'NV', '33': 'NH', '34': 'NJ', '35': 'NM', '36': 'NY', '37': 'NC', '38': 'ND', '39': 'OH', '40': 'OK', '41': 'OR', '42': 'PA', '44': 'RI', '45': 'SC', '46': 'SD', '47': 'TN', '48': 'TX', '49': 'UT', '50': 'VT', '51': 'VA', '53': 'WA', '54': 'WV', '55': 'WI', '56': 'WY', '72': 'PR', } const USPS_TO_FIPS2: Record = Object.fromEntries( Object.entries(FIPS2_TO_USPS).map(([fips, usps]) => [usps, fips]), ) /** Topo / Census ids may be 1 or "01"; normalize to 2-digit state FIPS for routes and JSON keys. */ function normalizeStateFips(raw: unknown): string | null { const s = String(raw ?? '').trim() if (!s) return null const n = Number.parseInt(s, 10) if (!Number.isFinite(n) || n < 1 || n > 99) return null return String(n).padStart(2, '0') } interface CensusMetric { slug: string label: string format: string table?: string } interface CensusManifest { vintage: string vintages?: string[] county_topo_cdn: string state_topo_cdn?: string metrics: CensusMetric[] place_states: string[] national_ref?: Record> paths: { county_metrics: string place_geojson: string state_metrics?: string state_trends?: string county_trends?: string place_trends?: string } } interface StateMetricsPayload { geography: string vintage: string values: Record> } interface CountyMetricsPayload { geography: string vintage: string values: Record> } /** Multi-year state series from ``state_trends.json`` */ interface StateTrendsPayload { geography: string vintages: string[] by_state: Record> } interface CountyPlaceTrendsPayload { geography: string state: string vintages: string[] byGeoid: Record> } type GeoJSONFeatureCollection = GeoJSON.FeatureCollection function stateMetricsFromTrends( trends: StateTrendsPayload, vintage: string, metricSlugs: string[], ): StateMetricsPayload { const values: Record> = {} for (const [st, row] of Object.entries(trends.by_state)) { const cell: Record = {} const nm = row.NAME if (typeof nm === 'string' && nm.trim()) cell.NAME = nm.trim() for (const slug of metricSlugs) { const series = row[slug] if (series && typeof series === 'object' && !Array.isArray(series)) { const v = (series as Record)[vintage] cell[slug] = typeof v === 'number' && Number.isFinite(v) ? v : null } else { cell[slug] = null } } values[st] = cell } return { geography: 'state', vintage, values } } function countyMetricsFromTrends( trends: CountyPlaceTrendsPayload, vintage: string, metricSlugs: string[], stateFips: string, ): CountyMetricsPayload { const values: Record> = {} const stp = stateFips.padStart(2, '0') for (const [gid, row] of Object.entries(trends.byGeoid)) { if (!gid.startsWith(stp)) continue const cell: Record = {} const nm = row.NAME if (typeof nm === 'string' && nm.trim()) cell.NAME = nm.trim() cell.GEOID = gid for (const slug of metricSlugs) { const series = row[slug] if (series && typeof series === 'object' && !Array.isArray(series)) { const v = (series as Record)[vintage] cell[slug] = typeof v === 'number' && Number.isFinite(v) ? v : null } else { cell[slug] = null } } const g5 = gid.replace(/\D/g, '').slice(-5).padStart(5, '0') values[g5] = cell } return { geography: 'county', vintage, values } } function mergePlaceGeoWithTrends( base: GeoJSONFeatureCollection | undefined, trends: CountyPlaceTrendsPayload | undefined, vintage: string, metricSlugs: string[], ): GeoJSONFeatureCollection | undefined { if (!base) return undefined if (!trends?.byGeoid) return base return { ...base, features: base.features.map((f) => { const p = (f.properties ?? {}) as Record const gid = String(p.GEOID ?? '') const row = trends.byGeoid[gid] if (!row) return f const np: Record = { ...p } for (const slug of metricSlugs) { const series = row[slug] if (series && typeof series === 'object' && !Array.isArray(series)) { const v = (series as Record)[vintage] if (typeof v === 'number' && Number.isFinite(v)) np[slug] = v } } return { ...f, properties: np } }), } } function manifestVintagesFromManifest(manifest: CensusManifest): string[] { const v = manifest.vintages if (Array.isArray(v) && v.length > 0) return [...v] return manifest.vintage ? [manifest.vintage] : [] } /** Dedupe + ascending calendar order for ACS end-years (slider / play). */ function chronoUniqueVintages(years: string[]): string[] { const seen = new Set() const out: string[] = [] for (const y of years) { if (!y || seen.has(y)) continue seen.add(y) out.push(y) } out.sort((a, b) => Number(a) - Number(b)) return out } /** * After the vintage list is swapped mid-play (manifest → trends), map the same calendar year * to its new index; if that year vanished, use the latest year still ≤ heldYear. */ function indexForHeldYearInNewVintages(vintages: string[], heldYear: string): number { if (!vintages.length) return 0 const exact = vintages.indexOf(heldYear) if (exact >= 0) return exact const hy = Number(heldYear) if (!Number.isFinite(hy)) return 0 let bestIdx = 0 let bestVal = -Infinity for (let i = 0; i < vintages.length; i++) { const y = Number(vintages[i]) if (!Number.isFinite(y)) continue if (y <= hy && y >= bestVal) { bestVal = y bestIdx = i } } if (bestVal > -Infinity) return bestIdx return 0 } function metricHasTrendSeriesInRow(row: Record, slug: string): boolean { const series = row[slug] return Boolean(series && typeof series === 'object' && !Array.isArray(series)) } function stateHasAnySeriesForSlug(trends: StateTrendsPayload, slug: string): boolean { return Object.values(trends.by_state).some((row) => metricHasTrendSeriesInRow(row as Record, slug), ) } function stateTrendSliderVintages(trends: StateTrendsPayload, metricSlug: string): string[] { const base = trends.vintages ?? [] if (!base.length) return base if (!stateHasAnySeriesForSlug(trends, metricSlug)) return base const filtered = base.filter((y) => Object.values(trends.by_state).some((row) => { const series = (row as Record)[metricSlug] const v = trendCell(series, y) return typeof v === 'number' && Number.isFinite(v) }), ) return filtered.length ? filtered : base } function countyPlaceSliderVintages( trends: CountyPlaceTrendsPayload, stateFips: string, metricSlug: string, ): string[] { const base = trends.vintages ?? [] if (!base.length) return base const stp = stateFips.padStart(2, '0') const hasAny = Object.entries(trends.byGeoid).some( ([gid, row]) => gid.startsWith(stp) && metricHasTrendSeriesInRow(row as Record, metricSlug), ) if (!hasAny) return base const filtered = base.filter((y) => Object.entries(trends.byGeoid).some(([gid, row]) => { if (!gid.startsWith(stp)) return false const series = (row as Record)[metricSlug] const v = trendCell(series, y) return typeof v === 'number' && Number.isFinite(v) }), ) return filtered.length ? filtered : base } function sliderVintages(args: { mode: 'us' | 'stateCounty' | 'place' manifest: CensusManifest metricSlug: string stateTrends: StateTrendsPayload | null | undefined countyTrends: CountyPlaceTrendsPayload | null | undefined placeTrends: CountyPlaceTrendsPayload | null | undefined stateFips: string | undefined }): string[] { const mv = chronoUniqueVintages(manifestVintagesFromManifest(args.manifest)) let resolved: string[] if (args.mode === 'us') { if (args.stateTrends?.vintages?.length) resolved = stateTrendSliderVintages(args.stateTrends, args.metricSlug) else resolved = mv.length ? mv : ['2022'] } else if (args.mode === 'stateCounty' && args.stateFips && args.countyTrends?.vintages?.length) { resolved = countyPlaceSliderVintages(args.countyTrends, args.stateFips, args.metricSlug) } else if (args.mode === 'place' && args.stateFips && args.placeTrends?.vintages?.length) { resolved = countyPlaceSliderVintages(args.placeTrends, args.stateFips, args.metricSlug) } else { resolved = mv.length ? mv : ['2022'] } return chronoUniqueVintages(resolved) } /** Top-N for race bar charts (states / counties / places). */ const CENSUS_TOP_BAR_ROW_LIMIT = 10 /** * Pool display values for the choropleth legend across every manifest vintage using trend * sidecars, so min/max (and percentile clipping) stay fixed while the year slider moves. */ function collectAllVintageDisplayValuesState( trends: StateTrendsPayload, vintages: string[], metricSlug: string, valueMode: CensusValueMode, nationalRef: CensusManifest['national_ref'], ): number[] { const vals: number[] = [] for (const vy of vintages) { const prevV = prevVintageInList(vintages, vy) const nat = nationalBaselineWithFallback(nationalRef, vy, metricSlug, { stateTrends: trends }) for (const row of Object.values(trends.by_state)) { const rec = row as Record const series = rec[metricSlug] const raw = trendCell(series, vy) let prev: number | null = null if (valueMode === 'yoy' && prevV) prev = trendCell(series, prevV) const d = displayValueForMode(valueMode, raw, prev, nat) if (typeof d === 'number' && Number.isFinite(d)) vals.push(d) } } return vals } function collectAllVintageDisplayValuesCounty( trends: CountyPlaceTrendsPayload, stateFips: string, vintages: string[], metricSlug: string, valueMode: CensusValueMode, nationalRef: CensusManifest['national_ref'], stateTrends: StateTrendsPayload | null | undefined, ): number[] { const stp = stateFips.padStart(2, '0') const vals: number[] = [] for (const vy of vintages) { const prevV = prevVintageInList(vintages, vy) const nat = nationalBaselineWithFallback(nationalRef, vy, metricSlug, { stateTrends }) for (const [gid, row] of Object.entries(trends.byGeoid)) { if (!gid.startsWith(stp)) continue const rec = row as Record const series = rec[metricSlug] const raw = trendCell(series, vy) let prev: number | null = null if (valueMode === 'yoy' && prevV) prev = trendCell(series, prevV) const d = displayValueForMode(valueMode, raw, prev, nat) if (typeof d === 'number' && Number.isFinite(d)) vals.push(d) } } return vals } function collectAllVintageDisplayValuesPlace( trends: CountyPlaceTrendsPayload, stateFips: string, vintages: string[], metricSlug: string, valueMode: CensusValueMode, nationalRef: CensusManifest['national_ref'], stateTrends: StateTrendsPayload | null | undefined, ): number[] { const stp = stateFips.padStart(2, '0') const vals: number[] = [] for (const vy of vintages) { const prevV = prevVintageInList(vintages, vy) const nat = nationalBaselineWithFallback(nationalRef, vy, metricSlug, { stateTrends }) for (const [gid, row] of Object.entries(trends.byGeoid)) { if (!gid.startsWith(stp)) continue const rec = row as Record const series = rec[metricSlug] const raw = trendCell(series, vy) let prev: number | null = null if (valueMode === 'yoy' && prevV) prev = trendCell(series, prevV) const d = displayValueForMode(valueMode, raw, prev, nat) if (typeof d === 'number' && Number.isFinite(d)) vals.push(d) } } return vals } function LabelWithInfo({ label, help, labelClassName = 'text-xs font-semibold uppercase tracking-wide text-slate-500', }: { label: string help: string labelClassName?: string }) { return ( {label} ) } function CensusMetricToolbarControl({ metricFullHelp, metrics, metricSlug, onPickMetric, unconstrainedWidth = false, }: { metricFullHelp: string metrics: CensusMetric[] metricSlug: string onPickMetric: (slug: string) => void /** When true, drop the narrow max-width so the control can fill a toolbar tile. */ unconstrainedWidth?: boolean }) { const current = metrics.find((m) => m.slug === metricSlug) return (
{current && censusMapShowOfficialCensusLabel(current.slug) ? ( Census label: {current.label} ) : null}
) } /** Thin heading row directly above the map (keeps `aria-labelledby` target). */ function CensusMapHeadingStrip({ titleId, title, insight, selectionActive, }: { titleId: string title: string /** Extra context (e.g. selected state vs national) shown under the title. */ insight?: string | null /** True when a leaderboard row is pinned so the strip reads as “this location is selected.” */ selectionActive?: boolean }) { return (

{title}

{insight ? (

{insight}

) : null}
) } /** Source line + “how to read” tips; panel opens on button click to keep the map chrome compact. */ function CensusMapExplainerDetails({ subtitle, calloutLines }: { subtitle: string; calloutLines: string[] }) { const panelId = useId() const [open, setOpen] = useState(false) return (
{open ? (

{subtitle}

{calloutLines.length > 0 ? (
    {calloutLines.map((line, i) => (
  • · {line}
  • ))}
) : null}
) : null}
) } function CensusMapAdvancedMapOptionsFlyout({ open, onClose, metricFullHelp, viz, setViz, scale, setScale, valueMode, setValueMode, focusSection, onConsumedFocusSection, yearControls, }: { open: boolean onClose: () => void metricFullHelp: string viz: 'filled' | 'bubble' setViz: (v: 'filled' | 'bubble') => void scale: CensusScaleId setScale: (s: CensusScaleId) => void valueMode: CensusValueMode setValueMode: (m: CensusValueMode) => void /** When opening from a toolbar affordance, scroll this block into view. */ focusSection?: 'year' | 'view' | 'scale' | 'values' | null onConsumedFocusSection?: () => void /** When provided, renders an in-flyout Year + Play section above the other sections. */ yearControls?: { vintages: string[] displayVintage: string singleVintage: boolean showPlay: boolean playing: boolean setPlaying: (v: boolean) => void onVintageChange: (v: string) => void onBeginPlay?: () => void yearHelp: string } }) { useEffect(() => { if (!open) return const prevOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } window.addEventListener('keydown', onKey) return () => { document.body.style.overflow = prevOverflow window.removeEventListener('keydown', onKey) } }, [open, onClose]) useEffect(() => { if (!open || !focusSection) return const id = focusSection === 'year' ? 'census-advanced-section-year' : focusSection === 'view' ? 'census-advanced-section-view' : focusSection === 'scale' ? 'census-advanced-section-scale' : 'census-advanced-section-values' const t = window.setTimeout(() => { document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) onConsumedFocusSection?.() }, 40) return () => window.clearTimeout(t) }, [open, focusSection, onConsumedFocusSection]) if (!open || typeof document === 'undefined') return null return createPortal(
{yearControls ? (
) : null}
, document.body, ) } function trendPointsFromSeries( vintages: string[], series: Record | undefined, ): { year: string; value: number | null }[] { if (!series || typeof series !== 'object' || Array.isArray(series)) return [] return vintages.map((y) => { const v = (series as Record)[y] return { year: y, value: typeof v === 'number' && Number.isFinite(v) ? v : null } }) } function VintageAndPlayControls({ vintages, displayVintage, singleVintage, showPlay, playing, setPlaying, onVintageChange, onBeginPlay, yearHelp = CENSUS_MAP_UI_HELP.year, }: { vintages: string[] displayVintage: string singleVintage: boolean showPlay: boolean playing: boolean setPlaying: (v: boolean) => void onVintageChange: (v: string) => void /** When starting Play, jump to the first year in the list (oldest). */ onBeginPlay?: () => void yearHelp?: string }) { const yearScrollRef = useRef(null) useEffect(() => { const root = yearScrollRef.current if (!root || !vintages.length) return const active = root.querySelector(`[data-census-vintage="${displayVintage}"]`) if (!active) return const run = () => active.scrollIntoView({ behavior: 'auto', inline: 'center', block: 'nearest' }) requestAnimationFrame(() => requestAnimationFrame(run)) }, [displayVintage, vintages]) return (
{vintages.map((y) => { const active = y === displayVintage return ( ) })}
{showPlay ? ( ) : null}
) } const explorerFilterTile = 'rounded-xl border border-slate-200 bg-white px-2 py-1.5 shadow-sm sm:px-2.5 sm:py-2 min-w-0 flex flex-col justify-center' function CensusExplorerFilterBar({ leadingSlot, metricFullHelp, metricChoices, metricSlug, onMetricChange, vintages, displayVintage, singleVintage, showPlay, playing, setPlaying, onVintageChange, onBeginPlay, yearHelp, onOpenAdvanced, }: { leadingSlot?: ReactNode metricFullHelp: string /** Metrics shown in the topic dropdown (excludes internal / table-total slugs). */ metricChoices: CensusMetric[] metricSlug: string onMetricChange: (slug: string) => void vintages: string[] displayVintage: string singleVintage: boolean showPlay: boolean playing: boolean setPlaying: (v: boolean) => void onVintageChange: (v: string) => void onBeginPlay?: () => void yearHelp: string onOpenAdvanced: (section: 'view' | 'scale' | 'values') => void }) { return (
{leadingSlot ? (
{leadingSlot}
) : null}
) } function AcTrendChart({ title, subtitle, readingLines, chartTitleId, points, format, metricHelp, metricTooltipLabel, }: { title: string subtitle?: string readingLines?: string[] chartTitleId?: string points: { year: string; value: number | null }[] format: (v: number) => string metricHelp?: string /** Shown in the point tooltip as ``{label}: {formatted value}``. */ metricTooltipLabel?: string }) { const readingPanelId = useId() const [readingOpen, setReadingOpen] = useState(false) const nonNull = points.filter((p) => p.value != null) if (nonNull.length < 2) { return (
{title} {subtitle ?

{subtitle}

: null}

Need at least two years with data for a trend line.

{readingLines?.length ? (
{readingOpen ? (
    {readingLines.map((line, i) => (
  • · {line}
  • ))}
) : null}
) : null}
) } return (
{title}
{subtitle ? (

{subtitle}

) : null}
{metricHelp ? : null}
{readingLines?.length ? (
{readingOpen ? (
    {readingLines.map((line, i) => (
  • · {line}
  • ))}
) : null}
) : null}
{ const n = Number(x) if (!Number.isFinite(n)) return '' if (Math.abs(n) >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` if (Math.abs(n) >= 1000) return `${Math.round(n / 1000)}k` return String(Math.round(n)) }} /> { const n = Number(value) const formatted = Number.isFinite(n) ? format(n) : String(value) const line = metricTooltipLabel ? `${metricTooltipLabel}: ${formatted}` : formatted return [line, ''] }} labelFormatter={(y) => `Year ${y}`} />
) } function countyGeoidFromFeature(f: GeoJSON.Feature): string { const id = f.id if (typeof id === 'number' && Number.isFinite(id)) return String(Math.trunc(id)).padStart(5, '0') if (typeof id === 'string' && /^\d+$/.test(id)) return id.padStart(5, '0') const p = f.properties as Record | null const raw = p?.GEOID ?? p?.GEO_ID ?? p?.geoid if (raw == null) return '' const digits = String(raw).replace(/\D/g, '') if (!digits) return '' return digits.length <= 5 ? digits.padStart(5, '0') : digits.slice(-5).padStart(5, '0') } /** 7-digit place GEOID aligned with ``placeDisplayByGeoid`` / map styling. */ function placeGeoid7FromProperties(p: Record | null, fallbackIdx: number): string { const raw = String(p?.GEOID ?? '').replace(/\D/g, '') if (!raw) return `idx_${fallbackIdx}` return raw.length <= 7 ? raw.padStart(7, '0') : raw.slice(-7).padStart(7, '0') } function buildStateCountyGeoJson( topology: { objects: { counties: unknown } }, values: Record>, stateFips: string, metricSlug: string, ): GeoJSONFeatureCollection | null { try { const fc = feature(topology as never, topology.objects.counties as never) as GeoJSON.FeatureCollection const features: GeoJSON.Feature[] = [] for (const f of fc.features) { const gid = countyGeoidFromFeature(f) if (gid.length !== 5 || !gid.startsWith(stateFips)) continue const row = values[gid] ?? {} const base = (f.properties as Record | undefined) ?? {} const name = typeof row.NAME === 'string' ? row.NAME : typeof base.name === 'string' ? base.name : gid const props: Record = { ...base, GEOID: gid, NAME: name, ...row, [metricSlug]: row[metricSlug], } features.push({ ...f, properties: props, id: gid }) } if (!features.length) return null return { type: 'FeatureCollection', features } } catch { return null } } function censusNull(v: unknown): v is number { return typeof v === 'number' && Number.isFinite(v) } function formatMetricValue( slug: string, v: number | null | undefined, metrics: CensusMetric[], valueMode: CensusValueMode = 'raw', ): string { const s = formatMetricValueDisplay(slug, v, metrics, valueMode) if (slug !== 'gini_income_inequality' || valueMode !== 'raw' || v == null || !Number.isFinite(v)) return s return `${s}${giniLetterSuffix(v)}` } function metricValueTooltipLine( metricLabel: string, slug: string, v: number | null | undefined, metrics: CensusMetric[], valueMode: CensusValueMode, ): string { return `${metricLabel}: ${formatMetricValue(slug, v, metrics, valueMode)}` } /** County / place drill-down: fit data, clamp zoom/pan so wheel zoom cannot leave the state/city context. */ function DrilldownMapBoundsController({ data }: { data: GeoJSONFeatureCollection }) { const map = useMap() useEffect(() => { if (!data?.features?.length) return const layer = L.geoJSON(data as never) const b = layer.getBounds() if (!b.isValid()) return let cancelled = false let clampOnMoveEnd: (() => void) | null = null /** Padding passed to getBoundsZoom (Leaflet subtracts this once from map width/height). */ const fitPad = L.point(28, 28) const run = () => { if (cancelled) return map.invalidateSize() const size = map.getSize() if (size.x < 32 || size.y < 32) return // “Contain” zooms bound how far users can zoom out / in while still framing the geography. const zLoose = map.getBoundsZoom(b.pad(0.22), false) const zTight = map.getBoundsZoom(b.pad(0.06), false) if (!Number.isFinite(zLoose) || !Number.isFinite(zTight)) return const zOut = Math.min(zLoose, zTight) const zIn = Math.max(zLoose, zTight) let safeMin = Math.max(5, Math.floor(zOut) - 1) // Initial view: fit the whole area in the map (inside=false). Use generous bounds padding so // narrow side-by-side layouts do not “over-zoom” into a few counties (e.g. Alabama in a short map pane). const zFit = map.getBoundsZoom(b.pad(0.14), false, fitPad) if (!Number.isFinite(zFit)) return let safeMax = Math.min(13, Math.max(Math.ceil(zIn) + 1, Math.ceil(zFit) + 2)) if (safeMax <= safeMin) safeMax = safeMin + 1 // Use floor(zFit) so the entire padded bounds stay in view; avoid +1 here — it was zooming one level too tight. let z = Math.min(safeMax, Math.max(safeMin, Math.floor(zFit))) map.setMinZoom(safeMin) map.setMaxZoom(safeMax) map.setMaxBounds(b.pad(0.32)) map.options.maxBoundsViscosity = 0.75 map.setView(b.getCenter(), z, { animate: false }) if (clampOnMoveEnd) { map.off('moveend', clampOnMoveEnd) clampOnMoveEnd = null } clampOnMoveEnd = () => { const zz = map.getZoom() if (zz > safeMax) map.setZoom(safeMax) else if (zz < safeMin) map.setZoom(safeMin) } if (!cancelled) map.once('moveend', clampOnMoveEnd) } const schedule = () => { requestAnimationFrame(() => requestAnimationFrame(run)) } map.whenReady(schedule) const host = map.getContainer().parentElement const ro = typeof ResizeObserver !== 'undefined' && host ? new ResizeObserver(() => { if (cancelled) return requestAnimationFrame(run) }) : null if (ro && host) ro.observe(host) return () => { cancelled = true ro?.disconnect() if (clampOnMoveEnd) map.off('moveend', clampOnMoveEnd) map.setMinZoom(5) map.setMaxZoom(13) map.setMaxBounds(null as any) map.options.maxBoundsViscosity = undefined } }, [map, data]) return null } function featureLatLng(feature: GeoJSON.Feature): { lat: number; lng: number } | null { try { const layer = L.geoJSON(feature as never) const c = layer.getBounds().getCenter() if (c && Number.isFinite(c.lat) && Number.isFinite(c.lng)) return { lat: c.lat, lng: c.lng } } catch { /* ignore */ } return null } const CHORO_LEGEND_GRADIENT_STOPS = 17 function ChoroplethLegend({ min, max, scale, format, valueMode = 'raw', extentPoolsAllVintages = false, metricHelp, semantics, letterGradeLegend, }: { min: number max: number scale: CensusScaleId format: (v: number) => string valueMode?: CensusValueMode /** When trend sidecars are used, min/max are from all vintages pooled (still ~4th–96th pct.). */ extentPoolsAllVintages?: boolean metricHelp?: string /** Plain-language ends of the ramp + one sentence on how to read color intensity. */ semantics?: CensusChoroLegendSemantics | null /** Optional strip (e.g. Gini A–F grades) shown under the semantics line. */ letterGradeLegend?: import('react').ReactNode }) { const legendHelpPanelId = useId() const [legendHelpOpen, setLegendHelpOpen] = useState(false) const n = CHORO_LEGEND_GRADIENT_STOPS const stops = Array.from({ length: n }, (_, i) => { const u = n <= 1 ? 0 : i / (n - 1) const v = min + u * (max - min) const t = metricToDisplayT(v, min, max, scale) ?? 0 return { offset: `${u * 100}%`, color: colorFromT(t), value: v } }) const tickUs = [0, 0.25, 0.5, 0.75, 1] as const const gradId = `census-ramp-${scale}-${Math.round(min)}-${Math.round(max)}` const legendInfoBody = [ 'Legend maps the displayed value to color using the selected transform. When multi-year data is loaded, low/high can be pooled across years so colors stay comparable as you change year.', metricHelp, ] .filter(Boolean) .join('\n\n') const legendFootnote = `Stops are evenly spaced in mapped value range; shading follows the selected transform (${CENSUS_SCALES.find((x) => x.id === scale)?.label ?? scale}). ${ valueMode === 'raw' ? extentPoolsAllVintages ? 'Percentile band (~4th–96th pct.) is computed across all years in the slider when multi-year trend data is present, so colors stay comparable as you change year.' : 'Extremes use percentile clipping so outliers do not wash out the map.' : valueMode === 'yoy' ? 'Legend shows percent change vs the prior year in the slider order.' : 'Legend shows percent difference from the national benchmark (population-weighted state composite when available).' }${ extentPoolsAllVintages && valueMode !== 'raw' ? ' Endpoints pool all years from trend data so the scale stays fixed while you scrub the year slider.' : '' }` return (
{metricHelp ? ( ) : ( What the colors mean )}
{semantics ? ( <>
{semantics.lowEnd} ←→ {semantics.highEnd}

{semantics.gradientHint}

) : null} {stops.map((s, i) => ( ))} {tickUs.map((frac) => ( {format(min + frac * (max - min))} ))} {letterGradeLegend ? (
{letterGradeLegend}
) : null}
{legendHelpOpen ? (

{legendInfoBody}

{legendFootnote}

) : null}
) } function BubbleLegend({ min, max, scale, format, metricHelp, letterGradeLegend, }: { min: number max: number scale: CensusScaleId format: (v: number) => string metricHelp?: string letterGradeLegend?: import('react').ReactNode }) { const legendHelpPanelId = useId() const [legendHelpOpen, setLegendHelpOpen] = useState(false) const refs = [0.15, 0.5, 0.88].map((u) => min + u * (max - min)) const items = refs.map((v) => ({ v, r: bubbleRadiusPx(v, min, max, scale, 4, 22), t: metricToDisplayT(v, min, max, scale), label: format(v), })) const bubbleInfoBody = [ 'Circle area encodes the mapped value; color follows the Deep Ocean ramp (steel blue → teal → deep emerald) as on the map.', metricHelp, ] .filter(Boolean) .join('\n\n') return (
{metricHelp ? ( ) : ( Bubble size scale )}
{items.map((it, i) => (
{it.label}
))}
{letterGradeLegend ? (
{letterGradeLegend}
) : null}
{legendHelpOpen ? (

{bubbleInfoBody}

) : null}
) } /** When the slider has 2+ vintages (from manifest or trend sidecars), advance years with Play. */ const PLAY_INTERVAL_MS = 1950 function CensusMapPage() { const navigate = useNavigate() const location = useLocation() const [searchParams, setSearchParams] = useSearchParams() const { vintage, metric, stateFips: stateFipsRaw } = useParams<{ vintage?: string metric?: string stateFips?: string }>() const stateFips = stateFipsRaw ? normalizeStateFips(stateFipsRaw) ?? (stateFipsRaw.length <= 2 ? stateFipsRaw.padStart(2, '0') : stateFipsRaw) : undefined const stateUsps = stateFips ? FIPS2_TO_USPS[stateFips] : undefined const stateName = stateUsps ? STATE_CODE_TO_NAME[stateUsps] : undefined const mapPrefix = useMemo(() => censusMapPathPrefix(location.pathname), [location.pathname]) const mode = useMemo(() => { if (location.pathname.includes('/census-map/place/') || location.pathname.includes('/data-explorer/map/place/')) return 'place' if (location.pathname.includes('/census-map/state/') || location.pathname.includes('/data-explorer/map/state/')) return 'stateCounty' return 'us' }, [location.pathname]) const viz: 'filled' | 'bubble' = searchParams.get('viz') === 'bubble' ? 'bubble' : 'filled' const scaleRaw = searchParams.get('scale') || 'linear' const scale: CensusScaleId = (['linear', 'sqrt', 'log', 'exp'].includes(scaleRaw) ? scaleRaw : 'linear') as CensusScaleId const setViz = (v: 'filled' | 'bubble') => { const next = new URLSearchParams(searchParams) if (v === 'filled') next.delete('viz') else next.set('viz', 'bubble') setSearchParams(next, { replace: true }) } const setScale = (s: CensusScaleId) => { const next = new URLSearchParams(searchParams) if (s === 'linear') next.delete('scale') else next.set('scale', s) setSearchParams(next, { replace: true }) } const valueModeRaw = searchParams.get('valueMode') || 'raw' const valueMode: CensusValueMode = ( ['raw', 'yoy', 'vs_natl'].includes(valueModeRaw) ? valueModeRaw : 'raw' ) as CensusValueMode const setValueMode = (m: CensusValueMode) => { const next = new URLSearchParams(searchParams) if (m === 'raw') next.delete('valueMode') else next.set('valueMode', m) setSearchParams(next, { replace: true }) } const [advancedMapOptionsOpen, setAdvancedMapOptionsOpen] = useState(false) const [advancedFocusSection, setAdvancedFocusSection] = useState<'year' | 'view' | 'scale' | 'values' | null>(null) const [pinnedAddress, setPinnedAddress] = useState<{ displayName: string shortLabel: string lat: number lon: number stateCode: string | null stateFips: string | null county: string city: string } | null>(null) const [censusOnboardingDismissed, setCensusOnboardingDismissed] = useState(() => { if (typeof window === 'undefined') return false try { return window.localStorage.getItem('open-navigator-census-map-onboarding-dismissed') === '1' } catch { return false } }) const dismissCensusOnboarding = useCallback(() => { try { if (typeof window !== 'undefined') { window.localStorage.setItem('open-navigator-census-map-onboarding-dismissed', '1') } } catch { /* ignore */ } setCensusOnboardingDismissed(true) }, []) const { data: manifest, isError: manifestError } = useQuery({ queryKey: ['census-map-manifest'], queryFn: async (): Promise => { const r = await fetch('/data/census-map/manifest.json') if (!r.ok) throw new Error('manifest') return r.json() }, }) const metricSlug = metric ?? 'median_household_income' const metricSlugsList = useMemo(() => (manifest?.metrics ?? []).map((m) => m.slug), [manifest?.metrics]) const { data: stateTrends, isFetched: stateTrendsFetched } = useQuery({ queryKey: ['census-state-trends'], queryFn: async (): Promise => { const r = await fetch('/data/census-map/state_trends.json') if (r.status === 404) return null if (!r.ok) throw new Error('state trends') return r.json() as StateTrendsPayload }, enabled: !!manifest, retry: false, }) const { data: countyTrends, isFetched: countyTrendsFetched } = useQuery({ queryKey: ['census-county-trends', stateFips], queryFn: async (): Promise => { const r = await fetch(`/data/census-map/county_trends_${stateFips}.json`) if (r.status === 404) return null if (!r.ok) throw new Error('county trends') return r.json() as CountyPlaceTrendsPayload }, enabled: !!manifest && mode === 'stateCounty' && !!stateFips, retry: false, }) const { data: placeTrends, isFetched: placeTrendsFetched } = useQuery({ queryKey: ['census-place-trends', stateFips], queryFn: async (): Promise => { const r = await fetch(`/data/census-map/place_trends_${stateFips}.json`) if (r.status === 404) return null if (!r.ok) throw new Error('place trends') return r.json() as CountyPlaceTrendsPayload }, enabled: !!manifest && mode === 'place' && !!stateFips, retry: false, }) const stateTrendsDriveStatePayload = mode === 'us' && !!stateTrends && stateHasAnySeriesForSlug(stateTrends, metricSlug) const countyTrendsDriveCountyPayload = mode === 'stateCounty' && !!countyTrends && !!stateFips && Object.entries(countyTrends.byGeoid).some( ([gid, row]) => gid.startsWith(stateFips.padStart(2, '0')) && metricHasTrendSeriesInRow(row as Record, metricSlug), ) const vintages = useMemo(() => { if (!manifest) return ['2022'] return sliderVintages({ mode, manifest, metricSlug, stateTrends, countyTrends, placeTrends, stateFips, }) }, [mode, manifest, metricSlug, stateTrends, countyTrends, placeTrends, stateFips]) const effectiveVintage = useMemo(() => { if (!manifest) return vintage ?? '2022' const list = vintages if (!list.length) return vintage ?? manifest.vintage ?? '2022' const latest = list[list.length - 1]! if (!vintage) return latest if (list.includes(vintage)) return vintage return latest }, [manifest, vintage, vintages]) const [playing, setPlaying] = useState(false) const [animIndex, setAnimIndex] = useState(0) const vintagesRef = useRef(vintages) vintagesRef.current = vintages /** While dragging the year slider, update charts immediately; URL follows on a short debounce. */ const [scrubVintage, setScrubVintage] = useState(null) const vintageNavigateTimerRef = useRef | null>(null) useEffect(() => { return () => { if (vintageNavigateTimerRef.current) window.clearTimeout(vintageNavigateTimerRef.current) } }, []) useEffect(() => { if (scrubVintage != null && vintage === scrubVintage) setScrubVintage(null) }, [vintage, scrubVintage]) useEffect(() => { setScrubVintage(null) }, [metricSlug, mode, stateFips]) useEffect(() => { if (playing) return const ix = vintages.indexOf(effectiveVintage) setAnimIndex(ix >= 0 ? ix : 0) }, [effectiveVintage, vintages.join(','), playing]) const vintagesPlayKeyRef = useRef('') const animIndexPlayRef = useRef(0) animIndexPlayRef.current = animIndex useLayoutEffect(() => { const key = vintages.join(',') if (playing) { const prevKey = vintagesPlayKeyRef.current if (prevKey && prevKey !== key) { const prevList = prevKey.split(',') const safeIx = Math.min( Math.max(0, animIndexPlayRef.current), Math.max(0, prevList.length - 1), ) const heldYear = prevList[safeIx] if (heldYear) { const nextIx = indexForHeldYearInNewVintages(vintages, heldYear) setAnimIndex(nextIx) } } } vintagesPlayKeyRef.current = key }, [playing, vintages]) const canPlayMultiVintage = vintages.length > 1 && (mode === 'us' || (mode === 'stateCounty' && !!stateFips) || (mode === 'place' && !!stateFips)) const canTrendAnimate = canPlayMultiVintage const displayVintage = useMemo(() => { if (playing && canTrendAnimate && vintages.length) { const ix = Math.min(Math.max(0, animIndex), vintages.length - 1) return vintages[ix]! } if (scrubVintage && vintages.includes(scrubVintage)) return scrubVintage return effectiveVintage }, [playing, canTrendAnimate, animIndex, vintages, scrubVintage, effectiveVintage]) useEffect(() => { if (!playing || !canTrendAnimate) return const t = window.setInterval(() => { setAnimIndex((i) => { const list = vintagesRef.current if (!list.length) return 0 const last = list.length - 1 if (i >= last) { queueMicrotask(() => setPlaying(false)) return last } return i + 1 }) }, PLAY_INTERVAL_MS) return () => window.clearInterval(t) }, [playing, canTrendAnimate]) useEffect(() => { setPlaying(false) }, [effectiveVintage, mode, metricSlug, valueMode]) useEffect(() => { if (valueMode !== 'raw' && viz === 'bubble') { const next = new URLSearchParams(searchParams) next.delete('viz') setSearchParams(next, { replace: true }) } }, [valueMode, viz, searchParams, setSearchParams]) const showPlay = canTrendAnimate const { data: statePayloadRaw } = useQuery({ queryKey: ['census-state-metrics', displayVintage], queryFn: async (): Promise => { const r = await fetch(`/data/census-map/${displayVintage}/state_metrics.json`) if (!r.ok) throw new Error('state metrics') return r.json() }, enabled: mode === 'us' && !!displayVintage && !stateTrendsDriveStatePayload, placeholderData: keepPreviousData, staleTime: 1000 * 60 * 60, retry: false, }) const statePayload = useMemo(() => { if (mode === 'us' && stateTrendsDriveStatePayload && stateTrends && displayVintage) { return stateMetricsFromTrends(stateTrends, displayVintage, metricSlugsList) } return statePayloadRaw }, [mode, stateTrendsDriveStatePayload, stateTrends, statePayloadRaw, displayVintage, metricSlugsList]) const { data: countyPayloadRaw, isError: countyPayloadError, isPending: countyPayloadLoading } = useQuery({ queryKey: ['census-county-metrics', displayVintage], queryFn: async (): Promise => { const r = await fetch(`/data/census-map/${displayVintage}/county_metrics.json`) if (!r.ok) throw new Error('county metrics') return r.json() }, enabled: mode === 'stateCounty' && !!displayVintage && !countyTrendsDriveCountyPayload, placeholderData: keepPreviousData, staleTime: 1000 * 60 * 60, retry: false, }) const countyPayload = useMemo(() => { if (mode === 'stateCounty' && countyTrendsDriveCountyPayload && countyTrends && stateFips && displayVintage) { return countyMetricsFromTrends(countyTrends, displayVintage, metricSlugsList, stateFips) } return countyPayloadRaw }, [mode, countyTrendsDriveCountyPayload, countyTrends, countyPayloadRaw, displayVintage, metricSlugsList, stateFips]) const { data: countyTopo, isPending: countyTopoLoading } = useQuery({ queryKey: ['census-county-topo', manifest?.county_topo_cdn], queryFn: async () => { const u = manifest!.county_topo_cdn || COUNTY_TOPO const r = await fetch(u) if (!r.ok) throw new Error('county topo') return r.json() }, enabled: mode === 'stateCounty' && !!manifest, staleTime: Infinity, }) const placeUrl = mode === 'place' && stateFips ? `/data/census-map/${displayVintage}/place_${stateFips}.geojson` : null const { data: placeGeo, isError: placeGeoError } = useQuery({ queryKey: ['census-place-geo', placeUrl], queryFn: async (): Promise => { const r = await fetch(placeUrl!) if (!r.ok) throw new Error('place geojson') return r.json() }, enabled: mode === 'place' && !!placeUrl, placeholderData: keepPreviousData, staleTime: 1000 * 60 * 60, retry: false, }) const placeGeoMerged = useMemo( () => mergePlaceGeoWithTrends(placeGeo, placeTrends ?? undefined, displayVintage, metricSlugsList), [placeGeo, placeTrends, displayVintage, metricSlugsList], ) const [countyHover, setCountyHover] = useState<{ id: string; name: string; value: number | null } | null>(null) const [placeHover, setPlaceHover] = useState<{ id: string; name: string; value: number | null } | null>(null) /** Bar-chart selection: highlight matching area on the map (click same bar again to clear). */ const [leaderboardPinnedId, setLeaderboardPinnedId] = useState(null) useEffect(() => { setLeaderboardPinnedId(null) }, [mode, stateFips, metricSlug]) const toggleLeaderboardPin = useCallback((id: string) => { setLeaderboardPinnedId((prev) => (prev === id ? null : id)) }, []) const placePinnedLabel = useMemo((): string | null => { if (mode !== 'place' || !leaderboardPinnedId || !placeGeoMerged?.features.length) return null const pid = leaderboardPinnedId for (const f of placeGeoMerged.features) { const p = f.properties as Record | null if (placeGeoid7FromProperties(p, 0) === pid) { const n = String(p?.NAME ?? '').trim() return n || null } } return null }, [mode, leaderboardPinnedId, placeGeoMerged]) const focusPinnedStateNarrative = useMemo(() => { if (mode !== 'us' || leaderboardPinnedId == null || leaderboardPinnedId === '' || !statePayload?.values) return null const sid = normalizeStateFips(leaderboardPinnedId) ?? leaderboardPinnedId const row = statePayload.values[sid] if (!row) return null const name = typeof row.NAME === 'string' && row.NAME.trim() ? row.NAME.trim() : sid return { geoid: sid, name } }, [mode, leaderboardPinnedId, statePayload]) const manifestMetrics = manifest?.metrics ?? [] const selectableMetrics = useMemo( () => manifestMetrics.filter((m) => !CENSUS_EXPLORER_HIDDEN_METRIC_SLUGS.has(m.slug)), [manifestMetrics], ) const metrics = manifestMetrics const placeStates = manifest?.place_states ?? [] const currentMetricMeta = useMemo(() => metrics.find((m) => m.slug === metricSlug), [metrics, metricSlug]) const metricFullHelp = useMemo( () => censusMetricFullHelp(metricSlug, currentMetricMeta), [metricSlug, currentMetricMeta], ) const choroLegendSemantics = useMemo( () => censusChoroLegendSemantics(metricSlug, valueMode, currentMetricMeta?.label ?? metricSlug), [metricSlug, valueMode, currentMetricMeta?.label], ) const giniRawLetterLegend = useMemo( () => (metricSlug === 'gini_income_inequality' && valueMode === 'raw' ? : null), [metricSlug, valueMode], ) const nationalContextNote = useMemo(() => { if (valueMode !== 'raw' || mode !== 'us') return null const baseline = nationalBaselineWithFallback(manifest?.national_ref, displayVintage, metricSlug, { stateRows: statePayload?.values, stateTrends, }) if (baseline == null || !Number.isFinite(baseline)) return null const label = currentMetricMeta?.label ?? metricSlug const formatted = formatMetricValue(metricSlug, baseline, metrics, 'raw') return `National benchmark for ${label}: ${formatted} (population-weighted U.S. composite when available).` }, [valueMode, mode, manifest?.national_ref, displayVintage, metricSlug, currentMetricMeta?.label, metrics, statePayload, stateTrends]) const [hoverRegion, setHoverRegion] = useState<{ id: string name: string value: number | null } | null>(null) const trendAsideCapturingHoverRef = useRef(false) const hoverClearTimerRef = useRef | null>(null) const cancelScheduledHoverClear = useCallback(() => { if (hoverClearTimerRef.current != null) { window.clearTimeout(hoverClearTimerRef.current) hoverClearTimerRef.current = null } }, []) const scheduleHoverRegionClear = useCallback(() => { cancelScheduledHoverClear() hoverClearTimerRef.current = window.setTimeout(() => { hoverClearTimerRef.current = null if (!trendAsideCapturingHoverRef.current) { setHoverRegion(null) } }, 240) }, [cancelScheduledHoverClear]) useEffect(() => { return () => { cancelScheduledHoverClear() } }, [cancelScheduledHoverClear]) useEffect(() => { cancelScheduledHoverClear() }, [metricSlug, mode, cancelScheduledHoverClear]) const usMapInnerRef = useRef(null) const [usMapTipPos, setUsMapTipPos] = useState<{ x: number; y: number } | null>(null) const updateUsMapTip = useCallback((clientX: number, clientY: number) => { const el = usMapInnerRef.current if (!el) return const r = el.getBoundingClientRect() const lx = clientX - r.left + el.scrollLeft const ly = clientY - r.top + el.scrollTop const estW = 280 const estH = 132 let left = lx + 14 let top = ly + 14 if (left + estW > el.scrollWidth - 8) left = Math.max(8, lx - estW - 14) if (top + estH > el.scrollTop + el.clientHeight - 8) top = Math.max(8, ly - estH - 14) left = Math.max(8, Math.min(left, Math.max(8, el.scrollWidth - estW - 8))) top = Math.max(8, Math.min(top, Math.max(8, el.scrollTop + el.clientHeight - estH - 8))) setUsMapTipPos({ x: left, y: top }) }, []) const [tableSort, setTableSort] = useState<{ key: 'name' | 'value' | 'geoid'; dir: 'asc' | 'desc' }>(() => ({ key: 'value', dir: censusMetricRankDirection(metricSlug) === 'lower' ? 'asc' : 'desc', })) useEffect(() => { setTableSort((prev) => prev.key === 'value' ? { key: 'value', dir: censusMetricRankDirection(metricSlug) === 'lower' ? 'asc' : 'desc' } : prev, ) }, [metricSlug]) const reduceMotion = useReducedMotion() const trendChartOpenCounty = Boolean(countyTrends && countyHover && stateFips) const trendFadeTransition = { duration: reduceMotion ? 0 : 0.28, ease: 'easeInOut' } const stateDisplayById = useMemo(() => { const out: Record = {} if (!statePayload || !metricSlug) return out const prevV = prevVintageInList(vintages, displayVintage) const nat = nationalBaselineWithFallback(manifest?.national_ref, displayVintage, metricSlug, { stateRows: statePayload.values, stateTrends, }) for (const [sid, row] of Object.entries(statePayload.values)) { const raw = typeof row[metricSlug] === 'number' && Number.isFinite(row[metricSlug]) ? row[metricSlug] : null let prev: number | null = null if (valueMode === 'yoy' && prevV && stateTrends?.by_state?.[sid]) { prev = trendCell((stateTrends.by_state[sid] as Record)[metricSlug], prevV) } out[sid] = displayValueForMode(valueMode, raw, prev, nat) } return out }, [ statePayload, metricSlug, valueMode, displayVintage, vintages.join(','), stateTrends, manifest?.national_ref, ]) useEffect(() => { setHoverRegion((prev) => { if (!prev?.id) return prev const row = statePayload?.values?.[prev.id] const name = row && typeof row.NAME === 'string' ? row.NAME : prev.name const disp = stateDisplayById[prev.id] ?? null if (name === prev.name && disp === prev.value) return prev return { ...prev, name, value: disp } }) }, [statePayload, stateDisplayById, displayVintage, metricSlug]) /** US trend line: hovered state, or the bar-chart pinned state when the pointer has left the map. */ const usTrendSubject = useMemo(() => { if (mode !== 'us') return null if (hoverRegion) return hoverRegion if (leaderboardPinnedId != null && leaderboardPinnedId !== '' && statePayload?.values) { const sid = normalizeStateFips(leaderboardPinnedId) ?? leaderboardPinnedId const row = statePayload.values[sid] if (!row) return null const name = typeof (row as { NAME?: string }).NAME === 'string' ? (row as { NAME: string }).NAME : sid const disp = stateDisplayById[sid] ?? null return { id: sid, name, value: disp } } return null }, [mode, hoverRegion, leaderboardPinnedId, statePayload, stateDisplayById]) const trendChartOpenUs = Boolean(stateTrends && usTrendSubject) const stateChoroPooledForLegend = useMemo((): number[] | null => { if (mode !== 'us' || !stateTrends || !metricSlug || vintages.length < 2) return null const arr = collectAllVintageDisplayValuesState( stateTrends, vintages, metricSlug, valueMode, manifest?.national_ref, ) return arr.length >= 20 ? arr : null }, [mode, stateTrends, vintages.join(','), metricSlug, valueMode, manifest?.national_ref]) const stateChoroExtent = useMemo(() => { if (stateChoroPooledForLegend?.length) return quantileExtent(stateChoroPooledForLegend) const vals = Object.values(stateDisplayById).filter( (x): x is number => typeof x === 'number' && Number.isFinite(x), ) if (!vals.length) return { min: 0, max: 1 } return quantileExtent(vals) }, [stateChoroPooledForLegend, stateDisplayById]) const stateBubbleExtent = useMemo(() => { if (!statePayload || !metricSlug) return { min: 0, max: 1 } const displayVals = Object.values(stateDisplayById).filter( (x): x is number => typeof x === 'number' && Number.isFinite(x), ) if (displayVals.length >= 2) { return minMaxExtent(displayVals) } const vals = Object.values(statePayload.values) .map((row) => row[metricSlug]) .filter((x): x is number => typeof x === 'number' && Number.isFinite(x)) if (!vals.length) return { min: 0, max: 1 } return minMaxExtent(vals) }, [statePayload, metricSlug, stateDisplayById]) const placeDisplayByGeoid = useMemo(() => { const out: Record = {} const g = placeGeoMerged if (!g || !metricSlug) return out const prevV = prevVintageInList(vintages, displayVintage) const nat = nationalBaselineWithFallback(manifest?.national_ref, displayVintage, metricSlug, { stateRows: statePayload?.values, stateTrends, }) for (const f of g.features) { const p = f.properties as Record | null const raw = typeof p?.[metricSlug] === 'number' && Number.isFinite(p[metricSlug]) ? p[metricSlug] : null const rawG = String(p?.GEOID ?? '').replace(/\D/g, '') const gid7 = rawG.length <= 7 ? rawG.padStart(7, '0') : rawG.slice(-7).padStart(7, '0') let prev: number | null = null if (valueMode === 'yoy' && prevV && placeTrends?.byGeoid?.[gid7]) { prev = trendCell((placeTrends.byGeoid[gid7] as Record)[metricSlug], prevV) } out[gid7] = displayValueForMode(valueMode, raw, prev, nat) } return out }, [ placeGeoMerged, metricSlug, valueMode, displayVintage, vintages.join(','), placeTrends, manifest?.national_ref, stateTrends, statePayload, ]) const placeTrendSubject = useMemo(() => { if (mode !== 'place') return null if (placeHover) return placeHover if (leaderboardPinnedId && placeGeoMerged?.features.length) { for (const f of placeGeoMerged.features) { const p = f.properties as Record | null const gid7 = placeGeoid7FromProperties(p, 0) if (gid7 === leaderboardPinnedId) { const name = String(p?.NAME ?? gid7) const value = placeDisplayByGeoid[gid7] ?? null return { id: gid7, name, value } } } } return null }, [mode, placeHover, leaderboardPinnedId, placeGeoMerged, placeDisplayByGeoid]) const trendChartOpenPlace = Boolean(placeTrends && placeTrendSubject) const placeChoroPooledForLegend = useMemo((): number[] | null => { if (mode !== 'place' || !placeTrends || !stateFips || !metricSlug || vintages.length < 2) return null const arr = collectAllVintageDisplayValuesPlace( placeTrends, stateFips, vintages, metricSlug, valueMode, manifest?.national_ref, stateTrends, ) return arr.length >= 20 ? arr : null }, [mode, placeTrends, stateFips, vintages.join(','), metricSlug, valueMode, manifest?.national_ref, stateTrends]) const placeChoroExtent = useMemo(() => { if (placeChoroPooledForLegend?.length) return quantileExtent(placeChoroPooledForLegend) const vals = Object.values(placeDisplayByGeoid).filter( (x): x is number => typeof x === 'number' && Number.isFinite(x), ) if (!vals.length) return { min: 0, max: 1 } return quantileExtent(vals) }, [placeChoroPooledForLegend, placeDisplayByGeoid]) const placeBubbleExtent = useMemo(() => { const g = placeGeoMerged if (!g || !metricSlug) return { min: 0, max: 1 } const displayVals = Object.values(placeDisplayByGeoid).filter( (x): x is number => typeof x === 'number' && Number.isFinite(x), ) if (displayVals.length >= 2) { return minMaxExtent(displayVals) } const vals = g.features .map((f) => { const v = (f.properties as Record | null)?.[metricSlug] return typeof v === 'number' && Number.isFinite(v) ? v : null }) .filter((x): x is number => x != null) if (!vals.length) return { min: 0, max: 1 } return minMaxExtent(vals) }, [placeGeoMerged, metricSlug, placeDisplayByGeoid]) const placeRankByGeoid = useMemo(() => { const m = new Map() const g = placeGeoMerged if (!g) return m const rows: { id: string; value: number }[] = [] for (const f of g.features) { const p = f.properties as Record | null const gid7 = placeGeoid7FromProperties(p, 0) const v = placeDisplayByGeoid[gid7] if (typeof v === 'number' && Number.isFinite(v)) rows.push({ id: gid7, value: v }) } rows.sort((a, b) => compareRankedMetricValues(a.value, b.value, metricSlug)) const total = rows.length rows.forEach((r, i) => m.set(r.id, { rank: i + 1, total })) return m }, [placeGeoMerged, placeDisplayByGeoid, metricSlug]) const stateCountyGeo = useMemo(() => { if (mode !== 'stateCounty' || !stateFips || !countyTopo || !countyPayload?.values) return null return buildStateCountyGeoJson(countyTopo, countyPayload.values, stateFips, metricSlug) }, [mode, stateFips, countyTopo, countyPayload, metricSlug]) const countyDisplayByGeoid = useMemo(() => { const out: Record = {} if (!stateCountyGeo || !metricSlug) return out const prevV = prevVintageInList(vintages, displayVintage) const nat = nationalBaselineWithFallback(manifest?.national_ref, displayVintage, metricSlug, { stateRows: statePayload?.values, stateTrends, }) for (const f of stateCountyGeo.features) { const p = f.properties as Record | null const gid = countyGeoidFromFeature(f as GeoJSON.Feature) const raw = typeof p?.[metricSlug] === 'number' && Number.isFinite(p[metricSlug]) ? p[metricSlug] : null let prev: number | null = null if (valueMode === 'yoy' && prevV && countyTrends?.byGeoid?.[gid]) { prev = trendCell((countyTrends.byGeoid[gid] as Record)[metricSlug], prevV) } out[gid] = displayValueForMode(valueMode, raw, prev, nat) } return out }, [ stateCountyGeo, metricSlug, valueMode, displayVintage, vintages.join(','), countyTrends, manifest?.national_ref, stateTrends, statePayload, ]) const focusPinnedCountyNarrative = useMemo(() => { if ( mode !== 'stateCounty' || leaderboardPinnedId == null || leaderboardPinnedId === '' || !stateCountyGeo?.features?.length ) return null for (const f of stateCountyGeo.features) { const gid = countyGeoidFromFeature(f as GeoJSON.Feature) if (gid !== leaderboardPinnedId) continue const p = (f as GeoJSON.Feature).properties as Record | null const name = String(p?.NAME ?? gid).trim() || gid return { geoid: gid, name } } return { geoid: leaderboardPinnedId, name: leaderboardPinnedId } }, [mode, leaderboardPinnedId, stateCountyGeo]) const narrativePack = useMemo((): CensusNarrativePack => { const label = currentMetricMeta?.label ?? metricSlug const region = mode === 'us' ? 'United States' : stateName && String(stateName).trim() ? String(stateName) : stateFips ? `State ${stateFips}` : 'This area' const geoLevel = mode === 'us' ? 'us_states' : mode === 'stateCounty' ? 'counties' : 'places' const focusFips = stateFips ? normalizeStateFips(stateFips) ?? stateFips : '' const stRow = focusFips ? stateTrends?.by_state?.[focusFips] : undefined const rawSeries = stRow?.[metricSlug] const stateMetricSeries = rawSeries != null && typeof rawSeries === 'object' && !Array.isArray(rawSeries) ? (rawSeries as Record) : undefined const focusState = (mode === 'stateCounty' || mode === 'place') && stateFips ? { stateName: region, stateFips: focusFips, stateMetricSeries } : null return buildCensusNarrativePack({ geoLevel, regionDisplayName: region, metricLabel: label, metricSlug, displayVintage, viz, valueMode, nationalRef: manifest?.national_ref, vintages, focusState, focusPlaceName: mode === 'place' ? placePinnedLabel : null, focusPinnedState: focusPinnedStateNarrative, focusPinnedCounty: focusPinnedCountyNarrative, }) }, [ mode, stateName, stateFips, currentMetricMeta?.label, metricSlug, displayVintage, viz, valueMode, manifest?.national_ref, vintages.join(','), stateTrends, placePinnedLabel, focusPinnedStateNarrative, focusPinnedCountyNarrative, ]) const mapHeadingSelectionActive = leaderboardPinnedId != null && String(leaderboardPinnedId).trim() !== '' const censusStaleDataNote = useMemo(() => { if (valueMode !== 'raw') return null const raw: number[] = [] if (mode === 'us' && statePayload?.values) { for (const row of Object.values(statePayload.values)) { const v = (row as Record)[metricSlug] if (typeof v === 'number' && Number.isFinite(v)) raw.push(v) } } else if (mode === 'stateCounty' && stateCountyGeo?.features?.length) { for (const f of stateCountyGeo.features) { const v = (f.properties as Record | null)?.[metricSlug] if (typeof v === 'number' && Number.isFinite(v)) raw.push(v) } } else if (mode === 'place' && placeGeoMerged?.features?.length) { for (const f of placeGeoMerged.features) { const v = (f.properties as Record | null)?.[metricSlug] if (typeof v === 'number' && Number.isFinite(v)) raw.push(v) } } return censusMetricStaleDataNote(metricSlug, valueMode, raw) }, [mode, valueMode, metricSlug, statePayload, stateCountyGeo, placeGeoMerged]) const countyChoroPooledForLegend = useMemo((): number[] | null => { if (mode !== 'stateCounty' || !countyTrends || !stateFips || !metricSlug || vintages.length < 2) return null const arr = collectAllVintageDisplayValuesCounty( countyTrends, stateFips, vintages, metricSlug, valueMode, manifest?.national_ref, stateTrends, ) return arr.length >= 20 ? arr : null }, [mode, countyTrends, stateFips, vintages.join(','), metricSlug, valueMode, manifest?.national_ref, stateTrends]) const countyChoroExtent = useMemo(() => { if (countyChoroPooledForLegend?.length) return quantileExtent(countyChoroPooledForLegend) const vals = Object.values(countyDisplayByGeoid).filter( (x): x is number => typeof x === 'number' && Number.isFinite(x), ) if (!vals.length) return { min: 0, max: 1 } return quantileExtent(vals) }, [countyChoroPooledForLegend, countyDisplayByGeoid]) const countyBubbleExtent = useMemo(() => { if (!stateCountyGeo || !metricSlug) return { min: 0, max: 1 } const displayVals = Object.values(countyDisplayByGeoid).filter( (x): x is number => typeof x === 'number' && Number.isFinite(x), ) if (displayVals.length >= 2) { return minMaxExtent(displayVals) } const vals = stateCountyGeo.features .map((f) => { const v = (f.properties as Record | null)?.[metricSlug] return typeof v === 'number' && Number.isFinite(v) ? v : null }) .filter((x): x is number => x != null) if (!vals.length) return { min: 0, max: 1 } return minMaxExtent(vals) }, [stateCountyGeo, metricSlug, countyDisplayByGeoid]) const countyRankByGeoid = useMemo(() => { const m = new Map() if (!stateCountyGeo) return m const rows: { id: string; value: number }[] = [] for (const f of stateCountyGeo.features) { const gid = countyGeoidFromFeature(f as GeoJSON.Feature) const v = countyDisplayByGeoid[gid] if (typeof v === 'number' && Number.isFinite(v)) rows.push({ id: gid, value: v }) } rows.sort((a, b) => compareRankedMetricValues(a.value, b.value, metricSlug)) const total = rows.length rows.forEach((r, i) => m.set(r.id, { rank: i + 1, total })) return m }, [stateCountyGeo, countyDisplayByGeoid, metricSlug]) /** Stable ref for react-leaflet GeoJSON `style` — inline functions change every render and trigger Leaflet * `setStyle` on all paths whenever e.g. `countyHover` updates, which clears hover / breaks tooltips. */ const countyFilledGeoJsonStyle = useCallback( (feature: GeoJSON.Feature) => { const gid = countyGeoidFromFeature(feature) const disp = countyDisplayByGeoid[gid] const t = metricToDisplayT(disp, countyChoroExtent.min, countyChoroExtent.max, scale) const isHL = leaderboardPinnedId != null && gid === leaderboardPinnedId return { fillColor: colorFromT(t), // pinned county pops on small screens via a thicker amber stroke + dashed // inner ring (the dashArray reads through any neighbor that overpaints). color: isHL ? '#b45309' : '#334155', weight: isHL ? 4.5 : 0.5, dashArray: isHL ? '0' : undefined, fillOpacity: isHL ? 0.96 : 0.88, } }, [scale, countyChoroExtent.min, countyChoroExtent.max, countyDisplayByGeoid, leaderboardPinnedId], ) const placeFilledGeoJsonStyle = useCallback( (feature: GeoJSON.Feature) => { const p = feature?.properties as Record | undefined const raw = String(p?.GEOID ?? '').replace(/\D/g, '') const gid7 = raw.length <= 7 ? raw.padStart(7, '0') : raw.slice(-7).padStart(7, '0') const disp = placeDisplayByGeoid[gid7] const t = metricToDisplayT(disp, placeChoroExtent.min, placeChoroExtent.max, scale) const isHL = leaderboardPinnedId != null && gid7 === leaderboardPinnedId return { fillColor: colorFromT(t), color: isHL ? '#b45309' : '#334155', weight: isHL ? 4.5 : 0.5, fillOpacity: isHL ? 0.96 : 0.88, } }, [scale, placeChoroExtent.min, placeChoroExtent.max, placeDisplayByGeoid, leaderboardPinnedId], ) const fmt = useCallback( (v: number) => formatMetricValue(metricSlug, v, metrics, valueMode), [metricSlug, metrics, valueMode], ) const formatAxisTick = useCallback( (x: number, span?: number) => formatCensusMapAxisTickForMetric(metricSlug, metrics, x, span, valueMode), [metricSlug, metrics, valueMode], ) const stateRows = useMemo(() => { if (!statePayload) return [] return Object.entries(statePayload.values).map(([st, row]) => { const name = typeof row.NAME === 'string' ? row.NAME : st const disp = stateDisplayById[st] ?? null return { geoid: st, name, value: disp } }) }, [statePayload, metricSlug, stateDisplayById]) const sortedStateRows = useMemo(() => { const arr = [...stateRows] const mul = tableSort.dir === 'asc' ? 1 : -1 arr.sort((a, b) => { if (tableSort.key === 'geoid') return mul * a.geoid.localeCompare(b.geoid) if (tableSort.key === 'name') return mul * a.name.localeCompare(b.name) const av = a.value ?? -Infinity const bv = b.value ?? -Infinity return mul * (av - bv) }) return arr }, [stateRows, tableSort]) const barData = useMemo(() => { const withVal = stateRows.filter((r) => r.value != null) withVal.sort((a, b) => compareRankedMetricValues(a.value!, b.value!, metricSlug)) return withVal.slice(0, CENSUS_TOP_BAR_ROW_LIMIT).map((r) => ({ name: r.name, fullName: r.name, value: r.value, geoid: r.geoid, })) }, [stateRows, metricSlug]) const stateRankByGeoid = useMemo(() => { const m = new Map() const withVal = stateRows.filter((r) => r.value != null) withVal.sort((a, b) => compareRankedMetricValues(a.value!, b.value!, metricSlug)) const total = withVal.length withVal.forEach((r, i) => m.set(r.geoid, { rank: i + 1, total })) return m }, [stateRows, metricSlug]) const onMetricChange = (slug: string) => { if (!manifest) return const list = sliderVintages({ mode, manifest, metricSlug: slug, stateTrends, countyTrends, placeTrends, stateFips, }) const nextV = list.includes(effectiveVintage) ? effectiveVintage : (list[list.length - 1] ?? effectiveVintage) const q = searchParams.toString() if (mode === 'place' && stateFips) { navigate(mapPathPlace(mapPrefix, stateFips, nextV, slug, q)) } else if (mode === 'stateCounty' && stateFips) { navigate(mapPathState(mapPrefix, stateFips, nextV, slug, q)) } else { navigate(mapPathUs(mapPrefix, nextV, slug, q)) } } const onVintageChange = (v: string) => { setPlaying(false) setScrubVintage(v) const q = searchParams.toString() if (vintageNavigateTimerRef.current) window.clearTimeout(vintageNavigateTimerRef.current) vintageNavigateTimerRef.current = window.setTimeout(() => { vintageNavigateTimerRef.current = null startTransition(() => { if (mode === 'place' && stateFips) { navigate(mapPathPlace(mapPrefix, stateFips, v, metricSlug, q), { replace: true }) } else if (mode === 'stateCounty' && stateFips) { navigate(mapPathState(mapPrefix, stateFips, v, metricSlug, q), { replace: true }) } else { navigate(mapPathUs(mapPrefix, v, metricSlug, q), { replace: true }) } }) }, 90) } const stateFill = useCallback( (stateId: string) => { if (!statePayload?.values) return '#e2e8f0' const disp = stateDisplayById[stateId] const t = metricToDisplayT(disp, stateChoroExtent.min, stateChoroExtent.max, scale) return colorFromT(t) }, [statePayload, stateDisplayById, stateChoroExtent.min, stateChoroExtent.max, scale], ) const onStateClick = (stateId: string) => { const fid = normalizeStateFips(stateId) if (!fid) return navigate(mapPathState(mapPrefix, fid, effectiveVintage, metricSlug, searchParams.toString())) } const toggleTableSort = (key: 'name' | 'value' | 'geoid') => { setTableSort((prev) => { if (prev.key === key) return { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } const valueDir = censusMetricRankDirection(metricSlug) === 'lower' ? 'asc' : 'desc' return { key, dir: key === 'value' ? valueDir : 'asc' } }) } if (manifestError) { return (

Census map

Static data is missing. Run{' '} .venv/bin/python scripts/datasources/census/export_census_map_static.py {' '} from the repo root after caching ACS parquets.

) } if (!manifest) { return
Loading census map…
} if (mode === 'us' && !stateTrendsFetched) { return
Loading census map…
} if (mode === 'stateCounty' && stateFips && !countyTrendsFetched) { return
Loading census map…
} if (mode === 'place' && stateFips && !placeTrendsFetched) { return
Loading census map…
} const knownSlugs = new Set(selectableMetrics.map((m) => m.slug)) if (metric && !knownSlugs.has(metric)) { const fallback = selectableMetrics[0]?.slug ?? metrics[0]?.slug ?? 'median_household_income' if (mode === 'place' && stateFips) { return } if (mode === 'stateCounty' && stateFips) { return } return } if (vintage && vintages.length && !vintages.includes(vintage)) { const q = searchParams.toString() if (mode === 'place' && stateFips) { return } if (mode === 'stateCounty' && stateFips) { return } return } const singleVintage = vintages.length <= 1 const mapToolbarDrillNav = (mode === 'place' || mode === 'stateCounty') && stateFips ? (
Back to US map {mode === 'stateCounty' && placeStates.includes(stateFips) ? ( Cities / places ) : null} {mode === 'place' ? ( Counties ) : null}
) : null const nestedInDataExplorer = location.pathname.includes('/data-explorer/map') const wrapExplorerMap = (child: JSX.Element) => nestedInDataExplorer ? (
{child}
) : ( child ) return (
{/* Mobile-only sticky reset chip — on small screens the "Back to US map" link is buried in the filter bar and scrolls off; this anchored pill keeps the way out always reachable. Hidden on >=md where the inline green button is already on-screen. */} {(mode === 'place' || mode === 'stateCounty') && stateFips ? (
US map {stateName ? ( {stateName} {mode === 'place' ? ' · places' : ' · counties'} ) : null}
) : null} {!nestedInDataExplorer ? (

Census explorer

Explore American Community Survey (5-year) estimates on the map. Pick a question and year, then click a state to open county or city views when that data is bundled for download.

) : null} {!censusOnboardingDismissed ? (

👋 Pick what you want to explore above, choose a year, then click any state on the map. Use the ranking panel on the right to jump to a state — bars are numbered #1–#10 for this metric.

) : null} { setAdvancedMapOptionsOpen(false) setAdvancedFocusSection(null) }} focusSection={advancedFocusSection} onConsumedFocusSection={() => setAdvancedFocusSection(null)} metricFullHelp={metricFullHelp} viz={viz} setViz={setViz} scale={scale} setScale={setScale} valueMode={valueMode} setValueMode={setValueMode} yearControls={ mode === 'us' ? { vintages, displayVintage, singleVintage, showPlay, playing, setPlaying, onVintageChange, onBeginPlay: () => setAnimIndex(0), yearHelp: `${CENSUS_MAP_UI_HELP.year}\n\n${metricFullHelp}`, } : undefined } /> {mode === 'us' && wrapExplorerMap(
{ const fips = r.stateCode ? USPS_TO_FIPS2[r.stateCode] ?? null : null setPinnedAddress({ displayName: r.displayName, shortLabel: r.shortLabel, lat: r.lat, lon: r.lon, stateCode: r.stateCode, stateFips: fips, county: r.county, city: r.city, }) }} onClear={() => setPinnedAddress(null)} />
{ setAdvancedFocusSection(section) setAdvancedMapOptionsOpen(true) }} />
{!statePayload ? (
Loading state map… If this hangs, run export (needs state_metrics.json).
) : ( <>
updateUsMapTip(e.clientX, e.clientY)} onMouseLeave={() => { setUsMapTipPos(null) }} > {({ geographies, projection }) => { const usBarPinFips = leaderboardPinnedId != null && leaderboardPinnedId !== '' ? normalizeStateFips(leaderboardPinnedId) ?? leaderboardPinnedId : null return ( <> {geographies.map((geo) => { const sid = normalizeStateFips(geo.id) ?? String(geo.id) const row = statePayload.values[sid] const name = (row as { NAME?: string } | undefined)?.NAME const isBubble = viz === 'bubble' const fill = isBubble ? 'rgba(248,250,252,0.94)' : stateFill(sid) const isPinned = usBarPinFips != null && sid === usBarPinFips const stroke = isPinned ? '#b45309' : isBubble ? '#64748b' : '#94a3b8' const sw = isPinned ? 2.35 : 0.55 return ( { cancelScheduledHoverClear() const disp = stateDisplayById[sid] ?? null setHoverRegion({ id: sid, name: typeof name === 'string' ? name : sid, value: disp, }) updateUsMapTip(e.clientX, e.clientY) }} onMouseLeave={() => scheduleHoverRegionClear()} onClick={() => onStateClick(sid)} /> ) })} {viz === 'bubble' && geographies.map((geo) => { const sid = normalizeStateFips(geo.id) ?? String(geo.id) const num = stateDisplayById[sid] ?? null if (num == null) return null const geom = geo.geometry if (!geom) return null let centroidPt try { centroidPt = geoCentroid({ type: 'Feature', properties: {}, geometry: geom, } as GeoJSON.Feature) } catch { return null } const pair = toLonLatPair(centroidPt) if (!pair) return null const xy = safeProjectScreen(projection, pair) if (!xy) return null const r = bubbleRadiusPx(num, stateBubbleExtent.min, stateBubbleExtent.max, scale, 4, 20) const bt = metricToDisplayT(num, stateBubbleExtent.min, stateBubbleExtent.max, scale) ?? 0 const isPinnedBubble = usBarPinFips != null && sid === usBarPinFips return ( ) })} {pinnedAddress ? (() => { const xy = safeProjectScreen(projection, [pinnedAddress.lon, pinnedAddress.lat]) if (!xy) return null return ( ) })() : null} ) }} {hoverRegion && usMapTipPos ? (
{hoverRegion.name}
{formatMetricValueCompact(metricSlug, hoverRegion.value, metrics, valueMode)} {(() => { const stRank = stateRankByGeoid.get(hoverRegion.id) if (!stRank) return null return ( {' '} · Ranked #{stRank.rank} of {stRank.total} ) })()}
{currentMetricMeta?.label ?? metricSlug}
Click for county-level map
) : null}
)}
{viz === 'filled' && (
)} {viz === 'bubble' && (
)}
U.S. Census Bureau ACS 5-year · state estimates ( state_metrics.json) {stateChoroPooledForLegend != null ? `Color scale range (~4th–96th pct., all years): ${fmt(stateChoroExtent.min)} — ${fmt(stateChoroExtent.max)}` : `Color scale range (~4th–96th pct.): ${fmt(stateChoroExtent.min)} — ${fmt(stateChoroExtent.max)}`}
)} {mode === 'stateCounty' && stateFips && wrapExplorerMap(
setAnimIndex(0)} yearHelp={`${CENSUS_MAP_UI_HELP.year}\n\n${metricFullHelp}`} onOpenAdvanced={(section) => { setAdvancedFocusSection(section) setAdvancedMapOptionsOpen(true) }} />
{!countyPayload && countyPayloadLoading && Loading metrics…} {countyTopoLoading && Loading boundaries…} {countyPayloadError && ( Missing county_metrics.json for this year — run census export with county ACS cached. )} {countyPayload && countyTopo && !stateCountyGeo && ( No county features for this state (check state FIPS). )}
{stateCountyGeo && ( {viz === 'filled' && ( { const p = feature.properties as Record const gid = countyGeoidFromFeature(feature as GeoJSON.Feature) const name = String(p?.NAME ?? gid ?? '') const disp = countyDisplayByGeoid[gid] ?? null const rk = countyRankByGeoid.get(gid) const rankLine = rk ? `
Ranked #${rk.rank} of ${rk.total}` : '' layer.bindPopup( `
${name}
${metricValueTooltipLine( currentMetricMeta?.label ?? metricSlug, metricSlug, disp, metrics, valueMode, )}${rankLine}
`, ) layer.on('mouseover', () => { setCountyHover({ id: gid, name, value: disp }) }) layer.on('mouseout', () => setCountyHover(null)) }} /> )} {viz === 'bubble' && stateCountyGeo.features.map((f, idx) => { const p = f.properties as Record | null const id = countyGeoidFromFeature(f as GeoJSON.Feature) || String(p?.GEOID ?? idx) const num = countyDisplayByGeoid[id] ?? null if (num == null || f.geometry == null) return null const ll = featureLatLng(f) if (!ll) return null // Smaller cap than the legend (4..22) on purpose: state-fit views // jam many counties into a narrow frame, and a 16px-radius disk // covers a whole county at that scale. 2..9 keeps neighbors visible. const r = bubbleRadiusPx(num, countyBubbleExtent.min, countyBubbleExtent.max, scale, 2, 9) const bt = metricToDisplayT(num, countyBubbleExtent.min, countyBubbleExtent.max, scale) ?? 0 const name = String(p?.NAME ?? id) const isHL = leaderboardPinnedId != null && id === leaderboardPinnedId return (
{name}
{formatMetricValueCompact(metricSlug, num, metrics, valueMode)}
{(() => { const rk = countyRankByGeoid.get(id) if (!rk) return null return (
Ranked #{rk.rank} of {rk.total}
) })()}
) })} {viz === 'bubble' && ( )}
)}
)} {mode === 'place' && wrapExplorerMap(
setAnimIndex(0)} yearHelp={`${CENSUS_MAP_UI_HELP.year}\n\n${metricFullHelp}`} onOpenAdvanced={(section) => { setAdvancedFocusSection(section) setAdvancedMapOptionsOpen(true) }} />
{!placeGeo && !placeGeoError && Loading…} {placeGeoError && ( Missing place_{stateFips}.geojson From repo root: cache place ACS, then export —{' '} .venv/bin/python scripts/datasources/census/download_census_acs_data.py --geography place --state {stateFips} --year {effectiveVintage} {' '} then{' '} .venv/bin/python scripts/datasources/census/export_census_map_static.py --year{' '} {effectiveVintage} --place-states {stateFips} )}
{placeGeoMerged && ( {viz === 'filled' && ( { const p = feature.properties as Record const gid7 = placeGeoid7FromProperties(p, 0) const name = String(p?.NAME ?? gid7 ?? '') const disp = placeDisplayByGeoid[gid7] ?? null const rk = placeRankByGeoid.get(gid7) const rankLine = rk ? `
Ranked #${rk.rank} of ${rk.total}` : '' layer.bindPopup( `
${name}
${metricValueTooltipLine( currentMetricMeta?.label ?? metricSlug, metricSlug, disp, metrics, valueMode, )}${rankLine}
`, ) layer.on('mouseover', () => { setPlaceHover({ id: gid7, name, value: disp }) }) layer.on('mouseout', () => setPlaceHover(null)) }} /> )} {viz === 'bubble' && placeGeoMerged.features.map((f, idx) => { const p = f.properties as Record | null const rawId = String(p?.GEOID ?? idx).replace(/\D/g, '') const gid7 = rawId.length <= 7 ? rawId.padStart(7, '0') : rawId.slice(-7).padStart(7, '0') const num = placeDisplayByGeoid[gid7] ?? null if (num == null || f.geometry == null) return null const ll = featureLatLng(f) if (!ll) return null const r = bubbleRadiusPx(num, placeBubbleExtent.min, placeBubbleExtent.max, scale, 4, 22) const bt = metricToDisplayT(num, placeBubbleExtent.min, placeBubbleExtent.max, scale) ?? 0 const name = String(p?.NAME ?? gid7) const isHL = leaderboardPinnedId != null && gid7 === leaderboardPinnedId return (
{name}
{formatMetricValueCompact(metricSlug, num, metrics, valueMode)}
{(() => { const rk = placeRankByGeoid.get(gid7) if (!rk) return null return (
Ranked #{rk.rank} of {rk.total}
) })()}
) })} {viz === 'bubble' && ( )}
)}
)}
) } type PlaceBarChartProps = { className?: string rowsScrollClassName?: string features: GeoJSON.Feature[] metricSlug: string metrics: CensusMetric[] valueMode: CensusValueMode displayByGeoid: Record playing?: boolean topN?: number narrativePack?: CensusNarrativePack | null geoLevel: 'county' | 'place' pinnedRowId?: string | null onTogglePinnedRow?: (id: string) => void /** State silhouette at top of strip while #1 row may be a county or place. */ leaderSilhouetteUsps?: string | null displayVintage: string } function PlaceBarChart(props: PlaceBarChartProps) { const { className = '', rowsScrollClassName, features, metricSlug, metrics, valueMode, displayByGeoid, playing = false, topN = CENSUS_TOP_BAR_ROW_LIMIT, narrativePack, geoLevel, pinnedRowId = null, onTogglePinnedRow, leaderSilhouetteUsps = null, displayVintage, } = props const rows = useMemo(() => { return features .map((f, i) => { const p = f.properties as Record | null const id = geoLevel === 'county' ? countyGeoidFromFeature(f as GeoJSON.Feature) || `idx_${i}` : placeGeoid7FromProperties(p, i) const name = String(p?.NAME ?? p?.GEOID ?? i) const disp = displayByGeoid[id] ?? null return { id, name, fullName: name, value: disp } }) .filter((r) => r.value != null) .sort((a, b) => compareRankedMetricValues(a.value!, b.value!, metricSlug)) .slice(0, topN) .map((r) => ({ id: r.id, label: truncateStateLabel(r.name, 20), fullName: r.fullName, value: r.value!, })) }, [features, metricSlug, topN, geoLevel, displayByGeoid]) const formatAxisTick = useCallback( (x: number, span?: number) => formatCensusMapAxisTickForMetric(metricSlug, metrics, x, span, valueMode), [metricSlug, metrics, valueMode], ) const metricMeta = useMemo(() => metrics.find((m) => m.slug === metricSlug), [metrics, metricSlug]) const winnerRankLabel = metricMeta?.label ?? metricSlug const winnerMetricHelp = useMemo(() => censusMetricFullHelp(metricSlug, metricMeta), [metricSlug, metricMeta]) const dataQualityNote = useMemo(() => { if (valueMode !== 'raw') return null const raw = features.map((f) => { const v = (f.properties as Record | null)?.[metricSlug] return typeof v === 'number' && Number.isFinite(v) ? v : null }) return censusMetricStaleDataNote(metricSlug, valueMode, raw) }, [features, metricSlug, valueMode]) return ( formatMetricValue(metricSlug, v, metrics, valueMode)} formatBarEnd={(v) => formatMetricValueCompact(metricSlug, v, metrics, valueMode)} formatAxisTick={formatAxisTick} playing={playing} leaderSilhouetteUsps={leaderSilhouetteUsps} vintageYear={displayVintage} yearHelp={CENSUS_MAP_UI_HELP.year} winnerCaption={censusMetricWinnerCaption(metricSlug)} winnerRankLabel={winnerRankLabel} winnerMetricHelp={winnerMetricHelp} readingCalloutLines={narrativePack?.barChartCallouts ?? null} dataQualityNote={dataQualityNote} selectedRowId={pinnedRowId} onRowClick={onTogglePinnedRow} /> ) } type PlaceTableProps = { features: GeoJSON.Feature[] geoLevel: 'county' | 'place' displayByGeoid: Record metricSlug: string metrics: CensusMetric[] valueMode: CensusValueMode tableLabel?: string } function PlaceTable(props: PlaceTableProps) { const { features, geoLevel, displayByGeoid, metricSlug, metrics, valueMode, tableLabel = 'All places' } = props const [sort, setSort] = useState<{ key: 'name' | 'value' | 'geoid'; dir: 'asc' | 'desc' }>(() => ({ key: 'value', dir: censusMetricRankDirection(metricSlug) === 'lower' ? 'asc' : 'desc', })) useEffect(() => { setSort((prev) => prev.key === 'value' ? { key: 'value', dir: censusMetricRankDirection(metricSlug) === 'lower' ? 'asc' : 'desc' } : prev, ) }, [metricSlug]) const rows = useMemo(() => { return features.map((f, i) => { const p = f.properties as Record | null const id = geoLevel === 'county' ? countyGeoidFromFeature(f as GeoJSON.Feature) || `idx_${i}` : placeGeoid7FromProperties(p, i) const name = String(p?.NAME ?? id) const disp = displayByGeoid[id] ?? null return { geoid: id, name, value: disp } }) }, [features, metricSlug, displayByGeoid, geoLevel]) const sorted = useMemo(() => { const arr = [...rows] const mul = sort.dir === 'asc' ? 1 : -1 arr.sort((a, b) => { if (sort.key === 'geoid') return mul * a.geoid.localeCompare(b.geoid) if (sort.key === 'name') return mul * a.name.localeCompare(b.name) const av = a.value ?? -Infinity const bv = b.value ?? -Infinity return mul * (av - bv) }) return arr }, [rows, sort]) const toggle = (key: 'name' | 'value' | 'geoid') => { setSort((prev) => { if (prev.key === key) return { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } const valueDir = censusMetricRankDirection(metricSlug) === 'lower' ? 'asc' : 'desc' return { key, dir: key === 'value' ? valueDir : 'asc' } }) } const tableHelp = useMemo( () => `${censusMetricFullHelp(metricSlug, metrics.find((m) => m.slug === metricSlug))}\n\n${CENSUS_MAP_UI_HELP.allGeographiesTable}`, [metricSlug, metrics], ) return (
{sorted.length} rows
{sorted.map((row) => ( ))}
{row.geoid} {row.name} {formatMetricValue(metricSlug, row.value, metrics, valueMode)}
) } export default CensusMapPage