-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Box points hover & select #2094
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
Changes from 1 commit
cfc8725
53c446b
581f3fa
e59f283
5c58049
aef61ae
d352fa5
6a878b0
0027b84
a1ea1e8
6bfcbe0
9ca410a
7e44c9c
f228a5e
b4d8044
9e69900
8901528
b66628e
a3ec75a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -203,5 +203,16 @@ module.exports = { | |
}, | ||
editType: 'plot' | ||
}, | ||
fillcolor: scatterAttrs.fillcolor | ||
fillcolor: scatterAttrs.fillcolor, | ||
hoveron: { | ||
valType: 'flaglist', | ||
flags: ['boxes', 'points'], | ||
dflt: 'boxes', | ||
role: 'info', | ||
editType: 'style', | ||
description: [ | ||
'Do the hover effects highlight individual boxes ', | ||
'or jitter points or both?' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't call them jitter points - sample points? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good eye. Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in a1ea1e8 |
||
].join(' ') | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,94 +14,167 @@ var Fx = require('../../components/fx'); | |
var Color = require('../../components/color'); | ||
|
||
module.exports = function hoverPoints(pointData, xval, yval, hovermode) { | ||
// closest mode: handicap box plots a little relative to others | ||
var cd = pointData.cd, | ||
trace = cd[0].trace, | ||
t = cd[0].t, | ||
xa = pointData.xa, | ||
ya = pointData.ya, | ||
closeData = [], | ||
dx, dy, distfn, boxDelta, | ||
posLetter, posAxis, | ||
val, valLetter, valAxis; | ||
|
||
// adjust inbox w.r.t. to calculate box size | ||
boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos; | ||
|
||
if(trace.orientation === 'h') { | ||
dx = function(di) { | ||
return Fx.inbox(di.min - xval, di.max - xval); | ||
}; | ||
dy = function(di) { | ||
var pos = di.pos + t.bPos - yval; | ||
return Fx.inbox(pos - boxDelta, pos + boxDelta); | ||
}; | ||
posLetter = 'y'; | ||
posAxis = ya; | ||
valLetter = 'x'; | ||
valAxis = xa; | ||
} else { | ||
dx = function(di) { | ||
var pos = di.pos + t.bPos - xval; | ||
return Fx.inbox(pos - boxDelta, pos + boxDelta); | ||
}; | ||
dy = function(di) { | ||
return Fx.inbox(di.min - yval, di.max - yval); | ||
}; | ||
posLetter = 'x'; | ||
posAxis = xa; | ||
valLetter = 'y'; | ||
valAxis = ya; | ||
} | ||
var cd = pointData.cd; | ||
var xa = pointData.xa; | ||
var ya = pointData.ya; | ||
|
||
var trace = cd[0].trace; | ||
var hoveron = trace.hoveron; | ||
var marker = trace.marker || {}; | ||
|
||
// output hover points array | ||
var closeData = []; | ||
// x/y/effective distance functions | ||
var dx, dy, distfn; | ||
// orientation-specific fields | ||
var posLetter, valLetter, posAxis, valAxis; | ||
// calcdata item | ||
var di; | ||
// hover point item extended from pointData | ||
var pointData2; | ||
// loop indices | ||
var i, j; | ||
|
||
if(hoveron.indexOf('boxes') !== -1) { | ||
var t = cd[0].t; | ||
|
||
// closest mode: handicap box plots a little relative to others | ||
// adjust inbox w.r.t. to calculate box size | ||
var boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos; | ||
|
||
if(trace.orientation === 'h') { | ||
dx = function(di) { | ||
return Fx.inbox(di.min - xval, di.max - xval); | ||
}; | ||
dy = function(di) { | ||
var pos = di.pos + t.bPos - yval; | ||
return Fx.inbox(pos - boxDelta, pos + boxDelta); | ||
}; | ||
posLetter = 'y'; | ||
posAxis = ya; | ||
valLetter = 'x'; | ||
valAxis = xa; | ||
} else { | ||
dx = function(di) { | ||
var pos = di.pos + t.bPos - xval; | ||
return Fx.inbox(pos - boxDelta, pos + boxDelta); | ||
}; | ||
dy = function(di) { | ||
return Fx.inbox(di.min - yval, di.max - yval); | ||
}; | ||
posLetter = 'x'; | ||
posAxis = xa; | ||
valLetter = 'y'; | ||
valAxis = ya; | ||
} | ||
|
||
distfn = Fx.getDistanceFunction(hovermode, dx, dy); | ||
Fx.getClosest(cd, distfn, pointData); | ||
|
||
distfn = Fx.getDistanceFunction(hovermode, dx, dy); | ||
Fx.getClosest(cd, distfn, pointData); | ||
// skip the rest (for this trace) if we didn't find a close point | ||
// and create the item(s) in closedata for this point | ||
if(pointData.index !== false) { | ||
di = cd[pointData.index]; | ||
|
||
// skip the rest (for this trace) if we didn't find a close point | ||
if(pointData.index === false) return; | ||
var lc = trace.line.color; | ||
var mc = marker.color; | ||
|
||
// create the item(s) in closedata for this point | ||
if(Color.opacity(lc) && trace.line.width) pointData.color = lc; | ||
else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc; | ||
else pointData.color = trace.fillcolor; | ||
|
||
// the closest data point | ||
var di = cd[pointData.index], | ||
lc = trace.line.color, | ||
mc = (trace.marker || {}).color; | ||
if(Color.opacity(lc) && trace.line.width) pointData.color = lc; | ||
else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc; | ||
else pointData.color = trace.fillcolor; | ||
pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true); | ||
pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true); | ||
|
||
pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true); | ||
pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true); | ||
Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text; | ||
pointData[posLetter + 'LabelVal'] = di.pos; | ||
|
||
Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text; | ||
pointData[posLetter + 'LabelVal'] = di.pos; | ||
// box plots: each "point" gets many labels | ||
var usedVals = {}; | ||
var attrs = ['med', 'min', 'q1', 'q3', 'max']; | ||
|
||
// box plots: each "point" gets many labels | ||
var usedVals = {}, | ||
attrs = ['med', 'min', 'q1', 'q3', 'max'], | ||
attr, | ||
pointData2; | ||
if(trace.boxmean) attrs.push('mean'); | ||
if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']); | ||
if(trace.boxmean) attrs.push('mean'); | ||
if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']); | ||
|
||
for(var i = 0; i < attrs.length; i++) { | ||
attr = attrs[i]; | ||
for(i = 0; i < attrs.length; i++) { | ||
var attr = attrs[i]; | ||
|
||
if(!(attr in di) || (di[attr] in usedVals)) continue; | ||
usedVals[di[attr]] = true; | ||
if(!(attr in di) || (di[attr] in usedVals)) continue; | ||
usedVals[di[attr]] = true; | ||
|
||
// copy out to a new object for each value to label | ||
val = valAxis.c2p(di[attr], true); | ||
pointData2 = Lib.extendFlat({}, pointData); | ||
pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val; | ||
pointData2[valLetter + 'LabelVal'] = di[attr]; | ||
pointData2.attr = attr; | ||
// copy out to a new object for each value to label | ||
var val = valAxis.c2p(di[attr], true); | ||
pointData2 = Lib.extendFlat({}, pointData); | ||
pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val; | ||
pointData2[valLetter + 'LabelVal'] = di[attr]; | ||
pointData2.attr = attr; | ||
|
||
if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { | ||
pointData2[valLetter + 'err'] = di.sd; | ||
if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { | ||
pointData2[valLetter + 'err'] = di.sd; | ||
} | ||
// only keep name on the first item (median) | ||
pointData.name = ''; | ||
|
||
closeData.push(pointData2); | ||
} | ||
} | ||
pointData.name = ''; // only keep name on the first item (median) | ||
closeData.push(pointData2); | ||
} | ||
|
||
if(hoveron.indexOf('points') !== -1) { | ||
var xPx = xa.c2p(xval); | ||
var yPx = ya.c2p(yval); | ||
|
||
// do not take jitter into consideration in compare hover modes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyone disagrees here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
var kx, ky; | ||
if(hovermode === 'closest') { | ||
kx = 'x'; | ||
ky = 'y'; | ||
} else { | ||
kx = 'xh'; | ||
ky = 'yh'; | ||
} | ||
|
||
dx = function(di) { | ||
var rad = Math.max(3, di.mrc || 0); | ||
return Math.max(Math.abs(xa.c2p(di[kx]) - xPx) - rad, 1 - 3 / rad); | ||
}; | ||
dy = function(di) { | ||
var rad = Math.max(3, di.mrc || 0); | ||
return Math.max(Math.abs(ya.c2p(di[ky]) - yPx) - rad, 1 - 3 / rad); | ||
}; | ||
distfn = Fx.getDistanceFunction(hovermode, dx, dy); | ||
|
||
for(i = 0; i < cd.length; i++) { | ||
di = cd[i]; | ||
|
||
for(j = 0; j < (di.pts || []).length; j++) { | ||
var pt = di.pts[j]; | ||
|
||
var newDistance = distfn(pt); | ||
if(newDistance <= pointData.distance) { | ||
pointData.distance = newDistance; | ||
|
||
var xc = xa.c2p(pt.x, true); | ||
var yc = ya.c2p(pt.y, true); | ||
var rad = pt.mrc || 1; | ||
|
||
pointData2 = Lib.extendFlat({}, pointData, { | ||
// corresponds to index in x/y input data array | ||
index: pt.i, | ||
color: marker.color, | ||
x0: xc - rad, | ||
x1: xc + rad, | ||
xLabelVal: pt.x, | ||
y0: yc - rad, | ||
y1: yc + rad, | ||
yLabelVal: pt.y | ||
}); | ||
|
||
closeData.push(pointData2); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return closeData; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So I guess I'm proposing:
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good point here. I'll implement There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's what I come up with: f228a5e
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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 for spelled out, I think you're right that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe even spell out There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
}; |
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.
Debatable choice here. Strictly speaking we need
dflt: 'boxes'
to keep backward compat, but perhaps someone can argue otherwise.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.
dflt: 'boxes'
👍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 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 aslayout.hovermode
on click for traces withboxpoints: 'all'
?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.
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.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.
dflt: boxes+points
👍 One could even argue that not being able to hover on the box points is a 🐞 .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.
done in 6bfcbe0