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

Box points hover & select #2094

Merged
merged 19 commits into from
Oct 19, 2017
Merged

Box points hover & select #2094

merged 19 commits into from
Oct 19, 2017

Conversation

etpinard
Copy link
Contributor

resolves #192

In more detail, this PRs:

  • adds support for dragmode lasso and select to box traces

peek 2017-10-16 23-38

  • adds hoveron attribute the box traces with two available flags: boxes (the current behavior) and points:

peek 2017-10-16 23-39

  • adds text attribute the box traces so that custom hover text can be added to each box point.

cc @alexcjohnson @chriddyp

- generate 'pts' array of objects similar to scatter pts inside
  box calcdata items, instead of simply keep track of all values
  corresponding to each box.
- fill in pt objects fill w/ jitter during Box.plot instead of mapping
  box 'val' array to d3-esque array of objects.
- in preparation for box select and 'points' hover,
  keep track of original val indices in pt object
- use pts array of object to determine which box points
  are inside selection polygon
- use index field (from Box.calc) to match each selected box point
  to underlying x/y input value.
- dim point node opacity upon selection
- flaglist w/ 'boxes' (current + default) and 'points'
- use point object x/y values (from Box.plot) to determine
  closest point(s)
- consider jitter offset under hovermode closest,
  but don't under 'x' or 'y' modes
- associated with each sample pt
- only visible on hover (for now)
@etpinard etpinard added this to the v1.32.0 milestone Oct 17, 2017

var n = Lib.findBin(pos[i], posBins);
if(n >= 0 && n < pLen) {
var pt = {v: v, i: i};
Copy link
Contributor Author

@etpinard etpinard Oct 17, 2017

Choose a reason for hiding this comment

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

This here was the key to make box point hover and selection somewhat clean.

Box calcdata traces have one item per box to be displayed. Box points data-esque array of objects used to be created during Box.plot, now the structure is setup here. Note that it is important to track the original sample pt index (that i above) to convert hovered are selected calcdata items to event data pointNumber corresponding to indices in the input x/y sample arrays

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Definitely nice and clean - I was curious whether this degraded performance for large data sets, but it looks like very little if at all

function r(n) { var v = 0; for(var i = 0; i < n; i++) v += Math.random(); return v; }
function a(n, m) { var out = new Array(n); for(var i = 0; i < n; i++) out[i] = r(m); return out; }
var y = a(1000000, 10)
// I had to patch timeit to support n = 1
timeit(function() { Plotly.newPlot(gd,[{type: 'box', y: y, jitter: 0.5, hoveron: 'points'}]) }, 1)

On my computer 1 million points (of which ~5k are outliers) takes ~3 sec, and difference between this branch and master is within the noise (~10% or less)

Copy link
Contributor Author

@etpinard etpinard Oct 18, 2017

Choose a reason for hiding this comment

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

(~10% or less)

Thanks for taking a look at this 🐎

hoveron: {
valType: 'flaglist',
flags: ['boxes', 'points'],
dflt: 'boxes',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Debatable choice here. Strictly speaking we need dflt: 'boxes' to keep backward compat, but perhaps someone can argue otherwise.

Copy link
Collaborator

Choose a reason for hiding this comment

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

dflt: 'boxes' 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree here, but perhaps to make this feature accessible to existing graphs, the show closest data on hover mode bar button could restyle hoveron to 'points+boxes' as well as layout.hovermode on click for traces with boxpoints: 'all'?

Copy link
Collaborator

Choose a reason for hiding this comment

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

oh, hmm... that's a good point actually. Can I change my answer to dflt: 'boxes+points'? I don't think turning on a new hover feature really amounts to a breaking change, I suppose in principle it can lead to events that a user application doesn't know how to handle but that seems pretty minor IMHO.

Copy link
Member

Choose a reason for hiding this comment

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

dflt: boxes+points 👍 One could even argue that not being able to hover on the box points is a 🐞 .

Copy link
Contributor Author

@etpinard etpinard Oct 18, 2017

Choose a reason for hiding this comment

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

done in 6bfcbe0

}
}
}

return closeData;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another debatable choice: this commit allows 'boxes' and 'points' hoveron picks to coexist. With hoveron: 'boxes+points', we get:

peek 2017-10-16 23-51

where box and point hover label can be displayed at the same time. Does anyone find this potentially problematic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... and under hovermode: 'x', this can happen:

peek 2017-10-17 00-05

Copy link
Collaborator

Choose a reason for hiding this comment

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

boxes+points could be useful, but I think we want to show fewer points. Currently it seems to return all the points that are in range, but most real-world box uses have far too many points to label them all, and we should match scatter which chooses the best point when more than one is in range.

