]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Legend plugin cleanup (#8109)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Mon, 14 Dec 2020 13:35:04 +0000 (15:35 +0200)
committerGitHub <noreply@github.com>
Mon, 14 Dec 2020 13:35:04 +0000 (15:35 +0200)
* Legend plugin cleanup

* cc1

* cc2

* cc3

* start/stop

src/helpers/helpers.extras.js
src/plugins/plugin.legend.js
src/plugins/plugin.title.js

index e821fb967eef4e4e2467dd727d134d905a852dc5..d62a7e19d66611cf7debc82931b9338a3938dab8 100644 (file)
@@ -38,3 +38,20 @@ export function throttled(fn, thisArg, updateFn) {
                }
        };
 }
+
+
+/**
+ * Converts 'start' to 'left', 'end' to 'right' and others to 'center'
+ * @param {string} align start, end, center
+ * @private
+ */
+export const _toLeftRightCenter = (align) => align === 'start' ? 'left' : align === 'end' ? 'right' : 'center';
+
+/**
+ * Returns `start`, `end` or `(start + end) / 2` depending on `align`
+ * @param {string} align start, end, center
+ * @param {number} start value for start
+ * @param {number} end value for end
+ * @private
+ */
+export const _alignStartEnd = (align, start, end) => align === 'start' ? start : align === 'end' ? end : (start + end) / 2;
index e90e58816950f4ab12362c71ae2fbec6401a925f..7c7fb8260e3ff9945b6fa0c98104c1e3feeb3878 100644 (file)
@@ -3,47 +3,39 @@ import Element from '../core/core.element';
 import layouts from '../core/core.layouts';
 import {drawPoint} from '../helpers/helpers.canvas';
 import {
-       callback as call, merge, valueOrDefault, isNullOrUndef, toFont, isObject,
+       callback as call, valueOrDefault, toFont, isObject,
        toPadding, getRtlAdapter, overrideTextDirection, restoreTextDirection,
        INFINITY
 } from '../helpers/index';
-
+import {_toLeftRightCenter, _alignStartEnd} from '../helpers/helpers.extras';
 /**
  * @typedef { import("../platform/platform.base").ChartEvent } ChartEvent
  */
 
