Skip to content

Commit d5303ab

Browse files
authored
Merge pull request #1432 from rpaskowitz/legend_show_only
Allow toggling legend to show just 1 series (or group) by double clicking
2 parents 4b7aa2b + a927b9f commit d5303ab

File tree

11 files changed

+354
-54
lines changed

11 files changed

+354
-54
lines changed

src/components/dragelement/index.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var Plotly = require('../../plotly');
1313
var Lib = require('../../lib');
1414

1515
var constants = require('../../plots/cartesian/constants');
16-
16+
var interactConstants = require('../../constants/interactions');
1717

1818
var dragElement = module.exports = {};
1919

@@ -37,12 +37,13 @@ dragElement.unhoverRaw = unhover.raw;
3737
* dx and dy are the net pixel offset of the drag,
3838
* dragged is true/false, has the mouse moved enough to
3939
* constitute a drag
40-
* doneFn (optional) function(dragged, numClicks)
40+
* doneFn (optional) function(dragged, numClicks, e)
4141
* executed on mouseup, or mouseout of window since
4242
* we don't get events after that
4343
* dragged is as in moveFn
4444
* numClicks is how many clicks we've registered within
4545
* a doubleclick time
46+
* e is the original event
4647
* setCursor (optional) function(event)
4748
* executed on mousemove before mousedown
4849
* the purpose of this callback is to update the mouse cursor before
@@ -51,7 +52,7 @@ dragElement.unhoverRaw = unhover.raw;
5152
dragElement.init = function init(options) {
5253
var gd = Lib.getPlotDiv(options.element) || {},
5354
numClicks = 1,
54-
DBLCLICKDELAY = constants.DBLCLICKDELAY,
55+
DBLCLICKDELAY = interactConstants.DBLCLICKDELAY,
5556
startX,
5657
startY,
5758
newMouseDownTime,
@@ -136,7 +137,7 @@ dragElement.init = function init(options) {
136137
numClicks = Math.max(numClicks - 1, 1);
137138
}
138139

139-
if(options.doneFn) options.doneFn(gd._dragged, numClicks);
140+
if(options.doneFn) options.doneFn(gd._dragged, numClicks, e);
140141

141142
if(!gd._dragged) {
142143
var e2 = document.createEvent('MouseEvents');

src/components/legend/draw.js

+126-26
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,23 @@ var Color = require('../color');
2121
var svgTextUtils = require('../../lib/svg_text_utils');
2222

2323
var constants = require('./constants');
24+
var interactConstants = require('../../constants/interactions');
2425
var getLegendData = require('./get_legend_data');
2526
var style = require('./style');
2627
var helpers = require('./helpers');
2728
var anchorUtils = require('./anchor_utils');
2829

30+
var SHOWISOLATETIP = true;
31+
var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY;
2932

3033
module.exports = function draw(gd) {
3134
var fullLayout = gd._fullLayout;
3235
var clipId = 'legend' + fullLayout._uid;
3336

3437
if(!fullLayout._infolayer || !gd.calcdata) return;
3538

39+
if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0;
40+
3641
var opts = fullLayout.legend,
3742
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts),
3843
hiddenSlices = fullLayout.hiddenlabels || [];
@@ -325,9 +330,26 @@ module.exports = function draw(gd) {
325330
xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor);
326331
yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor);
327332
},
328-
doneFn: function(dragged) {
333+
doneFn: function(dragged, numClicks, e) {
329334
if(dragged && xf !== undefined && yf !== undefined) {
330335
Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf});
336+
} else {
337+
var clickedTrace =
338+
fullLayout._infolayer.selectAll('g.traces').filter(function() {
339+
var bbox = this.getBoundingClientRect();
340+
return (e.clientX >= bbox.left && e.clientX <= bbox.right &&
341+
e.clientY >= bbox.top && e.clientY <= bbox.bottom);
342+
});
343+
if(clickedTrace.size() > 0) {
344+
if(numClicks === 1) {
345+
legend._clickTimeout = setTimeout(function() { handleClick(clickedTrace, gd, numClicks); }, DBLCLICKDELAY);
346+
} else if(numClicks === 2) {
347+
if(legend._clickTimeout) {
348+
clearTimeout(legend._clickTimeout);
349+
}
350+
handleClick(clickedTrace, gd, numClicks);
351+
}
352+
}
331353
}
332354
}
333355
});
@@ -395,9 +417,8 @@ function drawTexts(g, gd) {
395417
}
396418

