How it is built
For anyone who wants to know how it works. Every performance figure was measured on the machine that runs the site.
The pipeline
- Download the zip files from the open data platform, about 104 MB for three years
- Read the CSVs straight out of the zip with Python's zipfile, never extracting them — unpacked they are 1.5 GB
- De-duplicate, remove fallback coordinates
- Build SQLite: one grid of 12 metre cells with a month dimension, plus pre-aggregated tables for zoom levels 7 to 11
- The result is 385 MB
Why not store one grid per zoom level
The first version did exactly that: twelve levels stored separately, 180 MB for a single year. Switching to one fine grid rolled up with integer division at query time tripled the data covered (one year became two years and eight months) while shrinking the database to 385 MB. Only the low zoom levels, 7 to 11, are still pre-aggregated, because at that range a query would otherwise have to scan too many cells.
Tiles instead of a bounding box
The first API took a bounding box. The problem is that a bounding box changes with every decimal place, so panning slightly refetches the whole viewport and neither the browser nor a CDN can cache any of it. One response measured 954 KB.
With fixed tile URLs instead:
- First load 12 tiles, 195 KB
- Panning 200 pixels fetches only the 3 newly exposed tiles, about 10 KB
- Panning back zero requests, all cache hits, 14 ms
Tiles carry Cache-Control: max-age=86400. The data changes twice a month, so a day is safe.
Compression
GeoJSON is highly repetitive text and compresses extremely well. Measured on the same 1,044,568 byte response:
| gzip level | Compressed | Ratio | Server compress | Browser decompress |
|---|---|---|---|---|
| 1 | 69,132 | 15.1× | 1.8 ms | 1.28 ms |
| 4 | 58,194 | 17.9× | 2.6 ms | 1.25 ms |
| 6 | 53,255 | 19.6× | 4.3 ms | 1.19 ms |
| 9 | 50,967 | 20.5× | 10.4 ms | 1.27 ms |
Decompression sits at about 1.2 ms regardless of level, because that is native code inside the browser. Level 4 was chosen: 8% larger than level 6, but twice as fast to produce.
Front end
The map is MapLibre GL JS, the open-source fork of Mapbox GL, BSD licensed. It was chosen because it has a native heatmap layer, renders on the GPU, and needs neither an API key nor a credit card.
Google Maps' HeatmapLayer was deprecated in May 2025 and removed in version 3.65 in May 2026, with third-party libraries suggested instead. Google Maps also bills per load, which is a risk for a free tool.
Base tiles come from OpenFreeMap, again with no API key and no usage limits. Heatmap weights are computed in the browser, because tiles have to be pure data to stay cacheable, and normalising each tile separately would leave visible colour seams along the tile boundaries.
Back end
Flask and SQLite, in the same container as the other tools on this site but touching no GPU — no queue, no quota, and requests can run concurrently without limit.
Measured query time: 8 to 82 ms per tile depending on zoom level and extent.
Other tools on this site
- Old Photo Motion — Turn an old photo into a 6-second clip that blinks and smiles
- age-me — See the same person older or younger
- Campsite Legality Check — Which of 1,807 campgrounds comply, violate, or are not listed
- Flood Potential Map — WRA flood potential under 10 rainfall scenarios, by township
- Find My Pet — Lost & found pets across Taiwan on a map; list without a microchip, report sightings without login
All run on one machine at home; the data tools use Taiwan government open data and state their limits.