* `helpers.distanceBetweenPoints` was renamed to `helpers.math.distanceBetweenPoints`
* `helpers.drawRoundedRectangle` was renamed to `helpers.canvas.roundedRect`
* `helpers.getAngleFromPoint` was renamed to `helpers.math.getAngleFromPoint`
-* `helpers.getMaximumHeight` was renamed to `helpers.dom.getMaximumHeight`
-* `helpers.getMaximumWidth` was renamed to `helpers.dom.getMaximumWidth`
* `helpers.getRelativePosition` was renamed to `helpers.dom.getRelativePosition`
* `helpers.getStyle` was renamed to `helpers.dom.getStyle`
* `helpers.getValueOrDefault` was renamed to `helpers.valueOrDefault`
##### Canvas Helper
* The second parameter to `drawPoint` is now the full options object, so `style`, `rotation`, and `radius` are no longer passed explicitly
+* `helpers.getMaximumHeight` was replaced by `helpers.dom.getMaximumSize`
+* `helpers.getMaximumWidth` was replaced by `helpers.dom.getMaximumSize`
#### Platform
import {BasicPlatform, DomPlatform} from '../platform';
import PluginService from './core.plugins';
import registry from './core.registry';
-import {getMaximumWidth, getMaximumHeight, retinaScale} from '../helpers/helpers.dom';
+import {retinaScale} from '../helpers/helpers.dom';
import {mergeIf, merge, _merger, each, callback as callCallback, uid, valueOrDefault, _elementsEqual} from '../helpers/helpers.core';
import {clear as canvasClear, clipArea, unclipArea, _isPointInArea} from '../helpers/helpers.canvas';
// @ts-ignore
return item;
}
-function computeNewSize(canvas, width, height, aspectRatio) {
- if (width === undefined || height === undefined) {
- width = getMaximumWidth(canvas);
- height = getMaximumHeight(canvas);
- }
- // the canvas render width and height will be casted to integers so make sure that
- // the canvas display style uses the same integer values to avoid blurring effect.
-
- // Minimum values set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed
- width = Math.max(0, Math.floor(width));
- return {
- width,
- height: Math.max(0, Math.floor(aspectRatio ? width / aspectRatio : height))
- };
-}
-
class Chart {
// eslint-disable-next-line max-statements
const options = me.options;
const canvas = me.canvas;
const aspectRatio = options.maintainAspectRatio && me.aspectRatio;
- const newSize = computeNewSize(canvas, width, height, aspectRatio);
+ const newSize = me.platform.getMaximumSize(canvas, width, height, aspectRatio);
// detect devicePixelRation changes
const oldRatio = me.currentDevicePixelRatio;
-/**
- * Returns if the given value contains an effective constraint.
- * @private
- */
-function isConstrainedValue(value) {
- return value !== undefined && value !== null && value !== 'none';
-}
-
/**
* @private
*/
return parent;
}
-// Private helper function to convert max-width/max-height values that may be percentages into a number
+/**
+ * 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') {
return valueInPixels;
}
-/**
- * Returns the max width or height of the given DOM node in a cross-browser compatible fashion
- * @param {HTMLElement} domNode - the node to check the constraint on
- * @param {string} maxStyle - the style that defines the maximum for the direction we are using ('max-width' / 'max-height')
- * @param {string} percentageProperty - property of parent to use when calculating width as a percentage
- * @return {number=} number or undefined if no constraint
- * @see {@link https://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser}
- */
-function getConstraintDimension(domNode, maxStyle, percentageProperty) {
- const view = document.defaultView;
- const parentNode = _getParentNode(domNode);
- const constrainedNode = view.getComputedStyle(domNode)[maxStyle];
- const constrainedContainer = view.getComputedStyle(parentNode)[maxStyle];
- const hasCNode = isConstrainedValue(constrainedNode);
- const hasCContainer = isConstrainedValue(constrainedContainer);
- const infinity = Number.POSITIVE_INFINITY;
-
- if (hasCNode || hasCContainer) {
- return Math.min(
- hasCNode ? parseMaxStyle(constrainedNode, domNode, percentageProperty) : infinity,
- hasCContainer ? parseMaxStyle(constrainedContainer, parentNode, percentageProperty) : infinity);
- }
-}
+const getComputedStyle = (element) => window.getComputedStyle(element, null);
export function getStyle(el, property) {
return el.currentStyle ?
el.currentStyle[property] :
- document.defaultView.getComputedStyle(el, null).getPropertyValue(property);
+ getComputedStyle(el).getPropertyValue(property);
}
-/**
- * @private
- */
-function _calculatePadding(container, padding, parentDimension) {
- padding = getStyle(container, padding);
-
- // If the padding is not set at all and the node is not in the DOM, this can be an empty string
- // In that case, we need to handle it as no padding
- if (padding === '') {
- return 0;
+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;
}
-
- return padding.indexOf('%') > -1 ? parentDimension * parseInt(padding, 10) / 100 : parseInt(padding, 10);
+ result.width = result.left + result.right;
+ result.height = result.top + result.bottom;
+ return result;
}
-export function getRelativePosition(evt, chart) {
+function getCanvasPosition(evt, canvas) {
const e = evt.originalEvent || evt;
const touches = e.touches;
const source = touches && touches.length ? touches[0] : e;
const {offsetX, offsetY} = source;
-
+ let box = false;
+ let x, y;
if (offsetX > 0 || offsetY > 0) {
- return {
- x: offsetX,
- y: offsetY
- };
+ x = offsetX;
+ y = offsetY;
+ } else {
+ const rect = canvas.getBoundingClientRect();
+ x = source.clientX - rect.left;
+ y = source.clientY - rect.top;
+ box = true;
}
-
- return calculateRelativePositionFromClientXY(source, chart);
+ return {x, y, box};
}
-function calculateRelativePositionFromClientXY(source, chart) {
- const {clientX: x, clientY: y} = source;
-
- const canvasElement = chart.canvas;
- const devicePixelRatio = chart.currentDevicePixelRatio;
- const boundingRect = canvasElement.getBoundingClientRect();
- // Scale mouse coordinates into canvas coordinates
- // by following the pattern laid out by 'jerryj' in the comments of
- // https://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
- const paddingLeft = parseFloat(getStyle(canvasElement, 'padding-left'));
- const paddingTop = parseFloat(getStyle(canvasElement, 'padding-top'));
- const paddingRight = parseFloat(getStyle(canvasElement, 'padding-right'));
- const paddingBottom = parseFloat(getStyle(canvasElement, 'padding-bottom'));
- const width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight;
- const height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom;
-
- // We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However
- // the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here
+export function getRelativePosition(evt, chart) {
+ 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 - boundingRect.left - paddingLeft) / (width) * canvasElement.width / devicePixelRatio),
- y: Math.round((y - boundingRect.top - paddingTop) / (height) * canvasElement.height / devicePixelRatio)
+ x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio),
+ y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio)
};
}
-function fallbackIfNotValid(measure, fallback) {
- return typeof measure === 'number' ? measure : fallback;
+const infinity = Number.POSITIVE_INFINITY;
+
+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 contarinerPadding = getPositionedStyle(containerStyle, 'padding');
+ width = rect.width - contarinerPadding.width - containerBorder.width;
+ height = rect.height - contarinerPadding.height - containerBorder.height;
+ maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth');
+ maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight');
+ }
+ }
+ return {
+ width,
+ height,
+ maxWidth: maxWidth || infinity,
+ maxHeight: maxHeight || infinity
+ };
}
-function getMax(domNode, prop, fallback, paddings) {
- const container = _getParentNode(domNode);
- if (!container) {
- return fallbackIfNotValid(domNode[prop], domNode[fallback]);
+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;
}
-
- const value = container[prop];
- const padding = paddings.reduce((acc, cur) => acc + _calculatePadding(container, 'padding-' + cur, value), 0);
-
- const v = value - padding;
- const cv = getConstraintDimension(domNode, 'max-' + fallback, prop);
- return isNaN(cv) ? v : Math.min(v, cv);
+ width = Math.max(0, width - margins.width);
+ height = Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height - margins.height);
+ return {
+ width: Math.min(width, maxWidth, containerSize.maxWidth),
+ height: Math.min(height, maxHeight, containerSize.maxHeight)
+ };
}
-export const getMaximumWidth = (domNode) => getMax(domNode, 'clientWidth', 'width', ['left', 'right']);
-export const getMaximumHeight = (domNode) => getMax(domNode, 'clientHeight', 'height', ['top', 'bottom']);
-
export function retinaScale(chart, forceRatio) {
const pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
const {canvas, width, height} = chart;
return 1;
}
+ /**
+ * Returns the maximum size in pixels of given canvas element.
+ * @param {HTMLCanvasElement} element
+ * @param {number} [width] - content width of parent element
+ * @param {number} [height] - content height of parent element
+ * @param {number} [aspectRatio] - aspect ratio to maintain
+ */
+ getMaximumSize(element, width, height, aspectRatio) {
+ width = Math.max(0, width || element.width);
+ height = height || element.height;
+ return {
+ width,
+ height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height)
+ };
+ }
+
/**
* @param {HTMLCanvasElement} canvas
* @returns {boolean} true if the canvas is attached to the platform, false if not.
*/
import BasePlatform from './platform.base';
-import {_getParentNode, getRelativePosition, supportsEventListenerOptions, readUsedSize} from '../helpers/helpers.dom';
+import {_getParentNode, getRelativePosition, supportsEventListenerOptions, readUsedSize, getMaximumSize} from '../helpers/helpers.dom';
import {throttled} from '../helpers/helpers.extras';
import {isNullOrUndef} from '../helpers/helpers.core';
chart.canvas.removeEventListener(type, listener, eventListenerOptions);
}
-function createEvent(type, chart, x, y, nativeEvent) {
+function fromNativeEvent(event, chart) {
+ const type = EVENT_TYPES[event.type] || event.type;
+ const {x, y} = getRelativePosition(event, chart);
return {
type,
chart,
- native: nativeEvent || null,
+ native: event,
x: x !== undefined ? x : null,
y: y !== undefined ? y : null,
};
}
-function fromNativeEvent(event, chart) {
- const type = EVENT_TYPES[event.type] || event.type;
- const pos = getRelativePosition(event, chart);
- return createEvent(type, chart, pos.x, pos.y, event);
-}
-
function createAttachObserver(chart, type, listener) {
const canvas = chart.canvas;
const container = canvas && _getParentNode(canvas);
return window.devicePixelRatio;
}
+ /**
+ * @param {HTMLCanvasElement} canvas
+ * @param {number} [width] - content width of parent element
+ * @param {number} [height] - content height of parent element
+ * @param {number} [aspectRatio] - aspect ratio to maintain
+ */
+ getMaximumSize(canvas, width, height, aspectRatio) {
+ return getMaximumSize(canvas, width, height, aspectRatio);
+ }
/**
* @param {HTMLCanvasElement} canvas
helpers = window.Chart.helpers.dom;
});
- it ('should get the maximum width and height for a node', function() {
+ it ('should get the maximum size for a node', function() {
// Create div with fixed size as a test bed
var div = document.createElement('div');
div.style.width = '200px';
var innerDiv = document.createElement('div');
div.appendChild(innerDiv);
- expect(helpers.getMaximumWidth(innerDiv)).toBe(200);
- expect(helpers.getMaximumHeight(innerDiv)).toBe(300);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 200, height: 300}));
document.body.removeChild(div);
});
var innerDiv = document.createElement('div');
shadow.appendChild(innerDiv);
- expect(helpers.getMaximumWidth(innerDiv)).toBe(200);
- expect(helpers.getMaximumHeight(innerDiv)).toBe(300);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 200, height: 300}));
document.body.removeChild(div);
});
innerDiv.style.maxWidth = '150px';
div.appendChild(innerDiv);
- expect(helpers.getMaximumWidth(innerDiv)).toBe(150);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 150}));
document.body.removeChild(div);
});
innerDiv.style.maxHeight = '150px';
div.appendChild(innerDiv);
- expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150}));
document.body.removeChild(div);
});
var innerDiv = document.createElement('div');
parentDiv.appendChild(innerDiv);
- expect(helpers.getMaximumWidth(innerDiv)).toBe(150);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 150}));
document.body.removeChild(div);
});
innerDiv.style.height = '300px'; // make it large
parentDiv.appendChild(innerDiv);
- expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150}));
document.body.removeChild(div);
});
innerDiv.style.maxWidth = '50%';
div.appendChild(innerDiv);
- expect(helpers.getMaximumWidth(innerDiv)).toBe(100);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 100}));
document.body.removeChild(div);
});
- it ('should get the maximum height of a node that has a percentage max-height style', function() {
+ it('should get the maximum height of a node that has a percentage max-height style', function() {
// Create div with fixed size as a test bed
var div = document.createElement('div');
div.style.width = '200px';
innerDiv.style.maxHeight = '50%';
div.appendChild(innerDiv);
- expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150}));
document.body.removeChild(div);
});
var innerDiv = document.createElement('div');
parentDiv.appendChild(innerDiv);
- expect(helpers.getMaximumWidth(innerDiv)).toBe(100);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 100}));
document.body.removeChild(div);
});
innerDiv.style.height = '300px'; // make it large
parentDiv.appendChild(innerDiv);
- expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+ expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150}));
document.body.removeChild(div);
});
innerDiv.appendChild(canvas);
// No padding
- expect(helpers.getMaximumWidth(canvas)).toBe(300);
+ expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 300}));
// test with percentage
innerDiv.style.padding = '5%';
- expect(helpers.getMaximumWidth(canvas)).toBe(270);
+ expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 270}));
// test with pixels
innerDiv.style.padding = '10px';
- expect(helpers.getMaximumWidth(canvas)).toBe(280);
+ expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 280}));
document.body.removeChild(div);
});
describe('getRelativePosition', function() {
it('should use offsetX/Y when available', function() {
- const event = {offsetX: 0, offsetY: 10};
- const chart = undefined;
- expect(helpers.getRelativePosition(event, chart)).toEqual({x: 0, y: 10});
+ const event = {offsetX: 50, offsetY: 100};
+ const chart = window.acquireChart({}, {
+ canvas: {
+ height: 200,
+ width: 200,
+ }
+ });
+ expect(helpers.getRelativePosition(event, chart)).toEqual({x: 50, y: 100});
+
+ const chart2 = window.acquireChart({}, {
+ canvas: {
+ height: 200,
+ width: 200,
+ style: 'padding: 10px'
+ }
+ });
+ expect(helpers.getRelativePosition(event, chart2)).toEqual({
+ x: Math.round((event.offsetX - 10) / 180 * 200),
+ y: Math.round((event.offsetY - 10) / 180 * 200)
+ });
+
+ const chart3 = window.acquireChart({}, {
+ canvas: {
+ height: 200,
+ width: 200,
+ style: 'width: 400px, height: 400px; padding: 10px'
+ }
+ });
+ expect(helpers.getRelativePosition(event, chart3)).toEqual({
+ x: Math.round((event.offsetX - 10) / 360 * 400),
+ y: Math.round((event.offsetY - 10) / 360 * 400)
+ });
+
+ const chart4 = window.acquireChart({}, {
+ canvas: {
+ height: 200,
+ width: 200,
+ style: 'width: 400px, height: 400px; padding: 10px; position: absolute; left: 20, top: 20'
+ }
+ });
+ expect(helpers.getRelativePosition(event, chart4)).toEqual({
+ x: Math.round((event.offsetX - 10) / 360 * 400),
+ y: Math.round((event.offsetY - 10) / 360 * 400)
+ });
+
});
it('should calculate from clientX/Y as fallback', function() {
-export function getMaximumHeight(node: HTMLElement): number;
-export function getMaximumWidth(node: HTMLElement): number;
+export function getMaximumSize(node: HTMLElement, width?: number, height?: number, aspectRatio?: number): { width: number, height: number };
export function getRelativePosition(
evt: MouseEvent,
chart: { readonly canvas: HTMLCanvasElement }
* @returns {number} the current devicePixelRatio of the device this platform is connected to.
*/
getDevicePixelRatio(): number;
+ /**
+ * @param {HTMLCanvasElement} canvas - The canvas for which to calculate the maximum size
+ * @param {number} [width] - Parent element's content width
+ * @param {number} [height] - Parent element's content height
+ * @param {number} [aspectRatio] - The aspect ratio to maintain
+ * @returns { width: number, height: number } the maximum size available.
+ */
+ getMaximumSize(canvas: HTMLCanvasElement, width?: number, height?: number, aspectRatio?: number): { width: number, height: number };
/**
* @param {HTMLCanvasElement} canvas
* @returns {boolean} true if the canvas is attached to the platform, false if not.