From 319822966e8314b2228a3de865b5f5ba30b93955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 17:58:05 -0500 Subject: [PATCH 01/18] make Plotly.deleteTraces on 'surface' and 'mesh' traces work --- src/traces/mesh3d/convert.js | 2 +- src/traces/surface/convert.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traces/mesh3d/convert.js b/src/traces/mesh3d/convert.js index 22fabd640f4..efeed65b773 100644 --- a/src/traces/mesh3d/convert.js +++ b/src/traces/mesh3d/convert.js @@ -141,7 +141,7 @@ proto.update = function(data) { }; proto.dispose = function() { - this.glplot.remove(this.mesh); + this.scene.glplot.remove(this.mesh); this.mesh.dispose(); }; diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js index c782e786eac..36d4827257d 100644 --- a/src/traces/surface/convert.js +++ b/src/traces/surface/convert.js @@ -323,7 +323,7 @@ proto.update = function(data) { proto.dispose = function() { - this.glplot.remove(this.surface); + this.scene.glplot.remove(this.surface); this.surface.dispose(); }; From 4def84705d3e171957d9d5ed1715f73ccf5712f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:03:23 -0500 Subject: [PATCH 02/18] replace getSubplotIdsInData by findSubplotIds: - generalizes the 'find subplot' step where we look for subplots in user data and (now also) in layout before filling in the defaults into their 'full' counterparts. - Before, gl3d and geo defaults where also considering data-referenced subplots. Now, orphan (or data-less) subplot are valid. --- src/plots/plots.js | 61 ++++++++++++++++++------ test/jasmine/tests/plots_test.js | 79 ++++++++++++++++++++++---------- 2 files changed, 102 insertions(+), 38 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 3b1f9ad9d0a..8d5fe5500d8 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -140,8 +140,52 @@ plots.registerSubplot = function(_module) { subplotsRegistry[plotType] = _module; }; -// TODO separate the 'find subplot' step (which looks in layout) -// from the 'get subplot ids' step (which looks in fullLayout._plots) +/** + * Find subplot ids in data and layout. Meant to be used + * in the defaults step. Use plots.getSubplotIds to grab the current + * subplot ids later on in Plotly.plot. + * + * @param {array} data plotly data array + * (intended to be _fullData, but does not have to be). + * @param {object} layout plotly layout object + * (intended to be _fullLayout, but does not have to be). + * @param {string} type subplot type to look for. + * + * @return {array} list of subplot ids (strings). + * N.B. these ids possibly un-ordered. + * + * TODO incorporate cartesian/gl2d axis finders in this paradigm. + */ +plots.findSubplotIds = function findSubplotIds(data, layout, type) { + var subplotIds = []; + var i; + + if(plots.subplotsRegistry[type] === undefined) return subplotIds; + + var attr = plots.subplotsRegistry[type].attr; + + for(i = 0; i < data.length; i++) { + var trace = data[i]; + + if(plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr]) === -1) { + subplotIds.push(trace[attr]); + } + } + + var attrRegex = plots.subplotsRegistry[type].attrRegex, + layoutKeys = Object.keys(layout); + + for(i = 0; i < layoutKeys.length; i++) { + var layoutKey = layoutKeys[i]; + + if(subplotIds.indexOf(layoutKey) === -1 && attrRegex.test(layoutKey)) { + subplotIds.push(layoutKey); + } + } + + return subplotIds; +}; + plots.getSubplotIds = function getSubplotIds(layout, type) { if(plots.subplotsRegistry[type] === undefined) return []; @@ -165,19 +209,6 @@ plots.getSubplotIds = function getSubplotIds(layout, type) { return subplotIds; }; -plots.getSubplotIdsInData = function getSubplotsInData(data, type) { - if(plots.subplotsRegistry[type] === undefined) return []; - - var attr = plots.subplotsRegistry[type].attr, - subplotIds = [], - trace; - - for (var i = 0; i < data.length; i++) { - trace = data[i]; - if(Plotly.Plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr])===-1) { - subplotIds.push(trace[attr]); - } - } return subplotIds; }; diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index ecd24f4fc6c..388a0ba7497 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -145,34 +145,67 @@ describe('Test Plots', function() { }); }); - describe('Plots.getSubplotIdsInData', function() { - var getSubplotIdsInData = Plots.getSubplotIdsInData; - - var ids, data; - - it('it should return scene ids', function() { - data = [ - { - type: 'scatter3d', - scene: 'scene' - }, - { - type: 'surface', - scene: 'scene2' - }, - { - type: 'choropleth', - geo: 'geo' - } - ]; - - ids = getSubplotIdsInData(data, 'geo'); + describe('Plots.findSubplotIds', function() { + var findSubplotIds = Plots.findSubplotIds; + var ids; + + it('should return subplots ids found in the data', function() { + var data = [{ + type: 'scatter3d', + scene: 'scene' + }, { + type: 'surface', + scene: 'scene2' + }, { + type: 'choropleth', + geo: 'geo' + }]; + + ids = findSubplotIds(data, {}, 'geo'); expect(ids).toEqual(['geo']); - ids = getSubplotIdsInData(data, 'gl3d'); + ids = findSubplotIds(data, {}, 'gl3d'); expect(ids).toEqual(['scene', 'scene2']); }); + it('should return subplots ids found in layout', function() { + var layout = { + scene: {}, + geo: {}, + geo2: {} + }; + + ids = findSubplotIds([], layout, 'geo'); + expect(ids).toEqual(['geo', 'geo2']); + + ids = findSubplotIds([], layout, 'gl3d'); + expect(ids).toEqual(['scene']); + }); + + it('should return unique subplots ids found in data & layout', function() { + var data = [{ + type: 'scatter3d', + scene: 'scene' + }, { + type: 'surface', + scene: 'scene2' + }, { + type: 'choropleth', + geo: 'geo' + }]; + + var layout = { + scene: {}, + geo: {}, + geo2: {} + }; + + ids = findSubplotIds(data, layout, 'geo'); + expect(ids).toEqual(['geo', 'geo2']); + + ids = findSubplotIds(data, layout, 'gl3d'); + expect(ids).toEqual(['scene', 'scene2']); + }); }); describe('Plots.register, getModule, and traceIs', function() { From 464f761076c710623cf9daa469843ab7c9e09e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:09:43 -0500 Subject: [PATCH 03/18] robustify 'find axes' logic: - make sure that graph with only orphan cartesian are considered 'cartesian' - make sure that axes associated with GL2D traces aren't considered 'cartesian' - by making layout with { xaxis: {}, yaxis: {} } a 'hasCartesian' plot, deleting the last cartesian trace retains the axes. --- src/plots/cartesian/layout_defaults.js | 80 ++++++++++++++++++-------- test/jasmine/tests/axes_test.js | 74 ++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 28 deletions(-) diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 7d656c0463b..92969391248 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -20,28 +20,36 @@ var axisIds = require('./axis_ids'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - // get the full list of axes already defined var layoutKeys = Object.keys(layoutIn), - xaList = [], - yaList = [], + xaListCartesian = [], + yaListCartesian = [], + xaListGl2d = [], + yaListGl2d = [], outerTicks = {}, noGrids = {}, i; - for(i = 0; i < layoutKeys.length; i++) { - var key = layoutKeys[i]; - if(constants.xAxisMatch.test(key)) xaList.push(key); - else if(constants.yAxisMatch.test(key)) yaList.push(key); - } - + // look for axes in the data for(i = 0; i < fullData.length; i++) { - var trace = fullData[i], - xaName = axisIds.id2name(trace.xaxis), + var trace = fullData[i]; + var listX, listY; + + if(Plots.traceIs(trace, 'cartesian')) { + listX = xaListCartesian; + listY = yaListCartesian; + } + else if(Plots.traceIs(trace, 'gl2d')) { + listX = xaListGl2d; + listY = yaListGl2d; + } + else continue; + + var xaName = axisIds.id2name(trace.xaxis), yaName = axisIds.id2name(trace.yaxis); // add axes implied by traces - if(xaName && xaList.indexOf(xaName) === -1) xaList.push(xaName); - if(yaName && yaList.indexOf(yaName) === -1) yaList.push(yaName); + if(xaName && listX.indexOf(xaName) === -1) listX.push(xaName); + if(yaName && listY.indexOf(yaName) === -1) listY.push(yaName); // check for default formatting tweaks if(Plots.traceIs(trace, '2dMap')) { @@ -55,22 +63,46 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } } - function axSort(a,b) { - var aNum = Number(a.substr(5)||1), - bNum = Number(b.substr(5)||1); - return aNum - bNum; + // N.B. Ignore orphan axes (i.e. axes that have no data attached to them) + // if gl3d or geo is present on graph. This is retain backward compatible. + // + // TODO drop this in version 2.0 + var ignoreOrphan = (layoutOut._hasGL3D || layoutOut._hasGeo); + + if(!ignoreOrphan) { + for(i = 0; i < layoutKeys.length; i++) { + var key = layoutKeys[i]; + + // orphan layout axes are considered cartesian subplots + + if(xaListGl2d.indexOf(key) === -1 && + xaListCartesian.indexOf(key) === -1 && + constants.xAxisMatch.test(key)) { + xaListCartesian.push(key); + } + else if(yaListGl2d.indexOf(key) === -1 && + yaListCartesian.indexOf(key) === -1 && + constants.yAxisMatch.test(key)) { + yaListCartesian.push(key); + } + } } - if(layoutOut._hasCartesian || layoutOut._hasGL2D || !fullData.length) { - // make sure there's at least one of each and lists are sorted - if(!xaList.length) xaList = ['xaxis']; - else xaList.sort(axSort); + // make sure that plots with orphan cartesian axes + // are considered 'cartesian' + if(xaListCartesian.length && yaListCartesian.length) { + layoutOut._hasCartesian = true; + } - if(!yaList.length) yaList = ['yaxis']; - else yaList.sort(axSort); + function axSort(a, b) { + var aNum = Number(a.substr(5) || 1), + bNum = Number(b.substr(5) || 1); + return aNum - bNum; } - xaList.concat(yaList).forEach(function(axName){ + var xaList = xaListCartesian.concat(xaListGl2d).sort(axSort), + yaList = yaListCartesian.concat(yaListGl2d).sort(axSort); + var axLetter = axName.charAt(0), axLayoutIn = layoutIn[axName] || {}, axLayoutOut = {}, diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index b9866b580b3..3959356c46c 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -164,9 +164,12 @@ describe('Test axes', function() { }); describe('supplyLayoutDefaults', function() { - var layoutIn = {}, - layoutOut = {}, + var layoutIn, layoutOut, fullData; + + beforeEach(function() { + layoutOut = {}; fullData = []; + }); var supplyLayoutDefaults = Axes.supplyLayoutDefaults; @@ -193,7 +196,7 @@ describe('Test axes', function() { it('should set linewidth to default if linecolor is supplied and valid', function() { layoutIn = { - xaxis: {linecolor:'black'} + xaxis: { linecolor: 'black' } }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.linecolor).toBe('black'); @@ -202,7 +205,7 @@ describe('Test axes', function() { it('should set linecolor to default if linewidth is supplied and valid', function() { layoutIn = { - yaxis: {linewidth:2} + yaxis: { linewidth: 2 } }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.yaxis.linewidth).toBe(2); @@ -250,6 +253,69 @@ describe('Test axes', function() { expect(layoutOut.xaxis.zerolinewidth).toBe(undefined); expect(layoutOut.xaxis.zerolinecolor).toBe(undefined); }); + + it('should detect orphan axes (lone axes case)', function() { + layoutIn = { + xaxis: {}, + yaxis: {} + }; + fullData = []; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasCartesian).toBe(true); + }); + + it('should detect orphan axes (gl2d trace conflict case)', function() { + layoutIn = { + xaxis: {}, + yaxis: {} + }; + fullData = [{ + type: 'scattergl', + xaxis: 'x', + yaxis: 'y' + }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasCartesian).toBe(undefined); + }); + + it('should detect orphan axes (gl2d + cartesian case)', function() { + layoutIn = { + xaxis2: {}, + yaxis2: {} + }; + fullData = [{ + type: 'scattergl', + xaxis: 'x', + yaxis: 'y' + }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasCartesian).toBe(true); + }); + + it('should detect orphan axes (gl3d present case)', function() { + layoutIn = { + xaxis: {}, + yaxis: {} + }; + layoutOut._hasGL3D = true; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasCartesian).toBe(undefined); + }); + + it('should detect orphan axes (gl3d present case)', function() { + layoutIn = { + xaxis: {}, + yaxis: {} + }; + layoutOut._hasGeo = true; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasCartesian).toBe(undefined); + }); }); describe('handleTickValueDefaults', function() { From a289fd6465f1bf4e2ae3b73f7447b6b964fe8514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:13:13 -0500 Subject: [PATCH 04/18] use findSubplotIds in gl3d and geo defaults steps: - data-only subplot get a corresponding layout subplot 'for free' - plots with orphan scenes/geos are considered HasGL3D and hasGeo respectively. --- src/plots/geo/layout/defaults.js | 12 +++++- src/plots/gl3d/layout/defaults.js | 10 ++--- test/jasmine/tests/geolayout_test.js | 36 +++++++++++++++--- test/jasmine/tests/gl3dlayout_test.js | 53 +++++++++++++++++++++------ 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/src/plots/geo/layout/defaults.js b/src/plots/geo/layout/defaults.js index fd889a1bd6f..be9dd240959 100644 --- a/src/plots/geo/layout/defaults.js +++ b/src/plots/geo/layout/defaults.js @@ -17,9 +17,12 @@ var supplyGeoAxisLayoutDefaults = require('./axis_defaults'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - var geos = Plots.getSubplotIdsInData(fullData, 'geo'), + var geos = Plots.findSubplotIds(fullData, layoutIn, 'geo'), geosLength = geos.length; + if(geos.length) layoutOut._hasGeo = true; + else return; + var geoLayoutIn, geoLayoutOut; function coerce(attr, dflt) { @@ -28,7 +31,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { for(var i = 0; i < geosLength; i++) { var geo = geos[i]; - geoLayoutIn = layoutIn[geo] || {}; + + // geo traces get a layout geo for free! + if(layoutIn[geo]) geoLayoutIn = layoutIn[geo]; + else geoLayoutIn = layoutIn[geo] = {}; + + geoLayoutIn = layoutIn[geo]; geoLayoutOut = {}; coerce('domain.x'); diff --git a/src/plots/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index d31eb42a87b..9fd7ac2367f 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -16,12 +16,11 @@ var supplyGl3dAxisLayoutDefaults = require('./axis_defaults'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - if(!layoutOut._hasGL3D) return; + var scenes = Plots.findSubplotIds(fullData, layoutIn, 'gl3d'), + scenesLength = scenes.length; - var scenes = Plots.getSubplotIdsInData(fullData, 'gl3d'); - - // Get number of scenes to compute default scene domain - var scenesLength = scenes.length; + if(scenes.length) layoutOut._hasGL3D = true; + else return; var sceneLayoutIn, sceneLayoutOut; @@ -59,6 +58,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { * attributes like aspectratio can be written back dynamically. */ + // gl3d traces get a layout scene for free! if(layoutIn[scene] !== undefined) sceneLayoutIn = layoutIn[scene]; else layoutIn[scene] = sceneLayoutIn = {}; diff --git a/test/jasmine/tests/geolayout_test.js b/test/jasmine/tests/geolayout_test.js index f0928e6e48b..c0dbaaad9a8 100644 --- a/test/jasmine/tests/geolayout_test.js +++ b/test/jasmine/tests/geolayout_test.js @@ -8,15 +8,11 @@ describe('Test Geo layout defaults', function() { var supplyLayoutDefaults = Geo.supplyLayoutDefaults; describe('supplyLayoutDefaults', function() { - var layoutIn, layoutOut; - - var fullData = [{ - type: 'scattergeo', - geo: 'geo' - }]; + var layoutIn, layoutOut, fullData; beforeEach(function() { layoutOut = {}; + fullData = []; }); it('should not coerce projection.rotation if type is albers usa', function() { @@ -177,6 +173,34 @@ describe('Test Geo layout defaults', function() { }); }); + it('should detect orphan geos', function() { + layoutIn = { geo: {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasGeo).toBe(true); + }); + + it('should detect orphan geos (converse)', function() { + layoutIn = { 'not-gonna-work': {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasGeo).toBe(undefined); + }); + + it('should add geo data-only geos into layoutIn', function() { + layoutIn = {}; + fullData = [{ type: 'scattergeo', geo: 'geo' }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.geo).toEqual({}); + }); + + it('should add geo data-only geos into layoutIn (converse)', function() { + layoutIn = {}; + fullData = [{ type: 'scatter' }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.geo).toBe(undefined); + }); + }); }); diff --git a/test/jasmine/tests/gl3dlayout_test.js b/test/jasmine/tests/gl3dlayout_test.js index bd1d7fb0d80..4bc4d96ba95 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -5,12 +5,13 @@ describe('Test Gl3d layout defaults', function() { 'use strict'; describe('supplyLayoutDefaults', function() { - var supplyLayoutDefaults = Gl3d.supplyLayoutDefaults; var layoutIn, layoutOut, fullData; + var supplyLayoutDefaults = Gl3d.supplyLayoutDefaults; + beforeEach(function() { - layoutOut = {_hasGL3D: true}; - fullData = [{scene: 'scene', type: 'scatter3d'}]; + layoutOut = {}; + fullData = []; }); it('should coerce aspectmode=ratio when ratio data is valid', function() { @@ -155,7 +156,7 @@ describe('Test Gl3d layout defaults', function() { }); it('should coerce dragmode', function() { - layoutIn = {}; + layoutIn = { scene: {} }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.dragmode) .toBe('turntable', 'to turntable by default'); @@ -165,25 +166,25 @@ describe('Test Gl3d layout defaults', function() { expect(layoutOut.scene.dragmode) .toBe('orbit', 'to user val if valid'); - layoutIn = { dragmode: 'orbit' }; + layoutIn = { scene: {}, dragmode: 'orbit' }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.dragmode) .toBe('orbit', 'to user layout val if valid and 3d only'); - layoutIn = { dragmode: 'orbit' }; + layoutIn = { scene: {}, dragmode: 'orbit' }; layoutOut._hasCartesian = true; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.dragmode) .toBe('turntable', 'to default if not 3d only'); - layoutIn = { dragmode: 'not gonna work' }; + layoutIn = { scene: {}, dragmode: 'not gonna work' }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.dragmode) .toBe('turntable', 'to default if not valid'); }); it('should coerce hovermode', function() { - layoutIn = {}; + layoutIn = { scene: {} }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.hovermode) .toBe('closest', 'to closest by default'); @@ -193,21 +194,51 @@ describe('Test Gl3d layout defaults', function() { expect(layoutOut.scene.hovermode) .toBe(false, 'to user val if valid'); - layoutIn = { hovermode: false }; + layoutIn = { scene: {}, hovermode: false }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.hovermode) .toBe(false, 'to user layout val if valid and 3d only'); - layoutIn = { hovermode: false }; + layoutIn = { scene: {}, hovermode: false }; layoutOut._hasCartesian = true; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.hovermode) .toBe('closest', 'to default if not 3d only'); - layoutIn = { hovermode: 'not gonna work' }; + layoutIn = { scene: {}, hovermode: 'not gonna work' }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.hovermode) .toBe('closest', 'to default if not valid'); }); + + it('should detect orphan scenes', function() { + layoutIn = { scene: {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasGL3D).toBe(true); + }); + + it('should detect orphan scenes (converse)', function() { + layoutIn = { 'not-gonna-work': {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._hasGL3D).toBe(undefined); + }); + + it('should add scene data-only scenes into layoutIn', function() { + layoutIn = {}; + fullData = [{ type: 'scatter3d', scene: 'scene' }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.scene).toEqual({ + aspectratio: { x: 1, y: 1, z: 1 } + }); + }); + + it('should add scene data-only scenes into layoutIn (converse)', function() { + layoutIn = {}; + fullData = [{ type: 'scatter' }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.scene).toBe(undefined); + }); }); }); From 41a2a8d1193d8bb60a1b9c12bfdd045e7b79dc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:13:48 -0500 Subject: [PATCH 05/18] rm unnecessary hasCartesian check --- src/plots/cartesian/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index a27eb63f22f..efd4434031c 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -100,10 +100,8 @@ exports.plot = function(gd) { } // finally do all error bars at once - if(fullLayout._hasCartesian) { - ErrorBars.plot(gd, subplotInfo, cdError); - Lib.markTime('done ErrorBars'); - } + ErrorBars.plot(gd, subplotInfo, cdError); + Lib.markTime('done ErrorBars'); } // now draw stuff not on subplots (ie, only pies at the moment) From ef7f50120788d9ee0273b5161c37989ae1d0bc09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:15:03 -0500 Subject: [PATCH 06/18] ensure that Plots.getSubplotIds returns an ordered list of ids --- src/plots/plots.js | 31 +++++++++++++++++++++++-------- test/jasmine/tests/plots_test.js | 28 +++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 8d5fe5500d8..347769c9269 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -186,8 +186,19 @@ plots.findSubplotIds = function findSubplotIds(data, layout, type) { return subplotIds; }; +/** + * Get the ids of the current subplots. + * + * @param {object} layout plotly full layout object. + * @param {string} type subplot type to look for. + * + * @return {array} list of ordered subplot ids (strings). + * + */ plots.getSubplotIds = function getSubplotIds(layout, type) { - if(plots.subplotsRegistry[type] === undefined) return []; + var _module = plots.subplotsRegistry[type]; + + if(_module === undefined) return []; // layout must be 'fullLayout' here if(type === 'cartesian' && !layout._hasCartesian) return []; @@ -196,19 +207,23 @@ plots.getSubplotIds = function getSubplotIds(layout, type) { return Object.keys(layout._plots); } - var idRegex = plots.subplotsRegistry[type].idRegex, + var idRegex = _module.idRegex, layoutKeys = Object.keys(layout), - subplotIds = [], - layoutKey; + subplotIds = []; for(var i = 0; i < layoutKeys.length; i++) { - layoutKey = layoutKeys[i]; + var layoutKey = layoutKeys[i]; + if(idRegex.test(layoutKey)) subplotIds.push(layoutKey); } - return subplotIds; -}; - + // order the ids + var idLen = _module.idRoot.length; + subplotIds.sort(function(a, b) { + var aNum = +(a.substr(idLen) || 1), + bNum = +(b.substr(idLen) || 1); + return aNum - bNum; + }); return subplotIds; }; diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 388a0ba7497..8a0c9320554 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -102,12 +102,11 @@ describe('Test Plots', function() { describe('Plots.getSubplotIds', function() { var getSubplotIds = Plots.getSubplotIds; - var layout; - it('returns scene ids', function() { - layout = { - scene: {}, + it('returns scene ids in order', function() { + var layout = { scene2: {}, + scene: {}, scene3: {} }; @@ -116,13 +115,32 @@ describe('Test Plots', function() { expect(getSubplotIds(layout, 'cartesian')) .toEqual([]); + expect(getSubplotIds(layout, 'geo')) + .toEqual([]); + expect(getSubplotIds(layout, 'no-valid-subplot-type')) + .toEqual([]); + }); + it('returns geo ids in order', function() { + var layout = { + geo2: {}, + geo: {}, + geo3: {} + }; + + expect(getSubplotIds(layout, 'geo')) + .toEqual(['geo', 'geo2', 'geo3']); + + expect(getSubplotIds(layout, 'cartesian')) + .toEqual([]); + expect(getSubplotIds(layout, 'gl3d')) + .toEqual([]); expect(getSubplotIds(layout, 'no-valid-subplot-type')) .toEqual([]); }); it('returns cartesian ids', function() { - layout = { + var layout = { _plots: { xy: {}, x2y2: {} } }; From 0a6991e8a93a1cfb13765f8595fc34c79525f27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:18:05 -0500 Subject: [PATCH 07/18] make removing geo trace work: - by keeping track of traces in each geo from call to call, - make sure that its module's plot method is called so that it is properly removed from the DOM. --- src/plots/geo/geo.js | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index d2bfd2cf912..9a20153fa53 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -56,6 +56,8 @@ function Geo(options, fullLayout) { this.makeFramework(); this.updateFx(fullLayout.hovermode); + + this.traceHash = {}; } module.exports = Geo; @@ -152,25 +154,47 @@ function filterData(dataIn) { } proto.onceTopojsonIsLoaded = function(geoData, geoLayout) { - var traceData = {}; + var i; this.drawLayout(geoLayout); - for(var i = 0; i < geoData.length; i++) { + var traceHashOld = this.traceHash; + var traceHash = {}; + + for(i = 0; i < geoData.length; i++) { var trace = geoData[i]; - traceData[trace.type] = traceData[trace.type] || []; - traceData[trace.type].push(trace); + traceHash[trace.type] = traceHash[trace.type] || []; + traceHash[trace.type].push(trace); } - var traceKeys = Object.keys(traceData); - for(var j = 0; j < traceKeys.length; j++){ - var moduleData = traceData[traceKeys[j]]; + var moduleNamesOld = Object.keys(traceHashOld); + var moduleNames = Object.keys(traceHash); + + // when a trace gets deleted, make sure that its module's + // plot method is called so that it is properly + // removed from the DOM. + for(i = 0; i < moduleNamesOld.length; i++) { + var moduleName = moduleNamesOld[i]; + + if(moduleNames.indexOf(moduleName) === -1) { + var fakeModule = traceHashOld[moduleName][0]; + fakeModule.visible = false; + traceHash[moduleName] = [fakeModule]; + } + } + + moduleNames = Object.keys(traceHash); + + for(i = 0; i < moduleNames.length; i++) { + var moduleData = traceHash[moduleNames[i]]; var _module = moduleData[0]._module; _module.plot(this, filterData(moduleData), geoLayout); } + this.traceHash = traceHash; + this.render(); }; From b7f94716c6173a0aadc731fb7f0e78bb9268d830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:20:30 -0500 Subject: [PATCH 08/18] generalize cleanScenes step: - ensures the deleted geos are only properly removed from the DOM. - ensures the deleted contour, heatmap and colorbar are properly removed --- src/plots/geo/index.js | 13 ++++++++++++ src/plots/gl3d/index.js | 12 +++++++++++ src/plots/plots.js | 47 ++++++++++++++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/plots/geo/index.js b/src/plots/geo/index.js index cd4aabd81ef..0bf83683a9f 100644 --- a/src/plots/geo/index.js +++ b/src/plots/geo/index.js @@ -65,3 +65,16 @@ exports.plot = function plotGeo(gd) { geo.plot(fullGeoData, fullLayout, gd._promises); } }; + +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var oldGeoKeys = Plots.getSubplotIds(oldFullLayout, 'geo'); + + for(var i = 0; i < oldGeoKeys.length; i++) { + var oldGeoKey = oldGeoKeys[i]; + var oldGeo = oldFullLayout[oldGeoKey]._geo; + + if(!newFullLayout[oldGeoKey] && !!oldGeo) { + oldGeo.geoDiv.remove(); + } + } +}; diff --git a/src/plots/gl3d/index.js b/src/plots/gl3d/index.js index 5132c3cf995..23c06f5e924 100644 --- a/src/plots/gl3d/index.js +++ b/src/plots/gl3d/index.js @@ -66,6 +66,18 @@ exports.plot = function plotGl3d(gd) { } }; +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, 'gl3d'); + + for(var i = 0; i < oldSceneKeys.length; i++) { + var oldSceneKey = oldSceneKeys[i]; + + if(!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) { + oldFullLayout[oldSceneKey]._scene.destroy(); + } + } +}; + // clean scene ids, 'scene1' -> 'scene' exports.cleanId = function cleanId(id) { if (!id.match(/^scene[0-9]*$/)) return; diff --git a/src/plots/plots.js b/src/plots/plots.js index 347769c9269..c92f6120486 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -493,7 +493,8 @@ plots.supplyDefaults = function(gd) { // finally, fill in the pieces of layout that may need to look at data plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData); - cleanScenes(newFullLayout, oldFullLayout); + // clean subplots and other artifact from previous plot calls + cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); /* * Relink functions and underscore attributes to promote consistency between @@ -521,17 +522,47 @@ plots.supplyDefaults = function(gd) { } }; -function cleanScenes(newFullLayout, oldFullLayout) { - var oldSceneKey, - oldSceneKeys = plots.getSubplotIds(oldFullLayout, 'gl3d'); +function cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var i, j; - for (var i = 0; i < oldSceneKeys.length; i++) { - oldSceneKey = oldSceneKeys[i]; - if(!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) { - oldFullLayout[oldSceneKey]._scene.destroy(); + var plotTypes = Object.keys(subplotsRegistry); + for(i = 0; i < plotTypes.length; i++) { + var _module = subplotsRegistry[plotTypes[i]]; + + if(_module.clean) { + _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); } } + var hasPaper = !!oldFullLayout._paper; + var hasInfoLayer = !!oldFullLayout._infolayer; + + oldLoop: + for(i = 0; i < oldFullData.length; i++) { + var oldTrace = oldFullData[i]; + + for(j = 0; j < newFullData.length; j++) { + var newTrace = newFullData.length; + + if(oldTrace.uid === newTrace.uid) continue oldLoop; + } + + var uid = oldTrace.uid; + + // clean old heatmap and contour traces + if(hasPaper) { + oldFullLayout._paper.selectAll( + '.hm' + uid + + ',.contour' + uid + + ',#clip' + uid + ).remove(); + } + + // clean old colorbars + if(hasInfoLayer) { + oldFullLayout._infolayer.selectAll('.cb' + uid).remove(); + } + } } /** From 6ea30d5b2dbacf3b095ef23049a386c725f039ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:21:17 -0500 Subject: [PATCH 09/18] add geo and cartesian delete traces tests --- test/jasmine/tests/geo_interact_test.js | 48 +++++++- test/jasmine/tests/plot_interact_test.js | 144 +++++++++++++++++------ 2 files changed, 154 insertions(+), 38 deletions(-) diff --git a/test/jasmine/tests/geo_interact_test.js b/test/jasmine/tests/geo_interact_test.js index 9b50914ca32..99863495614 100644 --- a/test/jasmine/tests/geo_interact_test.js +++ b/test/jasmine/tests/geo_interact_test.js @@ -24,6 +24,18 @@ describe('Test geo interactions', function() { mouseEvent(type, 400, 160); } + function countTraces(type) { + return d3.selectAll('g.trace.' + type).size(); + } + + function countGeos() { + return d3.select('div.geo-container').selectAll('div').size(); + } + + function countColorBars() { + return d3.select('g.infolayer').selectAll('.cbbg').size(); + } + beforeEach(function(done) { gd = createGraphDiv(); Plotly.plot(gd, mock.data, mock.layout).then(done); @@ -181,10 +193,6 @@ describe('Test geo interactions', function() { }); describe('trace visibility toggle', function() { - function countTraces(type) { - return d3.selectAll('g.trace.' + type).size(); - } - it('should toggle scattergeo elements', function(done) { expect(countTraces('scattergeo')).toBe(1); expect(countTraces('choropleth')).toBe(1); @@ -218,5 +226,37 @@ describe('Test geo interactions', function() { }); }); + + describe('deleting traces and geos', function() { + it('should delete traces in succession', function(done) { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + expect(countGeos()).toBe(1); + expect(countColorBars()).toBe(1); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countTraces('scattergeo')).toBe(0); + expect(countTraces('choropleth')).toBe(1); + expect(countGeos()).toBe(1); + expect(countColorBars()).toBe(1); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countTraces('scattergeo')).toBe(0); + expect(countTraces('choropleth')).toBe(0); + expect(countGeos()).toBe(1); + expect(countColorBars()).toBe(0); + + Plotly.relayout(gd, 'geo', null).then(function() { + expect(countTraces('scattergeo')).toBe(0); + expect(countTraces('choropleth')).toBe(0); + expect(countGeos()).toBe(0); + expect(countColorBars()).toBe(0); + done(); + }); + }); + }); + }); + }); + }); }); diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index eae10037d55..236c06b6a8d 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -20,16 +20,30 @@ describe('Test plot structure', function() { afterEach(destroyGraphDiv); describe('cartesian plots', function() { + + function countSubplots() { + return d3.selectAll('g.subplot').size(); + } + + function countScatterTraces() { + return d3.selectAll('g.trace.scatter').size(); + } + + function countColorBars() { + return d3.selectAll('rect.cbbg').size(); + } + describe('scatter traces', function() { var mock = require('@mocks/14.json'); + var gd; beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + gd = createGraphDiv(); + Plotly.plot(gd, mock.data, mock.layout).then(done); }); it('has one *subplot xy* node', function() { - var nodes = d3.selectAll('g.subplot.xy'); - expect(nodes.size()).toEqual(1); + expect(countSubplots()).toEqual(1); }); it('has one *scatterlayer* node', function() { @@ -38,8 +52,7 @@ describe('Test plot structure', function() { }); it('has as many *trace scatter* nodes as there are traces', function() { - var nodes = d3.selectAll('g.trace.scatter'); - expect(nodes.size()).toEqual(mock.data.length); + expect(countScatterTraces()).toEqual(mock.data.length); }); it('has as many *point* nodes as there are traces', function() { @@ -61,50 +74,73 @@ describe('Test plot structure', function() { assertNamespaces(node); }); }); + + it('should delete be able to get deleted', function(done) { + expect(countScatterTraces()).toEqual(mock.data.length); + expect(countSubplots()).toEqual(1); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countSubplots()).toEqual(1); + done(); + }); + }); }); describe('contour/heatmap traces', function() { var mock = require('@mocks/connectgaps_2d.json'); + var gd; function extendMock() { - var mockCopy = Lib.extendDeep(mock); + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); // add a colorbar for testing - mockCopy.data[0].showscale = true; + mockData[0].showscale = true; + + return { + data: mockData, + layout: mockLayout + }; + } + + function assertHeatmapNodes(expectedCnt) { + var hmNodes = d3.selectAll('g.hm'); + expect(hmNodes.size()).toEqual(expectedCnt); + + var imageNodes = d3.selectAll('image'); + expect(imageNodes.size()).toEqual(expectedCnt); + } - return mockCopy; + function assertContourNodes(expectedCnt) { + var nodes = d3.selectAll('g.contour'); + expect(nodes.size()).toEqual(expectedCnt); } describe('initial structure', function() { beforeEach(function(done) { var mockCopy = extendMock(); + var gd = createGraphDiv(); - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout) + Plotly.plot(gd, mockCopy.data, mockCopy.layout) .then(done); }); it('has four *subplot* nodes', function() { - var nodes = d3.selectAll('g.subplot'); - expect(nodes.size()).toEqual(4); + expect(countSubplots()).toEqual(4); }); - // N.B. the contour traces both have a heatmap fill it('has four heatmap image nodes', function() { - var hmNodes = d3.selectAll('g.hm'); - expect(hmNodes.size()).toEqual(4); - - var imageNodes = d3.selectAll('image'); - expect(imageNodes.size()).toEqual(4); + // N.B. the contour traces both have a heatmap fill + assertHeatmapNodes(4); }); it('has two contour nodes', function() { - var nodes = d3.selectAll('g.contour'); - expect(nodes.size()).toEqual(2); + assertContourNodes(2); }); it('has one colorbar nodes', function() { - var nodes = d3.selectAll('rect.cbbg'); - expect(nodes.size()).toEqual(1); + expect(countColorBars()).toEqual(1); }); }); @@ -129,33 +165,73 @@ describe('Test plot structure', function() { }); it('has four *subplot* nodes', function() { - var nodes = d3.selectAll('g.subplot'); - expect(nodes.size()).toEqual(4); + expect(countSubplots()).toEqual(4); }); it('has two heatmap image nodes', function() { - var hmNodes = d3.selectAll('g.hm'); - expect(hmNodes.size()).toEqual(2); - - var imageNodes = d3.selectAll('image'); - expect(imageNodes.size()).toEqual(2); + assertHeatmapNodes(2); }); it('has two contour nodes', function() { - var nodes = d3.selectAll('g.contour'); - expect(nodes.size()).toEqual(2); + assertContourNodes(2); }); it('has one scatter node', function() { - var nodes = d3.selectAll('g.trace.scatter'); - expect(nodes.size()).toEqual(1); + expect(countScatterTraces()).toEqual(1); }); it('has no colorbar node', function() { - var nodes = d3.selectAll('rect.cbbg'); - expect(nodes.size()).toEqual(0); + expect(countColorBars()).toEqual(0); + }); + }); + + describe('structure after deleteTraces', function() { + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockCopy = extendMock(); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(done); }); + + it('should be removed of traces in sequence', function(done) { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(4); + assertContourNodes(2); + expect(countColorBars()).toEqual(1); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(3); + assertContourNodes(2); + expect(countColorBars()).toEqual(0); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(2); + assertContourNodes(2); + expect(countColorBars()).toEqual(0); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(1); + assertContourNodes(1); + expect(countColorBars()).toEqual(0); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(0); + assertContourNodes(0); + expect(countColorBars()).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + }); describe('pie traces', function() { From 2a3213177080dcc587b1d4af08cf1825263eef6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 19 Feb 2016 18:21:37 -0500 Subject: [PATCH 10/18] add blank plot types test mock --- test/image/baselines/plot_types_blank.png | Bin 0 -> 35369 bytes test/image/mocks/plot_types_blank.json | 63 ++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 test/image/baselines/plot_types_blank.png create mode 100644 test/image/mocks/plot_types_blank.json diff --git a/test/image/baselines/plot_types_blank.png b/test/image/baselines/plot_types_blank.png new file mode 100644 index 0000000000000000000000000000000000000000..3eadefc654cf540082e3e756c26a462441c7c862 GIT binary patch literal 35369 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 Date: Fri, 19 Feb 2016 18:21:51 -0500 Subject: [PATCH 11/18] lint --- src/plot_api/plot_api.js | 5 +++-- src/plots/cartesian/layout_defaults.js | 3 ++- src/plots/plots.js | 22 ++++++++++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d37e78635bd..d20aa239613 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -249,13 +249,13 @@ Plotly.plot = function(gd, data, layout, config) { function drawData() { var calcdata = gd.calcdata; - // in case of traces that were heatmaps or contour maps - // previously, remove them and their colorbars explicitly for(var i = 0; i < calcdata.length; i++) { var trace = calcdata[i][0].trace, isVisible = (trace.visible === true), uid = trace.uid; + // in case of traces that were heatmaps or contour maps + // previously, remove them and their colorbars explicitly if(!isVisible || !Plots.traceIs(trace, '2dMap')) { fullLayout._paper.selectAll( '.hm' + uid + @@ -489,6 +489,7 @@ function cleanLayout(layout) { if(!layout.xaxis) layout.xaxis = layout.xaxis1; delete layout.xaxis1; } + if(layout.yaxis1) { if(!layout.yaxis) layout.yaxis = layout.yaxis1; delete layout.yaxis1; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 92969391248..954de0ce30d 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -103,6 +103,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var xaList = xaListCartesian.concat(xaListGl2d).sort(axSort), yaList = yaListCartesian.concat(yaListGl2d).sort(axSort); + xaList.concat(yaList).forEach(function(axName) { var axLetter = axName.charAt(0), axLayoutIn = layoutIn[axName] || {}, axLayoutOut = {}, @@ -132,7 +133,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // so we don't have to repeat autotype unnecessarily, // copy an autotype back to layoutIn - if(!layoutIn[axName] && axLayoutIn.type!=='-') { + if(!layoutIn[axName] && axLayoutIn.type !== '-') { layoutIn[axName] = {type: axLayoutIn.type}; } diff --git a/src/plots/plots.js b/src/plots/plots.js index c92f6120486..452ff050a57 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -228,6 +228,16 @@ plots.getSubplotIds = function getSubplotIds(layout, type) { return subplotIds; }; +/** + * Get the data traces associated with a particular subplot. + * + * @param {object} layout plotly layout object + * (intended to be _fullLayout, but does not have to be). + * @param {string} type subplot type to look for. + * + * @return {array} array of plotly traces. + * + */ plots.getSubplotData = function getSubplotData(data, type, subplotId) { if(plots.subplotsRegistry[type] === undefined) return []; @@ -435,12 +445,12 @@ plots.sendDataToCloud = function(gd) { return false; }; +// fill in default values: +// gd.data, gd.layout: +// are precisely what the user specified +// gd._fullData, gd._fullLayout: +// are complete descriptions of how to draw the plot plots.supplyDefaults = function(gd) { - // fill in default values: - // gd.data, gd.layout: - // are precisely what the user specified - // gd._fullData, gd._fullLayout: - // are complete descriptions of how to draw the plot var oldFullLayout = gd._fullLayout || {}, newFullLayout = gd._fullLayout = {}, newLayout = gd.layout || {}, @@ -722,7 +732,7 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { var i, _module; - // TODO incorporate into subplotRegistry + // TODO incorporate into subplotsRegistry Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData); // plot module layout defaults From dc72d33308d39f8c1224468533622219cda57c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Feb 2016 09:54:18 -0500 Subject: [PATCH 12/18] fix typos --- src/plot_api/plot_api.js | 4 ++-- src/plots/plots.js | 33 ++++++++++++++++----------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d20aa239613..d91fcfa1c70 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -249,13 +249,13 @@ Plotly.plot = function(gd, data, layout, config) { function drawData() { var calcdata = gd.calcdata; + // in case of traces that were heatmaps or contour maps + // previously, remove them and their colorbars explicitly for(var i = 0; i < calcdata.length; i++) { var trace = calcdata[i][0].trace, isVisible = (trace.visible === true), uid = trace.uid; - // in case of traces that were heatmaps or contour maps - // previously, remove them and their colorbars explicitly if(!isVisible || !Plots.traceIs(trace, '2dMap')) { fullLayout._paper.selectAll( '.hm' + uid + diff --git a/src/plots/plots.js b/src/plots/plots.js index 452ff050a57..8e8c5a8e01c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -145,14 +145,14 @@ plots.registerSubplot = function(_module) { * in the defaults step. Use plots.getSubplotIds to grab the current * subplot ids later on in Plotly.plot. * - * @param {array} data plotly data array + * @param {array} data : plotly data array * (intended to be _fullData, but does not have to be). - * @param {object} layout plotly layout object + * @param {object} layout : plotly layout object * (intended to be _fullLayout, but does not have to be). - * @param {string} type subplot type to look for. + * @param {string} type : subplot type to look for. * * @return {array} list of subplot ids (strings). - * N.B. these ids possibly un-ordered. + * N.B. these ids are possibly un-ordered. * * TODO incorporate cartesian/gl2d axis finders in this paradigm. */ @@ -189,8 +189,8 @@ plots.findSubplotIds = function findSubplotIds(data, layout, type) { /** * Get the ids of the current subplots. * - * @param {object} layout plotly full layout object. - * @param {string} type subplot type to look for. + * @param {object} layout : plotly full layout object. + * @param {string} type : subplot type to look for. * * @return {array} list of ordered subplot ids (strings). * @@ -231,9 +231,9 @@ plots.getSubplotIds = function getSubplotIds(layout, type) { /** * Get the data traces associated with a particular subplot. * - * @param {object} layout plotly layout object + * @param {object} layout : plotly layout object * (intended to be _fullLayout, but does not have to be). - * @param {string} type subplot type to look for. + * @param {string} type : subplot type to look for. * * @return {array} array of plotly traces. * @@ -503,7 +503,7 @@ plots.supplyDefaults = function(gd) { // finally, fill in the pieces of layout that may need to look at data plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData); - // clean subplots and other artifact from previous plot calls + // clean subplots and other artifacts from previous plot calls cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); /* @@ -550,27 +550,26 @@ function cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout) { oldLoop: for(i = 0; i < oldFullData.length; i++) { var oldTrace = oldFullData[i]; + var oldUid = oldTrace.uid; for(j = 0; j < newFullData.length; j++) { - var newTrace = newFullData.length; + var newTrace = newFullData[j]; - if(oldTrace.uid === newTrace.uid) continue oldLoop; + if(oldUid === newTrace.uid) continue oldLoop; } - var uid = oldTrace.uid; - // clean old heatmap and contour traces if(hasPaper) { oldFullLayout._paper.selectAll( - '.hm' + uid + - ',.contour' + uid + - ',#clip' + uid + '.hm' + oldUid + + ',.contour' + oldUid + + ',#clip' + oldUid ).remove(); } // clean old colorbars if(hasInfoLayer) { - oldFullLayout._infolayer.selectAll('.cb' + uid).remove(); + oldFullLayout._infolayer.selectAll('.cb' + oldUid).remove(); } } } From bf6793e51888ea1bdacad25b45db0a928996dea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Feb 2016 18:39:04 -0500 Subject: [PATCH 13/18] write promise sequence as promise chains --- test/jasmine/tests/geo_interact_test.js | 51 +++++++++++++----------- test/jasmine/tests/plot_interact_test.js | 43 ++++++++++---------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/test/jasmine/tests/geo_interact_test.js b/test/jasmine/tests/geo_interact_test.js index 99863495614..9e421709fd0 100644 --- a/test/jasmine/tests/geo_interact_test.js +++ b/test/jasmine/tests/geo_interact_test.js @@ -201,11 +201,12 @@ describe('Test geo interactions', function() { expect(countTraces('scattergeo')).toBe(0); expect(countTraces('choropleth')).toBe(1); - Plotly.restyle(gd, 'visible', true, [0]).then(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - done(); - }); + return Plotly.restyle(gd, 'visible', true, [0]); + }).then(function() { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + + done(); }); }); @@ -217,11 +218,12 @@ describe('Test geo interactions', function() { expect(countTraces('scattergeo')).toBe(1); expect(countTraces('choropleth')).toBe(0); - Plotly.restyle(gd, 'visible', true, [1]).then(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - done(); - }); + return Plotly.restyle(gd, 'visible', true, [1]); + }).then(function() { + expect(countTraces('scattergeo')).toBe(1); + expect(countTraces('choropleth')).toBe(1); + + done(); }); }); @@ -240,20 +242,21 @@ describe('Test geo interactions', function() { expect(countGeos()).toBe(1); expect(countColorBars()).toBe(1); - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countTraces('scattergeo')).toBe(0); - expect(countTraces('choropleth')).toBe(0); - expect(countGeos()).toBe(1); - expect(countColorBars()).toBe(0); - - Plotly.relayout(gd, 'geo', null).then(function() { - expect(countTraces('scattergeo')).toBe(0); - expect(countTraces('choropleth')).toBe(0); - expect(countGeos()).toBe(0); - expect(countColorBars()).toBe(0); - done(); - }); - }); + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + expect(countTraces('scattergeo')).toBe(0); + expect(countTraces('choropleth')).toBe(0); + expect(countGeos()).toBe(1); + expect(countColorBars()).toBe(0); + + return Plotly.relayout(gd, 'geo', null); + }).then(function() { + expect(countTraces('scattergeo')).toBe(0); + expect(countTraces('choropleth')).toBe(0); + expect(countGeos()).toBe(0); + expect(countColorBars()).toBe(0); + + done(); }); }); }); diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index 236c06b6a8d..2fb7743ba47 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -206,27 +206,28 @@ describe('Test plot structure', function() { assertContourNodes(2); expect(countColorBars()).toEqual(0); - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countSubplots()).toEqual(4); - assertHeatmapNodes(2); - assertContourNodes(2); - expect(countColorBars()).toEqual(0); - - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countSubplots()).toEqual(4); - assertHeatmapNodes(1); - assertContourNodes(1); - expect(countColorBars()).toEqual(0); - - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countSubplots()).toEqual(4); - assertHeatmapNodes(0); - assertContourNodes(0); - expect(countColorBars()).toEqual(0); - done(); - }); - }); - }); + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(2); + assertContourNodes(2); + expect(countColorBars()).toEqual(0); + + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(1); + assertContourNodes(1); + expect(countColorBars()).toEqual(0); + + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(0); + assertContourNodes(0); + expect(countColorBars()).toEqual(0); + + done(); }); }); From 09de224e912d640a05924638d97887cefdd843c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 23 Feb 2016 11:42:19 -0500 Subject: [PATCH 14/18] lint --- src/plot_api/plot_api.js | 47 ++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d91fcfa1c70..de78f1eb980 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -50,16 +50,13 @@ var xmlnsNamespaces = require('../constants/xmlns_namespaces'); Plotly.plot = function(gd, data, layout, config) { Lib.markTime('in plot'); - gd = getGraphDiv(gd); - /* - * Events.init is idempotent and bails early if gd has already been init'd - */ + // Events.init is idempotent and bails early if gd has already been init'd Events.init(gd); var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]); - if(okToPlot===false) return Promise.reject(); + if(okToPlot === false) return Promise.reject(); // if there's no data or layout, and this isn't yet a plotly plot // container, log a warning to help plotly.js users debug @@ -89,22 +86,23 @@ Plotly.plot = function(gd, data, layout, config) { // complete, and empty out the promise list again. gd._promises = []; + var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data)); + // if there is already data on the graph, append the new data // if you only want to redraw, pass a non-array for data - var graphwasempty = ((gd.data||[]).length===0 && Array.isArray(data)); if(Array.isArray(data)) { cleanData(data, gd.data); - if(graphwasempty) gd.data=data; - else gd.data.push.apply(gd.data,data); + if(graphWasEmpty) gd.data = data; + else gd.data.push.apply(gd.data, data); // for routines outside graph_obj that want a clean tab // (rather than appending to an existing one) gd.empty // is used to determine whether to make a new tab - gd.empty=false; + gd.empty = false; } - if(!gd.layout || graphwasempty) gd.layout = cleanLayout(layout); + if(!gd.layout || graphWasEmpty) gd.layout = cleanLayout(layout); // if the user is trying to drag the axes, allow new data and layout // to come in but don't allow a replot. @@ -126,23 +124,24 @@ Plotly.plot = function(gd, data, layout, config) { // so we don't try to re-call Plotly.plot from inside // legend and colorbar, if margins changed gd._replotting = true; - var hasData = gd._fullData.length>0; + var hasData = gd._fullData.length > 0; + + var subplots = Plotly.Axes.getSubplots(gd).join(''), + oldSubplots = Object.keys(gd._fullLayout._plots || {}).join(''), + hasSameSubplots = (oldSubplots === subplots); // Make or remake the framework (ie container and axes) if we need to // note: if they container already exists and has data, // the new layout gets ignored (as it should) // but if there's no data there yet, it's just a placeholder... // then it should destroy and remake the plot - if (hasData) { - var subplots = Plotly.Axes.getSubplots(gd).join(''), - oldSubplots = Object.keys(gd._fullLayout._plots || {}).join(''); - - if(gd.framework!==makePlotFramework || graphwasempty || (oldSubplots!==subplots)) { + if(hasData) { + if(gd.framework !== makePlotFramework || graphWasEmpty || !hasSameSubplots) { gd.framework = makePlotFramework; makePlotFramework(gd); } } - else if(graphwasempty) makePlotFramework(gd); + else if(graphWasEmpty) makePlotFramework(gd); var fullLayout = gd._fullLayout; @@ -160,7 +159,7 @@ Plotly.plot = function(gd, data, layout, config) { } // in case it has changed, attach fullData traces to calcdata - for (var i = 0; i < gd.calcdata.length; i++) { + for(var i = 0; i < gd.calcdata.length; i++) { gd.calcdata[i][0].trace = gd._fullData[i]; } @@ -2133,8 +2132,12 @@ Plotly.relayout = function relayout(gd, astr, val) { undoit[ai] = (pleaf === 'reverse') ? vi : p.get(); // check autosize or autorange vs size and range - if(hw.indexOf(ai)!==-1) { doextra('autosize', false); } - else if(ai==='autosize') { doextra(hw, undefined); } + if(hw.indexOf(ai) !== -1) { + doextra('autosize', false); + } + else if(ai === 'autosize') { + doextra(hw, undefined); + } else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) { doextra(ptrunk+'.autorange', false); } @@ -2307,10 +2310,12 @@ Plotly.relayout = function relayout(gd, astr, val) { seq.push(function layoutReplot() { // force plot() to redo the layout gd.layout = undefined; + // force it to redo calcdata? if(docalc) gd.calcdata = undefined; + // replot with the modified layout - return Plotly.plot(gd,'',layout); + return Plotly.plot(gd, '', layout); }); } else if(ak.length) { From a2f2ce89eee98863d327be398345246a3a3ab853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 23 Feb 2016 11:53:24 -0500 Subject: [PATCH 15/18] assure that re-plots changing subplots pass through make framework --- src/plot_api/plot_api.js | 4 ++++ test/jasmine/tests/plot_interact_test.js | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index de78f1eb980..cf14b17c252 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -141,6 +141,10 @@ Plotly.plot = function(gd, data, layout, config) { makePlotFramework(gd); } } + else if(!hasSameSubplots) { + gd.framework = makePlotFramework; + makePlotFramework(gd); + } else if(graphWasEmpty) makePlotFramework(gd); var fullLayout = gd._fullLayout; diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index 2fb7743ba47..aeb84ddf683 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -75,13 +75,22 @@ describe('Test plot structure', function() { }); }); - it('should delete be able to get deleted', function(done) { + it('should be able to get deleted', function(done) { expect(countScatterTraces()).toEqual(mock.data.length); expect(countSubplots()).toEqual(1); Plotly.deleteTraces(gd, [0]).then(function() { expect(countScatterTraces()).toEqual(0); expect(countSubplots()).toEqual(1); + + return Plotly.relayout(gd, {xaxis: null, yaxis: null}); + }).then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); + + done(); + }); + }); done(); }); }); @@ -222,7 +231,7 @@ describe('Test plot structure', function() { return Plotly.deleteTraces(gd, [0]); }).then(function() { - expect(countSubplots()).toEqual(4); + expect(countSubplots()).toEqual(3); assertHeatmapNodes(0); assertContourNodes(0); expect(countColorBars()).toEqual(0); From ba6eeea66336b5e672f41879af3664a82ccaec5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 23 Feb 2016 11:54:28 -0500 Subject: [PATCH 16/18] assure that clearing cartesian axes in relayout lead to a re-calc --- src/plot_api/plot_api.js | 4 +++ test/jasmine/tests/plot_interact_test.js | 35 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index cf14b17c252..9a823c1dd67 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2161,6 +2161,10 @@ Plotly.relayout = function relayout(gd, astr, val) { else if(pleaf === 'tickmode') { doextra([ptrunk + '.tick0', ptrunk + '.dtick'], undefined); } + else if(/[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length) { + docalc = true; + } + // toggling log without autorange: need to also recalculate ranges // logical XOR (ie are we toggling log) if(pleaf==='type' && ((parentFull.type === 'log') !== (vi === 'log'))) { diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index aeb84ddf683..1f088c78cef 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -39,7 +39,11 @@ describe('Test plot structure', function() { beforeEach(function(done) { gd = createGraphDiv(); - Plotly.plot(gd, mock.data, mock.layout).then(done); + + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); + + Plotly.plot(gd, mockData, mockLayout).then(done); }); it('has one *subplot xy* node', function() { @@ -91,6 +95,35 @@ describe('Test plot structure', function() { done(); }); }); + + it('should restore layout axes when they get deleted', function(done) { + expect(countScatterTraces()).toEqual(mock.data.length); + expect(countSubplots()).toEqual(1); + + Plotly.relayout(gd, {xaxis: null, yaxis: null}).then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + + return Plotly.relayout(gd, 'xaxis', null); + }).then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + + return Plotly.relayout(gd, 'xaxis', {}); + }).then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + + return Plotly.relayout(gd, 'yaxis', null); + }).then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + + return Plotly.relayout(gd, 'yaxis', {}); + }).then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + done(); }); }); From 1de0d19bb274800b8d3fc02e12ab6b83dda04fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 23 Feb 2016 12:01:27 -0500 Subject: [PATCH 17/18] add tests checking axis ranges after partial deletion --- test/jasmine/tests/plot_interact_test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index 1f088c78cef..a44b0597678 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -5,6 +5,7 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var customMatchers = require('../assets/custom_matchers'); describe('Test plot structure', function() { @@ -97,32 +98,44 @@ describe('Test plot structure', function() { }); it('should restore layout axes when they get deleted', function(done) { + jasmine.addMatchers(customMatchers); + expect(countScatterTraces()).toEqual(mock.data.length); expect(countSubplots()).toEqual(1); Plotly.relayout(gd, {xaxis: null, yaxis: null}).then(function() { expect(countScatterTraces()).toEqual(1); expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); + expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); return Plotly.relayout(gd, 'xaxis', null); }).then(function() { expect(countScatterTraces()).toEqual(1); expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); + expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); return Plotly.relayout(gd, 'xaxis', {}); }).then(function() { expect(countScatterTraces()).toEqual(1); expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); + expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); return Plotly.relayout(gd, 'yaxis', null); }).then(function() { expect(countScatterTraces()).toEqual(1); expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); + expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); return Plotly.relayout(gd, 'yaxis', {}); }).then(function() { expect(countScatterTraces()).toEqual(1); expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); + expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); done(); }); From 245f8a44193724da7ac82ee2d73e707f62132a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 23 Feb 2016 17:16:41 -0500 Subject: [PATCH 18/18] add cartesian clean plot step to remove old pie traces --- src/plots/cartesian/index.js | 6 +++ test/jasmine/tests/plot_interact_test.js | 49 +++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index efd4434031c..ff15bfc4217 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -112,3 +112,9 @@ exports.plot = function(gd) { if(cdPie.length) Pie.plot(gd, cdPie); } }; + +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + if(oldFullLayout._hasPie && !newFullLayout._hasPie) { + oldFullLayout._pielayer.selectAll('g.trace').remove(); + } +}; diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index a44b0597678..8748b9d9bb2 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -292,9 +292,23 @@ describe('Test plot structure', function() { describe('pie traces', function() { var mock = require('@mocks/pie_simple.json'); + var gd; + + function countPieTraces() { + return d3.select('g.pielayer').selectAll('g.trace').size(); + } + + function countBarTraces() { + return d3.selectAll('g.trace.bars').size(); + } beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + gd = createGraphDiv(); + + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); + + Plotly.plot(gd, mockData, mockLayout).then(done); }); it('has as many *slice* nodes as there are pie items', function() { @@ -319,6 +333,39 @@ describe('Test plot structure', function() { var testerSVG = d3.selectAll('#js-plotly-tester'); assertNamespaces(testerSVG.node()); }); + + it('should be able to get deleted', function(done) { + expect(countPieTraces()).toEqual(1); + expect(countSubplots()).toEqual(0); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countPieTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); + + done(); + }); + }); + + it('should be able to be restyled to a bar chart and back', function(done) { + expect(countPieTraces()).toEqual(1); + expect(countBarTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); + + Plotly.restyle(gd, 'type', 'bar').then(function() { + expect(countPieTraces()).toEqual(0); + expect(countBarTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + + return Plotly.restyle(gd, 'type', 'pie'); + }).then(function() { + expect(countPieTraces()).toEqual(1); + expect(countBarTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); + + done(); + }); + + }); }); });