diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 9a0961f1661..df7a220aa54 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -314,7 +314,7 @@ proto.updateBaseLayers = function(fullLayout, geoLayout) { } else if(isLineLayer(d) || isFillLayer(d)) { path.datum(topojsonFeature(topojson, topojson.objects[d])); } else if(isAxisLayer(d)) { - path.datum(makeGraticule(d, geoLayout)) + path.datum(makeGraticule(d, geoLayout, fullLayout)) .call(Color.stroke, geoLayout[d].gridcolor) .call(Drawing.dashLine, '', geoLayout[d].gridwidth); } @@ -660,20 +660,58 @@ function getProjection(geoLayout) { return projection; } -function makeGraticule(axisName, geoLayout) { - var axisLayout = geoLayout[axisName]; - var dtick = axisLayout.dtick; +function makeGraticule(axisName, geoLayout, fullLayout) { + // equivalent to the d3 "ε" + var epsilon = 1e-6; + // same as the geoGraticule default + var precision = 2.5; + + var axLayout = geoLayout[axisName]; var scopeDefaults = constants.scopeDefaults[geoLayout.scope]; - var lonaxisRange = scopeDefaults.lonaxisRange; - var lataxisRange = scopeDefaults.lataxisRange; - var step = axisName === 'lonaxis' ? [dtick] : [0, dtick]; - - return d3.geo.graticule() - .extent([ - [lonaxisRange[0], lataxisRange[0]], - [lonaxisRange[1], lataxisRange[1]] - ]) - .step(step); + var rng; + var oppRng; + var coordFn; + + if(axisName === 'lonaxis') { + rng = scopeDefaults.lonaxisRange; + oppRng = scopeDefaults.lataxisRange; + coordFn = function(v, l) { return [v, l]; }; + } else if(axisName === 'lataxis') { + rng = scopeDefaults.lataxisRange; + oppRng = scopeDefaults.lonaxisRange; + coordFn = function(v, l) { return [l, v]; }; + } + + var dummyAx = { + type: 'linear', + range: [rng[0], rng[1] - epsilon], + tick0: axLayout.tick0, + dtick: axLayout.dtick + }; + + Axes.setConvert(dummyAx, fullLayout); + var vals = Axes.calcTicks(dummyAx); + + // remove duplicate on antimeridian + if(!geoLayout.isScoped && axisName === 'lonaxis') { + vals.pop(); + } + + var len = vals.length; + var coords = new Array(len); + + for(var i = 0; i < len; i++) { + var v = vals[i].x; + var line = coords[i] = []; + for(var l = oppRng[0]; l < oppRng[1] + precision; l += precision) { + line.push(coordFn(v, l)); + } + } + + return { + type: 'MultiLineString', + coordinates: coords + }; } // Returns polygon GeoJSON corresponding to lon/lat range box diff --git a/src/plots/geo/index.js b/src/plots/geo/index.js index 1bf078b4911..0d050771509 100644 --- a/src/plots/geo/index.js +++ b/src/plots/geo/index.js @@ -6,30 +6,33 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; -var createGeo = require('./geo'); var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData; var counterRegex = require('../../lib').counterRegex; -var GEO = 'geo'; - -exports.name = GEO; - -exports.attr = GEO; - -exports.idRoot = GEO; - -exports.idRegex = exports.attrRegex = counterRegex(GEO); - -exports.attributes = require('./layout/attributes'); - -exports.layoutAttributes = require('./layout/layout_attributes'); +var createGeo = require('./geo'); -exports.supplyLayoutDefaults = require('./layout/defaults'); +var GEO = 'geo'; +var counter = counterRegex(GEO); + +var attributes = {}; +attributes[GEO] = { + valType: 'subplotid', + role: 'info', + dflt: GEO, + editType: 'calc', + description: [ + 'Sets a reference between this trace\'s geospatial coordinates and', + 'a geographic map.', + 'If *geo* (the default value), the geospatial coordinates refer to', + '`layout.geo`.', + 'If *geo2*, the geospatial coordinates refer to `layout.geo2`,', + 'and so on.' + ].join(' ') +}; -exports.plot = function plotGeo(gd) { +function plotGeo(gd) { var fullLayout = gd._fullLayout; var calcData = gd.calcdata; var geoIds = fullLayout._subplots[GEO]; @@ -62,9 +65,9 @@ exports.plot = function plotGeo(gd) { geo.plot(geoCalcData, fullLayout, gd._promises); } -}; +} -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { +function clean(newFullData, newFullLayout, oldFullData, oldFullLayout) { var oldGeoKeys = oldFullLayout._subplots[GEO] || []; for(var i = 0; i < oldGeoKeys.length; i++) { @@ -76,9 +79,9 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) oldGeo.clipDef.remove(); } } -}; +} -exports.updateFx = function(gd) { +function updateFx(gd) { var fullLayout = gd._fullLayout; var subplotIds = fullLayout._subplots[GEO]; @@ -87,4 +90,18 @@ exports.updateFx = function(gd) { var subplotObj = subplotLayout._subplot; subplotObj.updateFx(fullLayout, subplotLayout); } +} + +module.exports = { + attr: GEO, + name: GEO, + idRoot: GEO, + idRegex: counter, + attrRegex: counter, + attributes: attributes, + layoutAttributes: require('./layout_attributes'), + supplyLayoutDefaults: require('./layout_defaults'), + plot: plotGeo, + updateFx: updateFx, + clean: clean }; diff --git a/src/plots/geo/layout/attributes.js b/src/plots/geo/layout/attributes.js deleted file mode 100644 index 1a9e85ce52a..00000000000 --- a/src/plots/geo/layout/attributes.js +++ /dev/null @@ -1,27 +0,0 @@ -/** -* Copyright 2012-2019, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - - -module.exports = { - geo: { - valType: 'subplotid', - role: 'info', - dflt: 'geo', - editType: 'calc', - description: [ - 'Sets a reference between this trace\'s geospatial coordinates and', - 'a geographic map.', - 'If *geo* (the default value), the geospatial coordinates refer to', - '`layout.geo`.', - 'If *geo2*, the geospatial coordinates refer to `layout.geo2`,', - 'and so on.' - ].join(' ') - } -}; diff --git a/src/plots/geo/layout/layout_attributes.js b/src/plots/geo/layout_attributes.js similarity index 97% rename from src/plots/geo/layout/layout_attributes.js rename to src/plots/geo/layout_attributes.js index 3fd4e194f37..6232c40fbf3 100644 --- a/src/plots/geo/layout/layout_attributes.js +++ b/src/plots/geo/layout_attributes.js @@ -8,10 +8,10 @@ 'use strict'; -var colorAttrs = require('../../../components/color/attributes'); -var domainAttrs = require('../../domain').attributes; -var constants = require('../constants'); -var overrideAll = require('../../../plot_api/edit_types').overrideAll; +var colorAttrs = require('../../components/color/attributes'); +var domainAttrs = require('../domain').attributes; +var constants = require('./constants'); +var overrideAll = require('../../plot_api/edit_types').overrideAll; var geoAxesAttrs = { range: { @@ -35,6 +35,7 @@ var geoAxesAttrs = { tick0: { valType: 'number', role: 'info', + dflt: 0, description: [ 'Sets the graticule\'s starting tick longitude/latitude.' ].join(' ') diff --git a/src/plots/geo/layout/defaults.js b/src/plots/geo/layout_defaults.js similarity index 95% rename from src/plots/geo/layout/defaults.js rename to src/plots/geo/layout_defaults.js index f9e58c35ec7..34da1d42edf 100644 --- a/src/plots/geo/layout/defaults.js +++ b/src/plots/geo/layout_defaults.js @@ -9,8 +9,8 @@ 'use strict'; -var handleSubplotDefaults = require('../../subplot_defaults'); -var constants = require('../constants'); +var handleSubplotDefaults = require('../subplot_defaults'); +var constants = require('./constants'); var layoutAttributes = require('./layout_attributes'); var axesNames = constants.axesNames; @@ -58,9 +58,8 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) { rangeDflt = [rot - hSpan, rot + hSpan]; } - var range = coerce(axisName + '.range', rangeDflt); - - coerce(axisName + '.tick0', range[0]); + coerce(axisName + '.range', rangeDflt); + coerce(axisName + '.tick0'); coerce(axisName + '.dtick', dtickDflt); show = coerce(axisName + '.showgrid'); diff --git a/test/image/baselines/geo_across-antimeridian.png b/test/image/baselines/geo_across-antimeridian.png index c54950d9c2d..6af53d1055e 100644 Binary files a/test/image/baselines/geo_across-antimeridian.png and b/test/image/baselines/geo_across-antimeridian.png differ diff --git a/test/image/baselines/geo_aitoff-sinusoidal.png b/test/image/baselines/geo_aitoff-sinusoidal.png index f1c6316363a..3795890a9bc 100644 Binary files a/test/image/baselines/geo_aitoff-sinusoidal.png and b/test/image/baselines/geo_aitoff-sinusoidal.png differ diff --git a/test/image/baselines/geo_custom-colorscale.png b/test/image/baselines/geo_custom-colorscale.png index bb8652ce625..db094a5ee95 100644 Binary files a/test/image/baselines/geo_custom-colorscale.png and b/test/image/baselines/geo_custom-colorscale.png differ diff --git a/test/image/baselines/geo_kavrayskiy7.png b/test/image/baselines/geo_kavrayskiy7.png index 9ced1a0afd4..660a0b1cd97 100644 Binary files a/test/image/baselines/geo_kavrayskiy7.png and b/test/image/baselines/geo_kavrayskiy7.png differ diff --git a/test/image/baselines/geo_stereographic.png b/test/image/baselines/geo_stereographic.png index a77239e0fd9..a857fa45406 100644 Binary files a/test/image/baselines/geo_stereographic.png and b/test/image/baselines/geo_stereographic.png differ diff --git a/test/image/baselines/geo_tick0.png b/test/image/baselines/geo_tick0.png new file mode 100644 index 00000000000..77aec957046 Binary files /dev/null and b/test/image/baselines/geo_tick0.png differ diff --git a/test/image/baselines/geo_winkel-tripel.png b/test/image/baselines/geo_winkel-tripel.png index 7ad7d616bd6..12b5dc0fdc8 100644 Binary files a/test/image/baselines/geo_winkel-tripel.png and b/test/image/baselines/geo_winkel-tripel.png differ diff --git a/test/image/mocks/geo_kavrayskiy7.json b/test/image/mocks/geo_kavrayskiy7.json index 7a93d492974..0cba35ed780 100644 --- a/test/image/mocks/geo_kavrayskiy7.json +++ b/test/image/mocks/geo_kavrayskiy7.json @@ -72,10 +72,7 @@ }, "lataxis": { "showgrid": true, - "range": [ - -75, - 85 - ], + "range": [ -75, 85 ], "gridwidth": 2, "gridcolor": "black" } diff --git a/test/image/mocks/geo_tick0.json b/test/image/mocks/geo_tick0.json new file mode 100644 index 00000000000..354d104b9f3 --- /dev/null +++ b/test/image/mocks/geo_tick0.json @@ -0,0 +1,72 @@ +{ + "data": [ + {"type": "scattergeo", "lon": [5], "lat": [2], "name": "lonaxis.tick0: 5 | lataxis.tick0: 2"}, + {"type": "scattergeo", "lon": [10], "lat": [1], "name": "lonaxis.tick0: 10 | lataxis.tick0: 1", "geo": "geo2"}, + {"type": "scattergeo", "lon": [40], "lat": [-40], "name": "lonaxis.tick0: 40 | lataxis.tick0: -40", "geo": "geo3"}, + {"type": "scattergeo", "lon": [73], "lat": [45], "name": "lonaxis.tick0: 73 | lataxis.tick0: 45", "geo": "geo4"} + ], + "layout": { + "legend": { + "x": -0.05, + "xanchor": "right", + "y": 0.5, + "yanchor": "middle" + }, + "grid": {"columns": 2, "rows": 2}, + "geo": { + "domain": {"row": 0, "column": 0}, + "lonaxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": 5 + }, + "lataxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": 2 + } + }, + "geo2": { + "domain": {"row": 0, "column": 1}, + "lonaxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": 10 + }, + "lataxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": 1 + } + }, + "geo3": { + "domain": {"row": 1, "column": 0}, + "lonaxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": 40 + }, + "lataxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": -40 + } + }, + "geo4": { + "domain": {"row": 1, "column": 1}, + "lonaxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": 73 + }, + "lataxis": { + "showgrid": true, + "gridcolor": "#444", + "tick0": 45 + } + }, + "margin": {"t": 10, "b": 10}, + "width": 1100, + "height": 400 + } +} diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index 42aec715ea8..66ea091c014 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -268,8 +268,8 @@ describe('Test Geo layout defaults', function() { expect(layoutOut.geo.lonaxis.range).toEqual(dfltLonaxisRange); expect(layoutOut.geo.lataxis.range).toEqual(dfltLataxisRange); - expect(layoutOut.geo.lonaxis.tick0).toEqual(dfltLonaxisRange[0]); - expect(layoutOut.geo.lataxis.tick0).toEqual(dfltLataxisRange[0]); + expect(layoutOut.geo.lonaxis.tick0).toEqual(0); + expect(layoutOut.geo.lataxis.tick0).toEqual(0); }); it('custom case for ' + s, function() { @@ -284,8 +284,8 @@ describe('Test Geo layout defaults', function() { expect(layoutOut.geo.lonaxis.range).toEqual(customLonaxisRange); expect(layoutOut.geo.lataxis.range).toEqual(customLataxisRange); - expect(layoutOut.geo.lonaxis.tick0).toEqual(customLonaxisRange[0]); - expect(layoutOut.geo.lataxis.tick0).toEqual(customLataxisRange[0]); + expect(layoutOut.geo.lonaxis.tick0).toEqual(0); + expect(layoutOut.geo.lataxis.tick0).toEqual(0); }); }); }); @@ -1459,11 +1459,13 @@ describe('Test event property of interactions on a geo plot:', function() { }); describe('Test geo base layers', function() { + var gd; + + beforeEach(function() { gd = createGraphDiv(); }); + afterEach(destroyGraphDiv); it('should clear obsolete features and layers on *geo.scope* relayout calls', function(done) { - var gd = createGraphDiv(); - function _assert(geojson, layers) { var cd0 = gd.calcdata[0]; var subplot = gd._fullLayout.geo._subplot; @@ -1518,6 +1520,63 @@ describe('Test geo base layers', function() { .catch(failTest) .then(done); }); + + it('should be able to relayout axis grid *tick0* / *dtick*', function(done) { + function findGridPath(axisName) { + return d3.select(gd).select(axisName + ' > path').attr('d'); + } + + function first(parts) { + return parts[1].split('L')[0].split(',').map(Number); + } + + function _assert(msg, exp) { + var lonParts = findGridPath('.lonaxis').split('M'); + var latParts = findGridPath('.lataxis').split('M'); + + expect(lonParts.length).toBe(exp.lonCnt, msg + ' - lonaxis grid segments'); + expect(latParts.length).toBe(exp.latCnt, msg + ' - lataxis grid segments'); + + expect(first(lonParts)).toBeCloseToArray(exp.lon0, 1, msg + ' - first lonaxis grid pt'); + expect(first(latParts)).toBeCloseToArray(exp.lat0, 1, msg + ' - first lataxis grid pt'); + } + + Plotly.plot(gd, [{type: 'scattergeo'}], { + geo: { + lonaxis: {showgrid: true}, + lataxis: {showgrid: true} + } + }) + .then(function() { + _assert('base', { + lonCnt: 12, lon0: [124.99, 369.99], + latCnt: 18, lat0: [80, 355] + }); + }) + .then(function() { return Plotly.relayout(gd, 'geo.lonaxis.tick0', 25); }) + .then(function() { + _assert('w/ lonaxis.tick0:25', { + lonCnt: 12, lon0: [117.49, 369.99], + latCnt: 18, lat0: [80, 355] + }); + }) + .then(function() { return Plotly.relayout(gd, 'geo.lataxis.tick0', 41); }) + .then(function() { + _assert('w/ lataxis.tick0:41', { + lonCnt: 12, lon0: [117.49, 369.99], + latCnt: 19, lat0: [80, 368.5] + }); + }) + .then(function() { return Plotly.relayout(gd, 'geo.lataxis.dtick', 45); }) + .then(function() { + _assert('w/ lataxis.dtick0:45', { + lonCnt: 12, lon0: [117.49, 369.99], + latCnt: 5, lat0: [80, 308.5] + }); + }) + .catch(failTest) + .then(done); + }); }); describe('Test geo zoom/pan/drag interactions:', function() {