diff --git a/draftlogs/7976_fix.md b/draftlogs/7976_fix.md new file mode 100644 index 00000000000..371bda8cd5c --- /dev/null +++ b/draftlogs/7976_fix.md @@ -0,0 +1 @@ + - Fix geo `albers`, `bertin1953`, `gringorten quincuncial`, `peirce quincuncial`, `sinu mollweide` and `wiechel` projections losing their D3 default rotation to the default `projection.rotation` values, and compose user-specified rotation on top of the projection's canonical orientation [[#7976](https://github.com/plotly/plotly.js/pull/7976)] diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 693c64550d5..410241f47a2 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -304,7 +304,11 @@ proto.updateProjection = function(geoCalcData, fullLayout) { // set 'pre-fit' projection projection .center([center.lon - rotation.lon, center.lat - rotation.lat]) - .rotate([-rotation.lon, -rotation.lat, rotation.roll]) + .rotate([ + -rotation.lon + projection.defaultRotation[0], + -rotation.lat + projection.defaultRotation[1], + rotation.roll + projection.defaultRotation[2] + ]) .parallels(projLayout.parallels); // fit projection 'scale' and 'translate' to set lon/lat ranges @@ -716,6 +720,12 @@ function getProjection(geoLayout) { projName = 'geo' + Lib.titleCase(projName); var projFn = geo[projName] || geoProjection[projName]; var projection = projFn(); + // Capture the rotation the projection factory ships with (identity for + // most projections, but e.g. albers defaults to [96, 0, 0]) so that the + // default projection.rotation attributes do not discard the canonical + // orientation. See #7949. + var projDefaultRotation = typeof projection.rotate === 'function' ? + projection.rotate() : [0, 0, 0]; var clipAngle = geoLayout._isSatellite ? Math.acos(1 / projLayout.distance) * 180 / Math.PI : @@ -754,6 +764,8 @@ function getProjection(geoLayout) { return projection.getPath().bounds(object); }; + projection.defaultRotation = projDefaultRotation; + projection.precision(constants.precision); if(geoLayout._isSatellite) { diff --git a/test/image/baselines/canada_geo_projections.png b/test/image/baselines/canada_geo_projections.png index c33cf2161ec..05178b4afdb 100644 Binary files a/test/image/baselines/canada_geo_projections.png and b/test/image/baselines/canada_geo_projections.png differ diff --git a/test/image/baselines/geo_fitbounds-locations.png b/test/image/baselines/geo_fitbounds-locations.png index ebf410cb1a6..e1dfe1f0b8c 100644 Binary files a/test/image/baselines/geo_fitbounds-locations.png and b/test/image/baselines/geo_fitbounds-locations.png differ diff --git a/test/image/baselines/various_geo_projections.png b/test/image/baselines/various_geo_projections.png index 4f2aa6e1ac4..553f7533983 100644 Binary files a/test/image/baselines/various_geo_projections.png and b/test/image/baselines/various_geo_projections.png differ diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index ab6f7122686..a5c937aad39 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -2876,3 +2876,42 @@ describe('plotly_relayouting', function() { }); }); }); + +describe('Test geo projection D3 default rotation (#7949)', function() { + var gd; + + beforeEach(function() { gd = createGraphDiv(); }); + afterEach(destroyGraphDiv); + + function subplot() { return gd._fullLayout.geo._subplot; } + + it('preserves the projection factory rotation at default projection.rotation', function(done) { + Plotly.newPlot(gd, [{ type: 'scattergeo', lon: [], lat: [] }], { + geo: { projection: { type: 'albers' } } + }).then(function() { + // d3-geo albers ships with rotation [96, 0, 0]; plotly's default + // projection.rotation attributes must not discard it. + // Compare with a tight numeric tolerance: the composed factory + + // user rotation can pick up float64 dust (e.g. 96.00000000000001 + // on linux runners), so exact equality is environment-dependent. + [0, 1, 2].forEach(function(axis) { + expect(subplot().projection.defaultRotation[axis]).toBeCloseTo([96, 0, 0][axis], 9); + expect(subplot().projection.rotate()[axis]).toBeCloseTo([96, 0, 0][axis], 9); + }); + }).then(function() { + return Plotly.relayout(gd, { 'geo.projection.rotation.lon': 10 }); + }).then(function() { + // user rotation composes on top of the factory rotation + expect(subplot().projection.rotate()[0]).toBeCloseTo(86, 9); + }).then(done, done.fail); + }); + + it('keeps identity-rotation projections exactly as before', function(done) { + Plotly.newPlot(gd, [{ type: 'scattergeo', lon: [], lat: [] }], { + geo: { projection: { type: 'mercator' } } + }).then(function() { + expect(subplot().projection.defaultRotation).toEqual([0, 0, 0]); + expect(subplot().projection.rotate()).toEqual([0, 0, 0]); + }).then(done, done.fail); + }); +});