diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index e950d614426..cc3a304f522 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -1932,22 +1932,34 @@ 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;
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();
+
+ 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();
+ ax2._input.range = axOut.range.slice();
+ }
+ }
+ }
}
+
return true;
}
@@ -1957,14 +1969,25 @@ function addAxRangeSequence(seq, rangesAltered) {
// executed after drawData
var drawAxes = rangesAltered ?
function(gd) {
- var opts = {skipTitle: true};
+ var axIds = [];
+ var skipTitle = true;
+
for(var id in rangesAltered) {
- if(Axes.getFromId(gd, id).automargin) {
- opts = {};
- break;
+ var ax = Axes.getFromId(gd, id);
+ axIds.push(id);
+
+ if(ax._matchGroup) {
+ for(var id2 in ax._matchGroup) {
+ 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..d108bec0966 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -697,17 +697,49 @@ 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);
}
enforceAxisConstraints(gd);
+
+ groupLoop:
+ 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(ax.autorange === false) continue groupLoop;
+
+ 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();
+ }
+ }
};
// An initial paint must be completed before these components can be
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/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..4cf2f1b70b1 100644
--- a/src/plots/cartesian/constraints.js
+++ b/src/plots/cartesian/constraints.js
@@ -6,18 +6,188 @@
* 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 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);
+
+ // 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',
+ values: letter === 'x' ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'],
+ dflt: letter === 'x' ? 'center' : 'middle'
+ }
+ }, 'constraintoward');
+
+ var matches, matchOpts;
+
+ 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');
+ }
+
+ // 'matches' wins over 'scaleanchor' (for now)
+ var scaleanchor, scaleOpts;
+
+ 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');
+ }
+
+ if(matches) {
+ delete containerOut.constrain;
+ 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.');
+ }
+
+ 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, scaleOpts.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 or this axis declares a *matches* constraint.');
+ }
+};
+
+// 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 = [];
+ for(j = 0; j < allAxisIds.length; j++) {
+ idj = allAxisIds[j];
+ if(idj === thisID) continue;
+
+ axj = layoutOut[id2name(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 < groups.length; i++) {
+ if(groups[i][thisID]) {
+ var thisGroup = groups[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/dragbox.js b/src/plots/cartesian/dragbox.js
index 8f44b934554..8ffd84fb657 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');
@@ -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);
}
@@ -333,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();
}
}
@@ -396,9 +414,11 @@ 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);
+ updateMatchedAxRange('x', updates);
}
if(zoomMode === 'xy' || zoomMode === 'y') {
zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes);
+ updateMatchedAxRange('y', updates);
}
removeZoombox(gd);
@@ -464,6 +484,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
for(i = 0; i < xaxes.length; i++) {
zoomWheelOneAxis(xaxes[i], xfrac, zoom);
}
+ updateMatchedAxRange('x');
scrollViewBox[2] *= zoom;
scrollViewBox[0] += scrollViewBox[2] * xfrac * (1 / zoom - 1);
@@ -474,6 +495,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
for(i = 0; i < yaxes.length; i++) {
zoomWheelOneAxis(yaxes[i], yfrac, zoom);
}
+ updateMatchedAxRange('y');
scrollViewBox[3] *= zoom;
scrollViewBox[1] += scrollViewBox[3] * (1 - yfrac) * (1 / zoom - 1);
@@ -510,8 +532,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
gd._fullLayout._replotting = true;
if(xActive === 'ew' || yActive === 'ns') {
- if(xActive) dragAxList(xaxes, dx);
- if(yActive) dragAxList(yaxes, 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;
@@ -542,7 +570,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;
@@ -562,7 +590,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
@@ -584,10 +612,39 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
}
}
+ 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() {
@@ -603,10 +660,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 = {};
@@ -625,9 +684,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:
@@ -664,10 +728,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);
}
@@ -709,10 +773,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),
@@ -764,6 +824,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);
@@ -772,6 +840,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);
@@ -805,7 +881,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);
}
@@ -813,8 +889,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;
}
}
}
@@ -828,7 +904,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;
@@ -843,8 +919,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;
@@ -897,17 +973,12 @@ function getEndText(ax, end) {
}
function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) {
- var i,
- axi,
- axRangeLinear0,
- axRangeLinearSpan;
-
- for(i = 0; i < axList.length; i++) {
- axi = axList[i];
+ 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)
@@ -920,8 +991,7 @@ function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) {
// zoom linked axes about their centers
if(linkedAxes && linkedAxes.length) {
var linkedR0Fraction = (r0Fraction + (1 - r1Fraction)) / 2;
-
- zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates);
+ zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates, [], []);
}
}
@@ -1048,15 +1118,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]) {
@@ -1065,7 +1134,7 @@ function calcLinks(gd, 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;
}
}
@@ -1082,7 +1151,7 @@ function calcLinks(gd, xaHash, yaHash) {
if(group[yID]) {
for(yLinkID in group) {
if(!(yLinkID.charAt(0) === 'x' ? xaHash : yaHash)[yLinkID]) {
- yLinks[yLinkID] = 1;
+ yLinks[yLinkID] = yID;
}
}
}
@@ -1118,6 +1187,8 @@ function calcLinks(gd, xaHash, yaHash) {
yaHash: yaHashLinked,
xaxes: xaxesLinked,
yaxes: yaxesLinked,
+ xLinks: xLinks,
+ yLinks: yLinks,
isSubplotConstrained: isSubplotConstrained
};
}
diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js
index c0f7f9b3a83..8c920ecbac0 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: {
@@ -211,6 +213,24 @@ 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 axes simultaneously in both a `scaleanchor` and a `matches` constraint',
+ 'is currently forbidden.',
+ 'Moreover, note that matching axes must have the same `type`.'
+ ].join(' ')
+ },
// ticks
tickmode: {
valType: 'enumerated',
diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js
index 8a3a05d940d..8a6f6235b10 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');
@@ -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,80 @@ 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);
+ var constraintGroups = layoutOut._axisConstraintGroups = [];
+ // similar to _axisConstraintGroups, but for matching axes
+ var matchGroups = 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 < 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) {
+ rng = axLayoutOut.range;
+ 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)];
+ rng = axLayoutOut.range;
+ autorange = axLayoutOut.autorange;
+ break;
+ }
+ }
+ // apply matching range attrs
+ for(axId in group) {
+ axLayoutOut = layoutOut[id2name(axId)];
+ if(axLayoutOut.matches) {
+ axLayoutOut.range = rng.slice();
+ axLayoutOut.autorange = autorange;
+ }
+ 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/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/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/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/axes_scaleanchor-constrain-domain-fixedrange.png b/test/image/baselines/axes_scaleanchor-constrain-domain-fixedrange.png
new file mode 100644
index 00000000000..60bd3831990
Binary files /dev/null and b/test/image/baselines/axes_scaleanchor-constrain-domain-fixedrange.png differ
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 00000000000..56599d8d2ce
Binary files /dev/null and b/test/image/baselines/axes_scaleanchor-with-matches.png differ
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 00000000000..9e208c4ae1b
Binary files /dev/null and b/test/image/baselines/hists-on-matching-axes.png differ
diff --git a/test/image/baselines/matching-categories.png b/test/image/baselines/matching-categories.png
new file mode 100644
index 00000000000..e5c61632275
Binary files /dev/null and b/test/image/baselines/matching-categories.png differ
diff --git a/test/image/baselines/splom_iris-matching.png b/test/image/baselines/splom_iris-matching.png
new file mode 100644
index 00000000000..c3d13e9bdeb
Binary files /dev/null and b/test/image/baselines/splom_iris-matching.png differ
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 00000000000..3872b87968f
Binary files /dev/null and b/test/image/baselines/splom_lower-nodiag-matching.png differ
diff --git a/test/image/mocks/axes_scaleanchor-constrain-domain-fixedrange.json b/test/image/mocks/axes_scaleanchor-constrain-domain-fixedrange.json
new file mode 100644
index 00000000000..47b6c7b3f86
--- /dev/null
+++ b/test/image/mocks/axes_scaleanchor-constrain-domain-fixedrange.json
@@ -0,0 +1,28 @@
+{
+ "data": [
+ {
+ "y": [2, 3, 4, 5, 6],
+ "x": [2, 3, 4, 5, 6],
+ "z": [
+ [0.5, 0.8, 0.6, 0.8, 0.2],
+ [0.4, 0.3, 0.7, 0.2, 0.1],
+ [0.7, 0.5, 0.9, 0.5, 0.3],
+ [0.5, 0.8, 0.6, 0.8, 0.2],
+ [0.4, 0.3, 0.7, 0.2, 0.1]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "constrain": "domain",
+ "fixedrange": true
+ },
+ "yaxis": {
+ "constrain": "domain",
+ "scaleanchor": "x",
+ "scaleratio": 1,
+ "fixedrange": true
+ }
+ }
+}
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..27d6ae1de0d
--- /dev/null
+++ b/test/image/mocks/axes_scaleanchor-with-matches.json
@@ -0,0 +1,30 @@
+{
+ "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], "xaxis": "x2", "yaxis":"y2"}
+ ],
+ "layout":{
+ "width": 500,
+ "height": 500,
+ "title": {"text": "Bottom subplot has scaleanchor constrained axes
Top subplot has matching axes"},
+ "xaxis": {
+ "constrain": "range"
+ },
+ "yaxis": {
+ "scaleanchor": "x",
+ "constrain": "range",
+ "domain": [0, 0.45],
+ "title": {"text": "1:1
x|y scale constrain range"}
+ },
+ "xaxis2": {
+ "anchor": "y2"
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "matches": "x2",
+ "domain": [0.55, 1],
+ "title": {"text": "matching x|y"}
+ },
+ "showlegend": false
+ }
+}
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"
+ }
+ }
+}
diff --git a/test/image/mocks/matching-categories.json b/test/image/mocks/matching-categories.json
new file mode 100644
index 00000000000..40554588ab7
--- /dev/null
+++ b/test/image/mocks/matching-categories.json
@@ -0,0 +1,92 @@
+{
+ "data": [
+ {
+ "x": [ "a", "b", "c" ],
+ "y": [ 1, 2, 1 ]
+ },
+ {
+ "x": [ "b", "c", "d", "e" ],
+ "y": [ 2, 1, 2, 3 ],
+ "xaxis": "x2",
+ "yaxis": "y2"
+ },
+ {
+ "x": [
+ [ 2018, 2019, 2019 ],
+ [ "q1", "q1", "q2" ]
+ ],
+ "y": [ 1, 2, 1 ],
+ "xaxis": "x3",
+ "yaxis": "y3"
+ },
+ {
+ "x": [
+ [ 2018, 2018, 2019, 2019 ],
+ [ "q1", "q2", "q1", "q2" ]
+ ],
+ "y": [ 2, 1, 2, 3 ],
+ "xaxis": "x4",
+ "yaxis": "y4"
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "domain": [ 0, 0.2 ]
+ },
+ "yaxis": {
+ "domain": [ 0.65, 1 ]
+ },
+ "xaxis2": {
+ "matches": "x",
+ "anchor": "y2",
+ "domain": [ 0.3, 1 ]
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [ 0.65, 1 ]
+ },
+ "xaxis3": {
+ "anchor": "y3",
+ "domain": [ 0, 0.2 ]
+ },
+ "yaxis3": {
+ "anchor": "x3",
+ "domain": [ 0, 0.5 ]
+ },
+ "xaxis4": {
+ "matches": "x3",
+ "anchor": "y4",
+ "domain": [ 0.3, 1 ]
+ },
+ "yaxis4": {
+ "anchor": "x4",
+ "domain": [ 0, 0.5 ]
+ },
+ "showlegend": false,
+ "margin": { "t": 40, "b": 40, "l": 20, "r": 20 },
+ "annotations": [
+ {
+ "text": "matching category x-axes",
+ "showarrow": false,
+ "xref": "paper",
+ "yref": "paper",
+ "x": 0,
+ "xanchor": "left",
+ "y": 1,
+ "yanchor": "bottom",
+ "bgcolor": "#d3d3d3"
+ },
+ {
+ "text": "matching multicategory x-axes",
+ "showarrow": false,
+ "xref": "paper",
+ "yref": "paper",
+ "x": 0,
+ "xanchor": "left",
+ "y": 0.5,
+ "yanchor": "bottom",
+ "bgcolor": "#d3d3d3"
+ }
+ ]
+ }
+}
diff --git a/test/image/mocks/splom_iris-matching.json b/test/image/mocks/splom_iris-matching.json
new file mode 100644
index 00000000000..9e98efa612c
--- /dev/null
+++ b/test/image/mocks/splom_iris-matching.json
@@ -0,0 +1,700 @@
+{
+ "data": [
+ {
+ "type": "splom",
+ "name": "Setosa",
+ "dimensions": [
+ {
+ "label": "SepalLength",
+ "values": [
+ "5.1",
+ "4.9",
+ "4.7",
+ "4.6",
+ "5.0",
+ "5.4",
+ "4.6",
+ "5.0",
+ "4.4",
+ "4.9",
+ "5.4",
+ "4.8",
+ "4.8",
+ "4.3",
+ "5.8",
+ "5.7",
+ "5.4",
+ "5.1",
+ "5.7",
+ "5.1",
+ "5.4",
+ "5.1",
+ "4.6",
+ "5.1",
+ "4.8",
+ "5.0",
+ "5.0",
+ "5.2",
+ "5.2",
+ "4.7",
+ "4.8",
+ "5.4",
+ "5.2",
+ "5.5",
+ "4.9",
+ "5.0",
+ "5.5",
+ "4.9",
+ "4.4",
+ "5.1",
+ "5.0",
+ "4.5",
+ "4.4",
+ "5.0",
+ "5.1",
+ "4.8",
+ "5.1",
+ "4.6",
+ "5.3",
+ "5.0"
+ ]
+ },
+ {
+ "label": "SepalWidth",
+ "values": [
+ "3.5",
+ "3.0",
+ "3.2",
+ "3.1",
+ "3.6",
+ "3.9",
+ "3.4",
+ "3.4",
+ "2.9",
+ "3.1",
+ "3.7",
+ "3.4",
+ "3.0",
+ "3.0",
+ "4.0",
+ "4.4",
+ "3.9",
+ "3.5",
+ "3.8",
+ "3.8",
+ "3.4",
+ "3.7",
+ "3.6",
+ "3.3",
+ "3.4",
+ "3.0",
+ "3.4",
+ "3.5",
+ "3.4",
+ "3.2",
+ "3.1",
+ "3.4",
+ "4.1",
+ "4.2",
+ "3.1",
+ "3.2",
+ "3.5",
+ "3.1",
+ "3.0",
+ "3.4",
+ "3.5",
+ "2.3",
+ "3.2",
+ "3.5",
+ "3.8",
+ "3.0",
+ "3.8",
+ "3.2",
+ "3.7",
+ "3.3"
+ ]
+ },
+ {
+ "label": "PetalLength",
+ "values": [
+ "1.4",
+ "1.4",
+ "1.3",
+ "1.5",
+ "1.4",
+ "1.7",
+ "1.4",
+ "1.5",
+ "1.4",
+ "1.5",
+ "1.5",
+ "1.6",
+ "1.4",
+ "1.1",
+ "1.2",
+ "1.5",
+ "1.3",
+ "1.4",
+ "1.7",
+ "1.5",
+ "1.7",
+ "1.5",
+ "1.0",
+ "1.7",
+ "1.9",
+ "1.6",
+ "1.6",
+ "1.5",
+ "1.4",
+ "1.6",
+ "1.6",
+ "1.5",
+ "1.5",
+ "1.4",
+ "1.5",
+ "1.2",
+ "1.3",
+ "1.5",
+ "1.3",
+ "1.5",
+ "1.3",
+ "1.3",
+ "1.3",
+ "1.6",
+ "1.9",
+ "1.4",
+ "1.6",
+ "1.4",
+ "1.5",
+ "1.4"
+ ]
+ },
+ {
+ "label": "PetalWidth",
+ "values": [
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.4",
+ "0.3",
+ "0.2",
+ "0.2",
+ "0.1",
+ "0.2",
+ "0.2",
+ "0.1",
+ "0.1",
+ "0.2",
+ "0.4",
+ "0.4",
+ "0.3",
+ "0.3",
+ "0.3",
+ "0.2",
+ "0.4",
+ "0.2",
+ "0.5",
+ "0.2",
+ "0.2",
+ "0.4",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.4",
+ "0.1",
+ "0.2",
+ "0.1",
+ "0.2",
+ "0.2",
+ "0.1",
+ "0.2",
+ "0.2",
+ "0.3",
+ "0.3",
+ "0.2",
+ "0.6",
+ "0.4",
+ "0.3",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2"
+ ]
+ }
+ ],
+ "marker": {
+ "color": "red"
+ }
+ },
+ {
+ "type": "splom",
+ "name": "Versicolor",
+ "dimensions": [
+ {
+ "label": "SepalLength",
+ "values": [
+ "7.0",
+ "6.4",
+ "6.9",
+ "5.5",
+ "6.5",
+ "5.7",
+ "6.3",
+ "4.9",
+ "6.6",
+ "5.2",
+ "5.0",
+ "5.9",
+ "6.0",
+ "6.1",
+ "5.6",
+ "6.7",
+ "5.6",
+ "5.8",
+ "6.2",
+ "5.6",
+ "5.9",
+ "6.1",
+ "6.3",
+ "6.1",
+ "6.4",
+ "6.6",
+ "6.8",
+ "6.7",
+ "6.0",
+ "5.7",
+ "5.5",
+ "5.5",
+ "5.8",
+ "6.0",
+ "5.4",
+ "6.0",
+ "6.7",
+ "6.3",
+ "5.6",
+ "5.5",
+ "5.5",
+ "6.1",
+ "5.8",
+ "5.0",
+ "5.6",
+ "5.7",
+ "5.7",
+ "6.2",
+ "5.1",
+ "5.7"
+ ]
+ },
+ {
+ "label": "SepalWidth",
+ "values": [
+ "3.2",
+ "3.2",
+ "3.1",
+ "2.3",
+ "2.8",
+ "2.8",
+ "3.3",
+ "2.4",
+ "2.9",
+ "2.7",
+ "2.0",
+ "3.0",
+ "2.2",
+ "2.9",
+ "2.9",
+ "3.1",
+ "3.0",
+ "2.7",
+ "2.2",
+ "2.5",
+ "3.2",
+ "2.8",
+ "2.5",
+ "2.8",
+ "2.9",
+ "3.0",
+ "2.8",
+ "3.0",
+ "2.9",
+ "2.6",
+ "2.4",
+ "2.4",
+ "2.7",
+ "2.7",
+ "3.0",
+ "3.4",
+ "3.1",
+ "2.3",
+ "3.0",
+ "2.5",
+ "2.6",
+ "3.0",
+ "2.6",
+ "2.3",
+ "2.7",
+ "3.0",
+ "2.9",
+ "2.9",
+ "2.5",
+ "2.8"
+ ]
+ },
+ {
+ "label": "PetalLength",
+ "values": [
+ "4.7",
+ "4.5",
+ "4.9",
+ "4.0",
+ "4.6",
+ "4.5",
+ "4.7",
+ "3.3",
+ "4.6",
+ "3.9",
+ "3.5",
+ "4.2",
+ "4.0",
+ "4.7",
+ "3.6",
+ "4.4",
+ "4.5",
+ "4.1",
+ "4.5",
+ "3.9",
+ "4.8",
+ "4.0",
+ "4.9",
+ "4.7",
+ "4.3",
+ "4.4",
+ "4.8",
+ "5.0",
+ "4.5",
+ "3.5",
+ "3.8",
+ "3.7",
+ "3.9",
+ "5.1",
+ "4.5",
+ "4.5",
+ "4.7",
+ "4.4",
+ "4.1",
+ "4.0",
+ "4.4",
+ "4.6",
+ "4.0",
+ "3.3",
+ "4.2",
+ "4.2",
+ "4.2",
+ "4.3",
+ "3.0",
+ "4.1"
+ ]
+ },
+ {
+ "label": "PetalWidth",
+ "values": [
+ "1.4",
+ "1.5",
+ "1.5",
+ "1.3",
+ "1.5",
+ "1.3",
+ "1.6",
+ "1.0",
+ "1.3",
+ "1.4",
+ "1.0",
+ "1.5",
+ "1.0",
+ "1.4",
+ "1.3",
+ "1.4",
+ "1.5",
+ "1.0",
+ "1.5",
+ "1.1",
+ "1.8",
+ "1.3",
+ "1.5",
+ "1.2",
+ "1.3",
+ "1.4",
+ "1.4",
+ "1.7",
+ "1.5",
+ "1.0",
+ "1.1",
+ "1.0",
+ "1.2",
+ "1.6",
+ "1.5",
+ "1.6",
+ "1.5",
+ "1.3",
+ "1.3",
+ "1.3",
+ "1.2",
+ "1.4",
+ "1.2",
+ "1.0",
+ "1.3",
+ "1.2",
+ "1.3",
+ "1.3",
+ "1.1",
+ "1.3"
+ ]
+ }
+ ],
+ "marker": {
+ "color": "green"
+ }
+ },
+ {
+ "type": "splom",
+ "name": "Virginica",
+ "dimensions": [
+ {
+ "label": "SepalLength",
+ "values": [
+ "6.3",
+ "5.8",
+ "7.1",
+ "6.3",
+ "6.5",
+ "7.6",
+ "4.9",
+ "7.3",
+ "6.7",
+ "7.2",
+ "6.5",
+ "6.4",
+ "6.8",
+ "5.7",
+ "5.8",
+ "6.4",
+ "6.5",
+ "7.7",
+ "7.7",
+ "6.0",
+ "6.9",
+ "5.6",
+ "7.7",
+ "6.3",
+ "6.7",
+ "7.2",
+ "6.2",
+ "6.1",
+ "6.4",
+ "7.2",
+ "7.4",
+ "7.9",
+ "6.4",
+ "6.3",
+ "6.1",
+ "7.7",
+ "6.3",
+ "6.4",
+ "6.0",
+ "6.9",
+ "6.7",
+ "6.9",
+ "5.8",
+ "6.8",
+ "6.7",
+ "6.7",
+ "6.3",
+ "6.5",
+ "6.2",
+ "5.9"
+ ]
+ },
+ {
+ "label": "SepalWidth",
+ "values": [
+ "3.3",
+ "2.7",
+ "3.0",
+ "2.9",
+ "3.0",
+ "3.0",
+ "2.5",
+ "2.9",
+ "2.5",
+ "3.6",
+ "3.2",
+ "2.7",
+ "3.0",
+ "2.5",
+ "2.8",
+ "3.2",
+ "3.0",
+ "3.8",
+ "2.6",
+ "2.2",
+ "3.2",
+ "2.8",
+ "2.8",
+ "2.7",
+ "3.3",
+ "3.2",
+ "2.8",
+ "3.0",
+ "2.8",
+ "3.0",
+ "2.8",
+ "3.8",
+ "2.8",
+ "2.8",
+ "2.6",
+ "3.0",
+ "3.4",
+ "3.1",
+ "3.0",
+ "3.1",
+ "3.1",
+ "3.1",
+ "2.7",
+ "3.2",
+ "3.3",
+ "3.0",
+ "2.5",
+ "3.0",
+ "3.4",
+ "3.0"
+ ]
+ },
+ {
+ "label": "PetalLength",
+ "values": [
+ "6.0",
+ "5.1",
+ "5.9",
+ "5.6",
+ "5.8",
+ "6.6",
+ "4.5",
+ "6.3",
+ "5.8",
+ "6.1",
+ "5.1",
+ "5.3",
+ "5.5",
+ "5.0",
+ "5.1",
+ "5.3",
+ "5.5",
+ "6.7",
+ "6.9",
+ "5.0",
+ "5.7",
+ "4.9",
+ "6.7",
+ "4.9",
+ "5.7",
+ "6.0",
+ "4.8",
+ "4.9",
+ "5.6",
+ "5.8",
+ "6.1",
+ "6.4",
+ "5.6",
+ "5.1",
+ "5.6",
+ "6.1",
+ "5.6",
+ "5.5",
+ "4.8",
+ "5.4",
+ "5.6",
+ "5.1",
+ "5.1",
+ "5.9",
+ "5.7",
+ "5.2",
+ "5.0",
+ "5.2",
+ "5.4",
+ "5.1"
+ ]
+ },
+ {
+ "label": "PetalWidth",
+ "values": [
+ "2.5",
+ "1.9",
+ "2.1",
+ "1.8",
+ "2.2",
+ "2.1",
+ "1.7",
+ "1.8",
+ "1.8",
+ "2.5",
+ "2.0",
+ "1.9",
+ "2.1",
+ "2.0",
+ "2.4",
+ "2.3",
+ "1.8",
+ "2.2",
+ "2.3",
+ "1.5",
+ "2.3",
+ "2.0",
+ "2.0",
+ "1.8",
+ "2.1",
+ "1.8",
+ "1.8",
+ "1.8",
+ "2.1",
+ "1.6",
+ "1.9",
+ "2.0",
+ "2.2",
+ "1.5",
+ "1.4",
+ "2.3",
+ "2.4",
+ "1.8",
+ "1.8",
+ "2.1",
+ "2.4",
+ "2.3",
+ "1.9",
+ "2.3",
+ "2.5",
+ "2.3",
+ "1.9",
+ "2.0",
+ "2.3",
+ "1.8"
+ ]
+ }
+ ],
+ "marker": {
+ "color": "blue"
+ }
+ }
+ ],
+ "layout": {
+ "title": "Iris dataset splom",
+ "xaxis": {"matches": "y"},
+ "xaxis2": {"matches": "y2"},
+ "xaxis3": {"matches": "y3"},
+ "xaxis4": {"matches": "y4"},
+ "width": 600,
+ "height": 500
+ }
+}
diff --git a/test/image/mocks/splom_lower-nodiag-matching.json b/test/image/mocks/splom_lower-nodiag-matching.json
new file mode 100644
index 00000000000..5dbecdb2b30
--- /dev/null
+++ b/test/image/mocks/splom_lower-nodiag-matching.json
@@ -0,0 +1,718 @@
+{
+ "data": [
+ {
+ "type": "splom",
+ "name": "Setosa",
+ "showupperhalf": false,
+ "diagonal": {"visible": false},
+ "dimensions": [
+ {
+ "axis": {"matches": true},
+ "label": "SepalLength",
+ "values": [
+ "5.1",
+ "4.9",
+ "4.7",
+ "4.6",
+ "5.0",
+ "5.4",
+ "4.6",
+ "5.0",
+ "4.4",
+ "4.9",
+ "5.4",
+ "4.8",
+ "4.8",
+ "4.3",
+ "5.8",
+ "5.7",
+ "5.4",
+ "5.1",
+ "5.7",
+ "5.1",
+ "5.4",
+ "5.1",
+ "4.6",
+ "5.1",
+ "4.8",
+ "5.0",
+ "5.0",
+ "5.2",
+ "5.2",
+ "4.7",
+ "4.8",
+ "5.4",
+ "5.2",
+ "5.5",
+ "4.9",
+ "5.0",
+ "5.5",
+ "4.9",
+ "4.4",
+ "5.1",
+ "5.0",
+ "4.5",
+ "4.4",
+ "5.0",
+ "5.1",
+ "4.8",
+ "5.1",
+ "4.6",
+ "5.3",
+ "5.0"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "SepalWidth",
+ "values": [
+ "3.5",
+ "3.0",
+ "3.2",
+ "3.1",
+ "3.6",
+ "3.9",
+ "3.4",
+ "3.4",
+ "2.9",
+ "3.1",
+ "3.7",
+ "3.4",
+ "3.0",
+ "3.0",
+ "4.0",
+ "4.4",
+ "3.9",
+ "3.5",
+ "3.8",
+ "3.8",
+ "3.4",
+ "3.7",
+ "3.6",
+ "3.3",
+ "3.4",
+ "3.0",
+ "3.4",
+ "3.5",
+ "3.4",
+ "3.2",
+ "3.1",
+ "3.4",
+ "4.1",
+ "4.2",
+ "3.1",
+ "3.2",
+ "3.5",
+ "3.1",
+ "3.0",
+ "3.4",
+ "3.5",
+ "2.3",
+ "3.2",
+ "3.5",
+ "3.8",
+ "3.0",
+ "3.8",
+ "3.2",
+ "3.7",
+ "3.3"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "PetalLength",
+ "values": [
+ "1.4",
+ "1.4",
+ "1.3",
+ "1.5",
+ "1.4",
+ "1.7",
+ "1.4",
+ "1.5",
+ "1.4",
+ "1.5",
+ "1.5",
+ "1.6",
+ "1.4",
+ "1.1",
+ "1.2",
+ "1.5",
+ "1.3",
+ "1.4",
+ "1.7",
+ "1.5",
+ "1.7",
+ "1.5",
+ "1.0",
+ "1.7",
+ "1.9",
+ "1.6",
+ "1.6",
+ "1.5",
+ "1.4",
+ "1.6",
+ "1.6",
+ "1.5",
+ "1.5",
+ "1.4",
+ "1.5",
+ "1.2",
+ "1.3",
+ "1.5",
+ "1.3",
+ "1.5",
+ "1.3",
+ "1.3",
+ "1.3",
+ "1.6",
+ "1.9",
+ "1.4",
+ "1.6",
+ "1.4",
+ "1.5",
+ "1.4"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "PetalWidth",
+ "values": [
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.4",
+ "0.3",
+ "0.2",
+ "0.2",
+ "0.1",
+ "0.2",
+ "0.2",
+ "0.1",
+ "0.1",
+ "0.2",
+ "0.4",
+ "0.4",
+ "0.3",
+ "0.3",
+ "0.3",
+ "0.2",
+ "0.4",
+ "0.2",
+ "0.5",
+ "0.2",
+ "0.2",
+ "0.4",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.4",
+ "0.1",
+ "0.2",
+ "0.1",
+ "0.2",
+ "0.2",
+ "0.1",
+ "0.2",
+ "0.2",
+ "0.3",
+ "0.3",
+ "0.2",
+ "0.6",
+ "0.4",
+ "0.3",
+ "0.2",
+ "0.2",
+ "0.2",
+ "0.2"
+ ]
+ }
+ ],
+ "marker": {
+ "color": "red"
+ }
+ },
+ {
+ "type": "splom",
+ "name": "Versicolor",
+ "showupperhalf": false,
+ "diagonal": {"visible": false},
+ "dimensions": [
+ {
+ "axis": {"matches": true},
+ "label": "SepalLength",
+ "values": [
+ "7.0",
+ "6.4",
+ "6.9",
+ "5.5",
+ "6.5",
+ "5.7",
+ "6.3",
+ "4.9",
+ "6.6",
+ "5.2",
+ "5.0",
+ "5.9",
+ "6.0",
+ "6.1",
+ "5.6",
+ "6.7",
+ "5.6",
+ "5.8",
+ "6.2",
+ "5.6",
+ "5.9",
+ "6.1",
+ "6.3",
+ "6.1",
+ "6.4",
+ "6.6",
+ "6.8",
+ "6.7",
+ "6.0",
+ "5.7",
+ "5.5",
+ "5.5",
+ "5.8",
+ "6.0",
+ "5.4",
+ "6.0",
+ "6.7",
+ "6.3",
+ "5.6",
+ "5.5",
+ "5.5",
+ "6.1",
+ "5.8",
+ "5.0",
+ "5.6",
+ "5.7",
+ "5.7",
+ "6.2",
+ "5.1",
+ "5.7"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "SepalWidth",
+ "values": [
+ "3.2",
+ "3.2",
+ "3.1",
+ "2.3",
+ "2.8",
+ "2.8",
+ "3.3",
+ "2.4",
+ "2.9",
+ "2.7",
+ "2.0",
+ "3.0",
+ "2.2",
+ "2.9",
+ "2.9",
+ "3.1",
+ "3.0",
+ "2.7",
+ "2.2",
+ "2.5",
+ "3.2",
+ "2.8",
+ "2.5",
+ "2.8",
+ "2.9",
+ "3.0",
+ "2.8",
+ "3.0",
+ "2.9",
+ "2.6",
+ "2.4",
+ "2.4",
+ "2.7",
+ "2.7",
+ "3.0",
+ "3.4",
+ "3.1",
+ "2.3",
+ "3.0",
+ "2.5",
+ "2.6",
+ "3.0",
+ "2.6",
+ "2.3",
+ "2.7",
+ "3.0",
+ "2.9",
+ "2.9",
+ "2.5",
+ "2.8"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "PetalLength",
+ "values": [
+ "4.7",
+ "4.5",
+ "4.9",
+ "4.0",
+ "4.6",
+ "4.5",
+ "4.7",
+ "3.3",
+ "4.6",
+ "3.9",
+ "3.5",
+ "4.2",
+ "4.0",
+ "4.7",
+ "3.6",
+ "4.4",
+ "4.5",
+ "4.1",
+ "4.5",
+ "3.9",
+ "4.8",
+ "4.0",
+ "4.9",
+ "4.7",
+ "4.3",
+ "4.4",
+ "4.8",
+ "5.0",
+ "4.5",
+ "3.5",
+ "3.8",
+ "3.7",
+ "3.9",
+ "5.1",
+ "4.5",
+ "4.5",
+ "4.7",
+ "4.4",
+ "4.1",
+ "4.0",
+ "4.4",
+ "4.6",
+ "4.0",
+ "3.3",
+ "4.2",
+ "4.2",
+ "4.2",
+ "4.3",
+ "3.0",
+ "4.1"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "PetalWidth",
+ "values": [
+ "1.4",
+ "1.5",
+ "1.5",
+ "1.3",
+ "1.5",
+ "1.3",
+ "1.6",
+ "1.0",
+ "1.3",
+ "1.4",
+ "1.0",
+ "1.5",
+ "1.0",
+ "1.4",
+ "1.3",
+ "1.4",
+ "1.5",
+ "1.0",
+ "1.5",
+ "1.1",
+ "1.8",
+ "1.3",
+ "1.5",
+ "1.2",
+ "1.3",
+ "1.4",
+ "1.4",
+ "1.7",
+ "1.5",
+ "1.0",
+ "1.1",
+ "1.0",
+ "1.2",
+ "1.6",
+ "1.5",
+ "1.6",
+ "1.5",
+ "1.3",
+ "1.3",
+ "1.3",
+ "1.2",
+ "1.4",
+ "1.2",
+ "1.0",
+ "1.3",
+ "1.2",
+ "1.3",
+ "1.3",
+ "1.1",
+ "1.3"
+ ]
+ }
+ ],
+ "marker": {
+ "color": "green"
+ }
+ },
+ {
+ "type": "splom",
+ "name": "Virginica",
+ "showupperhalf": false,
+ "diagonal": {"visible": false},
+ "dimensions": [
+ {
+ "axis": {"matches": true},
+ "label": "SepalLength",
+ "values": [
+ "6.3",
+ "5.8",
+ "7.1",
+ "6.3",
+ "6.5",
+ "7.6",
+ "4.9",
+ "7.3",
+ "6.7",
+ "7.2",
+ "6.5",
+ "6.4",
+ "6.8",
+ "5.7",
+ "5.8",
+ "6.4",
+ "6.5",
+ "7.7",
+ "7.7",
+ "6.0",
+ "6.9",
+ "5.6",
+ "7.7",
+ "6.3",
+ "6.7",
+ "7.2",
+ "6.2",
+ "6.1",
+ "6.4",
+ "7.2",
+ "7.4",
+ "7.9",
+ "6.4",
+ "6.3",
+ "6.1",
+ "7.7",
+ "6.3",
+ "6.4",
+ "6.0",
+ "6.9",
+ "6.7",
+ "6.9",
+ "5.8",
+ "6.8",
+ "6.7",
+ "6.7",
+ "6.3",
+ "6.5",
+ "6.2",
+ "5.9"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "SepalWidth",
+ "values": [
+ "3.3",
+ "2.7",
+ "3.0",
+ "2.9",
+ "3.0",
+ "3.0",
+ "2.5",
+ "2.9",
+ "2.5",
+ "3.6",
+ "3.2",
+ "2.7",
+ "3.0",
+ "2.5",
+ "2.8",
+ "3.2",
+ "3.0",
+ "3.8",
+ "2.6",
+ "2.2",
+ "3.2",
+ "2.8",
+ "2.8",
+ "2.7",
+ "3.3",
+ "3.2",
+ "2.8",
+ "3.0",
+ "2.8",
+ "3.0",
+ "2.8",
+ "3.8",
+ "2.8",
+ "2.8",
+ "2.6",
+ "3.0",
+ "3.4",
+ "3.1",
+ "3.0",
+ "3.1",
+ "3.1",
+ "3.1",
+ "2.7",
+ "3.2",
+ "3.3",
+ "3.0",
+ "2.5",
+ "3.0",
+ "3.4",
+ "3.0"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "PetalLength",
+ "values": [
+ "6.0",
+ "5.1",
+ "5.9",
+ "5.6",
+ "5.8",
+ "6.6",
+ "4.5",
+ "6.3",
+ "5.8",
+ "6.1",
+ "5.1",
+ "5.3",
+ "5.5",
+ "5.0",
+ "5.1",
+ "5.3",
+ "5.5",
+ "6.7",
+ "6.9",
+ "5.0",
+ "5.7",
+ "4.9",
+ "6.7",
+ "4.9",
+ "5.7",
+ "6.0",
+ "4.8",
+ "4.9",
+ "5.6",
+ "5.8",
+ "6.1",
+ "6.4",
+ "5.6",
+ "5.1",
+ "5.6",
+ "6.1",
+ "5.6",
+ "5.5",
+ "4.8",
+ "5.4",
+ "5.6",
+ "5.1",
+ "5.1",
+ "5.9",
+ "5.7",
+ "5.2",
+ "5.0",
+ "5.2",
+ "5.4",
+ "5.1"
+ ]
+ },
+ {
+ "axis": {"matches": true},
+ "label": "PetalWidth",
+ "values": [
+ "2.5",
+ "1.9",
+ "2.1",
+ "1.8",
+ "2.2",
+ "2.1",
+ "1.7",
+ "1.8",
+ "1.8",
+ "2.5",
+ "2.0",
+ "1.9",
+ "2.1",
+ "2.0",
+ "2.4",
+ "2.3",
+ "1.8",
+ "2.2",
+ "2.3",
+ "1.5",
+ "2.3",
+ "2.0",
+ "2.0",
+ "1.8",
+ "2.1",
+ "1.8",
+ "1.8",
+ "1.8",
+ "2.1",
+ "1.6",
+ "1.9",
+ "2.0",
+ "2.2",
+ "1.5",
+ "1.4",
+ "2.3",
+ "2.4",
+ "1.8",
+ "1.8",
+ "2.1",
+ "2.4",
+ "2.3",
+ "1.9",
+ "2.3",
+ "2.5",
+ "2.3",
+ "1.9",
+ "2.0",
+ "2.3",
+ "1.8"
+ ]
+ }
+ ],
+ "marker": {
+ "color": "blue"
+ }
+ }
+ ],
+ "layout": {
+ "title": "Iris dataset splom",
+ "width": 600,
+ "height": 500,
+ "legend": {
+ "x": 1,
+ "xanchor": "right"
+ }
+ }
+}
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) {
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/axes_test.js b/test/jasmine/tests/axes_test.js
index 03eb6563cad..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 = [];
@@ -670,6 +670,85 @@ 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('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('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
@@ -697,6 +776,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 +807,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, [-1, 3]);
+ });
});
describe('constraints relayout', function() {
@@ -1072,6 +1234,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;
diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js
index aeca9bf68a0..ed33efe04e2 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');
@@ -283,76 +285,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 + '"]');
}
@@ -371,164 +303,227 @@ describe('axis zoom/pan and main plot zoom', function() {
};
}
- function checkRanges(newRanges, msg) {
- msg = msg || '';
- if(msg) msg = ' - ' + msg;
-
+ function doScroll(subplot, directions, deltaY, opts) {
return function() {
- var allRanges = {
- xaxis: initialRange.slice(),
- yaxis: initialRange.slice(),
- xaxis2: initialRange.slice(),
- yaxis2: initialRange.slice(),
- xaxis3: initialRange.slice(),
- yaxis3: initialRange.slice()
+ 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 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];
+
+ 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(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]}, '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(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(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(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(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) {
@@ -589,67 +584,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);
@@ -677,28 +651,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');
@@ -720,6 +672,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', {
@@ -727,16 +686,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]
});
@@ -744,6 +701,703 @@ 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];
+ var opts = expi[2] || {};
+
+ axNames.forEach(function(n) {
+ var msgi = n + ' - ' + msg;
+ if(!opts.skipInput) 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);
+ });
+ });
+ });
+
+ 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);
+ });
+ });
});
describe('Event data:', function() {
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],
diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js
index 3e09444efd9..f0659631cbb 100644
--- a/test/jasmine/tests/splom_test.js
+++ b/test/jasmine/tests/splom_test.js
@@ -232,6 +232,7 @@ describe('Test splom trace defaults:', function() {
expect(subplots.yaxis).toEqual(['y6', 'y7']);
expect(subplots.cartesian).toEqual(['x6y6', 'x7y6', 'x7y7']);
});
+
it('should use special `grid.xside` and `grid.yside` defaults on splom w/o lower half generated grids', function() {
var gridOut;
@@ -525,6 +526,96 @@ describe('Test splom trace defaults:', function() {
expect(fullLayout.xaxis2.type).toBe('category');
expect(fullLayout.yaxis2.type).toBe('category');
});
+
+ it('axis *matches* setting should propagate to layout axis containers', function() {
+ _supply({
+ dimensions: [
+ {values: [1, 2, 1], axis: {matches: true}},
+ {values: [-1, 2, 3], axis: {matches: true}}
+ ]
+ }, {});
+
+ var fullLayout = gd._fullLayout;
+ expect(fullLayout.xaxis.matches).toBe('y');
+ expect(fullLayout.yaxis.matches).toBe('x');
+ expect(fullLayout.xaxis2.matches).toBe('y2');
+ expect(fullLayout.yaxis2.matches).toBe('x2');
+
+ var groups = fullLayout._axisMatchGroups;
+ expect(groups.length).toBe(2);
+ expect(groups).toContain({x: 1, y: 1});
+ expect(groups).toContain({x2: 1, y2: 1});
+ });
+
+ it('axis *matches* setting should propagate to layout axis containers (lower + no-diag case)', function() {
+ _supply({
+ diagonal: {visible: false},
+ showlowerhalf: false,
+ dimensions: [
+ {values: [1, 2, 1], axis: {matches: true}},
+ {values: [-1, 2, 3], axis: {matches: true}},
+ {values: [-10, 9, 3], axis: {matches: true}}
+ ]
+ }, {});
+
+ var fullLayout = gd._fullLayout;
+ expect(fullLayout.xaxis).toBe(undefined);
+ expect(fullLayout.yaxis.matches).toBe(undefined);
+ expect(fullLayout.xaxis2.matches).toBe('y2');
+ expect(fullLayout.yaxis2.matches).toBe('x2');
+ expect(fullLayout.xaxis3.matches).toBe(undefined);
+ expect(fullLayout.yaxis3).toBe(undefined);
+
+ var groups = fullLayout._axisMatchGroups;
+ expect(groups.length).toBe(1);
+ expect(groups).toContain({x2: 1, y2: 1});
+ });
+
+ it('axis *matches* setting should propagate to layout axis containers (upper + no-diag case)', function() {
+ _supply({
+ diagonal: {visible: false},
+ showupperhalf: false,
+ dimensions: [
+ {values: [1, 2, 1], axis: {matches: true}},
+ {values: [-1, 2, 3], axis: {matches: true}},
+ {values: [-10, 9, 3], axis: {matches: true}}
+ ]
+ }, {});
+
+ var fullLayout = gd._fullLayout;
+ expect(fullLayout.xaxis.matches).toBe(undefined);
+ expect(fullLayout.yaxis).toBe(undefined);
+ expect(fullLayout.xaxis2.matches).toBe('y2');
+ expect(fullLayout.yaxis2.matches).toBe('x2');
+ expect(fullLayout.xaxis3).toBe(undefined);
+ expect(fullLayout.yaxis3.matches).toBe(undefined);
+
+ var groups = fullLayout._axisMatchGroups;
+ expect(groups.length).toBe(1);
+ expect(groups).toContain({x2: 1, y2: 1});
+ });
+
+ it('axis *matches* in layout take precedence over dimensions settings', function() {
+ _supply({
+ dimensions: [
+ {values: [1, 2, 1], axis: {matches: true}},
+ {values: [-1, 2, 3], axis: {matches: true}}
+ ]
+ }, {
+ xaxis: {},
+ xaxis2: {matches: 'x'}
+ });
+
+ var fullLayout = gd._fullLayout;
+ expect(fullLayout.xaxis.matches).toBe('y');
+ expect(fullLayout.yaxis.matches).toBe('x');
+ expect(fullLayout.xaxis2.matches).toBe('x');
+ expect(fullLayout.yaxis2.matches).toBe('x2');
+
+ var groups = fullLayout._axisMatchGroups;
+ expect(groups.length).toBe(1);
+ expect(groups).toContain({x: 1, y: 1, x2: 1, y2: 1});
+ });
});
describe('Test splom trace calc step:', function() {