Skip to content
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

Merged
merged 9 commits into from
Feb 19, 2018
Merged

Generalize spikelines #2379

merged 9 commits into from
Feb 19, 2018

Conversation

alexcjohnson
Copy link
Collaborator

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, including scattergl, see #2247 (comment)

"width": 400,
"height": 400
}
}
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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)));
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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.

Copy link
Contributor

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;
Copy link
Collaborator Author

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.

Copy link
Contributor

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);
};
Copy link
Collaborator Author

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],
Copy link
Collaborator Author

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'
Copy link
Collaborator Author

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.

@alexcjohnson
Copy link
Collaborator Author

alexcjohnson commented Feb 17, 2018

@etpinard I had a spurious test failure in a gl test - do we want to allow tests to be marked BOTH @gl and @flaky?
(edit: failure here - https://circleci.com/gh/plotly/plotly.js/6963)

@alexcjohnson alexcjohnson mentioned this pull request Feb 17, 2018
1 task
@jackparmer jackparmer requested review from mdtusz and removed request for mdtusz February 19, 2018 04:04
@etpinard
Copy link
Contributor

do we want to allow tests to be marked BOTH @gl and @flaky?

I don't see a problem here.

Copy link
Contributor

@etpinard etpinard left a 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:

peek 2018-02-19 13-02

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,
Copy link
Contributor

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) &&
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call

peek 2018-02-19 12-25

😍

@@ -1309,21 +1309,20 @@ describe('hover on fill', function() {

function assertLabelsCorrect(mousePos, labelPos, labelText) {
return new Promise(function(resolve) {
Lib.clearThrottle();
Copy link
Contributor

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
Copy link
Contributor

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;
Copy link
Contributor

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 👍

@@ -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));
Copy link
Contributor

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?

Copy link
Collaborator Author

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

@alexcjohnson
Copy link
Collaborator Author

Now, this PR almost fixes #2370

And now it really does -> eb98c91

@alexcjohnson
Copy link
Collaborator Author

I'd like to see a few words in the spikedistance and hoverdistance attribute descriptions about how they only apply to point-like objects.

Good point. How's 31e4e34?

@etpinard
Copy link
Contributor

Nicely done. Thanks very much 💃

@alexcjohnson alexcjohnson merged commit ce5bc97 into master Feb 19, 2018
@alexcjohnson alexcjohnson deleted the generalize-spikelines branch February 19, 2018 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants