Skip to content

Commit aef61ae

Browse files
committed
introduce box 'hoveron'
- 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
1 parent 5c58049 commit aef61ae

File tree

5 files changed

+212
-81
lines changed

5 files changed

+212
-81
lines changed

src/traces/box/attributes.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,16 @@ module.exports = {
203203
},
204204
editType: 'plot'
205205
},
206-
fillcolor: scatterAttrs.fillcolor
206+
fillcolor: scatterAttrs.fillcolor,
207+
hoveron: {
208+
valType: 'flaglist',
209+
flags: ['boxes', 'points'],
210+
dflt: 'boxes',
211+
role: 'info',
212+
editType: 'style',
213+
description: [
214+
'Do the hover effects highlight individual boxes ',
215+
'or jitter points or both?'
216+
].join(' ')
217+
}
207218
};

src/traces/box/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
7171
coerce('marker.line.outlierwidth');
7272
}
7373
}
74+
75+
coerce('hoveron');
7476
};

src/traces/box/hover.js

+149-76
Original file line numberDiff line numberDiff line change
@@ -14,94 +14,167 @@ var Fx = require('../../components/fx');
1414
var Color = require('../../components/color');
1515

1616
module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
17-
// closest mode: handicap box plots a little relative to others
18-
var cd = pointData.cd,
19-
trace = cd[0].trace,
20-
t = cd[0].t,
21-
xa = pointData.xa,
22-
ya = pointData.ya,
23-
closeData = [],
24-
dx, dy, distfn, boxDelta,
25-
posLetter, posAxis,
26-
val, valLetter, valAxis;
27-
28-
// adjust inbox w.r.t. to calculate box size
29-
boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos;
30-
31-
if(trace.orientation === 'h') {
32-
dx = function(di) {
33-
return Fx.inbox(di.min - xval, di.max - xval);
34-
};
35-
dy = function(di) {
36-
var pos = di.pos + t.bPos - yval;
37-
return Fx.inbox(pos - boxDelta, pos + boxDelta);
38-
};
39-
posLetter = 'y';
40-
posAxis = ya;
41-
valLetter = 'x';
42-
valAxis = xa;
43-
} else {
44-
dx = function(di) {
45-
var pos = di.pos + t.bPos - xval;
46-
return Fx.inbox(pos - boxDelta, pos + boxDelta);
47-
};
48-
dy = function(di) {
49-
return Fx.inbox(di.min - yval, di.max - yval);
50-
};
51-
posLetter = 'x';
52-
posAxis = xa;
53-
valLetter = 'y';
54-
valAxis = ya;
55-
}
17+
var cd = pointData.cd;
18+
var xa = pointData.xa;
19+
var ya = pointData.ya;
20+
21+
var trace = cd[0].trace;
22+
var hoveron = trace.hoveron;
23+
var marker = trace.marker || {};
24+
25+
// output hover points array
26+
var closeData = [];
27+
// x/y/effective distance functions
28+
var dx, dy, distfn;
29+
// orientation-specific fields
30+
var posLetter, valLetter, posAxis, valAxis;
31+
// calcdata item
32+
var di;
33+
// hover point item extended from pointData
34+
var pointData2;
35+
// loop indices
36+
var i, j;
37+
38+
if(hoveron.indexOf('boxes') !== -1) {
39+
var t = cd[0].t;
40+
41+
// closest mode: handicap box plots a little relative to others
42+
// adjust inbox w.r.t. to calculate box size
43+
var boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos;
44+
45+
if(trace.orientation === 'h') {
46+
dx = function(di) {
47+
return Fx.inbox(di.min - xval, di.max - xval);
48+
};
49+
dy = function(di) {
50+
var pos = di.pos + t.bPos - yval;
51+
return Fx.inbox(pos - boxDelta, pos + boxDelta);
52+
};
53+
posLetter = 'y';
54+
posAxis = ya;
55+
valLetter = 'x';
56+
valAxis = xa;
57+
} else {
58+
dx = function(di) {
59+
var pos = di.pos + t.bPos - xval;
60+
return Fx.inbox(pos - boxDelta, pos + boxDelta);
61+
};
62+
dy = function(di) {
63+
return Fx.inbox(di.min - yval, di.max - yval);
64+
};
65+
posLetter = 'x';
66+
posAxis = xa;
67+
valLetter = 'y';
68+
valAxis = ya;
69+
}
70+
71+
distfn = Fx.getDistanceFunction(hovermode, dx, dy);
72+
Fx.getClosest(cd, distfn, pointData);
5673

57-
distfn = Fx.getDistanceFunction(hovermode, dx, dy);
58-
Fx.getClosest(cd, distfn, pointData);
74+
// skip the rest (for this trace) if we didn't find a close point
75+
// and create the item(s) in closedata for this point
76+
if(pointData.index !== false) {
77+
di = cd[pointData.index];
5978

60-
// skip the rest (for this trace) if we didn't find a close point
61-
if(pointData.index === false) return;
79+
var lc = trace.line.color;
80+
var mc = marker.color;
6281

63-
// create the item(s) in closedata for this point
82+
if(Color.opacity(lc) && trace.line.width) pointData.color = lc;
83+
else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc;
84+
else pointData.color = trace.fillcolor;
6485

65-
// the closest data point
66-
var di = cd[pointData.index],
67-
lc = trace.line.color,
68-
mc = (trace.marker || {}).color;
69-
if(Color.opacity(lc) && trace.line.width) pointData.color = lc;
70-
else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc;
71-
else pointData.color = trace.fillcolor;
86+
pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true);
87+
pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true);
7288

