← Station

A Themed World Map With No Map Library, No Tiles, No API Key

BLIP · · Engineering · 2 min read

The PLACES tab needed a map of ~119 cities that recolors with the site's four themes. Leaflet and Mapbox can't do that — raster tiles ignore your CSS. So I rendered the map as plain SVG with d3-geo at build time: one <path> for all land, a <circle> per city, colored entirely by --field-ink / --field-bg. It themes for free because it's just ink on a background.

The PLACES tab on /telemetry plots ~119 cities on a world map. The default move is Leaflet or Mapbox. Both are wrong here for the same reason: the site has four themes — dark, blueprint, plaque, light — and everything recolors through two CSS variables, --field-ink and --field-bg. A raster tile map is a stack of PNGs from someone else’s server. It cannot pick up your CSS. The map would sit there in Mapbox blue while the rest of the page turned into a blueprint.

So I didn’t use a map. I rendered one as SVG.

d3-geo does the projection at build time — it never ships to the browser. Equirectangular projection, fit to a 1000×500 box, fed the world-atlas 110m topojson (595 arcs, 177 countries). geoPath collapses the entire landmass into a single path string:

const projection = geoEquirectangular().fitSize([1000, 500], world);
const pathGen = geoPath(projection);
export const WORLD_PATH = pathGen(world) ?? "";   // one <path d="…">

City pins are the same projection applied to coordinates — projection([lng, lat]) → an [x, y] in the same space as the land:

const xy = projection([p.lng, p.lat]);  // → <circle cx cy>

The whole map is then <path class="tele-map-land"> plus a <circle> per city. Nothing in it has a hardcoded color — the land fill, the pin fill, the current-city dot all reference --field-ink. Cycle the theme and the map changes with everything else, instantly, because it’s drawing with the same ink as the text next to it.

Interaction without a map library is cheaper than it sounds. d3-zoom + d3-selection (a few KB, the only client-side cost) handle pan, scroll-zoom, and tooltips. The INDIA / WORLD toggle is just a viewBox swap — no re-projection, no tile reload. Because an equirectangular projection is linear, land and pins stay aligned at every zoom level, so “zoom to India” is one precomputed viewBox string:

const nw = projection([67.5, 37.6]);   // NW corner of India
const se = projection([98.5, 6.5]);    // SE corner
// → "679.5 128.7 102.1 102.4", computed once

Tally: 0 tiles, 0 API keys, 0 runtime map dependency on the server (the path is a static string baked at build), a few KB of d3 on the client for the pan/zoom. Against Leaflet’s ~140KB-plus-tiles, for a map that does exactly what this one needs to and themes itself.

A map is a hard problem when you need every road on Earth. When you need a styled silhouette of the world with dots on it, it’s a <path> and some <circle>s — and keeping it that primitive is what let it inherit the theme.

// Discussion

Comments are powered by GitHub Discussions via Giscus. Sign in with your GitHub account to add a reply, or discuss on X.

Keyboard Shortcuts

// navigate
1 2 3
Manifest · Station · Archive
Cycle sheets
// go to (press g, then…)
g h
Home
g s
Station
g a
Artifacts
g e
Telemetry
g n
Now
g w
Watching
g r
Reading
g u
Uses
g m
Playlist
g c
Contact
g o
Colophon
// station
[ ]
Switch stream (blips / broadcasts)
/
Focus search
// reading a post
Older · newer post
k j
Older · newer post
// general
t
Cycle theme
?
Toggle this panel
Esc
Close panel