diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md
index 35c75b246..ddffc43e3 100644
--- a/docs/configuration/legend.md
+++ b/docs/configuration/legend.md
@@ -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` | number|object | `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.
diff --git a/samples/legend/title.html b/samples/legend/title.html
new file mode 100644
index 000000000..7f5e334ec
--- /dev/null
+++ b/samples/legend/title.html
@@ -0,0 +1,153 @@
+
+
+
+
+ Legend Positions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/samples.js b/samples/samples.js
index c91d7cf14..53084d547 100644
--- a/samples/samples.js
+++ b/samples/samples.js
@@ -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'
diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js
index 6115a0ae0..2c7e7e761 100644
--- a/src/plugins/plugin.legend.js
+++ b/src/plugins/plugin.legend.js
@@ -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
*/
diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js
index 23072b9c9..6fe15c161 100644
--- a/test/specs/plugin.legend.tests.js
+++ b/test/specs/plugin.legend.tests.js
@@ -20,6 +20,12 @@ describe('Legend block tests', function() {
boxWidth: 40,
padding: 10,
generateLabels: jasmine.any(Function)
+ },
+
+ title: {
+ display: false,
+ position: 'center',
+ text: '',
}
});
});