397419
function setupTraceToggle(g, gd) {
398-
var hiddenSlices = gd._fullLayout.hiddenlabels ?
399-
gd._fullLayout.hiddenlabels.slice() :
400-
[];
420+
var newMouseDownTime,
421+
numClicks = 1;
401422

402423
var traceToggle = g.selectAll('rect')
403424
.data([0]);
@@ -408,41 +429,120 @@ function setupTraceToggle(g, gd) {
408429
.attr('pointer-events', 'all')
409430
.call(Color.fill, 'rgba(0,0,0,0)');
410431

411-
traceToggle.on('click', function() {
412-
if(gd._dragged) return;
413432

414-
var legendItem = g.data()[0][0],
415-
fullData = gd._fullData,
416-
trace = legendItem.trace,
417-
legendgroup = trace.legendgroup,
418-
traceIndicesInGroup = [],
419-
tracei,
420-
newVisible;
433+
traceToggle.on('mousedown', function() {
434+
newMouseDownTime = (new Date()).getTime();
435+
if(newMouseDownTime - gd._legendMouseDownTime < DBLCLICKDELAY) {
436+
// in a click train
437+
numClicks += 1;
438+
}
439+
else {
440+
// new click train
441+
numClicks = 1;
442+
gd._legendMouseDownTime = newMouseDownTime;
443+
}
444+
});
445+
traceToggle.on('mouseup', function() {
446+
if(gd._dragged || gd._editing) return;
447+
var legend = gd._fullLayout.legend;
421448

422-
if(Registry.traceIs(trace, 'pie')) {
423-
var thisLabel = legendItem.label,
424-
thisLabelIndex = hiddenSlices.indexOf(thisLabel);
449+
if((new Date()).getTime() - gd._legendMouseDownTime > DBLCLICKDELAY) {
450+
numClicks = Math.max(numClicks - 1, 1);
451+
}
452+
453+
if(numClicks === 1) {
454+
legend._clickTimeout = setTimeout(function() { handleClick(g, gd, numClicks); }, DBLCLICKDELAY);
455+
} else if(numClicks === 2) {
456+
if(legend._clickTimeout) {
457+
clearTimeout(legend._clickTimeout);
458+
}
459+
gd._legendMouseDownTime = 0;
460+
handleClick(g, gd, numClicks);
461+
}
462+
});
463+
}
464+
465+
function handleClick(g, gd, numClicks) {
466+
if(gd._dragged || gd._editing) return;
467+
var hiddenSlices = gd._fullLayout.hiddenlabels ?
468+
gd._fullLayout.hiddenlabels.slice() :
469+
[];
470+
471+
var legendItem = g.data()[0][0],
472+
fullData = gd._fullData,
473+
trace = legendItem.trace,
474+
legendgroup = trace.legendgroup,
475+
traceIndicesInGroup = [],
476+
tracei,
477+
newVisible;
425478

479+
480+
if(numClicks === 1 && SHOWISOLATETIP && gd.data && gd._context.showTips) {
481+
Lib.notifier('Double click on legend to isolate individual trace', 'long');
482+
SHOWISOLATETIP = false;
483+
} else {
484+
SHOWISOLATETIP = false;
485+
}
486+
if(Registry.traceIs(trace, 'pie')) {
487+
var thisLabel = legendItem.label,
488+
thisLabelIndex = hiddenSlices.indexOf(thisLabel);
489+
490+
if(numClicks === 1) {
426491
if(thisLabelIndex === -1) hiddenSlices.push(thisLabel);
427492
else hiddenSlices.splice(thisLabelIndex, 1);
493+
} else if(numClicks === 2) {
494+
hiddenSlices = [];
495+
gd.calcdata[0].forEach(function(d) {
496+
if(thisLabel !== d.label) {
497+
hiddenSlices.push(d.label);
498+
}
499+
});
500+
if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) {
501+
hiddenSlices = [];
502+
}
503+
}
504+
505+
Plotly.relayout(gd, 'hiddenlabels', hiddenSlices);
506+
} else {
507+
var allTraces = [],
508+
traceVisibility = [],
509+
i;
428510

429-
Plotly.relayout(gd, 'hiddenlabels', hiddenSlices);
511+
for(i = 0; i < fullData.length; i++) {
512+
allTraces.push(i);
513+
traceVisibility.push('legendonly');
514+
}
515+
516+
if(legendgroup === '') {
517+
traceIndicesInGroup = [trace.index];
518+
traceVisibility[trace.index] = true;
430519
} else {
431-
if(legendgroup === '') {
432-
traceIndicesInGroup = [trace.index];
433-
} else {
434-
for(var i = 0; i < fullData.length; i++) {
435-
tracei = fullData[i];
436-
if(tracei.legendgroup === legendgroup) {
437-
traceIndicesInGroup.push(tracei.index);
438-
}
520+
for(i = 0; i < fullData.length; i++) {
521+
tracei = fullData[i];
522+
if(tracei.legendgroup === legendgroup) {
523+
traceIndicesInGroup.push(tracei.index);
524+
traceVisibility[allTraces.indexOf(i)] = true;
439525
}
440526
}
527+
}
441528

529+
if(numClicks === 1) {
442530
newVisible = trace.visible === true ? 'legendonly' : true;
443531
Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup);
532+
} else if(numClicks === 2) {
533+
var sameAsLast = true;
534+
for(i = 0; i < fullData.length; i++) {
535+
if(fullData[i].visible !== traceVisibility[i]) {
536+
sameAsLast = false;
537+
break;
538+
}
539+
}
540+
if(sameAsLast) {
541+
traceVisibility = true;
542+
}
543+
Plotly.restyle(gd, 'visible', traceVisibility, allTraces);
444544
}
445-
});
545+
}
446546
}
447547

448548
function computeTextDimensions(g, gd) {

src/constants/interactions.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@ module.exports = {
1414
* Timing information for interactive elements
1515
*/
1616
SHOW_PLACEHOLDER: 100,
17-
HIDE_PLACEHOLDER: 1000
17+
HIDE_PLACEHOLDER: 1000,
18+
19+
// ms between first mousedown and 2nd mouseup to constitute dblclick...
20+
// we don't seem to have access to the system setting
21+
DBLCLICKDELAY: 300
1822
};

src/lib/svg_text_utils.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,8 @@ exports.makeEditable = function(context, _delegate, options) {
467467
}
468468

469469
function appendEditable() {
470-
var plotDiv = d3.select(Lib.getPlotDiv(that.node())),
470+
var gd = Lib.getPlotDiv(that.node()),
471+
plotDiv = d3.select(gd),
471472
container = plotDiv.select('.svg-container'),
472473
div = container.append('div');
473474
div.classed('plugin-editable editable', true)
@@ -487,6 +488,7 @@ exports.makeEditable = function(context, _delegate, options) {
487488
.text(options.text || that.attr('data-unformatted'))
488489
.call(alignHTMLWith(that, container, options))
489490
.on('blur', function() {
491+
gd._editing = false;
490492
that.text(this.textContent)
491493
.style({opacity: 1});
492494
var svgClass = d3.select(this).attr('class'),
@@ -503,13 +505,15 @@ exports.makeEditable = function(context, _delegate, options) {
503505
})
504506
.on('focus', function() {
505507
var context = this;
508+
gd._editing = true;
506509
d3.select(document).on('mouseup', function() {
507510
if(d3.event.target === context) return false;
508511
if(document.activeElement === div.node()) div.node().blur();
509512
});
510513
})
511514
.on('keyup', function() {
512515
if(d3.event.which === 27) {
516+
gd._editing = false;
513517
that.style({opacity: 1});
514518
d3.select(this)
515519
.style({opacity: 0})

src/plot_api/plot_api.js

+1
Original file line numberDiff line numberDiff line change
@@ -2794,6 +2794,7 @@ Plotly.purge = function purge(gd) {
27942794
delete gd._context;
27952795
delete gd._replotPending;
27962796
delete gd._mouseDownTime;
2797+
delete gd._legendMouseDownTime;
27972798
delete gd._hmpixcount;
27982799
delete gd._hmlumcount;
27992800

src/plots/cartesian/constants.js

-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ module.exports = {
2929
AX_ID_PATTERN: /^[xyz][0-9]*$/,
3030
AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/,
3131

32-
// ms between first mousedown and 2nd mouseup to constitute dblclick...
33-
// we don't seem to have access to the system setting
34-
DBLCLICKDELAY: 300,
35-
3632
// pixels to move mouse before you stop clamping to starting point
3733
MINDRAG: 8,
3834

test/jasmine/assets/double_click.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
var click = require('./click');
2-
var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY;
2+
var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY;
33

44
module.exports = function doubleClick(x, y) {
55
return new Promise(function(resolve) {

test/jasmine/tests/click_test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
var Plotly = require('@lib/index');
22
var Lib = require('@src/lib');
33
var Drawing = require('@src/components/drawing');
4-
var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY;
4+
var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY;
55

66
var d3 = require('d3');
77
var createGraphDiv = require('../assets/create_graph_div');

0 commit comments

Comments
 (0)