From 79eba45dad4235802ba47a3894854421b8b0df4e Mon Sep 17 00:00:00 2001 From: Tisankan Jeyakumar Date: Sat, 29 Aug 2026 19:27:02 +0530 Subject: [PATCH 1/2] feat: add legend.groupdoubleclick to set group behavior on double-click handleItemClick read groupclick for every action, so a double click could not differ from a single click. A figure could not keep togglegroup on a single click and isolate one trace on a double click. groupdoubleclick takes the coerced groupclick value as its default, so a figure that omits the attribute keeps the current behavior. The isolation path now honors the resolved group value, which corrects the case where groupclick is toggleitem and the double click still isolated a whole legendgroup. Closes #6812 --- src/components/legend/attributes.js | 11 ++++ src/components/legend/defaults.js | 3 +- src/components/legend/draw.js | 4 +- src/components/legend/handle_click.js | 14 +++-- src/types/generated/schema.d.ts | 2 + test/jasmine/tests/legend_test.js | 74 +++++++++++++++++++++++++++ test/plot-schema.json | 9 ++++ 7 files changed, 109 insertions(+), 8 deletions(-) diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js index 3fb8e5781e9..f8164a550a6 100644 --- a/src/components/legend/attributes.js +++ b/src/components/legend/attributes.js @@ -176,6 +176,17 @@ module.exports = { '*togglegroup* toggles the visibility of all items in the same legendgroup as the item clicked on the graph.' ].join(' ') }, + groupdoubleclick: { + valType: 'enumerated', + values: ['toggleitem', 'togglegroup'], + editType: 'legend', + description: [ + 'Determines the behavior on legend group item double-click.', + '*toggleitem* toggles the visibility of the individual item clicked on the graph.', + '*togglegroup* toggles the visibility of all items in the same legendgroup as the item clicked on the graph.', + 'Defaults to the value of `groupclick`.' + ].join(' ') + }, titleclick: { valType: 'enumerated', values: ['toggle', 'toggleothers', false], diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index eb8a6212a0f..2a65034b3bc 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -222,7 +222,8 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData, legendCount) { coerce('itemclick'); coerce('itemdoubleclick'); - coerce('groupclick'); + const groupClick = coerce('groupclick'); + coerce('groupdoubleclick', groupClick); coerce('xanchor', defaultXAnchor); coerce('yanchor', defaultYAnchor); diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 707f2a77364..7ed481e2393 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -544,7 +544,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) { if(clickVal === false) return; legend._clickTimeout = setTimeout(function() { if(!gd._fullLayout) return; - if(itemClick) handleItemClick(legendItem, gd, legend, itemClick); + if(itemClick) handleItemClick(legendItem, gd, legend, itemClick, numClicks); }, gd._context.doubleClickDelay); } else if(numClicks === 2) { if(legend._clickTimeout) clearTimeout(legend._clickTimeout); @@ -553,7 +553,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) { var dblClickVal = Events.triggerHandler(gd, 'plotly_legenddoubleclick', evtData); // Activate default double click behaviour only when both single click and double click values are not false if(dblClickVal !== false && clickVal !== false && itemDoubleClick) { - handleItemClick(legendItem, gd, legend, itemDoubleClick); + handleItemClick(legendItem, gd, legend, itemDoubleClick, numClicks); } } } diff --git a/src/components/legend/handle_click.js b/src/components/legend/handle_click.js index 6170f03bf8b..480e754a21a 100644 --- a/src/components/legend/handle_click.js +++ b/src/components/legend/handle_click.js @@ -14,10 +14,12 @@ var SHOWISOLATETIP = true; * @param {object} gd graph div * @param {object} legendObj the legend object from fullLayout * @param {string} mode toggle mode for the current action: 'toggle' | 'toggleothers' - * - 'toggle': Toggle visibility of this item (or group if groupclick is 'togglegroup') + * - 'toggle': Toggle visibility of this item (or group if the group behavior is 'togglegroup') * - 'toggleothers': Show only this item, hide all others (isolation mode) + * @param {number} numClicks 1 for a single click, 2 for a double click. Selects `groupclick` + * or `groupdoubleclick` as the group behavior. */ -exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) { +exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode, numClicks) { var fullLayout = gd._fullLayout; if (gd._dragged || gd._editing) return; @@ -25,7 +27,7 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) { var legendItem = g.data()[0][0]; if (legendItem.groupTitle && legendItem.noClick) return; - var groupClick = legendObj.groupclick; + const groupClick = numClicks === 2 ? legendObj.groupdoubleclick : legendObj.groupclick; // Show isolate tip on first single click when default behavior is active if ( @@ -196,13 +198,15 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) { // but also culls hidden traces. That means we have some work to do. var isClicked, isInGroup, notInLegend, otherState, _item; var isIsolated = true; + // 'toggleitem' isolates the clicked trace alone, so its group peers hide with the rest. + const isolateGroup = hasLegendgroup && toggleGroup; for (i = 0; i < allLegendItems.length; i++) { _item = allLegendItems[i]; isClicked = _item === fullTrace; notInLegend = _item.showlegend !== true; if (isClicked || notInLegend) continue; - isInGroup = hasLegendgroup && _item.legendgroup === legendgroup; + isInGroup = isolateGroup && _item.legendgroup === legendgroup; if ( !isInGroup && @@ -234,7 +238,7 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) { isClicked = _item === fullTrace; // N.B. consider traces that have a set legendgroup as toggleable notInLegend = _item.showlegend !== true && !_item.legendgroup; - isInGroup = isClicked || (hasLegendgroup && _item.legendgroup === legendgroup); + isInGroup = isClicked || (isolateGroup && _item.legendgroup === legendgroup); setVisibility(_item, isInGroup || notInLegend ? true : otherState); break; } diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 2c08fa0db33..2ac90f66e91 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -12727,6 +12727,8 @@ export interface Legend { * @default 'togglegroup' */ groupclick?: 'toggleitem' | 'togglegroup'; + /** Determines the behavior on legend group item double-click. *toggleitem* toggles the visibility of the individual item clicked on the graph. *togglegroup* toggles the visibility of all items in the same legendgroup as the item clicked on the graph. Defaults to the value of `groupclick`. */ + groupdoubleclick?: 'toggleitem' | 'togglegroup'; /** Sets the font for group titles in legend. Defaults to `legend.font` with its size increased about 10%. */ grouptitlefont?: Font; /** diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 97cd23f145a..bb4d8b61ac0 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -286,6 +286,28 @@ describe('legend defaults', function () { expect(layoutOut.legend.title.side).toEqual('left'); }); + it('should default `groupdoubleclick` to the `groupclick` value', function () { + fullData = allShown([{ type: 'scatter' }, { type: 'scatter' }]); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.groupdoubleclick).toBe('togglegroup'); + + layoutIn.legend = { groupclick: 'toggleitem' }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.groupdoubleclick).toBe('toggleitem'); + }); + + it('should coerce `groupdoubleclick` independently of `groupclick`', function () { + fullData = allShown([{ type: 'scatter' }, { type: 'scatter' }]); + + layoutIn.legend = { groupdoubleclick: 'toggleitem' }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.groupclick).toBe('togglegroup'); + expect(layoutOut.legend.groupdoubleclick).toBe('toggleitem'); + }); + describe('for horizontal legends', function () { var layoutInForHorizontalLegends; @@ -2482,6 +2504,58 @@ describe('legend interaction', function () { }); }); + describe('legendgroup visibility case of groupdoubleclick: "toggleitem"', function () { + beforeEach(function (done) { + Plotly.newPlot( + gd, + [ + { + x: [1, 2], + y: [3, 4], + visible: false + }, + { + x: [1, 2, 3, 4], + y: [0, 1, 2, 3], + legendgroup: 'foo' + }, + { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4] + }, + { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4], + legendgroup: 'foo' + } + ], + { + legend: { + groupdoubleclick: 'toggleitem' + } + } + ).then(done); + }); + + it('isolates the clicked item instead of its legendgroup', function (done) { + Promise.resolve() + .then(click(1, 2)) + .then(assertVisible([false, 'legendonly', 'legendonly', true])) + .then(click(1, 2)) + .then(assertVisible([false, true, true, true])) + .then(done, done.fail); + }); + + it('leaves the single click behavior to groupclick', function (done) { + Promise.resolve() + .then(click(1)) + .then(assertVisible([false, 'legendonly', true, 'legendonly'])) + .then(click(1)) + .then(assertVisible([false, true, true, true])) + .then(done, done.fail); + }); + }); + describe('legend visibility with *showlegend:false* traces', function () { beforeEach(function (done) { Plotly.newPlot(gd, [ diff --git a/test/plot-schema.json b/test/plot-schema.json index 4598de7f54c..3c8278724fe 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -3312,6 +3312,15 @@ "togglegroup" ] }, + "groupdoubleclick": { + "description": "Determines the behavior on legend group item double-click. *toggleitem* toggles the visibility of the individual item clicked on the graph. *togglegroup* toggles the visibility of all items in the same legendgroup as the item clicked on the graph. Defaults to the value of `groupclick`.", + "editType": "legend", + "valType": "enumerated", + "values": [ + "toggleitem", + "togglegroup" + ] + }, "grouptitlefont": { "color": { "editType": "legend", From 78549d2d678b53faaf1e67f1c3a43cb7cf33ccdf Mon Sep 17 00:00:00 2001 From: Tisankan Jeyakumar Date: Sat, 29 Aug 2026 19:28:38 +0530 Subject: [PATCH 2/2] docs: add draftlog for the groupdoubleclick attribute --- draftlogs/7997_add.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7997_add.md diff --git a/draftlogs/7997_add.md b/draftlogs/7997_add.md new file mode 100644 index 00000000000..d124caa320d --- /dev/null +++ b/draftlogs/7997_add.md @@ -0,0 +1 @@ +- Add `legend.groupdoubleclick` to set the group behavior for a legend double-click [[#7997](https://github.com/plotly/plotly.js/pull/7997)]