What it is
A scroll story over a dark world map of scheduled air routes: about 66,000 non-stop airline routes between 3,100 airports, taken from the OpenFlights data. Five chapters move the camera and switch between three views of the same data (arcs, hub columns, a heatmap); the last chapter is for exploring: search any airport to see where it flies, filter by airline, or click an airport on the map. The route list is a 2014 snapshot, so it describes that network, not today's.
Engineering decisions
- Two renderers behind one interface (
layers.ts). By default the data is drawn with MapLibre's own layers: great-circle lines coloured by weight, hexagons binned in code (hexbin in network.ts) and extruded, MapLibre's heatmap and circle layers. The MapLibre | deck.gl switch downloads deck.gl only when picked (about 230 KB more), which draws GPU arcs with a two-colour gradient and deck.gl's own hexagon and heatmap layers. Arc caps differ per renderer (MapLibre has to tile every line). - With deck.gl, the layers are drawn inside MapLibre's WebGL context (
MapboxOverlay in interleaved mode), so labels and arcs share one depth buffer and one frame. - The basemap is OpenFreeMap's vector tiles (no API key). The style is recoloured in code when it loads: land, water and borders take the example's palette, and every layer except borders and place names is hidden, so the data is what you see.
- The data is preprocessed by
scripts/build-night-routes-data.mjs into one compact JSON file (airports and airlines as rows, routes as flat index triples, about 280 KB gzipped) that loads in parallel with the map. The module that reads it (network.ts: decode, filter, rank, summarise, search, great-circle distance and interpolation) is plain TypeScript with unit tests. - Arcs are airport pairs, not individual routes: both directions and all airlines on a pair become one arc whose width and brightness grow with the number of airline routes. The heaviest pairs are drawn first and the count is capped (14,000 on capable devices, 5,000 on the low tier).
- The moving dots are a few hundred points walked along their great circles on the CPU each frame and drawn as one scatterplot layer; the loop runs only while the arcs view is on screen, the tab is visible and motion is allowed.
- Hub columns use deck.gl's
HexagonLayer (route count summed per cell); the heatmap uses HeatmapLayer weighted by the square root of the route count so a few mega-hubs do not wash out everything else. - The map is flat (mercator). MapLibre's globe works, but deck.gl 9.4 does not yet draw arcs correctly on it in interleaved mode.
- deck.gl 9.4 reads
map.transform, which MapLibre 6 moved behind its camera; RouteMap.ts exposes the same object again until deck.gl supports v6. MapLibre's worker is copied to public/vendor/ before dev and build (scripts/copy-vendor.mjs) and passed to setWorkerUrl, because the production build does not emit it. - The page scrolls through the map, so the map uses cooperative gestures: Ctrl/⌘ + wheel zooms and two fingers pan on touch. With the map focused, MapLibre's own keyboard controls work (arrows, plus, minus).
- Everything to read is HTML: chapter text, the airport card (destinations, airlines, most-served routes with distances, longest route) and a live region that announces the selection.
Performance notes
MapLibre loads only on this page; deck.gl only after the switch. Quality tiers: low (phones, 4 cores or fewer, or ?quality=low) caps the pixel ratio at 1, turns off antialiasing and draws fewer arcs and dots. The map, deck.gl and their GPU resources are released when the page unmounts.
Reduced motion
No moving dots, the camera jumps between chapters instead of flying, and the text does not animate in. Everything else works the same.
Data and credits
Airports, airlines and routes: OpenFlights (openflights.org/data), Open Database License. Basemap: OpenFreeMap, © OpenMapTiles, © OpenStreetMap contributors.