-
-
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
fdf1d65
0cde808
3a1336f
42c40f6
2b5d2af
6cbdb49
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 handleItemClick = require('./handle_click').handleItemClick; | ||
| var handleTitleClick = require('./handle_click').handleTitleClick; | ||
|
|
||
| var constants = require('./constants'); | ||
| var alignmentConstants = require('../../constants/alignment'); | ||
|
|
@@ -82,7 +83,7 @@ function drawOne(gd, opts) { | |
| var legendObj = opts || {}; | ||
|
|
||
| var fullLayout = gd._fullLayout; | ||
| var legendId = getId(legendObj); | ||
| var legendId = helpers.getId(legendObj); | ||
|
|
||
| var clipId, layer; | ||
|
|
||
|
|
@@ -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; | ||
alexshoe marked this conversation as resolved.
Show resolved
Hide 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 | ||
| 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) { | ||
|
|
@@ -479,7 +520,14 @@ function getTraceWidth(d, legendObj, textGap) { | |
| } | ||
|
|
||
| function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) { | ||
| var fullLayout = gd._fullLayout; | ||
| var trace = legendItem.data()[0][0].trace; | ||
| var legendId = trace.legend || 'legend'; | ||
| var legendObj = fullLayout[legendId]; | ||
|
|
||
| var itemClick = legendObj.itemclick; | ||
| var itemDoubleClick = legendObj.itemdoubleclick; | ||
|
|
||
| var evtData = { | ||
| event: evt, | ||
| node: legendItem.node(), | ||
|
|
@@ -490,7 +538,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) { | |
| frames: gd._transitionData._frames, | ||
| config: gd._context, | ||
| fullData: gd._fullData, | ||
| fullLayout: gd._fullLayout | ||
| fullLayout: fullLayout | ||
| }; | ||
|
|
||
| if(trace._group) { | ||
|
|
@@ -504,20 +552,22 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) { | |
| if(clickVal === false) return; | ||
| legend._clickTimeout = setTimeout(function() { | ||
| if(!gd._fullLayout) return; | ||
| handleClick(legendItem, gd, numClicks); | ||
| if(itemClick) handleItemClick(legendItem, gd, legendObj, itemClick); | ||
| }, gd._context.doubleClickDelay); | ||
| } else if(numClicks === 2) { | ||
| if(legend._clickTimeout) clearTimeout(legend._clickTimeout); | ||
| gd._legendMouseDownTime = 0; | ||
|
|
||
| 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) handleClick(legendItem, gd, numClicks); | ||
| if(dblClickVal !== false && clickVal !== false && itemDoubleClick) { | ||
| handleItemClick(legendItem, gd, legendObj, itemDoubleClick); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function drawTexts(g, gd, legendObj) { | ||
| var legendId = getId(legendObj); | ||
| var legendId = helpers.getId(legendObj); | ||
| var legendItem = g.data()[0][0]; | ||
| var trace = legendItem.trace; | ||
| var isPieLike = Registry.traceIs(trace, 'pie-like'); | ||
|
|
@@ -624,6 +674,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
+726
to
+740
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
+743
to
+761
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() { | ||
|
|
@@ -645,7 +781,7 @@ function computeTextDimensions(g, gd, legendObj, aTitle) { | |
| var mathjaxGroup = g.select('g[class*=math-group]'); | ||
| var mathjaxNode = mathjaxGroup.node(); | ||
|
|
||
| var legendId = getId(legendObj); | ||
| var legendId = helpers.getId(legendObj); | ||
| if(!legendObj) { | ||
| legendObj = gd._fullLayout[legendId]; | ||
| } | ||
|
|
@@ -750,7 +886,7 @@ function getTitleSize(legendObj) { | |
| */ | ||
| function computeLegendDimensions(gd, groups, traces, legendObj) { | ||
| var fullLayout = gd._fullLayout; | ||
| var legendId = getId(legendObj); | ||
| var legendId = helpers.getId(legendObj); | ||
| if(!legendObj) { | ||
| legendObj = fullLayout[legendId]; | ||
| } | ||
|
|
@@ -1009,7 +1145,3 @@ function getYanchor(legendObj) { | |
| Lib.isMiddleAnchor(legendObj) ? 'middle' : | ||
| 'top'; | ||
| } | ||
|
|
||
| function getId(legendObj) { | ||
| return legendObj._id || 'legend'; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.