@@ -18,7 +18,8 @@ module.exports = {
18
18
getAutoRange : getAutoRange ,
19
19
makePadFn : makePadFn ,
20
20
doAutoRange : doAutoRange ,
21
- expand : expand
21
+ expand : expand ,
22
+ findExtremes : findExtremes
22
23
} ;
23
24
24
25
// Find the autorange for this axis
@@ -364,6 +365,178 @@ function expand(ax, data, options) {
364
365
for ( i = len - 1 ; i >= iMax ; i -- ) addItem ( i ) ;
365
366
}
366
367
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
+
367
540
// In order to stop overflow errors, don't consider points
368
541
// too close to the limits of js floating point
369
542
function goodNumber ( v ) {
0 commit comments