+import type {
+ Chart,
+ Point,
+ FontSpec,
+ CanvasFontSpec,
+ PointStyle,
+ RenderTextOpts,
+ BackdropOptions
+} from '../types/index.js';
+import type {
+ TRBL,
+ SplinePoint,
+ RoundedRect,
+ TRBLCorners
+} from '../types/geometric.js';
import {isArray, isNullOrUndef} from './helpers.core.js';
import {PI, TAU, HALF_PI, QUARTER_PI, TWO_THIRDS_PI, RAD_PER_DEG} from './helpers.math.js';
-/**
- * Note: typedefs are auto-exported, so use a made-up `canvas` namespace where
- * necessary to avoid duplicates with `export * from './helpers`; see
- * https://github.com/microsoft/TypeScript/issues/46011
- * @typedef { import('../core/core.controller.js').default } canvas.Chart
- * @typedef { import('../types/index.js').Point } Point
- */
-
-/**
- * @namespace Chart.helpers.canvas
- */
-
/**
* Converts the given font object into a CSS font string.
- * @param {object} font - A font object.
- * @return {string|null} The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font
+ * @param font - A font object.
+ * @return The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font
* @private
*/
-export function toFontString(font) {
+export function toFontString(font: FontSpec) {
if (!font || isNullOrUndef(font.size) || isNullOrUndef(font.family)) {
return null;
}
/**
* @private
*/
-export function _measureText(ctx, data, gc, longest, string) {
+export function _measureText(
+ ctx: CanvasRenderingContext2D,
+ data: Record<string, number>,
+ gc: string[],
+ longest: number,
+ string: string
+) {
let textWidth = data[string];
if (!textWidth) {
textWidth = data[string] = ctx.measureText(string).width;
return longest;
}
+type Thing = string | undefined | null
+type Things = (Thing | Thing[])[]
+
/**
* @private
*/
-export function _longestText(ctx, font, arrayOfThings, cache) {
+// eslint-disable-next-line complexity
+export function _longestText(
+ ctx: CanvasRenderingContext2D,
+ font: string,
+ arrayOfThings: Things,
+ cache?: {data?: Record<string, number>, garbageCollect?: string[], font?: string}
+) {
cache = cache || {};
let data = cache.data = cache.data || {};
let gc = cache.garbageCollect = cache.garbageCollect || [];
ctx.font = font;
let longest = 0;
const ilen = arrayOfThings.length;
- let i, j, jlen, thing, nestedThing;
+ let i: number, j: number, jlen: number, thing: Thing | Thing[], nestedThing: Thing | Thing[];
for (i = 0; i < ilen; i++) {
thing = arrayOfThings[i];
// Undefined strings and arrays should not be measured
- if (thing !== undefined && thing !== null && isArray(thing) !== true) {
+ if (thing !== undefined && thing !== null && !isArray(thing)) {
longest = _measureText(ctx, data, gc, longest, thing);
} else if (isArray(thing)) {
// if it is an array lets measure each element
/**
* Returns the aligned pixel value to avoid anti-aliasing blur
- * @param {canvas.Chart} chart - The chart instance.
- * @param {number} pixel - A pixel value.
- * @param {number} width - The width of the element.
- * @returns {number} The aligned pixel value.
+ * @param chart - The chart instance.
+ * @param pixel - A pixel value.
+ * @param width - The width of the element.
+ * @returns The aligned pixel value.
* @private
*/
-export function _alignPixel(chart, pixel, width) {
+export function _alignPixel(chart: Chart, pixel: number, width: number) {
const devicePixelRatio = chart.currentDevicePixelRatio;
const halfWidth = width !== 0 ? Math.max(width / 2, 0.5) : 0;
return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth;
/**
* Clears the entire canvas.
- * @param {HTMLCanvasElement} canvas
- * @param {CanvasRenderingContext2D} [ctx]
*/
-export function clearCanvas(canvas, ctx) {
+export function clearCanvas(canvas: HTMLCanvasElement, ctx?: CanvasRenderingContext2D) {
ctx = ctx || canvas.getContext('2d');
ctx.save();
ctx.restore();
}
-export function drawPoint(ctx, options, x, y) {
+export interface DrawPointOptions {
+ pointStyle: PointStyle;
+ rotation?: number;
+ radius: number;
+ borderWidth: number;
+}
+
+export function drawPoint(
+ ctx: CanvasRenderingContext2D,
+ options: DrawPointOptions,
+ x: number,
+ y: number
+) {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
drawPointLegend(ctx, options, x, y, null);
}
-export function drawPointLegend(ctx, options, x, y, w) {
- let type, xOffset, yOffset, size, cornerRadius, width, xOffsetW, yOffsetW;
+// eslint-disable-next-line complexity
+export function drawPointLegend(
+ ctx: CanvasRenderingContext2D,
+ options: DrawPointOptions,
+ x: number,
+ y: number,
+ w: number
+) {
+ let type: string, xOffset: number, yOffset: number, size: number, cornerRadius: number, width: number, xOffsetW: number, yOffsetW: number;
const style = options.pointStyle;
const rotation = options.rotation;
const radius = options.radius;
switch (style) {
// Default includes circle
- default:
- if (w) {
- ctx.ellipse(x, y, w / 2, radius, 0, 0, TAU);
- } else {
- ctx.arc(x, y, radius, 0, TAU);
- }
- ctx.closePath();
- break;
- case 'triangle':
- width = w ? w / 2 : radius;
- ctx.moveTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
- rad += TWO_THIRDS_PI;
- ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
- rad += TWO_THIRDS_PI;
- ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
- ctx.closePath();
- break;
- case 'rectRounded':
+ default:
+ if (w) {
+ ctx.ellipse(x, y, w / 2, radius, 0, 0, TAU);
+ } else {
+ ctx.arc(x, y, radius, 0, TAU);
+ }
+ ctx.closePath();
+ break;
+ case 'triangle':
+ width = w ? w / 2 : radius;
+ ctx.moveTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
+ rad += TWO_THIRDS_PI;
+ ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
+ rad += TWO_THIRDS_PI;
+ ctx.lineTo(x + Math.sin(rad) * width, 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
// 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;
- xOffsetW = Math.cos(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);
- yOffset = Math.sin(rad + QUARTER_PI) * size;
- yOffsetW = Math.sin(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);
- ctx.arc(x - xOffsetW, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI);
- ctx.arc(x + yOffsetW, y - xOffset, cornerRadius, rad - HALF_PI, rad);
- ctx.arc(x + xOffsetW, y + yOffset, cornerRadius, rad, rad + HALF_PI);
- ctx.arc(x - yOffsetW, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI);
- ctx.closePath();
- break;
- case 'rect':
- if (!rotation) {
- size = Math.SQRT1_2 * radius;
- width = w ? w / 2 : size;
- ctx.rect(x - width, y - size, 2 * width, 2 * size);
+ cornerRadius = radius * 0.516;
+ size = radius - cornerRadius;
+ xOffset = Math.cos(rad + QUARTER_PI) * size;
+ xOffsetW = Math.cos(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);
+ yOffset = Math.sin(rad + QUARTER_PI) * size;
+ yOffsetW = Math.sin(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);
+ ctx.arc(x - xOffsetW, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI);
+ ctx.arc(x + yOffsetW, y - xOffset, cornerRadius, rad - HALF_PI, rad);
+ ctx.arc(x + xOffsetW, y + yOffset, cornerRadius, rad, rad + HALF_PI);
+ ctx.arc(x - yOffsetW, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI);
+ ctx.closePath();
break;
- }
- rad += QUARTER_PI;
+ case 'rect':
+ if (!rotation) {
+ size = Math.SQRT1_2 * radius;
+ width = w ? w / 2 : size;
+ ctx.rect(x - width, y - size, 2 * width, 2 * size);
+ break;
+ }
+ rad += QUARTER_PI;
/* falls through */
- case 'rectRot':
- xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
- xOffset = Math.cos(rad) * radius;
- yOffset = Math.sin(rad) * radius;
- yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
- ctx.moveTo(x - xOffsetW, y - yOffset);
- ctx.lineTo(x + yOffsetW, y - xOffset);
- ctx.lineTo(x + xOffsetW, y + yOffset);
- ctx.lineTo(x - yOffsetW, y + xOffset);
- ctx.closePath();
- break;
- case 'crossRot':
- rad += QUARTER_PI;
+ case 'rectRot':
+ xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
+ xOffset = Math.cos(rad) * radius;
+ yOffset = Math.sin(rad) * radius;
+ yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
+ ctx.moveTo(x - xOffsetW, y - yOffset);
+ ctx.lineTo(x + yOffsetW, y - xOffset);
+ ctx.lineTo(x + xOffsetW, y + yOffset);
+ ctx.lineTo(x - yOffsetW, y + xOffset);
+ ctx.closePath();
+ break;
+ case 'crossRot':
+ rad += QUARTER_PI;
/* falls through */
- case 'cross':
- xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
- xOffset = Math.cos(rad) * radius;
- yOffset = Math.sin(rad) * radius;
- yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
- ctx.moveTo(x - xOffsetW, y - yOffset);
- ctx.lineTo(x + xOffsetW, y + yOffset);
- ctx.moveTo(x + yOffsetW, y - xOffset);
- ctx.lineTo(x - yOffsetW, y + xOffset);
- break;
- case 'star':
- xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
- xOffset = Math.cos(rad) * radius;
- yOffset = Math.sin(rad) * radius;
- yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
- ctx.moveTo(x - xOffsetW, y - yOffset);
- ctx.lineTo(x + xOffsetW, y + yOffset);
- ctx.moveTo(x + yOffsetW, y - xOffset);
- ctx.lineTo(x - yOffsetW, y + xOffset);
- rad += QUARTER_PI;
- xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
- xOffset = Math.cos(rad) * radius;
- yOffset = Math.sin(rad) * radius;
- yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
- ctx.moveTo(x - xOffsetW, y - yOffset);
- ctx.lineTo(x + xOffsetW, y + yOffset);
- ctx.moveTo(x + yOffsetW, y - xOffset);
- ctx.lineTo(x - yOffsetW, y + xOffset);
- break;
- case 'line':
- xOffset = w ? w / 2 : 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) * (w ? w / 2 : radius), y + Math.sin(rad) * radius);
- break;
- case false:
- ctx.closePath();
- break;
+ case 'cross':
+ xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
+ xOffset = Math.cos(rad) * radius;
+ yOffset = Math.sin(rad) * radius;
+ yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
+ ctx.moveTo(x - xOffsetW, y - yOffset);
+ ctx.lineTo(x + xOffsetW, y + yOffset);
+ ctx.moveTo(x + yOffsetW, y - xOffset);
+ ctx.lineTo(x - yOffsetW, y + xOffset);
+ break;
+ case 'star':
+ xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
+ xOffset = Math.cos(rad) * radius;
+ yOffset = Math.sin(rad) * radius;
+ yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
+ ctx.moveTo(x - xOffsetW, y - yOffset);
+ ctx.lineTo(x + xOffsetW, y + yOffset);
+ ctx.moveTo(x + yOffsetW, y - xOffset);
+ ctx.lineTo(x - yOffsetW, y + xOffset);
+ rad += QUARTER_PI;
+ xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
+ xOffset = Math.cos(rad) * radius;
+ yOffset = Math.sin(rad) * radius;
+ yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
+ ctx.moveTo(x - xOffsetW, y - yOffset);
+ ctx.lineTo(x + xOffsetW, y + yOffset);
+ ctx.moveTo(x + yOffsetW, y - xOffset);
+ ctx.lineTo(x - yOffsetW, y + xOffset);
+ break;
+ case 'line':
+ xOffset = w ? w / 2 : 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) * (w ? w / 2 : radius), y + Math.sin(rad) * radius);
+ break;
+ case false:
+ ctx.closePath();
+ break;
}
ctx.fill();
/**
* Returns true if the point is inside the rectangle
- * @param {Point} point - The point to test
- * @param {object} area - The rectangle
- * @param {number} [margin] - allowed margin
- * @returns {boolean}
+ * @param point - The point to test
+ * @param area - The rectangle
+ * @param margin - allowed margin
* @private
*/
-export function _isPointInArea(point, area, margin) {
+export function _isPointInArea(
+ point: Point,
+ area: TRBL,
+ margin?: number
+) {
margin = margin || 0.5; // margin - default is to match rounded decimals
return !area || (point && point.x > area.left - margin && point.x < area.right + margin &&
point.y > area.top - margin && point.y < area.bottom + margin);
}
-export function clipArea(ctx, area) {
+export function clipArea(ctx: CanvasRenderingContext2D, area: TRBL) {
ctx.save();
ctx.beginPath();
ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top);
ctx.clip();
}
-export function unclipArea(ctx) {
+export function unclipArea(ctx: CanvasRenderingContext2D) {
ctx.restore();
}
/**
* @private
*/
-export function _steppedLineTo(ctx, previous, target, flip, mode) {
+export function _steppedLineTo(
+ ctx: CanvasRenderingContext2D,
+ previous: Point,
+ target: Point,
+ flip?: boolean,
+ mode?: string
+) {
if (!previous) {
return ctx.lineTo(target.x, target.y);
}
/**
* @private
*/
-export function _bezierCurveTo(ctx, previous, target, flip) {
+export function _bezierCurveTo(
+ ctx: CanvasRenderingContext2D,
+ previous: SplinePoint,
+ target: SplinePoint,
+ flip?: boolean
+) {
if (!previous) {
return ctx.lineTo(target.x, target.y);
}
target.y);
}
-/**
- * Render text onto the canvas
- */
-export function renderText(ctx, text, x, y, font, opts = {}) {
- const lines = isArray(text) ? text : [text];
- const stroke = opts.strokeWidth > 0 && opts.strokeColor !== '';
- let i, line;
-
- ctx.save();
- ctx.font = font.string;
- setRenderOpts(ctx, opts);
-
- for (i = 0; i < lines.length; ++i) {
- line = lines[i];
-
- if (opts.backdrop) {
- drawBackdrop(ctx, opts.backdrop);
- }
-
- if (stroke) {
- if (opts.strokeColor) {
- ctx.strokeStyle = opts.strokeColor;
- }
-
- if (!isNullOrUndef(opts.strokeWidth)) {
- ctx.lineWidth = opts.strokeWidth;
- }
-
- ctx.strokeText(line, x, y, opts.maxWidth);
- }
-
- ctx.fillText(line, x, y, opts.maxWidth);
- decorateText(ctx, x, y, line, opts);
-
- y += font.lineHeight;
- }
-
- ctx.restore();
-}
-
-function setRenderOpts(ctx, opts) {
+function setRenderOpts(ctx: CanvasRenderingContext2D, opts: RenderTextOpts) {
if (opts.translation) {
ctx.translate(opts.translation[0], opts.translation[1]);
}
}
}
-function decorateText(ctx, x, y, line, opts) {
+function decorateText(
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ y: number,
+ line: string,
+ opts: RenderTextOpts
+) {
if (opts.strikethrough || opts.underline) {
/**
* Now that IE11 support has been dropped, we can use more
}
}
-function drawBackdrop(ctx, opts) {
+function drawBackdrop(ctx: CanvasRenderingContext2D, opts: BackdropOptions) {
const oldColor = ctx.fillStyle;
- ctx.fillStyle = opts.color;
+ ctx.fillStyle = opts.color as string;
ctx.fillRect(opts.left, opts.top, opts.width, opts.height);
ctx.fillStyle = oldColor;
}
+/**
+ * Render text onto the canvas
+ */
+export function renderText(
+ ctx: CanvasRenderingContext2D,
+ text: string | string[],
+ x: number,
+ y: number,
+ font: CanvasFontSpec,
+ opts: RenderTextOpts = {}
+) {
+ const lines = isArray(text) ? text : [text];
+ const stroke = opts.strokeWidth > 0 && opts.strokeColor !== '';
+ let i: number, line: string;
+
+ ctx.save();
+ ctx.font = font.string;
+ setRenderOpts(ctx, opts);
+
+ for (i = 0; i < lines.length; ++i) {
+ line = lines[i];
+
+ if (opts.backdrop) {
+ drawBackdrop(ctx, opts.backdrop);
+ }
+
+ if (stroke) {
+ if (opts.strokeColor) {
+ ctx.strokeStyle = opts.strokeColor;
+ }
+
+ if (!isNullOrUndef(opts.strokeWidth)) {
+ ctx.lineWidth = opts.strokeWidth;
+ }
+
+ ctx.strokeText(line, x, y, opts.maxWidth);
+ }
+
+ ctx.fillText(line, x, y, opts.maxWidth);
+ decorateText(ctx, x, y, line, opts);
+
+ y += Number(font.lineHeight);
+ }
+
+ ctx.restore();
+}
+
/**
* Add a path of a rectangle with rounded corners to the current sub-path
- * @param {CanvasRenderingContext2D} ctx Context
- * @param {*} rect Bounding rect
+ * @param ctx - Context
+ * @param rect - Bounding rect
*/
-export function addRoundedRectPath(ctx, rect) {
+export function addRoundedRectPath(
+ ctx: CanvasRenderingContext2D,
+ rect: RoundedRect & { radius: TRBLCorners }
+) {
const {x, y, w, h, radius} = rect;
// top left arc