Originally published on hexisteme notes.
A pipeline I run turns geographic data into short map-explainer videos — 1080×1920, portrait, rendered with matplotlib. I had just applied a canvas-layout revision (v1.7) across 15 episodes and sat down to measure frames and confirm it had worked. Instead I found the actual cause of something I'd been treating as a composition-budget problem for weeks. A related note covered a case where a correct config value was being ignored. This one is a cousin of that failure: here the config was followed exactly, into a branch whose geometry nobody had checked.
The measurement
The check was simple. From each re-rendered episode I pulled the frame at t=12s and measured the share of bright, land-colored pixels inside the map band — the frame minus a 280px strip reserved at the top for hook text and a 520px strip reserved at the bottom for captions.
| episode | map-band occupancy | clipped at right axis edge |
|---|---|---|
| netherlands-below-sea | 42.7% | 65% |
| southamerica-east | 30.1% | 0% |
| africa-above-europe | 25.1% | 23% |
| norway-coast | 16.7% | 0% |
| swiss-glacier-border | 15.5% | 23% |
| poland-shift | 11.1% | 0% |
As a sanity check against the previous layout, gambia had gone from 35.7% under v1.6 to 41.6% under v1.7 — the revision was doing what it was supposed to. But poland comes in at 11.1%: same settings, same renderer, roughly a quarter of what the Netherlands episode gets. That gap between episodes, not the raw occupancy numbers, is what sent me looking.
Config couldn't explain it
Poland's scene 1 focuses on Ukraine alone: 18.1° of longitude by 8.0° of latitude, an aspect ratio of 2.26. The layout has an elongation guard — when a focus region's aspect ratio exceeds COMPOSITION_ELONGATION_RATIO (2.2), fill_fraction falls back from composition_target_width_fraction (0.85) to composition_min_width_fraction (0.55). Ukraine's 2.26 trips it.
The arithmetic the fallback implied:
- predicted width = 0.55 × 1015px axes width = 558px → measured 565px — close enough to call confirmed.
- predicted height = 0.55 × 1120px axes height = 616px → measured 250px — off by 2.5×.
Width matched. Height was wrong by 2.5×, and no fallback fraction explains that alone. The only hypothesis that produces exactly 250px is that the axes themselves are being drawn at equal aspect — longitude and latitude sharing the same pixels-per-degree. Under equal aspect, height falls out of width automatically: 8° of latitude × (1015px / 32.9 px-per-degree) = 247px. Measured: 250px. That's the answer — the axes box wasn't 1120px tall when this scene rendered; fill_fraction was never the problem.
The cause: one call, one missing argument
The territory under-glow — the soft highlight drawn beneath a focus country — is rendered with a call to ax.imshow() in src/shorts_factory/map_scenes.py:
ax.imshow( # <- no aspect argument
rgba, origin="upper", interpolation="nearest", extent=..., zorder=...,
)
matplotlib defaults imshow's aspect argument to the image.aspect rcParam, 'equal'. That default is easy to misread: it doesn't just draw the image at 1:1, it sets the aspect of the axes, governing everything else plotted on them for the rest of that draw. Another imshow call in the same file — the one that draws the progress bar — does pass aspect="auto" explicitly. One call site got it right. One didn't. And the one that didn't raised no exception and printed no warning. It just quietly reshaped the geometry of every scene that draws a highlight, which is nearly every scene in the pipeline.
The codebase already knew the symptom
What made this frustrating rather than satisfying is that the collapse had already been measured. A comment elsewhere in the same file records that when the under-glow flips the axes to aspect 1.0, the axes box can shrink to as little as 224px tall — measured on the canada episode the day before — and once the ruler and marker chips are already occupying that collapsed box, scene labels have nowhere left to go and get dropped entirely, even with plenty of empty screen above and below.
A nominal 1120px axes height collapsing to 224px had already been caught, measured, and written down. The response was to add a bounds_bbox parameter to the label-placement code, so a wider composition band could be passed in instead of the collapsed axes box — fixing where labels landed, and leaving the cause underneath exactly where it was.
Dead space is not a bug — it's the price of honest proportion
This changed how I think about the whole layout. aspect = 1.0 is the equirectangular projection this pipeline is built on — it's what makes latitude order, north–south distance, rhumb lines, and topology come out right. It isn't an accident waiting to be fixed. It's the projection the project chose.
Hold that aspect fixed and put a wide subject into a portrait frame, and the map becomes width-bound: it fills as much horizontal space as it's allowed, and height follows by proportion. The vertical space left over is not a config bug — it's geometrically necessary, and no fraction or fallback removes it without removing the constraint that makes the map honest.
The one way to actually remove it is aspect="auto", which stretches the bounding box to match the axes' shape instead of the data's. For Ukraine, that means going 2.5× taller than its real shape (2.26 / 0.906). That's not a layout fix, that's a lie about geography.
Which reframes the actual choice: not "eliminate the dead space" but "use it" or "pick subjects that are already tall." v1.7's change — raising fill from 0.70 to 0.85 — is the right lever inside that constraint, and the numbers back it up: gambia went from 35.7% to 41.6%, netherlands sits at 42.7%.
Where v1.7 doesn't reach
The elongation fallback is where the fix stops working: any episode whose focus region has an aspect ratio above 2.2 falls back to the 0.55 minimum and never sees the 0.85 target — exactly the branch behind the lowest-occupancy episodes in my table, poland at 11.1%.
The fallback's own justification is avoiding over-zoom, but that assumes aspect is free to vary, and under this renderer it isn't. Under equal aspect, width is the binding constraint: setting fill to 0.85 makes the focus occupy 85% of the axes width and nothing more, no matter how elongated the region is — it can't overflow the way the fallback assumes. For Ukraine, 0.85 instead of 0.55 means going from 565px to 863px wide, and 250px to 386px tall.
Rather than take that on faith, I instrumented the actual _apply_composition_budget → fit_bbox_to_composition calls and back-solved used_fill from each returned bbox. All 67 scenes in the corpus matched exactly 0.85 or 0.55 — no in-between values, confirming the branch really is binary in practice.
- 18 of 67 scenes hit the clamp: swiss 5/5, chile 3/4, kazakhstan 3/4, gambia 2/4, wakhan 2/4, poland 1/4, southamerica 1/5, africa 1/6.
-
Most clamped scenes are hand-framed. 27 of the 67 scenes specify an explicit
camera_bboxin the script. Kazakhstan's focus isKAZthroughout, yet scenes 0–2 (polygon-derived bbox) clamp and scene 3 (explicit bbox) doesn't — the clamp isn't selecting on anything about the country, it's selecting on how the shot was framed, and shrinking those hand-authored shots by 35%. -
3 of 67 scenes skip the composition budget entirely.
zoom="world"with nocamera_bboxroutes to_compute_world_zoom_bboxinstead, governed byworld_zoom_padding_fraction(0.6) — southamerica scenes 0 and 4, and norway scene 2. No amount of fill tuning touches these.
Measured, not predicted
I ran an A/B on the swiss episode — 5 of 5 scenes clamped — sampling 9 frames at 4-second intervals:
| min_width | map-band occupancy | clipped |
|---|---|---|
| 0.55 | 17.5% | 21% |
| 0.80 | 22.8% (1.31×) | 19% |
A naive area prediction from (0.80/0.55)² says 2.12×. Measured came back at 1.31×. Both are correct, and they measure different things: raising fill tightens the camera around the focus country, so it grows in frame while its neighbors leave the frame entirely — area gained by one country is partly area lost from the countries around it. On screen the difference isn't subtle even though the multiplier is smaller than predicted: at 0.55 Switzerland is an unresolvable blob, at 0.80 the western bulge and eastern Graubünden are both legible.
This doesn't close the gap everywhere: two of the three lowest-occupancy episodes — southamerica and norway — are on the world-scene path this fix doesn't reach. world_zoom_padding_fraction (0.6) is a separate lever, and world-scene margin might be deliberate — showing "where on Earth this is" arguably needs the extra space — so I'm treating it as a different problem.
What would prove this wrong, and what generalizes
Two conditions would falsify this analysis, on record rather than assumed away:
- If raising the elongation fallback makes some episode overflow, the equal-aspect assumption breaks somewhere in the path, and this needs revisiting.
- If scenes with no highlight genuinely exist, they'd keep
aspect="auto"while their neighbors collapse to 1.0, and map proportion would shift scene to scene inside one video — a separate, real bug that would outrank fixing the fallback fraction. (Poland's four scenes all draw a highlight, so it doesn't happen there.)
Three things about this generalize past matplotlib and past this pipeline:
-
A library call's default argument can silently redefine your layout.
imshowlooks like a function that draws an image; it also mutates the aspect of the axes it's drawn on. In the same file, one call passedaspect="auto"and one didn't, and the one that didn't raised nothing — it just changed the geometry of everything drawn after it. - You can measure a symptom precisely, write it down, and still never fix the cause. The 224px collapse was already measured and sitting in a comment. That knowledge got spent building a workaround in the label-placement code instead of tracing it upstream, and the cause became a permanent, invisible fixture of the system. A workaround can be where knowledge goes to die.
- "I raised the config value and the effect differs per episode" is itself a signal. I verified the v1.7 change on one episode — gambia, 35.7% to 41.6% — and rolled it out to 15. Only the spread across episodes afterward (11% to 43%) sent me back to look, and that's what surfaced both the fallback branch and the aspect collapse underneath it. Don't A/B on a single sample, especially when different samples take different code paths without telling you.
More notes at hexisteme.github.io/notes.
Top comments (0)