mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-07 00:36:50 +01:00
* Update the legend object during beforeUpdate When the legend is updated in afterUpdate, the position is not updated before the layout system runs. When the event that the legend position was changed, the legend would not move to the new position in the layout system. This causes the chart to render incorrectly because the layout system handled the layout as if the legend was still at the original position. The update is split into two passes to ensure that labels still update correctly after datasets (#6968)
769 lines
20 KiB
JavaScript
769 lines
20 KiB
JavaScript
import defaults from '../core/core.defaults';
|
|
import Element from '../core/core.element';
|
|
import layouts from '../core/core.layouts';
|
|
import {drawPoint} from '../helpers/helpers.canvas';
|
|
import {callback as call, mergeIf, valueOrDefault, isNullOrUndef} from '../helpers/helpers.core';
|
|
import {toFont, toPadding} from '../helpers/helpers.options';
|
|
import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl';
|
|
|
|
/**
|
|
* @typedef { import("../platform/platform.base").IEvent } IEvent
|
|
*/
|
|
|
|
defaults.set('legend', {
|
|
display: true,
|
|
position: 'top',
|
|
align: 'center',
|
|
fullWidth: true,
|
|
reverse: false,
|
|
weight: 1000,
|
|
|
|
// a callback that will handle
|
|
onClick(e, legendItem, legend) {
|
|
const index = legendItem.datasetIndex;
|
|
const ci = legend.chart;
|
|
if (ci.isDatasetVisible(index)) {
|
|
ci.hide(index);
|
|
legendItem.hidden = true;
|
|
} else {
|
|
ci.show(index);
|
|
legendItem.hidden = false;
|
|
}
|
|
},
|
|
|
|
onHover: null,
|
|
onLeave: null,
|
|
|
|
labels: {
|
|
boxWidth: 40,
|
|
padding: 10,
|
|
// Generates labels shown in the legend
|
|
// Valid properties to return:
|
|
// text : text to display
|
|
// fillStyle : fill of coloured box
|
|
// strokeStyle: stroke of coloured box
|
|
// hidden : if this legend item refers to a hidden item
|
|
// lineCap : cap style for line
|
|
// lineDash
|
|
// lineDashOffset :
|
|
// lineJoin :
|
|
// lineWidth :
|
|
generateLabels(chart) {
|
|
const datasets = chart.data.datasets;
|
|
const options = chart.options.legend || {};
|
|
const usePointStyle = options.labels && options.labels.usePointStyle;
|
|
|
|
return chart._getSortedDatasetMetas().map((meta) => {
|
|
const style = meta.controller.getStyle(usePointStyle ? 0 : undefined);
|
|
|
|
return {
|
|
text: datasets[meta.index].label,
|
|
fillStyle: style.backgroundColor,
|
|
hidden: !meta.visible,
|
|
lineCap: style.borderCapStyle,
|
|
lineDash: style.borderDash,
|
|
lineDashOffset: style.borderDashOffset,
|
|
lineJoin: style.borderJoinStyle,
|
|
lineWidth: style.borderWidth,
|
|
strokeStyle: style.borderColor,
|
|
pointStyle: style.pointStyle,
|
|
rotation: style.rotation,
|
|
|
|
// Below is extra data used for toggling the datasets
|
|
datasetIndex: meta.index
|
|
};
|
|
}, this);
|
|
}
|
|
},
|
|
|
|
title: {
|
|
display: false,
|
|
position: 'center',
|
|
text: '',
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
export class Legend extends Element {
|
|
|
|
constructor(config) {
|
|
super();
|
|
|
|
Object.assign(this, config);
|
|
|
|
// Contains hit boxes for each dataset (in dataset order)
|
|
this.legendHitBoxes = [];
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
this._hoveredItem = null;
|
|
|
|
// Are we in doughnut mode which has a different data type
|
|
this.doughnutMode = false;
|
|
|
|
this.chart = config.chart;
|
|
this.options = config.options;
|
|
this.ctx = config.ctx;
|
|
this.legendItems = undefined;
|
|
this.columnWidths = undefined;
|
|
this.columnHeights = undefined;
|
|
this.lineWidths = undefined;
|
|
this._minSize = undefined;
|
|
this.maxHeight = undefined;
|
|
this.maxWidth = undefined;
|
|
this.top = undefined;
|
|
this.bottom = undefined;
|
|
this.left = undefined;
|
|
this.right = undefined;
|
|
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 || {};
|
|
let legendItems = call(labelOpts.generateLabels, [me.chart], me) || [];
|
|
|
|
if (labelOpts.filter) {
|
|
legendItems = legendItems.filter((item) => labelOpts.filter(item, me.chart.data));
|
|
}
|
|
|
|
if (me.options.reverse) {
|
|
legendItems.reverse();
|
|
}
|
|
|
|
me.legendItems = legendItems;
|
|
}
|
|
|
|
afterBuildLabels() {}
|
|
|
|
beforeFit() {}
|
|
|
|
fit() {
|
|
const me = this;
|
|
const opts = me.options;
|
|
const labelOpts = opts.labels;
|
|
const display = opts.display;
|
|
|
|
const ctx = me.ctx;
|
|
const labelFont = toFont(labelOpts.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 minSize = me._minSize;
|
|
const isHorizontal = me.isHorizontal();
|
|
const titleHeight = me._computeTitleHeight();
|
|
|
|
if (isHorizontal) {
|
|
minSize.width = me.maxWidth; // fill all the width
|
|
minSize.height = display ? 10 : 0;
|
|
} else {
|
|
minSize.width = display ? 10 : 0;
|
|
minSize.height = me.maxHeight; // fill all the height
|
|
}
|
|
|
|
// Increase sizes here
|
|
if (!display) {
|
|
me.width = minSize.width = me.height = minSize.height = 0;
|
|
return;
|
|
}
|
|
ctx.font = labelFont.string;
|
|
|
|
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;
|
|
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'middle';
|
|
|
|
me.legendItems.forEach((legendItem, i) => {
|
|
const width = 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;
|
|
}
|
|
|
|
// Store the hitbox width and height here. Final position will be updated in `draw`
|
|
hitboxes[i] = {
|
|
left: 0,
|
|
top: 0,
|
|
width,
|
|
height: itemHeight
|
|
};
|
|
|
|
lineWidths[lineWidths.length - 1] += width + labelOpts.padding;
|
|
});
|
|
|
|
minSize.height += 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;
|
|
}
|
|
|
|
// Get max width
|
|
currentColWidth = Math.max(currentColWidth, itemWidth);
|
|
currentColHeight += fontSize + vPadding;
|
|
|
|
// Store the hitbox width and height here. Final position will be updated in `draw`
|
|
hitboxes[i] = {
|
|
left: 0,
|
|
top: 0,
|
|
width: itemWidth,
|
|
height: itemHeight,
|
|
};
|
|
});
|
|
|
|
totalWidth += currentColWidth;
|
|
columnWidths.push(currentColWidth);
|
|
columnHeights.push(currentColHeight);
|
|
minSize.width += totalWidth;
|
|
}
|
|
|
|
me.width = minSize.width;
|
|
me.height = minSize.height;
|
|
}
|
|
|
|
afterFit() {}
|
|
|
|
// 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 lineDefault = defaults.elements.line;
|
|
const legendHeight = me.height;
|
|
const columnHeights = me.columnHeights;
|
|
const legendWidth = me.width;
|
|
const lineWidths = me.lineWidths;
|
|
|
|
if (!opts.display) {
|
|
return;
|
|
}
|
|
|
|
me.drawTitle();
|
|
const rtlHelper = getRtlAdapter(opts.rtl, me.left, me._minSize.width);
|
|
const ctx = me.ctx;
|
|
const labelFont = toFont(labelOpts.font);
|
|
const fontColor = labelFont.color;
|
|
const fontSize = labelFont.size;
|
|
let cursor;
|
|
|
|
// Canvas setup
|
|
ctx.textAlign = rtlHelper.textAlign('left');
|
|
ctx.textBaseline = 'middle';
|
|
ctx.lineWidth = 0.5;
|
|
ctx.strokeStyle = fontColor; // for strikethrough effect
|
|
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;
|
|
|
|
// current position
|
|
const drawLegendBox = function(x, y, legendItem) {
|
|
if (isNaN(boxWidth) || boxWidth <= 0 || isNaN(boxHeight) || boxHeight < 0) {
|
|
return;
|
|
}
|
|
|
|
// Set the ctx for the box
|
|
ctx.save();
|
|
|
|
const lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth);
|
|
ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor);
|
|
ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle);
|
|
ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset);
|
|
ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle);
|
|
ctx.lineWidth = lineWidth;
|
|
ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor);
|
|
|
|
if (ctx.setLineDash) {
|
|
// IE 9 and 10 do not support line dash
|
|
ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash));
|
|
}
|
|
|
|
if (labelOpts && 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 = {
|
|
radius: boxWidth * Math.SQRT2 / 2,
|
|
pointStyle: legendItem.pointStyle,
|
|
rotation: legendItem.rotation,
|
|
borderWidth: lineWidth
|
|
};
|
|
const centerX = rtlHelper.xPlus(x, boxWidth / 2);
|
|
const centerY = y + fontSize / 2;
|
|
|
|
// Draw pointStyle as legend symbol
|
|
drawPoint(ctx, drawOptions, centerX, centerY);
|
|
} else {
|
|
// Draw box as legend symbol
|
|
// Adjust position when boxHeight < fontSize (want it centered)
|
|
const yBoxTop = y + Math.max((fontSize - boxHeight) / 2, 0);
|
|
|
|
ctx.fillRect(rtlHelper.leftForLtr(x, boxWidth), yBoxTop, boxWidth, boxHeight);
|
|
if (lineWidth !== 0) {
|
|
ctx.strokeRect(rtlHelper.leftForLtr(x, boxWidth), yBoxTop, boxWidth, boxHeight);
|
|
}
|
|
}
|
|
|
|
ctx.restore();
|
|
};
|
|
|
|
const fillText = function(x, y, legendItem, textWidth) {
|
|
const halfFontSize = fontSize / 2;
|
|
const xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize);
|
|
const yMiddle = y + (height / 2);
|
|
ctx.fillText(legendItem.text, xLeft, yMiddle);
|
|
|
|
if (legendItem.hidden) {
|
|
// Strikethrough the text if hidden
|
|
ctx.beginPath();
|
|
ctx.lineWidth = 2;
|
|
ctx.moveTo(xLeft, yMiddle);
|
|
ctx.lineTo(rtlHelper.xPlus(xLeft, textWidth), yMiddle);
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
|
|
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,
|
|
line: 0
|
|
};
|
|
} else {
|
|
cursor = {
|
|
x: me.left + labelOpts.padding,
|
|
y: me.top + alignmentOffset(legendHeight, columnHeights[0]) + titleHeight,
|
|
line: 0
|
|
};
|
|
}
|
|
|
|
overrideTextDirection(me.ctx, opts.textDirection);
|
|
|
|
const itemHeight = height + labelOpts.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);
|
|
|
|
// 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;
|
|
cursor.line++;
|
|
x = cursor.x = me.left + alignmentOffset(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;
|
|
cursor.line++;
|
|
y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]);
|
|
}
|
|
|
|
const realX = rtlHelper.x(x);
|
|
|
|
drawLegendBox(realX, y, legendItem);
|
|
|
|
hitboxes[i].left = rtlHelper.leftForLtr(realX, hitboxes[i].width);
|
|
hitboxes[i].top = y;
|
|
|
|
// Fill the actual label
|
|
fillText(realX, y, legendItem, textWidth);
|
|
|
|
if (isHorizontal) {
|
|
cursor.x += width + labelOpts.padding;
|
|
} else {
|
|
cursor.y += itemHeight;
|
|
}
|
|
});
|
|
|
|
restoreTextDirection(me.ctx, opts.textDirection);
|
|
}
|
|
|
|
/**
|
|
* @protected
|
|
*/
|
|
drawTitle() {
|
|
const me = this;
|
|
const opts = me.options;
|
|
const titleOpts = opts.title;
|
|
const titleFont = toFont(titleOpts.font);
|
|
const titlePadding = toPadding(titleOpts.padding);
|
|
|
|
if (!titleOpts.display) {
|
|
return;
|
|
}
|
|
|
|
const rtlHelper = getRtlAdapter(opts.rtl, me.left, me._minSize.width);
|
|
const ctx = me.ctx;
|
|
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 = titleFont.color;
|
|
ctx.fillStyle = titleFont.color;
|
|
ctx.font = titleFont.string;
|
|
|
|
ctx.fillText(titleOpts.text, x, y);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_computeTitleHeight() {
|
|
const titleOpts = this.options.title;
|
|
const titleFont = toFont(titleOpts.font);
|
|
const titlePadding = toPadding(titleOpts.padding);
|
|
return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getLegendItemAt(x, y) {
|
|
const me = this;
|
|
let i, hitBox, lh;
|
|
|
|
if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) {
|
|
// See if we are touching one of the dataset boxes
|
|
lh = me.legendHitBoxes;
|
|
for (i = 0; i < lh.length; ++i) {
|
|
hitBox = lh[i];
|
|
|
|
if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) {
|
|
// Touching an element
|
|
return me.legendItems[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Handle an event
|
|
* @param {IEvent} e - The event to handle
|
|
*/
|
|
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 {
|
|
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 (hoveredItem) {
|
|
call(opts.onHover, [e, hoveredItem, me], me);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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
|
|
* @private
|
|
*/
|
|
_element: Legend,
|
|
|
|
beforeInit(chart) {
|
|
const legendOpts = chart.options.legend;
|
|
|
|
if (legendOpts) {
|
|
createNewLegendAndAttach(chart, legendOpts);
|
|
}
|
|
},
|
|
|
|
// 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 = chart.options.legend;
|
|
const legend = chart.legend;
|
|
|
|
if (legendOpts) {
|
|
mergeIf(legendOpts, defaults.legend);
|
|
|
|
if (legend) {
|
|
layouts.configure(chart, legend, legendOpts);
|
|
legend.options = legendOpts;
|
|
} else {
|
|
createNewLegendAndAttach(chart, legendOpts);
|
|
}
|
|
} else if (legend) {
|
|
layouts.removeBox(chart, legend);
|
|
delete chart.legend;
|
|
}
|
|
},
|
|
|
|
// 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();
|
|
}
|
|
},
|
|
|
|
|
|
afterEvent(chart, e) {
|
|
const legend = chart.legend;
|
|
if (legend) {
|
|
legend.handleEvent(e);
|
|
}
|
|
}
|
|
};
|