Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bounds to range and autorange of cartesian, gl3d and radial axes #6547

Merged
merged 43 commits into from Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2b470ba
add autorange min max to cartesian, gl3d & radial axes
archmoj Mar 31, 2023
e981da5
test autorange min max on cartesian gl3d & radial axes
archmoj Mar 31, 2023
c7b57f5
add range min and max to cartesian, gl3d and radial axes
archmoj Apr 3, 2023
8c2320d
draft log for PR 6547
archmoj Apr 3, 2023
3cda56a
Merge remote-tracking branch 'origin/master' into autorange-bounds
archmoj May 2, 2023
3f2e6e7
rename autorange clip min max
archmoj May 2, 2023
cf7b264
add autorange exact min and max
archmoj May 2, 2023
78b5160
centralized function
archmoj May 2, 2023
8c61f11
add autorange include
archmoj May 2, 2023
c528781
revisit autorange include options
archmoj May 2, 2023
113ec69
centralize autorange options defaults
archmoj May 2, 2023
c7153ca
ensure min < max in case both defined
archmoj May 2, 2023
ab9d1cc
limit range during scattergl update
archmoj May 3, 2023
16a8213
limit corner draggers considering rangemin or rangemax
archmoj May 5, 2023
4d891b3
ensure ranges stay in bound after setting in dragAxList
archmoj May 10, 2023
d787b9f
revise attributes
archmoj May 17, 2023
cb55c08
introduce null range and new autorange options
archmoj May 18, 2023
595a00c
use null in ranges to set defaults
archmoj May 18, 2023
fc79461
declare isReversed() function
archmoj May 18, 2023
c38ed70
use null ranges as default values for autorange eoptions min/max allowed
archmoj May 23, 2023
f8a0501
Merge branch 'master' into autorange-bounds
archmoj May 30, 2023
14ea726
Merge remote-tracking branch 'origin/master' into autorange-bounds
archmoj Jul 19, 2023
13dfe31
update draftlog
archmoj Jul 20, 2023
308ae20
Merge remote-tracking branch 'origin/master' into autorange-bounds
archmoj Jul 25, 2023
e4f906c
Update src/plots/cartesian/layout_attributes.js
archmoj Jul 25, 2023
04b8efa
update schema test
archmoj Jul 25, 2023
086282c
fix hasValidMinAndMax for autorangeoptions allowed attrs
archmoj Jul 26, 2023
699fb78
keep null items in range when calling cleanRange
archmoj Aug 1, 2023
36cb0fa
do not set autorange to false in constraints.handleDefaults for paria…
archmoj Aug 1, 2023
8e707c6
test partial ranges on matching axes
archmoj Aug 1, 2023
c6a147f
Update src/plots/cartesian/layout_attributes.js
archmoj Aug 2, 2023
1f5b819
update schema
archmoj Aug 2, 2023
cc03cf8
Update src/plots/cartesian/layout_attributes.js
archmoj Aug 2, 2023
51ca21e
update schema
archmoj Aug 2, 2023
4527d5d
Merge branch 'master' into autorange-bounds
archmoj Aug 11, 2023
be1d805
keep _rangeInitial0 & _rangeInitial1 instead of _rangeInitial array
archmoj Aug 9, 2023
a7ce254
fix double click interactions for partial ranges
archmoj Aug 18, 2023
fbe1fb9
add jasmine test
archmoj Aug 18, 2023
f0d48e6
revisit axReverse in autorange
archmoj Aug 18, 2023
5d4f665
fix double click on min reversed and max reversed cases
archmoj Aug 18, 2023
faccfc6
validate partial ranges and set autorange true for invalid partial ra…
archmoj Aug 22, 2023
7776305
handle invalid range and reversed autorange
archmoj Aug 23, 2023
30a692e
do not accept partial ranges if autorange is set to true
archmoj Aug 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions 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)]

101 changes: 100 additions & 1 deletion src/plots/cartesian/autorange.js
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 @@ -86,6 +87,12 @@ function getAutoRange(gd, ax) {
if(ax.autorange === 'reversed') {
axReverse = true;
ax.autorange = true;
} else if(ax.autorange === 'min reversed') {
axReverse = true;
ax.autorange = 'min';
} else if(ax.autorange === 'max reversed') {
axReverse = true;
ax.autorange = 'max';
}

var rangeMode = ax.rangemode;
Expand Down Expand Up @@ -176,6 +183,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 +220,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 +634,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
@@ -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');
};
16 changes: 12 additions & 4 deletions src/plots/cartesian/axis_defaults.js
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,19 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,

setConvert(containerOut, layoutOut);

var autorangeDflt = !containerOut.isValidRange(containerIn.range);
if(autorangeDflt && options.reverseDflt) autorangeDflt = 'reversed';
coerce('minallowed');
coerce('maxallowed');
var range = coerce('range');

var autorangeDflt = containerOut.getAutorangeDflt(range, options);

var autoRange = coerce('autorange', autorangeDflt);
if(autoRange && (axType === 'linear' || axType === '-')) coerce('rangemode');
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
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
26 changes: 26 additions & 0 deletions src/plots/cartesian/dragbox.js
Expand Up @@ -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;
Expand Down Expand Up @@ -874,6 +875,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]]});
}
}
Expand Down Expand Up @@ -915,6 +923,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);
Expand All @@ -931,6 +947,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;
Expand Down Expand Up @@ -1096,6 +1120,8 @@ function dragAxList(axList, pix) {
axi.l2r(axi._rl[1] - pix / axi._m)
];
}

if(axi.limitRange) axi.limitRange();
}
}
}
Expand Down
75 changes: 72 additions & 3 deletions src/plots/cartesian/layout_attributes.js
Expand Up @@ -272,17 +272,69 @@ 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},
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*.'
'If `range` is provided and it has a value for both the',
' lower and upper bound, `autorange` is set to *false*.',
archmoj marked this conversation as resolved.
Show resolved Hide resolved
'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'],
Expand Down Expand Up @@ -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: {
Expand Down