@@ -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
369542function goodNumber ( v ) {
0 commit comments