diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f1ab41db719..d545a84d564 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]; } @@ -501,6 +504,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; @@ -2144,8 +2148,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 +2173,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 +2330,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..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/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/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/src/plots/plots.js b/src/plots/plots.js index 3b1f9ad9d0a..8e8c5a8e01c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -140,10 +140,65 @@ 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 are 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; +}; + +/** + * 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 +207,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 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 []; @@ -389,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 || {}, @@ -447,7 +503,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,17 +532,46 @@ 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]; + var 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(); + } + } } /** @@ -645,7 +731,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 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/image/baselines/plot_types_blank.png b/test/image/baselines/plot_types_blank.png new file mode 100644 index 00000000000..3eadefc654c Binary files /dev/null and b/test/image/baselines/plot_types_blank.png differ diff --git a/test/image/mocks/plot_types_blank.json b/test/image/mocks/plot_types_blank.json new file mode 100644 index 00000000000..73e242aea06 --- /dev/null +++ b/test/image/mocks/plot_types_blank.json @@ -0,0 +1,63 @@ +{ + "data": [], + "layout": { + "xaxis": { + "domain": [ + 0, + 0.5 + ] + }, + "yaxis": { + "domain": [ + 0, + 0.5 + ] + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0.5, + 1 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.5, + 1 + ] + }, + "scene": { + "domain": { + "x": [ + 0, + 0.5 + ], + "y": [ + 0.5, + 1 + ] + }, + "aspectratio": { + "x": 1, + "y": 1, + "z": 1 + } + }, + "geo": { + "domain": { + "x": [ + 0.5, + 1 + ], + "y": [ + 0, + 0.5 + ] + } + }, + "height": 450, + "width": 1000, + "autosize": true + } +} 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() { diff --git a/test/jasmine/tests/geo_interact_test.js b/test/jasmine/tests/geo_interact_test.js index 9b50914ca32..9e421709fd0 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(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/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); + }); }); }); 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..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: {} } }; @@ -145,34 +163,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() {