mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
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:
parent
5cca0bb866
commit
d04cdfc21f
@ -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|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
153
samples/legend/title.html
Normal 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>
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -20,6 +20,12 @@ describe('Legend block tests', function() {
|
||||
boxWidth: 40,
|
||||
padding: 10,
|
||||
generateLabels: jasmine.any(Function)
|
||||
},
|
||||
|
||||
title: {
|
||||
display: false,
|
||||
position: 'center',
|
||||
text: '',
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user