Skip to content

Commit be44366

Browse files
committed
add findExtremes
- a Axes.expend clone that does not append things to ax._min/ax._max, but instead returns two arrays, a min array and max array of potential data extremes
1 parent dfada6a commit be44366

File tree

1 file changed

+174
-1
lines changed

1 file changed

+174
-1
lines changed

src/plots/cartesian/autorange.js

+174-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ module.exports = {
1818
getAutoRange: getAutoRange,
1919
makePadFn: makePadFn,
2020
doAutoRange: doAutoRange,
21-
expand: expand
21+
expand: expand,
22+
findExtremes: findExtremes
2223
};
2324

2425
// Find the autorange for this axis
@@ -364,6 +365,178 @@ function expand(ax, data, options) {
364365
for(i = len - 1; i >= iMax; i--) addItem(i);
365366
}
366367

368+
/**
369+
* findExtremes
370+
*
371+
* Find min/max extremes of an array of coordinates on a given axis.
372+
*
373+
* Note that findExtremes is called during `calc`, when we don't yet know the axis
374+
* length; all the inputs should be based solely on the trace data, nothing
375+
* about the axis layout.
376+
*
377+
* Note that `ppad` and `vpad` as well as their asymmetric variants refer to
378+
* the before and after padding of the passed `data` array, not to the whole axis.
379+
*
380+
* @param {object} ax: full axis object
381+
* relies on
382+
* - ax.type
383+
* - ax._m (just its sign)
384+
* - ax.d2l
385+
* @param {array} data:
386+
* array of numbers (i.e. already run though ax.d2c)
387+
* @param {object} options:
388+
* available keys are:
389+
* vpad: (number or number array) pad values (data value +-vpad)
390+
* ppad: (number or number array) pad pixels (pixel location +-ppad)
391+
* ppadplus, ppadminus, vpadplus, vpadminus:
392+
* separate padding for each side, overrides symmetric
393+
* padded: (boolean) add 5% padding to both ends
394+
* (unless one end is overridden by tozero)
395+
* tozero: (boolean) make sure to include zero if axis is linear,
396+
* and make it a tight bound if possible
397+
*
398+
* @return {object}
399+
* - min {array of objects}
400+
* - max {array of objects}
401+
* each object item has fields:
402+
* - val {number}
403+
* - pad {number}
404+
* - extrappad {number}
405+
*/
406+
function findExtremes(ax, data, options) {
407+
if(!options) options = {};
408+
if(!ax._m) ax.setScale();
409+
410+
var minArray = [];
411+
var maxArray = [];
412+
413+
var len = data.length;
414+
var extrapad = options.padded || false;
415+
var tozero = options.tozero && (ax.type === 'linear' || ax.type === '-');
416+
var isLog = (ax.type === 'log');
417+
418+
var i, j, k, v, di, dmin, dmax, ppadiplus, ppadiminus, includeThis, vmin, vmax;
419+
420+
var hasArrayOption = false;
421+
422+
function makePadAccessor(item) {
423+
if(Array.isArray(item)) {
424+
hasArrayOption = true;
425+
return function(i) { return Math.max(Number(item[i]||0), 0); };
426+
}
427+
else {
428+
var v = Math.max(Number(item||0), 0);
429+
return function() { return v; };
430+
}
431+
}
432+
433+
var ppadplus = makePadAccessor((ax._m > 0 ?
434+
options.ppadplus : options.ppadminus) || options.ppad || 0);
435+
var ppadminus = makePadAccessor((ax._m > 0 ?
436+
options.ppadminus : options.ppadplus) || options.ppad || 0);
437+
var vpadplus = makePadAccessor(options.vpadplus || options.vpad);
438+
var vpadminus = makePadAccessor(options.vpadminus || options.vpad);
439+
440+
if(!hasArrayOption) {
441+
// with no arrays other than `data` we don't need to consider
442+
// every point, only the extreme data points
443+
vmin = Infinity;
444+
vmax = -Infinity;
445+
446+
if(isLog) {
447+
for(i = 0; i < len; i++) {
448+
v = data[i];
449+
// data is not linearized yet so we still have to filter out negative logs
450+
if(v < vmin && v > 0) vmin = v;
451+
if(v > vmax && v < FP_SAFE) vmax = v;
452+
}
453+
} else {
454+
for(i = 0; i < len; i++) {
455+
v = data[i];
456+
if(v < vmin && v > -FP_SAFE) vmin = v;
457+
if(v > vmax && v < FP_SAFE) vmax = v;
458+
}
459+
}
460+
461+
data = [vmin, vmax];
462+
len = 2;
463+
}
464+
465+
function addItem(i) {
466+
di = data[i];
467+
if(!isNumeric(di)) return;
468+
ppadiplus = ppadplus(i);
469+
ppadiminus = ppadminus(i);
470+
vmin = di - vpadminus(i);
471+
vmax = di + vpadplus(i);
472+
// special case for log axes: if vpad makes this object span
473+
// more than an order of mag, clip it to one order. This is so
474+
// we don't have non-positive errors or absurdly large lower
475+
// range due to rounding errors
476+
if(isLog && vmin < vmax / 10) vmin = vmax / 10;
477+
478+
dmin = ax.c2l(vmin);
479+
dmax = ax.c2l(vmax);
480+
481+
if(tozero) {
482+
dmin = Math.min(0, dmin);
483+
dmax = Math.max(0, dmax);
484+
}
485+
486+
for(k = 0; k < 2; k++) {
487+
var newVal = k ? dmax : dmin;
488+
if(goodNumber(newVal)) {
489+
var extremes = k ? maxArray : minArray;
490+
var newPad = k ? ppadiplus : ppadiminus;
491+
var atLeastAsExtreme = k ? greaterOrEqual : lessOrEqual;
492+
493+
includeThis = true;
494+
/*
495+
* Take items v from ax._min/_max and compare them to the presently active point:
496+
* - Since we don't yet know the relationship between pixels and values
497+
* (that's what we're trying to figure out!) AND we don't yet know how
498+
* many pixels `extrapad` represents (it's going to be 5% of the length,
499+
* but we don't want to have to redo _min and _max just because length changed)
500+
* two point must satisfy three criteria simultaneously for one to supersede the other:
501+
* - at least as extreme a `val`
502+
* - at least as big a `pad`
503+
* - an unpadded point cannot supersede a padded point, but any other combination can
504+
*
505+
* - If the item supersedes the new point, set includethis false
506+
* - If the new pt supersedes the item, delete it from ax._min/_max
507+
*/
508+
for(j = 0; j < extremes.length && includeThis; j++) {
509+
v = extremes[j];
510+
if(atLeastAsExtreme(v.val, newVal) && v.pad >= newPad && (v.extrapad || !extrapad)) {
511+
includeThis = false;
512+
break;
513+
} else if(atLeastAsExtreme(newVal, v.val) && v.pad <= newPad && (extrapad || !v.extrapad)) {
514+
extremes.splice(j, 1);
515+
j--;
516+
}
517+
}
518+
if(includeThis) {
519+
var clipAtZero = (tozero && newVal === 0);
520+
extremes.push({
521+
val: newVal,
522+
pad: clipAtZero ? 0 : newPad,
523+
extrapad: clipAtZero ? false : extrapad
524+
});
525+
}
526+
}
527+
}
528+
}
529+
530+
// For efficiency covering monotonic or near-monotonic data,
531+
// check a few points at both ends first and then sweep
532+
// through the middle
533+
var iMax = Math.min(6, len);
534+
for(i = 0; i < iMax; i++) addItem(i);
535+
for(i = len - 1; i >= iMax; i--) addItem(i);
536+
537+
return {min: minArray, max: maxArray};
538+
}
539+
367540
// In order to stop overflow errors, don't consider points
368541
// too close to the limits of js floating point
369542
function goodNumber(v) {

0 commit comments

Comments
 (0)