Skip to content

Commit f228a5e

Browse files
committed
aj-proof box hover logic
- 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
1 parent 7e44c9c commit f228a5e

File tree

5 files changed

+87
-66
lines changed

5 files changed

+87
-66
lines changed

src/components/fx/helpers.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ exports.p2c = function p2c(axArray, v) {
3434
};
3535

3636
exports.getDistanceFunction = function getDistanceFunction(mode, dx, dy, dxy) {
37-
if(mode === 'closest') return dxy || quadrature(dx, dy);
37+
if(mode === 'closest') return dxy || exports.quadrature(dx, dy);
3838
return mode === 'x' ? dx : dy;
3939
};
4040

@@ -77,13 +77,13 @@ exports.inbox = function inbox(v0, v1) {
7777
return Infinity;
7878
};
7979

80-
function quadrature(dx, dy) {
80+
exports.quadrature = function quadrature(dx, dy) {
8181
return function(di) {
8282
var x = dx(di),
8383
y = dy(di);
8484
return Math.sqrt(x * x + y * y);
8585
};
86-
}
86+
};
8787

8888
/** Appends values inside array attributes corresponding to given point number
8989
*

src/components/fx/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = {
3535
getDistanceFunction: helpers.getDistanceFunction,
3636
getClosest: helpers.getClosest,
3737
inbox: helpers.inbox,
38+
quadrature: helpers.quadrature,
3839
appendArrayPointValue: helpers.appendArrayPointValue,
3940

4041
castHoverOption: castHoverOption,

src/traces/box/hover.js

+53-40
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,15 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
2323
var hoveron = trace.hoveron;
2424
var marker = trace.marker || {};
2525

26-
// output hover points array
27-
var closeData = [];
26+
// output hover points components
27+
var closeBoxData = [];
28+
var closePtData;
2829
// x/y/effective distance functions
2930
var dx, dy, distfn;
3031
// orientation-specific fields
3132
var posLetter, valLetter, posAxis, valAxis;
3233
// calcdata item
3334
var di;
34-
// hover point item extended from pointData
35-
var pointData2;
3635
// loop indices
3736
var i, j;
3837

@@ -105,7 +104,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
105104

106105
// copy out to a new object for each value to label
107106
var val = valAxis.c2p(di[attr], true);
108-
pointData2 = Lib.extendFlat({}, pointData);
107+
var pointData2 = Lib.extendFlat({}, pointData);
108+
109109
pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val;
110110
pointData2[valLetter + 'LabelVal'] = di[attr];
111111
pointData2.attr = attr;
@@ -116,7 +116,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
116116
// only keep name on the first item (median)
117117
pointData.name = '';
118118

119-
closeData.push(pointData2);
119+
closeBoxData.push(pointData2);
120120
}
121121
}
122122
}
@@ -125,58 +125,71 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
125125
var xPx = xa.c2p(xval);
126126
var yPx = ya.c2p(yval);
127127

128-
// do not take jitter into consideration in compare hover modes
129-
var kx, ky;
130-
if(hovermode === 'closest') {
131-
kx = 'x';
132-
ky = 'y';
133-
} else {
134-
kx = 'xh';
135-
ky = 'yh';
136-
}
137-
138128
dx = function(di) {
139129
var rad = Math.max(3, di.mrc || 0);
140-
return Math.max(Math.abs(xa.c2p(di[kx]) - xPx) - rad, 1 - 3 / rad);
130+
return Math.max(Math.abs(xa.c2p(di.x) - xPx) - rad, 1 - 3 / rad);
141131
};
142132
dy = function(di) {
143133
var rad = Math.max(3, di.mrc || 0);
144-
return Math.max(Math.abs(ya.c2p(di[ky]) - yPx) - rad, 1 - 3 / rad);
134+
return Math.max(Math.abs(ya.c2p(di.y) - yPx) - rad, 1 - 3 / rad);
145135
};
146-
distfn = Fx.getDistanceFunction(hovermode, dx, dy);
136+
distfn = Fx.quadrature(dx, dy);
137+
138+
// show one point per trace
139+
var ijClosest = false;
140+
var pt;
147141

148142
for(i = 0; i < cd.length; i++) {
149143
di = cd[i];
150144

151145
for(j = 0; j < (di.pts || []).length; j++) {
152-
var pt = di.pts[j];
146+
pt = di.pts[j];
153147

154148
var newDistance = distfn(pt);
155149
if(newDistance <= pointData.distance) {
156150
pointData.distance = newDistance;
157-
158-
var xc = xa.c2p(pt.x, true);
159-
var yc = ya.c2p(pt.y, true);
160-
var rad = pt.mrc || 1;
161-
162-
pointData2 = Lib.extendFlat({}, pointData, {
163-
// corresponds to index in x/y input data array
164-
index: pt.i,
165-
color: marker.color,
166-
x0: xc - rad,
167-
x1: xc + rad,
168-
xLabelVal: pt.x,
169-
y0: yc - rad,
170-
y1: yc + rad,
171-
yLabelVal: pt.y
172-
});
173-
174-
fillHoverText(pt, trace, pointData2);
175-
closeData.push(pointData2);
151+
ijClosest = [i, j];
176152
}
177153
}
178154
}
155+
156+
if(ijClosest) {
157+
di = cd[ijClosest[0]];
158+
pt = di.pts[ijClosest[1]];
159+
160+
var xc = xa.c2p(pt.x, true);
161+
var yc = ya.c2p(pt.y, true);
162+
var rad = pt.mrc || 1;
163+
164+
closePtData = Lib.extendFlat({}, pointData, {
165+
// corresponds to index in x/y input data array
166+
index: pt.i,
167+
color: marker.color,
168+
name: trace.name,
169+
x0: xc - rad,
170+
x1: xc + rad,
171+
xLabelVal: pt.x,
172+
y0: yc - rad,
173+
y1: yc + rad,
174+
yLabelVal: pt.y
175+
});
176+
fillHoverText(pt, trace, closePtData);
177+
}
179178
}
180179

181-
return closeData;
180+
// In closest mode, show only one point or stats for one box, and points have priority
181+
// If there's a point in range and hoveron has points, show the best single point only.
182+
// If hoveron has boxes and there's no point in range (or hoveron doesn't have points), show the box stats.
183+
if(hovermode === 'closest') {
184+
if(closePtData) return [closePtData];
185+
return closeBoxData;
186+
}
187+
188+
// Otherwise in compare mode, allow a point AND the box stats to be labeled
189+
// If there are multiple boxes in range (ie boxmode = 'overlay') we'll see stats for all of them.
190+
if(closePtData) {
191+
closeBoxData.push(closePtData);
192+
return closeBoxData;
193+
}
194+
return closeBoxData;
182195
};

src/traces/box/plot.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -179,26 +179,23 @@ module.exports = function plot(gd, plotinfo, cdbox) {
179179
newJitter = trace.jitter * 2 / maxJitterFactor;
180180
}
181181

182-
// fills in 'x' and 'y' (with and w/o jitter offset) in calcdata 'pts' item
182+
// fills in 'x' and 'y' in calcdata 'pts' item
183183
for(i = 0; i < pts.length; i++) {
184184
var pt = pts[i];
185185
var v = pt.v;
186186

187187
var jitterOffset = trace.jitter ?
188-
bdPos * (newJitter * jitterFactors[i] * (rand() - 0.5)) :
188+
(newJitter * jitterFactors[i] * (rand() - 0.5)) :
189189
0;
190190

191-
var posPxNoJitter = d.pos + bPos + bdPos * trace.pointpos;
192-
var posPx = posPxNoJitter + jitterOffset;
191+
var posPx = d.pos + bPos + bdPos * (trace.pointpos + jitterOffset);
193192

194193
if(trace.orientation === 'h') {
195194
pt.y = posPx;
196-
pt.yh = posPxNoJitter;
197-
pt.x = pt.xh = v;
195+
pt.x = v;
198196
} else {
199197
pt.x = posPx;
200-
pt.xh = posPxNoJitter;
201-
pt.y = pt.yh = v;
198+
pt.y = v;
202199
}
203200

204201
// tag suspected outliers

test/jasmine/tests/box_test.js

+25-15
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ describe('Test box hover:', function() {
235235
fig.layout.hovermode = 'closest';
236236
return fig;
237237
},
238-
nums: ['(day 1, 0.7)', '(day 1, 0.6)', '(day 1, 0.6)'],
239-
name: ['radishes', 'radishes', 'radishes']
238+
nums: '(day 1, 0.7)',
239+
name: 'radishes'
240240
}, {
241241
desc: 'hoveron points | hovermode x',
242242
patch: function(fig) {
@@ -247,11 +247,11 @@ describe('Test box hover:', function() {
247247
fig.layout.hovermode = 'x';
248248
return fig;
249249
},
250-
nums: ['0', '0.3', '0.5', '0.6', '0.6', '0.7'],
251-
name: ['radishes', 'radishes', 'radishes', 'radishes', 'radishes', 'radishes'],
250+
nums: '0.7',
251+
name: 'radishes',
252252
axis: 'day 1'
253253
}, {
254-
desc: 'hoveron boxes+points | hovermode x',
254+
desc: 'hoveron boxes+points | hovermode x (hover on box only - same result as base)',
255255
patch: function(fig) {
256256
fig.data.forEach(function(trace) {
257257
trace.boxpoints = 'all';
@@ -260,12 +260,22 @@ describe('Test box hover:', function() {
260260
fig.layout.hovermode = 'x';
261261
return fig;
262262
},
263-
nums: [
264-
'0', '0.7', '0.6', '0.6', '0.5', '0.3', '0', '0.7', '0.6', '0.3', '0.55'
265-
],
266-
name: [
267-
'', '', '', '', '', '', '', '', '', '', 'radishes'
268-
],
263+
nums: ['0.55', '0', '0.3', '0.6', '0.7'],
264+
name: ['radishes', '', '', '', ''],
265+
axis: 'day 1'
266+
}, {
267+
desc: 'hoveron boxes+points | hovermode x (box AND closest point)',
268+
patch: function(fig) {
269+
fig.data.forEach(function(trace) {
270+
trace.boxpoints = 'all';
271+
trace.hoveron = 'points+boxes';
272+
trace.pointpos = 0;
273+
});
274+
fig.layout.hovermode = 'x';
275+
return fig;
276+
},
277+
nums: ['0.6', '0.55', '0', '0.3', '0.6', '0.7'],
278+
name: ['radishes', 'radishes', '', '', '', ''],
269279
axis: 'day 1'
270280
}, {
271281
desc: 'text items on hover',
@@ -278,8 +288,8 @@ describe('Test box hover:', function() {
278288
fig.layout.hovermode = 'closest';
279289
return fig;
280290
},
281-
nums: ['(day 1, 0.7)\nlook:0.7', '(day 1, 0.6)\nlook:0.6', '(day 1, 0.6)\nlook:0.6'],
282-
name: ['radishes', 'radishes', 'radishes']
291+
nums: '(day 1, 0.7)\nlook:0.7',
292+
name: 'radishes'
283293
}, {
284294
desc: 'only text items on hover',
285295
patch: function(fig) {
@@ -292,8 +302,8 @@ describe('Test box hover:', function() {
292302
fig.layout.hovermode = 'closest';
293303
return fig;
294304
},
295-
nums: ['look:0.7', 'look:0.6', 'look:0.6'],
296-
name: ['', '', '']
305+
nums: 'look:0.7',
306+
name: ''
297307
}].forEach(function(specs) {
298308
it('should generate correct hover labels ' + specs.desc, function(done) {
299309
run(specs).catch(fail).then(done);

0 commit comments

Comments
 (0)