diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index c483db44931..e43c0daf850 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2055,7 +2055,7 @@ function _relayout(gd, aobj) { // we're editing the (auto)range of, so we can tell the others constrained // to scale with them that it's OK for them to shrink var rangesAltered = {}; - var axId; + var axId, ax; function recordAlteredAxis(pleafPlus) { var axId = Axes.name2id(pleafPlus.split('.')[0]); @@ -2145,7 +2145,7 @@ function _relayout(gd, aobj) { // previously we did this for log <-> not-log, but now only do it // for log <-> linear if(pleaf === 'type') { - var ax = parentIn; + ax = parentIn; var toLog = parentFull.type === 'linear' && vi === 'log'; var fromLog = parentFull.type === 'log' && vi === 'linear'; @@ -2283,21 +2283,19 @@ function _relayout(gd, aobj) { } // figure out if we need to recalculate axis constraints - var constraints = fullLayout._axisConstraintGroups || []; for(axId in rangesAltered) { - for(i = 0; i < constraints.length; i++) { - var group = constraints[i]; - if(group[axId]) { - // Always recalc if we're changing constrained ranges. - // Otherwise it's possible to violate the constraints by - // specifying arbitrary ranges for all axes in the group. - // this way some ranges may expand beyond what's specified, - // as they do at first draw, to satisfy the constraints. - flags.calc = true; - for(var groupAxId in group) { - if(!rangesAltered[groupAxId]) { - Axes.getFromId(gd, groupAxId)._constraintShrinkable = true; - } + ax = Axes.getFromId(gd, axId); + var group = ax && ax._constraintGroup; + if(group) { + // Always recalc if we're changing constrained ranges. + // Otherwise it's possible to violate the constraints by + // specifying arbitrary ranges for all axes in the group. + // this way some ranges may expand beyond what's specified, + // as they do at first draw, to satisfy the constraints. + flags.calc = true; + for(var groupAxId in group) { + if(!rangesAltered[groupAxId]) { + Axes.getFromId(gd, groupAxId)._constraintShrinkable = true; } } } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 9769f214392..36d66ac20b9 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -667,57 +667,34 @@ exports.redrawReglTraces = function(gd) { }; exports.doAutoRangeAndConstraints = function(gd) { - var fullLayout = gd._fullLayout; var axList = Axes.list(gd, '', true); - var matchGroups = fullLayout._axisMatchGroups || []; - var axLookup = {}; var ax; - var axRng; + + var autoRangeDone = {}; for(var i = 0; i < axList.length; i++) { ax = axList[i]; - cleanAxisConstraints(gd, ax); - doAutoRange(gd, ax); - axLookup[ax._id] = 1; - } - - 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); - - // skip over 'missing' axes which do not pass through doAutoRange - if(!axLookup[ax._id]) continue; - // if one axis has autorange false, we're done - if(ax.autorange === false) continue groupLoop; - - axRng = Lib.simpleMap(ax.range, ax.r2l); - if(rng) { - if(rng[0] < rng[1]) { - rng[0] = Math.min(rng[0], axRng[0]); - rng[1] = Math.max(rng[1], axRng[1]); - } else { - rng[0] = Math.max(rng[0], axRng[0]); - rng[1] = Math.min(rng[1], axRng[1]); + if(!autoRangeDone[ax._id]) { + autoRangeDone[ax._id] = 1; + cleanAxisConstraints(gd, ax); + doAutoRange(gd, ax); + + // For matching axes, just propagate this autorange to the group. + // The extra arg to doAutoRange avoids recalculating the range, + // since doAutoRange by itself accounts for all matching axes. but + // there are other side-effects of doAutoRange that we still want. + var matchGroup = ax._matchGroup; + if(matchGroup) { + for(var id2 in matchGroup) { + var ax2 = Axes.getFromId(gd, id2); + doAutoRange(gd, ax2, ax.range); + autoRangeDone[id2] = 1; } - } else { - rng = axRng; } } - - for(id in group) { - ax = Axes.getFromId(gd, id); - ax.range = Lib.simpleMap(rng, ax.l2r); - ax._input.range = ax.range.slice(); - ax.setScale(); - } } + + enforceAxisConstraints(gd); }; // 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 d2c90fcb271..28c44197a0c 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -14,6 +14,8 @@ var Lib = require('../../lib'); var FP_SAFE = require('../../constants/numerical').FP_SAFE; var Registry = require('../../registry'); +var getFromId = require('./axis_ids').getFromId; + module.exports = { getAutoRange: getAutoRange, makePadFn: makePadFn, @@ -213,7 +215,7 @@ function makePadFn(ax) { return function getPad(pt) { return pt.pad + (pt.extrapad ? extrappad : 0); }; } -function concatExtremes(gd, ax) { +function concatExtremes(gd, ax, noMatch) { var axId = ax._id; var fullData = gd._fullData; var fullLayout = gd._fullLayout; @@ -242,14 +244,34 @@ function concatExtremes(gd, ax) { _concat(fullLayout.annotations || [], ax._annIndices || []); _concat(fullLayout.shapes || [], ax._shapeIndices || []); + // Include the extremes from other matched axes with this one + if(ax._matchGroup && !noMatch) { + for(var axId2 in ax._matchGroup) { + if(axId2 !== ax._id) { + var ax2 = getFromId(gd, axId2); + var extremes2 = concatExtremes(gd, ax2, true); + // convert padding on the second axis to the first with lenRatio + var lenRatio = ax._length / ax2._length; + for(j = 0; j < extremes2.min.length; j++) { + d = extremes2.min[j]; + collapseMinArray(minArray, d.val, d.pad * lenRatio, {extrapad: d.extrapad}); + } + for(j = 0; j < extremes2.max.length; j++) { + d = extremes2.max[j]; + collapseMaxArray(maxArray, d.val, d.pad * lenRatio, {extrapad: d.extrapad}); + } + } + } + } + return {min: minArray, max: maxArray}; } -function doAutoRange(gd, ax) { +function doAutoRange(gd, ax, presetRange) { ax.setScale(); if(ax.autorange) { - ax.range = getAutoRange(gd, ax); + ax.range = presetRange ? presetRange.slice() : getAutoRange(gd, ax); ax._r = ax.range.slice(); ax._rl = Lib.simpleMap(ax._r, ax.r2l); diff --git a/src/plots/cartesian/axis_ids.js b/src/plots/cartesian/axis_ids.js index dff56784fe3..db2c3705275 100644 --- a/src/plots/cartesian/axis_ids.js +++ b/src/plots/cartesian/axis_ids.js @@ -123,16 +123,6 @@ exports.idSort = function(id1, id2) { return +(id1.substr(1) || 1) - +(id2.substr(1) || 1); }; -exports.getAxisGroup = 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; -}; - /* * An axis reference (e.g., the contents at the 'xref' key of an object) might * have extra information appended. Extract the axis ID only. diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index d3c0c138152..85a4db0edf6 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -9,74 +9,280 @@ 'use strict'; var Lib = require('../../lib'); + +var autorange = require('./autorange'); var id2name = require('./axis_ids').id2name; +var layoutAttributes = require('./layout_attributes'); var scaleZoom = require('./scale_zoom'); -var makePadFn = require('./autorange').makePadFn; -var concatExtremes = require('./autorange').concatExtremes; +var setConvert = require('./set_convert'); var ALMOST_EQUAL = require('../../constants/numerical').ALMOST_EQUAL; var FROM_BL = require('../../constants/alignment').FROM_BL; -exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, opts) { - var allAxisIds = opts.allAxisIds; +exports.handleDefaults = function(layoutIn, layoutOut, opts) { + var axIds = opts.axIds; + var axHasImage = opts.axHasImage; + + // sets of axes linked by `scaleanchor` OR `matches` along with the + // scaleratios compounded together, populated in handleConstraintDefaults + var constraintGroups = layoutOut._axisConstraintGroups = []; + // similar to _axisConstraintGroups, but only matching axes + var matchGroups = layoutOut._axisMatchGroups = []; + + var i, group, axId, axName, axIn, axOut, attr, val; + + for(i = 0; i < axIds.length; i++) { + axName = id2name(axIds[i]); + axIn = layoutIn[axName]; + axOut = layoutOut[axName]; + + handleOneAxDefaults(axIn, axOut, { + axIds: axIds, + layoutOut: layoutOut, + hasImage: axHasImage[axName] + }); + } + + // save matchGroup on each matching axis + function stash(groups, stashAttr) { + for(i = 0; i < groups.length; i++) { + group = groups[i]; + for(axId in group) { + layoutOut[id2name(axId)][stashAttr] = group; + } + } + } + stash(matchGroups, '_matchGroup'); + + // If any axis in a constraint group is fixedrange, they all get fixed + // This covers matches axes, as they're now in the constraintgroup too + // and have not yet been removed (if the group is *only* matching) + for(i = 0; i < constraintGroups.length; i++) { + group = constraintGroups[i]; + for(axId in group) { + axOut = layoutOut[id2name(axId)]; + if(axOut.fixedrange) { + for(var axId2 in group) { + var axName2 = id2name(axId2); + if((layoutIn[axName2] || {}).fixedrange === false) { + Lib.warn( + 'fixedrange was specified as false for axis ' + + axName2 + ' but was overridden because another ' + + 'axis in its constraint group has fixedrange true' + ); + } + layoutOut[axName2].fixedrange = true; + } + break; + } + } + } + + // remove constraint groups that simply duplicate match groups + i = 0; + while(i < constraintGroups.length) { + group = constraintGroups[i]; + for(axId in group) { + axOut = layoutOut[id2name(axId)]; + if(axOut._matchGroup && Object.keys(axOut._matchGroup).length === Object.keys(group).length) { + constraintGroups.splice(i, 1); + i--; + } + break; + } + i++; + } + + // save constraintGroup on each constrained axis + stash(constraintGroups, '_constraintGroup'); + + // make sure `matching` axes share values of necessary attributes + // Precedence (base axis is the one that doesn't list a `matches`, ie others + // all point to it): + // (1) explicitly defined value in the base axis + // (2) explicitly defined in another axis (arbitrary order) + // (3) default in the base axis + var matchAttrs = [ + 'constrain', + 'range', + 'autorange', + 'rangemode', + 'rangebreaks', + 'categoryorder', + 'categoryarray' + ]; + var hasRange = false; + var hasDayOfWeekBreaks = false; + + function setAttrVal() { + val = axOut[attr]; + if(attr === 'rangebreaks') { + hasDayOfWeekBreaks = axOut._hasDayOfWeekBreaks; + } + } + + for(i = 0; i < matchGroups.length; i++) { + group = matchGroups[i]; + + // find 'matching' range attrs + for(var j = 0; j < matchAttrs.length; j++) { + attr = matchAttrs[j]; + val = null; + var baseAx; + for(axId in group) { + axName = id2name(axId); + axIn = layoutIn[axName]; + axOut = layoutOut[axName]; + if(!(attr in axOut)) { + continue; + } + if(!axOut.matches) { + baseAx = axOut; + // top priority: explicit value in base axis + if(attr in axIn) { + setAttrVal(); + break; + } + } + if(val === null && attr in axIn) { + // second priority: first explicit value in another axis + setAttrVal(); + } + } + + // special logic for coupling of range and autorange + // if nobody explicitly specifies autorange, but someone does + // explicitly specify range, autorange must be disabled. + if(attr === 'range' && val) { + hasRange = true; + } + if(attr === 'autorange' && val === null && hasRange) { + val = false; + } + + if(val === null && attr in baseAx) { + // fallback: default value in base axis + val = baseAx[attr]; + } + // but we still might not have a value, which is fine. + if(val !== null) { + for(axId in group) { + axOut = layoutOut[id2name(axId)]; + axOut[attr] = attr === 'range' ? val.slice() : val; + + if(attr === 'rangebreaks') { + axOut._hasDayOfWeekBreaks = hasDayOfWeekBreaks; + setConvert(axOut, layoutOut); + } + } + } + } + } +}; + +function handleOneAxDefaults(axIn, axOut, opts) { + var axIds = opts.axIds; var layoutOut = opts.layoutOut; - var scaleanchorDflt = opts.scaleanchorDflt; - var constrainDflt = opts.constrainDflt; + var hasImage = opts.hasImage; var constraintGroups = layoutOut._axisConstraintGroups; var matchGroups = layoutOut._axisMatchGroups; - var axId = containerOut._id; + var axId = axOut._id; var axLetter = axId.charAt(0); var splomStash = ((layoutOut._splomAxes || {})[axLetter] || {})[axId] || {}; - var thisID = containerOut._id; - var letter = thisID.charAt(0); + var thisID = axOut._id; + var isX = thisID.charAt(0) === 'x'; + + // Clear _matchGroup & _constraintGroup so relinkPrivateKeys doesn't keep + // an old one around. If this axis is in a group we'll set this again later + axOut._matchGroup = null; + axOut._constraintGroup = null; + + function coerce(attr, dflt) { + return Lib.coerce(axIn, axOut, layoutAttributes, attr, dflt); + } // coerce the constraint mechanics even if this axis has no scaleanchor // because it may be the anchor of another axis. - var constrain = coerce('constrain', constrainDflt); - Lib.coerce(containerIn, containerOut, { + coerce('constrain', hasImage ? 'domain' : 'range'); + Lib.coerce(axIn, axOut, { constraintoward: { valType: 'enumerated', - values: letter === 'x' ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'], - dflt: letter === 'x' ? 'center' : 'middle' + values: isX ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'], + dflt: isX ? 'center' : 'middle' } }, 'constraintoward'); - var matches, matchOpts; + // 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 axIds to enforce this, also matching axis types. + var thisType = axOut.type; + var i, idi; - if((containerIn.matches || splomStash.matches) && !containerOut.fixedrange) { - matchOpts = getConstraintOpts(matchGroups, thisID, allAxisIds, layoutOut); - matches = Lib.coerce(containerIn, containerOut, { + var linkableAxes = []; + for(i = 0; i < axIds.length; i++) { + idi = axIds[i]; + if(idi === thisID) continue; + + var axi = layoutOut[id2name(idi)]; + if(axi.type === thisType) { + linkableAxes.push(idi); + } + } + + var thisGroup = getConstraintGroup(constraintGroups, thisID); + if(thisGroup) { + var linkableAxesNoLoops = []; + for(i = 0; i < linkableAxes.length; i++) { + idi = linkableAxes[i]; + if(!thisGroup[idi]) linkableAxesNoLoops.push(idi); + } + linkableAxes = linkableAxesNoLoops; + } + + var canLink = linkableAxes.length; + + var matches, scaleanchor; + + if(canLink && (axIn.matches || splomStash.matches)) { + matches = Lib.coerce(axIn, axOut, { matches: { valType: 'enumerated', - values: matchOpts.linkableAxes || [], - dflt: splomStash.matches + values: linkableAxes, + dflt: linkableAxes.indexOf(splomStash.matches) !== -1 ? splomStash.matches : undefined } }, 'matches'); } - // 'matches' wins over 'scaleanchor' (for now) - var scaleanchor, scaleOpts; - - if(!matches && - !(containerOut.fixedrange && constrain !== 'domain') && - (containerIn.scaleanchor || scaleanchorDflt) - ) { - scaleOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut, constrain); - scaleanchor = Lib.coerce(containerIn, containerOut, { + // 'matches' wins over 'scaleanchor' - each axis can only specify one + // constraint, but you can chain matches and scaleanchor constraints by + // specifying them in separate axes. + var scaleanchorDflt = hasImage && !isX ? axOut.anchor : undefined; + if(canLink && !matches && (axIn.scaleanchor || scaleanchorDflt)) { + scaleanchor = Lib.coerce(axIn, axOut, { scaleanchor: { valType: 'enumerated', - values: scaleOpts.linkableAxes || [] + values: linkableAxes } }, 'scaleanchor', scaleanchorDflt); } 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.'); + axOut._matchGroup = updateConstraintGroups(matchGroups, thisID, matches, 1); + + // Also include match constraints in the scale groups + var matchedAx = layoutOut[id2name(matches)]; + var matchRatio = extent(layoutOut, axOut) / extent(layoutOut, matchedAx); + if(isX !== (matches.charAt(0) === 'x')) { + // We don't yet know the actual scale ratio of x/y matches constraints, + // due to possible automargins, so just leave a placeholder for this: + // 'x' means "x size over y size", 'y' means the inverse. + // in principle in the constraint group you could get multiple of these. + matchRatio = (isX ? 'x' : 'y') + matchRatio; + } + updateConstraintGroups(constraintGroups, thisID, matches, matchRatio); + } else if(axIn.matches && axIds.indexOf(axIn.matches) !== -1) { + Lib.warn('ignored ' + axOut._name + '.matches: "' + + axIn.matches + '" to avoid an infinite loop'); } if(scaleanchor) { @@ -87,64 +293,39 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, o // 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(!scaleratio) scaleratio = axOut.scaleratio = 1; + + updateConstraintGroups(constraintGroups, thisID, scaleanchor, scaleratio); + } else if(axIn.scaleanchor && axIds.indexOf(axIn.scaleanchor) !== -1) { + Lib.warn('ignored ' + axOut._name + '.scaleanchor: "' + + axIn.scaleanchor + '" to avoid either an infinite loop ' + + 'and possibly inconsistent scaleratios, or because 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); - } - } - } +function extent(layoutOut, ax) { + var domain = ax.domain; + if(!domain) { + // at this point overlaying axes haven't yet inherited their main domains + // TODO: constrain: domain with overlaying axes is likely a bug. + domain = layoutOut[id2name(ax.overlaying)].domain; } + return domain[1] - domain[0]; +} - for(i = 0; i < groups.length; i++) { +function getConstraintGroup(groups, thisID) { + for(var 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 groups[i]; } } - - return {linkableAxes: linkableAxes, thisGroup: null}; + return null; } /* * Add this axis to the axis constraint groups, which is the collection - * of axes that are all constrained together on scale. + * of axes that are all constrained together on scale (or matching). * * constraintGroups: a list of objects. each object is * {axis_id: scale_within_group}, where scale_within_group is @@ -153,12 +334,14 @@ function getConstraintOpts(groups, thisID, allAxisIds, layoutOut, constrain) { * * 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 + * thatID: the id of the axis to scale it with + * scaleratio: the ratio of this axis to the thatID axis */ -function updateConstraintGroups(constraintGroups, thisGroup, thisID, scaleanchor, scaleratio) { +function updateConstraintGroups(constraintGroups, thisID, thatID, scaleratio) { var i, j, groupi, keyj, thisGroupIndex; + var thisGroup = getConstraintGroup(constraintGroups, thisID); + if(thisGroup === null) { thisGroup = {}; thisGroup[thisID] = 1; @@ -171,38 +354,109 @@ function updateConstraintGroups(constraintGroups, thisGroup, thisID, scaleanchor 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. + // about the thatID 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]; + if(i !== thisGroupIndex && groupi[thatID]) { + var baseScale = groupi[thatID]; for(j = 0; j < thisGroupKeys.length; j++) { keyj = thisGroupKeys[j]; - groupi[keyj] = baseScale * scaleratio * thisGroup[keyj]; + groupi[keyj] = multiplyScales(baseScale, multiplyScales(scaleratio, thisGroup[keyj])); } constraintGroups.splice(thisGroupIndex, 1); return; } } - // otherwise, we insert the new scaleanchor axis as the base scale (1) + // otherwise, we insert the new thatID 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; + var key = thisGroupKeys[j]; + thisGroup[key] = multiplyScales(scaleratio, thisGroup[key]); } } - thisGroup[scaleanchor] = 1; + thisGroup[thatID] = 1; +} + +// scales may be numbers or 'x1.3', 'yy4.5' etc to multiply by as-yet-unknown +// ratios between x and y plot sizes n times +function multiplyScales(a, b) { + var aPrefix = ''; + var bPrefix = ''; + var aLen, bLen; + + if(typeof a === 'string') { + aPrefix = a.match(/^[xy]*/)[0]; + aLen = aPrefix.length; + a = +a.substr(aLen); + } + + if(typeof b === 'string') { + bPrefix = b.match(/^[xy]*/)[0]; + bLen = bPrefix.length; + b = +b.substr(bLen); + } + + var c = a * b; + + // just two numbers + if(!aLen && !bLen) { + return c; + } + + // one or more prefixes of the same type + if(!aLen || !bLen || aPrefix.charAt(0) === bPrefix.charAt(0)) { + return aPrefix + bPrefix + (a * b); + } + + // x and y cancel each other out exactly - back to a number + if(aLen === bLen) { + return c; + } + + // partial cancelation of prefixes + return (aLen > bLen ? aPrefix.substr(bLen) : bPrefix.substr(aLen)) + c; +} + +function finalRatios(group, fullLayout) { + var size = fullLayout._size; + var yRatio = size.h / size.w; + var out = {}; + var keys = Object.keys(group); + for(var i = 0; i < keys.length; i++) { + var key = keys[i]; + var val = group[key]; + + if(typeof val === 'string') { + var prefix = val.match(/^[xy]*/)[0]; + var pLen = prefix.length; + val = +val.substr(pLen); + var mult = prefix.charAt(0) === 'y' ? yRatio : (1 / yRatio); + for(var j = 0; j < pLen; j++) { + val *= mult; + } + } + + out[key] = val; + } + return out; } exports.enforce = function enforce(gd) { var fullLayout = gd._fullLayout; var constraintGroups = fullLayout._axisConstraintGroups || []; - var i, j, axisID, ax, normScale, mode, factor; + var i, j, group, axisID, ax, normScale, mode, factor; + // matching constraints are handled in the autorange code when autoranged, + // or in the supplyDefaults code when explicitly ranged. + // now we just need to handle scaleanchor constraints + // matches constraints that chain with scaleanchor constraints are included + // here too, but because matches has already been satisfied, + // any changes here should preserve that. for(i = 0; i < constraintGroups.length; i++) { - var group = constraintGroups[i]; + group = finalRatios(constraintGroups[i], fullLayout); var axisIDs = Object.keys(group); var minScale = Infinity; @@ -311,11 +565,11 @@ exports.enforce = function enforce(gd) { // *are* expanding to the full domain var outerMin = rangeCenter - halfRange * factor * 1.0001; var outerMax = rangeCenter + halfRange * factor * 1.0001; - var getPad = makePadFn(ax); + var getPad = autorange.makePadFn(ax); updateDomain(ax, factor); var m = Math.abs(ax._m); - var extremes = concatExtremes(gd, ax); + var extremes = autorange.concatExtremes(gd, ax); var minArray = extremes.min; var maxArray = extremes.max; var newVal; @@ -351,6 +605,16 @@ exports.enforce = function enforce(gd) { } }; +exports.getAxisGroup = 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; +}; + // For use before autoranging, check if this axis was previously constrained // by domain but no longer is exports.clean = function clean(gd, ax) { diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 8a77be91f93..e27c98c6063 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -125,10 +125,11 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { yActive = isDirectionActive(yaxes, ns); allFixedRanges = !yActive && !xActive; - 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; + links = calcLinks(gd, gd._fullLayout._axisConstraintGroups, xaHash, yaHash, matches); + var spConstrained = links.isSubplotConstrained || matches.isSubplotConstrained; + editX = ew || spConstrained; + editY = ns || spConstrained; var fullLayout = gd._fullLayout; hasScatterGl = fullLayout._has('scattergl'); @@ -567,6 +568,22 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { gd._fullLayout._replotting = true; if(xActive === 'ew' || yActive === 'ns') { + var spDx = xActive ? -dx : 0; + var spDy = yActive ? -dy : 0; + if(matches.isSubplotConstrained) { + if(xActive && yActive) { + var frac = (dx / pw - dy / ph) / 2; + dx = frac * pw; + dy = -frac * ph; + spDx = -dx; + spDy = -dy; + } + if(yActive) { + spDx = -spDy * pw / ph; + } else { + spDy = -spDx * ph / pw; + } + } if(xActive) { dragAxList(xaxes, dx); updateMatchedAxRange('x'); @@ -575,7 +592,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { dragAxList(yaxes, dy); updateMatchedAxRange('y'); } - updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); + updateSubplots([spDx, spDy, pw, ph]); ticksAndAnnotations(); gd.emit('plotly_relayouting', updates); return; @@ -606,15 +623,17 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { (movedAx._rl[end] - movedAx._rl[otherEnd]); } - if(links.isSubplotConstrained && xActive && yActive) { + var dxySign = ((xActive === 'w') === (yActive === 'n')) ? 1 : -1; + if(xActive && yActive && (links.isSubplotConstrained || matches.isSubplotConstrained)) { // dragging a corner of a constrained subplot: // respect the fixed corner, but harmonize dx and dy - var dxySign = ((xActive === 'w') === (yActive === 'n')) ? 1 : -1; var dxyFraction = (dx / pw + dxySign * dy / ph) / 2; dx = dxyFraction * pw; dy = dxySign * dxyFraction * ph; } + var xStart, yStart; + if(xActive === 'w') dx = dz(xaxes, 0, dx); else if(xActive === 'e') dx = dz(xaxes, 1, -dx); else if(!xActive) dx = 0; @@ -623,12 +642,16 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { else if(yActive === 's') dy = dz(yaxes, 0, -dy); else if(!yActive) dy = 0; - var xStart = (xActive === 'w') ? dx : 0; - var yStart = (yActive === 'n') ? dy : 0; + xStart = (xActive === 'w') ? dx : 0; + yStart = (yActive === 'n') ? dy : 0; - if(links.isSubplotConstrained) { + if( + (links.isSubplotConstrained && !matches.isSubplotConstrained) || + // NW or SE on matching axes - create a symmetric zoom + (matches.isSubplotConstrained && xActive && yActive && dxySign > 0) + ) { var i; - if(!xActive && yActive.length === 1) { + if(matches.isSubplotConstrained || (!xActive && yActive.length === 1)) { // dragging one end of the y axis of a constrained subplot // scale the other axis the same about its middle for(i = 0; i < xaxes.length; i++) { @@ -638,7 +661,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { dx = dy * pw / ph; xStart = dx / 2; } - if(!yActive && xActive.length === 1) { + if(matches.isSubplotConstrained || (!yActive && xActive.length === 1)) { for(i = 0; i < yaxes.length; i++) { yaxes[i].range = yaxes[i]._r.slice(); scaleZoom(yaxes[i], 1 - dx / pw); @@ -648,9 +671,24 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } - updateMatchedAxRange('x'); - updateMatchedAxRange('y'); - updateSubplots([xStart, yStart, pw - dx, ph - dy]); + if(!matches.isSubplotConstrained || !yActive) { + updateMatchedAxRange('x'); + } + if(!matches.isSubplotConstrained || !xActive) { + updateMatchedAxRange('y'); + } + var xSize = pw - dx; + var ySize = ph - dy; + if(matches.isSubplotConstrained && !(xActive && yActive)) { + if(xActive) { + yStart = xStart ? 0 : (dx * ph / pw); + ySize = xSize * ph / pw; + } else { + xStart = yStart ? 0 : (dy * pw / ph); + xSize = ySize * pw / ph; + } + } + updateSubplots([xStart, yStart, xSize, ySize]); ticksAndAnnotations(); gd.emit('plotly_relayouting', updates); } @@ -850,15 +888,15 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { xa = sp.xaxis; ya = sp.yaxis; - var editX2 = editX && !xa.fixedrange && xaHash[xa._id]; - var editY2 = editY && !ya.fixedrange && yaHash[ya._id]; + var editX2 = (editX || matches.isSubplotConstrained) && !xa.fixedrange && xaHash[xa._id]; + var editY2 = (editY || matches.isSubplotConstrained) && !ya.fixedrange && yaHash[ya._id]; var xScaleFactor2, yScaleFactor2; var clipDx, clipDy; if(editX2) { xScaleFactor2 = xScaleFactor; - clipDx = ew ? viewBox[0] : getShift(xa, xScaleFactor2); + clipDx = ew || matches.isSubplotConstrained ? viewBox[0] : getShift(xa, xScaleFactor2); } else if(matches.xaHash[xa._id]) { xScaleFactor2 = xScaleFactor; clipDx = viewBox[0] * xa._length / xa0._length; @@ -874,7 +912,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(editY2) { yScaleFactor2 = yScaleFactor; - clipDy = ns ? viewBox[1] : getShift(ya, yScaleFactor2); + clipDy = ns || matches.isSubplotConstrained ? viewBox[1] : getShift(ya, yScaleFactor2); } else if(matches.yaHash[ya._id]) { yScaleFactor2 = yScaleFactor; clipDy = viewBox[1] * ya._length / ya0._length; @@ -1164,11 +1202,13 @@ function xyCorners(box) { 'h' + clen + 'v3h-' + (clen + 3) + 'Z'; } -function calcLinks(gd, groups, xaHash, yaHash) { +function calcLinks(gd, groups, xaHash, yaHash, exclude) { var isSubplotConstrained = false; var xLinks = {}; var yLinks = {}; var xID, yID, xLinkID, yLinkID; + var xExclude = (exclude || {}).xaHash; + var yExclude = (exclude || {}).yaHash; for(var i = 0; i < groups.length; i++) { var group = groups[i]; @@ -1179,14 +1219,22 @@ function calcLinks(gd, groups, xaHash, yaHash) { // dragging them, so we know to scale these axes automatically too // to match the changes in the dragged x axes for(xLinkID in group) { - if(!(xLinkID.charAt(0) === 'x' ? xaHash : yaHash)[xLinkID]) { + if( + !(exclude && (xExclude[xLinkID] || yExclude[xLinkID])) && + !(xLinkID.charAt(0) === 'x' ? xaHash : yaHash)[xLinkID] + ) { xLinks[xLinkID] = xID; } } // check if the x and y axes of THIS drag are linked for(yID in yaHash) { - if(group[yID]) isSubplotConstrained = true; + if( + !(exclude && (xExclude[yID] || yExclude[yID])) && + group[yID] + ) { + isSubplotConstrained = true; + } } } } @@ -1196,7 +1244,10 @@ function calcLinks(gd, groups, xaHash, yaHash) { for(yID in yaHash) { if(group[yID]) { for(yLinkID in group) { - if(!(yLinkID.charAt(0) === 'x' ? xaHash : yaHash)[yLinkID]) { + if( + !(exclude && (xExclude[yLinkID] || yExclude[yLinkID])) && + !(yLinkID.charAt(0) === 'x' ? xaHash : yaHash)[yLinkID] + ) { yLinks[yLinkID] = yID; } } diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 43d8d2c2fdc..6c92547f86e 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -335,6 +335,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) purgeSubplotLayers(oldFullLayout._cartesianlayer.selectAll('.subplot'), oldFullLayout); oldFullLayout._defs.selectAll('.axesclip').remove(); delete oldFullLayout._axisConstraintGroups; + delete oldFullLayout._axisMatchGroups; } else if(oldSubplotList.cartesian) { // otherwise look for subplots we need to remove diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 9251e2cd50b..ef6b4205e15 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -223,13 +223,13 @@ module.exports = { constrain: { valType: 'enumerated', values: ['range', 'domain'], - dflt: 'range', role: 'info', editType: 'plot', description: [ 'If this axis needs to be compressed (either due to its own `scaleanchor` and', '`scaleratio` or those of the other axis), determines how that happens:', - 'by increasing the *range* (default), or by decreasing the *domain*.' + 'by increasing the *range*, or by decreasing the *domain*.', + 'Default is *domain* for axes containing image traces, *range* otherwise.' ].join(' ') }, // constraintoward: not used directly, just put here for reference diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 6ce3c069db1..bc43f6fcde4 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -19,7 +19,7 @@ var basePlotLayoutAttributes = require('../layout_attributes'); var layoutAttributes = require('./layout_attributes'); var handleTypeDefaults = require('./type_defaults'); var handleAxisDefaults = require('./axis_defaults'); -var handleConstraintDefaults = require('./constraints').handleConstraintDefaults; +var constraints = require('./constraints'); var handlePositionDefaults = require('./position_defaults'); var axisIds = require('./axis_ids'); @@ -378,96 +378,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // 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 - var constraintGroups = layoutOut._axisConstraintGroups = []; - // similar to _axisConstraintGroups, but for matching axes - var matchGroups = layoutOut._axisMatchGroups = []; - // make sure to include 'missing' axes here - var allAxisIdsIncludingMissing = allAxisIds.concat(missingMatchedAxisIds); - var axNamesIncludingMissing = axNames.concat(Lib.simpleMap(missingMatchedAxisIds, id2name)); - - for(i = 0; i < axNamesIncludingMissing.length; i++) { - axName = axNamesIncludingMissing[i]; - axLetter = axName.charAt(0); - axLayoutIn = layoutIn[axName]; - axLayoutOut = layoutOut[axName]; - - var scaleanchorDflt; - if(axLetter === 'y' && !axLayoutIn.hasOwnProperty('scaleanchor') && axHasImage[axName]) { - scaleanchorDflt = axLayoutOut.anchor; - } else { - scaleanchorDflt = undefined; - } - - var constrainDflt; - if(!axLayoutIn.hasOwnProperty('constrain') && axHasImage[axName]) { - constrainDflt = 'domain'; - } else { - constrainDflt = undefined; - } - - handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, { - allAxisIds: allAxisIdsIncludingMissing, - layoutOut: layoutOut, - scaleanchorDflt: scaleanchorDflt, - constrainDflt: constrainDflt - }); - } - - for(i = 0; i < matchGroups.length; i++) { - var group = matchGroups[i]; - var rng = null; - var autorange = null; - - // 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); - } - } - } - } - } - } - } + constraints.handleDefaults(layoutIn, layoutOut, { + axIds: allAxisIds.concat(missingMatchedAxisIds).sort(axisIds.idSort), + axHasImage: axHasImage + }); }; diff --git a/src/plots/cartesian/scale_zoom.js b/src/plots/cartesian/scale_zoom.js index 3cdc8b4495f..db6fbfad9a4 100644 --- a/src/plots/cartesian/scale_zoom.js +++ b/src/plots/cartesian/scale_zoom.js @@ -23,4 +23,5 @@ module.exports = function scaleZoom(ax, factor, centerFraction) { ax.l2r(center + (rangeLinear[0] - center) * factor), ax.l2r(center + (rangeLinear[1] - center) * factor) ]; + ax.setScale(); }; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index af34c0febe1..5d55e06fd73 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -368,17 +368,12 @@ 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); - } - } + var group = ax._matchGroup; + if(group && ax._categories.length === 0) { + for(var axId2 in group) { + if(axId2 !== axId) { + var ax2 = fullLayout[axisIds.id2name(axId2)]; + traceIndices = traceIndices.concat(ax2._traceIndices); } } } @@ -882,38 +877,26 @@ module.exports = function setConvert(ax, fullLayout) { // should skip if not category nor multicategory ax.clearCalc = function() { - var matchGroups = fullLayout._axisMatchGroups; - - if(matchGroups && matchGroups.length) { - var found = false; - - for(var i = 0; i < matchGroups.length; i++) { - var group = matchGroups[i]; - - if(group[axId]) { - found = true; - 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._emptyCategories(); - } + var group = ax._matchGroup; + if(group) { + 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(!found) ax._emptyCategories(); + + if(categories && categoriesMap) { + ax._categories = categories; + ax._categoriesMap = categoriesMap; + } else { + ax._emptyCategories(); + } } else { ax._emptyCategories(); } diff --git a/src/plots/plots.js b/src/plots/plots.js index bccb37b4130..4d26e8e9331 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -3234,12 +3234,8 @@ function sortAxisCategoriesByValue(axList, gd) { function setupAxisCategories(axList, fullData, fullLayout) { var axLookup = {}; - var i, ax, axId; - - for(i = 0; i < axList.length; i++) { - ax = axList[i]; - axId = ax._id; + function setupOne(ax) { ax.clearCalc(); if(ax.type === 'multicategory') { ax.setupMultiCategory(fullData); @@ -3248,13 +3244,14 @@ function setupAxisCategories(axList, fullData, fullLayout) { axLookup[ax._id] = 1; } + Lib.simpleMap(axList, setupOne); + // look into match groups for 'missing' axes var matchGroups = fullLayout._axisMatchGroups || []; - for(i = 0; i < matchGroups.length; i++) { - for(axId in matchGroups[i]) { + for(var i = 0; i < matchGroups.length; i++) { + for(var axId in matchGroups[i]) { if(!axLookup[axId]) { - ax = fullLayout[axisIDs.id2name(axId)]; - ax.clearCalc(); + setupOne(fullLayout[axisIDs.id2name(axId)]); } } } diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js index 1bb5dda2f81..719174b555d 100644 --- a/src/traces/bar/cross_trace_calc.js +++ b/src/traces/bar/cross_trace_calc.js @@ -14,7 +14,7 @@ var BADNUM = require('../../constants/numerical').BADNUM; var Registry = require('../../registry'); var Axes = require('../../plots/cartesian/axes'); -var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup; +var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup; var Sieve = require('./sieve.js'); /* diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index f9393cf766d..593a1670cff 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -15,7 +15,7 @@ var Registry = require('../../registry'); var handleXYDefaults = require('../scatter/xy_defaults'); var handlePeriodDefaults = require('../scatter/period_defaults'); var handleStyleDefaults = require('./style_defaults'); -var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup; +var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup; var attributes = require('./attributes'); var coerceFont = Lib.coerceFont; diff --git a/src/traces/box/cross_trace_calc.js b/src/traces/box/cross_trace_calc.js index 72b57856cfb..165ef398627 100644 --- a/src/traces/box/cross_trace_calc.js +++ b/src/traces/box/cross_trace_calc.js @@ -10,7 +10,7 @@ var Axes = require('../../plots/cartesian/axes'); var Lib = require('../../lib'); -var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup; +var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup; var orientations = ['v', 'h']; diff --git a/src/traces/histogram/cross_trace_defaults.js b/src/traces/histogram/cross_trace_defaults.js index 5a9edd34cdf..d900b44f95c 100644 --- a/src/traces/histogram/cross_trace_defaults.js +++ b/src/traces/histogram/cross_trace_defaults.js @@ -15,7 +15,7 @@ var traceIs = require('../../registry').traceIs; var handleGroupingDefaults = require('../bar/defaults').handleGroupingDefaults; var nestedProperty = Lib.nestedProperty; -var getAxisGroup = axisIds.getAxisGroup; +var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup; var BINATTRS = [ {aStr: {x: 'xbins.start', y: 'ybins.start'}, name: 'start'}, diff --git a/test/image/baselines/axes_breaks-matches.png b/test/image/baselines/axes_breaks-matches.png new file mode 100644 index 00000000000..7d5fb86addc Binary files /dev/null and b/test/image/baselines/axes_breaks-matches.png differ diff --git a/test/image/baselines/axes_chain_scaleanchor_matches.png b/test/image/baselines/axes_chain_scaleanchor_matches.png new file mode 100644 index 00000000000..026c906cb55 Binary files /dev/null and b/test/image/baselines/axes_chain_scaleanchor_matches.png differ diff --git a/test/image/baselines/axes_chain_scaleanchor_matches2.png b/test/image/baselines/axes_chain_scaleanchor_matches2.png new file mode 100644 index 00000000000..33bed3a95a6 Binary files /dev/null and b/test/image/baselines/axes_chain_scaleanchor_matches2.png differ diff --git a/test/image/baselines/axes_linked_date_autorange.png b/test/image/baselines/axes_linked_date_autorange.png index 46b9e97cb94..3a2da78fe9f 100644 Binary files a/test/image/baselines/axes_linked_date_autorange.png and b/test/image/baselines/axes_linked_date_autorange.png differ diff --git a/test/image/baselines/axes_scaleanchor-with-matches.png b/test/image/baselines/axes_scaleanchor-with-matches.png index 56599d8d2ce..29e4065ffd2 100644 Binary files a/test/image/baselines/axes_scaleanchor-with-matches.png and b/test/image/baselines/axes_scaleanchor-with-matches.png differ diff --git a/test/image/baselines/gl2d_marker_coloraxis.png b/test/image/baselines/gl2d_marker_coloraxis.png index e1ec515186a..4724448d777 100644 Binary files a/test/image/baselines/gl2d_marker_coloraxis.png and b/test/image/baselines/gl2d_marker_coloraxis.png differ diff --git a/test/image/baselines/matching-categories.png b/test/image/baselines/matching-categories.png index e5c61632275..31d2a3c52c7 100644 Binary files a/test/image/baselines/matching-categories.png and b/test/image/baselines/matching-categories.png differ diff --git a/test/image/mocks/axes_breaks-matches.json b/test/image/mocks/axes_breaks-matches.json new file mode 100644 index 00000000000..4e51c5ed55c --- /dev/null +++ b/test/image/mocks/axes_breaks-matches.json @@ -0,0 +1,71 @@ +{ + "data": [ + { + "mode": "lines+markers", + "x": [ + "2020-05-09", + "2020-05-10", + "2020-05-11", + "2020-05-12", + "2020-05-13", + "2020-05-14", + "2020-05-15", + "2020-05-16", + "2020-05-17", + "2020-05-18", + "2020-05-19", + "2020-05-20", + "2020-05-21", + "2020-05-22", + "2020-05-23", + "2020-05-24", + "2020-05-25", + "2020-05-26", + "2020-05-27", + "2020-05-28" + ] + }, + { + "mode": "lines+markers", + "xaxis": "x2", + "yaxis": "y2", + "x": [ + "2020-05-09", + "2020-05-10", + "2020-05-11", + "2020-05-12", + "2020-05-13", + "2020-05-14", + "2020-05-15", + "2020-05-16", + "2020-05-17", + "2020-05-18", + "2020-05-19", + "2020-05-20", + "2020-05-21", + "2020-05-22", + "2020-05-23", + "2020-05-24", + "2020-05-25", + "2020-05-26", + "2020-05-27", + "2020-05-28" + ], + "y0": 10 + } + ], + "layout": { + "showlegend": false, + "width": 600, + "height": 400, + "xaxis": {"domain": [0, 0.45]}, + "xaxis2": { + "rangebreaks": [{"bounds": ["sat", "mon"]}], + "matches": "x", + "anchor": "y2", + "domain": [0.55, 1] + }, + "yaxis2": {"anchor": "x2"}, + "title": {"text": "matches + rangebreaks"} + } +} diff --git a/test/image/mocks/axes_chain_scaleanchor_matches.json b/test/image/mocks/axes_chain_scaleanchor_matches.json new file mode 100644 index 00000000000..36d95c91580 --- /dev/null +++ b/test/image/mocks/axes_chain_scaleanchor_matches.json @@ -0,0 +1,26 @@ +{ + "data": [ + {"z": [[1, 2], [3, 4], [5, 6]], "type": "heatmap", "showscale": false}, + {"z": [[1, 2, 3], [6, 5, 4]], "type": "heatmap", "xaxis": "x2", "yaxis": "y2", "showscale": false}, + {"z": [[1, 6], [2, 5], [3, 4]], "type": "heatmap", "xaxis": "x3", "yaxis": "y3", "showscale": false}, + {"z": [[1, 2, 3], [4, 5, 6]], "type": "heatmap", "xaxis": "x4", "yaxis": "y4", "showscale": false} + ], + "layout": { + "xaxis": {"domain": [0, 0.4], "constrain": "domain"}, + "yaxis": {"domain": [0, 0.3], "constrain": "domain", "scaleanchor": "x", "title": {"text": "constrain domain"}}, + "xaxis2": {"domain": [0.6, 1], "matches": "x", "anchor": "y2", "title": {"text": "<- each right subplot matches
the axes left and below
and all are constrained
to square bricks."}}, + "yaxis2": {"domain": [0.2, 0.5], "matches": "y", "anchor": "x2"}, + "xaxis3": {"domain": [0, 0.4], "constrain": "range", "anchor": "y3"}, + "yaxis3": {"domain": [0.5, 0.8], "constrain": "range", "scaleanchor": "x3", "anchor": "x3", "title": {"text": "constrain range"}}, + "xaxis4": {"domain": [0.6, 1], "matches": "x3", "anchor": "y4"}, + "yaxis4": {"domain": [0.7, 1], "matches": "y3", "anchor": "x4"}, + "shapes": [ + {"x0": 0, "x1": 0.4, "y0": 0, "y1": 0.3, "xref": "paper", "yref": "paper", "type": "rect", "line": {"color": "#888", "dash": "dot"}}, + {"x0": 0.6, "x1": 1, "y0": 0.2, "y1": 0.5, "xref": "paper", "yref": "paper", "type": "rect", "line": {"color": "#888", "dash": "dot"}}, + {"x0": 0, "x1": 0.4, "y0": 0.5, "y1": 0.8, "xref": "paper", "yref": "paper", "type": "rect", "line": {"color": "#888", "dash": "dot"}}, + {"x0": 0.6, "x1": 1, "y0": 0.7, "y1": 1, "xref": "paper", "yref": "paper", "type": "rect", "line": {"color": "#888", "dash": "dot"}} + ], + "width": 700, + "height": 600 + } +} diff --git a/test/image/mocks/axes_chain_scaleanchor_matches2.json b/test/image/mocks/axes_chain_scaleanchor_matches2.json new file mode 100644 index 00000000000..aee211fd4c8 --- /dev/null +++ b/test/image/mocks/axes_chain_scaleanchor_matches2.json @@ -0,0 +1,82 @@ +{ + "data": [ + {"y":[1,2], "marker": {"color": "red"}}, + {"y":[1,2],"xaxis":"x2","yaxis":"y2", "marker": {"color": "red"}}, + {"y":[1,2],"xaxis":"x3","yaxis":"y3", "marker": {"color": "red"}}, + {"y":[1,2],"xaxis":"x4","yaxis":"y4", "marker": {"color": "red"}}, + {"y":[1,2],"xaxis":"x5","yaxis":"y5", "marker": {"color": "green"}}, + {"y":[1,2],"xaxis":"x6","yaxis":"y6", "marker": {"color": "green"}}, + {"y":[1,2],"xaxis":"x7","yaxis":"y7", "marker": {"color": "green"}}, + {"y":[1,2],"xaxis":"x8","yaxis":"y8", "marker": {"color": "green"}}, + {"y":[1,2],"xaxis":"x9","yaxis":"y9", "marker": {"color": "blue"}}, + {"y":[1,2],"xaxis":"x10","yaxis":"y10", "marker": {"color": "blue"}}, + {"y":[1,2],"xaxis":"x11","yaxis":"y11", "marker": {"color": "blue"}}, + {"y":[1,2],"xaxis":"x12","yaxis":"y12", "marker": {"color": "blue"}}, + {"y":[1,2],"xaxis":"x13","yaxis":"y13", "marker": {"color": "black"}}, + {"y":[1,2],"xaxis":"x14","yaxis":"y14", "marker": {"color": "black"}}, + {"y":[1,2],"xaxis":"x15","yaxis":"y15", "marker": {"color": "black"}}, + {"y":[1,2],"xaxis":"x16","yaxis":"y16", "marker": {"color": "black"}} + ], + "layout": { + "xaxis": {"domain": [0, 0.2], "anchor": "y"}, + "yaxis": {"domain": [0, 0.15], "anchor": "x", "matches": "x"}, + "xaxis2": {"domain": [0.25, 0.45], "anchor": "y2", "scaleanchor": "y"}, + "yaxis2": {"domain": [0, 0.15], "anchor": "x2", "matches": "x2"}, + "xaxis3": {"domain": [0.5, 0.7], "anchor": "y3", "scaleanchor": "y2"}, + "yaxis3": {"domain": [0, 0.15], "anchor": "x3", "matches": "x3"}, + "xaxis4": {"domain": [0.75, 0.95], "anchor": "y4", "scaleanchor": "y3"}, + "yaxis4": {"domain": [0, 0.15], "anchor": "x4", "matches": "x4"}, + + "xaxis5": {"domain": [0, 0.2], "anchor": "y5", "constrain": "domain"}, + "yaxis5": {"domain": [0.25, 0.4], "anchor": "x5", "matches": "x5", "constrain": "domain"}, + "xaxis6": {"domain": [0.25, 0.45], "anchor": "y6", "scaleanchor": "y5", "constrain": "domain"}, + "yaxis6": {"domain": [0.25, 0.4], "anchor": "x6", "matches": "x6", "constrain": "domain"}, + "xaxis7": {"domain": [0.5, 0.7], "anchor": "y7", "scaleanchor": "y6", "constrain": "domain"}, + "yaxis7": {"domain": [0.25, 0.4], "anchor": "x7", "matches": "x7", "constrain": "domain"}, + "xaxis8": {"domain": [0.75, 0.95], "anchor": "y8", "scaleanchor": "y7", "constrain": "domain"}, + "yaxis8": {"domain": [0.25, 0.4], "anchor": "x8", "matches": "x8", "constrain": "domain"}, + + "xaxis9": {"domain": [0, 0.15], "anchor": "y9"}, + "yaxis9": {"domain": [0.5, 0.7], "anchor": "x9", "matches": "x9"}, + "xaxis10": {"domain": [0.25, 0.4], "anchor": "y10", "scaleanchor": "y9"}, + "yaxis10": {"domain": [0.5, 0.7], "anchor": "x10", "matches": "x10"}, + "xaxis11": {"domain": [0.5, 0.65], "anchor": "y11", "scaleanchor": "y10"}, + "yaxis11": {"domain": [0.5, 0.7], "anchor": "x11", "matches": "x11"}, + "xaxis12": {"domain": [0.75, 0.9], "anchor": "y12", "scaleanchor": "y11"}, + "yaxis12": {"domain": [0.5, 0.7], "anchor": "x12", "matches": "x12"}, + + "xaxis13": {"domain": [0, 0.15], "anchor": "y13", "constrain": "domain"}, + "yaxis13": {"domain": [0.75, 0.95], "anchor": "x13", "matches": "x13", "constrain": "domain"}, + "xaxis14": {"domain": [0.25, 0.4], "anchor": "y14", "scaleanchor": "y13", "constrain": "domain"}, + "yaxis14": {"domain": [0.75, 0.95], "anchor": "x14", "matches": "x14", "constrain": "domain"}, + "xaxis15": {"domain": [0.5, 0.65], "anchor": "y15", "scaleanchor": "y14", "constrain": "domain"}, + "yaxis15": {"domain": [0.75, 0.95], "anchor": "x15", "matches": "x15", "constrain": "domain"}, + "xaxis16": {"domain": [0.75, 0.9], "anchor": "y16", "scaleanchor": "y15", "constrain": "domain"}, + "yaxis16": {"domain": [0.75, 0.95], "anchor": "x16", "matches": "x16", "constrain": "domain"}, + + "shapes": [ + {"x0": 0, "x1": 0.2, "y0": 0, "y1": 0.15, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.25, "x1": 0.45, "y0": 0, "y1": 0.15, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.5, "x1": 0.7, "y0": 0, "y1": 0.15, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.75, "x1": 0.95, "y0": 0, "y1": 0.15, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0, "x1": 0.2, "y0": 0.25, "y1": 0.4, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.25, "x1": 0.45, "y0": 0.25, "y1": 0.4, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.5, "x1": 0.7, "y0": 0.25, "y1": 0.4, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.75, "x1": 0.95, "y0": 0.25, "y1": 0.4, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0, "x1": 0.15, "y0": 0.5, "y1": 0.7, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.25, "x1": 0.4, "y0": 0.5, "y1": 0.7, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.5, "x1": 0.65, "y0": 0.5, "y1": 0.7, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.75, "x1": 0.9, "y0": 0.5, "y1": 0.7, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0, "x1": 0.15, "y0": 0.75, "y1": 0.95, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.25, "x1": 0.4, "y0": 0.75, "y1": 0.95, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.5, "x1": 0.65, "y0": 0.75, "y1": 0.95, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}}, + {"x0": 0.75, "x1": 0.9, "y0": 0.75, "y1": 0.95, "type": "rect", "xref": "paper", "yref": "paper", "line": {"color": "#ccc", "dash": "dot"}} + ], + "annotations": [{"x": 0, "y": 1, "xanchor": "left", "yanchor": "top", "xref": "paper", "yref": "paper", "showarrow": false, "align": "left", "text": "y matches same x, x scales to the previous y. Subplot aspect ratios compound"}], + + "width": 500, + "height": 500, + "margin": {"l": 50, "r": 0, "t": 0, "b": 50}, + "showlegend": false + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 009bd26e37a..d38894dd782 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -21,6 +21,7 @@ var ONEWEEK = numerical.ONEWEEK; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); +var negateIf = require('../assets/negate_if'); var selectButton = require('../assets/modebar_button'); var supplyDefaults = require('../assets/supply_defaults'); @@ -677,14 +678,10 @@ describe('Test axes', function() { ]); }); - var warnTxt = ' 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.'; - it('breaks scaleanchor loops and drops conflicting ratios', function() { var warnings = []; spyOn(Lib, 'warn').and.callFake(function(msg) { - warnings.push(msg); + warnings.push(msg.substr(0, msg.indexOf(' to avoid'))); }); layoutIn = { @@ -700,7 +697,8 @@ describe('Test axes', function() { yaxis4: {scaleanchor: 'y', scaleratio: 17}, // y<->y is OK now }; layoutOut._subplots.cartesian.push('x2y2', 'x3y3', 'x4y4'); - layoutOut._subplots.yaxis.push('x2', 'x3', 'x4', 'y2', 'y3', 'y4'); + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4'); + layoutOut._subplots.yaxis.push('y2', 'y3', 'y4'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -710,15 +708,15 @@ describe('Test axes', function() { ]); expect(warnings).toEqual([ - 'ignored yaxis.scaleanchor: "x"' + warnTxt, - 'ignored yaxis3.scaleanchor: "x2"' + warnTxt + 'ignored yaxis.scaleanchor: "x"', + 'ignored yaxis3.scaleanchor: "x2"' ]); }); it('silently drops invalid scaleanchor values', function() { var warnings = []; spyOn(Lib, 'warn').and.callFake(function(msg) { - warnings.push(msg); + warnings.push(msg.substr(0, msg.indexOf(' to avoid'))); }); layoutIn = { @@ -732,7 +730,7 @@ describe('Test axes', function() { supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut._axisConstraintGroups).toEqual([]); - expect(warnings).toEqual(['ignored xaxis.scaleanchor: "x"' + warnTxt]); + expect(warnings).toEqual(['ignored xaxis.scaleanchor: "x"']); ['xaxis', 'yaxis', 'xaxis2'].forEach(function(axName) { expect(layoutOut[axName].scaleanchor).toBeUndefined(axName); @@ -791,23 +789,23 @@ describe('Test axes', function() { expect(layoutOut.xaxis2.matches).toBe('x'); expect(layoutOut.xaxis2.scaleanchor).toBe(undefined); - expect(layoutOut.xaxis2.constrain).toBe(undefined); + // constrain is still coerced in case someone else scales to xaxis2 + expect(layoutOut.xaxis2.constrain).toBe('range'); 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() { + it('combines all chained scaled/matched axes into a group but drops match-only groups from constraintGroups', function() { layoutIn = { - // this one is ok + // this one big group xaxis: {}, yaxis: {scaleanchor: 'x'}, - // this one too - xaxis2: {}, - yaxis2: {matches: 'x2'}, - // not these ones - xaxis3: {scaleanchor: 'x2'}, - yaxis3: {scaleanchor: 'y2'} + xaxis2: {matches: 'x'}, + yaxis2: {matches: 'y'}, + // this is another group but only shows up in matchGroups + xaxis3: {}, + yaxis3: {matches: 'x3'} }; layoutOut._subplots.cartesian.push('x2y2, x3y3'); layoutOut._subplots.xaxis.push('x2', 'x3'); @@ -815,74 +813,76 @@ describe('Test axes', function() { supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._axisMatchGroups.length).toBe(1); - expect(layoutOut._axisMatchGroups).toContain({x2: 1, y2: 1}); + expect(layoutOut._axisMatchGroups).toEqual([{x: 1, x2: 1}, {y: 1, y2: 1}, {x3: 1, y3: 1}]); - expect(layoutOut._axisConstraintGroups.length).toBe(1); - expect(layoutOut._axisConstraintGroups).toContain({x: 1, y: 1}); + expect(layoutOut._axisConstraintGroups).toEqual([{x: 1, y: 1, x2: 1, y2: 1}]); }); - it('remove constraint group if they are one or zero items left in it', function() { + it('includes matches in constraintGroup when combined with scaleanchor', function() { layoutIn = { xaxis: {}, yaxis: {matches: 'x'}, - xaxis2: {scaleanchor: 'y'} + xaxis2: {scaleanchor: 'x'} }; 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); + expect(layoutOut._axisMatchGroups).toEqual([{x: 1, y: 1}]); + expect(layoutOut._axisConstraintGroups).toEqual([{x: 1, y: 'y1', x2: 1}]); }); - it('drops scaleanchor settings if either the axis or target has fixedrange', function() { + it('turns all scaled axes fixedrange if any is fixedrange', function() { // some of these will create warnings... not too important, so not going to test, // just want to keep the output clean // spyOn(Lib, 'warn'); layoutIn = { xaxis: {fixedrange: true, scaleanchor: 'y', scaleratio: 2}, - yaxis: {scaleanchor: 'x2', scaleratio: 3}, // only this one should survive + yaxis: {scaleanchor: 'x2', scaleratio: 3}, xaxis2: {}, yaxis2: {scaleanchor: 'x', scaleratio: 5} }; layoutOut._subplots.cartesian.push('x2y2'); - layoutOut._subplots.yaxis.push('x2', 'y2'); + layoutOut._subplots.xaxis.push('x2'); + layoutOut._subplots.yaxis.push('y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._axisConstraintGroups).toEqual([{x2: 1, y: 3}]); + expect(layoutOut._axisConstraintGroups).toEqual([{x2: 1, y: 3, x: 6, y2: 30}]); + expect(layoutOut.xaxis.scaleanchor).toBe('y'); + expect(layoutOut.xaxis.scaleratio).toBe(2); expect(layoutOut.yaxis.scaleanchor).toBe('x2'); expect(layoutOut.yaxis.scaleratio).toBe(3); + expect(layoutOut.yaxis2.scaleanchor).toBe('x'); + expect(layoutOut.yaxis2.scaleratio).toBe(5); - ['xaxis', 'yaxis2', 'xaxis2'].forEach(function(axName) { - expect(layoutOut[axName].scaleanchor).toBeUndefined(); - expect(layoutOut[axName].scaleratio).toBeUndefined(); + ['xaxis', 'yaxis', 'yaxis2', 'xaxis2'].forEach(function(axName) { + expect(layoutOut[axName].fixedrange).toBe(true, axName); }); }); - it('drops *matches* settings if either the axis or target has fixedrange', function() { + it('turns all matching axes fixedrange if any is fixedrange', function() { layoutIn = { xaxis: {fixedrange: true, matches: 'y'}, - yaxis: {matches: 'x2'}, // only this one should survive + yaxis: {matches: 'x2'}, xaxis2: {}, yaxis2: {matches: 'x'} }; layoutOut._subplots.cartesian.push('x2y2'); - layoutOut._subplots.yaxis.push('x2', 'y2'); + layoutOut._subplots.xaxis.push('x2'); + layoutOut._subplots.yaxis.push('y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._axisMatchGroups).toEqual([{x2: 1, y: 1}]); + expect(layoutOut._axisMatchGroups).toEqual([{x: 1, x2: 1, y: 1, y2: 1}]); expect(layoutOut.yaxis.matches).toBe('x2'); - ['xaxis', 'yaxis2', 'xaxis2'].forEach(function(axName) { - expect(layoutOut[axName].matches).toBeUndefined(); + ['xaxis', 'xaxis2', 'yaxis', 'yaxis2'].forEach(function(axName) { + negateIf(axName !== 'xaxis2', expect(layoutOut[axName].matches)).toBeUndefined(axName); + expect(layoutOut[axName].fixedrange).toBe(true, axName); }); }); @@ -990,7 +990,7 @@ describe('Test axes', function() { // matchee ax has range yaxis: {range: [0, 1]}, yaxis2: {matches: 'y'}, - // matcher ax has range (gets ignored) + // first explicit range gets copied to both xaxis3: {}, yaxis3: {range: [-1, 1], matches: 'x3'}, // both ax have range @@ -1013,13 +1013,14 @@ describe('Test axes', function() { names.forEach(function(n) { var ax = layoutOut[n]; expect(ax.autorange).toBe(autorange, n); - expect(ax.range).toEqual(rng); + expect(ax.range[0]).toBe(rng[0], n); + expect(ax.range[1]).toBe(rng[1], n); }); } _assertMatchingAxes(['xaxis', 'xaxis2'], true, [-1, 6]); _assertMatchingAxes(['yaxis', 'yaxis2'], false, [0, 1]); - _assertMatchingAxes(['xaxis3', 'yaxis3'], true, [-1, 6]); + _assertMatchingAxes(['xaxis3', 'yaxis3'], false, [-1, 1]); _assertMatchingAxes(['xaxis4', 'yaxis4'], false, [-1, 3]); }); @@ -1718,7 +1719,7 @@ describe('Test axes', function() { }) .then(function() { assertRanges('base (autoranged)', [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.245, 3.245], true], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.285, 3.245], true], [['yaxis'], [-0.211, 3.211], true] ]); }) @@ -1732,7 +1733,7 @@ describe('Test axes', function() { .then(function() { return Plotly.relayout(gd, 'xaxis2.autorange', true); }) .then(function() { assertRanges('back to autorange', [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.245, 3.245], true], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.285, 3.245], true], [['yaxis'], [-0.211, 3.211], true] ]); }) diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 819708b2de3..a351246fe22 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -872,7 +872,7 @@ describe('axis zoom/pan and main plot zoom', function() { var msgi = n + ' - ' + msg; if(opts.autorange) { expect(eventData[n + '.autorange']).toBe(true, 2, msgi + '|event data'); - } else if(!opts.noChange) { + } else if(!opts.noChange && !opts.noEventData) { 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]'); } @@ -970,14 +970,14 @@ describe('axis zoom/pan and main plot zoom', function() { dragmode: 'zoom' }; - var xr0 = [-0.245, 3.245]; + var xr0 = [-0.285, 3.246]; 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]], + [['xaxis', 'xaxis2', 'xaxis3'], [1.457, 2.328]], [['yaxis'], [1.179, 1.50]] ], dblclickSubplot: 'xy' @@ -985,7 +985,7 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'x-only zoombox on xy', drag: ['xy', 'nsew', 30, 0], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [1.494, 2.350]], + [['xaxis', 'xaxis2', 'xaxis3'], [1.457, 2.328]], [['yaxis'], yr0, {noChange: true}] ], dblclickSubplot: 'x2y' @@ -1003,7 +1003,7 @@ describe('axis zoom/pan and main plot zoom', function() { exp: [ // N.B. slightly different range result // due difference in ax._length - [['xaxis', 'xaxis2', 'xaxis3'], [1.492, 2.062]], + [['xaxis', 'xaxis2', 'xaxis3'], [1.468, 2.049]], [['yaxis'], [1.179, 1.50]] ], dblclickSubplot: 'x3y' @@ -1013,7 +1013,7 @@ describe('axis zoom/pan and main plot zoom', function() { exp: [ // Similarly here slightly different range result // due difference in ax._length - [['xaxis', 'xaxis2', 'xaxis3'], [1.485, 1.974]], + [['xaxis', 'xaxis2', 'xaxis3'], [1.470, 1.974]], [['yaxis'], [1.179, 1.50]] ], dblclickSubplot: 'xy' @@ -1021,7 +1021,7 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'drag ew on x2y', drag: ['x2y', 'ew', 30, 0], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.816, 2.675], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.866, 2.665], {dragged: true}], [['yaxis'], yr0, {noChange: true}] ], dblclickSubplot: 'x3y' @@ -1029,7 +1029,7 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'drag ew on x3y', drag: ['x3y', 'ew', 30, 0], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.734, 2.756], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.783, 2.748], {dragged: true}], [['yaxis'], yr0, {noChange: true}] ], dblclickSubplot: 'xy' @@ -1037,7 +1037,8 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'drag e on xy', drag: ['xy', 'e', 30, 30], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [xr0[0], 1.366], {dragged: true}], + // FIXME On CI we need 1.359 but locally it's 1.317 ?? + [['xaxis', 'xaxis2', 'xaxis3'], [xr0[0], 1.359], {dragged: true}], [['yaxis'], yr0, {noChange: true}] ], dblclickSubplot: 'x3y' @@ -1045,7 +1046,8 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'drag nw on x3y', drag: ['xy', 'nw', 30, 30], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-1.379, 3.245], {dragged: true}], + // FIXME On CI we need -1.425 but locally it's -1.442 ?? + [['xaxis', 'xaxis2', 'xaxis3'], [-1.425, xr0[1]], {dragged: true}], [['yaxis'], [-0.211, 3.565], {dragged: true}] ], dblclickSubplot: 'x3y' @@ -1054,7 +1056,7 @@ describe('axis zoom/pan and main plot zoom', function() { dragmode: 'pan', drag: ['xy', 'nsew', 30, 30], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-1.101, 2.390], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-1.157, 2.374], {dragged: true}], [['yaxis'], [0.109, 3.532], {dragged: true}] ], dblclickSubplot: 'x3y' @@ -1063,7 +1065,7 @@ describe('axis zoom/pan and main plot zoom', function() { dragmode: 'pan', drag: ['x2y', 'nsew', 30, 30], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.816, 2.675], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.866, 2.665], {dragged: true}], [['yaxis'], [0.109, 3.532], {dragged: true}] ], dblclickSubplot: 'x2y' @@ -1072,7 +1074,7 @@ describe('axis zoom/pan and main plot zoom', function() { dragmode: 'pan', drag: ['x3y', 'nsew', 30, 30], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.734, 2.756], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.783, 2.748], {dragged: true}], [['yaxis'], [0.109, 3.532], {dragged: true}] ], dblclickSubplot: 'xy' @@ -1080,7 +1082,7 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'scrolling on x3y subplot', scroll: ['x3y', 20], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.613, 3.245], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.655, 3.247], {dragged: true}], [['yaxis'], [-0.211, 3.571], {dragged: true}] ], dblclickSubplot: 'xy' @@ -1088,7 +1090,7 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'scrolling on x2y subplot', scroll: ['x2y', 20], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.613, 3.245], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.655, 3.247], {dragged: true}], [['yaxis'], [-0.211, 3.571], {dragged: true}] ], dblclickSubplot: 'xy' @@ -1096,7 +1098,7 @@ describe('axis zoom/pan and main plot zoom', function() { desc: 'scrolling on xy subplot', scroll: ['xy', 20], exp: [ - [['xaxis', 'xaxis2', 'xaxis3'], [-0.613, 3.245], {dragged: true}], + [['xaxis', 'xaxis2', 'xaxis3'], [-0.655, 3.247], {dragged: true}], [['yaxis'], [-0.211, 3.571], {dragged: true}] ], dblclickSubplot: 'x2y' @@ -1150,14 +1152,14 @@ describe('axis zoom/pan and main plot zoom', function() { }; var xr0 = [-0.211, 3.211]; - var yr0 = [-0.077, 3.163]; + var yr0 = [-0.234, 3.244]; 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}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.607, 4.085], {dragged: true}], ], trans: [-30, -30, -30, -45, -30, -52.5] }, { @@ -1165,7 +1167,7 @@ describe('axis zoom/pan and main plot zoom', function() { drag: ['xy2', 'nsew', 30, 30], exp: [ [['xaxis'], [-0.534, 2.888], {dragged: true}], - [['yaxis', 'yaxis2', 'yaxis3'], [0.444, 3.685], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.327, 3.805], {dragged: true}], ], trans: [-30, -20, -30, -30, -30, -35] }, { @@ -1173,7 +1175,7 @@ describe('axis zoom/pan and main plot zoom', function() { drag: ['xy3', 'nsew', 30, 30], exp: [ [['xaxis'], [-0.534, 2.888], {dragged: true}], - [['yaxis', 'yaxis2', 'yaxis3'], [0.370, 3.611], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.247, 3.725], {dragged: true}], ], trans: [-30, -17.142, -30, -25.71, -30, -30] }, { @@ -1181,7 +1183,7 @@ describe('axis zoom/pan and main plot zoom', function() { drag: ['xy2', 'ns', 0, 30], exp: [ [['xaxis'], xr0, {noChange: true}], - [['yaxis', 'yaxis2', 'yaxis3'], [0.444, 3.685], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [0.327, 3.805], {dragged: true}], ], trans: [0, -20, 0, -30, 0, -35] }, { @@ -1189,7 +1191,7 @@ describe('axis zoom/pan and main plot zoom', function() { drag: ['xy3', 'n', 0, 30], exp: [ [['xaxis'], xr0, {noChange: true}], - [['yaxis', 'yaxis2', 'yaxis3'], [yr0[0], 3.683], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [yr0[0], 3.802], {dragged: true}], ], trans: [0, -19.893, 0, -29.839, 0, -34.812], scale: [1, 1.160, 1, 1.160, 1, 1.160] @@ -1198,7 +1200,7 @@ describe('axis zoom/pan and main plot zoom', function() { drag: ['xy', 's', 0, 30], exp: [ [['xaxis'], xr0, {noChange: true}], - [['yaxis', 'yaxis2', 'yaxis3'], [1.617, yr0[1]], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3'], [1.586, yr0[1]], {dragged: true}], ], trans: [0, 0, 0, 0, 0, 0], scale: [1, 0.476, 1, 0.476, 1, 0.476] @@ -1588,6 +1590,130 @@ describe('axis zoom/pan and main plot zoom', function() { .catch(failTest) .then(done); }); + + it('matching and constrained subplots play nice together', function(done) { + var data = [ + {x: [0, 3], y: [0, 3]}, + {x: [0, 3], y: [1, 8], xaxis: 'x2', yaxis: 'y2'} + ]; + + var layout = { + width: 400, height: 350, margin: {l: 50, r: 50, t: 50, b: 50}, + yaxis: {domain: [0, 0.4], scaleanchor: 'x'}, + xaxis2: {anchor: 'y2'}, + yaxis2: {domain: [0.6, 1], matches: 'x2'}, + showlegend: false + }; + var x2y2, mx, my; + + makePlot(data, layout).then(function() { + assertRanges('base', [ + [['xaxis'], [-3.955, 6.955]], + [['yaxis'], [-0.318, 3.318]], + [['xaxis2', 'yaxis2'], [-0.588, 8.824]] + ]); + x2y2 = d3.select('.subplot.x2y2 .plot'); + expect(x2y2.attr('transform')).toBe('translate(50,50)'); + mx = gd._fullLayout.xaxis._m; + my = gd._fullLayout.yaxis._m; + }) + .then(function() { + var drag = makeDragFns('x2y2', 'ns', 30, 30); + return drag.start().then(function() { + assertRanges('during drag', [ + [['xaxis'], [-3.955, 6.955]], + [['yaxis'], [-0.318, 3.318]], + [['xaxis2', 'yaxis2'], [2.236, 11.648], {skipInput: true}] + ]); + // Check that the data container moves as it should with the axes + expect(x2y2.attr('transform')).toBe('translate(-40,80)scale(1,1)'); + }) + .then(drag.end); + }) + .then(_assert('after drag on x2y2 subplot', [ + [['xaxis'], [-3.955, 6.955], {noChange: true}], + [['yaxis'], [-0.318, 3.318], {noChange: true}], + [['xaxis2', 'yaxis2'], [2.236, 11.648], {dragged: true}] + ])) + .then(function() { + // make sure the ranges were correct when xy was redrawn + expect(gd._fullLayout.xaxis._m).toBe(mx); + expect(gd._fullLayout.yaxis._m).toBe(my); + }) + .then(doDblClick('x2y2', 'ew')) + .then(_assert('after double-click on x2', [ + [['xaxis'], [-3.955, 6.955], {noChange: true}], + [['yaxis'], [-0.318, 3.318], {noChange: true}], + [['xaxis2'], [-0.588, 8.824], {autorange: true}], + [['yaxis2'], [-0.588, 8.824], {noEventData: true}] + ])) + .then(function() { + expect(gd._fullLayout.xaxis._m).toBe(mx); + expect(gd._fullLayout.yaxis._m).toBe(my); + }) + .catch(failTest) + .then(done); + }); + + it('handles matching & scaleanchor chained together', function(done) { + var data = [ + {y: [1, 2]}, + {y: [0, 1], xaxis: 'x2', yaxis: 'y2'} + ]; + + var layout = { + width: 350, + height: 300, + margin: {l: 50, r: 50, t: 50, b: 50}, + showlegend: false, + xaxis: {domain: [0, 0.4]}, + yaxis: {domain: [0, 0.5], matches: 'x'}, + xaxis2: {domain: [0.6, 1], scaleanchor: 'x', anchor: 'y2'}, + yaxis2: {domain: [0.5, 1], matches: 'x2', anchor: 'x2'} + }; + + makePlot(data, layout).then(function() { + assertRanges('base', [ + [['xaxis', 'yaxis'], [-0.212, 2.212]], + [['xaxis2', 'yaxis2'], [-0.712, 1.712]] + ]); + }) + .then(function() { + var drag = makeDragFns('xy', 'sw', 30, -30); + return drag.start().then(function() { + assertRanges('during drag sw', [ + [['xaxis', 'yaxis'], [-1.251, 2.212], {skipInput: true}], + [['xaxis2', 'yaxis2'], [-1.232, 2.232], {skipInput: true}] + ]); + }) + .then(drag.end); + }) + .then(_assert('after drag sw on xy subplot', [ + [['xaxis', 'yaxis'], [-1.251, 2.212], {dragged: true}], + [['xaxis2', 'yaxis2'], [-1.232, 2.232], {dragged: true}] + ])) + .then(doDblClick('x2y2', 'nsew')) + .then(_assert('after double-click on x2', [ + [['xaxis', 'yaxis'], [-0.212, 2.212], {autorange: true}], + [['xaxis2', 'yaxis2'], [-0.712, 1.712], {autorange: true}] + ])) + .then(function() { + var drag = makeDragFns('xy', 'nw', 30, 30); + return drag.start().then(function() { + assertRanges('during drag nw', [ + [['xaxis', 'yaxis'], [-0.732, 2.732], {skipInput: true}], + [['xaxis2', 'yaxis2'], [-1.232, 2.232], {skipInput: true}] + ]); + }) + .then(drag.end); + }) + .then(_assert('after drag nw on xy subplot', [ + [['xaxis', 'yaxis'], [-0.732, 2.732], {dragged: true}], + [['xaxis2', 'yaxis2'], [-1.232, 2.232], {dragged: true}] + ])) + .catch(failTest) + .then(done); + }); }); describe('redrag behavior', function() { diff --git a/test/jasmine/tests/image_test.js b/test/jasmine/tests/image_test.js index 9363a730ffa..879f3eb47d7 100644 --- a/test/jasmine/tests/image_test.js +++ b/test/jasmine/tests/image_test.js @@ -208,11 +208,11 @@ describe('image smart layout defaults', function() { expect(gd._fullLayout.yaxis.scaleanchor).toBe('x'); }); - it('should NOT set scaleanchor if it\'s already defined', function() { - gd.data = [{type: 'image', z: [[[255, 0, 0]]]}]; + it('should NOT reset scaleanchor if it\'s already defined', function() { + gd.data = [{type: 'image', z: [[[255, 0, 0]]]}, {y: [5, 3, 2], xaxis: 'x3'}]; gd.layout = {yaxis: {scaleanchor: 'x3'}}; supplyAllDefaults(gd); - expect(gd._fullLayout.yaxis.scaleanchor).toBe(undefined); + expect(gd._fullLayout.yaxis.scaleanchor).toBe('x3'); }); it('should constrain axes to domain if images are present', function() { @@ -233,7 +233,7 @@ describe('image smart layout defaults', function() { it('should NOT constrain axes to domain if it\'s already defined', function() { gd.data = [{type: 'image', z: [[[255, 0, 0]]]}]; - gd.layout = {yaxis: {constrain: false}, xaxis: {constrain: false}}; + gd.layout = {yaxis: {constrain: 'range'}, xaxis: {constrain: 'range'}}; supplyAllDefaults(gd); expect(gd._fullLayout.xaxis.constrain).toBe('range'); expect(gd._fullLayout.yaxis.constrain).toBe('range'); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 7052124d76e..2b43f198418 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -537,9 +537,10 @@ describe('Test splom trace defaults:', function() { 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'); + // not necessary to set y axes matching x, since x already matches y + expect(fullLayout.yaxis.matches).toBe(undefined); + expect(fullLayout.yaxis2.matches).toBe(undefined); var groups = fullLayout._axisMatchGroups; expect(groups.length).toBe(2); @@ -562,7 +563,7 @@ describe('Test splom trace defaults:', function() { 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.yaxis2.matches).toBe(undefined); expect(fullLayout.xaxis3.matches).toBe(undefined); expect(fullLayout.yaxis3).toBe(undefined); @@ -586,7 +587,7 @@ describe('Test splom trace defaults:', function() { 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.yaxis2.matches).toBe(undefined); expect(fullLayout.xaxis3).toBe(undefined); expect(fullLayout.yaxis3.matches).toBe(undefined); @@ -608,7 +609,7 @@ describe('Test splom trace defaults:', function() { var fullLayout = gd._fullLayout; expect(fullLayout.xaxis.matches).toBe('y'); - expect(fullLayout.yaxis.matches).toBe('x'); + expect(fullLayout.yaxis.matches).toBe(undefined); expect(fullLayout.xaxis2.matches).toBe('x'); expect(fullLayout.yaxis2.matches).toBe('x2');