-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Clickable Legend Titles #7698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clickable Legend Titles #7698
Changes from 10 commits
88a6c0d
b2ef711
cdb570a
089fa73
207910e
2f934ad
cd92fea
53073d7
7c2e878
459e229
6bec6d1
fdf1d65
0cde808
3a1336f
42c40f6
2b5d2af
6cbdb49
6f6fd91
e3a068e
28990fb
a9809d2
1fc81de
fb5b11f
f820e7f
8ee735e
e0be8c5
0223993
e7a1915
dca2e38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,8 @@ var dragElement = require('../dragelement'); | |
| var Drawing = require('../drawing'); | ||
| var Color = require('../color'); | ||
| var svgTextUtils = require('../../lib/svg_text_utils'); | ||
| var handleClick = require('./handle_click'); | ||
| var handleClick = require('./handle_click').handleClick; | ||
| var handleTitleClick = require('./handle_click').handleTitleClick; | ||
|
|
||
| var constants = require('./constants'); | ||
| var alignmentConstants = require('../../constants/alignment'); | ||
|
|
@@ -180,8 +181,14 @@ function drawOne(gd, opts) { | |
| .text(title.text); | ||
|
|
||
| textLayout(titleEl, scrollBox, gd, legendObj, MAIN_TITLE); // handle mathjax or multi-line text and compute title height | ||
|
|
||
| // Set up title click if enabled and not in hover mode | ||
| if(!inHover && (legendObj.titleclick || legendObj.titledoubleclick)) { | ||
| setupTitleToggle(scrollBox, gd, legendObj, legendId); | ||
| } | ||
| } else { | ||
| scrollBox.selectAll('.' + legendId + 'titletext').remove(); | ||
| scrollBox.selectAll('.' + legendId + 'titletoggle').remove(); | ||
| } | ||
|
|
||
| var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) { | ||
|
|
@@ -198,7 +205,22 @@ function drawOne(gd, opts) { | |
| traces.exit().remove(); | ||
|
|
||
| traces.style('opacity', function(d) { | ||
| var trace = d[0].trace; | ||
| var legendItem = d[0]; | ||
| var trace = legendItem.trace; | ||
|
|
||
| // Toggle opacity of legend group titles if all items in the group are hidden | ||
| if(legendItem.groupTitle) { | ||
| var groupName = trace.legendgroup; | ||
| var shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; }); | ||
| var anyVisible = gd._fullData.concat(shapes).some(function(item) { | ||
| return item.legendgroup === groupName && | ||
| (item.legend || 'legend') === legendId && | ||
| item.visible === true; | ||
| }); | ||
|
|
||
| return anyVisible ? 1 : 0.5; | ||
| } | ||
|
|
||
| if(Registry.traceIs(trace, 'pie-like')) { | ||
| return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1; | ||
| } else { | ||
|
|
@@ -207,7 +229,12 @@ function drawOne(gd, opts) { | |
| }) | ||
| .each(function() { d3.select(this).call(drawTexts, gd, legendObj); }) | ||
| .call(style, gd, legendObj) | ||
| .each(function() { if(!inHover) d3.select(this).call(setupTraceToggle, gd, legendId); }); | ||
| .each(function(d) { | ||
| if(inHover) return; | ||
| // Don't create a click targets for group titles when groupclick is 'toggleitem' | ||
| if(d[0].groupTitle && legendObj.groupclick === 'toggleitem') return; | ||
|
alexshoe marked this conversation as resolved.
|
||
| d3.select(this).call(setupTraceToggle, gd, legendId); | ||
| }); | ||
|
|
||
| Lib.syncOrAsync([ | ||
| Plots.previousPromises, | ||
|
|
@@ -221,6 +248,20 @@ function drawOne(gd, opts) { | |
| // re-calculate title position after legend width is derived. To allow for horizontal alignment | ||
| if(title.text) { | ||
| horizontalAlignTitle(titleEl, legendObj, bw); | ||
|
|
||
| // Position click target for the title after dimensions are computed | ||
| if(!inHover && (legendObj.titleclick || legendObj.titledoubleclick)) { | ||
| positionTitleToggle(scrollBox, legendObj, legendId); | ||
| } | ||
|
|
||
| // Toggle opacity of legend titles if all items in the legend are hidden | ||
| var shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; }); | ||
| var anyVisible = gd._fullData.concat(shapes).some(function(item) { | ||
| var inThisLegend = (item.legend || 'legend') === legendId; | ||
| return inThisLegend && item.visible === true; | ||
| }); | ||
|
|
||
| titleEl.style('opacity', anyVisible ? 1 : 0.5); | ||
| } | ||
|
|
||
| if(!inHover) { | ||
|
|
@@ -624,6 +665,92 @@ function setupTraceToggle(g, gd, legendId) { | |
| }); | ||
| } | ||
|
|
||
| function setupTitleToggle(scrollBox, gd, legendObj, legendId) { | ||
| // For now, skip title click for legends containing pie-like traces | ||
| var hasPie = gd._fullData.some(function(trace) { | ||
| var legend = trace.legend || 'legend'; | ||
| var inThisLegend = Array.isArray(legend) ? legend.includes(legendId) : legend === legendId; | ||
| return inThisLegend && Registry.traceIs(trace, 'pie-like'); | ||
| }); | ||
| if(hasPie) return; | ||
|
|
||
| var doubleClickDelay = gd._context.doubleClickDelay; | ||
| var newMouseDownTime; | ||
| var numClicks = 1; | ||
|
|
||
| var titleToggle = Lib.ensureSingle(scrollBox, 'rect', legendId + 'titletoggle', function(s) { | ||
| if(!gd._context.staticPlot) { | ||
| s.style('cursor', 'pointer').attr('pointer-events', 'all'); | ||
| } | ||
| s.call(Color.fill, 'rgba(0,0,0,0)'); | ||
| }); | ||
|
|
||
| if(gd._context.staticPlot) return; | ||
|
|
||
| titleToggle.on('mousedown', function() { | ||
| newMouseDownTime = (new Date()).getTime(); | ||
| if(newMouseDownTime - gd._legendMouseDownTime < doubleClickDelay) { | ||
| // in a click train | ||
| numClicks += 1; | ||
| } else { | ||
| // new click train | ||
| numClicks = 1; | ||
| gd._legendMouseDownTime = newMouseDownTime; | ||
| } | ||
| }); | ||
| titleToggle.on('mouseup', function() { | ||
| if(gd._dragged || gd._editing) return; | ||
|
|
||
| if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) { | ||
| numClicks = Math.max(numClicks - 1, 1); | ||
| } | ||
|
|
||
| var evtData = { | ||
| event: d3.event, | ||
| legendId: legendId, | ||
| data: gd.data, | ||
| layout: gd.layout, | ||
| fullData: gd._fullData, | ||
| fullLayout: gd._fullLayout | ||
| }; | ||
|
|
||
| if(numClicks === 1 && legendObj.titleclick) { | ||
| var clickVal = Events.triggerHandler(gd, 'plotly_legendtitleclick', evtData); | ||
| if(clickVal === false) return; | ||
|
|
||
| legendObj._titleClickTimeout = setTimeout(function() { | ||
| if(gd._fullLayout) handleTitleClick(gd, legendObj, legendObj.titleclick); | ||
| }, doubleClickDelay); | ||
| } else if(numClicks === 2) { | ||
| if(legendObj._titleClickTimeout) clearTimeout(legendObj._titleClickTimeout); | ||
| gd._legendMouseDownTime = 0; | ||
|
|
||
| var dblClickVal = Events.triggerHandler(gd, 'plotly_legendtitledoubleclick', evtData); | ||
| if(dblClickVal !== false && legendObj.titledoubleclick) handleTitleClick(gd, legendObj, legendObj.titledoubleclick); | ||
| } | ||
| }); | ||
|
Comment on lines
+719
to
+733
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alexshoe Could some of the logic in the |
||
| } | ||
|
|
||
| function positionTitleToggle(scrollBox, legendObj, legendId) { | ||
| var titleToggle = scrollBox.select('.' + legendId + 'titletoggle'); | ||
| if(!titleToggle.size()) return; | ||
|
|
||
| var side = legendObj.title.side || 'top'; | ||
| var bw = legendObj.borderwidth; | ||
| var x = bw; | ||
| var width = legendObj._titleWidth + 2 * constants.titlePad; | ||
| var height = legendObj._titleHeight + 2 * constants.titlePad; | ||
|
|
||
|
|
||
| if(side === 'top center') { | ||
| x = bw + 0.5 * (legendObj._width - 2 * bw - width); | ||
| } else if(side === 'top right') { | ||
| x = legendObj._width - bw - width; | ||
| } | ||
|
|
||
| titleToggle.attr({ x: x, y: bw, width: width, height: height }); | ||
| } | ||
|
|
||
| function textLayout(s, g, gd, legendObj, aTitle) { | ||
| if(legendObj._inHover) s.attr('data-notex', true); // do not process MathJax for unified hover | ||
| svgTextUtils.convertToTspans(s, gd, function() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,21 @@ var pushUnique = Lib.pushUnique; | |
|
|
||
| var SHOWISOLATETIP = true; | ||
|
|
||
| module.exports = function handleClick(g, gd, numClicks) { | ||
| exports.handleClick = function handleClick(g, gd, numClicks) { | ||
|
alexshoe marked this conversation as resolved.
Outdated
|
||
| var fullLayout = gd._fullLayout; | ||
|
|
||
| if(gd._dragged || gd._editing) return; | ||
|
|
||
| var itemClick = fullLayout.legend.itemclick; | ||
| var itemDoubleClick = fullLayout.legend.itemdoubleclick; | ||
| var groupClick = fullLayout.legend.groupclick; | ||
|
|
||
| var legendItem = g.data()[0][0]; | ||
| if(legendItem.groupTitle && legendItem.noClick) return; | ||
|
|
||
| var legendId = legendItem.trace.legend || 'legend'; | ||
| var legendObj = fullLayout[legendId]; | ||
|
|
||
| var itemClick = legendObj.itemclick; | ||
| var itemDoubleClick = legendObj.itemdoubleclick; | ||
| var groupClick = legendObj.groupclick; | ||
|
|
||
| if(numClicks === 1 && itemClick === 'toggle' && itemDoubleClick === 'toggleothers' && | ||
| SHOWISOLATETIP && gd.data && gd._context.showTips | ||
|
|
@@ -35,9 +42,6 @@ module.exports = function handleClick(g, gd, numClicks) { | |
| fullLayout.hiddenlabels.slice() : | ||
| []; | ||
|
|
||
| var legendItem = g.data()[0][0]; | ||
| if(legendItem.groupTitle && legendItem.noClick) return; | ||
|
|
||
| var fullData = gd._fullData; | ||
| var shapesWithLegend = (fullLayout.shapes || []).filter(function(d) { return d.showlegend; }); | ||
| var allLegendItems = fullData.concat(shapesWithLegend); | ||
|
|
@@ -269,3 +273,64 @@ module.exports = function handleClick(g, gd, numClicks) { | |
| } | ||
| } | ||
| }; | ||
|
|
||
| exports.handleTitleClick = function handleTitleClick(gd, legendObj, mode) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also similar comment here to above -- check whether you can share some of this logic with the
alexshoe marked this conversation as resolved.
|
||
| var fullLayout = gd._fullLayout; | ||
| var fullData = gd._fullData; | ||
| var legendId = legendObj._id || 'legend'; | ||
| var shapesWithLegend = (fullLayout.shapes || []).filter(function(d) { return d.showlegend; }); | ||
| var allLegendItems = fullData.concat(shapesWithLegend); | ||
|
|
||
| function isInLegend(item) { | ||
| return (item.legend || 'legend') === legendId; | ||
| } | ||
|
|
||
| var toggleThisLegend; | ||
| var toggleOtherLegends; | ||
|
|
||
| if(mode === 'toggle') { | ||
|
alexshoe marked this conversation as resolved.
|
||
| // If any item is visible in this legend, hide all. If all are hidden, show all | ||
| var anyVisibleHere = allLegendItems.some(function(item) { | ||
| return isInLegend(item) && item.visible === true; | ||
| }); | ||
|
|
||
| toggleThisLegend = !anyVisibleHere; | ||
|
alexshoe marked this conversation as resolved.
|
||
| toggleOtherLegends = null; | ||
|
alexshoe marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| // isolate this legend or set all legends to visible | ||
| var anyVisibleElsewhere = allLegendItems.some(function(item) { | ||
| return !isInLegend(item) && item.visible === true && item.showlegend !== false; | ||
| }); | ||
|
|
||
| toggleThisLegend = true; | ||
| toggleOtherLegends = !anyVisibleElsewhere; | ||
| } | ||
|
|
||
| var dataUpdate = { visible: [] }; | ||
| var dataIndices = []; | ||
| var updatedShapes = (fullLayout.shapes || []).map(function(d) { return d._input; }); | ||
| var shapesUpdated = false; | ||
|
|
||
| for(var i = 0; i < allLegendItems.length; i++) { | ||
| var item = allLegendItems[i]; | ||
| var shouldShow = isInLegend(item) ? toggleThisLegend : toggleOtherLegends; | ||
| var newVis = shouldShow ? true : 'legendonly'; | ||
|
|
||
| // Only update if the item is visible and the visibility is different from the new visibility | ||
| if ((item.visible !== false) && (shouldShow !== null) && (item.visible !== newVis)) { | ||
| if(item._isShape) { | ||
| updatedShapes[item._index].visible = newVis; | ||
| shapesUpdated = true; | ||
| } else { | ||
| dataIndices.push(item.index); | ||
| dataUpdate.visible.push(newVis); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if(shapesUpdated) { | ||
| Registry.call('_guiUpdate', gd, dataUpdate, {shapes: updatedShapes}, dataIndices); | ||
| } else if(dataIndices.length) { | ||
| Registry.call('_guiRestyle', gd, dataUpdate, dataIndices); | ||
| } | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.