]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Delay animations until attached (#7370)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Wed, 20 May 2020 21:45:44 +0000 (00:45 +0300)
committerGitHub <noreply@github.com>
Wed, 20 May 2020 21:45:44 +0000 (17:45 -0400)
* Delay animations until attached
* Detect container detachment

docs/docs/getting-started/v3-migration.md
src/core/core.controller.js
src/helpers/helpers.dom.js
src/platform/platform.base.js
src/platform/platform.dom.js
test/specs/platform.dom.tests.js

index ebba06adde0a33122595ae74524b1bea13882621..14f2e2d21eb1f07055f56d9b64ed9ded183c7548 100644 (file)
@@ -141,7 +141,6 @@ options: {
 
 Animation system was completely rewritten in Chart.js v3. Each property can now be animated separately. Please see [animations](../configuration/animations.md) docs for details.
 
-
 #### Customizability
 
 * `custom` attribute of elements was removed. Please use scriptable options
@@ -169,6 +168,7 @@ Animation system was completely rewritten in Chart.js v3. Each property can now
 While the end-user migration for Chart.js 3 is fairly straight-forward, the developer migration can be more complicated. Please reach out for help in the #dev [Slack](https://chartjs-slack.herokuapp.com/) channel if tips on migrating would be helpful.
 
 Some of the biggest things that have changed:
+
 * There is a completely rewritten and more performant animation system.
   * `Element._model` and `Element._view` are no longer used and properties are now set directly on the elements. You will have to use the method `getProps` to access these properties inside most methods such as `inXRange`/`inYRange` and `getCenterPoint`. Please take a look at [the Chart.js-provided elements](https://github.com/chartjs/Chart.js/tree/master/src/elements) for examples.
   * When building the elements in a controller, it's now suggested to call `updateElement` to provide the element properties. There are also methods such as `getSharedOptions` and `includeOptions` that have been added to skip redundant computation. Please take a look at [the Chart.js-provided controllers](https://github.com/chartjs/Chart.js/tree/master/src/controllers) for examples.
@@ -187,6 +187,7 @@ A few changes were made to controllers that are more straight-forward, but will
 The following properties and methods were removed:
 
 #### Chart
+
 * `Chart.borderWidth`
 * `Chart.chart.chart`
 * `Chart.Bar`. New charts are created via `new Chart` and providing the appropriate `type` parameter
@@ -411,3 +412,4 @@ The APIs listed in this section have changed in signature or behaviour from vers
 * `Chart.platform` is no longer the platform object used by charts. Every chart instance now has a separate platform instance.
 * `Chart.platforms` is an object that contains two usable platform classes, `BasicPlatform` and `DomPlatform`. It also contains `BasePlatform`, a class that all platforms must extend from.
 * If the canvas passed in is an instance of `OffscreenCanvas`, the `BasicPlatform` is automatically used.
+* `isAttached` method was added to platform.
index c71c8c26f0933955482340196e2a414cce82dbaf..cdc82748a49a1efba202b2e29b9de1b4523d160b 100644 (file)
@@ -212,7 +212,7 @@ export default class Chart {
                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;
@@ -221,6 +221,7 @@ export default class Chart {
                this.$plugins = undefined;
                this.$proxies = {};
                this._hiddenIndices = {};
+               this.attached = true;
 
                // Add the chart instance to the global namespace
                Chart.instances[me.id] = me;
@@ -248,7 +249,9 @@ export default class Chart {
                Animator.listen(me, 'progress', onAnimationProgress);
 
                me._initialize();
-               me.update();
+               if (me.attached) {
+                       me.update();
+               }
        }
 
        /**
@@ -341,7 +344,8 @@ export default class Chart {
                                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');
                }
        }
 
@@ -664,7 +668,7 @@ export default class Chart {
                };
 
                if (Animator.has(me)) {
-                       if (!Animator.running(me)) {
+                       if (me.attached && !Animator.running(me)) {
                                Animator.start(me);
                        }
                } else {
@@ -938,24 +942,57 @@ export default class Chart {
        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;
                }
        }
 
index 74d8f957641a8158e39063335534cde356c52042..a0c29a7c4936cc30ec8ba90f30d2ebc9107b4b5a 100644 (file)
@@ -126,13 +126,14 @@ export function getRelativePosition(evt, chart) {
        };
 }
 
+function fallbackIfNotValid(measure, fallback) {
+       return typeof measure === 'number' ? measure : fallback;
+}
+
 export function getMaximumWidth(domNode) {
        const container = _getParentNode(domNode);
        if (!container) {
-               if (typeof domNode.clientWidth === 'number') {
-                       return domNode.clientWidth;
-               }
-               return domNode.width;
+               return fallbackIfNotValid(domNode.clientWidth, domNode.width);
        }
 
        const clientWidth = container.clientWidth;
@@ -147,10 +148,7 @@ export function getMaximumWidth(domNode) {
 export function getMaximumHeight(domNode) {
        const container = _getParentNode(domNode);
        if (!container) {
-               if (typeof domNode.clientHeight === 'number') {
-                       return domNode.clientHeight;
-               }
-               return domNode.height;
+               return fallbackIfNotValid(domNode.clientHeight, domNode.height);
        }
 
        const clientHeight = container.clientHeight;
index 801eecb4e20b17566b96210f498429237ba8fe80..434ede1c7803e5a613a6e11aa6b06c378895f3f8 100644 (file)
@@ -48,6 +48,14 @@ export default class BasePlatform {
        getDevicePixelRatio() {
                return 1;
        }
+
+       /**
+        * @param {HTMLCanvasElement} canvas
+        * @returns {boolean} true if the canvas is attached to the platform, false if not.
+        */
+       isAttached(canvas) { // eslint-disable-line no-unused-vars
+               return true;
+       }
 }
 
 /**
index ee5aafa80830ca2b975aa7d030827b01957bb2e4..e2c862370c0eda94efc56282167b85c1bf347433 100644 (file)
@@ -172,51 +172,17 @@ function throttled(fn, thisArg) {
        };
 }
 
-/**
- * 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);
                                }
                        }
                });
@@ -225,79 +191,76 @@ function watchForAttachment(element, fn) {
        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;
 }
 
 /**
@@ -379,22 +342,14 @@ export default class DomPlatform extends BasePlatform {
                // 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);
        }
 
 
@@ -405,21 +360,32 @@ export default class DomPlatform extends BasePlatform {
        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));
+       }
 }
index 78846cfb9768a1f11d65f08e218b9f5041ca33d4..263b031682b9ac807761842027399f103b011993 100644 (file)
@@ -1,3 +1,5 @@
+import {DomPlatform} from '../../src/platform/platforms';
+
 describe('Platform.dom', function() {
 
        describe('context acquisition', function() {
@@ -405,4 +407,24 @@ describe('Platform.dom', function() {
                        });
                });
        });
+
+       describe('isAttached', function() {
+               it('should detect detached when canvas is attached to DOM', function() {
+                       var platform = new DomPlatform();
+                       var canvas = document.createElement('canvas');
+                       var div = document.createElement('div');
+
+                       expect(platform.isAttached(canvas)).toEqual(false);
+                       div.appendChild(canvas);
+                       expect(platform.isAttached(canvas)).toEqual(false);
+                       document.body.appendChild(div);
+
+                       expect(platform.isAttached(canvas)).toEqual(true);
+
+                       div.removeChild(canvas);
+                       expect(platform.isAttached(canvas)).toEqual(false);
+                       document.body.removeChild(div);
+                       expect(platform.isAttached(canvas)).toEqual(false);
+               });
+       });
 });