]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Enhance context acquisition on chart creation
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Sat, 15 Oct 2016 21:40:22 +0000 (23:40 +0200)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sun, 16 Oct 2016 13:25:13 +0000 (08:25 -0500)
Add support for creating a chart from the canvas id and prevent exceptions, at construction time, when the given item doesn't provide a valid CanvasRenderingContext2D or when the getContext API is not accessible (e.g. undefined by add-ons to prevent fingerprinting). New jasmine matcher to verify chart validity.

.gitignore
docs/00-Getting-Started.md
src/core/core.controller.js
src/core/core.js
test/core.controller.tests.js
test/mockContext.js

index 478edb5eca4cfa000542a9eed21e627b9b0cc9b3..8ef0139ea96b216d8e52c67fbd276c8adb55c4be 100644 (file)
@@ -6,4 +6,5 @@
 
 .DS_Store
 .idea
+.vscode
 bower.json
index 910ea5bea0c3c73d2a060e752e724890cbf462bd..b7eb09dfc085c8124f4ff6846db564c6cacbf27f 100644 (file)
@@ -71,6 +71,7 @@ To create a chart, we need to instantiate the `Chart` class. To do this, we need
 var ctx = document.getElementById("myChart");
 var ctx = document.getElementById("myChart").getContext("2d");
 var ctx = $("#myChart");
