We set out to draw a metro map. We ended up building a system that draws any metro map. The distinction matters.
The Engine Is Dumb
Our rendering engine asks three questions at every station on every line:
How many lines share this segment? → offset.
Is there an angle change? → straight or bezier.
How many lines serve this station? → dot, pill, or transfer pill.
That's the whole decision tree. It doesn't know about junctions, corridors, branches, or merges. It doesn't know if the network has 5 stations or 500. It makes the same three decisions, in the same order, every time. The complexity lives in the data, not in the code.
This is, we think, the most important lesson of the guide. A dumb engine with rich data beats a smart engine every time. The dumb engine is debuggable. The dumb engine is predictable. The dumb engine works on the first try for any new dataset because it doesn't have special cases that break.
Connect the Dots
The core insight didn't come from mathematics or computer science. It came from looking at the problem like a child with a coloring book.
You know where the stations are. You know which lines connect them. Connect the dots.
We tried the sophisticated approach first — corridor detection, computational geometry, perpendicular offset curves with miter join handling. It worked, technically. But it was fragile, complex, and unmaintainable. The coloring book approach was simpler, more robust, and produced better results.
The lesson isn't "don't use math." The bezier smoothing is math. The perpendicular offset is math. The lesson is: use math in service of a simple mental model, not as a substitute for one.
One Idea, Three Applications
"Perpendicular to the track direction" appeared in three different contexts:
Chapter 2 — label placement (push labels away from the track)
Chapter 4 — parallel line offset (space multiple lines apart)
Chapter 5 — station pill orientation (rotate pills to span tracks)
One geometric idea, applied three ways. If you understand "perpendicular to the track," you understand transit map rendering. Everything else is detail.
How did I start this guide
I am building a real time underground tracker, and when working on the maps I thought of coloring books (thanks to my kids!). The "wait, why am I overcomplicating this?" question. It's just connecting dots, right?
So I did some research, played around with examples, studied my city's metro map... and I did a lot of experiments! Here's the result.
Your City
We built Metro Lumina — a fictional network that doesn't exist. But the code doesn't know that. It draws whatever data you give it.
Over 2,500 transit agencies publish GTFS data. Tokyo. London. New York. São Paulo. Berlin. Melbourne. Mexico City. Cairo. Your city, probably. The data is free. The parser is 30 lines. The renderer is the same one you've been reading about for twelve chapters.
Pick your city. Find its GTFS feed. Draw your own transit map. Share it.
The stations are waiting. Connect the dots!
Resources
We're sharing two things from this guide that you can use in your own projects:
Transit Engine — The complete rendering engine built throughout this guide. Drop it into any page, call TransitEngine.begin(), feed it your data, and you've got a transit map. Zero dependencies, ~880 lines, vanilla JavaScript.
Claude Skill for Transit Maps — A skill file you can give to Claude (or any LLM) so it can generate SVG transit maps for you. It encodes the mental model from this guide: connect the dots, offset parallel lines, orient station pills perpendicular to the track. Feed it your station data and it draws the map.