|
|
|
|
@@ -4,17 +4,14 @@
|
|
|
|
|
|
|
|
|
|
import helpers from '../helpers/index';
|
|
|
|
|
import BasePlatform from './platform.base';
|
|
|
|
|
import platform from './platform';
|
|
|
|
|
import {_getParentNode} from '../helpers/helpers.dom';
|
|
|
|
|
import ResizeObserver from 'resize-observer-polyfill';
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
import stylesheet from './platform.dom.css';
|
|
|
|
|
/**
|
|
|
|
|
* @typedef { import("../core/core.controller").default } Chart
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const EXPANDO_KEY = '$chartjs';
|
|
|
|
|
const CSS_PREFIX = 'chartjs-';
|
|
|
|
|
const CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor';
|
|
|
|
|
const CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor';
|
|
|
|
|
const CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation';
|
|
|
|
|
const ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart'];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DOM event types -> Chart.js event types.
|
|
|
|
|
@@ -52,6 +49,8 @@ function readUsedSize(element, property) {
|
|
|
|
|
* Initializes the canvas style and render size without modifying the canvas display size,
|
|
|
|
|
* since responsiveness is handled by the controller.resize() method. The config is used
|
|
|
|
|
* to determine the aspect ratio to apply in case no explicit height has been specified.
|
|
|
|
|
* @param {HTMLCanvasElement} canvas
|
|
|
|
|
* @param {{ options: any; }} config
|
|
|
|
|
*/
|
|
|
|
|
function initCanvas(canvas, config) {
|
|
|
|
|
const style = canvas.style;
|
|
|
|
|
@@ -78,6 +77,8 @@ function initCanvas(canvas, config) {
|
|
|
|
|
// elements, which would interfere with the responsive resize process.
|
|
|
|
|
// https://github.com/chartjs/Chart.js/issues/2538
|
|
|
|
|
style.display = style.display || 'block';
|
|
|
|
|
// Include possible borders in the size
|
|
|
|
|
style.boxSizing = style.boxSizing || 'border-box';
|
|
|
|
|
|
|
|
|
|
if (renderWidth === null || renderWidth === '') {
|
|
|
|
|
const displayWidth = readUsedSize(canvas, 'width');
|
|
|
|
|
@@ -169,147 +170,131 @@ function throttled(fn, thisArg) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createDiv(cls) {
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
el.className = cls || '';
|
|
|
|
|
return el;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Implementation based on https://github.com/marcj/css-element-queries
|
|
|
|
|
function createResizer(domPlatform, handler) {
|
|
|
|
|
const maxSize = 1000000;
|
|
|
|
|
|
|
|
|
|
// NOTE(SB) Don't use innerHTML because it could be considered unsafe.
|
|
|
|
|
// https://github.com/chartjs/Chart.js/issues/5902
|
|
|
|
|
const resizer = createDiv(CSS_SIZE_MONITOR);
|
|
|
|
|
const expand = createDiv(CSS_SIZE_MONITOR + '-expand');
|
|
|
|
|
const shrink = createDiv(CSS_SIZE_MONITOR + '-shrink');
|
|
|
|
|
|
|
|
|
|
expand.appendChild(createDiv());
|
|
|
|
|
shrink.appendChild(createDiv());
|
|
|
|
|
|
|
|
|
|
resizer.appendChild(expand);
|
|
|
|
|
resizer.appendChild(shrink);
|
|
|
|
|
domPlatform._reset = function() {
|
|
|
|
|
expand.scrollLeft = maxSize;
|
|
|
|
|
expand.scrollTop = maxSize;
|
|
|
|
|
shrink.scrollLeft = maxSize;
|
|
|
|
|
shrink.scrollTop = maxSize;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onScroll = function() {
|
|
|
|
|
domPlatform._reset();
|
|
|
|
|
handler();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
addListener(expand, 'scroll', onScroll.bind(expand, 'expand'));
|
|
|
|
|
addListener(shrink, 'scroll', onScroll.bind(shrink, 'shrink'));
|
|
|
|
|
|
|
|
|
|
return resizer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://davidwalsh.name/detect-node-insertion
|
|
|
|
|
function watchForRender(node, handler) {
|
|
|
|
|
const expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
|
|
|
|
|
const proxy = expando.renderProxy = function(e) {
|
|
|
|
|
if (e.animationName === CSS_RENDER_ANIMATION) {
|
|
|
|
|
handler();
|
|
|
|
|
/**
|
|
|
|
|
* Watch for resize of `element`.
|
|
|
|
|
* Calling `fn` is limited to once per animation frame
|
|
|
|
|
* @param {Element} element - The element to monitor
|
|
|
|
|
* @param {function} fn - Callback function to call when resized
|
|
|
|
|
* @return {ResizeObserver}
|
|
|
|
|
*/
|
|
|
|
|
function watchForResize(element, fn) {
|
|
|
|
|
const resize = throttled((width, height) => {
|
|
|
|
|
const w = element.clientWidth;
|
|
|
|
|
fn(width, height);
|
|
|
|
|
if (w < element.clientWidth) {
|
|
|
|
|
// If the container size shrank during chart resize, let's assume
|
|
|
|
|
// scrollbar appeared. So we resize again with the scrollbar visible -
|
|
|
|
|
// effectively making chart smaller and the scrollbar hidden again.
|
|
|
|
|
// Because we are inside `throttled`, and currently `ticking`, scroll
|
|
|
|
|
// events are ignored during this whole 2 resize process.
|
|
|
|
|
// If we assumed wrong and something else happened, we are resizing
|
|
|
|
|
// twice in a frame (potential performance issue)
|
|
|
|
|
fn();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, window);
|
|
|
|
|
|
|
|
|
|
ANIMATION_START_EVENTS.forEach((type) => {
|
|
|
|
|
addListener(node, type, proxy);
|
|
|
|
|
const observer = new ResizeObserver(entries => {
|
|
|
|
|
const entry = entries[0];
|
|
|
|
|
resize(entry.contentRect.width, entry.contentRect.height);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// #4737: Chrome might skip the CSS animation when the CSS_RENDER_MONITOR class
|
|
|
|
|
// is removed then added back immediately (same animation frame?). Accessing the
|
|
|
|
|
// `offsetParent` property will force a reflow and re-evaluate the CSS animation.
|
|
|
|
|
// https://gist.github.com/paulirish/5d52fb081b3570c81e3a#box-metrics
|
|
|
|
|
// https://github.com/chartjs/Chart.js/issues/4737
|
|
|
|
|
expando.reflow = !!node.offsetParent;
|
|
|
|
|
|
|
|
|
|
node.classList.add(CSS_RENDER_MONITOR);
|
|
|
|
|
observer.observe(element);
|
|
|
|
|
return observer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unwatchForRender(node) {
|
|
|
|
|
const expando = node[EXPANDO_KEY] || {};
|
|
|
|
|
const proxy = expando.renderProxy;
|
|
|
|
|
|
|
|
|
|
if (proxy) {
|
|
|
|
|
ANIMATION_START_EVENTS.forEach((type) => {
|
|
|
|
|
removeListener(node, type, proxy);
|
|
|
|
|
/**
|
|
|
|
|
* Detect attachment of `element` or its direct `parent` to DOM
|
|
|
|
|
* @param {Element} element - The element to watch for
|
|
|
|
|
* @param {function} fn - Callback function to call when attachment is detected
|
|
|
|
|
* @return {MutationObserver}
|
|
|
|
|
*/
|
|
|
|
|
function watchForAttachment(element, fn) {
|
|
|
|
|
const observer = new MutationObserver(entries => {
|
|
|
|
|
const parent = _getParentNode(element);
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
for (let i = 0; i < entry.addedNodes.length; i++) {
|
|
|
|
|
const added = entry.addedNodes[i];
|
|
|
|
|
if (added === element || added === parent) {
|
|
|
|
|
fn(entry.target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
delete expando.renderProxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node.classList.remove(CSS_RENDER_MONITOR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addResizeListener(node, listener, chart, domPlatform) {
|
|
|
|
|
const expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
|
|
|
|
|
|
|
|
|
|
// Let's keep track of this added resizer and thus avoid DOM query when removing it.
|
|
|
|
|
const resizer = expando.resizer = createResizer(domPlatform, throttled(() => {
|
|
|
|
|
if (expando.resizer) {
|
|
|
|
|
const container = chart.options.maintainAspectRatio && node.parentNode;
|
|
|
|
|
const w = container ? container.clientWidth : 0;
|
|
|
|
|
listener(createEvent('resize', chart));
|
|
|
|
|
if (container && container.clientWidth < w && chart.canvas) {
|
|
|
|
|
// If the container size shrank during chart resize, let's assume
|
|
|
|
|
// scrollbar appeared. So we resize again with the scrollbar visible -
|
|
|
|
|
// effectively making chart smaller and the scrollbar hidden again.
|
|
|
|
|
// Because we are inside `throttled`, and currently `ticking`, scroll
|
|
|
|
|
// events are ignored during this whole 2 resize process.
|
|
|
|
|
// If we assumed wrong and something else happened, we are resizing
|
|
|
|
|
// twice in a frame (potential performance issue)
|
|
|
|
|
listener(createEvent('resize', chart));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// The resizer needs to be attached to the node parent, so we first need to be
|
|
|
|
|
// sure that `node` is attached to the DOM before injecting the resizer element.
|
|
|
|
|
watchForRender(node, () => {
|
|
|
|
|
if (expando.resizer) {
|
|
|
|
|
const container = node.parentNode;
|
|
|
|
|
if (container && container !== resizer.parentNode) {
|
|
|
|
|
container.insertBefore(resizer, container.firstChild);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The container size might have changed, let's reset the resizer state.
|
|
|
|
|
domPlatform._reset();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
observer.observe(document, {childList: true, subtree: true});
|
|
|
|
|
return observer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeResizeListener(node) {
|
|
|
|
|
const expando = node[EXPANDO_KEY] || {};
|
|
|
|
|
const resizer = expando.resizer;
|
|
|
|
|
/**
|
|
|
|
|
* Watch for detachment of `element` from its direct `parent`.
|
|
|
|
|
* @param {Element} element - The element to watch
|
|
|
|
|
* @param {function} fn - Callback function to call when detached.
|
|
|
|
|
* @return {MutationObserver=}
|
|
|
|
|
*/
|
|
|
|
|
function watchForDetachment(element, fn) {
|
|
|
|
|
const parent = _getParentNode(element);
|
|
|
|
|
if (!parent) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const observer = new MutationObserver(entries => {
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
for (let i = 0; i < entry.removedNodes.length; i++) {
|
|
|
|
|
if (entry.removedNodes[i] === element) {
|
|
|
|
|
fn();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
observer.observe(parent, {childList: true});
|
|
|
|
|
return observer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete expando.resizer;
|
|
|
|
|
unwatchForRender(node);
|
|
|
|
|
|
|
|
|
|
if (resizer && resizer.parentNode) {
|
|
|
|
|
resizer.parentNode.removeChild(resizer);
|
|
|
|
|
/**
|
|
|
|
|
* @param {{ [x: string]: any; resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
|
|
|
|
|
* @param {string} type
|
|
|
|
|
*/
|
|
|
|
|
function removeObserver(proxies, type) {
|
|
|
|
|
const observer = proxies[type];
|
|
|
|
|
if (observer) {
|
|
|
|
|
observer.disconnect();
|
|
|
|
|
proxies[type] = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Injects CSS styles inline if the styles are not already present.
|
|
|
|
|
* @param {Node} rootNode - the HTMLDocument|ShadowRoot node to contain the <style>.
|
|
|
|
|
* @param {string} css - the CSS to be injected.
|
|
|
|
|
* @param {{ resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
|
|
|
|
|
*/
|
|
|
|
|
function injectCSS(rootNode, css) {
|
|
|
|
|
// https://stackoverflow.com/q/3922139
|
|
|
|
|
const expando = rootNode[EXPANDO_KEY] || (rootNode[EXPANDO_KEY] = {});
|
|
|
|
|
if (!expando.containsStyles) {
|
|
|
|
|
expando.containsStyles = true;
|
|
|
|
|
css = '/* Chart.js */\n' + css;
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
style.setAttribute('type', 'text/css');
|
|
|
|
|
style.appendChild(document.createTextNode(css));
|
|
|
|
|
rootNode.appendChild(style);
|
|
|
|
|
function unlistenForResize(proxies) {
|
|
|
|
|
removeObserver(proxies, 'attach');
|
|
|
|
|
removeObserver(proxies, 'detach');
|
|
|
|
|
removeObserver(proxies, 'resize');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {HTMLCanvasElement} canvas
|
|
|
|
|
* @param {{ resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
|
|
|
|
|
* @param {function} listener
|
|
|
|
|
*/
|
|
|
|
|
function listenForResize(canvas, proxies, listener) {
|
|
|
|
|
// Helper for recursing when canvas is detached from it's parent
|
|
|
|
|
const detached = () => listenForResize(canvas, proxies, listener);
|
|
|
|
|
|
|
|
|
|
// First make sure all observers are removed
|
|
|
|
|
unlistenForResize(proxies);
|
|
|
|
|
// Then check if we are attached
|
|
|
|
|
const container = _getParentNode(canvas);
|
|
|
|
|
if (container) {
|
|
|
|
|
// The canvas is attached (or was immediately re-attached when called through `detached`)
|
|
|
|
|
proxies.resize = watchForResize(container, listener);
|
|
|
|
|
proxies.detach = watchForDetachment(canvas, detached);
|
|
|
|
|
} else {
|
|
|
|
|
// The canvas is detached
|
|
|
|
|
proxies.attach = watchForAttachment(canvas, () => {
|
|
|
|
|
// The canvas was attached.
|
|
|
|
|
removeObserver(proxies, 'attach');
|
|
|
|
|
const parent = _getParentNode(canvas);
|
|
|
|
|
proxies.resize = watchForResize(parent, listener);
|
|
|
|
|
proxies.detach = watchForDetachment(canvas, detached);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -318,39 +303,12 @@ function injectCSS(rootNode, css) {
|
|
|
|
|
* @extends BasePlatform
|
|
|
|
|
*/
|
|
|
|
|
export default class DomPlatform extends BasePlatform {
|
|
|
|
|
/**
|
|
|
|
|
* @constructor
|
|
|
|
|
*/
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When `true`, prevents the automatic injection of the stylesheet required to
|
|
|
|
|
* correctly detect when the chart is added to the DOM and then resized. This
|
|
|
|
|
* switch has been added to allow external stylesheet (`dist/Chart(.min)?.js`)
|
|
|
|
|
* to be manually imported to make this library compatible with any CSP.
|
|
|
|
|
* See https://github.com/chartjs/Chart.js/issues/5208
|
|
|
|
|
*/
|
|
|
|
|
this.disableCSSInjection = platform.disableCSSInjection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes resources that depend on platform options.
|
|
|
|
|
* @param {HTMLCanvasElement} canvas - The Canvas element.
|
|
|
|
|
* @private
|
|
|
|
|
* @param {HTMLCanvasElement} canvas
|
|
|
|
|
* @param {{ options: { aspectRatio?: number; }; }} config
|
|
|
|
|
* @return {CanvasRenderingContext2D=}
|
|
|
|
|
*/
|
|
|
|
|
_ensureLoaded(canvas) {
|
|
|
|
|
if (!this.disableCSSInjection) {
|
|
|
|
|
// If the canvas is in a shadow DOM, then the styles must also be inserted
|
|
|
|
|
// into the same shadow DOM.
|
|
|
|
|
// https://github.com/chartjs/Chart.js/issues/5763
|
|
|
|
|
const root = canvas.getRootNode ? canvas.getRootNode() : document;
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const targetNode = root.host ? root : document.head;
|
|
|
|
|
injectCSS(targetNode, stylesheet);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acquireContext(canvas, config) {
|
|
|
|
|
// To prevent canvas fingerprinting, some add-ons undefine the getContext
|
|
|
|
|
// method, for example: https://github.com/kkapsner/CanvasBlocker
|
|
|
|
|
@@ -367,7 +325,6 @@ export default class DomPlatform extends BasePlatform {
|
|
|
|
|
if (context && context.canvas === canvas) {
|
|
|
|
|
// Load platform resources on first chart creation, to make it possible to
|
|
|
|
|
// import the library before setting platform options.
|
|
|
|
|
this._ensureLoaded(canvas);
|
|
|
|
|
initCanvas(canvas, config);
|
|
|
|
|
return context;
|
|
|
|
|
}
|
|
|
|
|
@@ -375,6 +332,9 @@ export default class DomPlatform extends BasePlatform {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {CanvasRenderingContext2D} context
|
|
|
|
|
*/
|
|
|
|
|
releaseContext(context) {
|
|
|
|
|
const canvas = context.canvas;
|
|
|
|
|
if (!canvas[EXPANDO_KEY]) {
|
|
|
|
|
@@ -407,39 +367,49 @@ export default class DomPlatform extends BasePlatform {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {Chart} chart
|
|
|
|
|
* @param {string} type
|
|
|
|
|
* @param {function} listener
|
|
|
|
|
*/
|
|
|
|
|
addEventListener(chart, type, listener) {
|
|
|
|
|
// Can have only one listener per type, so make sure previous is removed
|
|
|
|
|
this.removeEventListener(chart, type);
|
|
|
|
|
|
|
|
|
|
const canvas = chart.canvas;
|
|
|
|
|
const proxies = chart.$proxies || (chart.$proxies = {});
|
|
|
|
|
if (type === 'resize') {
|
|
|
|
|
// Note: the resize event is not supported on all browsers.
|
|
|
|
|
addResizeListener(canvas, listener, chart, this);
|
|
|
|
|
return;
|
|
|
|
|
return listenForResize(canvas, proxies, listener);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expando = listener[EXPANDO_KEY] || (listener[EXPANDO_KEY] = {});
|
|
|
|
|
const proxies = expando.proxies || (expando.proxies = {});
|
|
|
|
|
const proxy = proxies[chart.id + '_' + type] = throttled((event) => {
|
|
|
|
|
const proxy = proxies[type] = throttled((event) => {
|
|
|
|
|
listener(fromNativeEvent(event, chart));
|
|
|
|
|
}, chart);
|
|
|
|
|
|
|
|
|
|
addListener(canvas, type, proxy);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeEventListener(chart, type, listener) {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Chart} chart
|
|
|
|
|
* @param {string} type
|
|
|
|
|
*/
|
|
|
|
|
removeEventListener(chart, type) {
|
|
|
|
|
const canvas = chart.canvas;
|
|
|
|
|
const proxies = chart.$proxies || (chart.$proxies = {});
|
|
|
|
|
|
|
|
|
|
if (type === 'resize') {
|
|
|
|
|
// Note: the resize event is not supported on all browsers.
|
|
|
|
|
removeResizeListener(canvas);
|
|
|
|
|
return;
|
|
|
|
|
return unlistenForResize(proxies);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expando = listener[EXPANDO_KEY] || {};
|
|
|
|
|
const proxies = expando.proxies || {};
|
|
|
|
|
const proxy = proxies[chart.id + '_' + type];
|
|
|
|
|
const proxy = proxies[type];
|
|
|
|
|
if (!proxy) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeListener(canvas, type, proxy);
|
|
|
|
|
proxies[type] = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDevicePixelRatio() {
|
|
|
|
|
|