spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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 { atlasGeometryIso3 } from '@/lib/iso-numeric';67/**8 * Geometry side of the trial map — pure helpers (no React, no I/O) so the projection can be unit-9 * tested: TopoJSON → GeoJSON features keyed by ISO3, an Equal Earth projection fitted to the SVG10 * viewport, and a `project()` for the city layer. Equal Earth (Šavrič, Patterson & Jenny 2018) is11 * equal-area, so a country's visual weight is not inflated at high latitudes, and its outline is12 * familiar; it is fitted to a 960×480 viewBox and scaled by the browser.13 */1415export const MAP_WIDTH = 960;16export const MAP_HEIGHT = 480;1718export interface CountryFeature {19 /** ISO 3166-1 alpha-3, or null when Natural Earth has no code for the polygon (N. Cyprus, Somaliland). */20 iso3: string | null;21 /** Natural Earth short name (display fallback only). */22 name: string;23 /** SVG path data in viewBox coordinates. */24 d: string;25}2627export interface WorldGeometry {28 countries: CountryFeature[];29 /** Outline of the globe (the projection's sphere). */30 sphere: string;31 project: (lng: number, lat: number) => [number, number] | null;32}3334type CountriesTopology = Topology<{ countries: GeometryCollection<{ name: string }> }>;3536/** Fitted Equal Earth projection for the given viewport (default 960×480, 8 px padding). */37export function fitProjection(collection: FeatureCollection | GeoPermissibleObjects, width = MAP_WIDTH, height = MAP_HEIGHT, pad = 8): GeoProjection {38 return geoEqualEarth().fitExtent(39 [40 [pad, pad],41 [width - pad, height - pad],42 ],43 collection as GeoPermissibleObjects,44 );45}4647/** TopoJSON (world-atlas countries-110m) → projected SVG paths keyed by ISO3. */48export function buildWorldGeometry(topology: CountriesTopology, width = MAP_WIDTH, height = MAP_HEIGHT): WorldGeometry {49 const fc = feature(topology, topology.objects.countries) as FeatureCollection<Geometry, { name: string }>;50 // Fit on the sphere (not on the land bbox) so Antarctica's absence/presence never shifts the frame.51 const projection = fitProjection({ type: 'Sphere' }, width, height);52 // One decimal in a 960-unit viewBox is sub-pixel at any realistic display width and shrinks the53 // path data (shipped twice: HTML + RSC payload) by about 40 % versus the default 3 digits.54 const path = geoPath(projection).digits(1);55 const countries: CountryFeature[] = [];56 for (const f of fc.features as Array<Feature<Geometry, { name: string }>>) {57 const name = f.properties?.name ?? '';58 // Antarctica carries no trial sites and would dominate the lower band of the map.59 if (f.id === '010') continue;60 const d = path(f);61 if (!d) continue;62 countries.push({ iso3: atlasGeometryIso3(f.id as string | number | undefined, name), name, d });63 }64 const sphere = path({ type: 'Sphere' }) ?? '';65 const project = (lng: number, lat: number): [number, number] | null => {66 if (!Number.isFinite(lng) || !Number.isFinite(lat) || Math.abs(lat) > 90 || Math.abs(lng) > 180) return null;67 const p = projection([lng, lat]);68 return p ? [Math.round(p[0] * 10) / 10, Math.round(p[1] * 10) / 10] : null;69 };70 return { countries, sphere, project };71}72