Add the ability to add a title to the legend (#6906)

* Add the ability to add a title to the legend

- Legend title can be specified
- Font & color options added
- Padding option added
- Positioning option added
- Legend title sample file added
This commit is contained in:
Evert Timberg 2020-01-10 18:28:51 -05:00 committed by GitHub
parent 5cca0bb866
commit d04cdfc21f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 318 additions and 44 deletions

View File

@ -19,6 +19,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob
| `labels` | `object` | | See the [Legend Label Configuration](#legend-label-configuration) section below.
| `rtl` | `boolean` | | `true` for rendering the legends from right to left.
| `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'|'ltr` on the canvas for rendering the legend, regardless of the css specified on the canvas
| `title` | `object` | | See the [Legend Title Configuration](#legend-title-configuration) section below.
## Position
@ -55,6 +56,21 @@ The legend label configuration is nested below the legend configuration using th
| `filter` | `function` | `null` | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#legend-item-interface) and the chart data.
| `usePointStyle` | `boolean` | `false` | Label style will match corresponding point style (size is based on the mimimum value between boxWidth and fontSize).
## Legend Title Configuration
The legend title configuration is nested below the legend configuration using the `title` key.
| Name | Type | Default | Description
| ---- | ---- | ------- | -----------
| `display` | `boolean` | `false` | Is the legend title displayed.
| `fontSize` | `number` | `12` | Font size of text.
| `fontStyle` | `string` | `'normal'` | Font style of text.
| `fontColor` | `Color` | `'#666'` | Color of text.
| `fontFamily` | `string` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family of legend text.
| `lineHeight` | `number` | | Line height of the text. If unset, is computed from the font size.
| `padding` | <code>number&#124;object</code> | `0` | Padding around the title. If specified as a number, it applies evenly to all sides.
| `text` | `string` | | The string title.
## Legend Item Interface
Items passed to the legend `onClick` function are the ones returned from `labels.generateLabels`. These items must implement the following interface.

153
samples/legend/title.html Normal file
View File

@ -0,0 +1,153 @@
<!doctype html>
<html>
<head>
<title>Legend Positions</title>
<script src="../../dist/Chart.min.js"></script>
<script src="../utils.js"></script>
<style>
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.chart-container {
width: 500px;
margin-left: 40px;
margin-right: 40px;
}
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
</style>
</head>
<body>
<div class="container">
<div class="chart-container">
<canvas id="chart-legend-top-start"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-legend-top-center"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-legend-top-end"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-legend-left-start"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-legend-left-center"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-legend-left-end"></canvas>
</div>
</div>
<script>
var color = Chart.helpers.color;
function createConfig(legendPosition, titlePosition, align, colorName) {
return {
type: 'line',
data: {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
label: 'My First dataset',
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
],
backgroundColor: color(window.chartColors[colorName]).alpha(0.5).rgbString(),
borderColor: window.chartColors[colorName],
borderWidth: 1
}]
},
options: {
responsive: true,
legend: {
align: align,
position: legendPosition,
title: {
display: true,
text: 'Legend Title',
position: titlePosition,
}
},
scales: {
x: {
display: true,
scaleLabel: {
display: true,
labelString: 'Month'
}
},
y: {
display: true,
scaleLabel: {
display: true,
labelString: 'Value'
}
}
},
title: {
display: true,
text: 'Legend Title Position: ' + titlePosition
}
}
};
}
window.onload = function() {
[{
id: 'chart-legend-top-start',
align: 'start',
legendPosition: 'top',
titlePosition: 'start',
color: 'red'
}, {
id: 'chart-legend-top-center',
align: 'center',
legendPosition: 'top',
titlePosition: 'center',
color: 'orange'
}, {
id: 'chart-legend-top-end',
align: 'end',
legendPosition: 'top',
titlePosition: 'end',
color: 'yellow'
}, {
id: 'chart-legend-left-start',
align: 'start',
legendPosition: 'left',
titlePosition: 'start',
color: 'green'
}, {
id: 'chart-legend-left-center',
align: 'center',
legendPosition: 'left',
titlePosition: 'center',
color: 'blue'
}, {
id: 'chart-legend-left-end',
align: 'end',
legendPosition: 'left',
titlePosition: 'end',
color: 'purple'
}].forEach(function(details) {
var ctx = document.getElementById(details.id).getContext('2d');
var config = createConfig(details.legendPosition, details.titlePosition, details.align, details.color);
new Chart(ctx, config);
});
};
</script>
</body>
</html>

