Skip to content

Commit 02dabc7

Browse files
authored
Merge pull request #4386 from plotly/new-legends-and-add-title
Add title feature to legend and support legend option for various traces
2 parents dbf02f1 + 0a437a6 commit 02dabc7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1552
-58
lines changed

src/components/legend/attributes.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,38 @@ module.exports = {
193193
'Sets the vertical alignment of the symbols with respect to their associated text.',
194194
].join(' ')
195195
},
196+
title: {
197+
text: {
198+
valType: 'string',
199+
dflt: '',
200+
role: 'info',
201+
editType: 'legend',
202+
description: [
203+
'Sets the title of the legend.'
204+
].join(' ')
205+
},
206+
font: fontAttrs({
207+
editType: 'legend',
208+
description: [
209+
'Sets this legend\'s title font.'
210+
].join(' '),
211+
}),
212+
side: {
213+
valType: 'enumerated',
214+
values: ['top', 'left', 'top left'],
215+
role: 'style',
216+
editType: 'legend',
217+
description: [
218+
'Determines the location of legend\'s title',
219+
'with respect to the legend items.',
220+
'Defaulted to *top* with `orientation` is *h*.',
221+
'Defaulted to *left* with `orientation` is *v*.',
222+
'The *top left* options could be used to expand',
223+
'legend area in both x and y sides.'
224+
].join(' ')
225+
},
226+
editType: 'legend',
227+
},
228+
196229
editType: 'legend'
197230
};

