Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3126228

Browse files
authored
Merge pull request #7698 from alexshoe/clickable-legend-titles
Clickable Legend Titles
2 parents 398983d + dca2e38 commit 3126228

File tree

8 files changed

+545
-36
lines changed

8 files changed

+545
-36
lines changed

src/components/legend/attributes.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,32 @@ module.exports = {
176176
'*togglegroup* toggles the visibility of all items in the same legendgroup as the item clicked on the graph.'
177177
].join(' ')
178178
},
179+
titleclick: {
180+
valType: 'enumerated',
181+
values: ['toggle', 'toggleothers', false],
182+
editType: 'legend',
183+
description: [
184+
'Determines the behavior on legend title click.',
185+
'*toggle* toggles the visibility of all items in the legend.',
186+
'*toggleothers* toggles the visibility of all other legends.',
187+
'*false* disables legend title click interactions.',
188+
'Defaults to *toggle* when there are multiple legends, *false* otherwise.',
189+
'Not supported for legends containing pie and pie-like traces.'
190+
].join(' ')
191+
},
192+
titledoubleclick: {
193+
valType: 'enumerated',
194+
values: ['toggle', 'toggleothers', false],
195+
editType: 'legend',
196+
description: [
197+
'Determines the behavior on legend title double-click.',
198+
'*toggle* toggles the visibility of all items in the legend.',
199+
'*toggleothers* toggles the visibility of all other legends.',
200+
'*false* disables legend title double-click interactions.',
201+
'Defaults to *toggleothers* when there are multiple legends, *false* otherwise.',
202+
'Not supported for legends containing pie and pie-like traces.'
203+
].join(' ')
204+
},
179205
x: {
180206
valType: 'number',
181207
editType: 'legend',

src/components/legend/defaults.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ var attributes = require('./attributes');
99
var basePlotLayoutAttributes = require('../../plots/layout_attributes');
1010
var helpers = require('./helpers');
1111

12-
function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
12+
function groupDefaults(legendId, layoutIn, layoutOut, fullData, legendCount) {
1313
var containerIn = layoutIn[legendId] || {};
1414
var containerOut = Template.newContainer(layoutOut, legendId);
1515

@@ -238,6 +238,10 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
238238
});
239239

240240
Lib.coerceFont(coerce, 'title.font', dfltTitleFont);
241+
242+
const hasMultipleLegends = legendCount > 1;
243+
coerce('titleclick', hasMultipleLegends ? 'toggle' : false);
244+
coerce('titledoubleclick', hasMultipleLegends ? 'toggleothers' : false);
241245
}
242246
}
243247

@@ -277,7 +281,7 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
277281
for(i = 0; i < legends.length; i++) {
278282
var legendId = legends[i];
279283

280-
groupDefaults(legendId, layoutIn, layoutOut, allLegendsData);
284+
groupDefaults(legendId, layoutIn, layoutOut, allLegendsData, legends.length);
281285

282286
if(layoutOut[legendId]) {
283287
layoutOut[legendId]._id = legendId;

src/components/legend/draw.js

Lines changed: 144 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ var dragElement = require('../dragelement');
1010
var Drawing = require('../drawing');
1111
var Color = require('../color');
1212
var svgTextUtils = require('../../lib/svg_text_utils');
13-
var handleClick = require('./handle_click');
13+
var handleItemClick = require('./handle_click').handleItemClick;
14+
var handleTitleClick = require('./handle_click').handleTitleClick;
1415

1516
var constants = require('./constants');
1617
var alignmentConstants = require('../../constants/alignment');
@@ -82,7 +83,7 @@ function drawOne(gd, opts) {
8283
var legendObj = opts || {};
8384

8485
var fullLayout = gd._fullLayout;
85-
var legendId = getId(legendObj);
86+
var legendId = helpers.getId(legendObj);
8687

8788
var clipId, layer;
8889

@@ -180,8 +181,14 @@ function drawOne(gd, opts) {
180181
.text(title.text);
181182

182183
textLayout(titleEl, scrollBox, gd, legendObj, MAIN_TITLE); // handle mathjax or multi-line text and compute title height
184+
185+
// Set up title click if enabled and not in hover mode
186+
if(!inHover && (legendObj.titleclick || legendObj.titledoubleclick)) {
187+
setupTitleToggle(scrollBox, gd, legendObj, legendId);
188+
}
183189
} else {
184190
scrollBox.selectAll('.' + legendId + 'titletext').remove();
191+
scrollBox.selectAll('.' + legendId + 'titletoggle').remove();
185192
}
186193

187194
var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
@@ -198,7 +205,22 @@ function drawOne(gd, opts) {
198205
traces.exit().remove();
199206

200207
traces.style('opacity', function(d) {
201-
var trace = d[0].trace;
208+
const legendItem = d[0];
209+
const trace = legendItem.trace;
210+
211+
// Toggle opacity of legend group titles if all items in the group are hidden
212+
if(legendItem.groupTitle) {
213+
const groupName = trace.legendgroup;
214+
const shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; });
215+
const anyVisible = gd._fullData.concat(shapes).some(function(item) {
216+
return item.legendgroup === groupName &&
217+
(item.legend || 'legend') === legendId &&
218+
item.visible === true;
219+
});
220+
221+
return anyVisible ? 1 : 0.5;
222+
}
223+
202224
if(Registry.traceIs(trace, 'pie-like')) {
203225
return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
204226
} else {
@@ -207,20 +229,34 @@ function drawOne(gd, opts) {
207229
})
208230
.each(function() { d3.select(this).call(drawTexts, gd, legendObj); })
209231
.call(style, gd, legendObj)
210-
.each(function() { if(!inHover) d3.select(this).call(setupTraceToggle, gd, legendId); });
232+
.each(function(d) {
233+
if(inHover) return;
234+
// Don't create a click targets for group titles when groupclick is 'toggleitem'
235+
if(d[0].groupTitle && legendObj.groupclick === 'toggleitem') return;
236+
d3.select(this).call(setupTraceToggle, gd, legendId);
237+
});
211238

212239
Lib.syncOrAsync([
213240
Plots.previousPromises,
214-
function() { return computeLegendDimensions(gd, groups, traces, legendObj); },
241+
function() { return computeLegendDimensions(gd, groups, traces, legendObj, scrollBox); },
215242
function() {
216243
var gs = fullLayout._size;
217244
var bw = legendObj.borderwidth;
218245
var isPaperX = legendObj.xref === 'paper';
219246
var isPaperY = legendObj.yref === 'paper';
220247

221-
// re-calculate title position after legend width is derived. To allow for horizontal alignment
222248
if(title.text) {
223-
horizontalAlignTitle(titleEl, legendObj, bw);
249+
// Toggle opacity of legend titles if all items in the legend are hidden
250+
const shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; });
251+
const anyVisible = gd._fullData.concat(shapes).some(function(item) {
252+
const legendAttr = item.legend || 'legend';
253+
var inThisLegend = Array.isArray(legendAttr)
254+
? legendAttr.includes(legendId)
255+
: legendAttr === legendId;
256+
return inThisLegend && item.visible === true;
257+
});
258+
259+
titleEl.style('opacity', anyVisible ? 1 : 0.5);
224260
}
225261

226262
if(!inHover) {
@@ -458,7 +494,7 @@ function drawOne(gd, opts) {
458494
);
459495
});
460496
if(clickedTrace.size() > 0) {
461-
clickOrDoubleClick(gd, legend, clickedTrace, numClicks, e);
497+
clickOrDoubleClick(gd, legendObj, clickedTrace, numClicks, e);
462498
}
463499
}
464500
});
@@ -479,7 +515,12 @@ function getTraceWidth(d, legendObj, textGap) {
479515
}
480516

481517
function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
518+
var fullLayout = gd._fullLayout;
482519
var trace = legendItem.data()[0][0].trace;
520+
521+
var itemClick = legend.itemclick;
522+
var itemDoubleClick = legend.itemdoubleclick;
523+
483524
var evtData = {
484525
event: evt,
485526
node: legendItem.node(),
@@ -490,7 +531,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
490531
frames: gd._transitionData._frames,
491532
config: gd._context,
492533
fullData: gd._fullData,
493-
fullLayout: gd._fullLayout
534+
fullLayout: fullLayout
494535
};
495536

496537
if(trace._group) {
@@ -504,20 +545,22 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
504545
if(clickVal === false) return;
505546
legend._clickTimeout = setTimeout(function() {
506547
if(!gd._fullLayout) return;
507-
handleClick(legendItem, gd, numClicks);
548+
if(itemClick) handleItemClick(legendItem, gd, legend, itemClick);
508549
}, gd._context.doubleClickDelay);
509550
} else if(numClicks === 2) {
510551
if(legend._clickTimeout) clearTimeout(legend._clickTimeout);
511552
gd._legendMouseDownTime = 0;
512553

513554
var dblClickVal = Events.triggerHandler(gd, 'plotly_legenddoubleclick', evtData);
514555
// Activate default double click behaviour only when both single click and double click values are not false
515-
if(dblClickVal !== false && clickVal !== false) handleClick(legendItem, gd, numClicks);
556+
if(dblClickVal !== false && clickVal !== false && itemDoubleClick) {
557+
handleItemClick(legendItem, gd, legend, itemDoubleClick);
558+
}
516559
}
517560
}
518561

