Skip to content

Commit 8630ad7

Browse files
committed
draw multiple legends
1 parent afbcb18 commit 8630ad7

File tree

5 files changed

+141
-35
lines changed

5 files changed

+141
-35
lines changed

src/components/legend/defaults.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ var attributes = require('./attributes');
99
var basePlotLayoutAttributes = require('../../plots/layout_attributes');
1010
var helpers = require('./helpers');
1111

12-
function groupDefaults(legendGroup, layoutIn, layoutOut, fullData) {
13-
var name = 'legend' + legendGroup;
12+
function getLegendName(id) {
13+
return 'legend' + id;
14+
}
15+
16+
function groupDefaults(id, layoutIn, layoutOut, fullData) {
17+
var name = getLegendName(id);
1418
var containerIn = layoutIn[name] || {};
1519
var containerOut = Template.newContainer(layoutOut, name);
1620

@@ -147,6 +151,8 @@ function groupDefaults(legendGroup, layoutIn, layoutOut, fullData) {
147151

148152
Lib.coerceFont(coerce, 'title.font', dfltTitleFont);
149153
}
154+
155+
return true;
150156
}
151157

152158
module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
@@ -157,10 +163,18 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
157163
Lib.pushUnique(groups, fullData[i].legendgroup);
158164
}
159165

166+
layoutOut._legends = [];
160167
for(i = 0; i < groups.length; i++) {
161-
var groupName = groups[i];
162-
groupDefaults(groupName, layoutIn, layoutOut, fullData);
168+
var id = groups[i];
169+
var name = getLegendName(id);
170+
if(id && !layoutIn[name]) continue;
171+
172+
groupDefaults(id, layoutIn, layoutOut, fullData);
173+
174+
if(layoutOut[name]) {
175+
layoutOut[name]._id = id;
176+
}
163177

164-
layoutOut['legend' + groupName]._id = groupName;
178+
layoutOut._legends.push(id);
165179
}
166180
};

src/components/legend/draw.js

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,22 @@ var helpers = require('./helpers');
2525
var MAIN_TITLE = 1;
2626

2727
module.exports = function draw(gd, opts) {
28-
drawOne(gd, opts || gd._fullLayout.legend);
28+
if(opts) {
29+
drawOne(gd, opts);
30+
} else {
31+
var legendGroups = gd._fullLayout._legends;
32+
for(var i = 0; i < legendGroups.length; i++) {
33+
drawOne(gd, gd._fullLayout['legend' + legendGroups[i]]);
34+
}
35+
}
2936
};
3037

