Fix: maximum size and mouse position with styling (#7816)

Fix: maximum size and mouse position with styling
This commit is contained in:
Jukka Kurkela 2020-09-26 20:18:35 +03:00 committed by GitHub
parent 2bb23dfe8c
commit 10f393a58d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 192 additions and 139 deletions

View File

@ -380,8 +380,6 @@ The following properties were renamed during v3 development:
* `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`
@ -477,6 +475,8 @@ The APIs listed in this section have changed in signature or behaviour from vers
##### 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

View File

@ -6,7 +6,7 @@ import layouts from './core.layouts';
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
@ -214,22 +214,6 @@ function getCanvas(item) {
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
@ -355,7 +339,7 @@ class Chart {
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;

View File

@ -1,11 +1,3 @@
/**
* Returns if the given value contains an effective constraint.
* @private
*/
function isConstrainedValue(value) {
return value !== undefined && value !== null && value !== 'none';
}
/**
* @private
*/
@ -17,7 +9,10 @@ export function _getParentNode(domNode) {
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') {
@ -34,111 +29,117 @@ function parseMaxStyle(styleValue, node, parentProperty) {
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;
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);
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
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 getMax(domNode, prop, fallback, paddings) {
const container = _getParentNode(domNode);
if (!container) {
return fallbackIfNotValid(domNode[prop], domNode[fallback]);
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');
}
}
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);
return {
width,
height,
maxWidth: maxWidth || infinity,
maxHeight: maxHeight || infinity
};
}
export const getMaximumWidth = (domNode) => getMax(domNode, 'clientWidth', 'width', ['left', 'right']);
export const getMaximumHeight = (domNode) => getMax(domNode, 'clientHeight', 'height', ['top', 'bottom']);
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);
return {
width: Math.min(width, maxWidth, containerSize.maxWidth),
height: Math.min(height, maxHeight, containerSize.maxHeight)
};
}
export function retinaScale(chart, forceRatio) {
const pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1;

View File

@ -49,6 +49,22 @@ export default class BasePlatform {
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.

View File

@ -3,7 +3,7 @@
*/
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';
@ -102,22 +102,18 @@ function removeListener(chart, type, listener) {
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);
@ -371,6 +367,15 @@ export default class DomPlatform extends BasePlatform {
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

View File

@ -5,7 +5,7 @@ describe('DOM helpers tests', function() {
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';
@ -17,8 +17,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -42,8 +41,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -61,7 +59,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -79,7 +77,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -101,7 +99,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -124,7 +122,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -142,12 +140,12 @@ describe('DOM helpers tests', function() {
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';
@ -160,7 +158,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -182,7 +180,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -204,7 +202,7 @@ describe('DOM helpers tests', function() {
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);
});
@ -226,15 +224,15 @@ describe('DOM helpers tests', function() {
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);
});
@ -258,9 +256,51 @@ describe('DOM helpers tests', function() {
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() {

View File

@ -1,5 +1,4 @@
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 }

View File

@ -38,6 +38,14 @@ export class BasePlatform {
* @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.