From 681386563a6d9f0056f9a38975d3279ed25b5328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 24 Jan 2019 17:15:05 -0500 Subject: [PATCH 01/23] mv handleConstraintDefaults into constraints.js file --- src/plots/cartesian/constraint_defaults.js | 152 --------------------- src/plots/cartesian/constraints.js | 139 ++++++++++++++++++- src/plots/cartesian/layout_defaults.js | 2 +- 3 files changed, 138 insertions(+), 155 deletions(-) delete mode 100644 src/plots/cartesian/constraint_defaults.js diff --git a/src/plots/cartesian/constraint_defaults.js b/src/plots/cartesian/constraint_defaults.js deleted file mode 100644 index 3b275c457e4..00000000000 --- a/src/plots/cartesian/constraint_defaults.js +++ /dev/null @@ -1,152 +0,0 @@ -/** -* Copyright 2012-2019, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var Lib = require('../../lib'); -var id2name = require('./axis_ids').id2name; - - -module.exports = function handleConstraintDefaults(containerIn, containerOut, coerce, allAxisIds, layoutOut) { - var constraintGroups = layoutOut._axisConstraintGroups; - var thisID = containerOut._id; - var letter = thisID.charAt(0); - - if(containerOut.fixedrange) return; - - // coerce the constraint mechanics even if this axis has no scaleanchor - // because it may be the anchor of another axis. - coerce('constrain'); - Lib.coerce(containerIn, containerOut, { - constraintoward: { - valType: 'enumerated', - values: letter === 'x' ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'], - dflt: letter === 'x' ? 'center' : 'middle' - } - }, 'constraintoward'); - - if(!containerIn.scaleanchor) return; - - var constraintOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut); - - var scaleanchor = Lib.coerce(containerIn, containerOut, { - scaleanchor: { - valType: 'enumerated', - values: constraintOpts.linkableAxes - } - }, 'scaleanchor'); - - if(scaleanchor) { - var scaleratio = coerce('scaleratio'); - // TODO: I suppose I could do attribute.min: Number.MIN_VALUE to avoid zero, - // but that seems hacky. Better way to say "must be a positive number"? - // Of course if you use several super-tiny values you could eventually - // force a product of these to zero and all hell would break loose... - // Likewise with super-huge values. - if(!scaleratio) scaleratio = containerOut.scaleratio = 1; - - updateConstraintGroups(constraintGroups, constraintOpts.thisGroup, - thisID, scaleanchor, scaleratio); - } - else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) { - Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' + - containerIn.scaleanchor + '" to avoid either an infinite loop ' + - 'and possibly inconsistent scaleratios, or because the target' + - 'axis has fixed range.'); - } -}; - -function getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut) { - // If this axis is already part of a constraint group, we can't - // scaleanchor any other axis in that group, or we'd make a loop. - // Filter allAxisIds to enforce this, also matching axis types. - - var thisType = layoutOut[id2name(thisID)].type; - - var i, j, idj, axj; - - var linkableAxes = []; - for(j = 0; j < allAxisIds.length; j++) { - idj = allAxisIds[j]; - if(idj === thisID) continue; - - axj = layoutOut[id2name(idj)]; - if(axj.type === thisType && !axj.fixedrange) linkableAxes.push(idj); - } - - for(i = 0; i < constraintGroups.length; i++) { - if(constraintGroups[i][thisID]) { - var thisGroup = constraintGroups[i]; - - var linkableAxesNoLoops = []; - for(j = 0; j < linkableAxes.length; j++) { - idj = linkableAxes[j]; - if(!thisGroup[idj]) linkableAxesNoLoops.push(idj); - } - return {linkableAxes: linkableAxesNoLoops, thisGroup: thisGroup}; - } - } - - return {linkableAxes: linkableAxes, thisGroup: null}; -} - - -/* - * Add this axis to the axis constraint groups, which is the collection - * of axes that are all constrained together on scale. - * - * constraintGroups: a list of objects. each object is - * {axis_id: scale_within_group}, where scale_within_group is - * only important relative to the rest of the group, and defines - * the relative scales between all axes in the group - * - * thisGroup: the group the current axis is already in - * thisID: the id if the current axis - * scaleanchor: the id of the axis to scale it with - * scaleratio: the ratio of this axis to the scaleanchor axis - */ -function updateConstraintGroups(constraintGroups, thisGroup, thisID, scaleanchor, scaleratio) { - var i, j, groupi, keyj, thisGroupIndex; - - if(thisGroup === null) { - thisGroup = {}; - thisGroup[thisID] = 1; - thisGroupIndex = constraintGroups.length; - constraintGroups.push(thisGroup); - } - else { - thisGroupIndex = constraintGroups.indexOf(thisGroup); - } - - var thisGroupKeys = Object.keys(thisGroup); - - // we know that this axis isn't in any other groups, but we don't know - // about the scaleanchor axis. If it is, we need to merge the groups. - for(i = 0; i < constraintGroups.length; i++) { - groupi = constraintGroups[i]; - if(i !== thisGroupIndex && groupi[scaleanchor]) { - var baseScale = groupi[scaleanchor]; - for(j = 0; j < thisGroupKeys.length; j++) { - keyj = thisGroupKeys[j]; - groupi[keyj] = baseScale * scaleratio * thisGroup[keyj]; - } - constraintGroups.splice(thisGroupIndex, 1); - return; - } - } - - // otherwise, we insert the new scaleanchor axis as the base scale (1) - // in its group, and scale the rest of the group to it - if(scaleratio !== 1) { - for(j = 0; j < thisGroupKeys.length; j++) { - thisGroup[thisGroupKeys[j]] *= scaleratio; - } - } - thisGroup[scaleanchor] = 1; -} diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index cac5d918189..9403978d463 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -6,18 +6,153 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; +var Lib = require('../../lib'); 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; - var FROM_BL = require('../../constants/alignment').FROM_BL; +exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, allAxisIds, layoutOut) { + var constraintGroups = layoutOut._axisConstraintGroups; + var thisID = containerOut._id; + var letter = thisID.charAt(0); + + if(containerOut.fixedrange) return; + + // coerce the constraint mechanics even if this axis has no scaleanchor + // because it may be the anchor of another axis. + coerce('constrain'); + Lib.coerce(containerIn, containerOut, { + constraintoward: { + valType: 'enumerated', + values: letter === 'x' ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'], + dflt: letter === 'x' ? 'center' : 'middle' + } + }, 'constraintoward'); + + if(!containerIn.scaleanchor) return; + + var constraintOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut); + + var scaleanchor = Lib.coerce(containerIn, containerOut, { + scaleanchor: { + valType: 'enumerated', + values: constraintOpts.linkableAxes + } + }, 'scaleanchor'); + + if(scaleanchor) { + var scaleratio = coerce('scaleratio'); + // TODO: I suppose I could do attribute.min: Number.MIN_VALUE to avoid zero, + // but that seems hacky. Better way to say "must be a positive number"? + // Of course if you use several super-tiny values you could eventually + // force a product of these to zero and all hell would break loose... + // Likewise with super-huge values. + if(!scaleratio) scaleratio = containerOut.scaleratio = 1; + + updateConstraintGroups(constraintGroups, constraintOpts.thisGroup, + thisID, scaleanchor, scaleratio); + } + else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) { + Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' + + containerIn.scaleanchor + '" to avoid either an infinite loop ' + + 'and possibly inconsistent scaleratios, or because the target' + + 'axis has fixed range.'); + } +}; + +function getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut) { + // If this axis is already part of a constraint group, we can't + // scaleanchor any other axis in that group, or we'd make a loop. + // Filter allAxisIds to enforce this, also matching axis types. + + var thisType = layoutOut[id2name(thisID)].type; + + var i, j, idj, axj; + + var linkableAxes = []; + for(j = 0; j < allAxisIds.length; j++) { + idj = allAxisIds[j]; + if(idj === thisID) continue; + + axj = layoutOut[id2name(idj)]; + if(axj.type === thisType && !axj.fixedrange) linkableAxes.push(idj); + } + + for(i = 0; i < constraintGroups.length; i++) { + if(constraintGroups[i][thisID]) { + var thisGroup = constraintGroups[i]; + + var linkableAxesNoLoops = []; + for(j = 0; j < linkableAxes.length; j++) { + idj = linkableAxes[j]; + if(!thisGroup[idj]) linkableAxesNoLoops.push(idj); + } + return {linkableAxes: linkableAxesNoLoops, thisGroup: thisGroup}; + } + } + + return {linkableAxes: linkableAxes, thisGroup: null}; +} + +/* + * Add this axis to the axis constraint groups, which is the collection + * of axes that are all constrained together on scale. + * + * constraintGroups: a list of objects. each object is + * {axis_id: scale_within_group}, where scale_within_group is + * only important relative to the rest of the group, and defines + * the relative scales between all axes in the group + * + * thisGroup: the group the current axis is already in + * thisID: the id if the current axis + * scaleanchor: the id of the axis to scale it with + * scaleratio: the ratio of this axis to the scaleanchor axis + */ +function updateConstraintGroups(constraintGroups, thisGroup, thisID, scaleanchor, scaleratio) { + var i, j, groupi, keyj, thisGroupIndex; + + if(thisGroup === null) { + thisGroup = {}; + thisGroup[thisID] = 1; + thisGroupIndex = constraintGroups.length; + constraintGroups.push(thisGroup); + } + else { + thisGroupIndex = constraintGroups.indexOf(thisGroup); + } + + var thisGroupKeys = Object.keys(thisGroup); + + // we know that this axis isn't in any other groups, but we don't know + // about the scaleanchor axis. If it is, we need to merge the groups. + for(i = 0; i < constraintGroups.length; i++) { + groupi = constraintGroups[i]; + if(i !== thisGroupIndex && groupi[scaleanchor]) { + var baseScale = groupi[scaleanchor]; + for(j = 0; j < thisGroupKeys.length; j++) { + keyj = thisGroupKeys[j]; + groupi[keyj] = baseScale * scaleratio * thisGroup[keyj]; + } + constraintGroups.splice(thisGroupIndex, 1); + return; + } + } + + // otherwise, we insert the new scaleanchor axis as the base scale (1) + // in its group, and scale the rest of the group to it + if(scaleratio !== 1) { + for(j = 0; j < thisGroupKeys.length; j++) { + thisGroup[thisGroupKeys[j]] *= scaleratio; + } + } + thisGroup[scaleanchor] = 1; +} exports.enforce = function enforceAxisConstraints(gd) { var fullLayout = gd._fullLayout; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 8a3a05d940d..e82aca3ee89 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -17,7 +17,7 @@ var basePlotLayoutAttributes = require('../layout_attributes'); var layoutAttributes = require('./layout_attributes'); var handleTypeDefaults = require('./type_defaults'); var handleAxisDefaults = require('./axis_defaults'); -var handleConstraintDefaults = require('./constraint_defaults'); +var handleConstraintDefaults = require('./constraints').handleConstraintDefaults; var handlePositionDefaults = require('./position_defaults'); var axisIds = require('./axis_ids'); From 6f2bb788a866012e8f99bd9bc26079a50ae8a846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 23 Jan 2019 10:37:26 -0500 Subject: [PATCH 02/23] first cut at static `ax.matches` behavior --- src/plot_api/plot_api.js | 29 +++++-- src/plot_api/subroutines.js | 38 ++++++++- src/plots/cartesian/autorange.js | 2 + src/plots/cartesian/constraints.js | 31 +++++-- src/plots/cartesian/layout_attributes.js | 17 ++++ src/plots/cartesian/layout_defaults.js | 49 +++++++++-- test/jasmine/tests/axes_test.js | 102 +++++++++++++++++++++++ 7 files changed, 246 insertions(+), 22 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index e950d614426..1c1306acf9e 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1948,6 +1948,10 @@ function axRangeSupplyDefaultsByPass(gd, flags, specs) { axOut.range = axIn.range.slice(); axOut.cleanRange(); } + + // no need to consider matching axes here, + // if we keep block in doAutoRangeAndConstraints + return true; } @@ -1957,14 +1961,29 @@ function addAxRangeSequence(seq, rangesAltered) { // executed after drawData var drawAxes = rangesAltered ? function(gd) { - var opts = {skipTitle: true}; + var axIds = []; + var skipTitle = true; + var matchGroups = gd._fullLayout._axisMatchGroups || []; + for(var id in rangesAltered) { - if(Axes.getFromId(gd, id).automargin) { - opts = {}; - break; + var ax = Axes.getFromId(gd, id); + axIds.push(id); + + for(var i = 0; i < matchGroups.length; i++) { + var group = matchGroups[i]; + if(group[id]) { + for(var id2 in group) { + if(!rangesAltered[id2]) { + axIds.push(id2); + } + } + } } + + if(ax.automargin) skipTitle = false; } - return Axes.draw(gd, Object.keys(rangesAltered), opts); + + return Axes.draw(gd, axIds, {skipTitle: skipTitle}); } : function(gd) { return Axes.draw(gd, 'redraw'); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index a2bb7083dd1..25aa2817741 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -697,16 +697,48 @@ exports.redrawReglTraces = function(gd) { }; exports.doAutoRangeAndConstraints = function(gd) { + var fullLayout = gd._fullLayout; var axList = Axes.list(gd, '', true); + var matchGroups = fullLayout._axisMatchGroups || []; + var ax; for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; + ax = axList[i]; cleanAxisConstraints(gd, ax); - // in case margins changed, update scale - ax.setScale(); doAutoRange(gd, ax); } + // TODO bypass this when matching axes aren't autoranged? + for(var j = 0; j < matchGroups.length; j++) { + var group = matchGroups[j]; + var rng = null; + var id; + + for(id in group) { + ax = Axes.getFromId(gd, id); + + if(rng) { + if(rng[0] < rng[1]) { + rng[0] = Math.min(rng[0], ax.range[0]); + rng[1] = Math.max(rng[1], ax.range[1]); + } else { + rng[0] = Math.max(rng[0], ax.range[0]); + rng[1] = Math.min(rng[1], ax.range[1]); + } + } else { + rng = ax.range; + } + } + + for(id in group) { + ax = Axes.getFromId(gd, id); + ax.range = rng.slice(); + ax._input.range = rng.slice(); + ax.setScale(0); + } + } + + // TODO before or after matching axes? enforceAxisConstraints(gd); }; diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 55cf30c279b..153ac9f3f52 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -237,6 +237,8 @@ function concatExtremes(gd, ax) { } function doAutoRange(gd, ax) { + ax.setScale(); + if(ax.autorange) { ax.range = getAutoRange(gd, ax); diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index 9403978d463..6e8d1314a99 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -19,6 +19,7 @@ var FROM_BL = require('../../constants/alignment').FROM_BL; exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, allAxisIds, layoutOut) { var constraintGroups = layoutOut._axisConstraintGroups; + var matchGroups = layoutOut._axisMatchGroups; var thisID = containerOut._id; var letter = thisID.charAt(0); @@ -35,19 +36,29 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a } }, 'constraintoward'); - if(!containerIn.scaleanchor) return; + if(!containerIn.scaleanchor && !containerIn.matches) return; - var constraintOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut); + var opts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut); var scaleanchor = Lib.coerce(containerIn, containerOut, { scaleanchor: { valType: 'enumerated', - values: constraintOpts.linkableAxes + values: opts.linkableAxes } }, 'scaleanchor'); + var matches = Lib.coerce(containerIn, containerOut, { + matches: { + valType: 'enumerated', + values: opts.linkableAxes + } + }, 'matches'); + + var found = false; + if(scaleanchor) { var scaleratio = coerce('scaleratio'); + // TODO: I suppose I could do attribute.min: Number.MIN_VALUE to avoid zero, // but that seems hacky. Better way to say "must be a positive number"? // Of course if you use several super-tiny values you could eventually @@ -55,10 +66,18 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a // Likewise with super-huge values. if(!scaleratio) scaleratio = containerOut.scaleratio = 1; - updateConstraintGroups(constraintGroups, constraintOpts.thisGroup, - thisID, scaleanchor, scaleratio); + updateConstraintGroups(constraintGroups, opts.thisGroup, thisID, scaleanchor, scaleratio); + found = true; } - else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) { + + // TODO what happens when both scaleanchor and matches are present??? + + if(matches) { + updateConstraintGroups(matchGroups, opts.thisGroup, thisID, matches, 1); + found = true; + } + + if(!found && allAxisIds.indexOf(containerIn.scaleanchor) !== -1) { Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' + containerIn.scaleanchor + '" to avoid either an infinite loop ' + 'and possibly inconsistent scaleratios, or because the target' + diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index c0f7f9b3a83..7c44aaedd06 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -211,6 +211,23 @@ module.exports = { 'and *right* for x axes, and *top*, *middle* (default), and *bottom* for y axes.' ].join(' ') }, + matches: { + valType: 'enumerated', + values: [ + constants.idRegex.x.toString(), + constants.idRegex.y.toString() + ], + role: 'info', + editType: 'calc', + description: [ + 'If set to another axis id (e.g. `x2`, `y`), the range of this axis', + 'will match the range of the corresponding axis in data-coordinates space.', + 'Moreover, matching axes share auto-range values, category lists and', + 'histogram auto-bins.', + 'Note that setting `matches` and `scaleratio` under a *range* `constrain`', + 'to the same axis id is forbidden.' + ].join(' ') + }, // ticks tickmode: { valType: 'enumerated', diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index e82aca3ee89..dd71bc62610 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -124,6 +124,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')}; + var allAxisIds = counterAxes.x.concat(counterAxes.y); function getOverlayableAxes(axLetter, axName) { var list = (axLetter === 'x') ? xNames : yNames; @@ -199,14 +200,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { delete axLayoutOut.spikesnap; } - var positioningOptions = { + handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, { letter: axLetter, counterAxes: counterAxes[axLetter], overlayableAxes: overlayableAxes, grid: layoutOut.grid - }; - - handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions); + }); axLayoutOut._input = axLayoutIn; } @@ -247,22 +246,56 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { coerce('fixedrange', fixedRangeDflt); } - // Finally, handle scale constraints. We need to do this after all axes have - // coerced both `type` (so we link only axes of the same type) and + // Finally, handle scale constraints and matching axes. + // + // We need to do this after all axes have coerced both `type` + // (so we link only axes of the same type) and // `fixedrange` (so we can avoid linking from OR TO a fixed axis). // sets of axes linked by `scaleanchor` along with the scaleratios compounded // together, populated in handleConstraintDefaults layoutOut._axisConstraintGroups = []; - var allAxisIds = counterAxes.x.concat(counterAxes.y); + // similar to _axisConstraintGroups, but for matching axes + layoutOut._axisMatchGroups = []; for(i = 0; i < axNames.length; i++) { axName = axNames[i]; axLetter = axName.charAt(0); - axLayoutIn = layoutIn[axName]; axLayoutOut = layoutOut[axName]; handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, allAxisIds, layoutOut); } + + for(i = 0; i < layoutOut._axisMatchGroups.length; i++) { + var group = layoutOut._axisMatchGroups[i]; + var rng = null; + var autorange = null; + var axId; + + for(axId in group) { + axLayoutOut = layoutOut[id2name(axId)]; + if(!axLayoutOut.matches) { + rng = axLayoutOut.range; + autorange = axLayoutOut.autorange; + } + } + + if(rng === null || autorange === null) { + for(axId in group) { + axLayoutOut = layoutOut[id2name(axId)]; + rng = axLayoutOut.range; + autorange = axLayoutOut.autorange; + break; + } + } + + for(axId in group) { + axLayoutOut = layoutOut[id2name(axId)]; + if(axLayoutOut.matches) { + axLayoutOut.range = rng.slice(); + axLayoutOut.autorange = autorange; + } + } + } }; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 03eb6563cad..f04afb1800f 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -670,6 +670,25 @@ describe('Test axes', function() { }); }); + it('will not match axes of different types', function() { + layoutIn = { + xaxis: {type: 'linear'}, + yaxis: {type: 'log', matches: 'x'}, + xaxis2: {type: 'date', matches: 'y'}, + yaxis2: {type: 'category', matches: 'x2'} + }; + layoutOut._subplots.cartesian.push('x2y2'); + layoutOut._subplots.yaxis.push('x2', 'y2'); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups).toEqual([]); + + ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(axName) { + expect(layoutOut[axName].matches).toBeUndefined(); + }); + }); + it('drops scaleanchor settings if either the axis or target has fixedrange', function() { // some of these will create warnings... not too important, so not going to test, // just want to keep the output clean @@ -697,6 +716,26 @@ describe('Test axes', function() { }); }); + it('drops *matches* settings if either the axis or target has fixedrange', function() { + layoutIn = { + xaxis: {fixedrange: true, matches: 'y'}, + yaxis: {matches: 'x2'}, // only this one should survive + xaxis2: {}, + yaxis2: {matches: 'x'} + }; + layoutOut._subplots.cartesian.push('x2y2'); + layoutOut._subplots.yaxis.push('x2', 'y2'); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups).toEqual([{x2: 1, y: 1}]); + expect(layoutOut.yaxis.matches).toBe('x2'); + + ['xaxis', 'yaxis2', 'xaxis2'].forEach(function(axName) { + expect(layoutOut[axName].matches).toBeUndefined(); + }); + }); + it('should coerce hoverformat even on visible: false axes', function() { layoutIn = { xaxis: { @@ -708,6 +747,69 @@ describe('Test axes', function() { supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.hoverformat).toEqual('g'); }); + + it('should find matching groups', function() { + layoutIn = { + // both linked to 'base' ax + xaxis: {}, + xaxis2: {matches: 'x'}, + xaxis3: {matches: 'x'}, + // cascading links + yaxis: {}, + yaxis2: {anchor: 'x2', matches: 'y'}, + yaxis3: {anchor: 'x3', matches: 'y2'}, + }; + layoutOut._subplots.cartesian.push('x2y2', 'x3y3'); + layoutOut._subplots.xaxis.push('x2', 'x3'); + layoutOut._subplots.yaxis.push('y2', 'y3'); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(2); + expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x3: 1}); + expect(layoutOut._axisMatchGroups).toContain({y: 1, y2: 1, y3: 1}); + }); + + it('should match set axis range value for matching axes', function() { + layoutIn = { + // autorange case + xaxis: {}, + xaxis2: {matches: 'x'}, + // matchee ax has range + yaxis: {range: [0, 1]}, + yaxis2: {matches: 'y'}, + // matcher ax has range (gets ignored) + xaxis3: {}, + yaxis3: {range: [-1, 1], matches: 'x3'}, + // both ax have range + xaxis4: {range: [0, 2], matches: 'y4'}, + yaxis4: {range: [-1, 3], matches: 'x4'} + }; + layoutOut._subplots.cartesian.push('x2y2', 'x3y3', 'x4y4'); + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4'); + layoutOut._subplots.yaxis.push('y2', 'y3', 'y4'); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(4); + expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1}); + expect(layoutOut._axisMatchGroups).toContain({y: 1, y2: 1}); + expect(layoutOut._axisMatchGroups).toContain({x3: 1, y3: 1}); + expect(layoutOut._axisMatchGroups).toContain({x4: 1, y4: 1}); + + function _assertMatchingAxes(names, autorange, rng) { + names.forEach(function(n) { + var ax = layoutOut[n]; + expect(ax.autorange).toBe(autorange, n); + expect(ax.range).toEqual(rng); + }); + } + + _assertMatchingAxes(['xaxis', 'xaxis2'], true, [-1, 6]); + _assertMatchingAxes(['yaxis', 'yaxis2'], false, [0, 1]); + _assertMatchingAxes(['xaxis3', 'yaxis3'], true, [-1, 6]); + _assertMatchingAxes(['xaxis4', 'yaxis4'], false, [0, 2]); + }); }); describe('constraints relayout', function() { From 5ddbf2a42b048099196ce69d78fb4ced1b04479f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 1 Feb 2019 16:05:42 -0500 Subject: [PATCH 03/23] some linting & commenting in dragbox.js --- src/plots/cartesian/dragbox.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 8f44b934554..3705ee302de 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -280,16 +280,22 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { dragElement.init(dragOptions); - var x0, - y0, - box, - lum, - path0, - dimmed, - zoomMode, - zb, - corners; - + // x/y px position at start of drag + var x0, y0; + // bbox object of the zoombox + var box; + // luminance of bg behind zoombox + var lum; + // zoombox path outline + var path0; + // is zoombox dimmed (during drag) + var dimmed; + // 'x'-only, 'y' or 'xy' zooming + var zoomMode; + // zoombox d3 selection + var zb; + // zoombox corner d3 selection + var corners; // zoom takes over minDrag, so it also has to take over gd._dragged var zoomDragged; @@ -305,9 +311,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { dimmed = false; zoomMode = 'xy'; zoomDragged = false; - zb = makeZoombox(zoomlayer, lum, xs, ys, path0); - corners = makeCorners(zoomlayer, xs, ys); } From c60424bf2b851b5b0aaec8ced7641d9f51e6b88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 1 Feb 2019 16:08:14 -0500 Subject: [PATCH 04/23] first cut at zoom/pan/scroll `ax.matches` behavior --- src/plots/cartesian/dragbox.js | 142 ++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 39 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 3705ee302de..53f0d211c98 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -76,12 +76,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // which are the x/y {ax._id: ax} hash objects and their values // for linked axis relative to this subplot var links; + // similar to `links` but for matching axes + var matches; // set to ew/ns val when active, set to '' when inactive var xActive, yActive; // are all axes in this subplot are fixed? var allFixedRanges; - // is subplot constrained? - var isSubplotConstrained; // do we need to edit x/y ranges? var editX, editY; // graph-wide optimization flags @@ -119,10 +119,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { yActive = isDirectionActive(yaxes, ns); allFixedRanges = !yActive && !xActive; - links = calcLinks(gd, xaHash, yaHash); - isSubplotConstrained = links.isSubplotConstrained; - editX = ew || isSubplotConstrained; - editY = ns || isSubplotConstrained; + links = calcLinks(gd, gd._fullLayout._axisConstraintGroups, xaHash, yaHash); + matches = calcLinks(gd, gd._fullLayout._axisMatchGroups, xaHash, yaHash); + editX = ew || links.isSubplotConstrained || matches.isSubplotConstrained; + editY = ns || links.isSubplotConstrained || matches.isSubplotConstrained; var fullLayout = gd._fullLayout; hasScatterGl = fullLayout._has('scattergl'); @@ -337,22 +337,36 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { corners.attr('d', 'M0,0Z'); } - if(isSubplotConstrained) { + if(links.isSubplotConstrained) { if(dx > MINZOOM || dy > MINZOOM) { zoomMode = 'xy'; if(dx / pw > dy / ph) { dy = dx * ph / pw; if(y0 > y1) box.t = y0 - dy; else box.b = y0 + dy; - } - else { + } else { dx = dy * pw / ph; if(x0 > x1) box.l = x0 - dx; else box.r = x0 + dx; } corners.attr('d', xyCorners(box)); + } else { + noZoom(); } - else { + } + else if(matches.isSubplotConstrained) { + if(dx > MINZOOM || dy > MINZOOM) { + zoomMode = 'xy'; + + var r0 = Math.min(box.l / pw, (ph - box.b) / ph); + var r1 = Math.max(box.r / pw, (ph - box.t) / ph); + + box.l = r0 * pw; + box.r = r1 * pw; + box.b = (1 - r0) * ph; + box.t = (1 - r1) * ph; + corners.attr('d', xyCorners(box)); + } else { noZoom(); } } @@ -399,10 +413,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // TODO: edit linked axes in zoomAxRanges and in dragTail if(zoomMode === 'xy' || zoomMode === 'x') { - zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes); + zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes, matches.xaxes); } if(zoomMode === 'xy' || zoomMode === 'y') { - zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes); + zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes, matches.yaxes); } removeZoombox(gd); @@ -468,6 +482,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { for(i = 0; i < xaxes.length; i++) { zoomWheelOneAxis(xaxes[i], xfrac, zoom); } + updateMatchedAxes(matches.isSubplotConstrained ? yaxes : matches.xaxes, xaxes[0].range); scrollViewBox[2] *= zoom; scrollViewBox[0] += scrollViewBox[2] * xfrac * (1 / zoom - 1); @@ -478,6 +493,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { for(i = 0; i < yaxes.length; i++) { zoomWheelOneAxis(yaxes[i], yfrac, zoom); } + updateMatchedAxes(matches.isSubplotConstrained ? xaxes : matches.yaxes, yaxes[0].range); scrollViewBox[3] *= zoom; scrollViewBox[1] += scrollViewBox[3] * (1 - yfrac) * (1 / zoom - 1); @@ -513,9 +529,19 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // prevent axis drawing from monkeying with margins until we're done gd._fullLayout._replotting = true; + var matchedByXaxes; + var matchesByYaxes; + if(matches.isSubplotConstrained) { + matchedByXaxes = yaxes; + matchesByYaxes = xaxes; + } else { + matchedByXaxes = matches.xaxes; + matchesByYaxes = matches.yaxes; + } + if(xActive === 'ew' || yActive === 'ns') { - if(xActive) dragAxList(xaxes, dx); - if(yActive) dragAxList(yaxes, dy); + if(xActive) dragAxList(xaxes, matchedByXaxes, dx); + if(yActive) dragAxList(yaxes, matchesByYaxes, dy); updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); ticksAndAnnotations(); return; @@ -546,7 +572,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { (movedAx._rl[end] - movedAx._rl[otherEnd]); } - if(isSubplotConstrained && xActive && yActive) { + if(links.isSubplotConstrained && xActive && yActive) { // dragging a corner of a constrained subplot: // respect the fixed corner, but harmonize dx and dy var dxySign = ((xActive === 'w') === (yActive === 'n')) ? 1 : -1; @@ -566,7 +592,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { var x0 = (xActive === 'w') ? dx : 0; var y0 = (yActive === 'n') ? dy : 0; - if(isSubplotConstrained) { + if(links.isSubplotConstrained) { var i; if(!xActive && yActive.length === 1) { // dragging one end of the y axis of a constrained subplot @@ -588,6 +614,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } + updateMatchedAxes(matchedByXaxes, xaxes[0].range); + updateMatchedAxes(matchesByYaxes, yaxes[0].range); updateSubplots([x0, y0, pw - dx, ph - dy]); ticksAndAnnotations(); } @@ -607,10 +635,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(editX) { pushActiveAxIds(xaxes); pushActiveAxIds(links.xaxes); + pushActiveAxIds(matches.xaxes); } if(editY) { pushActiveAxIds(yaxes); pushActiveAxIds(links.yaxes); + pushActiveAxIds(matches.yaxes); } updates = {}; @@ -629,9 +659,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(gd._transitioningWithDuration) return; var doubleClickConfig = gd._context.doubleClick; - var axList = (xActive ? xaxes : []).concat(yActive ? yaxes : []); - var attrs = {}; + var axList = []; + if(xActive) axList = axList.concat(xaxes); + if(yActive) axList = axList.concat(yaxes); + if(matches.xaxes) axList = axList.concat(matches.xaxes); + if(matches.yaxes) axList = axList.concat(matches.yaxes); + + var attrs = {}; var ax, i, rangeInitial; // For reset+autosize mode: @@ -668,10 +703,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { else if(doubleClickConfig === 'reset') { // when we're resetting, reset all linked axes too, so we get back // to the fully-auto-with-constraints situation - if(xActive || isSubplotConstrained) axList = axList.concat(links.xaxes); - if(yActive && !isSubplotConstrained) axList = axList.concat(links.yaxes); + if(xActive || links.isSubplotConstrained) axList = axList.concat(links.xaxes); + if(yActive && !links.isSubplotConstrained) axList = axList.concat(links.yaxes); - if(isSubplotConstrained) { + if(links.isSubplotConstrained) { if(!xActive) axList = axList.concat(xaxes); else if(!yActive) axList = axList.concat(yaxes); } @@ -713,10 +748,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { ], gd); } - // x/y scaleFactor stash, - // minimizes number of per-point DOM updates in updateSubplots below - var xScaleFactorOld, yScaleFactorOld; - // updateSubplots - find all plot viewboxes that should be // affected by this drag, and update them. look for all plots // sharing an affected axis (including the one being dragged), @@ -768,6 +799,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(editX2) { xScaleFactor2 = xScaleFactor; clipDx = ew ? viewBox[0] : getShift(xa, xScaleFactor2); + } else if(matches.xaHash[xa._id]) { + xScaleFactor2 = xScaleFactor; + clipDx = viewBox[0] * xa._length / xa0._length; + } else if(matches.yaHash[xa._id]) { + xScaleFactor2 = yScaleFactor; + clipDx = yActive === 'ns' ? + -viewBox[1] * xa._length / ya0._length : + getShift(xa, xScaleFactor2, {n: 'top', s: 'bottom'}[yActive]); } else { xScaleFactor2 = getLinkedScaleFactor(xa, xScaleFactor, yScaleFactor); clipDx = scaleAndGetShift(xa, xScaleFactor2); @@ -776,6 +815,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(editY2) { yScaleFactor2 = yScaleFactor; clipDy = ns ? viewBox[1] : getShift(ya, yScaleFactor2); + } else if(matches.yaHash[ya._id]) { + yScaleFactor2 = yScaleFactor; + clipDy = viewBox[1] * ya._length / ya0._length; + } else if(matches.xaHash[ya._id]) { + yScaleFactor2 = xScaleFactor; + clipDy = xActive === 'ew' ? + -viewBox[0] * ya._length / xa0._length : + getShift(ya, yScaleFactor2, {e: 'right', w: 'left'}[xActive]); } else { yScaleFactor2 = getLinkedScaleFactor(ya, xScaleFactor, yScaleFactor); clipDy = scaleAndGetShift(ya, yScaleFactor2); @@ -809,7 +856,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // the scale of the trace group. // apply only when scale changes, as adjusting the scale of // all the points can be expansive. - if(xScaleFactor2 !== xScaleFactorOld || yScaleFactor2 !== yScaleFactorOld) { + if(xScaleFactor2 !== sp.xScaleFactor || yScaleFactor2 !== sp.yScaleFactor) { Drawing.setPointGroupScale(sp.zoomScalePts, xScaleFactor2, yScaleFactor2); Drawing.setTextPointsScale(sp.zoomScaleTxt, xScaleFactor2, yScaleFactor2); } @@ -817,8 +864,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { Drawing.hideOutsideRangePoints(sp.clipOnAxisFalseTraces, sp); // update x/y scaleFactor stash - xScaleFactorOld = xScaleFactor2; - yScaleFactorOld = yScaleFactor2; + sp.xScaleFactor = xScaleFactor2; + sp.yScaleFactor = yScaleFactor2; } } } @@ -832,7 +879,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(editX && links.xaHash[ax._id]) { return xScaleFactor; } - if(editY && (isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) { + if(editY && (links.isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) { return yScaleFactor; } return 0; @@ -847,8 +894,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { return 0; } - function getShift(ax, scaleFactor) { - return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle']; + function getShift(ax, scaleFactor, from) { + return ax._length * (1 - scaleFactor) * FROM_TL[from || ax.constraintoward || 'middle']; } return dragger; @@ -900,7 +947,7 @@ function getEndText(ax, end) { } } -function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) { +function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes, matchedAxes) { var i, axi, axRangeLinear0, @@ -922,14 +969,22 @@ function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) { } // zoom linked axes about their centers - if(linkedAxes && linkedAxes.length) { + if(linkedAxes.length) { var linkedR0Fraction = (r0Fraction + (1 - r1Fraction)) / 2; + zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates, [], []); + } - zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates); + // TODO is picking the first ax always ok in general? + // What if we're matching an overlaying axis? + var rng = axList[0].range; + for(i = 0; i < matchedAxes.length; i++) { + axi = matchedAxes[i]; + updates[axi._name + '.range[0]'] = rng[0]; + updates[axi._name + '.range[1]'] = rng[1]; } } -function dragAxList(axList, pix) { +function dragAxList(axList, matchedAxes, pix) { for(var i = 0; i < axList.length; i++) { var axi = axList[i]; if(!axi.fixedrange) { @@ -939,6 +994,16 @@ function dragAxList(axList, pix) { ]; } } + + updateMatchedAxes(matchedAxes, axList[0].range); +} + +function updateMatchedAxes(matchedAxes, rng) { + // TODO is picking the first ax always ok in general? + // What if we're matching an overlaying axis? + for(var i = 0; i < matchedAxes.length; i++) { + matchedAxes[i].range = rng.slice(); + } } // common transform for dragging one end of an axis @@ -1052,15 +1117,14 @@ function xyCorners(box) { 'h' + clen + 'v3h-' + (clen + 3) + 'Z'; } -function calcLinks(gd, xaHash, yaHash) { - var constraintGroups = gd._fullLayout._axisConstraintGroups; +function calcLinks(gd, groups, xaHash, yaHash) { var isSubplotConstrained = false; var xLinks = {}; var yLinks = {}; var xID, yID, xLinkID, yLinkID; - for(var i = 0; i < constraintGroups.length; i++) { - var group = constraintGroups[i]; + for(var i = 0; i < groups.length; i++) { + var group = groups[i]; // check if any of the x axes we're dragging is in this constraint group for(xID in xaHash) { if(group[xID]) { From 13007e029507d89410a222f635f3f9726e1fbb27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Jan 2019 13:53:04 -0500 Subject: [PATCH 05/23] add doScroll wrapper for dragbox tests --- test/jasmine/tests/cartesian_interact_test.js | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index aeca9bf68a0..6c36875849d 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -371,6 +371,19 @@ describe('axis zoom/pan and main plot zoom', function() { }; } + function doScroll(subplot, directions, deltaY, opts) { + return function() { + opts = opts || {}; + var edge = opts.edge || ''; + var dx = opts.dx || 0; + var dy = opts.dy || 0; + var dragger = getDragger(subplot, directions); + var coords = getNodeCoords(dragger, edge); + mouseEvent('scroll', coords.x + dx, coords.y + dy, {deltaY: deltaY, element: dragger}); + return delay(constants.REDRAWDELAY + 10)(); + }; + } + function checkRanges(newRanges, msg) { msg = msg || ''; if(msg) msg = ' - ' + msg; @@ -459,26 +472,11 @@ describe('axis zoom/pan and main plot zoom', function() { .then(doDblClick('x3y3', 'nsew')) .then(checkRanges({}, 'reset x3y3')) // scroll wheel - .then(function() { - var mainDrag = getDragger('xy', 'nsew'); - var mainDragCoords = getNodeCoords(mainDrag, 'se'); - mouseEvent('scroll', mainDragCoords.x, mainDragCoords.y, {deltaY: 20, element: mainDrag}); - }) - .then(delay(constants.REDRAWDELAY + 10)) + .then(doScroll('xy', 'nsew', 20, {edge: 'se'})) .then(checkRanges({xaxis: [-0.2103, 2], yaxis: [0, 2.2103]}, 'xy main scroll')) - .then(function() { - var ewDrag = getDragger('xy', 'ew'); - var ewDragCoords = getNodeCoords(ewDrag); - mouseEvent('scroll', ewDragCoords.x - 50, ewDragCoords.y, {deltaY: -20, element: ewDrag}); - }) - .then(delay(constants.REDRAWDELAY + 10)) + .then(doScroll('xy', 'ew', -20, {dx: -50})) .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0, 2.2103]}, 'x scroll')) - .then(function() { - var nsDrag = getDragger('xy', 'ns'); - var nsDragCoords = getNodeCoords(nsDrag); - mouseEvent('scroll', nsDragCoords.x, nsDragCoords.y - 50, {deltaY: -20, element: nsDrag}); - }) - .then(delay(constants.REDRAWDELAY + 10)) + .then(doScroll('xy', 'ns', -20, {dy: -50})) .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0.1578, 2.1578]}, 'y scroll')) .catch(failTest) .then(done); @@ -512,20 +510,9 @@ describe('axis zoom/pan and main plot zoom', function() { .then(doDblClick('xy', 'nsew')) .then(checkRanges({}, 'dblclick xy #3')) // scroll wheel - .then(function() { - var mainDrag = getDragger('xy', 'nsew'); - var mainDragCoords = getNodeCoords(mainDrag, 'se'); - mouseEvent('scroll', mainDragCoords.x, mainDragCoords.y, {deltaY: 20, element: mainDrag}); - }) - .then(delay(constants.REDRAWDELAY + 10)) - .then(checkRanges({xaxis: [-0.2103, 2], yaxis: [0, 2.2103], xaxis2: [-0.1052, 2.1052], yaxis2: [-0.1052, 2.1052]}, - 'scroll xy')) - .then(function() { - var ewDrag = getDragger('xy', 'ew'); - var ewDragCoords = getNodeCoords(ewDrag); - mouseEvent('scroll', ewDragCoords.x - 50, ewDragCoords.y, {deltaY: -20, element: ewDrag}); - }) - .then(delay(constants.REDRAWDELAY + 10)) + .then(doScroll('xy', 'nsew', 20, {edge: 'se'})) + .then(checkRanges({xaxis: [-0.2103, 2], yaxis: [0, 2.2103], xaxis2: [-0.1052, 2.1052], yaxis2: [-0.1052, 2.1052]}, 'scroll xy')) + .then(doScroll('xy', 'ew', -20, {dx: -50})) .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0.1052, 2.1052]}, 'scroll x')) .catch(failTest) .then(done); From e962e96ac31cf8329026e701399efef52fc3793f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Jan 2019 13:58:46 -0500 Subject: [PATCH 06/23] mv dragbox tests calling makePlot to own describe block --- test/jasmine/tests/cartesian_interact_test.js | 391 +++++++++--------- 1 file changed, 196 insertions(+), 195 deletions(-) diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 6c36875849d..75fae4e8276 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -283,76 +283,6 @@ describe('axis zoom/pan and main plot zoom', function() { afterEach(destroyGraphDiv); - var initialRange = [0, 2]; - var autoRange = [-0.1594, 2.1594]; - - function makePlot(constrainScales, layoutEdits) { - // mock with 4 subplots, 3 of which share some axes: - // - // | | - // y2| xy2 y3| x3y3 - // | | - // +--------- +---------- - // x3 - // | | - // y| xy | x2y - // | | - // +--------- +---------- - // x x2 - // - // each subplot is 200x200 px - // if constrainScales is used, x/x2/y/y2 are linked, as are x3/y3 - // layoutEdits are other changes to make to the layout - - var data = [ - {y: [0, 1, 2]}, - {y: [0, 1, 2], xaxis: 'x2'}, - {y: [0, 1, 2], yaxis: 'y2'}, - {y: [0, 1, 2], xaxis: 'x3', yaxis: 'y3'} - ]; - - var layout = { - width: 700, - height: 620, - margin: {l: 100, r: 100, t: 20, b: 100}, - showlegend: false, - xaxis: {domain: [0, 0.4], range: [0, 2]}, - yaxis: {domain: [0.15, 0.55], range: [0, 2]}, - xaxis2: {domain: [0.6, 1], range: [0, 2]}, - yaxis2: {domain: [0.6, 1], range: [0, 2]}, - xaxis3: {domain: [0.6, 1], range: [0, 2], anchor: 'y3'}, - yaxis3: {domain: [0.6, 1], range: [0, 2], anchor: 'x3'} - }; - - var config = {scrollZoom: true}; - - if(constrainScales) { - layout.yaxis.scaleanchor = 'x'; - layout.yaxis2.scaleanchor = 'x'; - layout.xaxis2.scaleanchor = 'y'; - layout.yaxis3.scaleanchor = 'x3'; - } - - if(layoutEdits) Lib.extendDeep(layout, layoutEdits); - - return Plotly.newPlot(gd, data, layout, config) - .then(checkRanges({}, 'initial')) - .then(function() { - expect(Object.keys(gd._fullLayout._plots).sort()) - .toEqual(['xy', 'xy2', 'x2y', 'x3y3'].sort()); - - // nsew, n, ns, s, w, ew, e, ne, nw, se, sw - expect(document.querySelectorAll('.drag[data-subplot="xy"]').length).toBe(11); - // same but no w, ew, e because x is on xy only - expect(document.querySelectorAll('.drag[data-subplot="xy2"]').length).toBe(8); - // y is on xy only so no n, ns, s - expect(document.querySelectorAll('.drag[data-subplot="x2y"]').length).toBe(8); - // all 11, as this is a fully independent subplot - expect(document.querySelectorAll('.drag[data-subplot="x3y3"]').length).toBe(11); - }); - - } - function getDragger(subplot, directions) { return document.querySelector('.' + directions + 'drag[data-subplot="' + subplot + '"]'); } @@ -384,138 +314,209 @@ describe('axis zoom/pan and main plot zoom', function() { }; } - function checkRanges(newRanges, msg) { - msg = msg || ''; - if(msg) msg = ' - ' + msg; - - return function() { - var allRanges = { - xaxis: initialRange.slice(), - yaxis: initialRange.slice(), - xaxis2: initialRange.slice(), - yaxis2: initialRange.slice(), - xaxis3: initialRange.slice(), - yaxis3: initialRange.slice() + describe('subplots with shared axes', function() { + var initialRange = [0, 2]; + var autoRange = [-0.1594, 2.1594]; + + function makePlot(constrainScales, layoutEdits) { + // mock with 4 subplots, 3 of which share some axes: + // + // | | + // y2| xy2 y3| x3y3 + // | | + // +--------- +---------- + // x3 + // | | + // y| xy | x2y + // | | + // +--------- +---------- + // x x2 + // + // each subplot is 200x200 px + // if constrainScales is used, x/x2/y/y2 are linked, as are x3/y3 + // layoutEdits are other changes to make to the layout + + var data = [ + {y: [0, 1, 2]}, + {y: [0, 1, 2], xaxis: 'x2'}, + {y: [0, 1, 2], yaxis: 'y2'}, + {y: [0, 1, 2], xaxis: 'x3', yaxis: 'y3'} + ]; + + var layout = { + width: 700, + height: 620, + margin: {l: 100, r: 100, t: 20, b: 100}, + showlegend: false, + xaxis: {domain: [0, 0.4], range: [0, 2]}, + yaxis: {domain: [0.15, 0.55], range: [0, 2]}, + xaxis2: {domain: [0.6, 1], range: [0, 2]}, + yaxis2: {domain: [0.6, 1], range: [0, 2]}, + xaxis3: {domain: [0.6, 1], range: [0, 2], anchor: 'y3'}, + yaxis3: {domain: [0.6, 1], range: [0, 2], anchor: 'x3'} }; - Lib.extendDeep(allRanges, newRanges); - for(var axName in allRanges) { - expect(gd.layout[axName].range).toBeCloseToArray(allRanges[axName], 3, axName + msg); - expect(gd._fullLayout[axName].range).toBeCloseToArray(gd.layout[axName].range, 6, axName + msg); + var config = {scrollZoom: true}; + + if(constrainScales) { + layout.yaxis.scaleanchor = 'x'; + layout.yaxis2.scaleanchor = 'x'; + layout.xaxis2.scaleanchor = 'y'; + layout.yaxis3.scaleanchor = 'x3'; } - }; - } - it('updates with correlated subplots & no constraints - zoom, dblclick, axis ends', function(done) { - makePlot() - // zoombox into a small point - drag starts from the center unless you specify otherwise - .then(doDrag('xy', 'nsew', 100, -50)) - .then(checkRanges({xaxis: [1, 2], yaxis: [1, 1.5]}, 'zoombox')) + if(layoutEdits) Lib.extendDeep(layout, layoutEdits); + + return Plotly.newPlot(gd, data, layout, config) + .then(checkRanges({}, 'initial')) + .then(function() { + expect(Object.keys(gd._fullLayout._plots).sort()) + .toEqual(['xy', 'xy2', 'x2y', 'x3y3'].sort()); + + // nsew, n, ns, s, w, ew, e, ne, nw, se, sw + expect(document.querySelectorAll('.drag[data-subplot="xy"]').length).toBe(11); + // same but no w, ew, e because x is on xy only + expect(document.querySelectorAll('.drag[data-subplot="xy2"]').length).toBe(8); + // y is on xy only so no n, ns, s + expect(document.querySelectorAll('.drag[data-subplot="x2y"]').length).toBe(8); + // all 11, as this is a fully independent subplot + expect(document.querySelectorAll('.drag[data-subplot="x3y3"]').length).toBe(11); + }); + } - // first dblclick reverts to saved ranges - .then(doDblClick('xy', 'nsew')) - .then(checkRanges({}, 'dblclick #1')) - // next dblclick autoscales (just that plot) - .then(doDblClick('xy', 'nsew')) - .then(checkRanges({xaxis: autoRange, yaxis: autoRange}, 'dblclick #2')) - // dblclick on one axis reverts just that axis to saved - .then(doDblClick('xy', 'ns')) - .then(checkRanges({xaxis: autoRange}, 'dblclick y')) - // dblclick the plot at this point (one axis default, the other autoscaled) - // and the whole thing is reverted to default - .then(doDblClick('xy', 'nsew')) - .then(checkRanges({}, 'dblclick #3')) - - // 1D zoombox - use the linked subplots - .then(doDrag('xy2', 'nsew', -100, 0)) - .then(checkRanges({xaxis: [0, 1]}, 'xy2 zoombox')) - .then(doDrag('x2y', 'nsew', 0, 50)) - .then(checkRanges({xaxis: [0, 1], yaxis: [0.5, 1]}, 'x2y zoombox')) - // dblclick on linked subplots just changes the linked axis - .then(doDblClick('xy2', 'nsew')) - .then(checkRanges({yaxis: [0.5, 1]}, 'dblclick xy2')) - .then(doDblClick('x2y', 'nsew')) - .then(checkRanges({}, 'dblclick x2y')) - // drag on axis ends - all these 1D draggers the opposite axis delta is irrelevant - .then(doDrag('xy2', 'n', 53, 100)) - .then(checkRanges({yaxis2: [0, 4]}, 'drag y2n')) - .then(doDrag('xy', 's', 53, -100)) - .then(checkRanges({yaxis: [-2, 2], yaxis2: [0, 4]}, 'drag ys')) - // expanding drag is highly nonlinear - .then(doDrag('x2y', 'e', 50, 53)) - .then(checkRanges({yaxis: [-2, 2], yaxis2: [0, 4], xaxis2: [0, 0.8751]}, 'drag x2e')) - .then(doDrag('x2y', 'w', -50, 53)) - .then(checkRanges({yaxis: [-2, 2], yaxis2: [0, 4], xaxis2: [0.4922, 0.8751]}, 'drag x2w')) - // reset all from the modebar - .then(function() { selectButton(gd._fullLayout._modeBar, 'resetScale2d').click(); }) - .then(checkRanges({}, 'final reset')) - .catch(failTest) - .then(done); - }); + function checkRanges(newRanges, msg) { + msg = msg || ''; + if(msg) msg = ' - ' + msg; + + return function() { + var allRanges = { + xaxis: initialRange.slice(), + yaxis: initialRange.slice(), + xaxis2: initialRange.slice(), + yaxis2: initialRange.slice(), + xaxis3: initialRange.slice(), + yaxis3: initialRange.slice() + }; + Lib.extendDeep(allRanges, newRanges); + + for(var axName in allRanges) { + expect(gd.layout[axName].range).toBeCloseToArray(allRanges[axName], 3, axName + msg); + expect(gd._fullLayout[axName].range).toBeCloseToArray(gd.layout[axName].range, 6, axName + msg); + } + }; + } - it('updates with correlated subplots & no constraints - middles, corners, and scrollwheel', function(done) { - makePlot() - // drag axis middles - .then(doDrag('x3y3', 'ew', 100, 0)) - .then(checkRanges({xaxis3: [-1, 1]}, 'drag x3ew')) - .then(doDrag('x3y3', 'ns', 53, 100)) - .then(checkRanges({xaxis3: [-1, 1], yaxis3: [1, 3]}, 'drag y3ns')) - // drag corners - .then(doDrag('x3y3', 'ne', -100, 100)) - .then(checkRanges({xaxis3: [-1, 3], yaxis3: [1, 5]}, 'zoom x3y3ne')) - .then(doDrag('x3y3', 'sw', 100, -100)) - .then(checkRanges({xaxis3: [-5, 3], yaxis3: [-3, 5]}, 'zoom x3y3sw')) - .then(doDrag('x3y3', 'nw', -50, -50)) - .then(checkRanges({xaxis3: [-0.5006, 3], yaxis3: [-3, 0.5006]}, 'zoom x3y3nw')) - .then(doDrag('x3y3', 'se', 50, 50)) - .then(checkRanges({xaxis3: [-0.5006, 1.0312], yaxis3: [-1.0312, 0.5006]}, 'zoom x3y3se')) - .then(doDblClick('x3y3', 'nsew')) - .then(checkRanges({}, 'reset x3y3')) - // scroll wheel - .then(doScroll('xy', 'nsew', 20, {edge: 'se'})) - .then(checkRanges({xaxis: [-0.2103, 2], yaxis: [0, 2.2103]}, 'xy main scroll')) - .then(doScroll('xy', 'ew', -20, {dx: -50})) - .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0, 2.2103]}, 'x scroll')) - .then(doScroll('xy', 'ns', -20, {dy: -50})) - .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0.1578, 2.1578]}, 'y scroll')) - .catch(failTest) - .then(done); - }); + it('updates with correlated subplots & no constraints - zoom, dblclick, axis ends', function(done) { + makePlot() + // zoombox into a small point - drag starts from the center unless you specify otherwise + .then(doDrag('xy', 'nsew', 100, -50)) + .then(checkRanges({xaxis: [1, 2], yaxis: [1, 1.5]}, 'zoombox')) + + // first dblclick reverts to saved ranges + .then(doDblClick('xy', 'nsew')) + .then(checkRanges({}, 'dblclick #1')) + // next dblclick autoscales (just that plot) + .then(doDblClick('xy', 'nsew')) + .then(checkRanges({xaxis: autoRange, yaxis: autoRange}, 'dblclick #2')) + // dblclick on one axis reverts just that axis to saved + .then(doDblClick('xy', 'ns')) + .then(checkRanges({xaxis: autoRange}, 'dblclick y')) + // dblclick the plot at this point (one axis default, the other autoscaled) + // and the whole thing is reverted to default + .then(doDblClick('xy', 'nsew')) + .then(checkRanges({}, 'dblclick #3')) + + // 1D zoombox - use the linked subplots + .then(doDrag('xy2', 'nsew', -100, 0)) + .then(checkRanges({xaxis: [0, 1]}, 'xy2 zoombox')) + .then(doDrag('x2y', 'nsew', 0, 50)) + .then(checkRanges({xaxis: [0, 1], yaxis: [0.5, 1]}, 'x2y zoombox')) + // dblclick on linked subplots just changes the linked axis + .then(doDblClick('xy2', 'nsew')) + .then(checkRanges({yaxis: [0.5, 1]}, 'dblclick xy2')) + .then(doDblClick('x2y', 'nsew')) + .then(checkRanges({}, 'dblclick x2y')) + // drag on axis ends - all these 1D draggers the opposite axis delta is irrelevant + .then(doDrag('xy2', 'n', 53, 100)) + .then(checkRanges({yaxis2: [0, 4]}, 'drag y2n')) + .then(doDrag('xy', 's', 53, -100)) + .then(checkRanges({yaxis: [-2, 2], yaxis2: [0, 4]}, 'drag ys')) + // expanding drag is highly nonlinear + .then(doDrag('x2y', 'e', 50, 53)) + .then(checkRanges({yaxis: [-2, 2], yaxis2: [0, 4], xaxis2: [0, 0.8751]}, 'drag x2e')) + .then(doDrag('x2y', 'w', -50, 53)) + .then(checkRanges({yaxis: [-2, 2], yaxis2: [0, 4], xaxis2: [0.4922, 0.8751]}, 'drag x2w')) + // reset all from the modebar + .then(function() { selectButton(gd._fullLayout._modeBar, 'resetScale2d').click(); }) + .then(checkRanges({}, 'final reset')) + .catch(failTest) + .then(done); + }); - it('updates linked axes when there are constraints', function(done) { - makePlot(true) - // zoombox - this *would* be 1D (dy=-1) but that's not allowed - .then(doDrag('xy', 'nsew', 100, -1)) - .then(checkRanges({xaxis: [1, 2], yaxis: [1, 2], xaxis2: [0.5, 1.5], yaxis2: [0.5, 1.5]}, 'zoombox xy')) - // first dblclick reverts to saved ranges - .then(doDblClick('xy', 'nsew')) - .then(checkRanges({}, 'dblclick xy')) - // next dblclick autoscales ALL linked plots - .then(doDblClick('xy', 'ns')) - .then(checkRanges({xaxis: autoRange, yaxis: autoRange, xaxis2: autoRange, yaxis2: autoRange}, 'dblclick y')) - // revert again - .then(doDblClick('xy', 'nsew')) - .then(checkRanges({}, 'dblclick xy #2')) - // corner drag - full distance in one direction and no shift in the other gets averaged - // into half distance in each - .then(doDrag('xy', 'ne', -200, 0)) - .then(checkRanges({xaxis: [0, 4], yaxis: [0, 4], xaxis2: [-1, 3], yaxis2: [-1, 3]}, 'zoom xy ne')) - // drag one end - .then(doDrag('xy', 's', 53, -100)) - .then(checkRanges({xaxis: [-2, 6], yaxis: [-4, 4], xaxis2: [-3, 5], yaxis2: [-3, 5]}, 'zoom y s')) - // middle of an axis - .then(doDrag('xy', 'ew', -100, 53)) - .then(checkRanges({xaxis: [2, 10], yaxis: [-4, 4], xaxis2: [-3, 5], yaxis2: [-3, 5]}, 'drag x ew')) - // revert again - .then(doDblClick('xy', 'nsew')) - .then(checkRanges({}, 'dblclick xy #3')) - // scroll wheel - .then(doScroll('xy', 'nsew', 20, {edge: 'se'})) - .then(checkRanges({xaxis: [-0.2103, 2], yaxis: [0, 2.2103], xaxis2: [-0.1052, 2.1052], yaxis2: [-0.1052, 2.1052]}, 'scroll xy')) - .then(doScroll('xy', 'ew', -20, {dx: -50})) - .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0.1052, 2.1052]}, 'scroll x')) - .catch(failTest) - .then(done); + it('updates with correlated subplots & no constraints - middles, corners, and scrollwheel', function(done) { + makePlot() + // drag axis middles + .then(doDrag('x3y3', 'ew', 100, 0)) + .then(checkRanges({xaxis3: [-1, 1]}, 'drag x3ew')) + .then(doDrag('x3y3', 'ns', 53, 100)) + .then(checkRanges({xaxis3: [-1, 1], yaxis3: [1, 3]}, 'drag y3ns')) + // drag corners + .then(doDrag('x3y3', 'ne', -100, 100)) + .then(checkRanges({xaxis3: [-1, 3], yaxis3: [1, 5]}, 'zoom x3y3ne')) + .then(doDrag('x3y3', 'sw', 100, -100)) + .then(checkRanges({xaxis3: [-5, 3], yaxis3: [-3, 5]}, 'zoom x3y3sw')) + .then(doDrag('x3y3', 'nw', -50, -50)) + .then(checkRanges({xaxis3: [-0.5006, 3], yaxis3: [-3, 0.5006]}, 'zoom x3y3nw')) + .then(doDrag('x3y3', 'se', 50, 50)) + .then(checkRanges({xaxis3: [-0.5006, 1.0312], yaxis3: [-1.0312, 0.5006]}, 'zoom x3y3se')) + .then(doDblClick('x3y3', 'nsew')) + .then(checkRanges({}, 'reset x3y3')) + // scroll wheel + .then(doScroll('xy', 'nsew', 20, {edge: 'se'})) + .then(checkRanges({xaxis: [-0.2103, 2], yaxis: [0, 2.2103]}, 'xy main scroll')) + .then(doScroll('xy', 'ew', -20, {dx: -50})) + .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0, 2.2103]}, 'x scroll')) + .then(doScroll('xy', 'ns', -20, {dy: -50})) + .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0.1578, 2.1578]}, 'y scroll')) + .catch(failTest) + .then(done); + }); + + it('updates linked axes when there are constraints', function(done) { + makePlot(true) + // zoombox - this *would* be 1D (dy=-1) but that's not allowed + .then(doDrag('xy', 'nsew', 100, -1)) + .then(checkRanges({xaxis: [1, 2], yaxis: [1, 2], xaxis2: [0.5, 1.5], yaxis2: [0.5, 1.5]}, 'zoombox xy')) + // first dblclick reverts to saved ranges + .then(doDblClick('xy', 'nsew')) + .then(checkRanges({}, 'dblclick xy')) + // next dblclick autoscales ALL linked plots + .then(doDblClick('xy', 'ns')) + .then(checkRanges({xaxis: autoRange, yaxis: autoRange, xaxis2: autoRange, yaxis2: autoRange}, 'dblclick y')) + // revert again + .then(doDblClick('xy', 'nsew')) + .then(checkRanges({}, 'dblclick xy #2')) + // corner drag - full distance in one direction and no shift in the other gets averaged + // into half distance in each + .then(doDrag('xy', 'ne', -200, 0)) + .then(checkRanges({xaxis: [0, 4], yaxis: [0, 4], xaxis2: [-1, 3], yaxis2: [-1, 3]}, 'zoom xy ne')) + // drag one end + .then(doDrag('xy', 's', 53, -100)) + .then(checkRanges({xaxis: [-2, 6], yaxis: [-4, 4], xaxis2: [-3, 5], yaxis2: [-3, 5]}, 'zoom y s')) + // middle of an axis + .then(doDrag('xy', 'ew', -100, 53)) + .then(checkRanges({xaxis: [2, 10], yaxis: [-4, 4], xaxis2: [-3, 5], yaxis2: [-3, 5]}, 'drag x ew')) + // revert again + .then(doDblClick('xy', 'nsew')) + .then(checkRanges({}, 'dblclick xy #3')) + // scroll wheel + .then(doScroll('xy', 'nsew', 20, {edge: 'se'})) + .then(checkRanges({xaxis: [-0.2103, 2], yaxis: [0, 2.2103], xaxis2: [-0.1052, 2.1052], yaxis2: [-0.1052, 2.1052]}, 'scroll xy')) + .then(doScroll('xy', 'ew', -20, {dx: -50})) + .then(checkRanges({xaxis: [-0.1578, 1.8422], yaxis: [0.1052, 2.1052]}, 'scroll x')) + .catch(failTest) + .then(done); + }); }); it('updates linked axes when there are constraints (axes_scaleanchor mock)', function(done) { From ca1de5db42324c813caa0d03b56d25d4a451310f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Jan 2019 14:18:39 -0500 Subject: [PATCH 07/23] DRY-up drag-start/assert/drag-end tests --- test/jasmine/assets/drag.js | 69 ++++++++---- test/jasmine/tests/cartesian_interact_test.js | 105 ++++++------------ test/jasmine/tests/polar_test.js | 76 ++++--------- 3 files changed, 108 insertions(+), 142 deletions(-) diff --git a/test/jasmine/assets/drag.js b/test/jasmine/assets/drag.js index 64721d36151..6777ceb2204 100644 --- a/test/jasmine/assets/drag.js +++ b/test/jasmine/assets/drag.js @@ -2,37 +2,63 @@ var isNumeric = require('fast-isnumeric'); var mouseEvent = require('./mouse_event'); var getNodeCoords = require('./get_node_coords'); -/* - * drag: grab a node and drag it (dx, dy) pixels - * optionally specify an edge ('n', 'se', 'w' etc) - * to grab it by an edge or corner (otherwise the middle is used) - */ -function drag(node, dx, dy, edge, x0, y0, nsteps, noCover) { - nsteps = nsteps || 1; +function makeFns(node, dx, dy, opts) { + opts = opts || {}; + + var nsteps = opts.nsteps || 1; + var edge = opts.edge || ''; + var noCover = Boolean(opts.noCover); var coords = getNodeCoords(node, edge); - var fromX = isNumeric(x0) ? x0 : coords.x; - var fromY = isNumeric(y0) ? y0 : coords.y; + var fromX = isNumeric(opts.x0) ? opts.x0 : coords.x; + var fromY = isNumeric(opts.y0) ? opts.y0 : coords.y; - mouseEvent('mousemove', fromX, fromY, {element: node}); - mouseEvent('mousedown', fromX, fromY, {element: node}); + var dragCoverNode; + var toX; + var toY; - var promise = (noCover ? Promise.resolve(node) : waitForDragCover()) - .then(function(dragCoverNode) { - var toX; - var toY; + function start() { + mouseEvent('mousemove', fromX, fromY, {element: node}); + mouseEvent('mousedown', fromX, fromY, {element: node}); - for(var i = 1; i <= nsteps; i++) { - toX = fromX + i * dx / nsteps; - toY = fromY + i * dy / nsteps; - mouseEvent('mousemove', toX, toY, {element: dragCoverNode}); - } + return (noCover ? Promise.resolve(node) : waitForDragCover()) + .then(function(_dragCoverNode) { + dragCoverNode = _dragCoverNode; + + for(var i = 1; i <= nsteps; i++) { + toX = fromX + i * dx / nsteps; + toY = fromY + i * dy / nsteps; + mouseEvent('mousemove', toX, toY, {element: dragCoverNode}); + } + }); + } + function end() { mouseEvent('mouseup', toX, toY, {element: dragCoverNode}); return noCover || waitForDragCoverRemoval(); + } + + return { + start: start, + end: end + }; +} + +/* + * drag: grab a node and drag it (dx, dy) pixels + * optionally specify an edge ('n', 'se', 'w' etc) + * to grab it by an edge or corner (otherwise the middle is used) + */ +function drag(node, dx, dy, edge, x0, y0, nsteps, noCover) { + var fns = makeFns(node, dx, dy, { + edge: edge, + x0: x0, + y0: y0, + nsteps: nsteps, + noCover: noCover }); - return promise; + return fns.start().then(fns.end); } function waitForDragCover() { @@ -78,5 +104,6 @@ function waitForDragCoverRemoval() { } module.exports = drag; +drag.makeFns = makeFns; drag.waitForDragCover = waitForDragCover; drag.waitForDragCoverRemoval = waitForDragCoverRemoval; diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 75fae4e8276..cdcb7bd2ded 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -314,6 +314,11 @@ describe('axis zoom/pan and main plot zoom', function() { }; } + function makeDragFns(subplot, directions, dx, dy, x0, y0) { + var dragger = getDragger(subplot, directions); + return drag.makeFns(dragger, dx, dy, {x0: x0, y0: y0}); + } + describe('subplots with shared axes', function() { var initialRange = [0, 2]; var autoRange = [-0.1594, 2.1594]; @@ -577,67 +582,46 @@ describe('axis zoom/pan and main plot zoom', function() { }); it('should draw correct zoomboxes corners', function(done) { - var dragCoverNode; - var p1; - - function _dragStart(p0, dp) { - var node = getDragger('xy', 'nsew'); - mouseEvent('mousemove', p0[0], p0[1], {element: node}); - mouseEvent('mousedown', p0[0], p0[1], {element: node}); - - var promise = drag.waitForDragCover().then(function(dcn) { - dragCoverNode = dcn; - p1 = [p0[0] + dp[0], p0[1] + dp[1]]; - mouseEvent('mousemove', p1[0], p1[1], {element: dragCoverNode}); - }); - return promise; - } - - function _assertAndDragEnd(msg, exp) { - var zl = d3.select(gd).select('g.zoomlayer'); - var d = zl.select('.zoombox-corners').attr('d'); - - if(exp.cornerCnt) { - var actual = (d.match(/Z/g) || []).length; - expect(actual).toBe(exp.cornerCnt, 'zoombox corner cnt: ' + msg); - } else { - expect(d).toBe('M0,0Z', 'no zoombox corners: ' + msg); - } - - mouseEvent('mouseup', p1[0], p1[1], {element: dragCoverNode}); - return drag.waitForDragCoverRemoval(); + function _run(msg, dp, exp) { + var drag = makeDragFns('xy', 'nsew', dp[0], dp[1], 170, 170); + + return drag.start().then(function() { + var zl = d3.select(gd).select('g.zoomlayer'); + var d = zl.select('.zoombox-corners').attr('d'); + + if(exp.cornerCnt) { + var actual = (d.match(/Z/g) || []).length; + expect(actual).toBe(exp.cornerCnt, 'zoombox corner cnt: ' + msg); + } else { + expect(d).toBe('M0,0Z', 'no zoombox corners: ' + msg); + } + }) + .then(drag.end); } Plotly.plot(gd, [{ y: [1, 2, 1] }]) - .then(function() { return _dragStart([170, 170], [30, 30]); }) .then(function() { - return _assertAndDragEnd('full-x full-y', {cornerCnt: 4}); + return _run('full-x full-y', [30, 30], {cornerCnt: 4}); }) - .then(function() { return _dragStart([170, 170], [5, 30]); }) .then(function() { - return _assertAndDragEnd('full-y', {cornerCnt: 2}); + return _run('full-y', [5, 30], {cornerCnt: 2}); }) - .then(function() { return _dragStart([170, 170], [30, 2]); }) .then(function() { - return _assertAndDragEnd('full-x', {cornerCnt: 2}); + return _run('full-x', [30, 2], {cornerCnt: 2}); }) .then(function() { return Plotly.relayout(gd, 'xaxis.fixedrange', true); }) - .then(function() { return _dragStart([170, 170], [30, 30]); }) .then(function() { - return _assertAndDragEnd('full-x full-y w/ fixed xaxis', {cornerCnt: 2}); + return _run('full-x full-y w/ fixed xaxis', [30, 30], {cornerCnt: 2}); }) - .then(function() { return _dragStart([170, 170], [30, 5]); }) .then(function() { - return _assertAndDragEnd('full-x w/ fixed xaxis', {cornerCnt: 0}); + return _run('full-x w/ fixed xaxis', [30, 5], {cornerCnt: 0}); }) .then(function() { return Plotly.relayout(gd, {'xaxis.fixedrange': false, 'yaxis.fixedrange': true}); }) - .then(function() { return _dragStart([170, 170], [30, 30]); }) .then(function() { - return _assertAndDragEnd('full-x full-y w/ fixed yaxis', {cornerCnt: 2}); + return _run('full-x full-y w/ fixed yaxis', [30, 30], {cornerCnt: 2}); }) - .then(function() { return _dragStart([170, 170], [5, 30]); }) .then(function() { - return _assertAndDragEnd('full-y w/ fixed yaxis', {cornerCnt: 0}); + return _run('full-y w/ fixed yaxis', [5, 30], {cornerCnt: 0}); }) .catch(failTest) .then(done); @@ -665,28 +649,6 @@ describe('axis zoom/pan and main plot zoom', function() { it('should compute correct multicategory tick label span during drag', function(done) { var fig = Lib.extendDeep({}, require('@mocks/multicategory.json')); - var dragCoverNode; - var p1; - - function _dragStart(draggerClassName, p0, dp) { - var node = getDragger('xy', draggerClassName); - mouseEvent('mousemove', p0[0], p0[1], {element: node}); - mouseEvent('mousedown', p0[0], p0[1], {element: node}); - - var promise = drag.waitForDragCover().then(function(dcn) { - dragCoverNode = dcn; - p1 = [p0[0] + dp[0], p0[1] + dp[1]]; - mouseEvent('mousemove', p1[0], p1[1], {element: dragCoverNode}); - }); - return promise; - } - - function _assertAndDragEnd(msg, exp) { - _assertLabels(msg, exp); - mouseEvent('mouseup', p1[0], p1[1], {element: dragCoverNode}); - return drag.waitForDragCoverRemoval(); - } - function _assertLabels(msg, exp) { var tickLabels = d3.select(gd).selectAll('.xtick > text'); expect(tickLabels.size()).toBe(exp.angle.length, msg + ' - # of tick labels'); @@ -708,6 +670,13 @@ describe('axis zoom/pan and main plot zoom', function() { }); } + function _run(msg, dp, exp) { + var drag = makeDragFns('xy', 'e', dp[0], dp[1], 585, 390); + return drag.start() + .then(function() { _assertLabels(msg, exp); }) + .then(drag.end); + } + Plotly.plot(gd, fig) .then(function() { _assertLabels('base', { @@ -715,16 +684,14 @@ describe('axis zoom/pan and main plot zoom', function() { y: [406, 406] }); }) - .then(function() { return _dragStart('edrag', [585, 390], [-340, 0]); }) .then(function() { - return _assertAndDragEnd('drag to wide-range -> rotates labels', { + return _run('drag to wide-range -> rotates labels', [-340, 0], { angle: [90, 90, 90, 90, 90, 90, 90], y: [430, 430] }); }) - .then(function() { return _dragStart('edrag', [585, 390], [100, 0]); }) .then(function() { - return _assertAndDragEnd('drag to narrow-range -> un-rotates labels', { + return _run('drag to narrow-range -> un-rotates labels', [100, 0], { angle: [0, 0, 0, 0, 0, 0, 0], y: [406, 406] }); diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js index 2d48c4afd1f..5e78ae92c12 100644 --- a/test/jasmine/tests/polar_test.js +++ b/test/jasmine/tests/polar_test.js @@ -1300,8 +1300,6 @@ describe('Test polar *gridshape linear* interactions', function() { it('should rotate all non-symmetrical layers on angular drag', function(done) { var evtCnt = 0; var evtData = {}; - var dragCoverNode; - var p1; var layersRotateFromZero = ['.plotbg > path', '.radial-grid']; var layersRotateFromRadialAxis = ['.radial-axis', '.radial-line > line']; @@ -1317,29 +1315,19 @@ describe('Test polar *gridshape linear* interactions', function() { } } - function _dragStart(p0, dp) { + function _run(msg, p0, dp, exp) { var node = d3.select('.polar > .draglayer > .angulardrag').node(); - mouseEvent('mousemove', p0[0], p0[1], {element: node}); - mouseEvent('mousedown', p0[0], p0[1], {element: node}); + var dragFns = drag.makeFns(node, dp[0], dp[1], {x0: p0[0], y0: p0[1]}); - var promise = drag.waitForDragCover().then(function(dcn) { - dragCoverNode = dcn; - p1 = [p0[0] + dp[0], p0[1] + dp[1]]; - mouseEvent('mousemove', p1[0], p1[1], {element: dragCoverNode}); - }); - return promise; - } - - function _assertAndDragEnd(msg, exp) { - layersRotateFromZero.forEach(function(q) { - _assertTransformRotate(msg, q, exp.fromZero); - }); - layersRotateFromRadialAxis.forEach(function(q) { - _assertTransformRotate(msg, q, exp.fromRadialAxis); - }); - - mouseEvent('mouseup', p1[0], p1[1], {element: dragCoverNode}); - return drag.waitForDragCoverRemoval(); + return dragFns.start().then(function() { + layersRotateFromZero.forEach(function(q) { + _assertTransformRotate(msg, q, exp.fromZero); + }); + layersRotateFromRadialAxis.forEach(function(q) { + _assertTransformRotate(msg, q, exp.fromRadialAxis); + }); + }) + .then(dragFns.end); } Plotly.plot(gd, [{ @@ -1370,9 +1358,8 @@ describe('Test polar *gridshape linear* interactions', function() { _assertTransformRotate('base', q, -90); }); }) - .then(function() { return _dragStart([150, 20], [30, 30]); }) .then(function() { - return _assertAndDragEnd('rotate clockwise', { + return _run('rotate clockwise', [150, 20], [30, 30], { fromZero: 7.2, fromRadialAxis: -82.8 }); @@ -1390,9 +1377,6 @@ describe('Test polar *gridshape linear* interactions', function() { }); it('should place zoombox handles at correct place on main drag', function(done) { - var dragCoverNode; - var p1; - // d attr to array of segment [x,y] function path2coords(path) { if(!path.size()) return [[]]; @@ -1407,29 +1391,19 @@ describe('Test polar *gridshape linear* interactions', function() { .reduce(function(a, b) { return a.concat(b); }); } - function _dragStart(p0, dp) { + function _run(msg, p0, dp, exp) { var node = d3.select('.polar > .draglayer > .maindrag').node(); - mouseEvent('mousemove', p0[0], p0[1], {element: node}); - mouseEvent('mousedown', p0[0], p0[1], {element: node}); - - var promise = drag.waitForDragCover().then(function(dcn) { - dragCoverNode = dcn; - p1 = [p0[0] + dp[0], p0[1] + dp[1]]; - mouseEvent('mousemove', p1[0], p1[1], {element: dragCoverNode}); - }); - return promise; - } + var dragFns = drag.makeFns(node, dp[0], dp[1], {x0: p0[0], y0: p0[1]}); - function _assertAndDragEnd(msg, exp) { - var zl = d3.select(gd).select('g.zoomlayer'); + return dragFns.start().then(function() { + var zl = d3.select(gd).select('g.zoomlayer'); - expect(path2coords(zl.select('.zoombox'))) - .toBeCloseTo2DArray(exp.zoombox, 2, msg + ' - zoombox'); - expect(path2coords(zl.select('.zoombox-corners'))) - .toBeCloseTo2DArray(exp.corners, 2, msg + ' - corners'); - - mouseEvent('mouseup', p1[0], p1[1], {element: dragCoverNode}); - return drag.waitForDragCoverRemoval(); + expect(path2coords(zl.select('.zoombox'))) + .toBeCloseTo2DArray(exp.zoombox, 2, msg + ' - zoombox'); + expect(path2coords(zl.select('.zoombox-corners'))) + .toBeCloseTo2DArray(exp.corners, 2, msg + ' - corners'); + }) + .then(dragFns.end); } Plotly.plot(gd, [{ @@ -1445,9 +1419,8 @@ describe('Test polar *gridshape linear* interactions', function() { height: 400, margin: {l: 50, t: 50, b: 50, r: 50} }) - .then(function() { return _dragStart([170, 170], [220, 220]); }) .then(function() { - _assertAndDragEnd('drag outward toward bottom right', { + return _run('drag outward toward bottom right', [170, 170], [220, 220], { zoombox: [ [-142.658, -46.353], [-88.167, 121.352], [88.167, 121.352], [142.658, -46.352], @@ -1470,9 +1443,8 @@ describe('Test polar *gridshape linear* interactions', function() { .then(function() { return Plotly.relayout(gd, 'polar.sector', [-90, 90]); }) - .then(function() { return _dragStart([200, 200], [200, 230]); }) .then(function() { - _assertAndDragEnd('half-sector, drag outward', { + return _run('half-sector, drag outward', [200, 200], [200, 230], { zoombox: [ [0, 121.352], [88.167, 121.352], [142.658, -46.352], [0, -150], From 3b314fe338031bbfcd5839553b70a7af2d3d242b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 1 Feb 2019 15:40:11 -0500 Subject: [PATCH 08/23] add toBeWithinArray custom jasmine matcher --- test/jasmine/assets/custom_matchers.js | 123 ++++++++++++++++--------- 1 file changed, 78 insertions(+), 45 deletions(-) diff --git a/test/jasmine/assets/custom_matchers.js b/test/jasmine/assets/custom_matchers.js index e77caf56e6a..f1d877d8d51 100644 --- a/test/jasmine/assets/custom_matchers.js +++ b/test/jasmine/assets/custom_matchers.js @@ -64,25 +64,11 @@ var matchers = { }; }, - // toBeCloseTo... but for arrays toBeCloseToArray: function() { return { compare: function(actual, expected, precision, msgExtra) { - precision = coercePosition(precision); - - var passed; - - if(Array.isArray(actual) && Array.isArray(expected)) { - var tested = actual.map(function(element, i) { - return isClose(element, expected[i], precision); - }); - - passed = ( - expected.length === actual.length && - tested.indexOf(false) < 0 - ); - } - else passed = false; + var testFn = makeIsCloseFn(coercePosition(precision)); + var passed = assertArray(actual, expected, testFn); var message = [ 'Expected', actual, 'to be close to', expected, msgExtra @@ -96,30 +82,11 @@ var matchers = { }; }, - // toBeCloseTo... but for 2D arrays toBeCloseTo2DArray: function() { return { compare: function(actual, expected, precision, msgExtra) { - precision = coercePosition(precision); - - var passed = true; - - if(expected.length !== actual.length) passed = false; - else { - for(var i = 0; i < expected.length; ++i) { - if(expected[i].length !== actual[i].length) { - passed = false; - break; - } - - for(var j = 0; j < expected[i].length; ++j) { - if(!isClose(actual[i][j], expected[i][j], precision)) { - passed = false; - break; - } - } - } - } + var testFn = makeIsCloseFn(coercePosition(precision)); + var passed = assert2DArray(actual, expected, testFn); var message = [ 'Expected', @@ -140,7 +107,29 @@ var matchers = { toBeWithin: function() { return { compare: function(actual, expected, tolerance, msgExtra) { - var passed = Math.abs(actual - expected) < tolerance; + var testFn = makeIsWithinFn(tolerance); + var passed = testFn(actual, expected); + + var message = [ + 'Expected', actual, + 'to be close to', expected, + 'within', tolerance, + msgExtra + ].join(' '); + + return { + pass: passed, + message: message + }; + } + }; + }, + + toBeWithinArray: function() { + return { + compare: function(actual, expected, tolerance, msgExtra) { + var testFn = makeIsWithinFn(tolerance); + var passed = assertArray(actual, expected, testFn); var message = [ 'Expected', actual, @@ -183,15 +172,59 @@ var matchers = { } }; -function isClose(actual, expected, precision) { - if(isNumeric(actual) && isNumeric(expected)) { - return Math.abs(actual - expected) < precision; +function assertArray(actual, expected, testFn) { + if(Array.isArray(actual) && Array.isArray(expected)) { + var tested = actual.map(function(element, i) { + return testFn(element, expected[i]); + }); + + return ( + expected.length === actual.length && + tested.indexOf(false) < 0 + ); } + return false; +} - return ( - actual === expected || - (isNaN(actual) && isNaN(expected)) - ); +function assert2DArray(actual, expected, testFn) { + if(expected.length !== actual.length) return false; + + for(var i = 0; i < expected.length; i++) { + if(expected[i].length !== actual[i].length) { + return false; + } + + for(var j = 0; j < expected[i].length; j++) { + if(!testFn(actual[i][j], expected[i][j])) { + return false; + } + } + } + return true; +} + +function makeIsCloseFn(precision) { + return function isClose(actual, expected) { + if(isNumeric(actual) && isNumeric(expected)) { + return Math.abs(actual - expected) < precision; + } + return ( + actual === expected || + (isNaN(actual) && isNaN(expected)) + ); + }; +} + +function makeIsWithinFn(tolerance) { + return function isWithin(actual, expected) { + if(isNumeric(actual) && isNumeric(expected)) { + return Math.abs(actual - expected) < tolerance; + } + return ( + actual === expected || + (isNaN(actual) && isNaN(expected)) + ); + }; } function coercePosition(precision) { From 6621419bc1c2c0701a5e0e5f2f4f153a0fde14ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 29 Jan 2019 18:29:26 -0500 Subject: [PATCH 09/23] add mucho matching axes dragbox tests --- test/jasmine/tests/cartesian_interact_test.js | 627 ++++++++++++++++++ 1 file changed, 627 insertions(+) diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index cdcb7bd2ded..195472a4238 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -2,6 +2,8 @@ var d3 = require('d3'); var Plotly = require('@lib/index'); var Lib = require('@src/lib'); +var Axes = require('@src/plots/cartesian/axes'); +var Drawing = require('@src/components/drawing'); var constants = require('@src/plots/cartesian/constants'); var createGraphDiv = require('../assets/create_graph_div'); @@ -699,6 +701,631 @@ describe('axis zoom/pan and main plot zoom', function() { .catch(failTest) .then(done); }); + + describe('updates matching axes', function() { + var TOL = 1.5; + var eventData; + + function assertRanges(msg, exp) { + exp.forEach(function(expi) { + var axNames = expi[0]; + var rng = expi[1]; + + axNames.forEach(function(n) { + var msgi = n + ' - ' + msg; + expect(gd.layout[n].range).toBeCloseToArray(rng, TOL, msgi + ' |input'); + expect(gd._fullLayout[n].range).toBeCloseToArray(rng, TOL, msgi + ' |full'); + }); + }); + } + + function assertEventData(msg, exp) { + if(eventData === null) { + return fail('plotly_relayout did not get triggered - ' + msg); + } + + exp.forEach(function(expi) { + var axNames = expi[0]; + var rng = expi[1]; + var opts = expi[2] || {}; + + axNames.forEach(function(n) { + var msgi = n + ' - ' + msg; + if(opts.autorange) { + expect(eventData[n + '.autorange']).toBe(true, 2, msgi + '|event data'); + } else if(!opts.noChange) { + expect(eventData[n + '.range[0]']).toBeCloseTo(rng[0], TOL, msgi + '|event data [0]'); + expect(eventData[n + '.range[1]']).toBeCloseTo(rng[1], TOL, msgi + '|event data [1]'); + } + }); + }); + + eventData = null; + } + + function assertAxesDrawCalls(msg, exp) { + var cnt = 0; + exp.forEach(function(expi) { + var axNames = expi[0]; + var opts = expi[2] || {}; + axNames.forEach(function() { + if(!opts.noChange) { + cnt++; + // called twice as many times on drag: + // - once per axis during mousemouve + // - once per raxis on mouseup + if(opts.dragged) cnt++; + } + }); + }); + + expect(Axes.drawOne).toHaveBeenCalledTimes(cnt); + Axes.drawOne.calls.reset(); + } + + function assertSubplotTranslateAndScale(msg, spIds, trans, scale) { + var gClips = d3.select(gd).select('g.clips'); + var uid = gd._fullLayout._uid; + var transActual = []; + var scaleActual = []; + var trans0 = []; + var scale1 = []; + + spIds.forEach(function(id) { + var rect = gClips.select('#clip' + uid + id + 'plot > rect'); + var t = Drawing.getTranslate(rect); + var s = Drawing.getScale(rect); + transActual.push(t.x, t.y); + scaleActual.push(s.x, s.y); + trans0.push(0, 0); + scale1.push(1, 1); + }); + + var transExp = trans ? trans : trans0; + var scaleExp = scale ? scale : scale1; + var msg1 = msg + ' [' + spIds.map(function(id) { return '..' + id; }).join(', ') + ']'; + expect(transActual).toBeWithinArray(transExp, 3, msg1 + ' clip translate'); + expect(scaleActual).toBeWithinArray(scaleExp, 3, msg1 + ' clip scale'); + } + + function _assert(msg, exp) { + return function() { + assertRanges(msg, exp); + assertEventData(msg, exp); + assertAxesDrawCalls(msg, exp); + }; + } + + function makePlot(data, layout, s) { + s = s || {}; + + var fig = {}; + fig.data = Lib.extendDeep([], data); + fig.layout = Lib.extendDeep({}, layout, {dragmode: s.dragmode}); + fig.config = {scrollZoom: true}; + + spyOn(Axes, 'drawOne').and.callThrough(); + eventData = null; + + return Plotly.plot(gd, fig).then(function() { + Axes.drawOne.calls.reset(); + gd.on('plotly_relayout', function(d) { eventData = d; }); + }); + } + + describe('no-constrained x-axes matching x-axes subplot case', function() { + var data = [ + { y: [1, 2, 1] }, + { y: [2, 1, 2, 3], xaxis: 'x2' }, + { y: [0, 1], xaxis: 'x3' } + ]; + + // N.B. ax._length are not equal here + var layout = { + xaxis: {domain: [0, 0.2]}, + xaxis2: {matches: 'x', domain: [0.3, 0.6]}, + xaxis3: {matches: 'x', domain: [0.65, 1]}, + yaxis: {}, + width: 800, + height: 500, + dragmode: 'zoom' + }; + + var xr0 = [-0.245, 3.245]; + var yr0 = [-0.211, 3.211]; + + var specs = [{ + desc: 'zoombox on xy', + drag: ['xy', 'nsew', 30, 30], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [1.494, 2.350]], + [['yaxis'], [1.179, 1.50]] + ], + dblclickSubplot: 'xy' + }, { + desc: 'x-only zoombox on xy', + drag: ['xy', 'nsew', 30, 0], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [1.494, 2.350]], + [['yaxis'], yr0, {noChange: true}] + ], + dblclickSubplot: 'x2y' + }, { + desc: 'y-only zoombox on xy', + drag: ['xy', 'nsew', 0, 30], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], xr0, {noChange: true}], + [['yaxis'], [1.179, 1.50]] + ], + dblclickSubplot: 'x3y' + }, { + desc: 'zoombox on x2y', + drag: ['x2y', 'nsew', 30, 30], + exp: [ + // N.B. slightly different range result + // due difference in ax._length + [['xaxis', 'xaxis2', 'xaxis3'], [1.492, 2.062]], + [['yaxis'], [1.179, 1.50]] + ], + dblclickSubplot: 'x3y' + }, { + desc: 'zoombox on x3y', + drag: ['x3y', 'nsew', 30, 30], + exp: [ + // Similarly here slightly different range result + // due difference in ax._length + [['xaxis', 'xaxis2', 'xaxis3'], [1.485, 1.974]], + [['yaxis'], [1.179, 1.50]] + ], + dblclickSubplot: 'xy' + }, { + desc: 'drag ew on x2y', + drag: ['x2y', 'ew', 30, 0], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.816, 2.675], {dragged: true}], + [['yaxis'], yr0, {noChange: true}] + ], + dblclickSubplot: 'x3y' + }, { + desc: 'drag ew on x3y', + drag: ['x3y', 'ew', 30, 0], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.734, 2.756], {dragged: true}], + [['yaxis'], yr0, {noChange: true}] + ], + dblclickSubplot: 'xy' + }, { + desc: 'drag e on xy', + drag: ['xy', 'e', 30, 30], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [xr0[0], 1.366], {dragged: true}], + [['yaxis'], yr0, {noChange: true}] + ], + dblclickSubplot: 'x3y' + }, { + desc: 'drag nw on x3y', + drag: ['xy', 'nw', 30, 30], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-1.379, 3.245], {dragged: true}], + [['yaxis'], [-0.211, 3.565], {dragged: true}] + ], + dblclickSubplot: 'x3y' + }, { + desc: 'panning on xy subplot', + dragmode: 'pan', + drag: ['xy', 'nsew', 30, 30], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-1.101, 2.390], {dragged: true}], + [['yaxis'], [0.109, 3.532], {dragged: true}] + ], + dblclickSubplot: 'x3y' + }, { + desc: 'panning on x2y subplot', + dragmode: 'pan', + drag: ['x2y', 'nsew', 30, 30], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.816, 2.675], {dragged: true}], + [['yaxis'], [0.109, 3.532], {dragged: true}] + ], + dblclickSubplot: 'x2y' + }, { + desc: 'panning on x3y subplot', + dragmode: 'pan', + drag: ['x3y', 'nsew', 30, 30], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.734, 2.756], {dragged: true}], + [['yaxis'], [0.109, 3.532], {dragged: true}] + ], + dblclickSubplot: 'xy' + }, { + desc: 'scrolling on x3y subplot', + scroll: ['x3y', 20], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.613, 3.245], {dragged: true}], + [['yaxis'], [-0.211, 3.571], {dragged: true}] + ], + dblclickSubplot: 'xy' + }, { + desc: 'scrolling on x2y subplot', + scroll: ['x2y', 20], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.613, 3.245], {dragged: true}], + [['yaxis'], [-0.211, 3.571], {dragged: true}] + ], + dblclickSubplot: 'xy' + }, { + desc: 'scrolling on xy subplot', + scroll: ['xy', 20], + exp: [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.613, 3.245], {dragged: true}], + [['yaxis'], [-0.211, 3.571], {dragged: true}] + ], + dblclickSubplot: 'x2y' + }]; + + specs.forEach(function(s) { + var msg = 'after ' + s.desc; + var msg2 = ['after dblclick on subplot', s.dblclickSubplot, msg].join(' '); + + it(s.desc, function(done) { + makePlot(data, layout, s).then(function() { + assertRanges('base', [ + [['xaxis', 'xaxis2', 'xaxis3'], xr0], + [['yaxis'], yr0] + ]); + }) + .then(function() { + if(s.scroll) { + return doScroll(s.scroll[0], 'nsew', s.scroll[1], {edge: 'se'})(); + } else { + return doDrag(s.drag[0], s.drag[1], s.drag[2], s.drag[3])(); + } + }) + .then(_assert(msg, s.exp)) + .then(doDblClick(s.dblclickSubplot, 'nsew')) + .then(_assert(msg2, [ + [['xaxis', 'xaxis2', 'xaxis3'], xr0, {autorange: true}], + [['yaxis'], yr0, {autorange: true}] + ])) + .catch(failTest) + .then(done); + }); + }); + }); + + describe('y-axes matching y-axes case', function() { + var data = [ + { y: [1, 2, 1] }, + { y: [2, 1, 2, 3], yaxis: 'y2' }, + { y: [0, 1], yaxis: 'y3' } + ]; + + // N.B. ax._length are not equal here + var layout = { + yaxis: {domain: [0, 0.2]}, + yaxis2: {matches: 'y', domain: [0.3, 0.6]}, + yaxis3: {matches: 'y2', domain: [0.65, 1]}, + width: 500, + height: 800, + dragmode: 'pan' + }; + + var xr0 = [-0.211, 3.211]; + var yr0 = [-0.077, 3.163]; + + var specs = [{ + desc: 'pan on xy', + drag: ['xy', 'nsew', 30, 30], + exp: [ + [['xaxis'], [-0.534, 2.888], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.706, 3.947], {dragged: true}], + ], + trans: [-30, -30, -30, -45, -30, -52.5] + }, { + desc: 'pan on xy2', + drag: ['xy2', 'nsew', 30, 30], + exp: [ + [['xaxis'], [-0.534, 2.888], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.444, 3.685], {dragged: true}], + ], + trans: [-30, -20, -30, -30, -30, -35] + }, { + desc: 'pan on xy3', + drag: ['xy3', 'nsew', 30, 30], + exp: [ + [['xaxis'], [-0.534, 2.888], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.370, 3.611], {dragged: true}], + ], + trans: [-30, -17.142, -30, -25.71, -30, -30] + }, { + desc: 'drag ns dragger on xy2', + drag: ['xy2', 'ns', 0, 30], + exp: [ + [['xaxis'], xr0, {noChange: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.444, 3.685], {dragged: true}], + ], + trans: [0, -20, 0, -30, 0, -35] + }, { + desc: 'drag n dragger on xy3', + drag: ['xy3', 'n', 0, 30], + exp: [ + [['xaxis'], xr0, {noChange: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [yr0[0], 3.683], {dragged: true}], + ], + trans: [0, -19.893, 0, -29.839, 0, -34.812], + scale: [1, 1.160, 1, 1.160, 1, 1.160] + }, { + desc: 'drag s dragger on xy', + drag: ['xy', 's', 0, 30], + exp: [ + [['xaxis'], xr0, {noChange: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [1.617, yr0[1]], {dragged: true}], + ], + trans: [0, 0, 0, 0, 0, 0], + scale: [1, 0.476, 1, 0.476, 1, 0.476] + }]; + + specs.forEach(function(s) { + var msg = 'after ' + s.desc; + + it(s.desc, function(done) { + makePlot(data, layout, s).then(function() { + assertRanges('base', [ + [['xaxis'], xr0], + [['yaxis', 'yaxis2', 'yaxis3'], yr0] + ]); + }) + .then(function() { + var drag = makeDragFns(s.drag[0], s.drag[1], s.drag[2], s.drag[3]); + return drag.start().then(function() { + assertSubplotTranslateAndScale(msg, ['xy', 'xy2', 'xy3'], s.trans, s.scale); + }) + .then(drag.end); + }) + .then(_assert(msg, s.exp)) + .catch(failTest) + .then(done); + }); + }); + }); + + describe('x <--> y axes matching case', function() { + /* + * y | y2 | + * | | + * m | | + * a | | + * t | | + * c | | + * h | | + * e | | + * s | | + * | | + * x2 ________________________ _________________________ + * + * x x2 matches y + */ + + var data = [ + { y: [1, 2, 1] }, + { y: [2, 3, -1, 5], xaxis: 'x2', yaxis: 'y2' }, + ]; + + var layout = { + xaxis: {domain: [0, 0.4]}, + xaxis2: {anchor: 'y2', domain: [0.6, 1], matches: 'y'}, + yaxis: {matches: 'x2'}, + yaxis2: {anchor: 'x2'}, + width: 700, + height: 500, + dragmode: 'pan' + }; + + var rm0 = [-0.237, 3.237]; + var rx0 = [-0.158, 2.158]; + var ry20 = [-1.422, 5.422]; + + var specs = [{ + desc: 'pan on xy subplot', + drag: ['xy', 'nsew', 30, 30], + exp: [ + [['yaxis', 'xaxis2'], [0.0886, 3.562], {dragged: true}], + [['xaxis'], [-0.496, 1.820], {dragged: true}], + [['yaxis2'], ry20, {noChange: true}], + ], + trans: [-30, -30, 19.275, 0] + }, { + desc: 'pan on x2y2 subplot', + drag: ['x2y2', 'nsew', 30, 30], + exp: [ + [['yaxis', 'xaxis2'], [-0.744, 2.730], {dragged: true}], + [['xaxis'], rx0, {noChange: true}], + [['yaxis2'], [-0.787, 6.064], {dragged: true}], + ], + trans: [0, 46.69, -30, -30] + }, { + desc: 'drag xy ns dragger', + drag: ['xy', 'ns', 0, 30], + exp: [ + [['yaxis', 'xaxis2'], [0.0886, 3.562], {dragged: true}], + [['xaxis'], rx0, {noChange: true}], + [['yaxis2'], ry20, {noChange: true}], + ], + trans: [0, -30, 19.275, 0] + }, { + desc: 'drag x2y2 ew dragger', + drag: ['x2y2', 'ew', 30, 0], + exp: [ + [['yaxis', 'xaxis2'], [-0.744, 2.730], {dragged: true}], + [['xaxis'], rx0, {noChange: true}], + [['yaxis2'], ry20, {noChange: true}], + ], + trans: [0, 46.692, -30, 0] + }, { + desc: 'drag xy n corner', + drag: ['xy', 'n', 0, 30], + exp: [ + [['yaxis', 'xaxis2'], [rm0[0], 3.596], {dragged: true}], + [['xaxis'], rx0, {noChange: true}], + [['yaxis2'], ry20, {noChange: true}], + ], + trans: [0, -33.103, 0, 0], + scale: [1, 1.103, 1.103, 1] + }, { + desc: 'drag xy s corner', + drag: ['xy', 's', 0, 30], + exp: [ + [['yaxis', 'xaxis2'], [0.174, rm0[1]], {dragged: true}], + [['xaxis'], rx0, {noChange: true}], + [['yaxis2'], ry20, {noChange: true}], + ], + trans: [0, 0, 24.367, 0], + scale: [1, 0.881, 0.881, 1] + }, { + desc: 'drag x2y2 e corner', + drag: ['x2y2', 'e', 30, 0], + exp: [ + [['yaxis', 'xaxis2'], [rm0[0], 2.486], {dragged: true}], + [['xaxis'], rx0, {noChange: true}], + [['yaxis2'], ry20, {noChange: true}], + ], + trans: [0, 69.094, 0, 0], + scale: [1, 0.784, 0.784, 1] + }, { + desc: 'drag x2y2 w corner', + drag: ['x2y2', 'w', 30, 0], + exp: [ + [['yaxis', 'xaxis2'], [-0.830, rm0[1]], {dragged: true}], + [['xaxis'], rx0, {noChange: true}], + [['yaxis2'], ry20, {noChange: true}], + ], + trans: [0, 0, -35.125, 0], + scale: [1, 1.170, 1.170, 1] + }]; + + specs.forEach(function(s) { + var msg = 'after ' + s.desc; + + it(s.desc, function(done) { + makePlot(data, layout, s).then(function() { + assertRanges('base', [ + [['yaxis', 'xaxis2'], rm0], + [['xaxis'], rx0], + [['yaxis2'], ry20], + ]); + }) + .then(function() { + var drag = makeDragFns(s.drag[0], s.drag[1], s.drag[2], s.drag[3]); + return drag.start().then(function() { + assertSubplotTranslateAndScale(msg, ['xy', 'x2y2'], s.trans, s.scale); + }) + .then(drag.end); + }) + .then(_assert(msg, s.exp)) + .catch(failTest) + .then(done); + }); + }); + }); + + describe('constrained subplot case', function() { + var data = [{ + type: 'splom', + // N.B. subplots xy and x2y2 are "constrained", + dimensions: [{ + values: [1, 2, 5, 3, 4], + label: 'A', + axis: {matches: true} + }, { + values: [2, 1, 0, 3, 4], + label: 'B', + axis: {matches: true} + }] + }]; + + var layout = {width: 500, height: 500}; + + var rng0 = {xy: [0.648, 5.351], x2y2: [-0.351, 4.351]}; + + var specs = [{ + desc: 'zoombox on constrained xy subplot', + drag: ['xy', 'nsew', 30, 30], + exp: [ + [['xaxis', 'yaxis'], [2.093, 3.860]], + [['xaxis2', 'yaxis2'], rng0.x2y2, {noChange: true}] + ], + zoombox: [60.509, 56.950], + dblclickSubplot: 'xy' + }, { + desc: 'zoombox on constrained x2y2 subplot', + drag: ['x2y2', 'nsew', 30, 30], + exp: [ + [['xaxis', 'yaxis'], rng0.xy, {noChange: true}], + [['xaxis2', 'yaxis2'], [1.075, 2.862]] + ], + zoombox: [61.177, 57.578], + dblclickSubplot: 'xy' + }, { + desc: 'drag ew on x2y2', + drag: ['x2y2', 'ew', 30, 0], + exp: [ + [['xaxis', 'yaxis'], rng0.xy, {noChange: true}], + [['xaxis2', 'yaxis2'], [-1.227, 3.475], {dragged: true}] + ], + dblclickSubplot: 'x2y2' + }, { + desc: 'scrolling on xy subplot', + scroll: ['xy', 20], + exp: [ + [['xaxis', 'yaxis'], [0.151, 5.896], {dragged: true}], + [['xaxis2', 'yaxis2'], rng0.x2y2, {noChange: true}] + ], + dblclickSubplot: 'x2y2' + }]; + + specs.forEach(function(s) { + var msg = 'after ' + s.desc; + var msg2 = ['after dblclick on subplot', s.dblclickSubplot, msg].join(' '); + var spmatch = s.dblclickSubplot.match(constants.SUBPLOT_PATTERN); + + it(s.desc, function(done) { + makePlot(data, layout, s).then(function() { + assertRanges('base', [ + [['xaxis', 'yaxis'], rng0.xy], + [['xaxis2', 'yaxis2'], rng0.x2y2] + ]); + }) + .then(function() { + if(s.scroll) { + return doScroll(s.scroll[0], 'nsew', s.scroll[1], {edge: 'se'})(); + } else { + var drag = makeDragFns(s.drag[0], s.drag[1], s.drag[2], s.drag[3]); + return drag.start().then(function() { + if(s.drag[1] === 'nsew') { + var zb = d3.select(gd).select('g.zoomlayer > path.zoombox'); + var d = zb.attr('d'); + var v = Number(d.split('v')[1].split('h')[0]); + var h = Number(d.split('h')[1].split('v')[0]); + expect(h).toBeCloseTo(s.zoombox[0], 1, 'zoombox horizontal span -' + msg); + expect(v).toBeCloseTo(s.zoombox[1], 1, 'zoombox vertical span -' + msg); + } + }) + .then(drag.end); + } + }) + .then(_assert(msg, s.exp)) + .then(doDblClick(s.dblclickSubplot, 'nsew')) + .then(_assert(msg2, [[ + ['xaxis' + spmatch[1], 'yaxis' + spmatch[2]], + rng0[s.dblclickSubplot], + {autorange: true} + ]])) + .catch(failTest) + .then(done); + }); + }); + }); + }); }); describe('Event data:', function() { From 91185051bfcce07edd5853c1bc4a894ecfa21031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 28 Jan 2019 16:05:22 -0500 Subject: [PATCH 10/23] align autobinning of histogram traces on matching axes --- src/traces/histogram/cross_trace_defaults.js | 21 +++-- .../baselines/hists-on-matching-axes.png | Bin 0 -> 23877 bytes test/image/mocks/hists-on-matching-axes.json | 84 ++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 test/image/baselines/hists-on-matching-axes.png create mode 100644 test/image/mocks/hists-on-matching-axes.json diff --git a/src/traces/histogram/cross_trace_defaults.js b/src/traces/histogram/cross_trace_defaults.js index 0a037c7a3ef..4df1814fc71 100644 --- a/src/traces/histogram/cross_trace_defaults.js +++ b/src/traces/histogram/cross_trace_defaults.js @@ -50,15 +50,16 @@ module.exports = function crossTraceDefaults(fullData, fullLayout) { binDirection = traceOut.orientation === 'v' ? 'x' : 'y'; // in overlay mode make a separate group for each trace // otherwise collect all traces of the same subplot & orientation - group = isOverlay ? traceOut.uid : (traceOut.xaxis + traceOut.yaxis + binDirection); - traceOut._groupName = group; - + group = traceOut._groupName = isOverlay ? traceOut.uid : ( + getAxisGroup(fullLayout, traceOut.xaxis) + + getAxisGroup(fullLayout, traceOut.yaxis) + + binDirection + ); binOpts = allBinOpts[group]; if(binOpts) { binOpts.traces.push(traceOut); - } - else { + } else { binOpts = allBinOpts[group] = { traces: [traceOut], direction: binDirection @@ -110,3 +111,13 @@ module.exports = function crossTraceDefaults(fullData, fullLayout) { } } }; + +function getAxisGroup(fullLayout, axId) { + var matchGroups = fullLayout._axisMatchGroups; + + for(var i = 0; i < matchGroups.length; i++) { + var group = matchGroups[i]; + if(group[axId]) return 'g' + i; + } + return axId; +} diff --git a/test/image/baselines/hists-on-matching-axes.png b/test/image/baselines/hists-on-matching-axes.png new file mode 100644 index 0000000000000000000000000000000000000000..9e208c4ae1ba6a244ab3d697543c936801b1f1d9 GIT binary patch literal 23877 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/hists-on-matching-axes.json b/test/image/mocks/hists-on-matching-axes.json new file mode 100644 index 00000000000..189712b4666 --- /dev/null +++ b/test/image/mocks/hists-on-matching-axes.json @@ -0,0 +1,84 @@ +{ + "data": [ + { + "type": "histogram", + "name": "sample A - matched", + "x": [1, 2, 3, 1, 1, 2, 3, 3], + "hoverlabel": { + "namelength": -1 + } + }, + { + "type": "histogram", + "name": "sample B - matched", + "x": [2.1, 2.1, 3.4, 1.3, 2.2, 2.1, 3.2, 4.1, 3.1], + "hoverlabel": { + "namelength": -1 + }, + "xaxis": "x2", + "yaxis": "y2" + }, + { + "type": "histogram", + "name": "sample A - not on matching axes", + "x": [1, 2, 3, 1, 1, 2, 3, 3], + "hoverlabel": { + "namelength": -1 + }, + "xaxis": "x3", + "yaxis": "y3" + }, + { + "type": "histogram", + "name": "sample B - not on matching axes", + "x": [2.1, 2.1, 3.4, 1.3, 2.2, 2.1, 3.2, 4.1, 3.1], + "hoverlabel": { + "namelength": -1 + }, + "xaxis": "x4", + "yaxis": "y4" + } + ], + "layout": { + "margin": {"t": 20, "b": 20}, + "showlegend": false, + "xaxis": { + "domain": [0, 0.4] + }, + "yaxis": { + "domain": [0.45, 1], + "title": { + "text": "Matched Axes" + } + }, + "xaxis2": { + "domain": [0.45, 1], + "anchor": "y2", + "matches": "x" + }, + "yaxis2": { + "domain": [0.45, 1], + "anchor": "x2", + "matches": "y" + }, + "xaxis3": { + "domain": [0, 0.4], + "anchor": "y3" + }, + "yaxis3": { + "domain": [0, 0.4], + "anchor": "x3", + "title": { + "text": "NOT Matched Axes" + } + }, + "xaxis4": { + "domain": [0.45, 1], + "anchor": "y4" + }, + "yaxis4": { + "domain": [0, 0.4], + "anchor": "x4" + } + } +} From d9c2d4e2cca5b3abeba8c4702ccd6d8b6b5ac806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 28 Jan 2019 18:35:23 -0500 Subject: [PATCH 11/23] link ax._categories & ax._categoriesMap to same ref for matching axes - that way, they are appends across matching axes and the category-to-position maps just works! --- src/plots/cartesian/set_convert.js | 69 +++++++++++--- test/image/baselines/matching-categories.png | Bin 0 -> 44170 bytes test/image/mocks/matching-categories.json | 92 +++++++++++++++++++ 3 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 test/image/baselines/matching-categories.png create mode 100644 test/image/mocks/matching-categories.json diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 3e40f35acd7..e22f2609728 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var d3 = require('d3'); @@ -63,7 +62,8 @@ function isValidCategory(v) { module.exports = function setConvert(ax, fullLayout) { fullLayout = fullLayout || {}; - var axLetter = (ax._id || 'x').charAt(0); + var axId = (ax._id || 'x'); + var axLetter = axId.charAt(0); function toLog(v, clip) { if(v > 0) return Math.log(v) / Math.LN10; @@ -307,10 +307,25 @@ module.exports = function setConvert(ax, fullLayout) { var traceIndices = ax._traceIndices; var i, j; + var matchGroups = fullLayout._axisMatchGroups; + if(matchGroups && matchGroups.length && ax._categories.length === 0) { + for(i = 0; i < matchGroups.length; i++) { + var group = matchGroups[i]; + if(group[axId]) { + for(var axId2 in group) { + if(axId2 !== axId) { + var ax2 = fullLayout[axisIds.id2name(axId2)]; + traceIndices = traceIndices.concat(ax2._traceIndices); + } + } + } + } + } + // [ [cnt, {$cat: index}], for 1,2 ] - var seen = ax._multicatSeen = [[0, {}], [0, {}]]; + var seen = [[0, {}], [0, {}]]; // [ [arrayIn[0][i], arrayIn[1][i]], for i .. N ] - var list = ax._multicatList = []; + var list = []; for(i = 0; i < traceIndices.length; i++) { var trace = fullData[traceIndices[i]]; @@ -558,15 +573,45 @@ module.exports = function setConvert(ax, fullLayout) { } }; + // should skip if not category nor multicategory ax.clearCalc = function() { - // 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(); - - // Build the lookup map for initialized categories - ax._categoriesMap = {}; - for(var j = 0; j < ax._categories.length; j++) { - ax._categoriesMap[ax._categories[j]] = j; + var matchGroups = fullLayout._axisMatchGroups; + + if(matchGroups && matchGroups.length) { + for(var i = 0; i < matchGroups.length; i++) { + var group = matchGroups[i]; + + if(group[axId]) { + var categories = null; + var categoriesMap = null; + + for(var axId2 in group) { + var ax2 = fullLayout[axisIds.id2name(axId2)]; + if(ax2._categories) { + categories = ax2._categories; + categoriesMap = ax2._categoriesMap; + break; + } + } + + if(categories && categoriesMap) { + ax._categories = categories; + ax._categoriesMap = categoriesMap; + } else { + ax._categories = []; + ax._categoriesMap = {}; + } + } + } + } else { + ax._categories = []; + ax._categoriesMap = {}; + } + + if(ax._initialCategories) { + for(var j = 0; j < ax._initialCategories.length; j++) { + setCategoryIndex(ax._initialCategories[j]); + } } }; diff --git a/test/image/baselines/matching-categories.png b/test/image/baselines/matching-categories.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c616322756e7d407472e5167b743e33b2ea908 GIT binary patch literal 44170 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 Date: Fri, 1 Feb 2019 15:54:05 -0500 Subject: [PATCH 12/23] add axis.matches boolean to splom dims - defaults to `false`, when set to `true`, the generated splom axes will match --- src/plots/cartesian/constraints.js | 8 +- src/traces/splom/attributes.js | 14 + src/traces/splom/defaults.js | 10 +- test/image/baselines/splom_iris-matching.png | Bin 0 -> 68086 bytes .../baselines/splom_lower-nodiag-matching.png | Bin 0 -> 51065 bytes test/image/mocks/splom_iris-matching.json | 700 +++++++++++++++++ .../mocks/splom_lower-nodiag-matching.json | 718 ++++++++++++++++++ test/jasmine/tests/splom_test.js | 91 +++ 8 files changed, 1535 insertions(+), 6 deletions(-) create mode 100644 test/image/baselines/splom_iris-matching.png create mode 100644 test/image/baselines/splom_lower-nodiag-matching.png create mode 100644 test/image/mocks/splom_iris-matching.json create mode 100644 test/image/mocks/splom_lower-nodiag-matching.json diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index 6e8d1314a99..1a83cd69bd5 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -20,6 +20,9 @@ var FROM_BL = require('../../constants/alignment').FROM_BL; exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, allAxisIds, layoutOut) { var constraintGroups = layoutOut._axisConstraintGroups; var matchGroups = layoutOut._axisMatchGroups; + var axId = containerOut._id; + var axLetter = axId.charAt(0); + var splomStash = ((layoutOut._splomAxes || {})[axLetter] || {})[axId] || {}; var thisID = containerOut._id; var letter = thisID.charAt(0); @@ -36,7 +39,7 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a } }, 'constraintoward'); - if(!containerIn.scaleanchor && !containerIn.matches) return; + if(!containerIn.scaleanchor && !containerIn.matches && !splomStash.matches) return; var opts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut); @@ -50,7 +53,8 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a var matches = Lib.coerce(containerIn, containerOut, { matches: { valType: 'enumerated', - values: opts.linkableAxes + values: opts.linkableAxes, + dflt: splomStash.matches } }, 'matches'); diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index 38ca5188545..889eefbab24 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -100,6 +100,20 @@ module.exports = { ].join(' ') }, + // TODO make 'true' the default in v2? + matches: { + valType: 'boolean', + dflt: false, + role: 'info', + editType: 'calc', + description: [ + 'Determines whether or not the x & y axes generated by this', + 'dimension match.', + 'Equivalent to setting the `matches` axis attribute in the layout', + 'with the correct axis id.' + ].join(' ') + }, + editType: 'calc+clearAxisTypes' }, diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index d363d299964..2970f3ac6a7 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -63,6 +63,7 @@ function dimensionDefaults(dimIn, dimOut) { else coerce('visible'); coerce('axis.type'); + coerce('axis.matches'); } function handleAxisDefaults(traceIn, traceOut, layout, coerce) { @@ -97,7 +98,7 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { var xList = []; var yList = []; - function fillAxisStashes(axId, dim, list) { + function fillAxisStashes(axId, counterAxId, dim, list) { if(!axId) return; var axLetter = axId.charAt(0); @@ -111,7 +112,8 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { if(dim) { s.label = dim.label || ''; if(dim.visible && dim.axis) { - s.type = dim.axis.type; + if(dim.axis.type) s.type = dim.axis.type; + if(dim.axis.matches) s.matches = counterAxId; } } } @@ -136,8 +138,8 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { undefined : yaxes[i]; - fillAxisStashes(xaId, dim, xList); - fillAxisStashes(yaId, dim, yList); + fillAxisStashes(xaId, yaId, dim, xList); + fillAxisStashes(yaId, xaId, dim, yList); diag[i] = [xaId, yaId]; } diff --git a/test/image/baselines/splom_iris-matching.png b/test/image/baselines/splom_iris-matching.png new file mode 100644 index 0000000000000000000000000000000000000000..c3d13e9bdeb04006679d89cdb0358534c8e6a2b8 GIT binary patch literal 68086 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 literal 0 HcmV?d00001 diff --git a/test/image/baselines/splom_lower-nodiag-matching.png b/test/image/baselines/splom_lower-nodiag-matching.png new file mode 100644 index 0000000000000000000000000000000000000000..3872b87968fa2c5c8d64b13af4c4fc7830135fa6 GIT binary patch literal 51065 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 z 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 Date: Fri, 1 Feb 2019 14:29:41 -0500 Subject: [PATCH 13/23] generalize "update matched ax rng" logic ... so that it works when matching an overlaying axis --- src/plots/cartesian/dragbox.js | 103 +++++++++--------- test/jasmine/tests/cartesian_interact_test.js | 74 ++++++++++++- 2 files changed, 126 insertions(+), 51 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 53f0d211c98..1804a5a9eec 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -413,10 +413,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // TODO: edit linked axes in zoomAxRanges and in dragTail if(zoomMode === 'xy' || zoomMode === 'x') { - zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes, matches.xaxes); + zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes); + updateMatchedAxRange('x', updates); } if(zoomMode === 'xy' || zoomMode === 'y') { - zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes, matches.yaxes); + zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes); + updateMatchedAxRange('y', updates); } removeZoombox(gd); @@ -482,7 +484,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { for(i = 0; i < xaxes.length; i++) { zoomWheelOneAxis(xaxes[i], xfrac, zoom); } - updateMatchedAxes(matches.isSubplotConstrained ? yaxes : matches.xaxes, xaxes[0].range); + updateMatchedAxRange('x'); scrollViewBox[2] *= zoom; scrollViewBox[0] += scrollViewBox[2] * xfrac * (1 / zoom - 1); @@ -493,7 +495,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { for(i = 0; i < yaxes.length; i++) { zoomWheelOneAxis(yaxes[i], yfrac, zoom); } - updateMatchedAxes(matches.isSubplotConstrained ? xaxes : matches.yaxes, yaxes[0].range); + updateMatchedAxRange('y'); scrollViewBox[3] *= zoom; scrollViewBox[1] += scrollViewBox[3] * (1 - yfrac) * (1 / zoom - 1); @@ -529,19 +531,15 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // prevent axis drawing from monkeying with margins until we're done gd._fullLayout._replotting = true; - var matchedByXaxes; - var matchesByYaxes; - if(matches.isSubplotConstrained) { - matchedByXaxes = yaxes; - matchesByYaxes = xaxes; - } else { - matchedByXaxes = matches.xaxes; - matchesByYaxes = matches.yaxes; - } - if(xActive === 'ew' || yActive === 'ns') { - if(xActive) dragAxList(xaxes, matchedByXaxes, dx); - if(yActive) dragAxList(yaxes, matchesByYaxes, dy); + if(xActive) { + dragAxList(xaxes, dx); + updateMatchedAxRange('x'); + } + if(yActive) { + dragAxList(yaxes, dy); + updateMatchedAxRange('y'); + } updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); ticksAndAnnotations(); return; @@ -614,12 +612,39 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } - updateMatchedAxes(matchedByXaxes, xaxes[0].range); - updateMatchedAxes(matchesByYaxes, yaxes[0].range); + updateMatchedAxRange('x'); + updateMatchedAxRange('y'); updateSubplots([x0, y0, pw - dx, ph - dy]); ticksAndAnnotations(); } + function updateMatchedAxRange(axLetter, out) { + var matchedAxes = matches.isSubplotConstrained ? + {x: yaxes, y: xaxes}[axLetter] : + matches[axLetter + 'axes']; + + var constrainedAxes = matches.isSubplotConstrained ? + {x: xaxes, y: yaxes}[axLetter] : + []; + + for(var i = 0; i < matchedAxes.length; i++) { + var ax = matchedAxes[i]; + var axId = ax._id; + var axId2 = matches.xLinks[axId] || matches.yLinks[axId]; + var ax2 = constrainedAxes[0] || xaHash[axId2] || yaHash[axId2]; + + if(ax2) { + var rng = ax2.range; + if(out) { + out[ax._name + '.range[0]'] = rng[0]; + out[ax._name + '.range[1]'] = rng[1]; + } else { + ax.range = rng; + } + } + } + } + // Draw ticks and annotations (and other components) when ranges change. // Also records the ranges that have changed for use by update at the end. function ticksAndAnnotations() { @@ -947,18 +972,13 @@ function getEndText(ax, end) { } } -function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes, matchedAxes) { - var i, - axi, - axRangeLinear0, - axRangeLinearSpan; - - for(i = 0; i < axList.length; i++) { - axi = axList[i]; +function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) { + for(var i = 0; i < axList.length; i++) { + var axi = axList[i]; if(axi.fixedrange) continue; - axRangeLinear0 = axi._rl[0]; - axRangeLinearSpan = axi._rl[1] - axRangeLinear0; + var axRangeLinear0 = axi._rl[0]; + var axRangeLinearSpan = axi._rl[1] - axRangeLinear0; axi.range = [ axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction), axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction) @@ -973,18 +993,9 @@ function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes, match var linkedR0Fraction = (r0Fraction + (1 - r1Fraction)) / 2; zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates, [], []); } - - // TODO is picking the first ax always ok in general? - // What if we're matching an overlaying axis? - var rng = axList[0].range; - for(i = 0; i < matchedAxes.length; i++) { - axi = matchedAxes[i]; - updates[axi._name + '.range[0]'] = rng[0]; - updates[axi._name + '.range[1]'] = rng[1]; - } } -function dragAxList(axList, matchedAxes, pix) { +function dragAxList(axList, pix) { for(var i = 0; i < axList.length; i++) { var axi = axList[i]; if(!axi.fixedrange) { @@ -994,16 +1005,6 @@ function dragAxList(axList, matchedAxes, pix) { ]; } } - - updateMatchedAxes(matchedAxes, axList[0].range); -} - -function updateMatchedAxes(matchedAxes, rng) { - // TODO is picking the first ax always ok in general? - // What if we're matching an overlaying axis? - for(var i = 0; i < matchedAxes.length; i++) { - matchedAxes[i].range = rng.slice(); - } } // common transform for dragging one end of an axis @@ -1133,7 +1134,7 @@ function calcLinks(gd, groups, xaHash, yaHash) { // to match the changes in the dragged x axes for(xLinkID in group) { if(!(xLinkID.charAt(0) === 'x' ? xaHash : yaHash)[xLinkID]) { - xLinks[xLinkID] = 1; + xLinks[xLinkID] = xID; } } @@ -1150,7 +1151,7 @@ function calcLinks(gd, groups, xaHash, yaHash) { if(group[yID]) { for(yLinkID in group) { if(!(yLinkID.charAt(0) === 'x' ? xaHash : yaHash)[yLinkID]) { - yLinks[yLinkID] = 1; + yLinks[yLinkID] = yID; } } } @@ -1186,6 +1187,8 @@ function calcLinks(gd, groups, xaHash, yaHash) { yaHash: yaHashLinked, xaxes: xaxesLinked, yaxes: yaxesLinked, + xLinks: xLinks, + yLinks: yLinks, isSubplotConstrained: isSubplotConstrained }; } diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 195472a4238..ed33efe04e2 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -710,10 +710,11 @@ describe('axis zoom/pan and main plot zoom', function() { exp.forEach(function(expi) { var axNames = expi[0]; var rng = expi[1]; + var opts = expi[2] || {}; axNames.forEach(function(n) { var msgi = n + ' - ' + msg; - expect(gd.layout[n].range).toBeCloseToArray(rng, TOL, msgi + ' |input'); + if(!opts.skipInput) expect(gd.layout[n].range).toBeCloseToArray(rng, TOL, msgi + ' |input'); expect(gd._fullLayout[n].range).toBeCloseToArray(rng, TOL, msgi + ' |full'); }); }); @@ -1325,6 +1326,77 @@ describe('axis zoom/pan and main plot zoom', function() { }); }); }); + + it('panning a matching overlaying axis', function(done) { + /* + * y | | y2 y3 | + * | | | + * | | o m | + * | | v a | + * | | e t | + * | | r c | + * | | l h | + * | | a e | + * | | y s | + * | | | + * ______________________ y y2 ____________________ + * + * x x2 + */ + var data = [ + { y: [1, 2, 1] }, + { y: [2, 1, 3, 4], yaxis: 'y2' }, + { y: [2, 3, -1, 5], xaxis: 'x2', yaxis: 'y3' } + ]; + + var layout = { + xaxis: {domain: [0, 0.4]}, + xaxis2: {anchor: 'y2', domain: [0.5, 1]}, + yaxis2: {anchor: 'x', overlaying: 'y', side: 'right'}, + yaxis3: {anchor: 'x2', matches: 'y2'}, + width: 700, + height: 500, + dragmode: 'pan' + }; + + makePlot(data, layout).then(function() { + assertRanges('base', [ + [['yaxis2', 'yaxis3'], [-1.422, 5.422]], + [['xaxis'], [-0.237, 3.237]], + [['yaxis'], [0.929, 2.070]], + [['xaxis2'], [-0.225, 3.222]] + ]); + }) + .then(function() { + var drag = makeDragFns('xy', 'nsew', 30, 30); + return drag.start().then(function() { + // N.B. it is with these values that Axes.drawOne gets called! + assertRanges('during drag', [ + [['yaxis2', 'yaxis3'], [-0.788, 6.0641], {skipInput: true}], + [['xaxis'], [-0.744, 2.730], {skipInput: true}], + [['yaxis'], [1.036, 2.177], {skipInput: true}], + [['xaxis2'], [-0.225, 3.222]] + ]); + }) + .then(drag.end); + }) + .then(_assert('after drag on xy subplot', [ + [['yaxis2', 'yaxis3'], [-0.788, 6.0641], {dragged: true}], + [['xaxis'], [-0.744, 2.730], {dragged: true}], + [['yaxis'], [1.036, 2.177], {dragged: true}], + [['xaxis2'], [-0.225, 3.222], {noChange: true}] + ])) + .then(function() { return Plotly.relayout(gd, 'dragmode', 'zoom'); }) + .then(doDrag('xy', 'nsew', 30, 30)) + .then(_assert('after zoombox on xy subplot', [ + [['yaxis2', 'yaxis3'], [2, 2.6417]], + [['xaxis'], [0.979, 1.486]], + [['yaxis'], [1.5, 1.609]], + [['xaxis2'], [-0.225, 3.222], {noChange: true}] + ])) + .catch(failTest) + .then(done); + }); }); }); From 9f2fad133f7f11e795076317701372f9ab3e22af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 1 Feb 2019 15:04:54 -0500 Subject: [PATCH 14/23] disallow constraining AND matching range --- src/plots/cartesian/constraints.js | 9 ++++++++- test/jasmine/tests/axes_test.js | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index 1a83cd69bd5..3b941a42a21 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -30,7 +30,7 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a // coerce the constraint mechanics even if this axis has no scaleanchor // because it may be the anchor of another axis. - coerce('constrain'); + var constrain = coerce('constrain'); Lib.coerce(containerIn, containerOut, { constraintoward: { valType: 'enumerated', @@ -58,6 +58,13 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a } }, 'matches'); + // disallow constraining AND matching range + if(constrain === 'range' && scaleanchor === matches) { + delete containerOut.scaleanchor; + delete containerOut.constrain; + scaleanchor = null; + } + var found = false; if(scaleanchor) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index f04afb1800f..f9b61467601 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -689,6 +689,24 @@ describe('Test axes', function() { }); }); + it('disallow constraining AND matching range', function() { + layoutIn = { + xaxis: {}, + xaxis2: {matches: 'x', scaleanchor: 'x'} + }; + layoutOut._subplots.cartesian.push('x2y'); + layoutOut._subplots.xaxis.push('x2'); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis2.matches).toBe('x'); + expect(layoutOut.xaxis2.scaleanchor).toBe(undefined); + expect(layoutOut.xaxis2.constrain).toBe(undefined); + + expect(layoutOut._axisConstraintGroups).toEqual([]); + expect(layoutOut._axisMatchGroups).toEqual([{x: 1, x2: 1}]); + }); + it('drops scaleanchor settings if either the axis or target has fixedrange', function() { // some of these will create warnings... not too important, so not going to test, // just want to keep the output clean From 8c09944acfe016dfd3ea0880e734f37e8dc18650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 1 Feb 2019 15:05:13 -0500 Subject: [PATCH 15/23] add "matches" + "scaleanchor" mock --- src/plot_api/subroutines.js | 5 ++-- src/plots/cartesian/constraints.js | 2 -- .../axes_scaleanchor-with-matches.png | Bin 0 -> 22036 bytes .../mocks/axes_scaleanchor-with-matches.json | 28 ++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 test/image/baselines/axes_scaleanchor-with-matches.png create mode 100644 test/image/mocks/axes_scaleanchor-with-matches.json diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 25aa2817741..f4811305d34 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -708,6 +708,8 @@ exports.doAutoRangeAndConstraints = function(gd) { doAutoRange(gd, ax); } + enforceAxisConstraints(gd); + // TODO bypass this when matching axes aren't autoranged? for(var j = 0; j < matchGroups.length; j++) { var group = matchGroups[j]; @@ -737,9 +739,6 @@ exports.doAutoRangeAndConstraints = function(gd) { ax.setScale(0); } } - - // TODO before or after matching axes? - enforceAxisConstraints(gd); }; // An initial paint must be completed before these components can be diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index 3b941a42a21..ac29625eb14 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -81,8 +81,6 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a found = true; } - // TODO what happens when both scaleanchor and matches are present??? - if(matches) { updateConstraintGroups(matchGroups, opts.thisGroup, thisID, matches, 1); found = true; diff --git a/test/image/baselines/axes_scaleanchor-with-matches.png b/test/image/baselines/axes_scaleanchor-with-matches.png new file mode 100644 index 0000000000000000000000000000000000000000..27c45f964212d2ce852dad1409eb2d6c5aa2763f GIT binary patch literal 22036 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 qxc*_k5GGwDi;;+?4!#kPtorKzNHo3BKLF-{Gl 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 BTblp? literal 0 HcmV?d00001 diff --git a/test/image/mocks/axes_scaleanchor-with-matches.json b/test/image/mocks/axes_scaleanchor-with-matches.json new file mode 100644 index 00000000000..c23ce1db40f --- /dev/null +++ b/test/image/mocks/axes_scaleanchor-with-matches.json @@ -0,0 +1,28 @@ +{ + "data":[ + {"x": [0,1,1,0,0,1,1,2,2,3,3,2,2,3], "y": [0,0,1,1,3,3,2,2,3,3,1,1,0,0]}, + {"x": [0,1,2,3], "y": [1,2,4,8], "yaxis":"y2"} + ], + "layout":{ + "width": 500, + "height": 500, + "xaxis": { + "constrain": "domain" + }, + "yaxis": { + "scaleanchor": "x", + "constrain": "domain", + "domain": [0, 0.45], + "title": {"text": "1:1
matching y range above"} + }, + "yaxis2": { + "matches": "y", + "constrain": "domain", + "scaleanchor": "x", + "scaleratio": 0.2, + "domain": [0.55, 1], + "title": {"text": "1:5
matching y range below"} + }, + "showlegend": false + } +} From c5f9e74e1df0b4f2c3a3248e64e71d22916711ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 1 Feb 2019 17:19:21 -0500 Subject: [PATCH 16/23] fix partial ax-range relayout calls for matching axes ... moreovwe, boast relayout perf for non-autorange matching axis groups. --- src/plot_api/plot_api.js | 26 +++++++++----- src/plot_api/subroutines.js | 3 +- test/jasmine/tests/axes_test.js | 60 +++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 1c1306acf9e..7b4102334fc 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1932,25 +1932,35 @@ exports.relayout = relayout; // Optimization mostly for large splom traces where // Plots.supplyDefaults can take > 100ms function axRangeSupplyDefaultsByPass(gd, flags, specs) { - var k; + var fullLayout = gd._fullLayout; + var axisMatchGroups = fullLayout._axisMatchGroups || []; if(!flags.axrange) return false; - for(k in flags) { + for(var k in flags) { if(k !== 'axrange' && flags[k]) return false; } - for(k in specs.rangesAltered) { - var axName = Axes.id2name(k); + for(var axId in specs.rangesAltered) { + var axName = Axes.id2name(axId); var axIn = gd.layout[axName]; - var axOut = gd._fullLayout[axName]; + var axOut = fullLayout[axName]; axOut.autorange = axIn.autorange; axOut.range = axIn.range.slice(); axOut.cleanRange(); - } - // no need to consider matching axes here, - // if we keep block in doAutoRangeAndConstraints + for(var i = 0; i < axisMatchGroups.length; i++) { + var group = axisMatchGroups[i]; + if(group[axId]) { + for(var axId2 in group) { + var ax2 = fullLayout[Axes.id2name(axId2)]; + ax2.autorange = axOut.autorange; + ax2.range = axOut.range.slice(); + ax2._input.range = axOut.range.slice(); + } + } + } + } return true; } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index f4811305d34..53a8e977d61 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -710,7 +710,7 @@ exports.doAutoRangeAndConstraints = function(gd) { enforceAxisConstraints(gd); - // TODO bypass this when matching axes aren't autoranged? + groupLoop: for(var j = 0; j < matchGroups.length; j++) { var group = matchGroups[j]; var rng = null; @@ -718,6 +718,7 @@ exports.doAutoRangeAndConstraints = function(gd) { for(id in group) { ax = Axes.getFromId(gd, id); + if(ax.autorange === false) continue groupLoop; if(rng) { if(rng[0] < rng[1]) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index f9b61467601..867679930ad 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1192,6 +1192,66 @@ describe('Test axes', function() { }); }); + describe('matching axes relayout calls', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function assertRanges(msg, exp) { + exp.forEach(function(expi) { + var axNames = expi[0]; + var rng = expi[1]; + var autorng = expi[2]; + + axNames.forEach(function(n) { + var msgi = n + ' - ' + msg; + expect(gd._fullLayout[n].range).toBeCloseToArray(rng, 1.5, msgi + ' |range'); + expect(gd._fullLayout[n].autorange).toBe(autorng, msgi + ' |autorange'); + }); + }); + } + + it('should auto-range according to all matching trace data', function(done) { + Plotly.plot(gd, [ + { y: [1, 2, 1] }, + { y: [2, 1, 2, 3], xaxis: 'x2' }, + { y: [0, 1], xaxis: 'x3' } + ], { + xaxis: {domain: [0, 0.2]}, + xaxis2: {matches: 'x', domain: [0.3, 0.6]}, + xaxis3: {matches: 'x', domain: [0.65, 1]}, + width: 800, + height: 500, + }) + .then(function() { + assertRanges('base (autoranged)', [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.245, 3.245], true], + [['yaxis'], [-0.211, 3.211], true] + ]); + }) + .then(function() { return Plotly.relayout(gd, 'xaxis.range', [-1, 4]); }) + .then(function() { + assertRanges('set range', [ + [['xaxis', 'xaxis2', 'xaxis3'], [-1, 4], false], + [['yaxis'], [-0.211, 3.211], true] + ]); + }) + .then(function() { return Plotly.relayout(gd, 'xaxis2.autorange', true); }) + .then(function() { + assertRanges('back to autorange', [ + [['xaxis', 'xaxis2', 'xaxis3'], [-0.245, 3.245], true], + [['yaxis'], [-0.211, 3.211], true] + ]); + }) + .catch(failTest) + .then(done); + }); + }); + describe('categoryorder', function() { var gd; From d0581e22b8ccacb76cce6d88954ca9b997cfce82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 5 Feb 2019 10:32:48 -0500 Subject: [PATCH 17/23] fix typo (ax.setScale has no arg) --- src/plot_api/subroutines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 53a8e977d61..d108bec0966 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -737,7 +737,7 @@ exports.doAutoRangeAndConstraints = function(gd) { ax = Axes.getFromId(gd, id); ax.range = rng.slice(); ax._input.range = rng.slice(); - ax.setScale(0); + ax.setScale(); } } }; From 6b34ae3574a08196090a00dfe61817ce51ea349f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 5 Feb 2019 10:32:33 -0500 Subject: [PATCH 18/23] use ax._matchGroup to improve matching axes relayout perf --- src/plot_api/plot_api.js | 20 +++++++------------- src/plots/cartesian/layout_defaults.js | 1 + 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 7b4102334fc..cc3a304f522 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1933,7 +1933,6 @@ exports.relayout = relayout; // Plots.supplyDefaults can take > 100ms function axRangeSupplyDefaultsByPass(gd, flags, specs) { var fullLayout = gd._fullLayout; - var axisMatchGroups = fullLayout._axisMatchGroups || []; if(!flags.axrange) return false; @@ -1949,10 +1948,9 @@ function axRangeSupplyDefaultsByPass(gd, flags, specs) { axOut.range = axIn.range.slice(); axOut.cleanRange(); - for(var i = 0; i < axisMatchGroups.length; i++) { - var group = axisMatchGroups[i]; - if(group[axId]) { - for(var axId2 in group) { + if(axOut._matchGroup) { + for(var axId2 in axOut._matchGroup) { + if(axId2 !== axId) { var ax2 = fullLayout[Axes.id2name(axId2)]; ax2.autorange = axOut.autorange; ax2.range = axOut.range.slice(); @@ -1973,19 +1971,15 @@ function addAxRangeSequence(seq, rangesAltered) { function(gd) { var axIds = []; var skipTitle = true; - var matchGroups = gd._fullLayout._axisMatchGroups || []; for(var id in rangesAltered) { var ax = Axes.getFromId(gd, id); axIds.push(id); - for(var i = 0; i < matchGroups.length; i++) { - var group = matchGroups[i]; - if(group[id]) { - for(var id2 in group) { - if(!rangesAltered[id2]) { - axIds.push(id2); - } + if(ax._matchGroup) { + for(var id2 in ax._matchGroup) { + if(!rangesAltered[id2]) { + axIds.push(id2); } } } diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index dd71bc62610..a2501508018 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -296,6 +296,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutOut.range = rng.slice(); axLayoutOut.autorange = autorange; } + axLayoutOut._matchGroup = group; } } }; From 99a9edb4f968be0eeba19dbfd42741c1c3576bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 6 Feb 2019 11:09:40 -0500 Subject: [PATCH 19/23] allow fixedrange subplots to scaleanchor with `constrain:domain` more info in: https://github.com/plotly/plotly.js/pull/3460 --- src/plots/cartesian/constraints.js | 51 +++++++++++------- ...caleanchor-constrain-domain-fixedrange.png | Bin 0 -> 22448 bytes ...aleanchor-constrain-domain-fixedrange.json | 28 ++++++++++ test/jasmine/tests/axes_test.js | 2 +- 4 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 test/image/baselines/axes_scaleanchor-constrain-domain-fixedrange.png create mode 100644 test/image/mocks/axes_scaleanchor-constrain-domain-fixedrange.json diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index ac29625eb14..adb4b14f31e 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -26,11 +26,10 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a var thisID = containerOut._id; var letter = thisID.charAt(0); - if(containerOut.fixedrange) return; - // coerce the constraint mechanics even if this axis has no scaleanchor // because it may be the anchor of another axis. var constrain = coerce('constrain'); + Lib.coerce(containerIn, containerOut, { constraintoward: { valType: 'enumerated', @@ -39,27 +38,31 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a } }, 'constraintoward'); - if(!containerIn.scaleanchor && !containerIn.matches && !splomStash.matches) return; + var scaleOpts = containerIn.scaleanchor && !(containerOut.fixedrange && constrain !== 'domain') ? + getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut, constrain) : + {}; - var opts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut); + var matchOpts = (containerIn.matches || splomStash.matches) && !containerOut.fixedrange ? + getConstraintOpts(matchGroups, thisID, allAxisIds, layoutOut) : + {}; var scaleanchor = Lib.coerce(containerIn, containerOut, { scaleanchor: { valType: 'enumerated', - values: opts.linkableAxes + values: scaleOpts.linkableAxes || [] } }, 'scaleanchor'); var matches = Lib.coerce(containerIn, containerOut, { matches: { valType: 'enumerated', - values: opts.linkableAxes, + values: matchOpts.linkableAxes || [], dflt: splomStash.matches } }, 'matches'); // disallow constraining AND matching range - if(constrain === 'range' && scaleanchor === matches) { + if(constrain === 'range' && scaleanchor && matches && scaleanchor === matches) { delete containerOut.scaleanchor; delete containerOut.constrain; scaleanchor = null; @@ -77,12 +80,12 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a // Likewise with super-huge values. if(!scaleratio) scaleratio = containerOut.scaleratio = 1; - updateConstraintGroups(constraintGroups, opts.thisGroup, thisID, scaleanchor, scaleratio); + updateConstraintGroups(constraintGroups, scaleOpts.thisGroup, thisID, scaleanchor, scaleratio); found = true; } if(matches) { - updateConstraintGroups(matchGroups, opts.thisGroup, thisID, matches, 1); + updateConstraintGroups(matchGroups, matchOpts.thisGroup, thisID, matches, 1); found = true; } @@ -94,13 +97,12 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a } }; -function getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut) { - // If this axis is already part of a constraint group, we can't - // scaleanchor any other axis in that group, or we'd make a loop. - // Filter allAxisIds to enforce this, also matching axis types. - +// If this axis is already part of a constraint group, we can't +// scaleanchor any other axis in that group, or we'd make a loop. +// Filter allAxisIds to enforce this, also matching axis types. +function getConstraintOpts(groups, thisID, allAxisIds, layoutOut, constrain) { + var doesNotConstrainRange = constrain !== 'range'; var thisType = layoutOut[id2name(thisID)].type; - var i, j, idj, axj; var linkableAxes = []; @@ -109,12 +111,23 @@ function getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut) { if(idj === thisID) continue; axj = layoutOut[id2name(idj)]; - if(axj.type === thisType && !axj.fixedrange) linkableAxes.push(idj); + if(axj.type === thisType) { + if(!axj.fixedrange) { + linkableAxes.push(idj); + } else if(doesNotConstrainRange && axj.anchor) { + // allow domain constraints on subplots where + // BOTH axes have fixedrange:true and constrain:domain + var counterAxj = layoutOut[id2name(axj.anchor)]; + if(counterAxj.fixedrange) { + linkableAxes.push(idj); + } + } + } } - for(i = 0; i < constraintGroups.length; i++) { - if(constraintGroups[i][thisID]) { - var thisGroup = constraintGroups[i]; + for(i = 0; i < groups.length; i++) { + if(groups[i][thisID]) { + var thisGroup = groups[i]; var linkableAxesNoLoops = []; for(j = 0; j < linkableAxes.length; j++) { diff --git a/test/image/baselines/axes_scaleanchor-constrain-domain-fixedrange.png b/test/image/baselines/axes_scaleanchor-constrain-domain-fixedrange.png new file mode 100644 index 0000000000000000000000000000000000000000..60bd3831990b9a45d1c6d2271bb37af2ecd0fdfe GIT binary patch literal 22448 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 Vc}W}!PU5A|Ib*0%aQf1n{{wTb Date: Tue, 12 Feb 2019 12:43:10 -0500 Subject: [PATCH 20/23] drop *scaleanchor* constraints for axes under *matches* constraint ... for now, until we found a way to apply domain scaleanchor constraints on matching axes (which is theoretically possible). --- src/plots/cartesian/constraints.js | 68 ++++++++---------- src/plots/cartesian/layout_attributes.js | 8 ++- src/plots/cartesian/layout_defaults.js | 35 +++++++-- .../axes_scaleanchor-with-matches.png | Bin 22036 -> 30228 bytes .../mocks/axes_scaleanchor-with-matches.json | 20 +++--- test/jasmine/tests/axes_test.js | 46 +++++++++++- 6 files changed, 120 insertions(+), 57 deletions(-) diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index adb4b14f31e..4cf2f1b70b1 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -29,7 +29,6 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a // coerce the constraint mechanics even if this axis has no scaleanchor // because it may be the anchor of another axis. var constrain = coerce('constrain'); - Lib.coerce(containerIn, containerOut, { constraintoward: { valType: 'enumerated', @@ -38,38 +37,41 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a } }, 'constraintoward'); - var scaleOpts = containerIn.scaleanchor && !(containerOut.fixedrange && constrain !== 'domain') ? - getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut, constrain) : - {}; + var matches, matchOpts; - var matchOpts = (containerIn.matches || splomStash.matches) && !containerOut.fixedrange ? - getConstraintOpts(matchGroups, thisID, allAxisIds, layoutOut) : - {}; + if((containerIn.matches || splomStash.matches) && !containerOut.fixedrange) { + matchOpts = getConstraintOpts(matchGroups, thisID, allAxisIds, layoutOut); + matches = Lib.coerce(containerIn, containerOut, { + matches: { + valType: 'enumerated', + values: matchOpts.linkableAxes || [], + dflt: splomStash.matches + } + }, 'matches'); + } - var scaleanchor = Lib.coerce(containerIn, containerOut, { - scaleanchor: { - valType: 'enumerated', - values: scaleOpts.linkableAxes || [] - } - }, 'scaleanchor'); + // 'matches' wins over 'scaleanchor' (for now) + var scaleanchor, scaleOpts; - var matches = Lib.coerce(containerIn, containerOut, { - matches: { - valType: 'enumerated', - values: matchOpts.linkableAxes || [], - dflt: splomStash.matches - } - }, 'matches'); + if(!matches && containerIn.scaleanchor && !(containerOut.fixedrange && constrain !== 'domain')) { + scaleOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut, constrain); + scaleanchor = Lib.coerce(containerIn, containerOut, { + scaleanchor: { + valType: 'enumerated', + values: scaleOpts.linkableAxes || [] + } + }, 'scaleanchor'); + } - // disallow constraining AND matching range - if(constrain === 'range' && scaleanchor && matches && scaleanchor === matches) { - delete containerOut.scaleanchor; + if(matches) { delete containerOut.constrain; - scaleanchor = null; + updateConstraintGroups(matchGroups, matchOpts.thisGroup, thisID, matches, 1); + } else if(allAxisIds.indexOf(containerIn.matches) !== -1) { + Lib.warn('ignored ' + containerOut._name + '.matches: "' + + containerIn.matches + '" to avoid either an infinite loop ' + + 'or because the target axis has fixed range.'); } - var found = false; - if(scaleanchor) { var scaleratio = coerce('scaleratio'); @@ -81,19 +83,11 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a if(!scaleratio) scaleratio = containerOut.scaleratio = 1; updateConstraintGroups(constraintGroups, scaleOpts.thisGroup, thisID, scaleanchor, scaleratio); - found = true; - } - - if(matches) { - updateConstraintGroups(matchGroups, matchOpts.thisGroup, thisID, matches, 1); - found = true; - } - - if(!found && allAxisIds.indexOf(containerIn.scaleanchor) !== -1) { + } else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) { Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' + containerIn.scaleanchor + '" to avoid either an infinite loop ' + - 'and possibly inconsistent scaleratios, or because the target' + - 'axis has fixed range.'); + 'and possibly inconsistent scaleratios, or because the target ' + + 'axis has fixed range or this axis declares a *matches* constraint.'); } }; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 7c44aaedd06..4f745199057 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -169,7 +169,9 @@ module.exports = { 'or the same letter (to match scales across subplots).', 'Loops (`yaxis: {scaleanchor: *x*}, xaxis: {scaleanchor: *y*}` or longer) are redundant', 'and the last constraint encountered will be ignored to avoid possible', - 'inconsistent constraints via `scaleratio`.' + 'inconsistent constraints via `scaleratio`.', + 'Note that setting axes simultaneously in both a `scaleanchor` and a `matches` constraint', + 'is currently forbidden.' ].join(' ') }, scaleratio: { @@ -224,8 +226,8 @@ module.exports = { 'will match the range of the corresponding axis in data-coordinates space.', 'Moreover, matching axes share auto-range values, category lists and', 'histogram auto-bins.', - 'Note that setting `matches` and `scaleratio` under a *range* `constrain`', - 'to the same axis id is forbidden.' + 'Note that setting axes simultaneously in both a `scaleanchor` and a `matches` constraint', + 'is currently forbidden.' ].join(' ') }, // ticks diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index a2501508018..8a6f6235b10 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -254,9 +254,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // sets of axes linked by `scaleanchor` along with the scaleratios compounded // together, populated in handleConstraintDefaults - layoutOut._axisConstraintGroups = []; + var constraintGroups = layoutOut._axisConstraintGroups = []; // similar to _axisConstraintGroups, but for matching axes - layoutOut._axisMatchGroups = []; + var matchGroups = layoutOut._axisMatchGroups = []; for(i = 0; i < axNames.length; i++) { axName = axNames[i]; @@ -267,12 +267,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, allAxisIds, layoutOut); } - for(i = 0; i < layoutOut._axisMatchGroups.length; i++) { - var group = layoutOut._axisMatchGroups[i]; + for(i = 0; i < matchGroups.length; i++) { + var group = matchGroups[i]; var rng = null; var autorange = null; var axId; + // find 'matching' range attrs for(axId in group) { axLayoutOut = layoutOut[id2name(axId)]; if(!axLayoutOut.matches) { @@ -280,7 +281,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { autorange = axLayoutOut.autorange; } } - + // if `ax.matches` values are reciprocal, + // pick values of first axis in group if(rng === null || autorange === null) { for(axId in group) { axLayoutOut = layoutOut[id2name(axId)]; @@ -289,7 +291,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { break; } } - + // apply matching range attrs for(axId in group) { axLayoutOut = layoutOut[id2name(axId)]; if(axLayoutOut.matches) { @@ -298,5 +300,26 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } axLayoutOut._matchGroup = group; } + + // remove matching axis from scaleanchor constraint groups (for now) + if(constraintGroups.length) { + for(axId in group) { + for(j = 0; j < constraintGroups.length; j++) { + var group2 = constraintGroups[j]; + for(var axId2 in group2) { + if(axId === axId2) { + Lib.warn('Axis ' + axId2 + ' is set with both ' + + 'a *scaleanchor* and *matches* constraint; ' + + 'ignoring the scale constraint.'); + + delete group2[axId2]; + if(Object.keys(group2).length < 2) { + constraintGroups.splice(j, 1); + } + } + } + } + } + } } }; diff --git a/test/image/baselines/axes_scaleanchor-with-matches.png b/test/image/baselines/axes_scaleanchor-with-matches.png index 27c45f964212d2ce852dad1409eb2d6c5aa2763f..56599d8d2cec71216e959b7ebb61fde432c1d953 100644 GIT binary patch literal 30228 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 literal 22036 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 qxc*_k5GGwDi;;+?4!#kPtorKzNHo3BKLF-{Gl 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 BTblp? diff --git a/test/image/mocks/axes_scaleanchor-with-matches.json b/test/image/mocks/axes_scaleanchor-with-matches.json index c23ce1db40f..27d6ae1de0d 100644 --- a/test/image/mocks/axes_scaleanchor-with-matches.json +++ b/test/image/mocks/axes_scaleanchor-with-matches.json @@ -1,27 +1,29 @@ { "data":[ {"x": [0,1,1,0,0,1,1,2,2,3,3,2,2,3], "y": [0,0,1,1,3,3,2,2,3,3,1,1,0,0]}, - {"x": [0,1,2,3], "y": [1,2,4,8], "yaxis":"y2"} + {"x": [0,1,2,3], "y": [1,2,4,8], "xaxis": "x2", "yaxis":"y2"} ], "layout":{ "width": 500, "height": 500, + "title": {"text": "Bottom subplot has scaleanchor constrained axes
Top subplot has matching axes"}, "xaxis": { - "constrain": "domain" + "constrain": "range" }, "yaxis": { "scaleanchor": "x", - "constrain": "domain", + "constrain": "range", "domain": [0, 0.45], - "title": {"text": "1:1
matching y range above"} + "title": {"text": "1:1
x|y scale constrain range"} + }, + "xaxis2": { + "anchor": "y2" }, "yaxis2": { - "matches": "y", - "constrain": "domain", - "scaleanchor": "x", - "scaleratio": 0.2, + "anchor": "x2", + "matches": "x2", "domain": [0.55, 1], - "title": {"text": "1:5
matching y range below"} + "title": {"text": "matching x|y"} }, "showlegend": false } diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 5a24b1ccf36..9a17b09222f 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -588,8 +588,8 @@ describe('Test axes', function() { }); var warnTxt = ' to avoid either an infinite loop and possibly ' + - 'inconsistent scaleratios, or because the targetaxis has ' + - 'fixed range.'; + 'inconsistent scaleratios, or because the target axis has ' + + 'fixed range or this axis declares a *matches* constraint.'; it('breaks scaleanchor loops and drops conflicting ratios', function() { var warnings = []; @@ -707,6 +707,48 @@ describe('Test axes', function() { expect(layoutOut._axisMatchGroups).toEqual([{x: 1, x2: 1}]); }); + it('remove axes from constraint groups if they are in a match group', function() { + layoutIn = { + // this one is ok + xaxis: {}, + yaxis: {scaleanchor: 'x'}, + // this one too + xaxis2: {}, + yaxis2: {matches: 'x2'}, + // not these ones + xaxis3: {scaleanchor: 'x2'}, + yaxis3: {scaleanchor: 'y2'} + }; + layoutOut._subplots.cartesian.push('x2y2, x3y3'); + layoutOut._subplots.xaxis.push('x2', 'x3'); + layoutOut._subplots.yaxis.push('y2', 'y3'); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(1); + expect(layoutOut._axisMatchGroups).toContain({x2: 1, y2: 1}); + + expect(layoutOut._axisConstraintGroups.length).toBe(1); + expect(layoutOut._axisConstraintGroups).toContain({x: 1, y: 1}); + }); + + it('remove constraint group if they are one or zero items left in it', function() { + layoutIn = { + xaxis: {}, + yaxis: {matches: 'x'}, + xaxis2: {scaleanchor: 'y'} + }; + layoutOut._subplots.cartesian.push('x2y'); + layoutOut._subplots.xaxis.push('x2'); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(1); + expect(layoutOut._axisMatchGroups).toContain({x: 1, y: 1}); + + expect(layoutOut._axisConstraintGroups.length).toBe(0); + }); + it('drops scaleanchor settings if either the axis or target has fixedrange', function() { // some of these will create warnings... not too important, so not going to test, // just want to keep the output clean From 70b10fff46eef9b7cd601317764ed9e3e4515324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 18 Feb 2019 12:59:49 -0500 Subject: [PATCH 21/23] add safe-guard in dragbox.js --- src/plots/cartesian/dragbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 1804a5a9eec..8ffd84fb657 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -989,7 +989,7 @@ function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) { } // zoom linked axes about their centers - if(linkedAxes.length) { + if(linkedAxes && linkedAxes.length) { var linkedR0Fraction = (r0Fraction + (1 - r1Fraction)) / 2; zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates, [], []); } From 772efe519828e6efdb436e4dfbb8a6275b230bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 18 Feb 2019 13:02:00 -0500 Subject: [PATCH 22/23] add info about matching axis type in attr description --- src/plots/cartesian/layout_attributes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 4f745199057..fd01a8f33ce 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -227,7 +227,8 @@ module.exports = { 'Moreover, matching axes share auto-range values, category lists and', 'histogram auto-bins.', 'Note that setting axes simultaneously in both a `scaleanchor` and a `matches` constraint', - 'is currently forbidden.' + 'is currently forbidden.', + 'Moreover, note that matching must have the same `type`.' ].join(' ') }, // ticks From 1713c83ee80bee37ee1e0ba9dd3a333cbca23963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 18 Feb 2019 13:18:16 -0500 Subject: [PATCH 23/23] fix typo --- src/plots/cartesian/layout_attributes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index fd01a8f33ce..8c920ecbac0 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -228,7 +228,7 @@ module.exports = { 'histogram auto-bins.', 'Note that setting axes simultaneously in both a `scaleanchor` and a `matches` constraint', 'is currently forbidden.', - 'Moreover, note that matching must have the same `type`.' + 'Moreover, note that matching axes must have the same `type`.' ].join(' ') }, // ticks