From: Evert Timberg Date: Fri, 10 Jan 2020 23:28:51 +0000 (-0500) Subject: Add the ability to add a title to the legend (#6906) X-Git-Tag: v3.0.0-alpha~138 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d04cdfc21fdb15948184bfe3a6f242a0ceec2e08;p=thirdparty%2FChart.js.git 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 --- 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; - - var ctx = me.ctx; + const me = this; + const opts = me.options; + const labelOpts = opts.labels; + const display = opts.display; - 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: '', } }); });