View File

@ -163,6 +163,9 @@
items: [{
title: 'Positioning',
path: 'legend/positioning.html'
}, {
title: 'Legend Title',
path: 'legend/title.html'
}, {
title: 'Point style',
path: 'legend/point-style.html'

View File

@ -72,6 +72,12 @@ defaults._set('legend', {
};
}, this);
}
},
title: {
display: false,
position: 'center',
text: '',
}
});
@ -210,21 +216,21 @@ class Legend extends Element {
beforeFit() {}
fit() {
var me = this;
var opts = me.options;
var labelOpts = opts.labels;
var display = opts.display;
const me = this;
const opts = me.options;
const labelOpts = opts.labels;
const display = opts.display;
var ctx = me.ctx;
var labelFont = helpers.options._parseFont(labelOpts);
var fontSize = labelFont.size;
const ctx = me.ctx;
const labelFont = helpers.options._parseFont(labelOpts);
const fontSize = labelFont.size;
// Reset hit boxes
var hitboxes = me.legendHitBoxes = [];
const hitboxes = me.legendHitBoxes = [];
var minSize = me._minSize;
var isHorizontal = me.isHorizontal();
const minSize = me._minSize;
const isHorizontal = me.isHorizontal();
const titleHeight = me._computeTitleHeight();
if (isHorizontal) {
minSize.width = me.maxWidth; // fill all the width
@ -242,18 +248,16 @@ class Legend extends Element {
ctx.font = labelFont.string;
if (isHorizontal) {
// Labels
// Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one
var lineWidths = me.lineWidths = [0];
var totalHeight = 0;
const lineWidths = me.lineWidths = [0];
let totalHeight = titleHeight;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
me.legendItems.forEach(function(legendItem, i) {
var boxWidth = getBoxWidth(labelOpts, fontSize);
var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
const boxWidth = getBoxWidth(labelOpts, fontSize);
const width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
if (i === 0 || lineWidths[lineWidths.length - 1] + width + 2 * labelOpts.padding > minSize.width) {
totalHeight += fontSize + labelOpts.padding;
@ -274,19 +278,20 @@ class Legend extends Element {
minSize.height += totalHeight;
} else {
var vPadding = labelOpts.padding;
var columnWidths = me.columnWidths = [];
var columnHeights = me.columnHeights = [];
var totalWidth = labelOpts.padding;
var currentColWidth = 0;
var currentColHeight = 0;
const vPadding = labelOpts.padding;
const columnWidths = me.columnWidths = [];
const columnHeights = me.columnHeights = [];
let totalWidth = labelOpts.padding;
let currentColWidth = 0;
let currentColHeight = 0;
let heightLimit = minSize.height - titleHeight;
me.legendItems.forEach(function(legendItem, i) {
var boxWidth = getBoxWidth(labelOpts, fontSize);
var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
const boxWidth = getBoxWidth(labelOpts, fontSize);
const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
// If too tall, go to new column
if (i > 0 && currentColHeight + fontSize + 2 * vPadding > minSize.height) {
if (i > 0 && currentColHeight + fontSize + 2 * vPadding > heightLimit) {
totalWidth += currentColWidth + labelOpts.padding;
columnWidths.push(currentColWidth); // previous column width
columnHeights.push(currentColHeight);
@ -326,26 +331,27 @@ class Legend extends Element {
// Actually draw the legend on the canvas
draw() {
var me = this;
var opts = me.options;
var labelOpts = opts.labels;
var defaultColor = defaults.color;
var lineDefault = defaults.elements.line;
var legendHeight = me.height;
var columnHeights = me.columnHeights;
var legendWidth = me.width;
var lineWidths = me.lineWidths;
const me = this;
const opts = me.options;
const labelOpts = opts.labels;
const defaultColor = defaults.color;
const lineDefault = defaults.elements.line;
const legendHeight = me.height;
const columnHeights = me.columnHeights;
const legendWidth = me.width;
const lineWidths = me.lineWidths;
if (!opts.display) {
return;
}
var rtlHelper = getRtlHelper(opts.rtl, me.left, me._minSize.width);
var ctx = me.ctx;
var fontColor = valueOrDefault(labelOpts.fontColor, defaults.fontColor);
var labelFont = helpers.options._parseFont(labelOpts);
var fontSize = labelFont.size;
var cursor;
me._drawTitle();
const rtlHelper = getRtlHelper(opts.rtl, me.left, me._minSize.width);
const ctx = me.ctx;
const fontColor = valueOrDefault(labelOpts.fontColor, defaults.fontColor);
const labelFont = helpers.options._parseFont(labelOpts);
const fontSize = labelFont.size;
let cursor;
// Canvas setup
ctx.textAlign = rtlHelper.textAlign('left');
@ -429,17 +435,18 @@ class Legend extends Element {
};
// Horizontal
var isHorizontal = me.isHorizontal();
const isHorizontal = me.isHorizontal();
const titleHeight = this._computeTitleHeight();
if (isHorizontal) {
cursor = {
x: me.left + alignmentOffset(legendWidth, lineWidths[0]),
y: me.top + labelOpts.padding,
y: me.top + labelOpts.padding + titleHeight,
line: 0
};
} else {
cursor = {
x: me.left + labelOpts.padding,
y: me.top + alignmentOffset(legendHeight, columnHeights[0]),
y: me.top + alignmentOffset(legendHeight, columnHeights[0]) + titleHeight,
line: 0
};
}
@ -490,6 +497,95 @@ class Legend extends Element {
helpers.rtl.restoreTextDirection(me.ctx, opts.textDirection);
}
_drawTitle() {
const me = this;
const opts = me.options;
const titleOpts = opts.title;
const titleFont = helpers.options._parseFont(titleOpts);
const titlePadding = helpers.options.toPadding(titleOpts.padding);
if (!titleOpts.display) {
return;
}
const rtlHelper = getRtlHelper(opts.rtl, me.left, me.minSize.width);
const ctx = me.ctx;
const fontColor = valueOrDefault(titleOpts.fontColor, defaults.fontColor);
const position = titleOpts.position;
let x, textAlign;
const halfFontSize = titleFont.size / 2;
let y = me.top + titlePadding.top + halfFontSize;
// These defaults are used when the legend is vertical.
// When horizontal, they are computed below.
let left = me.left;
let maxWidth = me.width;
if (this.isHorizontal()) {
// Move left / right so that the title is above the legend lines
maxWidth = Math.max(...me.lineWidths);
switch (opts.align) {
case 'start':
// left is already correct in this case
break;
case 'end':
left = me.right - maxWidth;
break;
default:
left = ((me.left + me.right) / 2) - (maxWidth / 2);
break;
}
} else {
// Move down so that the title is above the legend stack in every alignment
const maxHeight = Math.max(...me.columnHeights);
switch (opts.align) {
case 'start':
// y is already correct in this case
break;
case 'end':
y += me.height - maxHeight;
break;
default: // center
y += (me.height - maxHeight) / 2;
break;
}
}
// Now that we know the left edge of the inner legend box, compute the correct
// X coordinate from the title alignment
switch (position) {
case 'start':
x = left;
textAlign = 'left';
break;
case 'end':
x = left + maxWidth;
textAlign = 'right';
break;
default:
x = left + (maxWidth / 2);
textAlign = 'center';
break;
}
// Canvas setup
ctx.textAlign = rtlHelper.textAlign(textAlign);
ctx.textBaseline = 'middle';
ctx.strokeStyle = fontColor;
ctx.fillStyle = fontColor;
ctx.font = titleFont.string;
ctx.fillText(titleOpts.text, x, y);
}
_computeTitleHeight() {
const titleOpts = this.options.title;
const titleFont = helpers.options._parseFont(titleOpts);
const titlePadding = helpers.options.toPadding(titleOpts.padding);
return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0;
}
/**
* @private
*/

View File

@ -20,6 +20,12 @@ describe('Legend block tests', function() {
boxWidth: 40,
padding: 10,
generateLabels: jasmine.any(Function)
},
title: {
display: false,
position: 'center',
text: '',
}
});
});