3138
function drawOne(gd, opts) {
3239
var legendObj = opts || {};
3340

3441
var fullLayout = gd._fullLayout;
42+
var id = getId(legendObj);
43+
3544
var clipId, layer;
3645

3746
var inHover = legendObj._inHover;
@@ -40,7 +49,7 @@ function drawOne(gd, opts) {
4049
clipId = 'hover';
4150
} else {
4251
layer = fullLayout._infolayer;
43-
clipId = 'legend';
52+
clipId = 'legend' + id;
4453
}
4554
if(!layer) return;
4655
clipId += fullLayout._uid;
@@ -50,7 +59,7 @@ function drawOne(gd, opts) {
5059
var legendData;
5160
if(!inHover) {
5261
if(!gd.calcdata) return;
53-
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, legendObj);
62+
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, legendObj, fullLayout._legends.length > 1);
5463
} else {
5564
if(!legendObj.entries) return;
5665
legendData = getLegendData(legendObj.entries, legendObj);
@@ -59,12 +68,12 @@ function drawOne(gd, opts) {
5968
var hiddenSlices = fullLayout.hiddenlabels || [];
6069

6170
if(!inHover && (!fullLayout.showlegend || !legendData.length)) {
62-
layer.selectAll('.legend').remove();
71+
layer.selectAll('.legend' + id).remove();
6372
fullLayout._topdefs.select('#' + clipId).remove();
64-
return Plots.autoMargin(gd, 'legend');
73+
return Plots.autoMargin(gd, 'legend' + id);
6574
}
6675

67-
var legend = Lib.ensureSingle(layer, 'g', 'legend', function(s) {
76+
var legend = Lib.ensureSingle(layer, 'g', 'legend' + id, function(s) {
6877
if(!inHover) s.attr('pointer-events', 'all');
6978
});
7079

@@ -85,14 +94,14 @@ function drawOne(gd, opts) {
8594
legendObj._titleWidth = 0;
8695
legendObj._titleHeight = 0;
8796
if(title.text) {
88-
var titleEl = Lib.ensureSingle(scrollBox, 'text', 'legendtitletext');
97+
var titleEl = Lib.ensureSingle(scrollBox, 'text', 'legend' + id + 'titletext');
8998
titleEl.attr('text-anchor', 'start')
9099
.call(Drawing.font, title.font)
91100
.text(title.text);
92101

93102
textLayout(titleEl, scrollBox, gd, legendObj, MAIN_TITLE); // handle mathjax or multi-line text and compute title height
94103
} else {
95-
scrollBox.selectAll('.legendtitletext').remove();
104+
scrollBox.selectAll('.legend' + id + 'titletext').remove();
96105
}
97106

98107
var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
@@ -118,7 +127,7 @@ function drawOne(gd, opts) {
118127
})
119128
.each(function() { d3.select(this).call(drawTexts, gd, legendObj); })
120129
.call(style, gd, legendObj)
121-
.each(function() { if(!inHover) d3.select(this).call(setupTraceToggle, gd); });
130+
.each(function() { if(!inHover) d3.select(this).call(setupTraceToggle, gd, id); });
122131

123132
Lib.syncOrAsync([
124133
Plots.previousPromises,
@@ -128,7 +137,7 @@ function drawOne(gd, opts) {
128137
var bw = legendObj.borderwidth;
129138

130139
if(!inHover) {
131-
var expMargin = expandMargin(gd);
140+
var expMargin = expandMargin(gd, id);
132141

133142
// IF expandMargin return a Promise (which is truthy),
134143
// we're under a doAutoMargin redraw, so we don't have to
@@ -146,10 +155,10 @@ function drawOne(gd, opts) {
146155
ly = Lib.constrain(ly, 0, fullLayout.height - legendObj._effHeight);
147156

148157
if(lx !== lx0) {
149-
Lib.log('Constrain legend.x to make legend fit inside graph');
158+
Lib.log('Constrain legend' + id + '.x to make legend fit inside graph');
150159
}
151160
if(ly !== ly0) {
152-
Lib.log('Constrain legend.y to make legend fit inside graph');
161+
Lib.log('Constrain legend' + id + '.y to make legend fit inside graph');
153162
}
154163
}
155164

@@ -295,7 +304,7 @@ function drawOne(gd, opts) {
295304
}
296305

297306
function scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio) {
298-
legendObj._scrollY = gd._fullLayout.legend._scrollY = scrollBoxY;
307+
legendObj._scrollY = gd._fullLayout['legend' + id]._scrollY = scrollBoxY;
299308
Drawing.setTranslate(scrollBox, 0, -scrollBoxY);
300309

301310
Drawing.setRect(
@@ -331,11 +340,14 @@ function drawOne(gd, opts) {
331340
},
332341
doneFn: function() {
333342
if(xf !== undefined && yf !== undefined) {
334-
Registry.call('_guiRelayout', gd, {'legend.x': xf, 'legend.y': yf});
343+
var obj = {};
344+
obj['legend' + id + '.x'] = xf;
345+
obj['legend' + id + '.y'] = yf;
346+
Registry.call('_guiRelayout', gd, obj);
335347
}
336348
},
337349
clickFn: function(numClicks, e) {
338-
var clickedTrace = layer.selectAll('g.traces').filter(function() {
350+
var clickedTrace = groups.selectAll('g.traces').filter(function() {
339351
var bbox = this.getBoundingClientRect();
340352
return (
341353
e.clientX >= bbox.left && e.clientX <= bbox.right &&
@@ -403,6 +415,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
403415
}
404416

405417
function drawTexts(g, gd, legendObj) {
418+
var id = getId(legendObj);
406419
var legendItem = g.data()[0][0];
407420
var trace = legendItem.trace;
408421
var isPieLike = Registry.traceIs(trace, 'pie-like');
@@ -425,7 +438,7 @@ function drawTexts(g, gd, legendObj) {
425438
}
426439
}
427440

428-
var textEl = Lib.ensureSingle(g, 'text', 'legendtext');
441+
var textEl = Lib.ensureSingle(g, 'text', 'legend' + id + 'text');
429442

430443
textEl.attr('text-anchor', 'start')
431444
.call(Drawing.font, font)
@@ -479,12 +492,12 @@ function ensureLength(str, maxLength) {
479492
return str;
480493
}
481494

482-
function setupTraceToggle(g, gd) {
495+
function setupTraceToggle(g, gd, id) {
483496
var doubleClickDelay = gd._context.doubleClickDelay;
484497
var newMouseDownTime;
485498
var numClicks = 1;
486499

487-
var traceToggle = Lib.ensureSingle(g, 'rect', 'legendtoggle', function(s) {
500+
var traceToggle = Lib.ensureSingle(g, 'rect', 'legend' + id + 'toggle', function(s) {
488501
if(!gd._context.staticPlot) {
489502
s.style('cursor', 'pointer').attr('pointer-events', 'all');
490503
}
@@ -506,7 +519,7 @@ function setupTraceToggle(g, gd) {
506519
});
507520
traceToggle.on('mouseup', function() {
508521
if(gd._dragged || gd._editing) return;
509-
var legend = gd._fullLayout.legend;
522+
var legend = gd._fullLayout['legend' + id];
510523

511524
if((new Date()).getTime() - gd._legendMouseDownTime > doubleClickDelay) {
512525
numClicks = Math.max(numClicks - 1, 1);
@@ -532,7 +545,11 @@ function computeTextDimensions(g, gd, legendObj, aTitle) {
532545

533546
var mathjaxGroup = g.select('g[class*=math-group]');
534547
var mathjaxNode = mathjaxGroup.node();
535-
if(!legendObj) legendObj = gd._fullLayout.legend;
548+
549+
var id = getId(legendObj);
550+
if(!legendObj) {
551+
legendObj = gd._fullLayout['legend' + id];
552+
}
536553
var bw = legendObj.borderwidth;
537554
var font;
538555
if(aTitle === MAIN_TITLE) {
@@ -557,9 +574,12 @@ function computeTextDimensions(g, gd, legendObj, aTitle) {
557574
Drawing.setTranslate(mathjaxGroup, 0, height * 0.25);
558575
}
559576
} else {
560-
var textEl = g.select(aTitle === MAIN_TITLE ?
561-
'.legendtitletext' : '.legendtext'
562-
);
577+
var cls = '.legend' + id + (
578+
aTitle === MAIN_TITLE ? 'title' : ''
579+
) + 'text';
580+
581+
var textEl = g.select(cls);
582+
563583
var textLines = svgTextUtils.lineCount(textEl);
564584
var textNode = textEl.node();
565585

@@ -620,7 +640,7 @@ function getTitleSize(legendObj) {
620640
}
621641

622642
/*
623-
* Computes in fullLayout.legend:
643+
* Computes in fullLayout['legend' + id]:
624644
*
625645
* - _height: legend height including items past scrollbox height
626646
* - _maxHeight: maximum legend height before scrollbox is required
@@ -631,7 +651,10 @@ function getTitleSize(legendObj) {
631651
*/
632652
function computeLegendDimensions(gd, groups, traces, legendObj) {
633653
var fullLayout = gd._fullLayout;
634-
if(!legendObj) legendObj = fullLayout.legend;
654+
var id = getId(legendObj);
655+
if(!legendObj) {
656+
legendObj = fullLayout['legend' + id];
657+
}
635658
var gs = fullLayout._size;
636659

637660
var isVertical = helpers.isVertical(legendObj);
@@ -819,7 +842,7 @@ function computeLegendDimensions(gd, groups, traces, legendObj) {
819842
var edits = gd._context.edits;
820843
var isEditable = edits.legendText || edits.legendPosition;
821844
traces.each(function(d) {
822-
var traceToggle = d3.select(this).select('.legendtoggle');
845+
var traceToggle = d3.select(this).select('.legend' + id + 'toggle');
823846
var h = d[0].height;
824847
var legendgroup = d[0].trace.legendgroup;
825848
var traceWidth = getTraceWidth(d, legendObj, textGap);
@@ -834,13 +857,13 @@ function computeLegendDimensions(gd, groups, traces, legendObj) {
834857
});
835858
}
836859

837-
function expandMargin(gd) {
860+
function expandMargin(gd, id) {
838861
var fullLayout = gd._fullLayout;
839-
var legendObj = fullLayout.legend;
862+
var legendObj = fullLayout['legend' + id];
840863
var xanchor = getXanchor(legendObj);
841864
var yanchor = getYanchor(legendObj);
842865

843-
return Plots.autoMargin(gd, 'legend', {
866+
return Plots.autoMargin(gd, 'legend' + id, {
844867
x: legendObj.x,
845868
y: legendObj.y,
846869
l: legendObj._width * (FROM_TL[xanchor]),
@@ -861,3 +884,7 @@ function getYanchor(legendObj) {
861884
Lib.isMiddleAnchor(legendObj) ? 'middle' :
862885
'top';
863886
}
887+
888+
function getId(legendObj) {
889+
return legendObj._id || '';
890+
}

src/components/legend/get_legend_data.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
var Registry = require('../../registry');
44
var helpers = require('./helpers');
55

6-
module.exports = function getLegendData(calcdata, opts) {
6+
module.exports = function getLegendData(calcdata, opts, hasMultipleLegends) {
77
var inHover = opts._inHover;
88
var grouped = helpers.isGrouped(opts);
99
var reversed = helpers.isReversed(opts);
@@ -17,6 +17,8 @@ module.exports = function getLegendData(calcdata, opts) {
1717
var i, j;
1818

1919
function addOneItem(legendGroup, legendItem) {
20+
if(hasMultipleLegends && legendGroup !== opts._id) return;
21+
2022
// each '' legend group is treated as a separate group
2123
if(legendGroup === '' || !helpers.isGrouped(opts)) {
2224
// TODO: check this against fullData legendgroups?
23.4 KB
Loading
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"data": [
3+
{
4+
"y": [0]
5+
},
6+
{
7+
"y": [1]
8+
},
9+
{
10+
"y": [2]
11+
},
12+
{
13+
"y": [3],
14+
"legendgroup": "2"
15+
},
16+
{
17+
"y": [4],
18+
"legendgroup": "3"
19+
},
20+
{
21+
"y": [5],
22+
"legendgroup": "3"
23+
}
24+
],
25+
"layout": {
26+
"title": {
27+
"text": "Multiple legends"
28+
},
29+
"width": 500,
30+
"height": 500,
31+
"yaxis": {
32+
"autorange": "reversed"
33+
},
34+
"legend": {
35+
"bgcolor": "lightgray",
36+
"title": {
37+
"text": "Legend"
38+
}
39+
},
40+
"legend2": {
41+
"x": -0.05,
42+
"y": 0.5,
43+
"xanchor": "right",
44+
"yanchor": "middle",
45+
"bgcolor": "lightblue",
46+
"title": {
47+
"text": "Legend 2"
48+
}
49+
},
50+
"legend3": {
51+
"y": 0,
52+
"yanchor": "bottom",
53+
"bgcolor": "yellow",
54+
"title": {
55+
"text": "Legend 3"
56+
}
57+
},
58+
"hovermode": "x unified"
59+
},
60+
"config": {
61+
"editable": true
62+
}
63+
}

0 commit comments

Comments
 (0)