Skip to content

Commit 623fcd1

Browse files
authored
Merge pull request #5542 from plotly/fixup-bar-spike-dist
Fixup bar spike distance
2 parents 060e786 + 05c1c85 commit 623fcd1

File tree

3 files changed

+121
-14
lines changed

3 files changed

+121
-14
lines changed

src/components/fx/hover.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,11 @@ function _hover(gd, evt, subplot, noHoverEvent) {
248248
return dragElement.unhoverRaw(gd, evt);
249249
}
250250

251-
var hoverdistance = fullLayout.hoverdistance === -1 ? Infinity : fullLayout.hoverdistance;
252-
var spikedistance = fullLayout.spikedistance === -1 ? Infinity : fullLayout.spikedistance;
251+
var hoverdistance = fullLayout.hoverdistance;
252+
if(hoverdistance === -1) hoverdistance = Infinity;
253+
254+
var spikedistance = fullLayout.spikedistance;
255+
if(spikedistance === -1) spikedistance = Infinity;
253256

254257
// hoverData: the set of candidate points we've found to highlight
255258
var hoverData = [];

src/traces/bar/hover.js

+27-10
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ function hoverOnBars(pointData, xval, yval, hovermode) {
3131
var isClosest = (hovermode === 'closest');
3232
var isWaterfall = (trace.type === 'waterfall');
3333
var maxHoverDistance = pointData.maxHoverDistance;
34+
var maxSpikeDistance = pointData.maxSpikeDistance;
3435

3536
var posVal, sizeVal, posLetter, sizeLetter, dx, dy, pRangeCalc;
3637

@@ -61,40 +62,56 @@ function hoverOnBars(pointData, xval, yval, hovermode) {
6162
return Math.max(thisBarMaxPos(di), di.p + t.bardelta / 2);
6263
};
6364

64-
function _positionFn(_minPos, _maxPos) {
65+
function inbox(_minPos, _maxPos, maxDistance) {
6566
// add a little to the pseudo-distance for wider bars, so that like scatter,
6667
// if you are over two overlapping bars, the narrower one wins.
6768
return Fx.inbox(_minPos - posVal, _maxPos - posVal,
68-
maxHoverDistance + Math.min(1, Math.abs(_maxPos - _minPos) / pRangeCalc) - 1);
69+
maxDistance + Math.min(1, Math.abs(_maxPos - _minPos) / pRangeCalc) - 1);
6970
}
7071

7172
function positionFn(di) {
72-
return _positionFn(minPos(di), maxPos(di));
73+
return inbox(minPos(di), maxPos(di), maxHoverDistance);
7374
}
7475

7576
function thisBarPositionFn(di) {
76-
return _positionFn(thisBarMinPos(di), thisBarMaxPos(di));
77+
return inbox(thisBarMinPos(di), thisBarMaxPos(di), maxSpikeDistance);
7778
}
7879

79-
function sizeFn(di) {
80-
var v = sizeVal;
81-
var b = di.b;
80+
function getSize(di) {
8281
var s = di[sizeLetter];
8382

8483
if(isWaterfall) {
8584
var rawS = Math.abs(di.rawS) || 0;
86-
if(v > 0) {
85+
if(sizeVal > 0) {
8786
s += rawS;
88-
} else if(v < 0) {
87+
} else if(sizeVal < 0) {
8988
s -= rawS;
9089
}
9190
}
9291

92+
return s;
93+
}
94+
95+
function sizeFn(di) {
96+
var v = sizeVal;
97+
var b = di.b;
98+
var s = getSize(di);
99+
93100
// add a gradient so hovering near the end of a
94101
// bar makes it a little closer match
95102
return Fx.inbox(b - v, s - v, maxHoverDistance + (s - v) / (s - b) - 1);
96103
}
97104

105+
function thisBarSizeFn(di) {
106+
var v = sizeVal;
107+
var b = di.b;
108+
var s = getSize(di);
109+
110+
// add a gradient so hovering near the end of a
111+
// bar makes it a little closer match
112+
return Fx.inbox(b - v, s - v, maxSpikeDistance + (s - v) / (s - b) - 1);
113+
}
114+
98115
if(trace.orientation === 'h') {
99116
posVal = yval;
100117
sizeVal = xval;
@@ -158,7 +175,7 @@ function hoverOnBars(pointData, xval, yval, hovermode) {
158175
pointData.baseLabel = hoverLabelText(sa, di.b);
159176

160177
// spikelines always want "closest" distance regardless of hovermode
161-
pointData.spikeDistance = (sizeFn(di) + thisBarPositionFn(di)) / 2 - maxHoverDistance;
178+
pointData.spikeDistance = (thisBarSizeFn(di) + thisBarPositionFn(di)) / 2;
162179
// they also want to point to the data value, regardless of where the label goes
163180
// in case of bars shifted within groups
164181
pointData[posLetter + 'Spike'] = pa.c2p(di.p, true);

test/jasmine/tests/hover_spikeline_test.js

+89-2
Original file line numberDiff line numberDiff line change
@@ -419,12 +419,13 @@ describe('spikeline hover', function() {
419419
.then(done, done.fail);
420420
});
421421

422-
it('correctly select the closest bar even when setting spikedistance to -1', function(done) {
422+
it('correctly select the closest bar even when setting spikedistance to -1 (case of x hovermode)', function(done) {
423423
var mock = require('@mocks/bar_stack-with-gaps');
424424
var mockCopy = Lib.extendDeep({}, mock);
425425
mockCopy.layout.xaxis.showspikes = true;
426426
mockCopy.layout.yaxis.showspikes = true;
427427
mockCopy.layout.spikedistance = -1;
428+
mockCopy.layout.hovermode = 'x';
428429

429430
Plotly.newPlot(gd, mockCopy)
430431
.then(function() {
@@ -433,10 +434,96 @@ describe('spikeline hover', function() {
433434
expect(lines.size()).toBe(4);
434435
expect(lines[0][1].getAttribute('stroke')).toBe('#2ca02c');
435436

436-
_hover({xpx: 600, ypx: 200});
437+
_hover({xpx: 600, ypx: 100});
437438
lines = d3SelectAll('line.spikeline');
438439
expect(lines.size()).toBe(4);
440+
expect(lines[0][1].getAttribute('stroke')).toBe('#2ca02c');
441+
})
442+
.then(done, done.fail);
443+
});
444+
445+
it('correctly select the closest bar even when setting spikedistance to -1 (case of closest hovermode)', function(done) {
446+
var mock = require('@mocks/bar_stack-with-gaps');
447+
var mockCopy = Lib.extendDeep({}, mock);
448+
mockCopy.layout.xaxis.showspikes = true;
449+
mockCopy.layout.yaxis.showspikes = true;
450+
mockCopy.layout.spikedistance = -1;
451+
mockCopy.layout.hovermode = 'closest';
452+
453+
Plotly.newPlot(gd, mockCopy)
454+
.then(function() {
455+
_hover({xpx: 600, ypx: 400});
456+
var lines = d3SelectAll('line.spikeline');
457+
expect(lines.size()).toBe(4);
439458
expect(lines[0][1].getAttribute('stroke')).toBe('#1f77b4');
459+
460+
_hover({xpx: 600, ypx: 100});
461+
lines = d3SelectAll('line.spikeline');
462+
expect(lines.size()).toBe(4);
463+
expect(lines[0][1].getAttribute('stroke')).toBe('#2ca02c');
464+
})
465+
.then(done, done.fail);
466+
});
467+
468+
it('could select the closest scatter point inside bar', function(done) {
469+
Plotly.newPlot(gd, {
470+
data: [{
471+
type: 'scatter',
472+
marker: { color: 'green' },
473+
x: [
474+
-1,
475+
0,
476+
0.5,
477+
1
478+
],
479+
y: [
480+
0.1,
481+
0.2,
482+
0.25,
483+
0.3
484+
]
485+
},
486+
{
487+
type: 'bar',
488+
marker: { color: 'blue' },
489+
x: [
490+
-1,
491+
-0.2,
492+
1
493+
],
494+
y: [
495+
1,
496+
2,
497+
0.5
498+
]
499+
}],
500+
layout: {
501+
hovermode: 'x',
502+
xaxis: { showspikes: true },
503+
yaxis: { showspikes: true },
504+
showlegend: false,
505+
width: 500,
506+
height: 500,
507+
margin: {
508+
t: 50,
509+
b: 50,
510+
l: 50,
511+
r: 50,
512+
}
513+
}
514+
})
515+
.then(function() {
516+
var lines;
517+
518+
_hover({xpx: 200, ypx: 200});
519+
lines = d3SelectAll('line.spikeline');
520+
expect(lines.size()).toBe(4);
521+
expect(lines[0][1].getAttribute('stroke')).toBe('blue');
522+
523+
_hover({xpx: 200, ypx: 350});
524+
lines = d3SelectAll('line.spikeline');
525+
expect(lines.size()).toBe(4);
526+
expect(lines[0][1].getAttribute('stroke')).toBe('green');
440527
})
441528
.then(done, done.fail);
442529
});

0 commit comments

Comments
 (0)