From 225e47db4d2d7c6a80f8f12bab38f7c6d8f3d120 Mon Sep 17 00:00:00 2001 From: CenFangyu <164994318+Dmao233@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:29:33 +0000 Subject: [PATCH 1/5] Fix unified hover pattern swatch for bar shape.pattern arrays Unified hover built the bar swatch from pattern index 0. Use the hovered point index so color and pattern match that bar. Co-authored-by: CenFangyu --- draftlogs/XXXX_fix.md | 1 + src/components/legend/style.js | 23 ++++++++--- test/jasmine/tests/hover_test.js | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 draftlogs/XXXX_fix.md diff --git a/draftlogs/XXXX_fix.md b/draftlogs/XXXX_fix.md new file mode 100644 index 00000000000..e41c62222ff --- /dev/null +++ b/draftlogs/XXXX_fix.md @@ -0,0 +1 @@ +- Fix unified hover swatch color and pattern for bar `marker.pattern` arrays [[#XXXX](https://github.com/plotly/plotly.js/issues/8008)] diff --git a/src/components/legend/style.js b/src/components/legend/style.js index a149a535f84..96f098265ee 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -379,15 +379,26 @@ module.exports = function style(s, gd, legend) { var markerPattern = marker.pattern; var pAttr = Drawing.getPatternAttr; - var patternShape = markerPattern && (pAttr(markerPattern.shape, 0, '') || pAttr(markerPattern.path, 0, '')); + var patternIndex = 0; + if (legend._inHover && d0._distinct && d0.index != null) { + patternIndex = d0.index; + } + var patternShape = + markerPattern && + (pAttr(markerPattern.shape, patternIndex, '') || pAttr(markerPattern.path, patternIndex, '')); if (patternShape) { - var patternBGColor = pAttr(markerPattern.bgcolor, 0, null); - var patternFGColor = pAttr(markerPattern.fgcolor, 0, null); + var patternBGColor = pAttr(markerPattern.bgcolor, patternIndex, null); + var patternFGColor = pAttr(markerPattern.fgcolor, patternIndex, null); var patternFGOpacity = markerPattern.fgopacity; - var patternSize = dimAttr(markerPattern.size, 8, 10); - var patternSolidity = dimAttr(markerPattern.solidity, 0.5, 1); - var patternID = 'legend-' + trace.uid; + var patternSize = legend._inHover + ? dimAttr(pAttr(markerPattern.size, patternIndex, 8), 8, 10) + : dimAttr(markerPattern.size, 8, 10); + var patternSolidity = legend._inHover + ? dimAttr(pAttr(markerPattern.solidity, patternIndex, 0.5), 0.5, 1) + : dimAttr(markerPattern.solidity, 0.5, 1); + var patternID = (legend._inHover ? 'hover-' : 'legend-') + trace.uid; + if (legend._inHover) patternID += '-' + patternIndex; p.call( Drawing.pattern, 'legend', diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index 061fd8ac4e5..752fa3bbd6d 100644 --- a/test/jasmine/tests/hover_test.js +++ b/test/jasmine/tests/hover_test.js @@ -7103,6 +7103,75 @@ describe('hovermode: (x|y)unified', function () { .then(done, done.fail); }); + it('should style bar pattern swatches from the hovered point', function (done) { + function getSwatch() { + return getHoverLabel().select('g.legendpoints path'); + } + + function getPatternId(fill) { + var match = /url\(["']?#([^"')]+)["']?\)/.exec(fill); + return match ? match[1] : null; + } + + function getPatternStroke(patternId) { + var pattern = document.getElementById(patternId); + if (!pattern) return null; + var painted = pattern.querySelector('path, circle'); + return painted ? painted.getAttribute('stroke') || painted.getAttribute('fill') : null; + } + + var patternId0; + + Plotly.newPlot( + gd, + [ + { + type: 'bar', + x: [0, 1], + y: [1, 2], + marker: { + color: ['red', 'purple'], + pattern: { shape: ['/', '\\'] } + } + } + ], + { + hovermode: 'x unified', + showlegend: false, + width: 400, + height: 400, + margin: { t: 50, b: 50, l: 50, r: 50 } + } + ) + .then(function () { + _hover(gd, { xval: 0 }); + assertLabel({ title: '0', items: ['1'] }); + + patternId0 = getPatternId(getSwatch().node().style.fill); + expect(patternId0).toBeTruthy('first bar swatch uses a pattern fill'); + expect(getPatternStroke(patternId0)).toBe('rgb(255, 0, 0)'); + }) + .then(function () { + _hover(gd, { xval: 1 }); + assertLabel({ title: '1', items: ['2'] }); + + var patternId1 = getPatternId(getSwatch().node().style.fill); + expect(patternId1).toBeTruthy('second bar swatch uses a pattern fill'); + expect(patternId1).not.toBe(patternId0); + expect(getPatternStroke(patternId1)).toBe('rgb(128, 0, 128)'); + }) + .then(function () { + return Plotly.restyle(gd, 'marker.pattern.shape', [['/', '']]); + }) + .then(function () { + _hover(gd, { xval: 1 }); + assertLabel({ title: '1', items: ['2'] }); + expect(getSwatch().node().style.fill).toBe('rgb(128, 0, 128)'); + expect(getPatternId(getSwatch().node().style.fill)).toBe(null); + }) + .then(done, done.fail); + }); + it('should style funnel symbols accordingly', function (done) { var mock = require('../../image/mocks/funnel_custom.json'); var mockCopy = Lib.extendDeep({}, mock); From f341c93a021af2f3ad354c524bc0b71b3dae97ae Mon Sep 17 00:00:00 2001 From: CenFangyu <164994318+Dmao233@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:29:57 +0000 Subject: [PATCH 2/5] Rename draftlog to pull request 1 The fork pull request number is now known. Co-authored-by: CenFangyu --- draftlogs/1_fix.md | 1 + draftlogs/XXXX_fix.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 draftlogs/1_fix.md delete mode 100644 draftlogs/XXXX_fix.md diff --git a/draftlogs/1_fix.md b/draftlogs/1_fix.md new file mode 100644 index 00000000000..1eabb634731 --- /dev/null +++ b/draftlogs/1_fix.md @@ -0,0 +1 @@ +- Fix unified hover swatch color and pattern for bar `marker.pattern` arrays [[#1](https://github.com/Dmao233/plotly.js/pull/1)] diff --git a/draftlogs/XXXX_fix.md b/draftlogs/XXXX_fix.md deleted file mode 100644 index e41c62222ff..00000000000 --- a/draftlogs/XXXX_fix.md +++ /dev/null @@ -1 +0,0 @@ -- Fix unified hover swatch color and pattern for bar `marker.pattern` arrays [[#XXXX](https://github.com/plotly/plotly.js/issues/8008)] From b5a08dd32e86a8c117ab9a8d9796b76bf4e4f718 Mon Sep 17 00:00:00 2001 From: CenFangyu <164994318+Dmao233@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:33:55 +0000 Subject: [PATCH 3/5] Rename draftlog to pull request 8039 Co-authored-by: CenFangyu --- draftlogs/1_fix.md | 1 - draftlogs/8039_fix.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 draftlogs/1_fix.md create mode 100644 draftlogs/8039_fix.md diff --git a/draftlogs/1_fix.md b/draftlogs/1_fix.md deleted file mode 100644 index 1eabb634731..00000000000 --- a/draftlogs/1_fix.md +++ /dev/null @@ -1 +0,0 @@ -- Fix unified hover swatch color and pattern for bar `marker.pattern` arrays [[#1](https://github.com/Dmao233/plotly.js/pull/1)] diff --git a/draftlogs/8039_fix.md b/draftlogs/8039_fix.md new file mode 100644 index 00000000000..348d9b65bd8 --- /dev/null +++ b/draftlogs/8039_fix.md @@ -0,0 +1 @@ +- Fix unified hover swatch color and pattern for bar `marker.pattern` arrays [[#8039](https://github.com/plotly/plotly.js/pull/8039)] From 11a0a9f92d79b4b8d69c9f2630c346dfaeff1c02 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 15 Oct 2026 15:50:12 -0600 Subject: [PATCH 4/5] Refactor for formatting, syntax --- src/components/legend/style.js | 15 +++--- test/jasmine/tests/hover_test.js | 79 ++++++++++++++++---------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/components/legend/style.js b/src/components/legend/style.js index d5ef0b5177c..c62d9310b5d 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -387,26 +387,25 @@ module.exports = function style(s, gd, legend) { var markerPattern = marker.pattern; var pAttr = Drawing.getPatternAttr; - var patternIndex = 0; + let patternIndex = 0; if (legend._inHover && d0._distinct && d0.index != null) { patternIndex = d0.index; } - var patternShape = + const patternShape = markerPattern && (pAttr(markerPattern.shape, patternIndex, '') || pAttr(markerPattern.path, patternIndex, '')); if (patternShape) { - var patternBGColor = pAttr(markerPattern.bgcolor, patternIndex, null); - var patternFGColor = pAttr(markerPattern.fgcolor, patternIndex, null); + const patternBGColor = pAttr(markerPattern.bgcolor, patternIndex, null); + const patternFGColor = pAttr(markerPattern.fgcolor, patternIndex, null); var patternFGOpacity = markerPattern.fgopacity; - var patternSize = legend._inHover + const patternSize = legend._inHover ? dimAttr(pAttr(markerPattern.size, patternIndex, 8), 8, 10) : dimAttr(markerPattern.size, 8, 10); - var patternSolidity = legend._inHover + const patternSolidity = legend._inHover ? dimAttr(pAttr(markerPattern.solidity, patternIndex, 0.5), 0.5, 1) : dimAttr(markerPattern.solidity, 0.5, 1); - var patternID = (legend._inHover ? 'hover-' : 'legend-') + trace.uid; - if (legend._inHover) patternID += '-' + patternIndex; + const patternID = legend._inHover ? `hover-${trace.uid}-${patternIndex}` : `legend-${trace.uid}`; p.call( Drawing.pattern, 'legend', diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index 556caa7630c..597f4224959 100644 --- a/test/jasmine/tests/hover_test.js +++ b/test/jasmine/tests/hover_test.js @@ -7141,26 +7141,24 @@ describe('hovermode: (x|y)unified', function () { .then(done, done.fail); }); - it('should style bar pattern swatches from the hovered point', function (done) { - function getSwatch() { - return getHoverLabel().select('g.legendpoints path'); - } + it('should style bar pattern swatches from the hovered point', async () => { + const getSwatch = () => getHoverLabel().select('g.legendpoints path'); - function getPatternId(fill) { - var match = /url\(["']?#([^"')]+)["']?\)/.exec(fill); + const getPatternId = (fill) => { + const match = /url\(["']?#([^"')]+)["']?\)/.exec(fill); return match ? match[1] : null; - } + }; - function getPatternStroke(patternId) { - var pattern = document.getElementById(patternId); + const getPatternStroke = (patternId) => { + const pattern = document.getElementById(patternId); if (!pattern) return null; - var painted = pattern.querySelector('path, circle'); + const painted = pattern.querySelector('path, circle'); return painted ? painted.getAttribute('stroke') || painted.getAttribute('fill') : null; - } + }; - var patternId0; + const getSwatchStroke = () => getPatternStroke(getPatternId(getSwatch().node().style.fill)); - Plotly.newPlot( + await Plotly.newPlot( gd, [ { @@ -7180,34 +7178,37 @@ describe('hovermode: (x|y)unified', function () { height: 400, margin: { t: 50, b: 50, l: 50, r: 50 } } - ) - .then(function () { - _hover(gd, { xval: 0 }); - assertLabel({ title: '0', items: ['1'] }); + ); - patternId0 = getPatternId(getSwatch().node().style.fill); - expect(patternId0).toBeTruthy('first bar swatch uses a pattern fill'); - expect(getPatternStroke(patternId0)).toBe('rgb(255, 0, 0)'); - }) - .then(function () { - _hover(gd, { xval: 1 }); - assertLabel({ title: '1', items: ['2'] }); + _hover(gd, { xval: 0 }); + assertLabel({ title: '0', items: ['1'] }); - var patternId1 = getPatternId(getSwatch().node().style.fill); - expect(patternId1).toBeTruthy('second bar swatch uses a pattern fill'); - expect(patternId1).not.toBe(patternId0); - expect(getPatternStroke(patternId1)).toBe('rgb(128, 0, 128)'); - }) - .then(function () { - return Plotly.restyle(gd, 'marker.pattern.shape', [['/', '']]); - }) - .then(function () { - _hover(gd, { xval: 1 }); - assertLabel({ title: '1', items: ['2'] }); - expect(getSwatch().node().style.fill).toBe('rgb(128, 0, 128)'); - expect(getPatternId(getSwatch().node().style.fill)).toBe(null); - }) - .then(done, done.fail); + const patternId0 = getPatternId(getSwatch().node().style.fill); + expect(patternId0).toBeTruthy('first bar swatch uses a pattern fill'); + expect(getPatternStroke(patternId0)).toBe('rgb(255, 0, 0)'); + + _hover(gd, { xval: 1 }); + assertLabel({ title: '1', items: ['2'] }); + + const patternId1 = getPatternId(getSwatch().node().style.fill); + expect(patternId1).toBeTruthy('second bar swatch uses a pattern fill'); + expect(patternId1).not.toBe(patternId0); + expect(getPatternStroke(patternId1)).toBe('rgb(128, 0, 128)'); + + await Plotly.restyle(gd, 'marker.pattern.fgcolor', [['blue', 'green']]); + + _hover(gd, { xval: 0 }); + expect(getSwatchStroke()).toBe('rgb(0, 0, 255)'); + + _hover(gd, { xval: 1 }); + expect(getSwatchStroke()).toBe('rgb(0, 128, 0)'); + + await Plotly.restyle(gd, 'marker.pattern.shape', [['/', '']]); + + _hover(gd, { xval: 1 }); + assertLabel({ title: '1', items: ['2'] }); + expect(getSwatch().node().style.fill).toBe('rgb(128, 0, 128)'); + expect(getPatternId(getSwatch().node().style.fill)).toBe(null); }); it('should style funnel symbols accordingly', function (done) { From e8c6dd00676e6cb13ed0929f3cbbb650aa7bbba9 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 15 Oct 2026 15:50:57 -0600 Subject: [PATCH 5/5] Update draftlog --- draftlogs/8039_fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/draftlogs/8039_fix.md b/draftlogs/8039_fix.md index 348d9b65bd8..d5ba53c2088 100644 --- a/draftlogs/8039_fix.md +++ b/draftlogs/8039_fix.md @@ -1 +1 @@ -- Fix unified hover swatch color and pattern for bar `marker.pattern` arrays [[#8039](https://github.com/plotly/plotly.js/pull/8039)] +- Fix unified hover swatch color and pattern for bar `marker.pattern` arrays [[#8039](https://github.com/plotly/plotly.js/pull/8039)], with thanks to @Dmao233 for the contribution!