So I guess I'm proposing:

  • In closest mode, show only one point or stats for one box, and points have priority
    • If there's a point in range and hoveron has points, show the best single point only.
    • If hoveron has boxes and there's no point in range (or hoveron doesn't have points), show the box stats.
  • In compare mode I don't really see any extra useful thing to do with points - I think we should still only show one point per trace, so I guess that means not considering only x when choosing the best point? I know that's not consistent with other types, but I feel like usability is much more important.
    • Perhaps though if hoveron = 'boxes+points' we should allow a point AND the box stats to be labeled?
    • Already if there are multiple boxes in range (ie boxmode = 'overlay') we'll see stats for all of them, right? I think that's good.

One other thought: do we want to do something to distinguish statistics labels from point labels? Perhaps actually say what statistical measure is being shown? Which might be nice on its own merits! But we'd have to be careful about the whisker ends, whether they represent upper/lower fence or max/min.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but most real-world box uses have far too many points to label them all, and we should match scatter

Good point here. I'll implement ⤴️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's what I come up with: f228a5e

One other thought: do we want to do something to distinguish statistics labels from point labels? Perhaps actually say what statistical measure is being shown?

The above commit does not implement this. I think it's a good idea though. Should the statistical measures always show up next to their value in the hover labels or just when point and box hover label show simultaneously?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's a quick-and-easy version:

peek 2017-10-19 12-02

we might have to expand out uf and lf. Spelling out upper fence and lower fence looks like:

peek 2017-10-19 12-06

does that look too cluttered to you?

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 for spelled out, I think you're right that uf / lf will confuse people.

Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe even spell out median

Copy link
Contributor Author

Choose a reason for hiding this comment

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

full version:

peek 2017-10-19 13-04

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in a3ec75a (with 🔨 🌴 commit b66628e)

var xPx = xa.c2p(xval);
var yPx = ya.c2p(yval);

// do not take jitter into consideration in compare hover modes
Copy link
Contributor Author

@etpinard etpinard Oct 17, 2017

Choose a reason for hiding this comment

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

Anyone disagrees here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, and might even take it farther - can we make all of the hover labels line up horizontally, even though the points do not?

Copy link
Collaborator

Choose a reason for hiding this comment

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

oh hmm I guess https://github.com/plotly/plotly.js/pull/2094/files#r145223324 supersedes this comment, except perhaps if we show a point and stats in compare mode.

}

