-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generalize spikelines #2379
Generalize spikelines #2379
Conversation
"width": 400, | ||
"height": 400 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @dfcreative - needed so tests will pass, if this PR merges before #2377 you can merge master and put this back in there.
@@ -9,9 +9,6 @@ | |||
'use strict'; | |||
|
|||
module.exports = { | |||
// max pixels away from mouse to allow a point to highlight | |||
MAXDIST: 20, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not using this constant anymore, we just have the defaults in layout.hoverdistance
and layout.spikedistance
, with which the users of this constant were in conflict.
// count one edge as in, so that over continuous ranges you never get a gap | ||
exports.inbox = function inbox(v0, v1) { | ||
if(v0 * v1 < 0 || v0 === 0) { | ||
return constants.MAXDIST * (0.6 - 0.3 / Math.max(3, Math.abs(v0 - v1))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This built-in constant return value for Fx.inbox
was limiting our flexibility. inbox
is now a much simpler function (so in fact we could just 🔪 but it still makes the code a little more concise I think), just returning Infinity
outside the box or the passVal
you give it inside the box.
y0: point.y0, | ||
y1: point.y1, | ||
x: point.xSpike !== undefined ? point.xSpike : (point.x0 + point.x1) / 2, | ||
y: point.ySpike !== undefined ? point.ySpike : (point.y0 + point.y1) / 2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a module wants spikes NOT to just be drawn at the midpoint of the box hover labels are attached to, it just needs to specify xSpike
and/or ySpike
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brilliant ✨
} | ||
|
||
// xyz labels put all info in their main label, so have no need of a common label | ||
if(allHaveZ) showCommonLabel = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic about whether to show the common label has gotten a bit more complex... previously it was being implicitly handled by c0.distance <= opts.hoverdistance
, since most of the cases that shouldn't show a common label (like heatmaps) were also setting a pseudo-distance greater than the distance limit, which was always too convoluted to really understand, and we can't do anymore anyway due to the way we're calling hoverPoints
separately for spikelines if no points were found in the first round for hover labels. Now it's at least more explicit what's going on, but there might be a way to clean it up even more by perhaps having the hoverPoints
methods flag points that should or should not get a common label.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps having the hoverPoints methods flag points that should or should not get a common label.
This sounds like the most robust way forward down the road 👍
dx = (point.x1 + point.x0) / 2 - xpx, | ||
dy = (point.y1 + point.y0) / 2 - ypx; | ||
return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not replicating the distance function here, because that only works for scatter
anyhow. Now we're letting each hoverPoints
module tell us both a distance
(based on hovermode
) and a spikeDistance
(always as if in closest
mode, but may be calculated differently or omitted if that hover label should not make a spike line)
@@ -266,6 +266,7 @@ describe('Test box hover:', function() { | |||
fig.layout.hovermode = 'x'; | |||
return fig; | |||
}, | |||
pos: [215, 200], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not exactly sure how this happened, but it seems like box points are a little bit greedier now about taking hover events. The behavior feels right to me when playing with it, so I just moved this point over a little so that it's again grabbed by the box as intended.
nums: 'r: 2.920135\nθ: 138.2489°', | ||
name: 'Trial 4' | ||
nums: 'r: 3.886013\nθ: 125.2822°', | ||
name: 'Trial 3' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This (and below) is a weird one - as far as I can tell the new point is in fact the closest to the cursor, and the old one is a couple of pixels farther away, but I don't know what was making us pick the other one previously. I will note however that scattergl
(which this inherits from) and scatter
use different distance functions (mostly having to do with special handling when you're inside two markers, to prioritize the smaller marker), I didn't try to harmonize that here but we probably should at some point.
@etpinard I had a spurious test failure in a gl test - do we want to allow tests to be marked BOTH |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice job 🔒ing down spike behavior for all cartesian trace types 🥇
Along with adding a comment in bar/hover.js
, I'd like to see a few words in the spikedistance
and hoverdistance
attribute descriptions about how they only apply to point-like objects.
Making the hoverPoints
methods have more responsibility is definitely the right call. There's a lot ways we could clean them up, but at least we're getting a better handle of all the things they should do 👌
Now, this PR almost fixes #2370, off this branch I get:
where only the labels off the middle point are wrong.
y0: point.y0, | ||
y1: point.y1, | ||
x: point.xSpike !== undefined ? point.xSpike : (point.x0 + point.x1) / 2, | ||
y: point.ySpike !== undefined ? point.ySpike : (point.y0 + point.y1) / 2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brilliant ✨
var showCommonLabel = c0.distance <= opts.hoverdistance && | ||
(hovermode === 'x' || hovermode === 'y'); | ||
var showCommonLabel = ( | ||
(t0 !== undefined) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this extra t0 !== undefined
check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to #2379 (comment)
Previously c0.distance <= opts.hoverdistance
was strangely being used as a proxy for which trace types should show the common label. I suspect that clause could be removed entirely at this point, in fact... But now that we're not using that, it's important to ensure that there is a common label to display, otherwise you can get that little empty arrow in some cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know. Thanks!
@@ -63,6 +63,14 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay | |||
kdePointData[pLetter + '1'] = pOnPath[1]; | |||
kdePointData[vLetter + '0'] = kdePointData[vLetter + '1'] = vValPx; | |||
kdePointData[vLetter + 'Label'] = vLetter + ': ' + Axes.hoverLabelText(vAxis, vVal) + ', ' + cd[0].t.labels.kde + ' ' + kdeVal.toFixed(3); | |||
|
|||
// move the spike to the KDE point |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -1309,21 +1309,20 @@ describe('hover on fill', function() { | |||
|
|||
function assertLabelsCorrect(mousePos, labelPos, labelText) { | |||
return new Promise(function(resolve) { | |||
Lib.clearThrottle(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice touch!
|
||
it('positions spikes at the data value on grouped bars', function(done) { | ||
function _assertBarSpikes() { | ||
// regardless of hovermode, you must be actually over the bar to see its spikes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for comment here 📚
} | ||
|
||
// xyz labels put all info in their main label, so have no need of a common label | ||
if(allHaveZ) showCommonLabel = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps having the hoverPoints methods flag points that should or should not get a common label.
This sounds like the most robust way forward down the road 👍
src/traces/bar/hover.js
Outdated
@@ -49,15 +51,24 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { | |||
return Math.max(thisBarMaxPos(di), di.p + t.bardelta / 2); | |||
}; | |||
|
|||
function _positionFn(_minPos, _maxPos) { | |||
return Fx.inbox(_minPos - posVal, _maxPos - posVal, | |||
maxHoverDistance - Math.min(1, Math.abs(_maxPos - _minPos) / pRangeCalc)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind adding a one-line comment description what this passVal
corresponds to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good call - it was actually wrong -> ad38f8d
Good point. How's 31e4e34? |
Nicely done. Thanks very much 💃 |
Fixes #2336 and cleans up our spikelines framework so that each module's
hoverPoints
method can define its own spikelines behavior. Ensures that spikelines work correctly for all cartesian trace types, includingscattergl
, see #2247 (comment)