From 8ec7ce2f93a731d53c9330b7ef900a72367ac7aa Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:42:56 +0200 Subject: [PATCH 1/4] Gulp command switch to run specific test files Add the --inputs command switch to the unittest and unittestWatch tasks, to be able to run unit tests from the specified files only (e.g. gulp unittest --inputs=test/core.element.tests.js;test/core.helpers.tests.js). --- gulpfile.js | 15 +++++++++------ package.json | 3 ++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b3dab98d11a..f4a25cf2ff8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,6 +17,7 @@ var browserify = require('browserify'); var source = require('vinyl-source-stream'); var merge = require('merge-stream'); var collapse = require('bundle-collapser/plugin'); +var argv = require('yargs').argv var package = require('./package.json'); var srcDir = './src/'; @@ -33,11 +34,10 @@ var header = "/*!\n" + " */\n"; var preTestFiles = [ - './node_modules/moment/min/moment.min.js', + './node_modules/moment/min/moment.min.js' ]; var testFiles = [ - './test/mockContext.js', './test/*.js', // Disable tests which need to be rewritten based on changes introduced by @@ -157,10 +157,13 @@ function validHTMLTask() { } function startTest() { - var files = ['./src/**/*.js']; - Array.prototype.unshift.apply(files, preTestFiles); - Array.prototype.push.apply(files, testFiles); - return files; + return [].concat(preTestFiles).concat([ + './src/**/*.js', + './test/mockContext.js' + ]).concat( + argv.inputs? + argv.inputs.split(';'): + testFiles); } function unittestTask() { diff --git a/package.json b/package.json index e901b26c3f1..4d2bb804288 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "karma-jasmine": "^0.3.6", "karma-jasmine-html-reporter": "^0.1.8", "merge-stream": "^1.0.0", - "vinyl-source-stream": "^1.1.0" + "vinyl-source-stream": "^1.1.0", + "yargs": "^5.0.0" }, "spm": { "main": "Chart.js" From 16bcd6adc579cb3deae16ea915680bc219924cdc Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:43:52 +0200 Subject: [PATCH 2/4] Fix initial aspect ratio when not responsive When responsive is false and no canvas height explicitly set, the aspectRatio option wasn't applied because of the canvas default height. Prevent the retinaScale method to change the canvas display size since this method is called for none responsive charts, but instead make the resize() responsible of these changes. Also, as discussed some time ago, moved most of the core.js logic into core.controller.js. Clean up the destroy process and make sure that initial canvas values are properly saved and restored. --- src/core/core.controller.js | 198 ++++++++++++++++++++++++++++++------ src/core/core.helpers.js | 3 - src/core/core.js | 46 +-------- 3 files changed, 170 insertions(+), 77 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 30ba1341888..1d169adeac2 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,6 +3,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; @@ -13,40 +14,176 @@ 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; + }); + + delete canvas._chartjs; + } + + /** + * Initializes the given config with global and chart default values. + */ + function initConfig(config) { + config = config || {}; + return helpers.configMerge({ + options: helpers.configMerge( + Chart.defaults.global, + Chart.defaults[config.type], + config.options || {}), + data: { + datasets: [], + labels: [] + } + }, config); + } + /** * @class Chart.Controller * The main controller of a chart. */ - Chart.Controller = function(instance) { + Chart.Controller = function(context, config, instance) { + var me = this; + var canvas; - this.chart = instance; - this.config = instance.config; - this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {}); - this.id = helpers.uid(); + config = initConfig(config); + canvas = initCanvas(context.canvas, config); - Object.defineProperty(this, 'data', { + 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); + + me.id = helpers.uid(); + me.chart = instance; + me.config = instance.config; + me.options = me.config.options; + + Object.defineProperty(me, 'data', { get: function() { - return this.config.data; + return me.config.data; + } + }); + + // Always bind this so that if the responsive state changes we still work + helpers.addResizeListener(canvas.parentNode, function() { + if (me.config.options.responsive) { + me.resize(); } }); // Add the chart instance to the global namespace - Chart.instances[this.id] = this; + Chart.instances[me.id] = me; - if (this.options.responsive) { + if (me.options.responsive) { // Silent resize before chart draws - this.resize(true); + me.resize(true); } - this.initialize(); + me.initialize(); - return this; + return me; }; helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ { - initialize: function() { var me = this; + // Before init plugin notification Chart.plugins.notify('beforeInit', [me]); @@ -82,15 +219,17 @@ module.exports = function(Chart) { resize: function(silent) { var me = this; var chart = me.chart; + var options = me.options; var canvas = chart.canvas; - var newWidth = helpers.getMaximumWidth(canvas); - var aspectRatio = chart.aspectRatio; - var newHeight = (me.options.maintainAspectRatio && isNaN(aspectRatio) === false && isFinite(aspectRatio) && aspectRatio !== 0) ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas); + var aspectRatio = (options.maintainAspectRatio && chart.aspectRatio) || null; - var sizeChanged = chart.width !== newWidth || chart.height !== newHeight; + // the canvas render width and height will be casted to integers so make sure that + // the canvas display style uses the same integer values to avoid blurring effect. + var newWidth = Math.floor(helpers.getMaximumWidth(canvas)); + var newHeight = Math.floor(aspectRatio? newWidth / aspectRatio : helpers.getMaximumHeight(canvas)); - if (!sizeChanged) { - return me; + if (chart.width === newWidth && chart.height === newHeight) { + return; } canvas.width = chart.width = newWidth; @@ -98,6 +237,9 @@ module.exports = function(Chart) { helpers.retinaScale(chart); + canvas.style.width = newWidth + 'px'; + canvas.style.height = newHeight + 'px'; + // Notify any plugins about the resize var newSize = {width: newWidth, height: newHeight}; Chart.plugins.notify('resize', [me, newSize]); @@ -111,8 +253,6 @@ module.exports = function(Chart) { me.stop(); me.update(me.options.responsiveAnimationDuration); } - - return me; }, ensureScalesHaveIDs: function() { @@ -539,25 +679,23 @@ module.exports = function(Chart) { destroy: function() { var me = this; + var canvas = me.chart.canvas; + me.stop(); me.clear(); + helpers.unbindEvents(me, me.events); - helpers.removeResizeListener(me.chart.canvas.parentNode); - // Reset canvas height/width attributes - var canvas = me.chart.canvas; - canvas.width = me.chart.width; - canvas.height = me.chart.height; + if (canvas) { + helpers.removeResizeListener(canvas.parentNode); + releaseCanvas(canvas); + } // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here if (me.chart.originalDevicePixelRatio !== undefined) { me.chart.ctx.scale(1 / me.chart.originalDevicePixelRatio, 1 / me.chart.originalDevicePixelRatio); } - // Reset to the old style since it may have been changed by the device pixel ratio changes - canvas.style.width = me.chart.originalCanvasStyleWidth; - canvas.style.height = me.chart.originalCanvasStyleHeight; - Chart.plugins.notify('destroy', [me]); delete Chart.instances[me.id]; diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 8609acdd09d..e404b61b1cd 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -859,9 +859,6 @@ module.exports = function(Chart) { // when destroy is called chart.originalDevicePixelRatio = chart.originalDevicePixelRatio || pixelRatio; } - - canvas.style.width = width + 'px'; - canvas.style.height = height + 'px'; }; // -- Canvas methods helpers.clear = function(chart) { diff --git a/src/core/core.js b/src/core/core.js index 577db878e16..d2899783816 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -5,12 +5,6 @@ module.exports = function() { // Occupy the global variable of Chart, and create a simple base class var Chart = function(context, config) { var me = this; - var helpers = Chart.helpers; - me.config = config || { - data: { - datasets: [] - } - }; // Support a jQuery'd canvas element if (context.length && context[0].getContext) { @@ -22,44 +16,9 @@ module.exports = function() { context = context.getContext('2d'); } - me.ctx = context; - me.canvas = context.canvas; - - context.canvas.style.display = context.canvas.style.display || 'block'; - - // Figure out what the size of the chart will be. - // If the canvas has a specified width and height, we use those else - // we look to see if the canvas node has a CSS width and height. - // If there is still no height, fill the parent container - me.width = context.canvas.width || parseInt(helpers.getStyle(context.canvas, 'width'), 10) || helpers.getMaximumWidth(context.canvas); - me.height = context.canvas.height || parseInt(helpers.getStyle(context.canvas, 'height'), 10) || helpers.getMaximumHeight(context.canvas); - - me.aspectRatio = me.width / me.height; - - if (isNaN(me.aspectRatio) || isFinite(me.aspectRatio) === false) { - // If the canvas has no size, try and figure out what the aspect ratio will be. - // Some charts prefer square canvases (pie, radar, etc). If that is specified, use that - // else use the canvas default ratio of 2 - me.aspectRatio = config.aspectRatio !== undefined ? config.aspectRatio : 2; - } - - // Store the original style of the element so we can set it back - me.originalCanvasStyleWidth = context.canvas.style.width; - me.originalCanvasStyleHeight = context.canvas.style.height; - - // High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - helpers.retinaScale(me); - me.controller = new Chart.Controller(me); - - // Always bind this so that if the responsive state changes we still work - helpers.addResizeListener(context.canvas.parentNode, function() { - if (me.controller && me.controller.config.options.responsive) { - me.controller.resize(); - } - }); - - return me.controller ? me.controller : me; + me.controller = new Chart.Controller(context, config, me); + return me.controller; }; // Globally expose the defaults to allow for user updating/changing @@ -106,5 +65,4 @@ module.exports = function() { Chart.Chart = Chart; return Chart; - }; From d610987cfbc2ed37d5b2d508b1378f9cb401edd2 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:44:06 +0200 Subject: [PATCH 3/4] Fix radar default aspect ratio and samples Now that the aspect ratio is correctly handled, fix samples for charts with aspect ratio of 1 which was vertically too large. Also fix the default aspect ratio for radar charts which wasn't applied when creating a chart directly using new Chart(ctx, { type: 'radar' }). --- samples/doughnut.html | 2 +- samples/polar-area.html | 2 +- samples/radar-skip-points.html | 2 +- samples/radar.html | 2 +- src/charts/Chart.Radar.js | 1 - src/controllers/controller.radar.js | 1 + 6 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/doughnut.html b/samples/doughnut.html index 73c96ad802d..38a9d578234 100644 --- a/samples/doughnut.html +++ b/samples/doughnut.html @@ -15,7 +15,7 @@ -
+
diff --git a/samples/polar-area.html b/samples/polar-area.html index ae21e1d44ab..16572dd4514 100644 --- a/samples/polar-area.html +++ b/samples/polar-area.html @@ -15,7 +15,7 @@ -
+
diff --git a/samples/radar-skip-points.html b/samples/radar-skip-points.html index 9d0816d4b47..c1680afd0c9 100644 --- a/samples/radar-skip-points.html +++ b/samples/radar-skip-points.html @@ -15,7 +15,7 @@ -
+
diff --git a/samples/radar.html b/samples/radar.html index 4203cff1554..eadcd9ecab8 100644 --- a/samples/radar.html +++ b/samples/radar.html @@ -15,7 +15,7 @@ -
+
diff --git a/src/charts/Chart.Radar.js b/src/charts/Chart.Radar.js index 1648e9faca6..d17bd5d8108 100644 --- a/src/charts/Chart.Radar.js +++ b/src/charts/Chart.Radar.js @@ -3,7 +3,6 @@ module.exports = function(Chart) { Chart.Radar = function(context, config) { - config.options = Chart.helpers.configMerge({aspectRatio: 1}, config.options); config.type = 'radar'; return new Chart(context, config); diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 3c050595a83..6232165f38b 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -5,6 +5,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; Chart.defaults.radar = { + aspectRatio: 1, scale: { type: 'radialLinear' }, From 7d65bd3f52fc0f24c247f605cf7b0b6a3c7d0469 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:44:17 +0200 Subject: [PATCH 4/4] Initial core.controller.js unit tests --- test/core.controller.tests.js | 488 ++++++++++++++++++++++++++++++++++ test/mockContext.js | 48 +++- 2 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 test/core.controller.tests.js diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js new file mode 100644 index 00000000000..83eba3baf4d --- /dev/null +++ b/test/core.controller.tests.js @@ -0,0 +1,488 @@ +describe('Chart.Controller', function() { + + function processResizeEvent(chart, callback) { + setTimeout(callback, 100); + } + + 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: false)', function() { + it('should fill parent width and height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + }); + + it('should resize the canvas when parent width changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.width = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 350, + rw: 455, rh: 350, + }); + + wrapper.style.width = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 350, + rw: 150, rh: 350, + }); + + done(); + }); + }); + }); + + it('should resize the canvas when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 455, + rw: 300, rh: 455, + }); + + wrapper.style.height = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + done(); + }); + }); + }); + + it('should NOT include parent padding when resizing the canvas', function(done) { + var chart = acquireChart({ + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'padding: 50px; width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '355px'; + wrapper.style.width = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + done(); + }); + }); + + it('should resize the canvas when the canvas display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'display: none;' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + var canvas = chart.chart.canvas; + canvas.style.display = 'block'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + done(); + }); + }); + + it('should resize the canvas when the wrapper display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'display: none; width: 460px; height: 380px' + } + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.display = 'block'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 460, dh: 380, + rw: 460, rh: 380, + }); + + done(); + }); + }); + }); + + 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 apect ratio when parent width changes', function(done) { + var chart = acquireChart({ + type: 'line', // AR == 2 + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.width = '450px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 450, dh: 225, + rw: 450, rh: 225, + }); + + wrapper.style.width = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 75, + rw: 150, rh: 75, + }); + + done(); + }); + }); + }); + + it('should NOT resize the canvas when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + wrapper.style.height = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + done(); + }); + }); + }); + }); + + describe('controller.destroy', function() { + it('should restore canvas (and context) initial values', function(done) { + var chart = acquireChart({ + type: 'line', + 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'; + processResizeEvent(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(); + }); + }); + }); +}); diff --git a/test/mockContext.js b/test/mockContext.js index 9590b53608c..48e5ce30406 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -158,10 +158,55 @@ }; } + function toBeChartOfSize() { + return { + compare: function(actual, expected) { + 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; + } + + // 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 + toEqualOneOf: toEqualOneOf, + toBeChartOfSize: toBeChartOfSize }); }); @@ -214,6 +259,7 @@ } function releaseChart(chart) { + chart.destroy(); chart.chart.canvas.parentNode.remove(); delete charts[chart.id]; delete chart;