From f269b7a3f67260f3758604556941d6198c85fcb0 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 24 Sep 2026 20:12:34 -0600 Subject: [PATCH 1/5] fix: Pick the trace drawn last on a hover tie in scattergl and splom --- src/traces/scattergl/hover.js | 6 ++++-- src/traces/splom/hover.js | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index b153fb65434..9476ea88f41 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; @@ -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; } From 871402b7b1aa96dc1fc22ec1f325f12a98309e23 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 24 Sep 2026 20:12:53 -0600 Subject: [PATCH 2/5] Add tests --- test/jasmine/tests/hover_test.js | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index 061fd8ac4e5..c9e9d2cf975 100644 --- a/test/jasmine/tests/hover_test.js +++ b/test/jasmine/tests/hover_test.js @@ -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; From e17e5a16b43a2fdcc23618a9b95e41111378b149 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 24 Sep 2026 20:17:42 -0600 Subject: [PATCH 3/5] Add draftlog --- draftlogs/8079_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/8079_fix.md 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)] From ced1cfeff9453da7bc4c590d08b72961d88be952 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 25 Sep 2026 10:47:16 -0600 Subject: [PATCH 4/5] Run the scattergl hover loop forward in the else branch --- src/traces/scattergl/hover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index 9476ea88f41..624bf589b66 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -85,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]; From 54c5db6b21df7f963abf657fbc303512e3e36e9c Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 25 Sep 2026 10:47:30 -0600 Subject: [PATCH 5/5] Update tests per behavior change --- test/jasmine/tests/hover_test.js | 14 +++++++------- test/jasmine/tests/select_test.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index c9e9d2cf975..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' }); 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) {