diff --git a/draftlogs/8079_fix.md b/draftlogs/8079_fix.md new file mode 100644 index 00000000000..e1b139ad218 --- /dev/null +++ b/draftlogs/8079_fix.md @@ -0,0 +1 @@ +- Pick the trace drawn last on a hover tie in `scattergl` and `splom` (matching `scatter`) [[#8079](https://github.com/plotly/plotly.js/pull/8079)] diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index b153fb65434..624bf589b66 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -64,7 +64,8 @@ function hoverPoints(pointData, xval, yval, hovermode) { ) ? 0 : Infinity; } - if(dx < minDist) { + // Accept a tie so that the point drawn last wins, as in `Fx.getClosest` + if(dx <= minDist) { minDist = dx; pty = y[k]; dy = ya.c2p(pty) - ypx; @@ -84,7 +85,7 @@ function hoverPoints(pointData, xval, yval, hovermode) { } } } else { - for(i = ids.length - 1; i > -1; i--) { + for(i = 0; i < ids.length; i++) { k = ids[i]; ptx = x[k]; pty = y[k]; @@ -92,7 +93,8 @@ function hoverPoints(pointData, xval, yval, hovermode) { dy = ya.c2p(pty) - ypx; dist = Math.sqrt(dx * dx + dy * dy); - if(dist < minDist) { + // Accept a tie so that the trace drawn last wins, as in `Fx.getClosest` + if(dist <= minDist) { minDist = dxy = dist; closestId = k; } diff --git a/src/traces/splom/hover.js b/src/traces/splom/hover.js index d3b891542e2..251016b31d7 100644 --- a/src/traces/splom/hover.js +++ b/src/traces/splom/hover.js @@ -76,7 +76,8 @@ function _hoverPoints(pointData, xval, yval, axisHoversubplots) { var dy = ya.c2p(pty) - ypx; var dist = Math.sqrt(dx * dx + dy * dy); - if(axisHoversubplots || dist < minDist) { + // Accept a tie so that the point drawn last wins, as in `Fx.getClosest` + if(axisHoversubplots || dist <= minDist) { minDist = dxy = dist; id = i; } diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index 061fd8ac4e5..544bf2f0e29 100644 --- a/test/jasmine/tests/hover_test.js +++ b/test/jasmine/tests/hover_test.js @@ -3281,7 +3281,7 @@ describe('splom hover *axis* hoversubplots splom points on same position should assertFirstPointOn(gd, 'x', 'y'); expect(gd._hoverdata.length).toBe(4); assertHoverLabelContent({ - nums: ['1', '1', '1', '1'], + nums: ['1', '4', '4', '4'], name: ['', '', '', ''], axis: '1' }); @@ -3289,20 +3289,20 @@ describe('splom hover *axis* hoversubplots splom points on same position should Lib.clearThrottle(); Plotly.Fx.hover(gd, {}, 'xy2'); assertFirstPointOn(gd, 'x', 'y2'); - expect(gd._hoverdata.length).toBe(3); + expect(gd._hoverdata.length).toBe(4); assertHoverLabelContent({ - nums: ['1', '2', '2'], - name: ['', '', ''], + nums: ['1', '3', '3', '3'], + name: ['', '', '', ''], axis: '1' }); Lib.clearThrottle(); Plotly.Fx.hover(gd, {}, 'xy3'); assertFirstPointOn(gd, 'x', 'y3'); - expect(gd._hoverdata.length).toBe(3); + expect(gd._hoverdata.length).toBe(4); assertHoverLabelContent({ - nums: ['1', '2', '2'], - name: ['', '', ''], + nums: ['1', '3', '3', '3'], + name: ['', '', '', ''], axis: '1' }); @@ -5617,6 +5617,44 @@ describe('hover working with zorder', function () { }); }); +describe('hover on points at the same position', () => { + afterEach(destroyGraphDiv); + + ['scatter', 'scattergl', 'splom'].forEach((type) => { + const makeTrace = (x, y) => + type === 'splom' ? { type, dimensions: [{ values: x }, { values: y }] } : { type, x, y }; + // splom plots dimension 0 against dimension 1 on subplot xy2 + const subplot = type === 'splom' ? 'xy2' : 'xy'; + + it(`picks the trace drawn last for ${type} in closest hovermode`, async () => { + const gd = createGraphDiv(); + await Plotly.newPlot(gd, { + data: [makeTrace([0, 1, 2], [1, 0, 1]), makeTrace([0, 1, 2], [2, 0, 2])], + layout: { width: 400, height: 400, hovermode: 'closest' } + }); + + Fx.hover(gd, { xval: 1, yval: 0 }, subplot); + + expect(gd._hoverdata.length).toEqual(1); + expect(gd._hoverdata[0].curveNumber).toEqual(1); + expect(gd._hoverdata[0].pointNumber).toEqual(1); + }); + + it(`picks the point drawn last for ${type} in x hovermode`, async () => { + const gd = createGraphDiv(); + await Plotly.newPlot(gd, { + data: [makeTrace([0, 1, 1, 2], [1, 0, 0, 1])], + layout: { width: 400, height: 400, hovermode: 'x' } + }); + + Fx.hover(gd, { xval: 1, yval: 0 }, subplot); + + expect(gd._hoverdata.length).toEqual(1); + expect(gd._hoverdata[0].pointNumber).toEqual(2); + }); + }); +}); + describe('hover label rotation:', function () { var gd; diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 1f21365e14a..32a0ac8102f 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -677,7 +677,7 @@ describe('Click-to-select', function() { [ testCase('scatterpolargl', require('../../image/mocks/glpolar_scatter.json'), 130, 290, [[], [], [], [19], [], []], { dragmode: 'zoom' }), - testCase('splom', require('../../image/mocks/splom_lower.json'), 427, 400, [[], [7], []]) + testCase('splom', require('../../image/mocks/splom_lower.json'), 427, 400, [[], [43], []]) ] .forEach(function(testCase) { it('@gl trace type ' + testCase.label, function(done) {