Regions & SelectionATM
Every region on the map is a named, focusable path, and which ones are picked is one attribute on the map: data-selected="country:FRA country:DEU". A click picks a region, Shift-click adds one, the keyboard walks between neighbors, the State API sets any list - and data-highlight decides how a picked region shows: filled, with a marching outline, pulsing, or lifted off the map with its shadow underneath.
On this page (10)
§Picking
A click picks one region and a second click on it lets it go; Shift, Cmd or Ctrl adds to the selection. data-select="multiple" makes every click a toggle, none turns picking off. Keyboard: Tab to the map's regions, arrows to the nearest region in that direction, Enter or Space to pick, Escape to clear. Every change dispatches geo-map-select with the keys and names.
§Pick countries
data-select='multiple': each click toggles a country. The output under the map is bound to the selection; Clear is a data-geo-action button.
§Highlight styles
data-highlight styles the selection: fill (the default - the theme's primary color), outline (a marching dashed outline over a light tint), pulse (the fill breathes) and lift (the region rises a few pixels with a drop shadow, like a tile lifted off the board). Reduced motion keeps the lift and stops the marching and the pulse.
§Four ways to show a selection
§An animated tour
The selection is state, so a script can walk it: setState('selected', { selected }) moves the highlight, the CSS transition animates the fill from one region to the next. Hovering a region lifts it in every highlight style - the lift is drawn as a raised copy above the map, so it never hides under a neighbor.
§One region after another
Play steps through the Nordic countries one second apart; the highlight is data-highlight='lift'. Hover any country to see the hover lift.
§Styles from data
Any region, by key, can carry its own look over the palette: fill, stroke, strokeWidth, dash and opacity - in the layers JSON or through df$.shadcn.geoMap.setLayers(el, { styles }). Mixed toward the card color, one chart token makes a choropleth; df$.shadcn.geoMap.hatch(el) is a hatch fill for regions without data.
§A choropleth
The shares are illustrative numbers. Each country's fill mixes --chart-1 into the card color by its share; Switzerland has no value and is hatched.
§CSS view file
The stylesheet and the script are the Geo Map's.
@layer components { .geo-map { /* the map's own colors: derived from the theme, literal where the theme has no token (water, routes, cells) */ --geo-map-ocean: color-mix(in oklab, var(--background) 90%, #6c9bab); --geo-map-grid: color-mix(in oklab, var(--foreground) 13%, transparent); --geo-map-border: color-mix(in oklab, var(--foreground) 38%, transparent); --geo-map-route: #c14e2e; --geo-map-cell-fill: color-mix(in oklab, #4e958b 32%, transparent); --geo-map-cell-border: #347871; /* a pick: a coral that reads on every palette (the theme's primary is near-black in the default theme) */ --geo-map-select: oklch(0.6 0.19 25); --geo-map-mix: 100%; /* a tone's drawn lightness = l0 + l * lk, its chroma c * ck: identity in light mode; dark mode maps the pastels into the dark range instead of mixing them toward the background (which greyed them) */ --geo-map-l0: 0; --geo-map-lk: 1; --geo-map-ck: 1; --geo-map-callout-width: 10rem; --geo-map-callout-size: 0.875rem; --geo-map-tone-0: oklch(0.91 0.055 150); --geo-map-tone-1: oklch(0.93 0.06 95); --geo-map-tone-2: oklch(0.89 0.06 60); --geo-map-tone-3: oklch(0.88 0.05 18); --geo-map-tone-4: oklch(0.88 0.045 318); --geo-map-tone-5: oklch(0.88 0.045 260); --geo-map-tone-6: oklch(0.9 0.05 205); --geo-map-tone: var(--geo-map-tone-0); position: relative; isolation: isolate; overflow: hidden; block-size: var(--geo-map-height, 28rem); min-block-size: 16rem; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--card); color: var(--card-foreground); font-family: var(--font-sans); &[data-palette="ocean"] { --geo-map-tone-0: oklch(0.93 0.035 215); --geo-map-tone-1: oklch(0.89 0.05 225); --geo-map-tone-2: oklch(0.85 0.06 235); --geo-map-tone-3: oklch(0.81 0.07 245); --geo-map-tone-4: oklch(0.87 0.05 200); --geo-map-tone-5: oklch(0.83 0.06 255); --geo-map-tone-6: oklch(0.91 0.04 240); } &[data-palette="forest"] { --geo-map-tone-0: oklch(0.93 0.045 135); --geo-map-tone-1: oklch(0.89 0.07 135); --geo-map-tone-2: oklch(0.85 0.075 145); --geo-map-tone-3: oklch(0.81 0.075 158); --geo-map-tone-4: oklch(0.87 0.06 170); --geo-map-tone-5: oklch(0.83 0.065 140); --geo-map-tone-6: oklch(0.91 0.05 125); } &[data-palette="sunset"] { --geo-map-tone-0: oklch(0.92 0.06 82); --geo-map-tone-1: oklch(0.88 0.075 65); --geo-map-tone-2: oklch(0.84 0.085 50); --geo-map-tone-3: oklch(0.8 0.085 32); --geo-map-tone-4: oklch(0.86 0.065 15); --geo-map-tone-5: oklch(0.82 0.07 350); --geo-map-tone-6: oklch(0.9 0.06 40); } &[data-palette="heat"] { --geo-map-tone-0: oklch(0.96 0.055 95); --geo-map-tone-1: oklch(0.92 0.08 85); --geo-map-tone-2: oklch(0.88 0.09 72); --geo-map-tone-3: oklch(0.84 0.095 58); --geo-map-tone-4: oklch(0.8 0.1 45); --geo-map-tone-5: oklch(0.76 0.1 35); --geo-map-tone-6: oklch(0.72 0.1 27); } &[data-palette="mono"] { --geo-map-tone-0: color-mix(in oklab, var(--foreground) 5%, var(--card)); --geo-map-tone-1: color-mix(in oklab, var(--foreground) 9%, var(--card)); --geo-map-tone-2: color-mix(in oklab, var(--foreground) 13%, var(--card)); --geo-map-tone-3: color-mix(in oklab, var(--foreground) 17%, var(--card)); --geo-map-tone-4: color-mix(in oklab, var(--foreground) 7%, var(--card)); --geo-map-tone-5: color-mix(in oklab, var(--foreground) 11%, var(--card)); --geo-map-tone-6: color-mix(in oklab, var(--foreground) 15%, var(--card)); } &[data-palette="chart"] { --geo-map-tone-0: color-mix(in oklab, var(--chart-1) 45%, var(--card)); --geo-map-tone-1: color-mix(in oklab, var(--chart-2) 40%, var(--card)); --geo-map-tone-2: color-mix(in oklab, var(--chart-3) 35%, var(--card)); --geo-map-tone-3: color-mix(in oklab, var(--chart-4) 30%, var(--card)); --geo-map-tone-4: color-mix(in oklab, var(--chart-5) 25%, var(--card)); --geo-map-tone-5: color-mix(in oklab, var(--chart-1) 25%, var(--card)); --geo-map-tone-6: color-mix(in oklab, var(--chart-3) 20%, var(--card)); } &[data-variant="plain"] { border: 0; border-radius: 0; background: transparent; } } .dark .geo-map { --geo-map-ocean: color-mix(in oklab, var(--background) 92%, #5a9fb3); --geo-map-border: color-mix(in oklab, var(--background) 70%, transparent); --geo-map-grid: color-mix(in oklab, var(--foreground) 10%, transparent); --geo-map-route: #ff9c76; --geo-map-cell-fill: color-mix(in oklab, #54bbaf 25%, transparent); --geo-map-cell-border: #8adbd2; --geo-map-mix: 58%; --geo-map-select: oklch(0.72 0.16 28); --geo-map-l0: 0.2; --geo-map-lk: 0.33; --geo-map-ck: 0.9; &:is([data-palette="mono"], [data-palette="chart"]) { --geo-map-l0: 0; --geo-map-lk: 1; --geo-map-ck: 1; } } .geo-map-svg { display: block; inline-size: 100%; block-size: 100%; touch-action: none; cursor: grab; user-select: none; &:active { cursor: grabbing; } &:focus-visible { outline: 2px solid var(--ring); outline-offset: -3px; } } .geo-map-sphere path { fill: var(--geo-map-ocean); stroke: var(--geo-map-grid); } .geo-map-graticule path { fill: none; stroke: var(--geo-map-grid); stroke-width: 0.6; pointer-events: none; } /* a region's fill (and its lifted copy's): its tone, light as written; dark through the lightness map above. The context, the pick and the cells set their own fill on a more specific selector */ .geo-map-region, .geo-map-lifted { fill: color-mix(in oklab, var(--geo-map-tone) var(--geo-map-mix), var(--background)); @supports (color: oklch(from red l c h)) { fill: oklch( from var(--geo-map-tone) calc(var(--geo-map-l0) + l * var(--geo-map-lk)) calc(c * var(--geo-map-ck)) h ); } &[data-tone="0"] { --geo-map-tone: var(--geo-map-tone-0); } &[data-tone="1"] { --geo-map-tone: var(--geo-map-tone-1); } &[data-tone="2"] { --geo-map-tone: var(--geo-map-tone-2); } &[data-tone="3"] { --geo-map-tone: var(--geo-map-tone-3); } &[data-tone="4"] { --geo-map-tone: var(--geo-map-tone-4); } &[data-tone="5"] { --geo-map-tone: var(--geo-map-tone-5); } &[data-tone="6"] { --geo-map-tone: var(--geo-map-tone-6); } } .geo-map-region { stroke: var(--geo-map-border); stroke-width: 0.65; stroke-linejoin: round; vector-effect: non-scaling-stroke; transition: fill 180ms ease, opacity 180ms ease, stroke 180ms ease; outline: none; &:focus-visible { stroke: var(--ring); stroke-width: 2.5; } } .geo-map-regions .geo-map-region:hover, .geo-map-cells .geo-map-region:hover { stroke: var(--foreground); stroke-width: 1.2; } /* around a focus: the rest of the world, dimmed and inert */ .geo-map-context { pointer-events: none; & .geo-map-region { fill: color-mix(in oklab, var(--muted) 70%, var(--geo-map-ocean)); opacity: 0.55; } } .geo-map[data-border="none"] :is(.geo-map-regions, .geo-map-context) .geo-map-region { stroke: none; } .geo-map[data-border="dashed"] :is(.geo-map-regions, .geo-map-context) .geo-map-region { stroke-dasharray: 3 2; } .geo-map[data-border="cased"] :is(.geo-map-regions, .geo-map-context) .geo-map-region { stroke: var(--background); stroke-width: 2; paint-order: stroke fill; } /* the selection: fill (default), outline, pulse, lift */ .geo-map-regions .geo-map-region[aria-pressed="true"], .geo-map-cells .geo-map-region[aria-pressed="true"] { fill: color-mix(in oklab, var(--geo-map-select) 62%, var(--card)); stroke: var(--geo-map-select); stroke-width: 1.4; } .geo-map[data-highlight="outline"] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"] { fill: color-mix(in oklab, var(--geo-map-select) 14%, transparent); stroke: var(--geo-map-select); stroke-width: 2.4; stroke-dasharray: 6 4; animation: geo-map-march 900ms linear infinite; } .geo-map[data-highlight="pulse"] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"] { animation: geo-map-pulse 1.6s ease-in-out infinite; } .geo-map[data-highlight="lift"] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"] { fill: color-mix(in oklab, var(--foreground) 22%, var(--geo-map-ocean)); stroke: none; } @keyframes geo-map-march { to { stroke-dashoffset: -20; } } @keyframes geo-map-pulse { 50% { fill: color-mix(in oklab, var(--geo-map-select) 28%, var(--card)); stroke-width: 3; } } /* hover (and data-highlight="lift"): a copy of the region raised a little, its shadow on the map below */ .geo-map-lift { pointer-events: none; } .geo-map-lifted { stroke: var(--foreground); stroke-width: 1; stroke-linejoin: round; vector-effect: non-scaling-stroke; translate: 0 -3px; filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.28)); transition: translate 160ms ease-out, filter 160ms ease-out; @starting-style { translate: 0 0; filter: drop-shadow(0 0 0 rgb(0 0 0 / 0)); } &[aria-pressed="true"] { fill: color-mix(in oklab, var(--geo-map-select) 62%, var(--card)); stroke: var(--geo-map-select); } &.geo-map-cell { fill: var(--geo-map-cell-fill); stroke: var(--geo-map-cell-border); } } .geo-map[data-highlight="lift"] .geo-map-lifted[aria-pressed="true"] { translate: 0 -5px; filter: drop-shadow(0 7px 5px rgb(0 0 0 / 0.32)); } .geo-map-cells .geo-map-region { fill: var(--geo-map-cell-fill); stroke: var(--geo-map-cell-border); stroke-width: 1.2; } .geo-map-lines { pointer-events: none; } .geo-map-line { fill: none; stroke: var(--geo-map-route); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; } .geo-map-markers { pointer-events: none; } .geo-map-marker { & > :not(text) { fill: var(--geo-map-route); stroke: var(--background); stroke-width: 2; } & text { font: 500 11px var(--font-sans); fill: var(--foreground); stroke: var(--background); stroke-width: 3px; paint-order: stroke; stroke-linejoin: round; } } .geo-map-hatch { fill: none; stroke: var(--muted-foreground); stroke-width: 1; } /* callouts: a leader line from each region to its name outside the map, the name written on the line */ .geo-map-callout { cursor: pointer; } .geo-map-callout-line { fill: none; stroke: color-mix(in oklab, var(--foreground) 45%, transparent); stroke-width: 0.8; vector-effect: non-scaling-stroke; } .geo-map-callout-dot { fill: var(--foreground); } /* the script spaces the rows and shortens the names by this size (--geo-map-callout-size) */ .geo-map-callout-label { font: 500 var(--geo-map-callout-size) var(--font-sans); fill: var(--foreground); } .geo-map-callout:is([data-hover], [data-selected]) { & .geo-map-callout-line { stroke: var(--geo-map-select); stroke-width: 1.6; } & .geo-map-callout-dot { fill: var(--geo-map-select); } & .geo-map-callout-label { fill: var(--geo-map-select); font-weight: 600; } } .geo-map-tooltip { position: absolute; z-index: 3; max-inline-size: 14rem; padding: 0.35rem 0.6rem; border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--popover-foreground); background: var(--popover); box-shadow: var(--shadow-md); pointer-events: none; font-size: 0.75rem; white-space: nowrap; &[hidden] { display: none; } } .geo-map-error { position: absolute; inset: auto 1rem 1rem; padding: 0.5rem 0.75rem; border: 1px solid var(--destructive); border-radius: var(--radius-md); color: var(--destructive); background: var(--card); font-size: 0.8125rem; } .geo-map[data-geo-status="loading"] .geo-map-svg { animation: geo-map-loading 1.2s ease-in-out infinite alternate; } @keyframes geo-map-loading { to { opacity: 0.55; } } @media (prefers-reduced-motion: reduce) { .geo-map-region, .geo-map-lifted { transition: none; } .geo-map[data-highlight] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"], .geo-map[data-geo-status="loading"] .geo-map-svg { animation: none; } } @media (prefers-contrast: more) { .geo-map { --geo-map-border: var(--foreground); } .geo-map-region { stroke-width: 1; } .geo-map-callout-line { stroke: var(--foreground); } } @media (forced-colors: active) { .geo-map-sphere path { fill: Canvas; stroke: CanvasText; } .geo-map-region, .geo-map-lifted { fill: Canvas; stroke: CanvasText; } .geo-map-regions .geo-map-region[aria-pressed="true"], .geo-map-lifted[aria-pressed="true"] { fill: Highlight; stroke: CanvasText; } .geo-map-line, .geo-map-callout-line { stroke: CanvasText; } .geo-map-callout-label, .geo-map-marker text { fill: CanvasText; } }}§JS view file
// -- Geo Map (geo-map) -------------------------------------------------------// Vector geography in SVG: countries, continents and first-order administrative// regions (Natural Earth v5.1.2) on five projections, plus the library the map// is made of - WGS84 geodesics, S2 cells, a Morton spatial index and weighted// routing over application-supplied segments (df$.shadcn.geo).//// - Lazy: the official engines arrive on the first map or the first// df$.shadcn.geo.load() - d3-geo (projections, clipping, paths; with its// d3-array helper), GeographicLib (ellipsoidal geodesics) and defuss-dson// (the DGEO metadata), each from its pinned jsDelivr build (GEO_VENDOR_URLS,// or configure({ urls }) to self-host). S2, Morton, routing and the DGEO// decoder are this file's own code: a vendor would cost more than it buys -// VERIFIED: s2js, the reference tests/e2e/site/geo-lib.e2e.ts compares this// S2 against, bundles to 168,690 bytes minified; Morton and Dijkstra are a// few dozen lines each.// - Data: a manifest of DGEO chunks (data/ beside this file - the world// overview stays resident, a country's detail and its states load when they// come into view, the original 10m states from 24x). data-src, a// <meta name="geo-map-data">, configure({ data }) or the release on jsDelivr.// - Regions are state: the selection (one or several, picked by click or set by// setState) and the focus (a region framed in view, its children shown -// Europe shows its countries, Germany its states) are attributes on the// element, so CSS animates them and render() reproduces them.// - Callouts: a leader line from every region in view to its name outside the// map (data-callouts).//// State lives ON the element (data-* + the store); the bound `api` is the only// state mutator (AGENTS.md "State API").import { defussGlobals, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';const df$ = defussGlobals();const dfDollar = defussQuery();// VERIFIED: (verify's component types ratchet - tsc -p tsconfig.components.json) every type this file's API docs// state - arguments, return values, event details - holds against its code.// 'default' = nothing picked, 'selected' = one or more regions picked, 'focused' = a region framed with its childrenconst geoMapStates = ['default', 'selected', 'focused'];// -- types ----------------------------------------------------------------------/** A position in WGS84 degrees, longitude first (GeoJSON order): [longitude, latitude]. */export type LonLat = [number, number];/** A rectangle in degrees: [west, south, east, north]; west > east crosses the antimeridian. */export type GeoBounds = [number, number, number, number];/** A boundary level: continents, countries (and territories), first-order administrative regions. */export type GeoLevel = 'continent' | 'country' | 'state';/** A map projection. */export type GeoProjectionName = 'equalEarth' | 'equalArea' | 'mercator' | 'equirectangular' | 'orthographic';/** A fill palette for the regions. */export type GeoPalette = 'atlas' | 'ocean' | 'forest' | 'sunset' | 'heat' | 'mono' | 'chart';/** How region borders are drawn. */export type GeoBorder = 'natural' | 'cased' | 'dashed' | 'none';/** What a region is: its identity and place in the hierarchy. */export interface GeoRegionProperties { /** the id within its level: a continent's name, a country's ADM0_A3 code, a state's adm1_code */ id: string; /** the display name (English) */ name: string; /** its level */ level: GeoLevel; /** a state's country (ADM0_A3) */ parent?: string; /** a country's continent */ continent?: string;}/** A region's outline: a polygon or several, rings in D3 winding (the smaller spherical area is the inside). */export interface GeoGeometry { /** 'Polygon' or 'MultiPolygon' */ type: 'Polygon' | 'MultiPolygon'; /** rings of [longitude, latitude] positions (a MultiPolygon: one list of rings per polygon) */ coordinates: LonLat[][] | LonLat[][][];}/** A region as a GeoJSON feature. */export interface GeoRegion { /** always 'Feature' */ type: 'Feature'; /** identity, name, level and parent */ properties: GeoRegionProperties; /** the outline */ geometry: GeoGeometry;}/** A reference ellipsoid: equatorial radius and flattening. */export interface GeoEllipsoid { /** the equatorial radius in metres */ a: number; /** the flattening (0 = a sphere) */ f: number;}/** The inverse geodesic problem solved: how far apart two positions are and in which direction. */export interface GeoInverse { /** the surface distance along the geodesic, in metres */ meters: number; /** the bearing leaving the first position, degrees clockwise from north */ initialBearing: number; /** the bearing arriving at the second position */ finalBearing: number;}/** An S2 cell: its 64-bit id, its hexadecimal token and its level. */export interface S2Cell { /** the exact cell id (never a Number - 64 bits do not fit) */ id: bigint; /** the canonical lowercase hex token (trailing zeros dropped) */ token: string; /** 0 (a cube face) to 30 (about 1 cm²) */ level: number;}/** A node of a route graph. */export interface GeoRouteNode { /** a unique id */ id: string; /** where it is */ position: LonLat; /** a display name */ name?: string;}/** A segment between two nodes, with the length the application measured. */export interface GeoRouteSegment { /** a unique id */ id: string; /** the start node's id */ from: string; /** the end node's id */ to: string; /** the network length in metres - authoritative, independent of the drawn geometry */ lengthMeters: number; /** usable in both directions (default: only from the from node to the to node) */ bidirectional?: boolean; /** the drawn path; its first and last positions equal the nodes' (default: the straight pair) */ coordinates?: LonLat[];}/** A position inside a segment, as a fraction of its length. */export interface GeoSegmentPosition { /** the segment's id */ segment: string; /** 0 (its from node) to 1 (its to node) */ fraction: number;}/** One segment a route travels. */export interface GeoRouteStep { /** the segment's id */ segment: string; /** the node it leaves */ from: string; /** the node it reaches */ to: string; /** travelled against its direction */ reversed: boolean; /** the metres it contributes */ lengthMeters: number; /** its path, in travel order */ coordinates: LonLat[];}/** A shortest route. */export interface GeoRoute { /** the summed segment lengths in metres */ lengthMeters: number; /** the nodes passed, in order */ nodeIds: string[]; /** the segments travelled */ steps: GeoRouteStep[]; /** the whole path */ coordinates: LonLat[];}/** The nearest node to a position. */export interface GeoNearestNode { /** the node */ node: GeoRouteNode; /** its WGS84 distance from the position, in metres */ meters: number;}/** A weighted directed graph: Dijkstra over the supplied lengths, a Morton index over the node positions. */export interface GeoRouteGraph { /** the shortest route between two nodes, or null when none connects them */ route: (from: string, to: string) => GeoRoute | null; /** the shortest route between two positions inside segments (each cut proportionally) */ routeAt: (from: GeoSegmentPosition, to: GeoSegmentPosition) => GeoRoute | null; /** the node closest to a position (WGS84), optionally among the nodes in a rectangle */ nearestNode: (position: LonLat, bounds?: GeoBounds) => GeoNearestNode | null; /** the nodes inside a rectangle (Morton prefix ranges, then the exact test) */ nodesInBounds: (bounds: GeoBounds) => GeoRouteNode[]; /** the node ids in Morton (Z) order */ mortonOrder: () => string[];}/** A Morton (Z-order) index over positions. */export interface GeoMortonIndex { /** how many items it holds */ size: number; /** the items in Morton order */ ordered: () => unknown[]; /** the items inside a rectangle */ query: (bounds: GeoBounds) => unknown[];}/** A quantized TopoJSON topology (what a DGEO chunk decodes to). */export interface GeoTopology { /** always 'Topology' */ type: 'Topology'; /** the named geometry collections */ objects: Record<string, unknown>; /** the delta-encoded arcs */ arcs: number[][][]; /** the quantization transform */ transform?: { scale: [number, number]; translate: [number, number] };}/** A marker on the map. */export interface GeoMarker { /** a unique id */ id: string; /** a label beside it (and its accessible name) */ label?: string; /** where: [longitude, latitude], or an S2 cell's center */ position: LonLat | { s2: string }; /** a CSS color (default: the route color) */ color?: string; /** circle (default), diamond, square or pin */ shape?: 'circle' | 'diamond' | 'square' | 'pin';}/** A line on the map: given positions, or the geodesic between two. */export interface GeoLine { /** a unique id */ id: string; /** the positions it passes */ coordinates?: LonLat[]; /** with `to` instead of coordinates: the WGS84 geodesic from here */ from?: LonLat; /** the geodesic's end */ to?: LonLat; /** a CSS color */ color?: string; /** the stroke width in px */ width?: number; /** an SVG dash array ("4 3") */ dash?: string;}/** A region's or cell's own look, over its palette fill. */export interface GeoTileStyle { /** a CSS fill (a color, a token, the hatch url) */ fill?: string; /** a CSS stroke */ stroke?: string; /** the stroke width in px */ strokeWidth?: number; /** an SVG dash array */ dash?: string; /** 0 to 1 */ opacity?: number;}/** What a map draws over its boundaries (the script.geo-map-layers JSON has the same shape). */export interface GeoLayers { /** markers */ markers?: GeoMarker[]; /** lines */ lines?: GeoLine[]; /** S2 cells, by token */ cells?: string[]; /** per-region looks, by key (country:DEU, state:DEU-1591, continent:Europe, s2:47a1) */ styles?: Record<string, GeoTileStyle>;}/** A map's state: every field optional in setState() (merged into the current one); getState() reports all. */export interface GeoMapConfig { /** the projection (default equalEarth) */ projection?: GeoProjectionName; /** the boundary level, or 'auto' by zoom: continents below 2x, countries below 8x, states above (default country) */ level?: GeoLevel | 'auto'; /** the fill palette (default atlas) */ palette?: GeoPalette; /** the border style (default natural) */ border?: GeoBorder; /** the latitude / longitude grid (default true) */ graticule?: boolean; /** leader lines from the regions in view to their names outside the map (default false) */ callouts?: boolean; /** the picked regions' keys (level:id - country:FRA); a string may list them separated by spaces or commas */ selected?: string[] | string; /** the framed key (a region, or an S2 cell as s2:<token>), or null; its children are shown, the rest dimmed */ focus?: string | null; /** the view's center */ center?: LonLat; /** the zoom factor, 1 to 1024 */ zoom?: number;}/** setState() configs per state. */export interface GeoMapStateConfigs { /** Nothing picked, nothing framed - the selection and the focus are cleared. */ default: GeoMapConfig; /** One or more regions picked (`config.selected`): highlighted the way data-highlight says. */ selected: GeoMapConfig; /** A region framed in view (`config.focus`) with its children shown - a continent's countries, a country's states. */ focused: GeoMapConfig;}/** One search result. */export interface GeoSearchHit { /** the region's key (country:DEU) */ key: string; /** its name */ name: string; /** its level */ level: GeoLevel; /** what a list shows: the name, a state's country after a comma */ label: string;}/** A region drawn in the current view. */export interface GeoVisibleRegion { /** its key */ key: string; /** its name */ name: string; /** its level */ level: GeoLevel; /** its label point in SVG pixels */ anchor: [number, number] | null;}/** What the last render drew and loaded. */export interface GeoRenderStats { /** regions drawn */ visibleRegions: number; /** context regions drawn dimmed around a focus */ contextRegions: number; /** S2 cells drawn */ visibleCells: number; /** markers drawn */ visibleMarkers: number; /** callouts drawn */ callouts: number; /** milliseconds the render took */ renderMs: number; /** detail chunks in the cache */ cachedChunks: number; /** encoded bytes of those chunks */ loadedBytes: number; /** chunks still loading */ loading: number;}/** The `geo-map-select` event's detail: the picked regions. */export interface GeoMapSelectDetail { /** their keys */ keys: string[]; /** their names (a key whose region is not loaded names itself) */ names: string[];}/** The `geo-map-focus` event's detail: the framed region. */export interface GeoMapFocusDetail { /** its key, or null when the focus was cleared */ key: string | null; /** its name, or null */ name: string | null;}/** The `geo-map-view` event's detail: where the camera came to rest. */export interface GeoMapViewDetail { /** the center */ center: LonLat; /** the zoom factor */ zoom: number; /** the level drawn */ level: GeoLevel;}/** The `geo-map-error` event's detail: what could not be loaded. */export interface GeoMapErrorDetail { /** the message */ message: string; /** the URL that failed, or '' */ url: string;}/** The engines' URLs (the pinned official builds). */export interface GeoVendorUrls { /** d3-array (UMD; d3-geo's helpers) */ d3Array: string; /** d3-geo (UMD) */ d3Geo: string; /** GeographicLib geodesic (UMD) */ geodesic: string; /** defuss-dson (ESM) */ dson: string;}/** What configure() sets for every map and the library. */export interface GeoConfigureOptions { /** the default data manifest URL (a map's data-src wins) */ data?: string; /** engine URLs to load instead of the pinned jsDelivr builds (the same versions, self-hosted) */ urls?: Partial<GeoVendorUrls>; /** the bounds a focus frames, by region key, over the built-in ones (Europe, North America, Oceania) */ views?: Record<string, GeoBounds>;}// -- engines -------------------------------------------------------------------------------/** The pinned official builds the component loads (never @latest). */export const GEO_VENDOR_URLS: GeoVendorUrls = { d3Array: 'https://cdn.jsdelivr.net/npm/d3-array@3.2.4/dist/d3-array.min.js', d3Geo: 'https://cdn.jsdelivr.net/npm/d3-geo@3.1.1/dist/d3-geo.min.js', geodesic: 'https://cdn.jsdelivr.net/npm/geographiclib-geodesic@2.2.0/geographiclib-geodesic.min.js', dson: 'https://cdn.jsdelivr.net/npm/defuss-dson@1.1.2/dist/index.mjs',};/** * the data the release ships: its npm package on jsDelivr (pinned to the release core reports). Not the GitHub route * the docs load the bundles from - VERIFIED (curl, 2026-11-02): the repository is past jsDelivr's 50 MB limit for * /gh/, which answers a file's first fetch with 403 "Package size exceeded" now and then (3 of 31 fresh chunks), and * a map fetches hundreds of them; /npm/ (72 MB unpacked, within its limit) served 41 of 41 */const releaseData = (): string => `https://cdn.jsdelivr.net/npm/defuss-shadcn@${df$.shared?.abi ?? '0.0.0'}/dist/components/geo-map/data/manifest.json`;interface Engines { d3: any; geodesic: any; DSON: { parse(text: string): unknown; stringify(value: unknown): string };}let engines: Engines | null = null;let enginesPromise: Promise<Engines> | null = null;const settings: { data: string; urls: GeoVendorUrls; views: Record<string, GeoBounds> } = { data: '', urls: { ...GEO_VENDOR_URLS }, views: {},};const LOAD_MS = 20_000;const metaContent = (name: string): string => ((dfDollar(`meta[name="${name}"]`).get(0) ?? null) as HTMLMetaElement | null)?.content ?? '';/** the one vendor import site (verify: VENDOR_IMPORTS) - the official engines, never our code */function vendor(vendorUrl: string): Promise<any> { // a microtask later: a failing import is a rejection, never a synchronous throw out of the caller return Promise.resolve().then(() => import(/* @vite-ignore */ vendorUrl));}/** * Load the engines once: d3-geo with d3-array, GeographicLib and defuss-dson (the pinned builds, or the configured * URLs). Every map calls it; call it yourself before using the geodesic functions. A failed load can be retried. * @returns resolves when every engine is ready */function load(): Promise<void> { if (engines) return Promise.resolve(); if (!enginesPromise) { const urls = settings.urls; const present = (globalThis as any).d3; // a page that already has d3 (v7 bundles d3-geo) keeps it - the UMD builds install on that global const d3 = present?.geoPath && present?.geoEqualEarth ? Promise.resolve() : vendor(urls.d3Array).then(() => vendor(urls.d3Geo)); const pending = Promise.all([d3, vendor(urls.geodesic), vendor(urls.dson)]).then(([, , dson]) => { const g = globalThis as any; if (!g.d3?.geoPath) throw new Error('d3-geo did not install globalThis.d3'); if (!g.geodesic?.Geodesic) throw new Error('GeographicLib did not install globalThis.geodesic'); const DSON = dson?.DSON ?? dson?.default?.DSON; if (!DSON) throw new Error('defuss-dson exports no DSON'); engines = { d3: g.d3, geodesic: g.geodesic, DSON }; return engines; }); pending.catch(() => { if (enginesPromise === pending) enginesPromise = null; }); enginesPromise = pending; } let timer = 0; const deadline = new Promise<never>((_, reject) => { timer = setTimeout( () => reject(new Error(`the geography engines did not load within ${LOAD_MS / 1000} s`)), LOAD_MS, ) as unknown as number; }); return Promise.race([enginesPromise, deadline]) .then(() => undefined) .finally(() => clearTimeout(timer));}const ready = (): Engines => { if (!engines) throw new Error('the geography engines are not loaded - await df$.shadcn.geo.load() first'); return engines;};/** * Set the data URL, the engine URLs or the focus views for every map on the page (maps already showing keep their * data). Engine URLs only take effect before the engines first load. * @param options - `{ data, urls, views }` */function configure(options: GeoConfigureOptions): void { if (options.data !== undefined) settings.data = String(options.data); if (options.urls) settings.urls = { ...settings.urls, ...options.urls }; if (options.views) settings.views = { ...settings.views, ...options.views };}// -- validation ----------------------------------------------------------------------------function assertLonLat(point: unknown): asserts point is LonLat { const p = point as number[]; if (!Array.isArray(p) || p.length !== 2 || !p.every(Number.isFinite) || Math.abs(p[0]!) > 180 || Math.abs(p[1]!) > 90) throw new RangeError('expected [longitude, latitude] in [-180, 180] x [-90, 90] degrees');}function assertBounds(b: GeoBounds): void { if (!Array.isArray(b) || b.length !== 4) throw new RangeError('expected [west, south, east, north]'); assertLonLat([b[0], b[1]]); assertLonLat([b[2], b[3]]); if (b[1] > b[3]) throw new RangeError('south must not exceed north');}/** * Whether a position lies inside a rectangle (one whose west exceeds its east crosses the antimeridian). * @param point - [longitude, latitude] * @param bounds - [west, south, east, north] * @returns true inside or on the edge */function inBounds(point: LonLat, bounds: GeoBounds): boolean { return ( point[1] >= bounds[1] && point[1] <= bounds[3] && (bounds[0] <= bounds[2] ? point[0] >= bounds[0] && point[0] <= bounds[2] : point[0] >= bounds[0] || point[0] <= bounds[2]) );}const wrapLon = (lon: number): number => ((((lon + 180) % 360) + 360) % 360) - 180;const clampZoom = (z: number): number => Math.max(1, Math.min(1024, z));const clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));// -- geodesy (GeographicLib) ------------------------------------------------------------------/** WGS84: the GPS reference ellipsoid. */const WGS84: GeoEllipsoid = Object.freeze({ a: 6378137, f: 1 / 298.257223563 });const solvers = new Map<string, any>();function solver(ellipsoid: GeoEllipsoid = WGS84): any { const key = `${ellipsoid.a}/${ellipsoid.f}`; let s = solvers.get(key); if (!s) { if ( !Number.isFinite(ellipsoid.a) || ellipsoid.a <= 0 || !Number.isFinite(ellipsoid.f) || ellipsoid.f < 0 || ellipsoid.f >= 1 / 50 ) throw new RangeError('an ellipsoid needs a > 0 and 0 <= f < 1/50'); s = new (ready().geodesic.Geodesic.Geodesic)(ellipsoid.a, ellipsoid.f); solvers.set(key, s); } return s;}/** * The distance and bearings between two positions along the ellipsoid's surface (Karney's algorithm - nearly * antipodal points included). Excludes altitude, terrain and road detours. * @param a - [longitude, latitude] of the start * @param b - [longitude, latitude] of the end * @param ellipsoid - the reference ellipsoid (default WGS84) * @returns metres and the bearings at both ends */function inverse(a: LonLat, b: LonLat, ellipsoid?: GeoEllipsoid): GeoInverse { assertLonLat(a); assertLonLat(b); const r = solver(ellipsoid).Inverse(a[1], a[0], b[1], b[0]); return { meters: r.s12, initialBearing: r.azi1, finalBearing: r.azi2 };}/** * Where a geodesic leads: the position a given distance away from a start in a given bearing. * @param origin - [longitude, latitude] of the start * @param bearing - degrees clockwise from north * @param meters - the distance along the surface * @param ellipsoid - the reference ellipsoid (default WGS84) * @returns the end position */function direct(origin: LonLat, bearing: number, meters: number, ellipsoid?: GeoEllipsoid): LonLat { assertLonLat(origin); if (!Number.isFinite(bearing) || !Number.isFinite(meters)) throw new RangeError('a finite bearing and distance'); const p = solver(ellipsoid).Direct(origin[1], origin[0], bearing, meters); return [p.lon2, p.lat2];}/** * The geodesic between two positions as positions at most `maxStepMeters` apart - a line the map draws as the * shortest surface path. * @param a - the start * @param b - the end * @param maxStepMeters - the largest spacing between samples (default 20000) * @param ellipsoid - the reference ellipsoid (default WGS84) * @returns the positions, a first and b last */function geodesicPath(a: LonLat, b: LonLat, maxStepMeters: number = 20_000, ellipsoid?: GeoEllipsoid): LonLat[] { if (!Number.isFinite(maxStepMeters) || maxStepMeters <= 0) throw new RangeError('maxStepMeters must be positive'); const { meters } = inverse(a, b, ellipsoid); const n = Math.max(1, Math.ceil(meters / maxStepMeters)); if (n > 100_000) throw new RangeError('too many geodesic samples (at most 100000 segments)'); const line = solver(ellipsoid).InverseLine(a[1], a[0], b[1], b[0]); const points: LonLat[] = [[a[0], a[1]]]; for (let i = 1; i < n; i++) { const p = line.Position((meters * i) / n); points.push([p.lon2, p.lat2]); } points.push([b[0], b[1]]); return points;}/** * The length of a polyline along the ellipsoid: the sum of its segments' geodesic distances. * @param points - two or more positions * @param ellipsoid - the reference ellipsoid (default WGS84) * @returns metres */function polylineLength(points: LonLat[], ellipsoid?: GeoEllipsoid): number { let sum = 0; for (let i = 1; i < points.length; i++) sum += inverse(points[i - 1]!, points[i]!, ellipsoid).meters; return sum;}// -- S2 (cube faces, Hilbert curve, 64-bit ids) ----------------------------------------------// VERIFIED: (tests/e2e/geography/geo-lib.e2e.ts) tokens, levels, centers, vertices, edge neighbors, parents and// children equal s2js 1.44.0 (a port of Google's S2 library) on random positions and levels.const S2_MAX = 2 ** 30;const SWAP = 1;const INVERT = 2;const POS_TO_IJ = [ [0, 1, 3, 2], [0, 2, 3, 1], [3, 2, 0, 1], [3, 1, 0, 2],];const POS_TO_ORIENTATION = [SWAP, 0, 0, INVERT | SWAP];const LOOKUP_POS = new Int32Array(1024);const LOOKUP_IJ = new Int32Array(1024);function initLookup(level: number, i: number, j: number, orig: number, pos: number, orientation: number): void { if (level === 4) { const ij = (i << 4) + j; LOOKUP_POS[(ij << 2) + orig] = (pos << 2) + orientation; LOOKUP_IJ[(pos << 2) + orig] = (ij << 2) + orientation; return; } const r = POS_TO_IJ[orientation]!; for (let k = 0; k < 4; k++) initLookup( level + 1, (i << 1) + (r[k]! >> 1), (j << 1) + (r[k]! & 1), orig, (pos << 2) + k, orientation ^ POS_TO_ORIENTATION[k]!, );}for (const o of [0, SWAP, INVERT, SWAP | INVERT]) initLookup(0, 0, 0, o, 0, o);const RAD = 180 / Math.PI;function lonLatToXyz(p: LonLat): [number, number, number] { const phi = p[1] / RAD; const theta = p[0] / RAD; const c = Math.cos(phi); return [Math.cos(theta) * c, Math.sin(theta) * c, Math.sin(phi)];}function xyzToLonLat(x: number, y: number, z: number): LonLat { return [Math.atan2(y, x) * RAD, Math.atan2(z, Math.hypot(x, y)) * RAD];}function xyzToFaceUV(x: number, y: number, z: number): [number, number, number] { const ax = Math.abs(x); const ay = Math.abs(y); const az = Math.abs(z); let f = ax > ay ? (ax > az ? 0 : 2) : ay > az ? 1 : 2; if ((f === 0 && x < 0) || (f === 1 && y < 0) || (f === 2 && z < 0)) f += 3; switch (f) { case 0: return [0, y / x, z / x]; case 1: return [1, -x / y, z / y]; case 2: return [2, -x / z, -y / z]; case 3: return [3, z / x, y / x]; case 4: return [4, z / y, -x / y]; default: return [5, -y / z, -x / z]; }}function faceUVToXyz(f: number, u: number, v: number): [number, number, number] { switch (f) { case 0: return [1, u, v]; case 1: return [-u, 1, v]; case 2: return [-u, -v, 1]; case 3: return [-1, -v, -u]; case 4: return [v, -1, -u]; default: return [v, u, -1]; }}const stToUV = (s: number): number => (s >= 0.5 ? (4 * s * s - 1) / 3 : (1 - 4 * (1 - s) * (1 - s)) / 3);const uvToST = (u: number): number => (u >= 0 ? 0.5 * Math.sqrt(1 + 3 * u) : 1 - 0.5 * Math.sqrt(1 - 3 * u));const stToIJ = (s: number): number => clamp(Math.floor(S2_MAX * s), 0, S2_MAX - 1);function fromFaceIJ(f: number, i: number, j: number): bigint { let n = BigInt(f) << 60n; let bits = f & SWAP; for (let k = 7; k >= 0; k--) { bits += ((i >> (k * 4)) & 15) << 6; bits += ((j >> (k * 4)) & 15) << 2; bits = LOOKUP_POS[bits]!; n |= BigInt(bits >> 2) << BigInt(k * 8); bits &= SWAP | INVERT; } return n * 2n + 1n;}function faceIJ(id: bigint): [number, number, number] { const f = Number(id >> 61n); let orientation = f & SWAP; let i = 0; let j = 0; let nbits = 2; for (let k = 7; k >= 0; k--) { orientation += Number((id >> BigInt(k * 8 + 1)) & BigInt((1 << (2 * nbits)) - 1)) << 2; orientation = LOOKUP_IJ[orientation]!; i += (orientation >> 6) * 2 ** (k * 4); j += ((orientation >> 2) & 15) * 2 ** (k * 4); orientation &= SWAP | INVERT; nbits = 4; } return [f, i, j];}const lsb = (id: bigint): bigint => id & -id;const lsbForLevel = (level: number): bigint => 1n << BigInt(2 * (30 - level));function levelOf(id: bigint): number { let tz = 0; while (tz < 64 && ((id >> BigInt(tz)) & 1n) === 0n) tz++; return 30 - (tz >> 1);}const parentAt = (id: bigint, level: number): bigint => { const l = lsbForLevel(level); return (id & -l) | l;};const tokenOf = (id: bigint): string => id.toString(16).padStart(16, '0').replace(/0+$/, '') || 'X';/** * An S2 cell id from a hexadecimal token or a bigint, validated. A string is ALWAYS a hex token - pass a decimal id * as BigInt(decimalString). * @param cell - a token ('47a1') or an id * @returns the 64-bit id */function s2Id(cell: string | bigint): bigint { if (typeof cell === 'string' && !/^[0-9a-fA-F]{1,16}$/.test(cell)) throw new RangeError('expected a 1-16 digit hexadecimal S2 token'); const id = typeof cell === 'bigint' ? cell : BigInt(`0x${cell.padEnd(16, '0')}`); if (id <= 0n || id >= 1n << 64n || Number(id >> 61n) > 5 || (lsb(id) & 0x1555555555555555n) === 0n) throw new RangeError('invalid S2 cell id'); return id;}/** * The S2 cell holding a position at a level. * @param point - [longitude, latitude] * @param level - 0 to 30 (default 12, about 2 km across) * @returns its id, token and level */function s2Cell(point: LonLat, level: number = 12): S2Cell { assertLonLat(point); if (!Number.isInteger(level) || level < 0 || level > 30) throw new RangeError('an S2 level is 0 to 30'); const [f, u, v] = xyzToFaceUV(...lonLatToXyz(point)); const id = parentAt(fromFaceIJ(f, stToIJ(uvToST(u)), stToIJ(uvToST(v))), level); return { id, token: tokenOf(id), level };}/** * A cell's canonical token. * @param cell - a token or an id * @returns the lowercase hex token */function s2Token(cell: string | bigint): string { return tokenOf(s2Id(cell));}/** * A cell's level. * @param cell - a token or an id * @returns 0 to 30 */function s2Level(cell: string | bigint): number { return levelOf(s2Id(cell));}/** the cell's (s, t) square on its face: face, lower i and j, size in leaf cells */function cellSquare(id: bigint): [number, number, number, number] { const [f, i, j] = faceIJ(id); const size = 2 ** (30 - levelOf(id)); return [f, Math.floor(i / size) * size, Math.floor(j / size) * size, size];}/** * The center of a cell (in its face's (s, t) square - not the original GPS fix a cell was made from). * @param cell - a token or an id * @returns [longitude, latitude] */function s2Center(cell: string | bigint): LonLat { const [f, i, j, size] = cellSquare(s2Id(cell)); return xyzToLonLat(...faceUVToXyz(f, stToUV((i + size / 2) / S2_MAX), stToUV((j + size / 2) / S2_MAX)));}/** * A cell's four corners (counterclockwise, as S2 orders them). * @param cell - a token or an id * @returns four [longitude, latitude] positions */function s2Vertices(cell: string | bigint): LonLat[] { const [f, i, j, size] = cellSquare(s2Id(cell)); const u0 = stToUV(i / S2_MAX); const u1 = stToUV((i + size) / S2_MAX); const v0 = stToUV(j / S2_MAX); const v1 = stToUV((j + size) / S2_MAX); return [ [u0, v0], [u1, v0], [u1, v1], [u0, v1], ].map(([u, v]) => xyzToLonLat(...faceUVToXyz(f, u!, v!)));}/** * A cell as a region the map can draw (great-circle edges; its ring wound for D3). * @param cell - a token or an id * @returns a GeoJSON polygon feature (properties.id = the token) */function s2Polygon(cell: string | bigint): GeoRegion { const token = s2Token(cell); // S2 gives counterclockwise vertices; D3 takes the clockwise ring as the small region const ring = s2Vertices(cell).reverse(); ring.push([ring[0]![0], ring[0]![1]]); return { type: 'Feature', properties: { id: token, name: `S2 ${token}`, level: 'state' }, geometry: { type: 'Polygon', coordinates: [ring] }, };}function fromFaceIJWrap(f: number, i: number, j: number): bigint { i = clamp(i, -1, S2_MAX); j = clamp(j, -1, S2_MAX); const limit = 1 + Number.EPSILON; const u = clamp((2 * i + 1 - S2_MAX) / S2_MAX, -limit, limit); const v = clamp((2 * j + 1 - S2_MAX) / S2_MAX, -limit, limit); const [g, u2, v2] = xyzToFaceUV(...faceUVToXyz(f, u, v)); return fromFaceIJ(g, stToIJ(0.5 * (u2 + 1)), stToIJ(0.5 * (v2 + 1)));}/** * The four cells sharing an edge with a cell, at its level (below, right, above, left in its face's frame). * @param cell - a token or an id * @returns four tokens */function s2Neighbors(cell: string | bigint): string[] { const id = s2Id(cell); const level = levelOf(id); const size = 2 ** (30 - level); const [f, i, j] = faceIJ(id); return [ [i, j - size], [i + size, j], [i, j + size], [i - size, j], ].map(([a, b]) => tokenOf(parentAt(fromFaceIJWrap(f, a!, b!), level)));}/** * A cell's four children, one level finer (none at level 30). * @param cell - a token or an id * @returns their tokens in Hilbert order */function s2Children(cell: string | bigint): string[] { const id = s2Id(cell); if (levelOf(id) === 30) return []; const low = lsb(id); const begin = id - low + (low >> 2n); const step = low >> 1n; return [0n, 1n, 2n, 3n].map((k) => tokenOf(begin + k * step));}/** * The cell containing a cell at a coarser level. * @param cell - a token or an id * @param level - 0 to the cell's own level (default one coarser) * @returns the parent's token */function s2Parent(cell: string | bigint, level?: number): string { const id = s2Id(cell); const own = levelOf(id); const target = level ?? own - 1; if (!Number.isInteger(target) || target < 0 || target > own) throw new RangeError(`a parent level is 0 to ${own}`); return tokenOf(parentAt(id, target));}/** * A position from either form a marker takes: [longitude, latitude] as given, or an S2 cell's center. * @param value - a position or `{ s2: token }` * @returns [longitude, latitude] */function resolvePosition(value: LonLat | { s2: string | bigint }): LonLat { if (Array.isArray(value)) { assertLonLat(value); return [value[0], value[1]]; } return s2Center((value as { s2: string | bigint }).s2);}// -- Morton (Z-order over a longitude / latitude grid - not S2's Hilbert curve) ------------------function checkBits(bits: number): void { if (!Number.isInteger(bits) || bits < 1 || bits > 26) throw new RangeError('Morton bits are 1 to 26');}/** * Interleave two grid coordinates into a Morton key (x on the even bits, y on the odd). * @param x - 0 to 2^bits - 1 * @param y - 0 to 2^bits - 1 * @param bits - bits per coordinate, 1 to 26 (default 16) * @returns the key */function mortonEncode(x: number, y: number, bits: number = 16): bigint { checkBits(bits); if (![x, y].every((v) => Number.isInteger(v) && v >= 0 && v < 2 ** bits)) throw new RangeError('grid coordinate out of range'); const bx = BigInt(x); const by = BigInt(y); let key = 0n; for (let b = 0; b < bits; b++) { const s = BigInt(b); key |= (((bx >> s) & 1n) << (2n * s)) | (((by >> s) & 1n) << (2n * s + 1n)); } return key;}/** * Split a Morton key back into its grid coordinates. * @param key - the key * @param bits - bits per coordinate (default 16) * @returns [x, y] */function mortonDecode(key: bigint, bits: number = 16): [number, number] { checkBits(bits); if (key < 0n || key >= 1n << BigInt(2 * bits)) throw new RangeError('Morton key out of range'); let x = 0n; let y = 0n; for (let b = 0; b < bits; b++) { const s = BigInt(b); x |= ((key >> (2n * s)) & 1n) << s; y |= ((key >> (2n * s + 1n)) & 1n) << s; } return [Number(x), Number(y)];}function mortonGrid(p: LonLat, bits: number): [number, number] { const n = 2 ** bits; return [Math.min(n - 1, Math.floor(((p[0] + 180) / 360) * n)), Math.min(n - 1, Math.floor(((p[1] + 90) / 180) * n))];}/** * A position's Morton key on a 2^bits x 2^bits longitude / latitude grid. * @param point - [longitude, latitude] * @param bits - bits per coordinate (default 16) * @returns the key */function mortonKey(point: LonLat, bits: number = 16): bigint { assertLonLat(point); checkBits(bits); return mortonEncode(...mortonGrid(point, bits), bits);}/** * A Morton index over items with positions: sorted keys, rectangle queries by recursive prefix ranges, then the exact * position test (no grid false positives). * @param items - the items * @param position - an item's [longitude, latitude] * @param bits - bits per coordinate (default 16) * @returns the index */function mortonIndex<T>(items: readonly T[], position: (item: T) => LonLat, bits: number = 16): GeoMortonIndex { checkBits(bits); const entries = items .map((item) => { const point = position(item); assertLonLat(point); return { key: mortonKey(point, bits), point: [point[0], point[1]] as LonLat, item }; }) .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); const lowerBound = (key: bigint): number => { let lo = 0; let hi = entries.length; while (lo < hi) { const m = (lo + hi) >>> 1; if (entries[m]!.key < key) lo = m + 1; else hi = m; } return lo; }; const query = (bounds: GeoBounds): T[] => { assertBounds(bounds); if (bounds[0] > bounds[2]) return [...query([bounds[0], bounds[1], 180, bounds[3]]), ...query([-180, bounds[1], bounds[2], bounds[3]])]; const [x0, y0] = mortonGrid([bounds[0], bounds[1]], bits); const [x1, y1] = mortonGrid([bounds[2], bounds[3]], bits); const out: T[] = []; const walk = (x: number, y: number, size: number, depth: number, prefix: bigint): void => { if (x > x1 || y > y1 || x + size - 1 < x0 || y + size - 1 < y0) return; const shift = BigInt(2 * (bits - depth)); const begin = lowerBound(prefix << shift); const end = lowerBound((prefix + 1n) << shift); if (begin === end) return; // a small range is cheaper to test directly; pruning stays exact if (end - begin <= 32 || depth === bits || (x >= x0 && y >= y0 && x + size - 1 <= x1 && y + size - 1 <= y1)) { for (let i = begin; i < end; i++) if (inBounds(entries[i]!.point, bounds)) out.push(entries[i]!.item); return; } const half = size / 2; for (let q = 0; q < 4; q++) walk(x + (q & 1) * half, y + (q >> 1) * half, half, depth + 1, prefix * 4n + BigInt(q)); }; walk(0, 0, 2 ** bits, 0, 0n); return out; }; return { size: entries.length, ordered: () => entries.map((e) => e.item), query };}// -- routing (Dijkstra over supplied lengths) ---------------------------------------------------interface RouteArc { to: string; segment: Required<Pick<GeoRouteSegment, 'coordinates'>> & GeoRouteSegment; reversed: boolean;}/** a binary min-heap; stale Dijkstra entries are skipped on pop */class Queue { private values: { id: string; cost: number }[] = []; push(value: { id: string; cost: number }): void { let i = this.values.length; this.values.push(value); while (i > 0) { const p = (i - 1) >>> 1; if (this.values[p]!.cost <= value.cost) break; this.values[i] = this.values[p]!; i = p; } this.values[i] = value; } pop(): { id: string; cost: number } | undefined { if (!this.values.length) return undefined; const first = this.values[0]!; const last = this.values.pop()!; if (this.values.length) { let i = 0; while (i * 2 + 1 < this.values.length) { let c = i * 2 + 1; if (c + 1 < this.values.length && this.values[c + 1]!.cost < this.values[c]!.cost) c++; if (this.values[c]!.cost >= last.cost) break; this.values[i] = this.values[c]!; i = c; } this.values[i] = last; } return first; }}function pointOnLine(points: LonLat[], fraction: number): LonLat { if (fraction <= 0) return [points[0]![0], points[0]![1]]; const last = points[points.length - 1]!; if (fraction >= 1) return [last[0], last[1]]; const lengths = points.slice(1).map((p, i) => inverse(points[i]!, p).meters); const total = lengths.reduce((a, b) => a + b, 0); if (!total) return [points[0]![0], points[0]![1]]; const target = total * fraction; let before = 0; for (let i = 0; i < lengths.length; i++) { if (before + lengths[i]! >= target) return direct(points[i]!, inverse(points[i]!, points[i + 1]!).initialBearing, target - before); before += lengths[i]!; } return [last[0], last[1]];}function sliceLine(points: LonLat[], from: number, to: number): LonLat[] { const total = polylineLength(points); const out = [pointOnLine(points, from)]; let before = 0; for (let i = 1; i < points.length - 1; i++) { before += inverse(points[i - 1]!, points[i]!).meters; const f = total ? before / total : 0; if (f > from && f < to) out.push([points[i]![0], points[i]![1]]); } out.push(pointOnLine(points, to)); return out;}/** * A route graph over nodes and the segments between them. Dijkstra minimizes the SUPPLIED lengths (no A*: an * application's lengths may undercut the surface distance, so a geodesic heuristic would not be admissible); a * Morton index finds nodes by position. Needs the engines (df$.shadcn.geo.load()) for nearestNode and routeAt. * @param nodes - the nodes (unique ids) * @param segments - the segments (unique ids; lengths finite and >= 0) * @returns the graph */function routeGraph(nodes: GeoRouteNode[], segments: GeoRouteSegment[]): GeoRouteGraph { const byId = new Map<string, GeoRouteNode>(); const segs = new Map<string, RouteArc['segment']>(); const adjacency = new Map<string, RouteArc[]>(); for (const n of nodes) { if (!n.id || byId.has(n.id)) throw new Error(`duplicate or empty node id: ${n.id}`); assertLonLat(n.position); byId.set(n.id, { ...n, position: [n.position[0], n.position[1]] }); adjacency.set(n.id, []); } for (const edge of segments) { if (!edge.id || segs.has(edge.id)) throw new Error(`duplicate or empty segment id: ${edge.id}`); if (!byId.has(edge.from) || !byId.has(edge.to)) throw new Error(`unknown endpoint of segment ${edge.id}`); if (!Number.isFinite(edge.lengthMeters) || edge.lengthMeters < 0) throw new RangeError(`segment ${edge.id}: the length must be finite and >= 0`); const a = byId.get(edge.from)!.position; const b = byId.get(edge.to)!.position; const points = edge.coordinates?.map((p) => { assertLonLat(p); return [p[0], p[1]] as LonLat; }) ?? [a, b]; const end = points[points.length - 1]!; if (points.length < 2 || points[0]![0] !== a[0] || points[0]![1] !== a[1] || end[0] !== b[0] || end[1] !== b[1]) throw new Error(`segment ${edge.id}: its geometry must start and end at its nodes`); const segment = { ...edge, coordinates: points }; segs.set(edge.id, segment); adjacency.get(edge.from)!.push({ to: edge.to, segment, reversed: false }); if (edge.bidirectional) adjacency.get(edge.to)!.push({ to: edge.from, segment, reversed: true }); } const index = mortonIndex([...byId.values()], (n) => n.position); const copy = (n: GeoRouteNode): GeoRouteNode => ({ ...n, position: [n.position[0], n.position[1]] }); const route = (from: string, to: string): GeoRoute | null => { if (!byId.has(from) || !byId.has(to)) throw new Error('unknown route node'); const costs = new Map([[from, 0]]); const previous = new Map<string, { from: string; arc: RouteArc }>(); const queue = new Queue(); queue.push({ id: from, cost: 0 }); for (let current = queue.pop(); current; current = queue.pop()) { if (current.cost !== costs.get(current.id)) continue; if (current.id === to) break; for (const arc of adjacency.get(current.id)!) { const cost = current.cost + arc.segment.lengthMeters; if (!Number.isFinite(cost)) throw new RangeError('route cost overflow'); if (cost < (costs.get(arc.to) ?? Infinity)) { costs.set(arc.to, cost); previous.set(arc.to, { from: current.id, arc }); queue.push({ id: arc.to, cost }); } } } if (!costs.has(to)) return null; const steps: GeoRouteStep[] = []; const ids = [to]; for (let id = to; id !== from;) { const p = previous.get(id)!; const coordinates = p.arc.segment.coordinates.map((q) => [q[0], q[1]] as LonLat); if (p.arc.reversed) coordinates.reverse(); steps.push({ segment: p.arc.segment.id, from: p.from, to: id, reversed: p.arc.reversed, lengthMeters: p.arc.segment.lengthMeters, coordinates, }); id = p.from; ids.push(id); } steps.reverse(); ids.reverse(); const coordinates = steps.flatMap((step, i) => (i ? step.coordinates.slice(1) : step.coordinates)); if (!steps.length) { const p = byId.get(from)!.position; coordinates.push([p[0], p[1]], [p[0], p[1]]); } return { lengthMeters: costs.get(to)!, nodeIds: ids, steps, coordinates }; }; const routeAt = (from: GeoSegmentPosition, to: GeoSegmentPosition): GeoRoute | null => { const allNodes = [...byId.values()]; const cuts = new Map<string, Map<number, string>>(); const originals = new Map<string, string>(); let sequence = 0; const unique = (): string => { let id: string; do id = `__geo_cut_${sequence++}`; while (byId.has(id) || segs.has(id)); return id; }; const resolve = (p: GeoSegmentPosition): string => { const edge = segs.get(p.segment); if (!edge) throw new Error(`unknown segment: ${p.segment}`); if (!Number.isFinite(p.fraction) || p.fraction < 0 || p.fraction > 1) throw new RangeError('a fraction is 0 to 1'); if (p.fraction === 0) return edge.from; if (p.fraction === 1) return edge.to; if (!cuts.has(edge.id)) cuts.set(edge.id, new Map()); const map = cuts.get(edge.id)!; if (!map.has(p.fraction)) { const id = unique(); map.set(p.fraction, id); allNodes.push({ id, position: pointOnLine(edge.coordinates, p.fraction) }); } return map.get(p.fraction)!; }; const a = resolve(from); const b = resolve(to); const positionOf = (id: string): LonLat => allNodes.find((n) => n.id === id)!.position; const split: GeoRouteSegment[] = []; for (const edge of segs.values()) { const map = cuts.get(edge.id); if (!map) { split.push(edge); continue; } const parts: [number, string][] = [[0, edge.from], ...map.entries(), [1, edge.to]]; parts.sort((x, y) => x[0] - y[0]); for (let i = 1; i < parts.length; i++) { const prev = parts[i - 1]!; const next = parts[i]!; const coordinates = sliceLine(edge.coordinates, prev[0], next[0]); coordinates[0] = [...positionOf(prev[1])] as LonLat; coordinates[coordinates.length - 1] = [...positionOf(next[1])] as LonLat; const id = unique(); originals.set(id, edge.id); // the interior cut keeps the recorded length, proportionally split.push({ ...edge, id, from: prev[1], to: next[1], coordinates, lengthMeters: edge.lengthMeters * (next[0] - prev[0]), }); } } const result = routeGraph(allNodes, split).route(a, b); if (result) for (const step of result.steps) step.segment = originals.get(step.segment) ?? step.segment; return result; }; const nearestNode = (position: LonLat, bounds?: GeoBounds): GeoNearestNode | null => { assertLonLat(position); let best: GeoNearestNode | null = null; for (const node of (bounds ? index.query(bounds) : [...byId.values()]) as GeoRouteNode[]) { const meters = inverse(position, node.position).meters; if (!best || meters < best.meters) best = { node: copy(node), meters }; } return best; }; return { route, routeAt, nearestNode, nodesInBounds: (bounds) => (index.query(bounds) as GeoRouteNode[]).map(copy), mortonOrder: () => (index.ordered() as GeoRouteNode[]).map((n) => n.id), };}/** * The position a given distance along a route, measured in its own (supplied) metres; within a step its geometry is * parametrized by its ellipsoidal arc length. Needs the engines. * @param route - a route from a graph * @param meters - 0 to route.lengthMeters * @returns [longitude, latitude] */function positionAlongRoute(route: GeoRoute, meters: number): LonLat { if (!Number.isFinite(meters) || meters < 0 || meters > route.lengthMeters) throw new RangeError('a distance on the route'); let remaining = meters; for (const step of route.steps) { if (remaining <= step.lengthMeters && step.lengthMeters > 0) return pointOnLine(step.coordinates, remaining / step.lengthMeters); remaining -= step.lengthMeters; } const last = route.coordinates[route.coordinates.length - 1]!; return [last[0], last[1]];}// -- DGEO / TopoJSON decoding ---------------------------------------------------------------------const DGEO_HEADER = 20;const DGEO_MAX = 256 * 1024 * 1024;const MAX_ARCS = 2_000_000;const utf8 = new TextDecoder('utf-8', { fatal: true });function readVarint(data: Uint8Array, cursor: { i: number }, end: number): number { let value = 0; let scale = 1; for (let k = 0; k < 5; k++) { if (cursor.i >= end) throw new Error('DGEO: truncated varint'); const b = data[cursor.i++]!; value += (b & 127) * scale; if (value > 0xffffffff) throw new Error('DGEO: varint overflow'); if (b < 128) { if (k && b === 0) throw new Error('DGEO: noncanonical varint'); return value; } scale *= 128; } throw new Error('DGEO: oversized varint');}function checkTopology(t: GeoTopology, arcCount: number): void { if (!t || t.type !== 'Topology' || !t.objects || typeof t.objects !== 'object') throw new Error('invalid topology'); const tr = t.transform; if ( !tr || tr.scale.length !== 2 || tr.translate.length !== 2 || ![...tr.scale, ...tr.translate].every(Number.isFinite) ) throw new Error('a quantized topology needs its transform'); let refs = 0; const indices = (value: unknown, depth: number): void => { if (depth > 8 || ++refs > 10_000_000) throw new Error('excessive arc references'); if (Array.isArray(value)) { for (const x of value) indices(x, depth + 1); return; } if (!Number.isInteger(value) || ((value as number) < 0 ? -(value as number) - 1 : (value as number)) >= arcCount) throw new Error('arc reference out of range'); }; const visit = (o: any, depth: number): void => { if (!o || depth > 32) throw new Error('invalid geometry tree'); if (o.type === 'GeometryCollection') (o.geometries ?? []).forEach((g: unknown) => visit(g, depth + 1)); else if (['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'].includes(o.type)) indices(o.arcs, 0); }; Object.values(t.objects).forEach((o) => visit(o, 0));}/** * Decode a topology: DGEO bytes (DSON metadata + varint arcs) or quantized TopoJSON as UTF-8 JSON - for first-party * data (DSON is no sandbox for hostile metadata). Needs the engines. * @param bytes - the file's bytes * @returns the topology, arcs delta-encoded as in TopoJSON */function decode(bytes: Uint8Array): GeoTopology { if (bytes.length > DGEO_MAX) throw new Error('topology size limit exceeded'); if (bytes[0] !== 68) { const t = JSON.parse(utf8.decode(bytes)) as GeoTopology; if (!Array.isArray(t.arcs) || t.arcs.length > MAX_ARCS) throw new Error('invalid arcs'); checkTopology(t, t.arcs.length); return t; } if (bytes.length < DGEO_HEADER || bytes[1] !== 71 || bytes[2] !== 69 || bytes[3] !== 79) throw new Error('invalid DGEO header'); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if (view.getUint16(4, true) !== 1 || view.getUint16(6, true) !== 0) throw new Error('unsupported DGEO version'); const m = view.getUint32(8, true); const count = view.getUint32(12, true); const rawLength = view.getUint32(16, true); const base = DGEO_HEADER + m + (count + 1) * 4; if (m > 64 * 1024 * 1024 || count > MAX_ARCS || base + rawLength !== bytes.length) throw new Error('invalid DGEO lengths'); const meta = ready().DSON.parse(utf8.decode(bytes.subarray(DGEO_HEADER, DGEO_HEADER + m))) as GeoTopology; checkTopology(meta, count); const offset = (k: number): number => view.getUint32(DGEO_HEADER + m + k * 4, true); if (offset(0) !== 0 || offset(count) !== rawLength) throw new Error('invalid DGEO offset table'); const arcs: number[][][] = []; for (let k = 0; k < count; k++) { const start = offset(k); const stop = offset(k + 1); if (stop < start || stop > rawLength) throw new Error('non-monotonic DGEO offsets'); const cursor = { i: base + start }; const end = base + stop; const arc: number[][] = []; while (cursor.i < end) { const x = readVarint(bytes, cursor, end); const y = readVarint(bytes, cursor, end); arc.push([x % 2 ? -(x + 1) / 2 : x / 2, y % 2 ? -(y + 1) / 2 : y / 2]); } if (arc.length < 2) throw new Error('a DGEO arc needs two points'); arcs.push(arc); } return { ...meta, arcs };}/** * A topology object's geometries as GeoJSON features (TopoJSON's feature(): arcs stitched, shared points dropped, * reversed arcs turned). * @param topology - a decoded topology * @param object - the object's name (world.dgeo: 'countries', 'continents'; a chunk: 'regions') * @returns the features */function features(topology: GeoTopology, object: string): GeoRegion[] { const o = topology.objects[object] as any; if (!o) throw new Error(`the topology has no object "${object}"`); const [kx, ky] = topology.transform?.scale ?? [1, 1]; const [dx, dy] = topology.transform?.translate ?? [0, 0]; // the arcs, absolute and transformed once const arcs = topology.arcs.map((arc) => { let x = 0; let y = 0; return arc.map((p) => { x += p[0]!; y += p[1]!; return [x * kx + dx, y * ky + dy] as LonLat; }); }); const line = (indices: number[]): LonLat[] => { const points: LonLat[] = []; for (const i of indices) { if (points.length) points.pop(); const start = points.length; for (const p of arcs[i < 0 ? ~i : i]!) points.push([p[0], p[1]]); if (i < 0) { const tail = points.splice(start).reverse(); points.push(...tail); } } return points; }; const ring = (indices: number[]): LonLat[] => { const points = line(indices); while (points.length < 4) points.push([points[0]![0], points[0]![1]]); return points; }; const geometry = (g: any): any => { switch (g.type) { case 'Polygon': return { type: 'Polygon', coordinates: g.arcs.map(ring) }; case 'MultiPolygon': return { type: 'MultiPolygon', coordinates: g.arcs.map((p: number[][]) => p.map(ring)) }; case 'LineString': { const points = line(g.arcs); if (points.length < 2) points.push([points[0]![0], points[0]![1]]); return { type: 'LineString', coordinates: points }; } case 'MultiLineString': return { type: 'MultiLineString', coordinates: g.arcs.map(line) }; case 'Point': return { type: 'Point', coordinates: [g.coordinates[0] * kx + dx, g.coordinates[1] * ky + dy] }; case 'MultiPoint': return { type: 'MultiPoint', coordinates: g.coordinates.map((c: number[]) => [c[0]! * kx + dx, c[1]! * ky + dy]), }; case 'GeometryCollection': return { type: 'GeometryCollection', geometries: g.geometries.map(geometry) }; default: return null; } }; const feature = (g: any): GeoRegion => ({ type: 'Feature', ...(g.id != null ? { id: g.id } : {}), properties: g.properties ?? {}, geometry: geometry(g), }) as GeoRegion; return o.type === 'GeometryCollection' ? o.geometries.map(feature) : [feature(o)];}/** * A region from any GeoJSON polygon feature, its rings turned to D3's winding where needed (each ring by its * spherical area: an outer ring encloses the smaller side) - so RFC 7946 and D3-wound input both draw. The input is * not changed. Needs the engines. * @param region - a Polygon / MultiPolygon feature with properties `{ id, name, level, parent? }` * @returns the region, ready for a map */function rewind(region: GeoRegion): GeoRegion { const { d3 } = ready(); const g = region.geometry; if (!g || (g.type !== 'Polygon' && g.type !== 'MultiPolygon')) throw new Error('a region is a Polygon or MultiPolygon'); const fix = (polygon: LonLat[][]): LonLat[][] => polygon.map((ring, i) => { if (ring.length < 4) throw new Error('a ring needs four positions (closed)'); ring.forEach(assertLonLat); const copy = ring.map((p) => [p[0], p[1]] as LonLat); const big = d3.geoArea({ type: 'Polygon', coordinates: [copy] }) > 2 * Math.PI; // the outer ring encloses the small side, a hole the large one return (i === 0 ? big : !big) ? copy.reverse() : copy; }); const coordinates = g.type === 'Polygon' ? fix(g.coordinates as LonLat[][]) : (g.coordinates as LonLat[][][]).map(fix); return { type: 'Feature', properties: { ...region.properties }, geometry: { type: g.type, coordinates } as GeoGeometry, };}/** conservative rectangles for the broad phase: split strips, sampled parallels, a 0.1° latitude pad */function boundsProxy(id: string, bounds: GeoBounds): GeoRegion { const [west, south, east, north] = bounds; const intervals = west > east ? [ [west, 180], [-180, east], ] : [[west, east]]; const polygons: LonLat[][][] = []; const bottom = Math.max(-90, south - 0.1); const top = Math.min(90, north + 0.1); for (const [w, e] of intervals as [number, number][]) { const count = Math.max(1, Math.ceil((e - w) / 60)); for (let part = 0; part < count; part++) { const a = w + ((e - w) * part) / count; const b = w + ((e - w) * (part + 1)) / count; const steps = Math.max(1, Math.ceil((b - a) / 5)); const ring: LonLat[] = [ [a, bottom], [a, top], ]; for (let j = 1; j <= steps; j++) ring.push([a + ((b - a) * j) / steps, top]); ring.push([b, bottom]); for (let j = steps - 1; j >= 0; j--) ring.push([a + ((b - a) * j) / steps, bottom]); polygons.push([ring]); } } return { type: 'Feature', properties: { id, name: id, level: 'country' }, geometry: { type: 'MultiPolygon', coordinates: polygons }, };}// -- data -------------------------------------------------------------------------------------------interface GeoManifest { version: number; initial: string; details: Record<string, string>; states: Record<string, string>; stateDetails?: Record<string, string>; bounds?: Record<string, GeoBounds>; names?: string;}interface Dataset { url: string; manifest: GeoManifest | null; countries: GeoRegion[]; continents: GeoRegion[]; /** a GeoJSON source's states (memory - no chunks) */ memoryStates: GeoRegion[]; countryIds: Set<string>; proxies: Map<string, GeoRegion>; byKey: Map<string, GeoRegion>; chunks: Map<string, { regions: GeoRegion[]; bytes: number }>; loading: Set<string>; failed: Map<string, { until: number; attempts: number }>; bytes: number; maps: Set<HTMLElement>; names: Promise<SearchEntry[]> | null; /** state key → parent country id (names.json) */ parents: Map<string, string>;}interface SearchEntry { key: string; name: string; level: GeoLevel; label: string; norm: string;}const datasets = new Map<string, Promise<Dataset>>();const CACHE_BYTES = 24 * 1024 * 1024;const MAX_LOADS = 4;let activeLoads = 0;const loadQueue: { ds: Dataset; path: string }[] = [];const keyOf = (r: GeoRegion): string => `${r.properties.level}:${r.properties.id}`;async function fetchBytes(url: string): Promise<Uint8Array> { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`); return new Uint8Array(await response.arrayBuffer());}function index(ds: Dataset, regions: GeoRegion[]): void { for (const r of regions) ds.byKey.set(keyOf(r), r);}function openDataset(url: string): Promise<Dataset> { let known = datasets.get(url); if (!known) { known = (async () => { await load(); const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`); const json = (await response.json()) as any; const ds: Dataset = { url, manifest: null, countries: [], continents: [], memoryStates: [], countryIds: new Set(), proxies: new Map(), byKey: new Map(), chunks: new Map(), loading: new Set(), failed: new Map(), bytes: 0, maps: new Set(), names: null, parents: new Map(), }; if (json?.type === 'FeatureCollection') { // an application's own regions (GeoJSON): every level resident, wound for D3 const regions = (json.features as GeoRegion[]).map(rewind); ds.continents = regions.filter((r) => r.properties.level === 'continent'); ds.countries = regions.filter((r) => r.properties.level === 'country'); ds.memoryStates = regions.filter((r) => r.properties.level === 'state'); for (const s of ds.memoryStates) if (s.properties.parent) ds.parents.set(keyOf(s), s.properties.parent); } else { const manifest = json as GeoManifest; if (manifest.version !== 1 || !manifest.initial || !manifest.details || !manifest.states) throw new Error(`${url} is no geo-map manifest (version 1)`); ds.manifest = manifest; const topology = decode(await fetchBytes(new URL(manifest.initial, url).href)); ds.countries = features(topology, 'countries'); ds.continents = features(topology, 'continents'); for (const [id, b] of Object.entries(manifest.bounds ?? {})) ds.proxies.set(id, boundsProxy(id, b)); } ds.countryIds = new Set(ds.countries.map((r) => r.properties.id)); index(ds, ds.countries); index(ds, ds.continents); index(ds, ds.memoryStates); return ds; })(); known.catch(() => { if (datasets.get(url) === known) datasets.delete(url); }); datasets.set(url, known); } return known;}/** the chunk's regions when cached (touched for the LRU), else undefined - and wanted */function chunk(ds: Dataset, path: string, wanted: Set<string>): GeoRegion[] | undefined { wanted.add(path); const entry = ds.chunks.get(path); if (entry) { ds.chunks.delete(path); ds.chunks.set(path, entry); return entry.regions; } if ( !ds.loading.has(path) && (ds.failed.get(path)?.until ?? 0) <= Date.now() && !loadQueue.some((q) => q.ds === ds && q.path === path) ) loadQueue.push({ ds, path }); return undefined;}/** the maps on the page that read a dataset: a map that left it (an SPA swapped it out) lets go of its chunks - it * registers again on its next render if it comes back */function liveMaps(ds: Dataset): HTMLElement[] { for (const el of ds.maps) if (!el.isConnected) ds.maps.delete(el); return [...ds.maps];}const wantedBySomeMap = (ds: Dataset, path: string): boolean => liveMaps(ds).some((el) => el._geo?.wanted.has(path));function pump(): void { while (activeLoads < MAX_LOADS && loadQueue.length) { // the newest request first: the view the reader looks at now const next = loadQueue.pop()!; if (next.ds.chunks.has(next.path) || next.ds.loading.has(next.path) || !wantedBySomeMap(next.ds, next.path)) continue; activeLoads++; next.ds.loading.add(next.path); void loadChunk(next.ds, next.path); }}async function loadChunk(ds: Dataset, path: string): Promise<void> { const url = new URL(path, ds.url).href; try { const bytes = await fetchBytes(url); const regions = features(decode(bytes), 'regions'); ds.chunks.set(path, { regions, bytes: bytes.length }); ds.bytes += bytes.length; ds.failed.delete(path); index(ds, regions); // the budget counts encoded bytes (decoded heap is larger); what a map shows is never evicted for (const [key, entry] of ds.chunks) { if (ds.bytes <= CACHE_BYTES || ds.chunks.size <= 1) break; if (!wantedBySomeMap(ds, key)) { ds.chunks.delete(key); ds.bytes -= entry.bytes; } } } catch (err) { const attempts = (ds.failed.get(path)?.attempts ?? 0) + 1; ds.failed.set(path, { attempts, until: Date.now() + Math.min(60_000, 1000 * 2 ** attempts) }); for (const el of liveMaps(ds)) reportError(el, err, url); } finally { ds.loading.delete(path); activeLoads--; for (const el of liveMaps(ds)) requestRender(el); pump(); }}const normalize = (s: string): string => s .normalize('NFD') .replace(/[̀-ͯ]/g, '') .toLowerCase() .trim();/** the search entries: continents and countries now, every state once names.json arrived */function searchEntries(ds: Dataset): Promise<SearchEntry[]> { if (!ds.names) { const resident: SearchEntry[] = [...ds.continents, ...ds.countries, ...ds.memoryStates].map((r) => { const parent = r.properties.parent ? ds.byKey.get(`country:${r.properties.parent}`)?.properties.name : ''; const label = parent ? `${r.properties.name}, ${parent}` : r.properties.name; return { key: keyOf(r), name: r.properties.name, level: r.properties.level, label, norm: normalize(label) }; }); const namesPath = ds.manifest ? (ds.manifest.names ?? 'names.json') : ''; const pending: Promise<SearchEntry[]> = !namesPath ? Promise.resolve(resident) : fetch(new URL(namesPath, ds.url).href) .then((r) => (r.ok ? r.json() : { states: [] })) .catch(() => ({ states: [] })) .then((json: { states?: [string, string, string][] }) => { const states = (json.states ?? []).map(([id, name, parent]) => { ds.parents.set(`state:${id}`, parent); const country = ds.byKey.get(`country:${parent}`)?.properties.name ?? parent; const label = `${name}, ${country}`; return { key: `state:${id}`, name, level: 'state' as GeoLevel, label, norm: normalize(label) }; }); return [...resident, ...states]; }); ds.names = pending; } return ds.names;}function rank(entries: SearchEntry[], query: string, limit: number): GeoSearchHit[] { const q = normalize(query); if (!q) return []; const order: GeoLevel[] = ['continent', 'country', 'state']; const scored: [number, SearchEntry][] = []; for (const e of entries) { const name = normalize(e.name); const score = name === q || e.norm === q ? 0 : name.startsWith(q) ? 1 : name.split(/[\s-]+/).some((w) => w.startsWith(q)) ? 2 : e.norm.includes(q) ? 3 : -1; if (score >= 0) scored.push([score * 10 + order.indexOf(e.level), e]); } scored.sort((a, b) => a[0] - b[0] || a[1].label.localeCompare(b[1].label)); return scored.slice(0, limit).map(([, e]) => ({ key: e.key, name: e.name, level: e.level, label: e.label }));}// -- projection + framing ----------------------------------------------------------------------------const PROJECTIONS: GeoProjectionName[] = ['equalEarth', 'equalArea', 'mercator', 'equirectangular', 'orthographic'];const LEVELS: (GeoLevel | 'auto')[] = ['auto', 'continent', 'country', 'state'];const PALETTES: GeoPalette[] = ['atlas', 'ocean', 'forest', 'sunset', 'heat', 'mono', 'chart'];const BORDERS: GeoBorder[] = ['natural', 'cased', 'dashed', 'none'];const HOME: LonLat = [0, 12];interface Extent { x0: number; y0: number; x1: number; y1: number;}function makeProjection(name: GeoProjectionName, box: Extent, center: LonLat, zoom: number, clip = true): any { const { d3 } = ready(); const make = { equalEarth: d3.geoEqualEarth, // Lambert's azimuthal equal-area, turned to the camera: every area true, like Equal Earth, and the shapes true // at the center too - a framed country is drawn at its real size and in its real shape, wherever it lies equalArea: d3.geoAzimuthalEqualArea, mercator: d3.geoMercator, equirectangular: d3.geoEquirectangular, orthographic: d3.geoOrthographic, }[name]; const p = make().precision(0.35); p.fitExtent( [ [box.x0 + 12, box.y0 + 12], [Math.max(box.x0 + 13, box.x1 - 12), Math.max(box.y0 + 13, box.y1 - 12)], ], { type: 'Sphere' }, ); const scale = p.scale(); if (name === 'orthographic') p.rotate([-center[0], -center[1], 0]).clipAngle(90); else if (name === 'equalArea') p.rotate([-center[0], -center[1], 0]); else p.rotate([-center[0], 0, 0]).center([0, clamp(center[1], -85, 85)]); p.scale(scale * zoom).translate([(box.x0 + box.x1) / 2, (box.y0 + box.y1) / 2]); // a flat projection zoomed in never shows the empty band past a pole (the card's background over the Arctic, as a // wide Europe frame did): once the map is taller than its box the view stops at the pole's edge; zoomed out, the // whole outline stays where the camera puts it. Only the drawn view - frame() measures unclamped (clip false) if (clip && name !== 'orthographic' && name !== 'equalArea') { const [[, top], [, bottom]] = d3.geoPath(p).bounds({ type: 'Sphere' }) as [[number, number], [number, number]]; const dy = bottom - top <= box.y1 - box.y0 ? 0 : top > box.y0 ? box.y0 - top : bottom < box.y1 ? box.y1 - bottom : 0; if (dy) p.translate([(box.x0 + box.x1) / 2, (box.y0 + box.y1) / 2 + dy]); } if (clip) p.clipExtent([ [box.x0, box.y0], [box.x1, box.y1], ]); return p;}/** the built-in focus frames - where a continent's outline (Russia in Europe, the Aleutians) frames poorly */const VIEWS: Record<string, GeoBounds> = { 'continent:Europe': [-25, 34, 45, 71], 'continent:North America': [-168, 7, -20, 72], 'continent:Oceania': [110, -48, -175, 5],};/** * The part of a region a focus frames: its largest polygon plus every polygon near the growing cluster - France * frames the mainland and Corsica, not French Guiana; an archipelago chains together. */function mainBody(region: GeoRegion): GeoRegion { const { d3 } = ready(); const g = region.geometry; if (g.type !== 'MultiPolygon' || g.coordinates.length < 2) return region; const parts = (g.coordinates as LonLat[][][]).map((coordinates) => { const poly = { type: 'Polygon', coordinates }; return { coordinates, area: d3.geoArea(poly) as number, centroid: d3.geoCentroid(poly) as LonLat }; }); parts.sort((a, b) => b.area - a.area); const included = [parts[0]!]; let rest = parts.slice(1); for (let changed = true; changed;) { changed = false; const area = included.reduce((s, p) => s + p.area, 0); const center = d3.geoCentroid({ type: 'MultiPolygon', coordinates: included.map((p) => p.coordinates) }) as LonLat; const radius = Math.sqrt(area / Math.PI); const next = rest.filter((p) => { const near = d3.geoDistance(center, p.centroid) <= 2 * (radius + Math.sqrt(p.area / Math.PI)) + 0.02; if (near) { included.push(p); changed = true; } return !near; }); rest = next; } return { ...region, geometry: { type: 'MultiPolygon', coordinates: included.map((p) => p.coordinates) } };}/** the camera that frames a region (or bounds) inside a box */function frame(target: GeoRegion, box: Extent, projection: GeoProjectionName): { center: LonLat; zoom: number } { const { d3 } = ready(); const center = d3.geoCentroid(target) as LonLat; const c: LonLat = [wrapLon(center[0]), clamp(center[1], -85, 85)]; const p = makeProjection(projection, box, c, 1, false); const b = d3.geoPath(p).bounds(target); const w = b[1][0] - b[0][0]; const h = b[1][1] - b[0][1]; const pad = 28; // the target's size at zoom 1 may be far below a pixel (an S2 cell from level 8, a microstate): only a point (no // size) is guarded - a 1px floor capped every such frame at the box's width (390x on a 28rem map) const zoom = clampZoom( Math.min((box.x1 - box.x0 - pad * 2) / Math.max(1e-9, w), (box.y1 - box.y0 - pad * 2) / Math.max(1e-9, h)), ); return { center: c, zoom: Number.isFinite(zoom) ? zoom : 1 };}// -- the map instance ---------------------------------------------------------------------------------interface GeoView { projection: GeoProjectionName; level: GeoLevel | 'auto'; palette: GeoPalette; border: GeoBorder; graticule: boolean; callouts: boolean; selected: string[]; focus: string | null; center: LonLat; zoom: number;}interface GeoInstance { view: GeoView; ds: Dataset | null; status: 'loading' | 'ready' | 'error'; ready: Promise<void>; svg: SVGSVGElement; layers: Record<string, SVGGElement>; tooltip: HTMLOutputElement; hatch: string; width: number; height: number; projection: any; box: Extent; frameId: number; flight: { id: number; target: { center: LonLat; zoom: number }; done: () => void } | null; home: { center: LonLat; zoom: number }; /** the selection and focus the markup declared - what 'selected' / 'focused' fall back to without a config */ authored: { focus: string | null; selected: string[] }; restTimer: number; pendingFit: string | null; pointers: Map<number, [number, number]>; moved: boolean; drawn: Map<string, GeoRegion>; anchors: Map<string, [number, number]>; hover: string | null; roving: string | null; layersData: Required<GeoLayers>; cellRegions: GeoRegion[]; wanted: Set<string>; stats: GeoRenderStats; abort: AbortController; resize: ResizeObserver; attrs: MutationObserver; lastView: string;}const NS = 'http://www.w3.org/2000/svg';let nextId = 0;function svgEl<K extends keyof SVGElementTagNameMap>( tag: K, attrs: Record<string, string> = {},): SVGElementTagNameMap[K] { const el = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v); return el;}/** a layer's children replaced - through df$ (AGENTS.md "DOM through df$") */function setChildren(parent: Element, nodes: Element[]): void { dfDollar(parent).empty().append(nodes);}/** the one path of a layer (the sphere, the graticule): made once, its d updated */function layerPath(layer: SVGGElement, d: string): void { let p = layer.firstElementChild as SVGPathElement | null; if (!p) { p = svgEl('path'); dfDollar(layer).append(p); } if (p.getAttribute('d') !== d) p.setAttribute('d', d);}function hash(s: string): number { let h = 2166136261; for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619); return h >>> 0;}const reducedMotion = (): boolean => typeof globalThis.matchMedia === 'function' && globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches;/** the keys of a selection config: an array, or a string separated by spaces or commas */function keyList(value: unknown): string[] { const list = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(/[\s,]+/) : []; return [...new Set(list.map((k) => k.trim()).filter(Boolean))];}function parseCenter(value: string | undefined): LonLat | null { if (!value) return null; const parts = value.split(',').map((v) => Number(v.trim())); if (parts.length !== 2 || !parts.every(Number.isFinite)) return null; return [wrapLon(parts[0]!), clamp(parts[1]!, -85, 85)];}/** the view the element's attributes declare (the camera from the instance - it is no markup) */function viewFromAttributes(el: HTMLElement, camera: { center: LonLat; zoom: number }): GeoView { const d = el.dataset; const pick = <T extends string>(value: string | undefined, allowed: T[], fallback: T): T => allowed.includes(value as T) ? (value as T) : fallback; return { projection: pick(d.projection, PROJECTIONS, 'equalEarth'), level: pick(d.level, LEVELS, 'country'), palette: pick(d.palette, PALETTES, 'atlas'), border: pick(d.border, BORDERS, 'natural'), graticule: d.graticule !== 'false', callouts: d.callouts !== undefined && d.callouts !== 'false', selected: keyList(d.selected ?? ''), focus: d.focus || null, center: camera.center, zoom: camera.zoom, };}const stateNameOf = (v: Pick<GeoView, 'focus' | 'selected'>): string => v.focus ? 'focused' : v.selected.length ? 'selected' : 'default';/** a map's state as getState() reports it */function configOf(geo: GeoInstance): Required<Omit<GeoMapConfig, 'selected'>> & { selected: string[] } { const v = geo.view; const camera = geo.flight?.target ?? { center: v.center, zoom: v.zoom }; return { projection: v.projection, level: v.level, palette: v.palette, border: v.border, graticule: v.graticule, callouts: v.callouts, selected: [...v.selected], focus: v.focus, center: [camera.center[0], camera.center[1]], zoom: camera.zoom, };}function requestRender(el: HTMLElement): void { const geo = el._geo as GeoInstance | undefined; if (!geo || geo.frameId) return; geo.frameId = requestAnimationFrame(() => { geo.frameId = 0; renderMap(el); });}function reportError(el: HTMLElement, err: unknown, url: string): void { const message = err instanceof Error ? err.message : String(err); // a chunk or the data could not be loaded - the map keeps what it has el.dispatchEvent(new CustomEvent<GeoMapErrorDetail>('geo-map-error', { detail: { message, url }, bubbles: true }));}/** the level drawn: the configured one ('auto' by zoom) */function levelFor(view: GeoView): GeoLevel { return view.level === 'auto' ? (view.zoom >= 8 ? 'state' : view.zoom >= 2 ? 'country' : 'continent') : view.level;}/** the token of the S2 cell in focus (s2:<token>), or null - also for a focus that names no valid cell */function cellFocus(view: GeoView): string | null { if (!view.focus?.startsWith('s2:')) return null; try { return s2Token(view.focus.slice(3)); } catch { return null; }}/** the cells drawn: the focused cell's four children - the S2 hierarchy one level down - else the layer's cells */function cellsDrawn(geo: GeoInstance): GeoRegion[] { const token = cellFocus(geo.view); return token ? s2Children(token).map(s2Polygon) : geo.cellRegions;}/** the regions drawn - and, around a focus, the dimmed context */function selectRegions(geo: GeoInstance, path: any): { regions: GeoRegion[]; context: GeoRegion[] } { const ds = geo.ds!; const v = geo.view; const m = ds.manifest; const zoom = v.zoom; geo.wanted = new Set(); const visible = (r: GeoRegion): boolean => !!path(zoom >= 3 ? (ds.proxies.get(r.properties.id) ?? r) : r); const statesOf = (id: string): GeoRegion[] | undefined => { if (!m) return ds.memoryStates.filter((s) => s.properties.parent === id); const p = zoom >= 24 ? (m.stateDetails?.[id] ?? m.states[id]) : m.states[id]; return p ? chunk(ds, p, geo.wanted) : []; }; const countryShape = (r: GeoRegion): GeoRegion[] => { const p = zoom >= 3 && m ? m.details[r.properties.id] : undefined; return (p && chunk(ds, p, geo.wanted)) || [r]; }; const focus = v.focus ? ds.byKey.get(v.focus) : undefined; const focusKey = v.focus; // an S2 cell in focus frames a place, not a region: the regions are drawn as without one if (focusKey && !focusKey.startsWith('s2:')) { const [level, ...rest] = focusKey.split(':'); const id = rest.join(':'); const countries = ds.countries.filter(visible); if (level === 'continent') { const inside = countries.filter((c) => c.properties.continent === id); return { regions: inside.flatMap(countryShape), context: countries.filter((c) => c.properties.continent !== id).flatMap(countryShape), }; } const countryId = level === 'country' ? id : (focus?.properties.parent ?? ds.parents.get(focusKey) ?? ''); const states = countryId ? statesOf(countryId) : undefined; const home = ds.byKey.get(`country:${countryId}`); return { regions: states?.length ? states : home ? countryShape(home) : [], context: countries.filter((c) => c.properties.id !== countryId).flatMap(countryShape), }; } const level = levelFor(v); if (level === 'continent') return { regions: ds.continents, context: [] }; const regions: GeoRegion[] = []; for (const c of ds.countries) { if (!visible(c)) continue; if (level === 'state' && zoom >= 3) { const states = statesOf(c.properties.id); regions.push(...(states && states.length ? states : countryShape(c))); } else regions.push(...countryShape(c)); } // parents with states but no 50m country (Gibraltar, Akrotiri...): their states, by the bounds index if (level === 'state' && zoom >= 3 && m) for (const id of Object.keys(m.states)) { if (ds.countryIds.has(id)) continue; const proxy = ds.proxies.get(id); if (proxy && path(proxy)) regions.push(...(statesOf(id) ?? [])); } return { regions, context: [] };}/** a region's label point: the projected centroid of its largest polygon, moved inside when it falls out */function anchorOf(path: any, projection: any, region: GeoRegion): [number, number] | null { const { d3 } = ready(); const g = region.geometry; let largest: any = g; if (g.type === 'MultiPolygon') { let best = -1; for (const coordinates of g.coordinates as LonLat[][][]) { const poly = { type: 'Polygon', coordinates }; const a = path.area(poly); if (a > best) { best = a; largest = poly; } } } const c = path.centroid(largest) as [number, number]; if (!c.every(Number.isFinite)) return null; const inside = (pt: [number, number]): boolean => { const ll = projection.invert?.(pt); return !!ll && d3.geoContains(largest, ll); }; if (inside(c)) return c; const [[x0, y0], [x1, y1]] = path.bounds(largest); let best: [number, number] | null = null; let dist = Infinity; for (let i = 1; i < 8; i++) for (let j = 1; j < 8; j++) { const pt: [number, number] = [x0 + ((x1 - x0) * i) / 8, y0 + ((y1 - y0) * j) / 8]; const d = Math.hypot(pt[0] - c[0], pt[1] - c[1]); if (d < dist && inside(pt)) { best = pt; dist = d; } } return best ?? c;}/** a length custom property of the map in px (px, rem or em), else the fallback */function cssPx(el: HTMLElement, name: string, fallback: number): number { const raw = getComputedStyle(el).getPropertyValue(name).trim(); const n = parseFloat(raw); if (!Number.isFinite(n)) return fallback; if (raw.endsWith('rem')) return n * parseFloat(getComputedStyle(document.documentElement).fontSize || '16'); if (raw.endsWith('em')) return n * parseFloat(getComputedStyle(el).fontSize || '16'); return n;}/** the callouts' gutter width in px (--geo-map-callout-width, default 10rem) */const gutterOf = (el: HTMLElement): number => cssPx(el, '--geo-map-callout-width', 160);let measure: OffscreenCanvasRenderingContext2D | null | undefined;/** a name's width in px in a font - canvas text metrics, an estimate where there is no canvas */function textWidth(text: string, font: string, size: number): number { if (measure === undefined) measure = typeof OffscreenCanvas === 'undefined' ? null : new OffscreenCanvas(1, 1).getContext('2d'); if (!measure) return text.length * size * 0.58; measure.font = font; return measure.measureText(text).width;}function renderMap(el: HTMLElement): void { const geo = el._geo as GeoInstance | undefined; if (!geo || geo.status !== 'ready' || !geo.ds) return; const { d3 } = ready(); const start = performance.now(); const v = geo.view; const W = geo.width; const H = geo.height; const gutter = v.callouts ? Math.min(gutterOf(el), W / 3) : 0; geo.box = { x0: gutter, y0: 0, x1: W - gutter, y1: H }; geo.svg.setAttribute('viewBox', `0 0 ${W} ${H}`); geo.projection = makeProjection(v.projection, geo.box, v.center, v.zoom); // the camera follows a view stopped at a pole (makeProjection), so a pan toward it does not wind up unseen if (!geo.flight && v.projection !== 'orthographic' && v.projection !== 'equalArea') { const seen = geo.projection.invert?.([(geo.box.x0 + geo.box.x1) / 2, (geo.box.y0 + geo.box.y1) / 2]); if (seen && Number.isFinite(seen[1]) && Math.abs(seen[1] - v.center[1]) > 1e-6) v.center = [v.center[0], clamp(seen[1], -85, 85)]; } const path = d3.geoPath(geo.projection); const { regions, context } = selectRegions(geo, path); geo.ds.maps.add(el); pump(); const L = geo.layers; layerPath(L.sphere!, path({ type: 'Sphere' }) ?? ''); layerPath(L.graticule!, v.graticule ? (path(d3.geoGraticule10()) ?? '') : ''); const selected = new Set(v.selected); const styles = geo.layersData.styles; geo.drawn = new Map(); let visibleRegions = 0; let contextRegions = 0; let visibleCells = 0; const reconcile = (layer: SVGGElement, items: GeoRegion[], kind: 'region' | 'context' | 'cell'): void => { const existing = new Map<string, SVGPathElement>(); for (const child of [...layer.children]) existing.set(child.getAttribute('data-key') ?? '', child as SVGPathElement); const keep = new Set<string>(); for (const region of items) { const key = kind === 'cell' ? `s2:${region.properties.id}` : keyOf(region); if (keep.has(key)) continue; const d = path(region); if (!d) continue; keep.add(key); if (kind === 'context') contextRegions++; else { geo.drawn.set(key, region); if (kind === 'cell') visibleCells++; else visibleRegions++; } let p = existing.get(key); if (!p) { p = svgEl('path', { 'data-key': key, class: kind === 'cell' ? 'geo-map-region geo-map-cell' : 'geo-map-region', }); if (kind !== 'context') { p.setAttribute('role', 'button'); p.setAttribute('tabindex', '-1'); p.setAttribute( 'aria-label', `${region.properties.name} (${kind === 'cell' ? 'S2 cell' : region.properties.level})`, ); } layer.append(p); } if (p.getAttribute('d') !== d) p.setAttribute('d', d); if (kind !== 'cell') p.setAttribute('data-tone', String(hash(region.properties.id) % 7)); if (kind !== 'context') p.setAttribute('aria-pressed', selected.has(key) ? 'true' : 'false'); const style = styles[key]; p.removeAttribute('style'); if (style) { if (style.fill !== undefined) p.style.fill = style.fill; if (style.stroke !== undefined) p.style.stroke = style.stroke; if (style.strokeWidth !== undefined) p.style.strokeWidth = String(style.strokeWidth); if (style.opacity !== undefined) p.style.opacity = String(style.opacity); if (style.dash !== undefined) p.style.strokeDasharray = style.dash; } } for (const [key, p] of existing) if (!keep.has(key)) p.remove(); }; reconcile(L.context!, context, 'context'); reconcile(L.regions!, regions, 'region'); reconcile(L.cells!, cellsDrawn(geo), 'cell'); // the roving tab stop: the selected region in view, else the first drawn const stops = [...L.regions!.children, ...L.cells!.children] as SVGPathElement[]; const stop = stops.find((p) => p.getAttribute('data-key') === geo.roving) ?? stops.find((p) => p.getAttribute('aria-pressed') === 'true') ?? stops[0]; geo.roving = stop?.getAttribute('data-key') ?? null; for (const p of stops) { const t = p === stop ? '0' : '-1'; if (p.getAttribute('tabindex') !== t) p.setAttribute('tabindex', t); } // lines setChildren( L.lines!, geo.layersData.lines.flatMap((line) => { let coordinates = line.coordinates; if (!coordinates && line.from && line.to) coordinates = geodesicPath(line.from, line.to); const d = coordinates ? path({ type: 'LineString', coordinates }) : null; if (!d) return []; const p = svgEl('path', { d, class: 'geo-map-line', 'data-line': line.id }); if (line.color) p.style.stroke = line.color; if (line.width !== undefined) p.style.strokeWidth = String(line.width); if (line.dash) p.style.strokeDasharray = line.dash; return [p]; }), ); // markers let visibleMarkers = 0; setChildren( L.markers!, geo.layersData.markers.flatMap((marker) => { const pos = resolvePosition(marker.position); if (!path({ type: 'Point', coordinates: pos })) return []; const xy = geo.projection(pos) as [number, number]; visibleMarkers++; const g = svgEl('g', { class: 'geo-map-marker', transform: `translate(${xy[0].toFixed(2)},${xy[1].toFixed(2)})`, 'data-marker': marker.id, 'data-shape': marker.shape ?? 'circle', role: 'img', 'aria-label': marker.label ?? marker.id, }); const shape = marker.shape === 'diamond' ? svgEl('path', { d: 'M0,-7L7,0L0,7L-7,0Z' }) : marker.shape === 'square' ? svgEl('rect', { x: '-5', y: '-5', width: '10', height: '10', rx: '2' }) : marker.shape === 'pin' ? svgEl('path', { d: 'M0,1C-18,-14 -5,-22 0,-22C5,-22 18,-14 0,1Z' }) : svgEl('circle', { r: '5' }); if (marker.color) shape.style.fill = marker.color; g.append(shape); if (marker.label) { const text = svgEl('text', { x: '10', y: marker.shape === 'pin' ? '-12' : '-6' }); text.textContent = marker.label; g.append(text); } return [g]; }), ); // anchors (callouts + keyboard movement) for the regions drawn geo.anchors = new Map(); const callable = [...geo.drawn].filter(([k]) => !k.startsWith('s2:')); if (callable.length <= 400) for (const [key, region] of callable) { const a = anchorOf(path, geo.projection, region); if (a && a[0] >= geo.box.x0 && a[0] <= geo.box.x1 && a[1] >= 0 && a[1] <= H) geo.anchors.set(key, a); } let calloutCount = 0; if (v.callouts) calloutCount = drawCallouts(el, geo, W, H, gutter, path); else setChildren(L.callouts!, []); drawLift(geo); geo.stats = { visibleRegions, contextRegions, visibleCells, visibleMarkers, callouts: calloutCount, renderMs: performance.now() - start, cachedChunks: geo.ds.chunks.size, loadedBytes: geo.ds.bytes, // this map's chunks still on their way (a failed one waits for its retry) loading: [...geo.wanted].filter((p) => !geo.ds!.chunks.has(p) && !geo.ds!.failed.has(p)).length, }; if (el.hasAttribute('aria-busy') !== geo.stats.loading > 0) dfDollar(el).attr('aria-busy', geo.stats.loading ? 'true' : null); // the pending fit of a focused state: its chunk has arrived if (geo.pendingFit && geo.ds.byKey.has(geo.pendingFit) && !geo.flight) { const key = geo.pendingFit; geo.pendingFit = null; flyTo(el, targetFor(geo, key), false); } writeOutputs(el); // a render finished: what it drew and what is still loading el.dispatchEvent(new CustomEvent<GeoRenderStats>('geo-map-render', { detail: { ...geo.stats } }));}function drawCallouts(el: HTMLElement, geo: GeoInstance, W: number, H: number, gutter: number, path: any): number { const layer = geo.layers.callouts!; const focus = geo.view.focus; const entries = [...geo.anchors].map(([key, anchor]) => { const region = geo.drawn.get(key)!; return { key, anchor, name: region.properties.name, area: path.area(region) as number }; }); // the rows and the names follow the label's size (--geo-map-callout-size, default 0.875rem) const size = cssPx(el, '--geo-map-callout-size', 14); const row = Math.max(18, Math.round(size * 1.6)); // measured in the hover weight, so a highlighted name still fits const font = `600 ${size}px ${getComputedStyle(el).fontFamily}`; const room = gutter - 12; const fit = (name: string): string => { if (textWidth(name, font, size) <= room) return name; let n = name.length - 1; while (n > 1 && textWidth(`${name.slice(0, n)}…`, font, size) > room) n--; return `${name.slice(0, n).trimEnd()}…`; }; const capacity = Math.max(1, Math.floor((H - 16) / row)); const mid = (geo.box.x0 + geo.box.x1) / 2; // the largest regions when there are more than the gutters hold; then west to the left gutter, east to the right - // a full side gives its regions nearest the middle to the other one, so lines do not cross the map const shown = entries.sort((a, b) => b.area - a.area).slice(0, capacity * 2); shown.sort((a, b) => a.anchor[0] - b.anchor[0]); const west = shown.filter((e) => e.anchor[0] < mid).length; const split = clamp(west, shown.length - capacity, capacity); const sides: Record<'left' | 'right', typeof entries> = { left: shown.slice(0, split), right: shown.slice(split) }; const groups: SVGGElement[] = []; for (const side of ['left', 'right'] as const) { const list = sides[side].sort((a, b) => a.anchor[1] - b.anchor[1]); const top = Math.round(size) + row; const bottom = H - 8; const ys = list.map((e) => clamp(e.anchor[1], top, bottom)); for (let i = 1; i < ys.length; i++) ys[i] = Math.max(ys[i]!, ys[i - 1]! + row); if (ys.length && ys[ys.length - 1]! > bottom) { ys[ys.length - 1] = bottom; for (let i = ys.length - 2; i >= 0; i--) ys[i] = Math.min(ys[i]!, ys[i + 1]! - row); } list.forEach((e, i) => { const y = ys[i]!; const edge = side === 'left' ? gutter : W - gutter; const outer = side === 'left' ? 6 : W - 6; const g = svgEl('g', { class: 'geo-map-callout', 'data-key': e.key, 'data-side': side }); if (focus && e.key === focus) g.setAttribute('data-focus', ''); g.append( svgEl('polyline', { class: 'geo-map-callout-line', points: `${e.anchor[0].toFixed(1)},${e.anchor[1].toFixed(1)} ${edge.toFixed(1)},${y.toFixed(1)} ${outer.toFixed(1)},${y.toFixed(1)}`, }), svgEl('circle', { class: 'geo-map-callout-dot', cx: e.anchor[0].toFixed(1), cy: e.anchor[1].toFixed(1), r: '2.5', }), ); const text = svgEl('text', { class: 'geo-map-callout-label', x: String(outer), y: (y - Math.max(4, Math.round(size * 0.3))).toFixed(1), 'text-anchor': side === 'left' ? 'start' : 'end', }); text.textContent = fit(e.name); g.append(text); if (geo.view.selected.includes(e.key)) g.setAttribute('data-selected', ''); groups.push(g); }); } setChildren(layer, groups); return groups.length;}/** the lifted copies: the hovered region, and the selection when data-highlight="lift" */function drawLift(geo: GeoInstance): void { const el = dfDollar(geo.svg).closest('.geo-map').get(0) as HTMLElement | undefined; const keys = new Set<string>(); if (el?.dataset.highlight === 'lift') for (const k of geo.view.selected) keys.add(k); if (geo.hover) keys.add(geo.hover); const existing = new Map<string, SVGPathElement>(); for (const c of [...geo.layers.lift!.children]) existing.set(c.getAttribute('data-key') ?? '', c as SVGPathElement); const keep: SVGPathElement[] = []; for (const key of keys) { const source = (dfDollar(geo.svg) .find(`.geo-map-regions [data-key="${CSS.escape(key)}"], .geo-map-cells [data-key="${CSS.escape(key)}"]`) .get(0) ?? null) as unknown as SVGPathElement | null; if (!source) continue; let copy = existing.get(key); if (!copy) copy = svgEl('path', { 'data-key': key, class: 'geo-map-lifted' }); copy.setAttribute('d', source.getAttribute('d') ?? ''); for (const a of ['data-tone', 'style', 'aria-pressed']) { const value = source.getAttribute(a); if (value === null) copy.removeAttribute(a); else copy.setAttribute(a, value); } if (key.startsWith('s2:')) copy.classList.add('geo-map-cell'); keep.push(copy); } setChildren(geo.layers.lift!, keep);}/** the camera a key frames: a built-in or configured view, else the region's main body */function targetFor(geo: GeoInstance, key: string): { center: LonLat; zoom: number } { const box = geo.box.x1 > geo.box.x0 ? geo.box : { x0: 0, y0: 0, x1: geo.width, y1: geo.height }; const bounds = settings.views[key] ?? VIEWS[key]; if (bounds) return frame(boundsProxy(key, bounds), box, geo.view.projection); const cell = cellFocus({ ...geo.view, focus: key }); if (cell) return frame(s2Polygon(cell), box, geo.view.projection); const region = geo.ds?.byKey.get(key); if (region) return frame(region.properties.level === 'continent' ? region : mainBody(region), box, geo.view.projection); // a state whose chunk is not here yet: its country first, the state once it arrives const parent = geo.ds?.parents.get(key); const home = parent ? geo.ds?.byKey.get(`country:${parent}`) : undefined; geo.pendingFit = key; return home ? frame(mainBody(home), box, geo.view.projection) : { center: geo.view.center, zoom: geo.view.zoom };}/** move the camera - a flight (great-circle center, logarithmic zoom) unless motion is reduced or `animate` is false */function flyTo(el: HTMLElement, target: { center: LonLat; zoom: number }, animate = true): Promise<void> { const geo = el._geo as GeoInstance; const { d3 } = ready(); cancelFlight(geo); const from = { center: geo.view.center, zoom: geo.view.zoom }; const to = { center: [wrapLon(target.center[0]), clamp(target.center[1], -85, 85)] as LonLat, zoom: clampZoom(target.zoom), }; if (!animate || reducedMotion() || !geo.width) { geo.view.center = to.center; geo.view.zoom = to.zoom; requestRender(el); announceView(el); return Promise.resolve(); } const interpolate = d3.geoInterpolate(from.center, to.center); const distance = d3.geoDistance(from.center, to.center); const ms = clamp(420 + distance * 260 + Math.abs(Math.log2(to.zoom / from.zoom)) * 90, 450, 1400); let t0 = -1; return new Promise<void>((resolve) => { const step = (now: number): void => { // timed by the frames: the first frame's timestamp may come before performance.now() read at the start if (t0 < 0) t0 = now; const t = Math.min(1, (now - t0) / ms); const e = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2; const c = interpolate(e) as LonLat; geo.view.center = [wrapLon(c[0]), clamp(c[1], -85, 85)]; geo.view.zoom = from.zoom * (to.zoom / from.zoom) ** e; renderMap(el); if (t < 1 && geo.flight) { geo.flight.id = requestAnimationFrame(step); return; } geo.view.center = to.center; geo.view.zoom = to.zoom; geo.flight = null; renderMap(el); announceView(el); resolve(); }; geo.flight = { id: requestAnimationFrame(step), target: to, done: resolve }; });}/** stop a flight where it is (a pan, a zoom, another flight took over); its promise resolves */function cancelFlight(geo: GeoInstance): void { if (!geo.flight) return; cancelAnimationFrame(geo.flight.id); const done = geo.flight.done; geo.flight = null; done();}function announceView(el: HTMLElement): void { const geo = el._geo as GeoInstance; const v = geo.view; const key = `${v.center[0].toFixed(4)},${v.center[1].toFixed(4)},${v.zoom.toFixed(4)}`; if (key === geo.lastView) return; geo.lastView = key; // the camera came to rest (a pan, a zoom, a flight) el.dispatchEvent( new CustomEvent<GeoMapViewDetail>('geo-map-view', { detail: { center: [v.center[0], v.center[1]], zoom: v.zoom, level: levelFor(v) }, bubbles: true, }), );}// -- markup + state ---------------------------------------------------------------------------------/** set an attribute only when it says something other than the element already says (absent = the default) */function reflect(el: HTMLElement, name: string, value: string | null, fallback: string | null): void { const current = el.getAttribute(name); if ((current ?? fallback) === (value ?? fallback)) return; dfDollar(el).attr(name, value === fallback && current === null ? null : value);}/** * The markup of a state, on the live element and on render()'s detached copy: the view options, the selection * and the focus as data-* attributes. The SVG, the tooltip and the camera are the runtime's (no markup). */function applyMarkup(el: HTMLElement, name: string, config: GeoMapConfig): void { if (config.projection !== undefined) reflect(el, 'data-projection', config.projection, 'equalEarth'); if (config.level !== undefined) reflect(el, 'data-level', config.level, 'country'); if (config.palette !== undefined) reflect(el, 'data-palette', config.palette, 'atlas'); if (config.border !== undefined) reflect(el, 'data-border', config.border, 'natural'); if (config.graticule !== undefined) reflect(el, 'data-graticule', config.graticule ? null : 'false', null); if (config.callouts !== undefined) reflect(el, 'data-callouts', config.callouts ? 'true' : null, null); const selected = name === 'default' ? [] : keyList(config.selected ?? ''); const now = keyList(el.getAttribute('data-selected') ?? ''); if (selected.join(' ') !== now.join(' ')) dfDollar(el).attr('data-selected', selected.length ? selected.join(' ') : null); const focus = name === 'focused' ? config.focus || null : null; if ((el.getAttribute('data-focus') || null) !== focus) dfDollar(el).attr('data-focus', focus);}function checkConfig(config: GeoMapConfig): void { if (config.projection !== undefined && !PROJECTIONS.includes(config.projection)) throw new RangeError(`geo-map: unknown projection "${config.projection}" (${PROJECTIONS.join(', ')})`); if (config.level !== undefined && !LEVELS.includes(config.level)) throw new RangeError(`geo-map: unknown level "${config.level}" (${LEVELS.join(', ')})`); if (config.palette !== undefined && !PALETTES.includes(config.palette)) throw new RangeError(`geo-map: unknown palette "${config.palette}" (${PALETTES.join(', ')})`); if (config.border !== undefined && !BORDERS.includes(config.border)) throw new RangeError(`geo-map: unknown border "${config.border}" (${BORDERS.join(', ')})`); if (config.center !== undefined) assertLonLat(config.center); if ( config.zoom !== undefined && (!Number.isFinite(Number(config.zoom)) || Number(config.zoom) < 1 || Number(config.zoom) > 1024) ) throw new RangeError('geo-map: zoom is 1 to 1024');}/** the state's DOM work: the markup, then the view (a new focus flies there unless the config names a camera) */function triggerStateChange( el: HTMLElement, name: string, config: GeoMapConfig, incoming: GeoMapConfig,): Promise<void> { const geo = el._geo as GeoInstance; checkConfig(config); // a cell passed as the focus must name one; a focus already on the element (written from outside) is not // re-checked, so a map that holds an invalid one can still be reset if (incoming.focus?.startsWith('s2:')) s2Token(incoming.focus.slice(3)); // entered without saying what (nothing, or an empty value - the State tab's cleared field): the selection / // focus the markup declared if (name === 'focused' && !config.focus && geo.authored.focus) config = { ...config, focus: geo.authored.focus }; if (name === 'selected' && !keyList(config.selected ?? '').length) config = { ...config, selected: geo.authored.selected }; // the state the config amounts to: 'focused' without a focus is a selection (or nothing) let landed = name; if (name === 'focused' && !config.focus) landed = keyList(config.selected ?? '').length ? 'selected' : 'default'; if (name === 'selected' && !keyList(config.selected ?? '').length) landed = 'default'; applyMarkup(el, landed, config); if (landed !== name) el.dataset.stateName = landed; return geo.ready.then(() => { const before = geo.view.focus; syncView(el, false); if (geo.status !== 'ready') return undefined; // nothing drawn: the error alert stays, the attributes are the state const camera = incoming.center !== undefined || incoming.zoom !== undefined; if (camera) return flyTo( el, { center: (incoming.center as LonLat) ?? geo.view.center, zoom: Number(incoming.zoom ?? geo.view.zoom) }, false, ); if (geo.view.focus && geo.view.focus !== before) return flyTo(el, targetFor(geo, geo.view.focus)); requestRender(el); return undefined; });}/** take the element's attributes into the view; a focus that changed flies there (when `fly`) */function syncView(el: HTMLElement, fly: boolean): void { const geo = el._geo as GeoInstance | undefined; if (!geo) return; const previous = geo.view; const next = viewFromAttributes(el, { center: previous.center, zoom: previous.zoom }); const changed = JSON.stringify({ ...next, center: 0, zoom: 0 }) !== JSON.stringify({ ...previous, center: 0, zoom: 0 }); if (!changed) return; geo.view = next; if (next.selected.join(' ') !== previous.selected.join(' ')) announceSelection(el); if (next.focus !== previous.focus) { announceFocus(el); if (fly && next.focus && geo.status === 'ready') void flyTo(el, targetFor(geo, next.focus)); } requestRender(el);}function nameOfKey(geo: GeoInstance, key: string): string { return geo.ds?.byKey.get(key)?.properties.name ?? (key.startsWith('s2:') ? `S2 ${key.slice(3)}` : key);}function announceSelection(el: HTMLElement): void { const geo = el._geo as GeoInstance; const keys = [...geo.view.selected]; // the selection changed - by a click, a key or setState el.dispatchEvent( new CustomEvent<GeoMapSelectDetail>('geo-map-select', { detail: { keys, names: keys.map((k) => nameOfKey(geo, k)) }, bubbles: true, }), );}function announceFocus(el: HTMLElement): void { const geo = el._geo as GeoInstance; const key = geo.view.focus; // the focus changed: a region framed (its children shown), or the focus cleared el.dispatchEvent( new CustomEvent<GeoMapFocusDetail>('geo-map-focus', { detail: { key, name: key ? nameOfKey(geo, key) : null }, bubbles: true, }), );}/** Registry-level API; pass the element explicitly. Unknown names throw. */export const geoMapApi = componentState({ component: 'geo-map', states: geoMapStates, mergeConfig: true, apply: (el, state, _previous, incoming) => triggerStateChange(el as HTMLElement, state.name, state.config as GeoMapConfig, incoming as GeoMapConfig), // the state the attributes and the camera show now - a click or a pan changes them without setState read: (el) => { const geo = (el as HTMLElement)._geo as GeoInstance | undefined; if (!geo) return { name: (el as HTMLElement).dataset.stateName || 'default', config: {} }; const view = viewFromAttributes(el as HTMLElement, { center: geo.view.center, zoom: geo.view.zoom }); const config = configOf({ ...geo, view: { ...view, center: geo.view.center, zoom: geo.view.zoom } }); return { name: stateNameOf(view), config: config as unknown as Record<string, unknown> }; }, markup: (el, state) => applyMarkup(el as HTMLElement, state.name, state.config as GeoMapConfig),});/** commit what the user did (a pick, a focus by double click) the way setState would have written it */function userChange(el: HTMLElement, patch: { selected?: string[]; focus?: string | null }): void { const geo = el._geo as GeoInstance; const config = { ...configOf(geo), ...patch }; const name = stateNameOf({ focus: config.focus ?? null, selected: keyList(config.selected) }); if (patch.focus !== undefined && patch.focus !== geo.view.focus) { act(el, Promise.resolve(geoMapApi.setState(el, name, { selected: config.selected, focus: config.focus ?? null }))); return; } applyMarkup(el, name, config); syncView(el, true); geoMapApi.commit(el, name, configOf(geo) as unknown as Record<string, unknown>);}// -- interaction -------------------------------------------------------------------------------------function pixelOf(geo: GeoInstance, e: { clientX: number; clientY: number }): [number, number] { const r = geo.svg.getBoundingClientRect(); return [((e.clientX - r.left) * geo.width) / (r.width || 1), ((e.clientY - r.top) * geo.height) / (r.height || 1)];}function unprojectVisible(projection: any, pixel: [number, number]): LonLat | null { const { d3 } = ready(); const p = projection.invert?.(pixel); if (!p || !p.every(Number.isFinite) || Math.abs(p[1]) > 90) return null; if (!d3.geoPath(projection)({ type: 'Point', coordinates: p })) return null; const back = projection(p); if (!back || Math.hypot(back[0] - pixel[0], back[1] - pixel[1]) > 0.1) return null; return [p[0], p[1]];}function pan(el: HTMLElement, dx: number, dy: number): void { const geo = el._geo as GeoInstance; if (!geo.projection) return; cancelFlight(geo); const delta = RAD / geo.projection.scale(); geo.view.center = [wrapLon(geo.view.center[0] - dx * delta), clamp(geo.view.center[1] + dy * delta, -85, 85)]; requestRender(el);}function zoomAt(el: HTMLElement, factor: number, pixel?: [number, number]): void { const geo = el._geo as GeoInstance; if (!geo.projection) return; cancelFlight(geo); const zoom = clampZoom(geo.view.zoom * factor); let center: LonLat = [...geo.view.center]; const before = pixel ? unprojectVisible(geo.projection, pixel) : null; // keep the place under the cursor: the projections are not linear in their center, so the correction repeats for (let i = 0; pixel && before && i < 4; i++) { const after = unprojectVisible(makeProjection(geo.view.projection, geo.box, center, zoom), pixel); if (!after) break; const dLon = wrapLon(before[0] - after[0]); const dLat = before[1] - after[1]; center = [wrapLon(center[0] + dLon), clamp(center[1] + dLat, -85, 85)]; if (Math.abs(dLon) + Math.abs(dLat) < 1e-6) break; } geo.view.center = center; geo.view.zoom = zoom; requestRender(el);}function settleSoon(el: HTMLElement): void { const geo = el._geo as GeoInstance; clearTimeout(geo.restTimer); geo.restTimer = setTimeout(() => announceView(el), 160) as unknown as number;}const keyAt = (target: EventTarget | null): string | null => { if (!(target instanceof Element)) return null; const hit = dfDollar(target).closest('[data-key]').get(0) as Element | undefined; return hit && !dfDollar(hit).closest('.geo-map-context').get(0) ? hit.getAttribute('data-key') : null;};function pick(el: HTMLElement, key: string, additive: boolean): void { const geo = el._geo as GeoInstance; const mode = el.dataset.select ?? 'single'; if (mode === 'none') return; const now = geo.view.selected; let next: string[]; if (mode === 'multiple' || additive) next = now.includes(key) ? now.filter((k) => k !== key) : [...now, key]; else next = now.length === 1 && now[0] === key ? [] : [key]; geo.roving = key; userChange(el, { selected: next });}function showTooltip(geo: GeoInstance, key: string | null, pixel?: [number, number]): void { const t = geo.tooltip; if (!key) { t.hidden = true; return; } const region = geo.drawn.get(key); if (!region) return; const parent = region.properties.parent ? geo.ds?.byKey.get(`country:${region.properties.parent}`)?.properties.name : ''; dfDollar(t).text(parent ? `${region.properties.name}, ${parent}` : region.properties.name); t.hidden = false; if (pixel) { t.style.left = `${Math.min(pixel[0] + 14, geo.width - 8)}px`; t.style.top = `${pixel[1] + 12}px`; }}function setHover(el: HTMLElement, key: string | null): void { const geo = el._geo as GeoInstance; if (geo.hover === key) return; geo.hover = key; for (const c of dfDollar(geo.svg).find('.geo-map-callout').toArray() as Element[]) c.toggleAttribute('data-hover', c.getAttribute('data-key') === key); drawLift(geo); writeOutputs(el);}/** the region nearest in an arrow's direction from the roving one (by label points) */function moveRoving(el: HTMLElement, key: string, dir: [number, number]): void { const geo = el._geo as GeoInstance; const from = geo.anchors.get(key); if (!from) return; let best: string | null = null; let score = Infinity; for (const [k, a] of geo.anchors) { if (k === key) continue; const dx = a[0] - from[0]; const dy = a[1] - from[1]; const along = dx * dir[0] + dy * dir[1]; if (along <= 0) continue; const across = Math.abs(dx * dir[1] - dy * dir[0]); const s = along + across * 2; if (s < score) { score = s; best = k; } } if (!best) return; geo.roving = best; renderMap(el); const target = dfDollar(geo.svg) .find(`[data-key="${CSS.escape(best)}"][role="button"]`) .get(0) as unknown as SVGElement | undefined; target?.focus();}function bindInteractions(el: HTMLElement): void { const geo = el._geo as GeoInstance; const svg = geo.svg; const signal = geo.abort.signal; svg.addEventListener( 'pointerdown', (e) => { if (e.button !== 0) return; geo.pointers.set(e.pointerId, pixelOf(geo, e)); geo.moved = false; }, { signal }, ); svg.addEventListener( 'pointermove', (e) => { const next = pixelOf(geo, e); const previous = geo.pointers.get(e.pointerId); if (!previous) { const key = keyAt(e.target); setHover(el, key); showTooltip(geo, key, next); return; } if (Math.hypot(next[0] - previous[0], next[1] - previous[1]) < 0.5) return; if (!svg.hasPointerCapture(e.pointerId)) svg.setPointerCapture(e.pointerId); const others = [...geo.pointers.entries()].filter(([id]) => id !== e.pointerId); if (others.length) { const other = others[0]![1]; const a = Math.hypot(previous[0] - other[0], previous[1] - other[1]); const b = Math.hypot(next[0] - other[0], next[1] - other[1]); if (a > 2 && b > 2) zoomAt(el, b / a); } else pan(el, next[0] - previous[0], next[1] - previous[1]); if (Math.hypot(next[0] - previous[0], next[1] - previous[1]) > 1) geo.moved = true; geo.pointers.set(e.pointerId, next); showTooltip(geo, null); settleSoon(el); }, { signal }, ); const release = (e: PointerEvent): void => { geo.pointers.delete(e.pointerId); }; svg.addEventListener('pointerup', release, { signal }); svg.addEventListener('pointercancel', release, { signal }); svg.addEventListener('lostpointercapture', release, { signal }); svg.addEventListener( 'pointerleave', () => { setHover(el, null); showTooltip(geo, null); }, { signal }, ); svg.addEventListener( 'click', (e) => { if (geo.moved) return; const key = keyAt(e.target); if (key) pick(el, key, e.shiftKey || e.metaKey || e.ctrlKey); }, { signal }, ); svg.addEventListener( 'dblclick', (e) => { e.preventDefault(); const key = keyAt(e.target); // a region or an S2 cell: drill into it (its children); a state, a leaf cell, the background: zoom in there const drill = key && el.dataset.drill !== 'none' && !key.startsWith('state:') && !(key.startsWith('s2:') && s2Level(key.slice(3)) === 30); if (drill) userChange(el, { focus: key, selected: [] }); else zoomAt(el, 2, pixelOf(geo, e)); settleSoon(el); }, { signal }, ); svg.addEventListener( 'wheel', (e) => { e.preventDefault(); const delta = e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? geo.height : 1); zoomAt(el, Math.exp(-delta * 0.0015), pixelOf(geo, e)); settleSoon(el); }, { signal, passive: false }, ); svg.addEventListener( 'keydown', (e) => { const key = keyAt(e.target); const arrows: Record<string, [number, number]> = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1], }; if (key && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); pick(el, key, e.shiftKey); return; } if (key && arrows[e.key] && !e.shiftKey) { e.preventDefault(); moveRoving(el, key, arrows[e.key]!); return; } if (e.key === 'Escape') { if (geo.view.selected.length) userChange(el, { selected: [] }); else if (geo.view.focus) act(el, focusUp(el)); return; } if (!['+', '=', '-', 'Home', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return; e.preventDefault(); if (e.key === '+' || e.key === '=') zoomAt(el, 1.5); else if (e.key === '-') zoomAt(el, 1 / 1.5); else if (e.key === 'Home') act(el, reset(el)); else { const [x, y] = arrows[e.key]!; pan(el, -x * 50, -y * 50); } settleSoon(el); }, { signal }, ); svg.addEventListener( 'focusin', (e) => { const key = keyAt(e.target); if (key) { geo.roving = key; setHover(el, key); } }, { signal }, ); svg.addEventListener('focusout', () => setHover(el, null), { signal });}// -- outside controls + outputs ------------------------------------------------------------------function writeOutputs(el: HTMLElement): void { if (!el.id) return; const geo = el._geo as GeoInstance; for (const out of dfDollar(`output[data-geo-for="${CSS.escape(el.id)}"]`).toArray() as HTMLOutputElement[]) { const kind = out.dataset.geoOutput ?? 'selection'; const v = geo.view; // the level of an S2 focus's children; a focus that names no valid cell has none (and must not throw here) const cell = cellFocus(v); const text = kind === 'focus' ? v.focus ? nameOfKey(geo, v.focus) : 'World' : kind === 'zoom' ? `${v.zoom < 10 ? v.zoom.toFixed(1) : Math.round(v.zoom)}x` : kind === 'level' ? v.focus ? v.focus.startsWith('s2:') ? cell ? `S2 level ${s2Level(cell) + 1}` : '' : v.focus.startsWith('continent:') ? 'country' : 'state' : levelFor(v) : kind === 'hover' ? geo.hover ? nameOfKey(geo, geo.hover) : '' : kind === 'center' ? `${v.center[1].toFixed(2)}°, ${v.center[0].toFixed(2)}°` : kind === 'regions' ? String(geo.stats.visibleRegions) : v.selected.map((k) => nameOfKey(geo, k)).join(', '); if (out.textContent !== text) dfDollar(out).text(text); }}const mapFor = (control: Element): HTMLElement | null => { const id = control.getAttribute('data-geo-for') ?? ''; const el = (id ? dfDollar(`#${CSS.escape(id)}`).get(0) : null) as HTMLElement | null; return el?.matches('.geo-map') && el._geo ? el : null;};/** a datalist of the best matches under a search input */async function suggest(input: HTMLInputElement, el: HTMLElement): Promise<GeoSearchHit[]> { const hits = await search(el, input.value, 12); let list = (input.list ?? null) as HTMLDataListElement | null; if (!list) { list = document.createElement('datalist'); list.id = `${el.id}-geo-names-${nextId++}`; list.setAttribute('data-ce-chrome', ''); dfDollar(input).after(list); input.setAttribute('list', list.id); } setChildren( list, hits.map((h) => { const o = document.createElement('option'); o.value = h.label; return o; }), ); return hits;}async function runSearch(input: HTMLInputElement, el: HTMLElement): Promise<void> { const value = input.value.trim(); if (!value) return; const hits = await search(el, value, 12); const hit = hits.find((h) => normalize(h.label) === normalize(value)) ?? hits[0]; input.setCustomValidity(hit ? '' : `No region named "${value}"`); input.toggleAttribute('aria-invalid', !hit); if (!hit) { input.reportValidity(); return; } await focusRegion(el, hit.key);}/** a control's action, run without a caller to await it: a failure is reported as the map's geo-map-error */function act(el: HTMLElement, action: Promise<unknown>): void { action.catch((err) => reportError(el, err, ''));}function bindControls(): void { if (document.__geoMapInit) return; document.__geoMapInit = true; dfDollar(document).on('click', (e: Event) => { const control = e.target instanceof Element ? (dfDollar(e.target).closest('[data-geo-for][data-geo-action]').get(0) as HTMLElement | undefined) : undefined; if (!control || control.tagName === 'INPUT') return; const el = mapFor(control); if (!el) return; const value = control.dataset.value ?? ''; const geo = el._geo as GeoInstance; switch (control.dataset.geoAction) { case 'zoom-in': zoomAt(el, 1.6); settleSoon(el); break; case 'zoom-out': zoomAt(el, 1 / 1.6); settleSoon(el); break; case 'reset': act(el, reset(el)); break; case 'up': act(el, focusUp(el)); break; case 'focus': act(el, focusRegion(el, value || geo.view.selected[0] || null)); break; case 'select': act(el, select(el, keyList(value))); break; case 'clear': act(el, select(el, [])); break; } }); dfDollar(document).on('change', (e: Event) => { const control = e.target instanceof HTMLElement ? e.target : null; if (!control?.matches('[data-geo-for]')) return; const el = mapFor(control); if (!el) return; if (control.matches('input[data-geo-action="search"]')) { act(el, runSearch(control as HTMLInputElement, el)); return; } const param = control.dataset.geoParam; if (!param) return; const value = control instanceof HTMLInputElement && control.type === 'checkbox' ? control.checked : (control as HTMLInputElement).value; const current = el.api!.getState(); act(el, Promise.resolve(geoMapApi.setState(el, current.name, { [param]: value }))); }); dfDollar(document).on('input', (e: Event) => { const input = e.target instanceof HTMLInputElement ? e.target : null; if (!input?.matches('[data-geo-for][data-geo-action="search"]')) return; const el = mapFor(input); if (!el) return; input.setCustomValidity(''); input.removeAttribute('aria-invalid'); act(el, suggest(input, el)); }); dfDollar(document).on('keydown', (e: Event) => { const ev = e as KeyboardEvent; const input = ev.target instanceof HTMLInputElement ? ev.target : null; if (ev.key !== 'Enter' || !input?.matches('[data-geo-for][data-geo-action="search"]')) return; const el = mapFor(input); if (!el) return; ev.preventDefault(); act(el, runSearch(input, el)); });}// -- the public map API ---------------------------------------------------------------------------const mapOf = (el: HTMLElement | string): HTMLElement => { const node = (typeof el === 'string' ? dfDollar(el).get(0) : el) as HTMLElement | undefined; if (!node?.matches?.('.geo-map') || !node._geo) throw new Error('not an initialized .geo-map'); return node;};/** * Pick regions (an empty list clears the selection); they are highlighted the way data-highlight says. * @param el - the .geo-map element, or a selector for it * @param keys - region keys: country:FRA, state:DEU-1591, continent:Europe, s2:47a1 * @returns resolves when the map shows them */function select(el: HTMLElement | string, keys: string[]): Promise<void> { const node = mapOf(el); const geo = node._geo as GeoInstance; const name = geo.view.focus ? 'focused' : keys.length ? 'selected' : 'default'; return Promise.resolve(geoMapApi.setState(node, name, { selected: keys }) as Promise<void>);}/** * Frame a region and show its children - a continent's countries, a country's states; everything else is dimmed. * An S2 cell (s2:<token>) frames the cell and shows its four children. A name works too (the best search hit). null * clears the focus. * @param el - the .geo-map element, or a selector for it * @param region - a key (continent:Europe, country:DEU, s2:479f), a name ('Germany'), or null * @returns resolves when the camera has arrived */async function focusRegion(el: HTMLElement | string, region: string | null): Promise<void> { const node = mapOf(el); const geo = node._geo as GeoInstance; await geo.ready; let key = region; if (key?.startsWith('s2:')) key = `s2:${s2Token(key.slice(3))}`; else if (key && !/^(continent|country|state):/.test(key)) key = (await search(node, key, 1))[0]?.key ?? null; if (region && !key) throw new Error(`geo-map: no region named "${region}"`); if (key?.startsWith('state:') && geo.ds && !geo.ds.parents.has(key) && !geo.ds.byKey.has(key)) await searchEntries(geo.ds); await geoMapApi.setState(node, key ? 'focused' : 'default', { focus: key, selected: [] });}/** * Step out of the focus: a state to its country, a country to its continent, a continent to the world; an S2 cell * to its parent, a cube face to the world. * @param el - the .geo-map element, or a selector for it * @returns resolves when the camera has arrived */async function focusUp(el: HTMLElement | string): Promise<void> { const node = mapOf(el); const geo = node._geo as GeoInstance; const key = geo.view.focus; if (!key) return reset(node); const cell = cellFocus(geo.view); if (cell) return s2Level(cell) ? focusRegion(node, `s2:${s2Parent(cell)}`) : reset(node); const region = geo.ds?.byKey.get(key); const parent = key.startsWith('state:') ? `country:${region?.properties.parent ?? geo.ds?.parents.get(key) ?? ''}` : key.startsWith('country:') && region?.properties.continent ? `continent:${region.properties.continent}` : null; if (parent && geo.ds?.byKey.has(parent)) return focusRegion(node, parent); return reset(node);}/** * Back to the whole world: no focus, no selection, the authored camera. * @param el - the .geo-map element, or a selector for it * @returns resolves when the camera has arrived */async function reset(el: HTMLElement | string): Promise<void> { const node = mapOf(el); const geo = node._geo as GeoInstance; await geo.ready; await geoMapApi.setState(node, 'default', {}); if (geo.status === 'ready') await flyTo(node, geo.home);}/** * Find regions by name: continents and countries at once, every state once the names index has loaded. Accents and * case are ignored; whole names rank before prefixes, prefixes before parts. * @param el - the .geo-map element, or a selector for it * @param query - what was typed * @param limit - the most hits (default 10) * @returns the hits, best first */async function search(el: HTMLElement | string, query: string, limit: number = 10): Promise<GeoSearchHit[]> { const node = mapOf(el); const geo = node._geo as GeoInstance; await geo.ready; if (!geo.ds) return []; return rank(await searchEntries(geo.ds), query, limit);}/** * Zoom by a factor around the center (2 = twice as close). * @param el - the .geo-map element, or a selector for it * @param factor - greater than 0 */function zoomBy(el: HTMLElement | string, factor: number): void { if (!Number.isFinite(factor) || factor <= 0) throw new RangeError('a positive zoom factor'); const node = mapOf(el); zoomAt(node, factor); settleSoon(node);}/** * Move the camera - flying there unless `animate` is false or motion is reduced. * @param el - the .geo-map element, or a selector for it * @param center - [longitude, latitude] * @param zoom - 1 to 1024 (default: the current zoom) * @param animate - fly (default true) * @returns resolves when the camera has arrived */async function setView( el: HTMLElement | string, center: LonLat, zoom?: number, animate: boolean = true,): Promise<void> { const node = mapOf(el); const geo = node._geo as GeoInstance; assertLonLat(center); await geo.ready; await flyTo(node, { center, zoom: zoom ?? geo.view.zoom }, animate);}/** * Replace what the map draws over its boundaries - markers, lines, S2 cells, per-region looks (each one given * replaces that layer; leave one out to keep it). * @param el - the .geo-map element, or a selector for it * @param layers - `{ markers, lines, cells, styles }` */function setLayers(el: HTMLElement | string, layers: GeoLayers): void { const node = mapOf(el); const geo = node._geo as GeoInstance; const next = { ...geo.layersData }; if (layers.markers) { for (const m of layers.markers) resolvePosition(m.position); next.markers = layers.markers.map((m) => ({ ...m })); } if (layers.lines) { for (const l of layers.lines) { if (l.coordinates) { if (l.coordinates.length < 2) throw new RangeError(`line ${l.id}: two positions at least`); l.coordinates.forEach(assertLonLat); } else if (!l.from || !l.to) throw new RangeError(`line ${l.id}: coordinates, or from and to`); } next.lines = layers.lines.map((l) => ({ ...l })); } if (layers.cells) { next.cells = layers.cells.map((c) => s2Token(c)); geo.cellRegions = next.cells.map(s2Polygon); } if (layers.styles) next.styles = { ...layers.styles }; geo.layersData = next; requestRender(node);}/** * The regions drawn now, with their label points. * @param el - the .geo-map element, or a selector for it * @returns one entry per region in view */function visible(el: HTMLElement | string): GeoVisibleRegion[] { const geo = mapOf(el)._geo as GeoInstance; return [...geo.drawn].map(([key, r]) => ({ key, name: r.properties.name, level: r.properties.level, anchor: geo.anchors.get(key) ?? null, }));}/** * A region by key, when the map has it (resident, or its chunk loaded). * @param el - the .geo-map element, or a selector for it * @param key - country:DEU, state:DEU-1591, continent:Europe * @returns the region, or null */function region(el: HTMLElement | string, key: string): GeoRegion | null { return (mapOf(el)._geo as GeoInstance).ds?.byKey.get(key) ?? null;}/** * Where a position is drawn in the last rendered view (SVG pixels), or null when it is out of view or behind the * globe. * @param el - the .geo-map element, or a selector for it * @param point - [longitude, latitude] * @returns [x, y] or null */function project(el: HTMLElement | string, point: LonLat): [number, number] | null { const geo = mapOf(el)._geo as GeoInstance; assertLonLat(point); if (!geo.projection || !ready().d3.geoPath(geo.projection)({ type: 'Point', coordinates: point })) return null; return geo.projection(point) as [number, number];}/** * The position drawn at an SVG pixel of the last rendered view, or null off the globe. * @param el - the .geo-map element, or a selector for it * @param pixel - [x, y] * @returns [longitude, latitude] or null */function unproject(el: HTMLElement | string, pixel: [number, number]): LonLat | null { const geo = mapOf(el)._geo as GeoInstance; return geo.projection ? unprojectVisible(geo.projection, pixel) : null;}/** * What the last render drew and loaded. * @param el - the .geo-map element, or a selector for it * @returns the counts and timings */function stats(el: HTMLElement | string): GeoRenderStats { return { ...(mapOf(el)._geo as GeoInstance).stats };}/** * The fill that hatches a region (an SVG pattern of the map) - for `styles: { 'country:DEU': { fill } }`. * @param el - the .geo-map element, or a selector for it * @returns a CSS url() fill */function hatch(el: HTMLElement | string): string { return (mapOf(el)._geo as GeoInstance).hatch;}df$.geoMapApi = geoMapApi;df$.geoMapStates = geoMapStates;// public imperative APIs (AGENTS.md "No window globals": df$.shadcn.geoMap, df$.shadcn.geo)df$.geoMap = { select, focus: focusRegion, up: focusUp, reset, search, zoomBy, setView, setLayers, visible, region, project, unproject, stats, hatch,};df$.geo = { load, configure, urls: GEO_VENDOR_URLS, WGS84, inverse, direct, path: geodesicPath, length: polylineLength, s2Cell, s2Id, s2Token, s2Level, s2Center, s2Vertices, s2Polygon, s2Neighbors, s2Children, s2Parent, resolvePosition, inBounds, mortonEncode, mortonDecode, mortonKey, mortonIndex, routeGraph, positionAlongRoute, decode, features, rewind,};// -- init ----------------------------------------------------------------------------------------------function layersFrom(el: HTMLElement): Required<GeoLayers> { const script = dfDollar(el).children('script.geo-map-layers').get(0) as HTMLScriptElement | undefined; const empty = { markers: [], lines: [], cells: [], styles: {} }; if (!script) return empty; const json = JSON.parse(script.textContent || '{}') as GeoLayers; return { ...empty, ...json } as Required<GeoLayers>;}function dataUrl(el: HTMLElement): string { const raw = el.dataset.src || metaContent('geo-map-data') || settings.data || releaseData(); return new URL(raw, document.baseURI).href;}function mount(el: HTMLElement): void { const camera = { center: parseCenter(el.dataset.center) ?? HOME, zoom: clampZoom(Number(el.dataset.zoom) || 1), }; // runtime chrome (data-ce-chrome): the HTML Preview Editor never writes it into a source const svg = svgEl('svg', { 'data-ce-chrome': '', class: 'geo-map-svg', role: 'group', tabindex: '0', 'aria-roledescription': 'map', 'aria-label': el.getAttribute('aria-label') || 'Interactive map. Arrow keys pan, plus and minus zoom, Home resets.', }); const pattern = svgEl('pattern', { id: `geo-map-hatch-${nextId++}`, width: '8', height: '8', patternUnits: 'userSpaceOnUse', }); pattern.append(svgEl('path', { d: 'M-2,2L2,-2M0,8L8,0M6,10L10,6', class: 'geo-map-hatch' })); const defs = svgEl('defs'); defs.append(pattern); svg.append(defs); const layers: Record<string, SVGGElement> = {}; for (const name of ['sphere', 'graticule', 'context', 'regions', 'cells', 'lines', 'lift', 'markers', 'callouts']) { layers[name] = svgEl('g', { class: `geo-map-${name}` }); if (['sphere', 'graticule', 'context', 'lift', 'lines'].includes(name)) layers[name]!.setAttribute('aria-hidden', 'true'); svg.append(layers[name]!); } const tooltip = document.createElement('output'); tooltip.className = 'geo-map-tooltip'; tooltip.setAttribute('data-ce-chrome', ''); tooltip.hidden = true; tooltip.setAttribute('aria-hidden', 'true'); let layersData: Required<GeoLayers>; let layerError = ''; try { layersData = layersFrom(el); } catch (err) { layersData = { markers: [], lines: [], cells: [], styles: {} }; layerError = `script.geo-map-layers is no valid JSON: ${err instanceof Error ? err.message : err}`; } const geo: GeoInstance = { view: viewFromAttributes(el, camera), ds: null, status: 'loading', ready: Promise.resolve(), svg, layers, tooltip, hatch: `url(#${pattern.id})`, width: el.clientWidth || 800, height: el.clientHeight || 480, projection: null, box: { x0: 0, y0: 0, x1: 0, y1: 0 }, frameId: 0, flight: null, pendingFit: null, pointers: new Map(), moved: false, drawn: new Map(), anchors: new Map(), hover: null, roving: null, layersData, cellRegions: [], wanted: new Set(), stats: { visibleRegions: 0, contextRegions: 0, visibleCells: 0, visibleMarkers: 0, callouts: 0, renderMs: 0, cachedChunks: 0, loadedBytes: 0, loading: 0, }, abort: new AbortController(), resize: new ResizeObserver((entries) => { const r = entries[0]!.contentRect; if (r.width > 0 && r.height > 0 && (r.width !== geo.width || r.height !== geo.height)) { geo.width = r.width; geo.height = r.height; requestRender(el); } }), attrs: new MutationObserver(() => syncView(el, true)), lastView: '', home: camera, authored: { focus: el.dataset.focus || null, selected: keyList(el.dataset.selected ?? '') }, restTimer: 0, }; el._geo = geo; dfDollar(el).append(svg).append(tooltip); el.dataset.geoStatus = 'loading'; dfDollar(el).attr('aria-busy', 'true'); geo.ready = (async () => { try { await load(); try { layersData.cells = layersData.cells.map((c) => s2Token(c)); geo.cellRegions = layersData.cells.map(s2Polygon); for (const m of layersData.markers) resolvePosition(m.position); } catch (err) { layersData.cells = []; layersData.markers = layersData.markers.filter((m) => { try { resolvePosition(m.position); return true; } catch { return false; } }); layerError ||= `script.geo-map-layers: ${err instanceof Error ? err.message : err}`; } geo.ds = await openDataset(dataUrl(el)); geo.ds.maps.add(el); geo.status = 'ready'; el.dataset.geoStatus = 'ready'; // a focused map without an authored camera opens framed on its focus if (geo.view.focus && !el.dataset.center && !el.dataset.zoom) { geo.box = { x0: 0, y0: 0, x1: geo.width, y1: geo.height }; const target = targetFor(geo, geo.view.focus); geo.view.center = target.center; geo.view.zoom = target.zoom; } renderMap(el); announceView(el); if (layerError) { // the layers block is broken: the map draws without what it could not read, and says so dfDollar(el).append( dfDollar('<output class="geo-map-error" role="alert" data-ce-chrome></output>').text(layerError), ); reportError(el, new Error(layerError), ''); } } catch (err) { geo.status = 'error'; el.dataset.geoStatus = 'error'; dfDollar(el).attr('aria-busy', null); const message = err instanceof Error ? err.message : String(err); dfDollar(el).children('.geo-map-error').remove(); dfDollar(el).append( dfDollar('<output class="geo-map-error" role="alert" data-ce-chrome></output>').text( `The map could not be loaded. ${message}`, ), ); reportError(el, err, ''); } })(); geo.resize.observe(el); geo.attrs.observe(el, { attributes: true, attributeFilter: [ 'data-projection', 'data-level', 'data-palette', 'data-border', 'data-graticule', 'data-callouts', 'data-selected', 'data-focus', 'data-highlight', ], }); bindInteractions(el);}function init(): void { (dfDollar('.geo-map:not([data-init])').toArray() as HTMLElement[]).forEach((el) => { el.dataset.init = ''; bindControls(); mount(el); const geo = el._geo as GeoInstance; // el.store + el.api (AGENTS.md "State through stores"): the state the attributes declare const name = stateNameOf(geo.view); bindComponent(el, geoMapApi, { name, config: configOf(geo) as unknown as Record<string, unknown> }); // the first state change waits for the engines and the data: el.api.settled() resolves once the map is drawn void geoMapApi.setState(el, name, {}); });}init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub