@gum-jsx/maps
v2.0.0
Published
Static geographic maps for Gum.
Readme
@gum-jsx/maps
Static projected maps for Gum. The package accepts GeoJSON or TopoJSON,
projects geometry with d3-geo, and draws it through Gum's vector path system.
It does not fetch data while rendering.
For complete, offline figures, see the Maps gallery in the main docs.
Bundled geography
The package includes two small TopoJSON atlases, available without file loading or network access:
| Accessor | Source | Feature IDs |
| --- | --- | --- |
| world_countries(options?) | world-atlas 2.0.2, 1:110m countries | Three-digit country IDs, plus three stable local IDs |
| us_states(options?) | us-atlas 3.0.1, 1:10m states and territories | Two-digit state FIPS IDs |
Each call returns a fresh TopoJSONSource with its version and source URL in
provenance. You can inspect or modify its data without affecting later calls.
See bundled data for the original files, licenses, and local IDs.
Pass { ids: ['276', '040'] } to world_countries to select Germany and Austria,
or { ids: ['06', '32'] } to us_states for California and Nevada. IDs are exact
strings, including leading zeros. Unknown IDs are errors; repeated IDs select a
feature once and source order is retained. Omit ids for everything, or use
ids: [] for no features. Filtering preserves shared TopoJSON arcs and rebuilds
borders from the selected geometries. GeoDataOptions describes this option.
import { render_element, px } from '@gum-jsx/core'
import { GeoMap, world_countries } from '@gum-jsx/maps'
const world = world_countries()
const result = render_element(new GeoMap({
source: world,
width: px(900),
height: px(500),
fit_to: 'sphere',
padding: px(12),
background: '#dceef7',
styles: { '840': { fill: '#4079ad' } },
border_color: '#ffffff',
border_width: px(0.7),
}))
if (result.kind === 'svg') await Bun.write('world.svg', result.svg)The CLI includes the map elements and accessors by default, alongside math.
For example, save this as world.jsx:
<GeoMap
source={world_countries()}
width={px(900)}
height={px(500)}
background={interp(white, blue, 0.15)}
styles={id => id === '840' ? { fill: blue } : undefined}
/>Then render it with the CLI:
gum world.jsx -o world.svgGum .jsx files use the plugin's exports directly, without package imports.
For an embedded evaluator, import the package with
import * as maps from '@gum-jsx/maps', then construct
new Evaluator({ scope: maps }). In JSX, write dashed property names such as
fit-to and border-color.
Your own source
geojson(data) expects RFC 7946 longitude/latitude coordinates and winding by
default. It stitches antimeridian and polar cuts and changes polygon winding for
D3's spherical pipeline. Use { winding: 'd3' } only for GeoJSON that already
uses D3/TopoJSON's convention. topojson(data, objectName) reads a named
Topology object and retains its shared arcs for one-pass borders.
Both constructors accept ids too: geojson(data, { ids: ['A', 'B'] }) or
topojson(data, 'regions', { ids: ['A', 'B'] }). Filtering honors id_property
and does not mutate the input. Numeric source IDs are matched as strings;
features without explicit IDs cannot be selected by generated positional IDs.
Use the IDs present in your own source. The example key 840 is the numeric
ISO country ID used by some world TopoJSON files. Style dictionaries reject unknown
IDs, and all features must have explicit IDs when feature styles are used. For GeoJSON whose
IDs live in properties, set id_property, for example
geojson(data, { id_property: 'ADM0_A3' }). Name matching is left to the data
preparation step.
Feature styles
styles accepts either (id: string) => GeoStyle | undefined or an ID-to-style
dictionary. For example, use styles: id => ({ fill: colors[id] }), or
styles: { '840': { fill: '#4079ad', stroke: '#263238', stroke_width: px(2) } }.
Missing entries, undefined callback results, and omitted fields inherit the map's
style. The exported GeoStyle, GeoStyleMap, and GeoStyles types describe
these forms.
Styles support Gum paint properties, including fill, opacity, stroke,
stroke_width, stroke_dasharray, caps and joins, plus point_radius for point
features. Theme colors and px/em/fractional lengths resolve during layout.
Feature strokes are drawn after the shared borders. Shared borders keep their
map-wide paint; set border_mode: 'none' to draw only feature outlines, whose
adjacent edges can overlap.
Callbacks are evaluated once per feature when constructing a map with source;
their results are snapshotted and are not reevaluated during resizing. With
source_resource, use a style dictionary so the element remains an immutable
description independent of a particular layout pass.
Projection and layout
| Preset | Good starting point |
| --- | --- |
| naturalEarth1 (default) | General world illustrations |
| equalEarth | World thematic maps with comparable areas |
| albersUsa | United States with Alaska and Hawaii insets |
| orthographic | Globe views |
| equirectangular | Simple rectangular world views |
| mercator | Views where Mercator is specifically wanted |
The default fit target is sphere, except for albersUsa, which fits data.
Set fit_to: 'data' for a regional map or
fit_to: ['feature-id'] for a stable selected extent. The fit target
does not depend on feature styles.
Use bounds: [west, south, east, north] to fit a coordinate box in
longitude/latitude degrees, independently of source features. Bounds fit a
sampled geographic rectangle under the chosen projection and clip the map to its
projected outline, including water, borders, and child elements. Fitting preserves
proportions: a narrow region in a wide allocation leaves transparent space beside
it. Source features remain intact; clipping only limits the rendered view.
Combine source selection and bounds fitting:
<GeoMap
source={world_countries({ ids: ['276', '040'] })}
bounds={[5, 45, 18, 56]}
background={lightgray}
/>Longitudes must be in [-180, 180], latitude limits in [-90, 90] with south
less than north, and longitude span must be positive. West greater than east
crosses the antimeridian; [170, -20, -170, 20] with rotate={[-180, 0, 0]}
gives a continuous Pacific view. Bounds preserve the chosen rotation and clipping,
and fit only their visible portion. A target with no visible extent is an error.
Use [-180, south, 180, north] for a full longitude span. The exported GeoBounds
type names the four components. When supplied, bounds overrides fit_to for
fitting, clipping, and natural sizing; the unused fit target is not looked up.
Without bounds, fit_to accepts only 'sphere', 'data', or an array of IDs.
An empty selection can still use sphere or bounds fitting, but cannot fit to data.
Set center: [longitude, latitude] to pan that geographic point to the viewport
midpoint after fitting. The fit target still determines the scale; panning can
move some fitted geometry outside the viewport. Omit center to keep the fit
target centered automatically. center does not turn the globe or change which
hemisphere is visible. Use rotate: [-longitude, -latitude, 0] to face a location
on an orthographic globe. When both are supplied, center still refers to the
original geographic coordinates.
rotate, clip_angle, and precision follow D3's degree/pixel conventions.
albersUsa has fixed center, rotation, and clipping; it excludes US territories
beyond the lower 48 states, Alaska, and Hawaii.
The map derives its natural proportions from the projected fit target, including
rotation and clipping, and adds padding around that extent. Specify only
height to derive the width, or only width to derive the height. Box, Frame,
and stacks can hug the resulting size. With no dimensions or offers, the map fits
within 720 × 400 px. An explicit aspect overrides the preferred outer ratio;
two exact dimensions retain their allocated rectangle. Explicit width and
height use ordinary Gum sizing. Padding defaults to 0. padding, border_width, and
point_radius use Gum lengths; px() makes the intended unit unambiguous.
Nest core marks and annotations inside GeoMap. Use {lon, lat} records in
degrees for mark coordinates and direct-child pos values. The aliases
[longitude, latitude] and {x: longitude, y: latitude} also work. Both
components are required; mixing lon/lat with x/y in one record is an error.
Points, sampled Arrow routes, and labels follow the map's fit,
center, rotation, padding, and resizing. Hidden points are omitted and sampled
paths break at them. Child marker sizes, arrowheads, and text remain layout
lengths. Set route strokes explicitly, since children inherit map styles.
<GeoMap source={world_countries()} background="lightblue">
<Points points={[{lon: 2.35, lat: 48.86}]} point-size={px(8)} fill="red" />
<Text pos={{lon: 2.35, lat: 48.86}} anchor={['start', 'end']}>Paris</Text>
</GeoMap>Core projections map only supplied points; they do not resample paths or
split antimeridian crossings. Supply sampled routes and separate marks at seams
as needed. space="local" opts marks out; a Cartesian pos such as
[px(12), px(24)] positions annotations in local space. Local lengths require
x and y; geographic components must be numeric. GeoJSON, TopoJSON, center,
and the geographic projection helpers retain their established array formats.
For a point annotation outside the map subtree, project_geo_point(source, view, width, height,
[longitude, latitude]) returns local pixel coordinates or null if hidden by
the projection's spherical clipping. Bounds and viewport clips apply to the
GeoMap subtree; external annotations need their own clipping.
Pass the same projection, fit target, and padding to this helper as to GeoMap.
The helper's padding value is a number of pixels because it does not
run within Gum layout.
Borders and large sources
background fills the projected sphere behind the features, providing a water
color while leaving the area outside the projection transparent. It defaults to
none and accepts the same color strings and theme paints as other backgrounds.
The fill follows the map's projection, rotation, clipping, and fitted extent;
it forms a circle for an orthographic globe. With bounds fitting, the sphere is
cropped to the projected geographic box; all maps also clip to their allocated
rectangle. Albers USA uses its composite projection's clip
regions. This fills gaps and polygon holes in the supplied geography, so lakes
appear only where the source leaves them unfilled. Use an outer Box background
to give the surrounding rectangle a separate color.
border_mode accepts all (default), interior, or none. With TopoJSON,
the chosen borders are drawn once from a shared-arc mesh. GeoJSON can draw all
feature edges, but adjacent edges are repeated; interior requires TopoJSON.
Fills are always drawn before borders. Points render as small circular paths.
One GeoMap owns one source. For a large source used in several maps, install
it once on a LayoutPass and pass source_resource rather than copying it into
each map's immutable props:
import { LayoutPass, render_element } from '@gum-jsx/core'
import { prepare_geo_source } from '@gum-jsx/maps'
const prepared = prepare_geo_source(world)
const pass = new LayoutPass({ geography: { value: prepared, version: '2026-09' } })
const result = render_element(new GeoMap({
source_resource: 'geography',
width: px(900),
height: px(500),
}), { pass })Data preparation
For general world and regional illustrations, start with a versioned
Natural Earth Admin 0 countries
or Admin 1 states/provinces dataset. Choose 110m for a small world locator,
50m for a page-sized map, or 10m for a detailed region. Record the scale,
release, boundary viewpoint, and original URL in provenance. Convert the
source to RFC 7946 GeoJSON or to geographic TopoJSON, preserving a stable ID
field. Inspect disputed boundaries against the source's stated viewpoint.
For current US state and county maps, the Census cartographic boundary files are another source. Prepare the files outside rendering and commit or pin the result so a rerun uses the same geometry. Use topology-preserving simplification when producing a compact TopoJSON file; projection precision is a separate control for the curves generated at render time.
Performance
Run bun run perf in this package to measure bundled data, source preparation,
projections, clipping, and SVG output. Use --list, --filter <regex>, --smoke,
or --json to narrow or save a run.
See performance workloads and methodology.