node3.selectAll('.point').style('opacity', function(d) {
return d.dim ? DESELECTDIM : 1;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that this style update isn't compatible with the current trace attribute set.

To do so, we would need to add arrayOk support for marker.color, marker.opacity and marker.symbol would be pretty easy to add after d352fa5

Copy link
Contributor Author

@etpinard etpinard Oct 17, 2017

Choose a reason for hiding this comment

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

... that is if no one finds too awkward to have both box-wide (e.g. marker.outliercolor) and per-sample-pt attributes in the same trace.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems reasonable. outliercolor is only for boxpoints: 'suspectedoutliers', and in that mode selecting would be a bit weird. (side note: outliercolor should probably be described better in attributes... and perhaps the logic in defaults cleaned up or amended so we don't have the outliercolor attributes in _fullData if boxpoints !== 'suspectedoutliers'

Begs the question though of what selection (or crossfilter) should do in modes other than boxpoints: 'all' when at least the points within the box are hidden - what does selection do now? I could imagine it being cool to still be able to select the points, and have them pop into view when you do (likewise for crossfilter)... but that might be too complicated, dunno.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Begs the question though of what selection (or crossfilter) should do in modes other than boxpoints: 'all' when at least the points within the box are hidden - what does selection do now?

I'd vote for hiding the lasso and select mode bar buttons whenever boxpoints !== 'all' - similar to how we currently hide them from scatter-line-only graphs here. But maybe that's just because I'm lazy. cc @chriddyp @cpsievert any thoughts on this?

and perhaps the logic in defaults cleaned up or amended so we don't have the outliercolor attributes in _fullData if boxpoints !== 'suspectedoutliers'

I believe that's done already.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd vote for hiding the lasso and select mode bar buttons whenever boxpoints !== 'all'

We can certainly start that way anyhow, that's fine for this PR, we can revisit if there's interest later.

I believe that's done already.

It's messy logic - but that's actually the second coerce of one of them so the easy thing to do would be to delete them from the full trace if they're not needed. I guess the reason it's done this way is so you can set the default boxpoints to 'suspectedoutliers' if either of the outliercolor values is a real color - stricter than just existing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ha I see. I've been wanting to replace Lib.coerce2 with Lib.validate (which doesn't fill in fullData). I'll do this in this PR 🔨

Copy link
Contributor Author

@etpinard etpinard Oct 18, 2017

Choose a reason for hiding this comment

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

🔪ing Lib.coerce2 turned out to be a little trickier than I thought - and than I want to do in this PR. In brief, we can just replace coerce2 by validate, validate return false whenever an value isn't set while coerce2 returns the default value.

So, here's a quick 🛠️ and 🔒 in 9ca410a

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd vote for hiding the lasso and select mode bar buttons whenever boxpoints !== 'all'
We can certainly start that way anyhow, that's fine for this PR, we can revisit if there's interest later.

that's in b4d8044

@@ -62,6 +62,16 @@ module.exports = {
'missing and the position axis is categorical'
].join(' ')
},
text: extendFlat({}, scatterAttrs.text, {
Copy link
Contributor Author

@etpinard etpinard Oct 17, 2017

Choose a reason for hiding this comment

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

text is expected to be an array as long as the sample array y (x) for vertical (horizontal) box plots, matching each sample pt one-to-one.

[2, 0.3], [2, 0.6], [2, 0.6]
]);
assertLassoPoints([
['day 1', 'day 2', 'day 2', 'day 1', 'day 1'],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexcjohnson Is this the desired behavior, or did an extra c2d conversion get forgotten? I believe outputting the linearized continuous category equivalent would make sense here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... we might have to wait for v2 though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess c format would make sense for applications that want to perhaps overlay something on the selected points... but I'd think the d value is what the user would expect (and what they would generally want to use), wouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My argument for c:

d is lossy for categories. Converting c to d can easily be done by the user, the opposite cannot (unless we officially expose ax.d2c and friends).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So do you think the x/y coordinate should be converted to d too here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

d is lossy for categories.

These values had really better correspond to the actual data, not offset or jittered values... It's not important for category axes, but for non-lossy axes it is, otherwise you'll be pointed to the wrong data value! So in the end I don't think that this conversion is lossy, even though in general you're right.

Converting c to d can easily be done by the user, the opposite cannot (unless we officially expose ax.d2c and friends).

How do you convert c to d for categories without ax.c2d? It depends on all sorts of details of the graph. Granted enough people use those methods already that I think we have to consider them exposed...

So do you think the x/y coordinate should be converted to d too here?

Good catch - yeah, I guess so. Everything we report back out with events should be d, I would think. That's a pretty sweeping statement though, so it's worth discussing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How do you convert c to d for categories without ax.c2d?

Yeah, I guess that's kinda hard to in general. Put that in the 9am brain-cramp bin.

Everything we report back out with events should be d

I think so too. We should at the very least report everything in the same coordinate space. Right now, lasso coords are in d but point coords in are c even for scatter - which is ridiculous. I would consider this a bug, do you agree?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would consider this a bug, do you agree?

🐞
I hope not too many people depend on the current behavior with date axes, but yeah, I think we should unify on d ASAP. I guess the lesson is make sure there are tests on non-numeric axes whenever we add new features like this... otherwise the c/d distinction goes unnoticed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 8901528

editType: 'style',
description: [
'Do the hover effects highlight individual boxes ',
'or jitter points or both?'
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wouldn't call them jitter points - sample points?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good eye. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in a1ea1e8

- delete 'marker' container for box trace that
  don't have `boxpoints: false`
- do not coerce 'text' when `boxpoints: false`
- show only 'best' boxpoint
- consider opposite coordinates in compare modes to find that
  'best' boxpoint
- allow box AND boxpoint hover labels only in compare modes
- no need for x/y w/o jitter offset values in 'pts' calc items
@@ -24,7 +24,6 @@ module.exports = function selectPoints(searchInfo, polygon) {
cdi = cd[pt.pointNumber];
pt.a = cdi.a;
pt.b = cdi.b;
pt.c = cdi.c;
Copy link
Collaborator

Choose a reason for hiding this comment

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

haha right, more ternary cruft 😅

- dry special log-off-scale hover label text routine
- use it in hover.js cleanPoint
- allow hoverPoints modules to set their own xLabel and yLabel value
@alexcjohnson
Copy link
Collaborator

Looks great! 💃

@yniknafs
Copy link

@etpinard @alexcjohnson

Curious if this has been implemented in the newest version of plotly. Not sure if I'm missing something, but in this pen, the hoverinfo: text option is not working.

https://codepen.io/yniknafs/pen/KoJOwQ

@etpinard
Copy link
Contributor Author

@yniknafs your link is working for me:

peek 2018-04-10 16-55

@yniknafs
Copy link

@etpinard The hover is not triggered when the box is hovered on. Reading above it seemed to indicate that the box should also trigger hover, but it does not here.

@etpinard
Copy link
Contributor Author

etpinard commented Apr 10, 2018

Nop. That's not what this PR intended to add. As noted in the attribute description, text is meant to annotated each sample points, not each boxes.

@yniknafs
Copy link

yniknafs commented Apr 10, 2018 via email

@nmoreaud
Copy link

nmoreaud commented Aug 4, 2020

Hello, here is an example where I add another hover information : (https://codepen.io/kiruahxh/pen/PoNYvWw)

However, I found no information about the boxplot tooltip customization/replacement (ex : How can I show a multiline text instead of having multiple tooltips? How can I translate 'median' and 'min' keywords ? How can I feed a tooltip with the statistics automatically computed by plotly.js ?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

boxplot points hover events
5 participants