Skip to content

Commit

Permalink
Merge pull request #6547 from plotly/autorange-bounds
Browse files Browse the repository at this point in the history
Add bounds to range and autorange of cartesian, gl3d and radial axes
  • Loading branch information
archmoj authored Aug 24, 2023
2 parents 2aa7e47 + 30a692e commit 0f63668
Show file tree
Hide file tree
Showing 42 changed files with 1,160 additions and 111 deletions.
2 changes: 2 additions & 0 deletions draftlogs/6547_add.md
Original file line number Diff line number Diff line change
@@ -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)]

12 changes: 8 additions & 4 deletions src/components/modebar/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
105 changes: 101 additions & 4 deletions src/plots/cartesian/autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var getFromId = axIds.getFromId;
var isLinked = axIds.isLinked;

module.exports = {
applyAutorangeOptions: applyAutorangeOptions,
getAutoRange: getAutoRange,
makePadFn: makePadFn,
doAutoRange: doAutoRange,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -176,6 +181,10 @@ function getAutoRange(gd, ax) {
];
}

newRange = applyAutorangeOptions(newRange, ax);

if(ax.limitRange) ax.limitRange();

// maintain reversal
if(axReverse) newRange.reverse();

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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];
}
23 changes: 23 additions & 0 deletions src/plots/cartesian/autorange_options_defaults.js
Original file line number Diff line number Diff line change
@@ -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');
};
18 changes: 12 additions & 6 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
36 changes: 31 additions & 5 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion src/plots/cartesian/constraints.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 0f63668

Please sign in to comment.