From 41a46d4fbf3b596d460bbe6cf03497373ee8233f Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:40:14 -0400 Subject: [PATCH 1/2] fix: match scatter error bar opacity to selected markers --- draftlogs/0_fix.md | 1 + src/components/errorbars/style.js | 10 +++ src/traces/scatter/style.js | 1 + test/jasmine/tests/errorbars_test.js | 98 ++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 draftlogs/0_fix.md diff --git a/draftlogs/0_fix.md b/draftlogs/0_fix.md new file mode 100644 index 00000000000..041bed856c6 --- /dev/null +++ b/draftlogs/0_fix.md @@ -0,0 +1 @@ +- Make scatter error bars follow marker opacity during selection and deselection [[#0](https://github.com/plotly/plotly.js/pull/0)] diff --git a/src/components/errorbars/style.js b/src/components/errorbars/style.js index 2f9be42f39f..a0034d6935c 100644 --- a/src/components/errorbars/style.js +++ b/src/components/errorbars/style.js @@ -3,6 +3,8 @@ var d3 = require('@plotly/d3'); var Color = require('../color'); +const Drawing = require('../drawing'); +const subTypes = require('../../traces/scatter/subtypes'); module.exports = function style(traces) { @@ -12,14 +14,22 @@ module.exports = function style(traces) { var xObj = trace.error_x || {}; var s = d3.select(this); + let opacity = 1; + if (subTypes.hasMarkers(trace)) { + const marker = trace.marker; + const selectedOpacity = trace.selectedpoints && Drawing.makeSelectedPointStyleFns(trace).selectedOpacityFn; + opacity = selectedOpacity || ((point) => (point.mo === undefined ? marker.opacity : point.mo)); + } s.selectAll('path.yerror') + .style('opacity', opacity) .style('stroke-width', yObj.thickness + 'px') .call(Color.stroke, yObj.color); if(xObj.copy_ystyle) xObj = yObj; s.selectAll('path.xerror') + .style('opacity', opacity) .style('stroke-width', xObj.thickness + 'px') .call(Color.stroke, xObj.color); }); diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index 734ecf9f9d0..a4df60f98cf 100644 --- a/src/traces/scatter/style.js +++ b/src/traces/scatter/style.js @@ -50,6 +50,7 @@ function styleOnSelect(gd, cd, sel) { stylePoints(sel, trace, gd); styleText(sel, trace, gd); } + Registry.getComponentMethod('errorbars', 'style')(sel); } module.exports = { diff --git a/test/jasmine/tests/errorbars_test.js b/test/jasmine/tests/errorbars_test.js index 88ba7c152c6..9718a99505b 100644 --- a/test/jasmine/tests/errorbars_test.js +++ b/test/jasmine/tests/errorbars_test.js @@ -3,6 +3,8 @@ var Plotly = require('../../../lib/index'); var d3Select = require('../../strict-d3').select; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +const drag = require('../assets/drag'); +const doubleClick = require('../assets/double_click'); describe('errorbar plotting', function() { @@ -29,6 +31,102 @@ describe('errorbar plotting', function() { }); } + function checkPointOpacities(expected) { + for (const selector of ['.point', '.xerror', '.yerror']) { + const values = Array.from(gd.querySelectorAll(selector), (node) => Number(getComputedStyle(node).opacity)); + expect(values).toBeCloseToArray(expected, 5); + } + } + + it('applies marker opacity to error bars through selection and clear', async () => { + await Plotly.newPlot(gd, [{ + x: [1, 2, 3], + y: [1, 2, 3], + mode: 'markers', + marker: { opacity: 0.6 }, + error_x: { type: 'constant', value: 0.2 }, + error_y: { type: 'constant', value: 0.3 }, + selected: { marker: { opacity: 0.9 } }, + unselected: { marker: { opacity: 0.1 } } + }]); + checkPointOpacities([0.6, 0.6, 0.6]); + + await Plotly.restyle(gd, { selectedpoints: [[1]] }); + checkPointOpacities([0.1, 0.9, 0.1]); + + await Plotly.restyle(gd, { selectedpoints: [[]] }); + checkPointOpacities([0.1, 0.1, 0.1]); + + await Plotly.restyle(gd, { selectedpoints: null }); + checkPointOpacities([0.6, 0.6, 0.6]); + + await Plotly.restyle(gd, { 'marker.opacity': 0 }); + checkPointOpacities([0, 0, 0]); + }); + + it('uses point opacity arrays across missing data', async () => { + await Plotly.newPlot(gd, [{ + x: [1, null, 3], + y: [1, 2, 3], + mode: 'lines+markers', + marker: { opacity: [0.8, 0.4, 0.6] }, + error_x: { type: 'constant', value: 0.2 }, + error_y: { type: 'constant', value: 0.3 }, + selectedpoints: [2] + }]); + checkPointOpacities([0.16, 0.6]); + + await Plotly.restyle(gd, { selectedpoints: null }); + checkPointOpacities([0.8, 0.6]); + }); + + for (const dragmode of ['select', 'lasso']) { + it(`updates error bars after ${dragmode} and deselection`, async () => { + await Plotly.newPlot(gd, [{ + x: [1, 2, 3], + y: [1, 2, 3], + mode: 'markers', + selected: { marker: { opacity: 0.8 } }, + unselected: { marker: { opacity: 0.3 } }, + error_x: { type: 'constant', value: 0.2 }, + error_y: { type: 'constant', value: 0.3 } + }], { + width: 400, + height: 400, + margin: { l: 50, r: 50, t: 50, b: 50 }, + xaxis: { range: [0, 4] }, + yaxis: { range: [0, 4] }, + dragmode + }); + await drag({ + path: dragmode === 'select' + ? [[100, 300], [150, 250]] + : [[100, 300], [150, 300], [150, 250], [100, 250], [100, 300]] + }); + expect(gd.data[0].selectedpoints).toEqual([0]); + checkPointOpacities([0.8, 0.3, 0.3]); + + await doubleClick(200, 200); + checkPointOpacities([1, 1, 1]); + }); + } + + it('keeps line-only and bar error opacity independent of scatter marker styling', async () => { + await Plotly.newPlot(gd, [ + { + x: [1, 2], y: [1, 2], mode: 'lines', marker: { opacity: 0.2 }, + error_y: { type: 'constant', value: 0.2 } + }, + { + x: [1, 2], y: [1, 2], type: 'bar', marker: { opacity: 0.6 }, + error_y: { type: 'constant', value: 0.2 }, selectedpoints: [0] + } + ]); + for (const node of gd.querySelectorAll('.yerror')) { + expect(Number(getComputedStyle(node).opacity)).toBe(1); + } + }); + it('should autorange to the visible bars and remove invisible bars', function(done) { function check(xrange, yrange, xCount, yCount) { var xa = gd._fullLayout.xaxis; From a93bbceb5c5e0e79167652f99710d1402592dabc Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:42:47 -0400 Subject: [PATCH 2/2] docs: reference error bar fix in draftlog --- draftlogs/0_fix.md | 1 - draftlogs/8102_fix.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 draftlogs/0_fix.md create mode 100644 draftlogs/8102_fix.md diff --git a/draftlogs/0_fix.md b/draftlogs/0_fix.md deleted file mode 100644 index 041bed856c6..00000000000 --- a/draftlogs/0_fix.md +++ /dev/null @@ -1 +0,0 @@ -- Make scatter error bars follow marker opacity during selection and deselection [[#0](https://github.com/plotly/plotly.js/pull/0)] diff --git a/draftlogs/8102_fix.md b/draftlogs/8102_fix.md new file mode 100644 index 00000000000..a7983ece23a --- /dev/null +++ b/draftlogs/8102_fix.md @@ -0,0 +1 @@ +- Make scatter error bars follow marker opacity during selection and deselection [[#8102](https://github.com/plotly/plotly.js/pull/8102)]