Chapter Four

Two Lines Walk Into a Station

The moment it becomes a real transit map: parallel lines through shared corridors.

Everything up to now has been a single line. One color, one path, one sequence of stations. But a metro system isn't a line — it's a network. And networks mean shared infrastructure. Two lines running through the same station. Three lines sharing the same tunnel. Four lines converging on a central hub, then fanning out in different directions.

This is the chapter where our drawings start to look like real transit maps. The concept is straightforward: when multiple lines share a station pair, draw them parallel to each other with consistent spacing. The implementation uses the same perpendicular-to-the-track idea from Chapter 2, applied to line placement instead of label placement.

The Problem, Visually

Let's start with what goes wrong when you naively draw two lines through the same stations:

Live Editor — Two lines, one on top of the other

Line 2 exists in the data but is invisible — it's drawn at the exact same coordinates as Line 1, completely hidden behind it. You might catch a sliver of pink at the stroke edges, but that's it. This is useless as a map.

The Fix: Perpendicular Offset

The solution: offset each line perpendicular to the track direction. If the track runs horizontally, Line 1 goes slightly above center and Line 2 goes slightly below. If the track runs diagonally, the offset follows the perpendicular of that diagonal.

For N lines sharing a segment, center them around the track's geometric path:

offset[i] = (i - (N-1)/2) × spacing

With 2 lines and 4px spacing: Line 1 gets offset -2px, Line 2 gets +2px. With 3 lines: -4px, 0px, +4px. The formula always centers the group.

Live Editor — Parallel offset (adjust spacing below)
5px

Drag the slider. At 0px the lines overlap completely (the same problem as before). At 5px they're distinct but tight — the typical spacing for a transit map. At 20px they're comically far apart, losing the visual association of shared track.

The magic is in the perpOffset() function — it takes two station positions, computes the track angle between them, then returns the x/y displacement perpendicular to that angle. Four lines of math, and it works for any track direction.

Adding a Third Line

The same formula scales to any number of lines. Let's add Line 3, and while we're at it, make the track diagonal to prove the offset works in all orientations:

Live Editor — Three parallel lines on a diagonal

Three lines, diagonal track, smooth beziers, consistent parallel spacing. The same code works whether the track is horizontal, vertical, diagonal, or curved. The perpXY() offset doesn't care about orientation — it always pushes perpendicular to the local track direction.

The Simpler Model

At first, it's tempting to imagine a "corridor detection algorithm" — something that analyzes route polylines, discovers shared segments, computes centerlines, and resolves partial overlaps.

But for a transit map, that work is mostly unnecessary. We already know which lines share which stations, because the stop sequences tell us directly.

That's the core insight of this guide. Connect the dots. For each line, for each consecutive station pair, draw the segment with the appropriate offset. The shared corridors emerge naturally from the fact that the same station pair appears in multiple lines' stop sequences.

No corridor detection. No computational geometry. Just: which lines go from A to B? Offset them. Draw them.

The Interesting Part: Lines That Split

Parallel lines through shared stations are solved. But transit networks aren't just parallel — lines diverge. Line 1 and Line 2 share Harbour → Central, then Line 1 continues to Park North while Line 2 turns south to the Airport.

Here's the beautiful thing: the connect-the-dots model handles this automatically. No special junction logic needed.

Live Editor — Lines sharing track, then splitting

Look at what happened at Central. Lines 1 and 2 arrive together — parallel, evenly spaced — then peel apart. L1 curves upward to Park North. L2 curves downward to Airport. We didn't write any junction code. The divergence happened naturally because:

On the Harbour→Central segment, both lines share the corridor, so they get offset -3px and +3px respectively. On the Central→Park segment, only L1 exists, so it gets offset 0px (centered). On the Central→Airport segment, only L2 exists, offset 0px. The bezier smoothing handles the transition between the offset position and the centered position gracefully.

This is the connect the dots philosophy in action. Each line only knows its own station sequence. The parallel spacing and the smooth splitting are emergent properties of the offset math and the bezier smoothing working together.

The Key Function: getSegmentLines()

The crucial addition in this editor is getSegmentLines(stationA, stationB) — it answers: "how many lines travel between these two stations?" This determines how many parallel offsets we need for that segment. A segment shared by 3 lines needs 3 offsets. A segment with only 1 line needs 0 offset (centered).

The rest is just applying the offset formula from earlier. The function that seemed simple — "which lines share this segment?" — is the actual architectural insight of the entire system.

Four Lines, Full Complexity

Let's push it further. Four lines, a shared central corridor, and two branch points:

Live Editor — Four lines, shared trunk, two splits

Four lines. A shared trunk from Riverside through Central. L2 peels north to Hillside. L4 peels south to Beach. L1 and L3 continue east. All from the same 15 lines of offset + smoothing logic. No junction detection. No special cases. Connect the dots.

Study the shared trunk between Old Town and Central — four parallel colored lines, evenly spaced, with a glow. Then watch them fan out at the edges. This is the visual structure of a real transit map, built entirely from the "which lines share this segment?" question.

What We've Learned

This chapter is the conceptual peak of the guide. Everything before was setup. Everything after is refinement. The core idea fits in one sentence:

For each line, for each consecutive station pair, compute the perpendicular offset based on how many other lines share that segment. Draw a smooth path through the offset points.

That's it. That's the whole algorithm. The parallel corridors, the smooth junctions, the fan-outs — they're all emergent properties of this simple rule applied consistently.

In the next chapter, we'll make the station nodes worthy of the lines passing through them. Right now they're plain circles sitting on the centerline, ignoring the parallel tracks flowing around them. We need pills, transfers, and dots.

Drawing Transit — An open guide to programmatic transit maps