src/components/legend/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ module.exports = {
1515
scrollBarMargin: 4,
1616
scrollBarEnterAttrs: {rx: 20, ry: 3, width: 0, height: 0},
1717

18+
// number of px between legend title and (left) side of legend (always in x direction and from inner border)
19+
titlePad: 2,
1820
// number of px between legend symbol and legend text (always in x direction)
1921
textGap: 40,
2022
// number of px between each legend item (x and/or y direction)

src/components/legend/defaults.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
3333
// *would* be shown by default, toward the two traces you need to
3434
// ensure the legend is shown by default, because this can still help
3535
// disambiguate.
36-
if(trace.showlegend || trace._dfltShowLegend) {
36+
if(trace.showlegend || (
37+
trace._dfltShowLegend && !(
38+
trace._module &&
39+
trace._module.attributes &&
40+
trace._module.attributes.showlegend &&
41+
trace._module.attributes.showlegend.dflt === false
42+
)
43+
)) {
3744
legendTraceCount++;
3845
if(trace.showlegend) {
3946
legendReallyHasATrace = true;
@@ -116,4 +123,10 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
116123
coerce('yanchor', defaultYAnchor);
117124
coerce('valign');
118125
Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
126+
127+
var titleText = coerce('title.text');
128+
if(titleText) {
129+
coerce('title.side', orientation === 'h' ? 'left' : 'top');
130+
Lib.coerceFont(coerce, 'title.font', layoutOut.font);
131+
}
119132
};

src/components/legend/draw.js

Lines changed: 95 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ module.exports = function draw(gd) {
6565

6666
var scrollBox = Lib.ensureSingle(legend, 'g', 'scrollbox');
6767

68+
var title = opts.title;
69+
opts._titleWidth = 0;
70+
opts._titleHeight = 0;
71+
if(title.text) {
72+
var titleEl = Lib.ensureSingle(scrollBox, 'text', 'legendtitletext');
73+
titleEl.attr('text-anchor', 'start')
74+
.classed('user-select-none', true)
75+
.call(Drawing.font, title.font)
76+
.text(title.text);
77+
78+
textLayout(titleEl, scrollBox, gd); // handle mathjax or multi-line text and compute title height
79+
}
80+
6881
var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
6982
s.attr(constants.scrollBarEnterAttrs)
7083
.call(Color.fill, constants.scrollBarColor);
@@ -121,7 +134,7 @@ module.exports = function draw(gd) {
121134
}
122135

123136
// Set size and position of all the elements that make up a legend:
124-
// legend, background and border, scroll box and scroll bar
137+
// legend, background and border, scroll box and scroll bar as well as title
125138
Drawing.setTranslate(legend, lx, ly);
126139

127140
// to be safe, remove previous listeners
@@ -370,23 +383,17 @@ function drawTexts(g, gd) {
370383

371384
textEl.attr('text-anchor', 'start')
372385
.classed('user-select-none', true)
373-
.call(Drawing.font, fullLayout.legend.font)
386+
.call(Drawing.font, opts.font)
374387
.text(isEditable ? ensureLength(name, maxNameLength) : name);
375388

376389
svgTextUtils.positionText(textEl, constants.textGap, 0);
377390

378-
function textLayout(s) {
379-
svgTextUtils.convertToTspans(s, gd, function() {
380-
computeTextDimensions(g, gd);
381-
});
382-
}
383-
384391
if(isEditable) {
385392
textEl.call(svgTextUtils.makeEditable, {gd: gd, text: name})
386-
.call(textLayout)
393+
.call(textLayout, g, gd)
387394
.on('edit', function(newName) {
388395
this.text(ensureLength(newName, maxNameLength))
389-
.call(textLayout);
396+
.call(textLayout, g, gd);
390397

391398
var fullInput = legendItem.trace._fullInput || {};
392399
var update = {};
@@ -407,7 +414,7 @@ function drawTexts(g, gd) {
407414
return Registry.call('_guiRestyle', gd, update, traceIndex);
408415
});
409416
} else {
410-
textLayout(textEl);
417+
textLayout(textEl, g, gd);
411418
}
412419
}
413420

@@ -460,18 +467,24 @@ function setupTraceToggle(g, gd) {
460467
});
461468
}
462469

470+
function textLayout(s, g, gd) {
471+
svgTextUtils.convertToTspans(s, gd, function() {
472+
computeTextDimensions(g, gd);
473+
});
474+
}
475+
463476
function computeTextDimensions(g, gd) {
464477
var legendItem = g.data()[0][0];
465-
466-
if(!legendItem.trace.showlegend) {
478+
if(legendItem && !legendItem.trace.showlegend) {
467479
g.remove();
468480
return;
469481
}
470482

471483
var mathjaxGroup = g.select('g[class*=math-group]');
472484
var mathjaxNode = mathjaxGroup.node();
485+
var bw = gd._fullLayout.legend.borderwidth;
473486
var opts = gd._fullLayout.legend;
474-
var lineHeight = opts.font.size * LINE_SPACING;
487+
var lineHeight = (legendItem ? opts : opts.title).font.size * LINE_SPACING;
475488
var height, width;
476489

477490
if(mathjaxNode) {
@@ -480,24 +493,56 @@ function computeTextDimensions(g, gd) {
480493
height = mathjaxBB.height;
481494
width = mathjaxBB.width;
482495

483-
Drawing.setTranslate(mathjaxGroup, 0, (height / 4));
496+
if(legendItem) {
497+
Drawing.setTranslate(mathjaxGroup, 0, height * 0.25);
498+
} else { // case of title
499+
Drawing.setTranslate(mathjaxGroup, bw, height * 0.75 + bw);
500+
}
484501
} else {
485-
var text = g.select('.legendtext');
486-
var textLines = svgTextUtils.lineCount(text);
487-
var textNode = text.node();
502+
var textEl = g.select(legendItem ?
503+
'.legendtext' : '.legendtitletext'
504+
);
505+
var textLines = svgTextUtils.lineCount(textEl);
506+
var textNode = textEl.node();
488507

489508
height = lineHeight * textLines;
490509
width = textNode ? Drawing.bBox(textNode).width : 0;
491510

492511
// approximation to height offset to center the font
493512
// to avoid getBoundingClientRect
494-
var textY = lineHeight * (0.3 + (1 - textLines) / 2);
495-
svgTextUtils.positionText(text, constants.textGap, textY);
513+
var textY = lineHeight * ((textLines - 1) / 2 - 0.3);
514+
if(legendItem) {
515+
svgTextUtils.positionText(textEl, constants.textGap, -textY);
516+
} else { // case of title
517+
svgTextUtils.positionText(textEl, constants.titlePad + bw, lineHeight + bw);
518+
}
519+
}
520+
521+
if(legendItem) {
522+
legendItem.lineHeight = lineHeight;
523+
legendItem.height = Math.max(height, 16) + 3;
524+
legendItem.width = width;
525+
} else { // case of title
526+
opts._titleWidth = width;
527+
opts._titleHeight = height;
528+
}
529+
}
530+
531+
function getTitleSize(opts) {
532+
var w = 0;
533+
var h = 0;
534+
535+
var side = opts.title.side;
536+
if(side) {
537+
if(side.indexOf('left') !== -1) {
538+
w = opts._titleWidth;
539+
}
540+
if(side.indexOf('top') !== -1) {
541+
h = opts._titleHeight;
542+
}
496543
}
497544

498-
legendItem.lineHeight = lineHeight;
499-
legendItem.height = Math.max(height, 16) + 3;
500-
legendItem.width = width;
545+
return [w, h];
501546
}
502547

503548
/*
@@ -514,6 +559,7 @@ function computeLegendDimensions(gd, groups, traces) {
514559
var fullLayout = gd._fullLayout;
515560
var opts = fullLayout.legend;
516561
var gs = fullLayout._size;
562+
517563
var isVertical = helpers.isVertical(opts);
518564
var isGrouped = helpers.isGrouped(opts);
519565

@@ -537,11 +583,15 @@ function computeLegendDimensions(gd, groups, traces) {
537583
var toggleRectWidth = 0;
538584
opts._width = 0;
539585
opts._height = 0;
586+
var titleSize = getTitleSize(opts);
540587

541588
if(isVertical) {
542589
traces.each(function(d) {
543590
var h = d[0].height;
544-
Drawing.setTranslate(this, bw, itemGap + bw + opts._height + h / 2);
591+
Drawing.setTranslate(this,
592+
bw + titleSize[0],
593+
bw + titleSize[1] + opts._height + h / 2 + itemGap
594+
);
545595
opts._height += h;
546596
opts._width = Math.max(opts._width, d[0].width);
547597
});
@@ -591,7 +641,10 @@ function computeLegendDimensions(gd, groups, traces) {
591641
var offsetY = 0;
592642
d3.select(this).selectAll('g.traces').each(function(d) {
593643
var h = d[0].height;
594-
Drawing.setTranslate(this, 0, itemGap + bw + h / 2 + offsetY);
644+
Drawing.setTranslate(this,
645+
titleSize[0],
646+
titleSize[1] + bw + itemGap + h / 2 + offsetY
647+
);
595648
offsetY += h;
596649
maxWidthInGroup = Math.max(maxWidthInGroup, textGap + d[0].width);
597650
});
@@ -634,7 +687,10 @@ function computeLegendDimensions(gd, groups, traces) {
634687
maxItemHeightInRow = 0;
635688
}
636689

637-
Drawing.setTranslate(this, bw + offsetX, itemGap + bw + h / 2 + offsetY);
690+
Drawing.setTranslate(this,
691+
titleSize[0] + bw + offsetX,
692+
titleSize[1] + bw + offsetY + h / 2 + itemGap
693+
);
638694

639695
rowWidth = offsetX + w + itemGap;
640696
offsetX += next;
@@ -651,8 +707,19 @@ function computeLegendDimensions(gd, groups, traces) {
651707
}
652708
}
653709

654-
opts._width = Math.ceil(opts._width);
655-
opts._height = Math.ceil(opts._height);
710+
opts._width = Math.ceil(
711+
Math.max(
712+
opts._width + titleSize[0],
713+
opts._titleWidth + 2 * (bw + constants.titlePad)
714+
)
715+
);
716+
717+
opts._height = Math.ceil(
718+
Math.max(
719+
opts._height + titleSize[1],
720+
opts._titleHeight + 2 * (bw + constants.itemGap)
721+
)
722+
);
656723

657724
opts._effHeight = Math.min(opts._height, opts._maxHeight);
658725

0 commit comments

Comments
 (0)