import {INFINITY} from './helpers.math'; /** * Note: typedefs are auto-exported, so use a made-up `dom` namespace where * necessary to avoid duplicates with `export * from './helpers`; see * https://github.com/microsoft/TypeScript/issues/46011 * @typedef { import("../core/core.controller").default } dom.Chart * @typedef { import('../../types/index.esm').ChartEvent } ChartEvent */ /** * @private */ export function _isDomSupported() { return typeof window !== 'undefined' && typeof document !== 'undefined'; } /** * @private */ export function _getParentNode(domNode) { let parent = domNode.parentNode; if (parent && parent.toString() === '[object ShadowRoot]') { parent = parent.host; } return parent; } /** * convert max-width/max-height values that may be percentages into a number * @private */ function parseMaxStyle(styleValue, node, parentProperty) { let valueInPixels; 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) => window.getComputedStyle(element, null); export function getStyle(el, property) { return getComputedStyle(el).getPropertyValue(property); } const positions = ['top', 'right', 'bottom', 'left']; function getPositionedStyle(styles, style, suffix) { const result = {}; 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, y, target) => (x > 0 || y > 0) && (!target || !target.shadowRoot); /** * @param {Event} e * @param {HTMLCanvasElement} canvas * @returns {{x: number, y: number, box: boolean}} */ function getCanvasPosition(e, canvas) { // @ts-ignore const touches = e.touches; const source = touches && touches.length ? touches[0] : e; const {offsetX, offsetY} = source; 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|ChartEvent} evt * @param {dom.Chart} chart * @returns {{x: number, y: number}} */ export function getRelativePosition(evt, chart) { if ('native' in evt) { return evt; } 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(evt, 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, width, height) { let maxWidth, maxHeight; if (width === undefined || height === undefined) { const container = _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 => Math.round(v * 10) / 10; export function getMaximumSize(canvas, bbWidth, bbHeight, aspectRatio) { 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 ? Math.floor(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); } return { width, height }; } /** * @param {import('../core/core.controller').default} chart * @param {number} [forceRatio] * @param {boolean} [forceStyle] * @returns {boolean} True if the canvas context size or transformation has changed. */ export function retinaScale(chart, forceRatio, forceStyle) { const pixelRatio = forceRatio || 1; const deviceHeight = Math.floor(chart.height * pixelRatio); const deviceWidth = Math.floor(chart.width * pixelRatio); chart.height = deviceHeight / pixelRatio; chart.width = deviceWidth / pixelRatio; 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`; } if (chart.currentDevicePixelRatio !== pixelRatio || canvas.height !== deviceHeight || canvas.width !== deviceWidth) { chart.currentDevicePixelRatio = pixelRatio; canvas.height = deviceHeight; canvas.width = deviceWidth; 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; } }; // @ts-ignore window.addEventListener('test', null, options); // @ts-ignore 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 {number=} Size in pixels or undefined if unknown. */ export function readUsedSize(element, property) { const value = getStyle(element, property); const matches = value && value.match(/^(\d+)(\.\d+)?px$/); return matches ? +matches[1] : undefined; }