Skip to content

Commit

Permalink
Add support for detached canvas element (chartjs#4591)
Browse files Browse the repository at this point in the history
Allow to create a chart on a canvas not yet attached to the DOM (detection based on CSS animations described in https://davidwalsh.name/detect-node-insertion). The resize element (IFRAME) is added only when the canvas receives a parent or when `style.display` changes from `none`. This change also allows to re-parent the canvas under a different node (the resizer element following). This is a preliminary work for the DIV based resizer.
  • Loading branch information
simonbrunel authored and yofreke committed Dec 30, 2017
1 parent f1eed0d commit 797f304
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 52 deletions.
2 changes: 2 additions & 0 deletions src/chart.js
Expand Up @@ -60,6 +60,8 @@ plugins.push(

Chart.plugins.register(plugins);

Chart.platform.initialize();

module.exports = Chart;
if (typeof window !== 'undefined') {
window.Chart = Chart;
Expand Down
8 changes: 8 additions & 0 deletions src/core/core.helpers.js
Expand Up @@ -500,6 +500,10 @@ module.exports = function(Chart) {
};
helpers.getMaximumWidth = function(domNode) {
var container = domNode.parentNode;
if (!container) {
return domNode.clientWidth;
}

var paddingLeft = parseInt(helpers.getStyle(container, 'padding-left'), 10);
var paddingRight = parseInt(helpers.getStyle(container, 'padding-right'), 10);
var w = container.clientWidth - paddingLeft - paddingRight;
Expand All @@ -508,6 +512,10 @@ module.exports = function(Chart) {
};
helpers.getMaximumHeight = function(domNode) {
var container = domNode.parentNode;
if (!container) {
return domNode.clientHeight;
}

var paddingTop = parseInt(helpers.getStyle(container, 'padding-top'), 10);
var paddingBottom = parseInt(helpers.getStyle(container, 'padding-bottom'), 10);
var h = container.clientHeight - paddingTop - paddingBottom;
Expand Down
158 changes: 116 additions & 42 deletions src/platforms/platform.dom.js
Expand Up @@ -6,19 +6,21 @@

var helpers = require('../helpers/index');

var EXPANDO_KEY = '$chartjs';
var CSS_PREFIX = 'chartjs-';
var CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor';
var CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation';
var ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart'];

/**
* DOM event types -> Chart.js event types.
* Note: only events with different types are mapped.
* @see https://developer.mozilla.org/en-US/docs/Web/Events
*/

var eventTypeMap = {
// Touch events
var EVENT_TYPES = {
touchstart: 'mousedown',
touchmove: 'mousemove',
touchend: 'mouseup',

// Pointer events
pointerenter: 'mouseenter',
pointerdown: 'mousedown',
pointermove: 'mousemove',
Expand Down Expand Up @@ -56,7 +58,7 @@ function initCanvas(canvas, config) {
var renderWidth = canvas.getAttribute('width');

// Chart.js modifies some canvas values that we want to restore on destroy
canvas._chartjs = {
canvas[EXPANDO_KEY] = {
initial: {
height: renderHeight,
width: renderWidth,
Expand Down Expand Up @@ -140,11 +142,29 @@ function createEvent(type, chart, x, y, nativeEvent) {
}

function fromNativeEvent(event, chart) {
var type = eventTypeMap[event.type] || event.type;
var type = EVENT_TYPES[event.type] || event.type;
var pos = helpers.getRelativePosition(event, chart);
return createEvent(type, chart, pos.x, pos.y, event);
}

function throttled(fn, thisArg) {
var ticking = false;
var args = [];

return function() {
args = Array.prototype.slice.call(arguments);
thisArg = thisArg || this;

if (!ticking) {
ticking = true;
helpers.requestAnimFrame.call(window, function() {
ticking = false;
fn.apply(thisArg, args);
});
}
};
}

function createResizer(handler) {
var iframe = document.createElement('iframe');
iframe.className = 'chartjs-hidden-iframe';
Expand Down Expand Up @@ -176,7 +196,6 @@ function createResizer(handler) {
// https://github.com/chartjs/Chart.js/issues/3521
addEventListener(iframe, 'load', function() {
addEventListener(iframe.contentWindow || iframe, 'resize', handler);

// The iframe size might have changed while loading, which can also
// happen if the size has been changed while detached from the DOM.
handler();
Expand All @@ -185,45 +204,100 @@ function createResizer(handler) {
return iframe;
}

function addResizeListener(node, listener, chart) {
var stub = node._chartjs = {
ticking: false
};

// Throttle the callback notification until the next animation frame.
var notify = function() {
if (!stub.ticking) {
stub.ticking = true;
helpers.requestAnimFrame.call(window, function() {
if (stub.resizer) {
stub.ticking = false;
return listener(createEvent('resize', chart));
}
});
// https://davidwalsh.name/detect-node-insertion
function watchForRender(node, handler) {
var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});
var proxy = expando.renderProxy = function(e) {
if (e.animationName === CSS_RENDER_ANIMATION) {
handler();
}
};

// Let's keep track of this added iframe and thus avoid DOM query when removing it.
stub.resizer = createResizer(notify);
helpers.each(ANIMATION_START_EVENTS, function(type) {
addEventListener(node, type, proxy);
});

node.insertBefore(stub.resizer, node.firstChild);
node.classList.add(CSS_RENDER_MONITOR);
}

function removeResizeListener(node) {
if (!node || !node._chartjs) {
return;
function unwatchForRender(node) {
var expando = node[EXPANDO_KEY] || {};
var proxy = expando.renderProxy;

if (proxy) {
helpers.each(ANIMATION_START_EVENTS, function(type) {
removeEventListener(node, type, proxy);
});

delete expando.renderProxy;
}

var resizer = node._chartjs.resizer;
if (resizer) {
node.classList.remove(CSS_RENDER_MONITOR);
}

function addResizeListener(node, listener, chart) {
var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {});

// Let's keep track of this added resizer and thus avoid DOM query when removing it.
var resizer = expando.resizer = createResizer(throttled(function() {
if (expando.resizer) {
return 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, function() {
if (expando.resizer) {
var container = node.parentNode;
if (container && container !== resizer.parentNode) {
container.insertBefore(resizer, container.firstChild);
}
}
});
}

function removeResizeListener(node) {
var expando = node[EXPANDO_KEY] || {};
var resizer = expando.resizer;

delete expando.resizer;
unwatchForRender(node);

if (resizer && resizer.parentNode) {
resizer.parentNode.removeChild(resizer);
node._chartjs.resizer = null;
}
}

function injectCSS(platform, css) {
// http://stackoverflow.com/q/3922139
var style = platform._style || document.createElement('style');
if (!platform._style) {
platform._style = style;
css = '/* Chart.js */\n' + css;
style.setAttribute('type', 'text/css');
document.getElementsByTagName('head')[0].appendChild(style);
}

delete node._chartjs;
style.appendChild(document.createTextNode(css));
}

module.exports = {
initialize: function() {
var keyframes = 'from{opacity:0.99}to{opacity:1}';

injectCSS(this,
// DOM rendering detection
// https://davidwalsh.name/detect-node-insertion
'@-webkit-keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
'@keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
'.' + CSS_RENDER_MONITOR + '{' +
'-webkit-animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
'animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
'}'
);
},

acquireContext: function(item, config) {
if (typeof item === 'string') {
item = document.getElementById(item);
Expand Down Expand Up @@ -259,11 +333,11 @@ module.exports = {

releaseContext: function(context) {
var canvas = context.canvas;
if (!canvas._chartjs) {
if (!canvas[EXPANDO_KEY]) {
return;
}

var initial = canvas._chartjs.initial;
var initial = canvas[EXPANDO_KEY].initial;
['height', 'width'].forEach(function(prop) {
var value = initial[prop];
if (helpers.isNullOrUndef(value)) {
Expand All @@ -283,19 +357,19 @@ module.exports = {
// https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html
canvas.width = canvas.width;

delete canvas._chartjs;
delete canvas[EXPANDO_KEY];
},

addEventListener: function(chart, type, listener) {
var canvas = chart.canvas;
if (type === 'resize') {
// Note: the resize event is not supported on all browsers.
addResizeListener(canvas.parentNode, listener, chart);
addResizeListener(canvas, listener, chart);
return;
}

var stub = listener._chartjs || (listener._chartjs = {});
var proxies = stub.proxies || (stub.proxies = {});
var expando = listener[EXPANDO_KEY] || (listener[EXPANDO_KEY] = {});
var proxies = expando.proxies || (expando.proxies = {});
var proxy = proxies[chart.id + '_' + type] = function(event) {
listener(fromNativeEvent(event, chart));
};
Expand All @@ -307,12 +381,12 @@ module.exports = {
var canvas = chart.canvas;
if (type === 'resize') {
// Note: the resize event is not supported on all browsers.
removeResizeListener(canvas.parentNode, listener);
removeResizeListener(canvas, listener);
return;
}

var stub = listener._chartjs || {};
var proxies = stub.proxies || {};
var expando = listener[EXPANDO_KEY] || {};
var proxies = expando.proxies || {};
var proxy = proxies[chart.id + '_' + type];
if (!proxy) {
return;
Expand Down
5 changes: 5 additions & 0 deletions src/platforms/platform.js
Expand Up @@ -12,6 +12,11 @@ var implementation = require('./platform.dom');
* @since 2.4.0
*/
module.exports = helpers.extend({
/**
* @since 2.7.0
*/
initialize: function() {},

/**
* Called at chart construction time, returns a context2d instance implementing
* the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}.
Expand Down
4 changes: 2 additions & 2 deletions test/jasmine.matchers.js
Expand Up @@ -123,8 +123,8 @@ function toBeChartOfSize() {
var canvas = actual.ctx.canvas;
var style = getComputedStyle(canvas);
var pixelRatio = actual.options.devicePixelRatio || window.devicePixelRatio;
var dh = parseInt(style.height, 10);
var dw = parseInt(style.width, 10);
var dh = parseInt(style.height, 10) || 0;
var dw = parseInt(style.width, 10) || 0;
var rh = canvas.height;
var rw = canvas.width;
var orh = rh / pixelRatio;
Expand Down

0 comments on commit 797f304

Please sign in to comment.