From 69dc0c429e997e470a926467d18a616b63dfa7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 31 Jul 2018 14:19:49 -0400 Subject: [PATCH 01/21] DRY and small pref boost for scattergl - DRY using .plot & .cleear with getViewport helper fn - Dry .plot and .update with repeat helper fn - compute viewport only once per subplot, as opposed to once per trace. --- src/traces/scattergl/index.js | 150 ++++++++++++++++------------------ 1 file changed, 71 insertions(+), 79 deletions(-) diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index ab398fa97a9..68fdc94efe4 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -122,16 +122,17 @@ function calc(gd, trace) { scene.textOptions.push(opts.text); scene.textSelectedOptions.push(opts.textSel); scene.textUnselectedOptions.push(opts.textUnsel); - scene.count++; // stash scene ref stash._scene = scene; - stash.index = scene.count - 1; + stash.index = scene.count; stash.x = x; stash.y = y; stash.positions = positions; stash.count = count; + scene.count++; + gd.firstscatter = false; return [{x: false, y: false, t: stash, trace: trace}]; } @@ -230,19 +231,16 @@ function sceneUpdate(gd, subplot) { // apply new option to all regl components (used on drag) scene.update = function update(opt) { - var i; - var opts = new Array(scene.count); - for(i = 0; i < scene.count; i++) { - opts[i] = opt; - } + var opts = repeat(opt, scene.count); + if(scene.fill2d) scene.fill2d.update(opts); if(scene.scatter2d) scene.scatter2d.update(opts); if(scene.line2d) scene.line2d.update(opts); if(scene.error2d) scene.error2d.update(opts.concat(opts)); if(scene.select2d) scene.select2d.update(opts); if(scene.glText) { - for(i = 0; i < scene.count; i++) { - scene.glText[i].update(opts[i]); + for(var i = 0; i < scene.count; i++) { + scene.glText[i].update(opt); } } @@ -290,18 +288,7 @@ function sceneUpdate(gd, subplot) { }; scene.clear = function clear() { - var fullLayout = gd._fullLayout; - var vpSize = fullLayout._size; - var width = fullLayout.width; - var height = fullLayout.height; - var xaxis = subplot.xaxis; - var yaxis = subplot.yaxis; - var vp = [ - vpSize.l + xaxis.domain[0] * vpSize.w, - vpSize.b + yaxis.domain[0] * vpSize.h, - (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, - (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h - ]; + var vp = getViewport(gd._fullLayout, subplot.xaxis, subplot.yaxis); if(scene.select2d) { clearViewport(scene.select2d, vp); @@ -352,6 +339,18 @@ function sceneUpdate(gd, subplot) { return scene; } +function getViewport(fullLayout, xaxis, yaxis) { + var gs = fullLayout._size; + var width = fullLayout.width; + var height = fullLayout.height; + return [ + gs.l + xaxis.domain[0] * gs.w, + gs.b + yaxis.domain[0] * gs.h, + (width - gs.r) - (1 - xaxis.domain[1]) * gs.w, + (height - gs.t) - (1 - yaxis.domain[1]) * gs.h + ]; +} + function clearViewport(comp, vp) { var gl = comp.regl._gl; gl.enable(gl.SCISSOR_TEST); @@ -360,22 +359,26 @@ function clearViewport(comp, vp) { gl.clear(gl.COLOR_BUFFER_BIT); } +function repeat(opt, cnt) { + var opts = new Array(cnt); + for(var i = 0; i < cnt; i++) { + opts[i] = opt; + } + return opts; +} + function plot(gd, subplot, cdata) { if(!cdata.length) return; - var i; - var fullLayout = gd._fullLayout; - var scene = cdata[0][0].t._scene; - var dragmode = fullLayout.dragmode; + var scene = subplot._scene; + var xaxis = subplot.xaxis; + var yaxis = subplot.yaxis; + var i, j; // we may have more subplots than initialized data due to Axes.getSubplots method if(!scene) return; - var vpSize = fullLayout._size; - var width = fullLayout.width; - var height = fullLayout.height; - var success = prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); if(!success) { scene.init(); @@ -516,38 +519,20 @@ function plot(gd, subplot, cdata) { } } - var selectMode = dragmode === 'lasso' || dragmode === 'select'; + // form batch arrays, and check for selected points scene.selectBatch = null; scene.unselectBatch = null; + var dragmode = fullLayout.dragmode; + var selectMode = dragmode === 'lasso' || dragmode === 'select'; - // provide viewport and range - var vpRange = cdata.map(function(cdscatter) { - if(!cdscatter || !cdscatter[0] || !cdscatter[0].trace) return; - var cd = cdscatter[0]; - var trace = cd.trace; - var stash = cd.t; - var id = stash.index; + for(i = 0; i < cdata.length; i++) { + var cd0 = cdata[i][0]; + var trace = cd0.trace; + var stash = cd0.t; + var index = stash.index; var x = stash.x; var y = stash.y; - var xaxis = subplot.xaxis || AxisIDs.getFromId(gd, trace.xaxis || 'x'); - var yaxis = subplot.yaxis || AxisIDs.getFromId(gd, trace.yaxis || 'y'); - var i; - - var range = [ - (xaxis._rl || xaxis.range)[0], - (yaxis._rl || yaxis.range)[0], - (xaxis._rl || xaxis.range)[1], - (yaxis._rl || yaxis.range)[1] - ]; - - var viewport = [ - vpSize.l + xaxis.domain[0] * vpSize.w, - vpSize.b + yaxis.domain[0] * vpSize.h, - (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, - (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h - ]; - if(trace.selectedpoints || selectMode) { if(!selectMode) selectMode = true; @@ -558,37 +543,35 @@ function plot(gd, subplot, cdata) { // regenerate scene batch, if traces number changed during selection if(trace.selectedpoints) { - var selPts = scene.selectBatch[id] = Lib.selIndices2selPoints(trace); + var selPts = scene.selectBatch[index] = Lib.selIndices2selPoints(trace); var selDict = {}; - for(i = 0; i < selPts.length; i++) { - selDict[selPts[i]] = 1; + for(j = 0; j < selPts.length; j++) { + selDict[selPts[j]] = 1; } var unselPts = []; - for(i = 0; i < stash.count; i++) { - if(!selDict[i]) unselPts.push(i); + for(j = 0; j < stash.count; j++) { + if(!selDict[j]) unselPts.push(j); } - scene.unselectBatch[id] = unselPts; + scene.unselectBatch[index] = unselPts; } // precalculate px coords since we are not going to pan during select - var xpx = new Array(stash.count); - var ypx = new Array(stash.count); - for(i = 0; i < stash.count; i++) { - xpx[i] = xaxis.c2p(x[i]); - ypx[i] = yaxis.c2p(y[i]); + // TODO, could do better here e.g. + // - spin that in a webworker + // - compute selection from polygons in data coordinates + // (maybe just for linear axes) + var xpx = stash.xpx = new Array(stash.count); + var ypx = stash.ypx = new Array(stash.count); + for(j = 0; j < stash.count; j++) { + xpx[j] = xaxis.c2p(x[j]); + ypx[j] = yaxis.c2p(y[j]); } - stash.xpx = xpx; - stash.ypx = ypx; - } - else { + } else { stash.xpx = stash.ypx = null; } + } - return trace.visible ? - {viewport: viewport, range: range} : - null; - }); if(selectMode) { // create select2d @@ -618,6 +601,19 @@ function plot(gd, subplot, cdata) { } } + // provide viewport and range + var vpRange0 = { + viewport: getViewport(fullLayout, xaxis, yaxis), + // TODO do we need those fallbacks? + range: [ + (xaxis._rl || xaxis.range)[0], + (yaxis._rl || yaxis.range)[0], + (xaxis._rl || xaxis.range)[1], + (yaxis._rl || yaxis.range)[1] + ] + }; + var vpRange = repeat(vpRange0, scene.count); + // upload viewport/range data to GPU if(scene.fill2d) { scene.fill2d.update(vpRange); @@ -635,14 +631,10 @@ function plot(gd, subplot, cdata) { scene.select2d.update(vpRange); } if(scene.glText) { - scene.glText.forEach(function(text, i) { - text.update(vpRange[i]); - }); + scene.glText.forEach(function(text) { text.update(vpRange0); }); } scene.draw(); - - return; } From 41ad08ad17d3791a6c9fd7eca529988e22b9fdf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 23 Jul 2018 18:37:10 -0400 Subject: [PATCH 02/21] push trace module into fullLayout._modules even if visible:false - so that gl-based trace can call their plot methods w/ an empty array of traces and just work. - update and improve scatterlg visibility tests to reflect that `restyle(gd,visible,false)` no longer clear the context --- src/plots/cartesian/index.js | 2 +- src/plots/get_data.js | 1 + src/plots/plots.js | 4 ++-- test/jasmine/tests/cartesian_test.js | 2 +- test/jasmine/tests/gl2d_plot_interact_test.js | 15 +++++++++++++-- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 0ccbc57a4c4..c1619c91fd3 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -216,7 +216,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback var className = (_module.layerName || name + 'layer'); var plotMethod = _module.plot; - // plot all traces of this type on this subplot at once + // plot all visible traces of this type on this subplot at once cdModuleAndOthers = getModuleCalcData(cdSubplot, plotMethod); cdModule = cdModuleAndOthers[0]; // don't need to search the found traces again - in fact we need to NOT diff --git a/src/plots/get_data.js b/src/plots/get_data.js index 8d6bceb5b20..ca9e35c611b 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -69,6 +69,7 @@ exports.getModuleCalcData = function(calcdata, arg1) { for(var i = 0; i < calcdata.length; i++) { var cd = calcdata[i]; var trace = cd[0].trace; + // N.B. 'legendonly' traces do not make it pass here if(trace.visible !== true) continue; // group calcdata trace not by 'module' (as the name of this function diff --git a/src/plots/plots.js b/src/plots/plots.js index 15fddfc844c..09a6c51cfbd 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -696,7 +696,7 @@ plots._hasPlotType = function(category) { if(basePlotModules[i].name === category) return true; } - // check trace modules + // check trace modules (including non-visible:true) var modules = this._modules || []; for(i = 0; i < modules.length; i++) { var name = modules[i].name; @@ -912,7 +912,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var _module = fullTrace._module; if(!_module) return; - if(fullTrace.visible === true) Lib.pushUnique(modules, _module); + Lib.pushUnique(modules, _module); Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); cnt++; diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index 3cb326a940f..f225c12e232 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -197,7 +197,7 @@ describe('restyle', function() { return Plotly.restyle(gd, {visible: 'legendonly'}, 1); }) .then(function() { - expect(!!gd._fullLayout._plots.x2y2._scene).toBe(false); + expect(!!gd._fullLayout._plots.x2y2._scene).toBe(true); return Plotly.restyle(gd, {visible: true}, 1); }) .then(function() { diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js index 94516d5b7b7..827d283c94d 100644 --- a/test/jasmine/tests/gl2d_plot_interact_test.js +++ b/test/jasmine/tests/gl2d_plot_interact_test.js @@ -358,27 +358,38 @@ describe('@gl Test gl2d plots', function() { var _mock = Lib.extendDeep({}, mock); _mock.data[0].line.width = 5; + function assertDrawCall(msg, exp) { + var draw = gd._fullLayout._plots.xy._scene.scatter2d.draw; + expect(draw).toHaveBeenCalledTimes(exp, msg); + draw.calls.reset(); + } + Plotly.plot(gd, _mock) .then(delay(30)) .then(function() { + spyOn(gd._fullLayout._plots.xy._scene.scatter2d, 'draw'); return Plotly.restyle(gd, 'visible', 'legendonly'); }) .then(function() { - expect(gd.querySelector('.gl-canvas-context')).toBe(null); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).toBe(0); + assertDrawCall('legendonly', 0); return Plotly.restyle(gd, 'visible', true); }) .then(function() { expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); + assertDrawCall('back to visible', 1); return Plotly.restyle(gd, 'visible', false); }) .then(function() { - expect(gd.querySelector('.gl-canvas-context')).toBe(null); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).toBe(0); + assertDrawCall('visible false', 0); return Plotly.restyle(gd, 'visible', true); }) .then(function() { + assertDrawCall('back up', 1); expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); }) .catch(failTest) From 9c8ba0250dc616169e8e82baee8a289b7beb3777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 23 Jul 2018 18:39:08 -0400 Subject: [PATCH 03/21] fill in list of visible:true module in fullLayout._visibleModules - to not have to guard against visible!==true traces in _module.style - to shortcut full list of modules in other places downstream --- src/plots/plots.js | 15 ++++++++++----- test/jasmine/tests/transform_groupby_test.js | 1 + test/jasmine/tests/transform_multi_test.js | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 09a6c51cfbd..fae7126684e 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -277,6 +277,9 @@ var extraFormatKeys = [ * gd._fullLayout._modules * is a list of all the trace modules required to draw the plot. * + * gd._fullLayout._visibleModules + * subset of _modules, a list of modules corresponding to visible:true traces. + * * gd._fullLayout._basePlotModules * is a list of all the plot modules required to draw the plot. * @@ -378,6 +381,7 @@ plots.supplyDefaults = function(gd, opts) { // clear the lists of trace and baseplot modules, and subplots newFullLayout._modules = []; + newFullLayout._visibleModules = []; newFullLayout._basePlotModules = []; var subplots = newFullLayout._subplots = emptySubplotLists(); @@ -420,7 +424,7 @@ plots.supplyDefaults = function(gd, opts) { newFullLayout._has = plots._hasPlotType.bind(newFullLayout); // special cases that introduce interactions between traces - var _modules = newFullLayout._modules; + var _modules = newFullLayout._visibleModules; for(i = 0; i < _modules.length; i++) { var _module = _modules[i]; if(_module.cleanData) _module.cleanData(newFullData); @@ -898,6 +902,7 @@ plots.clearExpandedTraceDefaultColors = function(trace) { plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var modules = fullLayout._modules; + var visibleModules = fullLayout._visibleModules; var basePlotModules = fullLayout._basePlotModules; var cnt = 0; var colorCnt = 0; @@ -913,8 +918,8 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { if(!_module) return; Lib.pushUnique(modules, _module); + if(fullTrace.visible === true) Lib.pushUnique(visibleModules, _module); Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); - cnt++; // TODO: do we really want color not to increment for explicitly invisible traces? @@ -1475,7 +1480,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans } // trace module layout defaults - var modules = layoutOut._modules; + var modules = layoutOut._visibleModules; for(i = 0; i < modules.length; i++) { _module = modules[i]; @@ -1579,7 +1584,7 @@ plots.purge = function(gd) { }; plots.style = function(gd) { - var _modules = gd._fullLayout._modules; + var _modules = gd._fullLayout._visibleModules; var styleModules = []; var i; @@ -2567,7 +2572,7 @@ function clearAxesCalc(axList) { plots.doSetPositions = function(gd) { var fullLayout = gd._fullLayout; var subplots = fullLayout._subplots.cartesian; - var modules = fullLayout._modules; + var modules = fullLayout._visibleModules; var methods = []; var i, j; diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index 2c838336973..a1de229e472 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -15,6 +15,7 @@ function supplyDataDefaults(dataIn, dataOut) { return Plots.supplyDataDefaults(dataIn, dataOut, {}, { _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, _modules: [], + _visibleModules: [], _basePlotModules: [], _traceUids: dataIn.map(function() { return Lib.randstr(); }) }); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 5337fb0b388..f834135c0eb 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -16,6 +16,7 @@ var assertStyle = customAssertions.assertStyle; var mockFullLayout = { _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, _modules: [], + _visibleModules: [], _basePlotModules: [], _has: function() {}, _dfltTitle: {x: 'xxx', y: 'yyy'}, From 8ef5cb30f0f96d1b32a79a9d470d8473717651c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 23 Jul 2018 18:39:49 -0400 Subject: [PATCH 04/21] fix and :lock: splom trace visible toggling --- src/plots/cartesian/type_defaults.js | 2 +- test/jasmine/tests/splom_test.js | 29 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js index 7e5e7cc0ac6..5a9414054b6 100644 --- a/src/plots/cartesian/type_defaults.js +++ b/src/plots/cartesian/type_defaults.js @@ -108,7 +108,7 @@ function getFirstNonEmptyTrace(data, id, axLetter) { if(trace.type === 'splom' && trace._length > 0 && - trace['_' + axLetter + 'axes'][id] + (trace['_' + axLetter + 'axes'] || {})[id] ) { return trace; } diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index e894ab7fe88..981c4402c54 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -579,6 +579,35 @@ describe('@gl Test splom interactions:', function() { .catch(failTest) .then(done); }); + + it('should toggle trace correctly', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); + + function _assert(msg, exp) { + for(var i = 0; i < 3; i++) { + expect(Boolean(gd.calcdata[i][0].t._scene)) + .toBe(Boolean(exp[i]), msg + ' - trace ' + i); + } + } + + Plotly.plot(gd, fig).then(function() { + _assert('base', [1, 1, 1]); + return Plotly.restyle(gd, 'visible', 'legendonly', [0, 2]); + }) + .then(function() { + _assert('0-2 legendonly', [0, 1, 0]); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + _assert('all gone', [0, 0, 0]); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('all back', [1, 1, 1]); + }) + .catch(failTest) + .then(done); + }); }); describe('@gl Test splom hover:', function() { From cf0b19d526874d88360fbb298f89591b91175aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 26 Jul 2018 12:31:10 -0400 Subject: [PATCH 05/21] sub fail -> failTest --- test/jasmine/tests/bar_test.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index a1c242da8c4..08b546ad549 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -9,7 +9,7 @@ var Axes = require('@src/plots/cartesian/axes'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var checkTicks = require('../assets/custom_assertions').checkTicks; var supplyAllDefaults = require('../assets/supply_defaults'); @@ -897,7 +897,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -930,7 +930,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -961,7 +961,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -995,7 +995,7 @@ describe('A bar plot', function() { expect(foundTextNodes).toBe(true); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1144,7 +1144,7 @@ describe('A bar plot', function() { assertTextIsInsidePath(text20, path20); // inside assertTextIsInsidePath(text30, path30); // inside }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1225,7 +1225,7 @@ describe('A bar plot', function() { assertTextFont(textNodes[1], expected.outsidetextfont, 1); assertTextFont(textNodes[2], expected.insidetextfont, 2); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1291,7 +1291,7 @@ describe('A bar plot', function() { checkBarsMatch(['bottom', 'width'], 'final'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1328,7 +1328,7 @@ describe('A bar plot', function() { .then(function() { _assertNumberOfBarTextNodes(3); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1384,7 +1384,7 @@ describe('bar hover', function() { var mock = Lib.extendDeep({}, require('@mocks/11.json')); Plotly.plot(gd, mock.data, mock.layout) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1410,7 +1410,7 @@ describe('bar hover', function() { var mock = Lib.extendDeep({}, require('@mocks/bar_attrs_group_norm.json')); Plotly.plot(gd, mock.data, mock.layout) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1476,7 +1476,7 @@ describe('bar hover', function() { var out = _hover(gd, -0.25, 0.5, 'closest'); expect(out.text).toEqual('apple', 'hover text'); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1526,7 +1526,7 @@ describe('bar hover', function() { expect(out).toBe(false, hoverSpec); }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1564,7 +1564,7 @@ describe('bar hover', function() { expect(out.style).toEqual([1, 'red', 200, 1]); assertPos(out.pos, [222, 280, 168, 168]); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1594,7 +1594,7 @@ describe('bar hover', function() { out = _hover(gd, 10, 2, 'closest'); assertPos(out.pos, [145, 155, 15, 15]); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1699,7 +1699,7 @@ describe('bar hover', function() { [true, 3] ); }) - .catch(fail) + .catch(failTest) .then(done); }); }); From 9cc5fbe6babf13be4a63186f2ddf2f31ed5e517c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 13:47:37 -0400 Subject: [PATCH 06/21] add scatter visibility restyles tests --- test/jasmine/tests/scatter_test.js | 78 +++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 7ea0fa9cad9..dffed81956d 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -879,15 +879,16 @@ describe('end-to-end scatter tests', function() { .then(done); }); - it('should update axis range accordingly on marker.size edits', function(done) { - function _assert(msg, xrng, yrng) { - var fullLayout = gd._fullLayout; - expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng'); - expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng'); - } + function assertAxisRanges(msg, xrng, yrng) { + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng'); + expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng'); + } + var schema = Plotly.PlotSchema.get(); + + it('should update axis range accordingly on marker.size edits', function(done) { // edit types are important to this test - var schema = Plotly.PlotSchema.get(); expect(schema.traces.scatter.attributes.marker.size.editType) .toBe('calc', 'marker.size editType'); expect(schema.layout.layoutAttributes.xaxis.autorange.editType) @@ -895,29 +896,82 @@ describe('end-to-end scatter tests', function() { Plotly.plot(gd, [{ y: [1, 2, 1] }]) .then(function() { - _assert('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); + assertAxisRanges('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); return Plotly.relayout(gd, { 'xaxis.range': [0, 2], 'yaxis.range': [0, 2] }); }) .then(function() { - _assert('set rng / base marker.size', [0, 2], [0, 2]); + assertAxisRanges('set rng / base marker.size', [0, 2], [0, 2]); return Plotly.restyle(gd, 'marker.size', 50); }) .then(function() { - _assert('set rng / big marker.size', [0, 2], [0, 2]); + assertAxisRanges('set rng / big marker.size', [0, 2], [0, 2]); return Plotly.relayout(gd, { 'xaxis.autorange': true, 'yaxis.autorange': true }); }) .then(function() { - _assert('auto rng / big marker.size', [-0.28, 2.28], [0.75, 2.25]); + assertAxisRanges('auto rng / big marker.size', [-0.28, 2.28], [0.75, 2.25]); return Plotly.restyle(gd, 'marker.size', null); }) .then(function() { - _assert('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); + assertAxisRanges('auto rng / base marker.size', [-0.13, 2.13], [0.93, 2.07]); + }) + .catch(failTest) + .then(done); + }); + + it('should update axis range according to visible edits', function(done) { + Plotly.plot(gd, [ + {x: [1, 2, 3], y: [1, 2, 1]}, + {x: [4, 5, 6], y: [-1, -2, -1]} + ]) + .then(function() { + assertAxisRanges('both visible', [0.676, 6.323], [-2.29, 2.29]); + return Plotly.restyle(gd, 'visible', false, [1]); + }) + .then(function() { + assertAxisRanges('visible [true,false]', [0.87, 3.128], [0.926, 2.07]); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + assertAxisRanges('both invisible', [0.87, 3.128], [0.926, 2.07]); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + assertAxisRanges('visible [false,true]', [3.871, 6.128], [-2.07, -0.926]); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + assertAxisRanges('back to both visible', [0.676, 6.323], [-2.29, 2.29]); + }) + .catch(failTest) + .then(done); + }); + + it('should be able to start from visible:false', function(done) { + function _assert(msg, cnt) { + var layer = d3.select(gd).select('g.scatterlayer'); + expect(layer.selectAll('.point').size()).toBe(cnt, msg + '- scatter pts cnt'); + } + + Plotly.plot(gd, [{ + visible: false, + y: [1, 2, 1] + }]) + .then(function() { + _assert('visible:false', 0); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('visible:true', 3); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + _assert('back to visible:false', 0); }) .catch(failTest) .then(done); From dfada6ac7e144696d4edc42c33f1754d331cc93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 26 Jul 2018 12:36:59 -0400 Subject: [PATCH 07/21] add bar autorange tests & move 'b' init to setPositions - Bar.setPositions mutates 'b' in bar trace calcdata, so to reinit 'b' during setPositions so that it doesn't conflit with Bar.calc --- src/traces/bar/calc.js | 45 +------------------ src/traces/bar/set_positions.js | 41 +++++++++++++++++ test/jasmine/tests/bar_test.js | 80 +++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 43 deletions(-) diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index 423682eff04..10fc320af06 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -6,12 +6,8 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; -var isNumeric = require('fast-isnumeric'); -var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; - var Axes = require('../../plots/cartesian/axes'); var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleCalc = require('../../components/colorscale/calc'); @@ -27,24 +23,14 @@ module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), ya = Axes.getFromId(gd, trace.yaxis || 'y'), orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'), - sa, pos, size, i, scalendar; + pos, size, i; if(orientation === 'h') { - sa = xa; size = xa.makeCalcdata(trace, 'x'); pos = ya.makeCalcdata(trace, 'y'); - - // not sure if it really makes sense to have dates for bar size data... - // ideally if we want to make gantt charts or something we'd treat - // the actual size (trace.x or y) as time delta but base as absolute - // time. But included here for completeness. - scalendar = trace.xcalendar; - } - else { - sa = ya; + } else { size = ya.makeCalcdata(trace, 'y'); pos = xa.makeCalcdata(trace, 'x'); - scalendar = trace.ycalendar; } // create the "calculated data" to plot @@ -60,33 +46,6 @@ module.exports = function calc(gd, trace) { } } - // set base - var base = trace.base, - b; - - if(isArrayOrTypedArray(base)) { - for(i = 0; i < Math.min(base.length, cd.length); i++) { - b = sa.d2c(base[i], 0, scalendar); - if(isNumeric(b)) { - cd[i].b = +b; - cd[i].hasB = 1; - } - else cd[i].b = 0; - } - for(; i < cd.length; i++) { - cd[i].b = 0; - } - } - else { - b = sa.d2c(base, 0, scalendar); - var hasBase = isNumeric(b); - b = hasBase ? b : 0; - for(i = 0; i < cd.length; i++) { - cd[i].b = b; - if(hasBase) cd[i].hasB = 1; - } - } - // auto-z and autocolorscale if applicable if(hasColorscale(trace, 'marker')) { colorscaleCalc(trace, trace.marker.color, 'marker', 'c'); diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index b9a2fdf0746..a25f51636bf 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -65,6 +65,8 @@ function setGroupPositions(gd, pa, sa, calcTraces) { included, i, calcTrace, fullTrace; + initBase(gd, pa, sa, calcTraces); + if(overlay) { setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces); } @@ -110,6 +112,45 @@ function setGroupPositions(gd, pa, sa, calcTraces) { collectExtents(calcTraces, pa); } +function initBase(gd, pa, sa, calcTraces) { + var i, j; + + for(i = 0; i < calcTraces.length; i++) { + var cd = calcTraces[i]; + var trace = cd[0].trace; + var base = trace.base; + var b; + + // not sure if it really makes sense to have dates for bar size data... + // ideally if we want to make gantt charts or something we'd treat + // the actual size (trace.x or y) as time delta but base as absolute + // time. But included here for completeness. + var scalendar = trace.orientation === 'h' ? trace.xcalendar : trace.ycalendar; + + if(isArrayOrTypedArray(base)) { + for(j = 0; j < Math.min(base.length, cd.length); j++) { + b = sa.d2c(base[j], 0, scalendar); + if(isNumeric(b)) { + cd[j].b = +b; + cd[j].hasB = 1; + } + else cd[j].b = 0; + } + for(; j < cd.length; j++) { + cd[j].b = 0; + } + } else { + b = sa.d2c(base, 0, scalendar); + var hasBase = isNumeric(b); + b = hasBase ? b : 0; + for(j = 0; j < cd.length; j++) { + cd[j].b = b; + if(hasBase) cd[j].hasB = 1; + } + } + } +} + function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces) { var barnorm = gd._fullLayout.barnorm, diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 08b546ad549..5ae2f18cf04 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1333,6 +1333,86 @@ describe('A bar plot', function() { }); }); +describe('bar visibility toggling:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _assert(msg, xrng, yrng, calls) { + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng'); + expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng'); + + var setPositions = gd._fullData[0]._module.setPositions; + expect(setPositions).toHaveBeenCalledTimes(calls); + setPositions.calls.reset(); + } + + it('should update axis range according to visible edits (group case)', function(done) { + Plotly.plot(gd, [ + {type: 'bar', x: [1, 2, 3], y: [1, 2, 1]}, + {type: 'bar', x: [1, 2, 3], y: [-1, -2, -1]} + ]) + .then(function() { + spyOn(gd._fullData[0]._module, 'setPositions').and.callThrough(); + + _assert('base', [0.5, 3.5], [-2.222, 2.222], 0); + return Plotly.restyle(gd, 'visible', false, [1]); + }) + .then(function() { + _assert('visible [true,false]', [0.5, 3.5], [0, 2.105], 1); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + _assert('both invisible', [0.5, 3.5], [0, 2.105], 0); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + _assert('visible [false,true]', [0.5, 3.5], [-2.105, 0], 1); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('back to both visible', [0.5, 3.5], [-2.222, 2.222], 1); + }) + .catch(failTest) + .then(done); + }); + + it('should update axis range according to visible edits (stack case)', function(done) { + Plotly.plot(gd, [ + {type: 'bar', x: [1, 2, 3], y: [1, 2, 1]}, + {type: 'bar', x: [1, 2, 3], y: [2, 3, 2]} + ], {barmode: 'stack'}) + .then(function() { + spyOn(gd._fullData[0]._module, 'setPositions').and.callThrough(); + + _assert('base', [0.5, 3.5], [0, 5.263], 0); + return Plotly.restyle(gd, 'visible', false, [1]); + }) + .then(function() { + _assert('visible [true,false]', [0.5, 3.5], [0, 2.105], 1); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + _assert('both invisible', [0.5, 3.5], [0, 2.105], 0); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + _assert('visible [false,true]', [0.5, 3.5], [0, 3.157], 1); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + _assert('back to both visible', [0.5, 3.5], [0, 5.263], 1); + }) + .catch(failTest) + .then(done); + }); +}); + describe('bar hover', function() { 'use strict'; From be4436656a1431e149be01a9bed4538e1537bdf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 13:40:02 -0400 Subject: [PATCH 08/21] add findExtremes - a Axes.expend clone that does not append things to ax._min/ax._max, but instead returns two arrays, a min array and max array of potential data extremes --- src/plots/cartesian/autorange.js | 175 ++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 63017327ee0..ad6780b7de7 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -18,7 +18,8 @@ module.exports = { getAutoRange: getAutoRange, makePadFn: makePadFn, doAutoRange: doAutoRange, - expand: expand + expand: expand, + findExtremes: findExtremes }; // Find the autorange for this axis @@ -364,6 +365,178 @@ function expand(ax, data, options) { for(i = len - 1; i >= iMax; i--) addItem(i); } +/** + * findExtremes + * + * Find min/max extremes of an array of coordinates on a given axis. + * + * Note that findExtremes is called during `calc`, when we don't yet know the axis + * length; all the inputs should be based solely on the trace data, nothing + * about the axis layout. + * + * Note that `ppad` and `vpad` as well as their asymmetric variants refer to + * the before and after padding of the passed `data` array, not to the whole axis. + * + * @param {object} ax: full axis object + * relies on + * - ax.type + * - ax._m (just its sign) + * - ax.d2l + * @param {array} data: + * array of numbers (i.e. already run though ax.d2c) + * @param {object} options: + * available keys are: + * vpad: (number or number array) pad values (data value +-vpad) + * ppad: (number or number array) pad pixels (pixel location +-ppad) + * ppadplus, ppadminus, vpadplus, vpadminus: + * separate padding for each side, overrides symmetric + * padded: (boolean) add 5% padding to both ends + * (unless one end is overridden by tozero) + * tozero: (boolean) make sure to include zero if axis is linear, + * and make it a tight bound if possible + * + * @return {object} + * - min {array of objects} + * - max {array of objects} + * each object item has fields: + * - val {number} + * - pad {number} + * - extrappad {number} + */ +function findExtremes(ax, data, options) { + if(!options) options = {}; + if(!ax._m) ax.setScale(); + + var minArray = []; + var maxArray = []; + + var len = data.length; + var extrapad = options.padded || false; + var tozero = options.tozero && (ax.type === 'linear' || ax.type === '-'); + var isLog = (ax.type === 'log'); + + var i, j, k, v, di, dmin, dmax, ppadiplus, ppadiminus, includeThis, vmin, vmax; + + var hasArrayOption = false; + + function makePadAccessor(item) { + if(Array.isArray(item)) { + hasArrayOption = true; + return function(i) { return Math.max(Number(item[i]||0), 0); }; + } + else { + var v = Math.max(Number(item||0), 0); + return function() { return v; }; + } + } + + var ppadplus = makePadAccessor((ax._m > 0 ? + options.ppadplus : options.ppadminus) || options.ppad || 0); + var ppadminus = makePadAccessor((ax._m > 0 ? + options.ppadminus : options.ppadplus) || options.ppad || 0); + var vpadplus = makePadAccessor(options.vpadplus || options.vpad); + var vpadminus = makePadAccessor(options.vpadminus || options.vpad); + + if(!hasArrayOption) { + // with no arrays other than `data` we don't need to consider + // every point, only the extreme data points + vmin = Infinity; + vmax = -Infinity; + + if(isLog) { + for(i = 0; i < len; i++) { + v = data[i]; + // data is not linearized yet so we still have to filter out negative logs + if(v < vmin && v > 0) vmin = v; + if(v > vmax && v < FP_SAFE) vmax = v; + } + } else { + for(i = 0; i < len; i++) { + v = data[i]; + if(v < vmin && v > -FP_SAFE) vmin = v; + if(v > vmax && v < FP_SAFE) vmax = v; + } + } + + data = [vmin, vmax]; + len = 2; + } + + function addItem(i) { + di = data[i]; + if(!isNumeric(di)) return; + ppadiplus = ppadplus(i); + ppadiminus = ppadminus(i); + vmin = di - vpadminus(i); + vmax = di + vpadplus(i); + // special case for log axes: if vpad makes this object span + // more than an order of mag, clip it to one order. This is so + // we don't have non-positive errors or absurdly large lower + // range due to rounding errors + if(isLog && vmin < vmax / 10) vmin = vmax / 10; + + dmin = ax.c2l(vmin); + dmax = ax.c2l(vmax); + + if(tozero) { + dmin = Math.min(0, dmin); + dmax = Math.max(0, dmax); + } + + for(k = 0; k < 2; k++) { + var newVal = k ? dmax : dmin; + if(goodNumber(newVal)) { + var extremes = k ? maxArray : minArray; + var newPad = k ? ppadiplus : ppadiminus; + var atLeastAsExtreme = k ? greaterOrEqual : lessOrEqual; + + includeThis = true; + /* + * Take items v from ax._min/_max and compare them to the presently active point: + * - Since we don't yet know the relationship between pixels and values + * (that's what we're trying to figure out!) AND we don't yet know how + * many pixels `extrapad` represents (it's going to be 5% of the length, + * but we don't want to have to redo _min and _max just because length changed) + * two point must satisfy three criteria simultaneously for one to supersede the other: + * - at least as extreme a `val` + * - at least as big a `pad` + * - an unpadded point cannot supersede a padded point, but any other combination can + * + * - If the item supersedes the new point, set includethis false + * - If the new pt supersedes the item, delete it from ax._min/_max + */ + for(j = 0; j < extremes.length && includeThis; j++) { + v = extremes[j]; + if(atLeastAsExtreme(v.val, newVal) && v.pad >= newPad && (v.extrapad || !extrapad)) { + includeThis = false; + break; + } else if(atLeastAsExtreme(newVal, v.val) && v.pad <= newPad && (extrapad || !v.extrapad)) { + extremes.splice(j, 1); + j--; + } + } + if(includeThis) { + var clipAtZero = (tozero && newVal === 0); + extremes.push({ + val: newVal, + pad: clipAtZero ? 0 : newPad, + extrapad: clipAtZero ? false : extrapad + }); + } + } + } + } + + // For efficiency covering monotonic or near-monotonic data, + // check a few points at both ends first and then sweep + // through the middle + var iMax = Math.min(6, len); + for(i = 0; i < iMax; i++) addItem(i); + for(i = len - 1; i >= iMax; i--) addItem(i); + + return {min: minArray, max: maxArray}; +} + // In order to stop overflow errors, don't consider points // too close to the limits of js floating point function goodNumber(v) { From ad1ac1f307492186f879a27fff796973b6121838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 13:46:59 -0400 Subject: [PATCH 09/21] adapt getAutoRange and doAutoRange to trace _extremes - pass gd, to look for _extremes in gd._fullData and eventually in layout container to can expand the axis ranges --- src/components/rangeslider/calc_autorange.js | 2 +- src/plot_api/subroutines.js | 2 +- src/plots/cartesian/autorange.js | 160 ++++++--- test/jasmine/tests/axes_test.js | 332 ++++++++---------- test/jasmine/tests/bar_test.js | 26 +- test/jasmine/tests/gl2d_pointcloud_test.js | 32 +- .../tests/gl2d_scatterplot_contour_test.js | 4 - 7 files changed, 290 insertions(+), 268 deletions(-) diff --git a/src/components/rangeslider/calc_autorange.js b/src/components/rangeslider/calc_autorange.js index c722836a453..e7dbaca2f62 100644 --- a/src/components/rangeslider/calc_autorange.js +++ b/src/components/rangeslider/calc_autorange.js @@ -29,7 +29,7 @@ module.exports = function calcAutorange(gd) { if(opts && opts.visible && opts.autorange && ax._min.length && ax._max.length) { opts._input.autorange = true; - opts._input.range = opts.range = getAutoRange(ax); + opts._input.range = opts.range = getAutoRange(gd, ax); } } }; diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index d45aa6683d9..dcbcec47112 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -576,7 +576,7 @@ exports.doAutoRangeAndConstraints = function(gd) { for(var i = 0; i < axList.length; i++) { var ax = axList[i]; cleanAxisConstraints(gd, ax); - doAutoRange(ax); + doAutoRange(gd, ax); } enforceAxisConstraints(gd); diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index ad6780b7de7..f4500d7ad00 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var isNumeric = require('fast-isnumeric'); @@ -14,6 +13,8 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); var FP_SAFE = require('../../constants/numerical').FP_SAFE; +var Registry = require('../../registry'); + module.exports = { getAutoRange: getAutoRange, makePadFn: makePadFn, @@ -22,48 +23,63 @@ module.exports = { findExtremes: findExtremes }; -// Find the autorange for this axis -// -// assumes ax._min and ax._max have already been set by calling axes.expand -// using calcdata from all traces. These are arrays of objects: -// { -// val: calcdata value, -// pad: extra pixels beyond this value, -// extrapad: bool, does this point want 5% extra padding -// } -// -// Returns an array of [min, max]. These are calcdata for log and category axes -// and data for linear and date axes. -// -// TODO: we want to change log to data as well, but it's hard to do this -// maintaining backward compatibility. category will always have to use calcdata -// though, because otherwise values between categories (or outside all categories) -// would be impossible. -function getAutoRange(ax) { +/** + * getAutoRange + * + * Collects all _extremes values corresponding to a given axis + * and computes its auto range. + * + * getAutoRange uses return values from findExtremes where: + * + * { + * val: calcdata value, + * pad: extra pixels beyond this value, + * extrapad: bool, does this point want 5% extra padding + * } + * + * @param {object} gd: + * graph div object with filled in fullData and fullLayout, + * @param {object} ax: + * full axis object + * @return {array} + * an array of [min, max]. These are calcdata for log and category axes + * and data for linear and date axes. + * + * TODO: we want to change log to data as well, but it's hard to do this + * maintaining backward compatibility. category will always have to use calcdata + * though, because otherwise values between categories (or outside all categories) + * would be impossible. + */ +function getAutoRange(gd, ax) { + var i, j; var newRange = []; - var minmin = ax._min[0].val; - var maxmax = ax._max[0].val; - var mbest = 0; - var axReverse = false; var getPad = makePadFn(ax); + var minArray = concatExtremes(gd, ax, 'min'); + var maxArray = concatExtremes(gd, ax, 'max'); - var i, j, minpt, maxpt, minbest, maxbest, dp, dv; + if(minArray.length === 0 || maxArray.length === 0) { + return Lib.simpleMap(ax.range, ax.r2l); + } - for(i = 1; i < ax._min.length; i++) { + var minmin = minArray[0].val; + var maxmax = maxArray[0].val; + + for(i = 1; i < minArray.length; i++) { if(minmin !== maxmax) break; - minmin = Math.min(minmin, ax._min[i].val); + minmin = Math.min(minmin, minArray[i].val); } - for(i = 1; i < ax._max.length; i++) { + for(i = 1; i < maxArray.length; i++) { if(minmin !== maxmax) break; - maxmax = Math.max(maxmax, ax._max[i].val); + maxmax = Math.max(maxmax, maxArray[i].val); } + var axReverse = false; + if(ax.range) { var rng = Lib.simpleMap(ax.range, ax.r2l); axReverse = rng[1] < rng[0]; } - // one-time setting to easily reverse the axis // when plotting from code if(ax.autorange === 'reversed') { @@ -71,10 +87,13 @@ function getAutoRange(ax) { ax.autorange = true; } - for(i = 0; i < ax._min.length; i++) { - minpt = ax._min[i]; - for(j = 0; j < ax._max.length; j++) { - maxpt = ax._max[j]; + var mbest = 0; + var minpt, maxpt, minbest, maxbest, dp, dv; + + for(i = 0; i < minArray.length; i++) { + minpt = minArray[i]; + for(j = 0; j < maxArray.length; j++) { + maxpt = maxArray[j]; dv = maxpt.val - minpt.val; dp = ax._length - getPad(minpt) - getPad(maxpt); if(dv > 0 && dp > 0 && dv / dp > mbest) { @@ -90,11 +109,9 @@ function getAutoRange(ax) { var upper = minmin + 1; if(ax.rangemode === 'tozero') { newRange = minmin < 0 ? [lower, 0] : [0, upper]; - } - else if(ax.rangemode === 'nonnegative') { + } else if(ax.rangemode === 'nonnegative') { newRange = [Math.max(0, lower), Math.max(0, upper)]; - } - else { + } else { newRange = [lower, upper]; } } @@ -134,11 +151,9 @@ function getAutoRange(ax) { if(ax.rangemode === 'tozero') { if(newRange[0] < 0) { newRange = [newRange[0], 0]; - } - else if(newRange[0] > 0) { + } else if(newRange[0] > 0) { newRange = [0, newRange[0]]; - } - else { + } else { newRange = [0, 1]; } } @@ -174,15 +189,68 @@ function makePadFn(ax) { return function getPad(pt) { return pt.pad + (pt.extrapad ? extrappad : 0); }; } -function doAutoRange(ax) { +function concatExtremes(gd, ax, ext) { + var i; + var out = []; + + var fullData = gd._fullData; + + // should be general enough for 3d, polar etc. + + for(i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + var extremes = trace._extremes; + + if(trace.visible === true) { + if(Registry.traceIs(trace, 'cartesian')) { + var axId = ax._id; + if(extremes[axId]) { + out = out.concat(extremes[axId][ext]); + } + } else if(Registry.traceIs(trace, 'polar')) { + if(trace.subplot === ax._subplot) { + out = out.concat(extremes[ax._name][ext]); + } + } + } + } + + var fullLayout = gd._fullLayout; + var annotations = fullLayout.annotations; + var shapes = fullLayout.shapes; + + if(Array.isArray(annotations)) { + out = out.concat(concatComponentExtremes(annotations, ax, ext)); + } + if(Array.isArray(shapes)) { + out = out.concat(concatComponentExtremes(shapes, ax, ext)); + } + + return out; +} + +function concatComponentExtremes(items, ax, ext) { + var out = []; + var axId = ax._id; + var letter = axId.charAt(0); + + for(var i = 0; i < items.length; i++) { + var d = items[i]; + var extremes = d._extremes; + if(d.visible && d[letter + 'ref'] === axId && extremes[axId]) { + out = out.concat(extremes[axId][ext]); + } + } + return out; +} + +function doAutoRange(gd, ax) { if(!ax._length) ax.setScale(); - // TODO do we really need this? - var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length); var axIn; - if(ax.autorange && hasDeps) { - ax.range = getAutoRange(ax); + if(ax.autorange) { + ax.range = getAutoRange(gd, ax); ax._r = ax.range.slice(); ax._rl = Lib.simpleMap(ax._r, ax.r2l); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index fcb899b839c..6faec0a0666 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1421,234 +1421,208 @@ describe('Test axes', function() { describe('getAutoRange', function() { var getAutoRange = Axes.getAutoRange; - var ax; + var gd, ax; - it('returns reasonable range without explicit rangemode or autorange', function() { - ax = { - _min: [ - // add in an extrapad to verify that it gets used on _min - // with a _length of 100, extrapad increases pad by 5 - {val: 1, pad: 15, extrapad: true}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], + function mockGd(min, max) { + return { + _fullData: [{ + type: 'scatter', + visible: true, + xaxis: 'x', + _extremes: { + x: {min: min, max: max} + } + }], + _fullLayout: {} + }; + } + + function mockAx() { + return { + _id: 'x', type: 'linear', _length: 100 }; + } - expect(getAutoRange(ax)).toEqual([-0.5, 7]); + it('returns reasonable range without explicit rangemode or autorange', function() { + gd = mockGd([ + // add in an extrapad to verify that it gets used on _min + // with a _length of 100, extrapad increases pad by 5 + {val: 1, pad: 15, extrapad: true}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + + expect(getAutoRange(gd, ax)).toEqual([-0.5, 7]); }); it('reverses axes', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - autorange: 'reversed', - rangemode: 'normal', - _length: 100 - }; + gd = mockGd([ + {val: 1, pad: 20}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + ax.autorange = 'reversed'; + ax.rangemode = 'normarl'; - expect(getAutoRange(ax)).toEqual([7, -0.5]); + expect(getAutoRange(gd, ax)).toEqual([7, -0.5]); }); it('expands empty range', function() { - ax = { - _min: [ - {val: 2, pad: 0} - ], - _max: [ - {val: 2, pad: 0} - ], - type: 'linear', - rangemode: 'normal', - _length: 100 - }; + gd = mockGd([ + {val: 2, pad: 0} + ], [ + {val: 2, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'normal'; - expect(getAutoRange(ax)).toEqual([1, 3]); + expect(getAutoRange(gd, ax)).toEqual([1, 3]); }); it('returns a lower bound of 0 on rangemode tozero with positive points', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: 1, pad: 20}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([0, 7]); + expect(getAutoRange(gd, ax)).toEqual([0, 7]); }); it('returns an upper bound of 0 on rangemode tozero with negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: -5, pad: 20}, - {val: -4, pad: 0}, - {val: -6, pad: 10}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: -5, pad: 20}, + {val: -4, pad: 0}, + {val: -6, pad: 10}, + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([-12.5, 0]); + expect(getAutoRange(gd, ax)).toEqual([-12.5, 0]); }); it('returns a positive and negative range on rangemode tozero with positive and negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: 6, pad: 10}, + {val: 7, pad: 0}, + {val: 5, pad: 20} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([-15, 10]); + expect(getAutoRange(gd, ax)).toEqual([-15, 10]); }); it('reverses range after applying rangemode tozero', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - // add in an extrapad to verify that it gets used on _max - {val: 6, pad: 15, extrapad: true}, - {val: 7, pad: 0}, - {val: 5, pad: 10}, - ], - type: 'linear', - autorange: 'reversed', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: 1, pad: 20}, + {val: 3, pad: 0}, + {val: 2, pad: 10} + ], [ + // add in an extrapad to verify that it gets used on _max + {val: 6, pad: 15, extrapad: true}, + {val: 7, pad: 0}, + {val: 5, pad: 10} + ]); + ax = mockAx(); + ax.autorange = 'reversed'; + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([7.5, 0]); + expect(getAutoRange(gd, ax)).toEqual([7.5, 0]); }); it('expands empty positive range to something including 0 with rangemode tozero', function() { - ax = { - _min: [ - {val: 5, pad: 0} - ], - _max: [ - {val: 5, pad: 0} - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: 5, pad: 0} + ], [ + {val: 5, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([0, 6]); + expect(getAutoRange(gd, ax)).toEqual([0, 6]); }); it('expands empty negative range to something including 0 with rangemode tozero', function() { - ax = { - _min: [ - {val: -5, pad: 0} - ], - _max: [ - {val: -5, pad: 0} - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; + gd = mockGd([ + {val: -5, pad: 0} + ], [ + {val: -5, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'tozero'; - expect(getAutoRange(ax)).toEqual([-6, 0]); + expect(getAutoRange(gd, ax)).toEqual([-6, 0]); }); it('never returns a negative range when rangemode nonnegative is set with positive and negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: 6, pad: 20}, - {val: 7, pad: 0}, - {val: 5, pad: 10}, - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: 6, pad: 20}, + {val: 7, pad: 0}, + {val: 5, pad: 10} + ]); + ax = mockAx(); + ax.rangemode = 'nonnegative'; - expect(getAutoRange(ax)).toEqual([0, 7.5]); + expect(getAutoRange(gd, ax)).toEqual([0, 7.5]); }); it('never returns a negative range when rangemode nonnegative is set with only negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: -5, pad: 20}, - {val: -4, pad: 0}, - {val: -6, pad: 10}, - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; + gd = mockGd([ + {val: -10, pad: 20}, + {val: -8, pad: 0}, + {val: -9, pad: 10} + ], [ + {val: -5, pad: 20}, + {val: -4, pad: 0}, + {val: -6, pad: 10} + ]); + ax = mockAx(); + ax.rangemode = 'nonnegative'; - expect(getAutoRange(ax)).toEqual([0, 1]); + expect(getAutoRange(gd, ax)).toEqual([0, 1]); }); it('expands empty range to something nonnegative with rangemode nonnegative', function() { - ax = { - _min: [ - {val: -5, pad: 0} - ], - _max: [ - {val: -5, pad: 0} - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; + gd = mockGd([ + {val: -5, pad: 0} + ], [ + {val: -5, pad: 0} + ]); + ax = mockAx(); + ax.rangemode = 'nonnegative'; - expect(getAutoRange(ax)).toEqual([0, 1]); + expect(getAutoRange(gd, ax)).toEqual([0, 1]); }); }); diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 5ae2f18cf04..10ff65139d5 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -652,8 +652,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-5, 14], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-3.33, 3.33], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-5, 14], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-3.33, 3.33], undefined, '(ya.range)'); }); it('should expand size axis (overlay case)', function() { @@ -679,8 +679,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-11.11, 11.11], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-11.11, 11.11], undefined, '(ya.range)'); }); it('should expand size axis (relative case)', function() { @@ -702,8 +702,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-4.44, 4.44], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-4.44, 4.44], undefined, '(ya.range)'); }); it('should expand size axis (barnorm case)', function() { @@ -725,8 +725,8 @@ describe('Bar.setPositions', function() { var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-1.11, 1.11], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-1.11, 1.11], undefined, '(ya.range)'); }); it('should include explicit base in size axis range', function() { @@ -739,7 +739,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-2.5, 7.5]); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-2.5, 7.5]); }); }); @@ -753,7 +753,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toEqual(['2016-12-31', '2017-01-20']); + expect(Axes.getAutoRange(gd, ya)).toEqual(['2016-12-31', '2017-01-20']); }); }); @@ -767,7 +767,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-0.572, 10.873], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-0.572, 10.873], undefined, '(ya.range)'); }); it('works with log axes (stacked bars)', function() { @@ -780,7 +780,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-0.582, 11.059], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([-0.582, 11.059], undefined, '(ya.range)'); }); it('works with log axes (normalized bars)', function() { @@ -795,7 +795,7 @@ describe('Bar.setPositions', function() { }); var ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(ya)).toBeCloseToArray([1.496, 2.027], undefined, '(ya.range)'); + expect(Axes.getAutoRange(gd, ya)).toBeCloseToArray([1.496, 2.027], undefined, '(ya.range)'); }); }); diff --git a/test/jasmine/tests/gl2d_pointcloud_test.js b/test/jasmine/tests/gl2d_pointcloud_test.js index b43b7c02827..66f61898f29 100644 --- a/test/jasmine/tests/gl2d_pointcloud_test.js +++ b/test/jasmine/tests/gl2d_pointcloud_test.js @@ -161,43 +161,27 @@ describe('@gl pointcloud traces', function() { var mock = plotData; var scene2d; - var xBaselineMins = [{val: 0, pad: 50, extrapad: false}]; - var xBaselineMaxes = [{val: 9, pad: 50, extrapad: false}]; - - var yBaselineMins = [{val: 0, pad: 50, extrapad: false}]; - var yBaselineMaxes = [{val: 9, pad: 50, extrapad: false}]; - Plotly.plot(gd, mock) .then(function() { scene2d = gd._fullLayout._plots.xy._scene2d; - expect(scene2d.traces[gd._fullData[0].uid].type).toBe('pointcloud'); - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - return Plotly.relayout(gd, 'xaxis.range', [3, 6]); }).then(function() { - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - + expect(scene2d.xaxis.range).toEqual([3, 6]); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); return Plotly.relayout(gd, 'xaxis.autorange', true); }).then(function() { - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); return Plotly.relayout(gd, 'yaxis.range', [8, 20]); }).then(function() { - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toEqual([8, 20]); return Plotly.relayout(gd, 'yaxis.autorange', true); }).then(function() { - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); }) .catch(failTest) .then(done); diff --git a/test/jasmine/tests/gl2d_scatterplot_contour_test.js b/test/jasmine/tests/gl2d_scatterplot_contour_test.js index 96ad17c4562..853969b14d1 100644 --- a/test/jasmine/tests/gl2d_scatterplot_contour_test.js +++ b/test/jasmine/tests/gl2d_scatterplot_contour_test.js @@ -222,8 +222,6 @@ describe('@gl contourgl plots', function() { scene2d = gd._fullLayout._plots.xy._scene2d; expect(scene2d.traces[mock.data[0].uid].type).toEqual('contourgl'); - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0, extrapad: false}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0, extrapad: false}]); expect(scene2d.xaxis.range).toEqual([-1, 1]); return Plotly.relayout(gd, 'xaxis.range', [0, -10]); @@ -237,8 +235,6 @@ describe('@gl contourgl plots', function() { return Plotly.restyle(gd, 'type', 'heatmapgl'); }).then(function() { expect(scene2d.traces[mock.data[0].uid].type).toEqual('heatmapgl'); - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0, extrapad: false}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0, extrapad: false}]); expect(scene2d.xaxis.range).toEqual([1, -1]); return Plotly.relayout(gd, 'xaxis.range', [0, -10]); From 769c16020bb7f4a83f7cb69d8719e4b4fee18bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 13:42:40 -0400 Subject: [PATCH 10/21] fill trace._extremes with findExtremes in calc - replacing Axe.expand - for ErrorBars, we append the min/max arrays of the corresponding trace --- src/components/errorbars/calc.js | 24 +++++++++++++----------- src/plots/cartesian/axes.js | 1 + src/plots/plots.js | 5 ++++- src/traces/bar/set_positions.js | 20 ++++++++++++++++---- src/traces/box/calc.js | 3 ++- src/traces/box/set_positions.js | 17 ++++++++++------- src/traces/carpet/calc.js | 4 ++-- src/traces/heatmap/calc.js | 4 ++-- src/traces/ohlc/calc.js | 5 ++--- src/traces/scatter/calc.js | 4 ++-- src/traces/scattergl/convert.js | 6 ++---- src/traces/scattergl/index.js | 17 ++++++++++++----- src/traces/splom/index.js | 8 +++----- src/traces/violin/calc.js | 10 +++++++++- 14 files changed, 80 insertions(+), 48 deletions(-) diff --git a/src/components/errorbars/calc.js b/src/components/errorbars/calc.js index 124211f913b..5340cdc6b27 100644 --- a/src/components/errorbars/calc.js +++ b/src/components/errorbars/calc.js @@ -21,16 +21,15 @@ module.exports = function calc(gd) { var calcdata = gd.calcdata; for(var i = 0; i < calcdata.length; i++) { - var calcTrace = calcdata[i], - trace = calcTrace[0].trace; - - if(!Registry.traceIs(trace, 'errorBarsOK')) continue; - - var xa = Axes.getFromId(gd, trace.xaxis), - ya = Axes.getFromId(gd, trace.yaxis); - - calcOneAxis(calcTrace, trace, xa, 'x'); - calcOneAxis(calcTrace, trace, ya, 'y'); + var calcTrace = calcdata[i]; + var trace = calcTrace[0].trace; + + if(trace.visible === true && Registry.traceIs(trace, 'errorBarsOK')) { + var xa = Axes.getFromId(gd, trace.xaxis); + var ya = Axes.getFromId(gd, trace.yaxis); + calcOneAxis(calcTrace, trace, xa, 'x'); + calcOneAxis(calcTrace, trace, ya, 'y'); + } } }; @@ -57,5 +56,8 @@ function calcOneAxis(calcTrace, trace, axis, coord) { } } - Axes.expand(axis, vals, {padded: true}); + var extremes = Axes.findExtremes(axis, vals, {padded: true}); + var axId = axis._id; + trace._extremes[axId].min = trace._extremes[axId].min.concat(extremes.min); + trace._extremes[axId].max = trace._extremes[axId].max.concat(extremes.max); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 38d56e912ca..2c0585acee9 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -50,6 +50,7 @@ axes.getFromTrace = axisIds.getFromTrace; var autorange = require('./autorange'); axes.expand = autorange.expand; axes.getAutoRange = autorange.getAutoRange; +axes.findExtremes = autorange.findExtremes; /* * find the list of possible axes to reference with an xref or yref attribute diff --git a/src/plots/plots.js b/src/plots/plots.js index fae7126684e..90dcfaeea44 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -2455,10 +2455,13 @@ plots.doCalcdata = function(gd, traces) { } } - // find array attributes in trace for(i = 0; i < fullData.length; i++) { trace = fullData[i]; + trace._arrayAttrs = PlotSchema.findArrayAttributes(trace); + + // keep track of trace extremes (for autorange) in here + trace._extremes = {}; } // add polar axes to axis list diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index a25f51636bf..1021a400a16 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -496,7 +496,8 @@ function updatePositionAxis(gd, pa, sieve, allowMinDtick) { } } - Axes.expand(pa, [pMin, pMax], {padded: false}); + var extremes = Axes.findExtremes(pa, [pMin, pMax], {padded: false}); + putExtremes(calcTraces, pa, extremes); } function expandRange(range, newValue) { @@ -530,7 +531,8 @@ function setBaseAndTop(gd, sa, sieve) { } } - Axes.expand(sa, sRange, {tozero: true, padded: true}); + var extremes = Axes.findExtremes(sa, sRange, {tozero: true, padded: true}); + putExtremes(traces, sa, extremes); } @@ -568,7 +570,10 @@ function stackBars(gd, sa, sieve) { } // if barnorm is set, let normalizeBars update the axis range - if(!barnorm) Axes.expand(sa, sRange, {tozero: true, padded: true}); + if(!barnorm) { + var extremes = Axes.findExtremes(sa, sRange, {tozero: true, padded: true}); + putExtremes(traces, sa, extremes); + } } @@ -633,7 +638,8 @@ function normalizeBars(gd, sa, sieve) { } // update range of size axis - Axes.expand(sa, sRange, {tozero: true, padded: padded}); + var extremes = Axes.findExtremes(sa, sRange, {tozero: true, padded: padded}); + putExtremes(traces, sa, extremes); } @@ -641,6 +647,12 @@ function getAxisLetter(ax) { return ax._id.charAt(0); } +function putExtremes(cd, ax, extremes) { + for(var i = 0; i < cd.length; i++) { + cd[i][0].trace._extremes[ax._id] = extremes; + } +} + // find the full position span of bars at each position // for use by hover, to ensure labels move in if bars are // narrower than the space they're in. diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index d9f6aeb7e5b..095048f19a7 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -122,7 +122,8 @@ module.exports = function calc(gd, trace) { } calcSelection(cd, trace); - Axes.expand(valAxis, val, {padded: true}); + var extremes = Axes.findExtremes(valAxis, val, {padded: true}); + trace._extremes[valAxis._id] = extremes; if(cd.length > 0) { cd[0].t = { diff --git a/src/traces/box/set_positions.js b/src/traces/box/set_positions.js index ca460fd8297..c10f511ce83 100644 --- a/src/traces/box/set_positions.js +++ b/src/traces/box/set_positions.js @@ -86,12 +86,6 @@ function setPositionOffset(traceType, gd, boxList, posAxis, pad) { // check for forced minimum dtick Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true); - // set the width of all boxes - for(i = 0; i < boxList.length; i++) { - calcTrace = calcdata[boxList[i]]; - calcTrace[0].t.dPos = dPos; - } - var gap = fullLayout[traceType + 'gap']; var groupgap = fullLayout[traceType + 'groupgap']; var padfactor = (1 - gap) * (1 - groupgap) * dPos / fullLayout[numKey]; @@ -99,10 +93,19 @@ function setPositionOffset(traceType, gd, boxList, posAxis, pad) { // autoscale the x axis - including space for points if they're off the side // TODO: this will overdo it if the outermost boxes don't have // their points as far out as the other boxes - Axes.expand(posAxis, boxdv.vals, { + var extremes = Axes.findExtremes(posAxis, boxdv.vals, { vpadminus: dPos + pad[0] * padfactor, vpadplus: dPos + pad[1] * padfactor }); + + for(i = 0; i < boxList.length; i++) { + calcTrace = calcdata[boxList[i]]; + // set the width of all boxes + calcTrace[0].t.dPos = dPos; + // link extremes to all boxes + calcTrace[0].trace._extremes[posAxis._id] = extremes; + } + } module.exports = { diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index f9b0e65b648..3a69c021e40 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -82,8 +82,8 @@ module.exports = function calc(gd, trace) { xrange = [xc - dx * grow, xc + dx * grow]; yrange = [yc - dy * grow, yc + dy * grow]; - Axes.expand(xa, xrange, {padded: true}); - Axes.expand(ya, yrange, {padded: true}); + trace._extremes[xa._id] = Axes.findExtremes(xa, xrange, {padded: true}); + trace._extremes[ya._id] = Axes.findExtremes(ya, yrange, {padded: true}); // Enumerate the gridlines, both major and minor, and store them on the trace // object: diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index a1f0e7bb6c1..37477db0315 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -124,8 +124,8 @@ module.exports = function calc(gd, trace) { // handled in gl2d convert step if(!isGL2D) { - Axes.expand(xa, xArray); - Axes.expand(ya, yArray); + trace._extremes[xa._id] = Axes.findExtremes(xa, xArray); + trace._extremes[ya._id] = Axes.findExtremes(ya, yArray); } var cd0 = { diff --git a/src/traces/ohlc/calc.js b/src/traces/ohlc/calc.js index 42de0e1a086..f7553444bfa 100644 --- a/src/traces/ohlc/calc.js +++ b/src/traces/ohlc/calc.js @@ -25,8 +25,7 @@ function calc(gd, trace) { var cd = calcCommon(gd, trace, x, ya, ptFunc); - Axes.expand(xa, x, {vpad: minDiff / 2}); - + trace._extremes[xa._id] = Axes.findExtremes(xa, x, {vpad: minDiff / 2}); if(cd.length) { Lib.extendFlat(cd[0].t, { wHover: minDiff / 2, @@ -93,7 +92,7 @@ function calcCommon(gd, trace, x, ya, ptFunc) { } } - Axes.expand(ya, l.concat(h), {padded: true}); + trace._extremes[ya._id] = Axes.findExtremes(ya, l.concat(h), {padded: true}); if(cd.length) { cd[0].t = { diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index 97c3f187bdb..37a9068c8e9 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -97,8 +97,8 @@ function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) { } // N.B. asymmetric splom traces call this with blank {} xa or ya - if(xa._id) Axes.expand(xa, x, xOptions); - if(ya._id) Axes.expand(ya, y, yOptions); + if(xa._id) trace._extremes[xa._id] = Axes.findExtremes(xa, x, xOptions); + if(ya._id) trace._extremes[ya._id] = Axes.findExtremes(ya, y, yOptions); } function calcMarkerSize(trace, serieslen) { diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index dd0b3366c14..f0ffbf8220f 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -15,7 +15,6 @@ var rgba = require('color-normalize'); var Registry = require('../../registry'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); -var Axes = require('../../plots/cartesian/axes'); var AxisIDs = require('../../plots/cartesian/axis_ids'); var formatColor = require('../../lib/gl_format_color').formatColor; @@ -511,11 +510,10 @@ function convertErrorBarPositions(gd, trace, positions, x, y) { } } - Axes.expand(ax, [minShoe, maxHat], {padded: true}); - out[axLetter] = { positions: positions, - errors: errors + errors: errors, + _bnds: [minShoe, maxHat] }; } } diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 68fdc94efe4..a6f02b61bac 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -19,6 +19,7 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var prepareRegl = require('../../lib/prepare_regl'); var AxisIDs = require('../../plots/cartesian/axis_ids'); +var findExtremes = require('../../plots/cartesian/autorange').findExtremes; var Color = require('../../components/color'); var subTypes = require('../scatter/subtypes'); @@ -85,11 +86,9 @@ function calc(gd, trace) { var opts = sceneOptions(gd, subplot, trace, positions, x, y); var scene = sceneUpdate(gd, subplot); - // Re-use SVG scatter axis expansion routine except - // for graph with very large number of points where it - // performs poorly. - // In big data case, fake Axes.expand outputs with data bounds, - // and an average size for array marker.size inputs. + // 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(count < TOO_MANY_POINTS) { ppad = calcMarkerSize(trace, count); @@ -97,6 +96,8 @@ function calc(gd, trace) { ppad = 2 * (opts.marker.sizeAvg || Math.max(opts.marker.size, 3)); } calcAxisExpansion(gd, trace, xa, ya, x, y, ppad); + if(opts.errorX) expandForErrorBars(trace, xa, opts.errorX); + if(opts.errorY) expandForErrorBars(trace, ya, opts.errorY); // set flags to create scene renderers if(opts.fill && !scene.fill2d) scene.fill2d = true; @@ -137,6 +138,12 @@ function calc(gd, trace) { return [{x: false, y: false, t: stash, trace: trace}]; } +function expandForErrorBars(trace, ax, opts) { + var extremes = trace._extremes[ax._id]; + var errExt = findExtremes(ax, opts._bnds, {padded: true}); + extremes.min = extremes.min.concat(errExt.min); + extremes.max = extremes.max.concat(errExt.max); +} // create scene options function sceneOptions(gd, subplot, trace, positions, x, y) { diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index 6905ef6b247..c5613e5844f 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -66,11 +66,9 @@ function calc(gd, trace) { var xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {}; var ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {}; - // Re-use SVG scatter axis expansion routine except - // for graph with very large number of points where it - // performs poorly. - // In big data case, fake Axes.expand outputs with data bounds, - // and an average size for array marker.size inputs. + // 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)); diff --git a/src/traces/violin/calc.js b/src/traces/violin/calc.js index e4852c02b8d..1994f4235c3 100644 --- a/src/traces/violin/calc.js +++ b/src/traces/violin/calc.js @@ -35,6 +35,9 @@ module.exports = function calc(gd, trace) { }; } + var spanMin = Infinity; + var spanMax = -Infinity; + for(var i = 0; i < cd.length; i++) { var cdi = cd[i]; var vals = cdi.pts.map(helpers.extractVal); @@ -62,10 +65,15 @@ module.exports = function calc(gd, trace) { cdi.density[k] = {v: v, t: t}; } - Axes.expand(valAxis, span, {padded: true}); groupStats.maxCount = Math.max(groupStats.maxCount, vals.length); + + spanMin = Math.min(spanMin, span[0]); + spanMax = Math.max(spanMax, span[1]); } + var extremes = Axes.findExtremes(valAxis, [spanMin, spanMax], {padded: true}); + trace._extremes[valAxis._id] = extremes; + cd[0].t.labels.kde = Lib._(gd, 'kde:'); return cd; From 736ab69f0da772f15d6643704a8a3bac0ffbe24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 18:26:20 -0400 Subject: [PATCH 11/21] replace Axex.expand -> findExtremes in annotations and shapes --- src/components/annotations/calc_autorange.js | 101 +++++++++---------- src/components/shapes/calc_autorange.js | 10 +- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index e87752bb1e7..65b753e7077 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -34,63 +34,54 @@ function annAutorange(gd) { Lib.filterVisible(fullLayout.annotations).forEach(function(ann) { var xa = Axes.getFromId(gd, ann.xref); var ya = Axes.getFromId(gd, ann.yref); - var headSize = 3 * ann.arrowsize * ann.arrowwidth || 0; - var startHeadSize = 3 * ann.startarrowsize * ann.arrowwidth || 0; - var headPlus, headMinus, startHeadPlus, startHeadMinus; - - if(xa) { - headPlus = headSize + ann.xshift; - headMinus = headSize - ann.xshift; - startHeadPlus = startHeadSize + ann.xshift; - startHeadMinus = startHeadSize - ann.xshift; + ann._extremes = {}; + if(xa) calcAxisExpansion(ann, xa); + if(ya) calcAxisExpansion(ann, ya); + }); +} - if(ann.axref === ann.xref) { - // expand for the arrowhead (padded by arrowhead) - Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: headPlus, - ppadminus: headMinus - }); - // again for the textbox (padded by textbox) - Axes.expand(xa, [xa.r2c(ann.ax)], { - ppadplus: Math.max(ann._xpadplus, startHeadPlus), - ppadminus: Math.max(ann._xpadminus, startHeadMinus) - }); - } - else { - startHeadPlus = ann.ax ? startHeadPlus + ann.ax : startHeadPlus; - startHeadMinus = ann.ax ? startHeadMinus - ann.ax : startHeadMinus; - Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: Math.max(ann._xpadplus, headPlus, startHeadPlus), - ppadminus: Math.max(ann._xpadminus, headMinus, startHeadMinus) - }); - } - } +function calcAxisExpansion(ann, ax) { + var axId = ax._id; + var letter = axId.charAt(0); + var pos = ann[letter]; + var apos = ann['a' + letter]; + var ref = ann[letter + 'ref']; + var aref = ann['a' + letter + 'ref']; + var padplus = ann['_' + letter + 'padplus']; + var padminus = ann['_' + letter + 'padminus']; + var shift = {x: 1, y: -1}[letter] * ann[letter + 'shift']; + var headSize = 3 * ann.arrowsize * ann.arrowwidth || 0; + var headPlus = headSize + shift; + var headMinus = headSize - shift; + var startHeadSize = 3 * ann.startarrowsize * ann.arrowwidth || 0; + var startHeadPlus = startHeadSize + shift; + var startHeadMinus = startHeadSize - shift; + var extremes; - if(ya) { - headPlus = headSize - ann.yshift; - headMinus = headSize + ann.yshift; - startHeadPlus = startHeadSize - ann.yshift; - startHeadMinus = startHeadSize + ann.yshift; + if(aref === ref) { + // expand for the arrowhead (padded by arrowhead) + var extremeArrowHead = Axes.findExtremes(ax, [ax.r2c(pos)], { + ppadplus: headPlus, + ppadminus: headMinus + }); + // again for the textbox (padded by textbox) + var extremeText = Axes.findExtremes(ax, [ax.r2c(apos)], { + ppadplus: Math.max(padplus, startHeadPlus), + ppadminus: Math.max(padminus, startHeadMinus) + }); + extremes = { + min: [extremeArrowHead.min[0], extremeText.min[0]], + max: [extremeArrowHead.max[0], extremeText.max[0]] + }; + } else { + startHeadPlus = apos ? startHeadPlus + apos : startHeadPlus; + startHeadMinus = apos ? startHeadMinus - apos : startHeadMinus; + extremes = Axes.findExtremes(ax, [ax.r2c(pos)], { + ppadplus: Math.max(padplus, headPlus, startHeadPlus), + ppadminus: Math.max(padminus, headMinus, startHeadMinus) + }); + } - if(ann.ayref === ann.yref) { - Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: headPlus, - ppadminus: headMinus - }); - Axes.expand(ya, [ya.r2c(ann.ay)], { - ppadplus: Math.max(ann._ypadplus, startHeadPlus), - ppadminus: Math.max(ann._ypadminus, startHeadMinus) - }); - } - else { - startHeadPlus = ann.ay ? startHeadPlus + ann.ay : startHeadPlus; - startHeadMinus = ann.ay ? startHeadMinus - ann.ay : startHeadMinus; - Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: Math.max(ann._ypadplus, headPlus, startHeadPlus), - ppadminus: Math.max(ann._ypadminus, headMinus, startHeadMinus) - }); - } - } - }); + ann._extremes[axId] = extremes; } diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index 967cbcc4992..a05a6bc11c2 100644 --- a/src/components/shapes/calc_autorange.js +++ b/src/components/shapes/calc_autorange.js @@ -24,6 +24,7 @@ module.exports = function calcAutorange(gd) { for(var i = 0; i < shapeList.length; i++) { var shape = shapeList[i]; + shape._extremes = {}; var ax, bounds; @@ -33,8 +34,9 @@ module.exports = function calcAutorange(gd) { ax = Axes.getFromId(gd, shape.xref); bounds = shapeBounds(ax, vx0, vx1, shape.path, constants.paramIsX); - - if(bounds) Axes.expand(ax, bounds, calcXPaddingOptions(shape)); + if(bounds) { + shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcXPaddingOptions(shape)); + } } if(shape.yref !== 'paper') { @@ -43,7 +45,9 @@ module.exports = function calcAutorange(gd) { ax = Axes.getFromId(gd, shape.yref); bounds = shapeBounds(ax, vy0, vy1, shape.path, constants.paramIsY); - if(bounds) Axes.expand(ax, bounds, calcYPaddingOptions(shape)); + if(bounds) { + shape._extremes[ax._id] = Axes.findExtremes(ax, bounds, calcYPaddingOptions(shape)); + } } } }; From 6194457dc5ad64991a45f2028bfe356aba15b479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 18:29:10 -0400 Subject: [PATCH 12/21] :hocho: ax._min / ax._max logic for rangeslider - questionable commit, especially the part in autorange.js, but this doesn't make any test fail ?!? --- src/components/rangeslider/calc_autorange.js | 9 +++------ src/plots/cartesian/autorange.js | 6 +----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/rangeslider/calc_autorange.js b/src/components/rangeslider/calc_autorange.js index e7dbaca2f62..8681877621b 100644 --- a/src/components/rangeslider/calc_autorange.js +++ b/src/components/rangeslider/calc_autorange.js @@ -21,13 +21,10 @@ module.exports = function calcAutorange(gd) { // this step in subsequent draw calls. for(var i = 0; i < axes.length; i++) { - var ax = axes[i], - opts = ax[constants.name]; + var ax = axes[i]; + var opts = ax[constants.name]; - // Don't try calling getAutoRange if _min and _max are filled in. - // This happens on updates where the calc step is skipped. - - if(opts && opts.visible && opts.autorange && ax._min.length && ax._max.length) { + if(opts && opts.visible && opts.autorange) { opts._input.autorange = true; opts._input.range = opts.range = getAutoRange(gd, ax); } diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index f4500d7ad00..bacd3f7d2fa 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -267,11 +267,7 @@ function doAutoRange(gd, ax) { var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name]; if(axeRangeOpts) { if(axeRangeOpts.rangemode === 'auto') { - if(hasDeps) { - axeRangeOpts.range = getAutoRange(ax); - } else { - axeRangeOpts.range = ax._rangeInitial ? ax._rangeInitial.slice() : ax.range.slice(); - } + axeRangeOpts.range = getAutoRange(gd, ax); } } axIn = ax._anchorAxis._input; From 8cd06ae8f2ee056eba272c301e2ebf4bdb2cafed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 18:30:06 -0400 Subject: [PATCH 13/21] adapt enforceConstraints to new per trace/item _extremes --- src/plots/cartesian/autorange.js | 3 ++- src/plots/cartesian/constraints.js | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index bacd3f7d2fa..6fee268a780 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -20,7 +20,8 @@ module.exports = { makePadFn: makePadFn, doAutoRange: doAutoRange, expand: expand, - findExtremes: findExtremes + findExtremes: findExtremes, + concatExtremes: concatExtremes }; /** diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index f88bb397c10..f7d8848899f 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -12,6 +12,7 @@ var id2name = require('./axis_ids').id2name; var scaleZoom = require('./scale_zoom'); var makePadFn = require('./autorange').makePadFn; +var concatExtremes = require('./autorange').concatExtremes; var ALMOST_EQUAL = require('../../constants/numerical').ALMOST_EQUAL; @@ -112,7 +113,7 @@ exports.enforce = function enforceAxisConstraints(gd) { factor *= rangeShrunk; } - if(ax.autorange && ax._min.length && ax._max.length) { + if(ax.autorange) { /* * range & factor may need to change because range was * calculated for the larger scaling, so some pixel @@ -143,15 +144,17 @@ exports.enforce = function enforceAxisConstraints(gd) { var newVal; var k; - for(k = 0; k < ax._min.length; k++) { - newVal = ax._min[k].val - getPad(ax._min[k]) / m; + var minArray = concatExtremes(gd, ax, 'min'); + for(k = 0; k < minArray.length; k++) { + newVal = minArray[k].val - getPad(minArray[k]) / m; if(newVal > outerMin && newVal < rangeMin) { rangeMin = newVal; } } - for(k = 0; k < ax._max.length; k++) { - newVal = ax._max[k].val + getPad(ax._max[k]) / m; + var maxArray = concatExtremes(gd, ax, 'max'); + for(k = 0; k < maxArray.length; k++) { + newVal = maxArray[k].val + getPad(maxArray[k]) / m; if(newVal < outerMax && newVal > rangeMax) { rangeMax = newVal; } From 2a745deb1c4b88d8fd8ae44da2439ee9f5fdcba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 25 Jul 2018 18:32:02 -0400 Subject: [PATCH 14/21] adapt polar to new per trace/item _extremes - N.B. polar is the only subplot apart from cartesian that used Axes.expand and friends. --- src/plots/polar/layout_defaults.js | 6 +++--- src/plots/polar/polar.js | 9 ++------- src/traces/scatterpolar/calc.js | 2 +- src/traces/scatterpolargl/index.js | 8 +------- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index 5c103448f72..03f9bf78986 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -80,11 +80,11 @@ function handleDefaults(contIn, contOut, coerce, opts) { // Furthermore, angular axes don't have a set range. // // Mocked domains and ranges are set by the polar subplot instances, - // but Axes.expand uses the sign of _m to determine which padding value + // but Axes.findExtremes uses the sign of _m to determine which padding value // to use. // - // By setting, _m to 1 here, we make Axes.expand think that range[1] > range[0], - // and vice-versa for `autorange: 'reversed'` below. + // By setting, _m to 1 here, we make Axes.findExtremes think that + // range[1] > range[0], and vice-versa for `autorange: 'reversed'` below. axOut._m = 1; switch(axName) { diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 3a91414d45b..a43129e2a1f 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -300,9 +300,10 @@ proto.updateLayout = function(fullLayout, polarLayout) { proto.doAutoRange = function(fullLayout, polarLayout) { var radialLayout = polarLayout.radialaxis; var ax = this.radialAxis; + ax._subplot = this.id; setScale(ax, radialLayout, fullLayout); - doAutoRange(ax); + doAutoRange(this.gd, ax); radialLayout.range = ax.range.slice(); radialLayout._input.range = ax.range.slice(); @@ -1207,12 +1208,6 @@ proto.fillViewInitialKey = function(key, val) { function setScale(ax, axLayout, fullLayout) { Axes.setConvert(ax, fullLayout); - - // _min and _max are filled in during Axes.expand - // and cleared during Axes.setConvert - ax._min = axLayout._min; - ax._max = axLayout._max; - ax.setScale(); } diff --git a/src/traces/scatterpolar/calc.js b/src/traces/scatterpolar/calc.js index 7d9662324d0..d6d058fc942 100644 --- a/src/traces/scatterpolar/calc.js +++ b/src/traces/scatterpolar/calc.js @@ -48,7 +48,7 @@ module.exports = function calc(gd, trace) { } var ppad = calcMarkerSize(trace, len); - Axes.expand(radialAxis, rArray, {ppad: ppad}); + trace._extremes.radialaxis = Axes.findExtremes(radialAxis, rArray, {ppad: ppad}); calcColorscale(trace); arraysToCalcdata(cd, trace); diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index 963f9d98657..f9a01c2b6c8 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -36,13 +36,7 @@ function calc(container, trace) { stash.r = rArray; stash.theta = thetaArray; - Axes.expand(radialAxis, rArray, {tozero: true}); - - if(angularAxis.type !== 'linear') { - angularAxis.autorange = true; - Axes.expand(angularAxis, thetaArray); - delete angularAxis.autorange; - } + trace._extremes.radialaxis = Axes.findExtremes(radialAxis, rArray, {tozero: true}); return [{x: false, y: false, t: stash, trace: trace}]; } From 82d4bcc4b6f7e9583db90c0170e59e90ad5b4683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 26 Jul 2018 11:20:23 -0400 Subject: [PATCH 15/21] adapt gl2d to findExtremes --- src/plots/cartesian/autorange.js | 4 +--- src/plots/gl2d/scene2d.js | 2 +- src/traces/contourgl/convert.js | 7 ++++--- src/traces/heatmapgl/convert.js | 6 ++++-- src/traces/pointcloud/convert.js | 15 ++++++--------- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 6fee268a780..49f46162c8a 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -196,14 +196,12 @@ function concatExtremes(gd, ax, ext) { var fullData = gd._fullData; - // should be general enough for 3d, polar etc. - for(i = 0; i < fullData.length; i++) { var trace = fullData[i]; var extremes = trace._extremes; if(trace.visible === true) { - if(Registry.traceIs(trace, 'cartesian')) { + if(Registry.traceIs(trace, 'cartesian') || Registry.traceIs(trace, 'gl2d')) { var axId = ax._id; if(extremes[axId]) { out = out.concat(extremes[axId][ext]); diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 9bc5db60cf2..56e12929508 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -441,7 +441,7 @@ proto.plot = function(fullData, calcData, fullLayout) { ax = this[AXES[i]]; ax._length = options.viewBox[i + 2] - options.viewBox[i]; - doAutoRange(ax); + doAutoRange(this.graphDiv, ax); ax.setScale(); } diff --git a/src/traces/contourgl/convert.js b/src/traces/contourgl/convert.js index 417b6c096d1..32632c5ee11 100644 --- a/src/traces/contourgl/convert.js +++ b/src/traces/contourgl/convert.js @@ -125,9 +125,10 @@ proto.update = function(fullTrace, calcTrace) { this.contour.update(this.contourOptions); this.heatmap.update(this.heatmapOptions); - // expand axes - Axes.expand(this.scene.xaxis, calcPt.x); - Axes.expand(this.scene.yaxis, calcPt.y); + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + fullTrace._extremes[xa._id] = Axes.findExtremes(xa, calcPt.x); + fullTrace._extremes[ya._id] = Axes.findExtremes(ya, calcPt.y); }; proto.dispose = function() { diff --git a/src/traces/heatmapgl/convert.js b/src/traces/heatmapgl/convert.js index bb66bb1410f..8fc9980fcfb 100644 --- a/src/traces/heatmapgl/convert.js +++ b/src/traces/heatmapgl/convert.js @@ -95,8 +95,10 @@ proto.update = function(fullTrace, calcTrace) { this.heatmap.update(this.options); - Axes.expand(this.scene.xaxis, calcPt.x); - Axes.expand(this.scene.yaxis, calcPt.y); + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + fullTrace._extremes[xa._id] = Axes.findExtremes(xa, calcPt.x); + fullTrace._extremes[ya._id] = Axes.findExtremes(ya, calcPt.y); }; proto.dispose = function() { diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 721b964f5a5..9d7af610ab6 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -11,7 +11,7 @@ var createPointCloudRenderer = require('gl-pointcloud2d'); var str2RGBArray = require('../../lib/str2rgbarray'); -var expandAxis = require('../../plots/cartesian/autorange').expand; +var findExtremes = require('../../plots/cartesian/autorange').findExtremes; var getTraceColor = require('../scatter/get_trace_color'); function Pointcloud(scene, uid) { @@ -195,14 +195,11 @@ proto.updateFast = function(options) { this.pointcloud.update(this.pointcloudOptions); // add item for autorange routine - this.expandAxesFast(bounds, markerSizeMax / 2); // avoid axis reexpand just because of the adaptive point size -}; - -proto.expandAxesFast = function(bounds, markerSize) { - var pad = markerSize || 0.5; - - expandAxis(this.scene.xaxis, [bounds[0], bounds[2]], {ppad: pad}); - expandAxis(this.scene.yaxis, [bounds[1], bounds[3]], {ppad: pad}); + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + var pad = markerSizeMax / 2 || 0.5; + options._extremes[xa._id] = findExtremes(xa, [bounds[0], bounds[2]], {ppad: pad}); + options._extremes[ya._id] = findExtremes(ya, [bounds[1], bounds[3]], {ppad: pad}); }; proto.dispose = function() { From 29db388bb7db5dda2323b3704eeab0ca02bee0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 26 Jul 2018 11:20:42 -0400 Subject: [PATCH 16/21] :hocho: Axes.expand & adapt test for findExtremes --- src/plots/cartesian/autorange.js | 155 ----------------------------- src/plots/cartesian/axes.js | 1 - src/plots/cartesian/set_convert.js | 12 +-- test/jasmine/tests/axes_test.js | 143 ++++++++++---------------- test/jasmine/tests/contour_test.js | 1 + test/jasmine/tests/heatmap_test.js | 2 + 6 files changed, 55 insertions(+), 259 deletions(-) diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 49f46162c8a..9a20576127a 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -19,7 +19,6 @@ module.exports = { getAutoRange: getAutoRange, makePadFn: makePadFn, doAutoRange: doAutoRange, - expand: expand, findExtremes: findExtremes, concatExtremes: concatExtremes }; @@ -274,160 +273,6 @@ function doAutoRange(gd, ax) { } } -/* - * expand: if autoranging, include new data in the outer limits for this axis. - * Note that `expand` is called during `calc`, when we don't yet know the axis - * length; all the inputs should be based solely on the trace data, nothing - * about the axis layout. - * Note that `ppad` and `vpad` as well as their asymmetric variants refer to - * the before and after padding of the passed `data` array, not to the whole axis. - * - * @param {object} ax: the axis being expanded. The result will be more entries - * in ax._min and ax._max if necessary to include the new data - * @param {array} data: an array of numbers (ie already run through ax.d2c) - * @param {object} options: available keys are: - * vpad: (number or number array) pad values (data value +-vpad) - * ppad: (number or number array) pad pixels (pixel location +-ppad) - * ppadplus, ppadminus, vpadplus, vpadminus: - * separate padding for each side, overrides symmetric - * padded: (boolean) add 5% padding to both ends - * (unless one end is overridden by tozero) - * tozero: (boolean) make sure to include zero if axis is linear, - * and make it a tight bound if possible - */ -function expand(ax, data, options) { - if(!ax._min) ax._min = []; - if(!ax._max) ax._max = []; - if(!options) options = {}; - if(!ax._m) ax.setScale(); - - var len = data.length; - var extrapad = options.padded || false; - var tozero = options.tozero && (ax.type === 'linear' || ax.type === '-'); - var isLog = (ax.type === 'log'); - - var i, j, k, v, di, dmin, dmax, ppadiplus, ppadiminus, includeThis, vmin, vmax; - - var hasArrayOption = false; - - function makePadAccessor(item) { - if(Array.isArray(item)) { - hasArrayOption = true; - return function(i) { return Math.max(Number(item[i]||0), 0); }; - } - else { - var v = Math.max(Number(item||0), 0); - return function() { return v; }; - } - } - - var ppadplus = makePadAccessor((ax._m > 0 ? - options.ppadplus : options.ppadminus) || options.ppad || 0); - var ppadminus = makePadAccessor((ax._m > 0 ? - options.ppadminus : options.ppadplus) || options.ppad || 0); - var vpadplus = makePadAccessor(options.vpadplus || options.vpad); - var vpadminus = makePadAccessor(options.vpadminus || options.vpad); - - if(!hasArrayOption) { - // with no arrays other than `data` we don't need to consider - // every point, only the extreme data points - vmin = Infinity; - vmax = -Infinity; - - if(isLog) { - for(i = 0; i < len; i++) { - v = data[i]; - // data is not linearized yet so we still have to filter out negative logs - if(v < vmin && v > 0) vmin = v; - if(v > vmax && v < FP_SAFE) vmax = v; - } - } - else { - for(i = 0; i < len; i++) { - v = data[i]; - if(v < vmin && v > -FP_SAFE) vmin = v; - if(v > vmax && v < FP_SAFE) vmax = v; - } - } - - data = [vmin, vmax]; - len = 2; - } - - function addItem(i) { - di = data[i]; - if(!isNumeric(di)) return; - ppadiplus = ppadplus(i); - ppadiminus = ppadminus(i); - vmin = di - vpadminus(i); - vmax = di + vpadplus(i); - // special case for log axes: if vpad makes this object span - // more than an order of mag, clip it to one order. This is so - // we don't have non-positive errors or absurdly large lower - // range due to rounding errors - if(isLog && vmin < vmax / 10) vmin = vmax / 10; - - dmin = ax.c2l(vmin); - dmax = ax.c2l(vmax); - - if(tozero) { - dmin = Math.min(0, dmin); - dmax = Math.max(0, dmax); - } - - for(k = 0; k < 2; k++) { - var newVal = k ? dmax : dmin; - if(goodNumber(newVal)) { - var extremes = k ? ax._max : ax._min; - var newPad = k ? ppadiplus : ppadiminus; - var atLeastAsExtreme = k ? greaterOrEqual : lessOrEqual; - - includeThis = true; - /* - * Take items v from ax._min/_max and compare them to the presently active point: - * - Since we don't yet know the relationship between pixels and values - * (that's what we're trying to figure out!) AND we don't yet know how - * many pixels `extrapad` represents (it's going to be 5% of the length, - * but we don't want to have to redo _min and _max just because length changed) - * two point must satisfy three criteria simultaneously for one to supersede the other: - * - at least as extreme a `val` - * - at least as big a `pad` - * - an unpadded point cannot supersede a padded point, but any other combination can - * - * - If the item supersedes the new point, set includethis false - * - If the new pt supersedes the item, delete it from ax._min/_max - */ - for(j = 0; j < extremes.length && includeThis; j++) { - v = extremes[j]; - if(atLeastAsExtreme(v.val, newVal) && v.pad >= newPad && (v.extrapad || !extrapad)) { - includeThis = false; - break; - } - else if(atLeastAsExtreme(newVal, v.val) && v.pad <= newPad && (extrapad || !v.extrapad)) { - extremes.splice(j, 1); - j--; - } - } - if(includeThis) { - var clipAtZero = (tozero && newVal === 0); - extremes.push({ - val: newVal, - pad: clipAtZero ? 0 : newPad, - extrapad: clipAtZero ? false : extrapad - }); - } - } - } - } - - // For efficiency covering monotonic or near-monotonic data, - // check a few points at both ends first and then sweep - // through the middle - var iMax = Math.min(6, len); - for(i = 0; i < iMax; i++) addItem(i); - for(i = len - 1; i >= iMax; i--) addItem(i); -} - /** * findExtremes * diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 2c0585acee9..97b06b9c64a 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -48,7 +48,6 @@ axes.getFromId = axisIds.getFromId; axes.getFromTrace = axisIds.getFromTrace; var autorange = require('./autorange'); -axes.expand = autorange.expand; axes.getAutoRange = autorange.getAutoRange; axes.findExtremes = autorange.findExtremes; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index d3471547328..5c3a6295dd1 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -52,8 +52,7 @@ function fromLog(v) { * Creates/updates these conversion functions, and a few more utilities * like cleanRange, and makeCalcdata * - * also clears the autorange bounds ._min and ._max - * and the autotick constraints ._minDtick, ._forceTick0 + * also clears the autotick constraints ._minDtick, ._forceTick0 */ module.exports = function setConvert(ax, fullLayout) { fullLayout = fullLayout || {}; @@ -460,15 +459,6 @@ module.exports = function setConvert(ax, fullLayout) { }; ax.clearCalc = function() { - // for autoranging: arrays of objects: - // { - // val: axis value, - // pad: pixel padding, - // extrapad: boolean, should this val get 5% additional padding - // } - ax._min = []; - ax._max = []; - // initialize the category list, if there is one, so we start over // to be filled in later by ax.d2c ax._categories = (ax._initialCategories || []).slice(); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 6faec0a0666..57dd1221955 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1626,15 +1626,12 @@ describe('Test axes', function() { }); }); - describe('expand', function() { - var expand = Axes.expand; - var ax, data, options; + describe('findExtremes', function() { + var findExtremes = Axes.findExtremes; + var ax, data, options, out; - // Axes.expand modifies ax, so this provides a simple - // way of getting a new clean copy each time. function getDefaultAx() { return { - autorange: true, c2l: Number, type: 'linear', _m: 1 @@ -1645,55 +1642,42 @@ describe('Test axes', function() { ax = getDefaultAx(); data = [1, 4, 7, 2]; - expand(ax, data); - - expect(ax._min).toEqual([{val: 1, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 7, pad: 0, extrapad: false}]); + out = findExtremes(ax, data); + expect(out.min).toEqual([{val: 1, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 7, pad: 0, extrapad: false}]); }); it('calls ax.setScale if necessary', function() { - ax = { - autorange: true, - c2l: Number, - type: 'linear', - setScale: function() {} - }; + ax = getDefaultAx(); + delete ax._m; + ax.setScale = function() {}; spyOn(ax, 'setScale'); - expand(ax, [1]); - + findExtremes(ax, [1]); expect(ax.setScale).toHaveBeenCalled(); }); it('handles symmetric pads as numbers', function() { ax = getDefaultAx(); data = [1, 4, 2, 7]; - options = { - vpad: 2, - ppad: 10 - }; + options = {vpad: 2, ppad: 10}; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -1, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 9, pad: 10, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -1, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 9, pad: 10, extrapad: false}]); }); it('handles symmetric pads as number arrays', function() { ax = getDefaultAx(); data = [1, 4, 2, 7]; - options = { - vpad: [1, 10, 6, 3], - ppad: [0, 15, 20, 10] - }; - - expand(ax, data, options); + options = {vpad: [1, 10, 6, 3], ppad: [0, 15, 20, 10]}; - expect(ax._min).toEqual([ + out = findExtremes(ax, data, options); + expect(out.min).toEqual([ {val: -6, pad: 15, extrapad: false}, {val: -4, pad: 20, extrapad: false} ]); - expect(ax._max).toEqual([ + expect(out.max).toEqual([ {val: 14, pad: 15, extrapad: false}, {val: 8, pad: 20, extrapad: false} ]); @@ -1709,10 +1693,9 @@ describe('Test axes', function() { ppadplus: 20 }; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -4, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 11, pad: 20, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -4, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 11, pad: 20, extrapad: false}]); }); it('handles separate pads as number arrays', function() { @@ -1725,13 +1708,12 @@ describe('Test axes', function() { ppadplus: [0, 0, 40, 20] }; - expand(ax, data, options); - - expect(ax._min).toEqual([ + out = findExtremes(ax, data, options); + expect(out.min).toEqual([ {val: 1, pad: 30, extrapad: false}, {val: -3, pad: 10, extrapad: false} ]); - expect(ax._max).toEqual([ + expect(out.max).toEqual([ {val: 9, pad: 0, extrapad: false}, {val: 3, pad: 40, extrapad: false}, {val: 8, pad: 20, extrapad: false} @@ -1750,70 +1732,49 @@ describe('Test axes', function() { ppadplus: 40 }; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -1, pad: 20, extrapad: false}]); - expect(ax._max).toEqual([{val: 9, pad: 40, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -1, pad: 20, extrapad: false}]); + expect(out.max).toEqual([{val: 9, pad: 40, extrapad: false}]); }); it('adds 5% padding if specified by flag', function() { ax = getDefaultAx(); data = [1, 5]; - options = { - vpad: 1, - ppad: 10, - padded: true - }; + options = {vpad: 1, ppad: 10, padded: true}; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: 0, pad: 10, extrapad: true}]); - expect(ax._max).toEqual([{val: 6, pad: 10, extrapad: true}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: 0, pad: 10, extrapad: true}]); + expect(out.max).toEqual([{val: 6, pad: 10, extrapad: true}]); }); it('has lower bound zero with all positive data if tozero is sset', function() { ax = getDefaultAx(); data = [2, 5]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; + options = {vpad: 1, ppad: 10, tozero: true}; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: 0, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 6, pad: 10, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: 0, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 6, pad: 10, extrapad: false}]); }); it('has upper bound zero with all negative data if tozero is set', function() { ax = getDefaultAx(); data = [-7, -4]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; + options = {vpad: 1, ppad: 10, tozero: true}; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -8, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 0, pad: 0, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -8, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 0, pad: 0, extrapad: false}]); }); it('sets neither bound to zero with positive and negative data if tozero is set', function() { ax = getDefaultAx(); data = [-7, 4]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; + options = {vpad: 1, ppad: 10, tozero: true}; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -8, pad: 10, extrapad: false}]); - expect(ax._max).toEqual([{val: 5, pad: 10, extrapad: false}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: -8, pad: 10, extrapad: false}]); + expect(out.max).toEqual([{val: 5, pad: 10, extrapad: false}]); }); it('overrides padded with tozero', function() { @@ -1826,27 +1787,25 @@ describe('Test axes', function() { padded: true }; - expand(ax, data, options); - - expect(ax._min).toEqual([{val: 0, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 6, pad: 10, extrapad: true}]); + out = findExtremes(ax, data, options); + expect(out.min).toEqual([{val: 0, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 6, pad: 10, extrapad: true}]); }); it('should fail if no data is given', function() { ax = getDefaultAx(); - expect(function() { expand(ax); }).toThrow(); + expect(function() { findExtremes(ax); }).toThrow(); }); it('should return even if `autorange` is false', function() { ax = getDefaultAx(); - data = [2, 5]; - ax.autorange = false; ax.rangeslider = { autorange: false }; + data = [2, 5]; - expand(ax, data, {}); - expect(ax._min).toEqual([{val: 2, pad: 0, extrapad: false}]); - expect(ax._max).toEqual([{val: 5, pad: 0, extrapad: false}]); + out = findExtremes(ax, data, {}); + expect(out.min).toEqual([{val: 2, pad: 0, extrapad: false}]); + expect(out.max).toEqual([{val: 5, pad: 0, extrapad: false}]); }); }); diff --git a/test/jasmine/tests/contour_test.js b/test/jasmine/tests/contour_test.js index 1ae5858c3d6..4e34458328f 100644 --- a/test/jasmine/tests/contour_test.js +++ b/test/jasmine/tests/contour_test.js @@ -186,6 +186,7 @@ describe('contour calc', function() { supplyAllDefaults(gd); var fullTrace = gd._fullData[0]; + fullTrace._extremes = {}; var out = Contour.calc(gd, fullTrace)[0]; out.trace = fullTrace; diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index c65a7987043..692a6513d88 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -301,6 +301,8 @@ describe('heatmap calc', function() { var fullTrace = gd._fullData[0]; var fullLayout = gd._fullLayout; + fullTrace._extremes = {}; + var out = Heatmap.calc(gd, fullTrace)[0]; out._xcategories = fullLayout.xaxis._categories; out._ycategories = fullLayout.yaxis._categories; From 72f06a64cad7cdf0f5677f9f59999007c0035b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 27 Jul 2018 15:00:51 -0400 Subject: [PATCH 17/21] improve concatExtremes perf - by using ax.(_traceIndices, annIndices, shapeIndices), to keep track of traces, annotations and shapes plotted on axis ax. Adapt concatExtremes accordingly! --- src/components/annotations/defaults.js | 5 ++ src/components/shapes/defaults.js | 1 + src/plots/cartesian/autorange.js | 84 +++++++++----------------- src/plots/cartesian/constraints.js | 5 +- src/plots/cartesian/layout_defaults.js | 72 +++++++++++++++------- src/plots/polar/layout_defaults.js | 1 + src/plots/polar/polar.js | 1 - src/traces/scatterpolar/calc.js | 2 +- src/traces/scatterpolargl/index.js | 2 +- test/jasmine/tests/annotations_test.js | 22 +++++-- test/jasmine/tests/axes_test.js | 3 +- test/jasmine/tests/shapes_test.js | 8 +-- 12 files changed, 114 insertions(+), 92 deletions(-) diff --git a/src/components/annotations/defaults.js b/src/components/annotations/defaults.js index 2ca55c44bae..b04657c2995 100644 --- a/src/components/annotations/defaults.js +++ b/src/components/annotations/defaults.js @@ -48,6 +48,11 @@ function handleAnnotationDefaults(annIn, annOut, fullLayout) { // xref, yref var axRef = Axes.coerceRef(annIn, annOut, gdMock, axLetter, '', 'paper'); + if(axRef !== 'paper') { + var ax = Axes.getFromId(gdMock, axRef); + ax._annIndices.push(annOut._index); + } + // x, y Axes.coercePosition(annOut, gdMock, coerce, axRef, axLetter, 0.5); diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index bc2934e0fee..fff7f9d612b 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -61,6 +61,7 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { if(axRef !== 'paper') { ax = Axes.getFromId(gdMock, axRef); + ax._shapeIndices.push(shapeOut._index); r2pos = helpers.rangeToShapePosition(ax); pos2r = helpers.shapePositionToRange(ax); } diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 9a20576127a..dbd646eecde 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -13,8 +13,6 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); var FP_SAFE = require('../../constants/numerical').FP_SAFE; -var Registry = require('../../registry'); - module.exports = { getAutoRange: getAutoRange, makePadFn: makePadFn, @@ -31,16 +29,17 @@ module.exports = { * * getAutoRange uses return values from findExtremes where: * - * { - * val: calcdata value, - * pad: extra pixels beyond this value, - * extrapad: bool, does this point want 5% extra padding - * } - * * @param {object} gd: - * graph div object with filled in fullData and fullLayout, + * graph div object with filled-in fullData and fullLayout, in particular + * with filled-in '_extremes' containers: + * { + * val: calcdata value, + * pad: extra pixels beyond this value, + * extrapad: bool, does this point want 5% extra padding + * } * @param {object} ax: - * full axis object + * full axis object, in particular with filled-in '_traceIndices' + * and '_annIndices' / '_shapeIndices' if applicable * @return {array} * an array of [min, max]. These are calcdata for log and category axes * and data for linear and date axes. @@ -55,8 +54,9 @@ function getAutoRange(gd, ax) { var newRange = []; var getPad = makePadFn(ax); - var minArray = concatExtremes(gd, ax, 'min'); - var maxArray = concatExtremes(gd, ax, 'max'); + var extremes = concatExtremes(gd, ax); + var minArray = extremes.min; + var maxArray = extremes.max; if(minArray.length === 0 || maxArray.length === 0) { return Lib.simpleMap(ax.range, ax.r2l); @@ -189,57 +189,31 @@ function makePadFn(ax) { return function getPad(pt) { return pt.pad + (pt.extrapad ? extrappad : 0); }; } -function concatExtremes(gd, ax, ext) { - var i; - var out = []; - +function concatExtremes(gd, ax) { + var axId = ax._id; var fullData = gd._fullData; + var fullLayout = gd._fullLayout; + var minArray = []; + var maxArray = []; - for(i = 0; i < fullData.length; i++) { - var trace = fullData[i]; - var extremes = trace._extremes; - - if(trace.visible === true) { - if(Registry.traceIs(trace, 'cartesian') || Registry.traceIs(trace, 'gl2d')) { - var axId = ax._id; - if(extremes[axId]) { - out = out.concat(extremes[axId][ext]); - } - } else if(Registry.traceIs(trace, 'polar')) { - if(trace.subplot === ax._subplot) { - out = out.concat(extremes[ax._name][ext]); - } + function _concat(cont, indices) { + for(var i = 0; i < indices.length; i++) { + var item = cont[indices[i]]; + var extremes = (item._extremes || {})[axId]; + if(item.visible === true && extremes) { + minArray = minArray.concat(extremes.min); + maxArray = maxArray.concat(extremes.max); } } } - var fullLayout = gd._fullLayout; - var annotations = fullLayout.annotations; - var shapes = fullLayout.shapes; - - if(Array.isArray(annotations)) { - out = out.concat(concatComponentExtremes(annotations, ax, ext)); - } - if(Array.isArray(shapes)) { - out = out.concat(concatComponentExtremes(shapes, ax, ext)); - } - - return out; -} + _concat(fullData, ax._traceIndices); + _concat(fullLayout.annotations || [], ax._annIndices || []); + _concat(fullLayout.shapes || [], ax._shapeIndices || []); -function concatComponentExtremes(items, ax, ext) { - var out = []; - var axId = ax._id; - var letter = axId.charAt(0); + // TODO collapse more! - for(var i = 0; i < items.length; i++) { - var d = items[i]; - var extremes = d._extremes; - if(d.visible && d[letter + 'ref'] === axId && extremes[axId]) { - out = out.concat(extremes[axId][ext]); - } - } - return out; + return {min: minArray, max: maxArray}; } function doAutoRange(gd, ax) { diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index f7d8848899f..70859a2ad0b 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -141,10 +141,12 @@ exports.enforce = function enforceAxisConstraints(gd) { updateDomain(ax, factor); ax.setScale(); var m = Math.abs(ax._m); + var extremes = concatExtremes(gd, ax); + var minArray = extremes.min; + var maxArray = extremes.max; var newVal; var k; - var minArray = concatExtremes(gd, ax, 'min'); for(k = 0; k < minArray.length; k++) { newVal = minArray[k].val - getPad(minArray[k]) / m; if(newVal > outerMin && newVal < rangeMin) { @@ -152,7 +154,6 @@ exports.enforce = function enforceAxisConstraints(gd) { } } - var maxArray = concatExtremes(gd, ax, 'max'); for(k = 0; k < maxArray.length; k++) { newVal = maxArray[k].val + getPad(maxArray[k]) / m; if(newVal < outerMax && newVal > rangeMax) { diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index f17c71f7f19..6c2f824781e 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -9,7 +9,6 @@ 'use strict'; -var Registry = require('../../registry'); var Lib = require('../../lib'); var Color = require('../../components/color'); var Template = require('../../plot_api/plot_template'); @@ -20,31 +19,57 @@ var handleTypeDefaults = require('./type_defaults'); var handleAxisDefaults = require('./axis_defaults'); var handleConstraintDefaults = require('./constraint_defaults'); var handlePositionDefaults = require('./position_defaults'); + var axisIds = require('./axis_ids'); +var id2name = axisIds.id2name; +var name2id = axisIds.name2id; +var Registry = require('../../registry'); +var traceIs = Registry.traceIs; +var getComponentMethod = Registry.getComponentMethod; + +function appendList(cont, k, item) { + if(Array.isArray(cont[k])) cont[k].push(item); + else cont[k] = [item]; +} module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { + var ax2traces = {}; var xaCheater = {}; var xaNonCheater = {}; var outerTicks = {}; var noGrids = {}; - var i; + var i, j; // look for axes in the data for(i = 0; i < fullData.length; i++) { var trace = fullData[i]; - - if(!Registry.traceIs(trace, 'cartesian') && !Registry.traceIs(trace, 'gl2d')) { - continue; + if(!traceIs(trace, 'cartesian') && !traceIs(trace, 'gl2d')) continue; + + var xaName; + if(trace.xaxis) { + xaName = id2name(trace.xaxis); + appendList(ax2traces, xaName, trace); + } else if(trace.xaxes) { + for(j = 0; j < trace.xaxes.length; j++) { + appendList(ax2traces, id2name(trace.xaxes[j]), trace); + } } - var xaName = axisIds.id2name(trace.xaxis); - var yaName = axisIds.id2name(trace.yaxis); + var yaName; + if(trace.yaxis) { + yaName = id2name(trace.yaxis); + appendList(ax2traces, yaName, trace); + } else if(trace.yaxes) { + for(j = 0; j < trace.yaxes.length; j++) { + appendList(ax2traces, id2name(trace.yaxes[j]), trace); + } + } // Two things trigger axis visibility: // 1. is not carpet // 2. carpet that's not cheater - if(!Registry.traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) { + if(!traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) { if(xaName) xaNonCheater[xaName] = 1; } @@ -57,22 +82,22 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } // check for default formatting tweaks - if(Registry.traceIs(trace, '2dMap')) { - outerTicks[xaName] = true; - outerTicks[yaName] = true; + if(traceIs(trace, '2dMap')) { + outerTicks[xaName] = 1; + outerTicks[yaName] = 1; } - if(Registry.traceIs(trace, 'oriented')) { + if(traceIs(trace, 'oriented')) { var positionAxis = trace.orientation === 'h' ? yaName : xaName; - noGrids[positionAxis] = true; + noGrids[positionAxis] = 1; } } var subplots = layoutOut._subplots; var xIds = subplots.xaxis; var yIds = subplots.yaxis; - var xNames = Lib.simpleMap(xIds, axisIds.id2name); - var yNames = Lib.simpleMap(yIds, axisIds.id2name); + var xNames = Lib.simpleMap(xIds, id2name); + var yNames = Lib.simpleMap(yIds, id2name); var axNames = xNames.concat(yNames); // plot_bgcolor only makes sense if there's a (2D) plot! @@ -108,7 +133,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var axName2 = list[j]; if(axName2 !== axName && !(layoutIn[axName2] || {}).overlaying) { - out.push(axisIds.name2id(axName2)); + out.push(name2id(axName2)); } } @@ -127,7 +152,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutIn = layoutIn[axName]; axLayoutOut = Template.newContainer(layoutOut, axName, axLetter + 'axis'); - handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, fullData, axName); + var traces = ax2traces[axName] || []; + axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; }); + axLayoutOut._annIndices = []; + axLayoutOut._shapeIndices = []; + + handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, traces, axName); var overlayableAxes = getOverlayableAxes(axLetter, axName); @@ -136,7 +166,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { font: layoutOut.font, outerTicks: outerTicks[axName], showGrid: !noGrids[axName], - data: fullData, + data: traces, bgColor: bgColor, calendar: layoutOut.calendar, automargin: true, @@ -173,8 +203,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } // quick second pass for range slider and selector defaults - var rangeSliderDefaults = Registry.getComponentMethod('rangeslider', 'handleDefaults'); - var rangeSelectorDefaults = Registry.getComponentMethod('rangeselector', 'handleDefaults'); + var rangeSliderDefaults = getComponentMethod('rangeslider', 'handleDefaults'); + var rangeSelectorDefaults = getComponentMethod('rangeselector', 'handleDefaults'); for(i = 0; i < xNames.length; i++) { axName = xNames[i]; @@ -201,7 +231,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutIn = layoutIn[axName]; axLayoutOut = layoutOut[axName]; - var anchoredAxis = layoutOut[axisIds.id2name(axLayoutOut.anchor)]; + var anchoredAxis = layoutOut[id2name(axLayoutOut.anchor)]; var fixedRangeDflt = ( anchoredAxis && diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index 03f9bf78986..b83f486573a 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -55,6 +55,7 @@ function handleDefaults(contIn, contOut, coerce, opts) { // propagate the template. var axOut = contOut[axName] = {}; axOut._id = axOut._name = axName; + axOut._traceIndices = subplotData.map(function(t) { return t.index; }); var dataAttr = constants.axisName2dataArray[axName]; var axType = handleAxisTypeDefaults(axIn, axOut, coerceAxis, subplotData, dataAttr); diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index a43129e2a1f..e300807ed1c 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -300,7 +300,6 @@ proto.updateLayout = function(fullLayout, polarLayout) { proto.doAutoRange = function(fullLayout, polarLayout) { var radialLayout = polarLayout.radialaxis; var ax = this.radialAxis; - ax._subplot = this.id; setScale(ax, radialLayout, fullLayout); doAutoRange(this.gd, ax); diff --git a/src/traces/scatterpolar/calc.js b/src/traces/scatterpolar/calc.js index d6d058fc942..f2f10dd7f2d 100644 --- a/src/traces/scatterpolar/calc.js +++ b/src/traces/scatterpolar/calc.js @@ -48,7 +48,7 @@ module.exports = function calc(gd, trace) { } var ppad = calcMarkerSize(trace, len); - trace._extremes.radialaxis = Axes.findExtremes(radialAxis, rArray, {ppad: ppad}); + trace._extremes.x = Axes.findExtremes(radialAxis, rArray, {ppad: ppad}); calcColorscale(trace); arraysToCalcdata(cd, trace); diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index f9a01c2b6c8..b5a16ed6937 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -36,7 +36,7 @@ function calc(container, trace) { stash.r = rArray; stash.theta = thetaArray; - trace._extremes.radialaxis = Axes.findExtremes(radialAxis, rArray, {tozero: true}); + trace._extremes.x = Axes.findExtremes(radialAxis, rArray, {tozero: true}); return [{x: false, y: false, t: stash, trace: trace}]; } diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 1039facbd04..a4331307cfe 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -27,7 +27,13 @@ describe('Test annotations', function() { layoutOut._has = Plots._hasPlotType.bind(layoutOut); layoutOut._subplots = {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']}; ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(axName) { - if(!layoutOut[axName]) layoutOut[axName] = {type: 'linear', range: [0, 1]}; + if(!layoutOut[axName]) { + layoutOut[axName] = { + type: 'linear', + range: [0, 1], + _annIndices: [] + }; + } Axes.setConvert(layoutOut[axName]); }); @@ -90,7 +96,11 @@ describe('Test annotations', function() { }; var layoutOut = { - xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] } + xaxis: { + type: 'date', + range: ['2000-01-01', '2016-01-01'], + _annIndices: [] + } }; _supply(layoutIn, layoutOut); @@ -128,10 +138,10 @@ describe('Test annotations', function() { }; var layoutOut = { - xaxis: {type: 'linear', range: [0, 1]}, - yaxis: {type: 'date', range: ['2000-01-01', '2018-01-01']}, - xaxis2: {type: 'log', range: [1, 2]}, - yaxis2: {type: 'category', range: [0, 1]} + xaxis: {type: 'linear', range: [0, 1], _annIndices: []}, + yaxis: {type: 'date', range: ['2000-01-01', '2018-01-01'], _annIndices: []}, + xaxis2: {type: 'log', range: [1, 2], _annIndices: []}, + yaxis2: {type: 'category', range: [0, 1], _annIndices: []} }; _supply(layoutIn, layoutOut); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 57dd1221955..2538771fc4e 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1441,7 +1441,8 @@ describe('Test axes', function() { return { _id: 'x', type: 'linear', - _length: 100 + _length: 100, + _traceIndices: [0] }; } diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index f73a628de14..807efd92f65 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -86,10 +86,10 @@ describe('Test shapes defaults:', function() { it('should provide the right defaults on all axis types', function() { var fullLayout = { - xaxis: {type: 'linear', range: [0, 20]}, - yaxis: {type: 'log', range: [1, 5]}, - xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09']}, - yaxis2: {type: 'category', range: [-0.5, 7.5]}, + xaxis: {type: 'linear', range: [0, 20], _shapeIndices: []}, + yaxis: {type: 'log', range: [1, 5], _shapeIndices: []}, + xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09'], _shapeIndices: []}, + yaxis2: {type: 'category', range: [-0.5, 7.5], _shapeIndices: []}, _subplots: {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']} }; From a0bfaf36fe7406b3868ed882c09c73e71074999f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 30 Jul 2018 18:01:30 -0400 Subject: [PATCH 18/21] collapse trace extremes before getAutorange ... by factoring out logic from findExtremes --- src/plots/cartesian/autorange.js | 140 ++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 51 deletions(-) diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index dbd646eecde..cc33cd09767 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -195,14 +195,21 @@ function concatExtremes(gd, ax) { var fullLayout = gd._fullLayout; var minArray = []; var maxArray = []; + var i, j, d; function _concat(cont, indices) { - for(var i = 0; i < indices.length; i++) { + for(i = 0; i < indices.length; i++) { var item = cont[indices[i]]; var extremes = (item._extremes || {})[axId]; if(item.visible === true && extremes) { - minArray = minArray.concat(extremes.min); - maxArray = maxArray.concat(extremes.max); + for(j = 0; j < extremes.min.length; j++) { + d = extremes.min[j]; + collapseMinArray(minArray, d.val, d.pad, {extrapad: d.extrapad}); + } + for(j = 0; j < extremes.max.length; j++) { + d = extremes.max[j]; + collapseMaxArray(maxArray, d.val, d.pad, {extrapad: d.extrapad}); + } } } } @@ -211,8 +218,6 @@ function concatExtremes(gd, ax) { _concat(fullLayout.annotations || [], ax._annIndices || []); _concat(fullLayout.shapes || [], ax._shapeIndices || []); - // TODO collapse more! - return {min: minArray, max: maxArray}; } @@ -295,11 +300,9 @@ function findExtremes(ax, data, options) { var len = data.length; var extrapad = options.padded || false; var tozero = options.tozero && (ax.type === 'linear' || ax.type === '-'); - var isLog = (ax.type === 'log'); - - var i, j, k, v, di, dmin, dmax, ppadiplus, ppadiminus, includeThis, vmin, vmax; - + var isLog = ax.type === 'log'; var hasArrayOption = false; + var i, v, di, dmin, dmax, ppadiplus, ppadiminus, vmin, vmax; function makePadAccessor(item) { if(Array.isArray(item)) { @@ -344,6 +347,8 @@ function findExtremes(ax, data, options) { len = 2; } + var collapseOpts = {tozero: tozero, extrapad: extrapad}; + function addItem(i) { di = data[i]; if(!isNumeric(di)) return; @@ -364,48 +369,11 @@ function findExtremes(ax, data, options) { dmin = Math.min(0, dmin); dmax = Math.max(0, dmax); } - - for(k = 0; k < 2; k++) { - var newVal = k ? dmax : dmin; - if(goodNumber(newVal)) { - var extremes = k ? maxArray : minArray; - var newPad = k ? ppadiplus : ppadiminus; - var atLeastAsExtreme = k ? greaterOrEqual : lessOrEqual; - - includeThis = true; - /* - * Take items v from ax._min/_max and compare them to the presently active point: - * - Since we don't yet know the relationship between pixels and values - * (that's what we're trying to figure out!) AND we don't yet know how - * many pixels `extrapad` represents (it's going to be 5% of the length, - * but we don't want to have to redo _min and _max just because length changed) - * two point must satisfy three criteria simultaneously for one to supersede the other: - * - at least as extreme a `val` - * - at least as big a `pad` - * - an unpadded point cannot supersede a padded point, but any other combination can - * - * - If the item supersedes the new point, set includethis false - * - If the new pt supersedes the item, delete it from ax._min/_max - */ - for(j = 0; j < extremes.length && includeThis; j++) { - v = extremes[j]; - if(atLeastAsExtreme(v.val, newVal) && v.pad >= newPad && (v.extrapad || !extrapad)) { - includeThis = false; - break; - } else if(atLeastAsExtreme(newVal, v.val) && v.pad <= newPad && (extrapad || !v.extrapad)) { - extremes.splice(j, 1); - j--; - } - } - if(includeThis) { - var clipAtZero = (tozero && newVal === 0); - extremes.push({ - val: newVal, - pad: clipAtZero ? 0 : newPad, - extrapad: clipAtZero ? false : extrapad - }); - } - } + if(goodNumber(dmin)) { + collapseMinArray(minArray, dmin, ppadiminus, collapseOpts); + } + if(goodNumber(dmax)) { + collapseMaxArray(maxArray, dmax, ppadiplus, collapseOpts); } } @@ -419,6 +387,76 @@ function findExtremes(ax, data, options) { return {min: minArray, max: maxArray}; } +function collapseMinArray(array, newVal, newPad, opts) { + collapseArray(array, newVal, newPad, opts, lessOrEqual); +} + +function collapseMaxArray(array, newVal, newPad, opts) { + collapseArray(array, newVal, newPad, opts, greaterOrEqual); +} + +/** + * collapseArray + * + * Take items v from 'array' compare them to 'newVal', 'newPad' + * + * @param {array} array: + * current set of min or max extremes + * @param {number} newVal: + * new value to compare against + * @param {number} newPad: + * pad value associated with 'newVal' + * @param {object} opts: + * - tozero {boolean} + * - extrapad {number} + * @param {function} atLeastAsExtreme: + * comparison function, use + * - lessOrEqual for min 'array' and + * - greaterOrEqual for max 'array' + * + * In practice, 'array' is either + * - 'extremes[ax._id].min' or + * - 'extremes[ax._id].max + * found in traces and layout items that affect autorange. + * + * Since we don't yet know the relationship between pixels and values + * (that's what we're trying to figure out!) AND we don't yet know how + * many pixels `extrapad` represents (it's going to be 5% of the length, + * but we don't want to have to redo calc just because length changed) + * two point must satisfy three criteria simultaneously for one to supersede the other: + * - at least as extreme a `val` + * - at least as big a `pad` + * - an unpadded point cannot supersede a padded point, but any other combination can + * + * Then: + * - If the item supersedes the new point, set includeThis false + * - If the new pt supersedes the item, delete it from 'array' + */ +function collapseArray(array, newVal, newPad, opts, atLeastAsExtreme) { + var tozero = opts.tozero; + var extrapad = opts.extrapad; + var includeThis = true; + + for(var j = 0; j < array.length && includeThis; j++) { + var v = array[j]; + if(atLeastAsExtreme(v.val, newVal) && v.pad >= newPad && (v.extrapad || !extrapad)) { + includeThis = false; + break; + } else if(atLeastAsExtreme(newVal, v.val) && v.pad <= newPad && (extrapad || !v.extrapad)) { + array.splice(j, 1); + j--; + } + } + if(includeThis) { + var clipAtZero = (tozero && newVal === 0); + array.push({ + val: newVal, + pad: clipAtZero ? 0 : newPad, + extrapad: clipAtZero ? false : extrapad + }); + } +} + // In order to stop overflow errors, don't consider points // too close to the limits of js floating point function goodNumber(v) { From 33b4085010209af0f37b8a2ca538115be76bfc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 1 Aug 2018 11:06:00 -0400 Subject: [PATCH 19/21] mv repeat -> Lib.repeat --- src/lib/index.js | 15 +++++++++++++++ src/traces/scattergl/index.js | 12 ++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index 6e0a3e24a61..9df929ce3e4 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -150,6 +150,21 @@ lib.isIndex = function(v, len) { lib.noop = require('./noop'); lib.identity = require('./identity'); +/** + * create an array of length 'cnt' filled with 'v' at all indices + * + * @param {any} v + * @param {number} cnt + * @return {array} + */ +lib.repeat = function(v, cnt) { + var out = new Array(cnt); + for(var i = 0; i < cnt; i++) { + out[i] = v; + } + return out; +}; + /** * swap x and y of the same attribute in container cont * specify attr with a ? in place of x/y diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index a6f02b61bac..e4cf251a984 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -238,7 +238,7 @@ function sceneUpdate(gd, subplot) { // apply new option to all regl components (used on drag) scene.update = function update(opt) { - var opts = repeat(opt, scene.count); + var opts = Lib.repeat(opt, scene.count); if(scene.fill2d) scene.fill2d.update(opts); if(scene.scatter2d) scene.scatter2d.update(opts); @@ -366,14 +366,6 @@ function clearViewport(comp, vp) { gl.clear(gl.COLOR_BUFFER_BIT); } -function repeat(opt, cnt) { - var opts = new Array(cnt); - for(var i = 0; i < cnt; i++) { - opts[i] = opt; - } - return opts; -} - function plot(gd, subplot, cdata) { if(!cdata.length) return; @@ -619,7 +611,7 @@ function plot(gd, subplot, cdata) { (yaxis._rl || yaxis.range)[1] ] }; - var vpRange = repeat(vpRange0, scene.count); + var vpRange = Lib.repeat(vpRange0, scene.count); // upload viewport/range data to GPU if(scene.fill2d) { From d233f3bbadc2aacd6539fd397c701cf7dd9e5796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 1 Aug 2018 11:33:14 -0400 Subject: [PATCH 20/21] fix and :lock: _extremes in polar tranformed traces --- src/plots/polar/layout_defaults.js | 2 +- test/image/baselines/polar_transforms.png | Bin 0 -> 48588 bytes test/image/mocks/polar_transforms.json | 79 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 test/image/baselines/polar_transforms.png create mode 100644 test/image/mocks/polar_transforms.json diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index b83f486573a..850301deef2 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -55,7 +55,7 @@ function handleDefaults(contIn, contOut, coerce, opts) { // propagate the template. var axOut = contOut[axName] = {}; axOut._id = axOut._name = axName; - axOut._traceIndices = subplotData.map(function(t) { return t.index; }); + axOut._traceIndices = subplotData.map(function(t) { return t._expandedIndex; }); var dataAttr = constants.axisName2dataArray[axName]; var axType = handleAxisTypeDefaults(axIn, axOut, coerceAxis, subplotData, dataAttr); diff --git a/test/image/baselines/polar_transforms.png b/test/image/baselines/polar_transforms.png new file mode 100644 index 0000000000000000000000000000000000000000..8cebb23abcde796fa0bdabfe85250a5060824ba7 GIT binary patch literal 48588 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

ni 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 Qkib6`1r7O1S=", + "value": 0, + "preservegaps": true + }], + "name": "Groupby+filter" + }, + { + "type": "scatterpolar", + "r": [1, 2, 3, 4, -3], + "theta": [1.1, 2.2, 3.3, 4.4, 5.5], + "thetaunit": "radians", + "marker": { + "size": [0.3, 0.2, 0.1, 0.4, 0.5], + "sizeref": 0.01, + "color": [2, 4, 6, 10, 8], + "opacity": [0.9, 0.6, 0.2, 0.8, 1.0], + "line": { + "color": [2.2, 3.3, 4.4, 5.5, 1.1] + } + }, + "transforms": [{ + "type": "aggregate", + "groups": ["a", "b", "a", "a", "a"], + "aggregations": [ + {"target": "r", "func": "sum"}, + {"target": "theta", "func": "avg"}, + {"target": "marker.size", "func": "min"}, + {"target": "marker.color", "func": "max"}, + {"target": "marker.line.color", "func": "last"}, + {"target": "marker.line.width", "func": "count"} + ] + }], + "name": "Aggregate" + }, + { + "type": "scatterpolar", + "r": [1, 2, 3, 4, 5, 6], + "theta": [1, 4, 2, 6, 5, 3], + "thetaunit": "radians", + "transforms": [{ + "type": "sort", + "target": [1, 6, 2, 5, 3, 4] + }], + "name": "Sort" + }, + { + "type": "scatterpolar", + "r":[4, 5, 6, 4, 5, 6], + "theta": [1, 1, 1, 2, 2, 2], + "thetaunit": "radians", + "marker": {"color": [1, 2, 3, -1, -2, -3], "size": 12}, + "mode": "lines+markers", + "transforms": [ + {"type": "groupby", "groups": [1, 1, 1, 2, 2, 2]} + ] + }], + "layout": { + "width": 600, + "height": 400, + "title": "Transforms on polar subplot" + } +} From 89aebd1737bb3fe9b82ff61b8793050901eeeba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 1 Aug 2018 12:07:04 -0400 Subject: [PATCH 21/21] fix typos in :books: --- src/plots/cartesian/autorange.js | 4 ++-- src/plots/get_data.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index cc33cd09767..eaab8f37b5d 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -27,7 +27,7 @@ module.exports = { * Collects all _extremes values corresponding to a given axis * and computes its auto range. * - * getAutoRange uses return values from findExtremes where: + * Note that getAutoRange uses return values from findExtremes. * * @param {object} gd: * graph div object with filled-in fullData and fullLayout, in particular @@ -398,7 +398,7 @@ function collapseMaxArray(array, newVal, newPad, opts) { /** * collapseArray * - * Take items v from 'array' compare them to 'newVal', 'newPad' + * Takes items from 'array' and compares them to 'newVal', 'newPad'. * * @param {array} array: * current set of min or max extremes diff --git a/src/plots/get_data.js b/src/plots/get_data.js index ca9e35c611b..de6710a5159 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -69,7 +69,7 @@ exports.getModuleCalcData = function(calcdata, arg1) { for(var i = 0; i < calcdata.length; i++) { var cd = calcdata[i]; var trace = cd[0].trace; - // N.B. 'legendonly' traces do not make it pass here + // N.B. 'legendonly' traces do not make it past here if(trace.visible !== true) continue; // group calcdata trace not by 'module' (as the name of this function