spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import { geoEqualEarth, geoPath, type GeoPermissibleObjects, type GeoProjection } from 'd3-geo';2import { feature } from 'topojson-client';3import type { Topology, GeometryCollection } from 'topojson-specification';4import type { Feature, FeatureCollection, Geometry } from 'geojson';5import world from 'world-atlas/countries-110m.json';6import { atlasGeometryIso3 } from './iso-numeric';78/**9 * World geometry for the Choropleth: world-atlas 110m TopoJSON → Equal Earth projected SVG paths keyed by10 * ISO3. Computed once per server process (module scope); ≈ 90 KB of path data shipped to the client view.11 * The numeric → alpha-3 lookup prefers the API's `iso_numeric` (registry) and falls back to the static table.12 */13export const MAP_WIDTH = 960;14export const MAP_HEIGHT = 470;1516export interface CountryPath {17 iso3: string | null;18 /** ISO numeric id from the TopoJSON (3 digits) when present. */19 numeric: string | null;20 name: string;21 d: string;22}2324type CountriesTopology = Topology<{ countries: GeometryCollection<{ name: string }> }>;2526function fitProjection(width: number, height: number, pad = 4): GeoProjection {27 return geoEqualEarth().fitExtent(28 [29 [pad, pad],30 [width - pad, height - pad],31 ],32 { type: 'Sphere' } as GeoPermissibleObjects,33 );34}3536let cache: { paths: CountryPath[]; sphere: string } | null = null;3738export function worldPaths(): { paths: CountryPath[]; sphere: string } {39 if (cache) return cache;40 const topology = world as unknown as CountriesTopology;41 const fc = feature(topology, topology.objects.countries) as FeatureCollection<Geometry, { name: string }>;42 const projection = fitProjection(MAP_WIDTH, MAP_HEIGHT);43 const path = geoPath(projection).digits(1);44 const paths: CountryPath[] = [];45 for (const f of fc.features as Array<Feature<Geometry, { name: string }>>) {46 if (f.id === '010') continue; // Antarctica: no data, dominates the lower band47 const d = path(f);48 if (!d) continue;49 const numeric = f.id != null && f.id !== '' ? String(f.id).padStart(3, '0') : null;50 paths.push({ iso3: atlasGeometryIso3(f.id as string | number | undefined, f.properties?.name), numeric, name: f.properties?.name ?? '', d });51 }52 const sphere = path({ type: 'Sphere' }) ?? '';53 cache = { paths, sphere };54 return cache;55}5657/** Re-key a path's ISO3 using the registry's iso_numeric when available (API `/countries` → iso_numeric). */58export function isoLookupFromCountries(countries: Array<{ id: string; iso_numeric?: string | null }>): Map<string, string> {59 const m = new Map<string, string>();60 for (const c of countries) if (c.iso_numeric) m.set(String(c.iso_numeric).padStart(3, '0'), c.id);61 return m;62}6364/** Class index 0..k for a value against sorted quantile breaks (k = breaks.length). */65export function classIndex(value: number, breaks: number[]): number {66 let i = 0;67 while (i < breaks.length && value >= breaks[i]!) i++;68 return i;69}70