Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9174470

Browse files
committed
add title to legend
1 parent 1b54804 commit 9174470

File tree

5 files changed

+152
-27
lines changed

5 files changed

+152
-27
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,10 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
116116
coerce('yanchor', defaultYAnchor);
117117
coerce('valign');
118118
Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
119+
120+
var titleText = coerce('title.text');
121+
if(titleText) {
122+
coerce('title.side', orientation === 'h' ? 'left' : 'top');
123+
Lib.coerceFont(coerce, 'title.font', layoutOut.font);
124+
}
119125
};

src/components/legend/draw.js

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ module.exports = function draw(gd) {
6363
.call(Color.fill, opts.bgcolor)
6464
.style('stroke-width', opts.borderwidth + 'px');
6565

66+
67+
var title = fullLayout.legend.title;
68+
gd._fullLayout._legendTitleWidth = 0;
69+
gd._fullLayout._legendTitleHeight = 0;
70+
if(title.text) {
71+
var titleEl = Lib.ensureSingle(legend, 'text', 'legendtitletext');
72+
titleEl.attr('text-anchor', 'start')
73+
.classed('user-select-none', true)
74+
.call(Drawing.font, title.font)
75+
.text(title.text);
76+
77+
textLayout(titleEl, legend, gd); // handle mathjax or multi-line text and compute title height
78+
}
6679
var scrollBox = Lib.ensureSingle(legend, 'g', 'scrollbox');
6780

6881
var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) {
@@ -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
@@ -375,18 +388,12 @@ function drawTexts(g, gd) {
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,17 +467,25 @@ 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();
473-
var opts = gd._fullLayout.legend;
485+
var bw = gd._fullLayout.legend.borderwidth;
486+
var opts = legendItem ?
487+
gd._fullLayout.legend :
488+
gd._fullLayout.legend.title;
474489
var lineHeight = opts.font.size * LINE_SPACING;
475490
var height, width;
476491

@@ -480,24 +495,43 @@ function computeTextDimensions(g, gd) {
480495
height = mathjaxBB.height;
481496
width = mathjaxBB.width;
482497

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

489510
height = lineHeight * textLines;
490511
width = textNode ? Drawing.bBox(textNode).width : 0;
491512

492513
// approximation to height offset to center the font
493514
// to avoid getBoundingClientRect
494-
var textY = lineHeight * (0.3 + (1 - textLines) / 2);
495-
svgTextUtils.positionText(text, constants.textGap, textY);
515+
var textY = lineHeight * ((textLines - 1) / 2 - 0.3);
516+
if(legendItem) {
517+
svgTextUtils.positionText(textEl, constants.textGap, -textY);
518+
} else { // case of title
519+
svgTextUtils.positionText(textEl, constants.titlePad + bw, lineHeight + bw);
520+
}
496521
}
497522

498-
legendItem.lineHeight = lineHeight;
499-
legendItem.height = Math.max(height, 16) + 3;
500-
legendItem.width = width;
523+
if(legendItem) {
524+
legendItem.lineHeight = lineHeight;
525+
legendItem.height = Math.max(height, 16) + 3;
526+
legendItem.width = width;
527+
} else { // case of title
528+
if(opts.side.indexOf('left') !== -1) {
529+
gd._fullLayout._legendTitleWidth = width;
530+
}
531+
if(opts.side.indexOf('top') !== -1) {
532+
gd._fullLayout._legendTitleHeight = height;
533+
}
534+
}
501535
}
502536

503537
/*
@@ -514,6 +548,9 @@ function computeLegendDimensions(gd, groups, traces) {
514548
var fullLayout = gd._fullLayout;
515549
var opts = fullLayout.legend;
516550
var gs = fullLayout._size;
551+
opts._titleWidth = fullLayout._legendTitleWidth;
552+
opts._titleHeight = fullLayout._legendTitleHeight;
553+
517554
var isVertical = helpers.isVertical(opts);
518555
var isGrouped = helpers.isGrouped(opts);
519556

@@ -541,7 +578,10 @@ function computeLegendDimensions(gd, groups, traces) {
541578
if(isVertical) {
542579
traces.each(function(d) {
543580
var h = d[0].height;
544-
Drawing.setTranslate(this, bw, itemGap + bw + opts._height + h / 2);
581+
Drawing.setTranslate(this,
582+
bw + opts._titleWidth,
583+
bw + opts._titleHeight + opts._height + h / 2 + itemGap
584+
);
545585
opts._height += h;
546586
opts._width = Math.max(opts._width, d[0].width);
547587
});
@@ -591,7 +631,10 @@ function computeLegendDimensions(gd, groups, traces) {
591631
var offsetY = 0;
592632
d3.select(this).selectAll('g.traces').each(function(d) {
593633
var h = d[0].height;
594-
Drawing.setTranslate(this, 0, itemGap + bw + h / 2 + offsetY);
634+
Drawing.setTranslate(this,
635+
opts._titleWidth,
636+
bw + opts._titleHeight + itemGap + h / 2 + offsetY
637+
);
595638
offsetY += h;
596639
maxWidthInGroup = Math.max(maxWidthInGroup, textGap + d[0].width);
597640
});
@@ -634,7 +677,10 @@ function computeLegendDimensions(gd, groups, traces) {
634677
maxItemHeightInRow = 0;
635678
}
636679

637-
Drawing.setTranslate(this, bw + offsetX, itemGap + bw + h / 2 + offsetY);
680+
Drawing.setTranslate(this,
681+
bw + opts._titleWidth + offsetX,
682+
bw + opts._titleHeight + offsetY + h / 2 + itemGap
683+
);
638684

639685
rowWidth = offsetX + w + itemGap;
640686
offsetX += next;
@@ -651,8 +697,8 @@ function computeLegendDimensions(gd, groups, traces) {
651697
}
652698
}
653699

654-
opts._width = Math.ceil(opts._width);
655-
opts._height = Math.ceil(opts._height);
700+
opts._width = Math.ceil(opts._width + opts._titleWidth);
701+
opts._height = Math.ceil(opts._height + opts._titleHeight);
656702

657703
opts._effHeight = Math.min(opts._height, opts._maxHeight);
658704

test/jasmine/tests/legend_test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,44 @@ describe('legend defaults', function() {
172172
expect(layoutOut.legend.orientation).toEqual('v');
173173
});
174174

175+
it('should not coerce `title.font` and `title.side` if the `title.text` is blank', function() {
176+
var layoutWithTitle = Lib.extendDeep({
177+
legend: {
178+
title: {
179+
text: ''
180+
}
181+
}
182+
}, layoutIn);
183+
supplyLayoutDefaults(layoutWithTitle, layoutOut, []);
184+
expect(layoutOut.legend.title.font).toEqual(undefined);
185+
expect(layoutOut.legend.title.side).toEqual(undefined);
186+
});
187+
188+
it('should default `title.side` to *top* for vertical legends', function() {
189+
var layoutWithTitle = Lib.extendDeep({
190+
legend: {
191+
title: {
192+
text: 'Legend Title'
193+
}
194+
}
195+
}, layoutIn);
196+
supplyLayoutDefaults(layoutWithTitle, layoutOut, []);
197+
expect(layoutOut.legend.title.side).toEqual('top');
198+
});
199+
200+
it('should default `title.side` to *left* for horizontal legends', function() {
201+
var layoutWithTitle = Lib.extendDeep({
202+
legend: {
203+
orientation: 'h',
204+
title: {
205+
text: 'Legend Title'
206+
}
207+
}
208+
}, layoutIn);
209+
supplyLayoutDefaults(layoutWithTitle, layoutOut, []);
210+
expect(layoutOut.legend.title.side).toEqual('left');
211+
});
212+
175213
describe('for horizontal legends', function() {
176214
var layoutInForHorizontalLegends;
177215

0 commit comments

Comments
 (0)