+var ctx = "myChart";
 ```
 
 Once you have the element or context, you're ready to instantiate a pre-defined chart-type or create your own!
index 5e3632e4fde947ac8348bdcc265ffd082dcafbd5..c08d0192864588ddc6d43661a0562cb140200b2d 100644 (file)
@@ -112,6 +112,36 @@ module.exports = function(Chart) {
                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.
         */
@@ -136,21 +166,22 @@ module.exports = function(Chart) {
         * @class Chart.Controller
         * The main controller of a chart.
         */
-       Chart.Controller = function(context, config, instance) {
+       Chart.Controller = function(item, config, instance) {
                var me = this;
-               var canvas;
 
                config = initConfig(config);
-               canvas = initCanvas(context.canvas, config);
+
+               var context = acquireContext(item, config);
+               var canvas = context && context.canvas;
+               var height = canvas && canvas.height;
+               var width = canvas && canvas.width;
 
                instance.ctx = context;
                instance.canvas = canvas;
                instance.config = config;
-               instance.width = canvas.width;
-               instance.height = canvas.height;
-               instance.aspectRatio = canvas.width / canvas.height;
-
-               helpers.retinaScale(instance);
+               instance.width = width;
+               instance.height = height;
+               instance.aspectRatio = height? width / height : null;
 
                me.id = helpers.uid();
                me.chart = instance;
@@ -167,6 +198,17 @@ module.exports = function(Chart) {
                        }
                });
 
+               if (!context || !canvas) {
+                       // The given item is not a compatible context2d element, let's return before finalizing
+                       // the chart initialization but after setting basic chart / controller properties that
+                       // can help to figure out that the chart is not valid (e.g chart.canvas !== null);
+                       // https://github.com/chartjs/Chart.js/issues/2807
+                       console.error("Failed to create chart: can't acquire context from the given item");
+                       return me;
+               }
+
+               helpers.retinaScale(instance);
+
                // Responsiveness is currently based on the use of an iframe, however this method causes
                // performance issues and could be troublesome when used with ad blockers. So make sure
                // that the user is still able to create a chart without iframe when responsive is false.
@@ -593,7 +635,6 @@ module.exports = function(Chart) {
                        var meta, i, ilen;
 
                        me.stop();
-                       me.clear();
 
                        // dataset controllers need to cleanup associated data
                        for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) {
@@ -607,8 +648,10 @@ module.exports = function(Chart) {
                        if (canvas) {
                                helpers.unbindEvents(me, me.events);
                                helpers.removeResizeListener(canvas.parentNode);
+                               helpers.clear(me.chart);
                                releaseCanvas(canvas);
                                me.chart.canvas = null;
+                               me.chart.ctx = null;
                        }
 
                        // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here
index 319219f5d20bf0f7ce322210f81520f2be082aac..cea3cf30baeaa88184bb9e5d519b652bfe974b5f 100755 (executable)
@@ -3,22 +3,9 @@
 module.exports = function() {
 
        // Occupy the global variable of Chart, and create a simple base class
-       var Chart = function(context, config) {
-               var me = this;
-
-               // Support a jQuery'd canvas element
-               if (context.length && context[0].getContext) {
-                       context = context[0];
-               }
-
-               // Support a canvas domnode
-               if (context.getContext) {
-                       context = context.getContext('2d');
-               }
-
-               me.controller = new Chart.Controller(context, config, me);
-
-               return me.controller;
+       var Chart = function(item, config) {
+               this.controller = new Chart.Controller(item, config, this);
+               return this.controller;
        };
 
        // Globally expose the defaults to allow for user updating/changing
index d7548d4ce1f847d36d9a62d26dabca1bd4fc5ef8..a7b504ecdcce9b3bf7ade90dfc44ccff7723bc16 100644 (file)
@@ -4,7 +4,75 @@ describe('Chart.Controller', function() {
                setTimeout(callback, 100);
        }
 
-       describe('config', function() {
+       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({});
                        var data = chart.data;
index 83b73e31501da03d0520bd648557098a186abf50..8904cf8861207c477410be9b43f92c38ae374a71 100644 (file)
                };
        }
 
+       function toBeValidChart() {
+               return {
+                       compare: function(actual) {
+                               var chart = actual && actual.chart;
+                               var message = null;
+
+                               if (!(actual instanceof Chart.Controller)) {
+                                       message = 'Expected ' + actual + ' to be an instance of Chart.Controller';
+                               } else if (!(chart instanceof Chart)) {
+                                       message = 'Expected chart to be an instance of Chart';
+                               } else if (!(chart.canvas instanceof HTMLCanvasElement)) {
+                                       message = 'Expected canvas to be an instance of HTMLCanvasElement';
+                               } else if (!(chart.ctx instanceof CanvasRenderingContext2D)) {
+                                       message = 'Expected context to be an instance of CanvasRenderingContext2D';
+                               } else if (typeof chart.height !== 'number' || !isFinite(chart.height)) {
+                                       message = 'Expected height to be a strict finite number';
+                               } else if (typeof chart.width !== 'number' || !isFinite(chart.width)) {
+                                       message = 'Expected width to be a strict finite number';
+                               }
+
+                               return {
+                                       message: message? message : 'Expected ' + actual + ' to be valid chart',
+                                       pass: !message
+                               };
+                       }
+               };
+       }
+
        function toBeChartOfSize() {
                return {
                        compare: function(actual, expected) {
+                               var res = toBeValidChart().compare(actual);
+                               if (!res.pass) {
+                                       return res;
+                               }
+
                                var message = null;
-                               var chart, canvas, style, dh, dw, rh, rw;
-
-                               if (!actual || !(actual instanceof Chart.Controller)) {
-                                       message = 'Expected ' + actual + ' to be an instance of Chart.Controller.';
-                               } else {
-                                       chart = actual.chart;
-                                       canvas = chart.ctx.canvas;
-                                       style = getComputedStyle(canvas);
-                                       dh = parseInt(style.height);
-                                       dw = parseInt(style.width);
-                                       rh = canvas.height;
-                                       rw = canvas.width;
-
-                                       // sanity checks
-                                       if (chart.height !== rh) {
-                                               message = 'Expected chart height ' + chart.height + ' to be equal to render height ' + rh;
-                                       } else if (chart.width !== rw) {
-                                               message = 'Expected chart width ' + chart.width + ' to be equal to render width ' + rw;
-                                       }
+                               var chart = actual.chart;
+                               var canvas = chart.ctx.canvas;
+                               var style = getComputedStyle(canvas);
+                               var dh = parseInt(style.height, 10);
+                               var dw = parseInt(style.width, 10);
+                               var rh = canvas.height;
+                               var rw = canvas.width;
+
+                               // sanity checks
+                               if (chart.height !== rh) {
+                                       message = 'Expected chart height ' + chart.height + ' to be equal to render height ' + rh;
+                               } else if (chart.width !== rw) {
+                                       message = 'Expected chart width ' + chart.width + ' to be equal to render width ' + rw;
+                               }
 
-                                       // validity checks
-                                       if (dh !== expected.dh) {
-                                               message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh;
-                                       } else if (dw !== expected.dw) {
-                                               message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw;
-                                       } else if (rh !== expected.rh) {
-                                               message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh;
-                                       } else if (rw !== expected.rw) {
-                                               message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw;
-                                       }
+                               // validity checks
+                               if (dh !== expected.dh) {
+                                       message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh;
+                               } else if (dw !== expected.dw) {
+                                       message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw;
+                               } else if (rh !== expected.rh) {
+                                       message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh;
+                               } else if (rw !== expected.rw) {
+                                       message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw;
                                }
 
                                return {
                                        message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected,
                                        pass: !message
-                               }
+                               };
                        }
-               }
+               };
        }
 
        beforeEach(function() {
                jasmine.addMatchers({
                        toBeCloseToPixel: toBeCloseToPixel,
                        toEqualOneOf: toEqualOneOf,
+                       toBeValidChart: toBeValidChart,
                        toBeChartOfSize: toBeChartOfSize
                });
        });