Make sure a city map opens at the right zoom, and verify it the way that actually catches the failure. Use when a map looks zoomed out (or in) on load, after any change to the fit logic in pipeline/map_common.py, when publishing a city, and when checking the live site after a reboot. Covers the recurring fit race, the self-correcting guard that answers it, and scripts/check_map_view.js.
Install
npx skillscat add dacekroberts/expanded-heatmap/map-view Install via the SkillsCat registry.
A map that opens at the wrong zoom
Distilled from three incidents, each fixed where it broke - which is exactly
why there was a third.
The failure, three times
| When | City | Seen | Baked | What fixed it |
|---|---|---|---|---|
| 2026-09-21 | Edmonton | zoom 8.25, whole region in one cluster | 11.5 | an else branch restoring the baked view at full width |
| 2026-09-23 | Paris | zoom 9, labels flung to the edges; also a blank 0px map | 12.5 | FITTED_AT, and refusing a zero width |
| 2026-09-23 | Lille | zoom 8.25 | 11.75 | nothing targeted - it would not reproduce, which is what led to the guard |
Every time: intermittent, not specific to the city, and a reload fixed it.
Calgary rendered correctly beside the broken Edmonton. That signature is a
RACE in PHONE_FIT_SCRIPT: a fit pass runs while the embedded iframe is still
laying out, fits the network into whatever size the frame has at that moment,
and nothing afterwards undoes it.
A clue worth keeping, not yet a finding. Edmonton and Lille both broke to
exactly 8.25 from different baked zooms, and Paris - a network about half
their size - one step tighter at 9. A browser gives an iframe a default size
of 300x150 before its parent sizes it, and fitting a ~20 km network into
300x150 lands near zoom 8. It fits all three; nobody has proved it.
What answers it: the guard (2026-09-23)
Chasing triggers had been tried twice and a third appeared. So the guard
checks the outcome instead: until the reader first touches the map, it
compares the live zoom with what the view should be for the frame's current
width, and re-fits when they differ.
- "Should be" is exactly what
apply()produces: the baked view (HOME)
at full width,fitBounds(BOUNDS, padding)when narrower. It is computed withgetBoundsZoom, so a correct view is never touched and a normal load is
pixel-identical. - It runs every 500 ms for the first 20 s, and on
visibilitychange,pageshow, resize and when the map scrolls into view - so a page opened in a
background tab, or an embed below the fold, is covered too. - It stops for good at the first pointer, wheel, touch or key event inside
the map. A guard that restored the view under a reader's hand would be a new
bug. Never weaken this. The one exception (owner, 2026-10-02) is a touch
that cannot move the map: on a touch screen one-finger dragging is off
(TOUCH_GESTURE_SCRIPT), so a single finger only scrolls the page andsteering()lets the guard keep running; two fingers, or a tap (which
sends mousedown and click), still stop it. PADis shared byapply()and the guard. If the two paddings drifted
apart, the guard would "correct" a right view on every tick.- It exposes
window.__HEATMAP_VIEW- the inputs (home view, bounds, padding,
map width) and its owncorrectionscount - for the check below.
Proven on a served map before it shipped: a clean load was left alone; a
programmatic jump to 8.25 (a simulated race) was corrected within 1.2 s; a
simulated reader click followed by a zoom to 9 stayed at 9; and at 375 px
Edmonton held its bounds fit (11.75) rather than wrongly restoring the baked
11.5.
Do not answer the next variant with one more timed pass. The fit already
runs at 0 ms, rAF, 120, 400 and 1200 ms, and each extra pass was added after a
variant slipped past the previous set. If the guard ever fails, fix the guard.
Verifying a map's view: scripts/check_map_view.js
Read it, then run its contents with the browser javascript_tool. It finds the
map itself, standalone or two frames deep inside the live app, and recomputes
the expected zoom from the guard's INPUTS rather than trusting the guard's own
verdict.
Every rule here is a way this has already been got wrong:
- Read the map object, never judge the picture. Edmonton's broken view
was first read as "mid-load", then as "the bounds are correct" - both wrong,
both would have shipped it.getZoom()after the page settles is the
number that decides. - Load fresh at each size. Resizing a loaded page measures a stale layout.
- Do not touch the map before measuring - no click, no wheel, no key. A
touched map shows the reader's view; the check reports it UNMEASURED. - Run it more than once. It is a race: one clean load proves little.
corrections > 0on any load means the race FIRED and the guard repaired it- record that, it is the only measure there is of how often it happens.
- Check the live app, not only the local file. The race lives in the
embed; every incident above was seen on the deployed site. A hidden browser
pane (document.hidden: true) pauses rendering and may not exercise the
same timing - note it when reporting a clean result. A collapsed pane is
worse: Streamlit's app frame measures 0 px wide and squeezes the map to
16 px, where every zoom "matches" its own meaningless expectation. The
check's first live run PASSED exactly that, before it learned to refuse any
frame under 280 px as UNMEASURED. Emulate a real viewport size first. - Measure at the embedded width AND at phone width. The expected zoom
differs between them, and only the phone width exercises the bounds fit.
Reproduce an explanation before recording it
On 2026-09-23 Lille's 8.25 was first put down to a mouse wheel landing on the
map while the page scrolled, and the owner was told so. It was never
reproduced. Repeating the exact action - a fresh load, ten wheel ticks at the
same point - left the zoom untouched at 11.75, and the explanation died. The
record already held the answer: Edmonton's identical 8.25, and "a reload fixed
it". Search the record for the number before inventing a cause for it, and
re-run the action you are blaming before you blame it.
Related, and not this skill
Wheel zoom and cluster-click zoom felt laggy compared with the +/- buttons.
That was a separate question from whether the map opens at the right view, and
it was answered on 2026-09-23 by WHEEL_ZOOM_SCRIPT and animate_clusters inpipeline/map_common.py - see DECISIONS.md. The hypothesis written here
before it was measured ("how many fractional zoom levels a wheel gesture walks
through") held only for a trackpad; a mouse wheel did the OPPOSITE, zooming
less than asked because Leaflet discards every step that fires during a zoom
animation. Both fixes leave the guard alone: its touch detection is a capture
listener on the container, which runs before the wheel handler. Do not tune
the guard for zoom feel, and do not change zoomSnap for it.