Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/core/core.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,12 @@ class Chart {
this.attached = false;
this._animationsDisabled = undefined;
this.$context = undefined;
this._doResize = debounce(mode => this.update(mode), options.resizeDelay || 0);
this._doResize = debounce(mode => {
// A delayed resize can fire after destroy() has already nulled the canvas.
if (this.ctx !== null) {
this.update(mode);
}
}, options.resizeDelay || 0);
this._dataChanges = [];

// Add the chart instance to the global namespace
Expand Down Expand Up @@ -275,6 +280,10 @@ class Chart {
}

_resize(width, height) {
if (!this.canvas) {
return;
}

const options = this.options;
const canvas = this.canvas;
const aspectRatio = options.maintainAspectRatio && this.aspectRatio;
Expand Down Expand Up @@ -472,7 +481,12 @@ class Chart {
this.notifyPlugins('reset');
}

// eslint-disable-next-line max-statements
update(mode) {
if (this.ctx === null) {
return;
}

const config = this.config;

config.update();
Expand Down Expand Up @@ -941,6 +955,10 @@ class Chart {
this._stop();
this.config.clearCache();

if (this._doResize.cancel) {
this._doResize.cancel();
}

if (canvas) {
this.unbindEvents();
clearCanvas(canvas, ctx);
Expand Down Expand Up @@ -995,6 +1013,9 @@ class Chart {
* @private
*/
bindResponsiveEvents() {
if (!this.canvas) {
return;
}
if (!this._responsiveListeners) {
this._responsiveListeners = {};
}
Expand Down
40 changes: 30 additions & 10 deletions src/helpers/helpers.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ function parseMaxStyle(styleValue: string | number, node: HTMLElement, parentPro
return valueInPixels;
}

const getComputedStyle = (element: HTMLElement): CSSStyleDeclaration =>
element.ownerDocument.defaultView.getComputedStyle(element, null);
const getComputedStyle = (element?: HTMLElement | null): CSSStyleDeclaration | null => {
if (!element || !element.ownerDocument || !element.ownerDocument.defaultView) {
return null;
}
return element.ownerDocument.defaultView.getComputedStyle(element, null);
};

export function getStyle(el: HTMLElement, property: string): string {
return getComputedStyle(el).getPropertyValue(property);
export function getStyle(el: HTMLElement | null | undefined, property: string): string {
const style = getComputedStyle(el);
return style ? style.getPropertyValue(property) : '';
}

const positions = ['top', 'right', 'bottom', 'left'];
Expand Down Expand Up @@ -111,7 +116,14 @@ export function getRelativePosition(
}

const {canvas, currentDevicePixelRatio} = chart;
if (!canvas) {
return {x: 0, y: 0};
}

const style = getComputedStyle(canvas);
if (!style) {
return {x: 0, y: 0};
}
const borderBox = style.boxSizing === 'border-box';
const paddings = getPositionedStyle(style, 'padding');
const borders = getPositionedStyle(style, 'border', 'width');
Expand All @@ -130,17 +142,18 @@ export function getRelativePosition(
};
}

function getContainerSize(canvas: HTMLCanvasElement, width: number, height: number): Partial<Scale> {
// eslint-disable-next-line complexity
function getContainerSize(canvas?: HTMLCanvasElement, width?: number, height?: number): Partial<Scale> {
let maxWidth: number, maxHeight: number;

if (width === undefined || height === undefined) {
const container = canvas && _getParentNode(canvas);
if (!container) {
width = canvas.clientWidth;
height = canvas.clientHeight;
const containerStyle = container && getComputedStyle(container);
if (!container || !containerStyle) {
width = canvas ? canvas.clientWidth : 0;
height = canvas ? canvas.clientHeight : 0;
} else {
const rect = container.getBoundingClientRect(); // this is the border box of the container
const containerStyle = getComputedStyle(container);
const containerBorder = getPositionedStyle(containerStyle, 'border', 'width');
const containerPadding = getPositionedStyle(containerStyle, 'padding');
width = rect.width - containerPadding.width - containerBorder.width;
Expand All @@ -161,12 +174,19 @@ const round1 = (v: number) => Math.round(v * 10) / 10;

// eslint-disable-next-line complexity
export function getMaximumSize(
canvas: HTMLCanvasElement,
canvas?: HTMLCanvasElement,
bbWidth?: number,
bbHeight?: number,
aspectRatio?: number
): { width: number; height: number } {
if (!canvas) {
return {width: 0, height: 0};
}

const style = getComputedStyle(canvas);
if (!style) {
return {width: 0, height: 0};
}
const margins = getPositionedStyle(style, 'margin');
const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY;
const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY;
Expand Down
6 changes: 5 additions & 1 deletion src/helpers/helpers.extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function throttled<TArgs extends Array<any>>(
*/
export function debounce<TArgs extends Array<any>>(fn: (...args: TArgs) => void, delay: number) {
let timeout;
return function(...args: TArgs) {
const debounced = function(...args: TArgs) {
if (delay) {
clearTimeout(timeout);
timeout = setTimeout(fn, delay, args);
Expand All @@ -58,6 +58,10 @@ export function debounce<TArgs extends Array<any>>(fn: (...args: TArgs) => void,
}
return delay;
};
debounced.cancel = () => {
clearTimeout(timeout);
};
return debounced;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/platform/platform.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export default class BasePlatform {
* @param {number} [aspectRatio] - aspect ratio to maintain
*/
getMaximumSize(element, width, height, aspectRatio) {
width = Math.max(0, width || element.width);
height = height || element.height;
width = Math.max(0, width || (element ? element.width : 0));
height = height || (element ? element.height : 0);
return {
width,
height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height)
Expand Down
45 changes: 45 additions & 0 deletions test/specs/core.controller.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,51 @@ describe('Chart', function() {
});
});

describe('destroyed canvas', function() {
it('should not throw when update runs after destroy', function() {
const chart = acquireChart({
type: 'line',
data: {
labels: ['A'],
datasets: [{data: [1]}]
},
options: {
responsive: true,
animation: false
}
});

chart.destroy();

expect(function() {
chart.update();
}).not.toThrow();
});

it('should not throw when a delayed resize fires after destroy', function(done) {
const chart = acquireChart({
type: 'line',
data: {
labels: ['A'],
datasets: [{data: [1]}]
},
options: {
responsive: true,
resizeDelay: 20,
animation: false
}
});

chart.resize();
chart.destroy();

setTimeout(function() {
expect(chart.canvas).toBeNull();
done();
}, 50);
});
});

describe('data visibility', function() {
it('should hide a dataset', function() {
var chart = acquireChart({
Expand Down
45 changes: 45 additions & 0 deletions test/specs/helpers.dom.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -545,4 +545,49 @@ describe('DOM helpers tests', function() {

document.body.removeChild(container);
});

it('should not throw when getMaximumSize is called with a null canvas', function() {
var size;
expect(function() {
size = helpers.getMaximumSize(null);
}).not.toThrow();
expect(size).toEqual({width: 0, height: 0});

expect(function() {
size = helpers.getMaximumSize(undefined);
}).not.toThrow();
expect(size).toEqual({width: 0, height: 0});
});

it('should not throw when getMaximumSize is called with a detached canvas', function() {
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
document.body.removeChild(canvas);

expect(function() {
helpers.getMaximumSize(canvas);
}).not.toThrow();
});

it('should not throw when getStyle is called with a null element', function() {
var value;
expect(function() {
value = helpers.getStyle(null, 'width');
}).not.toThrow();
expect(value).toEqual('');
});

it('should not throw when getRelativePosition is called with a null canvas', function() {
const chart = {
canvas: null,
currentDevicePixelRatio: 1,
width: 100,
height: 100
};
var position;
expect(function() {
position = helpers.getRelativePosition({offsetX: 10, offsetY: 20}, chart);
}).not.toThrow();
expect(position).toEqual({x: 0, y: 0});
});
});
9 changes: 9 additions & 0 deletions test/specs/platform.basic.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ describe('Platform.basic', function() {
chart.destroy();
});

it('should not throw when getMaximumSize is called without an element', function() {
const platform = new Chart.platforms.BasePlatform();
var size;
expect(function() {
size = platform.getMaximumSize(null);
}).not.toThrow();
expect(size).toEqual({width: 0, height: 0});
});


it('supports choosing the BasicPlatform in a web worker', function(done) {
const canvas = document.createElement('canvas');
Expand Down