From 56f2ee936a0e0d84defeea1556ee051987fe55bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 18 Sep 2018 15:24:30 -0400 Subject: [PATCH 01/29] update scattergl & splom 'text' attr description - scattergl 'text' now has the same behavior a scatter - splom 'text' is hover-only --- src/traces/scattergl/attributes.js | 10 +--------- src/traces/splom/attributes.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index 385fb765742..94731763ffc 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -28,15 +28,7 @@ var attrs = module.exports = overrideAll({ y0: scatterAttrs.y0, dy: scatterAttrs.dy, - text: extendFlat({}, scatterAttrs.text, { - description: [ - 'Sets text elements associated with each (x,y) pair to appear on hover.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (x,y) coordinates.' - ].join(' ') - }), + text: scatterAttrs.text, hovertext: scatterAttrs.hovertext, textposition: scatterAttrs.textposition, diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index dbc9169bbe4..18ef1c76c9c 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -11,6 +11,7 @@ var scatterGlAttrs = require('../scattergl/attributes'); var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; var templatedArray = require('../../plot_api/plot_template').templatedArray; +var extendFlat = require('../../lib/extend').extendFlat; function makeAxesValObject(axLetter) { return { @@ -86,7 +87,15 @@ module.exports = { // mode: {}, (only 'markers' for now) - text: scatterGlAttrs.text, + text: extendFlat({}, scatterGlAttrs.text, { + description: [ + 'Sets text elements associated with each (x,y) pair to appear on hover.', + 'If a single string, the same string appears over', + 'all the data points.', + 'If an array of string, the items are mapped in order to the', + 'this trace\'s (x,y) coordinates.' + ].join(' ') + }), marker: scatterGlAttrs.marker, xaxes: makeAxesValObject('x'), From 3574ece2a1eff74e1009055d9bb1ca699bafc1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 18:44:50 -0400 Subject: [PATCH 02/29] move splom scene ref to fullLayout ... and out of the gd.calcdata[i][0].t stash, so that: - scene refs can be relinked properly on updates - scene refs can be destroyed properly when needed - move also visibileDims out of calcdata stash to fullData[i], for convenience. --- src/components/fx/hover.js | 4 ++ src/plots/cartesian/select.js | 4 +- src/traces/splom/base_plot.js | 65 ++++++++++++++------------------ src/traces/splom/index.js | 59 ++++++++++++++++------------- test/jasmine/tests/splom_test.js | 41 +++++++++++++------- 5 files changed, 95 insertions(+), 78 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 73ec3f3abca..fd798c3674c 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -396,6 +396,10 @@ function _hover(gd, evt, subplot, noHoverEvent) { if(fullLayout[subplotId]) { pointData.subplot = fullLayout[subplotId]._subplot; } + // add ref to splom scene + if(fullLayout._splomScenes && fullLayout._splomScenes[trace.uid]) { + pointData.scene = fullLayout._splomScenes[trace.uid]; + } closedataPreviousLength = hoverData.length; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index fdc869f8690..31200f8fb1f 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -514,7 +514,9 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) { // FIXME: make sure we don't have more than single axis for splom trace._xaxes[xAxisIds[0]] && trace._yaxes[yAxisIds[0]] ) { - searchTraces.push(createSearchInfo(trace._module, cd, xAxes[0], yAxes[0])); + var info = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]); + info.scene = gd._fullLayout._splomScenes[trace.uid]; + searchTraces.push(info); } else { if(xAxisIds.indexOf(trace.xaxis) === -1) continue; if(yAxisIds.indexOf(trace.yaxis) === -1) continue; diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index c1489ef2fe2..1b2939eb152 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -45,19 +45,18 @@ function drag(gd) { for(var i = 0; i < cd.length; i++) { var cd0 = cd[i][0]; var trace = cd0.trace; - var stash = cd0.t; - var scene = stash._scene; + var scene = fullLayout._splomScenes[trace.uid]; if(trace.type === 'splom' && scene && scene.matrix) { - dragOne(gd, trace, stash, scene); + dragOne(gd, trace, scene); } } } -function dragOne(gd, trace, stash, scene) { +function dragOne(gd, trace, scene) { var visibleLength = scene.matrixOptions.data.length; - var visibleDims = stash.visibleDims; - var ranges = new Array(visibleLength); + var visibleDims = trace._visibleDims; + var ranges = scene.viewOpts.ranges = new Array(visibleLength); for(var k = 0; k < visibleDims.length; k++) { var i = visibleDims[k]; @@ -168,46 +167,40 @@ function makeGridData(gd) { return gridBatches; } -function clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) { - var oldModules = oldFullLayout._modules || []; - var newModules = newFullLayout._modules || []; +function clean(newFullData, newFullLayout, oldFullData, oldFullLayout) { + oldLoop: + for(var i = 0; i < oldFullData.length; i++) { + var oldTrace = oldFullData[i]; - var hadSplom, hasSplom; - var i; + if(oldTrace.type === 'splom') { + for(var j = 0; j < newFullData.length; j++) { + var newTrace = newFullData[j]; - for(i = 0; i < oldModules.length; i++) { - if(oldModules[i].name === 'splom') { - hadSplom = true; - break; - } - } - for(i = 0; i < newModules.length; i++) { - if(newModules[i].name === 'splom') { - hasSplom = true; - break; - } - } + if(oldTrace.uid === newTrace.uid && newTrace.type === 'splom') { + continue oldLoop; + } + } - if(hadSplom && !hasSplom) { - for(i = 0; i < oldCalcdata.length; i++) { - var cd0 = oldCalcdata[i][0]; - var trace = cd0.trace; - var scene = cd0.t._scene; - - if( - trace.type === 'splom' && - scene && scene.matrix && scene.matrix.destroy - ) { - scene.matrix.destroy(); - cd0.t._scene = null; + if(oldFullLayout._splomScenes) { + var scene = oldFullLayout._splomScenes[oldTrace.uid]; + if(scene && scene.destroy) scene.destroy(); + // must first set scene to null in order to get garbage collected + oldFullLayout._splomScenes[oldTrace.uid] = null; + delete oldFullLayout._splomScenes[oldTrace.uid]; } } } + if(Object.keys(oldFullLayout._splomScenes || {}).length === 0) { + delete oldFullLayout._splomScenes; + } + if(oldFullLayout._splomGrid && (!newFullLayout._hasOnlyLargeSploms && oldFullLayout._hasOnlyLargeSploms)) { + // must first set scene to null in order to get garbage collected oldFullLayout._splomGrid.destroy(); oldFullLayout._splomGrid = null; + delete oldFullLayout._splomGrid; } Cartesian.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); @@ -228,7 +221,7 @@ function updateFx(gd) { var trace = cd0.trace; if(trace.type === 'splom') { - var scene = cd0.t._scene; + var scene = fullLayout._splomScenes[trace.uid]; if(scene.selectBatch === null) { scene.matrix.update(scene.matrixOptions, null); } diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index cbbade4c81b..903f420e6fe 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -30,14 +30,13 @@ var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS; function calc(gd, trace) { var dimensions = trace.dimensions; var commonLength = trace._length; - var stash = {}; var opts = {}; // 'c' for calculated, 'l' for linear, // only differ here for log axes, pass ldata to createMatrix as 'data' var cdata = opts.cdata = []; var ldata = opts.data = []; // keep track of visible dimensions - var visibleDims = stash.visibleDims = []; + var visibleDims = trace._visibleDims = []; var i, k, dim, xa, ya; function makeCalcdata(ax, dim) { @@ -107,22 +106,27 @@ function calc(gd, trace) { calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad); } - var scene = stash._scene = sceneUpdate(gd, stash); + var scene = sceneUpdate(gd, trace); if(!scene.matrix) scene.matrix = true; scene.matrixOptions = opts; scene.selectedOptions = convertMarkerSelection(trace, trace.selected); scene.unselectedOptions = convertMarkerSelection(trace, trace.unselected); - return [{x: false, y: false, t: stash, trace: trace}]; + return [{x: false, y: false, t: {}, trace: trace}]; } -function sceneUpdate(gd, stash) { - var scene = stash._scene; +function sceneUpdate(gd, trace) { + var fullLayout = gd._fullLayout; + var uid = trace.uid; - var reset = { - dirty: true - }; + // must place ref to 'scene' in fullLayout, so that: + // - it can be relinked properly on updates + // - it can be destroyed properly when needed + var splomScenes = fullLayout._splomScenes; + if(!splomScenes) splomScenes = fullLayout._splomScenes = {}; + + var reset = {dirty: true}; var first = { selectBatch: null, @@ -131,8 +135,10 @@ function sceneUpdate(gd, stash) { select: null }; + var scene = splomScenes[trace.uid]; + if(!scene) { - scene = stash._scene = Lib.extendFlat({}, reset, first); + scene = splomScenes[uid] = Lib.extendFlat({}, reset, first); scene.draw = function draw() { // draw traces in selection mode @@ -147,13 +153,13 @@ function sceneUpdate(gd, stash) { // remove scene resources scene.destroy = function destroy() { - if(scene.matrix) scene.matrix.destroy(); - + if(scene.matrix && scene.matrix.destroy) { + scene.matrix.destroy(); + } scene.matrixOptions = null; scene.selectBatch = null; scene.unselectBatch = null; - - stash._scene = null; + scene = null; }; } @@ -178,7 +184,7 @@ function plotOne(gd, cd0) { var gs = fullLayout._size; var trace = cd0.trace; var stash = cd0.t; - var scene = stash._scene; + var scene = fullLayout._splomScenes[trace.uid]; var matrixOpts = scene.matrixOptions; var cdata = matrixOpts.cdata; var regl = fullLayout._glcanvas.data()[0].regl; @@ -194,7 +200,7 @@ function plotOne(gd, cd0) { matrixOpts.upper = trace.showlowerhalf; matrixOpts.diagonal = trace.diagonal.visible; - var visibleDims = stash.visibleDims; + var visibleDims = trace._visibleDims; var visibleLength = cdata.length; var viewOpts = {}; viewOpts.ranges = new Array(visibleLength); @@ -305,8 +311,7 @@ function plotOne(gd, cd0) { function hoverPoints(pointData, xval, yval) { var cd = pointData.cd; var trace = cd[0].trace; - var stash = cd[0].t; - var scene = stash._scene; + var scene = pointData.scene; var cdata = scene.matrixOptions.cdata; var xa = pointData.xa; var ya = pointData.ya; @@ -314,8 +319,8 @@ function hoverPoints(pointData, xval, yval) { var ypx = ya.c2p(yval); var maxDistance = pointData.distance; - var xi = getDimIndex(trace, stash, xa); - var yi = getDimIndex(trace, stash, ya); + var xi = getDimIndex(trace, xa); + var yi = getDimIndex(trace, ya); if(xi === false || yi === false) return [pointData]; var x = cdata[xi]; @@ -352,7 +357,7 @@ function selectPoints(searchInfo, selectionTester) { var cd = searchInfo.cd; var trace = cd[0].trace; var stash = cd[0].t; - var scene = stash._scene; + var scene = searchInfo.scene; var cdata = scene.matrixOptions.cdata; var xa = searchInfo.xaxis; var ya = searchInfo.yaxis; @@ -364,8 +369,8 @@ function selectPoints(searchInfo, selectionTester) { var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace)); if(trace.visible !== true || hasOnlyLines) return selection; - var xi = getDimIndex(trace, stash, xa); - var yi = getDimIndex(trace, stash, ya); + var xi = getDimIndex(trace, xa); + var yi = getDimIndex(trace, ya); if(xi === false || yi === false) return selection; var xpx = stash.xpx[xi]; @@ -424,7 +429,7 @@ function style(gd, cds) { var fullLayout = gd._fullLayout; var cd0 = cds[0]; - var scene0 = cd0[0].t._scene; + var scene0 = fullLayout._splomScenes[cd0[0].trace.uid]; scene0.matrix.regl.clear({color: true, depth: true}); if(fullLayout._splomGrid) { @@ -432,7 +437,7 @@ function style(gd, cds) { } for(var i = 0; i < cds.length; i++) { - var scene = cds[i][0].t._scene; + var scene = fullLayout._splomScenes[cds[i][0].trace.uid]; scene.draw(); } @@ -446,11 +451,11 @@ function style(gd, cds) { } } -function getDimIndex(trace, stash, ax) { +function getDimIndex(trace, ax) { var axId = ax._id; var axLetter = axId.charAt(0); var ind = {x: 0, y: 1}[axLetter]; - var visibleDims = stash.visibleDims; + var visibleDims = trace._visibleDims; for(var k = 0; k < visibleDims.length; k++) { var i = visibleDims[k]; diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index b44bf5d5917..5014af82fed 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -422,10 +422,11 @@ describe('Test splom trace calc step:', function() { yaxis: {type: 'linear'} }); - var cd = gd.calcdata[0][0]; + var trace = gd._fullData[0]; + var scene = gd._fullLayout._splomScenes[trace.uid]; - expect(cd.t._scene.matrixOptions.data).toBeCloseTo2DArray([[2, 1, 2]]); - expect(cd.t.visibleDims).toEqual([1]); + expect(scene.matrixOptions.data).toBeCloseTo2DArray([[2, 1, 2]]); + expect(trace._visibleDims).toEqual([1]); expect(Lib.log).toHaveBeenCalledTimes(1); expect(Lib.log).toHaveBeenCalledWith('Skipping splom dimension 0 with conflicting axis types'); }); @@ -448,13 +449,14 @@ describe('Test splom interactions:', function() { Plotly.plot(gd, fig).then(function() { expect(gd._fullLayout._splomGrid).toBeDefined(); - expect(gd.calcdata[0][0].t._scene).toBeDefined(); + expect(gd._fullLayout._splomScenes).toBeDefined(); + expect(Object.keys(gd._fullLayout._splomScenes).length).toBe(1); - return Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout, gd.calcdata); + return Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); }) .then(function() { - expect(gd._fullLayout._splomGrid).toBe(null); - expect(gd.calcdata[0][0].t._scene).toBe(null); + expect(gd._fullLayout._splomGrid).toBeUndefined(); + expect(gd._fullLayout._splomScenes).toBeUndefined(); }) .catch(failTest) .then(done); @@ -658,14 +660,22 @@ describe('Test splom interactions:', function() { var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); function _assert(msg, exp) { + var splomScenes = gd._fullLayout._splomScenes; + var ids = Object.keys(splomScenes); + for(var i = 0; i < 3; i++) { - expect(Boolean(gd.calcdata[i][0].t._scene)) - .toBe(Boolean(exp[i]), msg + ' - trace ' + i); + var drawFn = splomScenes[ids[i]].draw; + expect(drawFn).toHaveBeenCalledTimes(exp[i], msg + ' - trace ' + i); + drawFn.calls.reset(); } } Plotly.plot(gd, fig).then(function() { - _assert('base', [1, 1, 1]); + var splomScenes = gd._fullLayout._splomScenes; + for(var k in splomScenes) { + spyOn(splomScenes[k], 'draw').and.callThrough(); + } + return Plotly.restyle(gd, 'visible', 'legendonly', [0, 2]); }) .then(function() { @@ -890,7 +900,8 @@ describe('Test splom drag:', function() { Plotly.plot(gd, fig) .then(function() { - var scene = gd.calcdata[0][0].t._scene; + var uid = gd._fullData[0].uid; + var scene = gd._fullLayout._splomScenes[uid]; spyOn(scene.matrix, 'update'); spyOn(scene.matrix, 'draw'); @@ -906,7 +917,8 @@ describe('Test splom drag:', function() { }) .then(function() { return _drag([130, 130], [150, 150]); }) .then(function() { - var scene = gd.calcdata[0][0].t._scene; + var uid = gd._fullData[0].uid; + var scene = gd._fullLayout._splomScenes[uid]; // N.B. _drag triggers two updateSubplots call // - 1 update and 1 draw call per updateSubplot // - 2 update calls (1 for data, 1 for view opts) @@ -1107,7 +1119,7 @@ describe('Test splom select:', function() { grid: {xgap: 0, ygap: 0} }; - var scene; + var uid, scene; function _assert(msg, exp) { expect(scene.matrix.update).toHaveBeenCalledTimes(exp.updateCnt, 'update cnt'); @@ -1122,7 +1134,8 @@ describe('Test splom select:', function() { } Plotly.plot(gd, fig).then(function() { - scene = gd.calcdata[0][0].t._scene; + uid = gd._fullData[0].uid; + scene = gd._fullLayout._splomScenes[uid]; spyOn(scene.matrix, 'update').and.callThrough(); spyOn(scene.matrix, 'draw').and.callThrough(); }) From 8d0cac5c916d6a0cf1759aef99273da4234609ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 18:47:10 -0400 Subject: [PATCH 03/29] use styleOnSelect for scatter[polar]gl and splom ... instead of 'style', for consistency. Other _module.style in the traces/ get called during Plots.style, something we don't want to do in regl traces, to avoid double-drawing. --- src/traces/scattergl/index.js | 6 ++---- src/traces/scatterpolargl/index.js | 2 +- src/traces/splom/index.js | 6 ++---- test/jasmine/tests/splom_test.js | 8 ++++---- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 14f8b27d021..4aa5bfb1abe 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -893,9 +893,7 @@ function selectPoints(searchInfo, selectionTester) { return selection; } -function style(gd, cds) { - if(!cds) return; - +function styleOnSelect(gd, cds) { var stash = cds[0][0].t; var scene = stash._scene; @@ -957,7 +955,7 @@ module.exports = { calc: calc, plot: plot, hoverPoints: hoverPoints, - style: style, + styleOnSelect: styleOnSelect, selectPoints: selectPoints, sceneOptions: sceneOptions, diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index b68229c4aff..70024feb649 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -183,7 +183,7 @@ module.exports = { calc: calc, plot: plot, hoverPoints: hoverPoints, - style: ScatterGl.style, + styleOnSelect: ScatterGl.styleOnSelect, selectPoints: ScatterGl.selectPoints, meta: { diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index 903f420e6fe..28ea2a47206 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -424,9 +424,7 @@ function selectPoints(searchInfo, selectionTester) { return selection; } -function style(gd, cds) { - if(!cds) return; - +function styleOnSelect(gd, cds) { var fullLayout = gd._fullLayout; var cd0 = cds[0]; var scene0 = fullLayout._splomScenes[cd0[0].trace.uid]; @@ -479,7 +477,7 @@ module.exports = { plot: plot, hoverPoints: hoverPoints, selectPoints: selectPoints, - style: style, + styleOnSelect: styleOnSelect, meta: { description: [ diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 5014af82fed..22ee527f69d 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -1087,20 +1087,20 @@ describe('Test splom select:', function() { Plotly.newPlot(gd, fig).then(function() { // 'scattergl' trace module - spyOn(gd._fullLayout._modules[0], 'style').and.callFake(function() { + spyOn(gd._fullLayout._modules[0], 'styleOnSelect').and.callFake(function() { cnt++; scatterGlCnt = cnt; }); // 'splom' trace module - spyOn(gd._fullLayout._modules[1], 'style').and.callFake(function() { + spyOn(gd._fullLayout._modules[1], 'styleOnSelect').and.callFake(function() { cnt++; splomCnt = cnt; }); }) .then(function() { return _select([[20, 395], [195, 205]]); }) .then(function() { - expect(gd._fullLayout._modules[0].style).toHaveBeenCalledTimes(1); - expect(gd._fullLayout._modules[1].style).toHaveBeenCalledTimes(1); + expect(gd._fullLayout._modules[0].styleOnSelect).toHaveBeenCalledTimes(1); + expect(gd._fullLayout._modules[1].styleOnSelect).toHaveBeenCalledTimes(1); expect(cnt).toBe(2); expect(splomCnt).toBe(1, 'splom redraw before scattergl'); From 8282d91293bcaa51cd5344af54bcf196289e4876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 18:49:28 -0400 Subject: [PATCH 04/29] compute marker.size axis 'ppad' value once per splom traces, ... as opposed to once per splom trace dimensions. --- src/traces/splom/index.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index 28ea2a47206..95abd208678 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -86,23 +86,21 @@ function calc(gd, trace) { var visibleLength = cdata.length; var hasTooManyPoints = (visibleLength * commonLength) > TOO_MANY_POINTS; + // Reuse SVG scatter axis expansion routine. + // For graphs with very large number of points and array marker.size, + // use average marker size instead to speed things up. + var ppad; + if(hasTooManyPoints) { + ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3)); + } else { + ppad = calcMarkerSize(trace, commonLength); + } + for(k = 0; k < visibleDims.length; k++) { i = visibleDims[k]; dim = dimensions[i]; - xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {}; ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {}; - - // Reuse SVG scatter axis expansion routine. - // For graphs with very large number of points and array marker.size, - // use average marker size instead to speed things up. - var ppad; - if(hasTooManyPoints) { - ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3)); - } else { - ppad = calcMarkerSize(trace, commonLength); - } - calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad); } From 73acd7e9d6b6816f27f20fcb9f6a280e83b9aede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 18:50:40 -0400 Subject: [PATCH 05/29] merge options before matrix.update call - matrix.update() can be expansive (something we should look into), so try to call it the least number of times possible. --- src/traces/splom/index.js | 6 +++--- test/jasmine/tests/splom_test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index 95abd208678..caf3471c79b 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -200,7 +200,7 @@ function plotOne(gd, cd0) { var visibleDims = trace._visibleDims; var visibleLength = cdata.length; - var viewOpts = {}; + var viewOpts = scene.viewOpts = {}; viewOpts.ranges = new Array(visibleLength); viewOpts.domains = new Array(visibleLength); @@ -298,8 +298,8 @@ function plotOne(gd, cd0) { } } else { - scene.matrix.update(matrixOpts, null); - scene.matrix.update(viewOpts, null); + var opts = Lib.extendFlat({}, matrixOpts, viewOpts); + scene.matrix.update(opts, null); stash.xpx = stash.ypx = null; } diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 22ee527f69d..7298e99231a 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -921,10 +921,10 @@ describe('Test splom drag:', function() { var scene = gd._fullLayout._splomScenes[uid]; // N.B. _drag triggers two updateSubplots call // - 1 update and 1 draw call per updateSubplot - // - 2 update calls (1 for data, 1 for view opts) + // - 1 update calls for data+view opts // during splom plot on mouseup // - 1 draw call during splom plot on mouseup - expect(scene.matrix.update).toHaveBeenCalledTimes(4); + expect(scene.matrix.update).toHaveBeenCalledTimes(3); expect(scene.matrix.draw).toHaveBeenCalledTimes(3); _assertRanges('after drag', [ From cc5f0caafe230c21b8f3f25d3b7b9b244be5ebf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 18:54:25 -0400 Subject: [PATCH 06/29] speed up 'axrange' edits - by just calling 'Axes.doTicks' instead of full `doTicksRelayout` which includes drawing the title - this also fixes (and locks) a bug where large splom would draw twice (once in doTicksRelayout and once in the drawData subroutine). --- src/plot_api/plot_api.js | 4 +- src/plot_api/subroutines.js | 8 +-- test/jasmine/tests/plot_api_test.js | 12 ++++- test/jasmine/tests/splom_test.js | 78 +++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 4fe0a28da59..1feb1659048 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1738,8 +1738,8 @@ function addAxRangeSequence(seq, rangesAltered) { // subroutine of its own so that finalDraw always gets // executed after drawData var doTicks = rangesAltered ? - function(gd) { return subroutines.doTicksRelayout(gd, rangesAltered); } : - subroutines.doTicksRelayout; + function(gd) { return Axes.doTicks(gd, Object.keys(rangesAltered), true); } : + function(gd) { return Axes.doTicks(gd, 'redraw'); }; seq.push( subroutines.doAutoRangeAndConstraints, diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index aa185cbf328..2e40323dedc 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -498,12 +498,8 @@ exports.doLegend = function(gd) { return Plots.previousPromises(gd); }; -exports.doTicksRelayout = function(gd, rangesAltered) { - if(rangesAltered) { - Axes.doTicks(gd, Object.keys(rangesAltered), true); - } else { - Axes.doTicks(gd, 'redraw'); - } +exports.doTicksRelayout = function(gd) { + Axes.doTicks(gd, 'redraw'); if(gd._fullLayout._hasOnlyLargeSploms) { clearGlCanvases(gd); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 4e38fc1e982..51999fab857 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -6,6 +6,7 @@ var Queue = require('@src/lib/queue'); var Scatter = require('@src/traces/scatter'); var Bar = require('@src/traces/bar'); var Legend = require('@src/components/legend'); +var Axes = require('@src/plots/cartesian/axes'); var pkg = require('../../../package.json'); var subroutines = require('@src/plot_api/subroutines'); var helpers = require('@src/plot_api/helpers'); @@ -531,12 +532,14 @@ describe('Test plot api', function() { mockedMethods.forEach(function(m) { spyOn(subroutines, m); }); + spyOn(Axes, 'doTicks'); }); function mock(gd) { mockedMethods.forEach(function(m) { subroutines[m].calls.reset(); }); + Axes.doTicks.calls.reset(); supplyAllDefaults(gd); Plots.doCalcdata(gd); @@ -634,7 +637,7 @@ describe('Test plot api', function() { }); it('should trigger minimal sequence for cartesian axis range updates', function() { - var seq = ['doAutoRangeAndConstraints', 'doTicksRelayout', 'drawData', 'finalDraw']; + var seq = ['doAutoRangeAndConstraints', 'drawData', 'finalDraw']; function _assert(msg) { expect(gd.calcdata).toBeDefined(); @@ -644,6 +647,9 @@ describe('Test plot api', function() { '# of ' + m + ' calls - ' + msg ); }); + expect(Axes.doTicks).toHaveBeenCalledTimes(1); + expect(Axes.doTicks.calls.allArgs()[0][1]).toEqual(['x']); + expect(Axes.doTicks.calls.allArgs()[0][2]).toBe(true, 'skip-axis-title argument'); } var specs = [ @@ -2588,6 +2594,7 @@ describe('Test plot api', function() { spyOn(annotations, 'drawOne').and.callThrough(); spyOn(annotations, 'draw').and.callThrough(); spyOn(images, 'draw').and.callThrough(); + spyOn(Axes, 'doTicks').and.callThrough(); }); afterEach(destroyGraphDiv); @@ -2823,10 +2830,11 @@ describe('Test plot api', function() { Plotly.newPlot(gd, data, layout) .then(countPlots) .then(function() { + expect(Axes.doTicks).toHaveBeenCalledWith(gd, ''); return Plotly.react(gd, data, layout2); }) .then(function() { - expect(subroutines.doTicksRelayout).toHaveBeenCalledTimes(1); + expect(Axes.doTicks).toHaveBeenCalledWith(gd, 'redraw'); expect(subroutines.layoutStyles).not.toHaveBeenCalled(); }) .catch(failTest) diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 7298e99231a..e6be74524a6 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -1,6 +1,7 @@ var Plotly = require('@lib'); var Lib = require('@src/lib'); var Plots = require('@src/plots/plots'); +var Axes = require('@src/plots/cartesian/axes'); var SUBPLOT_PATTERN = require('@src/plots/cartesian/constants').SUBPLOT_PATTERN; var d3 = require('d3'); @@ -747,6 +748,83 @@ describe('Test splom interactions:', function() { }); }); +describe('Test splom update switchboard:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + var methods; + + function addSpies() { + methods.forEach(function(m) { + var obj = m[0]; + var k = m[1]; + spyOn(obj, k).and.callThrough(); + }); + } + + function assertSpies(msg, exp) { + methods.forEach(function(m, i) { + var obj = m[0]; + var k = m[1]; + var expi = exp[i]; + expect(obj[k]).toHaveBeenCalledTimes(expi[1], expi[0]); + obj[k].calls.reset(); + }); + } + + it('@gl should trigger minimal sequence for axis range updates (large splom case)', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_large.json')); + var matrix, regl, splomGrid; + + Plotly.plot(gd, fig).then(function() { + var fullLayout = gd._fullLayout; + var trace = gd._fullData[0]; + var scene = fullLayout._splomScenes[trace.uid]; + matrix = scene.matrix; + regl = matrix.regl; + splomGrid = fullLayout._splomGrid; + + methods = [ + [Plots, 'supplyDefaults'], + [Axes, 'doTicks'], + [regl, 'clear'], + [splomGrid, 'update'] + ]; + addSpies(); + + expect(fullLayout.xaxis.range).toBeCloseToArray([-0.0921, 0.9574], 1, 'xrng (base)'); + + return Plotly.relayout(gd, 'xaxis.range', [0, 1]); + }) + .then(function() { + var msg = 'after update'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doTicks', 1], + ['regl clear', 1], + ['splom grid update', 1], + ['splom grid draw', 1], + ['splom matrix update', 1], + ['splom matrix draw', 1] + ]); + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 1], 1, 'xrng ' + msg); + expect(gd._fullLayout.xaxis.range).toBeCloseToArray([0, 1], 1, 'xrng ' + msg); + }) + .catch(failTest) + .then(done); + }); +}); + describe('Test splom hover:', function() { var gd; From 17e30d2f5f5bdb6296f90fdceea514b8c5dd8518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 18:59:38 -0400 Subject: [PATCH 07/29] aggressively try to speed 'axrange' edits for splom ... by passing Plots.supplyDefaults which can take more than 100ms for large splom traces (because of all those axes to coerce). - N.B. this commit applies to optimization to all 'axrange', I could make apply only to hasOnlyLargeSplom graphs if deemed too dangerous. --- src/plot_api/plot_api.js | 23 +++++++++++++++++------ src/plots/polar/layout_attributes.js | 10 ++++++++-- test/jasmine/tests/splom_test.js | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 1feb1659048..5ea6e0f55e6 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1701,15 +1701,26 @@ exports.relayout = function relayout(gd, astr, val) { seq.push(subroutines.layoutReplot); } else if(Object.keys(aobj).length) { - Plots.supplyDefaults(gd); + var flagList = Object.keys(flags).filter(function(k) { return flags[k]; }); + + // Optimization mostly for large splom traces where + // Plots.supplyDefaults can take > 100ms + if(flagList[0] === 'axrange' && flagList.length === 1) { + for(var k in specs.rangesAltered) { + var axName = Axes.id2name(k); + var axIn = gd.layout[axName]; + var axOut = gd._fullLayout[axName]; + axOut.autorange = axIn.autorange; + axOut.range = axIn.range.slice(); + axOut.cleanRange(); + } + } else { + Plots.supplyDefaults(gd); + } if(flags.legend) seq.push(subroutines.doLegend); if(flags.layoutstyle) seq.push(subroutines.layoutStyles); - - if(flags.axrange) { - addAxRangeSequence(seq, specs.rangesAltered); - } - + if(flags.axrange) addAxRangeSequence(seq, specs.rangesAltered); if(flags.ticks) seq.push(subroutines.doTicksRelayout); if(flags.modebar) seq.push(subroutines.doModeBar); if(flags.camera) seq.push(subroutines.doCamera); diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js index d7fa1d6c31f..8334b0c3d96 100644 --- a/src/plots/polar/layout_attributes.js +++ b/src/plots/polar/layout_attributes.js @@ -59,7 +59,7 @@ var radialAxisAttrs = { visible: extendFlat({}, axesAttrs.visible, {dflt: true}), type: axesAttrs.type, - autorange: axesAttrs.autorange, + autorange: extendFlat({}, axesAttrs.autorange, {editType: 'plot'}), rangemode: { valType: 'enumerated', values: ['tozero', 'nonnegative', 'normal'], @@ -75,7 +75,13 @@ var radialAxisAttrs = { 'of the input data (same behavior as for cartesian axes).' ].join(' ') }, - range: axesAttrs.range, + range: extendFlat({}, axesAttrs.range, { + items: [ + {valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}}, + {valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}} + ], + editType: 'plot' + }), categoryorder: axesAttrs.categoryorder, categoryarray: axesAttrs.categoryarray, diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index e6be74524a6..2c88cfa39bb 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -808,7 +808,7 @@ describe('Test splom update switchboard:', function() { var msg = 'after update'; assertSpies(msg, [ - ['supplyDefaults', 1], + ['supplyDefaults', 0], ['doTicks', 1], ['regl clear', 1], ['splom grid update', 1], From 4792f08771750a421e3e33895f602580ffa7415b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 19:03:58 -0400 Subject: [PATCH 08/29] optimize lsInner for splom ... cutting down ~200ms for 50 dims sploms - do not append to DOM, when plot_bgcolor === papep_bgcolor - do not append useless layer to DOM - do not try to setup plot area clip paths under hasOnlyLargeSplom regime - loop over subplots ids, instead of a costly selectAll() + .each() --- src/plot_api/subroutines.js | 209 +++++++++++++++++-------------- src/plots/cartesian/index.js | 1 - test/jasmine/tests/splom_test.js | 40 +++++- 3 files changed, 149 insertions(+), 101 deletions(-) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 2e40323dedc..54b0368907c 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -51,12 +51,25 @@ function lsInner(gd) { var gs = fullLayout._size; var pad = gs.p; var axList = Axes.list(gd, '', true); + var i, subplot, plotinfo, xa, ya; + + fullLayout._paperdiv.style({ + width: fullLayout.width + 'px', + height: fullLayout.height + 'px' + }) + .selectAll('.main-svg') + .call(Drawing.setSize, fullLayout.width, fullLayout.height); + gd._context.setBackground(gd, fullLayout.paper_bgcolor); + + exports.drawMainTitle(gd); + ModeBar.manage(gd); // _has('cartesian') means SVG specifically, not GL2D - but GL2D // can still get here because it makes some of the SVG structure // for shared features like selections. - var hasSVGCartesian = fullLayout._has('cartesian'); - var i; + if(!fullLayout._has('cartesian')) { + return gd._promises.length && Promise.all(gd._promises); + } function getLinePosition(ax, counterAx, side) { var lwHalf = ax._lw / 2; @@ -103,25 +116,21 @@ function lsInner(gd) { ax._mainSubplot = findMainSubplot(ax, fullLayout); } - fullLayout._paperdiv - .style({ - width: fullLayout.width + 'px', - height: fullLayout.height + 'px' - }) - .selectAll('.main-svg') - .call(Drawing.setSize, fullLayout.width, fullLayout.height); - - gd._context.setBackground(gd, fullLayout.paper_bgcolor); - - var subplotSelection = fullLayout._paper.selectAll('g.subplot'); - - // figure out which backgrounds we need to draw, and in which layers - // to put them + // figure out which backgrounds we need to draw, + // and in which layers to put them var lowerBackgroundIDs = []; + var backgroundIds = []; var lowerDomains = []; - subplotSelection.each(function(d) { - var subplot = d[0]; - var plotinfo = fullLayout._plots[subplot]; + // no need to draw background when paper and plot color are the same color, + // activate mode just for large splom (which benefit the most from this + // optimization), but this could apply to all cartesian subplots. + var noNeedForBg = ( + fullLayout._hasOnlyLargeSploms && + fullLayout.paper_bgcolor === fullLayout.plot_bgcolor + ); + + for(subplot in fullLayout._plots) { + plotinfo = fullLayout._plots[subplot]; if(plotinfo.mainplot) { // mainplot is a reference to the main plot this one is overlaid on @@ -131,23 +140,26 @@ function lsInner(gd) { plotinfo.bg.remove(); } plotinfo.bg = undefined; - return; - } - - var xDomain = plotinfo.xaxis.domain; - var yDomain = plotinfo.yaxis.domain; - var plotgroup = plotinfo.plotgroup; - - if(overlappingDomain(xDomain, yDomain, lowerDomains)) { - var pgNode = plotgroup.node(); - var plotgroupBg = plotinfo.bg = Lib.ensureSingle(plotgroup, 'rect', 'bg'); - pgNode.insertBefore(plotgroupBg.node(), pgNode.childNodes[0]); } else { - plotgroup.select('rect.bg').remove(); - lowerBackgroundIDs.push(subplot); - lowerDomains.push([xDomain, yDomain]); + var xDomain = plotinfo.xaxis.domain; + var yDomain = plotinfo.yaxis.domain; + var plotgroup = plotinfo.plotgroup; + + if(overlappingDomain(xDomain, yDomain, lowerDomains)) { + var pgNode = plotgroup.node(); + var plotgroupBg = plotinfo.bg = Lib.ensureSingle(plotgroup, 'rect', 'bg'); + pgNode.insertBefore(plotgroupBg.node(), pgNode.childNodes[0]); + backgroundIds.push(subplot); + } else { + plotgroup.select('rect.bg').remove(); + lowerDomains.push([xDomain, yDomain]); + if(!noNeedForBg) { + lowerBackgroundIDs.push(subplot); + backgroundIds.push(subplot); + } + } } - }); + } // now create all the lower-layer backgrounds at once now that // we have the list of subplots that need them @@ -163,13 +175,13 @@ function lsInner(gd) { fullLayout._plots[subplot].bg = d3.select(this); }); - subplotSelection.each(function(d) { - var subplot = d[0]; - var plotinfo = fullLayout._plots[subplot]; - var xa = plotinfo.xaxis; - var ya = plotinfo.yaxis; + // style all backgrounds + for(i = 0; i < backgroundIds.length; i++) { + plotinfo = fullLayout._plots[backgroundIds[i]]; + xa = plotinfo.xaxis; + ya = plotinfo.yaxis; - if(plotinfo.bg && hasSVGCartesian) { + if(plotinfo.bg) { plotinfo.bg .call(Drawing.setRect, xa._offset - pad, ya._offset - pad, @@ -177,72 +189,83 @@ function lsInner(gd) { .call(Color.fill, fullLayout.plot_bgcolor) .style('stroke-width', 0); } + } - // Clip so that data only shows up on the plot area. - var clipId = plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; + if(!fullLayout._hasOnlyLargeSploms) { + for(subplot in fullLayout._plots) { + plotinfo = fullLayout._plots[subplot]; + xa = plotinfo.xaxis; + ya = plotinfo.yaxis; - var plotClip = Lib.ensureSingleById(fullLayout._clips, 'clipPath', clipId, function(s) { - s.classed('plotclip', true) - .append('rect'); - }); + // Clip so that data only shows up on the plot area. + var clipId = plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; - plotinfo.clipRect = plotClip.select('rect').attr({ - width: xa._length, - height: ya._length - }); + var plotClip = Lib.ensureSingleById(fullLayout._clips, 'clipPath', clipId, function(s) { + s.classed('plotclip', true) + .append('rect'); + }); - Drawing.setTranslate(plotinfo.plot, xa._offset, ya._offset); + plotinfo.clipRect = plotClip.select('rect').attr({ + width: xa._length, + height: ya._length + }); - var plotClipId; - var layerClipId; + Drawing.setTranslate(plotinfo.plot, xa._offset, ya._offset); - if(plotinfo._hasClipOnAxisFalse) { - plotClipId = null; - layerClipId = clipId; - } else { - plotClipId = clipId; - layerClipId = null; - } + var plotClipId; + var layerClipId; - Drawing.setClipUrl(plotinfo.plot, plotClipId); + if(plotinfo._hasClipOnAxisFalse) { + plotClipId = null; + layerClipId = clipId; + } else { + plotClipId = clipId; + layerClipId = null; + } - // stash layer clipId value (null or same as clipId) - // to DRY up Drawing.setClipUrl calls on trace-module and trace layers - // downstream - plotinfo.layerClipId = layerClipId; + Drawing.setClipUrl(plotinfo.plot, plotClipId); - // figure out extra axis line and tick positions as needed - if(!hasSVGCartesian) return; + // stash layer clipId value (null or same as clipId) + // to DRY up Drawing.setClipUrl calls on trace-module and trace layers + // downstream + plotinfo.layerClipId = layerClipId; + } + } - var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop, - leftYLineWidth, rightYLineWidth; - var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight, - connectYBottom, connectYTop; - var extraSubplot; + var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop, + leftYLineWidth, rightYLineWidth; + var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight, + connectYBottom, connectYTop; + var extraSubplot; - function xLinePath(y) { - return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; - } + function xLinePath(y) { + return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; + } - function xLinePathFree(y) { - return 'M' + xa._offset + ',' + y + 'h' + xa._length; - } + function xLinePathFree(y) { + return 'M' + xa._offset + ',' + y + 'h' + xa._length; + } - function yLinePath(x) { - return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; - } + function yLinePath(x) { + return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; + } - function yLinePathFree(x) { - return 'M' + x + ',' + ya._offset + 'v' + ya._length; - } + function yLinePathFree(x) { + return 'M' + x + ',' + ya._offset + 'v' + ya._length; + } - function mainPath(ax, pathFn, pathFnFree) { - if(!ax.showline || subplot !== ax._mainSubplot) return ''; - if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition); - var out = pathFn(ax._mainLinePosition); - if(ax.mirror) out += pathFn(ax._mainMirrorPosition); - return out; - } + function mainPath(ax, pathFn, pathFnFree) { + if(!ax.showline || subplot !== ax._mainSubplot) return ''; + if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition); + var out = pathFn(ax._mainLinePosition); + if(ax.mirror) out += pathFn(ax._mainMirrorPosition); + return out; + } + + for(subplot in fullLayout._plots) { + plotinfo = fullLayout._plots[subplot]; + xa = plotinfo.xaxis; + ya = plotinfo.yaxis; /* * x lines get longer where they meet y lines, to make a crisp corner. @@ -323,11 +346,9 @@ function lsInner(gd) { ya.linecolor : 'rgba(0,0,0,0)'); } plotinfo.ylines.attr('d', yPath); - }); + } Axes.makeClipPaths(gd); - exports.drawMainTitle(gd); - ModeBar.manage(gd); return gd._promises.length && Promise.all(gd._promises); } diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index a71485c244b..255b2a5d837 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -464,7 +464,6 @@ function makeSubplotLayer(gd, plotinfo) { // and other places // - we don't (x|y)lines and (x|y)axislayer for most subplots // usually just the bottom x and left y axes. - plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot'); plotinfo.xlines = ensureSingle(plotgroup, 'path', 'xlines-above'); plotinfo.ylines = ensureSingle(plotgroup, 'path', 'ylines-above'); plotinfo.xaxislayer = ensureSingle(plotgroup, 'g', 'xaxislayer-above'); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 2c88cfa39bb..4802e881180 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -525,7 +525,9 @@ describe('Test splom interactions:', function() { function _assert(exp) { var msg = ' - call #' + cnt; - var subplots = d3.selectAll('g.cartesianlayer > g.subplot'); + var gd3 = d3.select(gd); + var subplots = gd3.selectAll('g.cartesianlayer > g.subplot'); + var bgs = gd3.selectAll('.bglayer > rect.bg'); expect(subplots.size()) .toBe(exp.subplotCnt, '# of ' + msg); @@ -546,22 +548,47 @@ describe('Test splom interactions:', function() { expect(!!gd._fullLayout._splomGrid) .toBe(exp.hasSplomGrid, 'has regl-line2d splom grid' + msg); + expect(bgs.size()).toBe(exp.bgCnt, '# of ' + msg); + cnt++; } Plotly.plot(gd, figLarge).then(function() { _assert({ subplotCnt: 400, - innerSubplotNodeCnt: 5, - hasSplomGrid: true + innerSubplotNodeCnt: 4, + hasSplomGrid: true, + bgCnt: 0 + }); + + return Plotly.relayout(gd, 'paper_bgcolor', 'red'); + }) + .then(function() { + _assert({ + subplotCnt: 400, + innerSubplotNodeCnt: 4, + hasSplomGrid: true, + bgCnt: 400 }); + + return Plotly.relayout(gd, 'plot_bgcolor', 'red'); + }) + .then(function() { + _assert({ + subplotCnt: 400, + innerSubplotNodeCnt: 4, + hasSplomGrid: true, + bgCnt: 0 + }); + return Plotly.restyle(gd, 'dimensions', [dimsSmall]); }) .then(function() { _assert({ subplotCnt: 25, innerSubplotNodeCnt: 17, - hasSplomGrid: false + hasSplomGrid: false, + bgCnt: 25 }); // make sure 'new' subplot layers are in order @@ -591,9 +618,10 @@ describe('Test splom interactions:', function() { // new subplots though have reduced number of children. innerSubplotNodeCnt: function(d) { var p = d.match(SUBPLOT_PATTERN); - return (p[1] > 5 || p[2] > 5) ? 5 : 17; + return (p[1] > 5 || p[2] > 5) ? 4 : 17; }, - hasSplomGrid: true + hasSplomGrid: true, + bgCnt: 0 }); }) .catch(failTest) From f415e9668d7002141de09cd7284b79ba3daaf93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 19:08:18 -0400 Subject: [PATCH 09/29] first-cut editType:'style' pathway for sploms - using `_module.editStyle`, and not _module.style which would lead to double drawing from Plots.style. - in brief, for 'style' edits, we: + clear gl canvas + merge new convertMarkerStyle() into matrixOptions + call matrix.update and matrix.draw --- src/plot_api/subroutines.js | 32 ++++++++++++++++--- src/traces/splom/attributes.js | 21 ++++++++++++- src/traces/splom/index.js | 18 +++++++++++ test/jasmine/tests/splom_test.js | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 54b0368907c..2b2025f574b 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -466,12 +466,34 @@ exports.drawMainTitle = function(gd) { // supplyDefaults brought in an array that was already // in gd.data but not in gd._fullData previously exports.doTraceStyle = function(gd) { - for(var i = 0; i < gd.calcdata.length; i++) { - var cdi = gd.calcdata[i], - _module = ((cdi[0] || {}).trace || {})._module || {}, - arraysToCalcdata = _module.arraysToCalcdata; + var fullLayout = gd._fullLayout; + var editStyleCalls = []; + var i; + + for(i = 0; i < gd.calcdata.length; i++) { + var cd = gd.calcdata[i]; + var cd0 = cd[0] || {}; + var trace = cd0.trace || {}; + var _module = trace._module || {}; + + var arraysToCalcdata = _module.arraysToCalcdata; + if(arraysToCalcdata) arraysToCalcdata(cd, trace); + + var editStyle = _module.editStyle; + if(editStyle) editStyleCalls.push({fn: editStyle, cd0: cd0}); + } + + if(editStyleCalls.length) { + clearGlCanvases(gd); - if(arraysToCalcdata) arraysToCalcdata(cdi, cdi[0].trace); + if(fullLayout._hasOnlyLargeSploms) { + fullLayout._splomGrid.draw(); + } + + for(i = 0; i < editStyleCalls.length; i++) { + var edit = editStyleCalls[i]; + edit.fn(gd, edit.cd0); + } } Plots.style(gd); diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index 18ef1c76c9c..e92c4bc603d 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -8,11 +8,16 @@ 'use strict'; +var scatterAttrs = require('../scatter/attributes'); +var colorAttrs = require('../../components/colorscale/attributes'); var scatterGlAttrs = require('../scattergl/attributes'); var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; var templatedArray = require('../../plot_api/plot_template').templatedArray; var extendFlat = require('../../lib/extend').extendFlat; +var scatterMarkerAttrs = scatterAttrs.marker; +var scatterMarkerLineAttrs = scatterMarkerAttrs.line; + function makeAxesValObject(axLetter) { return { valType: 'info_array', @@ -96,7 +101,21 @@ module.exports = { 'this trace\'s (x,y) coordinates.' ].join(' ') }), - marker: scatterGlAttrs.marker, + + marker: extendFlat({}, colorAttrs('marker'), { + symbol: scatterMarkerAttrs.symbol, + size: scatterMarkerAttrs.size, + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + opacity: scatterMarkerAttrs.opacity, + colorbar: scatterMarkerAttrs.colorbar, + line: extendFlat({}, colorAttrs('marker.line'), { + width: scatterMarkerLineAttrs.width, + editType: 'calc' + }), + editType: 'calc' + }), xaxes: makeAxesValObject('x'), yaxes: makeAxesValObject('y'), diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index caf3471c79b..17bc7b9f8d1 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -306,6 +306,23 @@ function plotOne(gd, cd0) { scene.draw(); } +function editStyle(gd, cd0) { + var trace = cd0.trace; + var scene = gd._fullLayout._splomScenes[trace.uid]; + + calcColorscales(trace); + + Lib.extendFlat(scene.matrixOptions, convertMarkerStyle(trace)); + // TODO [un]selected styles? + + var opts = Lib.extendFlat({}, scene.matrixOptions, scene.viewOpts); + + // TODO this is too long for arrayOk attributes! + scene.matrix.update(opts, null); + + scene.draw(); +} + function hoverPoints(pointData, xval, yval) { var cd = pointData.cd; var trace = cd[0].trace; @@ -476,6 +493,7 @@ module.exports = { hoverPoints: hoverPoints, selectPoints: selectPoints, styleOnSelect: styleOnSelect, + editStyle: editStyle, meta: { description: [ diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 4802e881180..2ac8801f16f 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -808,6 +808,10 @@ describe('Test splom update switchboard:', function() { }); } + function toPlainArray(typedArray) { + return Array.prototype.slice.call(typedArray); + } + it('@gl should trigger minimal sequence for axis range updates (large splom case)', function(done) { var fig = Lib.extendDeep({}, require('@mocks/splom_large.json')); var matrix, regl, splomGrid; @@ -851,6 +855,55 @@ describe('Test splom update switchboard:', function() { .catch(failTest) .then(done); }); + + it('@gl should trigger minimal sequence for marker style updates', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_0.json')); + var scene, matrix, regl; + + Plotly.plot(gd, fig).then(function() { + var fullLayout = gd._fullLayout; + var trace = gd._fullData[0]; + scene = fullLayout._splomScenes[trace.uid]; + matrix = scene.matrix; + regl = matrix.regl; + + methods = [ + [Plots, 'supplyDefaults'], + [Plots, 'doCalcdata'], + [Axes, 'doTicks'], + [regl, 'clear'], + [matrix, 'update'], + [matrix, 'draw'] + ]; + addSpies(); + + expect(toPlainArray(scene.matrixOptions.color)) + .toBeCloseToArray([31, 119, 180, 255], 1, 'base color'); + expect(scene.matrixOptions.size).toBe(3, 'base size'); + expect(fullLayout.xaxis.range).toBeCloseToArray([0.851, 3.148], 1, 'base xrng'); + + return Plotly.restyle(gd, 'marker.color', 'black'); + }) + .then(function() { + var msg = 'after scaler marker.color restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 0], + ['doTicks', 0], + ['regl clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(toPlainArray(scene.matrixOptions.color)) + .toBeCloseToArray([0, 0, 0, 255], 1, msg); + + return Plotly.restyle(gd, 'marker.color', [['red', 'green', 'blue']]); + }) + .catch(failTest) + .then(done); + }); }); describe('Test splom hover:', function() { From 3eb91c096cf673bfb8f0e4fd4276d4a9dd62d170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 19:09:09 -0400 Subject: [PATCH 10/29] no need to re-calc 'regl' traces in-and-out of arrayOk values --- src/plot_api/plot_api.js | 6 ++++-- test/jasmine/tests/splom_test.js | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 5ea6e0f55e6..ddbf6f6ffbd 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1572,8 +1572,10 @@ function _restyle(gd, aobj, traces) { else { if(valObject) { // must redo calcdata when restyling array values of arrayOk attributes - if(valObject.arrayOk && ( - Lib.isArrayOrTypedArray(newVal) || Lib.isArrayOrTypedArray(oldVal)) + // ... but no need to this for regl-based traces + if(valObject.arrayOk && + !Registry.traceIs(contFull, 'regl') && + (Lib.isArrayOrTypedArray(newVal) || Lib.isArrayOrTypedArray(oldVal)) ) { flags.calc = true; } diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 2ac8801f16f..a79a3226d3f 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -901,6 +901,27 @@ describe('Test splom update switchboard:', function() { return Plotly.restyle(gd, 'marker.color', [['red', 'green', 'blue']]); }) + .then(function() { + var msg = 'after arrayOk marker.color restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 0], + ['doTicks', 0], + ['clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(toPlainArray(scene.matrixOptions.colors[0])) + .toBeCloseToArray([1, 0, 0, 1], 1, msg + '- 0'); + expect(toPlainArray(scene.matrixOptions.colors[1])) + .toBeCloseToArray([0, 0.501, 0, 1], 1, msg + '- 1'); + expect(toPlainArray(scene.matrixOptions.colors[2])) + .toBeCloseToArray([0, 0, 1, 1], 1, msg + '- 2'); + + return Plotly.restyle(gd, 'marker.size', 20); + }) .catch(failTest) .then(done); }); From 145cad32dd3dddc7002b88de970495ceb6c1d051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 27 Sep 2018 19:10:29 -0400 Subject: [PATCH 11/29] try implementing a fast 'markerSize' editType - marker.size is hard to edit fast, as in general it can change the axis ranges --- src/plot_api/edit_types.js | 5 +++-- src/plot_api/plot_api.js | 13 ++++++++++++ src/traces/splom/attributes.js | 2 +- test/jasmine/tests/splom_test.js | 34 ++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/plot_api/edit_types.js b/src/plot_api/edit_types.js index 83cc9c8655b..b31724cd874 100644 --- a/src/plot_api/edit_types.js +++ b/src/plot_api/edit_types.js @@ -15,7 +15,7 @@ var isPlainObject = Lib.isPlainObject; var traceOpts = { valType: 'flaglist', extras: ['none'], - flags: ['calc', 'clearAxisTypes', 'plot', 'style', 'colorbars'], + flags: ['calc', 'clearAxisTypes', 'plot', 'style', 'markerSize', 'colorbars'], description: [ 'trace attributes should include an `editType` string matching this flaglist.', '*calc* is the most extensive: a full `Plotly.plot` starting by clearing `gd.calcdata`', @@ -24,7 +24,8 @@ var traceOpts = { 'cause the automatic axis type detection to change. Log type will not be cleared, as that', 'is never automatically chosen so must have been user-specified.', '*plot* calls `Plotly.plot` but without first clearing `gd.calcdata`.', - '*style* only calls `module.style` for all trace modules and redraws the legend.', + '*style* only calls `module.style` (or module.editStyle) for all trace modules and redraws the legend.', + '*markerSize* is like *style*, but propagate axis-range changes due to scatter `marker.size`', '*colorbars* only redraws colorbars.' ].join(' ') }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index ddbf6f6ffbd..816bc0e54a3 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1344,8 +1344,21 @@ exports.restyle = function restyle(gd, astr, val, _traces) { } else { seq.push(Plots.previousPromises); + // maybe only call Plots.supplyDataDefaults in the splom case, + // to skip over long and slow axes defaults Plots.supplyDefaults(gd); + if(flags.markerSize) { + Plots.doCalcdata(gd); + addAxRangeSequence(seq); + + // TODO + // if all axes have autorange:false, then + // proceed to subroutines.doTraceStyle(), + // otherwise we must go through addAxRangeSequence, + // which in general must redraws 'all' axes + } + if(flags.style) seq.push(subroutines.doTraceStyle); if(flags.colorbars) seq.push(subroutines.doColorBars); diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index e92c4bc603d..2a6aeb321de 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -104,7 +104,7 @@ module.exports = { marker: extendFlat({}, colorAttrs('marker'), { symbol: scatterMarkerAttrs.symbol, - size: scatterMarkerAttrs.size, + size: extendFlat({}, scatterMarkerAttrs.size, {editType: 'markerSize'}), sizeref: scatterMarkerAttrs.sizeref, sizemin: scatterMarkerAttrs.sizemin, sizemode: scatterMarkerAttrs.sizemode, diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index a79a3226d3f..776b01464aa 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -922,6 +922,40 @@ describe('Test splom update switchboard:', function() { return Plotly.restyle(gd, 'marker.size', 20); }) + .then(function() { + var msg = 'after scalar marker.size restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 1], + ['doTicks', 1], + ['regl clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(scene.matrixOptions.size).toBe(10, msg); + expect(gd._fullLayout.xaxis.range) + .toBeCloseToArray([0.753, 3.246], 1, 'xrng ' + msg); + + return Plotly.restyle(gd, 'marker.size', [[4, 10, 20]]); + }) + .then(function() { + var msg = 'after scalar marker.size restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 1], + ['doTicks', 1], + ['regl clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(scene.matrixOptions.sizes).toBeCloseToArray([2, 5, 10], 1, msg); + expect(gd._fullLayout.xaxis.range) + .toBeCloseToArray([0.853, 3.235], 1, 'xrng ' + msg); + }) .catch(failTest) .then(done); }); From 6de1ae4f1da1f5d84fd5c8d95d27f688132e2b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 1 Oct 2018 13:24:19 -0400 Subject: [PATCH 12/29] add supplyDefaults bypass 'axrange' optimization to Plotly.update --- src/plot_api/plot_api.js | 45 ++++++++++++++++------------- test/jasmine/tests/plot_api_test.js | 3 ++ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 816bc0e54a3..a749a2bf08f 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1716,22 +1716,7 @@ exports.relayout = function relayout(gd, astr, val) { seq.push(subroutines.layoutReplot); } else if(Object.keys(aobj).length) { - var flagList = Object.keys(flags).filter(function(k) { return flags[k]; }); - - // Optimization mostly for large splom traces where - // Plots.supplyDefaults can take > 100ms - if(flagList[0] === 'axrange' && flagList.length === 1) { - for(var k in specs.rangesAltered) { - var axName = Axes.id2name(k); - var axIn = gd.layout[axName]; - var axOut = gd._fullLayout[axName]; - axOut.autorange = axIn.autorange; - axOut.range = axIn.range.slice(); - axOut.cleanRange(); - } - } else { - Plots.supplyDefaults(gd); - } + axRangeSupplyDefaultsByPass(gd, flags, specs) || Plots.supplyDefaults(gd); if(flags.legend) seq.push(subroutines.doLegend); if(flags.layoutstyle) seq.push(subroutines.layoutStyles); @@ -1759,6 +1744,28 @@ exports.relayout = function relayout(gd, astr, val) { }); }; +// Optimization mostly for large splom traces where +// Plots.supplyDefaults can take > 100ms +function axRangeSupplyDefaultsByPass(gd, flags, specs) { + var k; + + if(!flags.axrange) return false; + + for(k in flags) { + if(k !== 'axrange' && flags[k]) return false; + } + + for(k in specs.rangesAltered) { + var axName = Axes.id2name(k); + var axIn = gd.layout[axName]; + var axOut = gd._fullLayout[axName]; + axOut.autorange = axIn.autorange; + axOut.range = axIn.range.slice(); + axOut.cleanRange(); + } + return true; +} + function addAxRangeSequence(seq, rangesAltered) { // N.B. leave as sequence of subroutines (for now) instead of // subroutine of its own so that finalDraw always gets @@ -2182,15 +2189,13 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { } else { seq.push(Plots.previousPromises); - Plots.supplyDefaults(gd); + axRangeSupplyDefaultsByPass(gd, relayoutFlags, relayoutSpecs) || Plots.supplyDefaults(gd); if(restyleFlags.style) seq.push(subroutines.doTraceStyle); if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); - if(relayoutFlags.axrange) { - addAxRangeSequence(seq, relayoutSpecs.rangesAltered); - } + if(relayoutFlags.axrange) addAxRangeSequence(seq, relayoutSpecs.rangesAltered); if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 51999fab857..6b974857fdd 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -533,6 +533,7 @@ describe('Test plot api', function() { spyOn(subroutines, m); }); spyOn(Axes, 'doTicks'); + spyOn(Plots, 'supplyDefaults').and.callThrough(); }); function mock(gd) { @@ -542,6 +543,7 @@ describe('Test plot api', function() { Axes.doTicks.calls.reset(); supplyAllDefaults(gd); + Plots.supplyDefaults.calls.reset(); Plots.doCalcdata(gd); gd.emit = function() {}; return gd; @@ -650,6 +652,7 @@ describe('Test plot api', function() { expect(Axes.doTicks).toHaveBeenCalledTimes(1); expect(Axes.doTicks.calls.allArgs()[0][1]).toEqual(['x']); expect(Axes.doTicks.calls.allArgs()[0][2]).toBe(true, 'skip-axis-title argument'); + expect(Plots.supplyDefaults).not.toHaveBeenCalled(); } var specs = [ From aaf63127bbad48adef96a19a4d456b7e66ce68b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 1 Oct 2018 16:16:54 -0400 Subject: [PATCH 13/29] apply no-need-for- optimization to all cartesian subplots ... but do draw when either plot or paper bgcolor are semi-transparent. --- src/plot_api/subroutines.js | 3 +- test/jasmine/assets/custom_assertions.js | 5 +- test/jasmine/tests/cartesian_test.js | 71 +++++++++++++++++++++++- test/jasmine/tests/splom_test.js | 2 +- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 2b2025f574b..70fde722d61 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -125,7 +125,8 @@ function lsInner(gd) { // activate mode just for large splom (which benefit the most from this // optimization), but this could apply to all cartesian subplots. var noNeedForBg = ( - fullLayout._hasOnlyLargeSploms && + Color.opacity(fullLayout.paper_bgcolor) === 1 && + Color.opacity(fullLayout.plot_bgcolor) === 1 && fullLayout.paper_bgcolor === fullLayout.plot_bgcolor ); diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js index 9923b23add9..86496bb2033 100644 --- a/test/jasmine/assets/custom_assertions.js +++ b/test/jasmine/assets/custom_assertions.js @@ -246,9 +246,6 @@ exports.assertElemInside = function(elem, container, msg) { * quick plot area dimension check: test width and/or height of the inner * plot area (single subplot) to verify that the margins are as expected * - * Note: if you use margin.pad on the plot, width and height will be larger - * than you expected by twice that padding. - * * opts can have keys (all optional): * width (exact width match) * height (exact height match) @@ -261,7 +258,7 @@ exports.assertPlotSize = function(opts, msg) { var widthLessThan = opts.widthLessThan; var heightLessThan = opts.heightLessThan; - var plotBB = d3.select('.bglayer .bg').node().getBoundingClientRect(); + var plotBB = d3.select('.plotclip > rect').node().getBoundingClientRect(); var actualWidth = plotBB.width; var actualHeight = plotBB.height; diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index 6f5c26b203b..e4d9888e2cf 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -520,7 +520,8 @@ describe('subplot creation / deletion:', function() { yaxis2: {domain: [0.5, 1], anchor: 'x2'}, yaxis3: {overlaying: 'y'}, // legend makes its own .bg rect - delete so we can ignore that here - showlegend: false + showlegend: false, + plot_bgcolor: '#d3d3d3' }) .then(function() { // touching but not overlapping: all backgrounds are in back @@ -553,6 +554,74 @@ describe('subplot creation / deletion:', function() { .then(done); }); + it('puts not have backgrounds nodes when plot and paper color match', function(done) { + Plotly.plot(gd, [ + {y: [1, 2, 3]}, + {y: [2, 3, 1], xaxis: 'x2', yaxis: 'y2'}, + {y: [3, 1, 2], yaxis: 'y3'} + ], { + xaxis: {domain: [0, 0.5]}, + xaxis2: {domain: [0.5, 1], anchor: 'y2'}, + yaxis: {domain: [0, 1]}, + yaxis2: {domain: [0.5, 1], anchor: 'x2'}, + yaxis3: {overlaying: 'y'}, + // legend makes its own .bg rect - delete so we can ignore that here + showlegend: false, + plot_bgcolor: 'white', + paper_bgcolor: 'white' + }) + .then(function() { + // touching but not overlapping, matching colors -> no + checkBGLayers(0, 0, ['xy', 'x2y2', 'xy3']); + + // now add a slight overlap: that's enough to put x2y2 in front + return Plotly.relayout(gd, {'xaxis2.domain': [0.49, 1]}); + }) + .then(function() { + // need to draw one backgroud + checkBGLayers(0, 1, ['xy', 'x2y2', 'xy3']); + + // x ranges overlap, but now y ranges are disjoint + return Plotly.relayout(gd, {'xaxis2.domain': [0, 1], 'yaxis.domain': [0, 0.5]}); + }) + .then(function() { + // disjoint, matching colors -> no + checkBGLayers(0, 0, ['xy', 'x2y2', 'xy3']); + + // regular inset + return Plotly.relayout(gd, { + 'xaxis.domain': [0, 1], + 'yaxis.domain': [0, 1], + 'xaxis2.domain': [0.6, 0.9], + 'yaxis2.domain': [0.6, 0.9] + }); + }) + .then(function() { + // need to draw one backgroud + checkBGLayers(0, 1, ['xy', 'x2y2', 'xy3']); + + // change paper color + return Plotly.relayout(gd, 'paper_bgcolor', 'black'); + }) + .then(function() { + // need a backgroud on main subplot to distinguish plot from + // paper color + checkBGLayers(1, 1, ['xy', 'x2y2', 'xy3']); + + // change bg colors to same semi-transparent color + return Plotly.relayout(gd, { + 'paper_bgcolor': 'rgba(255,0,0,0.2)', + 'plot_bgcolor': 'rgba(255,0,0,0.2)' + }); + }) + .then(function() { + // still need a to get correct semi-transparent look + checkBGLayers(1, 1, ['xy', 'x2y2', 'xy3']); + }) + .catch(failTest) + .then(done); + }); + it('should clear overlaid subplot trace layers on restyle', function(done) { var fig = Lib.extendDeep({}, require('@mocks/overlaying-axis-lines.json')); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 776b01464aa..5760f4bb528 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -588,7 +588,7 @@ describe('Test splom interactions:', function() { subplotCnt: 25, innerSubplotNodeCnt: 17, hasSplomGrid: false, - bgCnt: 25 + bgCnt: 0 }); // make sure 'new' subplot layers are in order From 478c6693ecb639f8b718b0ab5a608cefd6920c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 1 Oct 2018 16:34:11 -0400 Subject: [PATCH 14/29] speed up splom.clean --- src/traces/splom/base_plot.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index 1b2939eb152..c143a5d551a 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -168,20 +168,19 @@ function makeGridData(gd) { } function clean(newFullData, newFullLayout, oldFullData, oldFullLayout) { - oldLoop: - for(var i = 0; i < oldFullData.length; i++) { - var oldTrace = oldFullData[i]; - - if(oldTrace.type === 'splom') { - for(var j = 0; j < newFullData.length; j++) { - var newTrace = newFullData[j]; + var lookup = {}; + var i; - if(oldTrace.uid === newTrace.uid && newTrace.type === 'splom') { - continue oldLoop; - } + if(oldFullLayout._splomScenes) { + for(i = 0; i < newFullData.length; i++) { + var newTrace = newFullData[i]; + if(newTrace.type === 'splom') { + lookup[newTrace.uid] = 1; } - - if(oldFullLayout._splomScenes) { + } + for(i = 0; i < oldFullData.length; i++) { + var oldTrace = oldFullData[i]; + if(!lookup[oldTrace.uid]) { var scene = oldFullLayout._splomScenes[oldTrace.uid]; if(scene && scene.destroy) scene.destroy(); // must first set scene to null in order to get garbage collected From 81eb48b763cceeea3aa25892e4fc691cffd949f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 12:12:27 -0400 Subject: [PATCH 15/29] introduce redrawReglTraces subroutine - which (re)-draw scattergl, scatterpolargl, splom traces as well as splom regl grid line in one go, always in the correct order - packaging these gl draw calls in one subroutine is especially useful for drag and selections, where buffers of targeted traces/scene are updated, but *all* traces need to be redraw following clearGlCanvases --- src/plot_api/subroutines.js | 57 +++++++++++++++++++ src/traces/scattergl/index.js | 2 - src/traces/splom/base_plot.js | 6 +- src/traces/splom/index.js | 13 ++--- test/jasmine/tests/gl2d_plot_interact_test.js | 5 +- 5 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 70fde722d61..eb726c22877 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -600,6 +600,8 @@ exports.drawData = function(gd) { basePlotModules[i].plot(gd); } + exports.redrawReglTraces(gd); + // styling separate from drawing Plots.style(gd); @@ -613,6 +615,61 @@ exports.drawData = function(gd) { return Plots.previousPromises(gd); }; +// Draw (or redraw) all traces in one go, +// useful during drag and selection where buffers of targeted traces are updated, +// but all traces need to be redrawn following clearGlCanvases. +// +// Note that _module.plot for regl trace does NOT draw things +// on the canvas, they only update the buffers. +// Drawing is perform here. +// +// TODO try adding per-subplot option using gl.SCISSOR_TEST for +// non-overlaying, disjoint subplots. +// +// TODO try to include parcoords in here. +exports.redrawReglTraces = function(gd) { + var fullLayout = gd._fullLayout; + + if(fullLayout._has('regl')) { + var fullData = gd._fullData; + var cartesianIds = []; + var polarIds = []; + var i, sp; + + if(fullLayout._hasOnlyLargeSploms) { + fullLayout._splomGrid.draw(); + } + + // N.B. + // - Loop over fullData (not _splomScenes) to preserve splom trace-to-trace ordering + // - Fill list if subplot ids (instead of fullLayout._subplots) to handle cases where all traces + // of a given module are `visible !== true` + for(i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + + if(trace.visible === true) { + if(trace.type === 'splom') { + fullLayout._splomScenes[trace.uid].draw(); + } else if(trace.type === 'scattergl') { + Lib.pushUnique(cartesianIds, trace.xaxis + trace.yaxis); + } else if(trace.type === 'scatterpolargl') { + Lib.pushUnique(polarIds, trace.subplot); + } + } + } + + for(i = 0; i < cartesianIds.length; i++) { + sp = fullLayout._plots[cartesianIds[i]]; + if(sp._scene) sp._scene.draw(); + } + + for(i = 0; i < polarIds.length; i++) { + sp = fullLayout[polarIds[i]]._subplot; + if(sp._scene) sp._scene.draw(); + } + } +}; + exports.doAutoRangeAndConstraints = function(gd) { var axList = Axes.list(gd, '', true); diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 4aa5bfb1abe..457dbd540e5 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -640,8 +640,6 @@ function plot(gd, subplot, cdata) { if(scene.glText) { scene.glText.forEach(function(text) { text.update(vpRange0); }); } - - scene.draw(); } diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index c143a5d551a..8c13e18e5dc 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -28,7 +28,7 @@ function plot(gd) { if(!success) return; if(fullLayout._hasOnlyLargeSploms) { - drawGrid(gd); + updateGrid(gd); } _module.plot(gd, {}, splomCalcData); @@ -84,7 +84,7 @@ function dragOne(gd, trace, scene) { } } -function drawGrid(gd) { +function updateGrid(gd) { var fullLayout = gd._fullLayout; var regl = fullLayout._glcanvas.data()[0].regl; var splomGrid = fullLayout._splomGrid; @@ -92,9 +92,7 @@ function drawGrid(gd) { if(!splomGrid) { splomGrid = fullLayout._splomGrid = createLine(regl); } - splomGrid.update(makeGridData(gd)); - splomGrid.draw(); } function makeGridData(gd) { diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index 17bc7b9f8d1..ea759a89915 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -139,11 +139,12 @@ function sceneUpdate(gd, trace) { scene = splomScenes[uid] = Lib.extendFlat({}, reset, first); scene.draw = function draw() { - // draw traces in selection mode - if(scene.matrix && scene.selectBatch) { - scene.matrix.draw(scene.unselectBatch, scene.selectBatch); - } else if(scene.matrix) { - scene.matrix.draw(); + if(scene.matrix && scene.matrix.draw) { + if(scene.selectBatch) { + scene.matrix.draw(scene.unselectBatch, scene.selectBatch); + } else { + scene.matrix.draw(); + } } scene.dirty = false; @@ -302,8 +303,6 @@ function plotOne(gd, cd0) { scene.matrix.update(opts, null); stash.xpx = stash.ypx = null; } - - scene.draw(); } function editStyle(gd, cd0) { diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js index bb4e3a1dda5..aa01c2a671a 100644 --- a/test/jasmine/tests/gl2d_plot_interact_test.js +++ b/test/jasmine/tests/gl2d_plot_interact_test.js @@ -1182,7 +1182,10 @@ describe('Test scattergl autorange:', function() { describe('should return the approximative values for ~big~ data', function() { beforeEach(function() { - spyOn(ScatterGl, 'plot'); + // to avoid expansive draw calls (which could be problematic on CI) + spyOn(ScatterGl, 'plot').and.callFake(function(gd) { + gd._fullLayout._plots.xy._scene.scatter2d = {draw: function() {}}; + }); }); // threshold for 'fast' axis expansion routine From 02434517bd82b2dd1f0a903e0b441946b617a467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 12:13:23 -0400 Subject: [PATCH 16/29] :hocho: obsolete sortBasePlotModules ... no need for this now that we have redrawReglTraces --- src/plots/plots.js | 4 ---- src/plots/sort_modules.js | 25 ------------------------- test/jasmine/tests/plots_test.js | 20 -------------------- 3 files changed, 49 deletions(-) delete mode 100644 src/plots/sort_modules.js diff --git a/src/plots/plots.js b/src/plots/plots.js index 8f172d14ba0..b0092864c61 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -20,7 +20,6 @@ var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; var axisIDs = require('../plots/cartesian/axis_ids'); -var sortBasePlotModules = require('./sort_modules').sortBasePlotModules; var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); @@ -487,9 +486,6 @@ plots.supplyDefaults = function(gd, opts) { if(!skipUpdateCalc && oldCalcdata.length === newFullData.length) { plots.supplyDefaultsUpdateCalc(oldCalcdata, newFullData); } - - // sort base plot modules for consistent ordering - newFullLayout._basePlotModules.sort(sortBasePlotModules); }; plots.supplyDefaultsUpdateCalc = function(oldCalcdata, newFullData) { diff --git a/src/plots/sort_modules.js b/src/plots/sort_modules.js deleted file mode 100644 index 38090667d40..00000000000 --- a/src/plots/sort_modules.js +++ /dev/null @@ -1,25 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -// always plot splom before cartesian (i.e. scattergl traces) -function sortModules(a, b) { - if(a === 'splom') return -1; - if(b === 'splom') return 1; - return 0; -} - -function sortBasePlotModules(a, b) { - return sortModules(a.name, b.name); -} - -module.exports = { - sortBasePlotModules: sortBasePlotModules, - sortModules: sortModules -}; diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index ede60e8b130..6e00acd23d7 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -153,26 +153,6 @@ describe('Test Plots', function() { testSanitizeMarginsHasBeenCalledOnlyOnce(gd); }); - - it('should sort base plot modules on fullLayout object', function() { - var gd = Lib.extendDeep({}, require('@mocks/plot_types.json')); - gd.data.unshift({type: 'scattergl'}); - gd.data.push({type: 'splom'}); - - supplyAllDefaults(gd); - var names = gd._fullLayout._basePlotModules.map(function(m) { - return m.name; - }); - - expect(names).toEqual([ - 'splom', - 'cartesian', - 'gl3d', - 'geo', - 'pie', - 'ternary' - ]); - }); }); describe('Plots.supplyLayoutGlobalDefaults should', function() { From dcacd7813660d46a3758b0118c52c6b09f1c4e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 12:14:43 -0400 Subject: [PATCH 17/29] use redrawReglTraces on drag --- src/plots/cartesian/dragbox.js | 26 +++++++++++--------------- src/traces/scattergl/index.js | 2 -- src/traces/splom/base_plot.js | 4 +--- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index f9abcb1da6a..6f8d03c8cc2 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -16,13 +16,14 @@ var supportsPassive = require('has-passive-events'); var Registry = require('../../registry'); var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); -var clearGlCanvases = require('../../lib/clear_gl_canvases'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var Fx = require('../../components/fx'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../../components/dragelement'); var FROM_TL = require('../../constants/alignment').FROM_TL; +var clearGlCanvases = require('../../lib/clear_gl_canvases'); +var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var Plots = require('../plots'); @@ -84,7 +85,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // do we need to edit x/y ranges? var editX, editY; // graph-wide optimization flags - var hasScatterGl, hasOnlyLargeSploms, hasSplom, hasSVG; + var hasScatterGl, hasSplom, hasSVG; // collected changes to be made to the plot by relayout at the end var updates; @@ -125,8 +126,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { var fullLayout = gd._fullLayout; hasScatterGl = fullLayout._has('scattergl'); - hasOnlyLargeSploms = fullLayout._hasOnlyLargeSploms; - hasSplom = hasOnlyLargeSploms || fullLayout._has('splom'); + hasSplom = fullLayout._has('splom'); hasSVG = fullLayout._has('svg'); } @@ -744,33 +744,29 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { var subplots = fullLayout._subplots.cartesian; var i, sp, xa, ya; - if(hasSplom || hasScatterGl) { - clearGlCanvases(gd); - } - if(hasSplom) { Registry.subplotsRegistry.splom.drag(gd); - if(hasOnlyLargeSploms) return; } if(hasScatterGl) { - // loop over all subplots (w/o exceptions) here, - // as we cleared the gl canvases above for(i = 0; i < subplots.length; i++) { sp = plotinfos[subplots[i]]; xa = sp.xaxis; ya = sp.yaxis; - var scene = sp._scene; - if(scene) { - // FIXME: possibly we could update axis internal _r and _rl here + if(sp._scene) { var xrng = Lib.simpleMap(xa.range, xa.r2l); var yrng = Lib.simpleMap(ya.range, ya.r2l); - scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]}); + sp._scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]}); } } } + if(hasSplom || hasScatterGl) { + clearGlCanvases(gd); + redrawReglTraces(gd); + } + if(hasSVG) { var xScaleFactor = viewBox[2] / xa0._length; var yScaleFactor = viewBox[3] / ya0._length; diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 457dbd540e5..9de593704b9 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -252,8 +252,6 @@ function sceneUpdate(gd, subplot) { scene.glText[i].update(opt); } } - - scene.draw(); }; // draw traces in proper order diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index 8c13e18e5dc..ba7e805d892 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -39,7 +39,7 @@ function drag(gd) { var fullLayout = gd._fullLayout; if(fullLayout._hasOnlyLargeSploms) { - drawGrid(gd); + updateGrid(gd); } for(var i = 0; i < cd.length; i++) { @@ -77,10 +77,8 @@ function dragOne(gd, trace, scene) { if(scene.selectBatch) { scene.matrix.update({ranges: ranges}, {ranges: ranges}); - scene.matrix.draw(scene.unselectBatch, scene.selectBatch); } else { scene.matrix.update({ranges: ranges}); - scene.matrix.draw(); } } From 68dfbc17e80334ef472abd7a255070bb3135b199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 12:18:16 -0400 Subject: [PATCH 18/29] use redrawReglTraces on polar drag --- src/plots/polar/polar.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 0fc1b6e3a77..97142b3a31d 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -28,6 +28,8 @@ var prepSelect = require('../cartesian/select').prepSelect; var selectOnClick = require('../cartesian/select').selectOnClick; var clearSelect = require('../cartesian/select').clearSelect; var setCursor = require('../../lib/setcursor'); +var clearGlCanvases = require('../../lib/clear_gl_canvases'); +var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var MID_SHIFT = require('../../constants/alignment').MID_SHIFT; var constants = require('./constants'); @@ -1058,7 +1060,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) { .attr('transform', strTranslate(cx, cy)) .selectAll('path').attr('transform', null); - if(_this._scene) _this._scene.clear(); + var hasRegl = false; for(var traceType in _this.traceHash) { var moduleCalcData = _this.traceHash[traceType]; @@ -1066,6 +1068,12 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) { var _module = moduleCalcData[0][0].trace._module; var polarLayoutNow = gd._fullLayout[_this.id]; _module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow); + if(Registry.traceIs(traceType, 'gl') && moduleCalcDataVisible.length) hasRegl = true; + } + + if(hasRegl) { + clearGlCanvases(gd); + redrawReglTraces(gd); } } @@ -1185,7 +1193,7 @@ proto.updateAngularDrag = function(fullLayout) { scatterTraces.call(Drawing.hideOutsideRangePoints, _this); } - if(_this._scene) _this._scene.clear(); + var hasRegl = false; for(var traceType in _this.traceHash) { if(Registry.traceIs(traceType, 'gl')) { @@ -1193,8 +1201,14 @@ proto.updateAngularDrag = function(fullLayout) { var moduleCalcDataVisible = Lib.filterVisible(moduleCalcData); var _module = moduleCalcData[0][0].trace._module; _module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow); + if(moduleCalcDataVisible.length) hasRegl = true; } } + + if(hasRegl) { + clearGlCanvases(gd); + redrawReglTraces(gd); + } } function doneFn() { From c02330c95dc3b820f2d24592969a32529034b272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 12:32:11 -0400 Subject: [PATCH 19/29] fix #2797 - clear full canvas and use redrawReglTraces on selections - no longer need _module.styleOnSelect! - selections on overlaid subplots now work! - remove clearViewport (for now), we could bring it back again to optimize selections on disjoint subplots. --- src/plots/cartesian/select.js | 47 +++++++++------------------ src/traces/scattergl/index.js | 34 ------------------- src/traces/scatterpolargl/index.js | 1 - src/traces/splom/index.js | 26 --------------- test/jasmine/tests/gl2d_click_test.js | 43 ++++++++++++++++++++++++ test/jasmine/tests/splom_test.js | 15 +++++---- 6 files changed, 67 insertions(+), 99 deletions(-) diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 31200f8fb1f..35d39d285a6 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -19,7 +19,8 @@ var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); var makeEventData = require('../../components/fx/helpers').makeEventData; var getFromId = require('./axis_ids').getFromId; -var sortModules = require('../sort_modules').sortModules; +var clearGlCanvases = require('../../lib/clear_gl_canvases'); +var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var constants = require('./constants'); var MINSELECT = constants.MINSELECT; @@ -663,7 +664,7 @@ function isOnlyOnePointSelected(searchTraces) { } function updateSelectedState(gd, searchTraces, eventData) { - var i, j, searchInfo, trace; + var i, searchInfo, cd, trace; if(eventData) { var pts = eventData.points || []; @@ -696,43 +697,25 @@ function updateSelectedState(gd, searchTraces, eventData) { } } - // group searchInfo traces by trace modules - var lookup = {}; + var hasRegl = false; for(i = 0; i < searchTraces.length; i++) { searchInfo = searchTraces[i]; + cd = searchInfo.cd; + trace = cd[0].trace; - var name = searchInfo._module.name; - if(lookup[name]) { - lookup[name].push(searchInfo); - } else { - lookup[name] = [searchInfo]; + if(Registry.traceIs(trace, 'regl')) { + hasRegl = true; } + + var _module = searchInfo._module; + var fn = _module.styleOnSelect || _module.style; + if(fn) fn(gd, cd); } - var keys = Object.keys(lookup).sort(sortModules); - - for(i = 0; i < keys.length; i++) { - var items = lookup[keys[i]]; - var len = items.length; - var item0 = items[0]; - var trace0 = item0.cd[0].trace; - var _module = item0._module; - var styleSelection = _module.styleOnSelect || _module.style; - - if(Registry.traceIs(trace0, 'regl')) { - // plot regl traces per module - var cds = new Array(len); - for(j = 0; j < len; j++) { - cds[j] = items[j].cd; - } - styleSelection(gd, cds); - } else { - // plot svg trace per trace - for(j = 0; j < len; j++) { - styleSelection(gd, items[j].cd); - } - } + if(hasRegl) { + clearGlCanvases(gd); + redrawReglTraces(gd); } } diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 9de593704b9..8b4d7dc4f7f 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -294,18 +294,6 @@ function sceneUpdate(gd, subplot) { scene.dirty = false; }; - scene.clear = function clear() { - var vp = getViewport(gd._fullLayout, subplot.xaxis, subplot.yaxis); - - if(scene.select2d) { - clearViewport(scene.select2d, vp); - } - - var anyComponent = scene.scatter2d || scene.line2d || - (scene.glText || [])[0] || scene.fill2d || scene.error2d; - if(anyComponent) clearViewport(anyComponent, vp); - }; - // remove scene resources scene.destroy = function destroy() { if(scene.fill2d) scene.fill2d.destroy(); @@ -357,14 +345,6 @@ function getViewport(fullLayout, xaxis, yaxis) { ]; } -function clearViewport(comp, vp) { - var gl = comp.regl._gl; - gl.enable(gl.SCISSOR_TEST); - gl.scissor(vp[0], vp[1], vp[2] - vp[0], vp[3] - vp[1]); - gl.clearColor(0, 0, 0, 0); - gl.clear(gl.COLOR_BUFFER_BIT); -} - function plot(gd, subplot, cdata) { if(!cdata.length) return; @@ -889,19 +869,6 @@ function selectPoints(searchInfo, selectionTester) { return selection; } -function styleOnSelect(gd, cds) { - var stash = cds[0][0].t; - var scene = stash._scene; - - // don't clear the subplot if there are splom traces - // on the graph - if(!gd._fullLayout._has('splom')) { - scene.clear(); - } - - scene.draw(); -} - function styleTextSelection(cd) { var cd0 = cd[0]; var stash = cd0.t; @@ -951,7 +918,6 @@ module.exports = { calc: calc, plot: plot, hoverPoints: hoverPoints, - styleOnSelect: styleOnSelect, selectPoints: selectPoints, sceneOptions: sceneOptions, diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index 70024feb649..f317682eb90 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -183,7 +183,6 @@ module.exports = { calc: calc, plot: plot, hoverPoints: hoverPoints, - styleOnSelect: ScatterGl.styleOnSelect, selectPoints: ScatterGl.selectPoints, meta: { diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index ea759a89915..a08363f2f96 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -438,31 +438,6 @@ function selectPoints(searchInfo, selectionTester) { return selection; } -function styleOnSelect(gd, cds) { - var fullLayout = gd._fullLayout; - var cd0 = cds[0]; - var scene0 = fullLayout._splomScenes[cd0[0].trace.uid]; - scene0.matrix.regl.clear({color: true, depth: true}); - - if(fullLayout._splomGrid) { - fullLayout._splomGrid.draw(); - } - - for(var i = 0; i < cds.length; i++) { - var scene = fullLayout._splomScenes[cds[i][0].trace.uid]; - scene.draw(); - } - - // redraw all subplot with scattergl traces, - // as we cleared the whole canvas above - if(fullLayout._has('cartesian')) { - for(var k in fullLayout._plots) { - var sp = fullLayout._plots[k]; - if(sp._scene) sp._scene.draw(); - } - } -} - function getDimIndex(trace, ax) { var axId = ax._id; var axLetter = axId.charAt(0); @@ -491,7 +466,6 @@ module.exports = { plot: plot, hoverPoints: hoverPoints, selectPoints: selectPoints, - styleOnSelect: styleOnSelect, editStyle: editStyle, meta: { diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index c2b2121e386..34a826faca9 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -1135,4 +1135,47 @@ describe('@noCI Test gl2d lasso/select:', function() { .catch(failTest) .then(done); }); + + it('should work on overlaid subplots', function(done) { + gd = createGraphDiv(); + + var scene, scene2; + + Plotly.plot(gd, [{ + x: [1, 2, 3], + y: [40, 50, 60], + type: 'scattergl', + mode: 'markers' + }, { + x: [2, 3, 4], + y: [4, 5, 6], + yaxis: 'y2', + type: 'scattergl', + mode: 'markers' + }], { + xaxis: {domain: [0.2, 1]}, + yaxis2: {overlaying: 'y', side: 'left', position: 0}, + showlegend: false, + margin: {l: 0, t: 0, b: 0, r: 0}, + width: 400, + height: 400, + dragmode: 'select' + }) + .then(delay(100)) + .then(function() { + scene = gd._fullLayout._plots.xy._scene; + scene2 = gd._fullLayout._plots.xy2._scene; + + spyOn(scene.scatter2d, 'draw'); + spyOn(scene2.scatter2d, 'draw'); + }) + .then(function() { return select([[20, 20], [380, 250]]); }) + .then(function() { + expect(scene.scatter2d.draw).toHaveBeenCalledTimes(1); + expect(scene2.scatter2d.draw).toHaveBeenCalledTimes(1); + }) + .catch(failTest) + .then(done); + + }); }); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 5760f4bb528..0babe3221fa 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -1298,23 +1298,26 @@ describe('Test splom select:', function() { var cnt = 0; var scatterGlCnt = 0; var splomCnt = 0; + var scatterglScene, splomScene; Plotly.newPlot(gd, fig).then(function() { - // 'scattergl' trace module - spyOn(gd._fullLayout._modules[0], 'styleOnSelect').and.callFake(function() { + var fullLayout = gd._fullLayout; + scatterglScene = fullLayout._plots.xy._scene; + splomScene = fullLayout._splomScenes[gd._fullData[1].uid]; + + spyOn(scatterglScene, 'draw').and.callFake(function() { cnt++; scatterGlCnt = cnt; }); - // 'splom' trace module - spyOn(gd._fullLayout._modules[1], 'styleOnSelect').and.callFake(function() { + spyOn(splomScene, 'draw').and.callFake(function() { cnt++; splomCnt = cnt; }); }) .then(function() { return _select([[20, 395], [195, 205]]); }) .then(function() { - expect(gd._fullLayout._modules[0].styleOnSelect).toHaveBeenCalledTimes(1); - expect(gd._fullLayout._modules[1].styleOnSelect).toHaveBeenCalledTimes(1); + expect(scatterglScene.draw).toHaveBeenCalledTimes(1); + expect(splomScene.draw).toHaveBeenCalledTimes(1); expect(cnt).toBe(2); expect(splomCnt).toBe(1, 'splom redraw before scattergl'); From 02263b9683305088f2377c26f72b6ad54384b8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 12:33:04 -0400 Subject: [PATCH 20/29] use redrawReglTraces in edit subroutines --- src/plot_api/subroutines.js | 25 +++++++++++-------------- src/traces/splom/base_plot.js | 1 + src/traces/splom/index.js | 2 -- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index eb726c22877..6dd3cfa8efa 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -462,21 +462,21 @@ exports.drawMainTitle = function(gd) { }); }; -// First, see if we need to do arraysToCalcdata -// call it regardless of what change we made, in case -// supplyDefaults brought in an array that was already -// in gd.data but not in gd._fullData previously exports.doTraceStyle = function(gd) { - var fullLayout = gd._fullLayout; + var calcdata = gd.calcdata; var editStyleCalls = []; var i; - for(i = 0; i < gd.calcdata.length; i++) { - var cd = gd.calcdata[i]; + for(i = 0; i < calcdata.length; i++) { + var cd = calcdata[i]; var cd0 = cd[0] || {}; var trace = cd0.trace || {}; var _module = trace._module || {}; + // See if we need to do arraysToCalcdata + // call it regardless of what change we made, in case + // supplyDefaults brought in an array that was already + // in gd.data but not in gd._fullData previously var arraysToCalcdata = _module.arraysToCalcdata; if(arraysToCalcdata) arraysToCalcdata(cd, trace); @@ -485,16 +485,12 @@ exports.doTraceStyle = function(gd) { } if(editStyleCalls.length) { - clearGlCanvases(gd); - - if(fullLayout._hasOnlyLargeSploms) { - fullLayout._splomGrid.draw(); - } - for(i = 0; i < editStyleCalls.length; i++) { var edit = editStyleCalls[i]; edit.fn(gd, edit.cd0); } + clearGlCanvases(gd); + exports.redrawReglTraces(gd); } Plots.style(gd); @@ -546,8 +542,9 @@ exports.doTicksRelayout = function(gd) { Axes.doTicks(gd, 'redraw'); if(gd._fullLayout._hasOnlyLargeSploms) { + Registry.subplotsRegistry.splom.updateGrid(gd); clearGlCanvases(gd); - Registry.subplotsRegistry.splom.plot(gd); + exports.redrawReglTraces(gd); } exports.drawMainTitle(gd); diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index ba7e805d892..6e3b211fa05 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -234,6 +234,7 @@ module.exports = { drawFramework: Cartesian.drawFramework, plot: plot, drag: drag, + updateGrid: updateGrid, clean: clean, updateFx: updateFx, toSVG: Cartesian.toSVG diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index a08363f2f96..8e6e03e9091 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -318,8 +318,6 @@ function editStyle(gd, cd0) { // TODO this is too long for arrayOk attributes! scene.matrix.update(opts, null); - - scene.draw(); } function hoverPoints(pointData, xval, yval) { From f179c2f9a333393bebf0abbb00a08e503de2da75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 14:46:40 -0400 Subject: [PATCH 21/29] skip canvas/context size mismatch test on CI --- test/jasmine/tests/splom_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 0babe3221fa..f7561562582 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -722,7 +722,7 @@ describe('Test splom interactions:', function() { .then(done); }); - it('@gl should clear graph and replot when canvas and WebGL context dimensions do not match', function(done) { + it('@noCI @gl should clear graph and replot when canvas and WebGL context dimensions do not match', function(done) { var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); function assertDims(msg, w, h) { From 0aad7eaa048d155032af5591243a36bd7fc34608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 15:27:02 -0400 Subject: [PATCH 22/29] fixup (add missing @gl) --- test/jasmine/tests/gl2d_click_test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 34a826faca9..94d0d97d84a 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -1136,7 +1136,7 @@ describe('@noCI Test gl2d lasso/select:', function() { .then(done); }); - it('should work on overlaid subplots', function(done) { + it('@gl should work on overlaid subplots', function(done) { gd = createGraphDiv(); var scene, scene2; @@ -1176,6 +1176,5 @@ describe('@noCI Test gl2d lasso/select:', function() { }) .catch(failTest) .then(done); - }); }); From 078c0864014fb93c8ca8b0525353144a9d0eece0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 2 Oct 2018 16:54:09 -0400 Subject: [PATCH 23/29] clear axis types when restyling splom show(upper|lower)half .... and diagonal.visible --- src/plot_api/helpers.js | 43 ++++++++++++++++++++------------ src/traces/splom/attributes.js | 6 ++--- test/jasmine/tests/splom_test.js | 39 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 8a49ae911ae..efff787be25 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -18,6 +18,7 @@ var Plots = require('../plots/plots'); var AxisIds = require('../plots/cartesian/axis_ids'); var cleanId = AxisIds.cleanId; var getFromTrace = AxisIds.getFromTrace; +var id2name = AxisIds.id2name; var Color = require('../components/color'); @@ -570,25 +571,35 @@ exports.hasParent = function(aobj, attr) { * @param {object} layoutUpdate: any update being done concurrently to the layout, * which may supercede clearing the axis types */ -var axLetters = ['x', 'y', 'z']; +var xy = ['x', 'y']; +var xyz = ['x', 'y', 'z']; exports.clearAxisTypes = function(gd, traces, layoutUpdate) { for(var i = 0; i < traces.length; i++) { var trace = gd._fullData[i]; - for(var j = 0; j < 3; j++) { - var ax = getFromTrace(gd, trace, axLetters[j]); - - // do not clear log type - that's never an auto result so must have been intentional - if(ax && ax.type !== 'log') { - var axAttr = ax._name; - var sceneName = ax._id.substr(1); - if(sceneName.substr(0, 5) === 'scene') { - if(layoutUpdate[sceneName] !== undefined) continue; - axAttr = sceneName + '.' + axAttr; - } - var typeAttr = axAttr + '.type'; - - if(layoutUpdate[axAttr] === undefined && layoutUpdate[typeAttr] === undefined) { - Lib.nestedProperty(gd.layout, typeAttr).set(null); + var letters = Registry.traceIs(trace, 'gl3d') ? xyz : xy; + + for(var j = 0; j < letters.length; j++) { + var l = letters[j]; + var axes = trace.type === 'splom' ? + trace[l + 'axes'].map(function(id) { return gd._fullLayout[id2name(id)]; }) : + [getFromTrace(gd, trace, l)]; + + for(var k = 0; k < axes.length; k++) { + var ax = axes[k]; + + // do not clear log type - that's never an auto result so must have been intentional + if(ax && ax.type !== 'log') { + var axAttr = ax._name; + var sceneName = ax._id.substr(1); + if(sceneName.substr(0, 5) === 'scene') { + if(layoutUpdate[sceneName] !== undefined) continue; + axAttr = sceneName + '.' + axAttr; + } + var typeAttr = axAttr + '.type'; + + if(layoutUpdate[axAttr] === undefined && layoutUpdate[typeAttr] === undefined) { + Lib.nestedProperty(gd.layout, typeAttr).set(null); + } } } } diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index 2a6aeb321de..8c261a33abf 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -125,7 +125,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calc', + editType: 'calc+clearAxisTypes', description: [ 'Determines whether or not subplots on the diagonal are displayed.' ].join(' ') @@ -142,7 +142,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calc', + editType: 'calc+clearAxisTypes', description: [ 'Determines whether or not subplots on the upper half', 'from the diagonal are displayed.' @@ -152,7 +152,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calc', + editType: 'calc+clearAxisTypes', description: [ 'Determines whether or not subplots on the lower half', 'from the diagonal are displayed.' diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index f7561562582..4a6e0986f4f 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -774,6 +774,45 @@ describe('Test splom interactions:', function() { .catch(failTest) .then(done); }); + + it('@gl should clear axis auto-types when changing subplot arrangement', function(done) { + var data = [{ + type: 'splom', + showupperhalf: false, + diagonal: {visible: false}, + dimensions: [{ + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + }, { + values: ['lyndon', 'richard', 'gerald', 'jimmy', 'ronald', 'george', 'bill', 'georgeW', 'barack', 'donald'] + }, { + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + }] + }]; + + Plotly.plot(gd, data).then(function() { + expect(gd.layout.xaxis.type).toBe('linear'); + expect(gd.layout.xaxis2.type).toBe('category'); + expect(gd.layout.xaxis3).toBeUndefined(); + expect(gd.layout.yaxis.type).toBe('category'); + expect(gd.layout.yaxis2.type).toBe('linear'); + expect(gd.layout.yaxis3).toBeUndefined(); + + return Plotly.restyle(gd, { + 'showupperhalf': true, + 'diagonal.visible': true + }); + }) + .then(function() { + expect(gd.layout.xaxis.type).toBe('linear'); + expect(gd.layout.xaxis2.type).toBe('category'); + expect(gd.layout.xaxis3.type).toBe('linear'); + expect(gd.layout.yaxis.type).toBe('linear'); + expect(gd.layout.yaxis2.type).toBe('category'); + expect(gd.layout.yaxis3.type).toBe('linear'); + }) + .catch(failTest) + .then(done); + }); }); describe('Test splom update switchboard:', function() { From 00cae485bc5dc23a8da6c29d8eb5a43b222053ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 3 Oct 2018 12:32:16 -0400 Subject: [PATCH 24/29] Revert "clear axis types when restyling splom show(upper|lower)half" This reverts commit 078c0864014fb93c8ca8b0525353144a9d0eece0. --- src/plot_api/helpers.js | 43 ++++++++++++-------------------- src/traces/splom/attributes.js | 6 ++--- test/jasmine/tests/splom_test.js | 39 ----------------------------- 3 files changed, 19 insertions(+), 69 deletions(-) diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index efff787be25..8a49ae911ae 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -18,7 +18,6 @@ var Plots = require('../plots/plots'); var AxisIds = require('../plots/cartesian/axis_ids'); var cleanId = AxisIds.cleanId; var getFromTrace = AxisIds.getFromTrace; -var id2name = AxisIds.id2name; var Color = require('../components/color'); @@ -571,35 +570,25 @@ exports.hasParent = function(aobj, attr) { * @param {object} layoutUpdate: any update being done concurrently to the layout, * which may supercede clearing the axis types */ -var xy = ['x', 'y']; -var xyz = ['x', 'y', 'z']; +var axLetters = ['x', 'y', 'z']; exports.clearAxisTypes = function(gd, traces, layoutUpdate) { for(var i = 0; i < traces.length; i++) { var trace = gd._fullData[i]; - var letters = Registry.traceIs(trace, 'gl3d') ? xyz : xy; - - for(var j = 0; j < letters.length; j++) { - var l = letters[j]; - var axes = trace.type === 'splom' ? - trace[l + 'axes'].map(function(id) { return gd._fullLayout[id2name(id)]; }) : - [getFromTrace(gd, trace, l)]; - - for(var k = 0; k < axes.length; k++) { - var ax = axes[k]; - - // do not clear log type - that's never an auto result so must have been intentional - if(ax && ax.type !== 'log') { - var axAttr = ax._name; - var sceneName = ax._id.substr(1); - if(sceneName.substr(0, 5) === 'scene') { - if(layoutUpdate[sceneName] !== undefined) continue; - axAttr = sceneName + '.' + axAttr; - } - var typeAttr = axAttr + '.type'; - - if(layoutUpdate[axAttr] === undefined && layoutUpdate[typeAttr] === undefined) { - Lib.nestedProperty(gd.layout, typeAttr).set(null); - } + for(var j = 0; j < 3; j++) { + var ax = getFromTrace(gd, trace, axLetters[j]); + + // do not clear log type - that's never an auto result so must have been intentional + if(ax && ax.type !== 'log') { + var axAttr = ax._name; + var sceneName = ax._id.substr(1); + if(sceneName.substr(0, 5) === 'scene') { + if(layoutUpdate[sceneName] !== undefined) continue; + axAttr = sceneName + '.' + axAttr; + } + var typeAttr = axAttr + '.type'; + + if(layoutUpdate[axAttr] === undefined && layoutUpdate[typeAttr] === undefined) { + Lib.nestedProperty(gd.layout, typeAttr).set(null); } } } diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index 8c261a33abf..2a6aeb321de 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -125,7 +125,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calc+clearAxisTypes', + editType: 'calc', description: [ 'Determines whether or not subplots on the diagonal are displayed.' ].join(' ') @@ -142,7 +142,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calc+clearAxisTypes', + editType: 'calc', description: [ 'Determines whether or not subplots on the upper half', 'from the diagonal are displayed.' @@ -152,7 +152,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calc+clearAxisTypes', + editType: 'calc', description: [ 'Determines whether or not subplots on the lower half', 'from the diagonal are displayed.' diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 4a6e0986f4f..f7561562582 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -774,45 +774,6 @@ describe('Test splom interactions:', function() { .catch(failTest) .then(done); }); - - it('@gl should clear axis auto-types when changing subplot arrangement', function(done) { - var data = [{ - type: 'splom', - showupperhalf: false, - diagonal: {visible: false}, - dimensions: [{ - values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - }, { - values: ['lyndon', 'richard', 'gerald', 'jimmy', 'ronald', 'george', 'bill', 'georgeW', 'barack', 'donald'] - }, { - values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - }] - }]; - - Plotly.plot(gd, data).then(function() { - expect(gd.layout.xaxis.type).toBe('linear'); - expect(gd.layout.xaxis2.type).toBe('category'); - expect(gd.layout.xaxis3).toBeUndefined(); - expect(gd.layout.yaxis.type).toBe('category'); - expect(gd.layout.yaxis2.type).toBe('linear'); - expect(gd.layout.yaxis3).toBeUndefined(); - - return Plotly.restyle(gd, { - 'showupperhalf': true, - 'diagonal.visible': true - }); - }) - .then(function() { - expect(gd.layout.xaxis.type).toBe('linear'); - expect(gd.layout.xaxis2.type).toBe('category'); - expect(gd.layout.xaxis3.type).toBe('linear'); - expect(gd.layout.yaxis.type).toBe('linear'); - expect(gd.layout.yaxis2.type).toBe('category'); - expect(gd.layout.yaxis3.type).toBe('linear'); - }) - .catch(failTest) - .then(done); - }); }); describe('Test splom update switchboard:', function() { From 5878223e43c3834add3a2873b44316625b379266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 4 Oct 2018 11:25:51 -0400 Subject: [PATCH 25/29] make trace `(x|y)axes` always have the same length dimensions` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ... regardless of the partial visibilities. ie `trace.xaxes: ['x', 'x2', 'x3', 'x4']` even though we aren’t going to create an `xaxis4`. Should make it easier for users who don’t want to use the default axes but still want to be able to turn on/off the upper/lower/diag --- src/traces/splom/attributes.js | 7 +- src/traces/splom/defaults.js | 136 +++++++++++----------- test/jasmine/tests/splom_test.js | 191 ++++++++++++++++++++++++++++++- 3 files changed, 257 insertions(+), 77 deletions(-) diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index 2a6aeb321de..bceb9e68f83 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -31,9 +31,12 @@ function makeAxesValObject(axLetter) { }, description: [ 'Sets the list of ' + axLetter + ' axes', - 'corresponding to this splom trace.', + 'corresponding to dimensions of this splom trace.', 'By default, a splom will match the first N ' + axLetter + 'axes', - 'where N is the number of input dimensions.' + 'where N is the number of input dimensions.', + 'Note that, in case where `diagonal.visible` is false and `showupperhalf`', + 'or `showlowerhalf` is false, this splom trace will generate', + 'one less x-axis and one less y-axis.', ].join(' ') }; } diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index 37f6d1ca066..107015636a2 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -73,97 +73,95 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { var showDiag = traceOut.diagonal.visible; var i, j; - // N.B. one less x axis AND one less y axis when hiding one half and the diagonal - var axDfltLength = !showDiag && (!showUpper || !showLower) ? dimLength - 1 : dimLength; + var xAxesDflt = new Array(dimLength); + var yAxesDflt = new Array(dimLength); - var xaxes = coerce('xaxes', fillAxisIdArray('x', axDfltLength)); - var yaxes = coerce('yaxes', fillAxisIdArray('y', axDfltLength)); + for(i = 0; i < dimLength; i++) { + var suffix = i ? i + 1 : ''; + xAxesDflt[i] = 'x' + suffix; + yAxesDflt[i] = 'y' + suffix; + } - // to avoid costly indexOf - traceOut._xaxes = arrayToHashObject(xaxes); - traceOut._yaxes = arrayToHashObject(yaxes); + var xaxes = coerce('xaxes', xAxesDflt); + var yaxes = coerce('yaxes', yAxesDflt); - // allow users to under-specify number of axes - var axLength = Math.min(axDfltLength, xaxes.length, yaxes.length); + // build list of [x,y] axis corresponding to each dimensions[i], + // very useful for passing options to regl-splom + var diag = traceOut._diag = new Array(dimLength); - // fill in splom subplot keys - for(i = 0; i < axLength; i++) { - for(j = 0; j < axLength; j++) { - var id = xaxes[i] + yaxes[j]; + // lookup for 'drawn' x|y axes, to avoid costly indexOf downstream + traceOut._xaxes = {}; + traceOut._yaxes = {}; - if(i > j && showUpper) { - layout._splomSubplots[id] = 1; - } else if(i < j && showLower) { - layout._splomSubplots[id] = 1; - } else if(i === j && (showDiag || !showLower || !showUpper)) { - // need to include diagonal subplots when - // hiding one half and the diagonal - layout._splomSubplots[id] = 1; + // list of 'drawn' x|y axes, use to generate list of subplots + var xList = []; + var yList = []; + + function fillAxisStashes(axId, dim, list) { + if(!axId) return; + + var axLetter = axId.charAt(0); + var stash = layout._splomAxes[axLetter]; + + traceOut['_' + axLetter + 'axes'][axId] = 1; + list.push(axId); + + if(!(axId in stash)) { + var s = stash[axId] = {}; + if(dim) { + s.label = dim.label || ''; + if(dim.visible && dim.axis) { + s.type = dim.axis.type; + } } } } - // build list of [x,y] axis corresponding to each dimensions[i], - // very useful for passing options to regl-splom - var diag = traceOut._diag = new Array(dimLength); - // cases where showDiag and showLower or showUpper are false - // no special treatment as the xaxes and yaxes items no longer match - // the dimensions items 1-to-1 - var xShift = !showDiag && !showLower ? -1 : 0; - var yShift = !showDiag && !showUpper ? -1 : 0; + // no special treatment as the 'drawn' x-axes and y-axes no longer match + // the dimensions items and xaxes|yaxes 1-to-1 + var mustShiftX = !showDiag && !showLower; + var mustShiftY = !showDiag && !showUpper; for(i = 0; i < dimLength; i++) { var dim = dimensions[i]; - var xaId = xaxes[i + xShift]; - var yaId = yaxes[i + yShift]; - - fillAxisStash(layout, xaId, dim); - fillAxisStash(layout, yaId, dim); - - // note that some the entries here may be undefined - diag[i] = [xaId, yaId]; - } + var i0 = i === 0; + var iN = i === dimLength - 1; - // when lower half is omitted, override grid default - // to make sure axes remain on the left/bottom of the plot area - if(!showLower) { - layout._splomGridDflt.xside = 'bottom'; - layout._splomGridDflt.yside = 'left'; - } -} + var xaId = (i0 && mustShiftX) || (iN && mustShiftY) ? + undefined : + xaxes[i]; -function fillAxisIdArray(axLetter, len) { - var out = new Array(len); + var yaId = (i0 && mustShiftY) || (iN && mustShiftX) ? + undefined : + yaxes[i]; - for(var i = 0; i < len; i++) { - out[i] = axLetter + (i ? i + 1 : ''); + fillAxisStashes(xaId, dim, xList); + fillAxisStashes(yaId, dim, yList); + diag[i] = [xaId, yaId]; } - return out; -} - -function fillAxisStash(layout, axId, dim) { - if(!axId) return; - - var axLetter = axId.charAt(0); - var stash = layout._splomAxes[axLetter]; + // fill in splom subplot keys + for(i = 0; i < xList.length; i++) { + for(j = 0; j < yList.length; j++) { + var id = xList[i] + yList[j]; - if(!(axId in stash)) { - var s = stash[axId] = {}; - if(dim) { - s.label = dim.label || ''; - if(dim.visible && dim.axis) { - s.type = dim.axis.type; + if(i > j && showUpper) { + layout._splomSubplots[id] = 1; + } else if(i < j && showLower) { + layout._splomSubplots[id] = 1; + } else if(i === j && (showDiag || !showLower || !showUpper)) { + // need to include diagonal subplots when + // hiding one half and the diagonal + layout._splomSubplots[id] = 1; } } } -} -function arrayToHashObject(arr) { - var obj = {}; - for(var i = 0; i < arr.length; i++) { - obj[arr[i]] = 1; + // when lower half is omitted, override grid default + // to make sure axes remain on the left/bottom of the plot area + if(!showLower) { + layout._splomGridDflt.xside = 'bottom'; + layout._splomGridDflt.yside = 'left'; } - return obj; } diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index f7561562582..be7192abd8f 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -75,7 +75,7 @@ describe('Test splom trace defaults:', function() { expect(fullLayout.yaxis.domain).toBeCloseToArray([0, 1]); }); - it('should set `grid.xaxes` and `grid.yaxes` default using the new of dimensions', function() { + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions', function() { _supply({ dimensions: [ {values: [1, 2, 3]}, @@ -102,6 +102,105 @@ describe('Test splom trace defaults:', function() { expect(subplots.cartesian).toEqual(['xy', 'xy2', 'x2y', 'x2y2']); }); + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no upper half, no diagonal case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showupperhalf: false, + diagonal: {visible: false} + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x', 'x2', 'x3']); + expect(fullTrace.yaxes).toEqual(['y', 'y2', 'y3']); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.47]); + expect(fullLayout.yaxis2.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.yaxis3.domain).toBeCloseToArray([0, 0.47]); + + var subplots = fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x', 'x2']); + expect(subplots.yaxis).toEqual(['y2', 'y3']); + expect(subplots.cartesian).toEqual(['xy2', 'xy3', 'x2y3']); + }); + + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no lower half, no diagonal case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showlowerhalf: false, + diagonal: {visible: false} + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x', 'x2', 'x3']); + expect(fullTrace.yaxes).toEqual(['y', 'y2', 'y3']); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis2.domain).toBeCloseToArray([0, 0.47]); + expect(fullLayout.yaxis.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.xaxis3.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.47]); + + var subplots = fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x2', 'x3']); + expect(subplots.yaxis).toEqual(['y', 'y2']); + expect(subplots.cartesian).toEqual(['x2y', 'x3y', 'x3y2']); + }); + + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no upper half, no diagonal, set x|y axes case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showupperhalf: false, + diagonal: {visible: false}, + xaxes: ['x5', 'x6', 'x7'], + yaxes: ['y6', 'y7', 'y8'] + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x5', 'x6', 'x7']); + expect(fullTrace.yaxes).toEqual(['y6', 'y7', 'y8']); + + var subplots = gd._fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x5', 'x6']); + expect(subplots.yaxis).toEqual(['y7', 'y8']); + expect(subplots.cartesian).toEqual(['x5y7', 'x5y8', 'x6y8']); + }); + + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no lower half, no diagonal, set x|y axes case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showlowerhalf: false, + diagonal: {visible: false}, + xaxes: ['x5', 'x6', 'x7'], + yaxes: ['y6', 'y7', 'y8'] + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x5', 'x6', 'x7']); + expect(fullTrace.yaxes).toEqual(['y6', 'y7', 'y8']); + + var subplots = gd._fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x6', 'x7']); + expect(subplots.yaxis).toEqual(['y6', 'y7']); + expect(subplots.cartesian).toEqual(['x6y6', 'x7y6', 'x7y7']); + }); it('should use special `grid.xside` and `grid.yside` defaults on splom w/o lower half generated grids', function() { var gridOut; @@ -489,14 +588,14 @@ describe('Test splom interactions:', function() { return Plotly.restyle(gd, 'diagonal.visible', false); }) .then(function() { - _assert([1138, 7636, 1594, 4]); + _assert([1138, 7654, 1600]); return Plotly.restyle(gd, { showupperhalf: true, showlowerhalf: false }); }) .then(function() { - _assert([64, 8176, 1582, 112]); + _assert([8188, 112, 1588]); return Plotly.restyle(gd, 'diagonal.visible', true); }) .then(function() { @@ -648,21 +747,21 @@ describe('Test splom interactions:', function() { expect(gd._fullLayout.grid.yside).toBe('left', 'sanity check dflt grid.yside'); _assert({ - x: 433, x2: 433, x3: 433, + x2: 433, x3: 433, x4: 433, y: 80, y2: 80, y3: 80 }); return Plotly.relayout(gd, 'grid.yside', 'left plot'); }) .then(function() { _assert({ - x: 433, x2: 433, x3: 433, + x2: 433, x3: 433, x4: 433, y: 79, y2: 230, y3: 382 }); return Plotly.relayout(gd, 'grid.xside', 'bottom plot'); }) .then(function() { _assert({ - x: 212, x2: 323, x3: 433, + x2: 212, x3: 323, x4: 433, y: 79, y2: 230, y3: 382 }); }) @@ -774,6 +873,86 @@ describe('Test splom interactions:', function() { .catch(failTest) .then(done); }); + + it('@gl should update axis arrangement on show(upper|lower)half + diagonal.visible restyles', function(done) { + var seq = ['', '2', '3', '4']; + + function getAxesTypes(cont, letter) { + return seq.map(function(s) { + var ax = cont[letter + 'axis' + s]; + return ax ? ax.type : null; + }); + } + + // undefined means there's an axis object, but no 'type' key in it + // null means there's no axis object + function _assertAxisTypes(msg, exp) { + var xaxes = getAxesTypes(gd.layout, 'x'); + var yaxes = getAxesTypes(gd.layout, 'y'); + var fullXaxes = getAxesTypes(gd._fullLayout, 'x'); + var fullYaxes = getAxesTypes(gd._fullLayout, 'y'); + + expect(xaxes).toEqual(exp.xaxes, msg); + expect(fullXaxes).toEqual(exp.fullXaxes, msg); + expect(yaxes).toEqual(exp.yaxes, msg); + expect(fullYaxes).toEqual(exp.fullYaxes, msg); + } + + var data = [{ + type: 'splom', + showupperhalf: false, + diagonal: {visible: false}, + dimensions: [{ + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + }, { + values: ['lyndon', 'richard', 'gerald', 'jimmy', 'ronald', 'george', 'bill', 'georgeW', 'barack', 'donald'] + }, { + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + axis: {type: 'category'} + }, { + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + axis: {type: 'log'} + }] + }]; + + Plotly.plot(gd, data).then(function() { + _assertAxisTypes('no upper half / no diagonal', { + xaxes: ['linear', 'category', undefined, null], + fullXaxes: ['linear', 'category', 'category', null], + yaxes: [null, 'category', undefined, undefined], + fullYaxes: [null, 'category', 'category', 'log'] + }); + + return Plotly.restyle(gd, { + 'showupperhalf': true, + 'diagonal.visible': true + }); + }) + .then(function() { + _assertAxisTypes('full grid', { + xaxes: ['linear', 'category', undefined, undefined], + fullXaxes: ['linear', 'category', 'category', 'log'], + yaxes: ['linear', 'category', undefined, undefined], + fullYaxes: ['linear', 'category', 'category', 'log'] + }); + + return Plotly.restyle(gd, { + 'showlowerhalf': false, + 'diagonal.visible': false + }); + }) + .then(function() { + // TODO should we delete layout.xaxis and layout.yaxis4 here? + _assertAxisTypes('no lower half / no diagonal', { + xaxes: ['linear', 'category', undefined, undefined], + fullXaxes: [null, 'category', 'category', 'log'], + yaxes: ['linear', 'category', undefined, undefined], + fullYaxes: ['linear', 'category', 'category', null] + }); + }) + .catch(failTest) + .then(done); + }); }); describe('Test splom update switchboard:', function() { From c2ecb3ab179eb66e993ae0579d98eabbcb9637c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 4 Oct 2018 11:39:30 -0400 Subject: [PATCH 26/29] :hocho: obsolete args --- src/plot_api/plot_api.js | 7 +++---- src/plots/plots.js | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index a749a2bf08f..b887aef9d07 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -269,7 +269,7 @@ exports.plot = function(gd, data, layout, config) { Lib.error(msg); } else { Lib.log(msg + ' Clearing graph and plotting again.'); - Plots.cleanPlot([], {}, gd._fullData, fullLayout, gd.calcdata); + Plots.cleanPlot([], {}, gd._fullData, fullLayout); Plots.supplyDefaults(gd); fullLayout = gd._fullLayout; Plots.doCalcdata(gd); @@ -614,7 +614,7 @@ exports.newPlot = function(gd, data, layout, config) { gd = Lib.getGraphDiv(gd); // remove gl contexts - Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}, gd.calcdata || []); + Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}); Plots.purge(gd); return exports.plot(gd, data, layout, config); @@ -3199,10 +3199,9 @@ exports.purge = function purge(gd) { var fullLayout = gd._fullLayout || {}; var fullData = gd._fullData || []; - var calcdata = gd.calcdata || []; // remove gl contexts - Plots.cleanPlot([], {}, fullData, fullLayout, calcdata); + Plots.cleanPlot([], {}, fullData, fullLayout); // purge properties Plots.purge(gd); diff --git a/src/plots/plots.js b/src/plots/plots.js index b0092864c61..bb6b1573539 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -467,7 +467,7 @@ plots.supplyDefaults = function(gd, opts) { plots.linkSubplots(newFullData, newFullLayout, oldFullData, oldFullLayout); // clean subplots and other artifacts from previous plot calls - plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata); + plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); // relink functions and _ attributes to promote consistency between plots relinkPrivateKeys(newFullLayout, oldFullLayout); @@ -714,7 +714,7 @@ plots._hasPlotType = function(category) { return false; }; -plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) { +plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { var i, j; var basePlotModules = oldFullLayout._basePlotModules || []; @@ -722,7 +722,7 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou var _module = basePlotModules[i]; if(_module.clean) { - _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata); + _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); } } From 31d5606d9870127341a42d7b3dbc125fcd36a641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 4 Oct 2018 11:57:36 -0400 Subject: [PATCH 27/29] fixup comments --- src/plot_api/subroutines.js | 3 ++- test/jasmine/tests/splom_test.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 6dd3cfa8efa..8827d4846fb 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -612,7 +612,7 @@ exports.drawData = function(gd) { return Plots.previousPromises(gd); }; -// Draw (or redraw) all traces in one go, +// Draw (or redraw) all regl-based traces in one go, // useful during drag and selection where buffers of targeted traces are updated, // but all traces need to be redrawn following clearGlCanvases. // @@ -624,6 +624,7 @@ exports.drawData = function(gd) { // non-overlaying, disjoint subplots. // // TODO try to include parcoords in here. +// https://github.com/plotly/plotly.js/issues/3069 exports.redrawReglTraces = function(gd) { var fullLayout = gd._fullLayout; diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index be7192abd8f..bdac34a174e 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -942,7 +942,6 @@ describe('Test splom interactions:', function() { }); }) .then(function() { - // TODO should we delete layout.xaxis and layout.yaxis4 here? _assertAxisTypes('no lower half / no diagonal', { xaxes: ['linear', 'category', undefined, undefined], fullXaxes: [null, 'category', 'category', 'log'], From ea38664e124f2b4e6e73d7095f6d807a1c8ea897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 4 Oct 2018 11:58:14 -0400 Subject: [PATCH 28/29] add noCI tag to misbehaving tests --- test/jasmine/tests/select_test.js | 15 +++------------ test/jasmine/tests/splom_test.js | 2 +- test/jasmine/tests/streamtube_test.js | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 116a26b52b0..2a903e20c1c 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -649,25 +649,16 @@ describe('Click-to-select', function() { }); }); - // The gl traces: use @gl CI annotation + // The gl and mapbox traces: use @gl and @noCI tag [ testCase('scatterpolargl', require('@mocks/glpolar_scatter.json'), 130, 290, [[], [], [], [19], [], []], { dragmode: 'zoom' }), - testCase('splom', require('@mocks/splom_lower.json'), 427, 400, [[], [7], []]) - ] - .forEach(function(testCase) { - it('@gl trace type ' + testCase.label, function(done) { - _run(testCase, done); - }); - }); - - // The mapbox traces: use @noCI annotation cause they are usually too resource-intensive - [ + testCase('splom', require('@mocks/splom_lower.json'), 427, 400, [[], [7], []]), testCase('scattermapbox', require('@mocks/mapbox_0.json'), 650, 195, [[2], []], {}, { mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN }) ] .forEach(function(testCase) { - it('@noCI trace type ' + testCase.label, function(done) { + it('@noCI @gl trace type ' + testCase.label, function(done) { _run(testCase, done); }); }); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index bdac34a174e..ab8c21c7a92 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -1505,7 +1505,7 @@ describe('Test splom select:', function() { .then(done); }); - it('@gl should behave correctly during select->dblclick->pan scenarios', function(done) { + it('@noCI @gl should behave correctly during select->dblclick->pan scenarios', function(done) { var fig = Lib.extendDeep({}, require('@mocks/splom_0.json')); fig.layout = { width: 400, diff --git a/test/jasmine/tests/streamtube_test.js b/test/jasmine/tests/streamtube_test.js index e15eceb4bc4..647fb49f5a2 100644 --- a/test/jasmine/tests/streamtube_test.js +++ b/test/jasmine/tests/streamtube_test.js @@ -295,7 +295,7 @@ describe('Test streamtube interactions', function() { }); }); -describe('Test streamtube hover', function() { +describe('@noCI Test streamtube hover', function() { var gd; beforeEach(function() { From 4137433d1b9618c5362b5459462c4ec265d4ba47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 4 Oct 2018 13:26:08 -0400 Subject: [PATCH 29/29] place splom axes on bottom/left sides when just diagonal is missing from empet's bug report: https://community.plot.ly/t/ticklabels-in-splom-with-diagonal-visible-false-are-translated/14196 --- src/traces/splom/defaults.js | 7 +- test/image/baselines/splom_nodiag.png | Bin 0 -> 62937 bytes test/image/mocks/splom_nodiag.json | 705 ++++++++++++++++++++++++++ 3 files changed, 709 insertions(+), 3 deletions(-) create mode 100644 test/image/baselines/splom_nodiag.png create mode 100644 test/image/mocks/splom_nodiag.json diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index 107015636a2..57d43cfea26 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -158,9 +158,10 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { } } - // when lower half is omitted, override grid default - // to make sure axes remain on the left/bottom of the plot area - if(!showLower) { + // when lower half is omitted, or when just the diagonal is gone, + // override grid default to make sure axes remain on + // the left/bottom of the plot area + if(!showLower || (!showDiag && showUpper && showLower)) { layout._splomGridDflt.xside = 'bottom'; layout._splomGridDflt.yside = 'left'; } diff --git a/test/image/baselines/splom_nodiag.png b/test/image/baselines/splom_nodiag.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6a7e14579300275bd49c98a471428729c1892b GIT binary patch literal 62937 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 P2Kc+Hp|9Sc3Xl3<^rM`2 literal 0 HcmV?d00001 diff --git a/test/image/mocks/splom_nodiag.json b/test/image/mocks/splom_nodiag.json new file mode 100644 index 00000000000..65d98b216ee --- /dev/null +++ b/test/image/mocks/splom_nodiag.json @@ -0,0 +1,705 @@ +{ + "data": [ + { + "type": "splom", + "name": "Setosa", + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "5.1", + "4.9", + "4.7", + "4.6", + "5.0", + "5.4", + "4.6", + "5.0", + "4.4", + "4.9", + "5.4", + "4.8", + "4.8", + "4.3", + "5.8", + "5.7", + "5.4", + "5.1", + "5.7", + "5.1", + "5.4", + "5.1", + "4.6", + "5.1", + "4.8", + "5.0", + "5.0", + "5.2", + "5.2", + "4.7", + "4.8", + "5.4", + "5.2", + "5.5", + "4.9", + "5.0", + "5.5", + "4.9", + "4.4", + "5.1", + "5.0", + "4.5", + "4.4", + "5.0", + "5.1", + "4.8", + "5.1", + "4.6", + "5.3", + "5.0" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.5", + "3.0", + "3.2", + "3.1", + "3.6", + "3.9", + "3.4", + "3.4", + "2.9", + "3.1", + "3.7", + "3.4", + "3.0", + "3.0", + "4.0", + "4.4", + "3.9", + "3.5", + "3.8", + "3.8", + "3.4", + "3.7", + "3.6", + "3.3", + "3.4", + "3.0", + "3.4", + "3.5", + "3.4", + "3.2", + "3.1", + "3.4", + "4.1", + "4.2", + "3.1", + "3.2", + "3.5", + "3.1", + "3.0", + "3.4", + "3.5", + "2.3", + "3.2", + "3.5", + "3.8", + "3.0", + "3.8", + "3.2", + "3.7", + "3.3" + ] + }, + { + "label": "PetalLength", + "values": [ + "1.4", + "1.4", + "1.3", + "1.5", + "1.4", + "1.7", + "1.4", + "1.5", + "1.4", + "1.5", + "1.5", + "1.6", + "1.4", + "1.1", + "1.2", + "1.5", + "1.3", + "1.4", + "1.7", + "1.5", + "1.7", + "1.5", + "1.0", + "1.7", + "1.9", + "1.6", + "1.6", + "1.5", + "1.4", + "1.6", + "1.6", + "1.5", + "1.5", + "1.4", + "1.5", + "1.2", + "1.3", + "1.5", + "1.3", + "1.5", + "1.3", + "1.3", + "1.3", + "1.6", + "1.9", + "1.4", + "1.6", + "1.4", + "1.5", + "1.4" + ] + }, + { + "label": "PetalWidth", + "values": [ + "0.2", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.3", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.1", + "0.2", + "0.4", + "0.4", + "0.3", + "0.3", + "0.3", + "0.2", + "0.4", + "0.2", + "0.5", + "0.2", + "0.2", + "0.4", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.1", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.3", + "0.3", + "0.2", + "0.6", + "0.4", + "0.3", + "0.2", + "0.2", + "0.2", + "0.2" + ] + } + ], + "marker": { + "color": "red" + } + }, + { + "type": "splom", + "name": "Versicolor", + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "7.0", + "6.4", + "6.9", + "5.5", + "6.5", + "5.7", + "6.3", + "4.9", + "6.6", + "5.2", + "5.0", + "5.9", + "6.0", + "6.1", + "5.6", + "6.7", + "5.6", + "5.8", + "6.2", + "5.6", + "5.9", + "6.1", + "6.3", + "6.1", + "6.4", + "6.6", + "6.8", + "6.7", + "6.0", + "5.7", + "5.5", + "5.5", + "5.8", + "6.0", + "5.4", + "6.0", + "6.7", + "6.3", + "5.6", + "5.5", + "5.5", + "6.1", + "5.8", + "5.0", + "5.6", + "5.7", + "5.7", + "6.2", + "5.1", + "5.7" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.2", + "3.2", + "3.1", + "2.3", + "2.8", + "2.8", + "3.3", + "2.4", + "2.9", + "2.7", + "2.0", + "3.0", + "2.2", + "2.9", + "2.9", + "3.1", + "3.0", + "2.7", + "2.2", + "2.5", + "3.2", + "2.8", + "2.5", + "2.8", + "2.9", + "3.0", + "2.8", + "3.0", + "2.9", + "2.6", + "2.4", + "2.4", + "2.7", + "2.7", + "3.0", + "3.4", + "3.1", + "2.3", + "3.0", + "2.5", + "2.6", + "3.0", + "2.6", + "2.3", + "2.7", + "3.0", + "2.9", + "2.9", + "2.5", + "2.8" + ] + }, + { + "label": "PetalLength", + "values": [ + "4.7", + "4.5", + "4.9", + "4.0", + "4.6", + "4.5", + "4.7", + "3.3", + "4.6", + "3.9", + "3.5", + "4.2", + "4.0", + "4.7", + "3.6", + "4.4", + "4.5", + "4.1", + "4.5", + "3.9", + "4.8", + "4.0", + "4.9", + "4.7", + "4.3", + "4.4", + "4.8", + "5.0", + "4.5", + "3.5", + "3.8", + "3.7", + "3.9", + "5.1", + "4.5", + "4.5", + "4.7", + "4.4", + "4.1", + "4.0", + "4.4", + "4.6", + "4.0", + "3.3", + "4.2", + "4.2", + "4.2", + "4.3", + "3.0", + "4.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "1.4", + "1.5", + "1.5", + "1.3", + "1.5", + "1.3", + "1.6", + "1.0", + "1.3", + "1.4", + "1.0", + "1.5", + "1.0", + "1.4", + "1.3", + "1.4", + "1.5", + "1.0", + "1.5", + "1.1", + "1.8", + "1.3", + "1.5", + "1.2", + "1.3", + "1.4", + "1.4", + "1.7", + "1.5", + "1.0", + "1.1", + "1.0", + "1.2", + "1.6", + "1.5", + "1.6", + "1.5", + "1.3", + "1.3", + "1.3", + "1.2", + "1.4", + "1.2", + "1.0", + "1.3", + "1.2", + "1.3", + "1.3", + "1.1", + "1.3" + ] + } + ], + "marker": { + "color": "green" + } + }, + { + "type": "splom", + "name": "Virginica", + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "6.3", + "5.8", + "7.1", + "6.3", + "6.5", + "7.6", + "4.9", + "7.3", + "6.7", + "7.2", + "6.5", + "6.4", + "6.8", + "5.7", + "5.8", + "6.4", + "6.5", + "7.7", + "7.7", + "6.0", + "6.9", + "5.6", + "7.7", + "6.3", + "6.7", + "7.2", + "6.2", + "6.1", + "6.4", + "7.2", + "7.4", + "7.9", + "6.4", + "6.3", + "6.1", + "7.7", + "6.3", + "6.4", + "6.0", + "6.9", + "6.7", + "6.9", + "5.8", + "6.8", + "6.7", + "6.7", + "6.3", + "6.5", + "6.2", + "5.9" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.3", + "2.7", + "3.0", + "2.9", + "3.0", + "3.0", + "2.5", + "2.9", + "2.5", + "3.6", + "3.2", + "2.7", + "3.0", + "2.5", + "2.8", + "3.2", + "3.0", + "3.8", + "2.6", + "2.2", + "3.2", + "2.8", + "2.8", + "2.7", + "3.3", + "3.2", + "2.8", + "3.0", + "2.8", + "3.0", + "2.8", + "3.8", + "2.8", + "2.8", + "2.6", + "3.0", + "3.4", + "3.1", + "3.0", + "3.1", + "3.1", + "3.1", + "2.7", + "3.2", + "3.3", + "3.0", + "2.5", + "3.0", + "3.4", + "3.0" + ] + }, + { + "label": "PetalLength", + "values": [ + "6.0", + "5.1", + "5.9", + "5.6", + "5.8", + "6.6", + "4.5", + "6.3", + "5.8", + "6.1", + "5.1", + "5.3", + "5.5", + "5.0", + "5.1", + "5.3", + "5.5", + "6.7", + "6.9", + "5.0", + "5.7", + "4.9", + "6.7", + "4.9", + "5.7", + "6.0", + "4.8", + "4.9", + "5.6", + "5.8", + "6.1", + "6.4", + "5.6", + "5.1", + "5.6", + "6.1", + "5.6", + "5.5", + "4.8", + "5.4", + "5.6", + "5.1", + "5.1", + "5.9", + "5.7", + "5.2", + "5.0", + "5.2", + "5.4", + "5.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "2.5", + "1.9", + "2.1", + "1.8", + "2.2", + "2.1", + "1.7", + "1.8", + "1.8", + "2.5", + "2.0", + "1.9", + "2.1", + "2.0", + "2.4", + "2.3", + "1.8", + "2.2", + "2.3", + "1.5", + "2.3", + "2.0", + "2.0", + "1.8", + "2.1", + "1.8", + "1.8", + "1.8", + "2.1", + "1.6", + "1.9", + "2.0", + "2.2", + "1.5", + "1.4", + "2.3", + "2.4", + "1.8", + "1.8", + "2.1", + "2.4", + "2.3", + "1.9", + "2.3", + "2.5", + "2.3", + "1.9", + "2.0", + "2.3", + "1.8" + ] + } + ], + "marker": { + "color": "blue" + } + } + ], + "layout": { + "title": "Iris dataset splom", + "width": 600, + "height": 500, + "legend": { + "x": 0, + "xanchor": "left", + "y": 1, + "yanchor": "top" + } + } +}