All projects

Pulse
Live, and public
JavaScript, WebGL, MapLibre GL, OpenFreeMap, Python, GitHub Pages, Claude Code

A week of Toronto Bike Share, playing on a phone with no server

A map of 7.3 million Toronto Bike Share trips folded into one typical week. All the work happens once, ahead of time, so the phone only looks things up.

Draft

Data in or outA model proposesCode checks and decidesA person acts or approves

  1. In7.3 million 2025 trip records, City of Toronto open data
  2. CodeBatch job folds the year into an hourly typical week
  3. CodeWrites 1.9 MB of fixed binary files, no database
  4. PersonI check what the map shows against the numbers
  5. OutThe phone's GPU draws every station, no server

The problem

Toronto publishes every Bike Share trip as open data: 7.3 million of them for 2025, about 218 MB of CSV. A file like that only answers questions for someone who already knows what to ask. I wanted to watch the city’s bike traffic move on a phone, with nothing to install.

What it does

Every station is a sphere. Its size is how fast the station’s bike count is changing in that hour, and its colour is the direction: green stations are gaining bikes, red ones are losing them. The year is folded into one typical Monday-to-Sunday week, so you can watch downtown fill on weekday mornings and empty in the evening. There is also a day view, and a year view where each frame is a month and what animates is the change from one month to the next.

How it’s built

2025 is closed history. Every question anyone can ask of that year already has a fixed answer, so nothing is queried while the map plays. A Python batch job reads the City’s files once and writes compact binary files of 16-bit numbers: the flow in and out of every station for every hour of the week, at three levels of detail. The whole year comes to 1.9 MB. The page downloads those files, and the GPU draws each station as a shaded sphere in a custom WebGL layer over a MapLibre map. Dragging the time slider is a lookup in memory, and any static host can serve the page.

Every claim the short video version makes, such as downtown filling up in the morning, was checked against the numbers before it went in.

Decisions

How it broke, and what changed

Stations along Yonge Street in midtown stayed red all day. That looked like a bug, and half of it was. The colour flipped from red to green at one fixed threshold, applied to stations whose traffic differs by a factor of a thousand. At quiet stations, arrivals trickled in below the threshold while departures came in one commuter spike above it, so those stations latched red and never flipped back. One absolute threshold across a field that varied that much was a different rule for every station. Each station now has its own threshold: half of its own average absolute net flow.

The other half was real. Several of those stations do lose bikes over the week, one of them 62%. Midtown sits above the Davenport escarpment: people ride downhill and don’t ride back, and the trucks that rebalance the bikes aren’t in the trip data. Red is the honest answer there, so those stations were left alone.

A second bug kept coming back. Size is meant to carry the amount, colour the direction, and brightness nothing. Three times, in three versions, magnitude crept back in as colour saturation or brightness, so a small sphere also came out pale and dim. Each time the cause was written up correctly in the project log, and each time it returned, because a note doesn’t run. Now a check asserts the rule against the code. Before trusting it, I had it run against three deliberate breaks, one for each way the rule has failed, and confirmed it caught all three.

What’s still rough

How this was made: I wrote the specification, made the design decisions and tested the result. AI coding agents (Claude Code) wrote the code. More on how I work.