A temporal heatmap of San Francisco's metered parking. Pick any day-of-week and hour, and the map shows you typical occupancy across every metered block in the city — built from ~206M meter transactions pulled directly from the SF Open Data SODA API.
Live: https://sfparking.wolfie.gg
What it does
- 168-slot weekly profile per block: 7 days × 24 hours of occupancy, computed from real meter sessions
- Multi-tier visualization: heatmap at city zoom → 3D columns at neighborhood zoom → block-level paths and individual meter dots at street zoom
- Time playback: scrub through the week or hit play to watch demand pulse
- Block detail panel: per-block hour-by-hour breakdown, supply, enforcement schedule
- Comparison mode: pin a reference time, see deltas vs. any other slot
- Search + radius: find a specific address and see what parking looks like nearby
- Isochrone mode: pick an origin and see how far you can drive/bike/walk in N minutes (uses local Valhalla routing — see Optional below)
- Bike share view: overlay Bay Wheels station demand and visualize correlation with parking pressure
- Deeplinkable URL state: every selection (time, view, block, search, isochrone) is in the URL, so any view is shareable
Data sources
All data comes from public, unauthenticated endpoints. There are no API keys to configure.
| Source | Dataset | Used for |
|---|---|---|
| SF Open Data SODA API | 8vzz-qzz9 (Parking Meters) |
Active meter locations, block centroids |
| SF Open Data SODA API | imvp-dq3v (Meter Operating Schedules and Transaction Counts) |
~206M session records aggregated into 168-slot occupancy profiles |
| SF 311 service requests | (via SODA) | Off-hours parking pressure scores |
| Bay Wheels GBFS | bike share station status feed | Station capacity and trip data for the bike view |
The map basemap is CARTO Dark Matter (open vector tiles, no token).
Tech stack
- Frontend: Vite 7 + React 19 + TypeScript + Tailwind CSS v4
- Mapping: deck.gl v9 layers on top of MapLibre GL via
react-map-gl - Pipeline: Python 3 standard library only — no
requirements.txtneeded - Routing (optional): Valhalla running locally in Docker for isochrone computation
Setup
# 1. Install JS deps
pnpm install
# 2. Build the data (one-time, takes a few minutes)
pnpm fetch-meters # ~28k metered blocks → public/data/meter_locations.json
pnpm fetch-enforcement # block-level enforcement schedules
pnpm fetch-311 # 311 pressure scores
pnpm aggregate # paginated GROUP BY over the full transaction dataset
# Or just run the whole pipeline:
pnpm pipeline
# 3. Start the dev server
pnpm dev
Then open http://localhost:5173.
Project layout
sf-parking-heatmap/
How occupancy is computed
The transaction dataset (imvp-dq3v) gives one row per paid session with street_block, session_start_dt, etc. The pipeline:
- Aggregates server-side with
date_extract_dow()anddate_extract_hh()over a 90-day window —aggregate_parking.pymakes a few paginated GROUP BY calls instead of pulling raw rows - Maps SODA day-of-week (1=Sun..7=Sat) to ISO (0=Mon..6=Sun)
- Converts session counts to occupancy ratio:
(sessions_per_week × avg_session_hours × compliance_factor) / meter_count, clamped to[0, 1]
AVG_SESSION_HOURS = 1.2(SFMTA average)COMPLIANCE_FACTOR = 1.33(accounts for unpaid parkers)
- Blends in 311 pressure scores for off-hours when meters aren't enforced
The result is a 168-element array per block (dow * 24 + hour) shipped as a single JSON file.
Available scripts
pnpm dev # Vite dev server
pnpm build # Production build (tsc -b && vite build)
pnpm lint # ESLint
pnpm preview # Preview the built bundle
# Data pipeline
pnpm fetch-meters # Active meter locations
pnpm fetch-enforcement # Enforcement schedules
pnpm fetch-311 # 311 pressure data
pnpm fetch-supply # Total parking spaces per block
pnpm compute-paths # PCA block geometry
pnpm aggregate # Aggregate sessions → weekly profiles
pnpm aggregate-bikes # Bay Wheels demand profiles
pnpm pipeline # Run the core pipeline end-to-end
pnpm pipeline-full # Core pipeline + speed profiles + isochrones
Optional: isochrones
The isochrone view (drive/bike/walk reachability from any point) needs a routing engine. The repo includes a docker-compose.yml for Valhalla:
docker compose up -d # Downloads CA OSM extract on first run
pnpm build-speed-profiles # Cluster historical speeds into 6 profiles
pnpm compute-isochrones # Pre-compute isochrones for the grid
If you don't care about isochrones, skip this — the app degrades gracefully.
Caveats
- Occupancy is an estimate. It uses a fixed
AVG_SESSION_HOURSand aCOMPLIANCE_FACTORfor unpaid parkers. Both are tunable inscripts/aggregate_parking.py. - Only metered blocks. Non-metered streets aren't in the dataset.
- Typical week, not real-time. The pipeline aggregates the trailing 90 days into a typical-week profile. There's no live feed.
enforcedmask is per-block. During non-enforced hours the heatmap blends in 311 pressure scores rather than using meter sessions.