@@ -46,6 +46,12 @@ var multipleHoverPoints = {
4646 candlestick : true
4747} ;
4848
49+ var cartesianScatterPoints = {
50+ scatter : true ,
51+ scattergl : true ,
52+ splom : true
53+ } ;
54+
4955// fx.hover: highlight data on hover
5056// evt can be a mousemove event, or an object with data about what points
5157// to hover on
@@ -574,12 +580,15 @@ function _hover(gd, evt, subplot, noHoverEvent) {
574580
575581 findHoverPoints ( ) ;
576582
577- function selectClosestPoint ( pointsData , spikedistance ) {
583+ function selectClosestPoint ( pointsData , spikedistance , spikeOnWinning ) {
578584 var resultPoint = null ;
579585 var minDistance = Infinity ;
580586 var thisSpikeDistance ;
587+
581588 for ( var i = 0 ; i < pointsData . length ; i ++ ) {
582589 thisSpikeDistance = pointsData [ i ] . spikeDistance ;
590+ if ( spikeOnWinning && i === 0 ) thisSpikeDistance = - Infinity ;
591+
583592 if ( thisSpikeDistance <= minDistance && thisSpikeDistance <= spikedistance ) {
584593 resultPoint = pointsData [ i ] ;
585594 minDistance = thisSpikeDistance ;
@@ -616,19 +625,30 @@ function _hover(gd, evt, subplot, noHoverEvent) {
616625 } ;
617626 gd . _spikepoints = newspikepoints ;
618627
628+ var sortHoverData = function ( ) {
629+ hoverData . sort ( function ( d1 , d2 ) { return d1 . distance - d2 . distance ; } ) ;
630+
631+ // move period positioned points and box/bar-like traces to the end of the list
632+ hoverData = orderRangePoints ( hoverData , hovermode ) ;
633+ } ;
634+ sortHoverData ( ) ;
635+
636+ var axLetter = hovermode . charAt ( 0 ) ;
637+ var spikeOnWinning = ( axLetter === 'x' || axLetter === 'y' ) && hoverData [ 0 ] && cartesianScatterPoints [ hoverData [ 0 ] . trace . type ] ;
638+
619639 // Now if it is not restricted by spikedistance option, set the points to draw the spikelines
620640 if ( hasCartesian && ( spikedistance !== 0 ) ) {
621641 if ( hoverData . length !== 0 ) {
622642 var tmpHPointData = hoverData . filter ( function ( point ) {
623643 return point . ya . showspikes ;
624644 } ) ;
625- var tmpHPoint = selectClosestPoint ( tmpHPointData , spikedistance ) ;
645+ var tmpHPoint = selectClosestPoint ( tmpHPointData , spikedistance , spikeOnWinning ) ;
626646 spikePoints . hLinePoint = fillSpikePoint ( tmpHPoint ) ;
627647
628648 var tmpVPointData = hoverData . filter ( function ( point ) {
629649 return point . xa . showspikes ;
630650 } ) ;
631- var tmpVPoint = selectClosestPoint ( tmpVPointData , spikedistance ) ;
651+ var tmpVPoint = selectClosestPoint ( tmpVPointData , spikedistance , spikeOnWinning ) ;
632652 spikePoints . vLinePoint = fillSpikePoint ( tmpVPoint ) ;
633653 }
634654 }
@@ -650,14 +670,6 @@ function _hover(gd, evt, subplot, noHoverEvent) {
650670 }
651671 }
652672
653- var sortHoverData = function ( ) {
654- hoverData . sort ( function ( d1 , d2 ) { return d1 . distance - d2 . distance ; } ) ;
655-
656- // move period positioned points and box/bar-like traces to the end of the list
657- hoverData = orderRangePoints ( hoverData , hovermode ) ;
658- } ;
659- sortHoverData ( ) ;
660-
661673 if (
662674 helpers . isXYhover ( _mode ) &&
663675 hoverData [ 0 ] . length !== 0 &&
@@ -1071,41 +1083,89 @@ function createHoverText(hoverData, opts, gd) {
10711083 legendDraw ( gd , mockLegend ) ;
10721084
10731085 // Position the hover
1074- var winningPoint = hoverData [ 0 ] ;
1075- var ly = axLetter === 'y' ?
1076- ( winningPoint . y0 + winningPoint . y1 ) / 2 : Lib . mean ( hoverData . map ( function ( c ) { return ( c . y0 + c . y1 ) / 2 ; } ) ) ;
1077- var lx = axLetter === 'x' ?
1078- ( winningPoint . x0 + winningPoint . x1 ) / 2 : Lib . mean ( hoverData . map ( function ( c ) { return ( c . x0 + c . x1 ) / 2 ; } ) ) ;
1079-
10801086 var legendContainer = container . select ( 'g.legend' ) ;
10811087 var tbb = legendContainer . node ( ) . getBoundingClientRect ( ) ;
1082- lx += xa . _offset ;
1083- ly += ya . _offset - tbb . height / 2 ;
1084-
1085- // Change horizontal alignment to end up on screen
1086- var txWidth = tbb . width + 2 * HOVERTEXTPAD ;
1087- var anchorStartOK = lx + txWidth <= outerWidth ;
1088- var anchorEndOK = lx - txWidth >= 0 ;
1089- if ( ! anchorStartOK && anchorEndOK ) {
1090- lx -= txWidth ;
1088+ var tWidth = tbb . width + 2 * HOVERTEXTPAD ;
1089+ var tHeight = tbb . height + 2 * HOVERTEXTPAD ;
1090+ var winningPoint = hoverData [ 0 ] ;
1091+ var avgX = ( winningPoint . x0 + winningPoint . x1 ) / 2 ;
1092+ var avgY = ( winningPoint . y0 + winningPoint . y1 ) / 2 ;
1093+ // When a scatter (or e.g. heatmap) point wins, it's OK for the hovelabel to occlude the bar and other points.
1094+ var pointWon = ! (
1095+ Registry . traceIs ( winningPoint . trace , 'bar-like' ) ||
1096+ Registry . traceIs ( winningPoint . trace , 'box-violin' )
1097+ ) ;
1098+
1099+ var lyBottom , lyTop ;
1100+ if ( axLetter === 'y' ) {
1101+ if ( pointWon ) {
1102+ lyTop = avgY - HOVERTEXTPAD ;
1103+ lyBottom = avgY + HOVERTEXTPAD ;
1104+ } else {
1105+ lyTop = Math . min . apply ( null , hoverData . map ( function ( c ) { return Math . min ( c . y0 , c . y1 ) ; } ) ) ;
1106+ lyBottom = Math . max . apply ( null , hoverData . map ( function ( c ) { return Math . max ( c . y0 , c . y1 ) ; } ) ) ;
1107+ }
1108+ } else {
1109+ lyTop = lyBottom = Lib . mean ( hoverData . map ( function ( c ) { return ( c . y0 + c . y1 ) / 2 ; } ) ) - tHeight / 2 ;
1110+ }
1111+
1112+ var lxRight , lxLeft ;
1113+ if ( axLetter === 'x' ) {
1114+ if ( pointWon ) {
1115+ lxRight = avgX + HOVERTEXTPAD ;
1116+ lxLeft = avgX - HOVERTEXTPAD ;
1117+ } else {
1118+ lxRight = Math . max . apply ( null , hoverData . map ( function ( c ) { return Math . max ( c . x0 , c . x1 ) ; } ) ) ;
1119+ lxLeft = Math . min . apply ( null , hoverData . map ( function ( c ) { return Math . min ( c . x0 , c . x1 ) ; } ) ) ;
1120+ }
10911121 } else {
1092- lx += 2 * HOVERTEXTPAD ;
1122+ lxRight = lxLeft = Lib . mean ( hoverData . map ( function ( c ) { return ( c . x0 + c . x1 ) / 2 ; } ) ) - tWidth / 2 ;
10931123 }
10941124
1095- // Change vertical alignement to end up on screen
1096- var txHeight = tbb . height + 2 * HOVERTEXTPAD ;
1097- var overflowTop = ly <= outerTop ;
1098- var overflowBottom = ly + txHeight >= outerHeight ;
1099- var canFit = txHeight <= outerHeight ;
1100- if ( canFit ) {
1101- if ( overflowTop ) {
1102- ly = ya . _offset + 2 * HOVERTEXTPAD ;
1103- } else if ( overflowBottom ) {
1104- ly = outerHeight - txHeight ;
1125+ var xOffset = xa . _offset ;
1126+ var yOffset = ya . _offset ;
1127+ lyBottom += yOffset ;
1128+ lxRight += xOffset ;
1129+ lxLeft += xOffset - tWidth ;
1130+ lyTop += yOffset - tHeight ;
1131+
1132+ var lx , ly ; // top and left positions of the hover box
1133+
1134+ // horizontal alignment to end up on screen
1135+ if ( lxRight + tWidth < outerWidth && lxRight >= 0 ) {
1136+ lx = lxRight ;
1137+ } else if ( lxLeft + tWidth < outerWidth && lxLeft >= 0 ) {
1138+ lx = lxLeft ;
1139+ } else if ( xOffset + tWidth < outerWidth ) {
1140+ lx = xOffset ; // subplot left corner
1141+ } else {
1142+ // closest left or right side of the paper
1143+ if ( lxRight - avgX < avgX - lxLeft + tWidth ) {
1144+ lx = outerWidth - tWidth ;
1145+ } else {
1146+ lx = 0 ;
11051147 }
11061148 }
1107- legendContainer . attr ( 'transform' , strTranslate ( lx , ly ) ) ;
1149+ lx += HOVERTEXTPAD ;
1150+
1151+ // vertical alignement to end up on screen
1152+ if ( lyBottom + tHeight < outerHeight && lyBottom >= 0 ) {
1153+ ly = lyBottom ;
1154+ } else if ( lyTop + tHeight < outerHeight && lyTop >= 0 ) {
1155+ ly = lyTop ;
1156+ } else if ( yOffset + tHeight < outerHeight ) {
1157+ ly = yOffset ; // subplot top corner
1158+ } else {
1159+ // closest top or bottom side of the paper
1160+ if ( lyBottom - avgY < avgY - lyTop + tHeight ) {
1161+ ly = outerHeight - tHeight ;
1162+ } else {
1163+ ly = 0 ;
1164+ }
1165+ }
1166+ ly += HOVERTEXTPAD ;
11081167
1168+ legendContainer . attr ( 'transform' , strTranslate ( lx - 1 , ly - 1 ) ) ;
11091169 return legendContainer ;
11101170 }
11111171
@@ -1934,7 +1994,10 @@ function getCoord(axLetter, winningPoint, fullLayout) {
19341994 var val = winningPoint [ axLetter + 'Val' ] ;
19351995
19361996 if ( ax . type === 'category' ) val = ax . _categoriesMap [ val ] ;
1937- else if ( ax . type === 'date' ) val = ax . d2c ( val ) ;
1997+ else if ( ax . type === 'date' ) {
1998+ var period = winningPoint [ axLetter + 'Period' ] ;
1999+ val = ax . d2c ( period !== undefined ? period : val ) ;
2000+ }
19382001
19392002 var cd0 = winningPoint . cd [ winningPoint . index ] ;
19402003 if ( cd0 && cd0 . t && cd0 . t . posLetter === ax . _id ) {
0 commit comments