diff --git a/draftlogs/7960_fix.md b/draftlogs/7960_fix.md new file mode 100644 index 00000000000..69ad9ca9d90 --- /dev/null +++ b/draftlogs/7960_fix.md @@ -0,0 +1 @@ +- Ensure that hoverlabels on category axes with hovermode "x", "y", "x unified" and "y unified" never include data points from other categories than the hovered one [[#7960](https://github.com/plotly/plotly.js/pull/7960)] diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 342676cf654..d5db091eb54 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -972,6 +972,21 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { for (k = hoverData.length - 1; k > initLen - 1; k--) { insert(hoverData[k]); } + + // on category axes only keep points that fall in the hovered category + var winAx = winningPoint[axLetter + 'a']; + if (winAx && (winAx.type === 'category' || winAx.type === 'multicategory')) { + var winCat = winningPoint[axLetter + 'LabelVal']; + finalPoints = finalPoints.filter(function (point) { + var pointAx = point[axLetter + 'a']; + if (!pointAx || (pointAx.type !== 'category' && pointAx.type !== 'multicategory')) { + // rare case of data point on overlaying axis + return true; + } + return point[axLetter + 'LabelVal'] === winCat; + }); + } + hoverData = finalPoints; sortHoverData(); } diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index fd7fae8dbb1..180dc8b32db 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -142,7 +142,10 @@ module.exports = { 'This is only a real distance for hovering on point-like objects,', 'like scatter points. For area-like objects (bars, scatter fills, etc)', 'hovering is on inside the area and off outside, but these objects', - 'will not supersede hover on point-like objects in case of conflict.' + 'will not supersede hover on point-like objects in case of conflict.', + 'For hovermodes *x*, *y*, *x unified* and *y unified*', + 'on a category axis, only points that fall in the hovered category', + 'are shown in the hover label, regardless of this distance.' ].join(' ') }, spikedistance: { diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 8a855665e73..1a50b413e3a 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -16256,7 +16256,7 @@ export interface Layout { */ hoveranywhere?: boolean; /** - * Sets the default distance (in pixels) to look for data to add hover labels (-1 means no cutoff, 0 means no looking for data). This is only a real distance for hovering on point-like objects, like scatter points. For area-like objects (bars, scatter fills, etc) hovering is on inside the area and off outside, but these objects will not supersede hover on point-like objects in case of conflict. + * Sets the default distance (in pixels) to look for data to add hover labels (-1 means no cutoff, 0 means no looking for data). This is only a real distance for hovering on point-like objects, like scatter points. For area-like objects (bars, scatter fills, etc) hovering is on inside the area and off outside, but these objects will not supersede hover on point-like objects in case of conflict. For hovermodes *x*, *y*, *x unified* and *y unified* on a category axis, only points that fall in the hovered category are shown in the hover label, regardless of this distance. * @default 20 * Minimum: -1 */ diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index 597f4224959..e4074bf499d 100644 --- a/test/jasmine/tests/hover_test.js +++ b/test/jasmine/tests/hover_test.js @@ -7635,6 +7635,38 @@ describe('hovermode: (x|y)unified', function () { }) .then(done, done.fail); }); + + it('hover label should only show values of hovered category', function (done) { + Plotly.newPlot(gd, { + data: [ + { name: 'bar', + x: ['A', 'B'], + y: [1, 2], + type: 'bar' + }, + { + name: 'scatter', + x: ['A', 'B'], + y: [10, null], + type: 'scatter' + } + ], + layout: { + width: 400, + hovermode: 'x unified', + hoverdistance: 110 + } + }) + .then(function () { + _hover(gd, { xval: 1.1 }); + assertLabel({ title: 'B', items: ['bar : 2'] }); + }) + .then(function () { + _hover(gd, { xval: 0 }); + assertLabel({ title: 'A', items: ['bar : 1', 'scatter : 10'] }); + }) + .then(done, done.fail); + }); }); describe('hover on traces with (x|y)hoverformat', function () { diff --git a/test/plot-schema.json b/test/plot-schema.json index 4d51312e82d..2adb0136367 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -2822,7 +2822,7 @@ "valType": "boolean" }, "hoverdistance": { - "description": "Sets the default distance (in pixels) to look for data to add hover labels (-1 means no cutoff, 0 means no looking for data). This is only a real distance for hovering on point-like objects, like scatter points. For area-like objects (bars, scatter fills, etc) hovering is on inside the area and off outside, but these objects will not supersede hover on point-like objects in case of conflict.", + "description": "Sets the default distance (in pixels) to look for data to add hover labels (-1 means no cutoff, 0 means no looking for data). This is only a real distance for hovering on point-like objects, like scatter points. For area-like objects (bars, scatter fills, etc) hovering is on inside the area and off outside, but these objects will not supersede hover on point-like objects in case of conflict. For hovermodes *x*, *y*, *x unified* and *y unified* on a category axis, only points that fall in the hovered category are shown in the hover label, regardless of this distance.", "dflt": 20, "editType": "none", "min": -1,