/
githubmirror
/
Chart.js
Обзор
Документация
Войти
/
githubmirror
/
Chart.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/helpers/helpers.dom.ts
286 строк
9 KB
Josh Kelley
Use integer canvas dimensions (#12142)
16 ноя 2025, 21:42
Не верифицирован
16 ноя 2025, 21:42
2898a6d
Код
Авторство
О чём код?
import type {ChartArea, Scale} from '../types/index.js'; import type PrivateChart from '../core/core.controller.js'; import type {Chart, ChartEvent} from '../types.js'; import {INFINITY} from './helpers.math.js'; /** * @private */ export function _isDomSupported(): boolean { return typeof window !== 'undefined' && typeof document !== 'undefined'; } /** * @private */ export function _getParentNode(domNode: HTMLCanvasElement): HTMLCanvasElement { let parent = domNode.parentNode; if (parent && parent.toString() === '[object ShadowRoot]') { parent = (parent as ShadowRoot).host; } return parent as HTMLCanvasElement; } /** * convert max-width/max-height values that may be percentages into a number * @private */ function parseMaxStyle(styleValue: string | number, node: HTMLElement, parentProperty: string) { let valueInPixels: number; if (typeof styleValue === 'string') { valueInPixels = parseInt(styleValue, 10); if (styleValue.indexOf('%') !== -1) { // percentage * size in dimension valueInPixels = (valueInPixels / 100) * node.parentNode[parentProperty]; } } else { valueInPixels = styleValue; } return valueInPixels; } const getComputedStyle = (element: HTMLElement): CSSStyleDeclaration => element.ownerDocument.defaultView.getComputedStyle(element, null); export function getStyle(el: HTMLElement, property: string): string { return getComputedStyle(el).getPropertyValue(property); } const positions = ['top', 'right', 'bottom', 'left']; function getPositionedStyle(styles: CSSStyleDeclaration, style: string, suffix?: string): ChartArea { const result = {} as ChartArea; suffix = suffix ? '-' + suffix : ''; for (let i = 0; i < 4; i++) { const pos = positions[i]; result[pos] = parseFloat(styles[style + '-' + pos + suffix]) || 0; } result.width = result.left + result.right; result.height = result.top + result.bottom; return result; } const useOffsetPos = (x: number, y: number, target: HTMLElement | EventTarget) => (x > 0 || y > 0) && (!target || !(target as HTMLElement).shadowRoot); /** * @param e * @param canvas * @returns Canvas position */ function getCanvasPosition( e: Event | TouchEvent | MouseEvent, canvas: HTMLCanvasElement ): { x: number; y: number; box: boolean; } { const touches = (e as TouchEvent).touches; const source = (touches && touches.length ? touches[0] : e) as MouseEvent; const {offsetX, offsetY} = source as MouseEvent; let box = false; let x, y; if (useOffsetPos(offsetX, offsetY, e.target)) { x = offsetX; y = offsetY; } else { const rect = canvas.getBoundingClientRect(); x = source.clientX - rect.left; y = source.clientY - rect.top; box = true; } return {x, y, box}; } /** * Gets an event's x, y coordinates, relative to the chart area * @param event * @param chart * @returns x and y coordinates of the event */ export function getRelativePosition( event: Event | ChartEvent | TouchEvent | MouseEvent, chart: Chart | PrivateChart ): { x: number; y: number } { if ('native' in event) { return event; } const {canvas, currentDevicePixelRatio} = chart; const style = getComputedStyle(canvas); const borderBox = style.boxSizing === 'border-box'; const paddings = getPositionedStyle(style, 'padding'); const borders = getPositionedStyle(style, 'border', 'width'); const {x, y, box} = getCanvasPosition(event, canvas); const xOffset = paddings.left + (box && borders.left); const yOffset = paddings.top + (box && borders.top); let {width, height} = chart; if (borderBox) { width -= paddings.width + borders.width; height -= paddings.height + borders.height; } return { x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio), y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio) }; } function getContainerSize(canvas: HTMLCanvasElement, width: number, height: number): Partial<Scale> { let maxWidth: number, maxHeight: number; if (width === undefined || height === undefined) { const container = canvas && _getParentNode(canvas); if (!container) { width = canvas.clientWidth; height = canvas.clientHeight; } else { const rect = container.getBoundingClientRect(); // this is the border box of the container const containerStyle = getComputedStyle(container); const containerBorder = getPositionedStyle(containerStyle, 'border', 'width'); const containerPadding = getPositionedStyle(containerStyle, 'padding'); width = rect.width - containerPadding.width - containerBorder.width; height = rect.height - containerPadding.height - containerBorder.height; maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth'); maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight'); } } return { width, height, maxWidth: maxWidth || INFINITY, maxHeight: maxHeight || INFINITY }; } const round1 = (v: number) => Math.round(v * 10) / 10; // eslint-disable-next-line complexity export function getMaximumSize( canvas: HTMLCanvasElement, bbWidth?: number, bbHeight?: number, aspectRatio?: number ): { width: number; height: number } { const style = getComputedStyle(canvas); const margins = getPositionedStyle(style, 'margin'); const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY; const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY; const containerSize = getContainerSize(canvas, bbWidth, bbHeight); let {width, height} = containerSize; if (style.boxSizing === 'content-box') { const borders = getPositionedStyle(style, 'border', 'width'); const paddings = getPositionedStyle(style, 'padding'); width -= paddings.width + borders.width; height -= paddings.height + borders.height; } width = Math.max(0, width - margins.width); height = Math.max(0, aspectRatio ? width / aspectRatio : height - margins.height); width = round1(Math.min(width, maxWidth, containerSize.maxWidth)); height = round1(Math.min(height, maxHeight, containerSize.maxHeight)); if (width && !height) { // https://github.com/chartjs/Chart.js/issues/4659 // If the canvas has width, but no height, default to aspectRatio of 2 (canvas default) height = round1(width / 2); } const maintainHeight = bbWidth !== undefined || bbHeight !== undefined; if (maintainHeight && aspectRatio && containerSize.height && height > containerSize.height) { height = containerSize.height; width = round1(Math.floor(height * aspectRatio)); } return {width, height}; } /** * @param chart * @param forceRatio * @param forceStyle * @returns True if the canvas context size or transformation has changed. */ export function retinaScale( chart: Chart | PrivateChart, forceRatio: number, forceStyle?: boolean ): boolean | void { const pixelRatio = forceRatio || 1; const deviceHeight = round1(chart.height * pixelRatio); const deviceWidth = round1(chart.width * pixelRatio); (chart as PrivateChart).height = round1(chart.height); (chart as PrivateChart).width = round1(chart.width); const canvas = chart.canvas; // If no style has been set on the canvas, the render size is used as display size, // making the chart visually bigger, so let's enforce it to the "correct" values. // See https://github.com/chartjs/Chart.js/issues/3575 if (canvas.style && (forceStyle || (!canvas.style.height && !canvas.style.width))) { canvas.style.height = `${chart.height}px`; canvas.style.width = `${chart.width}px`; } const canvasHeight = Math.floor(deviceHeight); const canvasWidth = Math.floor(deviceWidth); if (chart.currentDevicePixelRatio !== pixelRatio || canvas.height !== canvasHeight || canvas.width !== canvasWidth) { (chart as PrivateChart).currentDevicePixelRatio = pixelRatio; canvas.height = canvasHeight; canvas.width = canvasWidth; chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); return true; } return false; } /** * Detects support for options object argument in addEventListener. * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support * @private */ export const supportsEventListenerOptions = (function() { let passiveSupported = false; try { const options = { get passive() { // This function will be called when the browser attempts to access the passive property. passiveSupported = true; return false; } } as EventListenerOptions; if (_isDomSupported()) { window.addEventListener('test', null, options); window.removeEventListener('test', null, options); } } catch (e) { // continue regardless of error } return passiveSupported; }()); /** * The "used" size is the final value of a dimension property after all calculations have * been performed. This method uses the computed style of `element` but returns undefined * if the computed style is not expressed in pixels. That can happen in some cases where * `element` has a size relative to its parent and this last one is not yet displayed, * for example because of `display: none` on a parent node. * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value * @returns Size in pixels or undefined if unknown. */ export function readUsedSize( element: HTMLElement, property: 'width' | 'height' ): number | undefined { const value = getStyle(element, property); const matches = value && value.match(/^(\d+)(\.\d+)?px$/); return matches ? +matches[1] : undefined; }