@@ -116,6 +116,13 @@ export interface IStatsBaseOptions {
116116 * @indexable
117117 */
118118 itemBorderWidth : number ;
119+ /**
120+ * hit radius for hit test of items
121+ * @default 0
122+ * @scriptable
123+ * @indexable
124+ */
125+ itemHitRadius : number ;
119126
120127 /**
121128 * padding that is added around the bounding box when computing a mouse hit
@@ -195,6 +202,7 @@ export const baseDefaults = {
195202 itemStyle : 'circle' ,
196203 itemRadius : 0 ,
197204 itemBorderWidth : 0 ,
205+ itemHitRadius : 0 ,
198206
199207 meanStyle : 'circle' ,
200208 meanRadius : 3 ,
@@ -399,7 +407,8 @@ export class StatsBase<T extends IStatsBaseProps & { mean?: number }, O extends
399407 }
400408 return (
401409 this . _boxInRange ( mouseX , mouseY , useFinalPosition ) ||
402- this . _outlierIndexInRange ( mouseX , mouseY , useFinalPosition ) >= 0
410+ this . _outlierIndexInRange ( mouseX , mouseY , useFinalPosition ) != null ||
411+ this . _itemIndexInRange ( mouseX , mouseY , useFinalPosition ) != null
403412 ) ;
404413 }
405414
@@ -422,23 +431,75 @@ export class StatsBase<T extends IStatsBaseProps & { mean?: number }, O extends
422431 /**
423432 * @hidden
424433 */
425- protected _outlierIndexInRange ( mouseX : number , mouseY : number , useFinalPosition ?: boolean ) : number {
434+ protected _outlierIndexInRange (
435+ mouseX : number ,
436+ mouseY : number ,
437+ useFinalPosition ?: boolean
438+ ) : { index : number ; x : number ; y : number } | null {
426439 const props = this . getProps ( [ 'x' , 'y' ] , useFinalPosition ) ;
427440 const hitRadius = this . options . outlierHitRadius ;
428441 const outliers = this . _getOutliers ( useFinalPosition ) ;
429442 const vertical = this . isVertical ( ) ;
430443
431444 // check if along the outlier line
432445 if ( ( vertical && Math . abs ( mouseX - props . x ) > hitRadius ) || ( ! vertical && Math . abs ( mouseY - props . y ) > hitRadius ) ) {
433- return - 1 ;
446+ return null ;
434447 }
435448 const toCompare = vertical ? mouseY : mouseX ;
436449 for ( let i = 0 ; i < outliers . length ; i += 1 ) {
437450 if ( Math . abs ( outliers [ i ] - toCompare ) <= hitRadius ) {
438- return i ;
451+ return vertical ? { index : i , x : props . x , y : outliers [ i ] } : { index : i , x : outliers [ i ] , y : props . y } ;
452+ }
453+ }
454+ return null ;
455+ }
456+
457+ /**
458+ * @hidden
459+ */
460+ protected _itemIndexInRange (
461+ mouseX : number ,
462+ mouseY : number ,
463+ useFinalPosition ?: boolean
464+ ) : { index : number ; x : number ; y : number } | null {
465+ const hitRadius = this . options . itemHitRadius ;
466+ if ( hitRadius <= 0 ) {
467+ return null ;
468+ }
469+ const props = this . getProps ( [ 'x' , 'y' , 'items' , 'width' , 'height' , 'outliers' ] , useFinalPosition ) ;
470+ const vert = this . isVertical ( ) ;
471+ const { options } = this ;
472+
473+ if ( options . itemRadius <= 0 || ! props . items || props . items . length <= 0 ) {
474+ return null ;
475+ }
476+ // jitter based on random data
477+ // use the dataset index and index to initialize the random number generator
478+ const random = rnd ( this . _datasetIndex * 1000 + this . _index ) ;
479+ const outliers = new Set ( props . outliers || [ ] ) ;
480+
481+ if ( vert ) {
482+ for ( let i = 0 ; i < props . items . length ; i ++ ) {
483+ const y = props . items [ i ] ;
484+ if ( ! outliers . has ( y ) ) {
485+ const x = props . x - props . width / 2 + random ( ) * props . width ;
486+ if ( Math . abs ( x - mouseX ) <= hitRadius && Math . abs ( y - mouseY ) <= hitRadius ) {
487+ return { index : i , x, y } ;
488+ }
489+ }
490+ }
491+ } else {
492+ for ( let i = 0 ; i < props . items . length ; i ++ ) {
493+ const x = props . items [ i ] ;
494+ if ( ! outliers . has ( x ) ) {
495+ const y = props . y - props . height / 2 + random ( ) * props . height ;
496+ if ( Math . abs ( x - mouseX ) <= hitRadius && Math . abs ( y - mouseY ) <= hitRadius ) {
497+ return { index : i , x, y } ;
498+ }
499+ }
439500 }
440501 }
441- return - 1 ;
502+ return null ;
442503 }
443504
444505 /**
@@ -482,28 +543,40 @@ export class StatsBase<T extends IStatsBaseProps & { mean?: number }, O extends
482543 if ( tooltip ) {
483544 // eslint-disable-next-line no-param-reassign
484545 delete tooltip . _tooltipOutlier ;
546+ // eslint-disable-next-line no-param-reassign
547+ delete tooltip . _tooltipItem ;
485548 }
486549
487- const props = this . getProps ( [ 'x' , 'y' ] ) ;
488- const index = this . _outlierIndexInRange ( eventPosition . x , eventPosition . y ) ;
489- if ( index < 0 || ! tooltip ) {
490- return this . getCenterPoint ( ) ;
550+ //outlier
551+ const info = this . _outlierIndexInRange ( eventPosition . x , eventPosition . y ) ;
552+ if ( info != null && tooltip ) {
553+ // hack in the data of the hovered outlier
554+ // eslint-disable-next-line no-param-reassign
555+ tooltip . _tooltipOutlier = {
556+ index : info . index ,
557+ datasetIndex : this . _datasetIndex ,
558+ } ;
559+ return {
560+ x : info . x ,
561+ y : info . y ,
562+ } ;
491563 }
492- // hack in the data of the hovered outlier
493- // eslint-disable-next-line no-param-reassign
494- tooltip . _tooltipOutlier = {
495- index,
496- datasetIndex : this . _datasetIndex ,
497- } ;
498- if ( this . isVertical ( ) ) {
564+ // items
565+ const itemInfo = this . _itemIndexInRange ( eventPosition . x , eventPosition . y ) ;
566+ if ( itemInfo != null && tooltip ) {
567+ // hack in the data of the hovered outlier
568+ // eslint-disable-next-line no-param-reassign
569+ tooltip . _tooltipItem = {
570+ index : itemInfo . index ,
571+ datasetIndex : this . _datasetIndex ,
572+ } ;
499573 return {
500- x : props . x as number ,
501- y : this . _getOutliers ( ) [ index ] ,
574+ x : itemInfo . x ,
575+ y : itemInfo . y ,
502576 } ;
503577 }
504- return {
505- x : this . _getOutliers ( ) [ index ] ,
506- y : props . y as number ,
507- } ;
578+
579+ // fallback
580+ return this . getCenterPoint ( ) ;
508581 }
509582}
0 commit comments