You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG]: layout.uirevision doesn't play well with map subplot auto-fit #7907
When a map subplot uses fitbounds, layout.uirevision fails to preserve the user's subsequent pan/zoom across Plotly.react calls. The map snaps back to the auto-fit view even when the uirevision value is unchanged.
Screenshots/Video
Screencast.From.2026-07-15.17-06-21.mp4
Steps to reproduce
Be on v4.0 (or the default branch after it gets released)
Plot a scattermap with uirevision set and no explicit center/zoom:
This works correctly for cartesian axes (via autorange) and for map subplots when the user has supplied an explicit center/zoom from the start. It only fails on the auto-fit path.
Root cause: the panned view is recorded, then discarded because the incoming figure has no view to compare against
Verified against 71a2ff73b (master, post v4.0.0).
Step 1 — pan persistence works. A mouse pan hits the moveend handler in src/plots/map/map.js (~line 501): Registry.call('_storeDirectGUIEdit', ...) writes map.center / map.zoom into bothgd.layout and _preGUI. So far so good — the user edit and its journal entry exist.
Step 2 — the uirevision gate passes. On Plotly.react(gd, gd.data, {uirevision: 'v1'}):
The edit key map.center matches the pattern /^(map\d*)\.(center|zoom|bearing|pitch)/ in layoutUIControlPatterns (src/plot_api/plot_api.js ~line 2318).
With no explicit attr, the revision attribute resolves to 'map.uirevision'; the subplot-level default inherits layout.uirevision (src/plots/plots.js ~line 1066), so oldRev === 'v1'.
getNewRev('map.uirevision', layout) finds nothing at map.uirevision in the incoming layout (there is no layout.map object at all), walks up, and falls back to container.uirevision === 'v1'.
newRev === oldRev → protection engaged.
Step 3 — the actual failure point. Protection means: restore the stored value only if the incoming figure agrees with it (src/plot_api/plot_api.js ~line 2427):
if(valsMatch(newVal,preGUIVal)){// restore the pre-GUI value and keep the editcontinue;}
Here preGUIVal is the panned {lon, lat} / zoom, but newVal — read from the incoming user layout — is undefined, because on the auto-fit path the caller never supplies map.center/map.zoom. valsMatch(undefined, {...}) is false, execution falls through to delete layoutPreGUI[key], and the panned view is dropped. Immediately afterwards, handleDefaults in src/plots/map/layout_defaults.js (~line 55–60) sees no user-supplied center/zoom (so isFitView trivially holds), recomputes _fitBounds, and updateMap calls map.fitBounds(...) — the snap-back you see in the video.
This also explains the two asymmetries in the report:
Explicit center/zoom from the start works: the incoming layout then carries values that match _preGUI (or differ intentionally), so either branch preserves intent.
Cartesian works: axes have exactly this "incoming value is absent" case hard-coded as the autorange/range[...] special case (~lines 2433–2452): when the tail is autorange and the incoming axis has none, the stored range is restored anyway. Map view attributes have no equivalent carve-out.
Fix direction
Two viable shapes, both small:
Mirror the autorange carve-out in applyUIRevisions: when the matched pattern is a map.* view attribute, the revision is unchanged, and newVal === undefined while the incoming subplot still requests auto-fit (fitbounds !== false), restore the stored value instead of dropping it. This keeps all the logic in one place but touches the generic engine.
Make the map's auto-fit revision-aware: stash the effective uirevision alongside _fitView at save time, and have handleDefaults skip setting _fitBounds when the revision is unchanged and the current view differs from the last computed fit. This contains the change entirely within the map subplot, which arguably matches how fix: Dynamically compute default zoom/center for map traces #7884 framed the deferred integration.
Option 2 looks lower-risk to me since it can't perturb cartesian/polar/scene behavior, but option 1 is the more principled fix if other subplot types grow auto-fit paths later. Happy to take a run at either if useful.
FYI your response was flagged as potentially AI generated on our end. If that's the case please make sure to review any AI content before posting it in the future.
Activity
CAOShurong commentedon Aug 25, 2026
Root cause: the panned view is recorded, then discarded because the incoming figure has no view to compare against
Verified against
71a2ff73b(master, post v4.0.0).Step 1 — pan persistence works. A mouse pan hits the
moveendhandler insrc/plots/map/map.js(~line 501):Registry.call('_storeDirectGUIEdit', ...)writesmap.center/map.zoominto bothgd.layoutand_preGUI. So far so good — the user edit and its journal entry exist.Step 2 — the uirevision gate passes. On
Plotly.react(gd, gd.data, {uirevision: 'v1'}):map.centermatches the pattern/^(map\d*)\.(center|zoom|bearing|pitch)/inlayoutUIControlPatterns(src/plot_api/plot_api.js~line 2318).attr, the revision attribute resolves to'map.uirevision'; the subplot-level default inheritslayout.uirevision(src/plots/plots.js~line 1066), sooldRev === 'v1'.getNewRev('map.uirevision', layout)finds nothing atmap.uirevisionin the incoming layout (there is nolayout.mapobject at all), walks up, and falls back tocontainer.uirevision === 'v1'.newRev === oldRev→ protection engaged.Step 3 — the actual failure point. Protection means: restore the stored value only if the incoming figure agrees with it (
src/plot_api/plot_api.js~line 2427):Here
preGUIValis the panned{lon, lat}/ zoom, butnewVal— read from the incoming user layout — isundefined, because on the auto-fit path the caller never suppliesmap.center/map.zoom.valsMatch(undefined, {...})is false, execution falls through todelete layoutPreGUI[key], and the panned view is dropped. Immediately afterwards,handleDefaultsinsrc/plots/map/layout_defaults.js(~line 55–60) sees no user-suppliedcenter/zoom(soisFitViewtrivially holds), recomputes_fitBounds, andupdateMapcallsmap.fitBounds(...)— the snap-back you see in the video.This also explains the two asymmetries in the report:
center/zoomfrom the start works: the incoming layout then carries values that match_preGUI(or differ intentionally), so either branch preserves intent.autorange/range[...]special case (~lines 2433–2452): when the tail isautorangeand the incoming axis has none, the stored range is restored anyway. Map view attributes have no equivalent carve-out.Fix direction
Two viable shapes, both small:
autorangecarve-out inapplyUIRevisions: when the matched pattern is amap.*view attribute, the revision is unchanged, andnewVal === undefinedwhile the incoming subplot still requests auto-fit (fitbounds !== false), restore the stored value instead of dropping it. This keeps all the logic in one place but touches the generic engine.uirevisionalongside_fitViewat save time, and havehandleDefaultsskip setting_fitBoundswhen the revision is unchanged and the current view differs from the last computed fit. This contains the change entirely within themapsubplot, which arguably matches how fix: Dynamically compute default zoom/center for map traces #7884 framed the deferred integration.Option 2 looks lower-risk to me since it can't perturb cartesian/polar/scene behavior, but option 1 is the more principled fix if other subplot types grow auto-fit paths later. Happy to take a run at either if useful.
robertclaus commentedon Aug 25, 2026
@CAOShurong thank you for the comment!
FYI your response was flagged as potentially AI generated on our end. If that's the case please make sure to review any AI content before posting it in the future.