From ecc35c527b8d91c2bdbc2cdc97ef667adfe7e3a3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Wed, 21 Dec 2016 10:22:05 -0500 Subject: [PATCH] Refactoring to put browser specific code in a new class (#3718) Refactoring to put browser specific code in a new class, BrowserPlatform. BrowserPlatform implements IPlatform. Chart.Platform is the constructor for the platform object that is attached to the chart instance. Plugins are notified about the event using the `onEvent` call. The legend plugin was converted to use onEvent instead of the older private `handleEvent` method. Wrote test to check that plugins are notified about events --- docs/09-Advanced.md | 8 + src/chart.js | 3 + src/core/core.controller.js | 156 ++------------ src/core/core.interaction.js | 31 ++- src/core/core.legend.js | 20 +- src/core/core.tooltip.js | 7 +- src/platforms/platform.dom.js | 237 +++++++++++++++++++++ test/core.controller.tests.js | 297 --------------------------- test/platform.dom.tests.js | 373 ++++++++++++++++++++++++++++++++++ 9 files changed, 677 insertions(+), 455 deletions(-) create mode 100644 src/platforms/platform.dom.js create mode 100644 test/platform.dom.tests.js diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index f76ac4b0d97..4c677c904d4 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -410,6 +410,7 @@ Plugins will be called at the following times * After datasets draw * Resize * Before an animation is started +* When an event occurs on the canvas (mousemove, click, etc). This requires the `options.events` property handled Plugins should derive from Chart.PluginBase and implement the following interface ```javascript @@ -437,6 +438,13 @@ Plugins should derive from Chart.PluginBase and implement the following interfac afterDatasetsDraw: function(chartInstance, easing) { }, destroy: function(chartInstance) { } + + /** + * Called when an event occurs on the chart + * @param e {Core.Event} the Chart.js wrapper around the native event. e.native is the original event + * @return {Boolean} true if the chart is changed and needs to re-render + */ + onEvent: function(chartInstance, e) {} } ``` diff --git a/src/chart.js b/src/chart.js index 7c490e7eabc..186d07a42f5 100644 --- a/src/chart.js +++ b/src/chart.js @@ -19,6 +19,9 @@ require('./core/core.legend')(Chart); require('./core/core.interaction')(Chart); require('./core/core.tooltip')(Chart); +// By default, we only load the browser platform. +Chart.platform = require('./platforms/platform.dom')(Chart); + require('./elements/element.arc')(Chart); require('./elements/element.line')(Chart); require('./elements/element.point')(Chart); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 4e28773880e..e64cb3a9dde 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -14,140 +14,6 @@ module.exports = function(Chart) { // Controllers available for dataset visualization eg. bar, line, slice, etc. Chart.controllers = {}; - /** - * The "used" size is the final value of a dimension property after all calculations have - * been performed. This method uses the computed style of `element` but returns undefined - * if the computed style is not expressed in pixels. That can happen in some cases where - * `element` has a size relative to its parent and this last one is not yet displayed, - * for example because of `display: none` on a parent node. - * TODO(SB) Move this method in the upcoming core.platform class. - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value - * @returns {Number} Size in pixels or undefined if unknown. - */ - function readUsedSize(element, property) { - var value = helpers.getStyle(element, property); - var matches = value && value.match(/(\d+)px/); - return matches? Number(matches[1]) : undefined; - } - - /** - * Initializes the canvas style and render size without modifying the canvas display size, - * since responsiveness is handled by the controller.resize() method. The config is used - * to determine the aspect ratio to apply in case no explicit height has been specified. - * TODO(SB) Move this method in the upcoming core.platform class. - */ - function initCanvas(canvas, config) { - var style = canvas.style; - - // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it - // returns null or '' if no explicit value has been set to the canvas attribute. - var renderHeight = canvas.getAttribute('height'); - var renderWidth = canvas.getAttribute('width'); - - // Chart.js modifies some canvas values that we want to restore on destroy - canvas._chartjs = { - initial: { - height: renderHeight, - width: renderWidth, - style: { - display: style.display, - height: style.height, - width: style.width - } - } - }; - - // Force canvas to display as block to avoid extra space caused by inline - // elements, which would interfere with the responsive resize process. - // https://github.com/chartjs/Chart.js/issues/2538 - style.display = style.display || 'block'; - - if (renderWidth === null || renderWidth === '') { - var displayWidth = readUsedSize(canvas, 'width'); - if (displayWidth !== undefined) { - canvas.width = displayWidth; - } - } - - if (renderHeight === null || renderHeight === '') { - if (canvas.style.height === '') { - // If no explicit render height and style height, let's apply the aspect ratio, - // which one can be specified by the user but also by charts as default option - // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. - canvas.height = canvas.width / (config.options.aspectRatio || 2); - } else { - var displayHeight = readUsedSize(canvas, 'height'); - if (displayWidth !== undefined) { - canvas.height = displayHeight; - } - } - } - - return canvas; - } - - /** - * Restores the canvas initial state, such as render/display sizes and style. - * TODO(SB) Move this method in the upcoming core.platform class. - */ - function releaseCanvas(canvas) { - if (!canvas._chartjs) { - return; - } - - var initial = canvas._chartjs.initial; - ['height', 'width'].forEach(function(prop) { - var value = initial[prop]; - if (value === undefined || value === null) { - canvas.removeAttribute(prop); - } else { - canvas.setAttribute(prop, value); - } - }); - - helpers.each(initial.style || {}, function(value, key) { - canvas.style[key] = value; - }); - - // The canvas render size might have been changed (and thus the state stack discarded), - // we can't use save() and restore() to restore the initial state. So make sure that at - // least the canvas context is reset to the default state by setting the canvas width. - // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html - canvas.width = canvas.width; - - delete canvas._chartjs; - } - - /** - * TODO(SB) Move this method in the upcoming core.platform class. - */ - function acquireContext(item, config) { - if (typeof item === 'string') { - item = document.getElementById(item); - } else if (item.length) { - // Support for array based queries (such as jQuery) - item = item[0]; - } - - if (item && item.canvas) { - // Support for any object associated to a canvas (including a context2d) - item = item.canvas; - } - - if (item instanceof HTMLCanvasElement) { - // To prevent canvas fingerprinting, some add-ons undefine the getContext - // method, for example: https://github.com/kkapsner/CanvasBlocker - // https://github.com/chartjs/Chart.js/issues/2807 - var context = item.getContext && item.getContext('2d'); - if (context instanceof CanvasRenderingContext2D) { - initCanvas(item, config); - return context; - } - } - - return null; - } - /** * Initializes the given config with global and chart default values. */ @@ -197,7 +63,7 @@ module.exports = function(Chart) { config = initConfig(config); - var context = acquireContext(item, config); + var context = Chart.platform.acquireContext(item, config); var canvas = context && context.canvas; var height = canvas && canvas.height; var width = canvas && canvas.width; @@ -696,7 +562,7 @@ module.exports = function(Chart) { helpers.unbindEvents(me, me.events); helpers.removeResizeListener(canvas.parentNode); helpers.clear(me.chart); - releaseCanvas(canvas); + Chart.platform.releaseContext(me.chart.ctx); me.chart.canvas = null; me.chart.ctx = null; } @@ -742,7 +608,6 @@ module.exports = function(Chart) { eventHandler: function(e) { var me = this; - var legend = me.legend; var tooltip = me.tooltip; var hoverOptions = me.options.hover; @@ -750,9 +615,12 @@ module.exports = function(Chart) { me._bufferedRender = true; me._bufferedRequest = null; - var changed = me.handleEvent(e); - changed |= legend && legend.handleEvent(e); - changed |= tooltip && tooltip.handleEvent(e); + // Create platform agnostic chart event using platform specific code + var chartEvent = Chart.platform.createEvent(e, me.chart); + + var changed = me.handleEvent(chartEvent); + changed |= tooltip && tooltip.handleEvent(chartEvent); + changed |= Chart.plugins.notify(me, 'onEvent', [chartEvent]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { @@ -776,7 +644,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * param e {Event} the event to handle + * param e {Core.Event} the event to handle * @return {Boolean} true if the chart needs to re-render */ handleEvent: function(e) { @@ -796,12 +664,14 @@ module.exports = function(Chart) { // On Hover hook if (hoverOptions.onHover) { - hoverOptions.onHover.call(me, e, me.active); + // Need to call with native event here to not break backwards compatibility + hoverOptions.onHover.call(me, e.native, me.active); } if (e.type === 'mouseup' || e.type === 'click') { if (options.onClick) { - options.onClick.call(me, e, me.active); + // Use e.native here for backwards compatibility + options.onClick.call(me, e.native, me.active); } } diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index aacdda19c38..0888fa9591b 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -3,6 +3,23 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + /** + * Helper function to get relative position for an event + * @param e {Event|Core.Event} the event to get the position for + * @param chart {chart} the chart + * @returns {Point} the event position + */ + function getRelativePosition(e, chart) { + if (e.native) { + return { + x: e.x, + y: e.y + }; + } + + return helpers.getRelativePosition(e, chart); + } + /** * Helper function to traverse all of the visible elements in the chart * @param chart {chart} the chart @@ -82,7 +99,7 @@ module.exports = function(Chart) { } function indexMode(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var distanceMetric = function(pt1, pt2) { return Math.abs(pt1.x - pt2.x); }; @@ -125,7 +142,7 @@ module.exports = function(Chart) { // Helper function for different modes modes: { single: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var elements = []; parseVisibleItems(chart, function(element) { @@ -166,7 +183,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ dataset: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false); if (items.length > 0) { @@ -193,7 +210,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ point: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); return getIntersectItems(chart, position); }, @@ -206,7 +223,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ nearest: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var nearestItems = getNearestItems(chart, position, options.intersect); // We have multiple items at the same distance from the event. Now sort by smallest @@ -238,7 +255,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ x: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; @@ -269,7 +286,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ y: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 4ab51ba5dbf..45f51d05923 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -439,7 +439,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param e {Core.Event} the event to handle * @return {Boolean} true if a change occured */ handleEvent: function(e) { @@ -460,9 +460,9 @@ module.exports = function(Chart) { return; } - var position = helpers.getRelativePosition(e, me.chart.chart), - x = position.x, - y = position.y; + // Chart event already has relative position in it + var x = e.x, + y = e.y; if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { // See if we are touching one of the dataset boxes @@ -473,11 +473,13 @@ module.exports = function(Chart) { if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { // Touching an element if (type === 'click') { - opts.onClick.call(me, e, me.legendItems[i]); + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, me.legendItems[i]); changed = true; break; } else if (type === 'mousemove') { - opts.onHover.call(me, e, me.legendItems[i]); + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, me.legendItems[i]); changed = true; break; } @@ -523,6 +525,12 @@ module.exports = function(Chart) { Chart.layoutService.removeBox(chartInstance, chartInstance.legend); delete chartInstance.legend; } + }, + onEvent: function(chartInstance, e) { + var legend = chartInstance.legend; + if (legend) { + legend.handleEvent(e); + } } }); }; diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index d99f2302ff5..c1ac7830739 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -763,7 +763,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param e {Core.Event} the event to handle * @returns {Boolean} true if the tooltip changed */ handleEvent: function(e) { @@ -785,7 +785,10 @@ module.exports = function(Chart) { me._lastActive = me._active; if (options.enabled || options.custom) { - me._eventPosition = helpers.getRelativePosition(e, me._chart); + me._eventPosition = { + x: e.x, + y: e.y + }; var model = me._model; me.update(true); diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js new file mode 100644 index 00000000000..fb41f88259f --- /dev/null +++ b/src/platforms/platform.dom.js @@ -0,0 +1,237 @@ +'use strict'; + +/** + * @interface IPlatform + * Allows abstracting platform dependencies away from the chart + */ +/** + * Creates a chart.js event from a platform specific event + * @method IPlatform#createEvent + * @param e {Event} : the platform event to translate + * @returns {Core.Event} chart.js event + */ +/** + * @method IPlatform#acquireContext + * @param item {Object} the context or canvas to use + * @param config {ChartOptions} the chart options + * @returns {CanvasRenderingContext2D} a context2d instance implementing the w3c Canvas 2D context API standard. + */ +/** + * @method IPlatform#releaseContext + * @param context {CanvasRenderingContext2D} the context to release. This is the item returned by @see {@link IPlatform#acquireContext} + */ + +// Chart.Platform implementation for targeting a web browser +module.exports = function(Chart) { + var helpers = Chart.helpers; + + /* + * Key is the browser event type + * Chart.js internal events are: + * mouseenter + * mousedown + * mousemove + * mouseup + * mouseout + * click + * dblclick + * contextmenu + * keydown + * keypress + * keyup + */ + var typeMap = { + // Mouse events + mouseenter: 'mouseenter', + mousedown: 'mousedown', + mousemove: 'mousemove', + mouseup: 'mouseup', + mouseout: 'mouseout', + mouseleave: 'mouseout', + click: 'click', + dblclick: 'dblclick', + contextmenu: 'contextmenu', + + // Touch events + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + + // Pointer events + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout', + + // Key events + keydown: 'keydown', + keypress: 'keypress', + keyup: 'keyup', + }; + + /** + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns {Number} Size in pixels or undefined if unknown. + */ + function readUsedSize(element, property) { + var value = helpers.getStyle(element, property); + var matches = value && value.match(/(\d+)px/); + return matches? Number(matches[1]) : undefined; + } + + /** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + */ + function initCanvas(canvas, config) { + var style = canvas.style; + + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + var renderHeight = canvas.getAttribute('height'); + var renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas._chartjs = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + + if (renderWidth === null || renderWidth === '') { + var displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (renderHeight === null || renderHeight === '') { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (config.options.aspectRatio || 2); + } else { + var displayHeight = readUsedSize(canvas, 'height'); + if (displayWidth !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; + } + + return { + /** + * Creates a Chart.js event from a raw event + * @method BrowserPlatform#createEvent + * @implements IPlatform.createEvent + * @param e {Event} the raw event (such as a mouse event) + * @param chart {Chart} the chart to use + * @returns {Core.Event} the chart.js event for this event + */ + createEvent: function(e, chart) { + var relativePosition = helpers.getRelativePosition(e, chart); + return { + // allow access to the native event + native: e, + + // our interal event type + type: typeMap[e.type], + + // width and height of chart + width: chart.width, + height: chart.height, + + // Position relative to the canvas + x: relativePosition.x, + y: relativePosition.y + }; + }, + + /** + * @method BrowserPlatform#acquireContext + * @implements IPlatform#acquireContext + */ + acquireContext: function(item, config) { + if (typeof item === 'string') { + item = document.getElementById(item); + } else if (item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; + } + + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + if (item instanceof HTMLCanvasElement) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + var context = item.getContext && item.getContext('2d'); + if (context instanceof CanvasRenderingContext2D) { + initCanvas(item, config); + return context; + } + } + + return null; + }, + + /** + * Restores the canvas initial state, such as render/display sizes and style. + * @method BrowserPlatform#releaseContext + * @implements IPlatform#releaseContext + */ + releaseContext: function(context) { + var canvas = context.canvas; + if (!canvas._chartjs) { + return; + } + + var initial = canvas._chartjs.initial; + ['height', 'width'].forEach(function(prop) { + var value = initial[prop]; + if (value === undefined || value === null) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + + helpers.each(initial.style || {}, function(value, key) { + canvas.style[key] = value; + }); + + // The canvas render size might have been changed (and thus the state stack discarded), + // we can't use save() and restore() to restore the initial state. So make sure that at + // least the canvas context is reset to the default state by setting the canvas width. + // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html + canvas.width = canvas.width; + + delete canvas._chartjs; + } + }; +}; diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index 44e7d3d21ed..0e895615812 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -13,74 +13,6 @@ describe('Chart.Controller', function() { Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler); } - describe('context acquisition', function() { - var canvasId = 'chartjs-canvas'; - - beforeEach(function() { - var canvas = document.createElement('canvas'); - canvas.setAttribute('id', canvasId); - window.document.body.appendChild(canvas); - }); - - afterEach(function() { - document.getElementById(canvasId).remove(); - }); - - // see https://github.com/chartjs/Chart.js/issues/2807 - it('should gracefully handle invalid item', function() { - var chart = new Chart('foobar'); - - expect(chart).not.toBeValidChart(); - - chart.destroy(); - }); - - it('should accept a DOM element id', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart(canvasId); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - - it('should accept a canvas element', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart(canvas); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - - it('should accept a canvas context2D', function() { - var canvas = document.getElementById(canvasId); - var context = canvas.getContext('2d'); - var chart = new Chart(context); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(context); - - chart.destroy(); - }); - - it('should accept an array containing canvas', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart([canvas]); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - }); - describe('config initialization', function() { it('should create missing config.data properties', function() { var chart = acquireChart({}); @@ -164,152 +96,7 @@ describe('Chart.Controller', function() { }); }); - describe('config.options.aspectRatio', function() { - it('should use default "global" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 620px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 620, dh: 310, - rw: 620, rh: 310, - }); - }); - - it('should use default "chart" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - type: 'doughnut', - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 425px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 425, dh: 425, - rw: 425, rh: 425, - }); - }); - - it('should use "user" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false, - aspectRatio: 3 - } - }, { - canvas: { - style: 'width: 405px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 405, dh: 135, - rw: 405, rh: 135, - }); - }); - - it('should not apply aspect ratio when height specified', function() { - var chart = acquireChart({ - options: { - responsive: false, - aspectRatio: 3 - } - }, { - canvas: { - style: 'width: 400px; height: 410px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 400, dh: 410, - rw: 400, rh: 410, - }); - }); - }); - describe('config.options.responsive: false', function() { - it('should use default canvas size for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: '' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 300, dh: 150, - rw: 300, rh: 150, - }); - }); - - it('should use canvas attributes for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: '', - width: 305, - height: 245, - } - }); - - expect(chart).toBeChartOfSize({ - dw: 305, dh: 245, - rw: 305, rh: 245, - }); - }); - - it('should use canvas style for render and display sizes (if no attributes)', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 345px; height: 125px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 345, dh: 125, - rw: 345, rh: 125, - }); - }); - - it('should use attributes for the render size and style for the display size', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 345px; height: 125px;', - width: 165, - height: 85, - } - }); - - expect(chart).toBeChartOfSize({ - dw: 345, dh: 125, - rw: 165, rh: 85, - }); - }); - it('should not inject the resizer element', function() { var chart = acquireChart({ options: { @@ -563,27 +350,6 @@ describe('Chart.Controller', function() { }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() { - it('should fill parent width and use aspect ratio to calculate height', function() { - var chart = acquireChart({ - options: { - responsive: true, - maintainAspectRatio: true - } - }, { - canvas: { - style: 'width: 150px; height: 245px' - }, - wrapper: { - style: 'width: 300px; height: 350px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 300, dh: 490, - rw: 300, rh: 490, - }); - }); - it('should resize the canvas with correct aspect ratio when parent width changes', function(done) { var chart = acquireChart({ type: 'line', // AR == 2 @@ -714,69 +480,6 @@ describe('Chart.Controller', function() { }); describe('controller.destroy', function() { - it('should reset context to default values', function() { - var chart = acquireChart({}); - var context = chart.chart.ctx; - - chart.destroy(); - - // https://www.w3.org/TR/2dcontext/#conformance-requirements - Chart.helpers.each({ - fillStyle: '#000000', - font: '10px sans-serif', - lineJoin: 'miter', - lineCap: 'butt', - lineWidth: 1, - miterLimit: 10, - shadowBlur: 0, - shadowColor: 'rgba(0, 0, 0, 0)', - shadowOffsetX: 0, - shadowOffsetY: 0, - strokeStyle: '#000000', - textAlign: 'start', - textBaseline: 'alphabetic' - }, function(value, key) { - expect(context[key]).toBe(value); - }); - }); - - it('should restore canvas initial values', function(done) { - var chart = acquireChart({ - options: { - responsive: true, - maintainAspectRatio: false - } - }, { - canvas: { - width: 180, - style: 'width: 512px; height: 480px' - }, - wrapper: { - style: 'width: 450px; height: 450px; position: relative' - } - }); - - var canvas = chart.chart.canvas; - var wrapper = canvas.parentNode; - wrapper.style.width = '475px'; - waitForResize(chart, function() { - expect(chart).toBeChartOfSize({ - dw: 475, dh: 450, - rw: 475, rh: 450, - }); - - chart.destroy(); - - expect(canvas.getAttribute('width')).toBe('180'); - expect(canvas.getAttribute('height')).toBe(null); - expect(canvas.style.width).toBe('512px'); - expect(canvas.style.height).toBe('480px'); - expect(canvas.style.display).toBe(''); - - done(); - }); - }); - it('should remove the resizer element when responsive: true', function() { var chart = acquireChart({ options: { diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js new file mode 100644 index 00000000000..f20b6e1ff68 --- /dev/null +++ b/test/platform.dom.tests.js @@ -0,0 +1,373 @@ +describe('Platform.dom', function() { + + function waitForResize(chart, callback) { + var resizer = chart.chart.canvas.parentNode._chartjs.resizer; + var content = resizer.contentWindow || resizer; + var state = content.document.readyState || 'complete'; + var handler = function() { + Chart.helpers.removeEvent(content, 'load', handler); + Chart.helpers.removeEvent(content, 'resize', handler); + setTimeout(callback, 50); + }; + + Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler); + } + + describe('context acquisition', function() { + var canvasId = 'chartjs-canvas'; + + beforeEach(function() { + var canvas = document.createElement('canvas'); + canvas.setAttribute('id', canvasId); + window.document.body.appendChild(canvas); + }); + + afterEach(function() { + document.getElementById(canvasId).remove(); + }); + + // see https://github.com/chartjs/Chart.js/issues/2807 + it('should gracefully handle invalid item', function() { + var chart = new Chart('foobar'); + + expect(chart).not.toBeValidChart(); + + chart.destroy(); + }); + + it('should accept a DOM element id', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvasId); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas element', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvas); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas context2D', function() { + var canvas = document.getElementById(canvasId); + var context = canvas.getContext('2d'); + var chart = new Chart(context); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(context); + + chart.destroy(); + }); + + it('should accept an array containing canvas', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart([canvas]); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + }); + + describe('config.options.aspectRatio', function() { + it('should use default "global" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 620px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 620, dh: 310, + rw: 620, rh: 310, + }); + }); + + it('should use default "chart" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + type: 'doughnut', + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 425px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 425, dh: 425, + rw: 425, rh: 425, + }); + }); + + it('should use "user" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 405px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 405, dh: 135, + rw: 405, rh: 135, + }); + }); + + it('should not apply aspect ratio when height specified', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 400px; height: 410px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 400, dh: 410, + rw: 400, rh: 410, + }); + }); + }); + + describe('config.options.responsive: false', function() { + it('should use default canvas size for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + }); + + it('should use canvas attributes for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '', + width: 305, + height: 245, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 305, dh: 245, + rw: 305, rh: 245, + }); + }); + + it('should use canvas style for render and display sizes (if no attributes)', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 345, rh: 125, + }); + }); + + it('should use attributes for the render size and style for the display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px;', + width: 165, + height: 85, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 165, rh: 85, + }); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: true)', function() { + it('should fill parent width and use aspect ratio to calculate height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 490, + rw: 300, rh: 490, + }); + }); + }); + + describe('controller.destroy', function() { + it('should reset context to default values', function() { + var chart = acquireChart({}); + var context = chart.chart.ctx; + + chart.destroy(); + + // https://www.w3.org/TR/2dcontext/#conformance-requirements + Chart.helpers.each({ + fillStyle: '#000000', + font: '10px sans-serif', + lineJoin: 'miter', + lineCap: 'butt', + lineWidth: 1, + miterLimit: 10, + shadowBlur: 0, + shadowColor: 'rgba(0, 0, 0, 0)', + shadowOffsetX: 0, + shadowOffsetY: 0, + strokeStyle: '#000000', + textAlign: 'start', + textBaseline: 'alphabetic' + }, function(value, key) { + expect(context[key]).toBe(value); + }); + }); + + it('should restore canvas initial values', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + width: 180, + style: 'width: 512px; height: 480px' + }, + wrapper: { + style: 'width: 450px; height: 450px; position: relative' + } + }); + + var canvas = chart.chart.canvas; + var wrapper = canvas.parentNode; + wrapper.style.width = '475px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 475, dh: 450, + rw: 475, rh: 450, + }); + + chart.destroy(); + + expect(canvas.getAttribute('width')).toBe('180'); + expect(canvas.getAttribute('height')).toBe(null); + expect(canvas.style.width).toBe('512px'); + expect(canvas.style.height).toBe('480px'); + expect(canvas.style.display).toBe(''); + + done(); + }); + }); + }); + + describe('event handling', function() { + it('should notify plugins about events', function() { + var notifiedEvent; + var plugin = { + onEvent: function(chart, e) { + notifiedEvent = e; + } + }; + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + }, + plugins: [plugin] + }); + + var node = chart.chart.canvas; + var rect = node.getBoundingClientRect(); + var clientX = (rect.left + rect.right) / 2; + var clientY = (rect.top + rect.bottom) / 2; + + var evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + clientX: clientX, + clientY: clientY + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check that notifiedEvent is correct + expect(notifiedEvent).not.toBe(undefined); + expect(notifiedEvent.native).toBe(evt); + + // Is type correctly translated + expect(notifiedEvent.type).toBe(evt.type); + + // Canvas width and height + expect(notifiedEvent.width).toBe(chart.chart.width); + expect(notifiedEvent.height).toBe(chart.chart.height); + + // Relative Position + expect(notifiedEvent.x).toBe(chart.chart.width / 2); + expect(notifiedEvent.y).toBe(chart.chart.height / 2); + }); + }); +});