diff --git a/draftlogs/6547_add.md b/draftlogs/6547_add.md new file mode 100644 index 00000000000..afd9a3f8ff6 --- /dev/null +++ b/draftlogs/6547_add.md @@ -0,0 +1,2 @@ + - Add bounds to range and autorange of cartesian, gl3d and radial axes [[#6547](https://github.com/plotly/plotly.js/pull/6547)] + diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 3428489b25b..67bc00abf55 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -271,12 +271,16 @@ function handleCartesian(gd, ev) { if(val === 'auto') { aobj[axName + '.autorange'] = true; } else if(val === 'reset') { - if(ax._rangeInitial === undefined) { + if(ax._rangeInitial0 === undefined && ax._rangeInitial1 === undefined) { aobj[axName + '.autorange'] = true; + } else if(ax._rangeInitial0 === undefined) { + aobj[axName + '.autorange'] = ax._autorangeInitial; + aobj[axName + '.range'] = [null, ax._rangeInitial1]; + } else if(ax._rangeInitial1 === undefined) { + aobj[axName + '.range'] = [ax._rangeInitial0, null]; + aobj[axName + '.autorange'] = ax._autorangeInitial; } else { - var rangeInitial = ax._rangeInitial.slice(); - aobj[axName + '.range[0]'] = rangeInitial[0]; - aobj[axName + '.range[1]'] = rangeInitial[1]; + aobj[axName + '.range'] = [ax._rangeInitial0, ax._rangeInitial1]; } // N.B. "reset" also resets showspikes diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index e069252767f..b41bbce29f0 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1843,6 +1843,17 @@ function axRangeSupplyDefaultsByPass(gd, flags, specs) { var axIn = gd.layout[axName]; var axOut = fullLayout[axName]; axOut.autorange = axIn.autorange; + + var r0 = axOut._rangeInitial0; + var r1 = axOut._rangeInitial1; + // partial range needs supplyDefaults + if( + (r0 === undefined && r1 !== undefined) || + (r0 !== undefined && r1 === undefined) + ) { + return false; + } + if(axIn.range) { axOut.range = axIn.range.slice(); } diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index d5e3a858eb1..9113cd855d4 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -13,6 +13,7 @@ var getFromId = axIds.getFromId; var isLinked = axIds.isLinked; module.exports = { + applyAutorangeOptions: applyAutorangeOptions, getAutoRange: getAutoRange, makePadFn: makePadFn, doAutoRange: doAutoRange, @@ -75,16 +76,20 @@ function getAutoRange(gd, ax) { maxmax = Math.max(maxmax, maxArray[i].val); } - var axReverse = false; + var autorange = ax.autorange; + var axReverse = + autorange === 'reversed' || + autorange === 'min reversed' || + autorange === 'max reversed'; - if(ax.range) { + if(!axReverse && ax.range) { var rng = Lib.simpleMap(ax.range, ax.r2l); axReverse = rng[1] < rng[0]; } + // one-time setting to easily reverse the axis // when plotting from code if(ax.autorange === 'reversed') { - axReverse = true; ax.autorange = true; } @@ -176,6 +181,10 @@ function getAutoRange(gd, ax) { ]; } + newRange = applyAutorangeOptions(newRange, ax); + + if(ax.limitRange) ax.limitRange(); + // maintain reversal if(axReverse) newRange.reverse(); @@ -209,7 +218,7 @@ function makePadFn(fullLayout, ax, max) { (ax.ticklabelposition || '').indexOf('inside') !== -1 || (anchorAxis.ticklabelposition || '').indexOf('inside') !== -1 ) { - var axReverse = ax.autorange === 'reversed'; + var axReverse = ax.isReversed(); if(!axReverse) { var rng = Lib.simpleMap(ax.range, ax.r2l); axReverse = rng[1] < rng[0]; @@ -623,3 +632,91 @@ function goodNumber(v) { function lessOrEqual(v0, v1) { return v0 <= v1; } function greaterOrEqual(v0, v1) { return v0 >= v1; } + +function applyAutorangeMinOptions(v, ax) { + var autorangeoptions = ax.autorangeoptions; + if( + autorangeoptions && + autorangeoptions.minallowed !== undefined && + hasValidMinAndMax(ax, autorangeoptions.minallowed, autorangeoptions.maxallowed) + ) { + return autorangeoptions.minallowed; + } + + if( + autorangeoptions && + autorangeoptions.clipmin !== undefined && + hasValidMinAndMax(ax, autorangeoptions.clipmin, autorangeoptions.clipmax) + ) { + return Math.max(v, ax.d2l(autorangeoptions.clipmin)); + } + return v; +} + +function applyAutorangeMaxOptions(v, ax) { + var autorangeoptions = ax.autorangeoptions; + + if( + autorangeoptions && + autorangeoptions.maxallowed !== undefined && + hasValidMinAndMax(ax, autorangeoptions.minallowed, autorangeoptions.maxallowed) + ) { + return autorangeoptions.maxallowed; + } + + if( + autorangeoptions && + autorangeoptions.clipmax !== undefined && + hasValidMinAndMax(ax, autorangeoptions.clipmin, autorangeoptions.clipmax) + ) { + return Math.min(v, ax.d2l(autorangeoptions.clipmax)); + } + + return v; +} + +function hasValidMinAndMax(ax, min, max) { + // in case both min and max are defined, ensure min < max + if( + min !== undefined && + max !== undefined + ) { + min = ax.d2l(min); + max = ax.d2l(max); + return min < max; + } + return true; +} + +// this function should be (and is) called before reversing the range +// so range[0] is the minimum and range[1] is the maximum +function applyAutorangeOptions(range, ax) { + if(!ax || !ax.autorangeoptions) return range; + + var min = range[0]; + var max = range[1]; + + var include = ax.autorangeoptions.include; + if(include !== undefined) { + var lMin = ax.d2l(min); + var lMax = ax.d2l(max); + + if(!Lib.isArrayOrTypedArray(include)) include = [include]; + for(var i = 0; i < include.length; i++) { + var v = ax.d2l(include[i]); + if(lMin >= v) { + lMin = v; + min = v; + } + if(lMax <= v) { + lMax = v; + max = v; + } + } + } + + min = applyAutorangeMinOptions(min, ax); + max = applyAutorangeMaxOptions(max, ax); + + return [min, max]; +} diff --git a/src/plots/cartesian/autorange_options_defaults.js b/src/plots/cartesian/autorange_options_defaults.js new file mode 100644 index 00000000000..8b7367e5ae2 --- /dev/null +++ b/src/plots/cartesian/autorange_options_defaults.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = function handleAutorangeOptionsDefaults(coerce, autorange, range) { + var minRange, maxRange; + if(range) { + var isReversed = ( + autorange === 'reversed' || + autorange === 'min reversed' || + autorange === 'max reversed' + ); + + minRange = range[isReversed ? 1 : 0]; + maxRange = range[isReversed ? 0 : 1]; + } + + var minallowed = coerce('autorangeoptions.minallowed', maxRange === null ? minRange : undefined); + var maxallowed = coerce('autorangeoptions.maxallowed', minRange === null ? maxRange : undefined); + + if(minallowed === undefined) coerce('autorangeoptions.clipmin'); + if(maxallowed === undefined) coerce('autorangeoptions.clipmax'); + + coerce('autorangeoptions.include'); +}; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index c131d92ee6b..d3a56d04ad3 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -321,14 +321,20 @@ axes.saveRangeInitial = function(gd, overwrite) { for(var i = 0; i < axList.length; i++) { var ax = axList[i]; - var isNew = (ax._rangeInitial === undefined); - var hasChanged = isNew || !( - ax.range[0] === ax._rangeInitial[0] && - ax.range[1] === ax._rangeInitial[1] + var isNew = + ax._rangeInitial0 === undefined && + ax._rangeInitial1 === undefined; + + var hasChanged = isNew || ( + ax.range[0] !== ax._rangeInitial0 || + ax.range[1] !== ax._rangeInitial1 ); - if((isNew && ax.autorange === false) || (overwrite && hasChanged)) { - ax._rangeInitial = ax.range.slice(); + var autorange = ax.autorange; + if((isNew && autorange !== true) || (overwrite && hasChanged)) { + ax._rangeInitial0 = (autorange === 'min' || autorange === 'max reversed') ? undefined : ax.range[0]; + ax._rangeInitial1 = (autorange === 'max' || autorange === 'min reversed') ? undefined : ax.range[1]; + ax._autorangeInitial = autorange; hasOneAxisChanged = true; } } diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index cb647101f16..fbfcbe41233 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -15,6 +15,7 @@ var handleTickLabelDefaults = require('./tick_label_defaults'); var handlePrefixSuffixDefaults = require('./prefix_suffix_defaults'); var handleCategoryOrderDefaults = require('./category_order_defaults'); var handleLineGridDefaults = require('./line_grid_defaults'); +var handleAutorangeOptionsDefaults = require('./autorange_options_defaults'); var setConvert = require('./set_convert'); var DAY_OF_WEEK = require('./constants').WEEKDAY_PATTERN; @@ -91,12 +92,37 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, setConvert(containerOut, layoutOut); - var autorangeDflt = !containerOut.isValidRange(containerIn.range); - if(autorangeDflt && options.reverseDflt) autorangeDflt = 'reversed'; - var autoRange = coerce('autorange', autorangeDflt); - if(autoRange && (axType === 'linear' || axType === '-')) coerce('rangemode'); + coerce('minallowed'); + coerce('maxallowed'); + var range = coerce('range'); + var autorangeDflt = containerOut.getAutorangeDflt(range, options); + var autorange = coerce('autorange', autorangeDflt); + + var shouldAutorange; + + // validate range and set autorange true for invalid partial ranges + if(range && ( + (range[0] === null && range[1] === null) || + ((range[0] === null || range[1] === null) && (autorange === 'reversed' || autorange === true)) || + (range[0] !== null && (autorange === 'min' || autorange === 'max reversed')) || + (range[1] !== null && (autorange === 'max' || autorange === 'min reversed')) + )) { + range = undefined; + delete containerOut.range; + containerOut.autorange = true; + shouldAutorange = true; + } + + if(!shouldAutorange) { + autorangeDflt = containerOut.getAutorangeDflt(range, options); + autorange = coerce('autorange', autorangeDflt); + } + + if(autorange) { + handleAutorangeOptionsDefaults(coerce, autorange, range); + if(axType === 'linear' || axType === '-') coerce('rangemode'); + } - coerce('range'); containerOut.cleanRange(); handleCategoryOrderDefaults(containerIn, containerOut, coerce, options); diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index d825cd92e3e..8adfa356b94 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -145,7 +145,12 @@ exports.handleDefaults = function(layoutIn, layoutOut, opts) { // 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) { + if(attr === 'range' && val && + axIn.range && + axIn.range.length === 2 && + axIn.range[0] !== null && + axIn.range[1] !== null + ) { hasRange = true; } if(attr === 'autorange' && val === null && hasRange) { diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 2c393fd2a0a..5446277b847 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -565,6 +565,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { 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; @@ -772,7 +773,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(matches.yaxes) axList = axList.concat(matches.yaxes); var attrs = {}; - var ax, i, rangeInitial; + var ax, i; // For reset+autosize mode: // If *any* of the main axes is not at its initial range @@ -784,11 +785,17 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { for(i = 0; i < axList.length; i++) { ax = axList[i]; - if((ax._rangeInitial && ( - ax.range[0] !== ax._rangeInitial[0] || - ax.range[1] !== ax._rangeInitial[1] + var r0 = ax._rangeInitial0; + var r1 = ax._rangeInitial1; + var hasRangeInitial = + r0 !== undefined || + r1 !== undefined; + + if((hasRangeInitial && ( + (r0 !== undefined && r0 !== ax.range[0]) || + (r1 !== undefined && r1 !== ax.range[1]) )) || - (!ax._rangeInitial && !ax.autorange) + (!hasRangeInitial && ax.autorange !== true) ) { doubleClickConfig = 'reset'; break; @@ -818,12 +825,19 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { ax = axList[i]; if(!ax.fixedrange) { - if(!ax._rangeInitial) { - attrs[ax._name + '.autorange'] = true; + var axName = ax._name; + + var autorangeInitial = ax._autorangeInitial; + if(ax._rangeInitial0 === undefined && ax._rangeInitial1 === undefined) { + attrs[axName + '.autorange'] = true; + } else if(ax._rangeInitial0 === undefined) { + attrs[axName + '.autorange'] = autorangeInitial; + attrs[axName + '.range'] = [null, ax._rangeInitial1]; + } else if(ax._rangeInitial1 === undefined) { + attrs[axName + '.range'] = [ax._rangeInitial0, null]; + attrs[axName + '.autorange'] = autorangeInitial; } else { - rangeInitial = ax._rangeInitial; - attrs[ax._name + '.range[0]'] = rangeInitial[0]; - attrs[ax._name + '.range[1]'] = rangeInitial[1]; + attrs[axName + '.range'] = [ax._rangeInitial0, ax._rangeInitial1]; } } } @@ -874,6 +888,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(sp._scene) { var xrng = Lib.simpleMap(xa.range, xa.r2l); var yrng = Lib.simpleMap(ya.range, ya.r2l); + + if(xa.limitRange) xa.limitRange(); + if(ya.limitRange) ya.limitRange(); + + xrng = xa.range; + yrng = ya.range; + sp._scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]}); } } @@ -915,6 +936,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { clipDx = scaleAndGetShift(xa, xScaleFactor2); } + if(xScaleFactor2 > 1 && ( + (xa.maxallowed !== undefined && editX === (xa.range[0] < xa.range[1] ? 'e' : 'w')) || + (xa.minallowed !== undefined && editX === (xa.range[0] < xa.range[1] ? 'w' : 'e')) + )) { + xScaleFactor2 = 1; + clipDx = 0; + } + if(editY2) { yScaleFactor2 = yScaleFactor; clipDy = ns || matches.isSubplotConstrained ? viewBox[1] : getShift(ya, yScaleFactor2); @@ -931,6 +960,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { clipDy = scaleAndGetShift(ya, yScaleFactor2); } + if(yScaleFactor2 > 1 && ( + (ya.maxallowed !== undefined && editY === (ya.range[0] < ya.range[1] ? 'n' : 's')) || + (ya.minallowed !== undefined && editY === (ya.range[0] < ya.range[1] ? 's' : 'n')) + )) { + yScaleFactor2 = 1; + clipDy = 0; + } + // don't scale at all if neither axis is scalable here if(!xScaleFactor2 && !yScaleFactor2) { continue; @@ -1096,6 +1133,8 @@ function dragAxList(axList, pix) { axi.l2r(axi._rl[1] - pix / axi._m) ]; } + + if(axi.limitRange) axi.limitRange(); } } } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index b3854603116..894dd649fa4 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -272,7 +272,7 @@ module.exports = { }, autorange: { valType: 'enumerated', - values: [true, false, 'reversed'], + values: [true, false, 'reversed', 'min reversed', 'max reversed', 'min', 'max'], dflt: true, editType: 'axrange', impliedEdits: {'range[0]': undefined, 'range[1]': undefined}, @@ -280,9 +280,61 @@ module.exports = { 'Determines whether or not the range of this axis is', 'computed in relation to the input data.', 'See `rangemode` for more info.', - 'If `range` is provided, then `autorange` is set to *false*.' + 'If `range` is provided and it has a value for both the', + 'lower and upper bound, `autorange` is set to *false*.', + 'Using *min* applies autorange only to set the minimum.', + 'Using *max* applies autorange only to set the maximum.', + 'Using *min reversed* applies autorange only to set the minimum on a reversed axis.', + 'Using *max reversed* applies autorange only to set the maximum on a reversed axis.', + 'Using *reversed* applies autorange on both ends and reverses the axis direction.', ].join(' ') }, + autorangeoptions: { + minallowed: { + valType: 'any', + editType: 'plot', + impliedEdits: {'range[0]': undefined, 'range[1]': undefined}, + description: [ + 'Use this value exactly as autorange minimum.' + ].join(' ') + }, + maxallowed: { + valType: 'any', + editType: 'plot', + impliedEdits: {'range[0]': undefined, 'range[1]': undefined}, + description: [ + 'Use this value exactly as autorange maximum.' + ].join(' ') + }, + clipmin: { + valType: 'any', + editType: 'plot', + impliedEdits: {'range[0]': undefined, 'range[1]': undefined}, + description: [ + 'Clip autorange minimum if it goes beyond this value.', + 'Has no effect when `autorangeoptions.minallowed` is provided.' + ].join(' ') + }, + clipmax: { + valType: 'any', + editType: 'plot', + impliedEdits: {'range[0]': undefined, 'range[1]': undefined}, + description: [ + 'Clip autorange maximum if it goes beyond this value.', + 'Has no effect when `autorangeoptions.maxallowed` is provided.' + ].join(' ') + }, + include: { + valType: 'any', + arrayOk: true, + editType: 'plot', + impliedEdits: {'range[0]': undefined, 'range[1]': undefined}, + description: [ + 'Ensure this value is included in autorange.' + ].join(' ') + }, + editType: 'plot' + }, rangemode: { valType: 'enumerated', values: ['normal', 'tozero', 'nonnegative'], @@ -317,7 +369,24 @@ module.exports = { 'will be accepted and converted to strings.', 'If the axis `type` is *category*, it should be numbers,', 'using the scale where each category is assigned a serial', - 'number from zero in the order it appears.' + 'number from zero in the order it appears.', + 'Leaving either or both elements `null` impacts the default `autorange`.', + ].join(' ') + }, + minallowed: { + valType: 'any', + editType: 'plot', + impliedEdits: {'^autorange': false}, + description: [ + 'Determines the minimum range of this axis.' + ].join(' ') + }, + maxallowed: { + valType: 'any', + editType: 'plot', + impliedEdits: {'^autorange': false}, + description: [ + 'Determines the maximum range of this axis.' ].join(' ') }, fixedrange: { diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index c8d00c9bbde..e4e41e2548e 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -432,6 +432,23 @@ module.exports = function setConvert(ax, fullLayout) { return (ax.r2l(v) - rl0) / (rl1 - rl0); }; + ax.limitRange = function(rangeAttr) { + var minallowed = ax.minallowed; + var maxallowed = ax.maxallowed; + if(minallowed === undefined && maxallowed === undefined) return; + + if(!rangeAttr) rangeAttr = 'range'; + var range = Lib.nestedProperty(ax, rangeAttr).get(); + var rng = Lib.simpleMap(range, ax.r2l); + var axrev = rng[1] < rng[0]; + if(axrev) rng.reverse(); + + var bounds = Lib.simpleMap([minallowed, maxallowed], ax.r2l); + + if(minallowed !== undefined && rng[0] < bounds[0]) range[axrev ? 1 : 0] = minallowed; + if(maxallowed !== undefined && rng[1] > bounds[1]) range[axrev ? 0 : 1] = maxallowed; + }; + /* * cleanRange: make sure range is a couplet of valid & distinct values * keep numbers away from the limits of floating point numbers, @@ -441,6 +458,11 @@ module.exports = function setConvert(ax, fullLayout) { * ax._r, rather than ax.range */ ax.cleanRange = function(rangeAttr, opts) { + ax._cleanRange(rangeAttr, opts); + ax.limitRange(rangeAttr); + }; + + ax._cleanRange = function(rangeAttr, opts) { if(!opts) opts = {}; if(!rangeAttr) rangeAttr = 'range'; @@ -464,6 +486,9 @@ module.exports = function setConvert(ax, fullLayout) { return; } + var nullRange0 = range[0] === null; + var nullRange1 = range[1] === null; + if(ax.type === 'date' && !ax.autorange) { // check if milliseconds or js date objects are provided for range // and convert to date strings @@ -488,7 +513,7 @@ module.exports = function setConvert(ax, fullLayout) { } } else { if(!isNumeric(range[i])) { - if(isNumeric(range[1 - i])) { + if(!(nullRange0 || nullRange1) && isNumeric(range[1 - i])) { range[i] = range[1 - i] * (i ? 10 : 0.1); } else { ax[rangeAttr] = dflt; @@ -855,12 +880,36 @@ module.exports = function setConvert(ax, fullLayout) { return arrayOut; }; - ax.isValidRange = function(range) { + ax.isValidRange = function(range, nullOk) { return ( Array.isArray(range) && range.length === 2 && - isNumeric(ax.r2l(range[0])) && - isNumeric(ax.r2l(range[1])) + ((nullOk && range[0] === null) || isNumeric(ax.r2l(range[0]))) && + ((nullOk && range[1] === null) || isNumeric(ax.r2l(range[1]))) + ); + }; + + ax.getAutorangeDflt = function(range, options) { + var autorangeDflt = !ax.isValidRange(range, 'nullOk'); + if(autorangeDflt && options && options.reverseDflt) autorangeDflt = 'reversed'; + else if(range) { + if(range[0] === null && range[1] === null) { + autorangeDflt = true; + } else if(range[0] === null && range[1] !== null) { + autorangeDflt = 'min'; + } else if(range[0] !== null && range[1] === null) { + autorangeDflt = 'max'; + } + } + return autorangeDflt; + }; + + ax.isReversed = function() { + var autorange = ax.autorange; + return ( + autorange === 'reversed' || + autorange === 'min reversed' || + autorange === 'max reversed' ); }; diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index 703c61cc76c..06542c01801 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -65,7 +65,17 @@ module.exports = overrideAll({ }), autotypenumbers: axesAttrs.autotypenumbers, autorange: axesAttrs.autorange, + autorangeoptions: { + minallowed: axesAttrs.autorangeoptions.minallowed, + maxallowed: axesAttrs.autorangeoptions.maxallowed, + clipmin: axesAttrs.autorangeoptions.clipmin, + clipmax: axesAttrs.autorangeoptions.clipmax, + include: axesAttrs.autorangeoptions.include, + editType: 'plot' + }, rangemode: axesAttrs.rangemode, + minallowed: axesAttrs.minallowed, + maxallowed: axesAttrs.maxallowed, range: extendFlat({}, axesAttrs.range, { items: [ {valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}}, diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 285f2e1c425..e320fe9e780 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -22,6 +22,8 @@ var createAxesOptions = require('./layout/convert'); var createSpikeOptions = require('./layout/spikes'); var computeTickMarks = require('./layout/tick_marks'); +var applyAutorangeOptions = require('../cartesian/autorange').applyAutorangeOptions; + var STATIC_CANVAS, STATIC_CONTEXT; var tabletmode = false; @@ -679,6 +681,8 @@ proto.plot = function(sceneData, fullLayout, layout) { }; } + var range; + if(axis.autorange) { sceneBounds[0][i] = Infinity; sceneBounds[1][i] = -Infinity; @@ -724,14 +728,24 @@ proto.plot = function(sceneData, fullLayout, layout) { sceneBounds[1][i] += d / 32.0; } - if(axis.autorange === 'reversed') { + range = [ + sceneBounds[0][i], + sceneBounds[1][i] + ]; + + range = applyAutorangeOptions(range, axis); + + sceneBounds[0][i] = range[0]; + sceneBounds[1][i] = range[1]; + + if(axis.isReversed()) { // swap bounds: var tmp = sceneBounds[0][i]; sceneBounds[0][i] = sceneBounds[1][i]; sceneBounds[1][i] = tmp; } } else { - var range = axis.range; + range = axis.range; sceneBounds[0][i] = axis.r2l(range[0]); sceneBounds[1][i] = axis.r2l(range[1]); } @@ -741,10 +755,17 @@ proto.plot = function(sceneData, fullLayout, layout) { } axisDataRange[i] = sceneBounds[1][i] - sceneBounds[0][i]; + axis.range = [ + sceneBounds[0][i], + sceneBounds[1][i] + ]; + + axis.limitRange(); + // Update plot bounds scene.glplot.setBounds(i, { - min: sceneBounds[0][i] * dataScale[i], - max: sceneBounds[1][i] * dataScale[i] + min: axis.range[0] * dataScale[i], + max: axis.range[1] * dataScale[i] }); } diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js index d274b0dd7fd..eeec131dac6 100644 --- a/src/plots/polar/layout_attributes.js +++ b/src/plots/polar/layout_attributes.js @@ -58,6 +58,14 @@ var radialAxisAttrs = { }), autotypenumbers: axesAttrs.autotypenumbers, + autorangeoptions: { + minallowed: axesAttrs.autorangeoptions.minallowed, + maxallowed: axesAttrs.autorangeoptions.maxallowed, + clipmin: axesAttrs.autorangeoptions.clipmin, + clipmax: axesAttrs.autorangeoptions.clipmax, + include: axesAttrs.autorangeoptions.include, + editType: 'plot' + }, autorange: extendFlat({}, axesAttrs.autorange, {editType: 'plot'}), rangemode: { valType: 'enumerated', @@ -73,6 +81,8 @@ var radialAxisAttrs = { 'of the input data (same behavior as for cartesian axes).' ].join(' ') }, + minallowed: extendFlat({}, axesAttrs.minallowed, {editType: 'plot'}), + maxallowed: extendFlat({}, axesAttrs.maxallowed, {editType: 'plot'}), range: extendFlat({}, axesAttrs.range, { items: [ {valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}}, diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index dbcaece4794..25dd745851d 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -13,6 +13,7 @@ var handleTickLabelDefaults = require('../cartesian/tick_label_defaults'); var handlePrefixSuffixDefaults = require('../cartesian/prefix_suffix_defaults'); var handleCategoryOrderDefaults = require('../cartesian/category_order_defaults'); var handleLineGridDefaults = require('../cartesian/line_grid_defaults'); +var handleAutorangeOptionsDefaults = require('../cartesian/autorange_options_defaults'); var autoType = require('../cartesian/axis_autotype'); var layoutAttributes = require('./layout_attributes'); @@ -76,12 +77,39 @@ function handleDefaults(contIn, contOut, coerce, opts) { switch(axName) { case 'radialaxis': - var autoRange = coerceAxis('autorange', !axOut.isValidRange(axIn.range)); - axIn.autorange = autoRange; - if(autoRange && (axType === 'linear' || axType === '-')) coerceAxis('rangemode'); - if(autoRange === 'reversed') axOut._m = -1; + coerceAxis('minallowed'); + coerceAxis('maxallowed'); + var range = coerceAxis('range'); + var autorangeDflt = axOut.getAutorangeDflt(range); + var autorange = coerceAxis('autorange', autorangeDflt); + var shouldAutorange; + + // validate range and set autorange true for invalid partial ranges + if(range && ( + (range[0] === null && range[1] === null) || + ((range[0] === null || range[1] === null) && (autorange === 'reversed' || autorange === true)) || + (range[0] !== null && (autorange === 'min' || autorange === 'max reversed')) || + (range[1] !== null && (autorange === 'max' || autorange === 'min reversed')) + )) { + range = undefined; + delete axOut.range; + axOut.autorange = true; + shouldAutorange = true; + } + + if(!shouldAutorange) { + autorangeDflt = axOut.getAutorangeDflt(range); + autorange = coerceAxis('autorange', autorangeDflt); + } + + axIn.autorange = autorange; + if(autorange) { + handleAutorangeOptionsDefaults(coerceAxis, autorange, range); + + if(axType === 'linear' || axType === '-') coerceAxis('rangemode'); + if(axOut.isReversed()) axOut._m = -1; + } - coerceAxis('range'); axOut.cleanRange('range', {dfltRange: [0, 1]}); break; diff --git a/test/image/baselines/axes_labels.png b/test/image/baselines/axes_labels.png index 5e4cf38041e..ec86c0b4c84 100644 Binary files a/test/image/baselines/axes_labels.png and b/test/image/baselines/axes_labels.png differ diff --git a/test/image/baselines/bar-colorscale-colorbar.png b/test/image/baselines/bar-colorscale-colorbar.png index f2a46640566..f7df963ab0b 100644 Binary files a/test/image/baselines/bar-colorscale-colorbar.png and b/test/image/baselines/bar-colorscale-colorbar.png differ diff --git a/test/image/baselines/bar_errorbars_inherit_color.png b/test/image/baselines/bar_errorbars_inherit_color.png index 42969eabd48..7a120ea2dc2 100644 Binary files a/test/image/baselines/bar_errorbars_inherit_color.png and b/test/image/baselines/bar_errorbars_inherit_color.png differ diff --git a/test/image/baselines/gl2d_12.png b/test/image/baselines/gl2d_12.png index bef4ac83571..4da1a0f6d4a 100644 Binary files a/test/image/baselines/gl2d_12.png and b/test/image/baselines/gl2d_12.png differ diff --git a/test/image/baselines/gl2d_axes_labels.png b/test/image/baselines/gl2d_axes_labels.png index 0e837c388b0..8e720fcb6b0 100644 Binary files a/test/image/baselines/gl2d_axes_labels.png and b/test/image/baselines/gl2d_axes_labels.png differ diff --git a/test/image/baselines/gl2d_no-clustering2.png b/test/image/baselines/gl2d_no-clustering2.png index 8a54ca589c0..5088111b46b 100644 Binary files a/test/image/baselines/gl2d_no-clustering2.png and b/test/image/baselines/gl2d_no-clustering2.png differ diff --git a/test/image/baselines/gl3d_cone-newplot_reversed_ranges.png b/test/image/baselines/gl3d_cone-newplot_reversed_ranges.png index 3b7f65f96c6..e217892fe16 100644 Binary files a/test/image/baselines/gl3d_cone-newplot_reversed_ranges.png and b/test/image/baselines/gl3d_cone-newplot_reversed_ranges.png differ diff --git a/test/image/baselines/gl3d_isosurface_5more-surfaces_between-ranges.png b/test/image/baselines/gl3d_isosurface_5more-surfaces_between-ranges.png index 18057c6463c..aa4777b5988 100644 Binary files a/test/image/baselines/gl3d_isosurface_5more-surfaces_between-ranges.png and b/test/image/baselines/gl3d_isosurface_5more-surfaces_between-ranges.png differ diff --git a/test/image/baselines/gl3d_isosurface_out_of_iso_range_case.png b/test/image/baselines/gl3d_isosurface_out_of_iso_range_case.png index e17cfe30dbb..d10f2dbc794 100644 Binary files a/test/image/baselines/gl3d_isosurface_out_of_iso_range_case.png and b/test/image/baselines/gl3d_isosurface_out_of_iso_range_case.png differ diff --git a/test/image/baselines/gl3d_mesh3d_enable-alpha-with-rgba-color.png b/test/image/baselines/gl3d_mesh3d_enable-alpha-with-rgba-color.png index c19366e0105..f2e541b7646 100644 Binary files a/test/image/baselines/gl3d_mesh3d_enable-alpha-with-rgba-color.png and b/test/image/baselines/gl3d_mesh3d_enable-alpha-with-rgba-color.png differ diff --git a/test/image/baselines/polar_fills.png b/test/image/baselines/polar_fills.png index 4f4fe0e66df..7b4e0ac3a37 100644 Binary files a/test/image/baselines/polar_fills.png and b/test/image/baselines/polar_fills.png differ diff --git a/test/image/baselines/zz-partial-autorange-matching-axes.png b/test/image/baselines/zz-partial-autorange-matching-axes.png new file mode 100644 index 00000000000..90b855d9a32 Binary files /dev/null and b/test/image/baselines/zz-partial-autorange-matching-axes.png differ diff --git a/test/image/mocks/axes_labels.json b/test/image/mocks/axes_labels.json index d300a497a4f..ce8a4367e98 100644 --- a/test/image/mocks/axes_labels.json +++ b/test/image/mocks/axes_labels.json @@ -53,6 +53,10 @@ ], "layout": { "xaxis": { + "autorangeoptions": { + "clipmin": 0.5, + "clipmax": 7.5 + }, "tickfont": { "color": "black", "family": "Old Standard TT, serif", @@ -72,6 +76,10 @@ "exponentformat": "e" }, "yaxis": { + "autorangeoptions": { + "clipmin": "5e-1", + "clipmax": "7.5" + }, "tickfont": { "color": "black", "family": "Old Standard TT, serif", diff --git a/test/image/mocks/bar-colorscale-colorbar.json b/test/image/mocks/bar-colorscale-colorbar.json index 1c5906d1afc..7a6aa323af4 100644 --- a/test/image/mocks/bar-colorscale-colorbar.json +++ b/test/image/mocks/bar-colorscale-colorbar.json @@ -58,6 +58,7 @@ "text": "bars + colorbar = <3" }, "xaxis": { + "autorangeoptions" : {"minallowed": 0.5}, "type": "linear", "range": [ -0.5, diff --git a/test/image/mocks/bar_errorbars_inherit_color.json b/test/image/mocks/bar_errorbars_inherit_color.json index a1b604d5bfa..2f3053705bb 100644 --- a/test/image/mocks/bar_errorbars_inherit_color.json +++ b/test/image/mocks/bar_errorbars_inherit_color.json @@ -63,6 +63,9 @@ ], "layout": { "width": 600, + "yaxis": { + "autorangeoptions" : {"maxallowed": 1100} + }, "title": { "text": "Bar chart error bars inherit color from marker.line not marker" } diff --git a/test/image/mocks/gl2d_12.json b/test/image/mocks/gl2d_12.json index ab5025c2abe..73182786c26 100644 --- a/test/image/mocks/gl2d_12.json +++ b/test/image/mocks/gl2d_12.json @@ -696,10 +696,6 @@ "height": 496, "xaxis": { "title": {"text": "GDP per capita (2000 dollars)"}, - "range": [ - 2.1277059579936974, - 5.024737566733606 - ], "domain": [ 0, 1 @@ -736,7 +732,10 @@ "position": 0, "mirror": "all", "overlaying": false, - "autorange": true + "autorange": true, + "autorangeoptions": { + "clipmin": "1e3" + } }, "yaxis": { "title": {"text": "Life Expectancy (years)"}, diff --git a/test/image/mocks/gl2d_axes_labels.json b/test/image/mocks/gl2d_axes_labels.json index 9ddebed7ff9..722a51a8323 100644 --- a/test/image/mocks/gl2d_axes_labels.json +++ b/test/image/mocks/gl2d_axes_labels.json @@ -53,6 +53,10 @@ ], "layout": { "xaxis": { + "autorangeoptions": { + "clipmin": 0.5, + "clipmax": 7.5 + }, "tickfont": { "color": "black", "family": "Old Standard TT, serif", @@ -72,6 +76,10 @@ "exponentformat": "e" }, "yaxis": { + "autorangeoptions": { + "clipmin": "5e-1", + "clipmax": "7.5" + }, "tickfont": { "color": "black", "family": "Old Standard TT, serif", diff --git a/test/image/mocks/gl2d_no-clustering2.json b/test/image/mocks/gl2d_no-clustering2.json index 7e103bccfe0..354670fcd1b 100644 --- a/test/image/mocks/gl2d_no-clustering2.json +++ b/test/image/mocks/gl2d_no-clustering2.json @@ -136779,19 +136779,13 @@ "height": 800, "xaxis": { "type": "linear", - "range": [ - -291004659.97729975, - 4918589252.903031 - ], - "autorange": true + "minallowed": 0, + "maxallowed": 4000000000 }, "yaxis": { "type": "linear", - "range": [ - -4316.259331857845, - 72693.25968258428 - ], - "autorange": true + "minallowed": 0, + "maxallowed": 70000 } } } diff --git a/test/image/mocks/gl3d_cone-newplot_reversed_ranges.json b/test/image/mocks/gl3d_cone-newplot_reversed_ranges.json index 5cabace3e83..86b3cf89a49 100644 --- a/test/image/mocks/gl3d_cone-newplot_reversed_ranges.json +++ b/test/image/mocks/gl3d_cone-newplot_reversed_ranges.json @@ -134,7 +134,20 @@ "text": "Cone objects with Y-axis using autorange: 'reversed'" }, "scene": { + "zaxis": { + "autorangeoptions": { + "clipmin": -0.6 + } + }, + "xaxis": { + "autorangeoptions": { + "clipmin": -0.6 + } + }, "yaxis": { + "autorangeoptions": { + "clipmax": 0.6 + }, "autorange": "reversed" }, "camera": { diff --git a/test/image/mocks/gl3d_isosurface_5more-surfaces_between-ranges.json b/test/image/mocks/gl3d_isosurface_5more-surfaces_between-ranges.json index 1c5ec45cd30..3777f130dde 100644 --- a/test/image/mocks/gl3d_isosurface_5more-surfaces_between-ranges.json +++ b/test/image/mocks/gl3d_isosurface_5more-surfaces_between-ranges.json @@ -2641,9 +2641,9 @@ "y": 1.3, "z": 1 }, - "xaxis": { "nticks": 12 }, - "yaxis": { "nticks": 12 }, - "zaxis": { "nticks": 12 }, + "xaxis": { "nticks": 12, "minallowed": 0.25 }, + "yaxis": { "nticks": 12, "minallowed": 0.25 }, + "zaxis": { "nticks": 12, "maxallowed": 0.75 }, "camera": { "eye": { "x": -1.2, diff --git a/test/image/mocks/gl3d_isosurface_out_of_iso_range_case.json b/test/image/mocks/gl3d_isosurface_out_of_iso_range_case.json index 5d5d317bbe7..77c5fff1a96 100644 --- a/test/image/mocks/gl3d_isosurface_out_of_iso_range_case.json +++ b/test/image/mocks/gl3d_isosurface_out_of_iso_range_case.json @@ -274,8 +274,18 @@ "height": 900, "scene": { "xaxis": { "nticks": 12}, - "yaxis": { "nticks": 12}, - "zaxis": { "nticks": 12}, + "yaxis": { + "autorangeoptions": { + "clipmax": 0 + }, + "nticks": 12 + }, + "zaxis": { + "autorangeoptions": { + "clipmax": 0 + }, + "nticks": 12 + }, "camera": { "eye": { "x": 1.4, diff --git a/test/image/mocks/gl3d_mesh3d_enable-alpha-with-rgba-color.json b/test/image/mocks/gl3d_mesh3d_enable-alpha-with-rgba-color.json index 7ad7098bded..8573b2b74ef 100644 --- a/test/image/mocks/gl3d_mesh3d_enable-alpha-with-rgba-color.json +++ b/test/image/mocks/gl3d_mesh3d_enable-alpha-with-rgba-color.json @@ -20,6 +20,17 @@ "title": { "text": "Should draw transparent mesh
when having transparent rgba color" }, + "scene": { + "xaxis": { + "autorangeoptions" : {"minallowed": -0.5} + }, + "yaxis": { + "autorangeoptions" : {"include": [2, -1]} + }, + "zaxis": { + "autorangeoptions" : {"maxallowed": 2.5} + } + }, "width": 400, "height": 400 } diff --git a/test/image/mocks/polar_fills.json b/test/image/mocks/polar_fills.json index 12c333a7af7..d42ee16c5db 100644 --- a/test/image/mocks/polar_fills.json +++ b/test/image/mocks/polar_fills.json @@ -37,7 +37,10 @@ "y": [0, 0.46] }, "radialaxis": { - "range": [0, 5] + "autorangeoptions": { + "clipmin": 0.5, + "clipmax": 3.5 + } } }, "polar2": { @@ -46,7 +49,10 @@ "y": [0.54, 1] }, "radialaxis": { - "range": [2, 5] + "autorangeoptions": { + "clipmin": 2, + "clipmax": 5 + } } }, "showlegend": false, diff --git a/test/image/mocks/zz-partial-autorange-matching-axes.json b/test/image/mocks/zz-partial-autorange-matching-axes.json new file mode 100644 index 00000000000..fc8a20f3ec6 --- /dev/null +++ b/test/image/mocks/zz-partial-autorange-matching-axes.json @@ -0,0 +1,105 @@ +{ + "data": [ + { + "x": [ + 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 + ], + "xaxis": "x", + "y": [ + 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 + ], + "yaxis": "y", + "mode": "markers" + }, + { + "x": [ + 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 + ], + "xaxis": "x2", + "y": [ + 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 + ], + "yaxis": "y2", + "mode": "markers" + }, + { + "x": [ + 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 + ], + "xaxis": "x3", + "y": [ + 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 + ], + "yaxis": "y3", + "mode": "markers" + } + ], + "layout": { + "xaxis": { + "anchor": "y", + "domain": [0.0, 0.3], + "title": { + "text": "sepal_width" + }, + "range": [1.5, null] + }, + "yaxis": { + "anchor": "x", + "domain": [0.0, 1.0], + "title": { + "text": "sepal_length" + }, + "range": [3, null] + }, + "xaxis2": { + "anchor": "y2", + "domain": [0.35, 0.65], + "matches": "x", + "title": { + "text": "sepal_width" + } + }, + "yaxis2": { + "anchor": "x2", + "matches": "y", + "showticklabels": false + }, + "xaxis3": { + "anchor": "y3", + "domain": [0.65, 1], + "matches": "x", + "title": { + "text": "sepal_width" + } + }, + "yaxis3": { + "anchor": "x3", + "matches": "y", + "showticklabels": false + }, + "showlegend": false, + "margin": { + "t": 50 + }, + "width": 1200, + "height": 400 + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index fdbdec6130e..8b65344e5bb 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -718,13 +718,22 @@ describe('Test axes', function() { it('should set autorange to true when input range is invalid', function() { layoutIn = { xaxis: { range: 'not-gonna-work' }, - xaxis2: { range: [1, 2, 3] }, + xaxis2: { range: [1] }, + xaxis3: { range: [null, null] }, yaxis: { range: ['a', 2] }, yaxis2: { range: [1, 'b'] }, - yaxis3: { range: [null, {}] } + yaxis3: { range: [undefined, {}] }, + yaxis4: { range: [1, null], autorange: 'min' }, // second range is null not first + yaxis5: { range: [null, 2], autorange: 'max' }, // first range is null not second + yaxis6: { range: [1, null], autorange: 'max reversed' }, // second range is null not first + yaxis7: { range: [null, 2], autorange: 'min reversed' }, // first range is null not second + yaxis8: { range: [1, null], autorange: 'reversed' }, + yaxis9: { range: [null, 2], autorange: 'reversed' }, + yaxis10: { range: [1, null], autorange: true }, + yaxis11: { range: [null, 2], autorange: true }, }; - layoutOut._subplots.cartesian.push('x2y2', 'xy3'); - layoutOut._subplots.yaxis.push('x2', 'y2', 'y3'); + layoutOut._subplots.cartesian.push('x2y2', 'xy3', 'x3y4', 'x3y5', 'x3y6', 'x3y7', 'x3y9', 'x3y9', 'x3y10', 'x3y11'); + layoutOut._subplots.yaxis.push('x2', 'x3', 'y2', 'y3', 'y4', 'y5', 'y6', 'y7', 'y8', 'y9', 'y10', 'y11'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -750,6 +759,36 @@ describe('Test axes', function() { }); }); + it('should set autorange to true when range[0] and range[1] are set to null', function() { + layoutIn = { + xaxis: { range: [null, null] } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.autorange).toBe(true); + }); + + it('should set autorange to min when range[0] is set to null', function() { + layoutIn = { + xaxis: { range: [null, 1] } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.autorange).toBe('min'); + }); + + it('should set autorange to max when range[1] is set to null', function() { + layoutIn = { + xaxis: { range: [1, null] } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.autorange).toBe('max'); + }); + it('only allows rangemode with linear axes', function() { layoutIn = { xaxis: {type: 'log', rangemode: 'tozero'}, @@ -2494,7 +2533,7 @@ describe('Test axes', function() { }; }); - it('should save range when autosize turned off and rangeInitial isn\'t defined', function() { + it('should save range when autosize turned off and rangeInitials are not defined', function() { ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(ax) { gd._fullLayout[ax].autorange = false; }); @@ -2502,39 +2541,64 @@ describe('Test axes', function() { hasOneAxisChanged = saveRangeInitial(gd); expect(hasOneAxisChanged).toBe(true); - expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.5, 1]); - expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); + expect(gd._fullLayout.xaxis._rangeInitial0).toEqual(0); + expect(gd._fullLayout.xaxis._rangeInitial1).toEqual(0.5); + + expect(gd._fullLayout.yaxis._rangeInitial0).toEqual(0); + expect(gd._fullLayout.yaxis._rangeInitial1).toEqual(0.5); + + expect(gd._fullLayout.xaxis2._rangeInitial0).toEqual(0.5); + expect(gd._fullLayout.xaxis2._rangeInitial1).toEqual(1); + + expect(gd._fullLayout.yaxis2._rangeInitial0).toEqual(0.5); + expect(gd._fullLayout.yaxis2._rangeInitial1).toEqual(1); }); - it('should not overwrite saved range if rangeInitial is defined', function() { + it('should not overwrite saved range if rangeInitials are defined', function() { ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(ax) { - gd._fullLayout[ax]._rangeInitial = gd._fullLayout[ax].range.slice(); + gd._fullLayout[ax]._rangeInitial0 = gd._fullLayout[ax].range[0]; + gd._fullLayout[ax]._rangeInitial1 = gd._fullLayout[ax].range[1]; gd._fullLayout[ax].range = [0, 1]; }); hasOneAxisChanged = saveRangeInitial(gd); expect(hasOneAxisChanged).toBe(false); - expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.5, 1]); - expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); + + expect(gd._fullLayout.xaxis._rangeInitial0).toEqual(0); + expect(gd._fullLayout.xaxis._rangeInitial1).toEqual(0.5); + + expect(gd._fullLayout.yaxis._rangeInitial0).toEqual(0); + expect(gd._fullLayout.yaxis._rangeInitial1).toEqual(0.5); + + expect(gd._fullLayout.xaxis2._rangeInitial0).toEqual(0.5); + expect(gd._fullLayout.xaxis2._rangeInitial1).toEqual(1); + + expect(gd._fullLayout.yaxis2._rangeInitial0).toEqual(0.5); + expect(gd._fullLayout.yaxis2._rangeInitial1).toEqual(1); }); it('should save range when overwrite option is on and range has changed', function() { ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(ax) { - gd._fullLayout[ax]._rangeInitial = gd._fullLayout[ax].range.slice(); + gd._fullLayout[ax]._rangeInitial0 = gd._fullLayout[ax].range[0]; + gd._fullLayout[ax]._rangeInitial1 = gd._fullLayout[ax].range[1]; }); gd._fullLayout.xaxis2.range = [0.2, 0.4]; hasOneAxisChanged = saveRangeInitial(gd, true); expect(hasOneAxisChanged).toBe(true); - expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.2, 0.4]); - expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); + + expect(gd._fullLayout.xaxis._rangeInitial0).toEqual(0); + expect(gd._fullLayout.xaxis._rangeInitial1).toEqual(0.5); + + expect(gd._fullLayout.yaxis._rangeInitial0).toEqual(0); + expect(gd._fullLayout.yaxis._rangeInitial1).toEqual(0.5); + + expect(gd._fullLayout.xaxis2._rangeInitial0).toEqual(0.2); + expect(gd._fullLayout.xaxis2._rangeInitial1).toEqual(0.4); + + expect(gd._fullLayout.yaxis2._rangeInitial0).toEqual(0.5); + expect(gd._fullLayout.yaxis2._rangeInitial1).toEqual(1); }); }); diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index c426bbb3844..9ae83eab8fe 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -958,9 +958,14 @@ describe('Test click interactions:', function() { var ya = fullLayout.yaxis; var ya2 = fullLayout.yaxis2; - expect(xa._rangeInitial).toBe(undefined); - expect(ya._rangeInitial).toBe(undefined); - expect(ya2._rangeInitial).toBe(undefined); + expect(xa._rangeInitial0).toBe(undefined); + expect(xa._rangeInitial1).toBe(undefined); + + expect(ya._rangeInitial0).toBe(undefined); + expect(ya._rangeInitial1).toBe(undefined); + + expect(ya2._rangeInitial0).toBe(undefined); + expect(ya2._rangeInitial1).toBe(undefined); expect(xa.range).toBeCloseToArray(exp.xRng, 1, msg); expect(ya.range).toBeCloseToArray(exp.yRng, 1, msg); @@ -969,8 +974,12 @@ describe('Test click interactions:', function() { Plotly.newPlot(gd, [], {}) .then(function() { - expect(gd._fullLayout.xaxis._rangeInitial).toBe(undefined); - expect(gd._fullLayout.yaxis._rangeInitial).toBe(undefined); + expect(gd._fullLayout.xaxis._rangeInitial0).toBe(undefined); + expect(gd._fullLayout.xaxis._rangeInitial1).toBe(undefined); + + expect(gd._fullLayout.yaxis._rangeInitial0).toBe(undefined); + expect(gd._fullLayout.yaxis._rangeInitial1).toBe(undefined); + expect(gd._fullLayout.yaxis2).toBe(undefined); }) .then(function() { return Plotly.react(gd, fig); }) @@ -1008,6 +1017,64 @@ describe('Test click interactions:', function() { }) .then(done, done.fail); }); + + it('when set to \'reset+autorange\' (the default) should autosize on 1st and 2nd double clicks (case of partial ranges)', function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.newPlot(gd, [{ + y: [1, 2, 3, 4]} + ], { + xaxis: {range: [1, null]}, + yaxis: {range: [null, 3]}, + width: 600, + height: 600 + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([1, 3.2]); + expect(gd.layout.yaxis.range).toBeCloseToArray([0.8, 3]); + + return doubleClick(300, 300); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-0.2, 3.2]); + expect(gd.layout.yaxis.range).toBeCloseToArray([0.8, 4.2]); + + return doubleClick(300, 300); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([1, 3.2]); + expect(gd.layout.yaxis.range).toBeCloseToArray([0.8, 3]); + }) + .then(done, done.fail); + }); + + it('when set to \'reset+autorange\' (the default) should autosize on 1st and 2nd double clicks (case of partial ranges reversed)', function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.newPlot(gd, [{ + y: [1, 2, 3, 4]} + ], { + xaxis: {range: [null, 1], autorange: 'max reversed'}, + yaxis: {range: [3, null], autorange: 'min reversed'}, + width: 600, + height: 600 + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([3.2, 1]); + expect(gd.layout.yaxis.range).toBeCloseToArray([3, 0.8]); + + return doubleClick(300, 300); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([3.2, -0.2]); + expect(gd.layout.yaxis.range).toBeCloseToArray([4.2, 0.8]); + + return doubleClick(300, 300); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([3.2, 1]); + expect(gd.layout.yaxis.range).toBeCloseToArray([3, 0.8]); + }) + .then(done, done.fail); + }); }); describe('zoom interactions', function() { diff --git a/test/plot-schema.json b/test/plot-schema.json index e83e2591818..60fbf07937d 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -4409,7 +4409,7 @@ "valType": "angle" }, "autorange": { - "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided, then `autorange` is set to *false*.", + "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided and it has a value for both the lower and upper bound, `autorange` is set to *false*. Using *min* applies autorange only to set the minimum. Using *max* applies autorange only to set the maximum. Using *min reversed* applies autorange only to set the minimum on a reversed axis. Using *max reversed* applies autorange only to set the maximum on a reversed axis. Using *reversed* applies autorange on both ends and reverses the axis direction.", "dflt": true, "editType": "plot", "impliedEdits": {}, @@ -4417,9 +4417,53 @@ "values": [ true, false, - "reversed" + "reversed", + "min reversed", + "max reversed", + "min", + "max" ] }, + "autorangeoptions": { + "clipmax": { + "description": "Clip autorange maximum if it goes beyond this value. Has no effect when `autorangeoptions.maxallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "clipmin": { + "description": "Clip autorange minimum if it goes beyond this value. Has no effect when `autorangeoptions.minallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "editType": "plot", + "include": { + "arrayOk": true, + "description": "Ensure this value is included in autorange.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "includesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `include`.", + "editType": "none", + "valType": "string" + }, + "maxallowed": { + "description": "Use this value exactly as autorange maximum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "minallowed": { + "description": "Use this value exactly as autorange minimum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "role": "object" + }, "autotypenumbers": { "description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.", "dflt": "convert types", @@ -4579,6 +4623,22 @@ "min": 0, "valType": "number" }, + "maxallowed": { + "description": "Determines the maximum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, + "minallowed": { + "description": "Determines the minimum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, "minexponent": { "description": "Hide SI prefix for 10^n if |n| is below this number. This only has an effect when `tickformat` is *SI* or *B*.", "dflt": 3, @@ -4595,7 +4655,7 @@ }, "range": { "anim": true, - "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears.", + "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears. Leaving either or both elements `null` impacts the default `autorange`.", "editType": "plot", "impliedEdits": { "autorange": false @@ -5520,7 +5580,7 @@ } }, "autorange": { - "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided, then `autorange` is set to *false*.", + "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided and it has a value for both the lower and upper bound, `autorange` is set to *false*. Using *min* applies autorange only to set the minimum. Using *max* applies autorange only to set the maximum. Using *min reversed* applies autorange only to set the minimum on a reversed axis. Using *max reversed* applies autorange only to set the maximum on a reversed axis. Using *reversed* applies autorange on both ends and reverses the axis direction.", "dflt": true, "editType": "plot", "impliedEdits": {}, @@ -5528,9 +5588,53 @@ "values": [ true, false, - "reversed" + "reversed", + "min reversed", + "max reversed", + "min", + "max" ] }, + "autorangeoptions": { + "clipmax": { + "description": "Clip autorange maximum if it goes beyond this value. Has no effect when `autorangeoptions.maxallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "clipmin": { + "description": "Clip autorange minimum if it goes beyond this value. Has no effect when `autorangeoptions.minallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "editType": "plot", + "include": { + "arrayOk": true, + "description": "Ensure this value is included in autorange.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "includesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `include`.", + "editType": "none", + "valType": "string" + }, + "maxallowed": { + "description": "Use this value exactly as autorange maximum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "minallowed": { + "description": "Use this value exactly as autorange minimum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "role": "object" + }, "autotypenumbers": { "description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.", "dflt": "convert types", @@ -5672,6 +5776,22 @@ "min": 0, "valType": "number" }, + "maxallowed": { + "description": "Determines the maximum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, + "minallowed": { + "description": "Determines the minimum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, "minexponent": { "description": "Hide SI prefix for 10^n if |n| is below this number. This only has an effect when `tickformat` is *SI* or *B*.", "dflt": 3, @@ -5701,7 +5821,7 @@ }, "range": { "anim": false, - "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears.", + "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears. Leaving either or both elements `null` impacts the default `autorange`.", "editType": "plot", "impliedEdits": { "autorange": false @@ -6088,7 +6208,7 @@ } }, "autorange": { - "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided, then `autorange` is set to *false*.", + "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided and it has a value for both the lower and upper bound, `autorange` is set to *false*. Using *min* applies autorange only to set the minimum. Using *max* applies autorange only to set the maximum. Using *min reversed* applies autorange only to set the minimum on a reversed axis. Using *max reversed* applies autorange only to set the maximum on a reversed axis. Using *reversed* applies autorange on both ends and reverses the axis direction.", "dflt": true, "editType": "plot", "impliedEdits": {}, @@ -6096,9 +6216,53 @@ "values": [ true, false, - "reversed" + "reversed", + "min reversed", + "max reversed", + "min", + "max" ] }, + "autorangeoptions": { + "clipmax": { + "description": "Clip autorange maximum if it goes beyond this value. Has no effect when `autorangeoptions.maxallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "clipmin": { + "description": "Clip autorange minimum if it goes beyond this value. Has no effect when `autorangeoptions.minallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "editType": "plot", + "include": { + "arrayOk": true, + "description": "Ensure this value is included in autorange.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "includesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `include`.", + "editType": "none", + "valType": "string" + }, + "maxallowed": { + "description": "Use this value exactly as autorange maximum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "minallowed": { + "description": "Use this value exactly as autorange minimum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "role": "object" + }, "autotypenumbers": { "description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.", "dflt": "convert types", @@ -6240,6 +6404,22 @@ "min": 0, "valType": "number" }, + "maxallowed": { + "description": "Determines the maximum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, + "minallowed": { + "description": "Determines the minimum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, "minexponent": { "description": "Hide SI prefix for 10^n if |n| is below this number. This only has an effect when `tickformat` is *SI* or *B*.", "dflt": 3, @@ -6269,7 +6449,7 @@ }, "range": { "anim": false, - "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears.", + "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears. Leaving either or both elements `null` impacts the default `autorange`.", "editType": "plot", "impliedEdits": { "autorange": false @@ -6656,7 +6836,7 @@ } }, "autorange": { - "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided, then `autorange` is set to *false*.", + "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided and it has a value for both the lower and upper bound, `autorange` is set to *false*. Using *min* applies autorange only to set the minimum. Using *max* applies autorange only to set the maximum. Using *min reversed* applies autorange only to set the minimum on a reversed axis. Using *max reversed* applies autorange only to set the maximum on a reversed axis. Using *reversed* applies autorange on both ends and reverses the axis direction.", "dflt": true, "editType": "plot", "impliedEdits": {}, @@ -6664,9 +6844,53 @@ "values": [ true, false, - "reversed" + "reversed", + "min reversed", + "max reversed", + "min", + "max" ] }, + "autorangeoptions": { + "clipmax": { + "description": "Clip autorange maximum if it goes beyond this value. Has no effect when `autorangeoptions.maxallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "clipmin": { + "description": "Clip autorange minimum if it goes beyond this value. Has no effect when `autorangeoptions.minallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "editType": "plot", + "include": { + "arrayOk": true, + "description": "Ensure this value is included in autorange.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "includesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `include`.", + "editType": "none", + "valType": "string" + }, + "maxallowed": { + "description": "Use this value exactly as autorange maximum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "minallowed": { + "description": "Use this value exactly as autorange minimum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "role": "object" + }, "autotypenumbers": { "description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.", "dflt": "convert types", @@ -6808,6 +7032,22 @@ "min": 0, "valType": "number" }, + "maxallowed": { + "description": "Determines the maximum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, + "minallowed": { + "description": "Determines the minimum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, "minexponent": { "description": "Hide SI prefix for 10^n if |n| is below this number. This only has an effect when `tickformat` is *SI* or *B*.", "dflt": 3, @@ -6837,7 +7077,7 @@ }, "range": { "anim": false, - "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears.", + "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears. Leaving either or both elements `null` impacts the default `autorange`.", "editType": "plot", "impliedEdits": { "autorange": false @@ -10404,7 +10644,7 @@ "valType": "flaglist" }, "autorange": { - "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided, then `autorange` is set to *false*.", + "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided and it has a value for both the lower and upper bound, `autorange` is set to *false*. Using *min* applies autorange only to set the minimum. Using *max* applies autorange only to set the maximum. Using *min reversed* applies autorange only to set the minimum on a reversed axis. Using *max reversed* applies autorange only to set the maximum on a reversed axis. Using *reversed* applies autorange on both ends and reverses the axis direction.", "dflt": true, "editType": "axrange", "impliedEdits": {}, @@ -10412,9 +10652,53 @@ "values": [ true, false, - "reversed" + "reversed", + "min reversed", + "max reversed", + "min", + "max" ] }, + "autorangeoptions": { + "clipmax": { + "description": "Clip autorange maximum if it goes beyond this value. Has no effect when `autorangeoptions.maxallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "clipmin": { + "description": "Clip autorange minimum if it goes beyond this value. Has no effect when `autorangeoptions.minallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "editType": "plot", + "include": { + "arrayOk": true, + "description": "Ensure this value is included in autorange.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "includesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `include`.", + "editType": "none", + "valType": "string" + }, + "maxallowed": { + "description": "Use this value exactly as autorange maximum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "minallowed": { + "description": "Use this value exactly as autorange minimum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "role": "object" + }, "autotypenumbers": { "description": "Using *strict* a numeric string in trace data is not converted to a number. Using *convert types* a numeric string in trace data may be treated as a number during automatic axis `type` detection. Defaults to layout.autotypenumbers.", "dflt": "convert types", @@ -10646,6 +10930,22 @@ "/^y([2-9]|[1-9][0-9]+)?( domain)?$/" ] }, + "maxallowed": { + "description": "Determines the maximum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, + "minallowed": { + "description": "Determines the minimum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, "minexponent": { "description": "Hide SI prefix for 10^n if |n| is below this number. This only has an effect when `tickformat` is *SI* or *B*.", "dflt": 3, @@ -10800,7 +11100,7 @@ }, "range": { "anim": true, - "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears.", + "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears. Leaving either or both elements `null` impacts the default `autorange`.", "editType": "axrange", "impliedEdits": { "autorange": false @@ -11663,7 +11963,7 @@ "valType": "flaglist" }, "autorange": { - "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided, then `autorange` is set to *false*.", + "description": "Determines whether or not the range of this axis is computed in relation to the input data. See `rangemode` for more info. If `range` is provided and it has a value for both the lower and upper bound, `autorange` is set to *false*. Using *min* applies autorange only to set the minimum. Using *max* applies autorange only to set the maximum. Using *min reversed* applies autorange only to set the minimum on a reversed axis. Using *max reversed* applies autorange only to set the maximum on a reversed axis. Using *reversed* applies autorange on both ends and reverses the axis direction.", "dflt": true, "editType": "axrange", "impliedEdits": {}, @@ -11671,9 +11971,53 @@ "values": [ true, false, - "reversed" + "reversed", + "min reversed", + "max reversed", + "min", + "max" ] }, + "autorangeoptions": { + "clipmax": { + "description": "Clip autorange maximum if it goes beyond this value. Has no effect when `autorangeoptions.maxallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "clipmin": { + "description": "Clip autorange minimum if it goes beyond this value. Has no effect when `autorangeoptions.minallowed` is provided.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "editType": "plot", + "include": { + "arrayOk": true, + "description": "Ensure this value is included in autorange.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "includesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `include`.", + "editType": "none", + "valType": "string" + }, + "maxallowed": { + "description": "Use this value exactly as autorange maximum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "minallowed": { + "description": "Use this value exactly as autorange minimum.", + "editType": "plot", + "impliedEdits": {}, + "valType": "any" + }, + "role": "object" + }, "autoshift": { "description": "Automatically reposition the axis to avoid overlap with other axes with the same `overlaying` value. This repositioning will account for any `shift` amount applied to other axes on the same side with `autoshift` is set to true. Only has an effect if `anchor` is set to *free*.", "dflt": false, @@ -11911,6 +12255,22 @@ "/^y([2-9]|[1-9][0-9]+)?( domain)?$/" ] }, + "maxallowed": { + "description": "Determines the maximum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, + "minallowed": { + "description": "Determines the minimum range of this axis.", + "editType": "plot", + "impliedEdits": { + "^autorange": false + }, + "valType": "any" + }, "minexponent": { "description": "Hide SI prefix for 10^n if |n| is below this number. This only has an effect when `tickformat` is *SI* or *B*.", "dflt": 3, @@ -12065,7 +12425,7 @@ }, "range": { "anim": true, - "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears.", + "description": "Sets the range of this axis. If the axis `type` is *log*, then you must take the log of your desired range (e.g. to set the range from 1 to 100, set the range from 0 to 2). If the axis `type` is *date*, it should be date strings, like date data, though Date objects and unix milliseconds will be accepted and converted to strings. If the axis `type` is *category*, it should be numbers, using the scale where each category is assigned a serial number from zero in the order it appears. Leaving either or both elements `null` impacts the default `autorange`.", "editType": "axrange", "impliedEdits": { "autorange": false