diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 2f08fafcbfb..2472ab9956e 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -33,6 +33,7 @@ var topojsonFeature = require('topojson').feature; function Geo(options, fullLayout) { this.id = options.id; + this.graphDiv = options.graphDiv; this.container = options.container; this.topojsonURL = options.topojsonURL; @@ -40,7 +41,7 @@ function Geo(options, fullLayout) { // a subset of https://github.com/d3/d3-geo-projection addProjectionsToD3(); - this.showHover = fullLayout.hovermode==='closest'; + this.showHover = (fullLayout.hovermode === 'closest'); this.hoverContainer = null; this.topojsonName = null; diff --git a/src/plots/geo/index.js b/src/plots/geo/index.js index 772db2ed0c1..cd4aabd81ef 100644 --- a/src/plots/geo/index.js +++ b/src/plots/geo/index.js @@ -52,6 +52,7 @@ exports.plot = function plotGeo(gd) { if(geo === undefined) { geo = new Geo({ id: geoId, + graphDiv: gd, container: fullLayout._geocontainer.node(), topojsonURL: gd._context.topojsonURL }, diff --git a/src/traces/choropleth/plot.js b/src/traces/choropleth/plot.js index db29ecb0444..5f5573485d1 100644 --- a/src/traces/choropleth/plot.js +++ b/src/traces/choropleth/plot.js @@ -59,46 +59,49 @@ plotChoropleth.calcGeoJSON = function(trace, topojson) { plotChoropleth.plot = function(geo, choroplethData, geoLayout) { var framework = geo.framework, - topojson = geo.topojson, gChoropleth = framework.select('g.choroplethlayer'), gBaseLayer = framework.select('g.baselayer'), gBaseLayerOverChoropleth = framework.select('g.baselayeroverchoropleth'), baseLayersOverChoropleth = constants.baseLayersOverChoropleth, layerName; - // TODO move to more d3-idiomatic pattern (that's work on replot) - // N.B. html('') does not work in IE11 - gChoropleth.selectAll('*').remove(); - gBaseLayerOverChoropleth.selectAll('*').remove(); - var gChoroplethTraces = gChoropleth - .selectAll('g.trace.scatter') + .selectAll('g.trace.choropleth') .data(choroplethData); gChoroplethTraces.enter().append('g') - .attr('class', 'trace choropleth'); + .attr('class', 'trace choropleth'); + + gChoroplethTraces.exit().remove(); gChoroplethTraces .each(function(trace) { if(trace.visible !== true) return; - var cdi = plotChoropleth.calcGeoJSON(trace, topojson), - cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace); + var cdi = plotChoropleth.calcGeoJSON(trace, geo.topojson), + cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace), + eventDataFunc = makeEventDataFunc(trace); - function handleMouseOver(d) { + function handleMouseOver(pt, ptIndex) { if(!geo.showHover) return; - var xy = geo.projection(d.properties.ct); - cleanHoverLabelsFunc(d); + var xy = geo.projection(pt.properties.ct); + cleanHoverLabelsFunc(pt); Plotly.Fx.loneHover({ x: xy[0], y: xy[1], - name: d.nameLabel, - text: d.textLabel + name: pt.nameLabel, + text: pt.textLabel }, { container: geo.hoverContainer.node() }); + + geo.graphDiv.emit('plotly_hover', eventDataFunc(pt, ptIndex)); + } + + function handleClick(pt, ptIndex) { + geo.graphDiv.emit('plotly_click', eventDataFunc(pt, ptIndex)); } d3.select(this) @@ -107,6 +110,7 @@ plotChoropleth.plot = function(geo, choroplethData, geoLayout) { .enter().append('path') .attr('class', 'choroplethlocation') .on('mouseover', handleMouseOver) + .on('click', handleClick) .on('mouseout', function() { Plotly.Fx.loneUnhover(geo.hoverContainer); }) @@ -118,6 +122,8 @@ plotChoropleth.plot = function(geo, choroplethData, geoLayout) { }); // some baselayers are drawn over choropleth + gBaseLayerOverChoropleth.selectAll('*').remove(); + for(var i = 0; i < baseLayersOverChoropleth.length; i++) { layerName = baseLayersOverChoropleth[i]; gBaseLayer.select('g.' + layerName).remove(); @@ -140,11 +146,11 @@ plotChoropleth.style = function(geo) { sclFunc = makeScaleFunction(scl, zmin, zmax); s.selectAll('path.choroplethlocation') - .each(function(d) { + .each(function(pt) { d3.select(this) - .attr('fill', function(d) { return sclFunc(d.z); }) - .call(Color.stroke, d.mlc || markerLine.color) - .call(Drawing.dashLine, '', d.mlw || markerLine.width); + .attr('fill', function(pt) { return sclFunc(pt.z); }) + .call(Color.stroke, pt.mlc || markerLine.color) + .call(Drawing.dashLine, '', pt.mlw || markerLine.width); }); }); }; @@ -153,9 +159,9 @@ function makeCleanHoverLabelsFunc(geo, trace) { var hoverinfo = trace.hoverinfo; if(hoverinfo === 'none') { - return function cleanHoverLabelsFunc(d) { - delete d.nameLabel; - delete d.textLabel; + return function cleanHoverLabelsFunc(pt) { + delete pt.nameLabel; + delete pt.textLabel; }; } @@ -174,20 +180,33 @@ function makeCleanHoverLabelsFunc(geo, trace) { return Plotly.Axes.tickText(axis, axis.c2l(val), 'hover').text; } - return function cleanHoverLabelsFunc(d) { + return function cleanHoverLabelsFunc(pt) { // put location id in name label container // if name isn't part of hoverinfo var thisText = []; - if(hasIdAsNameLabel) d.nameLabel = d.id; + if(hasIdAsNameLabel) pt.nameLabel = pt.id; else { - if(hasName) d.nameLabel = trace.name; - if(hasLocation) thisText.push(d.id); + if(hasName) pt.nameLabel = trace.name; + if(hasLocation) thisText.push(pt.id); } - if(hasZ) thisText.push(formatter(d.z)); - if(hasText) thisText.push(d.tx); + if(hasZ) thisText.push(formatter(pt.z)); + if(hasText) thisText.push(pt.tx); + + pt.textLabel = thisText.join('
'); + }; +} - d.textLabel = thisText.join('
'); +function makeEventDataFunc(trace) { + return function(pt, ptIndex) { + return {points: [{ + data: trace._input, + fullData: trace, + curveNumber: trace.index, + pointNumber: ptIndex, + location: pt.id, + z: pt.z + }]}; }; } diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js index 6f8691f4bee..eee16c1d477 100644 --- a/src/traces/scattergeo/plot.js +++ b/src/traces/scattergeo/plot.js @@ -116,19 +116,14 @@ function makeLineGeoJSON(trace) { } plotScatterGeo.plot = function(geo, scattergeoData) { - var gScatterGeo = geo.framework.select('g.scattergeolayer'), - topojson = geo.topojson; - - // TODO move to more d3-idiomatic pattern (that's work on replot) - // N.B. html('') does not work in IE11 - gScatterGeo.selectAll('*').remove(); - - var gScatterGeoTraces = gScatterGeo - .selectAll('g.trace.scatter') + var gScatterGeoTraces = geo.framework.select('.scattergeolayer') + .selectAll('g.trace.scattergeo') .data(scattergeoData); gScatterGeoTraces.enter().append('g') - .attr('class', 'trace scattergeo'); + .attr('class', 'trace scattergeo'); + + gScatterGeoTraces.exit().remove(); // TODO add hover - how? gScatterGeoTraces @@ -152,28 +147,37 @@ plotScatterGeo.plot = function(geo, scattergeoData) { return; } - var cdi = plotScatterGeo.calcGeoJSON(trace, topojson), - cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace); + 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); + hasNameLabel = ( + hoverinfo === 'all' || + hoverinfo.indexOf('name') !== -1 + ); - function handleMouseOver(d) { + function handleMouseOver(pt, ptIndex) { if(!geo.showHover) return; - var xy = geo.projection([d.lon, d.lat]); - cleanHoverLabelsFunc(d); + var xy = geo.projection([pt.lon, pt.lat]); + cleanHoverLabelsFunc(pt); Fx.loneHover({ x: xy[0], y: xy[1], name: hasNameLabel ? trace.name : undefined, - text: d.textLabel, - color: d.mc || (trace.marker || {}).color + text: pt.textLabel, + color: pt.mc || (trace.marker || {}).color }, { container: geo.hoverContainer.node() }); + + geo.graphDiv.emit('plotly_hover', eventDataFunc(pt, ptIndex)); + } + + function handleClick(pt, ptIndex) { + geo.graphDiv.emit('plotly_click', eventDataFunc(pt, ptIndex)); } if(showMarkers) { @@ -182,6 +186,7 @@ plotScatterGeo.plot = function(geo, scattergeoData) { .enter().append('path') .attr('class', 'point') .on('mouseover', handleMouseOver) + .on('click', handleClick) .on('mouseout', function() { Fx.loneUnhover(geo.hoverContainer); }) @@ -237,11 +242,13 @@ function makeCleanHoverLabelsFunc(geo, trace) { } var hoverinfoParts = (hoverinfo === 'all') ? - attributes.hoverinfo.flags : - hoverinfo.split('+'); + attributes.hoverinfo.flags : + hoverinfo.split('+'); - var hasLocation = (hoverinfoParts.indexOf('location') !== -1 && - Array.isArray(trace.locations)), + 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); @@ -251,18 +258,34 @@ function makeCleanHoverLabelsFunc(geo, trace) { return Axes.tickText(axis, axis.c2l(val), 'hover').text + '\u00B0'; } - return function cleanHoverLabelsFunc(d) { + return function cleanHoverLabelsFunc(pt) { var thisText = []; - if(hasLocation) thisText.push(d.location); + if(hasLocation) thisText.push(pt.location); else if(hasLon && hasLat) { - thisText.push('(' + formatter(d.lon) + ', ' + formatter(d.lat) + ')'); + thisText.push('(' + formatter(pt.lon) + ', ' + formatter(pt.lat) + ')'); } - else if(hasLon) thisText.push('lon: ' + formatter(d.lon)); - else if(hasLat) thisText.push('lat: ' + formatter(d.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); - if(hasText) thisText.push(d.tx || trace.text); + pt.textLabel = thisText.join('
'); + }; +} - d.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 + }]}; }; } diff --git a/test/jasmine/tests/geo_interact_test.js b/test/jasmine/tests/geo_interact_test.js index e59f5b62ef6..2c811b33b5e 100644 --- a/test/jasmine/tests/geo_interact_test.js +++ b/test/jasmine/tests/geo_interact_test.js @@ -14,14 +14,24 @@ describe('Test geo interactions', function() { describe('mock geo_first.json', function() { var mock = require('@mocks/geo_first.json'); + var gd; + + function mouseEventScatterGeo(type) { + mouseEvent(type, 300, 235); + } + + function mouseEventChoropleth(type) { + mouseEvent(type, 400, 160); + } beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + gd = createGraphDiv(); + Plotly.plot(gd, mock.data, mock.layout).then(done); }); - describe('scattegeo hover labels', function() { + describe('scattergeo hover labels', function() { beforeEach(function() { - mouseEvent('mouseover', 300, 235); + mouseEventScatterGeo('mouseover'); }); it('should show one hover text group', function() { @@ -41,9 +51,63 @@ describe('Test geo interactions', function() { }); }); + describe('scattergeo hover events', function() { + var ptData; + + beforeEach(function() { + gd.on('plotly_hover', function(eventData) { + ptData = eventData.points[0]; + }); + + mouseEventScatterGeo('mouseover'); + }); + + it('should contain the correct fields', function() { + expect(Object.keys(ptData)).toEqual([ + 'data', 'fullData', 'curveNumber', 'pointNumber', + 'lon', 'lat', 'location' + ]); + }); + + it('should show the correct point data', function() { + expect(ptData.lon).toEqual(0); + expect(ptData.lat).toEqual(0); + expect(ptData.location).toBe(null); + expect(ptData.curveNumber).toEqual(0); + expect(ptData.pointNumber).toEqual(0); + }); + }); + + describe('scattergeo click events', function() { + var ptData; + + beforeEach(function() { + gd.on('plotly_click', function(eventData) { + ptData = eventData.points[0]; + }); + + mouseEventScatterGeo('click'); + }); + + it('should contain the correct fields', function() { + expect(Object.keys(ptData)).toEqual([ + 'data', 'fullData', 'curveNumber', 'pointNumber', + 'lon', 'lat', 'location' + ]); + }); + + it('should show the correct point data', function() { + expect(ptData.lon).toEqual(0); + expect(ptData.lat).toEqual(0); + expect(ptData.location).toBe(null); + expect(ptData.curveNumber).toEqual(0); + expect(ptData.pointNumber).toEqual(0); + }); + }); + describe('choropleth hover labels', function() { beforeEach(function() { - mouseEvent('mouseover', 400, 160); + mouseEventChoropleth('mouseover'); }); it('should show one hover text group', function() { @@ -64,5 +128,57 @@ describe('Test geo interactions', function() { }); }); + describe('choropleth hover events', function() { + var ptData; + + beforeEach(function() { + gd.on('plotly_hover', function(eventData) { + ptData = eventData.points[0]; + }); + + mouseEventChoropleth('mouseover'); + }); + + it('should contain the correct fields', function() { + expect(Object.keys(ptData)).toEqual([ + 'data', 'fullData', 'curveNumber', 'pointNumber', + 'location', 'z' + ]); + }); + + it('should show the correct point data', function() { + expect(ptData.location).toBe('RUS'); + expect(ptData.z).toEqual(10); + expect(ptData.curveNumber).toEqual(1); + expect(ptData.pointNumber).toEqual(2); + }); + }); + + describe('choropleth click events', function() { + var ptData; + + beforeEach(function() { + gd.on('plotly_click', function(eventData) { + ptData = eventData.points[0]; + }); + + mouseEventChoropleth('click'); + }); + + it('should contain the correct fields', function() { + expect(Object.keys(ptData)).toEqual([ + 'data', 'fullData', 'curveNumber', 'pointNumber', + 'location', 'z' + ]); + }); + + it('should show the correct point data', function() { + expect(ptData.location).toBe('RUS'); + expect(ptData.z).toEqual(10); + expect(ptData.curveNumber).toEqual(1); + expect(ptData.pointNumber).toEqual(2); + }); + }); + }); });