- In order to support a strict content security policy (default-src 'self'),
- this page manually loads Chart.min.css and turns off the automatic style
- injection by setting Chart.platform.disableCSSInjection = true;.
-
-
-
-
-
-
-
diff --git a/samples/advanced/content-security-policy.js b/samples/advanced/content-security-policy.js
deleted file mode 100644
index a974fc6f349..00000000000
--- a/samples/advanced/content-security-policy.js
+++ /dev/null
@@ -1,53 +0,0 @@
-var utils = Samples.utils;
-
-utils.srand(110);
-// CSP: disable automatic style injection
-Chart.platform.disableCSSInjection = true;
-
-function generateData() {
- var DATA_COUNT = 16;
- var MIN_XY = -150;
- var MAX_XY = 100;
- var data = [];
- var i;
-
- for (i = 0; i < DATA_COUNT; ++i) {
- data.push({
- x: utils.rand(MIN_XY, MAX_XY),
- y: utils.rand(MIN_XY, MAX_XY),
- v: utils.rand(0, 1000)
- });
- }
-
- return data;
-}
-
-window.addEventListener('load', function() {
- new Chart('chart-0', {
- type: 'bubble',
- data: {
- datasets: [{
- backgroundColor: utils.color(0),
- data: generateData()
- }, {
- backgroundColor: utils.color(1),
- data: generateData()
- }]
- },
- options: {
- aspectRatio: 1,
- legend: false,
- tooltip: false,
- elements: {
- point: {
- radius: function(context) {
- var value = context.dataset.data[context.dataIndex];
- var size = context.chart.width;
- var base = Math.abs(value.v) / 1000;
- return (size / 24) * base;
- }
- }
- }
- }
- });
-});
diff --git a/samples/samples.js b/samples/samples.js
index 7811c8690d4..1f4fc37708f 100644
--- a/samples/samples.js
+++ b/samples/samples.js
@@ -244,9 +244,6 @@
items: [{
title: 'Progress bar',
path: 'advanced/progress-bar.html'
- }, {
- title: 'Content Security Policy',
- path: 'advanced/content-security-policy.html'
}, {
title: 'Polar Area Radial Gradient',
path: 'advanced/radial-gradient.html'
diff --git a/src/core/core.controller.js b/src/core/core.controller.js
index aca27dc759c..c3a29d5f22c 100644
--- a/src/core/core.controller.js
+++ b/src/core/core.controller.js
@@ -7,6 +7,7 @@ import layouts from './core.layouts';
import {BasicPlatform, DomPlatform} from '../platform/platforms';
import plugins from './core.plugins';
import scaleService from '../core/core.scaleService';
+import {getMaximumWidth, getMaximumHeight} from '../helpers/helpers.dom';
/**
* @typedef { import("../platform/platform.base").IEvent } IEvent
@@ -207,6 +208,7 @@ class Chart {
this.scales = {};
this.scale = undefined;
this.$plugins = undefined;
+ this.$proxies = {};
// Add the chart instance to the global namespace
Chart.instances[me.id] = me;
@@ -246,15 +248,15 @@ class Chart {
// Before init plugin notification
plugins.notify(me, 'beforeInit');
- helpers.dom.retinaScale(me, me.options.devicePixelRatio);
-
- me.bindEvents();
-
if (me.options.responsive) {
// Initial resize before chart draws (must be silent to preserve initial animations).
me.resize(true);
+ } else {
+ helpers.dom.retinaScale(me, me.options.devicePixelRatio);
}
+ me.bindEvents();
+
// After init plugin notification
plugins.notify(me, 'afterInit');
@@ -285,19 +287,25 @@ class Chart {
return this;
}
- resize(silent) {
+ resize(silent, width, height) {
const me = this;
const options = me.options;
const canvas = me.canvas;
- const aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null;
- const oldRatio = me.currentDevicePixelRatio;
+ const aspectRatio = options.maintainAspectRatio && me.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.
// Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed
- const newWidth = Math.max(0, Math.floor(helpers.dom.getMaximumWidth(canvas)));
- const newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.dom.getMaximumHeight(canvas)));
+ const newWidth = Math.max(0, Math.floor(width));
+ const newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : height));
+
+ // detect devicePixelRation changes
+ const oldRatio = me.currentDevicePixelRatio;
const newRatio = options.devicePixelRatio || me.platform.getDevicePixelRatio();
if (me.width === newWidth && me.height === newHeight && oldRatio === newRatio) {
@@ -932,11 +940,11 @@ class Chart {
listeners[type] = listener;
});
- // Elements used to detect size change should not be injected for non responsive charts.
- // See https://github.com/chartjs/Chart.js/issues/2210
if (me.options.responsive) {
- listener = function() {
- me.resize();
+ listener = function(width, height) {
+ if (me.canvas) {
+ me.resize(false, width, height);
+ }
};
me.platform.addEventListener(me, 'resize', listener);
diff --git a/src/helpers/helpers.dom.js b/src/helpers/helpers.dom.js
index 86114ff503e..e71ff3352d3 100644
--- a/src/helpers/helpers.dom.js
+++ b/src/helpers/helpers.dom.js
@@ -9,7 +9,7 @@ function isConstrainedValue(value) {
/**
* @private
*/
-function _getParentNode(domNode) {
+export function _getParentNode(domNode) {
let parent = domNode.parentNode;
if (parent && parent.toString() === '[object ShadowRoot]') {
parent = parent.host;
diff --git a/src/index.js b/src/index.js
index 695ae4fe9a3..d1b19287a79 100644
--- a/src/index.js
+++ b/src/index.js
@@ -16,7 +16,6 @@ import elements from './elements/index';
import Interaction from './core/core.interaction';
import layouts from './core/core.layouts';
import platforms from './platform/platforms';
-import platform from './platform/platform';
import pluginsCore from './core/core.plugins';
import Scale from './core/core.scale';
import scaleService from './core/core.scaleService';
@@ -35,7 +34,6 @@ Chart.elements = elements;
Chart.Interaction = Interaction;
Chart.layouts = layouts;
Chart.platforms = platforms;
-Chart.platform = platform;
Chart.plugins = pluginsCore;
Chart.Scale = Scale;
Chart.scaleService = scaleService;
diff --git a/src/platform/platform.dom.css b/src/platform/platform.dom.css
deleted file mode 100644
index 5e749593eeb..00000000000
--- a/src/platform/platform.dom.css
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * DOM element rendering detection
- * https://davidwalsh.name/detect-node-insertion
- */
-@keyframes chartjs-render-animation {
- from { opacity: 0.99; }
- to { opacity: 1; }
-}
-
-.chartjs-render-monitor {
- animation: chartjs-render-animation 0.001s;
-}
-
-/*
- * DOM element resizing detection
- * https://github.com/marcj/css-element-queries
- */
-.chartjs-size-monitor,
-.chartjs-size-monitor-expand,
-.chartjs-size-monitor-shrink {
- position: absolute;
- direction: ltr;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- overflow: hidden;
- pointer-events: none;
- visibility: hidden;
- z-index: -1;
-}
-
-.chartjs-size-monitor-expand > div {
- position: absolute;
- width: 1000000px;
- height: 1000000px;
- left: 0;
- top: 0;
-}
-
-.chartjs-size-monitor-shrink > div {
- position: absolute;
- width: 200%;
- height: 200%;
- left: 0;
- top: 0;
-}
diff --git a/src/platform/platform.dom.js b/src/platform/platform.dom.js
index 0bc5aebcc8c..220aa74db61 100644
--- a/src/platform/platform.dom.js
+++ b/src/platform/platform.dom.js
@@ -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);
+ });
+ observer.observe(document, {childList: true, subtree: true});
+ return observer;
}
-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);
+/**
+ * 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;
+ }
}
-
- // The container size might have changed, let's reset the resizer state.
- domPlatform._reset();
- }
+ });
});
+ observer.observe(parent, {childList: true});
+ return observer;
}
-function removeResizeListener(node) {
- const expando = node[EXPANDO_KEY] || {};
- const resizer = expando.resizer;
-
- 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