73-
pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true);
74-
pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true);
89+
Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text;
90+
pointData[posLetter + 'LabelVal'] = di.pos;
7591

76-
Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text;
77-
pointData[posLetter + 'LabelVal'] = di.pos;
92+
// box plots: each "point" gets many labels
93+
var usedVals = {};
94+
var attrs = ['med', 'min', 'q1', 'q3', 'max'];
7895

79-
// box plots: each "point" gets many labels
80-
var usedVals = {},
81-
attrs = ['med', 'min', 'q1', 'q3', 'max'],
82-
attr,
83-
pointData2;
84-
if(trace.boxmean) attrs.push('mean');
85-
if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']);
96+
if(trace.boxmean) attrs.push('mean');
97+
if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']);
8698

87-
for(var i = 0; i < attrs.length; i++) {
88-
attr = attrs[i];
99+
for(i = 0; i < attrs.length; i++) {
100+
var attr = attrs[i];
89101

90-
if(!(attr in di) || (di[attr] in usedVals)) continue;
91-
usedVals[di[attr]] = true;
102+
if(!(attr in di) || (di[attr] in usedVals)) continue;
103+
usedVals[di[attr]] = true;
92104

93-
// copy out to a new object for each value to label
94-
val = valAxis.c2p(di[attr], true);
95-
pointData2 = Lib.extendFlat({}, pointData);
96-
pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val;
97-
pointData2[valLetter + 'LabelVal'] = di[attr];
98-
pointData2.attr = attr;
105+
// copy out to a new object for each value to label
106+
var val = valAxis.c2p(di[attr], true);
107+
pointData2 = Lib.extendFlat({}, pointData);
108+
pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val;
109+
pointData2[valLetter + 'LabelVal'] = di[attr];
110+
pointData2.attr = attr;
99111

100-
if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
101-
pointData2[valLetter + 'err'] = di.sd;
112+
if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
113+
pointData2[valLetter + 'err'] = di.sd;
114+
}
115+
// only keep name on the first item (median)
116+
pointData.name = '';
117+
118+
closeData.push(pointData2);
119+
}
102120
}
103-
pointData.name = ''; // only keep name on the first item (median)
104-
closeData.push(pointData2);
105121
}
122+
123+
if(hoveron.indexOf('points') !== -1) {
124+
var xPx = xa.c2p(xval);
125+
var yPx = ya.c2p(yval);
126+
127+
// do not take jitter into consideration in compare hover modes
128+
var kx, ky;
129+
if(hovermode === 'closest') {
130+
kx = 'x';
131+
ky = 'y';
132+
} else {
133+
kx = 'xh';
134+
ky = 'yh';
135+
}
136+
137+
dx = function(di) {
138+
var rad = Math.max(3, di.mrc || 0);
139+
return Math.max(Math.abs(xa.c2p(di[kx]) - xPx) - rad, 1 - 3 / rad);
140+
};
141+
dy = function(di) {
142+
var rad = Math.max(3, di.mrc || 0);
143+
return Math.max(Math.abs(ya.c2p(di[ky]) - yPx) - rad, 1 - 3 / rad);
144+
};
145+
distfn = Fx.getDistanceFunction(hovermode, dx, dy);
146+
147+
for(i = 0; i < cd.length; i++) {
148+
di = cd[i];
149+
150+
for(j = 0; j < (di.pts || []).length; j++) {
151+
var pt = di.pts[j];
152+
153+
var newDistance = distfn(pt);
154+
if(newDistance <= pointData.distance) {
155+
pointData.distance = newDistance;
156+
157+
var xc = xa.c2p(pt.x, true);
158+
var yc = ya.c2p(pt.y, true);
159+
var rad = pt.mrc || 1;
160+
161+
pointData2 = Lib.extendFlat({}, pointData, {
162+
// corresponds to index in x/y input data array
163+
index: pt.i,
164+
color: marker.color,
165+
x0: xc - rad,
166+
x1: xc + rad,
167+
xLabelVal: pt.x,
168+
y0: yc - rad,
169+
y1: yc + rad,
170+
yLabelVal: pt.y
171+
});
172+
173+
closeData.push(pointData2);
174+
}
175+
}
176+
}
177+
}
178+
106179
return closeData;
107180
};

