Skip to content

[BUG]: Default zoom and center for scattermap traces does not show all points #7674

Description

@emilykl

Description

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.

Codepen

This Codepen demonstrates the issue:
https://codepen.io/emilykl-code/pen/bNpyXqQ
Image

Trondheim is one of the cities plotted, but it's not visible in the initial map view; you need to pan the map northward to see it.

Expected behavior

At a minimum, the initial map zoom and center should show all data points.

It's also probably reasonable to choose a default zoom such that the data points mostly fill the plot area.

Notes

Questions to answer:

  • Where is the (bad) current default coming from: Is it something we are choosing in plotly.js, or is it something being set by MapLibre?
  • Under what circumstances are points cut off? Does it only happen for points in the northern latitudes?

Activity

  1. emilykl commented on Dec 16, 2025

    @emilykl
    ContributorAuthor

    @palmerusaf this is the issue!

  2. palmerusaf commented on Jan 2, 2026

    @palmerusaf

    @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 handleDefaults O(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.

    What do you think?

  3. emilykl commented on Jan 6, 2026

    @emilykl
    ContributorAuthor

    @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.

  4. alexcjohnson commented on Jan 6, 2026

    @alexcjohnson
    Collaborator

    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.

  5. emilykl commented on Jan 6, 2026

    @emilykl
    ContributorAuthor

    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?

  6. palmerusaf commented on Jan 7, 2026

    @palmerusaf

    @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
    Image
    • After
    Image

    It even seems to work along the anti-meridian.

    Image Image

    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.

  7. palmerusaf commented on Jan 16, 2026

    @palmerusaf

    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 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

  8. added this to the v4.0.0 milestone on Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions