Skip to content

Commit 089fa73

Browse files
committed
Add legend title click toggle setup and event handling
1 parent cdb570a commit 089fa73

1 file changed

Lines changed: 130 additions & 3 deletions

File tree

src/components/legend/draw.js

Lines changed: 130 additions & 3 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 handleClick = require('./handle_click').handleClick;
14+
var handleTitleClick = require('./handle_click').handleTitleClick;
1415

1516
var constants = require('./constants');
1617
var alignmentConstants = require('../../constants/alignment');
@@ -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+
var legendItem = d[0];
209+
var trace = legendItem.trace;
210+
211+
// Toggle opacity of legend group titles if all items in the group are hidden
212+
if(legendItem.groupTitle) {
213+
var groupName = trace.legendgroup;
214+
var shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; });
215+
var 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,7 +229,12 @@ 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,
@@ -221,6 +248,20 @@ function drawOne(gd, opts) {
221248
// re-calculate title position after legend width is derived. To allow for horizontal alignment
222249
if(title.text) {
223250
horizontalAlignTitle(titleEl, legendObj, bw);
251+
252+
// Position click target for the title after dimensions are computed
253+
if(!inHover && (legendObj.titleclick || legendObj.titledoubleclick)) {
254+
positionTitleToggle(scrollBox, legendObj, legendId);
255+
}
256+
257+
// Toggle opacity of legend titles if all items in the legend are hidden
258+
var shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; });
259+
var anyVisible = gd._fullData.concat(shapes).some(function(item) {
260+
var inThisLegend = (item.legend || 'legend') === legendId;
261+
return inThisLegend && item.visible === true;
262+
});
263+
264+
titleEl.style('opacity', anyVisible ? 1 : 0.5);
224265
}
225266

226267
if(!inHover) {
@@ -624,6 +665,92 @@ function setupTraceToggle(g, gd, legendId) {
624665
});
625666
}
626667

668+
function setupTitleToggle(scrollBox, gd, legendObj, legendId) {
669+
// For now, skip title click for legends containing pie-like traces
670+
var hasPie = gd._fullData.some(function(trace) {
671+
var legend = trace.legend || 'legend';
672+
var inThisLegend = Array.isArray(legend) ? legend.includes(legendId) : legend === legendId;
673+
return inThisLegend && Registry.traceIs(trace, 'pie-like');
674+
});
675+
if(hasPie) return;
676+
677+
var doubleClickDelay = gd._context.doubleClickDelay;
678+
var newMouseDownTime;
679+
var numClicks = 1;
680+
681+
var titleToggle = Lib.ensureSingle(scrollBox, 'rect', legendId + 'titletoggle', function(s) {
682+
if(!gd._context.staticPlot) {
683+
s.style('cursor', 'pointer').attr('pointer-events', 'all');
684+
}
685+
s.call(Color.fill, 'rgba(0,0,0,0)');
686+
});
687+
688+
if(gd._context.staticPlot) return;
689+
690+
titleToggle.on('mousedown', function() {
691+
newMouseDownTime = (new Date()).getTime();
692+
if(newMouseDownTime - gd._legendMouseDownTime < doubleClickDelay) {
693+
// in a click train
694+
numClicks += 1;
695+
} else {
696+
// new click train
697+
numClicks = 1;
698+
gd._legendMouseDownTime = newMouseDownTime;
699+
}
700+
});
701+
titleToggle.on('mouseup', function() {
702+
if(gd._dragged || gd._editing) return;
703+
704+
if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) {
705+
numClicks = Math.max(numClicks - 1, 1);
706+
}
707+
708+
var evtData = {
709+
event: d3.event,
710+
legendId: legendId,
711+
data: gd.data,
712+
layout: gd.layout,
713+
fullData: gd._fullData,
714+
fullLayout: gd._fullLayout
715+
};
716+
717+
if(numClicks === 1 && legendObj.titleclick) {
718+
var clickVal = Events.triggerHandler(gd, 'plotly_legendtitleclick', evtData);
719+
if(clickVal === false) return;
720+
721+
legendObj._titleClickTimeout = setTimeout(function() {
722+
if(gd._fullLayout) handleTitleClick(gd, legendObj, legendObj.titleclick);
723+
}, doubleClickDelay);
724+
} else if(numClicks === 2) {
725+
if(legendObj._titleClickTimeout) clearTimeout(legendObj._titleClickTimeout);
726+
gd._legendMouseDownTime = 0;
727+
728+
var dblClickVal = Events.triggerHandler(gd, 'plotly_legendtitledoubleclick', evtData);
729+
if(dblClickVal !== false && legendObj.titledoubleclick) handleTitleClick(gd, legendObj, legendObj.titledoubleclick);
730+
}
731+
});
732+
}
733+
734+
function positionTitleToggle(scrollBox, legendObj, legendId) {
735+
var titleToggle = scrollBox.select('.' + legendId + 'titletoggle');
736+
if(!titleToggle.size()) return;
737+
738+
var side = legendObj.title.side || 'top';
739+
var bw = legendObj.borderwidth;
740+
var x = bw;
741+
var width = legendObj._titleWidth + 2 * constants.titlePad;
742+
var height = legendObj._titleHeight + 2 * constants.titlePad;
743+
744+
745+
if(side === 'top center') {
746+
x = bw + 0.5 * (legendObj._width - 2 * bw - width);
747+
} else if(side === 'top right') {
748+
x = legendObj._width - bw - width;
749+
}
750+
751+
titleToggle.attr({ x: x, y: bw, width: width, height: height });
752+
}
753+
627754
function textLayout(s, g, gd, legendObj, aTitle) {
628755
if(legendObj._inHover) s.attr('data-notex', true); // do not process MathJax for unified hover
629756
svgTextUtils.convertToTspans(s, gd, function() {

0 commit comments

Comments
 (0)