src/traces/box/plot.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ module.exports = function plot(gd, plotinfo, cdbox) {
183183
newJitter = trace.jitter * 2 / maxJitterFactor;
184184
}
185185

186-
// fills in 'x' and 'y' in calcdata 'pts' item
186+
// fills in 'x' and 'y' (with and w/o jitter offset) in calcdata 'pts' item
187187
for(i = 0; i < pts.length; i++) {
188188
var pt = pts[i];
189189
var v = pt.v;
@@ -192,14 +192,17 @@ module.exports = function plot(gd, plotinfo, cdbox) {
192192
bdPos * (newJitter * jitterFactors[i] * (rand() - 0.5)) :
193193
0;
194194

195-
var posPx = d.pos + bPos + bdPos * trace.pointpos + jitterOffset;
195+
var posPxNoJitter = d.pos + bPos + bdPos * trace.pointpos;
196+
var posPx = posPxNoJitter + jitterOffset;
196197

197198
if(trace.orientation === 'h') {
198199
pt.y = posPx;
199-
pt.x = v;
200+
pt.yh = posPxNoJitter;
201+
pt.x = pt.xh = v;
200202
} else {
201203
pt.x = posPx;
202-
pt.y = v;
204+
pt.xh = posPxNoJitter;
205+
pt.y = pt.yh = v;
203206
}
204207

205208
// tag suspected outliers

test/jasmine/tests/box_test.js

+42
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,48 @@ describe('Test box hover:', function() {
174174
nums: ['8.15', '0.75', '6.8', '10.25', '23.25', '5.25', '12'],
175175
name: ['', '', '', '', '', '', ''],
176176
axis: 'trace 0',
177+
}, {
178+
desc: 'hoveron points | hovermode closest',
179+
patch: function(fig) {
180+
fig.data.forEach(function(trace) {
181+
trace.boxpoints = 'all';
182+
trace.hoveron = 'points';
183+
});
184+
fig.layout.hovermode = 'closest';
185+
return fig;
186+
},
187+
nums: ['(day 1, 0.7)', '(day 1, 0.6)', '(day 1, 0.6)'],
188+
name: ['radishes', 'radishes', 'radishes']
189+
}, {
190+
desc: 'hoveron points | hovermode x',
191+
patch: function(fig) {
192+
fig.data.forEach(function(trace) {
193+
trace.boxpoints = 'all';
194+
trace.hoveron = 'points';
195+
});
196+
fig.layout.hovermode = 'x';
197+
return fig;
198+
},
199+
nums: ['0', '0.3', '0.5', '0.6', '0.6', '0.7'],
200+
name: ['radishes', 'radishes', 'radishes', 'radishes', 'radishes', 'radishes'],
201+
axis: 'day 1'
202+
}, {
203+
desc: 'hoveron boxes+points | hovermode x',
204+
patch: function(fig) {
205+
fig.data.forEach(function(trace) {
206+
trace.boxpoints = 'all';
207+
trace.hoveron = 'points+boxes';
208+
});
209+
fig.layout.hovermode = 'x';
210+
return fig;
211+
},
212+
nums: [
213+
'0', '0.7', '0.6', '0.6', '0.5', '0.3', '0', '0.7', '0.6', '0.3', '0.55'
214+
],
215+
name: [
216+
'', '', '', '', '', '', '', '', '', '', 'radishes'
217+
],
218+
axis: 'day 1'
177219
}].forEach(function(specs) {
178220
it('should generate correct hover labels ' + specs.desc, function(done) {
179221
run(specs).catch(fail).then(done);

0 commit comments

Comments
 (0)