SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
2.9 KB · 70 lines typescript
Raw Blame History
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