diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 653176fd260..a0544ca5b56 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -24,7 +24,7 @@ module.exports = { values: [true, false, 'legendonly'], role: 'info', dflt: true, - editType: 'calc', + editType: 'plot', description: [ 'Determines whether or not this trace is visible.', 'If *legendonly*, the trace is not drawn,', diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 0ccbc57a4c4..c1619c91fd3 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -216,7 +216,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback var className = (_module.layerName || name + 'layer'); var plotMethod = _module.plot; - // plot all traces of this type on this subplot at once + // plot all visible traces of this type on this subplot at once cdModuleAndOthers = getModuleCalcData(cdSubplot, plotMethod); cdModule = cdModuleAndOthers[0]; // don't need to search the found traces again - in fact we need to NOT diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js index 7e5e7cc0ac6..5a9414054b6 100644 --- a/src/plots/cartesian/type_defaults.js +++ b/src/plots/cartesian/type_defaults.js @@ -108,7 +108,7 @@ function getFirstNonEmptyTrace(data, id, axLetter) { if(trace.type === 'splom' && trace._length > 0 && - trace['_' + axLetter + 'axes'][id] + (trace['_' + axLetter + 'axes'] || {})[id] ) { return trace; } diff --git a/src/plots/get_data.js b/src/plots/get_data.js index 8d6bceb5b20..ca9e35c611b 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -69,6 +69,7 @@ exports.getModuleCalcData = function(calcdata, arg1) { for(var i = 0; i < calcdata.length; i++) { var cd = calcdata[i]; var trace = cd[0].trace; + // N.B. 'legendonly' traces do not make it pass here if(trace.visible !== true) continue; // group calcdata trace not by 'module' (as the name of this function diff --git a/src/plots/plots.js b/src/plots/plots.js index 15fddfc844c..fae7126684e 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -277,6 +277,9 @@ var extraFormatKeys = [ * gd._fullLayout._modules * is a list of all the trace modules required to draw the plot. * + * gd._fullLayout._visibleModules + * subset of _modules, a list of modules corresponding to visible:true traces. + * * gd._fullLayout._basePlotModules * is a list of all the plot modules required to draw the plot. * @@ -378,6 +381,7 @@ plots.supplyDefaults = function(gd, opts) { // clear the lists of trace and baseplot modules, and subplots newFullLayout._modules = []; + newFullLayout._visibleModules = []; newFullLayout._basePlotModules = []; var subplots = newFullLayout._subplots = emptySubplotLists(); @@ -420,7 +424,7 @@ plots.supplyDefaults = function(gd, opts) { newFullLayout._has = plots._hasPlotType.bind(newFullLayout); // special cases that introduce interactions between traces - var _modules = newFullLayout._modules; + var _modules = newFullLayout._visibleModules; for(i = 0; i < _modules.length; i++) { var _module = _modules[i]; if(_module.cleanData) _module.cleanData(newFullData); @@ -696,7 +700,7 @@ plots._hasPlotType = function(category) { if(basePlotModules[i].name === category) return true; } - // check trace modules + // check trace modules (including non-visible:true) var modules = this._modules || []; for(i = 0; i < modules.length; i++) { var name = modules[i].name; @@ -898,6 +902,7 @@ plots.clearExpandedTraceDefaultColors = function(trace) { plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var modules = fullLayout._modules; + var visibleModules = fullLayout._visibleModules; var basePlotModules = fullLayout._basePlotModules; var cnt = 0; var colorCnt = 0; @@ -912,9 +917,9 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var _module = fullTrace._module; if(!_module) return; - if(fullTrace.visible === true) Lib.pushUnique(modules, _module); + Lib.pushUnique(modules, _module); + if(fullTrace.visible === true) Lib.pushUnique(visibleModules, _module); Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); - cnt++; // TODO: do we really want color not to increment for explicitly invisible traces? @@ -1475,7 +1480,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans } // trace module layout defaults - var modules = layoutOut._modules; + var modules = layoutOut._visibleModules; for(i = 0; i < modules.length; i++) { _module = modules[i]; @@ -1579,7 +1584,7 @@ plots.purge = function(gd) { }; plots.style = function(gd) { - var _modules = gd._fullLayout._modules; + var _modules = gd._fullLayout._visibleModules; var styleModules = []; var i; @@ -2567,7 +2572,7 @@ function clearAxesCalc(axList) { plots.doSetPositions = function(gd) { var fullLayout = gd._fullLayout; var subplots = fullLayout._subplots.cartesian; - var modules = fullLayout._modules; + var modules = fullLayout._visibleModules; var methods = []; var i, j; diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index ab398fa97a9..d643e8a5520 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -122,16 +122,18 @@ function calc(gd, trace) { scene.textOptions.push(opts.text); scene.textSelectedOptions.push(opts.textSel); scene.textUnselectedOptions.push(opts.textUnsel); - scene.count++; // stash scene ref stash._scene = scene; - stash.index = scene.count - 1; + stash.index = scene.count; stash.x = x; stash.y = y; stash.positions = positions; stash.count = count; + scene.uid2batchIndex[trace.uid] = stash.index; + scene.count++; + gd.firstscatter = false; return [{x: false, y: false, t: stash, trace: trace}]; } @@ -194,6 +196,8 @@ function sceneUpdate(gd, subplot) { count: 0, // whether scene requires init hook in plot call (dirty plot call) dirty: true, + // trace uid to batch index + uid2batchIndex: {}, // last used options lineOptions: [], fillOptions: [], @@ -208,6 +212,7 @@ function sceneUpdate(gd, subplot) { }; var initOpts = { + visibleBatch: null, selectBatch: null, unselectBatch: null, // regl- component stubs, initialized in dirty plot call @@ -230,19 +235,16 @@ function sceneUpdate(gd, subplot) { // apply new option to all regl components (used on drag) scene.update = function update(opt) { - var i; - var opts = new Array(scene.count); - for(i = 0; i < scene.count; i++) { - opts[i] = opt; - } + var opts = repeat(opt, scene.count); + if(scene.fill2d) scene.fill2d.update(opts); if(scene.scatter2d) scene.scatter2d.update(opts); if(scene.line2d) scene.line2d.update(opts); if(scene.error2d) scene.error2d.update(opts.concat(opts)); if(scene.select2d) scene.select2d.update(opts); if(scene.glText) { - for(i = 0; i < scene.count; i++) { - scene.glText[i].update(opts[i]); + for(var i = 0; i < scene.count; i++) { + scene.glText[i].update(opt); } } @@ -251,38 +253,47 @@ function sceneUpdate(gd, subplot) { // draw traces in proper order scene.draw = function draw() { - var i; - for(i = 0; i < scene.count; i++) { - if(scene.fill2d && scene.fillOptions[i]) { - // must do all fills first - scene.fill2d.draw(i); + var visibleBatch = scene.visibleBatch; + var selectBatch = scene.selectBatch; + var unselectBatch = scene.unselectBatch; + var i, b; + + // must do all fills first + for(i = 0; i < visibleBatch.length; i++) { + b = visibleBatch[i]; + if(scene.fill2d && scene.fillOptions[b]) { + scene.fill2d.draw(b); } } - for(i = 0; i < scene.count; i++) { - if(scene.line2d && scene.lineOptions[i]) { - scene.line2d.draw(i); + + // traces in no-selection mode + for(i = 0; i < visibleBatch.length; i++) { + b = visibleBatch[i]; + if(scene.line2d && scene.lineOptions[b]) { + scene.line2d.draw(b); } - if(scene.error2d && scene.errorXOptions[i]) { - scene.error2d.draw(i); + if(scene.error2d && scene.errorXOptions[b]) { + scene.error2d.draw(b); } - if(scene.error2d && scene.errorYOptions[i]) { - scene.error2d.draw(i + scene.count); + if(scene.error2d && scene.errorYOptions[b]) { + scene.error2d.draw(b + scene.count); } - if(scene.scatter2d && scene.markerOptions[i] && (!scene.selectBatch || !scene.selectBatch[i])) { - // traces in no-selection mode - scene.scatter2d.draw(i); + if(scene.scatter2d && scene.markerOptions[b] && (!selectBatch || !selectBatch[b])) { + scene.scatter2d.draw(b); } } // draw traces in selection mode - if(scene.scatter2d && scene.select2d && scene.selectBatch) { - scene.select2d.draw(scene.selectBatch); - scene.scatter2d.draw(scene.unselectBatch); + if(scene.scatter2d && scene.select2d && selectBatch) { + scene.select2d.draw(selectBatch); + scene.scatter2d.draw(unselectBatch); } - for(i = 0; i < scene.count; i++) { - if(scene.glText[i] && scene.textOptions[i]) { - scene.glText[i].render(); + // draw text, including selected/unselected items + for(i = 0; i < visibleBatch.length; i++) { + b = visibleBatch[i]; + if(scene.glText[b] && scene.textOptions[b]) { + scene.glText[b].render(); } } @@ -290,18 +301,7 @@ function sceneUpdate(gd, subplot) { }; scene.clear = function clear() { - var fullLayout = gd._fullLayout; - var vpSize = fullLayout._size; - var width = fullLayout.width; - var height = fullLayout.height; - var xaxis = subplot.xaxis; - var yaxis = subplot.yaxis; - var vp = [ - vpSize.l + xaxis.domain[0] * vpSize.w, - vpSize.b + yaxis.domain[0] * vpSize.h, - (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, - (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h - ]; + var vp = getViewport(gd._fullLayout, subplot.xaxis, subplot.yaxis); if(scene.select2d) { clearViewport(scene.select2d, vp); @@ -352,6 +352,18 @@ function sceneUpdate(gd, subplot) { return scene; } +function getViewport(fullLayout, xaxis, yaxis) { + var gs = fullLayout._size; + var width = fullLayout.width; + var height = fullLayout.height; + return [ + gs.l + xaxis.domain[0] * gs.w, + gs.b + yaxis.domain[0] * gs.h, + (width - gs.r) - (1 - xaxis.domain[1]) * gs.w, + (height - gs.t) - (1 - yaxis.domain[1]) * gs.h + ]; +} + function clearViewport(comp, vp) { var gl = comp.regl._gl; gl.enable(gl.SCISSOR_TEST); @@ -360,22 +372,25 @@ function clearViewport(comp, vp) { gl.clear(gl.COLOR_BUFFER_BIT); } -function plot(gd, subplot, cdata) { - if(!cdata.length) return; +function repeat(opt, cnt) { + var opts = new Array(cnt); + for(var i = 0; i < cnt; i++) { + opts[i] = opt; + } + return opts; +} - var i; +function plot(gd, subplot, cdata) { + var i, j; var fullLayout = gd._fullLayout; - var scene = cdata[0][0].t._scene; - var dragmode = fullLayout.dragmode; + var scene = subplot._scene; + var xaxis = subplot.xaxis; + var yaxis = subplot.yaxis; // we may have more subplots than initialized data due to Axes.getSubplots method if(!scene) return; - var vpSize = fullLayout._size; - var width = fullLayout.width; - var height = fullLayout.height; - var success = prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); if(!success) { scene.init(); @@ -516,37 +531,22 @@ function plot(gd, subplot, cdata) { } } - var selectMode = dragmode === 'lasso' || dragmode === 'select'; + // form batch arrays, and check for selected points + scene.visibleBatch = []; scene.selectBatch = null; scene.unselectBatch = null; + var dragmode = fullLayout.dragmode; + var selectMode = dragmode === 'lasso' || dragmode === 'select'; - // provide viewport and range - var vpRange = cdata.map(function(cdscatter) { - if(!cdscatter || !cdscatter[0] || !cdscatter[0].trace) return; - var cd = cdscatter[0]; - var trace = cd.trace; - var stash = cd.t; - var id = stash.index; + for(i = 0; i < cdata.length; i++) { + var cd0 = cdata[i][0]; + var trace = cd0.trace; + var stash = cd0.t; + var batchIndex = scene.uid2batchIndex[trace.uid]; var x = stash.x; var y = stash.y; - var xaxis = subplot.xaxis || AxisIDs.getFromId(gd, trace.xaxis || 'x'); - var yaxis = subplot.yaxis || AxisIDs.getFromId(gd, trace.yaxis || 'y'); - var i; - - var range = [ - (xaxis._rl || xaxis.range)[0], - (yaxis._rl || yaxis.range)[0], - (xaxis._rl || xaxis.range)[1], - (yaxis._rl || yaxis.range)[1] - ]; - - var viewport = [ - vpSize.l + xaxis.domain[0] * vpSize.w, - vpSize.b + yaxis.domain[0] * vpSize.h, - (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, - (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h - ]; + scene.visibleBatch.push(batchIndex); if(trace.selectedpoints || selectMode) { if(!selectMode) selectMode = true; @@ -558,37 +558,34 @@ function plot(gd, subplot, cdata) { // regenerate scene batch, if traces number changed during selection if(trace.selectedpoints) { - var selPts = scene.selectBatch[id] = Lib.selIndices2selPoints(trace); + var selPts = scene.selectBatch[batchIndex] = Lib.selIndices2selPoints(trace); var selDict = {}; - for(i = 0; i < selPts.length; i++) { - selDict[selPts[i]] = 1; + for(j = 0; j < selPts.length; j++) { + selDict[selPts[j]] = 1; } var unselPts = []; - for(i = 0; i < stash.count; i++) { - if(!selDict[i]) unselPts.push(i); + for(j = 0; j < stash.count; j++) { + if(!selDict[j]) unselPts.push(j); } - scene.unselectBatch[id] = unselPts; + scene.unselectBatch[batchIndex] = unselPts; } // precalculate px coords since we are not going to pan during select - var xpx = new Array(stash.count); - var ypx = new Array(stash.count); - for(i = 0; i < stash.count; i++) { - xpx[i] = xaxis.c2p(x[i]); - ypx[i] = yaxis.c2p(y[i]); + // TODO, could do better here e.g. + // - spin that in a webworker + // - compute selection from polygons in data coordinates + // (maybe just for linear axes) + var xpx = stash.xpx = new Array(stash.count); + var ypx = stash.ypx = new Array(stash.count); + for(j = 0; j < stash.count; j++) { + xpx[j] = xaxis.c2p(x[j]); + ypx[j] = yaxis.c2p(y[j]); } - stash.xpx = xpx; - stash.ypx = ypx; - } - else { + } else { stash.xpx = stash.ypx = null; } - - return trace.visible ? - {viewport: viewport, range: range} : - null; - }); + } if(selectMode) { // create select2d @@ -618,6 +615,19 @@ function plot(gd, subplot, cdata) { } } + // provide viewport and range + var vpRange0 = { + viewport: getViewport(fullLayout, xaxis, yaxis), + // TODO do we need those fallbacks? + range: [ + (xaxis._rl || xaxis.range)[0], + (yaxis._rl || yaxis.range)[0], + (xaxis._rl || xaxis.range)[1], + (yaxis._rl || yaxis.range)[1] + ] + }; + var vpRange = repeat(vpRange0, scene.count); + // upload viewport/range data to GPU if(scene.fill2d) { scene.fill2d.update(vpRange); @@ -635,14 +645,10 @@ function plot(gd, subplot, cdata) { scene.select2d.update(vpRange); } if(scene.glText) { - scene.glText.forEach(function(text, i) { - text.update(vpRange[i]); - }); + scene.glText.forEach(function(text) { text.update(vpRange0); }); } scene.draw(); - - return; } diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index 963f9d98657..e82b8db30ea 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -54,6 +54,7 @@ function plot(container, subplot, cdata) { var scene = ScatterGl.sceneUpdate(container, subplot); scene.clear(); + scene.visibleBatch = []; cdata.forEach(function(cdscatter, traceIndex) { if(!cdscatter || !cdscatter[0] || !cdscatter[0].trace) return; @@ -64,6 +65,8 @@ function plot(container, subplot, cdata) { var thetaArray = stash.theta; var i, r, rr, theta, rad; + scene.uid2batchIndex[trace.uid] = traceIndex; + var subRArray = rArray.slice(); var subThetaArray = thetaArray.slice(); diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index 3cb326a940f..b44ba9008c2 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -194,14 +194,17 @@ describe('restyle', function() { }) .then(function() { expect(!!gd._fullLayout._plots.x2y2._scene).toBe(true); + expect(gd._fullLayout._plots.x2y2._scene.visibleBatch).toEqual([0]); return Plotly.restyle(gd, {visible: 'legendonly'}, 1); }) .then(function() { - expect(!!gd._fullLayout._plots.x2y2._scene).toBe(false); + expect(!!gd._fullLayout._plots.x2y2._scene).toBe(true); + expect(gd._fullLayout._plots.x2y2._scene.visibleBatch).toEqual([]); return Plotly.restyle(gd, {visible: true}, 1); }) .then(function() { expect(!!gd._fullLayout._plots.x2y2._scene).toBe(true); + expect(gd._fullLayout._plots.x2y2._scene.visibleBatch).toEqual([0]); }) .catch(failTest) .then(done); diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js index 94516d5b7b7..827d283c94d 100644 --- a/test/jasmine/tests/gl2d_plot_interact_test.js +++ b/test/jasmine/tests/gl2d_plot_interact_test.js @@ -358,27 +358,38 @@ describe('@gl Test gl2d plots', function() { var _mock = Lib.extendDeep({}, mock); _mock.data[0].line.width = 5; + function assertDrawCall(msg, exp) { + var draw = gd._fullLayout._plots.xy._scene.scatter2d.draw; + expect(draw).toHaveBeenCalledTimes(exp, msg); + draw.calls.reset(); + } + Plotly.plot(gd, _mock) .then(delay(30)) .then(function() { + spyOn(gd._fullLayout._plots.xy._scene.scatter2d, 'draw'); return Plotly.restyle(gd, 'visible', 'legendonly'); }) .then(function() { - expect(gd.querySelector('.gl-canvas-context')).toBe(null); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).toBe(0); + assertDrawCall('legendonly', 0); return Plotly.restyle(gd, 'visible', true); }) .then(function() { expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); + assertDrawCall('back to visible', 1); return Plotly.restyle(gd, 'visible', false); }) .then(function() { - expect(gd.querySelector('.gl-canvas-context')).toBe(null); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).toBe(0); + assertDrawCall('visible false', 0); return Plotly.restyle(gd, 'visible', true); }) .then(function() { + assertDrawCall('back up', 1); expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); }) .catch(failTest) diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index e894ab7fe88..a431d2fd8bd 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -579,6 +579,41 @@ describe('@gl Test splom interactions:', function() { .catch(failTest) .then(done); }); + + it('should toggle trace correctly', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); + + function _assert(msg, exp) { + for(var i = 0; i < 3; i++) { + var draw = gd.calcdata[i][0].t._scene.draw; + expect(draw).toHaveBeenCalledTimes(exp[i], msg + ' - trace ' + i); + draw.calls.reset(); + } + } + + Plotly.plot(gd, fig).then(function() { + spyOn(gd.calcdata[0][0].t._scene, 'draw'); + spyOn(gd.calcdata[1][0].t._scene, 'draw'); + spyOn(gd.calcdata[2][0].t._scene, 'draw'); + + return Plotly.restyle(gd, 'visible', 'legendonly', [0, 2]); + }) + .then(function() { + _assert('0-2 legendonly', [0, 1, 0]); + + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + _assert('all gone', [0, 0, 0]); + + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('all back', [1, 1, 1]); + }) + .catch(failTest) + .then(done); + }); }); describe('@gl Test splom hover:', function() { diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index 2c838336973..a1de229e472 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -15,6 +15,7 @@ function supplyDataDefaults(dataIn, dataOut) { return Plots.supplyDataDefaults(dataIn, dataOut, {}, { _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, _modules: [], + _visibleModules: [], _basePlotModules: [], _traceUids: dataIn.map(function() { return Lib.randstr(); }) }); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 5337fb0b388..f834135c0eb 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -16,6 +16,7 @@ var assertStyle = customAssertions.assertStyle; var mockFullLayout = { _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, _modules: [], + _visibleModules: [], _basePlotModules: [], _has: function() {}, _dfltTitle: {x: 'xxx', y: 'yyy'},