diff --git a/draftlogs/7895_change.md b/draftlogs/7895_change.md new file mode 100644 index 00000000000..2ded72c5d5f --- /dev/null +++ b/draftlogs/7895_change.md @@ -0,0 +1,3 @@ +- **Breaking:** Change `layout.geo.fitbounds` default from `false` to `'locations'` [[#7895](https://github.com/plotly/plotly.js/pull/7895)] + - `geo` subplots will now auto-fit the initial view to the trace data + - Set `fitbounds: false` explicitly to opt out diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 41f11041b61..c6df270b79f 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -594,6 +594,8 @@ modeBarButtons.hoverClosestGeo = { click: toggleHover }; +const ZOOM_STEP_GEO = 2; + function handleGeo(gd, ev) { const button = ev.currentTarget; const attr = button.getAttribute('data-attr'); @@ -603,21 +605,38 @@ function handleGeo(gd, ev) { for (const id of geoIds) { const geoLayout = fullLayout[id]; + const geoSubplot = geoLayout._subplot; if (attr === 'zoom') { - const { minscale, scale } = geoLayout.projection; + // Under fitbounds, geoLayout.projection.scale is undefined; read the + // effective view from the D3 projection state instead + const projection = geoSubplot.projection; + const effectiveScale = projection.scale() / geoSubplot.fitScale; // Convert to schema format (multiples of original scale) + const [rotationLon, rotationLat] = projection.rotate().map((d) => -d); // Flip sign because D3 rotation is opposite of ours + const [centerLon, centerLat] = projection.invert(geoSubplot.midPt); + + const { minscale } = geoLayout.projection; const maxscale = geoLayout.projection.maxscale ?? Infinity; // swap if user supplied min > max so clamping is well-defined const min = Math.min(minscale, maxscale); const max = Math.max(minscale, maxscale); - let newScale = val === 'in' ? 2 * scale : 0.5 * scale; + let newScale = val === 'in' ? ZOOM_STEP_GEO * effectiveScale : (1 / ZOOM_STEP_GEO) * effectiveScale; // clamp to [min, max] if (newScale > max) newScale = max; else if (newScale < min) newScale = min; - if (newScale !== scale) { - Registry.call('_guiRelayout', gd, id + '.projection.scale', newScale); + if (newScale !== effectiveScale) { + // Persist the currently-effective view attrs with the new scale; turn + // off fitbounds so auto-fit doesn't overwrite them on the ensuing replot + Registry.call('_guiRelayout', gd, { + [id + '.projection.scale']: newScale, + [id + '.projection.rotation.lon']: rotationLon, + [id + '.projection.rotation.lat']: rotationLat, + [id + '.center.lon']: centerLon, + [id + '.center.lat']: centerLat, + [id + '.fitbounds']: false + }); } } } diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 649cd0725fe..cba421464af 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -353,9 +353,12 @@ proto.updateProjection = function (geoCalcData, fullLayout) { // so here's this hack to make it respond to 'geoLayout.center' if (geoLayout._isAlbersUsa) { var centerPx = projection([center.lon, center.lat]); - var tt = projection.translate(); - - projection.translate([tt[0] - (centerPx[0] - tt[0]), tt[1] - (centerPx[1] - tt[1])]); + // If center isn't within the Albers USA bounds (clipped to the USA), + // `projection(...)` returns null so skip the recentering + if (centerPx) { + var tt = projection.translate(); + projection.translate([tt[0] - (centerPx[0] - tt[0]), tt[1] - (centerPx[1] - tt[1])]); + } } }; @@ -644,7 +647,9 @@ proto.saveViewInitial = function (geoLayout) { } else if (geoLayout._isClipped) { extra = { 'projection.rotation.lon': rotation.lon, - 'projection.rotation.lat': rotation.lat + 'projection.rotation.lat': rotation.lat, + 'center.lon': center.lon, + 'center.lat': center.lat }; } else { extra = { diff --git a/src/plots/geo/layout_attributes.js b/src/plots/geo/layout_attributes.js index 287c8304018..59ee7b3c65d 100644 --- a/src/plots/geo/layout_attributes.js +++ b/src/plots/geo/layout_attributes.js @@ -57,7 +57,7 @@ var attrs = (module.exports = overrideAll( fitbounds: { valType: 'enumerated', values: [false, 'locations', 'geojson'], - dflt: false, + dflt: 'locations', editType: 'plot', description: [ "Determines if this subplot's view settings are auto-computed to fit trace data.", @@ -74,9 +74,9 @@ var attrs = (module.exports = overrideAll( // TODO we should auto-fill `projection.parallels` for maps // with conic projection, but how? - "If *locations*, only the trace's visible locations are considered in the `fitbounds` computations.", - 'If *geojson*, the entire trace input `geojson` (if provided) is considered in the `fitbounds` computations,', - 'Defaults to *false*.' + "If *locations* (default), only the trace's visible locations are considered in the `fitbounds` computations.", + 'If *geojson*, the entire trace input `geojson` (if provided) is considered in the `fitbounds` computations.', + 'If *false*, the view settings are used as-is; set this to opt out of auto-fitting.' ].join(' ') }, diff --git a/src/plots/geo/layout_defaults.js b/src/plots/geo/layout_defaults.js index 2b02ce07de8..bfedd939d4a 100644 --- a/src/plots/geo/layout_defaults.js +++ b/src/plots/geo/layout_defaults.js @@ -22,24 +22,26 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { var subplotData = getSubplotData(opts.fullData, 'geo', opts.id); - var traceIndices = subplotData.map(function(t) { return t.index; }); + var traceIndices = subplotData.map(function (t) { + return t.index; + }); var resolution = coerce('resolution'); var scope = coerce('scope'); var scopeParams = constants.scopeDefaults[scope]; var projType = coerce('projection.type', scopeParams.projType); - var isAlbersUsa = geoLayoutOut._isAlbersUsa = projType === 'albers usa'; + var isAlbersUsa = (geoLayoutOut._isAlbersUsa = projType === 'albers usa'); // no other scopes are allowed for 'albers usa' projection - if(isAlbersUsa) scope = geoLayoutOut.scope = 'usa'; + if (isAlbersUsa) scope = geoLayoutOut.scope = 'usa'; - var isScoped = geoLayoutOut._isScoped = (scope !== 'world'); - var isSatellite = geoLayoutOut._isSatellite = projType === 'satellite'; - var isConic = geoLayoutOut._isConic = projType.indexOf('conic') !== -1 || projType === 'albers'; - var isClipped = geoLayoutOut._isClipped = !!constants.lonaxisSpan[projType]; + var isScoped = (geoLayoutOut._isScoped = scope !== 'world'); + var isSatellite = (geoLayoutOut._isSatellite = projType === 'satellite'); + var isConic = (geoLayoutOut._isConic = projType.indexOf('conic') !== -1 || projType === 'albers'); + var isClipped = (geoLayoutOut._isClipped = !!constants.lonaxisSpan[projType]); - if(geoLayoutIn.visible === false) { + if (geoLayoutIn.visible === false) { // should override template.layout.geo.show* - see issue 4482 // make a copy @@ -54,8 +56,8 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { newTemplate.showocean = false; newTemplate.showrivers = false; newTemplate.showsubunits = false; - if(newTemplate.lonaxis) newTemplate.lonaxis.showgrid = false; - if(newTemplate.lataxis) newTemplate.lataxis.showgrid = false; + if (newTemplate.lonaxis) newTemplate.lonaxis.showgrid = false; + if (newTemplate.lataxis) newTemplate.lataxis.showgrid = false; // set ref to copy geoLayoutOut._template = newTemplate; @@ -63,20 +65,17 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { var visible = coerce('visible'); var show; - for(var i = 0; i < axesNames.length; i++) { + for (var i = 0; i < axesNames.length; i++) { var axisName = axesNames[i]; var dtickDflt = [30, 10][i]; var rangeDflt; - if(isScoped) { + if (isScoped) { rangeDflt = scopeParams[axisName + 'Range']; } else { var dfltSpans = constants[axisName + 'Span']; var hSpan = (dfltSpans[projType] || dfltSpans['*']) / 2; - var rot = coerce( - 'projection.rotation.' + axisName.slice(0, 3), - scopeParams.projRotate[i] - ); + var rot = coerce('projection.rotation.' + axisName.slice(0, 3), scopeParams.projRotate[i]); rangeDflt = [rot - hSpan, rot + hSpan]; } @@ -85,7 +84,7 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { coerce(axisName + '.dtick', dtickDflt); show = coerce(axisName + '.showgrid', !visible ? false : undefined); - if(show) { + if (show) { coerce(axisName + '.gridcolor'); coerce(axisName + '.gridwidth'); coerce(axisName + '.griddash'); @@ -114,7 +113,7 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { var centerLon = (lon0 + lon1) / 2; var projLon; - if(!isAlbersUsa) { + if (!isAlbersUsa) { var dfltProjRotate = isScoped ? scopeParams.projRotate : [centerLon, 0, 0]; projLon = coerce('projection.rotation.lon', dfltProjRotate[0]); @@ -122,19 +121,19 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { coerce('projection.rotation.roll', dfltProjRotate[2]); show = coerce('showcoastlines', !isScoped && visible); - if(show) { + if (show) { coerce('coastlinecolor'); coerce('coastlinewidth'); } show = coerce('showocean', !visible ? false : undefined); - if(show) coerce('oceancolor'); + if (show) coerce('oceancolor'); } var centerLonDflt; var centerLatDflt; - if(isAlbersUsa) { + if (isAlbersUsa) { // 'albers usa' does not have a 'center', // these values were found using via: // projection.invert([geoLayout.center.lon, geoLayoutIn.center.lat]) @@ -148,12 +147,12 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { coerce('center.lon', centerLonDflt); coerce('center.lat', centerLatDflt); - if(isSatellite) { + if (isSatellite) { coerce('projection.tilt'); coerce('projection.distance'); } - if(isConic) { + if (isConic) { var dfltProjParallels = scopeParams.projParallels || [0, 60]; coerce('projection.parallels', dfltProjParallels); } @@ -163,24 +162,24 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { coerce('projection.maxscale'); show = coerce('showland', !visible ? false : undefined); - if(show) coerce('landcolor'); + if (show) coerce('landcolor'); show = coerce('showlakes', !visible ? false : undefined); - if(show) coerce('lakecolor'); + if (show) coerce('lakecolor'); show = coerce('showrivers', !visible ? false : undefined); - if(show) { + if (show) { coerce('rivercolor'); coerce('riverwidth'); } show = coerce('showcountries', isScoped && scope !== 'usa' && visible); - if(show) { + if (show) { coerce('countrycolor'); coerce('countrywidth'); } - if(scope === 'usa' || (scope === 'north america' && resolution === 50)) { + if (scope === 'usa' || (scope === 'north america' && resolution === 50)) { // Only works for: // USA states at 110m // USA states + Canada provinces at 50m @@ -189,10 +188,10 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { coerce('subunitwidth'); } - if(!isScoped) { + if (!isScoped) { // Does not work in non-world scopes show = coerce('showframe', visible); - if(show) { + if (show) { coerce('framecolor'); coerce('framewidth'); } @@ -200,26 +199,47 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { coerce('bgcolor'); - var fitBounds = coerce('fitbounds'); - - // clear attributes that will get auto-filled later - if(fitBounds) { - delete geoLayoutOut.projection.scale; - - if(isScoped) { - delete geoLayoutOut.center.lon; - delete geoLayoutOut.center.lat; - } else if(isClipped) { - delete geoLayoutOut.center.lon; - delete geoLayoutOut.center.lat; - delete geoLayoutOut.projection.rotation.lon; - delete geoLayoutOut.projection.rotation.lat; - delete geoLayoutOut.lonaxis.range; - delete geoLayoutOut.lataxis.range; + // Only use fitbounds if user hasn't set any view attributes. This prevents + // user-specified view info from being ignored. + coerce('fitbounds'); + if (geoLayoutOut.fitbounds) { + const centerIn = geoLayoutIn.center || {}; + const projectionIn = geoLayoutIn.projection || {}; + const rotationIn = projectionIn.rotation || {}; + const lonaxisIn = geoLayoutIn.lonaxis || {}; + const lataxisIn = geoLayoutIn.lataxis || {}; + const hasUserView = [ + centerIn.lon, + centerIn.lat, + rotationIn.lon, + rotationIn.lat, + projectionIn.scale, + lonaxisIn.range, + lataxisIn.range + ].some((d) => d !== undefined); + // The Albers projection doesn't need a fit, so skip here + if (hasUserView || isAlbersUsa) geoLayoutOut.fitbounds = false; + } + + // Set auto-filled view attributes to null so updateProjection can + // compute the fit from scratch and fullLayout matches user input + if (geoLayoutOut.fitbounds) { + geoLayoutOut.projection.scale = null; + + if (isScoped) { + geoLayoutOut.center.lon = null; + geoLayoutOut.center.lat = null; + } else if (isClipped) { + geoLayoutOut.center.lon = null; + geoLayoutOut.center.lat = null; + geoLayoutOut.projection.rotation.lon = null; + geoLayoutOut.projection.rotation.lat = null; + geoLayoutOut.lonaxis.range = null; + geoLayoutOut.lataxis.range = null; } else { - delete geoLayoutOut.center.lon; - delete geoLayoutOut.center.lat; - delete geoLayoutOut.projection.rotation.lon; + geoLayoutOut.center.lon = null; + geoLayoutOut.center.lat = null; + geoLayoutOut.projection.rotation.lon = null; } } } diff --git a/src/plots/geo/zoom.js b/src/plots/geo/zoom.js index 63892a4e85d..c3865e48e95 100644 --- a/src/plots/geo/zoom.js +++ b/src/plots/geo/zoom.js @@ -63,6 +63,8 @@ function sync(geo, projection, cb) { cb(set); set('projection.scale', projection.scale() / geo.fitScale); + // Turn off fitbounds so subsequent replays don't re-run the auto-fit and + // overwrite the user's dragged/scrolled position set('fitbounds', false); gd.emit('plotly_relayout', eventData); } diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 92fdc3e79d7..3f4d96b013e 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -11694,8 +11694,8 @@ export interface GeoLayout { countrywidth?: number; domain?: Domain; /** - * Determines if this subplot's view settings are auto-computed to fit trace data. On scoped maps, setting `fitbounds` leads to `center.lon` and `center.lat` getting auto-filled. On maps with a non-clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, and `projection.rotation.lon` getting auto-filled. On maps with a clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, `projection.rotation.lon`, `projection.rotation.lat`, `lonaxis.range` and `lataxis.range` getting auto-filled. If *locations*, only the trace's visible locations are considered in the `fitbounds` computations. If *geojson*, the entire trace input `geojson` (if provided) is considered in the `fitbounds` computations, Defaults to *false*. - * @default false + * Determines if this subplot's view settings are auto-computed to fit trace data. On scoped maps, setting `fitbounds` leads to `center.lon` and `center.lat` getting auto-filled. On maps with a non-clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, and `projection.rotation.lon` getting auto-filled. On maps with a clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, `projection.rotation.lon`, `projection.rotation.lat`, `lonaxis.range` and `lataxis.range` getting auto-filled. If *locations* (default), only the trace's visible locations are considered in the `fitbounds` computations. If *geojson*, the entire trace input `geojson` (if provided) is considered in the `fitbounds` computations. If *false*, the view settings are used as-is; set this to opt out of auto-fitting. + * @default 'locations' */ fitbounds?: false | 'locations' | 'geojson'; /** diff --git a/test/image/make_baseline.py b/test/image/make_baseline.py index 0c0934adc2c..bb36add0186 100644 --- a/test/image/make_baseline.py +++ b/test/image/make_baseline.py @@ -87,13 +87,17 @@ async def make_baselines_async(): if mathjax is not None: kopts["mathjax"] = mathjax + # Per-mock timeout so a runaway render surfaces as a named failure instead + # of hanging the whole shard until the runner times out. + PER_MOCK_TIMEOUT_SECONDS = 60 + async with kaleido.Kaleido(n=1, **kopts) as k: for name in allNames: outName = name if mathjax_version == 3: outName = "mathjax3___" + name - print(outName) + print(outName, flush=True) created = False @@ -124,23 +128,35 @@ async def make_baselines_async(): print(json.dumps(fig, indent=2)) try: - bytes = await k.calc_fig( - fig, - opts=dict( - format="png", - width=width, - height=height, + bytes = await asyncio.wait_for( + k.calc_fig( + fig, + opts=dict( + format="png", + width=width, + height=height, + ), + topojson=topojson, ), - topojson=topojson, + timeout=PER_MOCK_TIMEOUT_SECONDS, ) filename = os.path.join(dirOut, outName + ".png") with open(filename, "wb") as f: f.write(bytes) created = True + except asyncio.TimeoutError: + print( + f"timed out after {PER_MOCK_TIMEOUT_SECONDS}s", + flush=True, + ) + if attempt < MAX_RETRY: + print("retry", attempt + 1, "/", MAX_RETRY, flush=True) + else: + failed.append(outName) except Exception as e: - print(e) + print(e, flush=True) if attempt < MAX_RETRY: - print("retry", attempt + 1, "/", MAX_RETRY) + print("retry", attempt + 1, "/", MAX_RETRY, flush=True) else: failed.append(outName) diff --git a/test/jasmine/tests/choropleth_test.js b/test/jasmine/tests/choropleth_test.js index d26468b8fcb..d1e0444ed82 100644 --- a/test/jasmine/tests/choropleth_test.js +++ b/test/jasmine/tests/choropleth_test.js @@ -177,6 +177,10 @@ describe('Test choropleth hover:', function() { fontFamily: 'Arial' }; + // Tests here were written for `fitbounds: false`, so set that explicitly + fig.layout ||= {}; + fig.layout.geo = Lib.extendDeep({}, fig.layout.geo, { fitbounds: false }); + return Plotly.newPlot(gd, fig) .then(function() { if(hasCssTransform) { diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index ada6fbb6cf4..2f38d4a2b3f 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -103,7 +103,15 @@ describe('Test geo fitbounds with antimeridian-straddling points', function () { describe('Test Geo layout defaults', function () { var layoutAttributes = Geo.layoutAttributes; - var supplyLayoutDefaults = Geo.supplyLayoutDefaults; + // Tests here were written against `fitbounds` defaulting to `false`. Shim + // the helper to inject that default when a test doesn't set fitbounds itself. + const supplyLayoutDefaults = (layoutIn, layoutOut, fullData) => { + if (layoutIn.geo && !('fitbounds' in layoutIn.geo)) { + layoutIn.geo.fitbounds = false; + } + + return Geo.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + }; var layoutIn, layoutOut, fullData; @@ -546,7 +554,7 @@ describe('Test Geo layout defaults', function () { }); }); - describe('should clear attributes that get auto-filled under *fitbounds*', function () { + describe('should retain coerced attributes under *fitbounds*', function () { var vals = ['locations', 'geojson']; function _assert(exp) { @@ -599,9 +607,9 @@ describe('Test Geo layout defaults', function () { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); _assert({ - 'projection.scale': undefined, - 'center.lon': undefined, - 'center.lat': undefined, + 'projection.scale': null, + 'center.lon': null, + 'center.lat': null, 'projection.rotation.lon': 15, 'projection.rotation.lat': 0, 'lonaxis.range': [-30, 60], @@ -649,13 +657,13 @@ describe('Test Geo layout defaults', function () { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); _assert({ - 'projection.scale': undefined, - 'center.lon': undefined, - 'center.lat': undefined, - 'projection.rotation.lon': undefined, - 'projection.rotation.lat': undefined, - 'lonaxis.range': undefined, - 'lataxis.range': undefined + 'projection.scale': 2, + 'center.lon': 20, + 'center.lat': 20, + 'projection.rotation.lon': 20, + 'projection.rotation.lat': 20, + 'lonaxis.range': [-70, 110], + 'lataxis.range': [-70, 110] }); }); }); @@ -703,10 +711,10 @@ describe('Test Geo layout defaults', function () { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); _assert({ - 'projection.scale': undefined, - 'center.lon': undefined, - 'center.lat': undefined, - 'projection.rotation.lon': undefined, + 'projection.scale': 2, + 'center.lon': 20, + 'center.lat': 40, + 'projection.rotation.lon': 20, 'projection.rotation.lat': 0, 'lonaxis.range': [-90, 90], 'lataxis.range': [0, 80] @@ -933,6 +941,10 @@ describe('Test geo interactions', function () { gd = createGraphDiv(); var mockCopy = Lib.extendDeep({}, mock); + // These tests check hover/click coordinates assuming fitbounds is 'false', + // so set it here (v4 changed the default to 'locations') + mockCopy.layout.geo ||= {}; + mockCopy.layout.geo.fitbounds = false; Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(done); }); @@ -1666,7 +1678,7 @@ describe('Test geo interactions', function () { z: ['10', '20', '15'] } ], - layout: { geo: { scope: 'world' } } + layout: { geo: { scope: 'world', fitbounds: false } } }; var figUSA = { data: [ @@ -1677,7 +1689,7 @@ describe('Test geo interactions', function () { z: ['10', '20', '15'] } ], - layout: { geo: { scope: 'usa' } } + layout: { geo: { scope: 'usa', fitbounds: false } } }; var figNA = { data: [ @@ -1688,7 +1700,7 @@ describe('Test geo interactions', function () { z: ['10', '20', '15'] } ], - layout: { geo: { scope: 'north america' } } + layout: { geo: { scope: 'north america', fitbounds: false } } }; Plotly.react(gd, figWorld) @@ -2496,16 +2508,13 @@ describe('Test geo zoom/pan/drag interactions:', function () { }); it('- fitbounds case', function (done) { + // Clear the mock's user-set rotation so the fitbounds gate doesn't opt out + delete fig.layout.geo.projection.rotation; fig.layout.geo.fitbounds = 'locations'; newPlot(fig) .then(function () { - _assert( - 'base', - [[undefined, 0], [undefined, undefined], undefined], - [[-180, -0], [350, 260], [0, 0], 114.59], - undefined - ); + _assert('base', [[null, 0], [null, null], null], [[-180, -0], [350, 260], [0, 0], 114.59], undefined); return drag({ path: [ [350, 250], @@ -2555,7 +2564,7 @@ describe('Test geo zoom/pan/drag interactions:', function () { .then(function () { _assert( 'after double click', - [[undefined, 0], [undefined, undefined], undefined], + [[null, 0], [null, null], null], [[-180, -0], [350, 260], [0, 0], 114.59], 'dblclick' ); @@ -2657,11 +2666,13 @@ describe('Test geo zoom/pan/drag interactions:', function () { }); it('- fitbounds case', function (done) { + // Clear the mock's user-set rotation so the fitbounds gate doesn't opt out + delete fig.layout.geo.projection.rotation; fig.layout.geo.fitbounds = 'locations'; newPlot(fig) .then(function () { - _assert('base', [[undefined, undefined], undefined], [[-76.014, -19.8], 160], undefined); + _assert('base', [[null, null], null], [[-76.014, -19.735], 160], undefined); return drag({ path: [ [250, 250], @@ -2703,13 +2714,7 @@ describe('Test geo zoom/pan/drag interactions:', function () { return dblClick([350, 250]); }) .then(function () { - // resets to initial view - _assert( - 'after double click', - [[undefined, undefined], undefined], - [[-76.014, -19.8], 160], - 'dblclick' - ); + _assert('after double click', [[null, null], null], [[-76.014, -19.8], 160], 'dblclick'); }) .then(done, done.fail); }); @@ -2722,6 +2727,7 @@ describe('Test geo zoom/pan/drag interactions:', function () { fig = Lib.extendDeep({}, require('../../image/mocks/geo_europe-bubbles')); fig.layout.geo.resolution = 110; fig.layout.dragmode = 'pan'; + fig.layout.geo.fitbounds = false; // of layout width = height = 500 }); @@ -2810,12 +2816,7 @@ describe('Test geo zoom/pan/drag interactions:', function () { newPlot(fig) .then(function () { - _assert( - 'base', - [[undefined, undefined], undefined], - [[247, 260], [5.7998, 49.29], 504.8559], - undefined - ); + _assert('base', [[null, null], null], [[247, 260], [5.7998, 49.29], 504.8559], undefined); return drag({ path: [ [250, 250], @@ -2829,7 +2830,7 @@ describe('Test geo zoom/pan/drag interactions:', function () { 'after SW-NE drag', [[29.059, 42.38], 1.727], [[197, 210], [5.7988, 49.29], 504.8559], - ['geo.center.lon', 'geo.center.lon', 'geo.projection.scale', 'geo.fitbounds'] + ['geo.center.lon', 'geo.center.lat', 'geo.projection.scale', 'geo.fitbounds'] ); return scroll([300, 300], [-200, -200]); }) @@ -2859,7 +2860,7 @@ describe('Test geo zoom/pan/drag interactions:', function () { .then(function () { _assert( 'after double click', - [[undefined, undefined], undefined], + [[null, null], null], [[247, 260], [5.7998, 49.29], 504.8559], 'dblclick' ); @@ -2871,6 +2872,7 @@ describe('Test geo zoom/pan/drag interactions:', function () { it('should work for *albers usa* projections', function (done) { var fig = Lib.extendDeep({}, require('../../image/mocks/geo_choropleth-usa')); fig.layout.dragmode = 'pan'; + fig.layout.geo.fitbounds = false; // layout width = 870 // layout height = 598 @@ -3214,6 +3216,7 @@ describe('plotly_relayouting', function () { fig.layout.dragmode = dragmode; fig.layout.width = 700; fig.layout.height = 500; + fig.layout.geo.fitbounds = false; newPlot(fig) .then(function () { diff --git a/test/jasmine/tests/plot_api_react_test.js b/test/jasmine/tests/plot_api_react_test.js index 8eb8ffbef7f..11d0dcc097c 100644 --- a/test/jasmine/tests/plot_api_react_test.js +++ b/test/jasmine/tests/plot_api_react_test.js @@ -1411,7 +1411,7 @@ describe('Plotly.react and uirevision attributes', function() { }], layout: { uirevision: mainRev, - geo: {uirevision: geoRev} + geo: {uirevision: geoRev, fitbounds: false} } }; } @@ -2017,7 +2017,8 @@ describe('Test Plotly.react + interactions under uirevision:', function() { }], { width: 500, height: 500, - uirevision: true + uirevision: true, + geo: {fitbounds: false} }); } @@ -2044,7 +2045,7 @@ describe('Test Plotly.react + interactions under uirevision:', function() { _react() .then(function() { - expect(gd.layout.geo).toEqual({}); + expect(gd.layout.geo).toEqual({fitbounds: false}); var fullGeo = gd._fullLayout.geo; expect(fullGeo.projection.rotation.lon).toBe(0); diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js index 155a2f3cc06..4e98aed34fc 100644 --- a/test/jasmine/tests/scattergeo_test.js +++ b/test/jasmine/tests/scattergeo_test.js @@ -314,7 +314,9 @@ describe('Test scattergeo hover', function() { lon: [10, 20, 30], lat: [10, 20, 30], text: ['A', 'B', 'C'] - }]) + }], { + geo: { fitbounds: false } + }) .then(done); }); diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 2261a72b24b..8c10b7a8793 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -654,8 +654,8 @@ describe('Click-to-select', function() { { width: 1100, height: 450 }), testCase('ohlc', require('../../image/mocks/ohlc_first.json'), 669, 165, [9]), testCase('candlestick', require('../../image/mocks/finance_style.json'), 331, 162, [[], [5]]), - testCase('choropleth', require('../../image/mocks/geo_choropleth-text.json'), 440, 163, [6]), - testCase('scattergeo', require('../../image/mocks/geo_scattergeo-locations.json'), 285, 240, [1]), + testCase('choropleth', require('../../image/mocks/geo_choropleth-text.json'), 440, 163, [6], {geo: {fitbounds: false}}), + testCase('scattergeo', require('../../image/mocks/geo_scattergeo-locations.json'), 285, 240, [1], {geo: {fitbounds: false}}), testCase('scatterternary', require('../../image/mocks/ternary_markers.json'), 485, 335, [7]), // Note that first trace (carpet) in mock doesn't support selection, @@ -770,7 +770,7 @@ describe('Click-to-select', function() { describe('triggers \'plotly_selected\' before \'plotly_click\'', function() { [ testCase('cartesian', require('../../image/mocks/14.json'), 270, 160, [7]), - testCase('geo', require('../../image/mocks/geo_scattergeo-locations.json'), 285, 240, [1]), + testCase('geo', require('../../image/mocks/geo_scattergeo-locations.json'), 285, 240, [1], {geo: {fitbounds: false}}), testCase('ternary', require('../../image/mocks/ternary_markers.json'), 485, 335, [7]), testCase('polar', require('../../image/mocks/polar_scatter.json'), 130, 290, [[], [], [], [19], [], []], { dragmode: 'zoom' }) @@ -2359,7 +2359,8 @@ describe('Test select box and lasso per trace:', function() { showlegend: false, dragmode: 'select', width: 800, - height: 600 + height: 600, + geo: {fitbounds: false} } }; addInvisible(fig); @@ -2583,6 +2584,7 @@ describe('Test select box and lasso per trace:', function() { fig.layout.height = 450; fig.layout.dragmode = 'select'; fig.layout.geo.scope = 'europe'; + fig.layout.geo.fitbounds = false; addInvisible(fig, false); // add a trace with no locations which will then make trace invisible, lacking DOM elements diff --git a/test/plot-schema.json b/test/plot-schema.json index f4286815758..0565b9539bf 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -2169,8 +2169,8 @@ }, "editType": "plot", "fitbounds": { - "description": "Determines if this subplot's view settings are auto-computed to fit trace data. On scoped maps, setting `fitbounds` leads to `center.lon` and `center.lat` getting auto-filled. On maps with a non-clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, and `projection.rotation.lon` getting auto-filled. On maps with a clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, `projection.rotation.lon`, `projection.rotation.lat`, `lonaxis.range` and `lataxis.range` getting auto-filled. If *locations*, only the trace's visible locations are considered in the `fitbounds` computations. If *geojson*, the entire trace input `geojson` (if provided) is considered in the `fitbounds` computations, Defaults to *false*.", - "dflt": false, + "description": "Determines if this subplot's view settings are auto-computed to fit trace data. On scoped maps, setting `fitbounds` leads to `center.lon` and `center.lat` getting auto-filled. On maps with a non-clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, and `projection.rotation.lon` getting auto-filled. On maps with a clipped projection, setting `fitbounds` leads to `center.lon`, `center.lat`, `projection.rotation.lon`, `projection.rotation.lat`, `lonaxis.range` and `lataxis.range` getting auto-filled. If *locations* (default), only the trace's visible locations are considered in the `fitbounds` computations. If *geojson*, the entire trace input `geojson` (if provided) is considered in the `fitbounds` computations. If *false*, the view settings are used as-is; set this to opt out of auto-fitting.", + "dflt": "locations", "editType": "plot", "valType": "enumerated", "values": [