import { geoEqualEarth, geoPath, type GeoPermissibleObjects, type GeoProjection } from 'd3-geo'; import { feature } from 'topojson-client'; import type { Topology, GeometryCollection } from 'topojson-specification'; import type { Feature, FeatureCollection, Geometry } from 'geojson'; import world from 'world-atlas/countries-110m.json'; import { atlasGeometryIso3 } from './iso-numeric'; /** * World geometry for the Choropleth: world-atlas 110m TopoJSON → Equal Earth projected SVG paths keyed by * ISO3. Computed once per server process (module scope); ≈ 90 KB of path data shipped to the client view. * The numeric → alpha-3 lookup prefers the API's `iso_numeric` (registry) and falls back to the static table. */ export const MAP_WIDTH = 960; export const MAP_HEIGHT = 470; export interface CountryPath { iso3: string | null; /** ISO numeric id from the TopoJSON (3 digits) when present. */ numeric: string | null; name: string; d: string; } type CountriesTopology = Topology<{ countries: GeometryCollection<{ name: string }> }>; function fitProjection(width: number, height: number, pad = 4): GeoProjection { return geoEqualEarth().fitExtent( [ [pad, pad], [width - pad, height - pad], ], { type: 'Sphere' } as GeoPermissibleObjects, ); } let cache: { paths: CountryPath[]; sphere: string } | null = null; export function worldPaths(): { paths: CountryPath[]; sphere: string } { if (cache) return cache; const topology = world as unknown as CountriesTopology; const fc = feature(topology, topology.objects.countries) as FeatureCollection; const projection = fitProjection(MAP_WIDTH, MAP_HEIGHT); const path = geoPath(projection).digits(1); const paths: CountryPath[] = []; for (const f of fc.features as Array>) { if (f.id === '010') continue; // Antarctica: no data, dominates the lower band const d = path(f); if (!d) continue; const numeric = f.id != null && f.id !== '' ? String(f.id).padStart(3, '0') : null; paths.push({ iso3: atlasGeometryIso3(f.id as string | number | undefined, f.properties?.name), numeric, name: f.properties?.name ?? '', d }); } const sphere = path({ type: 'Sphere' }) ?? ''; cache = { paths, sphere }; return cache; } /** Re-key a path's ISO3 using the registry's iso_numeric when available (API `/countries` → iso_numeric). */ export function isoLookupFromCountries(countries: Array<{ id: string; iso_numeric?: string | null }>): Map { const m = new Map(); for (const c of countries) if (c.iso_numeric) m.set(String(c.iso_numeric).padStart(3, '0'), c.id); return m; } /** Class index 0..k for a value against sorted quantile breaks (k = breaks.length). */ export function classIndex(value: number, breaks: number[]): number { let i = 0; while (i < breaks.length && value >= breaks[i]!) i++; return i; }