From 02e325b6bc0c7d29f332a75400b019cc0cbd3a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 9 Mar 2016 15:04:34 -0500 Subject: [PATCH 1/3] ensure that old choropleth location are removed on update --- src/traces/choropleth/plot.js | 87 ++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/traces/choropleth/plot.js b/src/traces/choropleth/plot.js index 15a59206869..479910008bc 100644 --- a/src/traces/choropleth/plot.js +++ b/src/traces/choropleth/plot.js @@ -75,50 +75,51 @@ plotChoropleth.plot = function(geo, choroplethData, geoLayout) { gChoroplethTraces.exit().remove(); - gChoroplethTraces - .each(function(trace) { - var cdi = plotChoropleth.calcGeoJSON(trace, geo.topojson), - cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace), - eventDataFunc = makeEventDataFunc(trace); - - function handleMouseOver(pt, ptIndex) { - if(!geo.showHover) return; - - var xy = geo.projection(pt.properties.ct); - cleanHoverLabelsFunc(pt); - - Fx.loneHover({ - x: xy[0], - y: xy[1], - name: pt.nameLabel, - text: pt.textLabel - }, { - container: geo.hoverContainer.node() - }); + gChoroplethTraces.each(function(trace) { + var cdi = plotChoropleth.calcGeoJSON(trace, geo.topojson), + cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace), + eventDataFunc = makeEventDataFunc(trace); + + function handleMouseOver(pt, ptIndex) { + if(!geo.showHover) return; + + var xy = geo.projection(pt.properties.ct); + cleanHoverLabelsFunc(pt); + + Fx.loneHover({ + x: xy[0], + y: xy[1], + name: pt.nameLabel, + text: pt.textLabel + }, { + container: geo.hoverContainer.node() + }); + + geo.graphDiv.emit('plotly_hover', eventDataFunc(pt, ptIndex)); + } - geo.graphDiv.emit('plotly_hover', eventDataFunc(pt, ptIndex)); - } - - function handleClick(pt, ptIndex) { - geo.graphDiv.emit('plotly_click', eventDataFunc(pt, ptIndex)); - } - - d3.select(this) - .selectAll('path.choroplethlocation') - .data(cdi) - .enter().append('path') - .attr('class', 'choroplethlocation') - .on('mouseover', handleMouseOver) - .on('click', handleClick) - .on('mouseout', function() { - Fx.loneUnhover(geo.hoverContainer); - }) - .on('mousedown', function() { - // to simulate the 'zoomon' event - Fx.loneUnhover(geo.hoverContainer); - }) - .on('mouseup', handleMouseOver); // ~ 'zoomend' - }); + function handleClick(pt, ptIndex) { + geo.graphDiv.emit('plotly_click', eventDataFunc(pt, ptIndex)); + } + + var paths = d3.select(this).selectAll('path.choroplethlocation') + .data(cdi); + + paths.enter().append('path') + .classed('choroplethlocation', true) + .on('mouseover', handleMouseOver) + .on('click', handleClick) + .on('mouseout', function() { + Fx.loneUnhover(geo.hoverContainer); + }) + .on('mousedown', function() { + // to simulate the 'zoomon' event + Fx.loneUnhover(geo.hoverContainer); + }) + .on('mouseup', handleMouseOver); // ~ 'zoomend' + + paths.exit().remove(); + }); // some baselayers are drawn over choropleth gBaseLayerOverChoropleth.selectAll('*').remove(); From be45bb9795aa4f43713e70e9c30fe36c9c57c0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 9 Mar 2016 17:44:54 -0500 Subject: [PATCH 2/3] flatten scattergeo inner nodes: - remove (no need for an extra nested layer) - rm all inner node in so that order line/marker/text ordering is kept --- src/traces/scattergeo/plot.js | 168 +++++++++++++++++----------------- 1 file changed, 85 insertions(+), 83 deletions(-) diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js index 4408835a2ed..95962f078b4 100644 --- a/src/traces/scattergeo/plot.js +++ b/src/traces/scattergeo/plot.js @@ -125,83 +125,84 @@ plotScatterGeo.plot = function(geo, scattergeoData) { gScatterGeoTraces.exit().remove(); - // TODO add hover - how? - gScatterGeoTraces - .each(function(trace) { - if(!subTypes.hasLines(trace)) return; + // TODO find a way to order the inner nodes on update + gScatterGeoTraces.selectAll('*').remove(); - d3.select(this) - .append('path') - .datum(makeLineGeoJSON(trace)) - .attr('class', 'js-line'); - }); + gScatterGeoTraces.each(function(trace) { + var s = d3.select(this); - gScatterGeoTraces.append('g') - .attr('class', 'points') - .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 - ); - - 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() - }); - - geo.graphDiv.emit('plotly_hover', eventDataFunc(pt, ptIndex)); - } - - function handleClick(pt, ptIndex) { - geo.graphDiv.emit('plotly_click', eventDataFunc(pt, ptIndex)); - } - - if(showMarkers) { - s.selectAll('path.point') - .data(cdi) - .enter().append('path') - .attr('class', 'point') - .on('mouseover', handleMouseOver) - .on('click', handleClick) - .on('mouseout', function() { - Fx.loneUnhover(geo.hoverContainer); - }) - .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'); - } - }); + 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 + ); + + 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() + }); + + geo.graphDiv.emit('plotly_hover', eventDataFunc(pt, ptIndex)); + } + + 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); + }) + .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); }; @@ -209,15 +210,16 @@ plotScatterGeo.plot = function(geo, scattergeoData) { plotScatterGeo.style = function(geo) { var selection = geo.framework.selectAll('g.trace.scattergeo'); - selection.style('opacity', function(trace) { return trace.opacity; }); + selection.style('opacity', function(trace) { + return trace.opacity; + }); - selection.selectAll('g.points') - .each(function(trace) { - d3.select(this).selectAll('path.point') - .call(Drawing.pointStyle, trace); - d3.select(this).selectAll('text') - .call(Drawing.textPointStyle, trace); - }); + selection.each(function(trace) { + d3.select(this).selectAll('path.point') + .call(Drawing.pointStyle, trace); + d3.select(this).selectAll('text') + .call(Drawing.textPointStyle, trace); + }); // GeoJSON calc data is incompatible with Drawing.lineGroupStyle selection.selectAll('path.js-line') From 731ff1bff3ce008a0fd30a0f7abcb3cd3cdf17b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 9 Mar 2016 17:45:07 -0500 Subject: [PATCH 3/3] add geo streaming tests --- test/jasmine/tests/geo_interact_test.js | 222 +++++++++++++++++++++++- 1 file changed, 221 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/geo_interact_test.js b/test/jasmine/tests/geo_interact_test.js index 854c2f60154..e8b3b92aef4 100644 --- a/test/jasmine/tests/geo_interact_test.js +++ b/test/jasmine/tests/geo_interact_test.js @@ -1,6 +1,7 @@ var d3 = require('d3'); var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -38,7 +39,10 @@ describe('Test geo interactions', function() { beforeEach(function(done) { gd = createGraphDiv(); - Plotly.plot(gd, mock.data, mock.layout).then(done); + + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); }); describe('scattergeo hover labels', function() { @@ -261,5 +265,221 @@ describe('Test geo interactions', function() { }); }); + describe('streaming calls', function() { + var INTERVAL = 10; + + var N_MARKERS_AT_START = Math.min( + mock.data[0].lat.length, + mock.data[0].lon.length + ); + + var N_LOCATIONS_AT_START = mock.data[1].locations.length; + + var lonQueue = [45, -45, 12, 20], + latQueue = [-75, 80, 5, 10], + textQueue = ['c', 'd', 'e', 'f'], + locationsQueue = ['AUS', 'FRA', 'DEU', 'MEX'], + zQueue = [100, 20, 30, 12]; + + beforeEach(function(done) { + var update = { + mode: 'lines+markers+text', + text: [['a', 'b']], + 'marker.size': 10 + }; + + Plotly.restyle(gd, update, [0]).then(done); + }); + + function countScatterGeoLines() { + return d3.selectAll('g.trace.scattergeo') + .selectAll('path.js-line') + .size(); + } + + function countScatterGeoMarkers() { + return d3.selectAll('g.trace.scattergeo') + .selectAll('path.point') + .size(); + } + + function countScatterGeoTextGroups() { + return d3.selectAll('g.trace.scattergeo') + .selectAll('g') + .size(); + } + + function countScatterGeoTextNodes() { + return d3.selectAll('g.trace.scattergeo') + .selectAll('g') + .select('text') + .size(); + } + + function checkScatterGeoOrder() { + var order = ['js-path', 'point', null]; + var nodes = d3.selectAll('g.trace.scattergeo'); + + nodes.each(function() { + var list = []; + + d3.select(this).selectAll('*').each(function() { + var className = d3.select(this).attr('class'); + list.push(className); + }); + + var listSorted = list.slice().sort(function(a, b) { + return order.indexOf(a) - order.indexOf(b); + }); + + expect(list).toEqual(listSorted); + }); + } + + function countChoroplethPaths() { + return d3.selectAll('g.trace.choropleth') + .selectAll('path.choroplethlocation') + .size(); + } + + it('should be able to add line/marker/text nodes', function(done) { + var i = 0; + + var interval = setInterval(function() { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START + i); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START + i); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START + i); + checkScatterGeoOrder(); + + var trace = gd.data[0]; + trace.lon.push(lonQueue[i]); + trace.lat.push(latQueue[i]); + trace.text.push(textQueue[i]); + + if(i === lonQueue.length - 1) { + clearInterval(interval); + done(); + } + + Plotly.plot(gd); + i++; + }, INTERVAL); + }); + + it('should be able to shift line/marker/text nodes', function(done) { + var i = 0; + + var interval = setInterval(function() { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START); + checkScatterGeoOrder(); + + var trace = gd.data[0]; + trace.lon.push(lonQueue[i]); + trace.lat.push(latQueue[i]); + trace.text.push(textQueue[i]); + trace.lon.shift(); + trace.lat.shift(); + trace.text.shift(); + + if(i === lonQueue.length - 1) { + clearInterval(interval); + done(); + } + + Plotly.plot(gd); + i++; + }, INTERVAL); + }); + + it('should be able to update line/marker/text nodes', function(done) { + var i = 0; + + var interval = setInterval(function() { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START); + checkScatterGeoOrder(); + + var trace = gd.data[0]; + trace.lon.push(lonQueue[i]); + trace.lat.push(latQueue[i]); + trace.text.push(textQueue[i]); + trace.lon.shift(); + trace.lat.shift(); + trace.text.shift(); + + if(i === lonQueue.length - 1) { + clearInterval(interval); + done(); + } + + Plotly.plot(gd); + i++; + }, INTERVAL); + }); + + it('should be able to delete line/marker/text nodes and choropleth paths', function(done) { + var trace0 = gd.data[0]; + trace0.lon.shift(); + trace0.lat.shift(); + trace0.text.shift(); + + var trace1 = gd.data[1]; + trace1.locations.shift(); + + Plotly.plot(gd).then(function() { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START - 1); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START - 1); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START - 1); + checkScatterGeoOrder(); + + expect(countChoroplethPaths()).toBe(N_LOCATIONS_AT_START - 1); + + done(); + }); + }); + + it('should be able to update line/marker/text nodes and choropleth paths', function(done) { + var trace0 = gd.data[0]; + trace0.lon = lonQueue; + trace0.lat = latQueue; + trace0.text = textQueue; + + var trace1 = gd.data[1]; + trace1.locations = locationsQueue; + trace1.z = zQueue; + + Plotly.plot(gd).then(function() { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(lonQueue.length); + expect(countScatterGeoTextGroups()).toBe(textQueue.length); + expect(countScatterGeoTextNodes()).toBe(textQueue.length); + checkScatterGeoOrder(); + + expect(countChoroplethPaths()).toBe(locationsQueue.length); + + done(); + }); + }); + + }); }); });