Writing Field note

Technology · 4 min read

Building a San Francisco map feels a bit like cheating

I’ve always wanted a small, animated SVG map of just San Francisco. The city’s open data was already there. Codex made the rest feel easy.

In this article5 sections

I’ve always wanted my own map of San Francisco. Not a slippy map of the Bay Area with tiles and an API key. Just the city, drawn as SVG, with the supervisorial districts, a clean coastline, and some motion when motion helps.

I built it with Codex, using Astra 6. It went fast. A bit like cheating.

I used to be on Uber’s data visualization team, so I know roughly what this used to cost. We would have reached for something like deck.gl. Drawing shapes is the easy part. Finding usable geography, reconciling boundaries, placing labels, and making interaction work on a phone are where the time goes.

What I wanted

The list was short, and every item was a constraint:

  • Only San Francisco. The city fits comfortably in one SVG. It doesn’t need tiles, a WebGL context, or a map server.
  • District maps. The 2002, 2012, and 2022 supervisorial districts, so the same map can show how the lines moved.
  • Motion that knows when to stop. Animation should explain something, then get out of the way.
  • Light enough for a blog post. No runtime dependencies, and no multi-megabyte download to show one city.

The data was already public

I didn’t have to trace anything. The district boundaries, land mask, and highways come from DataSF. Parks come from Recreation and Parks property data. The BART station locations come from BART’s own geospatial files.

That’s the part that surprised me most. The hard input, accurate geography for the whole city, has been sitting in the open for years. What used to take time was turning it into something you could actually put on a page.

Open data still needed judgment. San Francisco has at least three sets of neighborhood boundaries: 117 SF Find areas from 2006, 41 analysis neighborhoods built from census tracts, and 92 realtor neighborhoods from 2010. They disagree, and none of them is official in the way a district line is. I kept them as separate sources instead of blending them into one confident-looking answer.

The realtor set also had 64 pairs of polygons that overlapped by tiny slivers, about a square meter in total. Nobody would see that on screen, but it leaves a few spots that belong to two neighborhoods at once. The cleanup assigns each sliver to one neighborhood by a fixed rule and checks that the outer boundary doesn’t change. The source notes record all of this, and they matter as much as the picture.

Motion that knows when to stop

What I really wanted to explore was motion and transit. The demo below moves a marker between San Francisco’s eight BART stations. You can play, pause, or scrub through it.

The station locations are BART’s. The straight connections and timing are illustrative. This is an animation experiment, not live service or a map of the tracks.

It doesn’t start until you press Play, and it pauses when the tab is hidden. In the full map, district crossfades and marker entrances turn off when the system asks for reduced motion. When markers update, existing ones keep their place and only new ones animate in, so a data refresh doesn’t make the whole map twitch.

Keeping it small

The package has no runtime dependencies. Geography is an explicit import, so a page only pays for the layers it uses. For a map around 800 pixels wide, the lighter preset simplifies lines below what a screen can show and shares borders between neighboring polygons instead of storing them twice. The coastline, realtor neighborhoods, parks, and BART stations for that preset come to about 63 KB gzipped.

That’s small enough that I stopped thinking about it, which was the goal.

Where Codex helped

I wanted to spend my attention on what the map should do, not on projection math and path strings. Codex handled most of that back-and-forth. The speed is my impression, not a benchmark.

It didn’t decide which boundaries belong on the map, whether three neighborhood definitions should stay separate, or when an animation was just decoration. I still had to look at the result and ask whether it made sense.

Open the map explorer or browse the code.

I finally have the map I kept meaning to build. Most of the experiment went into the animation I had in mind, not the plumbing to get there.

Was this useful?

What was missing?

Thanks for the feedback.