mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Delay animations until attached (#7370)
* Delay animations until attached * Detect container detachment
This commit is contained in:
parent
51be344717
commit
cfb5fba527
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
addListener(canvas, type, proxy);
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user