519562
function drawTexts(g, gd, legendObj) {
520-
var legendId = getId(legendObj);
563+
var legendId = helpers.getId(legendObj);
521564
var legendItem = g.data()[0][0];
522565
var trace = legendItem.trace;
523566
var isPieLike = Registry.traceIs(trace, 'pie-like');
@@ -624,6 +667,73 @@ function setupTraceToggle(g, gd, legendId) {
624667
});
625668
}
626669

670+
function setupTitleToggle(scrollBox, gd, legendObj, legendId) {
671+
// For now, skip title click for legends containing pie-like traces
672+
const hasPie = gd._fullData.some(function(trace) {
673+
const legend = trace.legend || 'legend';
674+
const inThisLegend = Array.isArray(legend) ? legend.includes(legendId) : legend === legendId;
675+
return inThisLegend && Registry.traceIs(trace, 'pie-like');
676+
});
677+
if(hasPie) return;
678+
679+
const doubleClickDelay = gd._context.doubleClickDelay;
680+
var newMouseDownTime;
681+
var numClicks = 1;
682+
683+
const titleToggle = Lib.ensureSingle(scrollBox, 'rect', legendId + 'titletoggle', function(s) {
684+
if(!gd._context.staticPlot) {
685+
s.style('cursor', 'pointer').attr('pointer-events', 'all');
686+
}
687+
s.call(Color.fill, 'rgba(0,0,0,0)');
688+
});
689+
690+
if(gd._context.staticPlot) return;
691+
692+
titleToggle.on('mousedown', function() {
693+
newMouseDownTime = (new Date()).getTime();
694+
if(newMouseDownTime - gd._legendMouseDownTime < doubleClickDelay) {
695+
// in a click train
696+
numClicks += 1;
697+
} else {
698+
// new click train
699+
numClicks = 1;
700+
gd._legendMouseDownTime = newMouseDownTime;
701+
}
702+
});
703+
titleToggle.on('mouseup', function() {
704+
if(gd._dragged || gd._editing) return;
705+
706+
if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) {
707+
numClicks = Math.max(numClicks - 1, 1);
708+
}
709+
710+
const evtData = {
711+
event: d3.event,
712+
legendId: legendId,
713+
data: gd.data,
714+
layout: gd.layout,
715+
fullData: gd._fullData,
716+
fullLayout: gd._fullLayout
717+
};
718+
719+
if(numClicks === 1 && legendObj.titleclick) {
720+
const clickVal = Events.triggerHandler(gd, 'plotly_legendtitleclick', evtData);
721+
if(clickVal === false) return;
722+
723+
legendObj._titleClickTimeout = setTimeout(function() {
724+
if(gd._fullLayout) handleTitleClick(gd, legendObj, legendObj.titleclick);
725+
}, doubleClickDelay);
726+
} else if(numClicks === 2) {
727+
if(legendObj._titleClickTimeout) clearTimeout(legendObj._titleClickTimeout);
728+
gd._legendMouseDownTime = 0;
729+
730+
const dblClickVal = Events.triggerHandler(gd, 'plotly_legendtitledoubleclick', evtData);
731+
if(dblClickVal !== false && legendObj.titledoubleclick) handleTitleClick(gd, legendObj, legendObj.titledoubleclick);
732+
}
733+
});
734+
}
735+
736+
627737
function textLayout(s, g, gd, legendObj, aTitle) {
628738
if(legendObj._inHover) s.attr('data-notex', true); // do not process MathJax for unified hover
629739
svgTextUtils.convertToTspans(s, gd, function() {
@@ -645,7 +755,7 @@ function computeTextDimensions(g, gd, legendObj, aTitle) {
645755
var mathjaxGroup = g.select('g[class*=math-group]');
646756
var mathjaxNode = mathjaxGroup.node();
647757

648-
var legendId = getId(legendObj);
758+
var legendId = helpers.getId(legendObj);
649759
if(!legendObj) {
650760
legendObj = gd._fullLayout[legendId];
651761
}
@@ -748,9 +858,9 @@ function getTitleSize(legendObj) {
748858
* - _width: legend width
749859
* - _maxWidth (for orientation:h only): maximum width before starting new row
750860
*/
751-
function computeLegendDimensions(gd, groups, traces, legendObj) {
861+
function computeLegendDimensions(gd, groups, traces, legendObj, scrollBox) {
752862
var fullLayout = gd._fullLayout;
753-
var legendId = getId(legendObj);
863+
var legendId = helpers.getId(legendObj);
754864
if(!legendObj) {
755865
legendObj = fullLayout[legendId];
756866
}
@@ -955,6 +1065,25 @@ function computeLegendDimensions(gd, groups, traces, legendObj) {
9551065
}
9561066
Drawing.setRect(traceToggle, 0, -h / 2, w, h);
9571067
});
1068+
1069+
// align legend title horizontally
1070+
var titleEl = scrollBox.select('.' + legendId + 'titletext');
1071+
if(titleEl.node()) {
1072+
horizontalAlignTitle(titleEl, legendObj, bw);
1073+
}
1074+
1075+
// position title click target to cover the title text, parallel to traceToggle above
1076+
var titleToggle = scrollBox.select('.' + legendId + 'titletoggle');
1077+
if(titleToggle.size() && titleEl.node()) {
1078+
var titleX = titleEl.attr('x') || 0;
1079+
var pad = constants.titlePad;
1080+
Drawing.setRect(titleToggle,
1081+
titleX - pad,
1082+
bw,
1083+
legendObj._titleWidth + 2 * pad,
1084+
legendObj._titleHeight + 2 * pad
1085+
);
1086+
}
9581087
}
9591088

9601089
function expandMargin(gd, legendId, lx, ly) {
@@ -1009,7 +1138,3 @@ function getYanchor(legendObj) {
10091138
Lib.isMiddleAnchor(legendObj) ? 'middle' :
10101139
'top';
10111140
}
1012-
1013-
function getId(legendObj) {
1014-
return legendObj._id || 'legend';
1015-
}

src/components/legend/get_legend_data.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ module.exports = function getLegendData(calcdata, opts, hasMultipleLegends) {
164164
trace: {
165165
showlegend: firstItemTrace.showlegend,
166166
legendgroup: firstItemTrace.legendgroup,
167+
legend: firstItemTrace.legend,
167168
visible: opts.groupclick === 'toggleitem' ? true : firstItemTrace.visible
168169
}
169170
});

0 commit comments

Comments
 (0)