-
-
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
base: master
Are you sure you want to change the base?
Clickable Legend Titles #7698
Changes from all commits
88a6c0d
b2ef711
cdb570a
089fa73
207910e
2f934ad
cd92fea
53073d7
7c2e878
459e229
6bec6d1
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; | ||
| const legendItem = d[0]; | ||
| const trace = legendItem.trace; | ||
|
|
||
| // Toggle opacity of legend group titles if all items in the group are hidden | ||
| if(legendItem.groupTitle) { | ||
| const groupName = trace.legendgroup; | ||
| const shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; }); | ||
| const 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; | ||
|
Comment on lines
+234
to
+235
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 Is this line an unrelated bugfix for groupclick? Or is it related to the titleclick functionality? (it's fine either way just want to clarify) |
||
| 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 | ||
| const shapes = (fullLayout.shapes || []).filter(function(s) { return s.showlegend; }); | ||
| const anyVisible = gd._fullData.concat(shapes).some(function(item) { | ||
| const 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 | ||
| const hasPie = gd._fullData.some(function(trace) { | ||
| const legend = trace.legend || 'legend'; | ||
| const inThisLegend = Array.isArray(legend) ? legend.includes(legendId) : legend === legendId; | ||
| return inThisLegend && Registry.traceIs(trace, 'pie-like'); | ||
| }); | ||
| if(hasPie) return; | ||
|
|
||
| const doubleClickDelay = gd._context.doubleClickDelay; | ||
| var newMouseDownTime; | ||
| var numClicks = 1; | ||
|
|
||
| const 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); | ||
| } | ||
|
|
||
| const evtData = { | ||
| event: d3.event, | ||
| legendId: legendId, | ||
| data: gd.data, | ||
| layout: gd.layout, | ||
| fullData: gd._fullData, | ||
| fullLayout: gd._fullLayout | ||
| }; | ||
|
|
||
| if(numClicks === 1 && legendObj.titleclick) { | ||
| const 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; | ||
|
|
||
| const dblClickVal = Events.triggerHandler(gd, 'plotly_legendtitledoubleclick', evtData); | ||
| if(dblClickVal !== false && legendObj.titledoubleclick) handleTitleClick(gd, legendObj, legendObj.titledoubleclick); | ||
| } | ||
| }); | ||
|
Comment on lines
+717
to
+731
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) { | ||
| const titleToggle = scrollBox.select('.' + legendId + 'titletoggle'); | ||
| if(!titleToggle.size()) return; | ||
|
|
||
| const side = legendObj.title.side || 'top'; | ||
| const bw = legendObj.borderwidth; | ||
| var x = bw; | ||
| const width = legendObj._titleWidth + 2 * constants.titlePad; | ||
| const 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 }); | ||
| } | ||
|
Comment on lines
+734
to
+752
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. Hmm, likewise this function seems like it's duplicating a lot of the title placement logic. I don't think we should be referencing parameters like |
||
|
|
||
| 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) { | ||
| 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; | ||
|
|
||
| const legendItem = g.data()[0][0]; | ||
| if(legendItem.groupTitle && legendItem.noClick) return; | ||
|
|
||
| const legendId = legendItem.trace.legend || 'legend'; | ||
|
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. If I understand correctly, this fixes a bug where 'secondary' legends (e.g. I believe that fixes this issue, can you confirm @alexshoe ? If so, please tag in PR description. |
||
| const legendObj = fullLayout[legendId]; | ||
|
|
||
| const itemClick = legendObj.itemclick; | ||
| const itemDoubleClick = legendObj.itemdoubleclick; | ||
| const 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) { | ||
| const fullLayout = gd._fullLayout; | ||
| const fullData = gd._fullData; | ||
| const legendId = legendObj._id || 'legend'; | ||
|
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. perhaps move the |
||
| const shapesWithLegend = (fullLayout.shapes || []).filter(function(d) { return d.showlegend; }); | ||
| const allLegendItems = fullData.concat(shapesWithLegend); | ||
|
|
||
| function isInLegend(item) { | ||
| return (item.legend || 'legend') === legendId; | ||
| } | ||
|
|
||
| var toggleThisLegend; | ||
| var toggleOtherLegends; | ||
|
|
||
| if(mode === 'toggle') { | ||
| // If any item is visible in this legend, hide all. If all are hidden, show all | ||
| const anyVisibleHere = allLegendItems.some(function(item) { | ||
| return isInLegend(item) && item.visible === true; | ||
| }); | ||
|
|
||
| toggleThisLegend = !anyVisibleHere; | ||
| toggleOtherLegends = null; | ||
| } else { | ||
| // isolate this legend or set all legends to visible | ||
| const anyVisibleElsewhere = allLegendItems.some(function(item) { | ||
| return !isInLegend(item) && item.visible === true && item.showlegend !== false; | ||
| }); | ||
|
|
||
| toggleThisLegend = true; | ||
| toggleOtherLegends = !anyVisibleElsewhere; | ||
| } | ||
|
|
||
| const dataUpdate = { visible: [] }; | ||
| const dataIndices = []; | ||
| const updatedShapes = (fullLayout.shapes || []).map(function(d) { return d._input; }); | ||
| var shapesUpdated = false; | ||
|
|
||
| for(var i = 0; i < allLegendItems.length; i++) { | ||
| const item = allLegendItems[i]; | ||
| const shouldShow = isInLegend(item) ? toggleThisLegend : toggleOtherLegends; | ||
| const 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.