diff --git a/devtools/test_dashboard/buttons.js b/devtools/test_dashboard/buttons.js index 94f282f97c6..3dab386271c 100644 --- a/devtools/test_dashboard/buttons.js +++ b/devtools/test_dashboard/buttons.js @@ -2,7 +2,7 @@ var Lib = require('@src/lib'); -var plotlist = document.getElementById('plot-list'); +var plotList = document.getElementById('plot-list'); var anchor = document.getElementById('embedded-graph'); var image = document.getElementById('embedded-image'); @@ -14,31 +14,30 @@ anchor.style.height = '600px'; anchor.style.width = '1000px'; function plotButtons(plots, figDir) { - Object.keys(plots).forEach(function(plotname) { - var button = document.createElement('button'); button.style.cssFloat = 'left'; button.style.width = '100px'; button.style.height = '40px'; - button.innerHTML = plotname; - plotlist.appendChild(button); + plotList.appendChild(button); button.addEventListener('click', function() { - var myImage = new Image(); myImage.src = figDir + plotname + '.png'; image.innerHTML = ''; image.appendChild(myImage); - - anchor.innerHTML = ''; + var currentGraphDiv = Tabs.getGraph(); + if(currentGraphDiv) Plotly.purge(currentGraphDiv); gd = document.createElement('div'); + gd.id = 'graph'; + + anchor.innerHTML = ''; anchor.appendChild(gd); var plot = plots[plotname]; @@ -58,7 +57,7 @@ function plotButtons(plots, figDir) { snapshot.innerHTML = 'snapshot'; snapshot.style.background = 'blue'; - plotlist.appendChild(snapshot); + plotList.appendChild(snapshot); snapshot.addEventListener('click', function() { @@ -111,7 +110,7 @@ function plotButtons(plots, figDir) { pummelButton.style.marginLeft = '25px'; pummelButton.innerHTML = 'pummel3d'; pummelButton.style.background = 'blue'; - plotlist.appendChild(pummelButton); + plotList.appendChild(pummelButton); var i = 0; var mock = require('@mocks/gl3d_marker-color.json'); @@ -147,7 +146,7 @@ function plotButtons(plots, figDir) { scrapeButton.style.marginLeft = '25px'; scrapeButton.innerHTML = 'scrape SVG'; scrapeButton.style.background = 'blue'; - plotlist.appendChild(scrapeButton); + plotList.appendChild(scrapeButton); scrapeButton.addEventListener('click', function() { Plotly.Snapshot.toSVG(Tabs.get()); diff --git a/devtools/test_dashboard/index.html b/devtools/test_dashboard/index.html index 9d1f78ef45a..20410f98f7b 100644 --- a/devtools/test_dashboard/index.html +++ b/devtools/test_dashboard/index.html @@ -27,6 +27,7 @@ getGraph: function() { return document.getElementById('embedded-graph').children[0]; }, + fresh: function() { var anchor = document.getElementById('embedded-graph'), graphDiv = Tabs.getGraph(); @@ -37,6 +38,7 @@ return graphDiv; }, + plotMock: function(mockName) { var mockURL = '../../test/image/mocks/' + mockName + '.json'; diff --git a/src/core.js b/src/core.js index 8bee6850352..489b2a8fbc3 100644 --- a/src/core.js +++ b/src/core.js @@ -28,6 +28,7 @@ exports.prependTraces = Plotly.prependTraces; exports.addTraces = Plotly.addTraces; exports.deleteTraces = Plotly.deleteTraces; exports.moveTraces = Plotly.moveTraces; +exports.purge = Plotly.purge; exports.setPlotConfig = require('./plot_api/set_plot_config'); exports.register = Plotly.register; diff --git a/src/lib/events.js b/src/lib/events.js index e4d67a53d90..7be09deedb0 100644 --- a/src/lib/events.js +++ b/src/lib/events.js @@ -114,7 +114,19 @@ var Events = { */ return jQueryHandlerValue !== undefined ? jQueryHandlerValue : nodeEventHandlerValue; + }, + + purge: function(plotObj) { + delete plotObj._ev; + delete plotObj.on; + delete plotObj.once; + delete plotObj.removeListener; + delete plotObj.removeAllListeners; + delete plotObj.emit; + + return plotObj; } + }; module.exports = Events; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index fceffae379d..1753264abf2 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2406,6 +2406,39 @@ Plotly.relayout = function relayout(gd, astr, val) { }); }; +/** + * Purge a graph container div back to its initial pre-Plotly.plot state + * + * @param {string id or DOM element} gd + * the id or DOM element of the graph container div + */ +Plotly.purge = function purge(gd) { + gd = getGraphDiv(gd); + + var fullLayout = gd._fullLayout || {}, + fullData = gd._fullData || []; + + // remove gl contexts + Plots.cleanPlot([], {}, fullData, fullLayout); + + // purge properties + Plots.purge(gd); + + // purge event emitter methods + Events.purge(gd); + + // remove plot container + if(fullLayout._container) fullLayout._container.remove(); + + delete gd._context; + delete gd._replotPending; + delete gd._mouseDownTime; + delete gd._hmpixcount; + delete gd._hmlumcount; + + return gd; +}; + /** * Reduce all reserved margin objects to a single required margin reservation. * @@ -2505,7 +2538,7 @@ function makePlotFramework(gd) { // Make the svg container fullLayout._paperdiv = fullLayout._container.selectAll('.svg-container').data([0]); fullLayout._paperdiv.enter().append('div') - .classed('svg-container',true) + .classed('svg-container', true) .style('position','relative'); // Initial autosize diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js index 22288ad4d69..d8bb69580b9 100644 --- a/src/plots/gl2d/index.js +++ b/src/plots/gl2d/index.js @@ -65,3 +65,17 @@ exports.plot = function plotGl2d(gd) { scene.plot(fullSubplotData, fullLayout, gd.layout); } }; + +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, 'gl2d'); + + for(var i = 0; i < oldSceneKeys.length; i++) { + var oldSubplot = oldFullLayout._plots[oldSceneKeys[i]], + xaName = oldSubplot.xaxis._name, + yaName = oldSubplot.yaxis._name; + + if(!!oldSubplot._scene2d && (!newFullLayout[xaName] || !newFullLayout[yaName])) { + oldSubplot._scene2d.destroy(); + } + } +}; diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 0cc7ddb847b..d386b47d58d 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -292,6 +292,13 @@ proto.cameraChanged = function() { proto.destroy = function() { this.glplot.dispose(); + + this.container.removeChild(this.canvas); + this.container.removeChild(this.svgContainer); + this.container.removeChild(this.mouseContainer); + + this.glplot = null; + this.stopped = true; }; proto.plot = function(fullData, fullLayout) { @@ -405,6 +412,7 @@ proto.plot = function(fullData, fullLayout) { proto.draw = function() { if(this.stopped) return; + requestAnimationFrame(this.redraw); var glplot = this.glplot, diff --git a/src/plots/plots.js b/src/plots/plots.js index a65162ffc92..6d3693b754c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -492,7 +492,7 @@ plots.supplyDefaults = function(gd) { plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData); // clean subplots and other artifacts from previous plot calls - cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); + plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); /* * Relink functions and underscore attributes to promote consistency between @@ -520,7 +520,7 @@ plots.supplyDefaults = function(gd) { } }; -function cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout) { +plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { var i, j; var plotTypes = Object.keys(subplotsRegistry); @@ -560,7 +560,7 @@ function cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout) { oldFullLayout._infolayer.selectAll('.cb' + oldUid).remove(); } } -} +}; /** * Relink private _keys and keys with a function value from one layout @@ -755,16 +755,13 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { } }; +// Remove all plotly attributes from a div so it can be replotted fresh +// TODO: these really need to be encapsulated into a much smaller set... plots.purge = function(gd) { - // remove all plotly attributes from a div so it can be replotted fresh - // TODO: these really need to be encapsulated into a much smaller set... // note: we DO NOT remove _context because it doesn't change when we insert // a new plot, and may have been set outside of our scope. - // clean up the gl and geo containers - // TODO unify subplot creation/update with d3.selection.order - // and/or subplot ids var fullLayout = gd._fullLayout || {}; if(fullLayout._glcontainer !== undefined) fullLayout._glcontainer.remove(); if(fullLayout._geocontainer !== undefined) fullLayout._geocontainer.remove(); diff --git a/test/jasmine/tests/events_test.js b/test/jasmine/tests/events_test.js index c68ccfd6cb2..98a45e6314a 100644 --- a/test/jasmine/tests/events_test.js +++ b/test/jasmine/tests/events_test.js @@ -181,8 +181,15 @@ describe('Events', function() { expect(eventBaton).toBe(3); expect(result).toBe('pong'); }); + }); + describe('purge', function() { + it('should remove all method from the plotObj', function() { + Events.init(plotObj); + Events.purge(plotObj); + expect(plotObj).toEqual({}); + }); }); }); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 4388e0a0778..92607cf5d42 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -36,14 +36,19 @@ describe('Test gl plot interactions', function() { sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); sceneIds.forEach(function(id) { - fullLayout[id]._scene.destroy(); + var scene = fullLayout[id]._scene; + + if(scene.glplot) scene.destroy(); }); sceneIds = Plots.getSubplotIds(fullLayout, 'gl2d'); sceneIds.forEach(function(id) { var scene2d = fullLayout._plots[id]._scene2d; - scene2d.stopped = true; - scene2d.destroy(); + + if(scene2d.glplot) { + scene2d.stopped = true; + scene2d.destroy(); + } }); destroyGraphDiv(); @@ -369,4 +374,43 @@ describe('Test gl plot interactions', function() { }); }); + + describe('Plots.cleanPlot', function() { + + it('should remove gl context from the graph div of a gl3d plot', function(done) { + gd = createGraphDiv(); + + var mockData = [{ + type: 'scatter3d' + }]; + + Plotly.plot(gd, mockData).then(function() { + expect(gd._fullLayout.scene._scene.glplot).toBeDefined(); + + Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); + expect(gd._fullLayout.scene._scene.glplot).toBe(null); + + done(); + }); + }); + + it('should remove gl context from the graph div of a gl2d plot', function(done) { + gd = createGraphDiv(); + + var mockData = [{ + type: 'scattergl', + x: [1,2,3], + y: [1,2,3] + }]; + + Plotly.plot(gd, mockData).then(function() { + expect(gd._fullLayout._plots.xy._scene2d.glplot).toBeDefined(); + + Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); + expect(gd._fullLayout._plots.xy._scene2d.glplot).toBe(null); + + done(); + }); + }); + }); }); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 9e71c4ca288..6d183f0b14d 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -620,6 +620,27 @@ describe('Test plot api', function() { }); }); + describe('Plotly.purge', function() { + + afterEach(destroyGraphDiv); + + it('should return the graph div in its original state', function(done) { + var gd = createGraphDiv(); + var initialKeys = Object.keys(gd); + var intialHTML = gd.innerHTML; + var mockData = [{ x: [1,2,3], y: [2,3,4] }]; + + Plotly.plot(gd, mockData).then(function() { + Plotly.purge(gd); + + expect(Object.keys(gd)).toEqual(initialKeys); + expect(gd.innerHTML).toEqual(intialHTML); + + done(); + }); + }); + }); + describe('cleanData', function() { var gd;