Fix geo fitbounds to choose a compact range across the antimeridian#7837
Fix geo fitbounds to choose a compact range across the antimeridian#7837SharadhNaidu wants to merge 4 commits into
Conversation
|
can any of the maintainer please review the fix for the issue please ? |
|
Hello @SharadhNaidu! Thanks for the PR. I'll try to review this, but it might take a bit to get to. |
|
In the meantime, could you provide some examples that this fixes and some testing steps? |
|
Give me some time please , i'll provide you with few examples with before and after , also the testing steps . |
|
here's the repro from the issue , before/after, and how to test. example (the case from #7844) Plotly.newPlot('graph', [{
type: 'scattergeo', mode: 'markers',
lat: [43.1155, 32.7157], lon: [131.8855, -179]
}], {
geo: { fitbounds: 'locations', projection: { type: 'equirectangular' } }
});the two points sit either side of ±180° before, before (v3.6.0) points squashed against opposite edges:
after (this PR) compact view across the antimeridian, framed like any other fitbounds map:
for reference, the eastern-most
what changed: testing
Manual: swap i'll attach a few more examples and tests . |
When `fitbounds` point data straddles +/-180 degrees longitude, the naive [min, max] range from getAutoRange includes the large empty span the long way round the globe, so the map zooms out far more than necessary (plotly/plotly.py#5539). Add getFitboundsLonRange, which finds the widest gap between consecutive longitudes and returns the complementary, antimeridian-crossing range when it is more compact. The override is scoped to longitude point data: it is skipped when a choropleth or location-based scattergeo trace is present (whose region extents are not captured here) and when the data spans the whole globe.
The antimeridian fix replaced the padded naive range with a tight [min, max], so markers ended up flush against the frame on straddling maps while every other fitbounds map leaves a margin. Scale the padding getAutoRange already applied to the naive range down to the narrower crossing range and apply it. The padding is symmetric, so the mid-longitude the projection centers on is unchanged. Loosen the integration test's range assertion to match.
8575782 to
8fd0ab1
Compare
|
I dont why the one CI run failed , its an animation/transition test , I dont think i have touch any path/file which required "test/jasmine/tests/transition_test.js" , dont know why it failed but just in case I rebased , to the recent changes . |
camdecoster
left a comment
There was a problem hiding this comment.
This is great! Thanks for the fix. It works well from my testing. I requested a few changes. Once those are done, this can be merged.
|
|
||
| // only visible traces contribute to the autorange above | ||
| if(fitTrace.visible !== true) continue; | ||
| if(fitTrace.locations) { |
There was a problem hiding this comment.
Without checking the array length, an empty array could match here. That seems unlikely, but it could happen.
| if(fitTrace.locations) { | |
| if(fitTrace.locations?.length) { |
| // For point data straddling the antimeridian (±180°), the naive [min, max] | ||
| // longitude range above can include a large empty span; prefer the compact | ||
| // crossing range instead. Skipped when a trace contributes region extents | ||
| // (choropleth or location-based scattergeo), whose geographic bounds are not | ||
| // captured by the point longitudes gathered here. |
There was a problem hiding this comment.
Could you update this wording?
| // For point data straddling the antimeridian (±180°), the naive [min, max] | |
| // longitude range above can include a large empty span; prefer the compact | |
| // crossing range instead. Skipped when a trace contributes region extents | |
| // (choropleth or location-based scattergeo), whose geographic bounds are not | |
| // captured by the point longitudes gathered here. | |
| // For point data straddling the antimeridian (±180°), the naive [min, max] | |
| // longitude range above can include a large empty span; prefer the compact | |
| // crossing range instead. Restricted to fitbounds='locations' with no | |
| // region-bearing traces: choropleth, scattergeo `locations`, and the | |
| // geojson-bbox path used by fitbounds='geojson' + locationmode='geojson-id' | |
| // all carry region extents that per-point lonlat centroids don't capture. |
| expect(lonRange[1]).toBeGreaterThan(181); | ||
| expect(lonRange[1] - lonRange[0]).toBeGreaterThan(49); | ||
| expect(lonRange[1] - lonRange[0]).toBeLessThan(70); | ||
| expect(geoLayout._subplot.projection.rotate()[0]).toBeCloseTo(-156.4, 0); |
There was a problem hiding this comment.
Could you tighten up this assertion a bit?
| expect(geoLayout._subplot.projection.rotate()[0]).toBeCloseTo(-156.4, 0); | |
| expect(geoLayout._subplot.projection.rotate()[0]).toBeCloseTo(-156.44, 1); |





fixes #7844.
With
fitbounds: "locations"on a geo subplot, points that straddle the antimeridian make the map zoom out far more than it should. The longitude range comes straight fromgetAutoRangeas a plain min/max, so it measures the span the long way around the globe. The issue's example (lon131.8855and-179) ends up with a ~311° span, when crossing the antimeridian only needs ~49°.I added a helper,
getFitboundsLonRange, that sorts the longitudes, finds the largest gap between neighbouring points, and if that gap is wider than the one the naive range leaves open across the antimeridian, returns the complementary range instead. The result can exceed 180° (e.g.[131.8855, 181]), butmakeRangeBoxalready handles ranges that cross the antimeridian, so the projection code is untouched.It runs right after
getAutoRange, so it can only shrink the range, never grow it. It deliberately stays out of the way when:Tests: unit tests on the helper (straddling, not straddling, whole globe, too few points) plus an integration test that renders both of the issue's cases and checks the fitted range and the projection rotation. The existing fitbounds mocks and the winkel-tripel draw-time test are all location-based or globe-spanning, so none of them shift.
One open question: the range I return is tight, whereas
getAutoRangepads a little for marker size, so in the crossing case points can sit right on the edge. Happy to mirror that padding if you'd prefer.