diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f1ab41db719..a9ff977ea9c 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,28 @@ 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(!hasSameSubplots) { + gd.framework = makePlotFramework; + makePlotFramework(gd); + } + else if(graphWasEmpty) makePlotFramework(gd); var fullLayout = gd._fullLayout; @@ -160,7 +163,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]; } @@ -2144,8 +2147,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); } @@ -2165,6 +2172,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'))) { @@ -2318,10 +2329,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) { diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index a27eb63f22f..ff15bfc4217 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) @@ -114,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/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 7d656c0463b..954de0ce30d 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,47 @@ 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); + + xaList.concat(yaList).forEach(function(axName) { var axLetter = axName.charAt(0), axLayoutIn = layoutIn[axName] || {}, axLayoutOut = {}, @@ -100,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/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(); }; 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/geo/layout/defaults.js b/src/plots/geo/layout/defaults.js index fd889a1bd6f..8e852eb5ac4 100644 --- a/src/plots/geo/layout/defaults.js +++ b/src/plots/geo/layout/defaults.js @@ -17,7 +17,9 @@ var supplyGeoAxisLayoutDefaults = require('./axis_defaults'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - var geos = Plots.getSubplotIdsInData(fullData, 'geo'), + if(!layoutOut._hasGeo) return; + + var geos = Plots.findSubplotIds(fullData, 'geo'), geosLength = geos.length; var geoLayoutIn, geoLayoutOut; @@ -28,7 +30,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/index.js b/src/plots/gl3d/index.js index dd500d98881..19dba4b1953 100644 --- a/src/plots/gl3d/index.js +++ b/src/plots/gl3d/index.js @@ -67,6 +67,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/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index d31eb42a87b..c4a107f9b38 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -18,10 +18,8 @@ var supplyGl3dAxisLayoutDefaults = require('./axis_defaults'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { if(!layoutOut._hasGL3D) return; - var scenes = Plots.getSubplotIdsInData(fullData, 'gl3d'); - - // Get number of scenes to compute default scene domain - var scenesLength = scenes.length; + var scenes = Plots.findSubplotIds(fullData, 'gl3d'), + scenesLength = scenes.length; var sceneLayoutIn, sceneLayoutOut; @@ -59,7 +57,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { * attributes like aspectratio can be written back dynamically. */ - if(layoutIn[scene] !== undefined) sceneLayoutIn = layoutIn[scene]; + // gl3d traces get a layout scene for free! + if(layoutIn[scene]) sceneLayoutIn = layoutIn[scene]; else layoutIn[scene] = sceneLayoutIn = {}; sceneLayoutOut = layoutOut[scene] || {}; diff --git a/src/plots/plots.js b/src/plots/plots.js index 575bad28f51..443951e13bc 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -140,10 +140,53 @@ 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. + * 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 {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, type) { + var subplotIds = []; + + if(plots.subplotsRegistry[type] === undefined) return subplotIds; + + var attr = plots.subplotsRegistry[type].attr; + + for(var i = 0; i < data.length; i++) { + var trace = data[i]; + + if(plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr]) === -1) { + subplotIds.push(trace[attr]); + } + } + + 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 []; @@ -152,36 +195,37 @@ 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; -}; - -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]); - } - } + // 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; }; +/** + * Get the data trace(s) associated with a given subplot. + * + * @param {array} data plotly full data array. + * @param {object} layout plotly full layout object. + * @param {string} subplotId subplot ids to look for. + * + * @return {array} list of trace objects. + * + */ plots.getSubplotData = function getSubplotData(data, type, subplotId) { if(plots.subplotsRegistry[type] === undefined) return []; @@ -447,7 +491,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 artifacts from previous plot calls + cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); /* * Relink functions and underscore attributes to promote consistency between @@ -475,16 +520,46 @@ plots.supplyDefaults = function(gd) { } }; -function cleanScenes(newFullLayout, oldFullLayout) { - var oldSceneKeys = plots.getSubplotIds(oldFullLayout, 'gl3d'); +function cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var i, j; - for(var i = 0; i < oldSceneKeys.length; i++) { - var 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], + oldUid = oldTrace.uid; + + for(j = 0; j < newFullData.length; j++) { + var newTrace = newFullData[j]; + + if(oldUid === newTrace.uid) continue oldLoop; + } + + // clean old heatmap and contour traces + if(hasPaper) { + oldFullLayout._paper.selectAll( + '.hm' + oldUid + + ',.contour' + oldUid + + ',#clip' + oldUid + ).remove(); + } + + // clean old colorbars + if(hasInfoLayer) { + oldFullLayout._infolayer.selectAll('.cb' + oldUid).remove(); + } + } } /** 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(); }; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index c2b7563fa3b..abb8a3c9953 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -167,9 +167,12 @@ describe('Test axes', function() { }); describe('supplyLayoutDefaults', function() { - var layoutIn = {}, - layoutOut = {}, + var layoutIn, layoutOut, fullData; + + beforeEach(function() { + layoutOut = {}; fullData = []; + }); var supplyLayoutDefaults = Axes.supplyLayoutDefaults; @@ -196,7 +199,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'); @@ -205,7 +208,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); @@ -253,6 +256,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('handleTickDefaults', function() { diff --git a/test/jasmine/tests/geo_interact_test.js b/test/jasmine/tests/geo_interact_test.js index 9b50914ca32..854c2f60154 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); @@ -193,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(); }); }); @@ -209,14 +218,48 @@ 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(); }); }); }); + + 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); + + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + expect(countTraces('scattergeo')).toBe(0); + expect(countTraces('choropleth')).toBe(0); + expect(countGeos()).toBe(0, '- trace-less geo subplot are deleted'); + 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/geolayout_test.js b/test/jasmine/tests/geolayout_test.js index f0928e6e48b..01740fb28ad 100644 --- a/test/jasmine/tests/geolayout_test.js +++ b/test/jasmine/tests/geolayout_test.js @@ -8,17 +8,29 @@ 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 = {}; + // if hasGeo is not at this stage, the default step is skipped + layoutOut = { _hasGeo: true }; + + // needs a geo-ref in a trace in order to be detected + fullData = [{ type: 'scattergeo', geo: 'geo' }]; }); + var seaFields = [ + 'showcoastlines', 'coastlinecolor', 'coastlinewidth', + 'showocean', 'oceancolor' + ]; + + var subunitFields = [ + 'showsubunits', 'subunitcolor', 'subunitwidth' + ]; + + var frameFields = [ + 'showframe', 'framecolor', 'framewidth' + ]; + it('should not coerce projection.rotation if type is albers usa', function() { layoutIn = { geo: { @@ -34,19 +46,25 @@ describe('Test Geo layout defaults', function() { supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.geo.projection.rotation).toBeUndefined(); + }); + + it('should not coerce projection.rotation if type is albers usa (converse)', function() { + layoutIn = { + geo: { + projection: { + rotation: { + lon: 10, + lat: 10 + } + } + } + }; - delete layoutIn.geo.projection.type; - layoutOut = {}; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.geo.projection.rotation).toBeDefined(); }); it('should not coerce coastlines and ocean if type is albers usa', function() { - var fields = [ - 'showcoastlines', 'coastlinecolor', 'coastlinewidth', - 'showocean', 'oceancolor' - ]; - layoutIn = { geo: { projection: { @@ -58,14 +76,21 @@ describe('Test Geo layout defaults', function() { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); - fields.forEach(function(field) { + seaFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeUndefined(); }); + }); + + it('should not coerce coastlines and ocean if type is albers usa (converse)', function() { + layoutIn = { + geo: { + showcoastlines: true, + showocean: true + } + }; - delete layoutIn.geo.projection.type; - layoutOut = {}; supplyLayoutDefaults(layoutIn, layoutOut, fullData); - fields.forEach(function(field) { + seaFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeDefined(); }); }); @@ -82,7 +107,7 @@ describe('Test Geo layout defaults', function() { } } }; - layoutOut = {}; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); } @@ -90,93 +115,116 @@ describe('Test Geo layout defaults', function() { testOne(projType); if (projType.indexOf('conic') !== -1) { expect(layoutOut.geo.projection.parallels).toBeDefined(); - } else { + } + else { expect(layoutOut.geo.projection.parallels).toBeUndefined(); } }); }); - it('should coerce subunits only when available ', function() { - var fields = [ - 'showsubunits', 'subunitcolor', 'subunitwidth' - ]; - + it('should coerce subunits only when available (usa case)', function() { layoutIn = { geo: { scope: 'usa' } }; - layoutOut = {}; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); - fields.forEach(function(field) { + subunitFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeDefined(); }); + }); + + it('should coerce subunits only when available (default case)', function() { + layoutIn = { geo: {} }; - delete layoutIn.geo.scope; - layoutOut = {}; supplyLayoutDefaults(layoutIn, layoutOut, fullData); - fields.forEach(function(field) { + subunitFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeUndefined(); }); + }); + it('should coerce subunits only when available (NA case)', function() { layoutIn = { geo: { scope: 'north america', resolution: 50 } }; - layoutOut = {}; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); - fields.forEach(function(field) { + subunitFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeDefined(); }); + }); + it('should coerce subunits only when available (NA case 2)', function() { layoutIn = { geo: { scope: 'north america', resolution: '50' } }; - layoutOut = {}; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); - fields.forEach(function(field) { + subunitFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeDefined(); }); + }); + + it('should coerce subunits only when available (NA case 2)', function() { + layoutIn = { + geo: { + scope: 'north america' + } + }; - delete layoutIn.geo.resolution; - layoutOut = {}; supplyLayoutDefaults(layoutIn, layoutOut, fullData); - fields.forEach(function(field) { + subunitFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeUndefined(); }); }); it('should not coerce frame unless for world scope', function() { - var fields = [ - 'showframe', 'framecolor', 'framewidth' - ], - scopes = layoutAttributes.scope.values; + var scopes = layoutAttributes.scope.values; function testOne(scope) { layoutIn = { geo: { scope: scope } }; - layoutOut = {}; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); } scopes.forEach(function(scope) { testOne(scope); if(scope === 'world') { - fields.forEach(function(field) { + frameFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeDefined(); }); - } else { - fields.forEach(function(field) { + } + else { + frameFields.forEach(function(field) { expect(layoutOut.geo[field]).toBeUndefined(); }); } }); }); + 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..466a24f3314 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -5,16 +5,19 @@ 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'}]; + // if hasGL3D is not at this stage, the default step is skipped + layoutOut = { _hasGL3D: true }; + + // needs a scene-ref in a trace in order to be detected + fullData = [ { type: 'scatter3d', scene: 'scene' }]; }); it('should coerce aspectmode=ratio when ratio data is valid', function() { - var aspectratio = { x: 1, y: 2, @@ -44,7 +47,6 @@ describe('Test Gl3d layout defaults', function() { it('should coerce aspectmode=auto when aspect ratio data is invalid', function() { - var aspectratio = { x: 'g', y: 2, @@ -72,7 +74,6 @@ describe('Test Gl3d layout defaults', function() { it('should coerce manual when valid ratio data but invalid aspectmode', function() { - var aspectratio = { x: 1, y: 2, @@ -100,7 +101,6 @@ describe('Test Gl3d layout defaults', function() { it('should not coerce manual when invalid ratio data but invalid aspectmode', function() { - var aspectratio = { x: 'g', y: 2, @@ -128,7 +128,6 @@ describe('Test Gl3d layout defaults', function() { it('should not coerce manual when valid ratio data and valid non-manual aspectmode', function() { - var aspectratio = { x: 1, y: 2, @@ -155,7 +154,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 +164,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 +192,39 @@ 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 add 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); + }); }); }); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 8aa12b16ad1..4388e0a0778 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -62,6 +62,10 @@ describe('Test gl plot interactions', function() { mouseEvent(type, 605, 271, opts); } + function countCanvases() { + return d3.selectAll('canvas').size(); + } + beforeEach(function(done) { gd = createGraphDiv(); Plotly.plot(gd, mock.data, mock.layout).then(function() { @@ -86,9 +90,6 @@ describe('Test gl plot interactions', function() { }); it('should have', function() { - node = d3.selectAll('canvas'); - expect(node[0].length).toEqual(1, 'one canvas node'); - node = d3.selectAll('g.hovertext'); expect(node.size()).toEqual(1, 'one hover text group'); @@ -142,6 +143,48 @@ describe('Test gl plot interactions', function() { expect(ptData.pointNumber).toEqual(2, 'pointNumber click data'); }); }); + + it('should be able to reversibly change trace type', function(done) { + var sceneLayout = { aspectratio: { x: 1, y: 1, z: 1 } }; + + expect(countCanvases()).toEqual(1); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeUndefined(); + expect(gd.layout.yaxis).toBeUndefined(); + expect(gd._fullLayout._hasGL3D).toBe(true); + expect(gd._fullLayout.scene._scene).toBeDefined(); + + Plotly.restyle(gd, 'type', 'scatter').then(function() { + expect(countCanvases()).toEqual(0); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeDefined(); + expect(gd.layout.yaxis).toBeDefined(); + expect(gd._fullLayout._hasGL3D).toBe(false); + expect(gd._fullLayout.scene).toBeUndefined(); + + return Plotly.restyle(gd, 'type', 'scatter3d'); + }).then(function() { + expect(countCanvases()).toEqual(1); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeDefined(); + expect(gd.layout.yaxis).toBeDefined(); + expect(gd._fullLayout._hasGL3D).toBe(true); + expect(gd._fullLayout.scene._scene).toBeDefined(); + + done(); + }); + }); + + it('should be able to delete the last trace', function(done) { + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countCanvases()).toEqual(0); + expect(gd._fullLayout._hasGL3D).toBe(false); + expect(gd._fullLayout.scene).toBeUndefined(); + + done(); + }); + }); + }); describe('gl2d plots', function() { diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index eae10037d55..8748b9d9bb2 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() { @@ -20,16 +21,34 @@ 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(); + + 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() { - var nodes = d3.selectAll('g.subplot.xy'); - expect(nodes.size()).toEqual(1); + expect(countSubplots()).toEqual(1); }); it('has one *scatterlayer* node', function() { @@ -38,8 +57,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 +79,123 @@ describe('Test plot structure', function() { assertNamespaces(node); }); }); + + 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(); + }); + }); + + 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(); + }); + }); }); 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,40 +220,95 @@ 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); + + 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(3); + assertHeatmapNodes(0); + assertContourNodes(0); + expect(countColorBars()).toEqual(0); + + done(); + }); + }); + + }); + }); 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() { @@ -187,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(); + }); + + }); }); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index ecd24f4fc6c..dd84121e275 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: {} } }; @@ -145,34 +163,28 @@ 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']); }); - }); describe('Plots.register, getModule, and traceIs', function() {