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;