diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js
index 18e4280abcc..7536885207f 100644
--- a/src/components/modebar/buttons.js
+++ b/src/components/modebar/buttons.js
@@ -420,7 +420,7 @@ function handleGeo(gd, ev) {
geoIds = Plots.getSubplotIds(fullLayout, 'geo');
for(var i = 0; i < geoIds.length; i++) {
- var geo = fullLayout[geoIds[i]]._geo;
+ var geo = fullLayout[geoIds[i]]._subplot;
if(attr === 'zoom') {
var scale = geo.projection.scale();
diff --git a/src/lib/geojson_utils.js b/src/lib/geojson_utils.js
new file mode 100644
index 00000000000..80751a1bcaa
--- /dev/null
+++ b/src/lib/geojson_utils.js
@@ -0,0 +1,130 @@
+/**
+* Copyright 2012-2016, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+/**
+ * Convert calcTrace to GeoJSON 'MultiLineString' coordinate arrays
+ *
+ * @param {object} calcTrace
+ * gd.calcdata item.
+ * Note that calcTrace[i].lonlat is assumed to be defined
+ *
+ * @return {array}
+ * return line coords array (or array of arrays)
+ *
+ */
+exports.calcTraceToLineCoords = function(calcTrace) {
+ var trace = calcTrace[0].trace,
+ connectgaps = trace.connectgaps;
+
+ var coords = [],
+ lineString = [];
+
+ for(var i = 0; i < calcTrace.length; i++) {
+ var calcPt = calcTrace[i];
+
+ lineString.push(calcPt.lonlat);
+
+ if(!connectgaps && calcPt.gapAfter && lineString.length > 0) {
+ coords.push(lineString);
+ lineString = [];
+ }
+ }
+
+ coords.push(lineString);
+
+ return coords;
+};
+
+
+/**
+ * Make line ('LineString' or 'MultiLineString') GeoJSON
+ *
+ * @param {array} coords
+ * results form calcTraceToLineCoords
+ * @param {object} trace
+ * (optional) full trace object to be added on to output
+ *
+ * @return {object} out
+ * GeoJSON object
+ *
+ */
+exports.makeLine = function(coords, trace) {
+ var out = {};
+
+ if(coords.length === 1) {
+ out = {
+ type: 'LineString',
+ coordinates: coords[0]
+ };
+ }
+ else {
+ out = {
+ type: 'MultiLineString',
+ coordinates: coords
+ };
+ }
+
+ if(trace) out.trace = trace;
+
+ return out;
+};
+
+/**
+ * Make polygon ('Polygon' or 'MultiPolygon') GeoJSON
+ *
+ * @param {array} coords
+ * results form calcTraceToLineCoords
+ * @param {object} trace
+ * (optional) full trace object to be added on to output
+ *
+ * @return {object} out
+ * GeoJSON object
+ */
+exports.makePolygon = function(coords, trace) {
+ var out = {};
+
+ if(coords.length === 1) {
+ out = {
+ type: 'Polygon',
+ coordinates: coords
+ };
+ }
+ else {
+ var _coords = new Array(coords.length);
+
+ for(var i = 0; i < coords.length; i++) {
+ _coords[i] = [coords[i]];
+ }
+
+ out = {
+ type: 'MultiPolygon',
+ coordinates: _coords
+ };
+ }
+
+ if(trace) out.trace = trace;
+
+ return out;
+};
+
+/**
+ * Make blank GeoJSON
+ *
+ * @return {object}
+ * Blank GeoJSON object
+ *
+ */
+exports.makeBlank = function() {
+ return {
+ type: 'Point',
+ coordinates: []
+ };
+};
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 758a778df92..5052445294e 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -312,7 +312,7 @@ exports.doModeBar = function(gd) {
subplotIds = Plots.getSubplotIds(fullLayout, 'geo');
for(i = 0; i < subplotIds.length; i++) {
- var geo = fullLayout[subplotIds[i]]._geo;
+ var geo = fullLayout[subplotIds[i]]._subplot;
geo.updateFx(fullLayout.hovermode);
}
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index bed9ad0b723..0907513d199 100644
--- a/src/plots/cartesian/graph_interact.js
+++ b/src/plots/cartesian/graph_interact.js
@@ -379,6 +379,7 @@ function hover(gd, evt, subplot) {
curvenum,
cd,
trace,
+ subplotId,
subploti,
mode,
xval,
@@ -468,7 +469,8 @@ function hover(gd, evt, subplot) {
if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue;
trace = cd[0].trace;
- subploti = subplots.indexOf(getSubplot(trace));
+ subplotId = getSubplot(trace);
+ subploti = subplots.indexOf(subplotId);
// within one trace mode can sometimes be overridden
mode = hovermode;
@@ -495,6 +497,11 @@ function hover(gd, evt, subplot) {
text: undefined
};
+ // add ref to subplot object (non-cartesian case)
+ if(fullLayout[subplotId]) {
+ pointData.subplot = fullLayout[subplotId]._subplot;
+ }
+
closedataPreviousLength = hoverData.length;
// for a highlighting array, figure out what
@@ -545,7 +552,6 @@ function hover(gd, evt, subplot) {
hoverData.splice(0, closedataPreviousLength);
distance = hoverData[0].distance;
}
-
}
// nothing left: remove all labels and quit
@@ -583,31 +589,35 @@ function hover(gd, evt, subplot) {
// other people and send it to the event
for(itemnum = 0; itemnum < hoverData.length; itemnum++) {
var pt = hoverData[itemnum];
+
var out = {
data: pt.trace._input,
fullData: pt.trace,
curveNumber: pt.trace.index,
- pointNumber: pt.index,
- x: pt.xVal,
- y: pt.yVal,
- xaxis: pt.xa,
- yaxis: pt.ya
+ pointNumber: pt.index
};
- if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal;
+
+ if(pt.trace._module.eventData) out = pt.trace._module.eventData(out, pt);
+ else {
+ out.x = pt.xVal;
+ out.y = pt.yVal;
+ out.xaxis = pt.xa;
+ out.yaxis = pt.ya;
+
+ if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal;
+ }
+
newhoverdata.push(out);
}
+
gd._hoverdata = newhoverdata;
if(!hoverChanged(gd, evt, oldhoverdata)) return;
- /* Emit the custom hover handler. Bind this like:
- * gd.on('hover.plotly', function(extras) {
- * // do something with extras.data
- * });
- */
if(oldhoverdata) {
gd.emit('plotly_unhover', { points: oldhoverdata });
}
+
gd.emit('plotly_hover', {
points: gd._hoverdata,
xaxes: xaArray,
@@ -620,7 +630,7 @@ function hover(gd, evt, subplot) {
// look for either .subplot (currently just ternary)
// or xaxis and yaxis attributes
function getSubplot(trace) {
- return trace.subplot || (trace.xaxis + trace.yaxis);
+ return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo;
}
fx.getDistanceFunction = function(mode, dx, dy, dxy) {
@@ -724,6 +734,7 @@ function cleanPoint(d, hovermode) {
if(infomode.indexOf('text') === -1) d.text = undefined;
if(infomode.indexOf('name') === -1) d.name = undefined;
}
+
return d;
}
diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js
index 968fc671251..91dfd67c461 100644
--- a/src/plots/geo/geo.js
+++ b/src/plots/geo/geo.js
@@ -16,8 +16,7 @@ var d3 = require('d3');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var Axes = require('../../plots/cartesian/axes');
-
-var filterVisible = require('../../lib/filter_visible');
+var Fx = require('../../plots/cartesian/graph_interact');
var addProjectionsToD3 = require('./projections');
var createGeoScale = require('./set_scale');
@@ -29,17 +28,16 @@ var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
var topojsonUtils = require('../../lib/topojson_utils');
var topojsonFeature = require('topojson').feature;
+// add a few projection types to d3.geo
+addProjectionsToD3(d3);
-function Geo(options, fullLayout) {
+function Geo(options, fullLayout) {
this.id = options.id;
this.graphDiv = options.graphDiv;
this.container = options.container;
this.topojsonURL = options.topojsonURL;
- // add a few projection types to d3.geo
- addProjectionsToD3(d3);
-
this.hoverContainer = null;
this.topojsonName = null;
@@ -55,6 +53,9 @@ function Geo(options, fullLayout) {
this.zoom = null;
this.zoomReset = null;
+ this.xaxis = null;
+ this.yaxis = null;
+
this.makeFramework();
this.updateFx(fullLayout.hovermode);
@@ -65,7 +66,7 @@ module.exports = Geo;
var proto = Geo.prototype;
-proto.plot = function(geoData, fullLayout, promises) {
+proto.plot = function(geoCalcData, fullLayout, promises) {
var _this = this,
geoLayout = fullLayout[_this.id],
graphSize = fullLayout._size;
@@ -90,6 +91,30 @@ proto.plot = function(geoData, fullLayout, promises) {
.call(_this.zoom)
.on('dblclick.zoom', _this.zoomReset);
+ _this.framework.on('mousemove', function() {
+ var mouse = d3.mouse(this),
+ lonlat = _this.projection.invert(mouse);
+
+ if(isNaN(lonlat[0]) || isNaN(lonlat[1])) return;
+
+ var evt = {
+ target: true,
+ xpx: mouse[0],
+ ypx: mouse[1]
+ };
+
+ _this.xaxis.c2p = function() { return mouse[0]; };
+ _this.xaxis.p2c = function() { return lonlat[0]; };
+ _this.yaxis.c2p = function() { return mouse[1]; };
+ _this.yaxis.p2c = function() { return lonlat[1]; };
+
+ Fx.hover(_this.graphDiv, evt, _this.id);
+ });
+
+ _this.framework.on('click', function() {
+ Fx.click(_this.graphDiv, { target: true });
+ });
+
topojsonNameNew = topojsonUtils.getTopojsonName(geoLayout);
if(_this.topojson === null || topojsonNameNew !== _this.topojsonName) {
@@ -97,7 +122,7 @@ proto.plot = function(geoData, fullLayout, promises) {
if(PlotlyGeoAssets.topojson[_this.topojsonName] !== undefined) {
_this.topojson = PlotlyGeoAssets.topojson[_this.topojsonName];
- _this.onceTopojsonIsLoaded(geoData, geoLayout);
+ _this.onceTopojsonIsLoaded(geoCalcData, geoLayout);
}
else {
topojsonPath = topojsonUtils.getTopojsonPath(
@@ -128,19 +153,19 @@ proto.plot = function(geoData, fullLayout, promises) {
_this.topojson = topojson;
PlotlyGeoAssets.topojson[_this.topojsonName] = topojson;
- _this.onceTopojsonIsLoaded(geoData, geoLayout);
+ _this.onceTopojsonIsLoaded(geoCalcData, geoLayout);
resolve();
});
}));
}
}
- else _this.onceTopojsonIsLoaded(geoData, geoLayout);
+ else _this.onceTopojsonIsLoaded(geoCalcData, geoLayout);
// TODO handle topojson-is-loading case
// to avoid making multiple request while streaming
};
-proto.onceTopojsonIsLoaded = function(geoData, geoLayout) {
+proto.onceTopojsonIsLoaded = function(geoCalcData, geoLayout) {
var i;
this.drawLayout(geoLayout);
@@ -148,11 +173,12 @@ proto.onceTopojsonIsLoaded = function(geoData, geoLayout) {
var traceHashOld = this.traceHash;
var traceHash = {};
- for(i = 0; i < geoData.length; i++) {
- var trace = geoData[i];
+ for(i = 0; i < geoCalcData.length; i++) {
+ var calcData = geoCalcData[i],
+ trace = calcData[0].trace;
traceHash[trace.type] = traceHash[trace.type] || [];
- traceHash[trace.type].push(trace);
+ traceHash[trace.type].push(calcData);
}
var moduleNamesOld = Object.keys(traceHashOld);
@@ -165,19 +191,21 @@ proto.onceTopojsonIsLoaded = function(geoData, geoLayout) {
var moduleName = moduleNamesOld[i];
if(moduleNames.indexOf(moduleName) === -1) {
- var fakeModule = traceHashOld[moduleName][0];
- fakeModule.visible = false;
- traceHash[moduleName] = [fakeModule];
+ var fakeCalcTrace = traceHashOld[moduleName][0],
+ fakeTrace = fakeCalcTrace[0].trace;
+
+ fakeTrace.visible = false;
+ traceHash[moduleName] = [fakeCalcTrace];
}
}
moduleNames = Object.keys(traceHash);
for(i = 0; i < moduleNames.length; i++) {
- var moduleData = traceHash[moduleNames[i]];
- var _module = moduleData[0]._module;
+ var moduleCalcData = traceHash[moduleNames[i]],
+ _module = moduleCalcData[0][0].trace._module;
- _module.plot(this, filterVisible(moduleData), geoLayout);
+ _module.plot(this, filterVisible(moduleCalcData), geoLayout);
}
this.traceHash = traceHash;
@@ -185,6 +213,19 @@ proto.onceTopojsonIsLoaded = function(geoData, geoLayout) {
this.render();
};
+function filterVisible(calcDataIn) {
+ var calcDataOut = [];
+
+ for(var i = 0; i < calcDataIn.length; i++) {
+ var calcTrace = calcDataIn[i],
+ trace = calcTrace[0].trace;
+
+ if(trace.visible === true) calcDataOut.push(calcTrace);
+ }
+
+ return calcDataOut;
+}
+
proto.updateFx = function(hovermode) {
this.showHover = (hovermode !== false);
@@ -251,6 +292,8 @@ proto.makeFramework = function() {
.attr('id', this.id)
.style('position', 'absolute');
+ // only choropleth traces use this,
+ // scattergeo traces use Fx.hover and fullLayout._hoverlayer
var hoverContainer = this.hoverContainer = geoDiv.append('svg');
hoverContainer
.attr(xmlnsNamespaces.svgAttrs)
@@ -280,14 +323,20 @@ proto.makeFramework = function() {
framework.on('dblclick.zoom', null);
// TODO use clip paths instead of nested SVG
+
+ this.xaxis = { _id: 'x' };
+ this.yaxis = { _id: 'y' };
};
proto.adjustLayout = function(geoLayout, graphSize) {
var domain = geoLayout.domain;
+ var left = graphSize.l + graphSize.w * domain.x[0] + geoLayout._marginX,
+ top = graphSize.t + graphSize.h * (1 - domain.y[1]) + geoLayout._marginY;
+
this.geoDiv.style({
- left: graphSize.l + graphSize.w * domain.x[0] + geoLayout._marginX + 'px',
- top: graphSize.t + graphSize.h * (1 - domain.y[1]) + geoLayout._marginY + 'px',
+ left: left + 'px',
+ top: top + 'px',
width: geoLayout._width + 'px',
height: geoLayout._height + 'px'
});
@@ -308,6 +357,12 @@ proto.adjustLayout = function(geoLayout, graphSize) {
height: geoLayout._height
})
.call(Color.fill, geoLayout.bgcolor);
+
+ this.xaxis._offset = left;
+ this.xaxis._length = geoLayout._width;
+
+ this.yaxis._offset = top;
+ this.yaxis._length = geoLayout._height;
};
proto.drawTopo = function(selection, layerName, geoLayout) {
@@ -431,27 +486,36 @@ proto.styleLayout = function(geoLayout) {
}
};
+proto.isLonLatOverEdges = function(lonlat) {
+ var clipAngle = this.clipAngle;
+
+ if(clipAngle === null) return false;
+
+ var p = this.projection.rotate(),
+ angle = d3.geo.distance(lonlat, [-p[0], -p[1]]),
+ maxAngle = clipAngle * Math.PI / 180;
+
+ return angle > maxAngle;
+};
+
// [hot code path] (re)draw all paths which depend on the projection
proto.render = function() {
- var framework = this.framework,
+ var _this = this,
+ framework = _this.framework,
gChoropleth = framework.select('g.choroplethlayer'),
gScatterGeo = framework.select('g.scattergeolayer'),
- projection = this.projection,
- path = this.path,
- clipAngle = this.clipAngle;
+ path = _this.path;
function translatePoints(d) {
- var lonlat = projection([d.lon, d.lat]);
- if(!lonlat) return null;
- return 'translate(' + lonlat[0] + ',' + lonlat[1] + ')';
+ var lonlatPx = _this.projection(d.lonlat);
+ if(!lonlatPx) return null;
+
+ return 'translate(' + lonlatPx[0] + ',' + lonlatPx[1] + ')';
}
// hide paths over edges of clipped projections
function hideShowPoints(d) {
- var p = projection.rotate(),
- angle = d3.geo.distance([d.lon, d.lat], [-p[0], -p[1]]),
- maxAngle = clipAngle * Math.PI / 180;
- return (angle > maxAngle) ? '0' : '1.0';
+ return _this.isLonLatOverEdges(d.lonlat) ? '0' : '1.0';
}
framework.selectAll('path.basepath').attr('d', path);
@@ -462,7 +526,7 @@ proto.render = function() {
gScatterGeo.selectAll('path.js-line').attr('d', path);
- if(clipAngle !== null) {
+ if(_this.clipAngle !== null) {
gScatterGeo.selectAll('path.point')
.style('opacity', hideShowPoints)
.attr('transform', translatePoints);
diff --git a/src/plots/geo/index.js b/src/plots/geo/index.js
index cd277b1c9ca..1ef3282e6ae 100644
--- a/src/plots/geo/index.js
+++ b/src/plots/geo/index.js
@@ -32,7 +32,7 @@ exports.supplyLayoutDefaults = require('./layout/defaults');
exports.plot = function plotGeo(gd) {
var fullLayout = gd._fullLayout,
- fullData = gd._fullData,
+ calcData = gd.calcdata,
geoIds = Plots.getSubplotIds(fullLayout, 'geo');
/**
@@ -45,8 +45,8 @@ exports.plot = function plotGeo(gd) {
for(var i = 0; i < geoIds.length; i++) {
var geoId = geoIds[i],
- fullGeoData = Plots.getSubplotData(fullData, 'geo', geoId),
- geo = fullLayout[geoId]._geo;
+ geoCalcData = getSubplotCalcData(calcData, geoId),
+ geo = fullLayout[geoId]._subplot;
// If geo is not instantiated, create one!
if(geo === undefined) {
@@ -59,10 +59,10 @@ exports.plot = function plotGeo(gd) {
fullLayout
);
- fullLayout[geoId]._geo = geo;
+ fullLayout[geoId]._subplot = geo;
}
- geo.plot(fullGeoData, fullLayout, gd._promises);
+ geo.plot(geoCalcData, fullLayout, gd._promises);
}
};
@@ -71,7 +71,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
for(var i = 0; i < oldGeoKeys.length; i++) {
var oldGeoKey = oldGeoKeys[i];
- var oldGeo = oldFullLayout[oldGeoKey]._geo;
+ var oldGeo = oldFullLayout[oldGeoKey]._subplot;
if(!newFullLayout[oldGeoKey] && !!oldGeo) {
oldGeo.geoDiv.remove();
@@ -87,7 +87,7 @@ exports.toSVG = function(gd) {
for(var i = 0; i < geoIds.length; i++) {
var geoLayout = fullLayout[geoIds[i]],
domain = geoLayout.domain,
- geoFramework = geoLayout._geo.framework;
+ geoFramework = geoLayout._subplot.framework;
geoFramework.attr('style', null);
geoFramework
@@ -102,3 +102,16 @@ exports.toSVG = function(gd) {
.appendChild(geoFramework.node());
}
};
+
+function getSubplotCalcData(calcData, id) {
+ var subplotCalcData = [];
+
+ for(var i = 0; i < calcData.length; i++) {
+ var calcTrace = calcData[i],
+ trace = calcTrace[0].trace;
+
+ if(trace.geo === id) subplotCalcData.push(calcTrace);
+ }
+
+ return subplotCalcData;
+}
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 195981f6185..6adf9c92d7a 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -1669,10 +1669,16 @@ plots.doCalcdata = function(gd, traces) {
if(_module.calc) cd = _module.calc(gd, trace);
}
- // make sure there is a first point
- // this ensures there is a calcdata item for every trace,
- // even if cartesian logic doesn't handle it
- if(!Array.isArray(cd) || !cd[0]) cd = [{x: false, y: false}];
+ // Make sure there is a first point.
+ //
+ // This ensures there is a calcdata item for every trace,
+ // even if cartesian logic doesn't handle it (for things like legends).
+ //
+ // Tag this artificial calc point with 'placeholder: true',
+ // to make it easier to skip over them in during the plot and hover step.
+ if(!Array.isArray(cd) || !cd[0]) {
+ cd = [{x: false, y: false, placeholder: true}];
+ }
// add the trace-wide properties to the first point,
// per point properties to every point
diff --git a/src/traces/choropleth/index.js b/src/traces/choropleth/index.js
index e782d6d0144..31cb74393e7 100644
--- a/src/traces/choropleth/index.js
+++ b/src/traces/choropleth/index.js
@@ -17,6 +17,9 @@ Choropleth.colorbar = require('../heatmap/colorbar');
Choropleth.calc = require('./calc');
Choropleth.plot = require('./plot').plot;
+// add dummy hover handler to skip Fx.hover w/o warnings
+Choropleth.hoverPoints = function() {};
+
Choropleth.moduleType = 'trace';
Choropleth.name = 'choropleth';
Choropleth.basePlotModule = require('../../plots/geo');
diff --git a/src/traces/choropleth/plot.js b/src/traces/choropleth/plot.js
index 0393b55b15a..2fac6ea4e06 100644
--- a/src/traces/choropleth/plot.js
+++ b/src/traces/choropleth/plot.js
@@ -58,7 +58,10 @@ plotChoropleth.calcGeoJSON = function(trace, topojson) {
return cdi;
};
-plotChoropleth.plot = function(geo, choroplethData, geoLayout) {
+plotChoropleth.plot = function(geo, calcData, geoLayout) {
+
+ function keyFunc(d) { return d[0].trace.uid; }
+
var framework = geo.framework,
gChoropleth = framework.select('g.choroplethlayer'),
gBaseLayer = framework.select('g.baselayer'),
@@ -68,15 +71,16 @@ plotChoropleth.plot = function(geo, choroplethData, geoLayout) {
var gChoroplethTraces = gChoropleth
.selectAll('g.trace.choropleth')
- .data(choroplethData, function(trace) { return trace.uid; });
+ .data(calcData, keyFunc);
gChoroplethTraces.enter().append('g')
.attr('class', 'trace choropleth');
gChoroplethTraces.exit().remove();
- gChoroplethTraces.each(function(trace) {
- var cdi = plotChoropleth.calcGeoJSON(trace, geo.topojson),
+ gChoroplethTraces.each(function(calcTrace) {
+ var trace = calcTrace[0].trace,
+ cdi = plotChoropleth.calcGeoJSON(trace, geo.topojson),
cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace),
eventDataFunc = makeEventDataFunc(trace);
@@ -143,8 +147,9 @@ plotChoropleth.plot = function(geo, choroplethData, geoLayout) {
plotChoropleth.style = function(geo) {
geo.framework.selectAll('g.trace.choropleth')
- .each(function(trace) {
- var s = d3.select(this),
+ .each(function(calcTrace) {
+ var trace = calcTrace[0].trace,
+ s = d3.select(this),
marker = trace.marker || {},
markerLine = marker.line || {},
zmin = trace.zmin,
diff --git a/src/traces/scatter/fillcolor_defaults.js b/src/traces/scatter/fillcolor_defaults.js
index 9871c09cd3b..2c60cc56d7a 100644
--- a/src/traces/scatter/fillcolor_defaults.js
+++ b/src/traces/scatter/fillcolor_defaults.js
@@ -12,7 +12,6 @@
var Color = require('../../components/color');
-// common to 'scatter' and 'scattergl'
module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coerce) {
var inheritColorFromMarker = false;
diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js
index 7ee7155fc1c..d7f68c40234 100644
--- a/src/traces/scattergeo/attributes.js
+++ b/src/traces/scattergeo/attributes.js
@@ -27,6 +27,7 @@ module.exports = {
valType: 'data_array',
description: 'Sets the latitude coordinates (in degrees North).'
},
+
locations: {
valType: 'data_array',
description: [
@@ -45,7 +46,9 @@ module.exports = {
'to regions on the map.'
].join(' ')
},
+
mode: extendFlat({}, scatterAttrs.mode, {dflt: 'markers'}),
+
text: extendFlat({}, scatterAttrs.text, {
description: [
'Sets text elements associated with each (lon,lat) pair',
@@ -56,11 +59,16 @@ module.exports = {
'this trace\'s (lon,lat) or `locations` coordinates.'
].join(' ')
}),
+ textfont: scatterAttrs.textfont,
+ textposition: scatterAttrs.textposition,
+
line: {
color: scatterLineAttrs.color,
width: scatterLineAttrs.width,
dash: scatterLineAttrs.dash
},
+ connectgaps: scatterAttrs.connectgaps,
+
marker: extendFlat({}, {
symbol: scatterMarkerAttrs.symbol,
opacity: scatterMarkerAttrs.opacity,
@@ -76,11 +84,25 @@ module.exports = {
},
colorAttributes('marker')
),
- textfont: scatterAttrs.textfont,
- textposition: scatterAttrs.textposition,
+
+ fill: {
+ valType: 'enumerated',
+ values: ['none', 'toself'],
+ dflt: 'none',
+ role: 'style',
+ description: [
+ 'Sets the area to fill with a solid color.',
+ 'Use with `fillcolor` if not *none*.',
+ '*toself* connects the endpoints of the trace (or each segment',
+ 'of the trace if it has gaps) into a closed shape.'
+ ].join(' ')
+ },
+ fillcolor: scatterAttrs.fillcolor,
+
hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
flags: ['lon', 'lat', 'location', 'text', 'name']
}),
+
_nestedModules: {
'marker.colorbar': 'Colorbar'
}
diff --git a/src/traces/scattergeo/calc.js b/src/traces/scattergeo/calc.js
index bb338a66f3b..db64733155a 100644
--- a/src/traces/scattergeo/calc.js
+++ b/src/traces/scattergeo/calc.js
@@ -9,13 +9,47 @@
'use strict';
-var calcColorscale = require('../scatter/colorscale_calc');
+var isNumeric = require('fast-isnumeric');
+
+var calcMarkerColorscale = require('../scatter/colorscale_calc');
module.exports = function calc(gd, trace) {
- var cd = [{x: false, y: false, trace: trace, t: {}}];
+ var hasLocationData = Array.isArray(trace.locations),
+ len = hasLocationData ? trace.locations.length : trace.lon.length;
+
+ var calcTrace = [],
+ cnt = 0;
+
+ for(var i = 0; i < len; i++) {
+ var calcPt = {},
+ skip;
+
+ if(hasLocationData) {
+ var loc = trace.locations[i];
+
+ calcPt.loc = loc;
+ skip = (typeof loc !== 'string');
+ }
+ else {
+ var lon = trace.lon[i],
+ lat = trace.lat[i];
+
+ calcPt.lonlat = [+lon, +lat];
+ skip = (!isNumeric(lon) || !isNumeric(lat));
+ }
+
+ if(skip) {
+ if(cnt > 0) calcTrace[cnt - 1].gapAfter = true;
+ continue;
+ }
+
+ cnt++;
+
+ calcTrace.push(calcPt);
+ }
- calcColorscale(trace);
+ calcMarkerColorscale(trace);
- return cd;
+ return calcTrace;
};
diff --git a/src/traces/scattergeo/defaults.js b/src/traces/scattergeo/defaults.js
index 1990a000dcd..509b3466ba0 100644
--- a/src/traces/scattergeo/defaults.js
+++ b/src/traces/scattergeo/defaults.js
@@ -15,6 +15,7 @@ var subTypes = require('../scatter/subtypes');
var handleMarkerDefaults = require('../scatter/marker_defaults');
var handleLineDefaults = require('../scatter/line_defaults');
var handleTextDefaults = require('../scatter/text_defaults');
+var handleFillColorDefaults = require('../scatter/fillcolor_defaults');
var attributes = require('./attributes');
@@ -35,6 +36,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
if(subTypes.hasLines(traceOut)) {
handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce);
+ coerce('connectgaps');
}
if(subTypes.hasMarkers(traceOut)) {
@@ -45,6 +47,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleTextDefaults(traceIn, traceOut, layout, coerce);
}
+ coerce('fill');
+ if(traceOut.fill !== 'none') {
+ handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce);
+ }
+
coerce('hoverinfo', (layout._dataLength === 1) ? 'lon+lat+location+text' : undefined);
};
diff --git a/src/traces/scattergeo/event_data.js b/src/traces/scattergeo/event_data.js
new file mode 100644
index 00000000000..a3421f95739
--- /dev/null
+++ b/src/traces/scattergeo/event_data.js
@@ -0,0 +1,19 @@
+/**
+* Copyright 2012-2016, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+
+module.exports = function eventData(out, pt) {
+ out.lon = pt.lon;
+ out.lat = pt.lat;
+ out.location = pt.lon ? pt.lon : null;
+
+ return out;
+};
diff --git a/src/traces/scattergeo/hover.js b/src/traces/scattergeo/hover.js
new file mode 100644
index 00000000000..200717080b8
--- /dev/null
+++ b/src/traces/scattergeo/hover.js
@@ -0,0 +1,108 @@
+/**
+* Copyright 2012-2016, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+var Fx = require('../../plots/cartesian/graph_interact');
+var Axes = require('../../plots/cartesian/axes');
+
+var getTraceColor = require('../scatter/get_trace_color');
+var attributes = require('./attributes');
+
+
+module.exports = function hoverPoints(pointData) {
+ var cd = pointData.cd,
+ trace = cd[0].trace,
+ xa = pointData.xa,
+ ya = pointData.ya,
+ geo = pointData.subplot;
+
+ if(cd[0].placeholder) return;
+
+ function c2p(lonlat) {
+ return geo.projection(lonlat);
+ }
+
+ function distFn(d) {
+ var lonlat = d.lonlat;
+
+ // this handles the not-found location feature case
+ if(lonlat[0] === null || lonlat[1] === null) return Infinity;
+
+ if(geo.isLonLatOverEdges(lonlat)) return Infinity;
+
+ var pos = c2p(lonlat);
+
+ var xPx = xa.c2p(),
+ yPx = ya.c2p();
+
+ var dx = Math.abs(xPx - pos[0]),
+ dy = Math.abs(yPx - pos[1]),
+ rad = Math.max(3, d.mrc || 0);
+
+ // N.B. d.mrc is the calculated marker radius
+ // which is only set for trace with 'markers' mode.
+
+ return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad);
+ }
+
+ Fx.getClosest(cd, distFn, pointData);
+
+ // skip the rest (for this trace) if we didn't find a close point
+ if(pointData.index === false) return;
+
+ var di = cd[pointData.index],
+ lonlat = di.lonlat,
+ pos = c2p(lonlat),
+ rad = di.mrc || 1;
+
+ pointData.x0 = pos[0] - rad;
+ pointData.x1 = pos[0] + rad;
+ pointData.y0 = pos[1] - rad;
+ pointData.y1 = pos[1] + rad;
+
+ pointData.loc = di.loc;
+ pointData.lat = lonlat[0];
+ pointData.lon = lonlat[1];
+
+ pointData.color = getTraceColor(trace, di);
+ pointData.extraText = getExtraText(trace, di, geo.mockAxis);
+
+ return [pointData];
+};
+
+function getExtraText(trace, pt, axis) {
+ var hoverinfo = trace.hoverinfo;
+
+ var parts = (hoverinfo === 'all') ?
+ attributes.hoverinfo.flags :
+ hoverinfo.split('+');
+
+ var hasLocation = parts.indexOf('location') !== -1 && Array.isArray(trace.locations),
+ hasLon = (parts.indexOf('lon') !== -1),
+ hasLat = (parts.indexOf('lat') !== -1),
+ hasText = (parts.indexOf('text') !== -1);
+
+ var text = [];
+
+ function format(val) {
+ return Axes.tickText(axis, axis.c2l(val), 'hover').text + '\u00B0';
+ }
+
+ if(hasLocation) text.push(pt.loc);
+ else if(hasLon && hasLat) {
+ text.push('(' + format(pt.lonlat[0]) + ', ' + format(pt.lonlat[1]) + ')');
+ }
+ else if(hasLon) text.push('lon: ' + format(pt.lonlat[0]));
+ else if(hasLat) text.push('lat: ' + format(pt.lonlat[1]));
+
+ if(hasText) text.push(pt.tx || trace.text);
+
+ return text.join('
');
+}
diff --git a/src/traces/scattergeo/index.js b/src/traces/scattergeo/index.js
index ef515a892f3..b8c441f6d1d 100644
--- a/src/traces/scattergeo/index.js
+++ b/src/traces/scattergeo/index.js
@@ -15,7 +15,9 @@ ScatterGeo.attributes = require('./attributes');
ScatterGeo.supplyDefaults = require('./defaults');
ScatterGeo.colorbar = require('../scatter/colorbar');
ScatterGeo.calc = require('./calc');
-ScatterGeo.plot = require('./plot').plot;
+ScatterGeo.plot = require('./plot');
+ScatterGeo.hoverPoints = require('./hover');
+ScatterGeo.eventData = require('./event_data');
ScatterGeo.moduleType = 'trace';
ScatterGeo.name = 'scattergeo';
diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js
index 921dce93b84..32055866a44 100644
--- a/src/traces/scattergeo/plot.js
+++ b/src/traces/scattergeo/plot.js
@@ -11,69 +11,104 @@
var d3 = require('d3');
-var Fx = require('../../plots/cartesian/graph_interact');
-var Axes = require('../../plots/cartesian/axes');
+var Drawing = require('../../components/drawing');
+var Color = require('../../components/color');
+var Lib = require('../../lib');
var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures;
var locationToFeature = require('../../lib/geo_location_utils').locationToFeature;
+var geoJsonUtils = require('../../lib/geojson_utils');
var arrayToCalcItem = require('../../lib/array_to_calc_item');
-
-var Color = require('../../components/color');
-var Drawing = require('../../components/drawing');
var subTypes = require('../scatter/subtypes');
-var attributes = require('./attributes');
-var plotScatterGeo = module.exports = {};
+module.exports = function plot(geo, calcData) {
+ function keyFunc(d) { return d[0].trace.uid; }
-plotScatterGeo.calcGeoJSON = function(trace, topojson) {
- var cdi = [],
- hasLocationData = Array.isArray(trace.locations);
+ var gScatterGeoTraces = geo.framework.select('.scattergeolayer')
+ .selectAll('g.trace.scattergeo')
+ .data(calcData, keyFunc);
- var len, features, getLonLat, locations;
+ gScatterGeoTraces.enter().append('g')
+ .attr('class', 'trace scattergeo');
- if(hasLocationData) {
- locations = trace.locations;
- len = locations.length;
- features = getTopojsonFeatures(trace, topojson);
- getLonLat = function(trace, i) {
- var feature = locationToFeature(trace.locationmode, locations[i], features);
+ gScatterGeoTraces.exit().remove();
- return (feature !== undefined) ?
- feature.properties.ct :
- undefined;
- };
- }
- else {
- len = trace.lon.length;
- getLonLat = function(trace, i) {
- return [trace.lon[i], trace.lat[i]];
- };
- }
+ // TODO find a way to order the inner nodes on update
+ gScatterGeoTraces.selectAll('*').remove();
- for(var i = 0; i < len; i++) {
- var lonlat = getLonLat(trace, i);
+ gScatterGeoTraces.each(function(calcTrace) {
+ var s = d3.select(this),
+ trace = calcTrace[0].trace,
+ convertToLonLatFn = makeConvertToLonLatFn(trace, geo.topojson);
- if(!lonlat) continue; // filter the blank points here
+ // skip over placeholder traces
+ if(calcTrace[0].placeholder) s.remove();
- var calcItem = {
- lon: lonlat[0],
- lat: lonlat[1],
- location: hasLocationData ? trace.locations[i] : null
- };
+ // just like calcTrace but w/o not-found location datum
+ var _calcTrace = [];
- arrayItemToCalcdata(trace, calcItem, i);
+ for(var i = 0; i < calcTrace.length; i++) {
+ var _calcPt = convertToLonLatFn(calcTrace[i]);
- cdi.push(calcItem);
- }
+ if(_calcPt) {
+ arrayItemToCalcdata(trace, calcTrace[i], i);
+ _calcTrace.push(_calcPt);
+ }
+ }
+
+ if(subTypes.hasLines(trace) || trace.fill !== 'none') {
+ var lineCoords = geoJsonUtils.calcTraceToLineCoords(_calcTrace);
+
+ var lineData = (trace.fill !== 'none') ?
+ geoJsonUtils.makePolygon(lineCoords, trace) :
+ geoJsonUtils.makeLine(lineCoords, trace);
+
+ s.selectAll('path.js-line')
+ .data([lineData])
+ .enter().append('path')
+ .classed('js-line', true);
+ }
+
+ if(subTypes.hasMarkers(trace)) {
+ s.selectAll('path.point').data(_calcTrace)
+ .enter().append('path')
+ .classed('point', true);
+ }
- if(cdi.length > 0) cdi[0].trace = trace;
+ if(subTypes.hasText(trace)) {
+ s.selectAll('g').data(_calcTrace)
+ .enter().append('g')
+ .append('text');
+ }
+ });
- return cdi;
+ // call style here within topojson request callback
+ style(geo);
};
-// similar Scatter.arraysToCalcdata but inside a filter loop
+function makeConvertToLonLatFn(trace, topojson) {
+ if(!Array.isArray(trace.locations)) return Lib.identity;
+
+ var features = getTopojsonFeatures(trace, topojson),
+ locationmode = trace.locationmode;
+
+ return function(calcPt) {
+ var feature = locationToFeature(locationmode, calcPt.loc, features);
+
+ if(feature) {
+ calcPt.lonlat = feature.properties.ct;
+ return calcPt;
+ }
+ else {
+ // mutate gd.calcdata so that hoverPoints knows to skip this datum
+ calcPt.lonlat = [null, null];
+ return false;
+ }
+ };
+}
+
function arrayItemToCalcdata(trace, calcItem, i) {
var marker = trace.marker;
@@ -100,199 +135,36 @@ function arrayItemToCalcdata(trace, calcItem, i) {
}
}
-function makeLineGeoJSON(trace) {
- var N = trace.lon.length,
- coordinates = new Array(N);
-
- for(var i = 0; i < N; i++) {
- coordinates[i] = [trace.lon[i], trace.lat[i]];
- }
-
- return {
- type: 'LineString',
- coordinates: coordinates,
- trace: trace
- };
-}
-
-plotScatterGeo.plot = function(geo, scattergeoData) {
- var gScatterGeoTraces = geo.framework.select('.scattergeolayer')
- .selectAll('g.trace.scattergeo')
- .data(scattergeoData, function(trace) { return trace.uid; });
-
- gScatterGeoTraces.enter().append('g')
- .attr('class', 'trace scattergeo');
-
- gScatterGeoTraces.exit().remove();
-
- // TODO find a way to order the inner nodes on update
- gScatterGeoTraces.selectAll('*').remove();
-
- gScatterGeoTraces.each(function(trace) {
- var s = d3.select(this);
-
- if(!subTypes.hasLines(trace)) return;
-
- s.selectAll('path.js-line')
- .data([makeLineGeoJSON(trace)])
- .enter().append('path')
- .classed('js-line', true);
-
- // TODO add hover - how?
- });
-
- gScatterGeoTraces.each(function(trace) {
- var s = d3.select(this),
- showMarkers = subTypes.hasMarkers(trace),
- showText = subTypes.hasText(trace);
-
- if(!showMarkers && !showText) return;
-
- var cdi = plotScatterGeo.calcGeoJSON(trace, geo.topojson),
- cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace),
- eventDataFunc = makeEventDataFunc(trace);
-
- var hoverinfo = trace.hoverinfo,
- hasNameLabel = (
- hoverinfo === 'all' ||
- hoverinfo.indexOf('name') !== -1
- );
-
- // keep ref to event data in this scope for plotly_unhover
- var eventData = null;
-
- function handleMouseOver(pt, ptIndex) {
- if(!geo.showHover) return;
-
- var xy = geo.projection([pt.lon, pt.lat]);
- cleanHoverLabelsFunc(pt);
-
- Fx.loneHover({
- x: xy[0],
- y: xy[1],
- name: hasNameLabel ? trace.name : undefined,
- text: pt.textLabel,
- color: pt.mc || (trace.marker || {}).color
- }, {
- container: geo.hoverContainer.node()
- });
-
- eventData = eventDataFunc(pt, ptIndex);
-
- geo.graphDiv.emit('plotly_hover', eventData);
- }
-
- function handleClick(pt, ptIndex) {
- geo.graphDiv.emit('plotly_click', eventDataFunc(pt, ptIndex));
- }
-
- if(showMarkers) {
- s.selectAll('path.point').data(cdi)
- .enter().append('path')
- .classed('point', true)
- .on('mouseover', handleMouseOver)
- .on('click', handleClick)
- .on('mouseout', function() {
- Fx.loneUnhover(geo.hoverContainer);
-
- geo.graphDiv.emit('plotly_unhover', eventData);
- })
- .on('mousedown', function() {
- // to simulate the 'zoomon' event
- Fx.loneUnhover(geo.hoverContainer);
- })
- .on('mouseup', handleMouseOver); // ~ 'zoomend'
- }
-
- if(showText) {
- s.selectAll('g').data(cdi)
- .enter().append('g')
- .append('text');
- }
- });
-
- plotScatterGeo.style(geo);
-};
-
-plotScatterGeo.style = function(geo) {
+function style(geo) {
var selection = geo.framework.selectAll('g.trace.scattergeo');
- selection.style('opacity', function(trace) {
- return trace.opacity;
+ selection.style('opacity', function(calcTrace) {
+ return calcTrace[0].trace.opacity;
});
- selection.each(function(trace) {
- d3.select(this).selectAll('path.point')
+ selection.each(function(calcTrace) {
+ var trace = calcTrace[0].trace,
+ group = d3.select(this);
+
+ group.selectAll('path.point')
.call(Drawing.pointStyle, trace);
- d3.select(this).selectAll('text')
+ group.selectAll('text')
.call(Drawing.textPointStyle, trace);
});
- // GeoJSON calc data is incompatible with Drawing.lineGroupStyle
+ // this part is incompatible with Drawing.lineGroupStyle
selection.selectAll('path.js-line')
.style('fill', 'none')
.each(function(d) {
- var trace = d.trace,
+ var path = d3.select(this),
+ trace = d.trace,
line = trace.line || {};
- d3.select(this)
- .call(Color.stroke, line.color)
+ path.call(Color.stroke, line.color)
.call(Drawing.dashLine, line.dash || '', line.width || 0);
- });
-};
-
-function makeCleanHoverLabelsFunc(geo, trace) {
- var hoverinfo = trace.hoverinfo;
- if(hoverinfo === 'none') {
- return function cleanHoverLabelsFunc(d) { delete d.textLabel; };
- }
-
- var hoverinfoParts = (hoverinfo === 'all') ?
- attributes.hoverinfo.flags :
- hoverinfo.split('+');
-
- var hasLocation = (
- hoverinfoParts.indexOf('location') !== -1 &&
- Array.isArray(trace.locations)
- ),
- hasLon = (hoverinfoParts.indexOf('lon') !== -1),
- hasLat = (hoverinfoParts.indexOf('lat') !== -1),
- hasText = (hoverinfoParts.indexOf('text') !== -1);
-
- function formatter(val) {
- var axis = geo.mockAxis;
- return Axes.tickText(axis, axis.c2l(val), 'hover').text + '\u00B0';
- }
-
- return function cleanHoverLabelsFunc(pt) {
- var thisText = [];
-
- if(hasLocation) thisText.push(pt.location);
- else if(hasLon && hasLat) {
- thisText.push('(' + formatter(pt.lon) + ', ' + formatter(pt.lat) + ')');
- }
- else if(hasLon) thisText.push('lon: ' + formatter(pt.lon));
- else if(hasLat) thisText.push('lat: ' + formatter(pt.lat));
-
- if(hasText) thisText.push(pt.tx || trace.text);
-
- pt.textLabel = thisText.join('
');
- };
-}
-
-function makeEventDataFunc(trace) {
- var hasLocation = Array.isArray(trace.locations);
-
- return function(pt, ptIndex) {
- return {points: [{
- data: trace._input,
- fullData: trace,
- curveNumber: trace.index,
- pointNumber: ptIndex,
- lon: pt.lon,
- lat: pt.lat,
- location: hasLocation ? pt.location : null
- }]};
- };
+ if(trace.fill !== 'none') {
+ path.call(Color.fill, trace.fillcolor);
+ }
+ });
}
diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js
index c66c259e809..a4fdd03e1de 100644
--- a/src/traces/scattermapbox/attributes.js
+++ b/src/traces/scattermapbox/attributes.js
@@ -91,18 +91,7 @@ module.exports = {
// line
},
- fill: {
- valType: 'enumerated',
- values: ['none', 'toself'],
- dflt: 'none',
- role: 'style',
- description: [
- 'Sets the area to fill with a solid color.',
- 'Use with `fillcolor` if not *none*.',
- '*toself* connects the endpoints of the trace (or each segment',
- 'of the trace if it has gaps) into a closed shape.'
- ].join(' ')
- },
+ fill: scatterGeoAttrs.fill,
fillcolor: scatterAttrs.fillcolor,
textfont: mapboxAttrs.layers.symbol.textfont,
diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js
index 8928d081fd9..2843c37958f 100644
--- a/src/traces/scattermapbox/convert.js
+++ b/src/traces/scattermapbox/convert.js
@@ -10,6 +10,8 @@
'use strict';
var Lib = require('../../lib');
+var geoJsonUtils = require('../../lib/geojson_utils');
+
var subTypes = require('../scatter/subtypes');
var convertTextOpts = require('../../plots/mapbox/convert_text_opts');
@@ -40,17 +42,17 @@ module.exports = function convert(calcTrace) {
symbol: symbol
};
- // early return is not visible
- if(!isVisible) return opts;
+ // early return if not visible or placeholder
+ if(!isVisible || calcTrace[0].placeholder) return opts;
// fill layer and line layer use the same coords
var coords;
if(hasFill || hasLines) {
- coords = getCoords(calcTrace);
+ coords = geoJsonUtils.calcTraceToLineCoords(calcTrace);
}
if(hasFill) {
- fill.geojson = makeFillGeoJSON(calcTrace, coords);
+ fill.geojson = geoJsonUtils.makePolygon(coords);
fill.layout.visibility = 'visible';
Lib.extendFlat(fill.paint, {
@@ -59,7 +61,7 @@ module.exports = function convert(calcTrace) {
}
if(hasLines) {
- line.geojson = makeLineGeoJSON(calcTrace, coords);
+ line.geojson = geoJsonUtils.makeLine(coords);
line.layout.visibility = 'visible';
Lib.extendFlat(line.paint, {
@@ -133,45 +135,12 @@ module.exports = function convert(calcTrace) {
function initContainer() {
return {
- geojson: makeBlankGeoJSON(),
+ geojson: geoJsonUtils.makeBlank(),
layout: { visibility: 'none' },
paint: {}
};
}
-function makeBlankGeoJSON() {
- return {
- type: 'Point',
- coordinates: []
- };
-}
-
-function makeFillGeoJSON(_, coords) {
- if(coords.length === 1) {
- return {
- type: 'Polygon',
- coordinates: coords
- };
- }
-
- var _coords = new Array(coords.length);
- for(var i = 0; i < coords.length; i++) {
- _coords[i] = [coords[i]];
- }
-
- return {
- type: 'MultiPolygon',
- coordinates: _coords
- };
-}
-
-function makeLineGeoJSON(_, coords) {
- return {
- type: 'MultiLineString',
- coordinates: coords
- };
-}
-
// N.B. `hash` is mutated here
//
// The `hash` object contains mapping between values
@@ -323,29 +292,6 @@ function calcCircleRadius(trace, hash) {
return out;
}
-function getCoords(calcTrace) {
- var trace = calcTrace[0].trace,
- connectgaps = trace.connectgaps;
-
- var coords = [],
- lineString = [];
-
- for(var i = 0; i < calcTrace.length; i++) {
- var calcPt = calcTrace[i];
-
- lineString.push(calcPt.lonlat);
-
- if(!connectgaps && calcPt.gapAfter && lineString.length > 0) {
- coords.push(lineString);
- lineString = [];
- }
- }
-
- coords.push(lineString);
-
- return coords;
-}
-
function getFillFunc(attr) {
if(Array.isArray(attr)) {
return function(v) { return v; };
diff --git a/src/traces/scattermapbox/event_data.js b/src/traces/scattermapbox/event_data.js
new file mode 100644
index 00000000000..1a0b379d548
--- /dev/null
+++ b/src/traces/scattermapbox/event_data.js
@@ -0,0 +1,18 @@
+/**
+* Copyright 2012-2016, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+
+module.exports = function eventData(out, pt) {
+ out.lon = pt.lon;
+ out.lat = pt.lat;
+
+ return out;
+};
diff --git a/src/traces/scattermapbox/hover.js b/src/traces/scattermapbox/hover.js
index 9bb9ea630c1..79af40ae66a 100644
--- a/src/traces/scattermapbox/hover.js
+++ b/src/traces/scattermapbox/hover.js
@@ -19,6 +19,8 @@ module.exports = function hoverPoints(pointData, xval, yval) {
xa = pointData.xa,
ya = pointData.ya;
+ if(cd[0].placeholder) return;
+
// compute winding number about [-180, 180] globe
var winding = (xval >= 0) ?
Math.floor((xval + 180) / 360) :
diff --git a/src/traces/scattermapbox/index.js b/src/traces/scattermapbox/index.js
index bb0a5af7c2b..e07c574be1c 100644
--- a/src/traces/scattermapbox/index.js
+++ b/src/traces/scattermapbox/index.js
@@ -16,6 +16,7 @@ ScatterMapbox.supplyDefaults = require('./defaults');
ScatterMapbox.colorbar = require('../scatter/colorbar');
ScatterMapbox.calc = require('./calc');
ScatterMapbox.hoverPoints = require('./hover');
+ScatterMapbox.eventData = require('./event_data');
ScatterMapbox.plot = require('./plot');
ScatterMapbox.moduleType = 'trace';
diff --git a/test/image/baselines/geo_connectgaps.png b/test/image/baselines/geo_connectgaps.png
new file mode 100644
index 00000000000..a33c521dcdd
Binary files /dev/null and b/test/image/baselines/geo_connectgaps.png differ
diff --git a/test/image/baselines/geo_fill.png b/test/image/baselines/geo_fill.png
new file mode 100644
index 00000000000..c3941d6d1af
Binary files /dev/null and b/test/image/baselines/geo_fill.png differ
diff --git a/test/image/baselines/geo_legendonly.png b/test/image/baselines/geo_legendonly.png
index 0d74a4a40ba..658fe60eb7d 100644
Binary files a/test/image/baselines/geo_legendonly.png and b/test/image/baselines/geo_legendonly.png differ
diff --git a/test/image/mocks/geo_connectgaps.json b/test/image/mocks/geo_connectgaps.json
new file mode 100644
index 00000000000..6e283068e7c
--- /dev/null
+++ b/test/image/mocks/geo_connectgaps.json
@@ -0,0 +1,59 @@
+{
+ "data": [
+ {
+ "type": "scattergeo",
+ "mode": "lines",
+ "lon": [
+ -50,
+ -50,
+ null,
+ "50",
+ "50"
+ ],
+ "lat": [
+ 10,
+ 50,
+ null,
+ "10",
+ "50"
+ ],
+ "line": {
+ "width": 2
+ },
+ "name": "connectgaps false"
+ },
+ {
+ "type": "scattergeo",
+ "mode": "lines",
+ "lon": [
+ -50,
+ -50,
+ null,
+ "50",
+ "50"
+ ],
+ "lat": [
+ -10,
+ -50,
+ null,
+ "-10",
+ "-50"
+ ],
+ "line": {
+ "width": 5
+ },
+ "connectgaps": true,
+ "name": "connectgaps true"
+ }
+ ],
+ "layout": {
+ "geo": {
+ "projection": {
+ "type": "eckert4"
+ }
+ },
+ "height": 450,
+ "width": 1100,
+ "autosize": true
+ }
+}
diff --git a/test/image/mocks/geo_fill.json b/test/image/mocks/geo_fill.json
new file mode 100644
index 00000000000..12d749caca7
--- /dev/null
+++ b/test/image/mocks/geo_fill.json
@@ -0,0 +1,109 @@
+{
+ "data": [
+ {
+ "type": "scattergeo",
+ "mode": "lines",
+ "fill": "toself",
+ "lon": [
+ -67.13734351262877,
+ -66.96466,
+ -68.03252,
+ -69.06,
+ -70.11617,
+ -70.64573401557249,
+ -70.75102474636725,
+ -70.79761105007827,
+ -70.98176001655037,
+ -70.94416541205806,
+ -71.08482,
+ -70.6600225491012,
+ -70.30495378282376,
+ -70.00014034695016,
+ -69.23708614772835,
+ -68.90478084987546,
+ -68.23430497910454,
+ -67.79035274928509,
+ -67.79141211614706,
+ -67.13734351262877,
+ null,
+ -76,
+ -76,
+ -74,
+ -74,
+ -76
+ ],
+ "lat": [
+ 45.137451890638886,
+ 44.8097,
+ 44.3252,
+ 43.98,
+ 43.68405,
+ 43.090083319667144,
+ 43.08003225358635,
+ 43.21973948828747,
+ 43.36789581966826,
+ 43.46633942318431,
+ 45.3052400000002,
+ 45.46022288673396,
+ 45.914794623389355,
+ 46.69317088478567,
+ 47.44777598732787,
+ 47.184794623394396,
+ 47.35462921812177,
+ 47.066248887716995,
+ 45.702585354182816,
+ 45.137451890638886,
+ null,
+ 44,
+ 46,
+ 46,
+ 44,
+ 44
+ ],
+ "line": {
+ "width": 6,
+ "color": "#756bb1"
+ },
+ "fillcolor": "#d3d3d3"
+ },
+ {
+ "type": "scattergeo",
+ "fill": "toself",
+ "lon": [
+ -75,
+ -77,
+ -77,
+ -75,
+ -75
+ ],
+ "lat": [
+ 47,
+ 47,
+ 49,
+ 49,
+ 47
+ ],
+ "marker": {
+ "size": 20
+ }
+ }
+ ],
+ "layout": {
+ "geo": {
+ "projection": {
+ "type": "natural earth"
+ },
+ "lonaxis": {
+ "range": [-80, -65]
+ },
+ "lataxis": {
+ "range": [42, 51]
+ },
+ "showland": true
+ },
+ "showlegend": false,
+ "height": 450,
+ "width": 1100,
+ "autosize": true
+ }
+}
diff --git a/test/jasmine/tests/geo_interact_test.js b/test/jasmine/tests/geo_interact_test.js
index 153ec79e9fe..1a709a1ce4f 100644
--- a/test/jasmine/tests/geo_interact_test.js
+++ b/test/jasmine/tests/geo_interact_test.js
@@ -7,6 +7,8 @@ var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var mouseEvent = require('../assets/mouse_event');
+var HOVERMINTIME = require('@src/plots/cartesian/constants').HOVERMINTIME;
+
describe('Test geo interactions', function() {
'use strict';
@@ -47,7 +49,7 @@ describe('Test geo interactions', function() {
describe('scattergeo hover labels', function() {
beforeEach(function() {
- mouseEventScatterGeo('mouseover');
+ mouseEventScatterGeo('mousemove');
});
it('should show one hover text group', function() {
@@ -68,14 +70,17 @@ describe('Test geo interactions', function() {
});
describe('scattergeo hover events', function() {
- var ptData;
+ var ptData, cnt;
beforeEach(function() {
+ cnt = 0;
+
gd.on('plotly_hover', function(eventData) {
ptData = eventData.points[0];
+ cnt++;
});
- mouseEventScatterGeo('mouseover');
+ mouseEventScatterGeo('mousemove');
});
it('should contain the correct fields', function() {
@@ -83,6 +88,7 @@ describe('Test geo interactions', function() {
'data', 'fullData', 'curveNumber', 'pointNumber',
'lon', 'lat', 'location'
]);
+ expect(cnt).toEqual(1);
});
it('should show the correct point data', function() {
@@ -91,6 +97,39 @@ describe('Test geo interactions', function() {
expect(ptData.location).toBe(null);
expect(ptData.curveNumber).toEqual(0);
expect(ptData.pointNumber).toEqual(0);
+ expect(cnt).toEqual(1);
+ });
+
+ it('should not be triggered when pt over on the other side of the globe', function(done) {
+ var update = {
+ 'geo.projection.type': 'orthographic',
+ 'geo.projection.rotation': { lon: 82, lat: -19 }
+ };
+
+ Plotly.relayout(gd, update).then(function() {
+ setTimeout(function() {
+ mouseEvent('mousemove', 288, 170);
+
+ expect(cnt).toEqual(1);
+
+ done();
+ }, HOVERMINTIME + 10);
+ });
+ });
+
+ it('should not be triggered when pt *location* does not have matching feature', function(done) {
+ var update = {
+ 'locations': [['CAN', 'AAA', 'USA']]
+ };
+
+ Plotly.restyle(gd, update).then(function() {
+ setTimeout(function() { mouseEvent('mousemove', 300, 230);
+
+ expect(cnt).toEqual(1);
+
+ done();
+ }, HOVERMINTIME + 10);
+ });
});
});
@@ -102,6 +141,7 @@ describe('Test geo interactions', function() {
ptData = eventData.points[0];
});
+ mouseEventScatterGeo('mousemove');
mouseEventScatterGeo('click');
});
@@ -124,13 +164,16 @@ describe('Test geo interactions', function() {
describe('scattergeo unhover events', function() {
var ptData;
- beforeEach(function() {
+ beforeEach(function(done) {
gd.on('plotly_unhover', function(eventData) {
ptData = eventData.points[0];
});
- mouseEventScatterGeo('mouseover');
- mouseEventScatterGeo('mouseout');
+ mouseEventScatterGeo('mousemove');
+ setTimeout(function() {
+ mouseEvent('mousemove', 400, 200);
+ done();
+ }, HOVERMINTIME + 10);
});
it('should contain the correct fields', function() {
@@ -419,6 +462,7 @@ describe('Test geo interactions', function() {
done();
}
+ gd.calcdata = undefined;
Plotly.plot(gd);
i++;
}, INTERVAL);
@@ -449,6 +493,7 @@ describe('Test geo interactions', function() {
done();
}
+ gd.calcdata = undefined;
Plotly.plot(gd);
i++;
}, INTERVAL);
@@ -479,6 +524,7 @@ describe('Test geo interactions', function() {
done();
}
+ gd.calcdata = undefined;
Plotly.plot(gd);
i++;
}, INTERVAL);
@@ -493,6 +539,7 @@ describe('Test geo interactions', function() {
var trace1 = gd.data[1];
trace1.locations.shift();
+ gd.calcdata = undefined;
Plotly.plot(gd).then(function() {
expect(countTraces('scattergeo')).toBe(1);
expect(countTraces('choropleth')).toBe(1);
@@ -519,6 +566,7 @@ describe('Test geo interactions', function() {
trace1.locations = locationsQueue;
trace1.z = zQueue;
+ gd.calcdata = undefined;
Plotly.plot(gd).then(function() {
expect(countTraces('scattergeo')).toBe(1);
expect(countTraces('choropleth')).toBe(1);
diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js
index 867c99c6e1c..789ae984efb 100644
--- a/test/jasmine/tests/mapbox_test.js
+++ b/test/jasmine/tests/mapbox_test.js
@@ -695,8 +695,7 @@ describe('mapbox plots', function() {
return _mouseEvent('mousemove', pointPos, function() {
expect(hoverData).not.toBe(undefined, 'firing on data points');
expect(Object.keys(hoverData)).toEqual([
- 'data', 'fullData', 'curveNumber', 'pointNumber',
- 'x', 'y', 'xaxis', 'yaxis'
+ 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat'
], 'returning the correct event data keys');
expect(hoverData.curveNumber).toEqual(0, 'returning the correct curve number');
expect(hoverData.pointNumber).toEqual(0, 'returning the correct point number');
@@ -706,8 +705,7 @@ describe('mapbox plots', function() {
return _mouseEvent('mousemove', blankPos, function() {
expect(unhoverData).not.toBe(undefined, 'firing on data points');
expect(Object.keys(unhoverData)).toEqual([
- 'data', 'fullData', 'curveNumber', 'pointNumber',
- 'x', 'y', 'xaxis', 'yaxis'
+ 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat'
], 'returning the correct event data keys');
expect(unhoverData.curveNumber).toEqual(0, 'returning the correct curve number');
expect(unhoverData.pointNumber).toEqual(0, 'returning the correct point number');
@@ -804,8 +802,7 @@ describe('mapbox plots', function() {
return _click(pointPos, function() {
expect(ptData).not.toBe(undefined, 'firing on data points');
expect(Object.keys(ptData)).toEqual([
- 'data', 'fullData', 'curveNumber', 'pointNumber',
- 'x', 'y', 'xaxis', 'yaxis'
+ 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat'
], 'returning the correct event data keys');
expect(ptData.curveNumber).toEqual(0, 'returning the correct curve number');
expect(ptData.pointNumber).toEqual(0, 'returning the correct point number');
diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js
index 80787a43e5b..80fede37ff6 100644
--- a/test/jasmine/tests/scattermapbox_test.js
+++ b/test/jasmine/tests/scattermapbox_test.js
@@ -244,13 +244,12 @@ describe('scattermapbox convert', function() {
function _convert(trace) {
var gd = { data: [trace] };
-
Plots.supplyDefaults(gd);
var fullTrace = gd._fullData[0];
- var calcTrace = ScatterMapbox.calc(gd, fullTrace);
- calcTrace[0].trace = fullTrace;
+ Plots.doCalcdata(gd, fullTrace);
+ var calcTrace = gd.calcdata[0];
return convert(calcTrace);
}
@@ -353,9 +352,9 @@ describe('scattermapbox convert', function() {
assertVisibility(opts, ['none', 'visible', 'none', 'visible']);
- var lineCoords = [[
+ var lineCoords = [
[10, 20], [20, 20], [30, 10], [20, 10], [10, 20]
- ]];
+ ];
expect(opts.line.geojson.coordinates).toEqual(lineCoords, 'have correct line coords');
@@ -413,6 +412,20 @@ describe('scattermapbox convert', function() {
expect(radii).toBeCloseToArray([0, 1, 0], 'link features to correct stops');
});
+ it('for input only blank pts', function() {
+ var opts = _convert(Lib.extendFlat({}, base, {
+ mode: 'lines',
+ lon: ['', null],
+ lat: [null, ''],
+ fill: 'toself'
+ }));
+
+ assertVisibility(opts, ['none', 'none', 'none', 'none']);
+
+ expect(opts.line.geojson.coordinates).toEqual([], 'have correct line coords');
+ expect(opts.fill.geojson.coordinates).toEqual([], 'have correct fill coords');
+ });
+
function assertVisibility(opts, expectations) {
var actual = ['fill', 'line', 'circle', 'symbol'].map(function(l) {
return opts[l].layout.visibility;