this.active = undefined;
this.lastActive = [];
this._lastEvent = undefined;
- /** @type {{resize?: function}} */
+ /** @type {{attach?: function, detach?: function, resize?: function}} */
this._listeners = {};
this._sortedMetasets = [];
this._updating = false;
this.$plugins = undefined;
this.$proxies = {};
this._hiddenIndices = {};
+ this.attached = true;
// Add the chart instance to the global namespace
Chart.instances[me.id] = me;
Animator.listen(me, 'progress', onAnimationProgress);
me._initialize();
- me.update();
+ if (me.attached) {
+ me.update();
+ }
}
/**
options.onResize(me, newSize);
}
- me.update('resize');
+ // Only apply 'resize' mode if we are attached, else do a regular update.
+ me.update(me.attached && 'resize');
}
}
};
if (Animator.has(me)) {
- if (!Animator.running(me)) {
+ if (me.attached && !Animator.running(me)) {
Animator.start(me);
}
} else {
bindEvents() {
const me = this;
const listeners = me._listeners;
+ const platform = me.platform;
+
+ const _add = (type, listener) => {
+ platform.addEventListener(me, type, listener);
+ listeners[type] = listener;
+ };
+ const _remove = (type, listener) => {
+ if (listeners[type]) {
+ platform.removeEventListener(me, type, listener);
+ delete listeners[type];
+ }
+ };
+
let listener = function(e) {
me._eventHandler(e);
};
- helpers.each(me.options.events, (type) => {
- me.platform.addEventListener(me, type, listener);
- listeners[type] = listener;
- });
+ helpers.each(me.options.events, (type) => _add(type, listener));
if (me.options.responsive) {
- listener = function(width, height) {
+ listener = (width, height) => {
if (me.canvas) {
me.resize(false, width, height);
}
};
- me.platform.addEventListener(me, 'resize', listener);
- listeners.resize = listener;
+ let detached; // eslint-disable-line prefer-const
+ const attached = () => {
+ _remove('attach', attached);
+
+ me.resize();
+ me.attached = true;
+
+ _add('resize', listener);
+ _add('detach', detached);
+ };
+
+ detached = () => {
+ me.attached = false;
+
+ _remove('resize', listener);
+ _add('attach', attached);
+ };
+
+ if (platform.isAttached(me.canvas)) {
+ attached();
+ } else {
+ detached();
+ }
+ } else {
+ me.attached = true;
}
}
};
}
-/**
- * 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
- */
-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);
-
- // @ts-ignore until https://github.com/Microsoft/TypeScript/issues/28502 implemented
- const observer = new ResizeObserver(entries => {
- const entry = entries[0];
- resize(entry.contentRect.width, entry.contentRect.height);
- });
- observer.observe(element);
- return observer;
-}
-
-/**
- * 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) {
+function createAttachObserver(chart, type, listener) {
+ const canvas = chart.canvas;
+ const container = canvas && _getParentNode(canvas);
+ const element = container || canvas;
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);
+ listener(entry.target);
}
}
});
return observer;
}
-/**
- * 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) {
+function createDetachObserver(chart, type, listener) {
+ const canvas = chart.canvas;
+ const container = canvas && _getParentNode(canvas);
+ if (!container) {
return;
}
const observer = new MutationObserver(entries => {
entries.forEach(entry => {
for (let i = 0; i < entry.removedNodes.length; i++) {
- if (entry.removedNodes[i] === element) {
- fn();
+ if (entry.removedNodes[i] === canvas) {
+ listener();
break;
}
}
});
});
- observer.observe(parent, {childList: true});
+ observer.observe(container, {childList: true});
return observer;
}
-/**
- * @param {{ [x: string]: any; resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
- * @param {string} type
- */
-function removeObserver(proxies, type) {
- const observer = proxies[type];
+function createResizeObserver(chart, type, listener) {
+ const canvas = chart.canvas;
+ const container = canvas && _getParentNode(canvas);
+ if (!container) {
+ return;
+ }
+ const resize = throttled((width, height) => {
+ const w = container.clientWidth;
+ listener(width, height);
+ if (w < container.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)
+ listener();
+ }
+ }, window);
+
+ // @ts-ignore until https://github.com/microsoft/TypeScript/issues/37861 implemented
+ const observer = new ResizeObserver(entries => {
+ const entry = entries[0];
+ resize(entry.contentRect.width, entry.contentRect.height);
+ });
+ observer.observe(container);
+ return observer;
+}
+
+function releaseObserver(canvas, type, observer) {
if (observer) {
observer.disconnect();
- proxies[type] = undefined;
}
}
-/**
- * @param {{ resize?: any; detach?: MutationObserver; attach?: MutationObserver; }} proxies
- */
-function unlistenForResize(proxies) {
- removeObserver(proxies, 'attach');
- removeObserver(proxies, 'detach');
- removeObserver(proxies, 'resize');
-}
+function createProxyAndListen(chart, type, listener) {
+ const canvas = chart.canvas;
+ const proxy = throttled((event) => {
+ // This case can occur if the chart is destroyed while waiting
+ // for the throttled function to occur. We prevent crashes by checking
+ // for a destroyed chart
+ if (chart.ctx !== null) {
+ listener(fromNativeEvent(event, chart));
+ }
+ }, chart);
-/**
- * @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);
- });
- }
+ addListener(canvas, type, proxy);
+
+ return proxy;
}
/**
// 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') {
- return listenForResize(canvas, proxies, listener);
- }
-
- const proxy = proxies[type] = throttled((event) => {
- // This case can occur if the chart is destroyed while waiting
- // for the throttled function to occur. We prevent crashes by checking
- // for a destroyed chart
- if (chart.ctx !== null) {
- listener(fromNativeEvent(event, chart));
- }
- }, chart);
-
- addListener(canvas, type, proxy);
+ const handlers = {
+ attach: createAttachObserver,
+ detach: createDetachObserver,
+ resize: createResizeObserver
+ };
+ const handler = handlers[type] || createProxyAndListen;
+ proxies[type] = handler(chart, type, listener);
}
removeEventListener(chart, type) {
const canvas = chart.canvas;
const proxies = chart.$proxies || (chart.$proxies = {});
-
- if (type === 'resize') {
- return unlistenForResize(proxies);
- }
-
const proxy = proxies[type];
+
if (!proxy) {
return;
}
- removeListener(canvas, type, proxy);
+ const handlers = {
+ attach: releaseObserver,
+ detach: releaseObserver,
+ resize: releaseObserver
+ };
+ const handler = handlers[type] || removeListener;
+ handler(canvas, type, proxy);
proxies[type] = undefined;
}
getDevicePixelRatio() {
return window.devicePixelRatio;
}
+
+
+ /**
+ * @param {HTMLCanvasElement} canvas
+ */
+ isAttached(canvas) {
+ const container = _getParentNode(canvas);
+ return !!(container && _getParentNode(container));
+ }
}