diff --git a/package-lock.json b/package-lock.json
index 086aee34d0f..1187a57c025 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -140,6 +140,15 @@
"@turf/meta": "6.x"
}
},
+ "@turf/bbox": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.0.1.tgz",
+ "integrity": "sha512-EGgaRLettBG25Iyx7VyUINsPpVj1x3nFQFiGS3ER8KCI1MximzNLsam3eXRabqQDjyAKyAE1bJ4EZEpGvspQxw==",
+ "requires": {
+ "@turf/helpers": "6.x",
+ "@turf/meta": "6.x"
+ }
+ },
"@turf/centroid": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-6.0.2.tgz",
diff --git a/package.json b/package.json
index 70d0c3ea466..435c1d80bf3 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"@plotly/d3-sankey": "0.7.2",
"@plotly/d3-sankey-circular": "0.33.1",
"@turf/area": "^6.0.1",
+ "@turf/bbox": "^6.0.1",
"@turf/centroid": "^6.0.2",
"alpha-shape": "^1.0.0",
"canvas-fit": "^1.5.0",
diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js
index bc9e259f1fc..a569748cbb4 100644
--- a/src/components/modebar/buttons.js
+++ b/src/components/modebar/buttons.js
@@ -303,7 +303,7 @@ function handleDrag3d(gd, ev) {
var button = ev.currentTarget;
var attr = button.getAttribute('data-attr');
var val = button.getAttribute('data-val') || true;
- var sceneIds = gd._fullLayout._subplots.gl3d;
+ var sceneIds = gd._fullLayout._subplots.gl3d || [];
var layoutUpdate = {};
var parts = attr.split('.');
@@ -468,7 +468,7 @@ function handleGeo(gd, ev) {
var attr = button.getAttribute('data-attr');
var val = button.getAttribute('data-val') || true;
var fullLayout = gd._fullLayout;
- var geoIds = fullLayout._subplots.geo;
+ var geoIds = fullLayout._subplots.geo || [];
for(var i = 0; i < geoIds.length; i++) {
var id = geoIds[i];
@@ -479,10 +479,12 @@ function handleGeo(gd, ev) {
var newScale = (val === 'in') ? 2 * scale : 0.5 * scale;
Registry.call('_guiRelayout', gd, id + '.projection.scale', newScale);
- } else if(attr === 'reset') {
- resetView(gd, 'geo');
}
}
+
+ if(attr === 'reset') {
+ resetView(gd, 'geo');
+ }
}
modeBarButtons.hoverClosestGl2d = {
diff --git a/src/lib/geo_location_utils.js b/src/lib/geo_location_utils.js
index 9790c3c7dee..85840ee2658 100644
--- a/src/lib/geo_location_utils.js
+++ b/src/lib/geo_location_utils.js
@@ -8,15 +8,24 @@
'use strict';
+var d3 = require('d3');
var countryRegex = require('country-regex');
-var Lib = require('../lib');
+var turfArea = require('@turf/area');
+var turfCentroid = require('@turf/centroid');
+var turfBbox = require('@turf/bbox');
+
+var identity = require('./identity');
+var loggers = require('./loggers');
+var isPlainObject = require('./is_plain_object');
+var nestedProperty = require('./nested_property');
+var polygon = require('./polygon');
// make list of all country iso3 ids from at runtime
var countryIds = Object.keys(countryRegex);
var locationmodeToIdFinder = {
- 'ISO-3': Lib.identity,
- 'USA-states': Lib.identity,
+ 'ISO-3': identity,
+ 'USA-states': identity,
'country names': countryNameToISO3
};
@@ -28,7 +37,7 @@ function countryNameToISO3(countryName) {
if(regex.test(countryName.trim().toLowerCase())) return iso3;
}
- Lib.log('Unrecognized country name: ' + countryName + '.');
+ loggers.log('Unrecognized country name: ' + countryName + '.');
return false;
}
@@ -64,7 +73,7 @@ function locationToFeature(locationmode, location, features) {
if(f.id === locationId) return f;
}
- Lib.log([
+ loggers.log([
'Location with id', locationId,
'does not have a matching topojson feature at this resolution.'
].join(' '));
@@ -73,6 +82,302 @@ function locationToFeature(locationmode, location, features) {
return false;
}
+function feature2polygons(feature) {
+ var geometry = feature.geometry;
+ var coords = geometry.coordinates;
+ var loc = feature.id;
+
+ var polygons = [];
+ var appendPolygon, j, k, m;
+
+ function doesCrossAntiMerdian(pts) {
+ for(var l = 0; l < pts.length - 1; l++) {
+ if(pts[l][0] > 0 && pts[l + 1][0] < 0) return l;
+ }
+ return null;
+ }
+
+ if(loc === 'RUS' || loc === 'FJI') {
+ // Russia and Fiji have landmasses that cross the antimeridian,
+ // we need to add +360 to their longitude coordinates, so that
+ // polygon 'contains' doesn't get confused when crossing the antimeridian.
+ //
+ // Note that other countries have polygons on either side of the antimeridian
+ // (e.g. some Aleutian island for the USA), but those don't confuse
+ // the 'contains' method; these are skipped here.
+ appendPolygon = function(_pts) {
+ var pts;
+
+ if(doesCrossAntiMerdian(_pts) === null) {
+ pts = _pts;
+ } else {
+ pts = new Array(_pts.length);
+ for(m = 0; m < _pts.length; m++) {
+ // do not mutate calcdata[i][j].geojson !!
+ pts[m] = [
+ _pts[m][0] < 0 ? _pts[m][0] + 360 : _pts[m][0],
+ _pts[m][1]
+ ];
+ }
+ }
+
+ polygons.push(polygon.tester(pts));
+ };
+ } else if(loc === 'ATA') {
+ // Antarctica has a landmass that wraps around every longitudes which
+ // confuses the 'contains' methods.
+ appendPolygon = function(pts) {
+ var crossAntiMeridianIndex = doesCrossAntiMerdian(pts);
+
+ // polygon that do not cross anti-meridian need no special handling
+ if(crossAntiMeridianIndex === null) {
+ return polygons.push(polygon.tester(pts));
+ }
+
+ // stitch polygon by adding pt over South Pole,
+ // so that it covers the projected region covers all latitudes
+ //
+ // Note that the algorithm below only works for polygons that
+ // start and end on longitude -180 (like the ones built by
+ // https://github.com/etpinard/sane-topojson).
+ var stitch = new Array(pts.length + 1);
+ var si = 0;
+
+ for(m = 0; m < pts.length; m++) {
+ if(m > crossAntiMeridianIndex) {
+ stitch[si++] = [pts[m][0] + 360, pts[m][1]];
+ } else if(m === crossAntiMeridianIndex) {
+ stitch[si++] = pts[m];
+ stitch[si++] = [pts[m][0], -90];
+ } else {
+ stitch[si++] = pts[m];
+ }
+ }
+
+ // polygon.tester by default appends pt[0] to the points list,
+ // we must remove it here, to avoid a jump in longitude from 180 to -180,
+ // that would confuse the 'contains' method
+ var tester = polygon.tester(stitch);
+ tester.pts.pop();
+ polygons.push(tester);
+ };
+ } else {
+ // otherwise using same array ref is fine
+ appendPolygon = function(pts) {
+ polygons.push(polygon.tester(pts));
+ };
+ }
+
+ switch(geometry.type) {
+ case 'MultiPolygon':
+ for(j = 0; j < coords.length; j++) {
+ for(k = 0; k < coords[j].length; k++) {
+ appendPolygon(coords[j][k]);
+ }
+ }
+ break;
+ case 'Polygon':
+ for(j = 0; j < coords.length; j++) {
+ appendPolygon(coords[j]);
+ }
+ break;
+ }
+
+ return polygons;
+}
+
+function getTraceGeojson(trace) {
+ var g = trace.geojson;
+ var PlotlyGeoAssets = window.PlotlyGeoAssets || {};
+ var geojsonIn = typeof g === 'string' ? PlotlyGeoAssets[g] : g;
+
+ // This should not happen, but just in case something goes
+ // really wrong when fetching the GeoJSON
+ if(!isPlainObject(geojsonIn)) {
+ loggers.error('Oops ... something went wrong when fetching ' + g);
+ return false;
+ }
+
+ return geojsonIn;
+}
+
+function extractTraceFeature(calcTrace) {
+ var trace = calcTrace[0].trace;
+
+ var geojsonIn = getTraceGeojson(trace);
+ if(!geojsonIn) return false;
+
+ var lookup = {};
+ var featuresOut = [];
+ var i;
+
+ for(i = 0; i < trace._length; i++) {
+ var cdi = calcTrace[i];
+ if(cdi.loc || cdi.loc === 0) {
+ lookup[cdi.loc] = cdi;
+ }
+ }
+
+ function appendFeature(fIn) {
+ var id = nestedProperty(fIn, trace.featureidkey || 'id').get();
+ var cdi = lookup[id];
+
+ if(cdi) {
+ var geometry = fIn.geometry;
+
+ if(geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
+ var fOut = {
+ type: 'Feature',
+ id: id,
+ geometry: geometry,
+ properties: {}
+ };
+
+ // Compute centroid, add it to the properties
+ fOut.properties.ct = findCentroid(fOut);
+
+ // Mutate in in/out features into calcdata
+ cdi.fIn = fIn;
+ cdi.fOut = fOut;
+
+ featuresOut.push(fOut);
+ } else {
+ loggers.log([
+ 'Location', cdi.loc, 'does not have a valid GeoJSON geometry.',
+ 'Traces with locationmode *geojson-id* only support',
+ '*Polygon* and *MultiPolygon* geometries.'
+ ].join(' '));
+ }
+ }
+
+ // remove key from lookup, so that we can track (if any)
+ // the locations that did not have a corresponding GeoJSON feature
+ delete lookup[id];
+ }
+
+ switch(geojsonIn.type) {
+ case 'FeatureCollection':
+ var featuresIn = geojsonIn.features;
+ for(i = 0; i < featuresIn.length; i++) {
+ appendFeature(featuresIn[i]);
+ }
+ break;
+ case 'Feature':
+ appendFeature(geojsonIn);
+ break;
+ default:
+ loggers.warn([
+ 'Invalid GeoJSON type', (geojsonIn.type || 'none') + '.',
+ 'Traces with locationmode *geojson-id* only support',
+ '*FeatureCollection* and *Feature* types.'
+ ].join(' '));
+ return false;
+ }
+
+ for(var loc in lookup) {
+ loggers.log([
+ 'Location *' + loc + '*',
+ 'does not have a matching feature with id-key',
+ '*' + trace.featureidkey + '*.'
+ ].join(' '));
+ }
+
+ return featuresOut;
+}
+
+// TODO this find the centroid of the polygon of maxArea
+// (just like we currently do for geo choropleth polygons),
+// maybe instead it would make more sense to compute the centroid
+// of each polygon and consider those on hover/select
+function findCentroid(feature) {
+ var geometry = feature.geometry;
+ var poly;
+
+ if(geometry.type === 'MultiPolygon') {
+ var coords = geometry.coordinates;
+ var maxArea = 0;
+
+ for(var i = 0; i < coords.length; i++) {
+ var polyi = {type: 'Polygon', coordinates: coords[i]};
+ var area = turfArea.default(polyi);
+ if(area > maxArea) {
+ maxArea = area;
+ poly = polyi;
+ }
+ }
+ } else {
+ poly = geometry;
+ }
+
+ return turfCentroid.default(poly).geometry.coordinates;
+}
+
+function fetchTraceGeoData(calcData) {
+ var PlotlyGeoAssets = window.PlotlyGeoAssets || {};
+ var promises = [];
+
+ function fetch(url) {
+ return new Promise(function(resolve, reject) {
+ d3.json(url, function(err, d) {
+ if(err) {
+ delete PlotlyGeoAssets[url];
+ var msg = err.status === 404 ?
+ ('GeoJSON at URL "' + url + '" does not exist.') :
+ ('Unexpected error while fetching from ' + url);
+ return reject(new Error(msg));
+ }
+
+ PlotlyGeoAssets[url] = d;
+ return resolve(d);
+ });
+ });
+ }
+
+ function wait(url) {
+ return new Promise(function(resolve, reject) {
+ var cnt = 0;
+ var interval = setInterval(function() {
+ if(PlotlyGeoAssets[url] && PlotlyGeoAssets[url] !== 'pending') {
+ clearInterval(interval);
+ return resolve(PlotlyGeoAssets[url]);
+ }
+ if(cnt > 100) {
+ clearInterval(interval);
+ return reject('Unexpected error while fetching from ' + url);
+ }
+ cnt++;
+ }, 50);
+ });
+ }
+
+ for(var i = 0; i < calcData.length; i++) {
+ var trace = calcData[i][0].trace;
+ var url = trace.geojson;
+
+ if(typeof url === 'string') {
+ if(!PlotlyGeoAssets[url]) {
+ PlotlyGeoAssets[url] = 'pending';
+ promises.push(fetch(url));
+ } else if(PlotlyGeoAssets[url] === 'pending') {
+ promises.push(wait(url));
+ }
+ }
+ }
+
+ return promises;
+}
+
+// TODO `turf/bbox` gives wrong result when the input feature/geometry
+// crosses the anti-meridian. We should try to implement our own bbox logic.
+function computeBbox(d) {
+ return turfBbox.default(d);
+}
+
module.exports = {
- locationToFeature: locationToFeature
+ locationToFeature: locationToFeature,
+ feature2polygons: feature2polygons,
+ getTraceGeojson: getTraceGeojson,
+ extractTraceFeature: extractTraceFeature,
+ fetchTraceGeoData: fetchTraceGeoData,
+ computeBbox: computeBbox
};
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index b675866b517..9e4bf22f47b 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -2442,7 +2442,7 @@ var layoutUIControlPatterns = [
{pattern: /(hover|drag)mode$/, attr: 'modebar.uirevision'},
{pattern: /^(scene\d*)\.camera/},
- {pattern: /^(geo\d*)\.(projection|center)/},
+ {pattern: /^(geo\d*)\.(projection|center|fitbounds)/},
{pattern: /^(ternary\d*\.[abc]axis)\.(min|title\.text)$/},
{pattern: /^(polar\d*\.radialaxis)\.((auto)?range|angle|title\.text)/},
{pattern: /^(polar\d*\.angularaxis)\.rotation/},
diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js
index 25f84dc5a8d..9d4df261169 100644
--- a/src/plots/geo/geo.js
+++ b/src/plots/geo/geo.js
@@ -19,6 +19,7 @@ var Drawing = require('../../components/drawing');
var Fx = require('../../components/fx');
var Plots = require('../plots');
var Axes = require('../cartesian/axes');
+var getAutoRange = require('../cartesian/autorange').getAutoRange;
var dragElement = require('../../components/dragelement');
var prepSelect = require('../cartesian/select').prepSelect;
var selectOnClick = require('../cartesian/select').selectOnClick;
@@ -26,6 +27,7 @@ var selectOnClick = require('../cartesian/select').selectOnClick;
var createGeoZoom = require('./zoom');
var constants = require('./constants');
+var geoUtils = require('../../lib/geo_location_utils');
var topojsonUtils = require('../../lib/topojson_utils');
var topojsonFeature = require('topojson-client').feature;
@@ -72,6 +74,7 @@ module.exports = function createGeo(opts) {
proto.plot = function(geoCalcData, fullLayout, promises) {
var _this = this;
var geoLayout = fullLayout[this.id];
+ var geoPromises = [];
var needsTopojson = false;
for(var k in constants.layerNameToAdjective) {
@@ -86,35 +89,34 @@ proto.plot = function(geoCalcData, fullLayout, promises) {
break;
}
}
- if(!needsTopojson) {
- return _this.update(geoCalcData, fullLayout);
- }
- var topojsonNameNew = topojsonUtils.getTopojsonName(geoLayout);
+ if(needsTopojson) {
+ var topojsonNameNew = topojsonUtils.getTopojsonName(geoLayout);
+ if(_this.topojson === null || topojsonNameNew !== _this.topojsonName) {
+ _this.topojsonName = topojsonNameNew;
- if(_this.topojson === null || topojsonNameNew !== _this.topojsonName) {
- _this.topojsonName = topojsonNameNew;
+ if(PlotlyGeoAssets.topojson[_this.topojsonName] === undefined) {
+ geoPromises.push(_this.fetchTopojson());
+ }
+ }
+ }
- if(PlotlyGeoAssets.topojson[_this.topojsonName] === undefined) {
- promises.push(_this.fetchTopojson().then(function(topojson) {
- PlotlyGeoAssets.topojson[_this.topojsonName] = topojson;
- _this.topojson = topojson;
- _this.update(geoCalcData, fullLayout);
- }));
- } else {
+ geoPromises = geoPromises.concat(geoUtils.fetchTraceGeoData(geoCalcData));
+
+ promises.push(new Promise(function(resolve, reject) {
+ Promise.all(geoPromises).then(function() {
_this.topojson = PlotlyGeoAssets.topojson[_this.topojsonName];
_this.update(geoCalcData, fullLayout);
- }
- } else {
- _this.update(geoCalcData, fullLayout);
- }
+ resolve();
+ })
+ .catch(reject);
+ }));
};
proto.fetchTopojson = function() {
- var topojsonPath = topojsonUtils.getTopojsonPath(
- this.topojsonURL,
- this.topojsonName
- );
+ var _this = this;
+ var topojsonPath = topojsonUtils.getTopojsonPath(_this.topojsonURL, _this.topojsonName);
+
return new Promise(function(resolve, reject) {
d3.json(topojsonPath, function(err, topojson) {
if(err) {
@@ -132,7 +134,9 @@ proto.fetchTopojson = function() {
].join(' ')));
}
}
- resolve(topojson);
+
+ PlotlyGeoAssets.topojson[_this.topojsonName] = topojson;
+ resolve();
});
});
};
@@ -140,18 +144,24 @@ proto.fetchTopojson = function() {
proto.update = function(geoCalcData, fullLayout) {
var geoLayout = fullLayout[this.id];
- var hasInvalidBounds = this.updateProjection(fullLayout, geoLayout);
- if(hasInvalidBounds) return;
-
// important: maps with choropleth traces have a different layer order
this.hasChoropleth = false;
+
for(var i = 0; i < geoCalcData.length; i++) {
- if(geoCalcData[i][0].trace.type === 'choropleth') {
+ var calcTrace = geoCalcData[i];
+ var trace = calcTrace[0].trace;
+
+ if(trace.type === 'choropleth') {
this.hasChoropleth = true;
- break;
+ }
+ if(trace.visible === true && trace._length > 0) {
+ trace._module.calcGeoJSON(calcTrace, fullLayout);
}
}
+ var hasInvalidBounds = this.updateProjection(geoCalcData, fullLayout);
+ if(hasInvalidBounds) return;
+
if(!this.viewInitial || this.scope !== geoLayout.scope) {
this.saveViewInitial(geoLayout);
}
@@ -174,20 +184,19 @@ proto.update = function(geoCalcData, fullLayout) {
this.render();
};
-proto.updateProjection = function(fullLayout, geoLayout) {
+proto.updateProjection = function(geoCalcData, fullLayout) {
+ var gd = this.graphDiv;
+ var geoLayout = fullLayout[this.id];
var gs = fullLayout._size;
var domain = geoLayout.domain;
var projLayout = geoLayout.projection;
- var rotation = projLayout.rotation || {};
- var center = geoLayout.center || {};
- var projection = this.projection = getProjection(geoLayout);
+ var lonaxis = geoLayout.lonaxis;
+ var lataxis = geoLayout.lataxis;
+ var axLon = lonaxis._ax;
+ var axLat = lataxis._ax;
- // set 'pre-fit' projection
- projection
- .center([center.lon - rotation.lon, center.lat - rotation.lat])
- .rotate([-rotation.lon, -rotation.lat, rotation.roll])
- .parallels(projLayout.parallels);
+ var projection = this.projection = getProjection(geoLayout);
// setup subplot extent [[x0,y0], [x1,y1]]
var extent = [[
@@ -198,11 +207,46 @@ proto.updateProjection = function(fullLayout, geoLayout) {
gs.t + gs.h * (1 - domain.y[0])
]];
- var lonaxis = geoLayout.lonaxis;
- var lataxis = geoLayout.lataxis;
- var rangeBox = makeRangeBox(lonaxis.range, lataxis.range);
+ var center = geoLayout.center || {};
+ var rotation = projLayout.rotation || {};
+ var lonaxisRange = lonaxis.range || [];
+ var lataxisRange = lataxis.range || [];
+
+ if(geoLayout.fitbounds) {
+ axLon._length = extent[1][0] - extent[0][0];
+ axLat._length = extent[1][1] - extent[0][1];
+ axLon.range = getAutoRange(gd, axLon);
+ axLat.range = getAutoRange(gd, axLat);
+
+ var midLon = (axLon.range[0] + axLon.range[1]) / 2;
+ var midLat = (axLat.range[0] + axLat.range[1]) / 2;
+
+ if(geoLayout._isScoped) {
+ center = {lon: midLon, lat: midLat};
+ } else if(geoLayout._isClipped) {
+ center = {lon: midLon, lat: midLat};
+ rotation = {lon: midLon, lat: midLat, roll: rotation.roll};
+
+ var projType = projLayout.type;
+ var lonHalfSpan = (constants.lonaxisSpan[projType] / 2) || 180;
+ var latHalfSpan = (constants.lataxisSpan[projType] / 2) || 180;
+
+ lonaxisRange = [midLon - lonHalfSpan, midLon + lonHalfSpan];
+ lataxisRange = [midLat - latHalfSpan, midLat + latHalfSpan];
+ } else {
+ center = {lon: midLon, lat: midLat};
+ rotation = {lon: midLon, lat: rotation.lat, roll: rotation.roll};
+ }
+ }
+
+ // set 'pre-fit' projection
+ projection
+ .center([center.lon - rotation.lon, center.lat - rotation.lat])
+ .rotate([-rotation.lon, -rotation.lat, rotation.roll])
+ .parallels(projLayout.parallels);
// fit projection 'scale' and 'translate' to set lon/lat ranges
+ var rangeBox = makeRangeBox(lonaxisRange, lataxisRange);
projection.fitExtent(extent, rangeBox);
var b = this.bounds = projection.getBounds(rangeBox);
@@ -214,12 +258,11 @@ proto.updateProjection = function(fullLayout, geoLayout) {
!isFinite(b[1][0]) || !isFinite(b[1][1]) ||
isNaN(t[0]) || isNaN(t[0])
) {
- var gd = this.graphDiv;
- var attrToUnset = ['projection.rotation', 'center', 'lonaxis.range', 'lataxis.range'];
+ var attrToUnset = ['fitbounds', 'projection.rotation', 'center', 'lonaxis.range', 'lataxis.range'];
var msg = 'Invalid geo settings, relayout\'ing to default view.';
var updateObj = {};
- // clear all attribute that could cause invalid bounds,
+ // clear all attributes that could cause invalid bounds,
// clear viewInitial to update reset-view behavior
for(var i = 0; i < attrToUnset.length; i++) {
@@ -233,6 +276,23 @@ proto.updateProjection = function(fullLayout, geoLayout) {
return msg;
}
+ if(geoLayout.fitbounds) {
+ var b2 = projection.getBounds(makeRangeBox(axLon.range, axLat.range));
+ var k2 = Math.min(
+ (b[1][0] - b[0][0]) / (b2[1][0] - b2[0][0]),
+ (b[1][1] - b[0][1]) / (b2[1][1] - b2[0][1])
+ );
+
+ if(isFinite(k2)) {
+ projection.scale(k2 * s);
+ } else {
+ Lib.warn('Something went wrong during' + this.id + 'fitbounds computations.');
+ }
+ } else {
+ // adjust projection to user setting
+ projection.scale(projLayout.scale * s);
+ }
+
// px coordinates of view mid-point,
// useful to update `geo.center` after interactions
var midPt = this.midPt = [
@@ -240,9 +300,7 @@ proto.updateProjection = function(fullLayout, geoLayout) {
(b[0][1] + b[1][1]) / 2
];
- // adjust projection to user setting
projection
- .scale(projLayout.scale * s)
.translate([t[0] + (midPt[0] - t[0]), t[1] + (midPt[1] - t[1])])
.clipExtent(b);
@@ -537,26 +595,31 @@ proto.saveViewInitial = function(geoLayout) {
var projLayout = geoLayout.projection;
var rotation = projLayout.rotation || {};
+ this.viewInitial = {
+ 'fitbounds': geoLayout.fitbounds,
+ 'projection.scale': projLayout.scale
+ };
+
+ var extra;
if(geoLayout._isScoped) {
- this.viewInitial = {
+ extra = {
'center.lon': center.lon,
'center.lat': center.lat,
- 'projection.scale': projLayout.scale
};
} else if(geoLayout._isClipped) {
- this.viewInitial = {
- 'projection.scale': projLayout.scale,
+ extra = {
'projection.rotation.lon': rotation.lon,
'projection.rotation.lat': rotation.lat
};
} else {
- this.viewInitial = {
+ extra = {
'center.lon': center.lon,
'center.lat': center.lat,
- 'projection.scale': projLayout.scale,
'projection.rotation.lon': rotation.lon
};
}
+
+ Lib.extendFlat(this.viewInitial, extra);
};
// [hot code path] (re)draw all paths which depend on the projection
diff --git a/src/plots/geo/layout_attributes.js b/src/plots/geo/layout_attributes.js
index 6232c40fbf3..d21bded596d 100644
--- a/src/plots/geo/layout_attributes.js
+++ b/src/plots/geo/layout_attributes.js
@@ -75,6 +75,33 @@ var attrs = module.exports = overrideAll({
].join(' ')
}),
+ fitbounds: {
+ valType: 'enumerated',
+ values: [false, 'locations', 'geojson'],
+ dflt: false,
+ role: 'info',
+ editType: 'plot',
+ 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 `lonaxis.range`',
+ 'getting auto-filled.',
+
+ // 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*.'
+ ].join(' ')
+ },
+
resolution: {
valType: 'enumerated',
values: [110, 50],
@@ -173,6 +200,12 @@ var attrs = module.exports = overrideAll({
].join(' ')
}
},
+ visible: {
+ valType: 'boolean',
+ role: 'info',
+ dflt: true,
+ description: 'Sets the default visibility of the base layers.'
+ },
showcoastlines: {
valType: 'boolean',
role: 'info',
diff --git a/src/plots/geo/layout_defaults.js b/src/plots/geo/layout_defaults.js
index 34da1d42edf..c169b050009 100644
--- a/src/plots/geo/layout_defaults.js
+++ b/src/plots/geo/layout_defaults.js
@@ -6,10 +6,12 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
+var Lib = require('../../lib');
var handleSubplotDefaults = require('../subplot_defaults');
+var getSubplotData = require('../get_data').getSubplotData;
+
var constants = require('./constants');
var layoutAttributes = require('./layout_attributes');
@@ -20,12 +22,14 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
type: 'geo',
attributes: layoutAttributes,
handleDefaults: handleGeoDefaults,
+ fullData: fullData,
partition: 'y'
});
};
-function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) {
- var show;
+function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
+ var subplotData = getSubplotData(opts.fullData, 'geo', opts.id);
+ var traceIndices = subplotData.map(function(t) { return t._expandedIndex; });
var resolution = coerce('resolution');
var scope = coerce('scope');
@@ -39,7 +43,10 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) {
var isScoped = geoLayoutOut._isScoped = (scope !== 'world');
var isConic = geoLayoutOut._isConic = projType.indexOf('conic') !== -1;
- geoLayoutOut._isClipped = !!constants.lonaxisSpan[projType];
+ var isClipped = geoLayoutOut._isClipped = !!constants.lonaxisSpan[projType];
+
+ var visible = coerce('visible');
+ var show;
for(var i = 0; i < axesNames.length; i++) {
var axisName = axesNames[i];
@@ -58,15 +65,29 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) {
rangeDflt = [rot - hSpan, rot + hSpan];
}
- coerce(axisName + '.range', rangeDflt);
+ var range = coerce(axisName + '.range', rangeDflt);
coerce(axisName + '.tick0');
coerce(axisName + '.dtick', dtickDflt);
- show = coerce(axisName + '.showgrid');
+ show = coerce(axisName + '.showgrid', !visible ? false : undefined);
if(show) {
coerce(axisName + '.gridcolor');
coerce(axisName + '.gridwidth');
}
+
+ // mock axis for autorange computations
+ geoLayoutOut[axisName]._ax = {
+ type: 'linear',
+ _id: axisName.slice(0, 3),
+ _traceIndices: traceIndices,
+ setScale: Lib.identity,
+ c2l: Lib.identity,
+ r2l: Lib.identity,
+ autorange: true,
+ range: range.slice(),
+ _m: 1,
+ _input: {}
+ };
}
var lonRange = geoLayoutOut.lonaxis.range;
@@ -87,13 +108,13 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) {
coerce('projection.rotation.lat', dfltProjRotate[1]);
coerce('projection.rotation.roll', dfltProjRotate[2]);
- show = coerce('showcoastlines', !isScoped);
+ show = coerce('showcoastlines', !isScoped && visible);
if(show) {
coerce('coastlinecolor');
coerce('coastlinewidth');
}
- show = coerce('showocean');
+ show = coerce('showocean', !visible ? false : undefined);
if(show) coerce('oceancolor');
}
@@ -121,19 +142,19 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) {
coerce('projection.scale');
- show = coerce('showland');
+ show = coerce('showland', !visible ? false : undefined);
if(show) coerce('landcolor');
- show = coerce('showlakes');
+ show = coerce('showlakes', !visible ? false : undefined);
if(show) coerce('lakecolor');
- show = coerce('showrivers');
+ show = coerce('showrivers', !visible ? false : undefined);
if(show) {
coerce('rivercolor');
coerce('riverwidth');
}
- show = coerce('showcountries', isScoped && scope !== 'usa');
+ show = coerce('showcountries', isScoped && scope !== 'usa' && visible);
if(show) {
coerce('countrycolor');
coerce('countrywidth');
@@ -143,14 +164,14 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) {
// Only works for:
// USA states at 110m
// USA states + Canada provinces at 50m
- coerce('showsubunits', true);
+ coerce('showsubunits', visible);
coerce('subunitcolor');
coerce('subunitwidth');
}
if(!isScoped) {
// Does not work in non-world scopes
- show = coerce('showframe', true);
+ show = coerce('showframe', visible);
if(show) {
coerce('framecolor');
coerce('framewidth');
@@ -158,4 +179,27 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) {
}
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;
+ } else {
+ delete geoLayoutOut.center.lon;
+ delete geoLayoutOut.center.lat;
+ delete geoLayoutOut.projection.rotation.lon;
+ }
+ }
}
diff --git a/src/plots/geo/zoom.js b/src/plots/geo/zoom.js
index 66ef7d15fd0..5a8eb4a737a 100644
--- a/src/plots/geo/zoom.js
+++ b/src/plots/geo/zoom.js
@@ -70,6 +70,7 @@ function sync(geo, projection, cb) {
cb(set);
set('projection.scale', projection.scale() / geo.fitScale);
+ set('fitbounds', false);
gd.emit('plotly_relayout', eventData);
}
@@ -179,7 +180,6 @@ function zoomNonClipped(geo, projection) {
'geo.center.lon': center[0],
'geo.center.lat': center[1],
'geo.projection.rotation.lon': -rotate[0]
-
});
}
diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js
index c8ab4b1e650..a435e17e38a 100644
--- a/src/plots/mapbox/mapbox.js
+++ b/src/plots/mapbox/mapbox.js
@@ -8,18 +8,17 @@
'use strict';
-/* global PlotlyGeoAssets:false */
-
var mapboxgl = require('mapbox-gl');
-var d3 = require('d3');
var Fx = require('../../components/fx');
var Lib = require('../../lib');
+var geoUtils = require('../../lib/geo_location_utils');
var Registry = require('../../registry');
var Axes = require('../cartesian/axes');
var dragElement = require('../../components/dragelement');
var prepSelect = require('../cartesian/select').prepSelect;
var selectOnClick = require('../cartesian/select').selectOnClick;
+
var constants = require('./constants');
var createMapboxLayer = require('./layers');
@@ -130,7 +129,7 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
map.once('load', resolve);
}));
- promises = promises.concat(self.fetchMapData(calcData, fullLayout));
+ promises = promises.concat(geoUtils.fetchTraceGeoData(calcData));
Promise.all(promises).then(function() {
self.fillBelowLookup(calcData, fullLayout);
@@ -140,39 +139,6 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
}).catch(reject);
};
-proto.fetchMapData = function(calcData) {
- var promises = [];
-
- function fetch(url) {
- return new Promise(function(resolve, reject) {
- d3.json(url, function(err, d) {
- if(err) {
- delete PlotlyGeoAssets[url];
- var msg = err.status === 404 ?
- ('GeoJSON at URL "' + url + '" does not exist.') :
- ('Unexpected error while fetching from ' + url);
- return reject(new Error(msg));
- }
-
- PlotlyGeoAssets[url] = d;
- resolve(d);
- });
- });
- }
-
- for(var i = 0; i < calcData.length; i++) {
- var trace = calcData[i][0].trace;
- var url = trace.geojson;
-
- if(typeof url === 'string' && !PlotlyGeoAssets[url]) {
- PlotlyGeoAssets[url] = 'pending';
- promises.push(fetch(url));
- }
- }
-
- return promises;
-};
-
proto.updateMap = function(calcData, fullLayout, resolve, reject) {
var self = this;
var map = self.map;
@@ -196,7 +162,7 @@ proto.updateMap = function(calcData, fullLayout, resolve, reject) {
}));
}
- promises = promises.concat(self.fetchMapData(calcData, fullLayout));
+ promises = promises.concat(geoUtils.fetchTraceGeoData(calcData));
Promise.all(promises).then(function() {
self.fillBelowLookup(calcData, fullLayout);
diff --git a/src/traces/choropleth/attributes.js b/src/traces/choropleth/attributes.js
index 413d2438fc5..e57d827760e 100644
--- a/src/traces/choropleth/attributes.js
+++ b/src/traces/choropleth/attributes.js
@@ -33,6 +33,21 @@ module.exports = extendFlat({
editType: 'calc',
description: 'Sets the color values.'
},
+ geojson: extendFlat({}, scatterGeoAttrs.geojson, {
+ description: [
+ 'Sets optional GeoJSON data associated with this trace.',
+ 'If not given, the features on the base map are used.',
+
+ 'It can be set as a valid GeoJSON object or as a URL string.',
+ 'Note that we only accept GeoJSONs of type *FeatureCollection* or *Feature*',
+ 'with geometries of type *Polygon* or *MultiPolygon*.'
+
+ // TODO add topojson support with additional 'topojsonobject' attr?
+ // https://github.com/topojson/topojson-specification/blob/master/README.md
+ ].join(' ')
+ }),
+ featureidkey: scatterGeoAttrs.featureidkey,
+
text: extendFlat({}, scatterGeoAttrs.text, {
description: 'Sets the text elements associated with each location.'
}),
diff --git a/src/traces/choropleth/defaults.js b/src/traces/choropleth/defaults.js
index 71a7b39ca7d..71656dc08bb 100644
--- a/src/traces/choropleth/defaults.js
+++ b/src/traces/choropleth/defaults.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Lib = require('../../lib');
@@ -28,7 +27,18 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
traceOut._length = Math.min(locations.length, z.length);
- coerce('locationmode');
+ var geojson = coerce('geojson');
+
+ var locationmodeDflt;
+ if((typeof geojson === 'string' && geojson !== '') || Lib.isPlainObject(geojson)) {
+ locationmodeDflt = 'geojson-id';
+ }
+
+ var locationMode = coerce('locationmode', locationmodeDflt);
+
+ if(locationMode === 'geojson-id') {
+ coerce('featureidkey');
+ }
coerce('text');
coerce('hovertext');
diff --git a/src/traces/choropleth/event_data.js b/src/traces/choropleth/event_data.js
index b672186b1ee..5f260bf39f7 100644
--- a/src/traces/choropleth/event_data.js
+++ b/src/traces/choropleth/event_data.js
@@ -12,10 +12,12 @@ module.exports = function eventData(out, pt, trace, cd, pointNumber) {
out.location = pt.location;
out.z = pt.z;
+ // include feature properties from input geojson
var cdi = cd[pointNumber];
- if(cdi.fIn) {
+ if(cdi.fIn && cdi.fIn.properties) {
out.properties = cdi.fIn.properties;
}
+ out.ct = cdi.ct;
return out;
};
diff --git a/src/traces/choropleth/index.js b/src/traces/choropleth/index.js
index 06a265df6c0..4214cf801a1 100644
--- a/src/traces/choropleth/index.js
+++ b/src/traces/choropleth/index.js
@@ -13,6 +13,7 @@ module.exports = {
supplyDefaults: require('./defaults'),
colorbar: require('../heatmap/colorbar'),
calc: require('./calc'),
+ calcGeoJSON: require('./plot').calcGeoJSON,
plot: require('./plot').plot,
style: require('./style').style,
styleOnSelect: require('./style').styleOnSelect,
diff --git a/src/traces/choropleth/plot.js b/src/traces/choropleth/plot.js
index dfb2762fdef..237598b12a5 100644
--- a/src/traces/choropleth/plot.js
+++ b/src/traces/choropleth/plot.js
@@ -11,18 +11,15 @@
var d3 = require('d3');
var Lib = require('../../lib');
-var polygon = require('../../lib/polygon');
-
+var geoUtils = require('../../lib/geo_location_utils');
var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures;
-var locationToFeature = require('../../lib/geo_location_utils').locationToFeature;
+var findExtremes = require('../../plots/cartesian/autorange').findExtremes;
+
var style = require('./style').style;
function plot(gd, geo, calcData) {
- for(var i = 0; i < calcData.length; i++) {
- calcGeoJSON(calcData[i], geo.topojson);
- }
-
var choroplethLayer = geo.layers.backplot.select('.choroplethlayer');
+
Lib.makeTraceGroups(choroplethLayer, calcData, 'trace choropleth').each(function(calcTrace) {
var sel = d3.select(this);
@@ -39,131 +36,51 @@ function plot(gd, geo, calcData) {
});
}
-function calcGeoJSON(calcTrace, topojson) {
+function calcGeoJSON(calcTrace, fullLayout) {
var trace = calcTrace[0].trace;
- var len = calcTrace.length;
- var features = getTopojsonFeatures(trace, topojson);
+ var geoLayout = fullLayout[trace.geo];
+ var geo = geoLayout._subplot;
+ var locationmode = trace.locationmode;
+ var len = trace._length;
+
+ var features = locationmode === 'geojson-id' ?
+ geoUtils.extractTraceFeature(calcTrace) :
+ getTopojsonFeatures(trace, geo.topojson);
+
+ var lonArray = [];
+ var latArray = [];
for(var i = 0; i < len; i++) {
var calcPt = calcTrace[i];
- var feature = locationToFeature(trace.locationmode, calcPt.loc, features);
-
- if(!feature) {
+ var feature = locationmode === 'geojson-id' ?
+ calcPt.fOut :
+ geoUtils.locationToFeature(locationmode, calcPt.loc, features);
+
+ if(feature) {
+ calcPt.geojson = feature;
+ calcPt.ct = feature.properties.ct;
+ calcPt._polygons = geoUtils.feature2polygons(feature);
+
+ var bboxFeature = geoUtils.computeBbox(feature);
+ lonArray.push(bboxFeature[0], bboxFeature[2]);
+ latArray.push(bboxFeature[1], bboxFeature[3]);
+ } else {
calcPt.geojson = null;
- continue;
}
-
- calcPt.geojson = feature;
- calcPt.ct = feature.properties.ct;
- calcPt._polygons = feature2polygons(feature);
- }
-}
-
-function feature2polygons(feature) {
- var geometry = feature.geometry;
- var coords = geometry.coordinates;
- var loc = feature.id;
-
- var polygons = [];
- var appendPolygon, j, k, m;
-
- function doesCrossAntiMerdian(pts) {
- for(var l = 0; l < pts.length - 1; l++) {
- if(pts[l][0] > 0 && pts[l + 1][0] < 0) return l;
- }
- return null;
- }
-
- if(loc === 'RUS' || loc === 'FJI') {
- // Russia and Fiji have landmasses that cross the antimeridian,
- // we need to add +360 to their longitude coordinates, so that
- // polygon 'contains' doesn't get confused when crossing the antimeridian.
- //
- // Note that other countries have polygons on either side of the antimeridian
- // (e.g. some Aleutian island for the USA), but those don't confuse
- // the 'contains' method; these are skipped here.
- appendPolygon = function(_pts) {
- var pts;
-
- if(doesCrossAntiMerdian(_pts) === null) {
- pts = _pts;
- } else {
- pts = new Array(_pts.length);
- for(m = 0; m < _pts.length; m++) {
- // do nut mutate calcdata[i][j].geojson !!
- pts[m] = [
- _pts[m][0] < 0 ? _pts[m][0] + 360 : _pts[m][0],
- _pts[m][1]
- ];
- }
- }
-
- polygons.push(polygon.tester(pts));
- };
- } else if(loc === 'ATA') {
- // Antarctica has a landmass that wraps around every longitudes which
- // confuses the 'contains' methods.
- appendPolygon = function(pts) {
- var crossAntiMeridianIndex = doesCrossAntiMerdian(pts);
-
- // polygon that do not cross anti-meridian need no special handling
- if(crossAntiMeridianIndex === null) {
- return polygons.push(polygon.tester(pts));
- }
-
- // stitch polygon by adding pt over South Pole,
- // so that it covers the projected region covers all latitudes
- //
- // Note that the algorithm below only works for polygons that
- // start and end on longitude -180 (like the ones built by
- // https://github.com/etpinard/sane-topojson).
- var stitch = new Array(pts.length + 1);
- var si = 0;
-
- for(m = 0; m < pts.length; m++) {
- if(m > crossAntiMeridianIndex) {
- stitch[si++] = [pts[m][0] + 360, pts[m][1]];
- } else if(m === crossAntiMeridianIndex) {
- stitch[si++] = pts[m];
- stitch[si++] = [pts[m][0], -90];
- } else {
- stitch[si++] = pts[m];
- }
- }
-
- // polygon.tester by default appends pt[0] to the points list,
- // we must remove it here, to avoid a jump in longitude from 180 to -180,
- // that would confuse the 'contains' method
- var tester = polygon.tester(stitch);
- tester.pts.pop();
- polygons.push(tester);
- };
- } else {
- // otherwise using same array ref is fine
- appendPolygon = function(pts) {
- polygons.push(polygon.tester(pts));
- };
}
- switch(geometry.type) {
- case 'MultiPolygon':
- for(j = 0; j < coords.length; j++) {
- for(k = 0; k < coords[j].length; k++) {
- appendPolygon(coords[j][k]);
- }
- }
- break;
- case 'Polygon':
- for(j = 0; j < coords.length; j++) {
- appendPolygon(coords[j]);
- }
- break;
+ if(geoLayout.fitbounds === 'geojson' && locationmode === 'geojson-id') {
+ var bboxGeojson = geoUtils.computeBbox(geoUtils.getTraceGeojson(trace));
+ lonArray = [bboxGeojson[0], bboxGeojson[2]];
+ latArray = [bboxGeojson[1], bboxGeojson[3]];
}
- return polygons;
+ var opts = {padded: true};
+ trace._extremes.lon = findExtremes(geoLayout.lonaxis._ax, lonArray, opts);
+ trace._extremes.lat = findExtremes(geoLayout.lataxis._ax, latArray, opts);
}
module.exports = {
- plot: plot,
- feature2polygons: feature2polygons
+ calcGeoJSON: calcGeoJSON,
+ plot: plot
};
diff --git a/src/traces/choroplethmapbox/attributes.js b/src/traces/choroplethmapbox/attributes.js
index 08e54b7d830..3191cf8bf0d 100644
--- a/src/traces/choroplethmapbox/attributes.js
+++ b/src/traces/choroplethmapbox/attributes.js
@@ -28,9 +28,7 @@ module.exports = extendFlat({
// Maybe start with only one value (that we could name e.g. 'geojson-id'),
// but eventually:
// - we could also support for our own dist/topojson/*
- // - some people might want `geojson-properties-name` to map data arrays to
- // GeoJSON features
- // locationmode: choroplethAttrs.locationmode,
+ // .. and locationmode: choroplethAttrs.locationmode,
z: {
valType: 'data_array',
@@ -47,11 +45,19 @@ module.exports = extendFlat({
editType: 'calc',
description: [
'Sets the GeoJSON data associated with this trace.',
- 'Can be set as a valid GeoJSON object or as URL string',
- 'Note that we only accept GeoJSON of type *FeatureCollection* and *Feature*',
- 'with geometries of type *Polygon* and *MultiPolygon*.'
+
+ 'It can be set as a valid GeoJSON object or as a URL string.',
+ 'Note that we only accept GeoJSONs of type *FeatureCollection* or *Feature*',
+ 'with geometries of type *Polygon* or *MultiPolygon*.'
].join(' ')
},
+ featureidkey: extendFlat({}, choroplethAttrs.featureidkey, {
+ description: [
+ 'Sets the key in GeoJSON features which is used as id to match the items',
+ 'included in the `locations` array.',
+ 'Support nested property, for example *properties.name*.'
+ ].join(' ')
+ }),
// TODO agree on name / behaviour
//
diff --git a/src/traces/choroplethmapbox/convert.js b/src/traces/choroplethmapbox/convert.js
index 8c1b00fb566..01e21a123aa 100644
--- a/src/traces/choroplethmapbox/convert.js
+++ b/src/traces/choroplethmapbox/convert.js
@@ -9,15 +9,13 @@
'use strict';
var isNumeric = require('fast-isnumeric');
-var turfArea = require('@turf/area');
-var turfCentroid = require('@turf/centroid');
var Lib = require('../../lib');
var Colorscale = require('../../components/colorscale');
var Drawing = require('../../components/drawing');
var makeBlank = require('../../lib/geojson_utils').makeBlank;
-var feature2polygons = require('../choropleth/plot').feature2polygons;
+var geoUtils = require('../../lib/geo_location_utils');
/* N.B.
*
@@ -52,25 +50,9 @@ function convert(calcTrace) {
if(!isVisible) return opts;
- var geojsonIn = typeof trace.geojson === 'string' ?
- (window.PlotlyGeoAssets || {})[trace.geojson] :
- trace.geojson;
+ var features = geoUtils.extractTraceFeature(calcTrace);
- // This should not happen, but just in case something goes
- // really wrong when fetching the GeoJSON
- if(!Lib.isPlainObject(geojsonIn)) {
- Lib.error('Oops ... something when wrong when fetching ' + trace.geojson);
- return opts;
- }
-
- var lookup = {};
- var featuresOut = [];
- var i;
-
- for(i = 0; i < calcTrace.length; i++) {
- var cdi = calcTrace[i];
- if(cdi.loc) lookup[cdi.loc] = cdi;
- }
+ if(!features) return opts;
var sclFunc = Colorscale.makeColorScaleFuncFromTrace(trace);
var marker = trace.marker;
@@ -94,63 +76,19 @@ function convert(calcTrace) {
lineWidthFn = function(d) { return d.mlw; };
}
- function appendFeature(fIn) {
- var cdi = lookup[fIn.id];
-
- if(cdi) {
- var geometry = fIn.geometry;
-
- if(geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
- var props = {fc: sclFunc(cdi.z)};
-
- if(opacityFn) props.mo = opacityFn(cdi);
- if(lineColorFn) props.mlc = lineColorFn(cdi);
- if(lineWidthFn) props.mlw = lineWidthFn(cdi);
-
- var fOut = {
- type: 'Feature',
- geometry: geometry,
- properties: props
- };
-
- cdi._polygons = feature2polygons(fOut);
- cdi.ct = findCentroid(fOut);
- cdi.fIn = fIn;
- cdi.fOut = fOut;
- featuresOut.push(fOut);
- } else {
- Lib.log([
- 'Location with id', cdi.loc, 'does not have a valid GeoJSON geometry,',
- 'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
- ].join(' '));
- }
+ for(var i = 0; i < calcTrace.length; i++) {
+ var cdi = calcTrace[i];
+ var fOut = cdi.fOut;
+
+ if(fOut) {
+ var props = fOut.properties;
+ props.fc = sclFunc(cdi.z);
+ if(opacityFn) props.mo = opacityFn(cdi);
+ if(lineColorFn) props.mlc = lineColorFn(cdi);
+ if(lineWidthFn) props.mlw = lineWidthFn(cdi);
+ cdi.ct = props.ct;
+ cdi._polygons = geoUtils.feature2polygons(fOut);
}
-
- // remove key from lookup, so that we can track (if any)
- // the locations that did not have a corresponding GeoJSON feature
- delete lookup[fIn.id];
- }
-
- switch(geojsonIn.type) {
- case 'FeatureCollection':
- var featuresIn = geojsonIn.features;
- for(i = 0; i < featuresIn.length; i++) {
- appendFeature(featuresIn[i]);
- }
- break;
- case 'Feature':
- appendFeature(geojsonIn);
- break;
- default:
- Lib.warn([
- 'Invalid GeoJSON type', (geojsonIn.type || 'none') + ',',
- 'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
- ].join(' '));
- return opts;
- }
-
- for(var loc in lookup) {
- Lib.log('Location with id ' + loc + ' does not have a matching feature');
}
var opacitySetting = opacityFn ?
@@ -175,7 +113,7 @@ function convert(calcTrace) {
fill.layout.visibility = 'visible';
line.layout.visibility = 'visible';
- opts.geojson = {type: 'FeatureCollection', features: featuresOut};
+ opts.geojson = {type: 'FeatureCollection', features: features};
convertOnSelect(calcTrace);
@@ -210,33 +148,6 @@ function convertOnSelect(calcTrace) {
return opts;
}
-// TODO this find the centroid of the polygon of maxArea
-// (just like we currently do for geo choropleth polygons),
-// maybe instead it would make more sense to compute the centroid
-// of each polygon and consider those on hover/select
-function findCentroid(feature) {
- var geometry = feature.geometry;
- var poly;
-
- if(geometry.type === 'MultiPolygon') {
- var coords = geometry.coordinates;
- var maxArea = 0;
-
- for(var i = 0; i < coords.length; i++) {
- var polyi = {type: 'Polygon', coordinates: coords[i]};
- var area = turfArea.default(polyi);
- if(area > maxArea) {
- maxArea = area;
- poly = polyi;
- }
- }
- } else {
- poly = geometry;
- }
-
- return turfCentroid.default(poly).geometry.coordinates;
-}
-
module.exports = {
convert: convert,
convertOnSelect: convertOnSelect
diff --git a/src/traces/choroplethmapbox/defaults.js b/src/traces/choroplethmapbox/defaults.js
index 050015ce5e1..d75aa3f9b5f 100644
--- a/src/traces/choroplethmapbox/defaults.js
+++ b/src/traces/choroplethmapbox/defaults.js
@@ -29,6 +29,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
return;
}
+ coerce('featureidkey');
+
traceOut._length = Math.min(locations.length, z.length);
coerce('below');
diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js
index 6c2cfa54a88..0723ea4cfb6 100644
--- a/src/traces/scattergeo/attributes.js
+++ b/src/traces/scattergeo/attributes.js
@@ -42,12 +42,44 @@ module.exports = overrideAll({
},
locationmode: {
valType: 'enumerated',
- values: ['ISO-3', 'USA-states', 'country names'],
+ values: ['ISO-3', 'USA-states', 'country names', 'geojson-id'],
role: 'info',
dflt: 'ISO-3',
description: [
'Determines the set of locations used to match entries in `locations`',
- 'to regions on the map.'
+ 'to regions on the map.',
+ 'Values *ISO-3*, *USA-states*, *country names* correspond to features on',
+ 'the base map and value *geojson-id* corresponds to features from a custom',
+ 'GeoJSON linked to the `geojson` attribute.'
+ ].join(' ')
+ },
+
+ geojson: {
+ valType: 'any',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets optional GeoJSON data associated with this trace.',
+ 'If not given, the features on the base map are used when `locations` is set.',
+
+ 'It can be set as a valid GeoJSON object or as a URL string.',
+ 'Note that we only accept GeoJSONs of type *FeatureCollection* or *Feature*',
+ 'with geometries of type *Polygon* or *MultiPolygon*.'
+
+ // TODO add topojson support with additional 'topojsonobject' attr?
+ // https://github.com/topojson/topojson-specification/blob/master/README.md
+ ].join(' ')
+ },
+ featureidkey: {
+ valType: 'string',
+ role: 'info',
+ editType: 'calc',
+ dflt: 'id',
+ description: [
+ 'Sets the key in GeoJSON features which is used as id to match the items',
+ 'included in the `locations` array.',
+ 'Only has an effect when `geojson` is set.',
+ 'Support nested property, for example *properties.name*.'
].join(' ')
},
diff --git a/src/traces/scattergeo/calc.js b/src/traces/scattergeo/calc.js
index 537b0562307..c24e811e1c1 100644
--- a/src/traces/scattergeo/calc.js
+++ b/src/traces/scattergeo/calc.js
@@ -18,17 +18,28 @@ var calcSelection = require('../scatter/calc_selection');
var _ = require('../../lib')._;
+function isNonBlankString(v) {
+ return v && typeof v === 'string';
+}
+
module.exports = function calc(gd, trace) {
var hasLocationData = Array.isArray(trace.locations);
var len = hasLocationData ? trace.locations.length : trace._length;
var calcTrace = new Array(len);
+ var isValidLoc;
+ if(trace.geojson) {
+ isValidLoc = function(v) { return isNonBlankString(v) || isNumeric(v); };
+ } else {
+ isValidLoc = isNonBlankString;
+ }
+
for(var i = 0; i < len; i++) {
var calcPt = calcTrace[i] = {};
if(hasLocationData) {
var loc = trace.locations[i];
- calcPt.loc = typeof loc === 'string' ? loc : null;
+ calcPt.loc = isValidLoc(loc) ? loc : null;
} else {
var lon = trace.lon[i];
var lat = trace.lat[i];
diff --git a/src/traces/scattergeo/defaults.js b/src/traces/scattergeo/defaults.js
index f4c8c8fad4f..f706ffd4e69 100644
--- a/src/traces/scattergeo/defaults.js
+++ b/src/traces/scattergeo/defaults.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var Lib = require('../../lib');
@@ -19,18 +18,41 @@ var handleFillColorDefaults = require('../scatter/fillcolor_defaults');
var attributes = require('./attributes');
-
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- var len = handleLonLatLocDefaults(traceIn, traceOut, coerce);
+ var locations = coerce('locations');
+ var len;
+
+ if(locations && locations.length) {
+ var geojson = coerce('geojson');
+ var locationmodeDflt;
+ if((typeof geojson === 'string' && geojson !== '') || Lib.isPlainObject(geojson)) {
+ locationmodeDflt = 'geojson-id';
+ }
+
+ var locationMode = coerce('locationmode', locationmodeDflt);
+
+ if(locationMode === 'geojson-id') {
+ coerce('featureidkey');
+ }
+
+ len = locations.length;
+ } else {
+ var lon = coerce('lon') || [];
+ var lat = coerce('lat') || [];
+ len = Math.min(lon.length, lat.length);
+ }
+
if(!len) {
traceOut.visible = false;
return;
}
+ traceOut._length = len;
+
coerce('text');
coerce('hovertext');
coerce('hovertemplate');
@@ -57,23 +79,3 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
Lib.coerceSelectionMarkerOpacity(traceOut, coerce);
};
-
-function handleLonLatLocDefaults(traceIn, traceOut, coerce) {
- var len = 0;
- var locations = coerce('locations');
-
- var lon, lat;
-
- if(locations) {
- coerce('locationmode');
- len = locations.length;
- return len;
- }
-
- lon = coerce('lon') || [];
- lat = coerce('lat') || [];
- len = Math.min(lon.length, lat.length);
- traceOut._length = len;
-
- return len;
-}
diff --git a/src/traces/scattergeo/event_data.js b/src/traces/scattergeo/event_data.js
index f52decf21ef..a4b197a6d12 100644
--- a/src/traces/scattergeo/event_data.js
+++ b/src/traces/scattergeo/event_data.js
@@ -10,10 +10,16 @@
'use strict';
-module.exports = function eventData(out, pt) {
+module.exports = function eventData(out, pt, trace, cd, pointNumber) {
out.lon = pt.lon;
out.lat = pt.lat;
out.location = pt.loc ? pt.loc : null;
+ // include feature properties from input geojson
+ var cdi = cd[pointNumber];
+ if(cdi.fIn && cdi.fIn.properties) {
+ out.properties = cdi.fIn.properties;
+ }
+
return out;
};
diff --git a/src/traces/scattergeo/index.js b/src/traces/scattergeo/index.js
index 27a6fe010e2..ad80eef949f 100644
--- a/src/traces/scattergeo/index.js
+++ b/src/traces/scattergeo/index.js
@@ -14,7 +14,8 @@ module.exports = {
colorbar: require('../scatter/marker_colorbar'),
formatLabels: require('./format_labels'),
calc: require('./calc'),
- plot: require('./plot'),
+ calcGeoJSON: require('./plot').calcGeoJSON,
+ plot: require('./plot').plot,
style: require('./style'),
styleOnSelect: require('../scatter/style').styleOnSelect,
hoverPoints: require('./hover'),
diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js
index 224ddd9903d..be9f7a4c083 100644
--- a/src/traces/scattergeo/plot.js
+++ b/src/traces/scattergeo/plot.js
@@ -6,23 +6,24 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var d3 = require('d3');
var Lib = require('../../lib');
-var BADNUM = require('../../constants/numerical').BADNUM;
var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures;
-var locationToFeature = require('../../lib/geo_location_utils').locationToFeature;
var geoJsonUtils = require('../../lib/geojson_utils');
+var geoUtils = require('../../lib/geo_location_utils');
+var findExtremes = require('../../plots/cartesian/autorange').findExtremes;
+var BADNUM = require('../../constants/numerical').BADNUM;
+
+var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
var subTypes = require('../scatter/subtypes');
var style = require('./style');
-module.exports = function plot(gd, geo, calcData) {
- for(var i = 0; i < calcData.length; i++) {
- calcGeoJSON(calcData[i], geo.topojson);
- }
+function plot(gd, geo, calcData) {
+ var scatterLayer = geo.layers.frontplot.select('.scatterlayer');
+ var gTraces = Lib.makeTraceGroups(scatterLayer, calcData, 'trace scattergeo');
function removeBADNUM(d, node) {
if(d.lonlat[0] === BADNUM) {
@@ -30,9 +31,6 @@ module.exports = function plot(gd, geo, calcData) {
}
}
- var scatterLayer = geo.layers.frontplot.select('.scatterlayer');
- var gTraces = Lib.makeTraceGroups(scatterLayer, calcData, 'trace scattergeo');
-
// TODO find a way to order the inner nodes on update
gTraces.selectAll('*').remove();
@@ -73,20 +71,57 @@ module.exports = function plot(gd, geo, calcData) {
// call style here within topojson request callback
style(gd, calcTrace);
});
-};
+}
-function calcGeoJSON(calcTrace, topojson) {
+function calcGeoJSON(calcTrace, fullLayout) {
var trace = calcTrace[0].trace;
+ var geoLayout = fullLayout[trace.geo];
+ var geo = geoLayout._subplot;
+ var len = trace._length;
+ var i, calcPt;
+
+ if(Array.isArray(trace.locations)) {
+ var locationmode = trace.locationmode;
+ var features = locationmode === 'geojson-id' ?
+ geoUtils.extractTraceFeature(calcTrace) :
+ getTopojsonFeatures(trace, geo.topojson);
- if(!Array.isArray(trace.locations)) return;
+ for(i = 0; i < len; i++) {
+ calcPt = calcTrace[i];
- var features = getTopojsonFeatures(trace, topojson);
- var locationmode = trace.locationmode;
+ var feature = locationmode === 'geojson-id' ?
+ calcPt.fOut :
+ geoUtils.locationToFeature(locationmode, calcPt.loc, features);
+
+ calcPt.lonlat = feature ? feature.properties.ct : [BADNUM, BADNUM];
+ }
+ }
- for(var i = 0; i < calcTrace.length; i++) {
- var calcPt = calcTrace[i];
- var feature = locationToFeature(locationmode, calcPt.loc, features);
+ var opts = {padded: true};
+ var lonArray;
+ var latArray;
+
+ if(geoLayout.fitbounds === 'geojson' && trace.locationmode === 'geojson-id') {
+ var bboxGeojson = geoUtils.computeBbox(geoUtils.getTraceGeojson(trace));
+ lonArray = [bboxGeojson[0], bboxGeojson[2]];
+ latArray = [bboxGeojson[1], bboxGeojson[3]];
+ } else {
+ lonArray = new Array(len);
+ latArray = new Array(len);
+ for(i = 0; i < len; i++) {
+ calcPt = calcTrace[i];
+ lonArray[i] = calcPt.lonlat[0];
+ latArray[i] = calcPt.lonlat[1];
+ }
- calcPt.lonlat = feature ? feature.properties.ct : [BADNUM, BADNUM];
+ opts.ppad = calcMarkerSize(trace, len);
}
+
+ trace._extremes.lon = findExtremes(geoLayout.lonaxis._ax, lonArray, opts);
+ trace._extremes.lat = findExtremes(geoLayout.lataxis._ax, latArray, opts);
}
+
+module.exports = {
+ calcGeoJSON: calcGeoJSON,
+ plot: plot
+};
diff --git a/test/image/baselines/geo_custom-geojson.png b/test/image/baselines/geo_custom-geojson.png
new file mode 100644
index 00000000000..c5287659445
Binary files /dev/null and b/test/image/baselines/geo_custom-geojson.png differ
diff --git a/test/image/baselines/geo_featureidkey.png b/test/image/baselines/geo_featureidkey.png
new file mode 100644
index 00000000000..34d2922a3d4
Binary files /dev/null and b/test/image/baselines/geo_featureidkey.png differ
diff --git a/test/image/baselines/geo_fitbounds-geojson.png b/test/image/baselines/geo_fitbounds-geojson.png
new file mode 100644
index 00000000000..d6c565042ad
Binary files /dev/null and b/test/image/baselines/geo_fitbounds-geojson.png differ
diff --git a/test/image/baselines/geo_fitbounds-locations.png b/test/image/baselines/geo_fitbounds-locations.png
new file mode 100644
index 00000000000..da9bd72700a
Binary files /dev/null and b/test/image/baselines/geo_fitbounds-locations.png differ
diff --git a/test/image/baselines/geo_fitbounds-scopes.png b/test/image/baselines/geo_fitbounds-scopes.png
new file mode 100644
index 00000000000..43b48b5df62
Binary files /dev/null and b/test/image/baselines/geo_fitbounds-scopes.png differ
diff --git a/test/image/baselines/mapbox_choropleth-raw-geojson.png b/test/image/baselines/mapbox_choropleth-raw-geojson.png
index acb1be264e4..c1b5bc08c5d 100644
Binary files a/test/image/baselines/mapbox_choropleth-raw-geojson.png and b/test/image/baselines/mapbox_choropleth-raw-geojson.png differ
diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js
index 71973e3e86d..2ada885b019 100644
--- a/test/image/compare_pixels_test.js
+++ b/test/image/compare_pixels_test.js
@@ -101,7 +101,9 @@ if(allMock || argv.filter) {
}
var FLAKY_LIST = [
- 'treemap_textposition'
+ 'treemap_textposition',
+ 'trace_metatext',
+ 'gl3d_directions-streamtube1'
];
console.log('');
diff --git a/test/image/mocks/geo_custom-geojson.json b/test/image/mocks/geo_custom-geojson.json
new file mode 100644
index 00000000000..1744a0fe629
--- /dev/null
+++ b/test/image/mocks/geo_custom-geojson.json
@@ -0,0 +1,88 @@
+{
+ "data": [
+ {
+ "type": "scattergeo",
+ "name": "scattergeo + RAW",
+ "locations": ["AL"],
+ "geojson": {
+ "type": "Feature",
+ "id": "AL",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ }
+ },
+ {
+ "type": "choropleth",
+ "name": "choropleth + URL",
+ "locations": ["NY", "MA", "VT"],
+ "z": [10, 20, 30],
+ "showscale": false,
+ "geojson": "https://raw.githubusercontent.com/python-visualization/folium/master/examples/data/us-states.json"
+ },
+
+ {
+ "type": "choropleth",
+ "name": "choropleth + RAW",
+ "locations": ["AL"],
+ "z": [10],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "id": "AL",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ },
+ "geo": "geo2"
+ },
+ {
+ "type": "scattergeo",
+ "name": "scattergeo + URL",
+ "locations": ["NY", "MA", "VT"],
+ "geojson": "https://raw.githubusercontent.com/python-visualization/folium/master/examples/data/us-states.json",
+ "geo": "geo2"
+ }
+ ],
+ "layout": {
+ "geo": {
+ "center": { "lon": -74.22, "lat": 42.35 },
+ "projection": {"scale": 3}
+ },
+ "geo2": {
+ "center": { "lon": -74.22, "lat": 42.35 },
+ "projection": {"scale": 3}
+ },
+ "width": 600,
+ "height": 500,
+ "showlegend": false,
+ "title": {
+ "text": "Geo subplots with custom GeoJSON features",
+ "x": 0.1,
+ "xref": "container",
+ "xanchor": "left"
+ }
+ }
+}
diff --git a/test/image/mocks/geo_featureidkey.json b/test/image/mocks/geo_featureidkey.json
new file mode 100644
index 00000000000..e9f5a52acc4
--- /dev/null
+++ b/test/image/mocks/geo_featureidkey.json
@@ -0,0 +1,73 @@
+{
+ "data": [
+ {
+ "type": "scattergeo",
+ "locations": ["AL"],
+ "featureidkey": "properties.name",
+ "geojson": {
+ "type": "Feature",
+ "properties": {
+ "name": "AL"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ }
+ },
+ {
+ "type": "choropleth",
+ "name": "choropleth + RAW",
+ "locations": ["AL"],
+ "featureidkey": "properties.id",
+ "z": [10],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "properties": {
+ "id": "AL"
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ }
+ }
+ ],
+ "layout": {
+ "geo": {
+ "center": { "lon": -86, "lat": 33 },
+ "projection": {"scale": 30},
+ "visible": false
+ },
+ "width": 600,
+ "height": 400,
+ "showlegend": false,
+ "title": {
+ "text": "Geo traces with set featureidkey",
+ "x": 0.1,
+ "xref": "container",
+ "xanchor": "left"
+ }
+ }
+}
diff --git a/test/image/mocks/geo_fitbounds-geojson.json b/test/image/mocks/geo_fitbounds-geojson.json
new file mode 100644
index 00000000000..82d3651426a
--- /dev/null
+++ b/test/image/mocks/geo_fitbounds-geojson.json
@@ -0,0 +1,180 @@
+{
+ "data": [{
+ "type": "choropleth",
+ "locations": [0],
+ "z": [10],
+ "showscale": false,
+ "geojson": {
+ "type": "FeatureCollection",
+ "features": [{
+ "type": "Feature",
+ "id": 0,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ }, {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[[-83.109191,35.00118],[-83.322791,34.787579],[-83.339222,34.683517],[-83.005129,34.469916],[-82.901067,34.486347],[-82.747713,34.26727],[-82.714851,34.152254],[-82.55602,33.94413],[-82.325988,33.81816],[-82.194542,33.631944],[-81.926172,33.462159],[-81.937125,33.347144],[-81.761863,33.160928],[-81.493493,33.007573],[-81.42777,32.843265],[-81.416816,32.629664],[-81.279893,32.558464],[-81.121061,32.290094],[-81.115584,32.120309],[-80.885553,32.032678],[-81.132015,31.693108],[-81.175831,31.517845],[-81.279893,31.364491],[-81.290846,31.20566],[-81.400385,31.13446],[-81.444201,30.707258],[-81.718048,30.745597],[-81.948079,30.827751],[-82.041187,30.751074],[-82.002849,30.564858],[-82.046664,30.362211],[-82.167157,30.356734],[-82.216449,30.570335],[-83.498053,30.647012],[-84.867289,30.712735],[-85.004212,31.003013],[-85.113751,31.27686],[-85.042551,31.539753],[-85.141136,31.840985],[-85.053504,32.01077],[-85.058981,32.13674],[-84.889196,32.262709],[-85.004212,32.322956],[-84.960397,32.421541],[-85.069935,32.580372],[-85.184951,32.859696],[-85.431413,34.124869],[-85.606675,34.984749],[-84.319594,34.990226],[-83.618546,34.984749],[-83.109191,35.00118]]]
+ }
+ }]
+ }
+ }, {
+ "type": "scattergeo",
+ "locations": [0],
+ "marker": {"size": 40},
+ "geo": "geo2",
+ "geojson": {
+ "type": "FeatureCollection",
+ "features": [{
+ "type": "Feature",
+ "id": 0,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ }, {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[[-83.109191,35.00118],[-83.322791,34.787579],[-83.339222,34.683517],[-83.005129,34.469916],[-82.901067,34.486347],[-82.747713,34.26727],[-82.714851,34.152254],[-82.55602,33.94413],[-82.325988,33.81816],[-82.194542,33.631944],[-81.926172,33.462159],[-81.937125,33.347144],[-81.761863,33.160928],[-81.493493,33.007573],[-81.42777,32.843265],[-81.416816,32.629664],[-81.279893,32.558464],[-81.121061,32.290094],[-81.115584,32.120309],[-80.885553,32.032678],[-81.132015,31.693108],[-81.175831,31.517845],[-81.279893,31.364491],[-81.290846,31.20566],[-81.400385,31.13446],[-81.444201,30.707258],[-81.718048,30.745597],[-81.948079,30.827751],[-82.041187,30.751074],[-82.002849,30.564858],[-82.046664,30.362211],[-82.167157,30.356734],[-82.216449,30.570335],[-83.498053,30.647012],[-84.867289,30.712735],[-85.004212,31.003013],[-85.113751,31.27686],[-85.042551,31.539753],[-85.141136,31.840985],[-85.053504,32.01077],[-85.058981,32.13674],[-84.889196,32.262709],[-85.004212,32.322956],[-84.960397,32.421541],[-85.069935,32.580372],[-85.184951,32.859696],[-85.431413,34.124869],[-85.606675,34.984749],[-84.319594,34.990226],[-83.618546,34.984749],[-83.109191,35.00118]]]
+ }
+ }]
+ }
+ }, {
+ "type": "choropleth",
+ "locations": [0],
+ "z": [10],
+ "showscale": false,
+ "geo": "geo3",
+ "geojson": {
+ "type": "FeatureCollection",
+ "features": [{
+ "type": "Feature",
+ "id": 0,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ }, {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[[-83.109191,35.00118],[-83.322791,34.787579],[-83.339222,34.683517],[-83.005129,34.469916],[-82.901067,34.486347],[-82.747713,34.26727],[-82.714851,34.152254],[-82.55602,33.94413],[-82.325988,33.81816],[-82.194542,33.631944],[-81.926172,33.462159],[-81.937125,33.347144],[-81.761863,33.160928],[-81.493493,33.007573],[-81.42777,32.843265],[-81.416816,32.629664],[-81.279893,32.558464],[-81.121061,32.290094],[-81.115584,32.120309],[-80.885553,32.032678],[-81.132015,31.693108],[-81.175831,31.517845],[-81.279893,31.364491],[-81.290846,31.20566],[-81.400385,31.13446],[-81.444201,30.707258],[-81.718048,30.745597],[-81.948079,30.827751],[-82.041187,30.751074],[-82.002849,30.564858],[-82.046664,30.362211],[-82.167157,30.356734],[-82.216449,30.570335],[-83.498053,30.647012],[-84.867289,30.712735],[-85.004212,31.003013],[-85.113751,31.27686],[-85.042551,31.539753],[-85.141136,31.840985],[-85.053504,32.01077],[-85.058981,32.13674],[-84.889196,32.262709],[-85.004212,32.322956],[-84.960397,32.421541],[-85.069935,32.580372],[-85.184951,32.859696],[-85.431413,34.124869],[-85.606675,34.984749],[-84.319594,34.990226],[-83.618546,34.984749],[-83.109191,35.00118]]]
+ }
+ }]
+ }
+ }, {
+ "type": "scattergeo",
+ "locations": [0],
+ "marker": {"size": 40},
+ "geo": "geo4",
+ "geojson": {
+ "type": "FeatureCollection",
+ "features": [{
+ "type": "Feature",
+ "id": 0,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
+ [-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
+ [-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
+ [-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
+ [-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
+ [-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
+ [-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
+ [-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
+ [-87.359296, 35.00118]
+ ]]
+ }
+ }, {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[[-83.109191,35.00118],[-83.322791,34.787579],[-83.339222,34.683517],[-83.005129,34.469916],[-82.901067,34.486347],[-82.747713,34.26727],[-82.714851,34.152254],[-82.55602,33.94413],[-82.325988,33.81816],[-82.194542,33.631944],[-81.926172,33.462159],[-81.937125,33.347144],[-81.761863,33.160928],[-81.493493,33.007573],[-81.42777,32.843265],[-81.416816,32.629664],[-81.279893,32.558464],[-81.121061,32.290094],[-81.115584,32.120309],[-80.885553,32.032678],[-81.132015,31.693108],[-81.175831,31.517845],[-81.279893,31.364491],[-81.290846,31.20566],[-81.400385,31.13446],[-81.444201,30.707258],[-81.718048,30.745597],[-81.948079,30.827751],[-82.041187,30.751074],[-82.002849,30.564858],[-82.046664,30.362211],[-82.167157,30.356734],[-82.216449,30.570335],[-83.498053,30.647012],[-84.867289,30.712735],[-85.004212,31.003013],[-85.113751,31.27686],[-85.042551,31.539753],[-85.141136,31.840985],[-85.053504,32.01077],[-85.058981,32.13674],[-84.889196,32.262709],[-85.004212,32.322956],[-84.960397,32.421541],[-85.069935,32.580372],[-85.184951,32.859696],[-85.431413,34.124869],[-85.606675,34.984749],[-84.319594,34.990226],[-83.618546,34.984749],[-83.109191,35.00118]]]
+ }
+ }]
+ }
+ }],
+ "layout": {
+ "width": 600,
+ "height": 400,
+ "showlegend": false,
+ "grid": {"rows": 2, "columns": 2},
+ "geo": {
+ "domain": {"row": 0, "column": 0},
+ "fitbounds": "geojson"
+ },
+ "geo2": {
+ "domain": {"row": 1, "column": 0},
+ "fitbounds": "geojson"
+ },
+ "geo3": {
+ "domain": {"row": 0, "column": 1},
+ "fitbounds": "locations"
+ },
+ "geo4": {
+ "domain": {"row": 1, "column": 1},
+ "fitbounds": "locations"
+ },
+ "annotations": [{
+ "text": "fitbounds: 'geojson'",
+ "font": {"size": 20},
+ "showarrow": false,
+ "xref": "paper",
+ "x": 0,
+ "xanchor": "left",
+ "yref": "paper",
+ "y": 1.02,
+ "yanchor": "bottom"
+ }, {
+ "text": "fitbounds: 'locations'",
+ "font": {"size": 20},
+ "showarrow": false,
+ "xref": "paper",
+ "x": 1,
+ "xanchor": "right",
+ "yref": "paper",
+ "y": 1.02,
+ "yanchor": "bottom"
+ }],
+ "margin": {"l": 20, "r": 20, "b": 20, "t": 40}
+ }
+}
diff --git a/test/image/mocks/geo_fitbounds-locations.json b/test/image/mocks/geo_fitbounds-locations.json
new file mode 100644
index 00000000000..afc408a140d
--- /dev/null
+++ b/test/image/mocks/geo_fitbounds-locations.json
@@ -0,0 +1,763 @@
+{
+ "data": [
+ {
+ "name": "equirectangular",
+ "geo": "geo",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "equirectangular"
+ },
+ {
+ "geo": "geo",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "equirectangular",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "mercator",
+ "geo": "geo2",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "mercator"
+ },
+ {
+ "geo": "geo2",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "mercator",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "orthographic",
+ "geo": "geo3",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "orthographic"
+ },
+ {
+ "geo": "geo3",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "orthographic",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "natural earth",
+ "geo": "geo4",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "natural earth"
+ },
+ {
+ "geo": "geo4",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "natural
earth",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "kavrayskiy7",
+ "geo": "geo5",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "kavrayskiy7"
+ },
+ {
+ "geo": "geo5",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "kavrayskiy7",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "miller",
+ "geo": "geo6",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "miller"
+ },
+ {
+ "geo": "geo6",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "miller",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "robinson",
+ "geo": "geo7",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "robinson"
+ },
+ {
+ "geo": "geo7",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "robinson",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "eckert4",
+ "geo": "geo8",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "eckert4"
+ },
+ {
+ "geo": "geo8",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "eckert4",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "azimuthal equal area",
+ "geo": "geo9",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "azimuthal equal area"
+ },
+ {
+ "geo": "geo9",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "azimuthal
equal area",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "azimuthal equidistant",
+ "geo": "geo10",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "azimuthal equidistant"
+ },
+ {
+ "geo": "geo10",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "azimuthal
equidistant",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "conic equal area",
+ "geo": "geo11",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "conic equal area"
+ },
+ {
+ "geo": "geo11",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "conic
equal area",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "conic conformal",
+ "geo": "geo12",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "conic conformal"
+ },
+ {
+ "geo": "geo12",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "conic
conformal",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "conic equidistant",
+ "geo": "geo13",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "conic equidistant"
+ },
+ {
+ "geo": "geo13",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "conic
equidistant",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "gnomonic",
+ "geo": "geo14",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "gnomonic"
+ },
+ {
+ "geo": "geo14",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "gnomonic",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "stereographic",
+ "geo": "geo15",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "stereographic"
+ },
+ {
+ "geo": "geo15",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "stereographic",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "mollweide",
+ "geo": "geo16",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "mollweide"
+ },
+ {
+ "geo": "geo16",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "mollweide",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "hammer",
+ "geo": "geo17",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "hammer"
+ },
+ {
+ "geo": "geo17",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "hammer",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "transverse mercator",
+ "geo": "geo18",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "transverse mercator"
+ },
+ {
+ "geo": "geo18",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "transverse
mercator",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "albers usa",
+ "geo": "geo19",
+ "type": "choropleth",
+ "locationmode": "USA-states",
+ "locations": [
+ "WA"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "albers usa"
+ },
+ {
+ "name": "albers usa",
+ "geo": "geo19",
+ "type": "scattergeo",
+ "locationmode": "USA-states",
+ "locations": [
+ "WA"
+ ],
+ "mode": "text",
+ "text": "albers
usa",
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "winkel tripel",
+ "geo": "geo20",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "winkel tripel"
+ },
+ {
+ "geo": "geo20",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "winkel
tripel",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "aitoff",
+ "geo": "geo21",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "aitoff"
+ },
+ {
+ "geo": "geo21",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "aitoff",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ },
+ {
+ "name": "sinusoidal",
+ "geo": "geo22",
+ "type": "choropleth",
+ "locations": [
+ "AUS"
+ ],
+ "z": [
+ 10
+ ],
+ "showscale": false,
+ "hovertemplate": "sinusoidal"
+ },
+ {
+ "geo": "geo22",
+ "type": "scattergeo",
+ "mode": "text",
+ "text": "sinusoidal",
+ "locations": [
+ "AUS"
+ ],
+ "hoverinfo": "skip"
+ }
+ ],
+ "layout": {
+ "grid": {
+ "rows": 8,
+ "columns": 3
+ },
+ "showlegend": false,
+ "width": 650,
+ "height": 1200,
+ "margin": {
+ "l": 20,
+ "t": 20,
+ "r": 20,
+ "b": 20
+ },
+ "annotations": [
+ {
+ "showarrow": false,
+ "text": "fitbounds
'locations'
for all
projection
types",
+ "font": {
+ "size": 24
+ },
+ "x": 1,
+ "xref": "paper",
+ "xanchor": "right",
+ "y": 0.1,
+ "yanchor": "bottom"
+ }
+ ],
+ "geo": {
+ "domain": {
+ "row": 0,
+ "column": 0
+ },
+ "projection": {
+ "type": "equirectangular"
+ },
+ "fitbounds": "locations"
+ },
+ "geo2": {
+ "domain": {
+ "row": 1,
+ "column": 0
+ },
+ "projection": {
+ "type": "mercator"
+ },
+ "fitbounds": "locations"
+ },
+ "geo3": {
+ "domain": {
+ "row": 2,
+ "column": 0
+ },
+ "projection": {
+ "type": "orthographic"
+ },
+ "fitbounds": "locations"
+ },
+ "geo4": {
+ "domain": {
+ "row": 3,
+ "column": 0
+ },
+ "projection": {
+ "type": "natural earth"
+ },
+ "fitbounds": "locations"
+ },
+ "geo5": {
+ "domain": {
+ "row": 4,
+ "column": 0
+ },
+ "projection": {
+ "type": "kavrayskiy7"
+ },
+ "fitbounds": "locations"
+ },
+ "geo6": {
+ "domain": {
+ "row": 5,
+ "column": 0
+ },
+ "projection": {
+ "type": "miller"
+ },
+ "fitbounds": "locations"
+ },
+ "geo7": {
+ "domain": {
+ "row": 6,
+ "column": 0
+ },
+ "projection": {
+ "type": "robinson"
+ },
+ "fitbounds": "locations"
+ },
+ "geo8": {
+ "domain": {
+ "row": 7,
+ "column": 0
+ },
+ "projection": {
+ "type": "eckert4"
+ },
+ "fitbounds": "locations"
+ },
+ "geo9": {
+ "domain": {
+ "row": 0,
+ "column": 1
+ },
+ "projection": {
+ "type": "azimuthal equal area"
+ },
+ "fitbounds": "locations"
+ },
+ "geo10": {
+ "domain": {
+ "row": 1,
+ "column": 1
+ },
+ "projection": {
+ "type": "azimuthal equidistant"
+ },
+ "fitbounds": "locations"
+ },
+ "geo11": {
+ "domain": {
+ "row": 2,
+ "column": 1
+ },
+ "projection": {
+ "type": "conic equal area"
+ },
+ "fitbounds": "locations"
+ },
+ "geo12": {
+ "domain": {
+ "row": 3,
+ "column": 1
+ },
+ "projection": {
+ "type": "conic conformal"
+ },
+ "fitbounds": "locations"
+ },
+ "geo13": {
+ "domain": {
+ "row": 4,
+ "column": 1
+ },
+ "projection": {
+ "type": "conic equidistant"
+ },
+ "fitbounds": "locations"
+ },
+ "geo14": {
+ "domain": {
+ "row": 5,
+ "column": 1
+ },
+ "projection": {
+ "type": "gnomonic"
+ },
+ "fitbounds": "locations"
+ },
+ "geo15": {
+ "domain": {
+ "row": 6,
+ "column": 1
+ },
+ "projection": {
+ "type": "stereographic"
+ },
+ "fitbounds": "locations"
+ },
+ "geo16": {
+ "domain": {
+ "row": 7,
+ "column": 1
+ },
+ "projection": {
+ "type": "mollweide"
+ },
+ "fitbounds": "locations"
+ },
+ "geo17": {
+ "domain": {
+ "row": 0,
+ "column": 2
+ },
+ "projection": {
+ "type": "hammer"
+ },
+ "fitbounds": "locations"
+ },
+ "geo18": {
+ "domain": {
+ "row": 1,
+ "column": 2
+ },
+ "projection": {
+ "type": "transverse mercator"
+ },
+ "fitbounds": "locations"
+ },
+ "geo19": {
+ "domain": {
+ "row": 2,
+ "column": 2
+ },
+ "projection": {
+ "type": "albers usa"
+ },
+ "fitbounds": "locations",
+ "scope": "usa"
+ },
+ "geo20": {
+ "domain": {
+ "row": 3,
+ "column": 2
+ },
+ "projection": {
+ "type": "winkel tripel"
+ },
+ "fitbounds": "locations"
+ },
+ "geo21": {
+ "domain": {
+ "row": 4,
+ "column": 2
+ },
+ "projection": {
+ "type": "aitoff"
+ },
+ "fitbounds": "locations"
+ },
+ "geo22": {
+ "domain": {
+ "row": 5,
+ "column": 2
+ },
+ "projection": {
+ "type": "sinusoidal"
+ },
+ "fitbounds": "locations"
+ }
+ }
+}
diff --git a/test/image/mocks/geo_fitbounds-scopes.json b/test/image/mocks/geo_fitbounds-scopes.json
new file mode 100644
index 00000000000..2d1420551a3
--- /dev/null
+++ b/test/image/mocks/geo_fitbounds-scopes.json
@@ -0,0 +1,433 @@
+{
+ "data": [
+ {
+ "name": "usa",
+ "geo": "geo",
+ "type": "choropleth",
+ "locations": [
+ 1
+ ],
+ "z": [
+ 1
+ ],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -120,
+ 40
+ ],
+ [
+ -110,
+ 40
+ ],
+ [
+ -110,
+ 30
+ ],
+ [
+ -120,
+ 30
+ ],
+ [
+ -120,
+ 40
+ ]
+ ]
+ ]
+ }
+ },
+ "marker": {
+ "opacity": 0.6
+ },
+ "hovertemplate": "(%{ct[0]},%{ct[1]}) usa"
+ },
+ {
+ "hovertemplate": "(%{lon},%{lat}) usa",
+ "geo": "geo",
+ "type": "scattergeo",
+ "lon": [
+ -115
+ ],
+ "lat": [
+ 45
+ ],
+ "marker": {
+ "size": 23
+ }
+ },
+ {
+ "name": "europe",
+ "geo": "geo2",
+ "type": "choropleth",
+ "locations": [
+ 1
+ ],
+ "z": [
+ 1
+ ],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 2,
+ 50
+ ],
+ [
+ 20,
+ 50
+ ],
+ [
+ 20,
+ 40
+ ],
+ [
+ 2,
+ 40
+ ],
+ [
+ 2,
+ 50
+ ]
+ ]
+ ]
+ }
+ },
+ "marker": {
+ "opacity": 0.6
+ },
+ "hovertemplate": "(%{ct[0]},%{ct[1]}) europe"
+ },
+ {
+ "hovertemplate": "(%{lon},%{lat}) europe",
+ "geo": "geo2",
+ "type": "scattergeo",
+ "lon": [
+ 24
+ ],
+ "lat": [
+ 45
+ ],
+ "marker": {
+ "size": 23
+ }
+ },
+ {
+ "name": "asia",
+ "geo": "geo3",
+ "type": "choropleth",
+ "locations": [
+ 1
+ ],
+ "z": [
+ 1
+ ],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 120,
+ 50
+ ],
+ [
+ 150,
+ 50
+ ],
+ [
+ 150,
+ 20
+ ],
+ [
+ 120,
+ 20
+ ],
+ [
+ 120,
+ 50
+ ]
+ ]
+ ]
+ }
+ },
+ "marker": {
+ "opacity": 0.6
+ },
+ "hovertemplate": "(%{ct[0]},%{ct[1]}) asia"
+ },
+ {
+ "hovertemplate": "(%{lon},%{lat}) asia",
+ "geo": "geo3",
+ "type": "scattergeo",
+ "lon": [
+ 155
+ ],
+ "lat": [
+ 30
+ ],
+ "marker": {
+ "size": 23
+ }
+ },
+ {
+ "name": "africa",
+ "geo": "geo4",
+ "type": "choropleth",
+ "locations": [
+ 1
+ ],
+ "z": [
+ 1
+ ],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 2,
+ 1
+ ],
+ [
+ 20,
+ 1
+ ],
+ [
+ 20,
+ -20
+ ],
+ [
+ 2,
+ -20
+ ],
+ [
+ 2,
+ 1
+ ]
+ ]
+ ]
+ }
+ },
+ "marker": {
+ "opacity": 0.6
+ },
+ "hovertemplate": "(%{ct[0]},%{ct[1]}) africa"
+ },
+ {
+ "hovertemplate": "(%{lon},%{lat}) africa",
+ "geo": "geo4",
+ "type": "scattergeo",
+ "lon": [
+ -2
+ ],
+ "lat": [
+ 10
+ ],
+ "marker": {
+ "size": 23
+ }
+ },
+ {
+ "name": "north america",
+ "geo": "geo5",
+ "type": "choropleth",
+ "locations": [
+ 1
+ ],
+ "z": [
+ 1
+ ],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -120,
+ 40
+ ],
+ [
+ -110,
+ 40
+ ],
+ [
+ -110,
+ 30
+ ],
+ [
+ -120,
+ 30
+ ],
+ [
+ -120,
+ 40
+ ]
+ ]
+ ]
+ }
+ },
+ "marker": {
+ "opacity": 0.6
+ },
+ "hovertemplate": "(%{ct[0]},%{ct[1]}) north america"
+ },
+ {
+ "hovertemplate": "(%{lon},%{lat}) north america",
+ "geo": "geo5",
+ "type": "scattergeo",
+ "lon": [
+ -125
+ ],
+ "lat": [
+ 25
+ ],
+ "marker": {
+ "size": 23
+ }
+ },
+ {
+ "name": "south america",
+ "geo": "geo6",
+ "type": "choropleth",
+ "locations": [
+ 1
+ ],
+ "z": [
+ 1
+ ],
+ "showscale": false,
+ "geojson": {
+ "type": "Feature",
+ "id": 1,
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -60,
+ -20
+ ],
+ [
+ -50,
+ -20
+ ],
+ [
+ -50,
+ -40
+ ],
+ [
+ -60,
+ -40
+ ],
+ [
+ -60,
+ -20
+ ]
+ ]
+ ]
+ }
+ },
+ "marker": {
+ "opacity": 0.6
+ },
+ "hovertemplate": "(%{ct[0]},%{ct[1]}) south america"
+ },
+ {
+ "hovertemplate": "(%{lon},%{lat}) south america",
+ "geo": "geo6",
+ "type": "scattergeo",
+ "lon": [
+ -55
+ ],
+ "lat": [
+ -45
+ ],
+ "marker": {
+ "size": 23
+ }
+ }
+ ],
+ "layout": {
+ "grid": {
+ "rows": 3,
+ "columns": 2
+ },
+ "showlegend": false,
+ "width": 650,
+ "height": 900,
+ "margin": {
+ "l": 20,
+ "t": 20,
+ "r": 20,
+ "b": 20
+ },
+ "geo": {
+ "domain": {
+ "row": 0,
+ "column": 0
+ },
+ "scope": "usa",
+ "fitbounds": "locations"
+ },
+ "geo2": {
+ "domain": {
+ "row": 1,
+ "column": 0
+ },
+ "scope": "europe",
+ "fitbounds": "locations"
+ },
+ "geo3": {
+ "domain": {
+ "row": 2,
+ "column": 0
+ },
+ "scope": "asia",
+ "fitbounds": "locations"
+ },
+ "geo4": {
+ "domain": {
+ "row": 0,
+ "column": 1
+ },
+ "scope": "africa",
+ "fitbounds": "locations"
+ },
+ "geo5": {
+ "domain": {
+ "row": 1,
+ "column": 1
+ },
+ "scope": "north america",
+ "fitbounds": "locations"
+ },
+ "geo6": {
+ "domain": {
+ "row": 2,
+ "column": 1
+ },
+ "scope": "south america",
+ "fitbounds": "locations"
+ }
+ }
+}
diff --git a/test/image/mocks/mapbox_choropleth-raw-geojson.json b/test/image/mocks/mapbox_choropleth-raw-geojson.json
index e3ae99ae8d2..9fc58b63210 100644
--- a/test/image/mocks/mapbox_choropleth-raw-geojson.json
+++ b/test/image/mocks/mapbox_choropleth-raw-geojson.json
@@ -28,6 +28,20 @@
]]
}
}
+ }, {
+ "type": "choroplethmapbox",
+ "locations": ["Georgia"],
+ "z": [5],
+ "featureidkey": "properties.name",
+ "coloraxis": "coloraxis",
+ "geojson": {
+ "type": "Feature",
+ "properties": {"name": "Georgia"},
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [[[-83.109191,35.00118],[-83.322791,34.787579],[-83.339222,34.683517],[-83.005129,34.469916],[-82.901067,34.486347],[-82.747713,34.26727],[-82.714851,34.152254],[-82.55602,33.94413],[-82.325988,33.81816],[-82.194542,33.631944],[-81.926172,33.462159],[-81.937125,33.347144],[-81.761863,33.160928],[-81.493493,33.007573],[-81.42777,32.843265],[-81.416816,32.629664],[-81.279893,32.558464],[-81.121061,32.290094],[-81.115584,32.120309],[-80.885553,32.032678],[-81.132015,31.693108],[-81.175831,31.517845],[-81.279893,31.364491],[-81.290846,31.20566],[-81.400385,31.13446],[-81.444201,30.707258],[-81.718048,30.745597],[-81.948079,30.827751],[-82.041187,30.751074],[-82.002849,30.564858],[-82.046664,30.362211],[-82.167157,30.356734],[-82.216449,30.570335],[-83.498053,30.647012],[-84.867289,30.712735],[-85.004212,31.003013],[-85.113751,31.27686],[-85.042551,31.539753],[-85.141136,31.840985],[-85.053504,32.01077],[-85.058981,32.13674],[-84.889196,32.262709],[-85.004212,32.322956],[-84.960397,32.421541],[-85.069935,32.580372],[-85.184951,32.859696],[-85.431413,34.124869],[-85.606675,34.984749],[-84.319594,34.990226],[-83.618546,34.984749],[-83.109191,35.00118]]]
+ }
+ }
}],
"layout": {
"width": 600,
diff --git a/test/jasmine/tests/choropleth_test.js b/test/jasmine/tests/choropleth_test.js
index 8c6cf96055f..9aee3409e56 100644
--- a/test/jasmine/tests/choropleth_test.js
+++ b/test/jasmine/tests/choropleth_test.js
@@ -3,6 +3,7 @@ var Choropleth = require('@src/traces/choropleth');
var Plotly = require('@lib');
var Plots = require('@src/plots/plots');
var Lib = require('@src/lib');
+var loggers = require('@src/lib/loggers');
var d3 = require('d3');
var createGraphDiv = require('../assets/create_graph_div');
@@ -88,6 +89,65 @@ describe('Test choropleth', function() {
expect(traceOut.marker.line.width).toBe(0, 'mlw');
expect(traceOut.marker.line.color).toBe(undefined, 'mlc');
});
+
+ it('should default locationmode to *geojson-id* when a valid *geojson* is provided', function() {
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ z: [1, 2],
+ geojson: 'url'
+ };
+ traceOut = {};
+ Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('geojson-id', 'valid url string');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ z: [1, 2],
+ geojson: {}
+ };
+ traceOut = {};
+ Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('geojson-id', 'valid object');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ z: [1, 2],
+ geojson: ''
+ };
+ traceOut = {};
+ Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('ISO-3', 'invalid sting');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ z: [1, 2],
+ geojson: []
+ };
+ traceOut = {};
+ Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('ISO-3', 'invalid object');
+ });
+
+ it('should only coerce *featureidkey* when locationmode is *geojson-id', function() {
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ z: [1, 2],
+ geojson: 'url',
+ featureidkey: 'properties.name'
+ };
+ traceOut = {};
+ Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.featureidkey).toBe('properties.name', 'coerced');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ z: [1, 2],
+ featureidkey: 'properties.name'
+ };
+ traceOut = {};
+ Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.featureidkey).toBe(undefined, 'NOT coerced');
+ });
});
});
@@ -242,6 +302,15 @@ describe('Test choropleth hover:', function() {
run(pos, fig, exp).then(done);
});
});
+
+ it('should include *properties* from input custom geojson', function(done) {
+ var fig = Lib.extendDeep({}, require('@mocks/geo_custom-geojson.json'));
+ fig.data = [fig.data[1]];
+ fig.data[0].hovertemplate = '%{properties.name}%{ct[0]:.1f} | %{ct[1]:.1f}';
+ fig.layout.geo.projection = {scale: 20};
+
+ run([300, 200], fig, ['New York', '-75.1 | 42.6']).then(done);
+ });
});
describe('choropleth drawing', function() {
@@ -254,7 +323,7 @@ describe('choropleth drawing', function() {
afterEach(destroyGraphDiv);
it('should not throw an error with bad locations', function(done) {
- spyOn(Lib, 'log');
+ spyOn(loggers, 'log');
Plotly.newPlot(gd, [{
locations: ['canada', 0, null, '', 'utopia'],
z: [1, 2, 3, 4, 5],
@@ -263,7 +332,7 @@ describe('choropleth drawing', function() {
}])
.then(function() {
// only utopia logs - others are silently ignored
- expect(Lib.log).toHaveBeenCalledTimes(1);
+ expect(loggers.log).toHaveBeenCalledTimes(1);
})
.catch(failTest)
.then(done);
diff --git a/test/jasmine/tests/choroplethmapbox_test.js b/test/jasmine/tests/choroplethmapbox_test.js
index 41657a1181a..1940e983e6f 100644
--- a/test/jasmine/tests/choroplethmapbox_test.js
+++ b/test/jasmine/tests/choroplethmapbox_test.js
@@ -1,6 +1,7 @@
var Plotly = require('@lib');
var Plots = require('@src/plots/plots');
var Lib = require('@src/lib');
+var loggers = require('@src/lib/loggers');
var convertModule = require('@src/traces/choroplethmapbox/convert');
var MAPBOX_ACCESS_TOKEN = require('@build/credentials.json').MAPBOX_ACCESS_TOKEN;
@@ -150,19 +151,19 @@ describe('Test choroplethmapbox convert:', function() {
});
it('should return early if something goes wrong while fetching a GeoJSON', function() {
- spyOn(Lib, 'error');
+ spyOn(loggers, 'error');
var opts = _convert({
locations: ['a'], z: [1],
geojson: 'url'
});
- expect(Lib.error).toHaveBeenCalledWith('Oops ... something when wrong when fetching url');
+ expect(loggers.error).toHaveBeenCalledWith('Oops ... something went wrong when fetching url');
expectBlank(opts);
});
describe('should warn when set GeoJSON is not a *FeatureCollection* or a *Feature* type and return early', function() {
- beforeEach(function() { spyOn(Lib, 'warn'); });
+ beforeEach(function() { spyOn(loggers, 'warn'); });
it('- case missing *type* key', function() {
var opts = _convert({
@@ -172,9 +173,9 @@ describe('Test choroplethmapbox convert:', function() {
}
});
expectBlank(opts);
- expect(Lib.warn).toHaveBeenCalledWith([
- 'Invalid GeoJSON type none,',
- 'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
+ expect(loggers.warn).toHaveBeenCalledWith([
+ 'Invalid GeoJSON type none.',
+ 'Traces with locationmode *geojson-id* only support *FeatureCollection* and *Feature* types.'
].join(' '));
});
@@ -186,15 +187,15 @@ describe('Test choroplethmapbox convert:', function() {
}
});
expectBlank(opts);
- expect(Lib.warn).toHaveBeenCalledWith([
- 'Invalid GeoJSON type nop!,',
- 'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
+ expect(loggers.warn).toHaveBeenCalledWith([
+ 'Invalid GeoJSON type nop!.',
+ 'Traces with locationmode *geojson-id* only support *FeatureCollection* and *Feature* types.'
].join(' '));
});
});
describe('should log when crossing a GeoJSON geometry that is not a *Polygon* or a *MultiPolygon* type', function() {
- beforeEach(function() { spyOn(Lib, 'log'); });
+ beforeEach(function() { spyOn(loggers, 'log'); });
it('- case missing geometry *type*', function() {
var trace = base();
@@ -202,9 +203,9 @@ describe('Test choroplethmapbox convert:', function() {
var opts = _convert(trace);
expect(opts.geojson.features.length).toBe(2, '# of feature to be rendered');
- expect(Lib.log).toHaveBeenCalledWith([
- 'Location with id b does not have a valid GeoJSON geometry,',
- 'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
+ expect(loggers.log).toHaveBeenCalledWith([
+ 'Location b does not have a valid GeoJSON geometry.',
+ 'Traces with locationmode *geojson-id* only support *Polygon* and *MultiPolygon* geometries.'
].join(' '));
});
@@ -214,15 +215,15 @@ describe('Test choroplethmapbox convert:', function() {
var opts = _convert(trace);
expect(opts.geojson.features.length).toBe(2, '# of feature to be rendered');
- expect(Lib.log).toHaveBeenCalledWith([
- 'Location with id c does not have a valid GeoJSON geometry,',
- 'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
+ expect(loggers.log).toHaveBeenCalledWith([
+ 'Location c does not have a valid GeoJSON geometry.',
+ 'Traces with locationmode *geojson-id* only support *Polygon* and *MultiPolygon* geometries.'
].join(' '));
});
});
it('should log when an entry set in *locations* does not a matching feature in the GeoJSON', function() {
- spyOn(Lib, 'log');
+ spyOn(loggers, 'log');
var trace = base();
trace.locations = ['a', 'b', 'c', 'd'];
@@ -230,7 +231,7 @@ describe('Test choroplethmapbox convert:', function() {
var opts = _convert(trace);
expect(opts.geojson.features.length).toBe(3, '# of features to be rendered');
- expect(Lib.log).toHaveBeenCalledWith('Location with id d does not have a matching feature');
+ expect(loggers.log).toHaveBeenCalledWith('Location *d* does not have a matching feature with id-key *id*.');
});
describe('should accept numbers as *locations* items', function() {
@@ -593,7 +594,7 @@ describe('@noCI Test choroplethmapbox hover:', function() {
desc: 'with "typeof number" locations[i] and feature id (in *name* label case)',
patch: function() {
var fig = Lib.extendDeep({}, require('@mocks/mapbox_choropleth-raw-geojson.json'));
- fig.data.shift();
+ fig.data = [fig.data[1]];
fig.data[0].locations = [100];
fig.data[0].geojson.id = 100;
return fig;
@@ -605,7 +606,7 @@ describe('@noCI Test choroplethmapbox hover:', function() {
desc: 'with "typeof number" locations[i] and feature id (in *nums* label case)',
patch: function() {
var fig = Lib.extendDeep({}, require('@mocks/mapbox_choropleth-raw-geojson.json'));
- fig.data.shift();
+ fig.data = [fig.data[1]];
fig.data[0].locations = [100];
fig.data[0].geojson.id = 100;
fig.data[0].hoverinfo = 'location+name';
@@ -618,14 +619,14 @@ describe('@noCI Test choroplethmapbox hover:', function() {
desc: 'with "typeof number" locations[i] and feature id (hovertemplate case)',
patch: function() {
var fig = Lib.extendDeep({}, require('@mocks/mapbox_choropleth-raw-geojson.json'));
- fig.data.shift();
+ fig.data = [fig.data[1]];
fig.data[0].locations = [100];
fig.data[0].geojson.id = 100;
- fig.data[0].hovertemplate = '### %{location}%{location} ###';
+ fig.data[0].hovertemplate = '### %{location}%{ct[0]:.1f} | %{ct[1]:.1f} ###';
return fig;
},
nums: '### 100',
- name: '100 ###',
+ name: '-86.7 | 32.0 ###',
evtPts: [{location: 100, z: 10, pointNumber: 0, curveNumber: 0}]
}];
diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js
index 897a84981fe..3828874767c 100644
--- a/test/jasmine/tests/geo_test.js
+++ b/test/jasmine/tests/geo_test.js
@@ -128,7 +128,7 @@ describe('Test Geo layout defaults', function() {
});
});
- it('should not coerce projection.parallels if type is conic', function() {
+ it('should only coerce projection.parallels if type is conic', function() {
var projTypes = layoutAttributes.projection.type.values;
function testOne(projType) {
@@ -463,6 +463,229 @@ describe('Test Geo layout defaults', function() {
});
});
});
+
+ describe('should clear attributes that get auto-filled under *fitbounds*', function() {
+ var vals = ['locations', 'geojson'];
+
+ function _assert(exp) {
+ expect(layoutOut.geo.projection.scale).toBe(exp['projection.scale'], 'projection.scale');
+ expect(layoutOut.geo.center.lon).toBe(exp['center.lon'], 'center.lon');
+ expect(layoutOut.geo.center.lat).toBe(exp['center.lat'], 'center.lat');
+ expect(layoutOut.geo.projection.rotation.lon).toBe(exp['projection.rotation.lon'], 'projection.rotation.lon');
+ expect(layoutOut.geo.projection.rotation.lat).toBe(exp['projection.rotation.lat'], 'projection.rotation.lat');
+ expect(layoutOut.geo.lonaxis.range).withContext('lonaxis.range').toEqual(exp['lonaxis.range'], 'lonaxis.range');
+ expect(layoutOut.geo.lataxis.range).withContext('lataxis.range').toEqual(exp['lataxis.range'], 'lataxis.range');
+ }
+
+ describe('- for scoped maps', function() {
+ it('fitbounds:false (base case)', function() {
+ layoutIn = {
+ geo: {
+ scope: 'europe',
+ fitbounds: false
+ }
+ };
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ 'projection.scale': 1,
+ 'center.lon': 15,
+ 'center.lat': 57.5,
+ 'projection.rotation.lon': 15,
+ 'projection.rotation.lat': 0,
+ 'lonaxis.range': [-30, 60],
+ 'lataxis.range': [30, 85]
+ });
+ });
+
+ vals.forEach(function(v) {
+ it('fitbounds:' + v, function() {
+ layoutIn = {
+ geo: {
+ scope: 'europe',
+ fitbounds: v
+ }
+ };
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ 'projection.scale': undefined,
+ 'center.lon': undefined,
+ 'center.lat': undefined,
+ 'projection.rotation.lon': 15,
+ 'projection.rotation.lat': 0,
+ 'lonaxis.range': [-30, 60],
+ 'lataxis.range': [30, 85]
+ });
+ });
+ });
+ });
+
+ describe('- for clipped projections', function() {
+ it('fitbounds:false (base case)', function() {
+ layoutIn = {
+ geo: {
+ projection: {
+ type: 'orthographic',
+ rotation: {lon: 20, lat: 20},
+ scale: 2
+ },
+ fitbounds: false,
+ }
+ };
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ '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]
+ });
+ });
+
+ vals.forEach(function(v) {
+ it('fitbounds:' + v, function() {
+ layoutIn = {
+ geo: {
+ projection: {
+ type: 'orthographic',
+ rotation: {lon: 20, lat: 20},
+ scale: 2
+ },
+ fitbounds: v
+ }
+ };
+ 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
+ });
+ });
+ });
+ });
+
+ describe('- for non-clipped projections', function() {
+ it('fitbounds:false (base case)', function() {
+ layoutIn = {
+ geo: {
+ projection: {
+ type: 'natural earth',
+ rotation: {lon: 20},
+ scale: 2
+ },
+ lonaxis: {range: [-90, 90]},
+ lataxis: {range: [0, 80]},
+ fitbounds: false,
+ }
+ };
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ '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]
+ });
+ });
+
+ vals.forEach(function(v) {
+ it('fitbounds:' + v, function() {
+ layoutIn = {
+ geo: {
+ projection: {
+ type: 'natural earth',
+ rotation: {lon: 20},
+ scale: 2
+ },
+ lonaxis: {range: [-90, 90]},
+ lataxis: {range: [0, 80]},
+ fitbounds: v,
+ }
+ };
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ 'projection.scale': undefined,
+ 'center.lon': undefined,
+ 'center.lat': undefined,
+ 'projection.rotation.lon': undefined,
+ 'projection.rotation.lat': 0,
+ 'lonaxis.range': [-90, 90],
+ 'lataxis.range': [0, 80]
+ });
+ });
+ });
+ });
+ });
+
+ describe('geo.visible should override show* defaults', function() {
+ var keys = [
+ 'lonaxis.showgrid',
+ 'lataxis.showgrid',
+ 'showcoastlines',
+ 'showocean',
+ 'showland',
+ 'showlakes',
+ 'showrivers',
+ 'showcountries',
+ 'showsubunits',
+ 'showframe'
+ ];
+
+ function _assert(extra) {
+ var geo = layoutOut.geo;
+ keys.forEach(function(k) {
+ var actual = Lib.nestedProperty(geo, k).get();
+ if(extra && k in extra) {
+ expect(actual).toBe(extra[k], k);
+ } else {
+ expect(actual).toBe(false, k);
+ }
+ });
+ }
+
+ it('- base case', function() {
+ layoutIn = {
+ geo: { visible: false }
+ };
+
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ showsubunits: undefined
+ });
+ });
+
+ it('- scoped case', function() {
+ layoutIn = {
+ geo: { scope: 'europe', visible: false }
+ };
+
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ showframe: undefined,
+ showsubunits: undefined
+ });
+ });
+
+ it('- scope:usa case', function() {
+ layoutIn = {
+ geo: { scope: 'usa', visible: false }
+ };
+
+ supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+ _assert({
+ showframe: undefined,
+ showcoastlines: undefined,
+ showocean: undefined
+ });
+ });
+ });
});
describe('geojson / topojson utils', function() {
@@ -701,7 +924,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData)).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
- 'location', 'z'
+ 'location', 'z', 'ct'
]);
});
@@ -729,7 +952,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData)).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
- 'location', 'z'
+ 'location', 'z', 'ct'
]);
});
@@ -761,7 +984,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData)).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
- 'location', 'z'
+ 'location', 'z', 'ct'
]);
});
@@ -1192,6 +1415,7 @@ describe('Test geo interactions', function() {
expect(geoLayout.lataxis.range).toEqual([-90, 90]);
expect(geo.viewInitial).toEqual({
+ 'fitbounds': false,
'projection.rotation.lon': 0,
'center.lon': 0,
'center.lat': 0,
@@ -1303,6 +1527,7 @@ describe('Test geo interactions', function() {
Plotly.react(gd, figWorld)
.then(function() {
_assertViewInitial('world scope', {
+ 'fitbounds': false,
'center.lon': 0,
'center.lat': 0,
'projection.scale': 1,
@@ -1312,6 +1537,7 @@ describe('Test geo interactions', function() {
.then(function() { return Plotly.react(gd, figUSA); })
.then(function() {
_assertViewInitial('react to usa scope', {
+ 'fitbounds': false,
'center.lon': -96.6,
'center.lat': 38.7,
'projection.scale': 1
@@ -1320,6 +1546,7 @@ describe('Test geo interactions', function() {
.then(function() { return Plotly.react(gd, figNA); })
.then(function() {
_assertViewInitial('react to NA scope', {
+ 'fitbounds': false,
'center.lon': -112.5,
'center.lat': 45,
'projection.scale': 1
@@ -1328,6 +1555,7 @@ describe('Test geo interactions', function() {
.then(function() { return Plotly.react(gd, figWorld); })
.then(function() {
_assertViewInitial('react back to world scope', {
+ 'fitbounds': false,
'center.lon': 0,
'center.lat': 0,
'projection.scale': 1,
@@ -1390,6 +1618,21 @@ describe('Test geo interactions', function() {
.catch(failTest)
.then(done);
});
+
+ it('- geo.visible:false', function(done) {
+ Plotly.plot(gd, [{
+ type: 'scattergeo',
+ lon: [0],
+ lat: [0]
+ }], {
+ geo: {visible: false}
+ })
+ .then(_assert(0))
+ .then(function() { return Plotly.relayout(gd, 'geo.visible', true); })
+ .then(_assert(1))
+ .catch(failTest)
+ .then(done);
+ });
});
});
@@ -1774,25 +2017,27 @@ describe('Test geo zoom/pan/drag interactions:', function() {
});
}
- it('should work for non-clipped projections', function(done) {
- var fig = Lib.extendDeep({}, require('@mocks/geo_winkel-tripel'));
- fig.layout.width = 700;
- fig.layout.height = 500;
- fig.layout.dragmode = 'pan';
+ describe('should work for non-clipped projections', function() {
+ var fig;
+
+ beforeEach(function() {
+ fig = Lib.extendDeep({}, require('@mocks/geo_winkel-tripel'));
+ fig.layout.width = 700;
+ fig.layout.height = 500;
+ fig.layout.dragmode = 'pan';
+ });
function _assert(step, attr, proj, eventKeys) {
var msg = '[' + step + '] ';
var geoLayout = gd._fullLayout.geo;
- var rotation = geoLayout.projection.rotation;
- var center = geoLayout.center;
+ var rotation = geoLayout.projection.rotation || {};
+ var center = geoLayout.center || {};
var scale = geoLayout.projection.scale;
- expect(rotation.lon).toBeCloseTo(attr[0][0], 1, msg + 'rotation.lon');
- expect(rotation.lat).toBeCloseTo(attr[0][1], 1, msg + 'rotation.lat');
- expect(center.lon).toBeCloseTo(attr[1][0], 1, msg + 'center.lon');
- expect(center.lat).toBeCloseTo(attr[1][1], 1, msg + 'center.lat');
- expect(scale).toBeCloseTo(attr[2], 1, msg + 'zoom');
+ expect([rotation.lon, rotation.lat]).toBeCloseToArray(attr[0], 1, msg + 'rotation.(lon|lat)');
+ expect([center.lon, center.lat]).toBeCloseToArray(attr[1], 1, msg + 'center.(lon|lat)');
+ expect(scale)[typeof scale === 'number' ? 'toBeCloseTo' : 'toBe'](attr[2], 1, msg + 'zoom');
var geo = geoLayout._subplot;
var rotate = geo.projection.rotate();
@@ -1811,85 +2056,145 @@ describe('Test geo zoom/pan/drag interactions:', function() {
assertEventData(msg, eventKeys);
}
- plot(fig).then(function() {
- _assert('base', [
- [-90, 0], [-90, 0], 1
- ], [
- [90, 0], [350, 260], [0, 0], 101.9
- ], undefined);
- return drag({path: [[350, 250], [400, 250]], noCover: true});
- })
- .then(function() {
- _assert('after east-west drag', [
- [-124.4, 0], [-124.4, 0], 1
- ], [
- [124.4, 0], [350, 260], [0, 0], 101.9
- ], [
- 'geo.projection.rotation.lon', 'geo.center.lon'
- ]);
- return drag({path: [[400, 250], [400, 300]], noCover: true});
- })
- .then(function() {
- _assert('after north-south drag', [
- [-124.4, 0], [-124.4, 28.1], 1
- ], [
- [124.4, 0], [350, 310], [0, 0], 101.9
- ], [
- 'geo.center.lat'
- ]);
- return scroll([200, 250], [-200, -200]);
- })
- .then(function() {
- _assert('after off-center scroll', [
- [-151.2, 0], [-151.2, 29.5], 1.3
- ], [
- [151.2, 0], [350, 329.2], [0, 0], 134.4
- ], [
- 'geo.projection.rotation.lon',
- 'geo.center.lon', 'geo.center.lat',
- 'geo.projection.scale'
- ]);
- return Plotly.relayout(gd, 'geo.showocean', false);
- })
- .then(function() {
- _assert('after some relayout call that causes a replot', [
- [-151.2, 0], [-151.2, 29.5], 1.3
- ], [
- // converts translate (px) to center (lonlat)
- [151.2, 0], [350, 260], [0, 29.5], 134.4
- ], [
- 'geo.showocean'
- ]);
- return dblClick([350, 250]);
- })
- .then(function() {
- // resets to initial view
- _assert('after double click', [
- [-90, 0], [-90, 0], 1
- ], [
- [90, 0], [350, 260], [0, 0], 101.9
- ], 'dblclick');
- })
- .catch(failTest)
- .then(done);
+ it('- base case', function(done) {
+ plot(fig).then(function() {
+ _assert('base', [
+ [-90, 0], [-90, 0], 1
+ ], [
+ [90, 0], [350, 260], [0, 0], 101.9
+ ], undefined);
+ return drag({path: [[350, 250], [400, 250]], noCover: true});
+ })
+ .then(function() {
+ _assert('after east-west drag', [
+ [-124.4, 0], [-124.4, 0], 1
+ ], [
+ [124.4, 0], [350, 260], [0, 0], 101.9
+ ], [
+ 'geo.projection.rotation.lon', 'geo.center.lon'
+ ]);
+ return drag({path: [[400, 250], [400, 300]], noCover: true});
+ })
+ .then(function() {
+ _assert('after north-south drag', [
+ [-124.4, 0], [-124.4, 28.1], 1
+ ], [
+ [124.4, 0], [350, 310], [0, 0], 101.9
+ ], [
+ 'geo.center.lat'
+ ]);
+ return scroll([200, 250], [-200, -200]);
+ })
+ .then(function() {
+ _assert('after off-center scroll', [
+ [-151.2, 0], [-151.2, 29.5], 1.3
+ ], [
+ [151.2, 0], [350, 329.2], [0, 0], 134.4
+ ], [
+ 'geo.projection.rotation.lon',
+ 'geo.center.lon', 'geo.center.lat',
+ 'geo.projection.scale'
+ ]);
+ return Plotly.relayout(gd, 'geo.showocean', false);
+ })
+ .then(function() {
+ _assert('after some relayout call that causes a replot', [
+ [-151.2, 0], [-151.2, 29.5], 1.3
+ ], [
+ // converts translate (px) to center (lonlat)
+ [151.2, 0], [350, 260], [0, 29.5], 134.4
+ ], [
+ 'geo.showocean'
+ ]);
+ return dblClick([350, 250]);
+ })
+ .then(function() {
+ // resets to initial view
+ _assert('after double click', [
+ [-90, 0], [-90, 0], 1
+ ], [
+ [90, 0], [350, 260], [0, 0], 101.9
+ ], 'dblclick');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('- fitbounds case', function(done) {
+ fig.layout.geo.fitbounds = 'locations';
+
+ plot(fig).then(function() {
+ _assert('base', [
+ [undefined, 0], [undefined, undefined], undefined
+ ], [
+ [-180, -0], [350, 260], [0, 0], 114.59
+ ], undefined);
+ return drag({path: [[350, 250], [400, 250]], noCover: true});
+ })
+ .then(function() {
+ _assert('after east-west drag', [
+ [149.40, 0], [149.40, 0], 1.1249
+ ], [
+ [-149.40, 0], [350, 260], [0, 0], 114.59
+ ], [
+ 'geo.projection.rotation.lon', 'geo.center.lon', 'geo.center.lat',
+ 'geo.projection.scale', 'geo.fitbounds'
+ ]);
+ return scroll([200, 250], [-200, -200]);
+ })
+ .then(function() {
+ _assert('after off-center scroll', [
+ [127.176, 0], [127.176, 1.21], 1.484
+ ], [
+ [-127.176, 0], [350, 263.195], [0, 0], 151.20
+ ], [
+ 'geo.projection.rotation.lon', 'geo.center.lon',
+ 'geo.center.lat', 'geo.projection.scale'
+ ]);
+ return Plotly.relayout(gd, 'geo.showocean', false);
+ })
+ .then(function() {
+ _assert('after some relayout call that causes a replot', [
+ // converts translate (px) to center (lonlat)
+ [127.176, 0], [127.176, 1.21], 1.484
+ ], [
+ [-127.176, 0], [350, 260], [0, 1.21], 151.20
+ ], [
+ 'geo.showocean'
+ ]);
+ return dblClick([350, 250]);
+ })
+ .then(function() {
+ _assert('after double click', [
+ [undefined, 0], [undefined, undefined], undefined
+ ], [
+ [-180, -0], [350, 260], [0, 0], 114.59
+ ], 'dblclick');
+ })
+ .catch(failTest)
+ .then(done);
+ });
});
- it('should work for clipped projections', function(done) {
- var fig = Lib.extendDeep({}, require('@mocks/geo_orthographic'));
- fig.layout.dragmode = 'pan';
+ describe('should work for clipped projections', function() {
+ var fig;
- // of layout width = height = 500
+ beforeEach(function() {
+ fig = Lib.extendDeep({}, require('@mocks/geo_orthographic'));
+ fig.layout.dragmode = 'pan';
+
+ // of layout width = height = 500
+ });
function _assert(step, attr, proj, eventKeys) {
var msg = '[' + step + '] ';
var geoLayout = gd._fullLayout.geo;
- var rotation = geoLayout.projection.rotation;
+ var rotation = geoLayout.projection.rotation || {};
var scale = geoLayout.projection.scale;
- expect(rotation.lon).toBeCloseTo(attr[0][0], 0, msg + 'rotation.lon');
- expect(rotation.lat).toBeCloseTo(attr[0][1], 0, msg + 'rotation.lat');
- expect(scale).toBeCloseTo(attr[1], 1, msg + 'zoom');
+ expect([rotation.lon, rotation.lat]).toBeCloseToArray(attr[0], -0.5, msg + 'rotation.(lon|lat)');
+ expect(scale)[typeof scale === 'number' ? 'toBeCloseTo' : 'toBe'](attr[1], 1, msg + 'zoom');
var geo = geoLayout._subplot;
var rotate = geo.projection.rotate();
@@ -1902,84 +2207,144 @@ describe('Test geo zoom/pan/drag interactions:', function() {
assertEventData(msg, eventKeys);
}
- plot(fig).then(function() {
- _assert('base', [
- [-75, 45], 1
- ], [
- [75, -45], 160
- ], undefined);
- return drag({path: [[250, 250], [300, 250]], noCover: true});
- })
- .then(function() {
- _assert('after east-west drag', [
- [-103.7, 49.3], 1
- ], [
- [103.7, -49.3], 160
- ], [
- 'geo.projection.rotation.lon', 'geo.projection.rotation.lat'
- ]);
- return drag({path: [[250, 250], [300, 300]], noCover: true});
- })
- .then(function() {
- _assert('after NW-SE drag', [
- [-135.5, 73.8], 1
- ], [
- [135.5, -73.8], 160
- ], [
- 'geo.projection.rotation.lon', 'geo.projection.rotation.lat'
- ]);
- return scroll([300, 300], [-200, -200]);
- })
- .then(function() {
- _assert('after scroll', [
- [-126.2, 67.1], 1.3
- ], [
- [126.2, -67.1], 211.1
- ], [
- 'geo.projection.rotation.lon', 'geo.projection.rotation.lat',
- 'geo.projection.scale'
- ]);
- return Plotly.relayout(gd, 'geo.showocean', false);
- })
- .then(function() {
- _assert('after some relayout call that causes a replot', [
- [-126.2, 67.1], 1.3
- ], [
- [126.2, -67.1], 211.1
- ], [
- 'geo.showocean'
- ]);
- return dblClick([350, 250]);
- })
- .then(function() {
- // resets to initial view
- _assert('after double click', [
- [-75, 45], 1
- ], [
- [75, -45], 160
- ], 'dblclick');
- })
- .catch(failTest)
- .then(done);
+ it('- base case', function(done) {
+ plot(fig).then(function() {
+ _assert('base', [
+ [-75, 45], 1
+ ], [
+ [75, -45], 160
+ ], undefined);
+ return drag({path: [[250, 250], [300, 250]], noCover: true});
+ })
+ .then(function() {
+ _assert('after east-west drag', [
+ [-103.7, 49.3], 1
+ ], [
+ [103.7, -49.3], 160
+ ], [
+ 'geo.projection.rotation.lon', 'geo.projection.rotation.lat'
+ ]);
+ return drag({path: [[250, 250], [300, 300]], noCover: true});
+ })
+ .then(function() {
+ _assert('after NW-SE drag', [
+ [-135.5, 73.8], 1
+ ], [
+ [135.5, -73.8], 160
+ ], [
+ 'geo.projection.rotation.lon', 'geo.projection.rotation.lat'
+ ]);
+ return scroll([300, 300], [-200, -200]);
+ })
+ .then(function() {
+ _assert('after scroll', [
+ [-126.2, 67.1], 1.3
+ ], [
+ [126.2, -67.1], 211.1
+ ], [
+ 'geo.projection.rotation.lon', 'geo.projection.rotation.lat',
+ 'geo.projection.scale'
+ ]);
+ return Plotly.relayout(gd, 'geo.showocean', false);
+ })
+ .then(function() {
+ _assert('after some relayout call that causes a replot', [
+ [-126.2, 67.1], 1.3
+ ], [
+ [126.2, -67.1], 211.1
+ ], [
+ 'geo.showocean'
+ ]);
+ return dblClick([350, 250]);
+ })
+ .then(function() {
+ // resets to initial view
+ _assert('after double click', [
+ [-75, 45], 1
+ ], [
+ [75, -45], 160
+ ], 'dblclick');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('- fitbounds case', function(done) {
+ fig.layout.geo.fitbounds = 'locations';
+
+ plot(fig).then(function() {
+ _assert('base', [
+ [undefined, undefined], undefined
+ ], [
+ [0.252, -19.8], 160
+ ], undefined);
+ return drag({path: [[250, 250], [300, 250]], noCover: true});
+ })
+ .then(function() {
+ _assert('after east-west drag', [
+ [-20.32, 21.226], 1
+ ], [
+ [20.32, -21.226], 160
+ ], [
+ 'geo.projection.rotation.lon', 'geo.projection.rotation.lat',
+ 'geo.projection.scale', 'geo.fitbounds'
+ ]);
+ return scroll([300, 300], [-100, -100]);
+ })
+ .then(function() {
+ _assert('after scroll', [
+ [-17.5597, 18.862], 1.1488
+ ], [
+ [17.5597, -18.862], 183.818
+ ], [
+ 'geo.projection.rotation.lon', 'geo.projection.rotation.lat',
+ 'geo.projection.scale'
+ ]);
+ return Plotly.relayout(gd, 'geo.showocean', false);
+ })
+ .then(function() {
+ _assert('after some relayout call that causes a replot', [
+ [-17.5597, 18.862], 1.1488
+ ], [
+ [17.5597, -18.862], 183.818
+ ], [
+ 'geo.showocean'
+ ]);
+ return dblClick([350, 250]);
+ })
+ .then(function() {
+ // resets to initial view
+ _assert('after double click', [
+ [undefined, undefined], undefined
+ ], [
+ [0.252, -19.8], 160
+ ], 'dblclick');
+ })
+ .catch(failTest)
+ .then(done);
+ });
});
- it('should work for scoped projections', function(done) {
- var fig = Lib.extendDeep({}, require('@mocks/geo_europe-bubbles'));
- fig.layout.geo.resolution = 110;
- fig.layout.dragmode = 'pan';
+ describe('should work for scoped projections', function() {
+ var fig;
+
+ beforeEach(function() {
+ fig = Lib.extendDeep({}, require('@mocks/geo_europe-bubbles'));
+ fig.layout.geo.resolution = 110;
+ fig.layout.dragmode = 'pan';
- // of layout width = height = 500
+ // of layout width = height = 500
+ });
function _assert(step, attr, proj, eventKeys) {
var msg = '[' + step + '] ';
var geoLayout = gd._fullLayout.geo;
- var center = geoLayout.center;
+ var center = geoLayout.center || {};
var scale = geoLayout.projection.scale;
- expect(center.lon).toBeCloseTo(attr[0][0], -0.5, msg + 'center.lon');
- expect(center.lat).toBeCloseTo(attr[0][1], -0.5, msg + 'center.lat');
- expect(scale).toBeCloseTo(attr[1], 1, msg + 'zoom');
+ expect([center.lon, center.lat]).toBeCloseToArray(attr[0], -0.5, msg + 'center.(lon|lat)');
+ expect(scale)[typeof scale === 'number' ? 'toBeCloseTo' : 'toBe'](attr[1], 1, msg + 'zoom');
var geo = geoLayout._subplot;
var translate = geo.projection.translate();
@@ -1995,55 +2360,112 @@ describe('Test geo zoom/pan/drag interactions:', function() {
assertEventData(msg, eventKeys);
}
- plot(fig).then(function() {
- _assert('base', [
- [15, 57.5], 1,
- ], [
- [247, 260], [0, 57.5], 292.2
- ], undefined);
- return drag({path: [[250, 250], [200, 200]], noCover: true});
- })
- .then(function() {
- _assert('after SW-NE drag', [
- [30.9, 46.2], 1
- ], [
- // changes translate(), but not center()
- [197, 210], [0, 57.5], 292.2
- ], [
- 'geo.center.lon', 'geo.center.lon'
- ]);
- return scroll([300, 300], [-200, -200]);
- })
- .then(function() {
- _assert('after scroll', [
- [34.3, 43.6], 1.3
- ], [
- [164.1, 181.2], [0, 57.5], 385.5
- ], [
- 'geo.center.lon', 'geo.center.lon', 'geo.projection.scale'
- ]);
- return Plotly.relayout(gd, 'geo.showlakes', true);
- })
- .then(function() {
- _assert('after some relayout call that causes a replot', [
- [34.3, 43.6], 1.3
- ], [
- // changes are now reflected in 'center'
- [247, 260], [19.3, 43.6], 385.5
- ], [
- 'geo.showlakes'
- ]);
- return dblClick([250, 250]);
- })
- .then(function() {
- _assert('after double click', [
- [15, 57.5], 1,
- ], [
- [247, 260], [0, 57.5], 292.2
- ], 'dblclick');
- })
- .catch(failTest)
- .then(done);
+ it('- base case', function(done) {
+ plot(fig).then(function() {
+ _assert('base', [
+ [15, 57.5], 1,
+ ], [
+ [247, 260], [0, 57.5], 292.2
+ ], undefined);
+ return drag({path: [[250, 250], [200, 200]], noCover: true});
+ })
+ .then(function() {
+ _assert('after SW-NE drag', [
+ [30.9, 46.2], 1
+ ], [
+ // changes translate(), but not center()
+ [197, 210], [0, 57.5], 292.2
+ ], [
+ 'geo.center.lon', 'geo.center.lon'
+ ]);
+ return scroll([300, 300], [-200, -200]);
+ })
+ .then(function() {
+ _assert('after scroll', [
+ [34.3, 43.6], 1.3
+ ], [
+ [164.1, 181.2], [0, 57.5], 385.5
+ ], [
+ 'geo.center.lon', 'geo.center.lon', 'geo.projection.scale'
+ ]);
+ return Plotly.relayout(gd, 'geo.showlakes', true);
+ })
+ .then(function() {
+ _assert('after some relayout call that causes a replot', [
+ [34.3, 43.6], 1.3
+ ], [
+ // changes are now reflected in 'center'
+ [247, 260], [19.3, 43.6], 385.5
+ ], [
+ 'geo.showlakes'
+ ]);
+ return dblClick([250, 250]);
+ })
+ .then(function() {
+ _assert('after double click', [
+ [15, 57.5], 1,
+ ], [
+ [247, 260], [0, 57.5], 292.2
+ ], 'dblclick');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('- fitbounds case', function(done) {
+ fig.layout.geo.fitbounds = 'locations';
+
+ plot(fig).then(function() {
+ _assert('base', [
+ [undefined, undefined], undefined,
+ ], [
+ [247, 260], [5.7998, 49.29], 504.8559
+ ], undefined);
+ return drag({path: [[250, 250], [200, 200]], noCover: true});
+ })
+ .then(function() {
+ _assert('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'
+ ]);
+ return scroll([300, 300], [-200, -200]);
+ })
+ .then(function() {
+ _assert('after scroll', [
+ [31.027, 40.91], 2.28
+ ], [
+ [164.09, 181.24], [5.7988, 49.29], 666.16
+ ], [
+ 'geo.center.lon', 'geo.center.lon',
+ 'geo.projection.scale'
+ ]);
+ return Plotly.relayout(gd, 'geo.showlakes', true);
+ })
+ .then(function() {
+ _assert('after some relayout call that causes a replot', [
+ [31.027, 40.91], 2.28
+ ], [
+ // changes are now reflected in 'center'
+ [247, 260], [16.027, 40.91], 666.16
+ ], [
+ 'geo.showlakes'
+ ]);
+ return dblClick([250, 250]);
+ })
+ .then(function() {
+ _assert('after double click', [
+ [undefined, undefined], undefined,
+ ], [
+ [247, 260], [5.7998, 49.29], 504.8559
+ ], 'dblclick');
+ })
+ .catch(failTest)
+ .then(done);
+ });
});
it('should work for *albers usa* projections', function(done) {
diff --git a/test/jasmine/tests/plot_api_react_test.js b/test/jasmine/tests/plot_api_react_test.js
index e622f3bb175..1050c7225fa 100644
--- a/test/jasmine/tests/plot_api_react_test.js
+++ b/test/jasmine/tests/plot_api_react_test.js
@@ -1450,6 +1450,39 @@ describe('Plotly.react and uirevision attributes', function() {
_run(fig, editView, checkOriginalView, checkEditedView).then(done);
});
+ it('preserves geo viewport changes using geo.uirevision (fitbounds case)', function(done) {
+ function fig(mainRev, geoRev) {
+ return {
+ data: [{
+ type: 'scattergeo', lon: [0, -75], lat: [0, 45]
+ }],
+ layout: {
+ uirevision: mainRev,
+ geo: {uirevision: geoRev, fitbounds: 'locations'}
+ }
+ };
+ }
+
+ function attrs(original) {
+ return {
+ 'geo.fitbounds': original ? ['locations', 'locations'] : false,
+ 'geo.projection.scale': original ? [undefined, undefined] : 3,
+ 'geo.projection.rotation.lon': original ? [undefined, undefined] : -45,
+ 'geo.center.lat': original ? [undefined, undefined] : 22,
+ 'geo.center.lon': original ? [undefined, undefined] : -45
+ };
+ }
+
+ function editView() {
+ return Registry.call('_guiRelayout', gd, attrs());
+ }
+
+ var checkOriginalView = checkState([], attrs(true));
+ var checkEditedView = checkState([], attrs());
+
+ _run(fig, editView, checkOriginalView, checkEditedView).then(done);
+ });
+
it('@gl preserves 3d camera changes using scene.uirevision', function(done) {
function fig(mainRev, sceneRev) {
return {
diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js
index 6fe010fe392..e13ef7bc097 100644
--- a/test/jasmine/tests/scattergeo_test.js
+++ b/test/jasmine/tests/scattergeo_test.js
@@ -1,6 +1,7 @@
var Plotly = require('@lib');
var Lib = require('@src/lib');
var BADNUM = require('@src/constants/numerical').BADNUM;
+var loggers = require('@src/lib/loggers');
var ScatterGeo = require('@src/traces/scattergeo');
@@ -86,6 +87,67 @@ describe('Test scattergeo defaults', function() {
traceOut = {};
testOne();
});
+
+ it('should default locationmode to *geojson-id* when a valid *geojson* is provided', function() {
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ geojson: 'url'
+ };
+ traceOut = {};
+ ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('geojson-id', 'valid url string');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ geojson: {}
+ };
+ traceOut = {};
+ ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('geojson-id', 'valid object');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ geojson: ''
+ };
+ traceOut = {};
+ ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('ISO-3', 'invalid sting');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ geojson: []
+ };
+ traceOut = {};
+ ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe('ISO-3', 'invalid object');
+
+ traceIn = {
+ lon: [20, 40],
+ lat: [20, 40]
+ };
+ traceOut = {};
+ ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.locationmode).toBe(undefined, 'lon/lat coordinates');
+ });
+
+ it('should only coerce *featureidkey* when locationmode is *geojson-id', function() {
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ geojson: 'url',
+ featureidkey: 'properties.name'
+ };
+ traceOut = {};
+ ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.featureidkey).toBe('properties.name', 'coerced');
+
+ traceIn = {
+ locations: ['CAN', 'USA'],
+ featureidkey: 'properties.name'
+ };
+ traceOut = {};
+ ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+ expect(traceOut.featureidkey).toBe(undefined, 'NOT coerced');
+ });
});
describe('Test scattergeo calc', function() {
@@ -385,6 +447,22 @@ describe('Test scattergeo hover', function() {
.then(done);
});
});
+
+ it('should include *properties* from input custom geojson', function(done) {
+ var fig = Lib.extendDeep({}, require('@mocks/geo_custom-geojson.json'));
+ fig.data = [fig.data[3]];
+ fig.data[0].geo = 'geo';
+ fig.data[0].marker = {size: 40};
+ fig.data[0].hovertemplate = '%{properties.name}LOOK';
+ fig.layout.geo.projection = {scale: 30};
+
+ Plotly.react(gd, fig)
+ .then(function() {
+ check([275, 255], ['New York', 'LOOK']);
+ })
+ .catch(failTest)
+ .then(done);
+ });
});
describe('scattergeo drawing', function() {
@@ -397,7 +475,7 @@ describe('scattergeo drawing', function() {
afterEach(destroyGraphDiv);
it('should not throw an error with bad locations', function(done) {
- spyOn(Lib, 'log');
+ spyOn(loggers, 'log');
Plotly.newPlot(gd, [{
locations: ['canada', 0, null, '', 'utopia'],
locationmode: 'country names',
@@ -405,7 +483,7 @@ describe('scattergeo drawing', function() {
}])
.then(function() {
// only utopia logs - others are silently ignored
- expect(Lib.log).toHaveBeenCalledTimes(1);
+ expect(loggers.log).toHaveBeenCalledTimes(1);
})
.catch(failTest)
.then(done);