diff --git a/.gitignore b/.gitignore index 8ef0139ea96..172413437e6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ .idea .vscode bower.json + +*.swp diff --git a/LICENSE.md b/LICENSE.md index dc93bcc36ce..620db307e3c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,5 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2016 Nick Downie + +Copyright (c) 2013-2017 Nick Downie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md index b7eb09dfc08..81e03c0a986 100644 --- a/docs/00-Getting-Started.md +++ b/docs/00-Getting-Started.md @@ -44,11 +44,11 @@ To import Chart.js using an awesome module loader: ```javascript // Using CommonJS -var Chart = require('src/chart.js') +var Chart = require('chart.js') var myChart = new Chart({...}) // ES6 -import Chart from 'src/chart.js' +import Chart from 'chart.js' let myChart = new Chart({...}) // Using requirejs diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index a4692208892..865a7558379 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -81,7 +81,7 @@ responsive | Boolean | true | Resizes the chart canvas when its container does. responsiveAnimationDuration | Number | 0 | Duration in milliseconds it takes to animate to new size after a resize event. maintainAspectRatio | Boolean | true | Maintain the original canvas aspect ratio `(width / height)` when resizing events | Array[String] | `["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"]` | Events that the chart should listen to for tooltips and hovering -onClick | Function | null | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed an array of active elements +onClick | Function | null | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed the event and an array of active elements legendCallback | Function | ` function (chart) { }` | Function to generate a legend. Receives the chart object to generate a legend from. Default implementation returns an HTML string. onResize | Function | null | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. @@ -154,6 +154,7 @@ fontColor | Color | "#666" | Font color inherited from global configuration fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family inherited from global configuration padding | Number | 10 | Padding between rows of colored boxes generateLabels: | Function | `function(chart) { }` | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#chart-configuration-legend-item-interface) for details. +filter | Function | null | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#chart-configuration-legend-item-interface) and the chart data usePointStyle | Boolean | false | Label style will match corresponding point style (size is based on fontSize, boxWidth is not used in this case). #### Legend Item Interface @@ -309,7 +310,7 @@ Name | Type | Default | Description mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details intersect | Boolean | true | if true, the hover mode only applies when the mouse position intersects an item on the chart animationDuration | Number | 400 | Duration in milliseconds it takes to animate hover style changes -onHover | Function | null | Called when any of the events fire. Called in the context of the chart and passed an array of active elements (bars, points, etc) +onHover | Function | null | Called when any of the events fire. Called in the context of the chart and passed the event and an array of active elements (bars, points, etc) ### Interaction Modes When configuring interaction with the graph via hover or tooltips, a number of different modes are available. @@ -323,7 +324,7 @@ nearest | Gets the item that is nearest to the point. The nearest item is determ single (deprecated) | Finds the first item that intersects the point and returns it. Behaves like 'nearest' mode with intersect = true. label (deprecated) | See `'index'` mode index | Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. -x-axis (deprecated) | Behaves like `'index'` mode with `intersect = true` +x-axis (deprecated) | Behaves like `'index'` mode with `intersect = false` dataset | Finds items in the same dataset. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. x | Returns all items that would intersect based on the `X` coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts y | Returns all items that would intersect based on the `Y` coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts. @@ -341,7 +342,7 @@ onComplete | Function | none | Callback called at the end of an animation. Passe #### Animation Callbacks -The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed an object that implements the following interface. An example usage of these callbacks can be found on [Github](https://github.com/chartjs/Chart.js/blob/master/samples/AnimationCallbacks/progress-bar.html). This sample displays a progress bar showing how far along the animation is. +The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed an object that implements the following interface. An example usage of these callbacks can be found on [Github](https://github.com/chartjs/Chart.js/blob/master/samples/animation/progress-bar.html). This sample displays a progress bar showing how far along the animation is. ```javascript { diff --git a/docs/02-Scales.md b/docs/02-Scales.md index ae7758326f1..af7812ce03b 100644 --- a/docs/02-Scales.md +++ b/docs/02-Scales.md @@ -36,7 +36,7 @@ afterFit | Function | undefined | Callback that runs after the scale fits to the afterUpdate | Function | undefined | Callback that runs at the end of the update process. Passed a single argument, the scale instance. **gridLines** | Object | - | See [grid line configuration](#grid-line-configuration) section. **scaleLabel** | Object | | See [scale title configuration](#scale-title-configuration) section. -**ticks** | Object | | See [ticks configuration](#ticks-configuration) section. +**ticks** | Object | | See [tick configuration](#tick-configuration) section. #### Grid Line Configuration diff --git a/docs/03-Line-Chart.md b/docs/03-Line-Chart.md index f62a20879df..24b77cfe6df 100644 --- a/docs/03-Line-Chart.md +++ b/docs/03-Line-Chart.md @@ -57,7 +57,7 @@ pointHitRadius | `Number or Array` | The pixel size of the non-displayed pointHoverBackgroundColor | `Color or Array` | Point background color when hovered pointHoverBorderColor | `Color or Array` | Point border color when hovered pointHoverBorderWidth | `Number or Array` | Border width of point when hovered -pointStyle | `String, Array, Image, Array` | The style of point. Options are 'circle', 'triangle', 'rect', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash'. If the option is an image, that image is drawn on the canvas using `drawImage`. +pointStyle | `String, Array, Image, Array` | The style of point. Options are 'circle', 'triangle', 'rect', 'rectRounded', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash'. If the option is an image, that image is drawn on the canvas using `drawImage`. showLine | `Boolean` | If false, the line is not drawn for this dataset spanGaps | `Boolean` | If true, lines will be drawn between points with no or null data steppedLine | `Boolean` | If true, the line is shown as a stepped line and 'lineTension' will be ignored diff --git a/docs/04-Bar-Chart.md b/docs/04-Bar-Chart.md index 777acc7a7e7..ef2cd3c19a6 100644 --- a/docs/04-Bar-Chart.md +++ b/docs/04-Bar-Chart.md @@ -49,6 +49,7 @@ borderSkipped | `String or Array` | Which edge to skip drawing the borde hoverBackgroundColor | `Color or Array` | Bar background color when hovered hoverBorderColor | `Color or Array` | Bar border color when hovered hoverBorderWidth | `Number or Array` | Border width of bar when hovered +stack | `String` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack) An example data object using these attributes is shown below. diff --git a/docs/05-Radar-Chart.md b/docs/05-Radar-Chart.md index f191c3a8178..977574faf6c 100644 --- a/docs/05-Radar-Chart.md +++ b/docs/05-Radar-Chart.md @@ -46,11 +46,11 @@ pointBackgroundColor | `Color or Array` | The fill color for points pointBorderWidth | `Number or Array` | The width of the point border in pixels pointRadius | `Number or Array` | The radius of the point shape. If set to 0, nothing is rendered. pointHoverRadius | `Number or Array` | The radius of the point when hovered -hitRadius | `Number or Array` | The pixel size of the non-displayed point that reacts to mouse events +pointHitRadius | `Number or Array` | The pixel size of the non-displayed point that reacts to mouse events pointHoverBackgroundColor | `Color or Array` | Point background color when hovered pointHoverBorderColor | `Color or Array` | Point border color when hovered pointHoverBorderWidth | `Number or Array` | Border width of point when hovered -pointStyle | `String or Array` | The style of point. Options include 'circle', 'triangle', 'rect', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash' +pointStyle | `String or Array` | The style of point. Options include 'circle', 'triangle', 'rect', 'rectRounded', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash' An example data object using these attributes is shown below. diff --git a/docs/07-Pie-Doughnut-Chart.md b/docs/07-Pie-Doughnut-Chart.md index 0b2cc115e43..99830132281 100644 --- a/docs/07-Pie-Doughnut-Chart.md +++ b/docs/07-Pie-Doughnut-Chart.md @@ -102,8 +102,10 @@ For example, we could have a doughnut chart that animates by scaling out from th ```javascript new Chart(ctx,{ type:"doughnut", - animation:{ - animateScale:true + options: { + animation:{ + animateScale:true + } } }); // This will create a chart with all of the default options, merged from the global config, diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 062bf59e8fb..05642fcab43 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -182,7 +182,7 @@ var myPieChart = new Chart(ctx, { }); ``` -See `sample/line-customTooltips.html` for examples on how to get started. +See `samples/tooltips/line-customTooltips.html` for examples on how to get started. ### Writing New Scale Types @@ -410,8 +410,9 @@ 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 +Plugins should implement the `IPlugin` interface: ```javascript { beforeInit: function(chartInstance) { }, @@ -437,6 +438,10 @@ 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 + beforeEvent: function(chartInstance, event) {} + afterEvent: function(chartInstance, event) {} } ``` diff --git a/docs/10-Notes.md b/docs/10-Notes.md index fd2125e171f..451a8a8cc90 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -79,12 +79,13 @@ Built in Chart Types There are many plugins that add additional functionality to Chart.js. Some particularly notable ones are listed here. In addition, many plugins can be found on the [Chart.js GitHub organization](https://github.com/chartjs). - - Chart.Zoom.js - Enable zooming and panning on charts - - Chart.Annotation.js - Draw lines and boxes on chart area + - chartjs-plugin-annotation.js - Draw lines and boxes on chart area + - chartjs-plugin-deferred.js - Defer initial chart update until chart scrolls into viewport + - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse + - chartjs-plugin-zoom.js - Enable zooming and panning on charts - Chart.BarFunnel.js - Adds a bar funnel chart type - - Chart.Deferred.js - Defer initial chart update until chart scrolls into viewport - - Chart.Smith.js - Adds a smith chart type - Chart.LinearGauge.js - Adds a linear gauge chart type + - Chart.Smith.js - Adds a smith chart type ### Popular Extensions @@ -101,6 +102,7 @@ There are many extensions which are available for use with popular frameworks. S - react-chartjs-2 #### Django + - Django JChart - Django Chartjs #### Ruby on Rails @@ -108,3 +110,6 @@ There are many extensions which are available for use with popular frameworks. S #### Laravel - laravel-chartjs + +#### Vue.js + - vue-chartjs diff --git a/gulpfile.js b/gulpfile.js index 1ce312d9098..6cd6efa5d91 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -29,7 +29,7 @@ var header = "/*!\n" + " * http://chartjs.org/\n" + " * Version: {{ version }}\n" + " *\n" + - " * Copyright 2016 Nick Downie\n" + + " * Copyright 2017 Nick Downie\n" + " * Released under the MIT license\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; @@ -71,7 +71,15 @@ function bowerTask() { homepage: package.homepage, license: package.license, version: package.version, - main: outDir + "Chart.js" + main: outDir + "Chart.js", + ignore: [ + '.github', + '.codeclimate.yml', + '.gitignore', + '.npmignore', + '.travis.yml', + 'scripts' + ] }, null, 2); return file('bower.json', json, { src: true }) diff --git a/package.json b/package.json index b8dba18746a..40eca264e17 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "main": "src/chart.js", "repository": { @@ -25,7 +25,7 @@ "gulp-replace": "^0.5.4", "gulp-size": "~0.4.0", "gulp-streamify": "^1.0.2", - "gulp-uglify": "~0.2.x", + "gulp-uglify": "~2.0.x", "gulp-util": "~2.2.x", "gulp-zip": "~3.2.0", "jasmine": "^2.3.2", diff --git a/samples/bar/bar-stacked-group.html b/samples/bar/bar-stacked-group.html new file mode 100644 index 00000000000..074e3eac08c --- /dev/null +++ b/samples/bar/bar-stacked-group.html @@ -0,0 +1,105 @@ + + + + + Stacked Bar Chart with Groups + + + + + + +
+ +
+ + + + + diff --git a/samples/line/line-stacked-area.html b/samples/line/line-stacked-area.html index 9621c14d511..041c9a84b72 100644 --- a/samples/line/line-stacked-area.html +++ b/samples/line/line-stacked-area.html @@ -121,7 +121,7 @@ }; document.getElementById('randomizeData').addEventListener('click', function() { - $.each(config.data.datasets, function(i, dataset) { + config.data.datasets.forEach(function(dataset) { dataset.data = dataset.data.map(function() { return randomScalingFactor(); }); diff --git a/samples/line/point-styles.html b/samples/line/point-styles.html index 547b1ea0a2b..0056e749976 100644 --- a/samples/line/point-styles.html +++ b/samples/line/point-styles.html @@ -70,6 +70,7 @@ 'circle', 'triangle', 'rect', + 'rectRounded', 'rectRot', 'cross', 'crossRot', diff --git a/samples/radar/radar.html b/samples/radar/radar.html index 507d5bb47d7..586e2df7aec 100644 --- a/samples/radar/radar.html +++ b/samples/radar/radar.html @@ -32,7 +32,7 @@ var config = { type: 'radar', data: { - labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], + labels: [["Eating", "Dinner"], ["Drinking", "Water"], "Sleeping", ["Designing", "Graphics"], "Coding", "Cycling", "Running"], datasets: [{ label: "My First dataset", backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(), diff --git a/samples/scales/linear/step-size.html b/samples/scales/linear/step-size.html index d38f3e1f0bb..ced0b6c3ca3 100644 --- a/samples/scales/linear/step-size.html +++ b/samples/scales/linear/step-size.html @@ -110,6 +110,65 @@ var ctx = document.getElementById("canvas").getContext("2d"); window.myLine = new Chart(ctx, config); }; + + document.getElementById('randomizeData').addEventListener('click', function() { + config.data.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + + window.myLine.update(); + }); + + var colorNames = Object.keys(window.chartColors); + document.getElementById('addDataset').addEventListener('click', function() { + var colorName = colorNames[config.data.datasets.length % colorNames.length]; + var newColor = window.chartColors[colorName]; + var newDataset = { + label: 'Dataset ' + config.data.datasets.length, + backgroundColor: newColor, + borderColor: newColor, + data: [], + fill: false + }; + + for (var index = 0; index < config.data.labels.length; ++index) { + newDataset.data.push(randomScalingFactor()); + } + + config.data.datasets.push(newDataset); + window.myLine.update(); + }); + + document.getElementById('addData').addEventListener('click', function() { + if (config.data.datasets.length > 0) { + var month = MONTHS[config.data.labels.length % MONTHS.length]; + config.data.labels.push(month); + + config.data.datasets.forEach(function(dataset) { + dataset.data.push(randomScalingFactor()); + }); + + window.myLine.update(); + } + }); + + document.getElementById('removeDataset').addEventListener('click', function() { + config.data.datasets.splice(0, 1); + window.myLine.update(); + }); + + document.getElementById('removeData').addEventListener('click', function() { + config.data.labels.splice(-1, 1); // remove the label first + + config.data.datasets.forEach(function(dataset, datasetIndex) { + dataset.data.pop(); + }); + + window.myLine.update(); + }); + diff --git a/src/chart.js b/src/chart.js index 2c5e628264c..77d2bb636c8 100644 --- a/src/chart.js +++ b/src/chart.js @@ -4,14 +4,15 @@ var Chart = require('./core/core.js')(); require('./core/core.helpers')(Chart); +require('./platforms/platform.js')(Chart); require('./core/core.canvasHelpers')(Chart); +require('./core/core.plugin.js')(Chart); require('./core/core.element')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); require('./core/core.layoutService')(Chart); require('./core/core.scaleService')(Chart); -require('./core/core.plugin.js')(Chart); require('./core/core.ticks.js')(Chart); require('./core/core.scale')(Chart); require('./core/core.title')(Chart); diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index e9a85aabd22..1d41386b6d7 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -35,21 +35,33 @@ module.exports = function(Chart) { initialize: function(chart, datasetIndex) { Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex); + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + + meta.stack = dataset.stack; // Use this to indicate that this is a bar dataset. - this.getMeta().bar = true; + meta.bar = true; }, - // Get the number of datasets that display bars. We use this to correctly calculate the bar width - getBarCount: function() { + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { var me = this; - var barCount = 0; + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); + + var stacks = []; helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { - var meta = me.chart.getDatasetMeta(datasetIndex); - if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) { - ++barCount; + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } }, me); - return barCount; + + return stacks.length; }, update: function(reset) { @@ -74,7 +86,7 @@ module.exports = function(Chart) { rectangle._datasetIndex = me.index; rectangle._index = index; - var ruler = me.getRuler(index); + var ruler = me.getRuler(index); // The index argument for compatible rectangle._model = { x: me.calculateBarX(index, me.index, ruler), y: reset ? scaleBase : me.calculateBarY(index, me.index), @@ -84,6 +96,7 @@ module.exports = function(Chart) { datasetLabel: dataset.label, // Appearance + horizontal: false, base: reset ? scaleBase : me.calculateBarBase(me.index, index), width: me.calculateBarWidth(ruler), backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), @@ -99,9 +112,11 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var base = 0; + var base = yScale.getBaseValue(); + var original = base; - if (yScale.options.stacked) { + if ((yScale.options.stacked === true) || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -109,9 +124,10 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); + base += value < 0 ? Math.min(currentVal, original) : Math.max(currentVal, original); } } @@ -121,34 +137,22 @@ module.exports = function(Chart) { return yScale.getBasePixel(); }, - getRuler: function(index) { + getRuler: function() { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var datasetCount = me.getBarCount(); + var stackCount = me.getStackCount(); - var tickWidth; - - if (xScale.options.type === 'category') { - tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index); - } else { - // Average width - tickWidth = xScale.width / xScale.ticks.length; - } + var tickWidth = xScale.width / xScale.ticks.length; var categoryWidth = tickWidth * xScale.options.categoryPercentage; var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; - var fullBarWidth = categoryWidth / datasetCount; - - if (xScale.ticks.length !== me.chart.data.labels.length) { - var perc = xScale.ticks.length / me.chart.data.labels.length; - fullBarWidth = fullBarWidth * perc; - } + var fullBarWidth = categoryWidth / stackCount; var barWidth = fullBarWidth * xScale.options.barPercentage; var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickWidth: tickWidth, categoryWidth: categoryWidth, categorySpacing: categorySpacing, @@ -159,46 +163,50 @@ module.exports = function(Chart) { }, calculateBarWidth: function(ruler) { - var xScale = this.getScaleForId(this.getMeta().xAxisID); + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); if (xScale.options.barThickness) { return xScale.options.barThickness; } - return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth; + return ruler.barWidth; }, - // Get bar index from the given dataset index accounting for the fact that not all bars are visible - getBarIndex: function(datasetIndex) { - var barIndex = 0; - var meta, j; + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var yScale = me.getScaleForId(meta.yAxisID); + var dsMeta, j; + var stacks = [meta.stack]; for (j = 0; j < datasetIndex; ++j) { - meta = this.chart.getDatasetMeta(j); - if (meta.bar && this.chart.isDatasetVisible(j)) { - ++barIndex; + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } } - return barIndex; + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; - if (xScale.options.stacked) { - return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; - } - return leftTick + (ruler.barWidth / 2) + ruler.categorySpacing + - (ruler.barWidth * barIndex) + + (ruler.barWidth * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); }, calculateBarY: function(index, datasetIndex) { @@ -207,15 +215,17 @@ module.exports = function(Chart) { var yScale = me.getScaleForId(meta.yAxisID); var value = Number(me.getDataset().data[index]); - if (yScale.options.stacked) { - - var sumPos = 0, - sumNeg = 0; + if (yScale.options.stacked || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { + var base = yScale.getBaseValue(); + var sumPos = base, + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -241,12 +251,14 @@ module.exports = function(Chart) { var dataset = me.getDataset(); var i, len; + Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); for (i = 0, len = metaData.length; i < len; ++i) { var d = dataset.data[i]; if (d !== null && d !== undefined && !isNaN(d)) { metaData[i].transition(easingDecimal).draw(); } } + Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); }, setHoverStyle: function(rectangle) { @@ -331,6 +343,27 @@ module.exports = function(Chart) { }; Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ + + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); + + var stacks = []; + helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + }, me); + + return stacks.length; + }, + updateElement: function(rectangle, index, reset) { var me = this; var meta = me.getMeta(); @@ -346,7 +379,7 @@ module.exports = function(Chart) { rectangle._datasetIndex = me.index; rectangle._index = index; - var ruler = me.getRuler(index); + var ruler = me.getRuler(index); // The index argument for compatible rectangle._model = { x: reset ? scaleBase : me.calculateBarX(index, me.index), y: me.calculateBarY(index, me.index, ruler), @@ -356,6 +389,7 @@ module.exports = function(Chart) { datasetLabel: dataset.label, // Appearance + horizontal: true, base: reset ? scaleBase : me.calculateBarBase(me.index, index), height: me.calculateBarHeight(ruler), backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), @@ -363,62 +397,6 @@ module.exports = function(Chart) { borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) }; - rectangle.draw = function() { - var ctx = this._chart.ctx; - var vm = this._view; - - var halfHeight = vm.height / 2, - topY = vm.y - halfHeight, - bottomY = vm.y + halfHeight, - right = vm.base - (vm.base - vm.x), - halfStroke = vm.borderWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - topY += halfStroke; - bottomY -= halfStroke; - right += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [vm.base, bottomY], - [vm.base, topY], - [right, topY], - [right, bottomY] - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); - if (startCorner === -1) { - startCorner = 0; - } - - function cornerAt(cornerIndex) { - return corners[(startCorner + cornerIndex) % 4]; - } - - // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); - for (var i = 1; i < 4; i++) { - ctx.lineTo.apply(ctx, cornerAt(i)); - } - - ctx.fill(); - if (vm.borderWidth) { - ctx.stroke(); - } - }; rectangle.pivot(); }, @@ -427,9 +405,11 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var base = 0; + var base = xScale.getBaseValue(); + var originalBase = base; - if (xScale.options.stacked) { + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -437,9 +417,10 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); + base += value < 0 ? Math.min(currentVal, originalBase) : Math.max(currentVal, originalBase); } } @@ -449,33 +430,22 @@ module.exports = function(Chart) { return xScale.getBasePixel(); }, - getRuler: function(index) { + getRuler: function() { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var datasetCount = me.getBarCount(); - - var tickHeight; - if (yScale.options.type === 'category') { - tickHeight = yScale.getPixelForTick(index + 1) - yScale.getPixelForTick(index); - } else { - // Average width - tickHeight = yScale.width / yScale.ticks.length; - } + var stackCount = me.getStackCount(); + + var tickHeight = yScale.height / yScale.ticks.length; var categoryHeight = tickHeight * yScale.options.categoryPercentage; var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2; - var fullBarHeight = categoryHeight / datasetCount; - - if (yScale.ticks.length !== me.chart.data.labels.length) { - var perc = yScale.ticks.length / me.chart.data.labels.length; - fullBarHeight = fullBarHeight * perc; - } + var fullBarHeight = categoryHeight / stackCount; var barHeight = fullBarHeight * yScale.options.barPercentage; var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickHeight: tickHeight, categoryHeight: categoryHeight, categorySpacing: categorySpacing, @@ -487,11 +457,33 @@ module.exports = function(Chart) { calculateBarHeight: function(ruler) { var me = this; - var yScale = me.getScaleForId(me.getMeta().yAxisID); + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); if (yScale.options.barThickness) { return yScale.options.barThickness; } - return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight; + return ruler.barHeight; + }, + + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var xScale = me.getScaleForId(meta.xAxisID); + var dsMeta, j; + var stacks = [meta.stack]; + + for (j = 0; j < datasetIndex; ++j) { + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + } + + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex) { @@ -500,15 +492,17 @@ module.exports = function(Chart) { var xScale = me.getScaleForId(meta.xAxisID); var value = Number(me.getDataset().data[index]); - if (xScale.options.stacked) { - - var sumPos = 0, - sumNeg = 0; + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { + var base = xScale.getBaseValue(); + var sumPos = base, + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -531,20 +525,16 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0; - if (yScale.options.stacked) { - return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing; - } - return topTick + (ruler.barHeight / 2) + ruler.categorySpacing + - (ruler.barHeight * barIndex) + + (ruler.barHeight * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); } }); }; diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 3dcd33e9f63..3d478058279 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -183,7 +183,7 @@ module.exports = function(Chart) { chart.borderWidth = me.getMaxBorderWidth(meta.data); chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); - chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 1, 0); + chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); chart.offsetX = offset.x * chart.outerRadius; chart.offsetY = offset.y * chart.outerRadius; @@ -191,7 +191,7 @@ module.exports = function(Chart) { meta.total = me.calculateTotal(); me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); - me.innerRadius = me.outerRadius - chart.radiusLength; + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); helpers.each(meta.data, function(arc, index) { me.updateElement(arc, index, reset); diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 59d097b1be7..806ab4f3f58 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -139,11 +139,11 @@ module.exports = function(Chart) { var dataset = this.getDataset(); var custom = point.custom || {}; - if (custom.borderWidth) { + if (!isNaN(custom.borderWidth)) { borderWidth = custom.borderWidth; - } else if (dataset.pointBorderWidth) { + } else if (!isNaN(dataset.pointBorderWidth)) { borderWidth = helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, borderWidth); - } else if (dataset.borderWidth) { + } else if (!isNaN(dataset.borderWidth)) { borderWidth = dataset.borderWidth; } @@ -292,14 +292,16 @@ module.exports = function(Chart) { points[i].transition(easingDecimal); } + Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); // Transition and Draw the line if (lineEnabled(me.getDataset(), me.chart.options)) { meta.dataset.transition(easingDecimal).draw(); } + Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); // Draw the points for (i=0, ilen=points.length; i x)); + }; helpers.max = function(array) { return array.reduce(function(max, value) { if (!isNaN(value)) { @@ -361,7 +365,10 @@ module.exports = function(Chart) { pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; if (pointAfter && !pointAfter.model.skip) { - pointCurrent.deltaK = (pointAfter.model.y - pointCurrent.model.y) / (pointAfter.model.x - pointCurrent.model.x); + var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x); + + // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 + pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0; } if (!pointBefore || pointBefore.model.skip) { @@ -671,16 +678,6 @@ module.exports = function(Chart) { return window.setTimeout(callback, 1000 / 60); }; }()); - helpers.cancelAnimFrame = (function() { - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - }()); // -- DOM methods helpers.getRelativePosition = function(evt, chart) { var mouseX, mouseY; @@ -737,23 +734,6 @@ module.exports = function(Chart) { node['on' + eventType] = helpers.noop; } }; - helpers.bindEvents = function(chartInstance, arrayOfEvents, handler) { - // Create the events object if it's not already present - var events = chartInstance.events = chartInstance.events || {}; - - helpers.each(arrayOfEvents, function(eventName) { - events[eventName] = function() { - handler.apply(chartInstance, arguments); - }; - helpers.addEvent(chartInstance.chart.canvas, eventName, events[eventName]); - }); - }; - helpers.unbindEvents = function(chartInstance, arrayOfEvents) { - var canvas = chartInstance.chart.canvas; - helpers.each(arrayOfEvents, function(handler, eventName) { - helpers.removeEvent(canvas, eventName, handler); - }); - }; // Private helper function to convert max-width/max-height values that may be percentages into a number function parseMaxStyle(styleValue, node, parentProperty) { @@ -944,73 +924,6 @@ module.exports = function(Chart) { return color(c); }; - helpers.addResizeListener = function(node, callback) { - var iframe = document.createElement('iframe'); - iframe.className = 'chartjs-hidden-iframe'; - iframe.style.cssText = - 'display:block;'+ - 'overflow:hidden;'+ - 'border:0;'+ - 'margin:0;'+ - 'top:0;'+ - 'left:0;'+ - 'bottom:0;'+ - 'right:0;'+ - 'height:100%;'+ - 'width:100%;'+ - 'position:absolute;'+ - 'pointer-events:none;'+ - 'z-index:-1;'; - - // Prevent the iframe to gain focus on tab. - // https://github.com/chartjs/Chart.js/issues/3090 - iframe.tabIndex = -1; - - // Let's keep track of this added iframe and thus avoid DOM query when removing it. - var stub = node._chartjs = { - resizer: iframe, - ticking: false - }; - - // Throttle the callback notification until the next animation frame. - var notify = function() { - if (!stub.ticking) { - stub.ticking = true; - helpers.requestAnimFrame.call(window, function() { - if (stub.resizer) { - stub.ticking = false; - return callback(); - } - }); - } - }; - - // If the iframe is re-attached to the DOM, the resize listener is removed because the - // content is reloaded, so make sure to install the handler after the iframe is loaded. - // https://github.com/chartjs/Chart.js/issues/3521 - helpers.addEvent(iframe, 'load', function() { - helpers.addEvent(iframe.contentWindow || iframe, 'resize', notify); - - // The iframe size might have changed while loading, which can also - // happen if the size has been changed while detached from the DOM. - notify(); - }); - - node.insertBefore(iframe, node.firstChild); - }; - helpers.removeResizeListener = function(node) { - if (!node || !node._chartjs) { - return; - } - - var iframe = node._chartjs.resizer; - if (iframe) { - iframe.parentNode.removeChild(iframe); - node._chartjs.resizer = null; - } - - delete node._chartjs; - }; helpers.isArray = Array.isArray? function(obj) { return Array.isArray(obj); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index aacdda19c38..03423c0d604 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -3,6 +3,23 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + /** + * Helper function to get relative position for an event + * @param {Event|IEvent} 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); }; @@ -118,14 +135,14 @@ module.exports = function(Chart) { */ /** - * @namespace Chart.Interaction * Contains interaction related functions + * @namespace Chart.Interaction */ Chart.Interaction = { // Helper function for different modes modes: { single: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var elements = []; parseVisibleItems(chart, function(element) { @@ -166,7 +183,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ dataset: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false); if (items.length > 0) { @@ -193,7 +210,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ point: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); return getIntersectItems(chart, position); }, @@ -206,7 +223,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ nearest: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var nearestItems = getNearestItems(chart, position, options.intersect); // We have multiple items at the same distance from the event. Now sort by smallest @@ -238,7 +255,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ x: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; @@ -269,7 +286,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ y: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index 220a2534484..39cf26b9006 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -148,15 +148,36 @@ module.exports = function(Chart) { minBoxSizes.push({ horizontal: isHorizontal, minSize: minSize, - box: box + box: box, }); } helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); + // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) + var maxHorizontalLeftPadding = 0; + var maxHorizontalRightPadding = 0; + var maxVerticalTopPadding = 0; + var maxVerticalBottomPadding = 0; + + helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { + if (horizontalBox.getPadding) { + var boxPadding = horizontalBox.getPadding(); + maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); + maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); + } + }); + + helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { + if (verticalBox.getPadding) { + var boxPadding = verticalBox.getPadding(); + maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); + maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); + } + }); + // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could // be if the axes are drawn at their minimum sizes. - // Steps 5 & 6 var totalLeftBoxesWidth = leftPadding; var totalRightBoxesWidth = rightPadding; @@ -172,8 +193,8 @@ module.exports = function(Chart) { if (minBoxSize) { if (box.isHorizontal()) { var scaleMargin = { - left: totalLeftBoxesWidth, - right: totalRightBoxesWidth, + left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), + right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), top: 0, bottom: 0 }; @@ -251,6 +272,15 @@ module.exports = function(Chart) { totalBottomBoxesHeight += box.height; }); + // We may be adding some padding to account for rotated x axis labels + var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); + totalLeftBoxesWidth += leftPaddingAddition; + totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); + + var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); + totalTopBoxesHeight += topPaddingAddition; + totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); + // Figure out if our chart area changed. This would occur if the dataset layout label rotation // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do // without calling `fit` again @@ -283,8 +313,8 @@ module.exports = function(Chart) { } // Step 7 - Position the boxes - var left = leftPadding; - var top = topPadding; + var left = leftPadding + leftPaddingAddition; + var top = topPadding + topPaddingAddition; function placeBox(box) { if (box.isHorizontal()) { diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 9ea5787db9d..4816b285f2e 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -162,10 +162,20 @@ module.exports = function(Chart) { beforeBuildLabels: noop, buildLabels: function() { var me = this; - me.legendItems = me.options.labels.generateLabels.call(me, me.chart); + var labelOpts = me.options.labels; + var legendItems = labelOpts.generateLabels.call(me, me.chart); + + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } + if (me.options.reverse) { - me.legendItems.reverse(); + legendItems.reverse(); } + + me.legendItems = legendItems; }, afterBuildLabels: noop, @@ -404,7 +414,7 @@ module.exports = function(Chart) { } } else if (y + itemHeight > me.bottom) { x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top; + y = cursor.y = me.top + labelOpts.padding; cursor.line++; } @@ -429,7 +439,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param {IEvent} event - The event to handle * @return {Boolean} true if a change occured */ handleEvent: function(e) { @@ -450,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 @@ -463,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; } @@ -479,20 +491,45 @@ module.exports = function(Chart) { } }); + function createNewLegendAndAttach(chartInstance, legendOpts) { + var legend = new Chart.Legend({ + ctx: chartInstance.chart.ctx, + options: legendOpts, + chart: chartInstance + }); + chartInstance.legend = legend; + Chart.layoutService.addBox(chartInstance, legend); + } + // Register the legend plugin Chart.plugins.register({ beforeInit: function(chartInstance) { - var opts = chartInstance.options; - var legendOpts = opts.legend; + var legendOpts = chartInstance.options.legend; if (legendOpts) { - chartInstance.legend = new Chart.Legend({ - ctx: chartInstance.chart.ctx, - options: legendOpts, - chart: chartInstance - }); + createNewLegendAndAttach(chartInstance, legendOpts); + } + }, + beforeUpdate: function(chartInstance) { + var legendOpts = chartInstance.options.legend; + + if (legendOpts) { + legendOpts = helpers.configMerge(Chart.defaults.global.legend, legendOpts); - Chart.layoutService.addBox(chartInstance, chartInstance.legend); + if (chartInstance.legend) { + chartInstance.legend.options = legendOpts; + } else { + createNewLegendAndAttach(chartInstance, legendOpts); + } + } else { + Chart.layoutService.removeBox(chartInstance, chartInstance.legend); + delete chartInstance.legend; + } + }, + afterEvent: function(chartInstance, e) { + var legend = chartInstance.legend; + if (legend) { + legend.handleEvent(e); } } }); diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index fe6ac31f170..bda2ff45739 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -2,7 +2,9 @@ module.exports = function(Chart) { - var noop = Chart.helpers.noop; + var helpers = Chart.helpers; + + Chart.defaults.global.plugins = {}; /** * The plugin service singleton @@ -10,8 +12,20 @@ module.exports = function(Chart) { * @since 2.1.0 */ Chart.plugins = { + /** + * Globally registered plugins. + * @private + */ _plugins: [], + /** + * This identifier is used to invalidate the descriptors cache attached to each chart + * when a global plugin is registered or unregistered. In this case, the cache ID is + * incremented and descriptors are regenerated during following API calls. + * @private + */ + _cacheId: 0, + /** * Registers the given plugin(s) if not already registered. * @param {Array|Object} plugins plugin instance(s). @@ -23,6 +37,8 @@ module.exports = function(Chart) { p.push(plugin); } }); + + this._cacheId++; }, /** @@ -37,6 +53,8 @@ module.exports = function(Chart) { p.splice(idx, 1); } }); + + this._cacheId++; }, /** @@ -45,6 +63,7 @@ module.exports = function(Chart) { */ clear: function() { this._plugins = []; + this._cacheId++; }, /** @@ -66,64 +85,241 @@ module.exports = function(Chart) { }, /** - * Calls registered plugins on the specified extension, with the given args. This - * method immediately returns as soon as a plugin explicitly returns false. The + * Calls enabled plugins for `chart` on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The * returned value can be used, for instance, to interrupt the current action. - * @param {String} extension the name of the plugin method to call (e.g. 'beforeUpdate'). - * @param {Array} [args] extra arguments to apply to the extension call. + * @param {Object} chart - The chart instance for which plugins should be called. + * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] - Extra arguments to apply to the hook call. * @returns {Boolean} false if any of the plugins return false, else returns true. */ - notify: function(extension, args) { - var plugins = this._plugins; - var ilen = plugins.length; - var i, plugin; + notify: function(chart, hook, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; for (i=0; i tickWidth && labelRotation < tickOpts.maxRotation) { + var angleRadians = helpers.toRadians(labelRotation); + cosRotation = Math.cos(angleRadians); + sinRotation = Math.sin(angleRadians); + + if (sinRotation * originalLabelWidth > me.maxHeight) { + // go back one step + labelRotation--; + break; } - var originalLabelWidth = helpers.longestText(context, tickLabelFont, me.ticks, me.longestTextCache); - var labelWidth = originalLabelWidth; - var cosRotation; - var sinRotation; - - // Allow 3 pixels x2 padding either side for label readability - // only the index matters for a dataset scale, but we want a consistent interface between scales - var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; - - // Max label rotation can be set or default to 90 - also act as a loop counter - while (labelWidth > tickWidth && me.labelRotation < optionTicks.maxRotation) { - cosRotation = Math.cos(helpers.toRadians(me.labelRotation)); - sinRotation = Math.sin(helpers.toRadians(me.labelRotation)); - - firstRotated = cosRotation * firstWidth; - - // We're right aligning the text now. - if (firstRotated + tickFontSize / 2 > me.yLabelWidth) { - me.paddingLeft = firstRotated + tickFontSize / 2; - } - - me.paddingRight = tickFontSize / 2; - - if (sinRotation * originalLabelWidth > me.maxHeight) { - // go back one step - me.labelRotation--; - break; - } - me.labelRotation++; - labelWidth = cosRotation * originalLabelWidth; - } + labelRotation++; + labelWidth = cosRotation * originalLabelWidth; } } - if (me.margins) { - me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); - me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); - } + me.labelRotation = labelRotation; }, afterCalculateTickRotation: function() { helpers.callCallback(this.options.afterCalculateTickRotation, [this]); @@ -267,20 +274,14 @@ module.exports = function(Chart) { }; var opts = me.options; - var globalDefaults = Chart.defaults.global; var tickOpts = opts.ticks; var scaleLabelOpts = opts.scaleLabel; var gridLineOpts = opts.gridLines; var display = opts.display; var isHorizontal = me.isHorizontal(); - var tickFontSize = helpers.getValueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(tickOpts.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(tickOpts.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - - var scaleLabelFontSize = helpers.getValueOrDefault(scaleLabelOpts.fontSize, globalDefaults.defaultFontSize); - + var tickFont = parseFontOptions(tickOpts); + var scaleLabelFontSize = parseFontOptions(scaleLabelOpts).size * 1.5; var tickMarkLength = opts.gridLines.tickMarkLength; // Width @@ -301,78 +302,84 @@ module.exports = function(Chart) { // Are we showing a title for the scale? if (scaleLabelOpts.display && display) { if (isHorizontal) { - minSize.height += (scaleLabelFontSize * 1.5); + minSize.height += scaleLabelFontSize; } else { - minSize.width += (scaleLabelFontSize * 1.5); + minSize.width += scaleLabelFontSize; } } + // Don't bother fitting the ticks if we are not showing them if (tickOpts.display && display) { - // Don't bother fitting the ticks if we are not showing them - if (!me.longestTextCache) { - me.longestTextCache = {}; - } - - var largestTextWidth = helpers.longestText(me.ctx, tickLabelFont, me.ticks, me.longestTextCache); + var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, me.ticks, me.longestTextCache); var tallestLabelHeightInLines = helpers.numberOfLabelLines(me.ticks); - var lineSpace = tickFontSize * 0.5; + var lineSpace = tickFont.size * 0.5; if (isHorizontal) { // A horizontal axis is more constrained by the height. me.longestLabelWidth = largestTextWidth; + var angleRadians = helpers.toRadians(me.labelRotation); + var cosRotation = Math.cos(angleRadians); + var sinRotation = Math.sin(angleRadians); + // TODO - improve this calculation - var labelHeight = (Math.sin(helpers.toRadians(me.labelRotation)) * me.longestLabelWidth) + (tickFontSize * tallestLabelHeightInLines) + (lineSpace * tallestLabelHeightInLines); + var labelHeight = (sinRotation * largestTextWidth) + + (tickFont.size * tallestLabelHeightInLines) + + (lineSpace * tallestLabelHeightInLines); minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight); - me.ctx.font = tickLabelFont; + me.ctx.font = tickFont.font; + + var firstTick = me.ticks[0]; + var firstLabelWidth = computeTextSize(me.ctx, firstTick, tickFont.font); - var firstLabelWidth = me.ctx.measureText(me.ticks[0]).width; - var lastLabelWidth = me.ctx.measureText(me.ticks[me.ticks.length - 1]).width; + var lastTick = me.ticks[me.ticks.length - 1]; + var lastLabelWidth = computeTextSize(me.ctx, lastTick, tickFont.font); // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated // by the font height - var cosRotation = Math.cos(helpers.toRadians(me.labelRotation)); - var sinRotation = Math.sin(helpers.toRadians(me.labelRotation)); - me.paddingLeft = me.labelRotation !== 0 ? (cosRotation * firstLabelWidth) + 3 : firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - me.paddingRight = me.labelRotation !== 0 ? (sinRotation * (tickFontSize / 2)) + 3 : lastLabelWidth / 2 + 3; // when rotated + if (me.labelRotation !== 0) { + me.paddingLeft = opts.position === 'bottom'? (cosRotation * firstLabelWidth) + 3: (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges + me.paddingRight = opts.position === 'bottom'? (cosRotation * lineSpace) + 3: (cosRotation * lastLabelWidth) + 3; + } else { + me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges + me.paddingRight = lastLabelWidth / 2 + 3; + } } else { // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first - var maxLabelWidth = me.maxWidth - minSize.width; - // Account for padding - var mirror = tickOpts.mirror; - if (!mirror) { - largestTextWidth += me.options.ticks.padding; - } else { - // If mirrored text is on the inside so don't expand - largestTextWidth = 0; - } - if (largestTextWidth < maxLabelWidth) { - // We don't need all the room - minSize.width += largestTextWidth; + if (tickOpts.mirror) { + largestTextWidth = 0; } else { - // Expand to max size - minSize.width = me.maxWidth; + largestTextWidth += me.options.ticks.padding; } - - me.paddingTop = tickFontSize / 2; - me.paddingBottom = tickFontSize / 2; + minSize.width += largestTextWidth; + me.paddingTop = tickFont.size / 2; + me.paddingBottom = tickFont.size / 2; } } + me.handleMargins(); + + me.width = minSize.width; + me.height = minSize.height; + }, + + /** + * Handle margins and padding interactions + * @private + */ + handleMargins: function() { + var me = this; if (me.margins) { me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); } - - me.width = minSize.width; - me.height = minSize.height; - }, + afterFit: function() { helpers.callCallback(this.options.afterFit, [this]); }, @@ -452,15 +459,18 @@ module.exports = function(Chart) { }, getBasePixel: function() { + return this.getPixelForValue(this.getBaseValue()); + }, + + getBaseValue: function() { var me = this; var min = me.min; var max = me.max; - return me.getPixelForValue( - me.beginAtZero? 0: + return me.beginAtZero ? 0: min < 0 && max < 0? max : min > 0 && max > 0? min : - 0); + 0; }, // Actually draw the scale on the canvas @@ -490,19 +500,14 @@ module.exports = function(Chart) { } var tickFontColor = helpers.getValueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); - var tickFontSize = helpers.getValueOrDefault(optionTicks.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(optionTicks.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(optionTicks.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - var tl = gridLines.tickMarkLength; + var tickFont = parseFontOptions(optionTicks); + + var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; var borderDash = helpers.getValueOrDefault(gridLines.borderDash, globalDefaults.borderDash); var borderDashOffset = helpers.getValueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); var scaleLabelFontColor = helpers.getValueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); - var scaleLabelFontSize = helpers.getValueOrDefault(scaleLabel.fontSize, globalDefaults.defaultFontSize); - var scaleLabelFontStyle = helpers.getValueOrDefault(scaleLabel.fontStyle, globalDefaults.defaultFontStyle); - var scaleLabelFontFamily = helpers.getValueOrDefault(scaleLabel.fontFamily, globalDefaults.defaultFontFamily); - var scaleLabelFont = helpers.fontString(scaleLabelFontSize, scaleLabelFontStyle, scaleLabelFontFamily); + var scaleLabelFont = parseFontOptions(scaleLabel); var labelRotationRadians = helpers.toRadians(me.labelRotation); var cosRotation = Math.cos(labelRotationRadians); @@ -578,15 +583,21 @@ module.exports = function(Chart) { var textBaseline = 'middle'; if (isHorizontal) { - if (!isRotated) { - textBaseline = options.position === 'top' ? 'bottom' : 'top'; - } - textAlign = isRotated ? 'right' : 'center'; + if (options.position === 'bottom') { + // bottom + textBaseline = !isRotated? 'top':'middle'; + textAlign = !isRotated? 'center': 'right'; + labelY = me.top + tl; + } else { + // top + textBaseline = !isRotated? 'bottom':'middle'; + textAlign = !isRotated? 'center': 'left'; + labelY = me.bottom - tl; + } var xLineValue = me.getPixelForTick(index) + helpers.aliasPixel(lineWidth); // xvalues for grid lines labelX = me.getPixelForTick(index, gridLines.offsetGridLines) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - labelY = (isRotated) ? me.top + 12 : options.position === 'top' ? me.bottom - tl : me.top + tl; tx1 = tx2 = x1 = x2 = xLineValue; ty1 = yTickStart; @@ -594,23 +605,20 @@ module.exports = function(Chart) { y1 = chartArea.top; y2 = chartArea.bottom; } else { - if (options.position === 'left') { - if (optionTicks.mirror) { - labelX = me.right + optionTicks.padding; - textAlign = 'left'; - } else { - labelX = me.right - optionTicks.padding; - textAlign = 'right'; - } - // right side - } else if (optionTicks.mirror) { - labelX = me.left - optionTicks.padding; - textAlign = 'right'; + var isLeft = options.position === 'left'; + var tickPadding = optionTicks.padding; + var labelXOffset; + + if (optionTicks.mirror) { + textAlign = isLeft ? 'left' : 'right'; + labelXOffset = tickPadding; } else { - labelX = me.left + optionTicks.padding; - textAlign = 'left'; + textAlign = isLeft ? 'right' : 'left'; + labelXOffset = tl + tickPadding; } + labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; + var yLineValue = me.getPixelForTick(index); // xvalues for grid lines yLineValue += helpers.aliasPixel(lineWidth); labelY = me.getPixelForTick(index, gridLines.offsetGridLines); @@ -675,17 +683,17 @@ module.exports = function(Chart) { context.save(); context.translate(itemToDraw.labelX, itemToDraw.labelY); context.rotate(itemToDraw.rotation); - context.font = tickLabelFont; + context.font = tickFont.font; context.textBaseline = itemToDraw.textBaseline; context.textAlign = itemToDraw.textAlign; var label = itemToDraw.label; if (helpers.isArray(label)) { - for (var i = 0, y = -(label.length - 1)*tickFontSize*0.75; i < label.length; ++i) { + for (var i = 0, y = 0; i < label.length; ++i) { // We just make sure the multiline element is a string here.. context.fillText('' + label[i], 0, y); // apply same lineSpacing as calculated @ L#320 - y += (tickFontSize * 1.5); + y += (tickFont.size * 1.5); } } else { context.fillText(label, 0, 0); @@ -702,10 +710,10 @@ module.exports = function(Chart) { if (isHorizontal) { scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFontSize / 2) : me.top + (scaleLabelFontSize / 2); + scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFont.size / 2) : me.top + (scaleLabelFont.size / 2); } else { var isLeft = options.position === 'left'; - scaleLabelX = isLeft ? me.left + (scaleLabelFontSize / 2) : me.right - (scaleLabelFontSize / 2); + scaleLabelX = isLeft ? me.left + (scaleLabelFont.size / 2) : me.right - (scaleLabelFont.size / 2); scaleLabelY = me.top + ((me.bottom - me.top) / 2); rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; } @@ -716,7 +724,7 @@ module.exports = function(Chart) { context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillStyle = scaleLabelFontColor; // render in correct colour - context.font = scaleLabelFont; + context.font = scaleLabelFont.font; context.fillText(scaleLabel.labelString, 0, 0); context.restore(); } diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index f11f1192bdf..8d84f1f236a 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -67,8 +67,8 @@ module.exports = function(Chart) { // If min, max and stepSize is set and they make an evenly spaced scale use it. if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - var minMaxDeltaDivisibleByStepSize = ((generationOptions.max - generationOptions.min) % generationOptions.stepSize) === 0; - if (minMaxDeltaDivisibleByStepSize) { + // If very close to our whole number, use it. + if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { niceMin = generationOptions.min; niceMax = generationOptions.max; } @@ -109,27 +109,33 @@ module.exports = function(Chart) { // the graph var tickVal = getValueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); - while (tickVal < dataRange.max) { - ticks.push(tickVal); + var endExp = Math.floor(helpers.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp; + var significand; - var exp; - var significand; + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); - if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); - significand = Math.round(dataRange.minNotZero / Math.pow(10, exp)); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; - } + ticks.push(tickVal); + tickVal = significand * Math.pow(10, exp); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)); + } + + do { + ticks.push(tickVal); + ++significand; if (significand === 10) { significand = 1; ++exp; } tickVal = significand * Math.pow(10, exp); - } + } while (exp < endExp || (exp === endExp && significand < endSignificand)); var lastTick = getValueOrDefault(generationOptions.max, tickVal); ticks.push(lastTick); diff --git a/src/core/core.title.js b/src/core/core.title.js index a7663258a13..5b2d989f8a7 100644 --- a/src/core/core.title.js +++ b/src/core/core.title.js @@ -22,7 +22,6 @@ module.exports = function(Chart) { initialize: function(config) { var me = this; helpers.extend(me, config); - me.options = helpers.configMerge(Chart.defaults.global.title, config.options); // Contains hit boxes for each dataset (in dataset order) me.legendHitBoxes = []; @@ -30,12 +29,7 @@ module.exports = function(Chart) { // These methods are ordered by lifecycle. Utilities then follow. - beforeUpdate: function() { - var chartOpts = this.chart.options; - if (chartOpts && chartOpts.title) { - this.options = helpers.configMerge(Chart.defaults.global.title, chartOpts.title); - } - }, + beforeUpdate: noop, update: function(maxWidth, maxHeight, margins) { var me = this; @@ -187,20 +181,39 @@ module.exports = function(Chart) { } }); + function createNewTitleBlockAndAttach(chartInstance, titleOpts) { + var title = new Chart.Title({ + ctx: chartInstance.chart.ctx, + options: titleOpts, + chart: chartInstance + }); + chartInstance.titleBlock = title; + Chart.layoutService.addBox(chartInstance, title); + } + // Register the title plugin Chart.plugins.register({ beforeInit: function(chartInstance) { - var opts = chartInstance.options; - var titleOpts = opts.title; + var titleOpts = chartInstance.options.title; + + if (titleOpts) { + createNewTitleBlockAndAttach(chartInstance, titleOpts); + } + }, + beforeUpdate: function(chartInstance) { + var titleOpts = chartInstance.options.title; if (titleOpts) { - chartInstance.titleBlock = new Chart.Title({ - ctx: chartInstance.chart.ctx, - options: titleOpts, - chart: chartInstance - }); + titleOpts = helpers.configMerge(Chart.defaults.global.title, titleOpts); - Chart.layoutService.addBox(chartInstance, chartInstance.titleBlock); + if (chartInstance.titleBlock) { + chartInstance.titleBlock.options = titleOpts; + } else { + createNewTitleBlockAndAttach(chartInstance, titleOpts); + } + } else { + Chart.layoutService.removeBox(chartInstance, chartInstance.titleBlock); + delete chartInstance.titleBlock; } } }); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index d99f2302ff5..32589b652e6 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -763,7 +763,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param {IEvent} 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/elements/element.point.js b/src/elements/element.point.js index fe3a6696bd1..c68edc7861b 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -56,13 +56,17 @@ module.exports = function(Chart) { padding: vm.radius + vm.borderWidth }; }, - draw: function() { + draw: function(chartArea) { var vm = this._view; + var model = this._model; var ctx = this._chart.ctx; var pointStyle = vm.pointStyle; var radius = vm.radius; var x = vm.x; var y = vm.y; + var color = Chart.helpers.color; + var errMargin = 1.01; // 1.01 is margin for Accumulated error. (Especially Edge, IE.) + var ratio = 0; if (vm.skip) { return; @@ -72,6 +76,24 @@ module.exports = function(Chart) { ctx.lineWidth = helpers.getValueOrDefault(vm.borderWidth, globalOpts.elements.point.borderWidth); ctx.fillStyle = vm.backgroundColor || defaultColor; + // Cliping for Points. + // going out from inner charArea? + if ((chartArea !== undefined) && ((model.x < chartArea.left) || (chartArea.right*errMargin < model.x) || (model.y < chartArea.top) || (chartArea.bottom*errMargin < model.y))) { + // Point fade out + if (model.x < chartArea.left) { + ratio = (x - model.x) / (chartArea.left - model.x); + } else if (chartArea.right*errMargin < model.x) { + ratio = (model.x - x) / (model.x - chartArea.right); + } else if (model.y < chartArea.top) { + ratio = (y - model.y) / (chartArea.top - model.y); + } else if (chartArea.bottom*errMargin < model.y) { + ratio = (model.y - y) / (model.y - chartArea.bottom); + } + ratio = Math.round(ratio*100) / 100; + ctx.strokeStyle = color(ctx.strokeStyle).alpha(ratio).rgbString(); + ctx.fillStyle = color(ctx.fillStyle).alpha(ratio).rgbString(); + } + Chart.canvasHelpers.drawPoint(ctx, pointStyle, radius, x, y); } }); diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index 427916791ed..c3b81976140 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -53,39 +53,71 @@ module.exports = function(Chart) { draw: function() { var ctx = this._chart.ctx; var vm = this._view; - - var halfWidth = vm.width / 2, - leftX = vm.x - halfWidth, - rightX = vm.x + halfWidth, - top = vm.base - (vm.base - vm.y), - halfStroke = vm.borderWidth / 2; + var left, right, top, bottom, signX, signY, borderSkipped; + var borderWidth = vm.borderWidth; + + if (!vm.horizontal) { + // bar + left = vm.x - vm.width / 2; + right = vm.x + vm.width / 2; + top = vm.y; + bottom = vm.base; + signX = 1; + signY = bottom > top? 1: -1; + borderSkipped = vm.borderSkipped || 'bottom'; + } else { + // horizontal bar + left = vm.base; + right = vm.x; + top = vm.y - vm.height / 2; + bottom = vm.y + vm.height / 2; + signX = right > left? 1: -1; + signY = 1; + borderSkipped = vm.borderSkipped || 'left'; + } // Canvas doesn't allow us to stroke inside the width so we can // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; + if (borderWidth) { + // borderWidth shold be less than bar width and bar height. + var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom)); + borderWidth = borderWidth > barSize? barSize: borderWidth; + var halfStroke = borderWidth / 2; + // Adjust borderWidth when bar top position is near vm.base(zero). + var borderLeft = left + (borderSkipped !== 'left'? halfStroke * signX: 0); + var borderRight = right + (borderSkipped !== 'right'? -halfStroke * signX: 0); + var borderTop = top + (borderSkipped !== 'top'? halfStroke * signY: 0); + var borderBottom = bottom + (borderSkipped !== 'bottom'? -halfStroke * signY: 0); + // not become a vertical line? + if (borderLeft !== borderRight) { + top = borderTop; + bottom = borderBottom; + } + // not become a horizontal line? + if (borderTop !== borderBottom) { + left = borderLeft; + right = borderRight; + } } ctx.beginPath(); ctx.fillStyle = vm.backgroundColor; ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; + ctx.lineWidth = borderWidth; // Corner points, from bottom-left to bottom-right clockwise // | 1 2 | // | 0 3 | var corners = [ - [leftX, vm.base], - [leftX, top], - [rightX, top], - [rightX, vm.base] + [left, bottom], + [left, top], + [right, top], + [right, bottom] ]; // Find first (starting) corner with fallback to 'bottom' var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); + var startCorner = borders.indexOf(borderSkipped, 0); if (startCorner === -1) { startCorner = 0; } @@ -104,7 +136,7 @@ module.exports = function(Chart) { } ctx.fill(); - if (vm.borderWidth) { + if (borderWidth) { ctx.stroke(); } }, diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js new file mode 100644 index 00000000000..abfb3dee3e1 --- /dev/null +++ b/src/platforms/platform.dom.js @@ -0,0 +1,277 @@ +'use strict'; + +// Chart.Platform implementation for targeting a web browser +module.exports = function(Chart) { + var helpers = Chart.helpers; + + // DOM event types -> Chart.js event types. + // Note: only events with different types are mapped. + // https://developer.mozilla.org/en-US/docs/Web/Events + var eventTypeMap = { + // Touch events + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + + // Pointer events + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout' + }; + + /** + * 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; + } + + function createEvent(type, chart, x, y, native) { + return { + type: type, + chart: chart, + native: native || null, + x: x !== undefined? x : null, + y: y !== undefined? y : null, + }; + } + + function fromNativeEvent(event, chart) { + var type = eventTypeMap[event.type] || event.type; + var pos = helpers.getRelativePosition(event, chart); + return createEvent(type, chart, pos.x, pos.y, event); + } + + function createResizer(handler) { + var iframe = document.createElement('iframe'); + iframe.className = 'chartjs-hidden-iframe'; + iframe.style.cssText = + 'display:block;'+ + 'overflow:hidden;'+ + 'border:0;'+ + 'margin:0;'+ + 'top:0;'+ + 'left:0;'+ + 'bottom:0;'+ + 'right:0;'+ + 'height:100%;'+ + 'width:100%;'+ + 'position:absolute;'+ + 'pointer-events:none;'+ + 'z-index:-1;'; + + // Prevent the iframe to gain focus on tab. + // https://github.com/chartjs/Chart.js/issues/3090 + iframe.tabIndex = -1; + + // If the iframe is re-attached to the DOM, the resize listener is removed because the + // content is reloaded, so make sure to install the handler after the iframe is loaded. + // https://github.com/chartjs/Chart.js/issues/3521 + helpers.addEvent(iframe, 'load', function() { + helpers.addEvent(iframe.contentWindow || iframe, 'resize', handler); + + // The iframe size might have changed while loading, which can also + // happen if the size has been changed while detached from the DOM. + handler(); + }); + + return iframe; + } + + function addResizeListener(node, listener, chart) { + var stub = node._chartjs = { + ticking: false + }; + + // Throttle the callback notification until the next animation frame. + var notify = function() { + if (!stub.ticking) { + stub.ticking = true; + helpers.requestAnimFrame.call(window, function() { + if (stub.resizer) { + stub.ticking = false; + return listener(createEvent('resize', chart)); + } + }); + } + }; + + // Let's keep track of this added iframe and thus avoid DOM query when removing it. + stub.resizer = createResizer(notify); + + node.insertBefore(stub.resizer, node.firstChild); + } + + function removeResizeListener(node) { + if (!node || !node._chartjs) { + return; + } + + var resizer = node._chartjs.resizer; + if (resizer) { + resizer.parentNode.removeChild(resizer); + node._chartjs.resizer = null; + } + + delete node._chartjs; + } + + return { + 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; + }, + + 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; + }, + + addEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + addResizeListener(canvas.parentNode, listener, chart.chart); + return; + } + + var stub = listener._chartjs || (listener._chartjs = {}); + var proxies = stub.proxies || (stub.proxies = {}); + var proxy = proxies[chart.id + '_' + type] = function(event) { + listener(fromNativeEvent(event, chart.chart)); + }; + + helpers.addEvent(canvas, type, proxy); + }, + + removeEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + removeResizeListener(canvas.parentNode, listener); + return; + } + + var stub = listener._chartjs || {}; + var proxies = stub.proxies || {}; + var proxy = proxies[chart.id + '_' + type]; + if (!proxy) { + return; + } + + helpers.removeEvent(canvas, type, proxy); + } + }; +}; diff --git a/src/platforms/platform.js b/src/platforms/platform.js new file mode 100644 index 00000000000..0f27e5868a6 --- /dev/null +++ b/src/platforms/platform.js @@ -0,0 +1,69 @@ +'use strict'; + +// By default, select the browser (DOM) platform. +// @TODO Make possible to select another platform at build time. +var implementation = require('./platform.dom.js'); + +module.exports = function(Chart) { + /** + * @namespace Chart.platform + * @see https://chartjs.gitbooks.io/proposals/content/Platform.html + * @since 2.4.0 + */ + Chart.platform = { + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {*} item - The native item from which to acquire context (platform specific) + * @param {Object} options - The chart options + * @returns {CanvasRenderingContext2D} context2d instance + */ + acquireContext: function() {}, + + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {Boolean} true if the method succeeded, else false + */ + releaseContext: function() {}, + + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {String} type - The ({@link IEvent}) type to listen for + * @param {Function} listener - Receives a notification (an object that implements + * the {@link IEvent} interface) when an event of the specified type occurs. + */ + addEventListener: function() {}, + + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart -Chart from which to remove the listener + * @param {String} type - The ({@link IEvent}) type to remove + * @param {Function} listener - The listener function to remove from the event target. + */ + removeEventListener: function() {} + }; + + /** + * @interface IPlatform + * Allows abstracting platform dependencies away from the chart + * @borrows Chart.platform.acquireContext as acquireContext + * @borrows Chart.platform.releaseContext as releaseContext + * @borrows Chart.platform.addEventListener as addEventListener + * @borrows Chart.platform.removeEventListener as removeEventListener + */ + + /** + * @interface IEvent + * @prop {String} type - The event type name, possible values are: + * 'contextmenu', 'mouseenter', 'mousedown', 'mousemove', 'mouseup', 'mouseout', + * 'click', 'dblclick', 'keydown', 'keypress', 'keyup' and 'resize' + * @prop {*} native - The original native event (null for emulated events, e.g. 'resize') + * @prop {Number} x - The mouse x position, relative to the canvas (null for incompatible events) + * @prop {Number} y - The mouse y position, relative to the canvas (null for incompatible events) + */ + + Chart.helpers.extend(Chart.platform, implementation(Chart)); +}; diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index fb90e6333f1..3cee4ebf71f 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -54,10 +54,10 @@ module.exports = function(Chart) { var data = me.chart.data; var isHorizontal = me.isHorizontal(); - if ((data.xLabels && isHorizontal) || (data.yLabels && !isHorizontal)) { + if (data.yLabels && !isHorizontal) { return me.getRightValue(data.datasets[datasetIndex].data[index]); } - return me.ticks[index]; + return me.ticks[index - me.minIndex]; }, // Used to get data value locations. Value can either be an index or a numerical value @@ -73,9 +73,8 @@ module.exports = function(Chart) { } if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueWidth = innerWidth / offsetAmt; - var widthOffset = (valueWidth * (index - me.minIndex)) + me.paddingLeft; + var valueWidth = me.width / offsetAmt; + var widthOffset = (valueWidth * (index - me.minIndex)); if (me.options.gridLines.offsetGridLines && includeOffset || me.maxIndex === me.minIndex && includeOffset) { widthOffset += (valueWidth / 2); @@ -83,9 +82,8 @@ module.exports = function(Chart) { return me.left + Math.round(widthOffset); } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - var valueHeight = innerHeight / offsetAmt; - var heightOffset = (valueHeight * (index - me.minIndex)) + me.paddingTop; + var valueHeight = me.height / offsetAmt; + var heightOffset = (valueHeight * (index - me.minIndex)); if (me.options.gridLines.offsetGridLines && includeOffset) { heightOffset += (valueHeight / 2); @@ -101,15 +99,13 @@ module.exports = function(Chart) { var value; var offsetAmt = Math.max((me.ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); var horz = me.isHorizontal(); - var innerDimension = horz ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); - var valueDimension = innerDimension / offsetAmt; + var valueDimension = (horz ? me.width : me.height) / offsetAmt; pixel -= horz ? me.left : me.top; if (me.options.gridLines.offsetGridLines) { pixel -= (valueDimension / 2); } - pixel -= horz ? me.paddingLeft : me.paddingTop; if (pixel <= 0) { value = 0; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index e8afa84f8f1..319243b23e5 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -28,21 +28,43 @@ module.exports = function(Chart) { me.min = null; me.max = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = { + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = { positiveValues: [], negativeValues: [] }; } // Store these per type - var positiveValues = valuesPerType[meta.type].positiveValues; - var negativeValues = valuesPerType[meta.type].negativeValues; + var positiveValues = valuesPerStack[key].positiveValues; + var negativeValues = valuesPerStack[key].negativeValues; if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { @@ -65,7 +87,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + helpers.each(valuesPerStack, function(valuesForType) { var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); var minVal = helpers.min(values); var maxVal = helpers.max(values); @@ -132,31 +154,25 @@ module.exports = function(Chart) { // This must be called after fit has been run so that // this.left, this.top, this.right, and this.bottom have been defined var me = this; - var paddingLeft = me.paddingLeft; - var paddingBottom = me.paddingBottom; var start = me.start; var rightValue = +me.getRightValue(value); var pixel; - var innerDimension; var range = me.end - start; if (me.isHorizontal()) { - innerDimension = me.width - (paddingLeft + me.paddingRight); - pixel = me.left + (innerDimension / range * (rightValue - start)); - return Math.round(pixel + paddingLeft); + pixel = me.left + (me.width / range * (rightValue - start)); + return Math.round(pixel); } - innerDimension = me.height - (me.paddingTop + paddingBottom); - pixel = (me.bottom - paddingBottom) - (innerDimension / range * (rightValue - start)); + + pixel = me.bottom - (me.height / range * (rightValue - start)); return Math.round(pixel); }, getValueForPixel: function(pixel) { var me = this; var isHorizontal = me.isHorizontal(); - var paddingLeft = me.paddingLeft; - var paddingBottom = me.paddingBottom; - var innerDimension = isHorizontal ? me.width - (paddingLeft + me.paddingRight) : me.height - (me.paddingTop + paddingBottom); - var offset = (isHorizontal ? pixel - me.left - paddingLeft : me.bottom - paddingBottom - pixel) / innerDimension; + var innerDimension = isHorizontal ? me.width : me.height; + var offset = (isHorizontal ? pixel - me.left : me.bottom - pixel) / innerDimension; return me.start + ((me.end - me.start) * offset); }, getPixelForTick: function(index) { diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 9d87459406a..5d1d329f9ab 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -32,18 +32,40 @@ module.exports = function(Chart) { me.max = null; me.minNotZero = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = []; + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = []; } helpers.each(dataset.data, function(rawValue, index) { - var values = valuesPerType[meta.type]; + var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); if (isNaN(value) || meta.data[index].hidden) { return; @@ -61,7 +83,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + helpers.each(valuesPerStack, function(valuesForType) { var minVal = helpers.min(valuesForType); var maxVal = helpers.max(valuesForType); me.min = me.min === null ? minVal : Math.min(me.min, minVal); @@ -162,46 +184,42 @@ module.exports = function(Chart) { var start = me.start; var newVal = +me.getRightValue(value); var range; - var paddingTop = me.paddingTop; - var paddingBottom = me.paddingBottom; - var paddingLeft = me.paddingLeft; var opts = me.options; var tickOpts = opts.ticks; if (me.isHorizontal()) { range = helpers.log10(me.end) - helpers.log10(start); // todo: if start === 0 if (newVal === 0) { - pixel = me.left + paddingLeft; + pixel = me.left; } else { - innerDimension = me.width - (paddingLeft + me.paddingRight); + innerDimension = me.width; pixel = me.left + (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); - pixel += paddingLeft; } } else { // Bottom - top since pixels increase downward on a screen - innerDimension = me.height - (paddingTop + paddingBottom); + innerDimension = me.height; if (start === 0 && !tickOpts.reverse) { range = helpers.log10(me.end) - helpers.log10(me.minNotZero); if (newVal === start) { - pixel = me.bottom - paddingBottom; + pixel = me.bottom; } else if (newVal === me.minNotZero) { - pixel = me.bottom - paddingBottom - innerDimension * 0.02; + pixel = me.bottom - innerDimension * 0.02; } else { - pixel = me.bottom - paddingBottom - innerDimension * 0.02 - (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + pixel = me.bottom - innerDimension * 0.02 - (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); } } else if (me.end === 0 && tickOpts.reverse) { range = helpers.log10(me.start) - helpers.log10(me.minNotZero); if (newVal === me.end) { - pixel = me.top + paddingTop; + pixel = me.top; } else if (newVal === me.minNotZero) { - pixel = me.top + paddingTop + innerDimension * 0.02; + pixel = me.top + innerDimension * 0.02; } else { - pixel = me.top + paddingTop + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + pixel = me.top + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); } } else { range = helpers.log10(me.end) - helpers.log10(start); - innerDimension = me.height - (paddingTop + paddingBottom); - pixel = (me.bottom - paddingBottom) - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); + innerDimension = me.height; + pixel = me.bottom - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); } } return pixel; @@ -212,11 +230,11 @@ module.exports = function(Chart) { var value, innerDimension; if (me.isHorizontal()) { - innerDimension = me.width - (me.paddingLeft + me.paddingRight); - value = me.start * Math.pow(10, (pixel - me.left - me.paddingLeft) * range / innerDimension); + innerDimension = me.width; + value = me.start * Math.pow(10, (pixel - me.left) * range / innerDimension); } else { // todo: if start === 0 - innerDimension = me.height - (me.paddingTop + me.paddingBottom); - value = Math.pow(10, (me.bottom - me.paddingBottom - pixel) * range / innerDimension) / me.start; + innerDimension = me.height; + value = Math.pow(10, (me.bottom - pixel) * range / innerDimension) / me.start; } return value; } diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index cc7c6557d74..b51fa698bd9 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -47,10 +47,266 @@ module.exports = function(Chart) { } }; + function getValueCount(scale) { + return !scale.options.lineArc ? scale.chart.data.labels.length : 0; + } + + function getPointLabelFontOptions(scale) { + var pointLabelOptions = scale.options.pointLabels; + var fontSize = helpers.getValueOrDefault(pointLabelOptions.fontSize, globalDefaults.defaultFontSize); + var fontStyle = helpers.getValueOrDefault(pointLabelOptions.fontStyle, globalDefaults.defaultFontStyle); + var fontFamily = helpers.getValueOrDefault(pointLabelOptions.fontFamily, globalDefaults.defaultFontFamily); + var font = helpers.fontString(fontSize, fontStyle, fontFamily); + + return { + size: fontSize, + style: fontStyle, + family: fontFamily, + font: font + }; + } + + function measureLabelSize(ctx, fontSize, label) { + if (helpers.isArray(label)) { + return { + w: helpers.longestText(ctx, ctx.font, label), + h: (label.length * fontSize) + ((label.length - 1) * 1.5 * fontSize) + }; + } + + return { + w: ctx.measureText(label).width, + h: fontSize + }; + } + + function determineLimits(angle, pos, size, min, max) { + if (angle === min || angle === max) { + return { + start: pos - (size / 2), + end: pos + (size / 2) + }; + } else if (angle < min || angle > max) { + return { + start: pos - size - 5, + end: pos + }; + } + + return { + start: pos, + end: pos + size + 5 + }; + } + + /** + * Helper function to fit a radial linear scale with point labels + */ + function fitWithPointLabels(scale) { + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + var plFont = getPointLabelFontOptions(scale); + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); + var furthestLimits = { + l: scale.width, + r: 0, + t: scale.height, + b: 0 + }; + var furthestAngles = {}; + var i; + var textSize; + var pointPosition; + + scale.ctx.font = plFont.font; + scale._pointLabelSizes = []; + + var valueCount = getValueCount(scale); + for (i = 0; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, largestPossibleRadius); + textSize = measureLabelSize(scale.ctx, plFont.size, scale.pointLabels[i] || ''); + scale._pointLabelSizes[i] = textSize; + + // Add quarter circle to make degree 0 mean top of circle + var angleRadians = scale.getIndexAngle(i); + var angle = helpers.toDegrees(angleRadians) % 360; + var hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); + var vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); + + if (hLimits.start < furthestLimits.l) { + furthestLimits.l = hLimits.start; + furthestAngles.l = angleRadians; + } + + if (hLimits.end > furthestLimits.r) { + furthestLimits.r = hLimits.end; + furthestAngles.r = angleRadians; + } + + if (vLimits.start < furthestLimits.t) { + furthestLimits.t = vLimits.start; + furthestAngles.t = angleRadians; + } + + if (vLimits.end > furthestLimits.b) { + furthestLimits.b = vLimits.end; + furthestAngles.b = angleRadians; + } + } + + scale.setReductions(largestPossibleRadius, furthestLimits, furthestAngles); + } + + /** + * Helper function to fit a radial linear scale with no point labels + */ + function fit(scale) { + var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); + scale.drawingArea = Math.round(largestPossibleRadius); + scale.setCenterPoint(0, 0, 0, 0); + } + + function getTextAlignForAngle(angle) { + if (angle === 0 || angle === 180) { + return 'center'; + } else if (angle < 180) { + return 'left'; + } + + return 'right'; + } + + function fillText(ctx, text, position, fontSize) { + if (helpers.isArray(text)) { + var y = position.y; + var spacing = 1.5 * fontSize; + + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], position.x, y); + y+= spacing; + } + } else { + ctx.fillText(text, position.x, position.y); + } + } + + function adjustPointPositionForLabelHeight(angle, textSize, position) { + if (angle === 90 || angle === 270) { + position.y -= (textSize.h / 2); + } else if (angle > 270 || angle < 90) { + position.y -= textSize.h; + } + } + + function drawPointLabels(scale) { + var ctx = scale.ctx; + var getValueOrDefault = helpers.getValueOrDefault; + var opts = scale.options; + var angleLineOpts = opts.angleLines; + var pointLabelOpts = opts.pointLabels; + + ctx.lineWidth = angleLineOpts.lineWidth; + ctx.strokeStyle = angleLineOpts.color; + + var outerDistance = scale.getDistanceFromCenterForValue(opts.reverse ? scale.min : scale.max); + + // Point Label Font + var plFont = getPointLabelFontOptions(scale); + + ctx.textBaseline = 'top'; + + for (var i = getValueCount(scale) - 1; i >= 0; i--) { + if (angleLineOpts.display) { + var outerPosition = scale.getPointPosition(i, outerDistance); + ctx.beginPath(); + ctx.moveTo(scale.xCenter, scale.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = scale.getPointPosition(i, outerDistance + 5); + + // Keep this in loop since we may support array properties here + var pointLabelFontColor = getValueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); + ctx.font = plFont.font; + ctx.fillStyle = pointLabelFontColor; + + var angleRadians = scale.getIndexAngle(i); + var angle = helpers.toDegrees(angleRadians); + ctx.textAlign = getTextAlignForAngle(angle); + adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); + fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.size); + } + } + + function drawRadiusLine(scale, gridLineOpts, radius, index) { + var ctx = scale.ctx; + ctx.strokeStyle = helpers.getValueAtIndexOrDefault(gridLineOpts.color, index - 1); + ctx.lineWidth = helpers.getValueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); + + if (scale.options.lineArc) { + // Draw circular arcs between the points + ctx.beginPath(); + ctx.arc(scale.xCenter, scale.yCenter, radius, 0, Math.PI * 2); + ctx.closePath(); + ctx.stroke(); + } else { + // Draw straight lines connecting each index + var valueCount = getValueCount(scale); + + if (valueCount === 0) { + return; + } + + ctx.beginPath(); + var pointPosition = scale.getPointPosition(0, radius); + ctx.moveTo(pointPosition.x, pointPosition.y); + + for (var i = 1; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, radius); + ctx.lineTo(pointPosition.x, pointPosition.y); + } + + ctx.closePath(); + ctx.stroke(); + } + } + + function numberOrZero(param) { + return helpers.isNumber(param) ? param : 0; + } + var LinearRadialScale = Chart.LinearScaleBase.extend({ - getValueCount: function() { - return this.chart.data.labels.length; - }, setDimensions: function() { var me = this; var opts = me.options; @@ -68,9 +324,8 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var chart = me.chart; - me.min = null; - me.max = null; - + var min = Number.POSITIVE_INFINITY; + var max = Number.NEGATIVE_INFINITY; helpers.each(chart.data.datasets, function(dataset, datasetIndex) { if (chart.isDatasetVisible(datasetIndex)) { @@ -82,21 +337,15 @@ module.exports = function(Chart) { return; } - if (me.min === null) { - me.min = value; - } else if (value < me.min) { - me.min = value; - } - - if (me.max === null) { - me.max = value; - } else if (value > me.max) { - me.max = value; - } + min = Math.min(value, min); + max = Math.max(value, max); }); } }); + me.min = (min === Number.POSITIVE_INFINITY ? 0 : min); + me.max = (max === Number.NEGATIVE_INFINITY ? 0 : max); + // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero me.handleTickRangeOptions(); }, @@ -116,122 +365,46 @@ module.exports = function(Chart) { return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); }, fit: function() { - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - var pointLabels = this.options.pointLabels; - var pointLabelFontSize = helpers.getValueOrDefault(pointLabels.fontSize, globalDefaults.defaultFontSize); - var pointLabeFontStyle = helpers.getValueOrDefault(pointLabels.fontStyle, globalDefaults.defaultFontStyle); - var pointLabeFontFamily = helpers.getValueOrDefault(pointLabels.fontFamily, globalDefaults.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = helpers.min([(this.height / 2 - pointLabelFontSize - 5), this.width / 2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft; - this.ctx.font = pointLabeFont; - - for (i = 0; i < this.getValueCount(); i++) { - // 5px to space the text slightly out - similar to what we do in the draw function. - pointPosition = this.getPointPosition(i, largestPossibleRadius); - textWidth = this.ctx.measureText(this.pointLabels[i] ? this.pointLabels[i] : '').width + 5; - - // Add quarter circle to make degree 0 mean top of circle - var angleRadians = this.getIndexAngle(i) + (Math.PI / 2); - var angle = (angleRadians * 360 / (2 * Math.PI)) % 360; - - if (angle === 0 || angle === 180) { - // At angle 0 and 180, we're at exactly the top/bottom - // of the radar chart, so text will be aligned centrally, so we'll half it and compare - // w/left and right text sizes - halfTextWidth = textWidth / 2; - if (pointPosition.x + halfTextWidth > furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } else if (angle < 180) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - // More than half the values means we'll right align the text - } else if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } + if (this.options.lineArc) { + fit(this); + } else { + fitWithPointLabels(this); } - - xProtrusionLeft = furthestLeft; - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2); - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (helpers.isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2); - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); }, - setCenterPoint: function(leftMovement, rightMovement) { + /** + * Set radius reductions and determine new radius and center point + * @private + */ + setReductions: function(largestPossibleRadius, furthestLimits, furthestAngles) { + var me = this; + var radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l); + var radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r); + var radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t); + var radiusReductionBottom = -Math.max(furthestLimits.b - me.height, 0) / Math.cos(furthestAngles.b); + + radiusReductionLeft = numberOrZero(radiusReductionLeft); + radiusReductionRight = numberOrZero(radiusReductionRight); + radiusReductionTop = numberOrZero(radiusReductionTop); + radiusReductionBottom = numberOrZero(radiusReductionBottom); + + me.drawingArea = Math.min( + Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), + Math.round(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2)); + me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom); + }, + setCenterPoint: function(leftMovement, rightMovement, topMovement, bottomMovement) { var me = this; var maxRight = me.width - rightMovement - me.drawingArea, - maxLeft = leftMovement + me.drawingArea; + maxLeft = leftMovement + me.drawingArea, + maxTop = topMovement + me.drawingArea, + maxBottom = me.height - bottomMovement - me.drawingArea; me.xCenter = Math.round(((maxLeft + maxRight) / 2) + me.left); - // Always vertically in the centre as the text height doesn't change - me.yCenter = Math.round((me.height / 2) + me.top); + me.yCenter = Math.round(((maxTop + maxBottom) / 2) + me.top); }, getIndexAngle: function(index) { - var angleMultiplier = (Math.PI * 2) / this.getValueCount(); + var angleMultiplier = (Math.PI * 2) / getValueCount(this); var startAngle = this.chart.options && this.chart.options.startAngle ? this.chart.options.startAngle : 0; @@ -239,7 +412,7 @@ module.exports = function(Chart) { var startAngleRadians = startAngle * Math.PI * 2 / 360; // Start from the top instead of right, so remove a quarter of the circle - return index * angleMultiplier - (Math.PI / 2) + startAngleRadians; + return index * angleMultiplier + startAngleRadians; }, getDistanceFromCenterForValue: function(value) { var me = this; @@ -257,7 +430,7 @@ module.exports = function(Chart) { }, getPointPosition: function(index, distanceFromCenter) { var me = this; - var thisAngle = me.getIndexAngle(index); + var thisAngle = me.getIndexAngle(index) - (Math.PI / 2); return { x: Math.round(Math.cos(thisAngle) * distanceFromCenter) + me.xCenter, y: Math.round(Math.sin(thisAngle) * distanceFromCenter) + me.yCenter @@ -284,8 +457,6 @@ module.exports = function(Chart) { var opts = me.options; var gridLineOpts = opts.gridLines; var tickOpts = opts.ticks; - var angleLineOpts = opts.angleLines; - var pointLabelOpts = opts.pointLabels; var getValueOrDefault = helpers.getValueOrDefault; if (opts.display) { @@ -305,29 +476,7 @@ module.exports = function(Chart) { // Draw circular lines around the scale if (gridLineOpts.display && index !== 0) { - ctx.strokeStyle = helpers.getValueAtIndexOrDefault(gridLineOpts.color, index - 1); - ctx.lineWidth = helpers.getValueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); - - if (opts.lineArc) { - // Draw circular arcs between the points - ctx.beginPath(); - ctx.arc(me.xCenter, me.yCenter, yCenterOffset, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - } else { - // Draw straight lines connecting each index - ctx.beginPath(); - for (var i = 0; i < me.getValueCount(); i++) { - var pointPosition = me.getPointPosition(i, yCenterOffset); - if (i === 0) { - ctx.moveTo(pointPosition.x, pointPosition.y); - } else { - ctx.lineTo(pointPosition.x, pointPosition.y); - } - } - ctx.closePath(); - ctx.stroke(); - } + drawRadiusLine(me, gridLineOpts, yCenterOffset, index); } if (tickOpts.display) { @@ -354,59 +503,7 @@ module.exports = function(Chart) { }); if (!opts.lineArc) { - ctx.lineWidth = angleLineOpts.lineWidth; - ctx.strokeStyle = angleLineOpts.color; - - var outerDistance = me.getDistanceFromCenterForValue(opts.reverse ? me.min : me.max); - - // Point Label Font - var pointLabelFontSize = getValueOrDefault(pointLabelOpts.fontSize, globalDefaults.defaultFontSize); - var pointLabeFontStyle = getValueOrDefault(pointLabelOpts.fontStyle, globalDefaults.defaultFontStyle); - var pointLabeFontFamily = getValueOrDefault(pointLabelOpts.fontFamily, globalDefaults.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - for (var i = me.getValueCount() - 1; i >= 0; i--) { - if (angleLineOpts.display) { - var outerPosition = me.getPointPosition(i, outerDistance); - ctx.beginPath(); - ctx.moveTo(me.xCenter, me.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = me.getPointPosition(i, outerDistance + 5); - - // Keep this in loop since we may support array properties here - var pointLabelFontColor = getValueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); - ctx.font = pointLabeFont; - ctx.fillStyle = pointLabelFontColor; - - var pointLabels = me.pointLabels; - - // Add quarter circle to make degree 0 mean top of circle - var angleRadians = this.getIndexAngle(i) + (Math.PI / 2); - var angle = (angleRadians * 360 / (2 * Math.PI)) % 360; - - if (angle === 0 || angle === 180) { - ctx.textAlign = 'center'; - } else if (angle < 180) { - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (angle === 90 || angle === 270) { - ctx.textBaseline = 'middle'; - } else if (angle > 270 || angle < 90) { - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(pointLabels[i] ? pointLabels[i] : '', pointLabelPosition.x, pointLabelPosition.y); - } + drawPointLabels(me); } } } diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index c69bcf21e75..9a3e31e63b3 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -240,7 +240,7 @@ module.exports = function(Chart) { me.unitScale = helpers.getValueOrDefault(me.options.time.unitStepSize, 1); } else { // Determine the smallest needed unit of the time - var innerWidth = me.isHorizontal() ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); + var innerWidth = me.isHorizontal() ? me.width : me.height; // Crude approximation of what the label length might be var tempFirstLabel = me.tickFormatFunction(me.firstTick, 0, []); @@ -324,7 +324,7 @@ module.exports = function(Chart) { me.ticks.push(me.firstTick.clone()); // For every unit in between the first and last moment, create a moment and add it to the ticks tick - for (var i = 1; i <= me.scaleSizeInUnits; ++i) { + for (var i = me.unitScale; i <= me.scaleSizeInUnits; i += me.unitScale) { var newTick = roundedStart.clone().add(i, me.tickUnit); // Are we greater than the max time @@ -332,9 +332,7 @@ module.exports = function(Chart) { break; } - if (i % me.unitScale === 0) { - me.ticks.push(newTick); - } + me.ticks.push(newTick); } // Always show the right tick @@ -360,9 +358,10 @@ module.exports = function(Chart) { getLabelForIndex: function(index, datasetIndex) { var me = this; var label = me.chart.data.labels && index < me.chart.data.labels.length ? me.chart.data.labels[index] : ''; + var value = me.chart.data.datasets[datasetIndex].data[index]; - if (typeof me.chart.data.datasets[datasetIndex].data[0] === 'object') { - label = me.getRightValue(me.chart.data.datasets[datasetIndex].data[index]); + if (value !== null && typeof value === 'object') { + label = me.getRightValue(value); } // Format nicely @@ -409,14 +408,11 @@ module.exports = function(Chart) { var decimal = offset !== 0 ? offset / me.scaleSizeInUnits : offset; if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueOffset = (innerWidth * decimal) + me.paddingLeft; - + var valueOffset = (me.width * decimal); return me.left + Math.round(valueOffset); } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - var heightOffset = (innerHeight * decimal) + me.paddingTop; + var heightOffset = (me.height * decimal); return me.top + Math.round(heightOffset); } }, @@ -425,8 +421,8 @@ module.exports = function(Chart) { }, getValueForPixel: function(pixel) { var me = this; - var innerDimension = me.isHorizontal() ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); - var offset = (pixel - (me.isHorizontal() ? me.left + me.paddingLeft : me.top + me.paddingTop)) / innerDimension; + var innerDimension = me.isHorizontal() ? me.width : me.height; + var offset = (pixel - (me.isHorizontal() ? me.left : me.top)) / innerDimension; offset *= me.scaleSizeInUnits; return me.firstTick.clone().add(moment.duration(offset, me.tickUnit).asSeconds(), 'seconds'); }, diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index a77eea22ead..38f1b733a1a 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -52,41 +52,612 @@ describe('Bar controller tests', function() { expect(meta.yAxisID).toBe('firstYScaleID'); }); - it('should correctly count the number of bar datasets', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [ - {data: [], type: 'line'}, - {data: [], hidden: true}, - {data: []}, - {data: []} - ], - labels: [] - } + it('should correctly count the number of stacks ignoring datasets of other types and hidden datasets', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], type: 'line'}, + {data: [], hidden: true}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(2); }); + }); - var meta = chart.getDatasetMeta(1); - expect(meta.controller.getBarCount()).toBe(2); + it('should correctly count the number of stacks when a group is not specified', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(4); + }); }); - it('should correctly get the bar index accounting for hidden datasets', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [ - {data: []}, - {data: [], hidden: true}, - {data: [], type: 'line'}, - {data: []} - ], - labels: [] - } + it('should correctly count the number of stacks when a group is not specified and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(1); + }); + }); + + it('should correctly count the number of stacks when a group is not specified and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(4); }); + }); - var meta = chart.getDatasetMeta(1); - expect(meta.controller.getBarIndex(0)).toBe(0); - expect(meta.controller.getBarIndex(3)).toBe(1); + it('should correctly count the number of stacks when a group is specified for some', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(3); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly get the stack index accounting for datasets of other types and hidden datasets', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: [], hidden: true}, + {data: [], type: 'line'}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is not specified', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is not specified and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(0); + expect(meta.controller.getStackIndex(3)).toBe(0); + }); + }); + + it('should correctly get the stack index when a group is not specified and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is specified for some', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(2); + }); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is specified for all', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); }); it('should create rectangle elements for each data item during initialization', function() { @@ -155,8 +726,8 @@ describe('Bar controller tests', function() { expect(meta.data.length).toBe(2); [ - {x: 122, y: 484}, - {x: 234, y: 32} + {x: 113, y: 484}, + {x: 229, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(1); expect(meta.data[i]._index).toBe(i); @@ -217,9 +788,9 @@ describe('Bar controller tests', function() { var bar1 = meta.data[0]; var bar2 = meta.data[1]; - expect(bar1._model.x).toBeCloseToPixel(194); + expect(bar1._model.x).toBeCloseToPixel(187); expect(bar1._model.y).toBeCloseToPixel(132); - expect(bar2._model.x).toBeCloseToPixel(424); + expect(bar2._model.x).toBeCloseToPixel(422); expect(bar2._model.y).toBeCloseToPixel(32); }); @@ -239,8 +810,7 @@ describe('Bar controller tests', function() { options: { scales: { xAxes: [{ - type: 'category', - stacked: true + type: 'category' }], yAxes: [{ type: 'linear', @@ -253,10 +823,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 91, x: 95, y: 161}, - {b: 290, w: 91, x: 209, y: 419}, - {b: 290, w: 91, x: 322, y: 161}, - {b: 290, w: 91, x: 436, y: 419} + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -267,10 +837,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 91, x: 95, y: 32}, - {b: 290, w: 91, x: 209, y: 97}, - {b: 161, w: 91, x: 322, y: 161}, - {b: 419, w: 91, x: 436, y: 471} + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -295,8 +865,64 @@ describe('Bar controller tests', function() { options: { scales: { xAxes: [{ - type: 'category', + type: 'category' + }], + yAxes: [{ + type: 'linear', stacked: true + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} + ].forEach(function(values, i) { + expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} + ].forEach(function(values, i) { + expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart if the group name is same', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1', + stack: 'stack1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2', + stack: 'stack1' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' }], yAxes: [{ type: 'linear', @@ -309,10 +935,148 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 91, x: 95, y: 161}, - {b: 290, w: 91, x: 209, y: 419}, - {b: 290, w: 91, x: 322, y: 161}, - {b: 290, w: 91, x: 436, y: 419} + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} + ].forEach(function(values, i) { + expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta = chart.getDatasetMeta(1); + + [ + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart if the group name is different', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [1, 2], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + stacked: true, + type: 'linear' + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + + [ + {x: 108, y: 258}, + {x: 224, y: 32} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(484); + expect(meta.data[i]._model.width).toBeCloseToPixel(40); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [0.5, 1], + stack: 'stack2' + }, { + data: [0.5, 1], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + stacked: true, + type: 'linear' + }] + } + } + }); + + var meta = chart.getDatasetMeta(2); + + [ + {b: 371, x: 108, y: 258}, + {b: 258, x: 224, y: 32} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta.data[i]._model.width).toBeCloseToPixel(40); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should update elements when the scales are stacked and the y axis is logarithmic', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, 100, 10, 100], + label: 'dataset1' + }, { + data: [100, 10, 0, 100], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category', + stacked: true, + barPercentage: 1 + }], + yAxes: [{ + type: 'logarithmic', + stacked: true + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 484, w: 92, x: 94, y: 379}, + {b: 484, w: 92, x: 208, y: 122}, + {b: 484, w: 92, x: 322, y: 379}, + {b: 484, w: 92, x: 436, y: 122} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -323,10 +1087,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 91, x: 95, y: 32}, - {b: 290, w: 91, x: 209, y: 97}, - {b: 161, w: 91, x: 322, y: 161}, - {b: 419, w: 91, x: 436, y: 471} + {b: 379, w: 92, x: 94, y: 109}, + {b: 122, w: 92, x: 208, y: 109}, + {b: 379, w: 92, x: 322, y: 379}, + {b: 122, w: 92, x: 436, y: 25} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -487,4 +1251,160 @@ describe('Bar controller tests', function() { expect(bar._model.borderColor).toBe('rgb(0, 255, 0)'); expect(bar._model.borderWidth).toBe(1.5); }); + + describe('Bar width', function() { + beforeEach(function() { + // 2 datasets + this.data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70], + }, { + data: [10, 20, 30, 40, 50, 60, 70], + }] + }; + }); + + afterEach(function() { + var chart = window.acquireChart(this.config); + var meta = chart.getDatasetMeta(0); + var xScale = chart.scales[meta.xAxisID]; + var yScale = chart.scales[meta.yAxisID]; + + var categoryPercentage = xScale.options.categoryPercentage; + var barPercentage = xScale.options.barPercentage; + var stacked = yScale.options.stacked; + + var totalBarWidth = 0; + for (var i = 0; i < chart.data.datasets.length; i++) { + var bars = chart.getDatasetMeta(i).data; + for (var j = xScale.minIndex; j <= xScale.maxIndex; j++) { + totalBarWidth += bars[j]._model.width; + } + if (stacked) { + break; + } + } + + var actualValue = totalBarWidth; + var expectedValue = xScale.width * categoryPercentage * barPercentage; + expect(actualValue).toBeCloseToPixel(expectedValue); + + }); + + it('should correctly set bar width when min and max option is set.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + scales: { + xAxes: [{ + ticks: { + min: 'March', + max: 'May', + }, + }] + } + } + }; + }); + + it('should correctly set bar width when scale are stacked with min and max options.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + scales: { + xAxes: [{ + ticks: { + min: 'March', + max: 'May', + } + }], + yAxes: [{ + stacked: true + }] + } + } + }; + }); + }); + + describe('Bar height (horizontalBar type)', function() { + beforeEach(function() { + // 2 datasets + this.data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70], + }, { + data: [10, 20, 30, 40, 50, 60, 70], + }] + }; + }); + + afterEach(function() { + var chart = window.acquireChart(this.config); + var meta = chart.getDatasetMeta(0); + var xScale = chart.scales[meta.xAxisID]; + var yScale = chart.scales[meta.yAxisID]; + + var categoryPercentage = yScale.options.categoryPercentage; + var barPercentage = yScale.options.barPercentage; + var stacked = xScale.options.stacked; + + var totalBarHeight = 0; + for (var i = 0; i < chart.data.datasets.length; i++) { + var bars = chart.getDatasetMeta(i).data; + for (var j = yScale.minIndex; j <= yScale.maxIndex; j++) { + totalBarHeight += bars[j]._model.height; + } + if (stacked) { + break; + } + } + + var actualValue = totalBarHeight; + var expectedValue = yScale.height * categoryPercentage * barPercentage; + expect(actualValue).toBeCloseToPixel(expectedValue); + + }); + + it('should correctly set bar height when min and max option is set.', function() { + this.config = { + type: 'horizontalBar', + data: this.data, + options: { + scales: { + yAxes: [{ + ticks: { + min: 'March', + max: 'May', + }, + }] + } + } + }; + }); + + it('should correctly set bar height when scale are stacked with min and max options.', function() { + this.config = { + type: 'horizontalBar', + data: this.data, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + ticks: { + min: 'March', + max: 'May', + } + }] + } + } + }; + }); + }); }); diff --git a/test/controller.bubble.tests.js b/test/controller.bubble.tests.js index 977b2d517b1..4914e1005b0 100644 --- a/test/controller.bubble.tests.js +++ b/test/controller.bubble.tests.js @@ -134,9 +134,9 @@ describe('Bubble controller tests', function() { var meta = chart.getDatasetMeta(0); [ - {r: 5, x: 38, y: 32}, - {r: 1, x: 189, y: 484}, - {r: 2, x: 341, y: 461}, + {r: 5, x: 28, y: 32}, + {r: 1, x: 183, y: 484}, + {r: 2, x: 338, y: 461}, {r: 1, x: 492, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._model.radius).toBe(expected.r); diff --git a/test/controller.doughnut.tests.js b/test/controller.doughnut.tests.js index d9319bb807a..f4c0c959e30 100644 --- a/test/controller.doughnut.tests.js +++ b/test/controller.doughnut.tests.js @@ -40,6 +40,20 @@ describe('Doughnut controller tests', function() { expect(meta.data[3] instanceof Chart.elements.Arc).toBe(true); }); + it('should set the innerRadius to 0 if the config option is 0', function() { + var chart = window.acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: [] + } + }); + + expect(chart.innerRadius).toBe(0); + }); + it ('should reset and update elements', function() { var chart = window.acquireChart({ type: 'doughnut', diff --git a/test/controller.line.tests.js b/test/controller.line.tests.js index 3c66b268ec0..2341b403cd8 100644 --- a/test/controller.line.tests.js +++ b/test/controller.line.tests.js @@ -203,8 +203,8 @@ describe('Line controller tests', function() { [ - {x: 44, y: 484}, - {x: 193, y: 32} + {x: 33, y: 484}, + {x: 186, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(0); expect(meta.data[i]._index).toBe(i); @@ -250,7 +250,7 @@ describe('Line controller tests', function() { var meta = chart.getDatasetMeta(0); // 1 point var point = meta.data[0]; - expect(point._model.x).toBeCloseToPixel(267); + expect(point._model.x).toBeCloseToPixel(262); // 2 points chart.data.labels = ['One', 'Two']; @@ -259,7 +259,7 @@ describe('Line controller tests', function() { var points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(37); + expect(points[0]._model.x).toBeCloseToPixel(27); expect(points[1]._model.x).toBeCloseToPixel(498); // 3 points @@ -269,8 +269,8 @@ describe('Line controller tests', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(37); - expect(points[1]._model.x).toBeCloseToPixel(265); + expect(points[0]._model.x).toBeCloseToPixel(27); + expect(points[1]._model.x).toBeCloseToPixel(260); expect(points[2]._model.x).toBeCloseToPixel(493); // 4 points @@ -280,9 +280,9 @@ describe('Line controller tests', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(37); - expect(points[1]._model.x).toBeCloseToPixel(190); - expect(points[2]._model.x).toBeCloseToPixel(343); + expect(points[0]._model.x).toBeCloseToPixel(27); + expect(points[1]._model.x).toBeCloseToPixel(184); + expect(points[2]._model.x).toBeCloseToPixel(340); expect(points[3]._model.x).toBeCloseToPixel(497); }); @@ -311,9 +311,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 38, y: 161}, - {x: 189, y: 419}, - {x: 341, y: 161}, + {x: 28, y: 161}, + {x: 183, y: 419}, + {x: 338, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -323,9 +323,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 38, y: 32}, - {x: 189, y: 97}, - {x: 341, y: 161}, + {x: 28, y: 32}, + {x: 183, y: 97}, + {x: 338, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); @@ -366,9 +366,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 76, y: 161}, - {x: 215, y: 419}, - {x: 353, y: 161}, + {x: 56, y: 161}, + {x: 202, y: 419}, + {x: 347, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -378,9 +378,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 76, y: 32}, - {x: 215, y: 97}, - {x: 353, y: 161}, + {x: 56, y: 32}, + {x: 202, y: 97}, + {x: 347, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); @@ -438,9 +438,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 38, y: 161}, - {x: 189, y: 419}, - {x: 341, y: 161}, + {x: 28, y: 161}, + {x: 183, y: 419}, + {x: 338, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -450,9 +450,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 38, y: 32}, - {x: 189, y: 97}, - {x: 341, y: 161}, + {x: 28, y: 32}, + {x: 183, y: 97}, + {x: 338, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); @@ -486,9 +486,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 38, y: 161}, - {x: 189, y: 419}, - {x: 341, y: 161}, + {x: 28, y: 161}, + {x: 183, y: 419}, + {x: 338, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -498,9 +498,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 38, y: 32}, - {x: 189, y: 97}, - {x: 341, y: 161}, + {x: 28, y: 32}, + {x: 183, y: 97}, + {x: 338, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); @@ -753,4 +753,23 @@ describe('Line controller tests', function() { expect(point._model.borderWidth).toBe(5.5); expect(point._model.radius).toBe(4.4); }); + + it('should allow 0 as a point border width', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1', + pointBorderWidth: 0 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[0]; + + expect(point._model.borderWidth).toBe(0); + }); }); diff --git a/test/controller.polarArea.tests.js b/test/controller.polarArea.tests.js index 3bd95eb231e..de7d4acfaac 100644 --- a/test/controller.polarArea.tests.js +++ b/test/controller.polarArea.tests.js @@ -96,9 +96,9 @@ describe('Polar area controller tests', function() { expect(meta.data.length).toBe(4); [ - {o: 156, s: -0.5 * Math.PI, e: 0}, - {o: 211, s: 0, e: 0.5 * Math.PI}, - {o: 45, s: 0.5 * Math.PI, e: Math.PI}, + {o: 168, s: -0.5 * Math.PI, e: 0}, + {o: 228, s: 0, e: 0.5 * Math.PI}, + {o: 48, s: 0.5 * Math.PI, e: Math.PI}, {o: 0, s: Math.PI, e: 1.5 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); @@ -140,7 +140,7 @@ describe('Polar area controller tests', function() { expect(meta.data[0]._model.x).toBeCloseToPixel(256); expect(meta.data[0]._model.y).toBeCloseToPixel(272); expect(meta.data[0]._model.innerRadius).toBeCloseToPixel(0); - expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(156); + expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(168); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ startAngle: -0.5 * Math.PI, endAngle: 0, @@ -178,9 +178,9 @@ describe('Polar area controller tests', function() { expect(meta.data.length).toBe(4); [ - {o: 156, s: 0, e: 0.5 * Math.PI}, - {o: 211, s: 0.5 * Math.PI, e: Math.PI}, - {o: 45, s: Math.PI, e: 1.5 * Math.PI}, + {o: 168, s: 0, e: 0.5 * Math.PI}, + {o: 228, s: 0.5 * Math.PI, e: Math.PI}, + {o: 48, s: Math.PI, e: 1.5 * Math.PI}, {o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); diff --git a/test/controller.radar.tests.js b/test/controller.radar.tests.js index 449d295c9c4..4b9c1f696dc 100644 --- a/test/controller.radar.tests.js +++ b/test/controller.radar.tests.js @@ -179,7 +179,7 @@ describe('Radar controller tests', function() { }); // Use dataset level styles for lines & points - chart.data.datasets[0].tension = 0; + chart.data.datasets[0].lineTension = 0; chart.data.datasets[0].backgroundColor = 'rgb(98, 98, 98)'; chart.data.datasets[0].borderColor = 'rgb(8, 8, 8)'; chart.data.datasets[0].borderWidth = 0.55; @@ -452,4 +452,21 @@ describe('Radar controller tests', function() { expect(point._model.borderWidth).toBe(5.5); expect(point._model.radius).toBe(4.4); }); + + it('should allow pointBorderWidth to be set to 0', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4], + pointBorderWidth: 0 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[0]; + expect(point._model.borderWidth).toBe(0); + }); }); diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index cc99b0d7fb1..da4b77d1d21 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -13,74 +13,6 @@ describe('Chart.Controller', function() { Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler); } - describe('context acquisition', function() { - var canvasId = 'chartjs-canvas'; - - beforeEach(function() { - var canvas = document.createElement('canvas'); - canvas.setAttribute('id', canvasId); - window.document.body.appendChild(canvas); - }); - - afterEach(function() { - document.getElementById(canvasId).remove(); - }); - - // see https://github.com/chartjs/Chart.js/issues/2807 - it('should gracefully handle invalid item', function() { - var chart = new Chart('foobar'); - - expect(chart).not.toBeValidChart(); - - chart.destroy(); - }); - - it('should accept a DOM element id', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart(canvasId); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - - it('should accept a canvas element', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart(canvas); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - - it('should accept a canvas context2D', function() { - var canvas = document.getElementById(canvasId); - var context = canvas.getContext('2d'); - var chart = new Chart(context); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(context); - - chart.destroy(); - }); - - it('should accept an array containing canvas', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart([canvas]); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - }); - describe('config initialization', function() { it('should create missing config.data properties', function() { var chart = acquireChart({}); @@ -164,152 +96,7 @@ describe('Chart.Controller', function() { }); }); - describe('config.options.aspectRatio', function() { - it('should use default "global" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 620px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 620, dh: 310, - rw: 620, rh: 310, - }); - }); - - it('should use default "chart" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - type: 'doughnut', - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 425px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 425, dh: 425, - rw: 425, rh: 425, - }); - }); - - it('should use "user" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false, - aspectRatio: 3 - } - }, { - canvas: { - style: 'width: 405px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 405, dh: 135, - rw: 405, rh: 135, - }); - }); - - it('should not apply aspect ratio when height specified', function() { - var chart = acquireChart({ - options: { - responsive: false, - aspectRatio: 3 - } - }, { - canvas: { - style: 'width: 400px; height: 410px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 400, dh: 410, - rw: 400, rh: 410, - }); - }); - }); - describe('config.options.responsive: false', function() { - it('should use default canvas size for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: '' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 300, dh: 150, - rw: 300, rh: 150, - }); - }); - - it('should use canvas attributes for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: '', - width: 305, - height: 245, - } - }); - - expect(chart).toBeChartOfSize({ - dw: 305, dh: 245, - rw: 305, rh: 245, - }); - }); - - it('should use canvas style for render and display sizes (if no attributes)', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 345px; height: 125px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 345, dh: 125, - rw: 345, rh: 125, - }); - }); - - it('should use attributes for the render size and style for the display size', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 345px; height: 125px;', - width: 165, - height: 85, - } - }); - - expect(chart).toBeChartOfSize({ - dw: 345, dh: 125, - rw: 165, rh: 85, - }); - }); - it('should not inject the resizer element', function() { var chart = acquireChart({ options: { @@ -563,27 +350,6 @@ describe('Chart.Controller', function() { }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() { - it('should fill parent width and use aspect ratio to calculate height', function() { - var chart = acquireChart({ - options: { - responsive: true, - maintainAspectRatio: true - } - }, { - canvas: { - style: 'width: 150px; height: 245px' - }, - wrapper: { - style: 'width: 300px; height: 350px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 300, dh: 490, - rw: 300, rh: 490, - }); - }); - it('should resize the canvas with correct aspect ratio when parent width changes', function(done) { var chart = acquireChart({ type: 'line', // AR == 2 @@ -714,69 +480,6 @@ describe('Chart.Controller', function() { }); describe('controller.destroy', function() { - it('should reset context to default values', function() { - var chart = acquireChart({}); - var context = chart.chart.ctx; - - chart.destroy(); - - // https://www.w3.org/TR/2dcontext/#conformance-requirements - Chart.helpers.each({ - fillStyle: '#000000', - font: '10px sans-serif', - lineJoin: 'miter', - lineCap: 'butt', - lineWidth: 1, - miterLimit: 10, - shadowBlur: 0, - shadowColor: 'rgba(0, 0, 0, 0)', - shadowOffsetX: 0, - shadowOffsetY: 0, - strokeStyle: '#000000', - textAlign: 'start', - textBaseline: 'alphabetic' - }, function(value, key) { - expect(context[key]).toBe(value); - }); - }); - - it('should restore canvas initial values', function(done) { - var chart = acquireChart({ - options: { - responsive: true, - maintainAspectRatio: false - } - }, { - canvas: { - width: 180, - style: 'width: 512px; height: 480px' - }, - wrapper: { - style: 'width: 450px; height: 450px; position: relative' - } - }); - - var canvas = chart.chart.canvas; - var wrapper = canvas.parentNode; - wrapper.style.width = '475px'; - waitForResize(chart, function() { - expect(chart).toBeChartOfSize({ - dw: 475, dh: 450, - rw: 475, rh: 450, - }); - - chart.destroy(); - - expect(canvas.getAttribute('width')).toBe('180'); - expect(canvas.getAttribute('height')).toBe(null); - expect(canvas.style.width).toBe('512px'); - expect(canvas.style.height).toBe('480px'); - expect(canvas.style.display).toBe(''); - - done(); - }); - }); - it('should remove the resizer element when responsive: true', function() { var chart = acquireChart({ options: { @@ -830,4 +533,124 @@ describe('Chart.Controller', function() { expect(meta.data[3]._model.y).toBe(484); }); }); + + describe('config update', function() { + it ('should update scales options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + chart.options.scales.yAxes[0].ticks.min = 0; + chart.options.scales.yAxes[0].ticks.max = 10; + chart.update(); + + var yScale = chart.scales['y-axis-0']; + expect(yScale.options.ticks.min).toBe(0); + expect(yScale.options.ticks.max).toBe(10); + }); + + it ('should update tooltip options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + var newTooltipConfig = { + mode: 'dataset', + intersect: false + }; + chart.options.tooltips = newTooltipConfig; + + chart.update(); + expect(chart.tooltip._options).toEqual(jasmine.objectContaining(newTooltipConfig)); + }); + }); + + describe('plugin.extensions', function() { + it ('should notify plugin in correct order', function(done) { + var plugin = this.plugin = {id: 'foobar'}; + var sequence = []; + var hooks = { + init: [ + 'beforeInit', + 'afterInit' + ], + update: [ + 'beforeUpdate', + 'beforeLayout', + 'afterLayout', + 'beforeDatasetsUpdate', + 'afterDatasetsUpdate', + 'afterUpdate', + ], + render: [ + 'beforeRender', + 'beforeDraw', + 'beforeDatasetsDraw', + 'afterDatasetsDraw', + 'afterDraw', + 'afterRender', + ], + resize: [ + 'resize' + ], + destroy: [ + 'destroy' + ] + }; + + Object.keys(hooks).forEach(function(group) { + hooks[group].forEach(function(name) { + plugin[name] = function() { + sequence.push(name); + }; + }); + }); + + var chart = window.acquireChart({ + plugins: [plugin], + options: { + responsive: true + } + }, { + wrapper: { + style: 'width: 300px' + } + }); + + chart.chart.canvas.parentNode.style.width = '400px'; + waitForResize(chart, function() { + chart.destroy(); + + expect(sequence).toEqual([].concat( + hooks.init, + hooks.update, + hooks.render, + hooks.resize, + hooks.update, + hooks.render, + hooks.destroy + )); + + done(); + }); + }); + }); }); diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index 4a7e9f555f1..296958051d6 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -206,7 +206,7 @@ describe('Core helper tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: merged.scales.yAxes[1].ticks.callback, // make it nicer, then check explicitly below @@ -242,7 +242,7 @@ describe('Core helper tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: merged.scales.yAxes[2].ticks.callback, // make it nicer, then check explicitly below @@ -301,6 +301,11 @@ describe('Core helper tests', function() { expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true); }); + it('should correctly determine if a numbers are essentially whole', function() { + expect(helpers.almostWhole(0.99999, 0.0001)).toBe(true); + expect(helpers.almostWhole(0.9, 0.0001)).toBe(false); + }); + it('should generate integer ids', function() { var uid = helpers.uid(); expect(uid).toEqual(jasmine.any(Number)); @@ -412,6 +417,7 @@ describe('Core helper tests', function() { {_model: {x: 27, y: 125, skip: false}}, {_model: {x: 30, y: 105, skip: false}}, {_model: {x: 33, y: 110, skip: false}}, + {_model: {x: 33, y: 110, skip: false}}, {_model: {x: 36, y: 170, skip: false}} ]; helpers.splineCurveMonotone(dataPoints); @@ -532,9 +538,20 @@ describe('Core helper tests', function() { y: 110, skip: false, controlPointPreviousX: 32, - controlPointPreviousY: 105, + controlPointPreviousY: 110, + controlPointNextX: 33, + controlPointNextY: 110 + } + }, + { + _model: { + x: 33, + y: 110, + skip: false, + controlPointPreviousX: 33, + controlPointPreviousY: 110, controlPointNextX: 34, - controlPointNextY: 115 + controlPointNextY: 110 } }, { diff --git a/test/core.legend.tests.js b/test/core.legend.tests.js index 6d539fd1ec2..109a209e05c 100644 --- a/test/core.legend.tests.js +++ b/test/core.legend.tests.js @@ -90,6 +90,71 @@ describe('Legend block tests', function() { }]); }); + it('should filter items', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'miter', + data: [], + legendHidden: true + }, { + label: 'dataset3', + borderWidth: 10, + borderColor: 'green', + pointStyle: 'crossRot', + data: [] + }], + labels: [] + }, + options: { + legend: { + labels: { + filter: function(legendItem, data) { + var dataset = data.datasets[legendItem.datasetIndex]; + return !dataset.legendHidden; + } + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + fillStyle: '#f31', + hidden: false, + lineCap: 'butt', + lineDash: [2, 2], + lineDashOffset: 5.5, + lineJoin: undefined, + lineWidth: undefined, + strokeStyle: undefined, + pointStyle: undefined, + datasetIndex: 0 + }, { + text: 'dataset3', + fillStyle: undefined, + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 10, + strokeStyle: 'green', + pointStyle: 'crossRot', + datasetIndex: 2 + }]); + }); + it('should draw correctly', function() { var chart = window.acquireChart({ type: 'bar', @@ -302,4 +367,66 @@ describe('Legend block tests', function() { "args": ["dataset3", 228, 132] }]);*/ }); + + describe('config update', function() { + it ('should update the options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + legend: { + display: true + } + } + }); + expect(chart.legend.options.display).toBe(true); + + chart.options.legend.display = false; + chart.update(); + expect(chart.legend.options.display).toBe(false); + }); + + it ('should remove the legend if the new options are false', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + } + }); + expect(chart.legend).not.toBe(undefined); + + chart.options.legend = false; + chart.update(); + expect(chart.legend).toBe(undefined); + }); + + it ('should create the legend if the legend options are changed to exist', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + legend: false + } + }); + expect(chart.legend).toBe(undefined); + + chart.options.legend = {}; + chart.update(); + expect(chart.legend).not.toBe(undefined); + expect(chart.legend.options).toEqual(jasmine.objectContaining(Chart.defaults.global.legend)); + }); + }); }); diff --git a/test/core.plugin.tests.js b/test/core.plugin.tests.js index 59a71b1c55e..387d78808c7 100644 --- a/test/core.plugin.tests.js +++ b/test/core.plugin.tests.js @@ -1,16 +1,13 @@ describe('Chart.plugins', function() { - var oldPlugins; - - beforeAll(function() { - oldPlugins = Chart.plugins.getAll(); - }); - - afterAll(function() { - Chart.plugins.register(oldPlugins); + beforeEach(function() { + this._plugins = Chart.plugins.getAll(); + Chart.plugins.clear(); }); - beforeEach(function() { + afterEach(function() { Chart.plugins.clear(); + Chart.plugins.register(this._plugins); + delete this._plugins; }); describe('Chart.plugins.register', function() { @@ -66,63 +63,282 @@ describe('Chart.plugins', function() { }); describe('Chart.plugins.notify', function() { - it('should call plugins with arguments', function() { - var myplugin = { - count: 0, - trigger: function(chart) { - myplugin.count = chart.count; + it('should call inline plugins with arguments', function() { + var plugin = {hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin] + }); + + spyOn(plugin, 'hook'); + + Chart.plugins.notify(chart, 'hook', 42); + expect(plugin.hook.calls.count()).toBe(1); + expect(plugin.hook.calls.first().args[0]).toBe(chart); + expect(plugin.hook.calls.first().args[1]).toBe(42); + expect(plugin.hook.calls.first().args[2]).toEqual({}); + }); + + it('should call global plugins with arguments', function() { + var plugin = {hook: function() {}}; + var chart = window.acquireChart({}); + + spyOn(plugin, 'hook'); + + Chart.plugins.register(plugin); + Chart.plugins.notify(chart, 'hook', 42); + expect(plugin.hook.calls.count()).toBe(1); + expect(plugin.hook.calls.first().args[0]).toBe(chart); + expect(plugin.hook.calls.first().args[1]).toBe(42); + expect(plugin.hook.calls.first().args[2]).toEqual({}); + }); + + it('should call plugin only once even if registered multiple times', function() { + var plugin = {hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin, plugin] + }); + + spyOn(plugin, 'hook'); + + Chart.plugins.register([plugin, plugin]); + Chart.plugins.notify(chart, 'hook'); + expect(plugin.hook.calls.count()).toBe(1); + }); + + it('should call plugins in the correct order (global first)', function() { + var results = []; + var chart = window.acquireChart({ + plugins: [{ + hook: function() { + results.push(1); + } + }, { + hook: function() { + results.push(2); + } + }, { + hook: function() { + results.push(3); + } + }] + }); + + Chart.plugins.register([{ + hook: function() { + results.push(4); } - }; + }, { + hook: function() { + results.push(5); + } + }, { + hook: function() { + results.push(6); + } + }]); - Chart.plugins.register(myplugin); - Chart.plugins.notify('trigger', [{count: 10}]); - expect(myplugin.count).toBe(10); + var ret = Chart.plugins.notify(chart, 'hook'); + expect(ret).toBeTruthy(); + expect(results).toEqual([4, 5, 6, 1, 2, 3]); }); it('should return TRUE if no plugin explicitly returns FALSE', function() { - Chart.plugins.register({ - check: function() {} + var chart = window.acquireChart({ + plugins: [{ + hook: function() {} + }, { + hook: function() { + return null; + } + }, { + hook: function() { + return 0; + } + }, { + hook: function() { + return true; + } + }, { + hook: function() { + return 1; + } + }] }); - Chart.plugins.register({ - check: function() { - return; - } + + var plugins = chart.config.plugins; + plugins.forEach(function(plugin) { + spyOn(plugin, 'hook').and.callThrough(); }); - Chart.plugins.register({ - check: function() { - return null; - } + + var ret = Chart.plugins.notify(chart, 'hook'); + expect(ret).toBeTruthy(); + plugins.forEach(function(plugin) { + expect(plugin.hook).toHaveBeenCalled(); }); - Chart.plugins.register({ - check: function() { - return 42; - } + }); + + it('should return FALSE if any plugin explicitly returns FALSE', function() { + var chart = window.acquireChart({ + plugins: [{ + hook: function() {} + }, { + hook: function() { + return null; + } + }, { + hook: function() { + return false; + } + }, { + hook: function() { + return 42; + } + }, { + hook: function() { + return 'bar'; + } + }] }); - var res = Chart.plugins.notify('check'); - expect(res).toBeTruthy(); + + var plugins = chart.config.plugins; + plugins.forEach(function(plugin) { + spyOn(plugin, 'hook').and.callThrough(); + }); + + var ret = Chart.plugins.notify(chart, 'hook'); + expect(ret).toBeFalsy(); + expect(plugins[0].hook).toHaveBeenCalled(); + expect(plugins[1].hook).toHaveBeenCalled(); + expect(plugins[2].hook).toHaveBeenCalled(); + expect(plugins[3].hook).not.toHaveBeenCalled(); + expect(plugins[4].hook).not.toHaveBeenCalled(); }); + }); - it('should return FALSE if no plugin explicitly returns FALSE', function() { - Chart.plugins.register({ - check: function() {} + describe('config.options.plugins', function() { + it('should call plugins with options at last argument', function() { + var plugin = {id: 'foo', hook: function() {}}; + var chart = window.acquireChart({ + options: { + plugins: { + foo: {a: '123'}, + } + } }); - Chart.plugins.register({ - check: function() { - return; + + spyOn(plugin, 'hook'); + + Chart.plugins.register(plugin); + Chart.plugins.notify(chart, 'hook'); + Chart.plugins.notify(chart, 'hook', ['bla']); + Chart.plugins.notify(chart, 'hook', ['bla', 42]); + + expect(plugin.hook.calls.count()).toBe(3); + expect(plugin.hook.calls.argsFor(0)[1]).toEqual({a: '123'}); + expect(plugin.hook.calls.argsFor(1)[2]).toEqual({a: '123'}); + expect(plugin.hook.calls.argsFor(2)[3]).toEqual({a: '123'}); + }); + + it('should call plugins with options associated to their identifier', function() { + var plugins = { + a: {id: 'a', hook: function() {}}, + b: {id: 'b', hook: function() {}}, + c: {id: 'c', hook: function() {}} + }; + + Chart.plugins.register(plugins.a); + + var chart = window.acquireChart({ + plugins: [plugins.b, plugins.c], + options: { + plugins: { + a: {a: '123'}, + b: {b: '456'}, + c: {c: '789'} + } } }); - Chart.plugins.register({ - check: function() { - return false; + + spyOn(plugins.a, 'hook'); + spyOn(plugins.b, 'hook'); + spyOn(plugins.c, 'hook'); + + Chart.plugins.notify(chart, 'hook'); + + expect(plugins.a.hook).toHaveBeenCalled(); + expect(plugins.b.hook).toHaveBeenCalled(); + expect(plugins.c.hook).toHaveBeenCalled(); + expect(plugins.a.hook.calls.first().args[1]).toEqual({a: '123'}); + expect(plugins.b.hook.calls.first().args[1]).toEqual({b: '456'}); + expect(plugins.c.hook.calls.first().args[1]).toEqual({c: '789'}); + }); + + it('should not called plugins when config.options.plugins.{id} is FALSE', function() { + var plugins = { + a: {id: 'a', hook: function() {}}, + b: {id: 'b', hook: function() {}}, + c: {id: 'c', hook: function() {}} + }; + + Chart.plugins.register(plugins.a); + + var chart = window.acquireChart({ + plugins: [plugins.b, plugins.c], + options: { + plugins: { + a: false, + b: false + } } }); - Chart.plugins.register({ - check: function() { - return 42; + + spyOn(plugins.a, 'hook'); + spyOn(plugins.b, 'hook'); + spyOn(plugins.c, 'hook'); + + Chart.plugins.notify(chart, 'hook'); + + expect(plugins.a.hook).not.toHaveBeenCalled(); + expect(plugins.b.hook).not.toHaveBeenCalled(); + expect(plugins.c.hook).toHaveBeenCalled(); + }); + + it('should call plugins with default options when plugin options is TRUE', function() { + var plugin = {id: 'a', hook: function() {}}; + + Chart.defaults.global.plugins.a = {a: 42}; + Chart.plugins.register(plugin); + + var chart = window.acquireChart({ + options: { + plugins: { + a: true + } } }); - var res = Chart.plugins.notify('check'); - expect(res).toBeFalsy(); + + spyOn(plugin, 'hook'); + + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({a: 42}); + }); + + + it('should call plugins with default options if plugin config options is undefined', function() { + var plugin = {id: 'a', hook: function() {}}; + + Chart.defaults.global.plugins.a = {a: 'foobar'}; + Chart.plugins.register(plugin); + spyOn(plugin, 'hook'); + + var chart = window.acquireChart(); + + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({a: 'foobar'}); }); }); }); diff --git a/test/core.title.tests.js b/test/core.title.tests.js index 7046052781e..f334a1c6c67 100644 --- a/test/core.title.tests.js +++ b/test/core.title.tests.js @@ -207,4 +207,66 @@ describe('Title block tests', function() { args: [] }]); }); + + describe('config update', function() { + it ('should update the options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + title: { + display: true + } + } + }); + expect(chart.titleBlock.options.display).toBe(true); + + chart.options.title.display = false; + chart.update(); + expect(chart.titleBlock.options.display).toBe(false); + }); + + it ('should remove the title if the new options are false', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + } + }); + expect(chart.titleBlock).not.toBe(undefined); + + chart.options.title = false; + chart.update(); + expect(chart.titleBlock).toBe(undefined); + }); + + it ('should create the title if the title options are changed to exist', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + title: false + } + }); + expect(chart.titleBlock).toBe(undefined); + + chart.options.title = {}; + chart.update(); + expect(chart.titleBlock).not.toBe(undefined); + expect(chart.titleBlock.options).toEqual(jasmine.objectContaining(Chart.defaults.global.title)); + }); + }); }); diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 1a2d8fb0bbc..d96dfa302d4 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -113,7 +113,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.x).toBeCloseToPixel(263); expect(tooltip._view.y).toBeCloseToPixel(155); }); @@ -310,7 +310,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.x).toBeCloseToPixel(263); expect(tooltip._view.y).toBeCloseToPixel(312); }); @@ -459,7 +459,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(216); + expect(tooltip._view.x).toBeCloseToPixel(211); expect(tooltip._view.y).toBeCloseToPixel(190); }); @@ -539,7 +539,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.x).toBeCloseToPixel(263); expect(tooltip._view.y).toBeCloseToPixel(155); }); diff --git a/test/element.point.tests.js b/test/element.point.tests.js index 61fe3d09a4e..01eba3046ef 100644 --- a/test/element.point.tests.js +++ b/test/element.point.tests.js @@ -208,6 +208,30 @@ describe('Point element tests', function() { args: [] }]); + var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); + var drawRoundedRectangle = Chart.helpers.drawRoundedRectangle; + var offset = point._view.radius / Math.SQRT2; + Chart.helpers.drawRoundedRectangle = drawRoundedRectangleSpy; + mockContext.resetCalls(); + point._view.pointStyle = 'rectRounded'; + point.draw(); + + expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( + mockContext, + 10 - offset, + 15 - offset, + Math.SQRT2 * 2, + Math.SQRT2 * 2, + 2 / 2 + ); + expect(mockContext.getCalls()).toContain( + jasmine.objectContaining({ + name: 'fill', + args: [], + }) + ); + + Chart.helpers.drawRoundedRectangle = drawRoundedRectangle; mockContext.resetCalls(); point._view.pointStyle = 'rectRot'; point.draw(); diff --git a/test/element.rectangle.tests.js b/test/element.rectangle.tests.js index b833862bd6f..e72117f5a36 100644 --- a/test/element.rectangle.tests.js +++ b/test/element.rectangle.tests.js @@ -207,10 +207,10 @@ describe('Rectangle element tests', function() { args: [8.5, 0] }, { name: 'lineTo', - args: [8.5, 15.5] + args: [8.5, 14.5] // This is a minus bar. Not 15.5 }, { name: 'lineTo', - args: [11.5, 15.5] + args: [11.5, 14.5] }, { name: 'lineTo', args: [11.5, 0] diff --git a/test/mockContext.js b/test/mockContext.js index d065f2a7ad9..0d12384778d 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -274,6 +274,7 @@ var canvas = document.createElement('canvas'); var chart, key; + config = config || {}; options = options || {}; options.canvas = options.canvas || {height: 512, width: 512}; options.wrapper = options.wrapper || {class: 'chartjs-wrapper'}; diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js new file mode 100644 index 00000000000..a022cc75d5a --- /dev/null +++ b/test/platform.dom.tests.js @@ -0,0 +1,369 @@ +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 = { + afterEvent: 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); + + // Relative Position + expect(notifiedEvent.x).toBe(chart.chart.width / 2); + expect(notifiedEvent.y).toBe(chart.chart.height / 2); + }); + }); +}); diff --git a/test/scale.category.tests.js b/test/scale.category.tests.js index 13955d35be4..c474dd9c7fd 100644 --- a/test/scale.category.tests.js +++ b/test/scale.category.tests.js @@ -36,7 +36,7 @@ describe('Category scale tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below @@ -161,305 +161,226 @@ describe('Category scale tests', function() { }); it ('Should get the correct pixel for a value when horizontal', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, - id: scaleID - }); - - var minSize = scale.update(600, 100); - - expect(scale.width).toBe(600); - expect(scale.height).toBe(28); - expect(scale.paddingTop).toBe(0); - expect(scale.paddingBottom).toBe(0); - expect(scale.paddingLeft).toBe(28); - expect(scale.paddingRight).toBe(48); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 600, - height: 28, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom' + }], + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 605; - scale.bottom = 33; + var xScale = chart.scales.xScale0; + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(23); + expect(xScale.getValueForPixel(33)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(85); - expect(scale.getValueForPixel(33)).toBe(0); - expect(scale.getValueForPixel(85)).toBe(0); + expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(487); + expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(487); + expect(xScale.getValueForPixel(487)).toBe(4); - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(452); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(505); - expect(scale.getValueForPixel(453)).toBe(4); - expect(scale.getValueForPixel(505)).toBe(4); + xScale.options.gridLines.offsetGridLines = true; - config.gridLines.offsetGridLines = false; + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(69); + expect(xScale.getValueForPixel(33)).toBe(0); + expect(xScale.getValueForPixel(78)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(33); - expect(scale.getValueForPixel(33)).toBe(0); - - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(557); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(557); - expect(scale.getValueForPixel(557)).toBe(4); + expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(395); + expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(441); + expect(xScale.getValueForPixel(397)).toBe(4); + expect(xScale.getValueForPixel(441)).toBe(4); }); it ('Should get the correct pixel for a value when there are repeated labels', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick1', 'tick3', 'tick3', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, - id: scaleID - }); - - var minSize = scale.update(600, 100); - - expect(scale.width).toBe(600); - expect(scale.height).toBe(28); - expect(scale.paddingTop).toBe(0); - expect(scale.paddingBottom).toBe(0); - expect(scale.paddingLeft).toBe(28); - expect(scale.paddingRight).toBe(48); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 600, - height: 28, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom' + }], + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 605; - scale.bottom = 33; - - expect(scale.getPixelForValue('tick_1', 1, 0, false)).toBe(138); - expect(scale.getPixelForValue('tick_1', 1, 0, true)).toBe(190); + var xScale = chart.scales.xScale0; + expect(xScale.getPixelForValue('tick_1', 0, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue('tick_1', 1, 0, false)).toBeCloseToPixel(139); }); it ('Should get the correct pixel for a value when horizontal and zoomed', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - config.ticks.min = 'tick2'; - config.ticks.max = 'tick4'; - - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, - id: scaleID - }); - - var minSize = scale.update(600, 100); - - expect(scale.width).toBe(600); - expect(scale.height).toBe(28); - expect(scale.paddingTop).toBe(0); - expect(scale.paddingBottom).toBe(0); - expect(scale.paddingLeft).toBe(28); - expect(scale.paddingRight).toBe(28); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 600, - height: 28, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom', + ticks: { + min: 'tick2', + max: 'tick4' + } + }], + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 605; - scale.bottom = 33; - - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(124); + var xScale = chart.scales.xScale0; + expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(23); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(396); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(486); + expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(496); + expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(496); - config.gridLines.offsetGridLines = false; + xScale.options.gridLines.offsetGridLines = true; - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(33); + expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(102); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(577); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(577); + expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(338); + expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(419); }); it ('should get the correct pixel for a value when vertical', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - config.position = 'left'; - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: ['3', '5', '1', '4', '2'] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + yLabels: ['1', '2', '3', '4', '5'] }, - id: scaleID - }); - - var minSize = scale.update(100, 200); - - expect(scale.width).toBe(100); - expect(scale.height).toBe(200); - expect(scale.paddingTop).toBe(6); - expect(scale.paddingBottom).toBe(6); - expect(scale.paddingLeft).toBe(0); - expect(scale.paddingRight).toBe(0); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 100, - height: 200, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom', + }], + yAxes: [{ + id: 'yScale0', + type: 'category', + position: 'left' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 105; - scale.bottom = 205; + var yScale = chart.scales.yScale0; + expect(yScale.getPixelForValue(0, 0, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 0, 0, true)).toBe(32); + expect(yScale.getValueForPixel(32)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(30); - expect(scale.getValueForPixel(11)).toBe(0); - expect(scale.getValueForPixel(30)).toBe(0); + expect(yScale.getPixelForValue(0, 4, 0, false)).toBe(484); + expect(yScale.getPixelForValue(0, 4, 0, true)).toBe(484); + expect(yScale.getValueForPixel(484)).toBe(4); - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(161); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(180); - expect(scale.getValueForPixel(162)).toBe(4); + yScale.options.gridLines.offsetGridLines = true; - config.gridLines.offsetGridLines = false; + expect(yScale.getPixelForValue(0, 0, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 0, 0, true)).toBe(77); + expect(yScale.getValueForPixel(32)).toBe(0); + expect(yScale.getValueForPixel(77)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(11); - expect(scale.getValueForPixel(11)).toBe(0); - - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(199); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(199); - expect(scale.getValueForPixel(199)).toBe(4); + expect(yScale.getPixelForValue(0, 4, 0, false)).toBe(394); + expect(yScale.getPixelForValue(0, 4, 0, true)).toBe(439); + expect(yScale.getValueForPixel(394)).toBe(4); + expect(yScale.getValueForPixel(439)).toBe(4); }); it ('should get the correct pixel for a value when vertical and zoomed', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - config.ticks.min = 'tick2'; - config.ticks.max = 'tick4'; - config.position = 'left'; - - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: ['3', '5', '1', '4', '2'] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + yLabels: ['1', '2', '3', '4', '5'] }, - id: scaleID - }); - - var minSize = scale.update(100, 200); - - expect(scale.width).toBe(70); - expect(scale.height).toBe(200); - expect(scale.paddingTop).toBe(6); - expect(scale.paddingBottom).toBe(6); - expect(scale.paddingLeft).toBe(0); - expect(scale.paddingRight).toBe(0); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 70, - height: 200, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom', + }], + yAxes: [{ + id: 'yScale0', + type: 'category', + position: 'left', + ticks: { + min: '2', + max: '4' + } + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 75; - scale.bottom = 205; + var yScale = chart.scales.yScale0; - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(42); + expect(yScale.getPixelForValue(0, 1, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 1, 0, true)).toBe(32); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(136); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(168); + expect(yScale.getPixelForValue(0, 3, 0, false)).toBe(484); + expect(yScale.getPixelForValue(0, 3, 0, true)).toBe(484); - config.gridLines.offsetGridLines = false; + yScale.options.gridLines.offsetGridLines = true; - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(11); + expect(yScale.getPixelForValue(0, 1, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 1, 0, true)).toBe(107); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(199); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(199); + expect(yScale.getPixelForValue(0, 3, 0, false)).toBe(333); + expect(yScale.getPixelForValue(0, 3, 0, true)).toBe(409); }); }); diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 7102a6f4a8c..44c4dcf29e3 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -34,7 +34,7 @@ describe('Linear Scale', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this work nicer, then check below @@ -660,12 +660,12 @@ describe('Linear Scale', function() { var xScale = chart.scales.xScale0; expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(501); // right - paddingRight - expect(xScale.getPixelForValue(-1, 0, 0)).toBeCloseToPixel(41); // left + paddingLeft - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(271); // halfway*/ + expect(xScale.getPixelForValue(-1, 0, 0)).toBeCloseToPixel(31); // left + paddingLeft + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(266); // halfway*/ expect(xScale.getValueForPixel(501)).toBeCloseTo(1, 1e-2); - expect(xScale.getValueForPixel(41)).toBeCloseTo(-1, 1e-2); - expect(xScale.getValueForPixel(271)).toBeCloseTo(0, 1e-2); + expect(xScale.getValueForPixel(31)).toBeCloseTo(-1, 1e-2); + expect(xScale.getValueForPixel(266)).toBeCloseTo(0, 1e-2); var yScale = chart.scales.yScale0; expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(32); // right - paddingRight @@ -718,8 +718,8 @@ describe('Linear Scale', function() { expect(xScale.paddingTop).toBeCloseToPixel(0); expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(0); - expect(xScale.paddingRight).toBeCloseToPixel(13.5); - expect(xScale.width).toBeCloseToPixel(471); + expect(xScale.paddingRight).toBeCloseToPixel(0); + expect(xScale.width).toBeCloseToPixel(468); expect(xScale.height).toBeCloseToPixel(28); var yScale = chart.scales.yScale0; @@ -727,7 +727,7 @@ describe('Linear Scale', function() { expect(yScale.paddingBottom).toBeCloseToPixel(0); expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); - expect(yScale.width).toBeCloseToPixel(41); + expect(yScale.width).toBeCloseToPixel(30); expect(yScale.height).toBeCloseToPixel(452); // Extra size when scale label showing @@ -738,15 +738,15 @@ describe('Linear Scale', function() { expect(xScale.paddingTop).toBeCloseToPixel(0); expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(0); - expect(xScale.paddingRight).toBeCloseToPixel(13.5); - expect(xScale.width).toBeCloseToPixel(453); + expect(xScale.paddingRight).toBeCloseToPixel(0); + expect(xScale.width).toBeCloseToPixel(450); expect(xScale.height).toBeCloseToPixel(46); expect(yScale.paddingTop).toBeCloseToPixel(0); expect(yScale.paddingBottom).toBeCloseToPixel(0); expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); - expect(yScale.width).toBeCloseToPixel(59); + expect(yScale.width).toBeCloseToPixel(48); expect(yScale.height).toBeCloseToPixel(434); }); diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index 9636a0b375c..016737c71ee 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -33,7 +33,7 @@ describe('Logarithmic Scale tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below @@ -77,15 +77,13 @@ describe('Logarithmic Scale tests', function() { }, { id: 'yScale1', type: 'logarithmic' - }, - { - id: 'yScale2', - type: 'logarithmic' - }, - { - id: 'yScale3', - type: 'logarithmic' - }] + }, { + id: 'yScale2', + type: 'logarithmic' + }, { + id: 'yScale3', + type: 'logarithmic' + }] } } }); @@ -140,11 +138,10 @@ describe('Logarithmic Scale tests', function() { }, { id: 'yScale2', type: 'logarithmic' - }, - { - id: 'yScale3', - type: 'logarithmic' - }] + }, { + id: 'yScale3', + type: 'logarithmic' + }] } } }); @@ -343,7 +340,7 @@ describe('Logarithmic Scale tests', function() { data: [10, 5, 1, 5, 78, 100] }, { yAxisID: 'yScale1', - data: [-1000, 1000], + data: [0, 1000], }, { type: 'bar', yAxisID: 'yScale0', @@ -383,7 +380,7 @@ describe('Logarithmic Scale tests', function() { type: 'bar' }, { yAxisID: 'yScale1', - data: [-1000, 1000], + data: [0, 1000], type: 'bar' }, { yAxisID: 'yScale0', @@ -717,14 +714,14 @@ describe('Logarithmic Scale tests', function() { }); var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight - expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(48); // left + paddingLeft - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(283); // halfway - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. + expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(482); // right - paddingRight + expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(37); // left + paddingLeft + expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(270); // halfway + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(37); // 0 is invalid, put it on the left. - expect(xScale.getValueForPixel(495)).toBeCloseTo(80, 1e-4); + expect(xScale.getValueForPixel(481.5)).toBeCloseToPixel(80); expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(283)).toBeCloseTo(10, 1e-4); + expect(xScale.getValueForPixel(270)).toBeCloseTo(10, 1e-4); var yScale = chart.scales.yScale; expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index 7ac95ac39a6..69d6c167f92 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -48,7 +48,7 @@ describe('Test the radial linear scale', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, showLabelBackdrop: true, display: true, @@ -342,9 +342,9 @@ describe('Test the radial linear scale', function() { } }); - expect(chart.scale.drawingArea).toBe(225); - expect(chart.scale.xCenter).toBe(256); - expect(chart.scale.yCenter).toBe(272); + expect(chart.scale.drawingArea).toBe(233); + expect(chart.scale.xCenter).toBe(247); + expect(chart.scale.yCenter).toBe(280); }); it('should correctly get the label for a given data index', function() { @@ -390,16 +390,16 @@ describe('Test the radial linear scale', function() { }); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(0); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(225); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(233); expect(chart.scale.getPointPositionForValue(1, 5)).toEqual({ - x: 269, - y: 268, + x: 261, + y: 275, }); chart.scale.options.reverse = true; chart.update(); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(225); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(233); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(0); }); @@ -431,14 +431,14 @@ describe('Test the radial linear scale', function() { var slice = 72; // (360 / 5) for (var i = 0; i < 5; i++) { - expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe(15 + (slice * i) - 90); + expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe(15 + (slice * i)); } chart.options.startAngle = 0; chart.update(); for (var x = 0; x < 5; x++) { - expect(radToNearestDegree(chart.scale.getIndexAngle(x))).toBe((slice * x) - 90); + expect(radToNearestDegree(chart.scale.getIndexAngle(x))).toBe((slice * x)); } }); }); diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index 896ff00d4f7..ba2ef2f8a2b 100755 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -58,7 +58,7 @@ describe('Time scale tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below, @@ -406,11 +406,11 @@ describe('Time scale tests', function() { var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(78); + expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(71); expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(452); - expect(xScale.getPixelForValue('2015-01-01T20:00:00')).toBeCloseToPixel(78); + expect(xScale.getPixelForValue('2015-01-01T20:00:00')).toBeCloseToPixel(71); - expect(xScale.getValueForPixel(78)).toBeCloseToTime({ + expect(xScale.getValueForPixel(71)).toBeCloseToTime({ value: moment(chart.data.labels[0]), unit: 'hour', threshold: 0.75 @@ -428,7 +428,7 @@ describe('Time scale tests', function() { datasets: [{ xAxisID: 'xScale0', yAxisID: 'yScale0', - data: [] + data: [null, 10, 3] }], labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, @@ -449,6 +449,7 @@ describe('Time scale tests', function() { }); var xScale = chart.scales.xScale0; + expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); });