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]: Default zoom and center for scattermap traces does not show all points #7674
When creating a scattermap trace, users have the option to set the layout.map.zoom and layout.map.center options to control which section of the map is shown.
If zoom and center are not provided, the trace should default to showing all datapoints; however this is not what happens -- sometimes datapoints are outside the map area.
@ndrezn reported a similar issue which seems to be caused by plotly.py, but this one is reproducible in plotly.js.
@emilykl
After stepping through the code I discovered that ultimately these defaults are being set from static values located in src/plots/map/layout_attributes.js. The defaults are coordinates (0,0) for center and 1 for zoom. I was able to confirm this by changing the lon default to -98 and observing the map center over the USA rather than Africa.
These defaults get applied with the coerce('center.lon'); callback in the handleDefaults helper function inside module src/plots/map/layout_defaults.js. We don't have access to data in the handleDefaults scope. This function is passed as a callback to handleSubplotDefaults which is a function shared by other seven other plots. Looking at how other plots implement handleDefaults it looks like there may be some flexibility change the signature and inject data under the opts parameter without causing cascading changes to other plots.
One way we could fit all the data points is to get the north, south, east, and west bounds. North and south are easy since the map doesn't wrap at the poles. You could get the min bounds for west/east by sorting all lon points then using the sliding window method to find the min distance that includes all points. This would make handleDefaultsO(nlogn) for the sorting though. There might be a greedy algo that will run in O(n), but I'm not sure if it would always give you a min bounding box for all coordinates.
Once you have the bounds you could set those as defaults instead of center/zoom or use them to calculate center/zoom defaults.
@palmerusaf This is great, thanks for looking into it thoroughly. Happy New Year!
It's a good point that calculating the centroid of a set of geo locations isn't trivial. I suppose we probably shouldn't put ourselves in the position of manually performing that calculation during supplyDefaults, but that would be the place to put it, if we want to. (I think we might have access to the data, inside either containerIn or containerOut, and you're right that we can also change the function signature if necessary.)
The ideal situation would be to just let MapLibre handle it, but I'm not sure it's capable. Could you verify what happens if undefined / null or no value at all is passed for zoom and center when initializing the map, and whether it's different from the current behavior?
One other thing to check is whether MapLibre itself, or another geo library we already bundle, offers a utility for finding the centroid and/or the correct zoom level for a given set of points.
I suppose we probably shouldn't put ourselves in the position of manually performing that calculation during supplyDefaults
Despite the fact that this does set some default values, normally we don't allow supplyDefaults to do ANYTHING that loops over all the data in a trace, because supplyDefaults happens so often and a loop like that can take a long time. This is analogous to axis qutorange in cartesian plots, which we do in the calc step and only when we think the data has changed. That said that pattern is annoying - it requires us to push those calculated values back into the input figure so that we can use them in later renders. So I bet there's a better pattern we can use that does do this calculation during supplyDefaults but caches the result - the key then being to properly determine when to bust the cache.
Thanks @alexcjohnson, that makes sense. So if we can come up with a reasonable caching pattern, then you think it would be reasonable to do some kind of zoom/center calculation in supplyDefaults?
@emilykl
I just tried every variation of null/undefined and the behavior is the same as omitting the parameters outright.
I've been researching the MapLibre docs, but I haven't found anything useful yet. It seems most of the API deals with exposing map controls rather than calculations. This is the right place to look right?
We are using Turf.js already which does a lot of map calculations, but it seems to struggle when crossing the anti-meridian. I even found this TODO that suggests rolling our own bounding box algo. This has been an open issue for a while. It even has its own label, so it's not just bbox that turf struggles with.
I've been playing around with the code to see what I can come up with. The initial results look promising.
Before
After
It even seems to work along the anti-meridian.
But it still needs some work. I think the main issue is that I'm using linear measurements for center/zoom scaling, and the map isn't linear in the y-direction. We probably need some of that fancy map math for the scaling, or at least a library that uses some.
It does however look like you can do this in the constructor if I'm reading this right. So we'd only have to come up with min/max lat/lon. Note the we're already using a variable bounds as an alias for maxBounds, but it looks like the mapLibre bounds does the zooming/centering we need whereas maxBounds limits the moveable area. https://github.com/maplibre/maplibre-gl-js/blob/b8f34428adc052583c7197daaf11b5e6deb54343/src/ui/map.ts#L311
Activity
emilykl commentedon Dec 16, 2025
@palmerusaf this is the issue!
palmerusaf commentedon Jan 2, 2026
@emilykl
After stepping through the code I discovered that ultimately these defaults are being set from static values located in
src/plots/map/layout_attributes.js. The defaults are coordinates (0,0) for center and 1 for zoom. I was able to confirm this by changing the lon default to -98 and observing the map center over the USA rather than Africa.These defaults get applied with the
coerce('center.lon');callback in thehandleDefaultshelper function inside modulesrc/plots/map/layout_defaults.js. We don't have access to data in thehandleDefaultsscope. This function is passed as a callback tohandleSubplotDefaultswhich is a function shared by other seven other plots. Looking at how other plots implementhandleDefaultsit looks like there may be some flexibility change the signature and inject data under theoptsparameter without causing cascading changes to other plots.One way we could fit all the data points is to get the north, south, east, and west bounds. North and south are easy since the map doesn't wrap at the poles. You could get the min bounds for west/east by sorting all lon points then using the sliding window method to find the min distance that includes all points. This would make
handleDefaultsO(nlogn)for the sorting though. There might be a greedy algo that will run inO(n), but I'm not sure if it would always give you a min bounding box for all coordinates.Once you have the bounds you could set those as defaults instead of center/zoom or use them to calculate center/zoom defaults.
What do you think?
emilykl commentedon Jan 6, 2026
@palmerusaf This is great, thanks for looking into it thoroughly. Happy New Year!
It's a good point that calculating the centroid of a set of geo locations isn't trivial. I suppose we probably shouldn't put ourselves in the position of manually performing that calculation during
supplyDefaults, but that would be the place to put it, if we want to. (I think we might have access to the data, inside eithercontainerInorcontainerOut, and you're right that we can also change the function signature if necessary.)The ideal situation would be to just let MapLibre handle it, but I'm not sure it's capable. Could you verify what happens if
undefined/nullor no value at all is passed forzoomandcenterwhen initializing the map, and whether it's different from the current behavior?One other thing to check is whether MapLibre itself, or another geo library we already bundle, offers a utility for finding the centroid and/or the correct zoom level for a given set of points.
alexcjohnson commentedon Jan 6, 2026
Despite the fact that this does set some default values, normally we don't allow
supplyDefaultsto do ANYTHING that loops over all the data in a trace, becausesupplyDefaultshappens so often and a loop like that can take a long time. This is analogous to axis qutorange in cartesian plots, which we do in the calc step and only when we think the data has changed. That said that pattern is annoying - it requires us to push those calculated values back into the input figure so that we can use them in later renders. So I bet there's a better pattern we can use that does do this calculation duringsupplyDefaultsbut caches the result - the key then being to properly determine when to bust the cache.emilykl commentedon Jan 6, 2026
Thanks @alexcjohnson, that makes sense. So if we can come up with a reasonable caching pattern, then you think it would be reasonable to do some kind of zoom/center calculation in
supplyDefaults?palmerusaf commentedon Jan 7, 2026
@emilykl
I just tried every variation of null/undefined and the behavior is the same as omitting the parameters outright.
I've been researching the MapLibre docs, but I haven't found anything useful yet. It seems most of the API deals with exposing map controls rather than calculations. This is the right place to look right?
We are using Turf.js already which does a lot of map calculations, but it seems to struggle when crossing the anti-meridian. I even found this TODO that suggests rolling our own bounding box algo. This has been an open issue for a while. It even has its own label, so it's not just bbox that turf struggles with.
I've been playing around with the code to see what I can come up with. The initial results look promising.
It even seems to work along the anti-meridian.
But it still needs some work. I think the main issue is that I'm using linear measurements for center/zoom scaling, and the map isn't linear in the y-direction. We probably need some of that fancy map math for the scaling, or at least a library that uses some.
Here is a link to the code.
palmerusaf commentedon Jan 16, 2026
Update it looks like maplibre does have a fitBounds method on the map object, but the map object isn't created until after defaults are set so you don't have access to it in the default handlers.
https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#fitbounds
It does however look like you can do this in the constructor if I'm reading this right. So we'd only have to come up with min/max lat/lon. Note the we're already using a variable
boundsas an alias formaxBounds, but it looks like the mapLibreboundsdoes the zooming/centering we need whereasmaxBoundslimits the moveable area.https://github.com/maplibre/maplibre-gl-js/blob/b8f34428adc052583c7197daaf11b5e6deb54343/src/ui/map.ts#L311