]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Refactoring to put browser specific code in a new class (#3718)
authorEvert Timberg <evert.timberg+github@gmail.com>
Wed, 21 Dec 2016 15:22:05 +0000 (10:22 -0500)
committerGitHub <noreply@github.com>
Wed, 21 Dec 2016 15:22:05 +0000 (10:22 -0500)
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
src/chart.js
src/core/core.controller.js
src/core/core.interaction.js
src/core/core.legend.js
src/core/core.tooltip.js
src/platforms/platform.dom.js [new file with mode: 0644]
test/core.controller.tests.js
test/platform.dom.tests.js [new file with mode: 0644]

index f76ac4b0d97d8c5f373d767f13f70a7bc5b9fe7f..4c677c904d4144f6d46ba23c3cc4215dea31e9bf 100644 (file)
@@ -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) {}
 }
 ```
 
index 7c490e7eabcc8820250cc82917933a38d7569be0..186d07a42f5824c504d40b22baadd300f0150d31 100644 (file)
@@ -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);
index 4e28773880edee9593afde8bc3d1737b16f1af66..e64cb3a9dde92f5e5b9874cd6e0179ea67f4d00d 100644 (file)
@@ -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);
                                }
                        }
 
index aacdda19c3845fc8fe36f3f1a52ccb26e48ab08f..0888fa9591ba30ac2e5b6c162c73f36053f4f0ac 100644 (file)
@@ -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;
 
index 4ab51ba5dbf94c9f2afbc3e6a733a9ee50fc1006..45f51d05923b7134e67b94431727b73d04f82bef 100644 (file)
@@ -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);
+                       }
                }
        });
 };
index d99f2302ff55b1cedc49968e86e10327f99aa73c..c1ac7830739145e792340536c3a1066664a8c0a1 100755 (executable)
@@ -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 (file)
index 0000000..fb41f88
--- /dev/null
@@ -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;
+               }
+       };
+};
index 44e7d3d21edfc00391ccddefa0bcb03ab7fb3ef2..0e8956158123b83eb6ffc13a8e5a3295e045baf9 100644 (file)
@@ -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 (file)
index 0000000..f20b6e1
--- /dev/null
@@ -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);
+               });
+       });
+});