-/**
- * Helper function to get the box width based on the usePointStyle option
- * @param {object} labelOpts - the label options on the legend
- * @param {number} fontSize - the label font size
- * @return {number} width of the color box area
- */
-function getBoxWidth(labelOpts, fontSize) {
-       const {boxWidth} = labelOpts;
-       return (labelOpts.usePointStyle && boxWidth > fontSize) || isNullOrUndef(boxWidth) ?
-               fontSize :
-               boxWidth;
-}
+const getBoxSize = (labelOpts, fontSize) => {
+       let {boxHeight = fontSize, boxWidth = fontSize} = labelOpts;
 
-/**
- * Helper function to get the box height
- * @param {object} labelOpts - the label options on the legend
- * @param {*} fontSize - the label font size
- * @return {number} height of the color box area
- */
-function getBoxHeight(labelOpts, fontSize) {
-       const {boxHeight} = labelOpts;
-       return (labelOpts.usePointStyle && boxHeight > fontSize) || isNullOrUndef(boxHeight) ?
-               fontSize :
-               boxHeight;
-}
+       if (labelOpts.usePointStyle) {
+               boxHeight = Math.min(boxHeight, fontSize);
+               boxWidth = Math.min(boxWidth, fontSize);
+       }
+
+       return {
+               boxWidth,
+               boxHeight,
+               itemHeight: Math.max(fontSize, boxHeight)
+       };
+};
 
 export class Legend extends Element {
 
+       /**
+        * @param {{ ctx: any; options: any; chart: any; }} config
+        */
        constructor(config) {
                super();
 
-               Object.assign(this, config);
+               this._added = false;
 
                // Contains hit boxes for each dataset (in dataset order)
                this.legendHitBoxes = [];
@@ -60,10 +52,8 @@ export class Legend extends Element {
                this.options = config.options;
                this.ctx = config.ctx;
                this.legendItems = undefined;
-               this.columnWidths = undefined;
-               this.columnHeights = undefined;
+               this.columnSizes = undefined;
                this.lineWidths = undefined;
-               this._minSize = undefined;
                this.maxHeight = undefined;
                this.maxWidth = undefined;
                this.top = undefined;
@@ -73,86 +63,37 @@ export class Legend extends Element {
                this.height = undefined;
                this.width = undefined;
                this._margins = undefined;
-               this.paddingTop = undefined;
-               this.paddingBottom = undefined;
-               this.paddingLeft = undefined;
-               this.paddingRight = undefined;
                this.position = undefined;
                this.weight = undefined;
                this.fullWidth = undefined;
        }
 
-       // These methods are ordered by lifecycle. Utilities then follow.
-       // Any function defined here is inherited by all legend types.
-       // Any function can be extended by the legend type
-
-       beforeUpdate() {}
-
        update(maxWidth, maxHeight, margins) {
                const me = this;
 
-               // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
-               me.beforeUpdate();
-
-               // Absorb the master measurements
                me.maxWidth = maxWidth;
                me.maxHeight = maxHeight;
                me._margins = margins;
 
-               // Dimensions
-               me.beforeSetDimensions();
                me.setDimensions();
-               me.afterSetDimensions();
-               // Labels
-               me.beforeBuildLabels();
                me.buildLabels();
-               me.afterBuildLabels();
-
-               // Fit
-               me.beforeFit();
                me.fit();
-               me.afterFit();
-               //
-               me.afterUpdate();
        }
 
-       afterUpdate() {}
-
-       beforeSetDimensions() {}
-
        setDimensions() {
                const me = this;
-               // Set the unconstrained dimension before label rotation
+
                if (me.isHorizontal()) {
-                       // Reset position before calculating rotation
                        me.width = me.maxWidth;
                        me.left = 0;
                        me.right = me.width;
                } else {
                        me.height = me.maxHeight;
-
-                       // Reset position before calculating rotation
                        me.top = 0;
                        me.bottom = me.height;
                }
-
-               // Reset padding
-               me.paddingLeft = 0;
-               me.paddingTop = 0;
-               me.paddingRight = 0;
-               me.paddingBottom = 0;
-
-               // Reset minSize
-               me._minSize = {
-                       width: 0,
-                       height: 0
-               };
        }
 
-       afterSetDimensions() {}
-
-       beforeBuildLabels() {}
-
        buildLabels() {
                const me = this;
                const labelOpts = me.options.labels || {};
@@ -173,151 +114,134 @@ export class Legend extends Element {
                me.legendItems = legendItems;
        }
 
-       afterBuildLabels() {}
-
-       beforeFit() {}
-
        fit() {
                const me = this;
-               const opts = me.options;
-               const labelOpts = opts.labels;
-               const display = opts.display;
+               const {options, ctx} = me;
 
                // The legend may not be displayed for a variety of reasons including
                // the fact that the defaults got set to `false`.
                // When the legend is not displayed, there are no guarantees that the options
                // are correctly formatted so we need to bail out as early as possible.
-               const minSize = me._minSize;
-               if (!display) {
-                       me.width = minSize.width = me.height = minSize.height = 0;
+               if (!options.display) {
+                       me.width = me.height = 0;
                        return;
                }
 
-               const ctx = me.ctx;
+               const labelOpts = options.labels;
                const labelFont = toFont(labelOpts.font, me.chart.options.font);
                const fontSize = labelFont.size;
-               const boxWidth = getBoxWidth(labelOpts, fontSize);
-               const boxHeight = getBoxHeight(labelOpts, fontSize);
-               const itemHeight = Math.max(boxHeight, fontSize);
-
-               // Reset hit boxes
-               const hitboxes = me.legendHitBoxes = [];
-               const isHorizontal = me.isHorizontal();
                const titleHeight = me._computeTitleHeight();
+               const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize);
 
-               if (isHorizontal) {
-                       minSize.width = me.maxWidth; // fill all the width
-                       minSize.height = display ? 10 : 0;
+               let width, height;
+
+               ctx.font = labelFont.string;
+
+               if (me.isHorizontal()) {
+                       width = me.maxWidth; // fill all the width
+                       height = me._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10;
                } else {
-                       minSize.width = display ? 10 : 0;
-                       minSize.height = me.maxHeight; // fill all the height
+                       height = me.maxHeight; // fill all the height
+                       width = me._fitCols(titleHeight, fontSize, boxWidth, itemHeight) + 10;
                }
 
-               ctx.font = labelFont.string;
+               me.width = Math.min(width, options.maxWidth || INFINITY);
+               me.height = Math.min(height, options.maxHeight || INFINITY);
+       }
 
-               if (isHorizontal) {
-                       // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one
-                       const lineWidths = me.lineWidths = [0];
-                       let totalHeight = titleHeight;
+       /**
+        * @private
+        */
+       _fitRows(titleHeight, fontSize, boxWidth, itemHeight) {
+               const me = this;
+               const {ctx, maxWidth} = me;
+               const padding = me.options.labels.padding;
+               const hitboxes = me.legendHitBoxes = [];
+               // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one
+               const lineWidths = me.lineWidths = [0];
+               let totalHeight = titleHeight;
 
-                       ctx.textAlign = 'left';
-                       ctx.textBaseline = 'middle';
+               ctx.textAlign = 'left';
+               ctx.textBaseline = 'middle';
 
-                       me.legendItems.forEach((legendItem, i) => {
-                               const width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
+               me.legendItems.forEach((legendItem, i) => {
+                       const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
 
-                               if (i === 0 || lineWidths[lineWidths.length - 1] + width + 2 * labelOpts.padding > minSize.width) {
-                                       totalHeight += itemHeight + labelOpts.padding;
-                                       lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0;
-                               }
+                       if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) {
+                               totalHeight += itemHeight + padding;
+                               lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0;
+                       }
 
-                               // Store the hitbox width and height here. Final position will be updated in `draw`
-                               hitboxes[i] = {
-                                       left: 0,
-                                       top: 0,
-                                       width,
-                                       height: itemHeight
-                               };
+                       // Store the hitbox width and height here. Final position will be updated in `draw`
+                       hitboxes[i] = {left: 0, top: 0, width: itemWidth, height: itemHeight};
 
-                               lineWidths[lineWidths.length - 1] += width + labelOpts.padding;
-                       });
+                       lineWidths[lineWidths.length - 1] += itemWidth + padding;
 
-                       minSize.height += totalHeight;
+               });
+               return totalHeight;
+       }
 
-               } else {
-                       const vPadding = labelOpts.padding;
-                       const columnWidths = me.columnWidths = [];
-                       const columnHeights = me.columnHeights = [];
-                       let totalWidth = labelOpts.padding;
-                       let currentColWidth = 0;
-                       let currentColHeight = 0;
-
-                       const heightLimit = minSize.height - titleHeight;
-                       me.legendItems.forEach((legendItem, i) => {
-                               const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
-
-                               // If too tall, go to new column
-                               if (i > 0 && currentColHeight + fontSize + 2 * vPadding > heightLimit) {
-                                       totalWidth += currentColWidth + labelOpts.padding;
-                                       columnWidths.push(currentColWidth); // previous column width
-                                       columnHeights.push(currentColHeight);
-                                       currentColWidth = 0;
-                                       currentColHeight = 0;
-                               }
+       _fitCols(titleHeight, fontSize, boxWidth, itemHeight) {
+               const me = this;
+               const {ctx, maxHeight} = me;
+               const padding = me.options.labels.padding;
+               const hitboxes = me.legendHitBoxes = [];
+               const columnSizes = me.columnSizes = [];
+               let totalWidth = padding;
+               let currentColWidth = 0;
+               let currentColHeight = 0;
 
-                               // Get max width
-                               currentColWidth = Math.max(currentColWidth, itemWidth);
-                               currentColHeight += fontSize + vPadding;
+               const heightLimit = maxHeight - titleHeight;
+               me.legendItems.forEach((legendItem, i) => {
+                       const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
 
-                               // Store the hitbox width and height here. Final position will be updated in `draw`
-                               hitboxes[i] = {
-                                       left: 0,
-                                       top: 0,
-                                       width: itemWidth,
-                                       height: itemHeight,
-                               };
-                       });
+                       // If too tall, go to new column
+                       if (i > 0 && currentColHeight + fontSize + 2 * padding > heightLimit) {
+                               totalWidth += currentColWidth + padding;
+                               columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size
+                               currentColWidth = currentColHeight = 0;
+                       }
 
-                       totalWidth += currentColWidth;
-                       columnWidths.push(currentColWidth);
-                       columnHeights.push(currentColHeight);
-                       minSize.width += totalWidth;
-               }
+                       // Get max width
+                       currentColWidth = Math.max(currentColWidth, itemWidth);
+                       currentColHeight += fontSize + padding;
 
-               me.width = Math.min(minSize.width, opts.maxWidth || INFINITY);
-               me.height = Math.min(minSize.height, opts.maxHeight || INFINITY);
-       }
+                       // Store the hitbox width and height here. Final position will be updated in `draw`
+                       hitboxes[i] = {left: 0, top: 0, width: itemWidth, height: itemHeight};
+               });
 
-       afterFit() {}
+               totalWidth += currentColWidth;
+               columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size
+
+               return totalWidth;
+       }
 
-       // Shared Methods
        isHorizontal() {
                return this.options.position === 'top' || this.options.position === 'bottom';
        }
 
-       // Actually draw the legend on the canvas
        draw() {
-               const me = this;
-               const opts = me.options;
-               const labelOpts = opts.labels;
-               const defaultColor = defaults.color;
-               const legendHeight = me.height;
-               const columnHeights = me.columnHeights;
-               const legendWidth = me.width;
-               const lineWidths = me.lineWidths;
-
-               if (!opts.display) {
-                       return;
+               if (this.options.display) {
+                       this._draw();
                }
+       }
 
-               me.drawTitle();
-               const rtlHelper = getRtlAdapter(opts.rtl, me.left, me._minSize.width);
-               const ctx = me.ctx;
+       /**
+        * @private
+        */
+       _draw() {
+               const me = this;
+               const {options: opts, height: legendHeight, width: legendWidth, columnSizes, lineWidths, ctx, legendHitBoxes} = me;
+               const {align, labels: labelOpts} = opts;
+               const defaultColor = defaults.color;
+               const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width);
                const labelFont = toFont(labelOpts.font, me.chart.options.font);
-               const fontColor = labelOpts.color || defaultColor;
+               const {color: fontColor, padding} = labelOpts;
                const fontSize = labelFont.size;
                let cursor;
 
+               me.drawTitle();
+
                // Canvas setup
                ctx.textAlign = rtlHelper.textAlign('left');
                ctx.textBaseline = 'middle';
@@ -326,10 +250,7 @@ export class Legend extends Element {
                ctx.fillStyle = fontColor; // render in correct colour
                ctx.font = labelFont.string;
 
-               const boxWidth = getBoxWidth(labelOpts, fontSize);
-               const boxHeight = getBoxHeight(labelOpts, fontSize);
-               const height = Math.max(fontSize, boxHeight);
-               const hitboxes = me.legendHitBoxes;
+               const {boxWidth, boxHeight, itemHeight} = getBoxSize(labelOpts, fontSize);
 
                // current position
                const drawLegendBox = function(x, y, legendItem) {
@@ -350,7 +271,7 @@ export class Legend extends Element {
 
                        ctx.setLineDash(valueOrDefault(legendItem.lineDash, []));
 
-                       if (labelOpts && labelOpts.usePointStyle) {
+                       if (labelOpts.usePointStyle) {
                                // Recalculate x and y for drawPoint() because its expecting
                                // x and y to be center of figure (instead of top left)
                                const drawOptions = {
@@ -381,7 +302,7 @@ export class Legend extends Element {
                const fillText = function(x, y, legendItem, textWidth) {
                        const halfFontSize = fontSize / 2;
                        const xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize);
-                       const yMiddle = y + (height / 2);
+                       const yMiddle = y + (itemHeight / 2);
                        ctx.fillText(legendItem.text, xLeft, yMiddle);
 
                        if (legendItem.hidden) {
@@ -394,74 +315,60 @@ export class Legend extends Element {
                        }
                };
 
-               const alignmentOffset = function(dimension, blockSize) {
-                       switch (opts.align) {
-                       case 'start':
-                               return labelOpts.padding;
-                       case 'end':
-                               return dimension - blockSize;
-                       default: // center
-                               return (dimension - blockSize + labelOpts.padding) / 2;
-                       }
-               };
-
                // Horizontal
                const isHorizontal = me.isHorizontal();
                const titleHeight = this._computeTitleHeight();
                if (isHorizontal) {
                        cursor = {
-                               x: me.left + alignmentOffset(legendWidth, lineWidths[0]),
-                               y: me.top + labelOpts.padding + titleHeight,
+                               x: me.left + _alignStartEnd(align, padding, legendWidth - lineWidths[0]),
+                               y: me.top + padding + titleHeight,
                                line: 0
                        };
                } else {
                        cursor = {
-                               x: me.left + labelOpts.padding,
-                               y: me.top + alignmentOffset(legendHeight, columnHeights[0]) + titleHeight,
+                               x: me.left + padding,
+                               y: me.top + _alignStartEnd(align, padding, legendHeight - columnSizes[0].height) + titleHeight,
                                line: 0
                        };
                }
 
                overrideTextDirection(me.ctx, opts.textDirection);
 
-               const itemHeight = height + labelOpts.padding;
+               const lineHeight = itemHeight + padding;
                me.legendItems.forEach((legendItem, i) => {
                        const textWidth = ctx.measureText(legendItem.text).width;
                        const width = boxWidth + (fontSize / 2) + textWidth;
                        let x = cursor.x;
                        let y = cursor.y;
 
-                       rtlHelper.setWidth(me._minSize.width);
+                       rtlHelper.setWidth(me.width);
 
-                       // Use (me.left + me._minSize.width) and (me.top + me._minSize.height)
-                       // instead of me.right and me.bottom because me.width and me.height
-                       // may have been changed since me._minSize was calculated
                        if (isHorizontal) {
-                               if (i > 0 && x + width + labelOpts.padding > me.left + me._minSize.width) {
-                                       y = cursor.y += itemHeight;
+                               if (i > 0 && x + width + padding > me.right) {
+                                       y = cursor.y += lineHeight;
                                        cursor.line++;
-                                       x = cursor.x = me.left + alignmentOffset(legendWidth, lineWidths[cursor.line]);
+                                       x = cursor.x = me.left + _alignStartEnd(align, padding, legendWidth - lineWidths[cursor.line]);
                                }
-                       } else if (i > 0 && y + itemHeight > me.top + me._minSize.height) {
-                               x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding;
+                       } else if (i > 0 && y + lineHeight > me.bottom) {
+                               x = cursor.x = x + columnSizes[cursor.line].width + padding;
                                cursor.line++;
-                               y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]);
+                               y = cursor.y = me.top + _alignStartEnd(align, padding, legendHeight - columnSizes[cursor.line].height);
                        }
 
                        const realX = rtlHelper.x(x);
 
                        drawLegendBox(realX, y, legendItem);
 
-                       hitboxes[i].left = rtlHelper.leftForLtr(realX, hitboxes[i].width);
-                       hitboxes[i].top = y;
+                       legendHitBoxes[i].left = rtlHelper.leftForLtr(realX, legendHitBoxes[i].width);
+                       legendHitBoxes[i].top = y;
 
                        // Fill the actual label
                        fillText(realX, y, legendItem, textWidth);
 
                        if (isHorizontal) {
-                               cursor.x += width + labelOpts.padding;
+                               cursor.x += width + padding;
                        } else {
-                               cursor.y += itemHeight;
+                               cursor.y += lineHeight;
                        }
                });
 
@@ -482,11 +389,9 @@ export class Legend extends Element {
                        return;
                }
 
-               const rtlHelper = getRtlAdapter(opts.rtl, me.left, me._minSize.width);
+               const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width);
                const ctx = me.ctx;
                const position = titleOpts.position;
-               let x, textAlign;
-
                const halfFontSize = titleFont.size / 2;
                let y = me.top + titlePadding.top + halfFontSize;
 
@@ -498,52 +403,19 @@ export class Legend extends Element {
                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;
-                       }
+                       left = _alignStartEnd(opts.align, left, me.right - maxWidth);
                } 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;
-                       }
+                       const maxHeight = me.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0);
+                       y = _alignStartEnd(opts.align, y, me.height - maxHeight);
                }
 
                // 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;
-               }
+               const x = _alignStartEnd(position, left, left + maxWidth);
 
                // Canvas setup
-               ctx.textAlign = rtlHelper.textAlign(textAlign);
+               ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position));
                ctx.textBaseline = 'middle';
                ctx.strokeStyle = titleOpts.color;
                ctx.fillStyle = titleOpts.color;
@@ -592,73 +464,53 @@ export class Legend extends Element {
        handleEvent(e) {
                const me = this;
                const opts = me.options;
-               const type = e.type === 'mouseup' ? 'click' : e.type;
-
-               if (type === 'mousemove') {
-                       if (!opts.onHover && !opts.onLeave) {
-                               return;
-                       }
-               } else if (type === 'click') {
-                       if (!opts.onClick) {
-                               return;
-                       }
-               } else {
+               if (!isListened(e.type, opts)) {
                        return;
                }
 
                // Chart event already has relative position in it
                const hoveredItem = me._getLegendItemAt(e.x, e.y);
 
-               if (type === 'click') {
-                       if (hoveredItem) {
-                               call(opts.onClick, [e, hoveredItem, me], me);
-                       }
-               } else {
-                       if (opts.onLeave && hoveredItem !== me._hoveredItem) {
-                               if (me._hoveredItem) {
-                                       call(opts.onLeave, [e, me._hoveredItem, me], me);
-                               }
-                               me._hoveredItem = hoveredItem;
+               if (e.type === 'mousemove') {
+                       const previous = me._hoveredItem;
+                       if (previous && previous !== hoveredItem) {
+                               call(opts.onLeave, [e, previous, me], me);
                        }
 
+                       me._hoveredItem = hoveredItem;
+
                        if (hoveredItem) {
                                call(opts.onHover, [e, hoveredItem, me], me);
                        }
+               } else if (hoveredItem) {
+                       call(opts.onClick, [e, hoveredItem, me], me);
                }
        }
 }
 
-function resolveOptions(options) {
-       return options !== false && merge(Object.create(null), [defaults.plugins.legend, options]);
-}
-
-function createNewLegendAndAttach(chart, legendOpts) {
-       const legend = new Legend({
-               ctx: chart.ctx,
-               options: legendOpts,
-               chart
-       });
-
-       layouts.configure(chart, legend, legendOpts);
-       layouts.addBox(chart, legend);
-       chart.legend = legend;
+function isListened(type, opts) {
+       if (type === 'mousemove' && (opts.onHover || opts.onLeave)) {
+               return true;
+       }
+       if (opts.onClick && (type === 'click' || type === 'mouseup')) {
+               return true;
+       }
+       return false;
 }
 
 export default {
        id: 'legend',
 
        /**
-        * Backward compatibility: since 2.1.5, the legend is registered as a plugin, making
-        * Chart.Legend obsolete. To avoid a breaking change, we export the Legend as part of
-        * the plugin, which one will be re-exposed in the chart.js file.
-        * https://github.com/chartjs/Chart.js/pull/2640
+        * For tests
         * @private
         */
        _element: Legend,
 
-       start(chart) {
-               const legendOpts = resolveOptions(chart.options.plugins.legend);
-               createNewLegendAndAttach(chart, legendOpts);
+       start(chart, _args, options) {
+               const legend = chart.legend = new Legend({ctx: chart.ctx, options, chart});
+               layouts.configure(chart, legend, options);
+               layouts.addBox(chart, legend);
        },
 
        stop(chart) {
@@ -669,37 +521,21 @@ export default {
        // During the beforeUpdate step, the layout configuration needs to run
        // This ensures that if the legend position changes (via an option update)
        // the layout system respects the change. See https://github.com/chartjs/Chart.js/issues/7527
-       beforeUpdate(chart) {
-               const legendOpts = resolveOptions(chart.options.plugins.legend);
+       beforeUpdate(chart, _args, options) {
                const legend = chart.legend;
-
-               if (legendOpts) {
-                       if (legend) {
-                               layouts.configure(chart, legend, legendOpts);
-                               legend.options = legendOpts;
-                       } else {
-                               createNewLegendAndAttach(chart, legendOpts);
-                       }
-               } else if (legend) {
-                       layouts.removeBox(chart, legend);
-                       delete chart.legend;
-               }
+               layouts.configure(chart, legend, options);
+               legend.options = options;
        },
 
        // The labels need to be built after datasets are updated to ensure that colors
        // and other styling are correct. See https://github.com/chartjs/Chart.js/issues/6968
        afterUpdate(chart) {
-               if (chart.legend) {
-                       chart.legend.buildLabels();
-               }
+               chart.legend.buildLabels();
        },
 
 
        afterEvent(chart, args) {
-               const legend = chart.legend;
-               if (legend) {
-                       legend.handleEvent(args.event);
-               }
+               chart.legend.handleEvent(args.event);
        },
 
        defaults: {
index 04e59629b90aca56a6f20db59ba3f18581801337..eebb70c7f827a65df08b566397b66c35a3d27c20 100644 (file)
@@ -1,20 +1,18 @@
 import Element from '../core/core.element';
 import layouts from '../core/core.layouts';
 import {PI, isArray, toPadding, toFont} from '../helpers';
-
-const toLeftRightCenter = (align) => align === 'start' ? 'left' : align === 'end' ? 'right' : 'center';
-const alignStartEnd = (align, start, end) => align === 'start' ? start : align === 'end' ? end : (start + end) / 2;
+import {_toLeftRightCenter, _alignStartEnd} from '../helpers/helpers.extras';
 
 export class Title extends Element {
+       /**
+        * @param {{ ctx: any; options: any; chart: any; }} config
+        */
        constructor(config) {
                super();
 
-               Object.assign(this, config);
-
                this.chart = config.chart;
                this.options = config.options;
                this.ctx = config.ctx;
-               this._margins = undefined;
                this._padding = undefined;
                this.top = undefined;
                this.bottom = undefined;
@@ -22,58 +20,35 @@ export class Title extends Element {
                this.right = undefined;
                this.width = undefined;
                this.height = undefined;
-               this.maxWidth = undefined;
-               this.maxHeight = undefined;
                this.position = undefined;
                this.weight = undefined;
                this.fullWidth = undefined;
        }
 
-       update(maxWidth, maxHeight, margins) {
-               const me = this;
-
-               me.maxWidth = maxWidth;
-               me.maxHeight = maxHeight;
-               me._margins = margins;
-
-               me.setDimensions();
-
-               me.fit();
-       }
-
-       setDimensions() {
-               const me = this;
-               // Set the unconstrained dimension before label rotation
-               if (me.isHorizontal()) {
-                       // Reset position before calculating rotation
-                       me.width = me.maxWidth;
-                       me.left = 0;
-                       me.right = me.width;
-               } else {
-                       me.height = me.maxHeight;
-
-                       // Reset position before calculating rotation
-                       me.top = 0;
-                       me.bottom = me.height;
-               }
-       }
-
-       fit() {
+       update(maxWidth, maxHeight) {
                const me = this;
                const opts = me.options;
-               const minSize = {};
-               const isHorizontal = me.isHorizontal();
+
+               me.left = 0;
+               me.top = 0;
 
                if (!opts.display) {
-                       me.width = minSize.width = me.height = minSize.height = 0;
+                       me.width = me.height = me.right = me.bottom = 0;
                        return;
                }
 
+               me.width = me.right = maxWidth;
+               me.height = me.bottom = maxHeight;
+
                const lineCount = isArray(opts.text) ? opts.text.length : 1;
                me._padding = toPadding(opts.padding);
                const textSize = lineCount * toFont(opts.font, me.chart.options.font).lineHeight + me._padding.height;
-               me.width = minSize.width = isHorizontal ? me.maxWidth : textSize;
-               me.height = minSize.height = isHorizontal ? textSize : me.maxHeight;
+
+               if (me.isHorizontal()) {
+                       me.height = textSize;
+               } else {
+                       me.width = textSize;
+               }
        }
 
        isHorizontal() {
@@ -88,17 +63,17 @@ export class Title extends Element {
                let maxWidth, titleX, titleY;
 
                if (this.isHorizontal()) {
-                       titleX = alignStartEnd(align, left, right);
+                       titleX = _alignStartEnd(align, left, right);
                        titleY = top + offset;
                        maxWidth = right - left;
                } else {
                        if (options.position === 'left') {
                                titleX = left + offset;
-                               titleY = alignStartEnd(align, bottom, top);
+                               titleY = _alignStartEnd(align, bottom, top);
                                rotation = PI * -0.5;
                        } else {
                                titleX = right - offset;
-                               titleY = alignStartEnd(align, top, bottom);
+                               titleY = _alignStartEnd(align, top, bottom);
                                rotation = PI * 0.5;
                        }
                        maxWidth = bottom - top;
@@ -127,7 +102,7 @@ export class Title extends Element {
 
                ctx.translate(titleX, titleY);
                ctx.rotate(rotation);
-               ctx.textAlign = toLeftRightCenter(opts.align);
+               ctx.textAlign = _toLeftRightCenter(opts.align);
                ctx.textBaseline = 'middle';
 
                const text = opts.text;