chartjs-plugin-annotation
Version:
Annotations for Chart.js
1,640 lines (1,497 loc) • 91 kB
JavaScript
/*!
* chartjs-plugin-annotation v3.1.0
* https://www.chartjs.org/chartjs-plugin-annotation/index
* (c) 2024 chartjs-plugin-annotation Contributors
* Released under the MIT License
*/
import { Element, DoughnutController, defaults, Animations, Chart } from 'chart.js';
import { distanceBetweenPoints, toRadians, isObject, valueOrDefault, defined, isFunction, callback, isArray, toFont, addRoundedRectPath, toTRBLCorners, QUARTER_PI, PI, HALF_PI, TWO_THIRDS_PI, TAU, isNumber, RAD_PER_DEG, toPadding, isFinite, getAngleFromPoint, toDegrees, clipArea, unclipArea } from 'chart.js/helpers';
/**
* @typedef { import("chart.js").ChartEvent } ChartEvent
* @typedef { import('../../types/element').AnnotationElement } AnnotationElement
*/
const interaction = {
modes: {
/**
* Point mode returns all elements that hit test based on the event position
* @param {AnnotationElement[]} visibleElements - annotation elements which are visible
* @param {ChartEvent} event - the event we are find things at
* @return {AnnotationElement[]} - elements that are found
*/
point(visibleElements, event) {
return filterElements(visibleElements, event, {intersect: true});
},
/**
* Nearest mode returns the element closest to the event position
* @param {AnnotationElement[]} visibleElements - annotation elements which are visible
* @param {ChartEvent} event - the event we are find things at
* @param {Object} options - interaction options to use
* @return {AnnotationElement[]} - elements that are found (only 1 element)
*/
nearest(visibleElements, event, options) {
return getNearestItem(visibleElements, event, options);
},
/**
* x mode returns the elements that hit-test at the current x coordinate
* @param {AnnotationElement[]} visibleElements - annotation elements which are visible
* @param {ChartEvent} event - the event we are find things at
* @param {Object} options - interaction options to use
* @return {AnnotationElement[]} - elements that are found
*/
x(visibleElements, event, options) {
return filterElements(visibleElements, event, {intersect: options.intersect, axis: 'x'});
},
/**
* y mode returns the elements that hit-test at the current y coordinate
* @param {AnnotationElement[]} visibleElements - annotation elements which are visible
* @param {ChartEvent} event - the event we are find things at
* @param {Object} options - interaction options to use
* @return {AnnotationElement[]} - elements that are found
*/
y(visibleElements, event, options) {
return filterElements(visibleElements, event, {intersect: options.intersect, axis: 'y'});
}
}
};
/**
* Returns all elements that hit test based on the event position
* @param {AnnotationElement[]} visibleElements - annotation elements which are visible
* @param {ChartEvent} event - the event we are find things at
* @param {Object} options - interaction options to use
* @return {AnnotationElement[]} - elements that are found
*/
function getElements(visibleElements, event, options) {
const mode = interaction.modes[options.mode] || interaction.modes.nearest;
return mode(visibleElements, event, options);
}
function inRangeByAxis(element, event, axis) {
if (axis !== 'x' && axis !== 'y') {
return element.inRange(event.x, event.y, 'x', true) || element.inRange(event.x, event.y, 'y', true);
}
return element.inRange(event.x, event.y, axis, true);
}
function getPointByAxis(event, center, axis) {
if (axis === 'x') {
return {x: event.x, y: center.y};
} else if (axis === 'y') {
return {x: center.x, y: event.y};
}
return center;
}
function filterElements(visibleElements, event, options) {
return visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, options.axis));
}
function getNearestItem(visibleElements, event, options) {
let minDistance = Number.POSITIVE_INFINITY;
return filterElements(visibleElements, event, options)
.reduce((nearestItems, element) => {
const center = element.getCenterPoint();
const evenPoint = getPointByAxis(event, center, options.axis);
const distance = distanceBetweenPoints(event, evenPoint);
if (distance < minDistance) {
nearestItems = [element];
minDistance = distance;
} else if (distance === minDistance) {
// Can have multiple items at the same distance in which case we sort by size
nearestItems.push(element);
}
return nearestItems;
}, [])
.sort((a, b) => a._index - b._index)
.slice(0, 1); // return only the top item;
}
/**
* @typedef {import('chart.js').Point} Point
*/
/**
* Rotate a `point` relative to `center` point by `angle`
* @param {Point} point - the point to rotate
* @param {Point} center - center point for rotation
* @param {number} angle - angle for rotation, in radians
* @returns {Point} rotated point
*/
function rotated(point, center, angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const cx = center.x;
const cy = center.y;
return {
x: cx + cos * (point.x - cx) - sin * (point.y - cy),
y: cy + sin * (point.x - cx) + cos * (point.y - cy)
};
}
const isOlderPart = (act, req) => req > act || (act.length > req.length && act.slice(0, req.length) === req);
/**
* @typedef { import('chart.js').Point } Point
* @typedef { import('chart.js').InteractionAxis } InteractionAxis
* @typedef { import('../../types/element').AnnotationElement } AnnotationElement
*/
const EPSILON = 0.001;
const clamp = (x, from, to) => Math.min(to, Math.max(from, x));
/**
* @param {{value: number, start: number, end: number}} limit
* @param {number} hitSize
* @returns {boolean}
*/
const inLimit = (limit, hitSize) => limit.value >= limit.start - hitSize && limit.value <= limit.end + hitSize;
/**
* @param {Object} obj
* @param {number} from
* @param {number} to
* @returns {Object}
*/
function clampAll(obj, from, to) {
for (const key of Object.keys(obj)) {
obj[key] = clamp(obj[key], from, to);
}
return obj;
}
/**
* @param {Point} point
* @param {Point} center
* @param {number} radius
* @param {number} hitSize
* @returns {boolean}
*/
function inPointRange(point, center, radius, hitSize) {
if (!point || !center || radius <= 0) {
return false;
}
return (Math.pow(point.x - center.x, 2) + Math.pow(point.y - center.y, 2)) <= Math.pow(radius + hitSize, 2);
}
/**
* @param {Point} point
* @param {{x: number, y: number, x2: number, y2: number}} rect
* @param {InteractionAxis} axis
* @param {{borderWidth: number, hitTolerance: number}} hitsize
* @returns {boolean}
*/
function inBoxRange(point, {x, y, x2, y2}, axis, {borderWidth, hitTolerance}) {
const hitSize = (borderWidth + hitTolerance) / 2;
const inRangeX = point.x >= x - hitSize - EPSILON && point.x <= x2 + hitSize + EPSILON;
const inRangeY = point.y >= y - hitSize - EPSILON && point.y <= y2 + hitSize + EPSILON;
if (axis === 'x') {
return inRangeX;
} else if (axis === 'y') {
return inRangeY;
}
return inRangeX && inRangeY;
}
/**
* @param {Point} point
* @param {rect: {x: number, y: number, x2: number, y2: number}, center: {x: number, y: number}} element
* @param {InteractionAxis} axis
* @param {{rotation: number, borderWidth: number, hitTolerance: number}}
* @returns {boolean}
*/
function inLabelRange(point, {rect, center}, axis, {rotation, borderWidth, hitTolerance}) {
const rotPoint = rotated(point, center, toRadians(-rotation));
return inBoxRange(rotPoint, rect, axis, {borderWidth, hitTolerance});
}
/**
* @param {AnnotationElement} element
* @param {boolean} useFinalPosition
* @returns {Point}
*/
function getElementCenterPoint(element, useFinalPosition) {
const {centerX, centerY} = element.getProps(['centerX', 'centerY'], useFinalPosition);
return {x: centerX, y: centerY};
}
/**
* @param {string} pkg
* @param {string} min
* @param {string} ver
* @param {boolean} [strict=true]
* @returns {boolean}
*/
function requireVersion(pkg, min, ver, strict = true) {
const parts = ver.split('.');
let i = 0;
for (const req of min.split('.')) {
const act = parts[i++];
if (parseInt(req, 10) < parseInt(act, 10)) {
break;
}
if (isOlderPart(act, req)) {
if (strict) {
throw new Error(`${pkg} v${ver} is not supported. v${min} or newer is required.`);
} else {
return false;
}
}
}
return true;
}
const isPercentString = (s) => typeof s === 'string' && s.endsWith('%');
const toPercent = (s) => parseFloat(s) / 100;
const toPositivePercent = (s) => clamp(toPercent(s), 0, 1);
const boxAppering = (x, y) => ({x, y, x2: x, y2: y, width: 0, height: 0});
const defaultInitAnimation = {
box: (properties) => boxAppering(properties.centerX, properties.centerY),
doughnutLabel: (properties) => boxAppering(properties.centerX, properties.centerY),
ellipse: (properties) => ({centerX: properties.centerX, centerY: properties.centerX, radius: 0, width: 0, height: 0}),
label: (properties) => boxAppering(properties.centerX, properties.centerY),
line: (properties) => boxAppering(properties.x, properties.y),
point: (properties) => ({centerX: properties.centerX, centerY: properties.centerY, radius: 0, width: 0, height: 0}),
polygon: (properties) => boxAppering(properties.centerX, properties.centerY)
};
/**
* @typedef { import('chart.js').FontSpec } FontSpec
* @typedef { import('chart.js').Point } Point
* @typedef { import('chart.js').Padding } Padding
* @typedef { import('../../types/element').AnnotationBoxModel } AnnotationBoxModel
* @typedef { import('../../types/element').AnnotationElement } AnnotationElement
* @typedef { import('../../types/options').AnnotationPointCoordinates } AnnotationPointCoordinates
* @typedef { import('../../types/label').CoreLabelOptions } CoreLabelOptions
* @typedef { import('../../types/label').LabelPositionObject } LabelPositionObject
*/
/**
* @param {number} size
* @param {number|string} position
* @returns {number}
*/
function getRelativePosition(size, position) {
if (position === 'start') {
return 0;
}
if (position === 'end') {
return size;
}
if (isPercentString(position)) {
return toPositivePercent(position) * size;
}
return size / 2;
}
/**
* @param {number} size
* @param {number|string} value
* @param {boolean} [positivePercent=true]
* @returns {number}
*/
function getSize(size, value, positivePercent = true) {
if (typeof value === 'number') {
return value;
} else if (isPercentString(value)) {
return (positivePercent ? toPositivePercent(value) : toPercent(value)) * size;
}
return size;
}
/**
* @param {{x: number, width: number}} size
* @param {CoreLabelOptions} options
* @returns {number}
*/
function calculateTextAlignment(size, options) {
const {x, width} = size;
const textAlign = options.textAlign;
if (textAlign === 'center') {
return x + width / 2;
} else if (textAlign === 'end' || textAlign === 'right') {
return x + width;
}
return x;
}
/**
* @param {Point} point
* @param {{height: number, width: number}} labelSize
* @param {{borderWidth: number, position: {LabelPositionObject|string}, xAdjust: number, yAdjust: number}} options
* @param {Padding|undefined} padding
* @returns {{x: number, y: number, x2: number, y2: number, height: number, width: number, centerX: number, centerY: number}}
*/
function measureLabelRectangle(point, labelSize, {borderWidth, position, xAdjust, yAdjust}, padding) {
const hasPadding = isObject(padding);
const width = labelSize.width + (hasPadding ? padding.width : 0) + borderWidth;
const height = labelSize.height + (hasPadding ? padding.height : 0) + borderWidth;
const positionObj = toPosition(position);
const x = calculateLabelPosition$1(point.x, width, xAdjust, positionObj.x);
const y = calculateLabelPosition$1(point.y, height, yAdjust, positionObj.y);
return {
x,
y,
x2: x + width,
y2: y + height,
width,
height,
centerX: x + width / 2,
centerY: y + height / 2
};
}
/**
* @param {LabelPositionObject|string} value
* @param {string|number} defaultValue
* @returns {LabelPositionObject}
*/
function toPosition(value, defaultValue = 'center') {
if (isObject(value)) {
return {
x: valueOrDefault(value.x, defaultValue),
y: valueOrDefault(value.y, defaultValue),
};
}
value = valueOrDefault(value, defaultValue);
return {
x: value,
y: value
};
}
/**
* @param {CoreLabelOptions} options
* @param {number} fitRatio
* @returns {boolean}
*/
const shouldFit = (options, fitRatio) => options && options.autoFit && fitRatio < 1;
/**
* @param {CoreLabelOptions} options
* @param {number} fitRatio
* @returns {FontSpec[]}
*/
function toFonts(options, fitRatio) {
const optFont = options.font;
const fonts = isArray(optFont) ? optFont : [optFont];
if (shouldFit(options, fitRatio)) {
return fonts.map(function(f) {
const font = toFont(f);
font.size = Math.floor(f.size * fitRatio);
font.lineHeight = f.lineHeight;
return toFont(font);
});
}
return fonts.map(f => toFont(f));
}
/**
* @param {AnnotationPointCoordinates} options
* @returns {boolean}
*/
function isBoundToPoint(options) {
return options && (defined(options.xValue) || defined(options.yValue));
}
function calculateLabelPosition$1(start, size, adjust = 0, position) {
return start - getRelativePosition(size, position) + adjust;
}
/**
* @param {Chart} chart
* @param {AnnotationBoxModel} properties
* @param {CoreAnnotationOptions} options
* @returns {AnnotationElement}
*/
function initAnimationProperties(chart, properties, options) {
const initAnim = options.init;
if (!initAnim) {
return;
} else if (initAnim === true) {
return applyDefault(properties, options);
}
return execCallback(chart, properties, options);
}
/**
* @param {Object} options
* @param {Array} hooks
* @param {Object} hooksContainer
* @returns {boolean}
*/
function loadHooks(options, hooks, hooksContainer) {
let activated = false;
hooks.forEach(hook => {
if (isFunction(options[hook])) {
activated = true;
hooksContainer[hook] = options[hook];
} else if (defined(hooksContainer[hook])) {
delete hooksContainer[hook];
}
});
return activated;
}
function applyDefault(properties, options) {
const type = options.type || 'line';
return defaultInitAnimation[type](properties);
}
function execCallback(chart, properties, options) {
const result = callback(options.init, [{chart, properties, options}]);
if (result === true) {
return applyDefault(properties, options);
} else if (isObject(result)) {
return result;
}
}
const widthCache = new Map();
const notRadius = (radius) => isNaN(radius) || radius <= 0;
const fontsKey = (fonts) => fonts.reduce(function(prev, item) {
prev += item.string;
return prev;
}, '');
/**
* @typedef { import('chart.js').Point } Point
* @typedef { import('../../types/label').CoreLabelOptions } CoreLabelOptions
* @typedef { import('../../types/options').PointAnnotationOptions } PointAnnotationOptions
*/
/**
* Determine if content is an image or a canvas.
* @param {*} content
* @returns boolean|undefined
* @todo move this function to chart.js helpers
*/
function isImageOrCanvas(content) {
if (content && typeof content === 'object') {
const type = content.toString();
return (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]');
}
}
/**
* Set the translation on the canvas if the rotation must be applied.
* @param {CanvasRenderingContext2D} ctx - chart canvas context
* @param {Point} point - the point of translation
* @param {number} rotation - rotation (in degrees) to apply
*/
function translate(ctx, {x, y}, rotation) {
if (rotation) {
ctx.translate(x, y);
ctx.rotate(toRadians(rotation));
ctx.translate(-x, -y);
}
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {Object} options
* @returns {boolean|undefined}
*/
function setBorderStyle(ctx, options) {
if (options && options.borderWidth) {
ctx.lineCap = options.borderCapStyle || 'butt';
ctx.setLineDash(options.borderDash);
ctx.lineDashOffset = options.borderDashOffset;
ctx.lineJoin = options.borderJoinStyle || 'miter';
ctx.lineWidth = options.borderWidth;
ctx.strokeStyle = options.borderColor;
return true;
}
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {Object} options
*/
function setShadowStyle(ctx, options) {
ctx.shadowColor = options.backgroundShadowColor;
ctx.shadowBlur = options.shadowBlur;
ctx.shadowOffsetX = options.shadowOffsetX;
ctx.shadowOffsetY = options.shadowOffsetY;
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {CoreLabelOptions} options
* @returns {{width: number, height: number}}
*/
function measureLabelSize(ctx, options) {
const content = options.content;
if (isImageOrCanvas(content)) {
const size = {
width: getSize(content.width, options.width),
height: getSize(content.height, options.height)
};
return size;
}
const fonts = toFonts(options);
const strokeWidth = options.textStrokeWidth;
const lines = isArray(content) ? content : [content];
const mapKey = lines.join() + fontsKey(fonts) + strokeWidth + (ctx._measureText ? '-spriting' : '');
if (!widthCache.has(mapKey)) {
widthCache.set(mapKey, calculateLabelSize(ctx, lines, fonts, strokeWidth));
}
return widthCache.get(mapKey);
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {{x: number, y: number, width: number, height: number}} rect
* @param {Object} options
*/
function drawBox(ctx, rect, options) {
const {x, y, width, height} = rect;
ctx.save();
setShadowStyle(ctx, options);
const stroke = setBorderStyle(ctx, options);
ctx.fillStyle = options.backgroundColor;
ctx.beginPath();
addRoundedRectPath(ctx, {
x, y, w: width, h: height,
radius: clampAll(toTRBLCorners(options.borderRadius), 0, Math.min(width, height) / 2)
});
ctx.closePath();
ctx.fill();
if (stroke) {
ctx.shadowColor = options.borderShadowColor;
ctx.stroke();
}
ctx.restore();
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {{x: number, y: number, width: number, height: number}} rect
* @param {CoreLabelOptions} options
* @param {number} fitRatio
*/
function drawLabel(ctx, rect, options, fitRatio) {
const content = options.content;
if (isImageOrCanvas(content)) {
ctx.save();
ctx.globalAlpha = getOpacity(options.opacity, content.style.opacity);
ctx.drawImage(content, rect.x, rect.y, rect.width, rect.height);
ctx.restore();
return;
}
const labels = isArray(content) ? content : [content];
const fonts = toFonts(options, fitRatio);
const optColor = options.color;
const colors = isArray(optColor) ? optColor : [optColor];
const x = calculateTextAlignment(rect, options);
const y = rect.y + options.textStrokeWidth / 2;
ctx.save();
ctx.textBaseline = 'middle';
ctx.textAlign = options.textAlign;
if (setTextStrokeStyle(ctx, options)) {
applyLabelDecoration(ctx, {x, y}, labels, fonts);
}
applyLabelContent(ctx, {x, y}, labels, {fonts, colors});
ctx.restore();
}
function setTextStrokeStyle(ctx, options) {
if (options.textStrokeWidth > 0) {
// https://stackoverflow.com/questions/13627111/drawing-text-with-an-outer-stroke-with-html5s-canvas
ctx.lineJoin = 'round';
ctx.miterLimit = 2;
ctx.lineWidth = options.textStrokeWidth;
ctx.strokeStyle = options.textStrokeColor;
return true;
}
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {{radius: number, options: PointAnnotationOptions}} element
* @param {number} x
* @param {number} y
*/
function drawPoint(ctx, element, x, y) {
const {radius, options} = element;
const style = options.pointStyle;
const rotation = options.rotation;
let rad = (rotation || 0) * RAD_PER_DEG;
if (isImageOrCanvas(style)) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rad);
ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height);
ctx.restore();
return;
}
if (notRadius(radius)) {
return;
}
drawPointStyle(ctx, {x, y, radius, rotation, style, rad});
}
function drawPointStyle(ctx, {x, y, radius, rotation, style, rad}) {
let xOffset, yOffset, size, cornerRadius;
ctx.beginPath();
switch (style) {
// Default includes circle
default:
ctx.arc(x, y, radius, 0, TAU);
ctx.closePath();
break;
case 'triangle':
ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius);
rad += TWO_THIRDS_PI;
ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius);
rad += TWO_THIRDS_PI;
ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius);
ctx.closePath();
break;
case 'rectRounded':
// NOTE: the rounded rect implementation changed to use `arc` instead of
// `quadraticCurveTo` since it generates better results when rect is
// almost a circle. 0.516 (instead of 0.5) produces results with visually
// closer proportion to the previous impl and it is inscribed in the
// circle with `radius`. For more details, see the following PRs:
// https://github.com/chartjs/Chart.js/issues/5597
// https://github.com/chartjs/Chart.js/issues/5858
cornerRadius = radius * 0.516;
size = radius - cornerRadius;
xOffset = Math.cos(rad + QUARTER_PI) * size;
yOffset = Math.sin(rad + QUARTER_PI) * size;
ctx.arc(x - xOffset, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI);
ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad);
ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI);
ctx.arc(x - yOffset, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI);
ctx.closePath();
break;
case 'rect':
if (!rotation) {
size = Math.SQRT1_2 * radius;
ctx.rect(x - size, y - size, 2 * size, 2 * size);
break;
}
rad += QUARTER_PI;
/* falls through */
case 'rectRot':
xOffset = Math.cos(rad) * radius;
yOffset = Math.sin(rad) * radius;
ctx.moveTo(x - xOffset, y - yOffset);
ctx.lineTo(x + yOffset, y - xOffset);
ctx.lineTo(x + xOffset, y + yOffset);
ctx.lineTo(x - yOffset, y + xOffset);
ctx.closePath();
break;
case 'crossRot':
rad += QUARTER_PI;
/* falls through */
case 'cross':
xOffset = Math.cos(rad) * radius;
yOffset = Math.sin(rad) * radius;
ctx.moveTo(x - xOffset, y - yOffset);
ctx.lineTo(x + xOffset, y + yOffset);
ctx.moveTo(x + yOffset, y - xOffset);
ctx.lineTo(x - yOffset, y + xOffset);
break;
case 'star':
xOffset = Math.cos(rad) * radius;
yOffset = Math.sin(rad) * radius;
ctx.moveTo(x - xOffset, y - yOffset);
ctx.lineTo(x + xOffset, y + yOffset);
ctx.moveTo(x + yOffset, y - xOffset);
ctx.lineTo(x - yOffset, y + xOffset);
rad += QUARTER_PI;
xOffset = Math.cos(rad) * radius;
yOffset = Math.sin(rad) * radius;
ctx.moveTo(x - xOffset, y - yOffset);
ctx.lineTo(x + xOffset, y + yOffset);
ctx.moveTo(x + yOffset, y - xOffset);
ctx.lineTo(x - yOffset, y + xOffset);
break;
case 'line':
xOffset = Math.cos(rad) * radius;
yOffset = Math.sin(rad) * radius;
ctx.moveTo(x - xOffset, y - yOffset);
ctx.lineTo(x + xOffset, y + yOffset);
break;
case 'dash':
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius);
break;
}
ctx.fill();
}
function calculateLabelSize(ctx, lines, fonts, strokeWidth) {
ctx.save();
const count = lines.length;
let width = 0;
let height = strokeWidth;
for (let i = 0; i < count; i++) {
const font = fonts[Math.min(i, fonts.length - 1)];
ctx.font = font.string;
const text = lines[i];
width = Math.max(width, ctx.measureText(text).width + strokeWidth);
height += font.lineHeight;
}
ctx.restore();
return {width, height};
}
function applyLabelDecoration(ctx, {x, y}, labels, fonts) {
ctx.beginPath();
let lhs = 0;
labels.forEach(function(l, i) {
const f = fonts[Math.min(i, fonts.length - 1)];
const lh = f.lineHeight;
ctx.font = f.string;
ctx.strokeText(l, x, y + lh / 2 + lhs);
lhs += lh;
});
ctx.stroke();
}
function applyLabelContent(ctx, {x, y}, labels, {fonts, colors}) {
let lhs = 0;
labels.forEach(function(l, i) {
const c = colors[Math.min(i, colors.length - 1)];
const f = fonts[Math.min(i, fonts.length - 1)];
const lh = f.lineHeight;
ctx.beginPath();
ctx.font = f.string;
ctx.fillStyle = c;
ctx.fillText(l, x, y + lh / 2 + lhs);
lhs += lh;
ctx.fill();
});
}
function getOpacity(value, elementValue) {
const opacity = isNumber(value) ? value : elementValue;
return isNumber(opacity) ? clamp(opacity, 0, 1) : 1;
}
const positions = ['left', 'bottom', 'top', 'right'];
/**
* @typedef { import('../../types/element').AnnotationElement } AnnotationElement
*/
/**
* Drawa the callout component for labels.
* @param {CanvasRenderingContext2D} ctx - chart canvas context
* @param {AnnotationElement} element - the label element
*/
function drawCallout(ctx, element) {
const {pointX, pointY, options} = element;
const callout = options.callout;
const calloutPosition = callout && callout.display && resolveCalloutPosition(element, callout);
if (!calloutPosition || isPointInRange(element, callout, calloutPosition)) {
return;
}
ctx.save();
ctx.beginPath();
const stroke = setBorderStyle(ctx, callout);
if (!stroke) {
return ctx.restore();
}
const {separatorStart, separatorEnd} = getCalloutSeparatorCoord(element, calloutPosition);
const {sideStart, sideEnd} = getCalloutSideCoord(element, calloutPosition, separatorStart);
if (callout.margin > 0 || options.borderWidth === 0) {
ctx.moveTo(separatorStart.x, separatorStart.y);
ctx.lineTo(separatorEnd.x, separatorEnd.y);
}
ctx.moveTo(sideStart.x, sideStart.y);
ctx.lineTo(sideEnd.x, sideEnd.y);
const rotatedPoint = rotated({x: pointX, y: pointY}, element.getCenterPoint(), toRadians(-element.rotation));
ctx.lineTo(rotatedPoint.x, rotatedPoint.y);
ctx.stroke();
ctx.restore();
}
function getCalloutSeparatorCoord(element, position) {
const {x, y, x2, y2} = element;
const adjust = getCalloutSeparatorAdjust(element, position);
let separatorStart, separatorEnd;
if (position === 'left' || position === 'right') {
separatorStart = {x: x + adjust, y};
separatorEnd = {x: separatorStart.x, y: y2};
} else {
// position 'top' or 'bottom'
separatorStart = {x, y: y + adjust};
separatorEnd = {x: x2, y: separatorStart.y};
}
return {separatorStart, separatorEnd};
}
function getCalloutSeparatorAdjust(element, position) {
const {width, height, options} = element;
const adjust = options.callout.margin + options.borderWidth / 2;
if (position === 'right') {
return width + adjust;
} else if (position === 'bottom') {
return height + adjust;
}
return -adjust;
}
function getCalloutSideCoord(element, position, separatorStart) {
const {y, width, height, options} = element;
const start = options.callout.start;
const side = getCalloutSideAdjust(position, options.callout);
let sideStart, sideEnd;
if (position === 'left' || position === 'right') {
sideStart = {x: separatorStart.x, y: y + getSize(height, start)};
sideEnd = {x: sideStart.x + side, y: sideStart.y};
} else {
// position 'top' or 'bottom'
sideStart = {x: separatorStart.x + getSize(width, start), y: separatorStart.y};
sideEnd = {x: sideStart.x, y: sideStart.y + side};
}
return {sideStart, sideEnd};
}
function getCalloutSideAdjust(position, options) {
const side = options.side;
if (position === 'left' || position === 'top') {
return -side;
}
return side;
}
function resolveCalloutPosition(element, options) {
const position = options.position;
if (positions.includes(position)) {
return position;
}
return resolveCalloutAutoPosition(element, options);
}
function resolveCalloutAutoPosition(element, options) {
const {x, y, x2, y2, width, height, pointX, pointY, centerX, centerY, rotation} = element;
const center = {x: centerX, y: centerY};
const start = options.start;
const xAdjust = getSize(width, start);
const yAdjust = getSize(height, start);
const xPoints = [x, x + xAdjust, x + xAdjust, x2];
const yPoints = [y + yAdjust, y2, y, y2];
const result = [];
for (let index = 0; index < 4; index++) {
const rotatedPoint = rotated({x: xPoints[index], y: yPoints[index]}, center, toRadians(rotation));
result.push({
position: positions[index],
distance: distanceBetweenPoints(rotatedPoint, {x: pointX, y: pointY})
});
}
return result.sort((a, b) => a.distance - b.distance)[0].position;
}
function isPointInRange(element, callout, position) {
const {pointX, pointY} = element;
const margin = callout.margin;
let x = pointX;
let y = pointY;
if (position === 'left') {
x += margin;
} else if (position === 'right') {
x -= margin;
} else if (position === 'top') {
y += margin;
} else if (position === 'bottom') {
y -= margin;
}
return element.inRange(x, y);
}
const limitedLineScale = {
xScaleID: {min: 'xMin', max: 'xMax', start: 'left', end: 'right', startProp: 'x', endProp: 'x2'},
yScaleID: {min: 'yMin', max: 'yMax', start: 'bottom', end: 'top', startProp: 'y', endProp: 'y2'}
};
/**
* @typedef { import("chart.js").Chart } Chart
* @typedef { import("chart.js").Scale } Scale
* @typedef { import("chart.js").Point } Point
* @typedef { import('../../types/element').AnnotationBoxModel } AnnotationBoxModel
* @typedef { import('../../types/options').CoreAnnotationOptions } CoreAnnotationOptions
* @typedef { import('../../types/options').LineAnnotationOptions } LineAnnotationOptions
* @typedef { import('../../types/options').PointAnnotationOptions } PointAnnotationOptions
* @typedef { import('../../types/options').PolygonAnnotationOptions } PolygonAnnotationOptions
*/
/**
* @param {Scale} scale
* @param {number|string} value
* @param {number} fallback
* @returns {number}
*/
function scaleValue(scale, value, fallback) {
value = typeof value === 'number' ? value : scale.parse(value);
return isFinite(value) ? scale.getPixelForValue(value) : fallback;
}
/**
* Search the scale defined in chartjs by the axis related to the annotation options key.
* @param {{ [key: string]: Scale }} scales
* @param {CoreAnnotationOptions} options
* @param {string} key
* @returns {string}
*/
function retrieveScaleID(scales, options, key) {
const scaleID = options[key];
if (scaleID || key === 'scaleID') {
return scaleID;
}
const axis = key.charAt(0);
const axes = Object.values(scales).filter((scale) => scale.axis && scale.axis === axis);
if (axes.length) {
return axes[0].id;
}
return axis;
}
/**
* @param {Scale} scale
* @param {{min: number, max: number, start: number, end: number}} options
* @returns {{start: number, end: number}|undefined}
*/
function getDimensionByScale(scale, options) {
if (scale) {
const reverse = scale.options.reverse;
const start = scaleValue(scale, options.min, reverse ? options.end : options.start);
const end = scaleValue(scale, options.max, reverse ? options.start : options.end);
return {
start,
end
};
}
}
/**
* @param {Chart} chart
* @param {CoreAnnotationOptions} options
* @returns {Point}
*/
function getChartPoint(chart, options) {
const {chartArea, scales} = chart;
const xScale = scales[retrieveScaleID(scales, options, 'xScaleID')];
const yScale = scales[retrieveScaleID(scales, options, 'yScaleID')];
let x = chartArea.width / 2;
let y = chartArea.height / 2;
if (xScale) {
x = scaleValue(xScale, options.xValue, xScale.left + xScale.width / 2);
}
if (yScale) {
y = scaleValue(yScale, options.yValue, yScale.top + yScale.height / 2);
}
return {x, y};
}
/**
* @param {Chart} chart
* @param {CoreAnnotationOptions} options
* @returns {AnnotationBoxModel}
*/
function resolveBoxProperties(chart, options) {
const scales = chart.scales;
const xScale = scales[retrieveScaleID(scales, options, 'xScaleID')];
const yScale = scales[retrieveScaleID(scales, options, 'yScaleID')];
if (!xScale && !yScale) {
return {};
}
let {left: x, right: x2} = xScale || chart.chartArea;
let {top: y, bottom: y2} = yScale || chart.chartArea;
const xDim = getChartDimensionByScale(xScale, {min: options.xMin, max: options.xMax, start: x, end: x2});
x = xDim.start;
x2 = xDim.end;
const yDim = getChartDimensionByScale(yScale, {min: options.yMin, max: options.yMax, start: y2, end: y});
y = yDim.start;
y2 = yDim.end;
return {
x,
y,
x2,
y2,
width: x2 - x,
height: y2 - y,
centerX: x + (x2 - x) / 2,
centerY: y + (y2 - y) / 2
};
}
/**
* @param {Chart} chart
* @param {PointAnnotationOptions|PolygonAnnotationOptions} options
* @returns {AnnotationBoxModel}
*/
function resolvePointProperties(chart, options) {
if (!isBoundToPoint(options)) {
const box = resolveBoxProperties(chart, options);
let radius = options.radius;
if (!radius || isNaN(radius)) {
radius = Math.min(box.width, box.height) / 2;
options.radius = radius;
}
const size = radius * 2;
const adjustCenterX = box.centerX + options.xAdjust;
const adjustCenterY = box.centerY + options.yAdjust;
return {
x: adjustCenterX - radius,
y: adjustCenterY - radius,
x2: adjustCenterX + radius,
y2: adjustCenterY + radius,
centerX: adjustCenterX,
centerY: adjustCenterY,
width: size,
height: size,
radius
};
}
return getChartCircle(chart, options);
}
/**
* @param {Chart} chart
* @param {LineAnnotationOptions} options
* @returns {AnnotationBoxModel}
*/
function resolveLineProperties(chart, options) {
const {scales, chartArea} = chart;
const scale = scales[options.scaleID];
const area = {x: chartArea.left, y: chartArea.top, x2: chartArea.right, y2: chartArea.bottom};
if (scale) {
resolveFullLineProperties(scale, area, options);
} else {
resolveLimitedLineProperties(scales, area, options);
}
return area;
}
/**
* @param {Chart} chart
* @param {CoreAnnotationOptions} options
* @param {boolean} [centerBased=false]
* @returns {AnnotationBoxModel}
*/
function resolveBoxAndLabelProperties(chart, options) {
const properties = resolveBoxProperties(chart, options);
properties.initProperties = initAnimationProperties(chart, properties, options);
properties.elements = [{
type: 'label',
optionScope: 'label',
properties: resolveLabelElementProperties$1(chart, properties, options),
initProperties: properties.initProperties
}];
return properties;
}
function getChartCircle(chart, options) {
const point = getChartPoint(chart, options);
const size = options.radius * 2;
return {
x: point.x - options.radius + options.xAdjust,
y: point.y - options.radius + options.yAdjust,
x2: point.x + options.radius + options.xAdjust,
y2: point.y + options.radius + options.yAdjust,
centerX: point.x + options.xAdjust,
centerY: point.y + options.yAdjust,
radius: options.radius,
width: size,
height: size
};
}
function getChartDimensionByScale(scale, options) {
const result = getDimensionByScale(scale, options) || options;
return {
start: Math.min(result.start, result.end),
end: Math.max(result.start, result.end)
};
}
function resolveFullLineProperties(scale, area, options) {
const min = scaleValue(scale, options.value, NaN);
const max = scaleValue(scale, options.endValue, min);
if (scale.isHorizontal()) {
area.x = min;
area.x2 = max;
} else {
area.y = min;
area.y2 = max;
}
}
function resolveLimitedLineProperties(scales, area, options) {
for (const scaleId of Object.keys(limitedLineScale)) {
const scale = scales[retrieveScaleID(scales, options, scaleId)];
if (scale) {
const {min, max, start, end, startProp, endProp} = limitedLineScale[scaleId];
const dim = getDimensionByScale(scale, {min: options[min], max: options[max], start: scale[start], end: scale[end]});
area[startProp] = dim.start;
area[endProp] = dim.end;
}
}
}
function calculateX({properties, options}, labelSize, position, padding) {
const {x: start, x2: end, width: size} = properties;
return calculatePosition({start, end, size, borderWidth: options.borderWidth}, {
position: position.x,
padding: {start: padding.left, end: padding.right},
adjust: options.label.xAdjust,
size: labelSize.width
});
}
function calculateY({properties, options}, labelSize, position, padding) {
const {y: start, y2: end, height: size} = properties;
return calculatePosition({start, end, size, borderWidth: options.borderWidth}, {
position: position.y,
padding: {start: padding.top, end: padding.bottom},
adjust: options.label.yAdjust,
size: labelSize.height
});
}
function calculatePosition(boxOpts, labelOpts) {
const {start, end, borderWidth} = boxOpts;
const {position, padding: {start: padStart, end: padEnd}, adjust} = labelOpts;
const availableSize = end - borderWidth - start - padStart - padEnd - labelOpts.size;
return start + borderWidth / 2 + adjust + getRelativePosition(availableSize, position);
}
function resolveLabelElementProperties$1(chart, properties, options) {
const label = options.label;
label.backgroundColor = 'transparent';
label.callout.display = false;
const position = toPosition(label.position);
const padding = toPadding(label.padding);
const labelSize = measureLabelSize(chart.ctx, label);
const x = calculateX({properties, options}, labelSize, position, padding);
const y = calculateY({properties, options}, labelSize, position, padding);
const width = labelSize.width + padding.width;
const height = labelSize.height + padding.height;
return {
x,
y,
x2: x + width,
y2: y + height,
width,
height,
centerX: x + width / 2,
centerY: y + height / 2,
rotation: label.rotation
};
}
const moveHooks = ['enter', 'leave'];
/**
* @typedef { import("chart.js").Chart } Chart
* @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions
*/
const eventHooks = moveHooks.concat('click');
/**
* @param {Chart} chart
* @param {Object} state
* @param {AnnotationPluginOptions} options
*/
function updateListeners(chart, state, options) {
state.listened = loadHooks(options, eventHooks, state.listeners);
state.moveListened = false;
moveHooks.forEach(hook => {
if (isFunction(options[hook])) {
state.moveListened = true;
}
});
if (!state.listened || !state.moveListened) {
state.annotations.forEach(scope => {
if (!state.listened && isFunction(scope.click)) {
state.listened = true;
}
if (!state.moveListened) {
moveHooks.forEach(hook => {
if (isFunction(scope[hook])) {
state.listened = true;
state.moveListened = true;
}
});
}
});
}
}
/**
* @param {Object} state
* @param {ChartEvent} event
* @param {AnnotationPluginOptions} options
* @return {boolean|undefined}
*/
function handleEvent(state, event, options) {
if (state.listened) {
switch (event.type) {
case 'mousemove':
case 'mouseout':
return handleMoveEvents(state, event, options);
case 'click':
return handleClickEvents(state, event, options);
}
}
}
function handleMoveEvents(state, event, options) {
if (!state.moveListened) {
return;
}
let elements;
if (event.type === 'mousemove') {
elements = getElements(state.visibleElements, event, options.interaction);
} else {
elements = [];
}
const previous = state.hovered;
state.hovered = elements;
const context = {state, event};
let changed = dispatchMoveEvents(context, 'leave', previous, elements);
return dispatchMoveEvents(context, 'enter', elements, previous) || changed;
}
function dispatchMoveEvents({state, event}, hook, elements, checkElements) {
let changed;
for (const element of elements) {
if (checkElements.indexOf(element) < 0) {
changed = dispatchEvent(element.options[hook] || state.listeners[hook], element, event) || changed;
}
}
return changed;
}
function handleClickEvents(state, event, options) {
const listeners = state.listeners;
const elements = getElements(state.visibleElements, event, options.interaction);
let changed;
for (const element of elements) {
changed = dispatchEvent(element.options.click || listeners.click, element, event) || changed;
}
return changed;
}
function dispatchEvent(handler, element, event) {
return callback(handler, [element.$context, event]) === true;
}
/**
* @typedef { import("chart.js").Chart } Chart
* @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions
* @typedef { import('../../types/element').AnnotationElement } AnnotationElement
*/
const elementHooks = ['afterDraw', 'beforeDraw'];
/**
* @param {Chart} chart
* @param {Object} state
* @param {AnnotationPluginOptions} options
*/
function updateHooks(chart, state, options) {
const visibleElements = state.visibleElements;
state.hooked = loadHooks(options, elementHooks, state.hooks);
if (!state.hooked) {
visibleElements.forEach(scope => {
if (!state.hooked) {
elementHooks.forEach(hook => {
if (isFunction(scope.options[hook])) {
state.hooked = true;
}
});
}
});
}
}
/**
* @param {Object} state
* @param {AnnotationElement} element
* @param {string} hook
*/
function invokeHook(state, element, hook) {
if (state.hooked) {
const callbackHook = element.options[hook] || state.hooks[hook];
return callback(callbackHook, [element.$context]);
}
}
/**
* @typedef { import("chart.js").Chart } Chart
* @typedef { import("chart.js").Scale } Scale
* @typedef { import('../../types/options').CoreAnnotationOptions } CoreAnnotationOptions
*/
/**
* @param {Chart} chart
* @param {Scale} scale
* @param {CoreAnnotationOptions[]} annotations
*/
function adjustScaleRange(chart, scale, annotations) {
const range = getScaleLimits(chart.scales, scale, annotations);
let changed = changeScaleLimit(scale, range, 'min', 'suggestedMin');
changed = changeScaleLimit(scale, range, 'max', 'suggestedMax') || changed;
if (changed && isFunction(scale.handleTickRangeOptions)) {
scale.handleTickRangeOptions();
}
}
/**
* @param {CoreAnnotationOptions[]} annotations
* @param {{ [key: string]: Scale }} scales
*/
function verifyScaleOptions(annotations, scales) {
for (const annotation of annotations) {
verifyScaleIDs(annotation, scales);
}
}
function changeScaleLimit(scale, range, limit, suggestedLimit) {
if (isFinite(range[limit]) && !scaleLimitDefined(scale.options, limit, suggestedLimit)) {
const changed = scale[limit] !== range[limit];
scale[limit] = range[limit];
return changed;
}
}
function scaleLimitDefined(scaleOptions, limit, suggestedLimit) {
return defined(scaleOptions[limit]) || defined(scaleOptions[suggestedLimit]);
}
function verifyScaleIDs(annotation, scales) {
for (const key of ['scaleID', 'xScaleID', 'yScaleID']) {
const scaleID = retrieveScaleID(scales, annotation, key);
if (scaleID && !scales[scaleID] && verifyProperties(annotation, key)) {
console.warn(`No scale found with id '${scaleID}' for annotation '${annotation.id}'`);
}
}
}
function verifyProperties(annotation, key) {
if (key === 'scaleID') {
return true;
}
const axis = key.charAt(0);
for (const prop of ['Min', 'Max', 'Value']) {
if (defined(annotation[axis + prop])) {
return true;
}
}
return false;
}
function getScaleLimits(scales, scale, annotations) {
const axis = scale.axis;
const scaleID = scale.id;
const scaleIDOption = axis + 'ScaleID';
const limits = {
min: valueOrDefault(scale.min, Number.NEGATIVE_INFINITY),
max: valueOrDefault(scale.max, Number.POSITIVE_INFINITY)
};
for (const annotation of annotations) {
if (annotation.scaleID === scaleID) {
updateLimits(annotation, scale, ['value', 'endValue'], limits);
} else if (retrieveScaleID(scales, annotation, scaleIDOption) === scaleID) {
updateLimits(annotation, scale, [axis + 'Min', axis + 'Max', axis + 'Value'], limits);
}
}
return limits;
}
function updateLimits(annotation, scale, props, limits) {
for (const prop of props) {
const raw = annotation[prop];
if (defined(raw)) {
const value = scale.parse(raw);
limits.min = Math.min(limits.min, value);
limits.max = Math.max(limits.max, value);
}
}
}
class BoxAnnotation extends Element {
inRange(mouseX, mouseY, axis, useFinalPosition) {
const {x, y} = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation));
return inBoxRange({x, y}, this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition), axis, this.options);
}
getCenterPoint(useFinalPosition) {
return getElementCenterPoint(this, useFinalPosition);
}
draw(ctx) {
ctx.save();
translate(ctx, this.getCenterPoint(), this.options.rotation);
drawBox(ctx, this, this.options);
ctx.restore();
}
get label() {
return this.elements && this.elements[0];
}
resolveElementProperties(chart, options) {
return resolveBoxAndLabelProperties(chart, options);
}
}
BoxAnnotation.id = 'boxAnnotation';
BoxAnnotation.defaults = {
adjustScaleRange: true,
backgroundShadowColor: 'transparent',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0,
borderJoinStyle: 'miter',
borderRadius: 0,
borderShadowColor: 'transparent',
borderWidth: 1,
display: true,
init: undefined,
hitTolerance: 0,
label: {
backgroundColor: 'transparent',
borderWidth: 0,
callout: {
display: false
},
color: 'black',
content: null,
display: false,
drawTime: undefined,
font: {
family: undefined,
lineHeight: undefined,
size: undefined,
style: undefined,
weight: 'bold'
},
height: undefined,
hitTolerance: undefined,
opacity: undefined,
padding: 6,
position: 'center',
rotation: undefined,
textAlign: 'start',
textStrokeColor: undefined,
textStrokeWidth: 0,
width: undefined,
xAdjust: 0,
yAdjust: 0,
z: undefined
},
rotation: 0,
shadowBlur: 0,
shadowOffsetX: 0,
shadowOffsetY: 0,
xMax: undefined,
xMin: undefined,
xScaleID: undefined,
yMax: undefined,
yMin: undefined,
yScaleID: undefined,
z: 0
};
BoxAnnotation.defaultRoutes = {
borderColor: 'color',
backgroundColor: 'color'
};
BoxAnnotation.descriptors = {
label: {
_fallback: true
}
};
class DoughnutLabelAnnotation extends Element {
inRange(mouseX, mouseY, axis, useFinalPosition) {
return inLabelRange(
{x: mouseX, y: mouseY},
{rect: this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition), center: this.getCenterPoint(useFinalPosition)},
axis,
{rotation: this.rotation, borderWidth: 0, hitTolerance: this.options.hitTolerance}
);
}
getCenterPoint(useFinalPosition) {
return getElementCenterPoint(this, useFinalPosition);
}
draw(ctx) {
const options = this.options;
if (!options.display || !options.content) {
return;
}
drawBackground(ctx, this);
ctx.save();
translate(ctx, this.getCenterPoint(), this.rotation);
drawLabel(ctx, this, options, this._fitRatio);
ctx.restore();
}
resolveElementProperties(chart, options) {
const meta = getDatasetMeta(chart, options);
if (!meta) {
return {};
}
const {controllerMeta, point, radius} = getControllerMeta(chart, options, meta);
let labelSize = measureLabelSize(chart.ctx, options);
const _fitRatio = getFitRatio(labelSize, radius);
if (shouldFit(options, _fitRatio)) {
labelSize = {width: labelSize.width * _fitRatio, height: labelSize.height * _fitRatio};
}
const {position, xAdjust, yAdjust} = options;
const boxSize = measureLabelRectangle(point, labelSize, {borderWidth: 0, position, xAdjust, yAdjust});
return {
initProperties: initAnimationProperties(chart, boxSize, options),
...boxSize,
...controllerMeta,
rotation: options.rotation,
_fitRatio
};
}
}
DoughnutLabelAnnotation.id = 'doughnutLabelAnnotation';
DoughnutLabelAnnotation.defaults = {
autoFit: true,
autoHide: true,
backgroundColor: 'transparent',
backgroundShadowColor: 'transparent',
borderColor: 'transparent',
borderDash: [],
borderDashOffset: 0,
borderJoinStyle: 'miter',
borderShadowColor: 'transparent',
borderWidth: 0,
color: 'black',
content: null,
display: true,
font: {
family: undefined,
lineHeight: undefined,
size: undefined,
style: undefined,
weight: undefined
},
height: undefined,
hitTolerance: 0,
init: undefined,
opacity: undefined,
position: 'center',
rotation: 0,
shadowBlur: 0,
shadowOffsetX: 0,
shadowOffsetY: 0,
spacing: 1,
textAlign: 'center',
textStrokeColor: undefined,
textStrokeWidth: 0,
width: undefined,
xAdjust: 0,
yAdjust: 0
};
DoughnutLabelAnnotation.defaultRoutes = {
};
function getDatasetMeta(chart, options) {
return chart.getSortedVisibleDatasetMetas().reduce(function(result, value) {
const controller = value.controller;
if (controller instanceof DoughnutController &&
isC