-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Allow toggling legend to show just 1 series (or group) by double clicking #1432
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
Changes from 4 commits
afeaf5e
895ba89
afcf4f4
c5a2ff8
fb37088
1fe3b08
edc30ac
a927b9f
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 |
---|---|---|
|
@@ -21,18 +21,23 @@ var Color = require('../color'); | |
var svgTextUtils = require('../../lib/svg_text_utils'); | ||
|
||
var constants = require('./constants'); | ||
var interactConstants = require('../../constants/interactions'); | ||
var getLegendData = require('./get_legend_data'); | ||
var style = require('./style'); | ||
var helpers = require('./helpers'); | ||
var anchorUtils = require('./anchor_utils'); | ||
|
||
var SHOWISOLATETIP = true; | ||
var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY; | ||
|
||
module.exports = function draw(gd) { | ||
var fullLayout = gd._fullLayout; | ||
var clipId = 'legend' + fullLayout._uid; | ||
|
||
if(!fullLayout._infolayer || !gd.calcdata) return; | ||
|
||
if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0; | ||
|
||
var opts = fullLayout.legend, | ||
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts), | ||
hiddenSlices = fullLayout.hiddenlabels || []; | ||
|
@@ -325,9 +330,28 @@ module.exports = function draw(gd) { | |
xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor); | ||
yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor); | ||
}, | ||
doneFn: function(dragged) { | ||
doneFn: function(dragged, numClicks, e) { | ||
if(dragged && xf !== undefined && yf !== undefined) { | ||
Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf}); | ||
} else { | ||
var traces = [], | ||
clickedTrace; | ||
traces = fullLayout._infolayer.selectAll('g.traces').filter(function() { | ||
var bbox = this.getBoundingClientRect(); | ||
return (e.clientX >= bbox.left && e.clientX <= bbox.right && | ||
e.clientY >= bbox.top && e.clientY <= bbox.bottom); | ||
})[0]; | ||
if(traces.length > 0) { | ||
clickedTrace = d3.select(traces[0]); | ||
if(numClicks === 1) { | ||
legend._clickTimeout = setTimeout(function() { handleClick(clickedTrace, gd, numClicks); }, DBLCLICKDELAY); | ||
} else if(numClicks === 2) { | ||
if(legend._clickTimeout) { | ||
clearTimeout(legend._clickTimeout); | ||
} | ||
handleClick(clickedTrace, gd, numClicks); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
@@ -395,9 +419,8 @@ function drawTexts(g, gd) { | |
} | ||
|
||
function setupTraceToggle(g, gd) { | ||
var hiddenSlices = gd._fullLayout.hiddenlabels ? | ||
gd._fullLayout.hiddenlabels.slice() : | ||
[]; | ||
var newMouseDownTime, | ||
numClicks = 1; | ||
|
||
var traceToggle = g.selectAll('rect') | ||
.data([0]); | ||
|
@@ -408,41 +431,119 @@ function setupTraceToggle(g, gd) { | |
.attr('pointer-events', 'all') | ||
.call(Color.fill, 'rgba(0,0,0,0)'); | ||
|
||
traceToggle.on('click', function() { | ||
if(gd._dragged) return; | ||
|
||
var legendItem = g.data()[0][0], | ||
fullData = gd._fullData, | ||
trace = legendItem.trace, | ||
legendgroup = trace.legendgroup, | ||
traceIndicesInGroup = [], | ||
tracei, | ||
newVisible; | ||
traceToggle.on('mousedown', function() { | ||
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. I wonder if we could reuse the dragelement abstraction here? This is a non- ⛔ comment of course. 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. Possibly - but this reminds me that we had better ensure that this change is compatible with the additional interactions the legend supports in
@rpaskowitz you can convert the plot you're testing to editable by calling: Plotly.newPlot(gd, gd.data, gd.layout, {editable: true}) |
||
newMouseDownTime = (new Date()).getTime(); | ||
if(newMouseDownTime - gd._legendMouseDownTime < DBLCLICKDELAY) { | ||
// in a click train | ||
numClicks += 1; | ||
} | ||
else { | ||
// new click train | ||
numClicks = 1; | ||
gd._legendMouseDownTime = newMouseDownTime; | ||
} | ||
}); | ||
traceToggle.on('mouseup', function() { | ||
if(gd._dragged || gd._editing) return; | ||
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. I'd like to get @etpinard 's take on this, but my feeling is we don't want to introduce another Also it's not clear to me that the right course of action is to quit the new handler if we're editing something else. I wonder if it would work to instead make a helper to ensure the edit box's blur handler gets called as it should? Honestly I don't understand why that's not already happening, but what if we just made a helper (as an export of
If what the user did was clicked on the editable item accidentally, then wanted to move on to the interaction they really intended, quitting here will just add an extra click for them, and I guess there could be weird side-effects if you type something in an edit box, then go click somewhere else, such that before the new handler can execute the plot has to redraw. The most extreme case I think would be if you have the legend off the right side, then you edit some legend text and make it longer, and we auto-expand the plot margin to accommodate it. That results in a very aggressive redraw that might actually delete and recreate the whole rest of the plot (not very d3-idiomatic, I know... we're working on it!). Would need a bit of playing around to see if that would cause any problems... 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. I'll admit that blocking other interactions while the edit to go on was more of a secondary benefit to introducing this - the primary motivation was that it served as a way to prevent the When the traceToggle use to use If there's a suggestion on how to prevent both actions from firing, I can remove this flag and leave the question on how interactions should be dealt with generally to another ticket. 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. @alexcjohnson made some valid points above. But as With regards to adding another 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. Good call @etpinard - we'll dig into this a little more with #1437 but @rpaskowitz lets keep it as is for this PR. |
||
var legend = gd._fullLayout.legend; | ||
|
||
if(Registry.traceIs(trace, 'pie')) { | ||
var thisLabel = legendItem.label, | ||
thisLabelIndex = hiddenSlices.indexOf(thisLabel); | ||
if((new Date()).getTime() - gd._legendMouseDownTime > DBLCLICKDELAY) { | ||
numClicks = Math.max(numClicks - 1, 1); | ||
} | ||
|
||
if(numClicks === 1) { | ||
legend._clickTimeout = setTimeout(function() { handleClick(g, gd, numClicks); }, DBLCLICKDELAY); | ||
} else if(numClicks === 2) { | ||
if(legend._clickTimeout) { | ||
clearTimeout(legend._clickTimeout); | ||
} | ||
handleClick(g, gd, numClicks); | ||
} | ||
}); | ||
} | ||
|
||
function handleClick(g, gd, numClicks) { | ||
if(gd._dragged || gd._editing) return; | ||
var hiddenSlices = gd._fullLayout.hiddenlabels ? | ||
gd._fullLayout.hiddenlabels.slice() : | ||
[]; | ||
|
||
var legendItem = g.data()[0][0], | ||
fullData = gd._fullData, | ||
trace = legendItem.trace, | ||
legendgroup = trace.legendgroup, | ||
traceIndicesInGroup = [], | ||
tracei, | ||
newVisible; | ||
|
||
|
||
if(numClicks === 1 && SHOWISOLATETIP && gd.data && gd._context.showTips) { | ||
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. We don't need |
||
Lib.notifier('Double click on legend to isolate individual trace', 'long'); | ||
SHOWISOLATETIP = false; | ||
} else { | ||
SHOWISOLATETIP = false; | ||
} | ||
if(Registry.traceIs(trace, 'pie')) { | ||
var thisLabel = legendItem.label, | ||
thisLabelIndex = hiddenSlices.indexOf(thisLabel); | ||
|
||
if(numClicks === 1) { | ||
if(thisLabelIndex === -1) hiddenSlices.push(thisLabel); | ||
else hiddenSlices.splice(thisLabelIndex, 1); | ||
} else if(numClicks === 2) { | ||
hiddenSlices = []; | ||
gd.calcdata[0].forEach(function(d) { | ||
if(thisLabel !== d.label) { | ||
hiddenSlices.push(d.label); | ||
} | ||
}); | ||
if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) { | ||
hiddenSlices = []; | ||
} | ||
} | ||
|
||
Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); | ||
} else { | ||
var allTraces = [], | ||
traceVisibility = [], | ||
i; | ||
|
||
Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); | ||
for(i = 0; i < fullData.length; i++) { | ||
allTraces.push(i); | ||
traceVisibility.push('legendonly'); | ||
} | ||
|
||
if(legendgroup === '') { | ||
traceIndicesInGroup = [trace.index]; | ||
traceVisibility[trace.index] = true; | ||
} else { | ||
if(legendgroup === '') { | ||
traceIndicesInGroup = [trace.index]; | ||
} else { | ||
for(var i = 0; i < fullData.length; i++) { | ||
tracei = fullData[i]; | ||
if(tracei.legendgroup === legendgroup) { | ||
traceIndicesInGroup.push(tracei.index); | ||
} | ||
for(i = 0; i < fullData.length; i++) { | ||
tracei = fullData[i]; | ||
if(tracei.legendgroup === legendgroup) { | ||
traceIndicesInGroup.push(tracei.index); | ||
traceVisibility[allTraces.indexOf(i)] = true; | ||
} | ||
} | ||
} | ||
|
||
if(numClicks === 1) { | ||
newVisible = trace.visible === true ? 'legendonly' : true; | ||
Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup); | ||
} else if(numClicks === 2) { | ||
var sameAsLast = true; | ||
for(i = 0; i < fullData.length; i++) { | ||
if(fullData[i].visible !== traceVisibility[i]) { | ||
sameAsLast = false; | ||
break; | ||
} | ||
} | ||
if(sameAsLast) { | ||
traceVisibility = true; | ||
} | ||
Plotly.restyle(gd, 'visible', traceVisibility, allTraces); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
function computeTextDimensions(g, gd) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-blocking, but I find it confusing to dig into d3's nested arrays directly. Preferable to leave
traces
as a selection, then usetraces.size()
instead oftraces[0].length
, and then you don't need to re-select it to makeclickedTrace
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing.