]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add the ability to add a title to the legend (#6906)
authorEvert Timberg <evert.timberg+github@gmail.com>
Fri, 10 Jan 2020 23:28:51 +0000 (18:28 -0500)
committerGitHub <noreply@github.com>
Fri, 10 Jan 2020 23:28:51 +0000 (18:28 -0500)
* 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

docs/configuration/legend.md
samples/legend/title.html [new file with mode: 0644]
samples/samples.js
src/plugins/plugin.legend.js
test/specs/plugin.legend.tests.js

index 35c75b2465fe6ff5c1d2c9a90beda5c6fcb910f8..ddffc43e3f18b0dcc5c1a0c5a5eaa792fc553a74 100644 (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.
diff --git a/samples/legend/title.html b/samples/legend/title.html
new file mode 100644 (file)
index 0000000..7f5e334
--- /dev/null
@@ -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>
index c91d7cf149f13a56cb0e2773e53ec680f1958d85..53084d5475494789f8fcbbb9bd395679aa8be435 100644 (file)
                items: [{
                        title: 'Positioning',
                        path: 'legend/positioning.html'
+               }, {
+                       title: 'Legend Title',
+                       path: 'legend/title.html'
                }, {
                        title: 'Point style',
                        path: 'legend/point-style.html'
index 6115a0ae0b7c8dca33a50b8e83364111dd35536e..2c7e7e76141a8e4682e49898ca597317af1adabc 100644 (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;
-
-               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
         */
index 23072b9c9aaf0ce24e7327a9a909a778628a0438..6fe15c161ec275187ccbaf59f881cb85555da0e5 100644 (file)
@@ -20,6 +20,12 @@ describe('Legend block tests', function() {
                                boxWidth: 40,
                                padding: 10,
                                generateLabels: jasmine.any(Function)
+                       },
+
+                       title: {
+                               display: false,
+                               position: 'center',
+                               text: '',
                        }
                });
        });