Skip to content

A world map, and clicking a country to select it - #82

Merged
ralyodio merged 2 commits into
mainfrom
feat/world-map
Sep 9, 2026
Merged

A world map, and clicking a country to select it#82
ralyodio merged 2 commits into
mainfrom
feat/world-map

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 9, 2026

Copy link
Copy Markdown
Contributor
ui.worldMap({ onSelect: country => console.log(country?.name) });

bun examples/world.ts for the clickable version — hover, click, z to zoom to the selection, r to go back.

Drawing it is nearly free

Once the canvas draws in the caller's own coordinates (#80), longitude and latitude are just another pair of axes. The world is 258 polylines in degrees and needs no projection of its own beyond choosing which window on the globe to show.

Clicking it is the part worth building

A click arrives as a terminal cell; a country is a polygon. Answering "what did they click" means turning the cell back into degrees and ray-casting it against the outlines.

Bounding boxes would be far cheaper and consistently wrong: Russia's box covers most of the northern hemisphere and Chile's covers Argentina. The lookup is exact, and open water comes back as nothing rather than as whichever box happened to be first.

The inverse has to agree with the drawing, or every click lands somewhere the user did not point at — and nothing about the map would look wrong while it did. So degreesAt takes the cell centre through the same 2×4 Braille denominators the canvas projects with, and a test round-trips all 2,600 cells of a 100×26 map back to themselves.

The data

Natural Earth 1:110m Admin 0 countries, which is public domain, simplified by Douglas-Peucker to one degree. A world map across eighty columns is about 160 Braille pixels wide, so one pixel is two degrees — one degree of tolerance is already finer than the terminal can show. That keeps 171 of the 177 countries in 1,982 points; the six it drops are island states smaller than a pixel, which could be neither seen nor clicked.

scripts/generate-world.ts emits the dataset for every port from that one source, so the six copies cannot drift: regenerating rewrites all of them or none. Roughly 230KB in total, 32–44KB per language, all derived rather than pasted.

C++ gets the coordinates as a flat pool with index tables rather than nested initialiser lists — the nested form is the same data and minutes of compile time, and C++ is the one language where that bites.

Rings are drawn closed, so a coastline has no seam where the ring began.

Agreement is checked, not assumed

Every port ships a world-probe that prints what the lookup answers for thirteen places — nine capitals, four patches of open ocean — plus four cell clicks and three raw projections. All five native ports diff clean against the TypeScript reference.

That catches what the render fixtures cannot: a country lookup can be wrong in a way that still draws a perfectly good-looking map.

Three fixtures pin the drawing itself — the whole globe, a window zoomed on Japan, and two highlighted countries.

Verified

  • 353 TypeScript tests under both bun test and node --test, typecheck clean
  • 93 widget scenes matching the reference in Rust, Go, Python, Zig and C++ (was 90)
  • five world probes diffing clean against TypeScript
  • 11/11 ctest, every widget gallery, next build, the benchmark

Not in the website widget gallery: that promises a runnable example in all eleven languages, and the four bridge languages reach the library through a narrower core.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy

ralyodio and others added 2 commits September 9, 2026 00:07
  ui.worldMap({ onSelect: country => console.log(country?.name) });

The map is the canvas doing what it already does. Once it draws in the
caller's own coordinates, longitude and latitude are just another pair of
axes, so the world is 258 polylines in degrees and needs no projection of its
own beyond choosing which window on the globe to show.

The half worth building is the other direction. A click arrives as a terminal
cell, and a country is a polygon, so answering "what did they click" means
turning the cell back into degrees and ray-casting it against the outlines.
Doing it with bounding boxes would be far cheaper and consistently wrong:
Russia's box covers most of the northern hemisphere and Chile's covers
Argentina. The lookup is exact, and open water comes back as nothing rather
than as whichever box happened to be first.

The inverse has to agree with the drawing or every click lands somewhere the
user did not point at -- and nothing about the map would look wrong while it
did. So `degreesAt` takes the cell centre through the same 2x4 Braille
denominators the canvas projects with, and a test round-trips all 2600 cells
of a 100x26 map back to themselves.

The data is Natural Earth 1:110m Admin 0, which is public domain, simplified
by Douglas-Peucker to one degree. A world map across eighty columns is about
160 Braille pixels wide, so a pixel is two degrees and one degree of tolerance
is already finer than the terminal can show; it keeps 171 of the 177 countries
in 1982 points. The six it drops are island states smaller than a pixel, which
could be neither seen nor clicked. `scripts/generate-world.ts` fetches and
regenerates it, so the 36KB of coordinates is derived rather than pasted.

Rings are drawn closed, so a coastline has no seam where the ring began.

TypeScript only. The shape and the lookup port cleanly, but the dataset would
be a seventh copy in every port, and that is a call about repo weight rather
than about the feature -- so it is not in the widget gallery either, which
promises a runnable example in all eleven languages.

`bun examples/world.ts` for the clickable version: hover, click, z to zoom to
the selection, r to go back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
The generator now emits the dataset for every port from the one Natural Earth
source, so the six copies cannot drift: regenerating rewrites all of them or
none of them. Roughly 230KB in total, 32-44KB per language, all derived.

C++ gets the coordinates as a flat pool with index tables rather than nested
initialiser lists. The nested form is the same data and minutes of compile
time, and C++ is the one language where that bites; the pool is turned into
the shape the rest of the code wants once, on first use.

Agreement is checked rather than assumed. Every port ships a `world-probe`
that prints what the lookup answers for thirteen places -- nine capitals, four
patches of open ocean -- plus four cell clicks and three raw projections, and
all five ports diff clean against the TypeScript reference. That catches what
the render fixtures cannot: a country lookup can be wrong in a way that still
draws a perfectly good-looking map.

Three fixtures pin the drawing itself: the whole globe, a window zoomed on
Japan, and two highlighted countries. 93 widget scenes now, up from 90.

Verified: 353 TS tests under bun and node; 93 widget scenes matching in Rust,
Go, Python, Zig and C++; five world probes diffing clean against TypeScript;
11/11 ctest; every gallery; the site and the benchmark.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
@ralyodio
ralyodio merged commit 543f759 into main Sep 9, 2026
18 checks passed
@ralyodio
ralyodio deleted the feat/world-map branch September 9, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant