@@ -119,64 +119,118 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
119119 }
120120 }
121121
122- // even if hoveron is 'fills', only use it if we have polygons too
123- if ( hoveron . indexOf ( 'fills' ) !== - 1 && trace . _polygons ) {
124- var polygons = trace . _polygons ;
122+ function isHoverPointInFillElement ( el ) {
123+ // Uses SVGElement.isPointInFill to accurately determine wether
124+ // the hover point / cursor is contained in the fill, taking
125+ // curved or jagged edges into account, which the Polygon-based
126+ // approach does not.
127+ if ( ! el ) {
128+ return false ;
129+ }
130+ var svgElement = el . node ( ) ;
131+ try {
132+ var domPoint = new DOMPoint ( pt [ 0 ] , pt [ 1 ] ) ;
133+ return svgElement . isPointInFill ( domPoint ) ;
134+ } catch ( TypeError ) {
135+ var svgPoint = svgElement . ownerSVGElement . createSVGPoint ( ) ;
136+ svgPoint . x = pt [ 0 ] ;
137+ svgPoint . y = pt [ 1 ] ;
138+ return svgElement . isPointInFill ( svgPoint ) ;
139+ }
140+ }
141+
142+ function getHoverLabelPosition ( polygons ) {
143+ // Uses Polygon s to determine the left- and right-most x-coordinates
144+ // of the subshape of the fill that contains the hover point / cursor.
145+ // Doing this with the SVGElement directly is quite tricky, so this falls
146+ // back to the existing relatively simple code, accepting some small inaccuracies
147+ // of label positioning for curved/jagged edges.
148+ var i ;
125149 var polygonsIn = [ ] ;
126- var inside = false ;
127150 var xmin = Infinity ;
128151 var xmax = - Infinity ;
129152 var ymin = Infinity ;
130153 var ymax = - Infinity ;
131-
132- var i , j , polygon , pts , xCross , x0 , x1 , y0 , y1 ;
154+ var yPos ;
133155
134156 for ( i = 0 ; i < polygons . length ; i ++ ) {
135- polygon = polygons [ i ] ;
136- // TODO: this is not going to work right for curved edges, it will
137- // act as though they're straight. That's probably going to need
138- // the elements themselves to capture the events. Worth it?
157+ var polygon = polygons [ i ] ;
158+ // This is not going to work right for curved or jagged edges, it will
159+ // act as though they're straight.
139160 if ( polygon . contains ( pt ) ) {
140- inside = ! inside ;
141- // TODO: need better than just the overall bounding box
142161 polygonsIn . push ( polygon ) ;
143162 ymin = Math . min ( ymin , polygon . ymin ) ;
144163 ymax = Math . max ( ymax , polygon . ymax ) ;
145164 }
146165 }
147166
148- if ( inside ) {
149- // constrain ymin/max to the visible plot, so the label goes
150- // at the middle of the piece you can see
151- ymin = Math . max ( ymin , 0 ) ;
152- ymax = Math . min ( ymax , ya . _length ) ;
153-
154- // find the overall left-most and right-most points of the
155- // polygon(s) we're inside at their combined vertical midpoint.
156- // This is where we will draw the hover label.
157- // Note that this might not be the vertical midpoint of the
158- // whole trace, if it's disjoint.
159- var yAvg = ( ymin + ymax ) / 2 ;
160- for ( i = 0 ; i < polygonsIn . length ; i ++ ) {
161- pts = polygonsIn [ i ] . pts ;
162- for ( j = 1 ; j < pts . length ; j ++ ) {
163- y0 = pts [ j - 1 ] [ 1 ] ;
164- y1 = pts [ j ] [ 1 ] ;
165- if ( ( y0 > yAvg ) !== ( y1 >= yAvg ) ) {
166- x0 = pts [ j - 1 ] [ 0 ] ;
167- x1 = pts [ j ] [ 0 ] ;
168- if ( y1 - y0 ) {
169- xCross = x0 + ( x1 - x0 ) * ( yAvg - y0 ) / ( y1 - y0 ) ;
170- xmin = Math . min ( xmin , xCross ) ;
171- xmax = Math . max ( xmax , xCross ) ;
172- }
167+ // The above found no polygon that contains the cursor, but we know that
168+ // the cursor must be inside the fill as determined by the SVGElement
169+ // (so we are probably close to a curved/jagged edge...).
170+ if ( polygonsIn . length === 0 ) {
171+ return null ;
172+ }
173+
174+ // constrain ymin/max to the visible plot, so the label goes
175+ // at the middle of the piece you can see
176+ ymin = Math . max ( ymin , 0 ) ;
177+ ymax = Math . min ( ymax , ya . _length ) ;
178+
179+ yPos = ( ymin + ymax ) / 2 ;
180+
181+ // find the overall left-most and right-most points of the
182+ // polygon(s) we're inside at their combined vertical midpoint.
183+ // This is where we will draw the hover label.
184+ // Note that this might not be the vertical midpoint of the
185+ // whole trace, if it's disjoint.
186+ var j , pts , xAtYPos , x0 , x1 , y0 , y1 ;
187+ for ( i = 0 ; i < polygonsIn . length ; i ++ ) {
188+ pts = polygonsIn [ i ] . pts ;
189+ for ( j = 1 ; j < pts . length ; j ++ ) {
190+ y0 = pts [ j - 1 ] [ 1 ] ;
191+ y1 = pts [ j ] [ 1 ] ;
192+ if ( ( y0 > yPos ) !== ( y1 >= yPos ) ) {
193+ x0 = pts [ j - 1 ] [ 0 ] ;
194+ x1 = pts [ j ] [ 0 ] ;
195+ if ( y1 - y0 ) {
196+ xAtYPos = x0 + ( x1 - x0 ) * ( yPos - y0 ) / ( y1 - y0 ) ;
197+ xmin = Math . min ( xmin , xAtYPos ) ;
198+ xmax = Math . max ( xmax , xAtYPos ) ;
173199 }
174200 }
175201 }
202+ }
203+
204+ // constrain xmin/max to the visible plot now too
205+ xmin = Math . max ( xmin , 0 ) ;
206+ xmax = Math . min ( xmax , xa . _length ) ;
207+
208+ return {
209+ x0 : xmin ,
210+ x1 : xmax ,
211+ y0 : yPos ,
212+ y1 : yPos ,
213+ } ;
214+ }
176215
177- // constrain xmin/max to the visible plot now too
178- xmin = Math . max ( xmin , 0 ) ;
179- xmax = Math . min ( xmax , xa . _length ) ;
216+ // even if hoveron is 'fills', only use it if we have a fill element too
217+ if ( hoveron . indexOf ( 'fills' ) !== - 1 && trace . _fillElement ) {
218+ var inside = isHoverPointInFillElement ( trace . _fillElement ) && ! isHoverPointInFillElement ( trace . _fillExclusionElement ) ;
219+
220+ if ( inside ) {
221+ var hoverLabelCoords = getHoverLabelPosition ( trace . _polygons ) ;
222+
223+ // getHoverLabelPosition may return null if the cursor / hover point is not contained
224+ // in any of the trace's polygons, which can happen close to curved edges. in that
225+ // case we fall back to displaying the hover label at the cursor position.
226+ if ( hoverLabelCoords === null ) {
227+ hoverLabelCoords = {
228+ x0 : pt [ 0 ] ,
229+ x1 : pt [ 0 ] ,
230+ y0 : pt [ 1 ] ,
231+ y1 : pt [ 1 ]
232+ } ;
233+ }
180234
181235 // get only fill or line color for the hover color
182236 var color = Color . defaultLine ;
@@ -189,10 +243,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
189243 // never let a 2D override 1D type as closest point
190244 // also: no spikeDistance, it's not allowed for fills
191245 distance : pointData . maxHoverDistance ,
192- x0 : xmin ,
193- x1 : xmax ,
194- y0 : yAvg ,
195- y1 : yAvg ,
246+ x0 : hoverLabelCoords . x0 ,
247+ x1 : hoverLabelCoords . x1 ,
248+ y0 : hoverLabelCoords . y0 ,
249+ y1 : hoverLabelCoords . y1 ,
196250 color : color ,
197251 hovertemplate : false
198252 } ) ;
0 commit comments