From ffbdb483105c750c836fde32501f65d9577ddc83 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Wed, 1 Nov 2017 08:00:10 -0700 Subject: [PATCH 01/56] Upgrade dependencies (incl. ESLint 4) (#4738) --- .codeclimate.yml | 2 +- .eslintrc | 8 ++------ package.json | 27 ++++++++++++++------------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index fcc885c82..bd3237643 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -6,7 +6,7 @@ engines: - javascript eslint: enabled: true - channel: "eslint-3" + channel: "eslint-4" fixme: enabled: true ratings: diff --git a/.eslintrc b/.eslintrc index 6a31e4b6e..c0e1b26ce 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,3 @@ -ecmaFeatures: - modules: true - jsx: true - env: amd: true browser: true @@ -72,7 +68,7 @@ rules: no-lone-blocks: 2 no-loop-func: 2 no-magic-number: 0 - no-multi-spaces: 2 + no-multi-spaces: [2, {ignoreEOLComments: true}] no-multi-str: 2 no-native-reassign: 2 no-new-func: 2 @@ -141,7 +137,7 @@ rules: func-style: 0 id-length: 0 id-match: 0 - indent: [2, tab] + indent: [2, tab, {flatTernaryExpressions: true}] jsx-quotes: 0 key-spacing: 2 keyword-spacing: 2 diff --git a/package.json b/package.json index a7a504887..83987fd12 100644 --- a/package.json +++ b/package.json @@ -10,30 +10,31 @@ "url": "https://github.com/chartjs/Chart.js.git" }, "devDependencies": { - "browserify": "^14.3.0", - "browserify-istanbul": "^2.0.0", - "bundle-collapser": "^1.2.1", + "browserify": "^14.5.0", + "browserify-istanbul": "^3.0.1", + "bundle-collapser": "^1.3.0", "child-process-promise": "^2.2.1", - "coveralls": "^2.13.1", - "gitbook-cli": "^2.3.0", + "coveralls": "^3.0.0", + "eslint": "^4.9.0", + "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", "gulp-connect": "~5.0.0", - "gulp-eslint": "^3.0.1", + "gulp-eslint": "^4.0.0", "gulp-file": "^0.3.0", "gulp-html-validator": "^0.0.5", "gulp-insert": "~0.5.0", - "gulp-replace": "^0.5.4", + "gulp-replace": "^0.6.1", "gulp-size": "~2.1.0", "gulp-streamify": "^1.0.2", "gulp-uglify": "~3.0.x", "gulp-util": "~3.0.x", "gulp-zip": "~4.0.0", - "jasmine": "^2.6.0", - "jasmine-core": "^2.6.2", - "karma": "^1.7.0", + "jasmine": "^2.8.0", + "jasmine-core": "^2.8.0", + "karma": "^1.7.1", "karma-browserify": "^5.1.1", - "karma-chrome-launcher": "^2.1.1", + "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", @@ -42,13 +43,13 @@ "pixelmatch": "^4.0.2", "vinyl-source-stream": "^1.1.0", "watchify": "^3.9.0", - "yargs": "^8.0.1" + "yargs": "^9.0.1" }, "spm": { "main": "Chart.js" }, "dependencies": { "chartjs-color": "~2.2.0", - "moment": "~2.18.0" + "moment": "~2.19.1" } } From 7f751c8d8082e204269945958e41d72c5d2f7d52 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 1 Nov 2017 16:00:41 +0100 Subject: [PATCH 02/56] Suppress coveralls errors if run from fork (#4699) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 862d8b044..9b38ab3c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: - gulp docs - gulp package - gulp bower - - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls + - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls || true notifications: slack: chartjs:pcfCZR6ugg5TEcaLtmIfQYuA From 34709826cd05cb6bacd2150726768bd619b7c765 Mon Sep 17 00:00:00 2001 From: Aspaldiko Date: Thu, 2 Nov 2017 16:51:36 +0400 Subject: [PATCH 03/56] Fix incorrect samples titles (#4914) --- samples/scales/gridlines-display.html | 2 +- samples/scales/gridlines-style.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/scales/gridlines-display.html b/samples/scales/gridlines-display.html index 0c2dc114e..c21469329 100644 --- a/samples/scales/gridlines-display.html +++ b/samples/scales/gridlines-display.html @@ -2,7 +2,7 @@ - Suggested Min/Max Settings + Grid Lines Display Settings + + + +
+ +
+ + + + + diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 5fd4b2d6a..17e6c3bfa 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -45,17 +45,21 @@ module.exports = function(Chart) { function updateConfig(chart) { var newOptions = chart.options; - // Update Scale(s) with options - if (newOptions.scale) { - chart.scale.options = newOptions.scale; - } else if (newOptions.scales) { - newOptions.scales.xAxes.concat(newOptions.scales.yAxes).forEach(function(scaleOptions) { - chart.scales[scaleOptions.id].options = scaleOptions; - }); - } + helpers.each(chart.scales, function(scale) { + Chart.layoutService.removeBox(chart, scale); + }); + newOptions = helpers.configMerge( + Chart.defaults.global, + Chart.defaults[chart.config.type], + newOptions); + + chart.options = chart.config.options = newOptions; + chart.ensureScalesHaveIDs(); + chart.buildOrUpdateScales(); // Tooltip chart.tooltip._options = newOptions.tooltips; + chart.tooltip.initialize(); } function positionIsHorizontal(position) { @@ -143,7 +147,7 @@ module.exports = function(Chart) { // Make sure scales have IDs and are built before we build any controllers. me.ensureScalesHaveIDs(); - me.buildScales(); + me.buildOrUpdateScales(); me.initToolTip(); // After init plugin notification @@ -223,11 +227,15 @@ module.exports = function(Chart) { /** * Builds a map of scale ID to scale object for future lookup. */ - buildScales: function() { + buildOrUpdateScales: function() { var me = this; var options = me.options; - var scales = me.scales = {}; + var scales = me.scales || {}; var items = []; + var updated = Object.keys(scales).reduce(function(obj, id) { + obj[id] = false; + return obj; + }, {}); if (options.scales) { items = items.concat( @@ -251,24 +259,35 @@ module.exports = function(Chart) { helpers.each(items, function(item) { var scaleOptions = item.options; + var id = scaleOptions.id; var scaleType = helpers.valueOrDefault(scaleOptions.type, item.dtype); - var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); - if (!scaleClass) { - return; - } if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { scaleOptions.position = item.dposition; } - var scale = new scaleClass({ - id: scaleOptions.id, - options: scaleOptions, - ctx: me.ctx, - chart: me - }); + updated[id] = true; + var scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + scale.options = scaleOptions; + scale.ctx = me.ctx; + scale.chart = me; + } else { + var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); + if (!scaleClass) { + return; + } + scale = new scaleClass({ + id: id, + type: scaleType, + options: scaleOptions, + ctx: me.ctx, + chart: me + }); + scales[scale.id] = scale; + } - scales[scale.id] = scale; scale.mergeTicksOptions(); // TODO(SB): I think we should be able to remove this custom case (options.scale) @@ -278,6 +297,14 @@ module.exports = function(Chart) { me.scale = scale; } }); + // clear up discarded scales + helpers.each(updated, function(hasUpdated, id) { + if (!hasUpdated) { + delete scales[id]; + } + }); + + me.scales = scales; Chart.scaleService.addScalesToLayout(this); }, @@ -301,6 +328,7 @@ module.exports = function(Chart) { if (meta.controller) { meta.controller.updateIndex(datasetIndex); + meta.controller.linkScales(); } else { var ControllerClass = Chart.controllers[meta.type]; if (ControllerClass === undefined) { diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 67dbe27f2..ee6158c35 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -111,10 +111,10 @@ module.exports = function(Chart) { var meta = me.getMeta(); var dataset = me.getDataset(); - if (meta.xAxisID === null) { + if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; } - if (meta.yAxisID === null) { + if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; } }, diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index b2615f14e..1ca699bc1 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -775,6 +775,38 @@ describe('Chart', function() { }); describe('config update', function() { + it ('should update options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + chart.options = { + responsive: false, + scales: { + yAxes: [{ + ticks: { + min: 0, + 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 scales options', function() { var chart = acquireChart({ type: 'line', @@ -798,6 +830,79 @@ describe('Chart', function() { expect(yScale.options.ticks.max).toBe(10); }); + it ('should update scales options from new object', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + var newScalesConfig = { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + }; + chart.options.scales = newScalesConfig; + + 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 remove discarded scale', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true, + scales: { + yAxes: [{ + id: 'yAxis0', + ticks: { + min: 0, + max: 10 + } + }] + } + } + }); + + var newScalesConfig = { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + }; + chart.options.scales = newScalesConfig; + + chart.update(); + + var yScale = chart.scales.yAxis0; + expect(yScale).toBeUndefined(); + var newyScale = chart.scales['y-axis-0']; + expect(newyScale.options.ticks.min).toBe(0); + expect(newyScale.options.ticks.max).toBe(10); + }); + it ('should update tooltip options', function() { var chart = acquireChart({ type: 'line', From 9a7182ba36d3a9f1e73f2d2cdc0011ae7837d672 Mon Sep 17 00:00:00 2001 From: beiz23 Date: Thu, 30 Nov 2017 22:41:32 +0900 Subject: [PATCH 17/56] Fix typos and broken links in the docs (#5010) --- docs/SUMMARY.md | 1 + docs/axes/cartesian/README.md | 6 +++--- docs/axes/cartesian/linear.md | 4 ++-- docs/axes/radial/linear.md | 4 ++-- docs/axes/styling.md | 4 ++-- docs/charts/line.md | 2 +- docs/configuration/tooltip.md | 2 +- docs/developers/axes.md | 4 ++-- docs/getting-started/README.md | 2 +- docs/getting-started/installation.md | 2 +- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 606300900..8591dad86 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -7,6 +7,7 @@ * [Usage](getting-started/usage.md) * [General](general/README.md) * [Responsive](general/responsive.md) + * [Pixel Ratio](general/device-pixel-ratio.md) * [Interactions](general/interactions/README.md) * [Events](general/interactions/events.md) * [Modes](general/interactions/modes.md) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 64ce32073..8518da9f2 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -75,13 +75,13 @@ var myChart = new Chart(ctx, { data: { datasets: [{ data: [20, 50, 100, 75, 25, 0], - label: 'Left dataset' + label: 'Left dataset', // This binds the dataset to the left y axis yAxisID: 'left-y-axis' }, { - data: [0.1, 0.5, 1.0, 2.0, 1.5, 0] - label: 'Right dataset' + data: [0.1, 0.5, 1.0, 2.0, 1.5, 0], + label: 'Right dataset', // This binds the dataset to the right y axis yAxisID: 'right-y-axis', diff --git a/docs/axes/cartesian/linear.md b/docs/axes/cartesian/linear.md index 00f7aced1..1d0292074 100644 --- a/docs/axes/cartesian/linear.md +++ b/docs/axes/cartesian/linear.md @@ -20,7 +20,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -43,7 +43,7 @@ let chart = new Chart(ctx, { scales: { yAxes: [{ ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } }] diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index 1636781a8..adebe77cc 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -36,7 +36,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -58,7 +58,7 @@ let chart = new Chart(ctx, { options: { scale: { ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } } diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 1de31e01f..f60afd9bb 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -35,8 +35,8 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). | `reverse` | `Boolean` | `false` | Reverses order of tick labels. -| `minor` | `object` | `{}` | Minor ticks configuration. Ommited options are inherited from options above. -| `major` | `object` | `{}` | Major ticks configuration. Ommited options are inherited from options above. +| `minor` | `object` | `{}` | Minor ticks configuration. Omitted options are inherited from options above. +| `major` | `object` | `{}` | Major ticks configuration. Omitted options are inherited from options above. ## Minor Tick Configuration The minorTick configuration is nested under the ticks configuration in the `minor` key. It defines options for the minor tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. diff --git a/docs/charts/line.md b/docs/charts/line.md index 40441b7af..90471e462 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -119,7 +119,7 @@ The `data` property of a dataset for a line chart can be passed in two formats. data: [20, 10] ``` -When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#Category Axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. +When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#category-cartesian-axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. ### Point[] diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 51004843a..1be2c26bc 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -30,7 +30,7 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | `footerFontSize` | `Number` | `12` | Footer font size | `footerFontStyle` | `String` | `'bold'` | Footer font style | `footerFontColor` | `Color` | `'#fff'` | Footer font color -| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each fotter line. +| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each footer line. | `footerMarginTop` | `Number` | `6` | Margin to add before drawing the footer. | `xPadding` | `Number` | `6` | Padding to add on left and right of tooltip. | `yPadding` | `Number` | `6` | Padding to add on top and bottom of tooltip. diff --git a/docs/developers/axes.md b/docs/developers/axes.md index 8d120195a..7d6c74e32 100644 --- a/docs/developers/axes.md +++ b/docs/developers/axes.md @@ -78,14 +78,14 @@ To work with Chart.js, custom scale types must implement the following interface // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param index: index into the ticks array - // @param includeOffset: if true, get the pixel halway between the given tick and the next + // @param includeOffset: if true, get the pixel halfway between the given tick and the next getPixelForTick: function(index, includeOffset) {}, // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param value : the value to get the pixel for // @param index : index into the data array of the value // @param datasetIndex : index of the dataset the value comes from - // @param includeOffset : if true, get the pixel halway between the given tick and the next + // @param includeOffset : if true, get the pixel halfway between the given tick and the next getPixelForValue: function(value, index, datasetIndex, includeOffset) {} // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 9266b3099..bff52c72d 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -40,4 +40,4 @@ var chart = new Chart(ctx, { It's that easy to get started using Chart.js! From here you can explore the many options that can help you customise your charts with scales, tooltips, labels, colors, custom actions, and much more. -There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attatched to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file +There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attached to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index c8291d260..613d7aa7a 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -36,7 +36,7 @@ https://www.jsdelivr.com/package/npm/chart.js?path=dist You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). -If you download or clone the repository, you must [build](../developers/contributing.md#building-chartjs) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. +If you download or clone the repository, you must [build](../developers/contributing.md#building-and-testing) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. # Selecting the Correct Build From b835df02cd05830fb734806f3af27fa43cc5674c Mon Sep 17 00:00:00 2001 From: JohnShaft Date: Fri, 1 Dec 2017 18:56:49 +0100 Subject: [PATCH 18/56] Add Angular2+ libraries for Chart.js in docs (#5006) --- docs/notes/extensions.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 9b17418e2..0998641ba 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -25,7 +25,12 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ## Integrations -### Angular +### Angular (v2+) + + - emn178/angular2-chartjs + - valor-software/ng2-charts + +### Angular (v1) - angular-chart.js - tc-angular-chartjs - angular-chartjs From 15d1056b537a07b613f6dfe56fc920390c1dd267 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 2 Dec 2017 12:38:36 +0100 Subject: [PATCH 19/56] Implement equally sized bars (#4994) When `barThickness: undefined|null` (default), we compute an optimal sample size based on the smallest tick interval reduced to prevent any bar to overlap (bar equally sized). Also added support for a special `barThickness: 'flex'` value (previous default) that globally arranges bars side by side to prevent any gap when percentage options are 1 (variable bar sizes). --- .gitignore | 1 + src/controllers/controller.bar.js | 150 +++++++++++++----- test/.eslintrc | 1 + .../bar-thickness-absolute.json | 42 +++++ .../controller.bar/bar-thickness-absolute.png | Bin 0 -> 5055 bytes .../bar-thickness-flex-offset.json | 42 +++++ .../bar-thickness-flex-offset.png | Bin 0 -> 4583 bytes .../controller.bar/bar-thickness-flex.json | 41 +++++ .../controller.bar/bar-thickness-flex.png | Bin 0 -> 5095 bytes .../controller.bar/bar-thickness-max.json | 41 +++++ .../controller.bar/bar-thickness-max.png | Bin 0 -> 4421 bytes .../bar-thickness-min-interval.json | 40 +++++ .../bar-thickness-min-interval.png | Bin 0 -> 5180 bytes .../bar-thickness-multiple.json | 46 ++++++ .../controller.bar/bar-thickness-multiple.png | Bin 0 -> 5847 bytes .../bar-thickness-no-overlap.json | 46 ++++++ .../bar-thickness-no-overlap.png | Bin 0 -> 4211 bytes .../controller.bar/bar-thickness-offset.json | 47 ++++++ .../controller.bar/bar-thickness-offset.png | Bin 0 -> 6577 bytes .../bar-thickness-single-xy.json | 40 +++++ .../bar-thickness-single-xy.png | Bin 0 -> 4514 bytes .../controller.bar/bar-thickness-single.json | 43 +++++ .../controller.bar/bar-thickness-single.png | Bin 0 -> 4374 bytes .../controller.bar/bar-thickness-stacked.json | 48 ++++++ .../controller.bar/bar-thickness-stacked.png | Bin 0 -> 5586 bytes test/jasmine.utils.js | 3 - test/specs/controller.bar.tests.js | 82 +--------- 27 files changed, 586 insertions(+), 127 deletions(-) create mode 100644 test/fixtures/controller.bar/bar-thickness-absolute.json create mode 100644 test/fixtures/controller.bar/bar-thickness-absolute.png create mode 100644 test/fixtures/controller.bar/bar-thickness-flex-offset.json create mode 100644 test/fixtures/controller.bar/bar-thickness-flex-offset.png create mode 100644 test/fixtures/controller.bar/bar-thickness-flex.json create mode 100644 test/fixtures/controller.bar/bar-thickness-flex.png create mode 100644 test/fixtures/controller.bar/bar-thickness-max.json create mode 100644 test/fixtures/controller.bar/bar-thickness-max.png create mode 100644 test/fixtures/controller.bar/bar-thickness-min-interval.json create mode 100644 test/fixtures/controller.bar/bar-thickness-min-interval.png create mode 100644 test/fixtures/controller.bar/bar-thickness-multiple.json create mode 100644 test/fixtures/controller.bar/bar-thickness-multiple.png create mode 100644 test/fixtures/controller.bar/bar-thickness-no-overlap.json create mode 100644 test/fixtures/controller.bar/bar-thickness-no-overlap.png create mode 100644 test/fixtures/controller.bar/bar-thickness-offset.json create mode 100644 test/fixtures/controller.bar/bar-thickness-offset.png create mode 100644 test/fixtures/controller.bar/bar-thickness-single-xy.json create mode 100644 test/fixtures/controller.bar/bar-thickness-single-xy.png create mode 100644 test/fixtures/controller.bar/bar-thickness-single.json create mode 100644 test/fixtures/controller.bar/bar-thickness-single.png create mode 100644 test/fixtures/controller.bar/bar-thickness-stacked.json create mode 100644 test/fixtures/controller.bar/bar-thickness-stacked.png diff --git a/.gitignore b/.gitignore index 53ce8fedb..cfb878a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ bower.json *.log *.swp +*.stackdump diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index b811c6f05..ff2b56ae5 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -95,6 +95,93 @@ defaults._set('horizontalBar', { } }); +/** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ +function computeMinSampleSize(scale, pixels) { + var min = scale.isHorizontal() ? scale.width : scale.height; + var ticks = scale.getTicks(); + var prev, curr, i, ilen; + + for (i = 1, ilen = pixels.length; i < ilen; ++i) { + min = Math.min(min, pixels[i] - pixels[i - 1]); + } + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + min = i > 0 ? Math.min(min, curr - prev) : min; + prev = curr; + } + + return min; +} + +/** + * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, + * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This + * mode currently always generates bars equally sized (until we introduce scriptable options?). + * @private + */ +function computeFitCategoryTraits(index, ruler, options) { + var thickness = options.barThickness; + var count = ruler.stackCount; + var curr = ruler.pixels[index]; + var size, ratio; + + if (helpers.isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * count; + ratio = 1; + } + + return { + chunk: size / count, + ratio: ratio, + start: curr - (size / 2) + }; +} + +/** + * Computes an "optimal" category that globally arranges bars side by side (no gap when + * percentage options are 1), based on the previous and following categories. This mode + * generates bars with different widths when data are not evenly spaced. + * @private + */ +function computeFlexCategoryTraits(index, ruler, options) { + var pixels = ruler.pixels; + var curr = pixels[index]; + var prev = index > 0 ? pixels[index - 1] : null; + var next = index < pixels.length - 1 ? pixels[index + 1] : null; + var percent = options.categoryPercentage; + var start, size; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale end extremity. + prev = curr - (next === null ? ruler.end - curr : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + + start = curr - ((curr - prev) / 2) * percent; + size = ((next - prev) / 2) * percent; + + return { + chunk: size / ruler.stackCount, + ratio: options.barPercentage, + start: start + }; +} + module.exports = function(Chart) { Chart.controllers.bar = Chart.DatasetController.extend({ @@ -262,17 +349,22 @@ module.exports = function(Chart) { var scale = me.getIndexScale(); var stackCount = me.getStackCount(); var datasetIndex = me.index; - var pixels = []; var isHorizontal = scale.isHorizontal(); var start = isHorizontal ? scale.left : scale.top; var end = start + (isHorizontal ? scale.width : scale.height); - var i, ilen; + var pixels = []; + var i, ilen, min; for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { pixels.push(scale.getPixelForValue(null, i, datasetIndex)); } + min = helpers.isNullOrUndef(scale.options.barThickness) + ? computeMinSampleSize(scale, pixels) + : -1; + return { + min: min, pixels: pixels, start: start, end: end, @@ -332,51 +424,21 @@ module.exports = function(Chart) { calculateBarIndexPixels: function(datasetIndex, index, ruler) { var me = this; var options = ruler.scale.options; - var meta = me.getMeta(); - var stackIndex = me.getStackIndex(datasetIndex, meta.stack); - var pixels = ruler.pixels; - var base = pixels[index]; - var length = pixels.length; - var start = ruler.start; - var end = ruler.end; - var leftSampleSize, rightSampleSize, leftCategorySize, rightCategorySize, fullBarSize, size; + var range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options) + : computeFitCategoryTraits(index, ruler, options); - if (length === 1) { - leftSampleSize = base > start ? base - start : end - base; - rightSampleSize = base < end ? end - base : base - start; - } else { - if (index > 0) { - leftSampleSize = (base - pixels[index - 1]) / 2; - if (index === length - 1) { - rightSampleSize = leftSampleSize; - } - } - if (index < length - 1) { - rightSampleSize = (pixels[index + 1] - base) / 2; - if (index === 0) { - leftSampleSize = rightSampleSize; - } - } - } - - leftCategorySize = leftSampleSize * options.categoryPercentage; - rightCategorySize = rightSampleSize * options.categoryPercentage; - fullBarSize = (leftCategorySize + rightCategorySize) / ruler.stackCount; - size = fullBarSize * options.barPercentage; - - size = Math.min( - helpers.valueOrDefault(options.barThickness, size), - helpers.valueOrDefault(options.maxBarThickness, Infinity)); - - base -= leftCategorySize; - base += fullBarSize * stackIndex; - base += (fullBarSize - size) / 2; + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + helpers.valueOrDefault(options.maxBarThickness, Infinity), + range.chunk * range.ratio); return { - size: size, - base: base, - head: base + size, - center: base + size / 2 + base: center - size / 2, + head: center + size / 2, + center: center, + size: size }; }, diff --git a/test/.eslintrc b/test/.eslintrc index 8e8f899bf..9d98c4528 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -11,3 +11,4 @@ globals: rules: # Best Practices complexity: 0 + max-statements: 0 diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.json b/test/fixtures/controller.bar/bar-thickness-absolute.json new file mode 100644 index 000000000..599b090d6 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-absolute.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2019", "2024", "2025"], + "datasets": [{ + "backgroundColor": "rgba(255, 99, 132, 0.5)", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": 128, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.png b/test/fixtures/controller.bar/bar-thickness-absolute.png new file mode 100644 index 0000000000000000000000000000000000000000..40172b39241f2a6e13d201f7a47509137acc468e GIT binary patch literal 5055 zcmeHLe^gU-8-MQQA{Pi8>=>pT*g%F!uR}A%6F+XTcSFf(?|F;fw^xU!Q)HUUOg4nU zm$D{epsxp)S!~)lQc$2$NV3L`l|UHlgi1Na29$^}_XBYZ*w%N}@f^!PJN?uCxaZ#I zJkR&@e7?_lew|yfK`e?0TMz~S5o>Y;{{kTKCjs;v{QCMpPXPe?tu=zj{w?2RHD*-3 zTrh9XV6On}+1nL+73+n`aTzjy^9*HdyPx`SC;)5HD z={wm`ezzq=nl^~C*y)FEyRw8cx}ZCb2nj^mfJjRx!*4U0A`f7(^Wjxd7?;t<1l$IK z{AZ?=d|r9Hx7!h;)?HWjxRhC6m@;R*C0j#QmX=PBSV!G$#p@Hm#k=m};$)TaQXTD8 zYN@)vcDuU&0n;O;ritD&lx4w_yMz5jvxKB9Pqj9>l%e{`<&z0GVJ%4%tWs|v6k?IHDIupoPKSQA8i*#(-Pa^e7MC;b5e5=ToBsk z+;tQy7y6Ya*CP0s=T`rl^`P4u{TOZ!hCQb;q`I)e_V`HQj8q-tx5fc}Co~x~!wOx^~4+RjEEF z@in$!H4a;n5921q zNS3G_<8wgvb&*aYgdWw86{~oV9BGSdV58ybZHE+Bxkf%@ehkF11Ma7jVWfM!aUmPE z4VAd~iC`1TZ%7#EdEeaCk_^e=;{#*SsFC_0)J zfep#tj8B5R9LEYN2kI)WFRP439j_?Hq)8x6k{whhLQJSL*1&<0&e6@!MkD1hMK7NR z(sPCH$0tBvmy_aiA(*pxl?df8$4U+Tqy)WgxXgsoYw1H-}q<-`n&Fs6`eU*h&JI;=)6F~KLi(@mTSyp2aQyh9G_ybxW&e`a$ zW}%e2XI(4F4KZs9x#WSLJ*T!%mgn{?^{UyZGNgYpo&2swRI-p{iM-80p-f(!<7JwO zbvqgIQci$wH0kLq--#bN*aT<*&vxRGGww zm5bU~M5i-OEriSAmL{lQz(D;_CqEl6JIdr}G-&tCV(LXJ7D#I+^xyBo@a?!j=*A1`Q*2oMZJ@Ykdc zoVE3Y?(+Zf QFFJrV{}c-vAJ2dN7yOH(lK=n! literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.json b/test/fixtures/controller.bar/bar-thickness-flex-offset.json new file mode 100644 index 000000000..1776b07be --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex-offset.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.png b/test/fixtures/controller.bar/bar-thickness-flex-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..e20cc4eb450c8f90b94da145252619ba60cec024 GIT binary patch literal 4583 zcmeHK>r)d~6hC{j;0A-)Xtj!nOAwV7LA2JOJQfI`#p3(XN`)#dK1oEyAWC*)%R_C{ zX~$@dQroe9utht>X2%I@XKM#PQNCD%Izl(dEc>tQKnfjy!re&S2G0tx`jY@xf;Lg5h ziy|8?{d8f}X(nc^n5?a6xNyVo^736ZpG?nDSG(R_d3l|&Ew{9+tJ|)gaQ?@xVrRYX z%ouk`x0*1VE0Gu~`|h1^w6xq8I6>-Bww_45o=mf*Xo_+zo0m`~^(q;mdwMm9HVq5W zDSzx__mAV?_o1<@*ugMD#}ZFCU6N0TeE&_^tr@Q6pfp$KCsytk;CTh**+M(@JlL=7 zTdFZx1u$7F`-8*`G-bQ7N=5o@0=RgVmj@6ct>C{%;bCLe(j8I?+6&c==H*E=Eq$;c z$Rc&4(5>30AbFu2IG0~N#02v^1jFsw+{9!ac9k3b$w1Vx_)X=EDvC0y^6D*81V%W2 zWx*H{jk}gvuG=_cAes&e`IeXCQM-72uZn00;k3WZqVpXG!KM0-1oI*u+JG=yXYpWC zGaH%bLj>!Z42@|94{`dt3MQ*mfYZC)N?}ch5!J&-47?nP3B5Dx%n(t5St~e?l1@WG zqjB;%q5w-y%Ko$!VX*%8a51a%Lx=+2w(8|@+?6r$ufcRx5)WDDzZ%Nam7zs-cx4iY z!0Pn}D}?$1(nub}-AV?!C~**FCe7>DMG!SoqpW-@h)pckbM7jJTpPl|waHHd8T-cq z=ypbI)*1mFL2jZbF%&gXdu{|1iOK_qwF*)UdXBMs1IRIsgUlmW_A+%Jp=V`UwndtP zk*_gT6VZ*@T}SRtX7spHzH+k0P$Vq7SIrqmV1XyS z7MLU#p-@!%10|k98+kGJGU-9+Xpr!596KCwNYm>Sq8}kS4VICt4&l_5p2|*=U%{v% z-iRG6+HhBN!-EX=v_Ga0xO)-j3PFqUpKbLQF&9>|ZY=2w0RvEU!dMRK2GG;!7fQad z<|q?4hJz686N-*PLT9BN4Q1kHW8JPbBR7YkjmWQCD+US{-28L>qaAoWXs&;#HP$MHLyI{m%-R^Of%n||?qd^a6pD}wchMGUe$I$ytn>iO= zJ%wW;Un(6Za*O9y_KQ2rY9cdNOTsdE$h|qTEer>n-IEQ|qt4nGp{lrP1723G1=Puo zFN-VS!C6OKIu8%Wzv@%);qd?BgS?P3)?zn#nJtFOZM|1I@rD@ucxj1345p|(^Ye{1 Z$qf1fHLEYRwyyyEW~OH9tCDjpe*@UW)UW^m literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-flex.json b/test/fixtures/controller.bar/bar-thickness-flex.json new file mode 100644 index 000000000..0bef9db18 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex.png b/test/fixtures/controller.bar/bar-thickness-flex.png new file mode 100644 index 0000000000000000000000000000000000000000..791a29d25d380f64f8c33fc3a928cebc531348cb GIT binary patch literal 5095 zcmeHLZB!Fy7QQn~U;;=d1Q4o_k?@hO;HG}CAW#QI0*D~h^{XhMrAp6g0jnhn!UU*A zMLTKV*Y4S6{>+?tC+~gl z^E~&y&zqwgA{Fy2U$q2)d10Z!Zvv3`NC0h)x5k3Iy8*=K!-9i0sdf%Mdj0&Ku+1S| zX=0_NuQdp>MAi9fb?ge8Y{q?;T0S+rjUoT4_z)*L(aop z2^3Qt&OyYAcg~yEaZoFM`Qs3hR7xZASK`3J#(^%Hy%)kkwNvxsE%5a6GCz{Gc4NUj z{Pp`OLK^0q5fsxF%E6xO{%D1e<||Zz3km8+n&y3aE=^nVF)1DjP^&3Q&%GA%tC;ua zfuu`3ScnuqpV>jB5p8Slf532{ZXIxkm@GSzWE+e8L>!yRL(aYUTmO1(h6`6p38G5$ zjl$WTg}m;GB#KFpvhZVW@9<*S_xKbBHMeEJtkSXn3Pth9)fi?EF4X7xv$GW#J-W^% z56Ha~#eFgM=&*o0@BfLE*K?Sw#!fmV)H`!PP?2H=&InVDOGq6Iy7v1a9K4CX zkQwh}Pf$OLM+HiqRBT+0a0&l&a1ka^fj<+>0oAnhcO?D2XQ!1uP2n-~5f4e?mvElI zoI{FT4ixd=d#trV#A#0Okg%-uiWY7i*u9Tp9Fa8ZqPM>uLDZI*qim{9^3X;l)EcF1 z3Y)_NUHI-M6jzjfK0lQgF=<5*ogOd87vK~p~Yg77)a}T>&YvM8Uw_ zr!*@s;K5PW7}|j}$5{V6yf;O`Fp|zDoyL^cw~eQWkgEA%%|W=<83i}YF$BT5-9C#k z%J|>Ixbvye`6Q{wBnZj}b6ya4=wUQTcFK0Xf{OJaNzEPAu#RL~zibk5N|qqx8;+gS zVVT&)Jgt@dh(JjaQdT!R|BHaqDK=_ZNb+;)+gpmeie zfTQLby$`Az zWTa-=hUVWlT{JBxh|%}DpBP+-Sa{OoFtLSX!#|JfL0DBExm(dZ*+Oc#3-%UBT~@qf zV-!42;b8xC&FmTcBuZ@18IY|}mzuLzZ6W%y^@1o4zFX|I4YSH|_-({=vQKA5YTB<3 ztwMy-PS+ah6-cI`cJ1t^kaPdtHWe;^XIvA)#8v-x`D0?v;LN~d<6hHyh~|Uu>PG^x zoa%0R|DvV1q9qMCu*k+N*EwK<aXJb=Df1XUJc1N< z2EHGi;SS~doqS5MC2ZMxfd?7m4VG?iB_pHq&?C_^)V;$N6jzhco`Kv?E>9jw%}0hu zl8?VwOfZ$VtHuz~y6c4kic=>L8`ysw%fkK?v^2NdlOOhH;qq*|@d1K==4fh1Dz+q5 z{#y@Yg}iLXkrSRQblZRC#e!w96;C#8UWg@UT^MJ#irC+Q!X4^(UWUEwjrEVFpGdI; zQy-NZ!y&kT>7D=-o3<*Rz;N5aF7#qIi`*AjS4;}$sQmAzk}(N72X2<7j~_ZbJCQo@ z`OM#I5;J~%F*T4Fe(`S}WJWwHJKsw`AhE5Lf=-t{oz(HeJEo~a^~UN02UDe^+i?8b zy859Vc;B&aTZSiGNIT_&;>`JB%knztEAWPu0qy%J)bHx7MW3PeY+LLtD82ow-Y0`N zh*vGGguYs=zlLwU6$}H?<&JNMao{D|k<3BNS1qb6CxZVXkoI zI>THS{Xf_yI@??GGfeYuZ!}HoN79YjMAJ-)Y4)7)=guyzB;&=Mfq{V z-am61eQ{TXfYM7NY!+2z;h`$1W!YVHw7pJIsw^m0UbA+`-TP?Yi&oX|+RhE%9c-C2k+BcE(g}j+G{(bM|dkU0*=yDXU$wMJ$8|-_*W_ z*7l9<90Xi{%8>pT$k{Hi2YqW&=~(t3XkO^>{RYIYC?_v=Bc2h`s(DalH!Y$*#6bSC zLpKg07y&)PL_)`2Wgk(j|U))qTcBS_g7zUW+e zu;JL!ti)|@1On((7M;NS65I--PMN|6igCaug_1bL@vQ+_p@ZV>Gx0AlCMNyq-JJW* zx#yjHKA&^$JDILbUM+f31OTg3UQlEJAiN}idJI20t9o?+;>S}I@|R0gQ}@34tac|| z(YKf)%;5Y6wT!6*0qd zK%fahovO{?bc8j}#}>&ENzn{z3x|ya;a*4j(NJNU?&i%b4Dv=MJ(2-YkoBApzsqsZ z;>B+_q_f}`e{F*=ADGa1yF+p8`y52ReK<3mKMh&dxQ$SjND%1@<4vjV6iAYLn)g8! z2sPOI_Eo!$6b0LPA5tVB+0l#UXZgcvDB^u6KM}>)MSoq(iOxZF+tEd zF~^}SnItVCyIk*Bj2v7za9BhjFN8GECnf_i?0gazw;(dUor8rxZ-&zg$ryeDM4lGY z@V%@eD1*Tm47}?vlu7V$*{bfDA#90h`(QhrC1v0}*@wBZRd{D(U6(2Jr(g!Q4d31L zBTa*@?U-85S*D77p`T-wEc|vkP0HF%V;oD#g9tIYH`Wr!UB>5=SXe*jXP^jl-3|S% z1X1-x%t;{B7;R-*oPG3*qbF#_whH67%9n<%xo0TaF7UW|%JdH;ChnZ6Q(bq>e=BJ- zn1|b(+ULq|9sLr6`n5!Fx+IGQS?YbCgi!mmwB6A&;W7#t+TsF)id*!}j>usWB?Ip{PrivcTmag=#c-blK@(9H_{ei|rlTY?sBrRhDJ=Qdy3s_4nl^g`* zTuxBHFU`O;a6rAH4-A8tj{NETzX^inJqf3Y0|lc7g8)dZ(e#nINTOx2@KCWYPX#RC zQJ9KJ0TVuAnBM>lZX4x;HOWG*r@6*l13&DkQD9P+;ZKRWkyTm=m(&Evu{wM=EZYTn z2J4WOWP9(0y2)syC{BFSxr1bvML^*iFb~IGLYldWvFUI)*dLBnp-6+H8Ke4xr0^gU zSlo@sE3u}vP@|45j^&x_ZdD<;PRFmfXd=A>K{=*-W&4|FVQ}f%RkM(*{VK)Xi~?}9 z5aav_Pl-Gw@}S5=G>;bjpX9n^Gt{YE3yU4UtM!J3Eel7CZ+(oAtvY@vCt)wFf5;em zUoUfb**Yg0S4hjRGtGC3sl0RCqdJ9NSP>nBUzNdN_`vne4Zg;7zcmmtKKaqvh$Zf|gT1?;Qh|(~aWLaB2 o4CS{@OgS=bjRA5^yTwe{B_qn#^L9?!jen0o%1))CDN$4Y7tnsWwEzGB literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.json b/test/fixtures/controller.bar/bar-thickness-min-interval.json new file mode 100644 index 000000000..e5861685c --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-min-interval.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.png b/test/fixtures/controller.bar/bar-thickness-min-interval.png new file mode 100644 index 0000000000000000000000000000000000000000..ae01a9b945c4ef691bd0a861cdce518f6c23e872 GIT binary patch literal 5180 zcmeHLT}V@57=FKRCpl-fJT8Mu%42Q1ewDh=Na{G1wlp0gSW(ML`nhSNprqzHvmYzS zriLYy*bf*5DNz}Px{At}7}mvtEuAS=gtgR`ZEoxANcKZFK{vsbj~rCW)3z zeO&CWIR*(gX|d zW^_9}bzk71gxr=Z0C&+eq?ob{jIb_h5@E|o9>*CE zad4-1!F~x(N75xK24@h0ZR&~*vvIZzggkV|xo}z1#W1jI^hEb~7qg`# zS6(zQUNr(Ce1l&j-<`<1 zuW&%RFvHX?M5yVoi=tEKGk}|c)FfoHurPifCMx&=wJic%^;AwncKdm-T*8wJjuZ4-Urd95b&9nFGPT zq@It%PWvR*C(HK&GywdxOA78D7HK1;By2o)6&6%Z8= z6%Z8=6%ZBpYXwY|$m8uzYFmGB^qv2G!^*(>5rcV6TOvGn{OmhyXJ$Z;$$vZOx%9>1 zXlpV1`r;~r1BKqLm?=&E&5Mk@*a$gFlziMglaCDwJ~m8b38Xu$|AVE$^(4hz?hio> z?`ux;zGlffjEg9Wo1=iLDp2vZNkx4R8*<3ffI^9MyaCcpY*+y&te{zq>ys(2K3r3X zDjJB1M^>J3vmDaHE~;9|K#dhtYRt@|xCaU-`=vR8&6mRw9;lqKTjRYcKw1st>IwTs zu+ehRYQ=^{g}*0faU}lrp&J<*@&%qaQcIhcB6>xO@E37sPv$~s?}p~r1>W`$e=7*u LtlZ4&>Y}o5GpccS literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.json b/test/fixtures/controller.bar/bar-thickness-multiple.json new file mode 100644 index 000000000..fc39849ae --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-multiple.json @@ -0,0 +1,46 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.png b/test/fixtures/controller.bar/bar-thickness-multiple.png new file mode 100644 index 0000000000000000000000000000000000000000..d38405292c3b496ae1414813c6bfd120f83ffc8a GIT binary patch literal 5847 zcmeHLYfuwc6u!GIbupj;96(VMQK3}?v10)-B!wuTLZZ{53egGG+N@1)))r}!getwXq`3F2?@NVfYG`+AUd@CT zQ}={tYNsfw>H<$XNz=4>m8%(8tVpv<)S%*L0R+WDJrc&d%1zHk`unv<^-YGrIq{m& z!&1;;T;Me-9J?x!j~p<#nzVKnGS7XCVp!5O3n$_jmjs=bsPZh?R@f3kiSk4-oT{Ut zXw-2Uj#M<9m%2b~{|q}!mO7EYNazx&Vm_L(k;UD7upOcL&s)MGWX>?VD1c@%A?iSo zH|3TVf@1CgED-r<;p0lR6H(Cc@q8z3sUYu61aM1?Ee|F@2A2ggxhg*w&`chjaD_0p zAxuQek75^4$~aPoQ|YVNEMOJQBu~vE{wg+XfG2z6MhCmAo3kXAgtaH*X9(*+rvrCo zPU8xkU^iO9HtjaB&8u*88#8I2#-tq*ruFUfwNucLjKtR>Z_FOBh$) zx@eSEbPZp#kcGW?efLaFVxMcE+8 zs>bu9OyR^K`QA!q`&uKe+;+W0a*F^R;jQENRKmvuP8K-bOKo>_r-Ll949&xFB6#*n z;Np|HCX0d-J+2kgnlA{Tzsx?haj?idh`b7(&azP`WVU4*vgX)Rb?<5Y)_oTJAF+Xrf8$UEEc%#kNT1e#U zFNwl4CM#AZ?cCjsp z$UCqog9evNf!GS+uCOFJ8`w+fz}Y1L>qJrd;y?|4&)Mji1@e>?_-UzGta7h$Zpn_be%ngfX*^CHtWfIs+x zQQ<}eITlMH!h=EU29)mEX-VWXQZsH7APBc$l`RE(mLSI*Er}o)tcF1=bI?qPc^Lk8 zLXiDAL#Zf99~l*m^(m^Vn@SCae6jph{|uhK*J)HV*__JUSKRVyPZ(&8M9+O?3v!Qx zoD)_yZz&6Wfl*UMOEaAk4csp?%H6>G6&6sn1|sL62ksp!JXhRpXB~FNjyW)ces-0_hGzGg|UKf+WosGaN;5NpL0=ss9!+m7tL_b}MbGt5cIOYLh%~teu6Zs7^Y_Pm z*n%W%sJ}kA9hxn@d}!y0)L-)CWG9CVd4D^*2`3ujpz}+!c3-y?*!xv~%e*4{1yPU6 X8|KBe-5M4ZPf}8qETL*q0C;`9?wd!Y{!UnQH*F?uo$uQO^Qi|!V|hsc`MT&kWG9uouR1^Iv*PL#PKejwmF|U(mX5EM zqNdC}uL_KVn!CAr9)e;xL2A9Q(Nc|A6LBwB!!zt>$Eb|AWx82I*IejU(cUjWcg)ZX zK~gl3IbxX7ruNG7uiIM&!h5a6ZM^WH8j`Z$Vabha!xTbClN3H&!&EXFVr-Qw-#WnI?|bDvr* z6w0Ujd8Ra;kK$_@OzIzSKUMNK>GOfa5VSh;uDpYz%s$GSN`+i3l2r&8Upt(xednSn zpcNAGw&;ha_*69wx<4O&EVv9u46dP&t!8HeoR%rW5F}{q*Fa}d8-Zc8>$V^P1xBtu z=3_Vx_EQ-ikoqG>LvaL62Y&;Z7}8jNP3T1_9cWKd@evJ@=XY?=0VEKdrk`Ok9HkSH z97_TJJXfcKH0@@AlGC6rYoQh=v}lGc?(a>4njU-GZEH}^mMuUO8?YJ}+;Hmq#RSPh z)xAuD`IHr36uS;45p)>G>CEt=Ysnlny=|c5)zkJx1$xpZ~YJ=m$ns8ewUxG`xQxtk76HJM$2#97|;}Bd$uHpfUWIn^WekYxzK*DAq@%oK>WBv#N+rVZxe>RYv zwn*{NW&+stgZPIdF0eb%)M2PhJ*!O0QuwXT)`b1HBq=xsE>%hme_YP$lju~^X+xLU zn6I9DR5};dukX=C1V8ybhbwe4e!13OMqgV>_vA$sy6?)7cLWMVGaJqA8(k)o8L_2B ziB`Lc@}92T8R%OUNmqpk%BX*?_f}kkXFP*}TNDDNl3M4+YQ^kkRfn}A@c&m;aN-5J zRFm>bkFlAfTsUSruXIG!7xOzRDjUM5w9R8HAL}HA;xj8Iag^zTa(D{?hK%;C7%U$f h)L};Qz4VmX;hTBc-E)lg0{oc(5@Hgg+Ye@({Rii=3_t(? literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-offset.json b/test/fixtures/controller.bar/bar-thickness-offset.json new file mode 100644 index 000000000..b31eb739d --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-offset.json @@ -0,0 +1,47 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-offset.png b/test/fixtures/controller.bar/bar-thickness-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..8dcecac88a409dff7867684f0987662f1cc4f46e GIT binary patch literal 6577 zcmeI1dr(tX9>>qU1PCCT2rdYLP1YAwQ1A(uLZWCXB4|sisJwiEyU1H4kPw2dB4Y(B zAdN(VrE80{pdvvGk|-h~V0<P+BsAGF;?NZln|8v(uX9xG*@5C3hE3~{-J6a3a+92_J`hGf# z*|}EDqvA<5x~XMNw^ke;J~Cd^TwKr9Pz1{30{S7nKz1scEzii3Ue=t?%ECbKTey2V zVO^tGWyCcSSwusRYy5n!VAJ<5^Yx*jfc4VQe+(u}XPpN{Ml_mXZ0~yDH5d5gJ2bRk zHD4#*+y*AcR?z0bs09#&&6I1d0Po<_!29;@2nPQe1H>#D>avDw)pOulRQfUq?uFq* zW`?UPn(<^6n&H&mfs-^# zL(jg16E|O@2nuca()x>K@BRknpFP}=g zLIj3olwgSJG_x6&47>0+8aqbH+gpVdg}b14b%uQN^gw|Xw!IFZuXvocMv=E9)h_x_ z=$$FnWU$T+2otoo3TNMLHB9y2o^2&gG=UR@Y9X3&sbjc=pJpY&hv>@&m6QlobFA)1 z9vs+Ghcs)RSKC^XM{}03T!8S7Pl6|@4%}ELdK>_56K8D*{H)b3Sg?Xwbe4he1Jm?J z0R8Y^<1YoUZ`eMBUFB}xb_kIS?{-00hCKuHK8_)SyZdc6!w9-a?b`vW7PfS2nFLEF z{Ljl__%FaCfj8s8%ic4Q9L%C>q$S)VIM6gS(=QjK85>I2$^?@IsE~ESq1L5v!)O*)fitVLD+)`m?FUb zm_d#m}9b--exEpVikI*fd)V33#&Lc6MfZI86^41(R3Pl#svzh^VwKbWs7_9V# zuvE9P{}={L(5!sqj0M5^o_?6{XdZyhf%{0r$pf`s1dy*OueK)FeuNMfIFAUI7jaa!}IN}1L}K+@gk zc-FYj9@^?rLswtD$c+t^EQ4Xm#LRIlB8w1AAkSkG%<>3oHj1>nf6f5^`m#8fuOsx|NhUb2S{7!XxV@MZQuUgqy)Yc6Rhkh zaWEVE92ITaD!TsF-MGOcG~+8d+syBaE3U$L2}B=fBKl??iDqo|3oaaVRXn)Vm@#`L zI6E*`YWkXcB)>;#C@(uS_`!;5?@GtR@VJ}3Yfz#E{%$}(dp(qHdMp@z{{)4oKa;*N zUzTHDp%Uv;iK4@Sc~j3z1T8}1A3l6aiz>LmRpe2|mO|L|9a*w~)~2CZ<-t+=;)H*S zwH-B*081*8O!ktB zZDP_Wj?sreB0fs{E3GwxhmvAUa}zBJ%A!#%qWb^LwkTEujvN6PZZ-(xhmWecmG^2T zbmk-a%xro>zKP=}s*H@2s`pSwGp6ns7v{>3muivi5LQ3c7t0 z8N_<>urPrVhFCG34WU!z_-oG;VC9~>jaFI;s=lMN{bXYa+%2S%E)2eSMeDKsmdZxBxZRYclMZED0)lfrdm0Wc` zg@C*O@~6T7q90hf2|AN!$LQ-F|Ce7JZNY-7ps9{GQu2tr42drlgYVK`A0>|JEQ zI{#Opt)DIb#f~7Z;n==uNAfI=(!$#}p-j7=b>wpwrwT6fU$b#qv5qPKJC1aH_G=MK fy92n%3ZcHs(Z24L4=Kp67!bZGYGdPu#H0TJN7H47 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.json b/test/fixtures/controller.bar/bar-thickness-single-xy.json new file mode 100644 index 000000000..76caa37fa --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-single-xy.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [{"x": "2022", "y": 42}] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.png b/test/fixtures/controller.bar/bar-thickness-single-xy.png new file mode 100644 index 0000000000000000000000000000000000000000..26171e531a2b87dd1fcfb8014cb437a531fbe1c4 GIT binary patch literal 4514 zcmeI0`%e^C6vxlK49iGanFX=RW0_r~+7*z{ip63aV0jxvx*%;qC)NfVP$FmwvC6EZ z3L>~z*GD3SL`gw;Y}1w$f^@ID?9<=Qb;#mn#} zAZ3ahH+uH;xrIhzV^=Lxloe`-T! zy#*+GnXSh>PVz{v&^XpsJpbqNsj-oq$+8qy}0O1*@dHpJ+~Iv^*{8~9>) zg5-iJALB|Sw{Iw~lai(FEYq~J?S zwMo}u8p%VpP+ExO56(CO$q^XmSmBRJPh%n$=IiWJtZ)rTRnuurByzlt>yEB~-EK_N z(UOB!HP-3ZR{cIYfaJyI1m+fsp*zjpp=iSVcjZK=a*;ghZ@$fzF=Gb=@L#%-4MUOD zwA=J>6B{r`W1o>0j50@34qxFqkb#y!Z6vDhO193l3icxTo04j)KaxjVVw0^Q)@bHG z#XR}iE84AZ1P79nrZ{5^p0Fd!*h}spxxt*yjAH)ZB~8UpHR;mG(v_tvOIN;i)GNYo^I(l4m zT{D(7UJz`k#Z_D@Zp`}?Dq**vLdbO54PlvV3OW$1mnwAsL-x&&P4=lD-n8j?-{*Os z-~YL!$EaSmBt-CnfDjU*RAlB5LgOn%0w>|)d(-7Lga~_-nXl$mti0>G@Wtw%LwL@} zJvX}rBN->}Ww48Os16UNEAkUgFP9$us{xnMu?+KeSMNK{pm~X)^seCb8fBI?V@;IWm954q8B|@o{@U}lr-?yLG zV()y+3lz$NWF-28+#qJVrV-OPtm*^usPJUCMJ#~{nIFtFvYl8^CIg5oq#cS z@!?b;?tIyi8KR?k*5~h58 z-a>c9Y@%>S5!kfnrT5x=1>p%{bdDQK;PV6v<14DR&LbTGN21`P6t{|eSe_dWpfOV= zOrMFRH_Dp29RkL9X%?<+3zm`FL36M+&&2*L8V~Mv&Ei7UMkDG}L|yN`DCHOb(36M# z35dF8@-Hj1skLEexI8i9#45+W4LQDo7@H(X(+tgKSY|l`&XMq<4PpN0#w3u!J%UgTD|In&k*KqpwzW-YeLK&EFPq8QXteC4Oei>7^-4T zT9T5geJ_QplMY1PPaXd?m$%68|D{R zGFKsvvzi6O@%0V>bxDyB(@}b%+cj@$DA{)lGZE${piC>_)`XZ!1`lSuY(uCW4n$oM zYbEE7TIy`%;wdv%C;bq2gmn$Zk+O^kp@fmV!BC2&?opMmARN8Kc5r~}Fl!-qSzCX^ zBlG~tAjBztjR8b`iUvT-L`aBG^b12<{9y;#b!|GTojC)PmS%2EhzY57T??I&gHTs% z5cRn*HMvpHxip8A{;uNc`fHTRgMB3^!apvanVbG`;U5?Ne;3dG&GL^6|G4=7#)b3Y9D-L8dmEzmI>>Vurr}-0 d)*jl+zg+8GC)Z!M<9!EFE?$=TO-Au2{{nTa>rMaw literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.json b/test/fixtures/controller.bar/bar-thickness-stacked.json new file mode 100644 index 000000000..aa2d82553 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-stacked.json @@ -0,0 +1,48 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "stacked": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "stacked": true, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.png b/test/fixtures/controller.bar/bar-thickness-stacked.png new file mode 100644 index 0000000000000000000000000000000000000000..696829ee39b400f7bd77dbc1b930784d3cf42542 GIT binary patch literal 5586 zcmeI0dr(tn7QoNFbD=k&B?Jq!IBS4_Aj(5$&~7QYAYcMnL+g%itJ`2@mSvYUUF{6C zJaWTQ9#u3vEViIQZO83)6_qNW0>Lh8k@`s0;v)oHr6`mTKn)O*>`APYcK_W!c02ha zljJ++e&5MC=lh*=QkJ@Q&3v~P+yG#{Sd{Q80Er(7pk48;Q*|i=;JFXQ330#9d1K^O z$7he;i90{Gy=q?QIq&tUM{bD&CKfbiX_;le(_VO;^IpK`Sy9yH{`lX{d*~sd55DA* z-B`NwN}{_qldPcs?(KO`K*cNftSmk?FqnUOh3t~f{LzRdX0UE_*j!t(axgaA$L7gr zSRhFGXBvyl^0>RkY_8FNv$AEpbM&aL*6WFIgB#6;b$rF);#bWUE*Ey+@!qGzrzm<; zC?AeeD;7I8*&W9~O!3CIT~Wa0zx4?V&AhMb9C|UX1Yz{us(l^>xXB)p>`#u|?$9&( zQj}&(b7IEN@vyDS!O%W-V0W*#p|`PnzqEZh&uTDw=5P@47AYREZNB7Jx4G~|438Z}Z| zlhK>Q_^cSR4s|C!ap^9M%DJU*suFG_k@db9y0=6a`ri}B_E4H=9&~9o{R>| z9bv(}n=e`U@w1n7E_ z&;JlAcCY`KFFbW$)9!ix(^B!vAriiuo~ zgxJdheT_^DbP%Gjf{R94ruXx?Pf~Q%s)6y`mvXEiihhWWQI}MFCQ1*DK|JWWpj2`07RpQ zlSrrM{?GEuG9i52D?e-WlG&Of^G{SMCaqH2H6(^E;^o)aZN@X*!!oncyg%&f;j=Ba zUTOY!p=#ki%NpC=lIS zKu7YUdKLGoOqr9_Yx-=L)h*_k#cdPSoI1-LYyy1U*iQuztnF-oLhZmdPv{T>C8!GG z!v|Mtg+8!O43YqXTdJ?4SqK8MvJ3l!6s+q9b^t9YaN(*bCY}dnUH41eX?DD5(lu=f zhZW!RV;Ovv3Lv2RrU!y!V2V4t7sVv-!1ICxVjez^tEo>;aIXH(`oY+vpES#@+P<02 zGv6A^JPKx{)_#k$F6|dKYoPk9`q1H6E=Vn$oRmunn^xMI*V3+du_Drs(?N#z02m5wzn2peY=h;W9A2KbRQR}R)G}$7O=`MKd52qj+~s-ZlTyWS_ho&i+H4%mw-{C| zQW%W6ZFlu+p(xDDadUAK-7Og?EP8eRqb-YOF8cUzn@e-0G?VI^M@dM}|(h{*(OaTm^XQ#oWnR88RySo|AA~lbUo!H7tv)e%czLx`W~z#9~oC903IqLx@H;2%A?h>R2c0A{DbvmRD%oX6T+FB Date: Sat, 2 Dec 2017 15:24:57 +0100 Subject: [PATCH 20/56] Fix issue #4928: linear tick generator doesn't round values to needed precision. (#4943) * Fix issue 4928 - linear tick generator doesn't round values to needed precision. * Improve: replace toPrecision() in toString() to improve readability. * Fix: logarithmic tick generator doesn't round values to needed precision. * Fix: rounding tick values didn't work for negative values. * Add: Core ticks tests --- src/core/core.ticks.js | 13 +++-- test/specs/core.ticks.tests.js | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 test/specs/core.ticks.tests.js diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 2dbc27474..655369a17 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -80,10 +80,15 @@ module.exports = { numSpaces = Math.ceil(numSpaces); } - // Put the values into the ticks array + var precision = 1; + if (spacing < 1) { + precision = Math.pow(10, spacing.toString().length - 2); + niceMin = Math.round(niceMin * precision) / precision; + niceMax = Math.round(niceMax * precision) / precision; + } ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); for (var j = 1; j < numSpaces; ++j) { - ticks.push(niceMin + (j * spacing)); + ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); } ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); @@ -121,6 +126,7 @@ module.exports = { exp = Math.floor(helpers.log10(tickVal)); significand = Math.floor(tickVal / Math.pow(10, exp)); } + var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; do { ticks.push(tickVal); @@ -129,9 +135,10 @@ module.exports = { if (significand === 10) { significand = 1; ++exp; + precision = exp >= 0 ? 1 : precision; } - tickVal = significand * Math.pow(10, exp); + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; } while (exp < endExp || (exp === endExp && significand < endSignificand)); var lastTick = valueOrDefault(generationOptions.max, tickVal); diff --git a/test/specs/core.ticks.tests.js b/test/specs/core.ticks.tests.js new file mode 100644 index 000000000..6d7c2b3c1 --- /dev/null +++ b/test/specs/core.ticks.tests.js @@ -0,0 +1,87 @@ +describe('Test tick generators', function() { + it('Should generate linear spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + legend: { + display: false, + }, + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom', + ticks: { + callback: function(value) { + return value.toString(); + } + } + }], + yAxes: [{ + type: 'linear', + ticks: { + callback: function(value) { + return value.toString(); + } + } + }] + } + } + }); + + var xAxis = chart.scales['x-axis-0']; + var yAxis = chart.scales['y-axis-0']; + + expect(xAxis.ticks).toEqual(['-1', '-0.8', '-0.6', '-0.4', '-0.2', '0', '0.2', '0.4', '0.6', '0.8', '1']); + expect(yAxis.ticks).toEqual(['1', '0.8', '0.6', '0.4', '0.2', '0', '-0.2', '-0.4', '-0.6', '-0.8', '-1']); + }); + + it('Should generate logarithmic spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + legend: { + display: false, + }, + scales: { + xAxes: [{ + type: 'logarithmic', + position: 'bottom', + ticks: { + min: 0.1, + max: 1, + callback: function(value) { + return value.toString(); + } + } + }], + yAxes: [{ + type: 'logarithmic', + ticks: { + min: 0.1, + max: 1, + callback: function(value) { + return value.toString(); + } + } + }] + } + } + }); + + var xAxis = chart.scales['x-axis-0']; + var yAxis = chart.scales['y-axis-0']; + + expect(xAxis.ticks).toEqual(['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); + expect(yAxis.ticks).toEqual(['1', '0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.1']); + }); +}); From 4e47c178e42fe84e40d39f9e2b7e26030cc0fc23 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Wed, 13 Dec 2017 00:43:17 +0100 Subject: [PATCH 21/56] Fix tooltip animation when target changes while animating (#5005) * Fix issue #4989 - tooltip in 'index' mode doesn't animate smoothly. * Change: different approach for smooth tooltip animation in 'index' mode, when target doesn't change. * Fix: jslint error * Fix: remove spyOn pivot from test * Add: setAnimating-flag in transition used to set on tooltip.transition to keep track of tooltip animation. * Decrease code complexity * Revert transition and complexity changes Add: use 'tooltip._start' as workaround check for tooltip animation status --- src/core/core.controller.js | 10 +++++++++- src/core/core.tooltip.js | 28 +++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 17e6c3bfa..6026e8620 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -849,7 +849,15 @@ module.exports = function(Chart) { me._bufferedRequest = null; var changed = me.handleEvent(e); - changed |= tooltip && tooltip.handleEvent(e); + // for smooth tooltip animations issue #4989 + // the tooltip should be the source of change + // Animation check workaround: + // tooltip._start will be null when tooltip isn't animating + if (tooltip) { + changed = tooltip._start + ? tooltip.handleEvent(e) + : changed | tooltip.handleEvent(e); + } plugins.notify(me, 'afterEvent', [e]); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 0072580df..9b09d7604 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -852,25 +852,19 @@ module.exports = function(Chart) { // Remember Last Actives changed = !helpers.arrayEquals(me._active, me._lastActive); - // If tooltip didn't change, do not handle the target event - if (!changed) { - return false; - } + // Only handle target event on tooltip change + if (changed) { + me._lastActive = me._active; - me._lastActive = me._active; + if (options.enabled || options.custom) { + me._eventPosition = { + x: e.x, + y: e.y + }; - if (options.enabled || options.custom) { - me._eventPosition = { - x: e.x, - y: e.y - }; - - var model = me._model; - me.update(true); - me.pivot(); - - // See if our tooltip position changed - changed |= (model.x !== me._model.x) || (model.y !== me._model.y); + me.update(true); + me.pivot(); + } } return changed; From f0bf3954fe363eda86a3db170063aba34f1be08e Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Wed, 13 Dec 2017 00:43:51 +0100 Subject: [PATCH 22/56] Fix issues #4572 and #4703 (#4959) - issue #4572: logarithmic type if all numbers are zero browser crashes "Out of memory" - issue #4703: [BUG] Browser unresponsive on bubble chart with logarithmic axes --- src/scales/scale.logarithmic.js | 59 ++- test/specs/scale.logarithmic.tests.js | 501 ++++++++++++++++++++------ 2 files changed, 437 insertions(+), 123 deletions(-) diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 09296c195..87fe9b1af 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -18,11 +18,9 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var opts = me.options; - var tickOpts = opts.ticks; var chart = me.chart; var data = chart.data; var datasets = data.datasets; - var valueOrDefault = helpers.valueOrDefault; var isHorizontal = me.isHorizontal(); function IDMatches(meta) { return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; @@ -68,27 +66,23 @@ module.exports = function(Chart) { helpers.each(dataset.data, function(rawValue, index) { var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } - values[index] = values[index] || 0; - - if (opts.relativePoints) { - values[index] = 100; - } else { - // Don't need to split positive and negative since the log scale can't handle a 0 crossing - values[index] += value; - } + values[index] += value; }); } }); 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); - me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + if (valuesForType.length > 0) { + var minVal = helpers.min(valuesForType); + var maxVal = helpers.max(valuesForType); + me.min = me.min === null ? minVal : Math.min(me.min, minVal); + me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + } }); } else { @@ -97,7 +91,8 @@ module.exports = function(Chart) { if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } @@ -121,6 +116,17 @@ module.exports = function(Chart) { }); } + // Common base implementation to handle ticks.min, ticks.max + this.handleTickRangeOptions(); + }, + handleTickRangeOptions: function() { + var me = this; + var opts = me.options; + var tickOpts = opts.ticks; + var valueOrDefault = helpers.valueOrDefault; + var DEFAULT_MIN = 1; + var DEFAULT_MAX = 10; + me.min = valueOrDefault(tickOpts.min, me.min); me.max = valueOrDefault(tickOpts.max, me.max); @@ -129,8 +135,25 @@ module.exports = function(Chart) { me.min = Math.pow(10, Math.floor(helpers.log10(me.min)) - 1); me.max = Math.pow(10, Math.floor(helpers.log10(me.max)) + 1); } else { - me.min = 1; - me.max = 10; + me.min = DEFAULT_MIN; + me.max = DEFAULT_MAX; + } + } + if (me.min === null) { + me.min = Math.pow(10, Math.floor(helpers.log10(me.max)) - 1); + } + if (me.max === null) { + me.max = me.min !== 0 + ? Math.pow(10, Math.floor(helpers.log10(me.min)) + 1) + : DEFAULT_MAX; + } + if (me.minNotZero === null) { + if (me.min > 0) { + me.minNotZero = me.min; + } else if (me.max < 1) { + me.minNotZero = Math.pow(10, Math.floor(helpers.log10(me.max))); + } else { + me.minNotZero = DEFAULT_MIN; } } }, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 3fd921977..3d79e6cfc 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -690,142 +690,433 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale1.getLabelForIndex(0, 2)).toBe(150); }); - it('should get the correct pixel value for a point', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - xAxisID: 'xScale', // for the horizontal scale - yAxisID: 'yScale', - data: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}] - }], + describe('when', function() { + var data = [ + { + data: [1, 39], + stack: 'stack' }, - options: { - scales: { - xAxes: [{ - id: 'xScale', - type: 'logarithmic' - }], - yAxes: [{ - id: 'yScale', - type: 'logarithmic' - }] - } + { + data: [1, 39], + stack: 'stack' + }, + ]; + var dataWithEmptyStacks = [ + { + data: [] + }, + { + data: [] } - }); - - var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight - expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(37 + 6); // left + paddingLeft + lineSpace - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(278 + 6 / 2); // halfway - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(37 + 6); // 0 is invalid, put it on the left. - - expect(xScale.getValueForPixel(495)).toBeCloseToPixel(80); - expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(278)).toBeCloseTo(10, 1e-4); - - var yScale = chart.scales.yScale; - expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(246); // halfway - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // 0 is invalid. force it on bottom - - expect(yScale.getValueForPixel(32)).toBeCloseTo(80, 1e-4); - expect(yScale.getValueForPixel(484)).toBeCloseTo(1, 1e-4); - expect(yScale.getValueForPixel(246)).toBeCloseTo(10, 1e-4); - }); - - it('should get the correct pixel value for a point when 0 values are present or min: 0', function() { + ].concat(data); var config = [ { - dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + axis: 'y', + firstTick: 1, // start of the axis (minimum) + describe: 'all stacks are defined' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'y', + scale: { + yAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + scale: { + yAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + firstTick: 1, + describe: 'all stacks are defined' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'x', + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not all stacks are defined and ticks.min: 0' + }, + ]; + config.forEach(function(setup) { + var scaleConfig = {}; + var type, chartStart, chartEnd; + + if (setup.axis === 'x') { + type = 'horizontalBar'; + chartStart = 'left'; + chartEnd = 'right'; + } else { + type = 'bar'; + chartStart = 'bottom'; + chartEnd = 'top'; + } + scaleConfig[setup.axis + 'Axes'] = [{ + type: 'logarithmic' + }]; + Chart.helpers.extend(scaleConfig, setup.scale); + var description = 'dataset has stack option and ' + setup.describe + + ' and axis is "' + setup.axis + '";'; + describe(description, function() { + it('should define the correct axis limits', function() { + var chart = window.acquireChart({ + type: type, + data: { + labels: ['category 1', 'category 2'], + datasets: setup.data || data, + }, + options: { + scales: scaleConfig + } + }); + + var axisID = setup.axis + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = 80; // last tick (should be first available tick after: 2 * 39) + var start = chart.chartArea[chartStart]; + var end = chart.chartArea[chartEnd]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[chartEnd]; + end = chart.chartArea[chartStart]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); + }); + + describe('when', function() { + var config = [ + { + dataset: [], firstTick: 1, // value of the first tick - lastTick: 80 + lastTick: 10, // value of the last tick + describe: 'empty dataset, without ticks.min/max' + }, + { + dataset: [], + scale: {stacked: true}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, without ticks.min/max, with stacked: true' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'bar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'horizontalBar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + dataset: [], + scale: {ticks: {min: 1}}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, ticks.min: 1, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.max: 80, without ticks.min' + }, + { + dataset: [], + scale: {ticks: {max: 0.8}}, + firstTick: 0.01, + lastTick: 0.8, + describe: 'empty dataset, ticks.max: 0.8, without ticks.min' + }, + { + dataset: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 1, y: 1}, max point {x:78, y:78}' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' + } + ]; + axes.forEach(function(axis) { + var expectation = 'min = ' + setup.firstTick + ', max = ' + setup.lastTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); // 0 is invalid, put it at the start. + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); + }); + }); + + describe('when', function() { + var config = [ + { + dataset: [], + scale: {ticks: {min: 0}}, + firstTick: 1, // value of the first tick + lastTick: 10, // value of the last tick + describe: 'empty dataset, ticks.min: 0, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.min: 0, ticks.max: 80' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 0.8}}, + firstTick: 0.1, + lastTick: 0.8, + describe: 'empty dataset, ticks.min: 0, ticks.max: 0.8' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 1.2, y: 1.2}' }, { dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], firstTick: 6, - lastTick: 80 + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 6.3, y: 6.3}' }, { dataset: [{x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], scale: {ticks: {min: 0}}, firstTick: 1, - lastTick: 80 + lastTick: 80, + describe: 'dataset min point {x: 1.2, y: 1.2}, max point {x:78, y:78}, ticks.min: 0' }, { dataset: [{x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], scale: {ticks: {min: 0}}, firstTick: 6, - lastTick: 80 + lastTick: 80, + describe: 'dataset min point {x: 6.3, y: 6.3}, max point {x:78, y:78}, ticks.min: 0' }, ]; - Chart.helpers.each(config, function(setup) { - var xScaleConfig = { - type: 'logarithmic' - }; - var yScaleConfig = { - type: 'logarithmic' - }; - Chart.helpers.extend(xScaleConfig, setup.scale); - Chart.helpers.extend(yScaleConfig, setup.scale); - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - data: setup.dataset - }], - }, - options: { - scales: { - xAxes: [xScaleConfig], - yAxes: [yScaleConfig] - } - } - }); - - var chartArea = chart.chartArea; - var expectations = [ + config.forEach(function(setup) { + var axes = [ { - id: 'x-axis-0', // horizontal scale - axis: 'xAxes', - start: chartArea.left, - end: chartArea.right + id: 'x', // horizontal scale + start: 'left', + end: 'right' }, { - id: 'y-axis-0', // vertical scale - axis: 'yAxes', - start: chartArea.bottom, - end: chartArea.top + id: 'y', // vertical scale + start: 'bottom', + end: 'top' } ]; - Chart.helpers.each(expectations, function(expectation) { - var scale = chart.scales[expectation.id]; - var firstTick = setup.firstTick; - var lastTick = setup.lastTick; - var fontSize = chart.options.defaultFontSize; - var start = expectation.start; - var end = expectation.end; - var sign = scale.isHorizontal() ? 1 : -1; + axes.forEach(function(axis) { + var expectation = 'min = 0, max = ' + setup.lastTick + ', first tick = ' + setup.firstTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); - expect(scale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); - expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start + sign * fontSize); + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var fontSize = chart.options.defaultFontSize; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + var sign = scale.isHorizontal() ? 1 : -1; - expect(scale.getValueForPixel(start)).toBeCloseTo(0); - expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick); - expect(scale.getValueForPixel(start + sign * fontSize)).toBeCloseTo(firstTick); + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start + sign * fontSize); - chart.options.scales[expectation.axis][0].ticks.reverse = true; // Reverse mode - chart.update(); + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start + sign * fontSize)).toBeCloseTo(firstTick, 4); - expect(scale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(end); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(start); - expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(end - sign * fontSize); + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); - expect(scale.getValueForPixel(end)).toBeCloseTo(0); - expect(scale.getValueForPixel(start)).toBeCloseTo(lastTick); - expect(scale.getValueForPixel(end - sign * fontSize)).toBeCloseTo(firstTick); + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start - sign * fontSize, 4); + + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start - sign * fontSize)).toBeCloseTo(firstTick, 4); + }); + }); }); }); }); From f9beedba40217f07169ce9764f4371d40ee72367 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Thu, 14 Dec 2017 16:03:07 +0100 Subject: [PATCH 23/56] Fix issue #5029 (#5041) - infinite loop in generating time axis, due to insufficient bounds checking. --- src/scales/scale.time.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 4fe39b810..cfa9f829a 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -240,7 +240,7 @@ function determineStepSize(min, max, unit, capacity) { var i, ilen, factor; if (!steps) { - return Math.ceil(range / ((capacity || 1) * milliseconds)); + return Math.ceil(range / (capacity * milliseconds)); } for (i = 0, ilen = steps.length; i < ilen; ++i) { @@ -748,7 +748,8 @@ module.exports = function(Chart) { var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height; - return Math.floor(innerWidth / tickLabelWidth); + var capacity = Math.floor(innerWidth / tickLabelWidth); + return capacity > 0 ? capacity : 1; } }); From b04ce949d12870e57e51ea998f5f32a1a94f5f8e Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Fri, 15 Dec 2017 18:10:30 +0100 Subject: [PATCH 24/56] Use time.unit option to create default min/max for empty chart (#5045) --- src/scales/scale.time.js | 7 ++++--- test/specs/scale.time.tests.js | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index cfa9f829a..90904f9e6 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -504,6 +504,7 @@ module.exports = function(Chart) { var me = this; var chart = me.chart; var timeOpts = me.options.time; + var unit = timeOpts.unit || 'day'; var min = MAX_INTEGER; var max = MIN_INTEGER; var timestamps = []; @@ -555,9 +556,9 @@ module.exports = function(Chart) { min = parse(timeOpts.min, me) || min; max = parse(timeOpts.max, me) || max; - // In case there is no valid min/max, let's use today limits - min = min === MAX_INTEGER ? +moment().startOf('day') : min; - max = max === MIN_INTEGER ? +moment().endOf('day') + 1 : max; + // In case there is no valid min/max, set limits based on unit time option + min = min === MAX_INTEGER ? +moment().startOf(unit) : min; + max = max === MIN_INTEGER ? +moment().endOf(unit) + 1 : max; // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 19189040e..e4c1739e6 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -703,7 +703,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2019', '2020', '2025', '2042']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -714,6 +714,19 @@ describe('Time scale tests', function() { expect(scale.max).toEqual(+moment().endOf('day') + 1); expect(getTicksLabels(scale)).toEqual([]); }); + it ('should correctly handle empty `data.labels` using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); describe('is "data"', function() { @@ -784,7 +797,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -796,6 +809,21 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2018', '2020', '2043']); }); + it ('should correctly handle empty `data.labels` and hidden datasets using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + var meta = chart.getDatasetMeta(1); + meta.hidden = true; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); }); From ce1fc28b743d518add5c89653108a287f6aa911d Mon Sep 17 00:00:00 2001 From: Boyi C Date: Tue, 19 Dec 2017 03:24:02 +0800 Subject: [PATCH 25/56] Improve point.xRange and point.yRange performance (#5062) --- src/elements/element.point.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/elements/element.point.js b/src/elements/element.point.js index d5116a259..eab5b31d4 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -24,12 +24,12 @@ defaults._set('global', { function xRange(mouseX) { var vm = this._view; - return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseX - vm.x) < vm.radius + vm.hitRadius) : false; } function yRange(mouseY) { var vm = this._view; - return vm ? (Math.pow(mouseY - vm.y, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; } module.exports = Element.extend({ From 92d033beb2d29fc296aee23b9e9d9c4be1fadd15 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Sat, 23 Dec 2017 14:34:55 +0100 Subject: [PATCH 26/56] Optimization: prevent double ticks array reverse for vertical logarithmic axis (#5076) with ticks option 'reverse: true'. --- src/scales/scale.logarithmic.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 87fe9b1af..372efe7ad 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -161,6 +161,7 @@ module.exports = function(Chart) { var me = this; var opts = me.options; var tickOpts = opts.ticks; + var reverse = !me.isHorizontal(); var generationOptions = { min: tickOpts.min, @@ -168,25 +169,22 @@ module.exports = function(Chart) { }; var ticks = me.ticks = Ticks.generators.logarithmic(generationOptions, me); - if (!me.isHorizontal()) { - // We are in a vertical orientation. The top value is the highest. So reverse the array - ticks.reverse(); - } - // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale me.max = helpers.max(ticks); me.min = helpers.min(ticks); if (tickOpts.reverse) { - ticks.reverse(); - + reverse = !reverse; me.start = me.max; me.end = me.min; } else { me.start = me.min; me.end = me.max; } + if (reverse) { + ticks.reverse(); + } }, convertTicksToLabels: function() { this.tickValues = this.ticks.slice(); From 9874a754e0324cf49df9b2e2a485e44c729eeec3 Mon Sep 17 00:00:00 2001 From: Cameron Childress Date: Fri, 29 Dec 2017 02:52:17 -0500 Subject: [PATCH 27/56] Adding helpful note about legendCallback (#5094) I didn't realize you had to call generateLegend() manually and a quick search tells me that lots of other people have run into this too. --- docs/configuration/legend.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index bce69af79..52bee8687 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -164,3 +164,7 @@ var chart = new Chart(ctx, { } }); ``` + +Note that legendCallback is not called automatically and you must call `generateLegend()` yourself in code when creating a legend using this method. + + From fcd463354bae115b4847272ba44f23c57cc0d6ed Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 31 Dec 2017 15:17:22 +0100 Subject: [PATCH 28/56] Update license year and copyright holders (#5053) --- LICENSE.md | 2 +- gulpfile.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 620db307e..29c941dcc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2017 Nick Downie +Copyright (c) 2018 Chart.js Contributors 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/gulpfile.js b/gulpfile.js index bb42ea9d3..5a3093e44 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -29,7 +29,7 @@ var header = "/*!\n" + " * http://chartjs.org/\n" + " * Version: {{ version }}\n" + " *\n" + - " * Copyright 2017 Nick Downie\n" + + " * Copyright " + (new Date().getFullYear()) + " Chart.js Contributors\n" + " * Released under the MIT license\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; From 33c7d941f7caa5363f68ac2135a66d7c2328e196 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 6 Jan 2018 09:59:47 -0800 Subject: [PATCH 29/56] Add back Chart.Ticks.formatters (#5088) --- src/chart.js | 1 + src/core/core.ticks.js | 141 -------------------------------- src/scales/scale.linearbase.js | 58 ++++++++++++- src/scales/scale.logarithmic.js | 54 +++++++++++- test/specs/core.ticks.tests.js | 9 ++ 5 files changed, 119 insertions(+), 144 deletions(-) diff --git a/src/chart.js b/src/chart.js index df17bb92c..8bd497614 100644 --- a/src/chart.js +++ b/src/chart.js @@ -13,6 +13,7 @@ Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); Chart.platform = require('./platforms/platform'); +Chart.Ticks = require('./core/core.ticks'); require('./core/core.plugin')(Chart); require('./core/core.animation')(Chart); diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 655369a17..3898842a4 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -7,147 +7,6 @@ var helpers = require('../helpers/index'); * @namespace Chart.Ticks */ module.exports = { - /** - * Namespace to hold generators for different types of ticks - * @namespace Chart.Ticks.generators - */ - generators: { - /** - * Interface for the options provided to the numeric tick generator - * @interface INumericTickGenerationOptions - */ - /** - * The maximum number of ticks to display - * @name INumericTickGenerationOptions#maxTicks - * @type Number - */ - /** - * The distance between each tick. - * @name INumericTickGenerationOptions#stepSize - * @type Number - * @optional - */ - /** - * Forced minimum for the ticks. If not specified, the minimum of the data range is used to calculate the tick minimum - * @name INumericTickGenerationOptions#min - * @type Number - * @optional - */ - /** - * The maximum value of the ticks. If not specified, the maximum of the data range is used to calculate the tick maximum - * @name INumericTickGenerationOptions#max - * @type Number - * @optional - */ - - /** - * Generate a set of linear ticks - * @method Chart.Ticks.generators.linear - * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks - * @param dataRange {IRange} the range of the data - * @returns {Array} array of tick values - */ - linear: function(generationOptions, dataRange) { - var ticks = []; - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - var spacing; - if (generationOptions.stepSize && generationOptions.stepSize > 0) { - spacing = generationOptions.stepSize; - } else { - var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false); - spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true); - } - var niceMin = Math.floor(dataRange.min / spacing) * spacing; - var niceMax = Math.ceil(dataRange.max / spacing) * spacing; - - // If min, max and stepSize is set and they make an evenly spaced scale use it. - if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - // 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; - } - } - - var numSpaces = (niceMax - niceMin) / spacing; - // If very close to our rounded value, use it. - if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { - numSpaces = Math.round(numSpaces); - } else { - numSpaces = Math.ceil(numSpaces); - } - - var precision = 1; - if (spacing < 1) { - precision = Math.pow(10, spacing.toString().length - 2); - niceMin = Math.round(niceMin * precision) / precision; - niceMax = Math.round(niceMax * precision) / precision; - } - ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); - } - ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); - - return ticks; - }, - - /** - * Generate a set of logarithmic ticks - * @method Chart.Ticks.generators.logarithmic - * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks - * @param dataRange {IRange} the range of the data - * @returns {Array} array of tick values - */ - logarithmic: function(generationOptions, dataRange) { - var ticks = []; - var valueOrDefault = helpers.valueOrDefault; - - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); - - var endExp = Math.floor(helpers.log10(dataRange.max)); - var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); - var exp, significand; - - if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); - significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); - - ticks.push(tickVal); - tickVal = significand * Math.pow(10, exp); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)); - } - var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; - - do { - ticks.push(tickVal); - - ++significand; - if (significand === 10) { - significand = 1; - ++exp; - precision = exp >= 0 ? 1 : precision; - } - - tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; - } while (exp < endExp || (exp === endExp && significand < endSignificand)); - - var lastTick = valueOrDefault(generationOptions.max, tickVal); - ticks.push(lastTick); - - return ticks; - } - }, - /** * Namespace to hold formatters for different types of ticks * @namespace Chart.Ticks.formatters diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 0d9a33bf2..3e4b5c083 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -1,7 +1,61 @@ 'use strict'; var helpers = require('../helpers/index'); -var Ticks = require('../core/core.ticks'); + +/** + * Generate a set of linear ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + var spacing; + if (generationOptions.stepSize && generationOptions.stepSize > 0) { + spacing = generationOptions.stepSize; + } else { + var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false); + spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true); + } + var niceMin = Math.floor(dataRange.min / spacing) * spacing; + var niceMax = Math.ceil(dataRange.max / spacing) * spacing; + + // If min, max and stepSize is set and they make an evenly spaced scale use it. + if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + // 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; + } + } + + var numSpaces = (niceMax - niceMin) / spacing; + // If very close to our rounded value, use it. + if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + + var precision = 1; + if (spacing < 1) { + precision = Math.pow(10, spacing.toString().length - 2); + niceMin = Math.round(niceMin * precision) / precision; + niceMax = Math.round(niceMax * precision) / precision; + } + ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); + for (var j = 1; j < numSpaces; ++j) { + ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); + } + ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); + + return ticks; +} + module.exports = function(Chart) { @@ -102,7 +156,7 @@ module.exports = function(Chart) { max: tickOpts.max, stepSize: helpers.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) }; - var ticks = me.ticks = Ticks.generators.linear(numericGeneratorOptions, me); + var ticks = me.ticks = generateTicks(numericGeneratorOptions, me); me.handleDirectionalChanges(); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 372efe7ad..74a210e44 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -3,6 +3,58 @@ var helpers = require('../helpers/index'); var Ticks = require('../core/core.ticks'); +/** + * Generate a set of logarithmic ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + var valueOrDefault = helpers.valueOrDefault; + + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); + + var endExp = Math.floor(helpers.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp, significand; + + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); + + ticks.push(tickVal); + tickVal = significand * Math.pow(10, exp); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)); + } + var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + + do { + ticks.push(tickVal); + + ++significand; + if (significand === 10) { + significand = 1; + ++exp; + precision = exp >= 0 ? 1 : precision; + } + + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; + } while (exp < endExp || (exp === endExp && significand < endSignificand)); + + var lastTick = valueOrDefault(generationOptions.max, tickVal); + ticks.push(lastTick); + + return ticks; +} + + module.exports = function(Chart) { var defaultConfig = { @@ -167,7 +219,7 @@ module.exports = function(Chart) { min: tickOpts.min, max: tickOpts.max }; - var ticks = me.ticks = Ticks.generators.logarithmic(generationOptions, me); + var ticks = me.ticks = generateTicks(generationOptions, me); // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale diff --git a/test/specs/core.ticks.tests.js b/test/specs/core.ticks.tests.js index 6d7c2b3c1..01cecc546 100644 --- a/test/specs/core.ticks.tests.js +++ b/test/specs/core.ticks.tests.js @@ -1,4 +1,13 @@ describe('Test tick generators', function() { + // formatters are used as default config values so users want to be able to reference them + it('Should expose formatters api', function() { + expect(typeof Chart.Ticks).toBeDefined(); + expect(typeof Chart.Ticks.formatters).toBeDefined(); + expect(typeof Chart.Ticks.formatters.values).toBe('function'); + expect(typeof Chart.Ticks.formatters.linear).toBe('function'); + expect(typeof Chart.Ticks.formatters.logarithmic).toBe('function'); + }); + it('Should generate linear spaced ticks with correct precision', function() { var chart = window.acquireChart({ type: 'line', From 9ddb449ad1abae8dd95639a60311fb8bfc17e572 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 6 Jan 2018 23:59:13 +0100 Subject: [PATCH 30/56] Use the Chart.js shared ESLint config (#5112) An ESLint shareable config has been created (from this repository) in the attempt to homogenize Chart.js hosted projects and plugins style. Rename `.eslintrc` files to `.eslintrc.yml` since the name has been deprecated. Fix the CC badge (maintainability) and disable CodeClimate ESLint plugin because it doesn't support custom shareable config but also because it already executes relevant checks as part of the regular process. --- .codeclimate.yml | 32 ++--- .eslintrc | 220 ------------------------------ .eslintrc.yml | 5 + README.md | 2 +- package.json | 1 + test/{.eslintrc => .eslintrc.yml} | 0 6 files changed, 20 insertions(+), 240 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.yml rename test/{.eslintrc => .eslintrc.yml} (100%) diff --git a/.codeclimate.yml b/.codeclimate.yml index bd3237643..0b8340feb 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,25 +1,19 @@ -engines: +version: "2" +plugins: duplication: enabled: true config: languages: - - javascript - eslint: - enabled: true - channel: "eslint-4" + - javascript fixme: enabled: true -ratings: - paths: - - "src/**/*.js" -exclude_paths: -- '.github/' -- 'dist/' -- 'test/' -- 'docs/' -- 'samples/' -- 'scripts/' -- '**.md' -- '**.json' -- 'gulpfile.js' -- 'karma.conf.js' +exclude_patterns: + - "dist/" + - "docs/" + - "samples/" + - "scripts/" + - "test/" + - "*.js" + - "*.json" + - "*.md" + - ".*" diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c0e1b26ce..000000000 --- a/.eslintrc +++ /dev/null @@ -1,220 +0,0 @@ -env: - amd: true - browser: true - es6: true - jquery: true - node: true - -# http://eslint.org/docs/rules/ -rules: - # Possible Errors - no-cond-assign: 2 - no-console: [2, {allow: [warn, error]}] - no-constant-condition: 2 - no-control-regex: 2 - no-debugger: 2 - no-dupe-args: 2 - no-dupe-keys: 2 - no-duplicate-case: 2 - no-empty: 2 - no-empty-character-class: 2 - no-ex-assign: 2 - no-extra-boolean-cast: 2 - no-extra-parens: [2, functions] - no-extra-semi: 2 - no-func-assign: 2 - no-inner-declarations: [2, functions] - no-invalid-regexp: 2 - no-irregular-whitespace: 2 - no-negated-in-lhs: 2 - no-obj-calls: 2 - no-regex-spaces: 2 - no-sparse-arrays: 2 - no-unexpected-multiline: 2 - no-unreachable: 2 - use-isnan: 2 - valid-jsdoc: 0 - valid-typeof: 2 - - # Best Practices - accessor-pairs: 2 - array-callback-return: 0 - block-scoped-var: 0 - complexity: [2, 10] - consistent-return: 0 - curly: [2, all] - default-case: 2 - dot-location: 0 - dot-notation: 2 - eqeqeq: 2 - guard-for-in: 2 - no-alert: 2 - no-caller: 2 - no-case-declarations: 2 - no-div-regex: 2 - no-else-return: 2 - no-empty-pattern: 2 - no-eq-null: 2 - no-eval: 2 - no-extend-native: 2 - no-extra-bind: 2 - no-fallthrough: 2 - no-floating-decimal: 2 - no-implicit-coercion: 0 - no-implied-eval: 2 - no-invalid-this: 0 - no-iterator: 2 - no-labels: 2 - no-lone-blocks: 2 - no-loop-func: 2 - no-magic-number: 0 - no-multi-spaces: [2, {ignoreEOLComments: true}] - no-multi-str: 2 - no-native-reassign: 2 - no-new-func: 2 - no-new-wrappers: 2 - no-new: 2 - no-octal-escape: 2 - no-octal: 2 - no-proto: 2 - no-redeclare: 2 - no-return-assign: 2 - no-script-url: 2 - no-self-compare: 2 - no-sequences: 2 - no-throw-literal: 0 - no-unused-expressions: 2 - no-useless-call: 2 - no-useless-concat: 2 - no-void: 2 - no-warning-comments: 0 - no-with: 2 - radix: 2 - vars-on-top: 0 - wrap-iife: 2 - yoda: [1, never] - - # Strict - strict: 0 - - # Variables - init-declarations: 0 - no-catch-shadow: 2 - no-delete-var: 2 - no-label-var: 2 - no-shadow-restricted-names: 2 - no-shadow: 2 - no-undef-init: 2 - no-undef: 2 - no-undefined: 0 - no-unused-vars: 2 - no-use-before-define: 2 - - # Node.js and CommonJS - callback-return: 2 - global-require: 2 - handle-callback-err: 2 - no-mixed-requires: 0 - no-new-require: 0 - no-path-concat: 2 - no-process-exit: 2 - no-restricted-modules: 0 - no-sync: 0 - - # Stylistic Issues - array-bracket-spacing: [2, never] - block-spacing: 0 - brace-style: [2, 1tbs] - camelcase: 2 - comma-dangle: [2, only-multiline] - comma-spacing: 2 - comma-style: [2, last] - computed-property-spacing: [2, never] - consistent-this: [2, me] - eol-last: 2 - func-call-spacing: 0 - func-names: [2, never] - func-style: 0 - id-length: 0 - id-match: 0 - indent: [2, tab, {flatTernaryExpressions: true}] - jsx-quotes: 0 - key-spacing: 2 - keyword-spacing: 2 - linebreak-style: 0 - lines-around-comment: 0 - max-depth: 0 - max-len: 0 - max-lines: 0 - max-nested-callbacks: 0 - max-params: 0 - max-statements-per-line: 0 - max-statements: [2, 30] - multiline-ternary: 0 - new-cap: 0 - new-parens: 2 - newline-after-var: 0 - newline-before-return: 0 - newline-per-chained-call: 0 - no-array-constructor: 0 - no-bitwise: 0 - no-continue: 0 - no-inline-comments: 0 - no-lonely-if: 2 - no-mixed-operators: 0 - no-mixed-spaces-and-tabs: 2 - no-multiple-empty-lines: [2, {max: 2}] - no-negated-condition: 0 - no-nested-ternary: 0 - no-new-object: 0 - no-plusplus: 0 - no-restricted-syntax: 0 - no-spaced-func: 0 - no-ternary: 0 - no-trailing-spaces: 2 - no-underscore-dangle: 0 - no-unneeded-ternary: 0 - no-whitespace-before-property: 2 - object-curly-newline: 0 - object-curly-spacing: [2, never] - object-property-newline: 0 - one-var-declaration-per-line: 2 - one-var: [2, {initialized: never}] - operator-assignment: 0 - operator-linebreak: 0 - padded-blocks: 0 - quote-props: [2, as-needed] - quotes: [2, single, {avoidEscape: true}] - require-jsdoc: 0 - semi-spacing: 2 - semi: [2, always] - sort-keys: 0 - sort-vars: 0 - space-before-blocks: [2, always] - space-before-function-paren: [2, never] - space-in-parens: [2, never] - space-infix-ops: 2 - space-unary-ops: [2, {words: true, nonwords: false}] - spaced-comment: [2, always] - unicode-bom: 0 - wrap-regex: 2 - - # ECMAScript 6 - arrow-body-style: 0 - arrow-parens: 0 - arrow-spacing: 0 - constructor-super: 0 - generator-star-spacing: 0 - no-arrow-condition: 0 - no-class-assign: 0 - no-const-assign: 0 - no-dupe-class-members: 0 - no-this-before-super: 0 - no-var: 0 - object-shorthand: 0 - prefer-arrow-callback: 0 - prefer-const: 0 - prefer-reflect: 0 - prefer-spread: 0 - prefer-template: 0 - require-yield: 0 diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 000000000..8f9b4af30 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,5 @@ +extends: chartjs + +env: + browser: true + node: true diff --git a/README.md b/README.md index 767950b88..10d2c1d8d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chart.js -[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![codeclimate](https://img.shields.io/codeclimate/github/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) +[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) *Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) diff --git a/package.json b/package.json index 3ebc721f9..510c141cd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "child-process-promise": "^2.2.1", "coveralls": "^3.0.0", "eslint": "^4.9.0", + "eslint-config-chartjs": "git+https://github.com/chartjs/eslint-config-chartjs.git", "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", diff --git a/test/.eslintrc b/test/.eslintrc.yml similarity index 100% rename from test/.eslintrc rename to test/.eslintrc.yml From ce27fe5ea6040523a61cef198ccc1e866d07ad82 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 7 Jan 2018 23:38:26 +0100 Subject: [PATCH 31/56] Make `Chart.layout(Service)` importable (#5113) Rename (and deprecate) `Chart.layoutService` to `Chart.layout` and make it importable. --- src/chart.js | 11 +- src/core/core.controller.js | 5 +- src/core/core.layout.js | 419 +++++++++++++++++++++++ src/core/core.layoutService.js | 422 ------------------------ src/core/core.scaleService.js | 3 +- src/plugins/plugin.legend.js | 2 +- src/plugins/plugin.title.js | 4 +- test/specs/global.deprecations.tests.js | 13 +- 8 files changed, 448 insertions(+), 431 deletions(-) create mode 100644 src/core/core.layout.js delete mode 100644 src/core/core.layoutService.js diff --git a/src/chart.js b/src/chart.js index 8bd497614..b46e66b9e 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,6 +12,7 @@ Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); +Chart.layout = require('./core/core.layout'); Chart.platform = require('./platforms/platform'); Chart.Ticks = require('./core/core.ticks'); @@ -19,7 +20,6 @@ require('./core/core.plugin')(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.scale')(Chart); require('./core/core.tooltip')(Chart); @@ -77,3 +77,12 @@ if (typeof window !== 'undefined') { * @private */ Chart.canvasHelpers = Chart.helpers.canvas; + +/** + * Provided for backward compatibility, use Chart.layout instead. + * @namespace Chart.layoutService + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +Chart.layoutService = Chart.layout; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 6026e8620..2d143f6e0 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,6 +3,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); +var layout = require('./core.layout'); var platform = require('../platforms/platform'); module.exports = function(Chart) { @@ -46,7 +47,7 @@ module.exports = function(Chart) { var newOptions = chart.options; helpers.each(chart.scales, function(scale) { - Chart.layoutService.removeBox(chart, scale); + layout.removeBox(chart, scale); }); newOptions = helpers.configMerge( @@ -435,7 +436,7 @@ module.exports = function(Chart) { return; } - Chart.layoutService.update(this, this.width, this.height); + layout.update(this, this.width, this.height); /** * Provided for backward compatibility, use `afterLayout` instead. diff --git a/src/core/core.layout.js b/src/core/core.layout.js new file mode 100644 index 000000000..b99612bbe --- /dev/null +++ b/src/core/core.layout.js @@ -0,0 +1,419 @@ +'use strict'; + +var helpers = require('../helpers/index'); + +function filterByPosition(array, position) { + return helpers.where(array, function(v) { + return v.position === position; + }); +} + +function sortByWeight(array, reverse) { + array.forEach(function(v, i) { + v._tmpIndex_ = i; + return v; + }); + array.sort(function(a, b) { + var v0 = reverse ? b : a; + var v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0._tmpIndex_ - v1._tmpIndex_ : + v0.weight - v1.weight; + }); + array.forEach(function(v) { + delete v._tmpIndex_; + }); +} + +/** + * @interface ILayoutItem + * @prop {String} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {Function} update - Takes two parameters: width and height. Returns size of item + * @prop {Function} getPadding - Returns an object with padding on the edges + * @prop {Number} width - Width of item. Must be valid after update() + * @prop {Number} height - Height of item. Must be valid after update() + * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + +// The layout service is very self explanatory. It's responsible for the layout within a chart. +// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need +// It is this service's responsibility of carrying out that layout. +module.exports = { + defaults: {}, + + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {ILayoutItem} item - the item to add to be layed out + */ + addBox: function(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + + // initialize item with default values + item.fullWidth = item.fullWidth || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + + chart.boxes.push(item); + }, + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {Object} layoutItem - the item to remove from the layout + */ + removeBox: function(chart, layoutItem) { + var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {Object} item - the item to configure with the given options + * @param {Object} options - the new item options. + */ + configure: function(chart, item, options) { + var props = ['fullWidth', 'position', 'weight']; + var ilen = props.length; + var i = 0; + var prop; + + for (; i < ilen; ++i) { + prop = props[i]; + if (options.hasOwnProperty(prop)) { + item[prop] = options[prop]; + } + } + }, + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {Number} width - the width to fit into + * @param {Number} height - the height to fit into + */ + update: function(chart, width, height) { + if (!chart) { + return; + } + + var layoutOptions = chart.options.layout || {}; + var padding = helpers.options.toPadding(layoutOptions.padding); + var leftPadding = padding.left; + var rightPadding = padding.right; + var topPadding = padding.top; + var bottomPadding = padding.bottom; + + var leftBoxes = filterByPosition(chart.boxes, 'left'); + var rightBoxes = filterByPosition(chart.boxes, 'right'); + var topBoxes = filterByPosition(chart.boxes, 'top'); + var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); + var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); + + // Sort boxes by weight. A higher weight is further away from the chart area + sortByWeight(leftBoxes, true); + sortByWeight(rightBoxes, false); + sortByWeight(topBoxes, true); + sortByWeight(bottomBoxes, false); + + // Essentially we now have any number of boxes on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays + // These locations are single-box locations only, when trying to register a chartArea location that is already taken, + // an error will be thrown. + // + // |----------------------------------------------------| + // | T1 (Full Width) | + // |----------------------------------------------------| + // | | | T2 | | + // | |----|-------------------------------------|----| + // | | | C1 | | C2 | | + // | | |----| |----| | + // | | | | | + // | L1 | L2 | ChartArea (C0) | R1 | + // | | | | | + // | | |----| |----| | + // | | | C3 | | C4 | | + // | |----|-------------------------------------|----| + // | | | B1 | | + // |----------------------------------------------------| + // | B2 (Full Width) | + // |----------------------------------------------------| + // + // What we do to find the best sizing, we do the following + // 1. Determine the minimum size of the chart area. + // 2. Split the remaining width equally between each vertical axis + // 3. Split the remaining height equally between each horizontal axis + // 4. Give each layout the maximum size it can be. The layout will return it's minimum size + // 5. Adjust the sizes of each axis based on it's minimum reported size. + // 6. Refit each axis + // 7. Position each axis in the final location + // 8. Tell the chart the final location of the chart area + // 9. Tell any axes that overlay the chart area the positions of the chart area + + // Step 1 + var chartWidth = width - leftPadding - rightPadding; + var chartHeight = height - topPadding - bottomPadding; + var chartAreaWidth = chartWidth / 2; // min 50% + var chartAreaHeight = chartHeight / 2; // min 50% + + // Step 2 + var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); + + // Step 3 + var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); + + // Step 4 + var maxChartAreaWidth = chartWidth; + var maxChartAreaHeight = chartHeight; + var minBoxSizes = []; + + function getMinimumBoxSize(box) { + var minSize; + var isHorizontal = box.isHorizontal(); + + if (isHorizontal) { + minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); + maxChartAreaHeight -= minSize.height; + } else { + minSize = box.update(verticalBoxWidth, maxChartAreaHeight); + maxChartAreaWidth -= minSize.width; + } + + minBoxSizes.push({ + horizontal: isHorizontal, + minSize: minSize, + 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; + var totalTopBoxesHeight = topPadding; + var totalBottomBoxesHeight = bottomPadding; + + // Function to fit a box + function fitBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { + return minBox.box === box; + }); + + if (minBoxSize) { + if (box.isHorizontal()) { + var scaleMargin = { + left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), + right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), + top: 0, + bottom: 0 + }; + + // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends + // on the margin. Sometimes they need to increase in size slightly + box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); + } else { + box.update(minBoxSize.minSize.width, maxChartAreaHeight); + } + } + } + + // Update, and calculate the left and right margins for the horizontal boxes + helpers.each(leftBoxes.concat(rightBoxes), fitBox); + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + // Set the Left and Right margins for the horizontal boxes + helpers.each(topBoxes.concat(bottomBoxes), fitBox); + + // Figure out how much margin is on the top and bottom of the vertical boxes + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + + helpers.each(bottomBoxes, function(box) { + totalBottomBoxesHeight += box.height; + }); + + function finalFitVerticalBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { + return minSize.box === box; + }); + + var scaleMargin = { + left: 0, + right: 0, + top: totalTopBoxesHeight, + bottom: totalBottomBoxesHeight + }; + + if (minBoxSize) { + box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); + } + } + + // Let the left layout know the final margin + helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); + + // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) + totalLeftBoxesWidth = leftPadding; + totalRightBoxesWidth = rightPadding; + totalTopBoxesHeight = topPadding; + totalBottomBoxesHeight = bottomPadding; + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + helpers.each(bottomBoxes, function(box) { + 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 + var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; + var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; + + if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { + helpers.each(leftBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(rightBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(topBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + helpers.each(bottomBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + maxChartAreaHeight = newMaxChartAreaHeight; + maxChartAreaWidth = newMaxChartAreaWidth; + } + + // Step 7 - Position the boxes + var left = leftPadding + leftPaddingAddition; + var top = topPadding + topPaddingAddition; + + function placeBox(box) { + if (box.isHorizontal()) { + box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; + box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; + box.top = top; + box.bottom = top + box.height; + + // Move to next point + top = box.bottom; + + } else { + + box.left = left; + box.right = left + box.width; + box.top = totalTopBoxesHeight; + box.bottom = totalTopBoxesHeight + maxChartAreaHeight; + + // Move to next point + left = box.right; + } + } + + helpers.each(leftBoxes.concat(topBoxes), placeBox); + + // Account for chart width and height + left += maxChartAreaWidth; + top += maxChartAreaHeight; + + helpers.each(rightBoxes, placeBox); + helpers.each(bottomBoxes, placeBox); + + // Step 8 + chart.chartArea = { + left: totalLeftBoxesWidth, + top: totalTopBoxesHeight, + right: totalLeftBoxesWidth + maxChartAreaWidth, + bottom: totalTopBoxesHeight + maxChartAreaHeight + }; + + // Step 9 + helpers.each(chartAreaBoxes, function(box) { + box.left = chart.chartArea.left; + box.top = chart.chartArea.top; + box.right = chart.chartArea.right; + box.bottom = chart.chartArea.bottom; + + box.update(maxChartAreaWidth, maxChartAreaHeight); + }); + } +}; diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js deleted file mode 100644 index df28f974e..000000000 --- a/src/core/core.layoutService.js +++ /dev/null @@ -1,422 +0,0 @@ -'use strict'; - -var helpers = require('../helpers/index'); - -module.exports = function(Chart) { - - function filterByPosition(array, position) { - return helpers.where(array, function(v) { - return v.position === position; - }); - } - - function sortByWeight(array, reverse) { - array.forEach(function(v, i) { - v._tmpIndex_ = i; - return v; - }); - array.sort(function(a, b) { - var v0 = reverse ? b : a; - var v1 = reverse ? a : b; - return v0.weight === v1.weight ? - v0._tmpIndex_ - v1._tmpIndex_ : - v0.weight - v1.weight; - }); - array.forEach(function(v) { - delete v._tmpIndex_; - }); - } - - /** - * @interface ILayoutItem - * @prop {String} position - The position of the item in the chart layout. Possible values are - * 'left', 'top', 'right', 'bottom', and 'chartArea' - * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area - * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down - * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) - * @prop {Function} update - Takes two parameters: width and height. Returns size of item - * @prop {Function} getPadding - Returns an object with padding on the edges - * @prop {Number} width - Width of item. Must be valid after update() - * @prop {Number} height - Height of item. Must be valid after update() - * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update - * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update - * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update - * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update - */ - - // The layout service is very self explanatory. It's responsible for the layout within a chart. - // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need - // It is this service's responsibility of carrying out that layout. - Chart.layoutService = { - defaults: {}, - - /** - * Register a box to a chart. - * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. - * @param {Chart} chart - the chart to use - * @param {ILayoutItem} item - the item to add to be layed out - */ - addBox: function(chart, item) { - if (!chart.boxes) { - chart.boxes = []; - } - - // initialize item with default values - item.fullWidth = item.fullWidth || false; - item.position = item.position || 'top'; - item.weight = item.weight || 0; - - chart.boxes.push(item); - }, - - /** - * Remove a layoutItem from a chart - * @param {Chart} chart - the chart to remove the box from - * @param {Object} layoutItem - the item to remove from the layout - */ - removeBox: function(chart, layoutItem) { - var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; - if (index !== -1) { - chart.boxes.splice(index, 1); - } - }, - - /** - * Sets (or updates) options on the given `item`. - * @param {Chart} chart - the chart in which the item lives (or will be added to) - * @param {Object} item - the item to configure with the given options - * @param {Object} options - the new item options. - */ - configure: function(chart, item, options) { - var props = ['fullWidth', 'position', 'weight']; - var ilen = props.length; - var i = 0; - var prop; - - for (; i < ilen; ++i) { - prop = props[i]; - if (options.hasOwnProperty(prop)) { - item[prop] = options[prop]; - } - } - }, - - /** - * Fits boxes of the given chart into the given size by having each box measure itself - * then running a fitting algorithm - * @param {Chart} chart - the chart - * @param {Number} width - the width to fit into - * @param {Number} height - the height to fit into - */ - update: function(chart, width, height) { - if (!chart) { - return; - } - - var layoutOptions = chart.options.layout || {}; - var padding = helpers.options.toPadding(layoutOptions.padding); - var leftPadding = padding.left; - var rightPadding = padding.right; - var topPadding = padding.top; - var bottomPadding = padding.bottom; - - var leftBoxes = filterByPosition(chart.boxes, 'left'); - var rightBoxes = filterByPosition(chart.boxes, 'right'); - var topBoxes = filterByPosition(chart.boxes, 'top'); - var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); - var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); - - // Sort boxes by weight. A higher weight is further away from the chart area - sortByWeight(leftBoxes, true); - sortByWeight(rightBoxes, false); - sortByWeight(topBoxes, true); - sortByWeight(bottomBoxes, false); - - // Essentially we now have any number of boxes on each of the 4 sides. - // Our canvas looks like the following. - // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and - // B1 is the bottom axis - // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays - // These locations are single-box locations only, when trying to register a chartArea location that is already taken, - // an error will be thrown. - // - // |----------------------------------------------------| - // | T1 (Full Width) | - // |----------------------------------------------------| - // | | | T2 | | - // | |----|-------------------------------------|----| - // | | | C1 | | C2 | | - // | | |----| |----| | - // | | | | | - // | L1 | L2 | ChartArea (C0) | R1 | - // | | | | | - // | | |----| |----| | - // | | | C3 | | C4 | | - // | |----|-------------------------------------|----| - // | | | B1 | | - // |----------------------------------------------------| - // | B2 (Full Width) | - // |----------------------------------------------------| - // - // What we do to find the best sizing, we do the following - // 1. Determine the minimum size of the chart area. - // 2. Split the remaining width equally between each vertical axis - // 3. Split the remaining height equally between each horizontal axis - // 4. Give each layout the maximum size it can be. The layout will return it's minimum size - // 5. Adjust the sizes of each axis based on it's minimum reported size. - // 6. Refit each axis - // 7. Position each axis in the final location - // 8. Tell the chart the final location of the chart area - // 9. Tell any axes that overlay the chart area the positions of the chart area - - // Step 1 - var chartWidth = width - leftPadding - rightPadding; - var chartHeight = height - topPadding - bottomPadding; - var chartAreaWidth = chartWidth / 2; // min 50% - var chartAreaHeight = chartHeight / 2; // min 50% - - // Step 2 - var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); - - // Step 3 - var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); - - // Step 4 - var maxChartAreaWidth = chartWidth; - var maxChartAreaHeight = chartHeight; - var minBoxSizes = []; - - function getMinimumBoxSize(box) { - var minSize; - var isHorizontal = box.isHorizontal(); - - if (isHorizontal) { - minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); - maxChartAreaHeight -= minSize.height; - } else { - minSize = box.update(verticalBoxWidth, maxChartAreaHeight); - maxChartAreaWidth -= minSize.width; - } - - minBoxSizes.push({ - horizontal: isHorizontal, - minSize: minSize, - 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; - var totalTopBoxesHeight = topPadding; - var totalBottomBoxesHeight = bottomPadding; - - // Function to fit a box - function fitBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { - return minBox.box === box; - }); - - if (minBoxSize) { - if (box.isHorizontal()) { - var scaleMargin = { - left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), - right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), - top: 0, - bottom: 0 - }; - - // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends - // on the margin. Sometimes they need to increase in size slightly - box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); - } else { - box.update(minBoxSize.minSize.width, maxChartAreaHeight); - } - } - } - - // Update, and calculate the left and right margins for the horizontal boxes - helpers.each(leftBoxes.concat(rightBoxes), fitBox); - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - // Set the Left and Right margins for the horizontal boxes - helpers.each(topBoxes.concat(bottomBoxes), fitBox); - - // Figure out how much margin is on the top and bottom of the vertical boxes - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - function finalFitVerticalBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { - return minSize.box === box; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: totalTopBoxesHeight, - bottom: totalBottomBoxesHeight - }; - - if (minBoxSize) { - box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); - } - } - - // Let the left layout know the final margin - helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); - - // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) - totalLeftBoxesWidth = leftPadding; - totalRightBoxesWidth = rightPadding; - totalTopBoxesHeight = topPadding; - totalBottomBoxesHeight = bottomPadding; - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - helpers.each(bottomBoxes, function(box) { - 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 - var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; - var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; - - if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { - helpers.each(leftBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(rightBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(topBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - helpers.each(bottomBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - maxChartAreaHeight = newMaxChartAreaHeight; - maxChartAreaWidth = newMaxChartAreaWidth; - } - - // Step 7 - Position the boxes - var left = leftPadding + leftPaddingAddition; - var top = topPadding + topPaddingAddition; - - function placeBox(box) { - if (box.isHorizontal()) { - box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; - box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; - box.top = top; - box.bottom = top + box.height; - - // Move to next point - top = box.bottom; - - } else { - - box.left = left; - box.right = left + box.width; - box.top = totalTopBoxesHeight; - box.bottom = totalTopBoxesHeight + maxChartAreaHeight; - - // Move to next point - left = box.right; - } - } - - helpers.each(leftBoxes.concat(topBoxes), placeBox); - - // Account for chart width and height - left += maxChartAreaWidth; - top += maxChartAreaHeight; - - helpers.each(rightBoxes, placeBox); - helpers.each(bottomBoxes, placeBox); - - // Step 8 - chart.chartArea = { - left: totalLeftBoxesWidth, - top: totalTopBoxesHeight, - right: totalLeftBoxesWidth + maxChartAreaWidth, - bottom: totalTopBoxesHeight + maxChartAreaHeight - }; - - // Step 9 - helpers.each(chartAreaBoxes, function(box) { - box.left = chart.chartArea.left; - box.top = chart.chartArea.top; - box.right = chart.chartArea.right; - box.bottom = chart.chartArea.bottom; - - box.update(maxChartAreaWidth, maxChartAreaHeight); - }); - } - }; -}; diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index 23cabe610..bfbf83209 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -2,6 +2,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); +var layout = require('./core.layout'); module.exports = function(Chart) { @@ -38,7 +39,7 @@ module.exports = function(Chart) { scale.fullWidth = scale.options.fullWidth; scale.position = scale.options.position; scale.weight = scale.options.weight; - Chart.layoutService.addBox(chart, scale); + layout.addBox(chart, scale); }); } }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 71e0e4110..7e0e429d0 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -3,6 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layout = require('../core/core.layout'); defaults._set('global', { legend: { @@ -81,7 +82,6 @@ defaults._set('global', { module.exports = function(Chart) { - var layout = Chart.layoutService; var noop = helpers.noop; /** diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index ebefed294..8eac103f5 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -3,6 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layout = require('../core/core.layout'); defaults._set('global', { title: { @@ -19,7 +20,6 @@ defaults._set('global', { module.exports = function(Chart) { - var layout = Chart.layoutService; var noop = helpers.noop; Chart.Title = Element.extend({ @@ -235,7 +235,7 @@ module.exports = function(Chart) { createNewTitleBlockAndAttach(chart, titleOpts); } } else if (titleBlock) { - Chart.layoutService.removeBox(chart, titleBlock); + layout.removeBox(chart, titleBlock); delete chart.titleBlock; } } diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index f1091464d..fee37288d 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,4 +1,13 @@ describe('Deprecations', function() { + describe('Version 2.8.0', function() { + describe('Chart.layoutService', function() { + it('should be defined and an alias of Chart.layout', function() { + expect(Chart.layoutService).toBeDefined(); + expect(Chart.layoutService).toBe(Chart.layout); + }); + }); + }); + describe('Version 2.7.0', function() { describe('Chart.Controller.update(duration, lazy)', function() { it('should add an animation with the provided options', function() { @@ -302,8 +311,8 @@ describe('Deprecations', function() { 'afterLayout' ]; - var override = Chart.layoutService.update; - Chart.layoutService.update = function() { + var override = Chart.layout.update; + Chart.layout.update = function() { sequence.push('layoutUpdate'); override.apply(this, arguments); }; From fb3ea03440769a267880ba8721d14a3939792718 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 8 Jan 2018 11:48:59 +0100 Subject: [PATCH 32/56] Make `Chart.plugins` importable (#5114) Explicitly deprecate (since 2.1.5) `Chart.Legend` and `Chart.Title`. --- src/chart.js | 54 +- src/core/core.controller.js | 2 +- src/core/core.plugin.js | 395 ----------- src/core/core.plugins.js | 372 +++++++++++ src/plugins/index.js | 6 + src/plugins/plugin.filler.js | 543 ++++++++------- src/plugins/plugin.legend.js | 855 ++++++++++++------------ src/plugins/plugin.title.js | 407 +++++------ test/specs/global.deprecations.tests.js | 18 + test/specs/plugin.legend.tests.js | 5 - test/specs/plugin.title.tests.js | 5 - 11 files changed, 1351 insertions(+), 1311 deletions(-) delete mode 100644 src/core/core.plugin.js create mode 100644 src/core/core.plugins.js create mode 100644 src/plugins/index.js diff --git a/src/chart.js b/src/chart.js index b46e66b9e..70d70a669 100644 --- a/src/chart.js +++ b/src/chart.js @@ -14,9 +14,9 @@ Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); Chart.layout = require('./core/core.layout'); Chart.platform = require('./platforms/platform'); +Chart.plugins = require('./core/core.plugins'); Chart.Ticks = require('./core/core.ticks'); -require('./core/core.plugin')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); @@ -50,15 +50,12 @@ require('./charts/Chart.Radar')(Chart); require('./charts/Chart.Scatter')(Chart); // Loading built-it plugins -var plugins = []; - -plugins.push( - require('./plugins/plugin.filler')(Chart), - require('./plugins/plugin.legend')(Chart), - require('./plugins/plugin.title')(Chart) -); - -Chart.plugins.register(plugins); +var plugins = require('./plugins'); +for (var k in plugins) { + if (plugins.hasOwnProperty(k)) { + Chart.plugins.register(plugins[k]); + } +} Chart.platform.initialize(); @@ -69,6 +66,43 @@ if (typeof window !== 'undefined') { // DEPRECATIONS +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Legend + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Legend = plugins.legend._element; + +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Title + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Title = plugins.title._element; + +/** + * Provided for backward compatibility, use Chart.plugins instead + * @namespace Chart.pluginService + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.pluginService = Chart.plugins; + +/** + * Provided for backward compatibility, inheriting from Chart.PlugingBase has no + * effect, instead simply create/register plugins via plain JavaScript objects. + * @interface Chart.PluginBase + * @deprecated since version 2.5.0 + * @todo remove at version 3 + * @private + */ +Chart.PluginBase = Chart.Element.extend({}); + /** * Provided for backward compatibility, use Chart.helpers.canvas instead. * @namespace Chart.canvasHelpers diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 2d143f6e0..157bc50a4 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -5,9 +5,9 @@ var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); var layout = require('./core.layout'); var platform = require('../platforms/platform'); +var plugins = require('./core.plugins'); module.exports = function(Chart) { - var plugins = Chart.plugins; // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js deleted file mode 100644 index 0b7423a69..000000000 --- a/src/core/core.plugin.js +++ /dev/null @@ -1,395 +0,0 @@ -'use strict'; - -var defaults = require('./core.defaults'); -var Element = require('./core.element'); -var helpers = require('../helpers/index'); - -defaults._set('global', { - plugins: {} -}); - -module.exports = function(Chart) { - - /** - * The plugin service singleton - * @namespace Chart.plugins - * @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). - */ - register: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - if (p.indexOf(plugin) === -1) { - p.push(plugin); - } - }); - - this._cacheId++; - }, - - /** - * Unregisters the given plugin(s) only if registered. - * @param {Array|Object} plugins plugin instance(s). - */ - unregister: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - var idx = p.indexOf(plugin); - if (idx !== -1) { - p.splice(idx, 1); - } - }); - - this._cacheId++; - }, - - /** - * Remove all registered plugins. - * @since 2.1.5 - */ - clear: function() { - this._plugins = []; - this._cacheId++; - }, - - /** - * Returns the number of registered plugins? - * @returns {Number} - * @since 2.1.5 - */ - count: function() { - return this._plugins.length; - }, - - /** - * Returns all registered plugin instances. - * @returns {Array} array of plugin objects. - * @since 2.1.5 - */ - getAll: function() { - return this._plugins; - }, - - /** - * 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 {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(chart, hook, args) { - var descriptors = this.descriptors(chart); - var ilen = descriptors.length; - var i, descriptor, plugin, params, method; - - for (i = 0; i < ilen; ++i) { - descriptor = descriptors[i]; - plugin = descriptor.plugin; - method = plugin[hook]; - if (typeof method === 'function') { - params = [chart].concat(args || []); - params.push(descriptor.options); - if (method.apply(plugin, params) === false) { - return false; - } - } - } - - return true; - }, - - /** - * Returns descriptors of enabled plugins for the given chart. - * @returns {Array} [{ plugin, options }] - * @private - */ - descriptors: function(chart) { - var cache = chart._plugins || (chart._plugins = {}); - if (cache.id === this._cacheId) { - return cache.descriptors; - } - - var plugins = []; - var descriptors = []; - var config = (chart && chart.config) || {}; - var options = (config.options && config.options.plugins) || {}; - - this._plugins.concat(config.plugins || []).forEach(function(plugin) { - var idx = plugins.indexOf(plugin); - if (idx !== -1) { - return; - } - - var id = plugin.id; - var opts = options[id]; - if (opts === false) { - return; - } - - if (opts === true) { - opts = helpers.clone(defaults.global.plugins[id]); - } - - plugins.push(plugin); - descriptors.push({ - plugin: plugin, - options: opts || {} - }); - }); - - cache.descriptors = descriptors; - cache.id = this._cacheId; - return descriptors; - } - }; - - /** - * Plugin extension hooks. - * @interface IPlugin - * @since 2.1.0 - */ - /** - * @method IPlugin#beforeInit - * @desc Called before initializing `chart`. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterInit - * @desc Called after `chart` has been initialized and before the first update. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeUpdate - * @desc Called before updating `chart`. If any plugin returns `false`, the update - * is cancelled (and thus subsequent render(s)) until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart update. - */ - /** - * @method IPlugin#afterUpdate - * @desc Called after `chart` has been updated and before rendering. Note that this - * hook will not be called if the chart update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsUpdate - * @desc Called before updating the `chart` datasets. If any plugin returns `false`, - * the datasets update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} false to cancel the datasets update. - * @since version 2.1.5 - */ - /** - * @method IPlugin#afterDatasetsUpdate - * @desc Called after the `chart` datasets have been updated. Note that this hook - * will not be called if the datasets update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @since version 2.1.5 - */ - /** - * @method IPlugin#beforeDatasetUpdate - * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin - * returns `false`, the datasets update is cancelled until another `update` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetUpdate - * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note - * that this hook will not be called if the datasets update has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeLayout - * @desc Called before laying out `chart`. If any plugin returns `false`, - * the layout update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart layout. - */ - /** - * @method IPlugin#afterLayout - * @desc Called after the `chart` has been layed out. Note that this hook will not - * be called if the layout update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeRender - * @desc Called before rendering `chart`. If any plugin returns `false`, - * the rendering is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart rendering. - */ - /** - * @method IPlugin#afterRender - * @desc Called after the `chart` has been fully rendered (and animation completed). Note - * that this hook will not be called if the rendering has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDraw - * @desc Called before drawing `chart` at every animation frame specified by the given - * easing value. If any plugin returns `false`, the frame drawing is cancelled until - * another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart drawing. - */ - /** - * @method IPlugin#afterDraw - * @desc Called after the `chart` has been drawn for the specific easing value. Note - * that this hook will not be called if the drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsDraw - * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, - * the datasets drawing is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetsDraw - * @desc Called after the `chart` datasets have been drawn. Note that this hook - * will not be called if the datasets drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetDraw - * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets - * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing - * is cancelled until another `render` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetDraw - * @desc Called after the `chart` datasets at the given `args.index` have been drawn - * (datasets are drawn in the reverse order). Note that this hook will not be called - * if the datasets drawing has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeTooltipDraw - * @desc Called before drawing the `tooltip`. If any plugin returns `false`, - * the tooltip drawing is cancelled until another `render` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart tooltip drawing. - */ - /** - * @method IPlugin#afterTooltipDraw - * @desc Called after drawing the `tooltip`. Note that this hook will not - * be called if the tooltip drawing has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeEvent - * @desc Called before processing the specified `event`. If any plugin returns `false`, - * the event will be discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterEvent - * @desc Called after the `event` has been consumed. Note that this hook - * will not be called if the `event` has been previously discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#resize - * @desc Called after the chart as been resized. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} size - The new canvas display size (eq. canvas.style width & height). - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#destroy - * @desc Called after the chart as been destroyed. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - - /** - * Provided for backward compatibility, use Chart.plugins instead - * @namespace Chart.pluginService - * @deprecated since version 2.1.5 - * @todo remove at version 3 - * @private - */ - Chart.pluginService = Chart.plugins; - - /** - * Provided for backward compatibility, inheriting from Chart.PlugingBase has no - * effect, instead simply create/register plugins via plain JavaScript objects. - * @interface Chart.PluginBase - * @deprecated since version 2.5.0 - * @todo remove at version 3 - * @private - */ - Chart.PluginBase = Element.extend({}); -}; diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js new file mode 100644 index 000000000..f5e8d10d8 --- /dev/null +++ b/src/core/core.plugins.js @@ -0,0 +1,372 @@ +'use strict'; + +var defaults = require('./core.defaults'); +var helpers = require('../helpers/index'); + +defaults._set('global', { + plugins: {} +}); + +/** + * The plugin service singleton + * @namespace Chart.plugins + * @since 2.1.0 + */ +module.exports = { + /** + * 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). + */ + register: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + if (p.indexOf(plugin) === -1) { + p.push(plugin); + } + }); + + this._cacheId++; + }, + + /** + * Unregisters the given plugin(s) only if registered. + * @param {Array|Object} plugins plugin instance(s). + */ + unregister: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + var idx = p.indexOf(plugin); + if (idx !== -1) { + p.splice(idx, 1); + } + }); + + this._cacheId++; + }, + + /** + * Remove all registered plugins. + * @since 2.1.5 + */ + clear: function() { + this._plugins = []; + this._cacheId++; + }, + + /** + * Returns the number of registered plugins? + * @returns {Number} + * @since 2.1.5 + */ + count: function() { + return this._plugins.length; + }, + + /** + * Returns all registered plugin instances. + * @returns {Array} array of plugin objects. + * @since 2.1.5 + */ + getAll: function() { + return this._plugins; + }, + + /** + * 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 {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(chart, hook, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; + + for (i = 0; i < ilen; ++i) { + descriptor = descriptors[i]; + plugin = descriptor.plugin; + method = plugin[hook]; + if (typeof method === 'function') { + params = [chart].concat(args || []); + params.push(descriptor.options); + if (method.apply(plugin, params) === false) { + return false; + } + } + } + + return true; + }, + + /** + * Returns descriptors of enabled plugins for the given chart. + * @returns {Array} [{ plugin, options }] + * @private + */ + descriptors: function(chart) { + var cache = chart._plugins || (chart._plugins = {}); + if (cache.id === this._cacheId) { + return cache.descriptors; + } + + var plugins = []; + var descriptors = []; + var config = (chart && chart.config) || {}; + var options = (config.options && config.options.plugins) || {}; + + this._plugins.concat(config.plugins || []).forEach(function(plugin) { + var idx = plugins.indexOf(plugin); + if (idx !== -1) { + return; + } + + var id = plugin.id; + var opts = options[id]; + if (opts === false) { + return; + } + + if (opts === true) { + opts = helpers.clone(defaults.global.plugins[id]); + } + + plugins.push(plugin); + descriptors.push({ + plugin: plugin, + options: opts || {} + }); + }); + + cache.descriptors = descriptors; + cache.id = this._cacheId; + return descriptors; + } +}; + +/** + * Plugin extension hooks. + * @interface IPlugin + * @since 2.1.0 + */ +/** + * @method IPlugin#beforeInit + * @desc Called before initializing `chart`. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterInit + * @desc Called after `chart` has been initialized and before the first update. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeUpdate + * @desc Called before updating `chart`. If any plugin returns `false`, the update + * is cancelled (and thus subsequent render(s)) until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart update. + */ +/** + * @method IPlugin#afterUpdate + * @desc Called after `chart` has been updated and before rendering. Note that this + * hook will not be called if the chart update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsUpdate + * @desc Called before updating the `chart` datasets. If any plugin returns `false`, + * the datasets update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} false to cancel the datasets update. + * @since version 2.1.5 +*/ +/** + * @method IPlugin#afterDatasetsUpdate + * @desc Called after the `chart` datasets have been updated. Note that this hook + * will not be called if the datasets update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @since version 2.1.5 + */ +/** + * @method IPlugin#beforeDatasetUpdate + * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin + * returns `false`, the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetUpdate + * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note + * that this hook will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeLayout + * @desc Called before laying out `chart`. If any plugin returns `false`, + * the layout update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart layout. + */ +/** + * @method IPlugin#afterLayout + * @desc Called after the `chart` has been layed out. Note that this hook will not + * be called if the layout update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeRender + * @desc Called before rendering `chart`. If any plugin returns `false`, + * the rendering is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart rendering. + */ +/** + * @method IPlugin#afterRender + * @desc Called after the `chart` has been fully rendered (and animation completed). Note + * that this hook will not be called if the rendering has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDraw + * @desc Called before drawing `chart` at every animation frame specified by the given + * easing value. If any plugin returns `false`, the frame drawing is cancelled until + * another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart drawing. + */ +/** + * @method IPlugin#afterDraw + * @desc Called after the `chart` has been drawn for the specific easing value. Note + * that this hook will not be called if the drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsDraw + * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, + * the datasets drawing is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetsDraw + * @desc Called after the `chart` datasets have been drawn. Note that this hook + * will not be called if the datasets drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetDraw + * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets + * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing + * is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetDraw + * @desc Called after the `chart` datasets at the given `args.index` have been drawn + * (datasets are drawn in the reverse order). Note that this hook will not be called + * if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeTooltipDraw + * @desc Called before drawing the `tooltip`. If any plugin returns `false`, + * the tooltip drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart tooltip drawing. + */ +/** + * @method IPlugin#afterTooltipDraw + * @desc Called after drawing the `tooltip`. Note that this hook will not + * be called if the tooltip drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeEvent + * @desc Called before processing the specified `event`. If any plugin returns `false`, + * the event will be discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterEvent + * @desc Called after the `event` has been consumed. Note that this hook + * will not be called if the `event` has been previously discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#resize + * @desc Called after the chart as been resized. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} size - The new canvas display size (eq. canvas.style width & height). + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#destroy + * @desc Called after the chart as been destroyed. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 000000000..1cd981516 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = {}; +module.exports.filler = require('./plugin.filler'); +module.exports.legend = require('./plugin.legend'); +module.exports.title = require('./plugin.title'); diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js index cf0226542..eb8dad4c3 100644 --- a/src/plugins/plugin.filler.js +++ b/src/plugins/plugin.filler.js @@ -18,304 +18,301 @@ defaults._set('global', { } }); -module.exports = function() { +var mappers = { + dataset: function(source) { + var index = source.fill; + var chart = source.chart; + var meta = chart.getDatasetMeta(index); + var visible = meta && chart.isDatasetVisible(index); + var points = (visible && meta.dataset._children) || []; + var length = points.length || 0; - var mappers = { - dataset: function(source) { - var index = source.fill; - var chart = source.chart; - var meta = chart.getDatasetMeta(index); - var visible = meta && chart.isDatasetVisible(index); - var points = (visible && meta.dataset._children) || []; - var length = points.length || 0; + return !length ? null : function(point, i) { + return (i < length && points[i]._view) || null; + }; + }, - return !length ? null : function(point, i) { - return (i < length && points[i]._view) || null; + boundary: function(source) { + var boundary = source.boundary; + var x = boundary ? boundary.x : null; + var y = boundary ? boundary.y : null; + + return function(point) { + return { + x: x === null ? point.x : x, + y: y === null ? point.y : y, }; - }, + }; + } +}; - boundary: function(source) { - var boundary = source.boundary; - var x = boundary ? boundary.x : null; - var y = boundary ? boundary.y : null; +// @todo if (fill[0] === '#') +function decodeFill(el, index, count) { + var model = el._model || {}; + var fill = model.fill; + var target; - return function(point) { - return { - x: x === null ? point.x : x, - y: y === null ? point.y : y, - }; - }; - } - }; - - // @todo if (fill[0] === '#') - function decodeFill(el, index, count) { - var model = el._model || {}; - var fill = model.fill; - var target; - - if (fill === undefined) { - fill = !!model.backgroundColor; - } - - if (fill === false || fill === null) { - return false; - } - - if (fill === true) { - return 'origin'; - } - - target = parseFloat(fill, 10); - if (isFinite(target) && Math.floor(target) === target) { - if (fill[0] === '-' || fill[0] === '+') { - target = index + target; - } - - if (target === index || target < 0 || target >= count) { - return false; - } - - return target; - } - - switch (fill) { - // compatibility - case 'bottom': - return 'start'; - case 'top': - return 'end'; - case 'zero': - return 'origin'; - // supported boundaries - case 'origin': - case 'start': - case 'end': - return fill; - // invalid fill values - default: - return false; - } + if (fill === undefined) { + fill = !!model.backgroundColor; } - function computeBoundary(source) { - var model = source.el._model || {}; - var scale = source.el._scale || {}; - var fill = source.fill; - var target = null; - var horizontal; - - if (isFinite(fill)) { - return null; - } - - // Backward compatibility: until v3, we still need to support boundary values set on - // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and - // controllers might still use it (e.g. the Smith chart). - - if (fill === 'start') { - target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; - } else if (fill === 'end') { - target = model.scaleTop === undefined ? scale.top : model.scaleTop; - } else if (model.scaleZero !== undefined) { - target = model.scaleZero; - } else if (scale.getBasePosition) { - target = scale.getBasePosition(); - } else if (scale.getBasePixel) { - target = scale.getBasePixel(); - } - - if (target !== undefined && target !== null) { - if (target.x !== undefined && target.y !== undefined) { - return target; - } - - if (typeof target === 'number' && isFinite(target)) { - horizontal = scale.isHorizontal(); - return { - x: horizontal ? target : null, - y: horizontal ? null : target - }; - } - } - - return null; - } - - function resolveTarget(sources, index, propagate) { - var source = sources[index]; - var fill = source.fill; - var visited = [index]; - var target; - - if (!propagate) { - return fill; - } - - while (fill !== false && visited.indexOf(fill) === -1) { - if (!isFinite(fill)) { - return fill; - } - - target = sources[fill]; - if (!target) { - return false; - } - - if (target.visible) { - return fill; - } - - visited.push(fill); - fill = target.fill; - } - + if (fill === false || fill === null) { return false; } - function createMapper(source) { - var fill = source.fill; - var type = 'dataset'; + if (fill === true) { + return 'origin'; + } - if (fill === false) { - return null; + target = parseFloat(fill, 10); + if (isFinite(target) && Math.floor(target) === target) { + if (fill[0] === '-' || fill[0] === '+') { + target = index + target; } + if (target === index || target < 0 || target >= count) { + return false; + } + + return target; + } + + switch (fill) { + // compatibility + case 'bottom': + return 'start'; + case 'top': + return 'end'; + case 'zero': + return 'origin'; + // supported boundaries + case 'origin': + case 'start': + case 'end': + return fill; + // invalid fill values + default: + return false; + } +} + +function computeBoundary(source) { + var model = source.el._model || {}; + var scale = source.el._scale || {}; + var fill = source.fill; + var target = null; + var horizontal; + + if (isFinite(fill)) { + return null; + } + + // Backward compatibility: until v3, we still need to support boundary values set on + // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and + // controllers might still use it (e.g. the Smith chart). + + if (fill === 'start') { + target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; + } else if (fill === 'end') { + target = model.scaleTop === undefined ? scale.top : model.scaleTop; + } else if (model.scaleZero !== undefined) { + target = model.scaleZero; + } else if (scale.getBasePosition) { + target = scale.getBasePosition(); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } + + if (target !== undefined && target !== null) { + if (target.x !== undefined && target.y !== undefined) { + return target; + } + + if (typeof target === 'number' && isFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal ? target : null, + y: horizontal ? null : target + }; + } + } + + return null; +} + +function resolveTarget(sources, index, propagate) { + var source = sources[index]; + var fill = source.fill; + var visited = [index]; + var target; + + if (!propagate) { + return fill; + } + + while (fill !== false && visited.indexOf(fill) === -1) { if (!isFinite(fill)) { - type = 'boundary'; + return fill; } - return mappers[type](source); + target = sources[fill]; + if (!target) { + return false; + } + + if (target.visible) { + return fill; + } + + visited.push(fill); + fill = target.fill; } - function isDrawable(point) { - return point && !point.skip; + return false; +} + +function createMapper(source) { + var fill = source.fill; + var type = 'dataset'; + + if (fill === false) { + return null; } - function drawArea(ctx, curve0, curve1, len0, len1) { - var i; + if (!isFinite(fill)) { + type = 'boundary'; + } - if (!len0 || !len1) { + return mappers[type](source); +} + +function isDrawable(point) { + return point && !point.skip; +} + +function drawArea(ctx, curve0, curve1, len0, len1) { + var i; + + if (!len0 || !len1) { + return; + } + + // building first area curve (normal) + ctx.moveTo(curve0[0].x, curve0[0].y); + for (i = 1; i < len0; ++i) { + helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); + } + + // joining the two area curves + ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); + + // building opposite area curve (reverse) + for (i = len1 - 1; i > 0; --i) { + helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); + } +} + +function doFill(ctx, points, mapper, view, color, loop) { + var count = points.length; + var span = view.spanGaps; + var curve0 = []; + var curve1 = []; + var len0 = 0; + var len1 = 0; + var i, ilen, index, p0, p1, d0, d1; + + ctx.beginPath(); + + for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { + index = i % count; + p0 = points[index]._view; + p1 = mapper(p0, index, view); + d0 = isDrawable(p0); + d1 = isDrawable(p1); + + if (d0 && d1) { + len0 = curve0.push(p0); + len1 = curve1.push(p1); + } else if (len0 && len1) { + if (!span) { + drawArea(ctx, curve0, curve1, len0, len1); + len0 = len1 = 0; + curve0 = []; + curve1 = []; + } else { + if (d0) { + curve0.push(p0); + } + if (d1) { + curve1.push(p1); + } + } + } + } + + drawArea(ctx, curve0, curve1, len0, len1); + + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); +} + +module.exports = { + id: 'filler', + + afterDatasetsUpdate: function(chart, options) { + var count = (chart.data.datasets || []).length; + var propagate = options.propagate; + var sources = []; + var meta, i, el, source; + + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + el = meta.dataset; + source = null; + + if (el && el._model && el instanceof elements.Line) { + source = { + visible: chart.isDatasetVisible(i), + fill: decodeFill(el, i, count), + chart: chart, + el: el + }; + } + + meta.$filler = source; + sources.push(source); + } + + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source) { + continue; + } + + source.fill = resolveTarget(sources, i, propagate); + source.boundary = computeBoundary(source); + source.mapper = createMapper(source); + } + }, + + beforeDatasetDraw: function(chart, args) { + var meta = args.meta.$filler; + if (!meta) { return; } - // building first area curve (normal) - ctx.moveTo(curve0[0].x, curve0[0].y); - for (i = 1; i < len0; ++i) { - helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); - } + var ctx = chart.ctx; + var el = meta.el; + var view = el._view; + var points = el._children || []; + var mapper = meta.mapper; + var color = view.backgroundColor || defaults.global.defaultColor; - // joining the two area curves - ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); - - // building opposite area curve (reverse) - for (i = len1 - 1; i > 0; --i) { - helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); + if (mapper && color && points.length) { + helpers.canvas.clipArea(ctx, chart.chartArea); + doFill(ctx, points, mapper, view, color, el._loop); + helpers.canvas.unclipArea(ctx); } } - - function doFill(ctx, points, mapper, view, color, loop) { - var count = points.length; - var span = view.spanGaps; - var curve0 = []; - var curve1 = []; - var len0 = 0; - var len1 = 0; - var i, ilen, index, p0, p1, d0, d1; - - ctx.beginPath(); - - for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { - index = i % count; - p0 = points[index]._view; - p1 = mapper(p0, index, view); - d0 = isDrawable(p0); - d1 = isDrawable(p1); - - if (d0 && d1) { - len0 = curve0.push(p0); - len1 = curve1.push(p1); - } else if (len0 && len1) { - if (!span) { - drawArea(ctx, curve0, curve1, len0, len1); - len0 = len1 = 0; - curve0 = []; - curve1 = []; - } else { - if (d0) { - curve0.push(p0); - } - if (d1) { - curve1.push(p1); - } - } - } - } - - drawArea(ctx, curve0, curve1, len0, len1); - - ctx.closePath(); - ctx.fillStyle = color; - ctx.fill(); - } - - return { - id: 'filler', - - afterDatasetsUpdate: function(chart, options) { - var count = (chart.data.datasets || []).length; - var propagate = options.propagate; - var sources = []; - var meta, i, el, source; - - for (i = 0; i < count; ++i) { - meta = chart.getDatasetMeta(i); - el = meta.dataset; - source = null; - - if (el && el._model && el instanceof elements.Line) { - source = { - visible: chart.isDatasetVisible(i), - fill: decodeFill(el, i, count), - chart: chart, - el: el - }; - } - - meta.$filler = source; - sources.push(source); - } - - for (i = 0; i < count; ++i) { - source = sources[i]; - if (!source) { - continue; - } - - source.fill = resolveTarget(sources, i, propagate); - source.boundary = computeBoundary(source); - source.mapper = createMapper(source); - } - }, - - beforeDatasetDraw: function(chart, args) { - var meta = args.meta.$filler; - if (!meta) { - return; - } - - var ctx = chart.ctx; - var el = meta.el; - var view = el._view; - var points = el._children || []; - var mapper = meta.mapper; - var color = view.backgroundColor || defaults.global.defaultColor; - - if (mapper && color && points.length) { - helpers.canvas.clipArea(ctx, chart.chartArea); - doFill(ctx, points, mapper, view, color, el._loop); - helpers.canvas.unclipArea(ctx); - } - } - }; }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 7e0e429d0..3715ea3d5 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -5,6 +5,8 @@ var Element = require('../core/core.element'); var helpers = require('../helpers/index'); var layout = require('../core/core.layout'); +var noop = helpers.noop; + defaults._set('global', { legend: { display: true, @@ -80,488 +82,495 @@ defaults._set('global', { } }); -module.exports = function(Chart) { +/** + * Helper function to get the box width based on the usePointStyle option + * @param labelopts {Object} the label options on the legend + * @param fontSize {Number} the label font size + * @return {Number} width of the color box area + */ +function getBoxWidth(labelOpts, fontSize) { + return labelOpts.usePointStyle ? + fontSize * Math.SQRT2 : + labelOpts.boxWidth; +} - var noop = helpers.noop; +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Legend = Element.extend({ - /** - * Helper function to get the box width based on the usePointStyle option - * @param labelopts {Object} the label options on the legend - * @param fontSize {Number} the label font size - * @return {Number} width of the color box area - */ - function getBoxWidth(labelOpts, fontSize) { - return labelOpts.usePointStyle ? - fontSize * Math.SQRT2 : - labelOpts.boxWidth; - } + initialize: function(config) { + helpers.extend(this, config); - Chart.Legend = Element.extend({ + // Contains hit boxes for each dataset (in dataset order) + this.legendHitBoxes = []; - initialize: function(config) { - helpers.extend(this, config); + // Are we in doughnut mode which has a different data type + this.doughnutMode = false; + }, - // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; + // These methods are ordered by lifecycle. Utilities then follow. + // Any function defined here is inherited by all legend types. + // Any function can be extended by the legend type - // Are we in doughnut mode which has a different data type - this.doughnutMode = false; - }, + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; - // These methods are ordered by lifecycle. Utilities then follow. - // Any function defined here is inherited by all legend types. - // Any function can be extended by the legend type + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - }, - afterUpdate: noop, + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // + me.afterUpdate(); - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; + return me.minSize; + }, + afterUpdate: noop, - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } + // - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } - // + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; - beforeBuildLabels: noop, - buildLabels: function() { - var me = this; - var labelOpts = me.options.labels || {}; - var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, - if (labelOpts.filter) { - legendItems = legendItems.filter(function(item) { - return labelOpts.filter(item, me.chart.data); + // + + beforeBuildLabels: noop, + buildLabels: function() { + var me = this; + var labelOpts = me.options.labels || {}; + var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; + + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } + + if (me.options.reverse) { + legendItems.reverse(); + } + + me.legendItems = legendItems; + }, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var display = opts.display; + + var ctx = me.ctx; + + var globalDefault = defaults.global; + var valueOrDefault = helpers.valueOrDefault; + var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); + var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); + var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); + var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + + // Reset hit boxes + var hitboxes = me.legendHitBoxes = []; + + var minSize = me.minSize; + var isHorizontal = me.isHorizontal(); + + if (isHorizontal) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = display ? 10 : 0; + } else { + minSize.width = display ? 10 : 0; + minSize.height = me.maxHeight; // fill all the height + } + + // Increase sizes here + if (display) { + ctx.font = labelFont; + + if (isHorizontal) { + // Labels + + // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one + var lineWidths = me.lineWidths = [0]; + var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; + + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + helpers.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + + if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { + totalHeight += fontSize + (labelOpts.padding); + lineWidths[lineWidths.length] = me.left; + } + + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: width, + height: fontSize + }; + + lineWidths[lineWidths.length - 1] += width + labelOpts.padding; }); + + minSize.height += totalHeight; + + } else { + var vPadding = labelOpts.padding; + var columnWidths = me.columnWidths = []; + var totalWidth = labelOpts.padding; + var currentColWidth = 0; + var currentColHeight = 0; + var itemHeight = fontSize + vPadding; + + helpers.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + + // If too tall, go to new column + if (currentColHeight + itemHeight > minSize.height) { + totalWidth += currentColWidth + labelOpts.padding; + columnWidths.push(currentColWidth); // previous column width + + currentColWidth = 0; + currentColHeight = 0; + } + + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight; + + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: itemWidth, + height: fontSize + }; + }); + + totalWidth += currentColWidth; + columnWidths.push(currentColWidth); + minSize.width += totalWidth; } + } - if (me.options.reverse) { - legendItems.reverse(); - } + me.width = minSize.width; + me.height = minSize.height; + }, + afterFit: noop, - me.legendItems = legendItems; - }, - afterBuildLabels: noop, + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, - // - - beforeFit: noop, - fit: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var display = opts.display; + // Actually draw the legend on the canvas + draw: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var globalDefault = defaults.global; + var lineDefault = globalDefault.elements.line; + var legendWidth = me.width; + var lineWidths = me.lineWidths; + if (opts.display) { var ctx = me.ctx; - - var globalDefault = defaults.global; var valueOrDefault = helpers.valueOrDefault; + var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var cursor; - // Reset hit boxes - var hitboxes = me.legendHitBoxes = []; + // Canvas setup + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.strokeStyle = fontColor; // for strikethrough effect + ctx.fillStyle = fontColor; // render in correct colour + ctx.font = labelFont; - var minSize = me.minSize; + var boxWidth = getBoxWidth(labelOpts, fontSize); + var hitboxes = me.legendHitBoxes; + + // current position + var drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0) { + return; + } + + // Set the ctx for the box + ctx.save(); + + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); + ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); + ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); + ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); + ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); + var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); + + if (ctx.setLineDash) { + // IE 9 and 10 do not support line dash + ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); + } + + if (opts.labels && opts.labels.usePointStyle) { + // Recalculate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + var radius = fontSize * Math.SQRT2 / 2; + var offSet = radius / Math.SQRT2; + var centerX = x + offSet; + var centerY = y + offSet; + + // Draw pointStyle as legend symbol + helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); + } else { + // Draw box as legend symbol + if (!isLineWidthZero) { + ctx.strokeRect(x, y, boxWidth, fontSize); + } + ctx.fillRect(x, y, boxWidth, fontSize); + } + + ctx.restore(); + }; + var fillText = function(x, y, legendItem, textWidth) { + var halfFontSize = fontSize / 2; + var xLeft = boxWidth + halfFontSize + x; + var yMiddle = y + halfFontSize; + + ctx.fillText(legendItem.text, xLeft, yMiddle); + + if (legendItem.hidden) { + // Strikethrough the text if hidden + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.moveTo(xLeft, yMiddle); + ctx.lineTo(xLeft + textWidth, yMiddle); + ctx.stroke(); + } + }; + + // Horizontal var isHorizontal = me.isHorizontal(); - if (isHorizontal) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = display ? 10 : 0; + cursor = { + x: me.left + ((legendWidth - lineWidths[0]) / 2), + y: me.top + labelOpts.padding, + line: 0 + }; } else { - minSize.width = display ? 10 : 0; - minSize.height = me.maxHeight; // fill all the height + cursor = { + x: me.left + labelOpts.padding, + y: me.top + labelOpts.padding, + line: 0 + }; } - // Increase sizes here - if (display) { - ctx.font = labelFont; + var itemHeight = fontSize + labelOpts.padding; + helpers.each(me.legendItems, function(legendItem, i) { + var textWidth = ctx.measureText(legendItem.text).width; + var width = boxWidth + (fontSize / 2) + textWidth; + var x = cursor.x; + var y = cursor.y; if (isHorizontal) { - // Labels - - // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one - var lineWidths = me.lineWidths = [0]; - var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; - - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - - helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - - if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { - totalHeight += fontSize + (labelOpts.padding); - lineWidths[lineWidths.length] = me.left; - } - - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: width, - height: fontSize - }; - - lineWidths[lineWidths.length - 1] += width + labelOpts.padding; - }); - - minSize.height += totalHeight; - - } else { - var vPadding = labelOpts.padding; - var columnWidths = me.columnWidths = []; - var totalWidth = labelOpts.padding; - var currentColWidth = 0; - var currentColHeight = 0; - var itemHeight = fontSize + vPadding; - - helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - - // If too tall, go to new column - if (currentColHeight + itemHeight > minSize.height) { - totalWidth += currentColWidth + labelOpts.padding; - columnWidths.push(currentColWidth); // previous column width - - currentColWidth = 0; - currentColHeight = 0; - } - - // Get max width - currentColWidth = Math.max(currentColWidth, itemWidth); - currentColHeight += itemHeight; - - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: itemWidth, - height: fontSize - }; - }); - - totalWidth += currentColWidth; - columnWidths.push(currentColWidth); - minSize.width += totalWidth; - } - } - - me.width = minSize.width; - me.height = minSize.height; - }, - afterFit: noop, - - // Shared Methods - isHorizontal: function() { - return this.options.position === 'top' || this.options.position === 'bottom'; - }, - - // Actually draw the legend on the canvas - draw: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var globalDefault = defaults.global; - var lineDefault = globalDefault.elements.line; - var legendWidth = me.width; - var lineWidths = me.lineWidths; - - if (opts.display) { - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var cursor; - - // Canvas setup - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - ctx.lineWidth = 0.5; - ctx.strokeStyle = fontColor; // for strikethrough effect - ctx.fillStyle = fontColor; // render in correct colour - ctx.font = labelFont; - - var boxWidth = getBoxWidth(labelOpts, fontSize); - var hitboxes = me.legendHitBoxes; - - // current position - var drawLegendBox = function(x, y, legendItem) { - if (isNaN(boxWidth) || boxWidth <= 0) { - return; - } - - // Set the ctx for the box - ctx.save(); - - ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); - ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); - ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); - ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); - ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); - ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); - var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); - - if (ctx.setLineDash) { - // IE 9 and 10 do not support line dash - ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); - } - - if (opts.labels && opts.labels.usePointStyle) { - // Recalculate x and y for drawPoint() because its expecting - // x and y to be center of figure (instead of top left) - var radius = fontSize * Math.SQRT2 / 2; - var offSet = radius / Math.SQRT2; - var centerX = x + offSet; - var centerY = y + offSet; - - // Draw pointStyle as legend symbol - helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); - } else { - // Draw box as legend symbol - if (!isLineWidthZero) { - ctx.strokeRect(x, y, boxWidth, fontSize); - } - ctx.fillRect(x, y, boxWidth, fontSize); - } - - ctx.restore(); - }; - var fillText = function(x, y, legendItem, textWidth) { - var halfFontSize = fontSize / 2; - var xLeft = boxWidth + halfFontSize + x; - var yMiddle = y + halfFontSize; - - ctx.fillText(legendItem.text, xLeft, yMiddle); - - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(xLeft, yMiddle); - ctx.lineTo(xLeft + textWidth, yMiddle); - ctx.stroke(); - } - }; - - // Horizontal - var isHorizontal = me.isHorizontal(); - if (isHorizontal) { - cursor = { - x: me.left + ((legendWidth - lineWidths[0]) / 2), - y: me.top + labelOpts.padding, - line: 0 - }; - } else { - cursor = { - x: me.left + labelOpts.padding, - y: me.top + labelOpts.padding, - line: 0 - }; - } - - var itemHeight = fontSize + labelOpts.padding; - helpers.each(me.legendItems, function(legendItem, i) { - var textWidth = ctx.measureText(legendItem.text).width; - var width = boxWidth + (fontSize / 2) + textWidth; - var x = cursor.x; - var y = cursor.y; - - if (isHorizontal) { - if (x + width >= legendWidth) { - y = cursor.y += itemHeight; - cursor.line++; - x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); - } - } else if (y + itemHeight > me.bottom) { - x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top + labelOpts.padding; + if (x + width >= legendWidth) { + y = cursor.y += itemHeight; cursor.line++; + x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); } - - drawLegendBox(x, y, legendItem); - - hitboxes[i].left = x; - hitboxes[i].top = y; - - // Fill the actual label - fillText(x, y, legendItem, textWidth); - - if (isHorizontal) { - cursor.x += width + (labelOpts.padding); - } else { - cursor.y += itemHeight; - } - - }); - } - }, - - /** - * Handle an event - * @private - * @param {IEvent} event - The event to handle - * @return {Boolean} true if a change occured - */ - handleEvent: function(e) { - var me = this; - var opts = me.options; - var type = e.type === 'mouseup' ? 'click' : e.type; - var changed = false; - - if (type === 'mousemove') { - if (!opts.onHover) { - return; + } else if (y + itemHeight > me.bottom) { + x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; + y = cursor.y = me.top + labelOpts.padding; + cursor.line++; } - } else if (type === 'click') { - if (!opts.onClick) { - return; + + drawLegendBox(x, y, legendItem); + + hitboxes[i].left = x; + hitboxes[i].top = y; + + // Fill the actual label + fillText(x, y, legendItem, textWidth); + + if (isHorizontal) { + cursor.x += width + (labelOpts.padding); + } else { + cursor.y += itemHeight; } - } else { + + }); + } + }, + + /** + * Handle an event + * @private + * @param {IEvent} event - The event to handle + * @return {Boolean} true if a change occured + */ + handleEvent: function(e) { + var me = this; + var opts = me.options; + var type = e.type === 'mouseup' ? 'click' : e.type; + var changed = false; + + if (type === 'mousemove') { + if (!opts.onHover) { return; } + } else if (type === 'click') { + if (!opts.onClick) { + return; + } + } else { + return; + } - // Chart event already has relative position in it - var x = e.x; - var y = e.y; + // Chart event already has relative position in it + var x = e.x; + var 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 - var lh = me.legendHitBoxes; - for (var i = 0; i < lh.length; ++i) { - var hitBox = lh[i]; + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + // See if we are touching one of the dataset boxes + var lh = me.legendHitBoxes; + for (var i = 0; i < lh.length; ++i) { + var hitBox = lh[i]; - if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { - // Touching an element - if (type === 'click') { - // use e.native for backwards compatibility - opts.onClick.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } else if (type === 'mousemove') { - // use e.native for backwards compatibility - opts.onHover.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + // Touching an element + if (type === 'click') { + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, me.legendItems[i]); + changed = true; + break; + } else if (type === 'mousemove') { + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, me.legendItems[i]); + changed = true; + break; } } } - - return changed; } + + return changed; + } +}); + +function createNewLegendAndAttach(chart, legendOpts) { + var legend = new Legend({ + ctx: chart.ctx, + options: legendOpts, + chart: chart }); - function createNewLegendAndAttach(chart, legendOpts) { - var legend = new Chart.Legend({ - ctx: chart.ctx, - options: legendOpts, - chart: chart - }); + layout.configure(chart, legend, legendOpts); + layout.addBox(chart, legend); + chart.legend = legend; +} - layout.configure(chart, legend, legendOpts); - layout.addBox(chart, legend); - chart.legend = legend; - } +module.exports = { + id: 'legend', - return { - id: 'legend', + /** + * Backward compatibility: since 2.1.5, the legend is registered as a plugin, making + * Chart.Legend obsolete. To avoid a breaking change, we export the Legend as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Legend, - beforeInit: function(chart) { - var legendOpts = chart.options.legend; + beforeInit: function(chart) { + var legendOpts = chart.options.legend; - if (legendOpts) { + if (legendOpts) { + createNewLegendAndAttach(chart, legendOpts); + } + }, + + beforeUpdate: function(chart) { + var legendOpts = chart.options.legend; + var legend = chart.legend; + + if (legendOpts) { + helpers.mergeIf(legendOpts, defaults.global.legend); + + if (legend) { + layout.configure(chart, legend, legendOpts); + legend.options = legendOpts; + } else { createNewLegendAndAttach(chart, legendOpts); } - }, - - beforeUpdate: function(chart) { - var legendOpts = chart.options.legend; - var legend = chart.legend; - - if (legendOpts) { - helpers.mergeIf(legendOpts, defaults.global.legend); - - if (legend) { - layout.configure(chart, legend, legendOpts); - legend.options = legendOpts; - } else { - createNewLegendAndAttach(chart, legendOpts); - } - } else if (legend) { - layout.removeBox(chart, legend); - delete chart.legend; - } - }, - - afterEvent: function(chart, e) { - var legend = chart.legend; - if (legend) { - legend.handleEvent(e); - } + } else if (legend) { + layout.removeBox(chart, legend); + delete chart.legend; } - }; + }, + + afterEvent: function(chart, e) { + var legend = chart.legend; + if (legend) { + legend.handleEvent(e); + } + } }; diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index 8eac103f5..0a233f9bc 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -5,6 +5,8 @@ var Element = require('../core/core.element'); var helpers = require('../helpers/index'); var layout = require('../core/core.layout'); +var noop = helpers.noop; + defaults._set('global', { title: { display: false, @@ -18,226 +20,233 @@ defaults._set('global', { } }); -module.exports = function(Chart) { +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Title = Element.extend({ + initialize: function(config) { + var me = this; + helpers.extend(me, config); - var noop = helpers.noop; + // Contains hit boxes for each dataset (in dataset order) + me.legendHitBoxes = []; + }, - Chart.Title = Element.extend({ - initialize: function(config) { - var me = this; - helpers.extend(me, config); + // These methods are ordered by lifecycle. Utilities then follow. - // Contains hit boxes for each dataset (in dataset order) - me.legendHitBoxes = []; - }, + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; - // These methods are ordered by lifecycle. Utilities then follow. + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - - }, - afterUpdate: noop, + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // + me.afterUpdate(); - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; + return me.minSize; - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } + }, + afterUpdate: noop, - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; + // - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; - // - - beforeBuildLabels: noop, - buildLabels: noop, - afterBuildLabels: noop, - - // - - beforeFit: noop, - fit: function() { - var me = this; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var display = opts.display; - var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); - var minSize = me.minSize; - var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; - - if (me.isHorizontal()) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = textSize; - } else { - minSize.width = textSize; - minSize.height = me.maxHeight; // fill all the height - } - - me.width = minSize.width; - me.height = minSize.height; - - }, - afterFit: noop, - - // Shared Methods - isHorizontal: function() { - var pos = this.options.position; - return pos === 'top' || pos === 'bottom'; - }, - - // Actually draw the title block on the canvas - draw: function() { - var me = this; - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var globalDefaults = defaults.global; - - if (opts.display) { - var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); - var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); - var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); - var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var offset = lineHeight / 2 + opts.padding; - var rotation = 0; - var top = me.top; - var left = me.left; - var bottom = me.bottom; - var right = me.right; - var maxWidth, titleX, titleY; - - ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour - ctx.font = titleFont; - - // Horizontal - if (me.isHorizontal()) { - titleX = left + ((right - left) / 2); // midpoint of the width - titleY = top + offset; - maxWidth = right - left; - } else { - titleX = opts.position === 'left' ? left + offset : right - offset; - titleY = top + ((bottom - top) / 2); - maxWidth = bottom - top; - rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); - } - - ctx.save(); - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - var text = opts.text; - if (helpers.isArray(text)) { - var y = 0; - for (var i = 0; i < text.length; ++i) { - ctx.fillText(text[i], 0, y, maxWidth); - y += lineHeight; - } - } else { - ctx.fillText(text, 0, 0, maxWidth); - } - - ctx.restore(); - } + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; } + + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, + + // + + beforeBuildLabels: noop, + buildLabels: noop, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var display = opts.display; + var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); + var minSize = me.minSize; + var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; + var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); + var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; + + if (me.isHorizontal()) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = textSize; + } else { + minSize.width = textSize; + minSize.height = me.maxHeight; // fill all the height + } + + me.width = minSize.width; + me.height = minSize.height; + + }, + afterFit: noop, + + // Shared Methods + isHorizontal: function() { + var pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + }, + + // Actually draw the title block on the canvas + draw: function() { + var me = this; + var ctx = me.ctx; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var globalDefaults = defaults.global; + + if (opts.display) { + var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); + var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); + var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); + var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); + var offset = lineHeight / 2 + opts.padding; + var rotation = 0; + var top = me.top; + var left = me.left; + var bottom = me.bottom; + var right = me.right; + var maxWidth, titleX, titleY; + + ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour + ctx.font = titleFont; + + // Horizontal + if (me.isHorizontal()) { + titleX = left + ((right - left) / 2); // midpoint of the width + titleY = top + offset; + maxWidth = right - left; + } else { + titleX = opts.position === 'left' ? left + offset : right - offset; + titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; + rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); + } + + ctx.save(); + ctx.translate(titleX, titleY); + ctx.rotate(rotation); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + var text = opts.text; + if (helpers.isArray(text)) { + var y = 0; + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], 0, y, maxWidth); + y += lineHeight; + } + } else { + ctx.fillText(text, 0, 0, maxWidth); + } + + ctx.restore(); + } + } +}); + +function createNewTitleBlockAndAttach(chart, titleOpts) { + var title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart: chart }); - function createNewTitleBlockAndAttach(chart, titleOpts) { - var title = new Chart.Title({ - ctx: chart.ctx, - options: titleOpts, - chart: chart - }); + layout.configure(chart, title, titleOpts); + layout.addBox(chart, title); + chart.titleBlock = title; +} - layout.configure(chart, title, titleOpts); - layout.addBox(chart, title); - chart.titleBlock = title; - } +module.exports = { + id: 'title', - return { - id: 'title', + /** + * Backward compatibility: since 2.1.5, the title is registered as a plugin, making + * Chart.Title obsolete. To avoid a breaking change, we export the Title as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Title, - beforeInit: function(chart) { - var titleOpts = chart.options.title; + beforeInit: function(chart) { + var titleOpts = chart.options.title; - if (titleOpts) { + if (titleOpts) { + createNewTitleBlockAndAttach(chart, titleOpts); + } + }, + + beforeUpdate: function(chart) { + var titleOpts = chart.options.title; + var titleBlock = chart.titleBlock; + + if (titleOpts) { + helpers.mergeIf(titleOpts, defaults.global.title); + + if (titleBlock) { + layout.configure(chart, titleBlock, titleOpts); + titleBlock.options = titleOpts; + } else { createNewTitleBlockAndAttach(chart, titleOpts); } - }, - - beforeUpdate: function(chart) { - var titleOpts = chart.options.title; - var titleBlock = chart.titleBlock; - - if (titleOpts) { - helpers.mergeIf(titleOpts, defaults.global.title); - - if (titleBlock) { - layout.configure(chart, titleBlock, titleOpts); - titleBlock.options = titleOpts; - } else { - createNewTitleBlockAndAttach(chart, titleOpts); - } - } else if (titleBlock) { - layout.removeBox(chart, titleBlock); - delete chart.titleBlock; - } + } else if (titleBlock) { + layout.removeBox(chart, titleBlock); + delete chart.titleBlock; } - }; + } }; diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index fee37288d..08d5e79db 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -381,5 +381,23 @@ describe('Deprecations', function() { expect(Chart.pluginService).toBe(Chart.plugins); }); }); + + describe('Chart.Legend', function() { + it('should be defined and an instance of Chart.Element', function() { + var legend = new Chart.Legend({}); + expect(Chart.Legend).toBeDefined(); + expect(legend).not.toBe(undefined); + expect(legend instanceof Chart.Element).toBeTruthy(); + }); + }); + + describe('Chart.Title', function() { + it('should be defined and an instance of Chart.Element', function() { + var title = new Chart.Title({}); + expect(Chart.Title).toBeDefined(); + expect(title).not.toBe(undefined); + expect(title instanceof Chart.Element).toBeTruthy(); + }); + }); }); }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index b950c3601..5b75069aa 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -1,10 +1,5 @@ // Test the rectangle element describe('Legend block tests', function() { - it('Should be constructed', function() { - var legend = new Chart.Legend({}); - expect(legend).not.toBe(undefined); - }); - it('should have the correct default config', function() { expect(Chart.defaults.global.legend).toEqual({ display: true, diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js index edeb32a8b..28786f054 100644 --- a/test/specs/plugin.title.tests.js +++ b/test/specs/plugin.title.tests.js @@ -1,11 +1,6 @@ // Test the rectangle element describe('Title block tests', function() { - it('Should be constructed', function() { - var title = new Chart.Title({}); - expect(title).not.toBe(undefined); - }); - it('Should have the correct default config', function() { expect(Chart.defaults.global.title).toEqual({ display: false, From 6bea15e7cf89003e3a5945a20cf1d2cc5096728e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 9 Jan 2018 14:12:40 +0100 Subject: [PATCH 33/56] Rename Chart.layout to Chart.layouts (#5118) Chart.layouts seems more consistent with other service names (Chart.plugins, Chart.scales, etc.) but also more inline with the service which handle many layout (one per charts). --- src/chart.js | 6 +++--- src/core/core.controller.js | 6 +++--- src/core/{core.layout.js => core.layouts.js} | 0 src/core/core.scaleService.js | 4 ++-- src/plugins/plugin.legend.js | 10 +++++----- src/plugins/plugin.title.js | 10 +++++----- ...layoutService.tests.js => core.layouts.tests.js} | 13 +++++++++++-- test/specs/global.deprecations.tests.js | 8 ++++---- 8 files changed, 33 insertions(+), 24 deletions(-) rename src/core/{core.layout.js => core.layouts.js} (100%) rename test/specs/{core.layoutService.tests.js => core.layouts.tests.js} (97%) diff --git a/src/chart.js b/src/chart.js index 70d70a669..a958e343f 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,7 +12,7 @@ Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); -Chart.layout = require('./core/core.layout'); +Chart.layouts = require('./core/core.layouts'); Chart.platform = require('./platforms/platform'); Chart.plugins = require('./core/core.plugins'); Chart.Ticks = require('./core/core.ticks'); @@ -113,10 +113,10 @@ Chart.PluginBase = Chart.Element.extend({}); Chart.canvasHelpers = Chart.helpers.canvas; /** - * Provided for backward compatibility, use Chart.layout instead. + * Provided for backward compatibility, use Chart.layouts instead. * @namespace Chart.layoutService * @deprecated since version 2.8.0 * @todo remove at version 3 * @private */ -Chart.layoutService = Chart.layout; +Chart.layoutService = Chart.layouts; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 157bc50a4..e32255edc 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,7 +3,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); -var layout = require('./core.layout'); +var layouts = require('./core.layouts'); var platform = require('../platforms/platform'); var plugins = require('./core.plugins'); @@ -47,7 +47,7 @@ module.exports = function(Chart) { var newOptions = chart.options; helpers.each(chart.scales, function(scale) { - layout.removeBox(chart, scale); + layouts.removeBox(chart, scale); }); newOptions = helpers.configMerge( @@ -436,7 +436,7 @@ module.exports = function(Chart) { return; } - layout.update(this, this.width, this.height); + layouts.update(this, this.width, this.height); /** * Provided for backward compatibility, use `afterLayout` instead. diff --git a/src/core/core.layout.js b/src/core/core.layouts.js similarity index 100% rename from src/core/core.layout.js rename to src/core/core.layouts.js diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index bfbf83209..f2ea01d32 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -2,7 +2,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); -var layout = require('./core.layout'); +var layouts = require('./core.layouts'); module.exports = function(Chart) { @@ -39,7 +39,7 @@ module.exports = function(Chart) { scale.fullWidth = scale.options.fullWidth; scale.position = scale.options.position; scale.weight = scale.options.weight; - layout.addBox(chart, scale); + layouts.addBox(chart, scale); }); } }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 3715ea3d5..3f1559c30 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -3,7 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); -var layout = require('../core/core.layout'); +var layouts = require('../core/core.layouts'); var noop = helpers.noop; @@ -523,8 +523,8 @@ function createNewLegendAndAttach(chart, legendOpts) { chart: chart }); - layout.configure(chart, legend, legendOpts); - layout.addBox(chart, legend); + layouts.configure(chart, legend, legendOpts); + layouts.addBox(chart, legend); chart.legend = legend; } @@ -556,13 +556,13 @@ module.exports = { helpers.mergeIf(legendOpts, defaults.global.legend); if (legend) { - layout.configure(chart, legend, legendOpts); + layouts.configure(chart, legend, legendOpts); legend.options = legendOpts; } else { createNewLegendAndAttach(chart, legendOpts); } } else if (legend) { - layout.removeBox(chart, legend); + layouts.removeBox(chart, legend); delete chart.legend; } }, diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index 0a233f9bc..47588844d 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -3,7 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); -var layout = require('../core/core.layout'); +var layouts = require('../core/core.layouts'); var noop = helpers.noop; @@ -206,8 +206,8 @@ function createNewTitleBlockAndAttach(chart, titleOpts) { chart: chart }); - layout.configure(chart, title, titleOpts); - layout.addBox(chart, title); + layouts.configure(chart, title, titleOpts); + layouts.addBox(chart, title); chart.titleBlock = title; } @@ -239,13 +239,13 @@ module.exports = { helpers.mergeIf(titleOpts, defaults.global.title); if (titleBlock) { - layout.configure(chart, titleBlock, titleOpts); + layouts.configure(chart, titleBlock, titleOpts); titleBlock.options = titleOpts; } else { createNewTitleBlockAndAttach(chart, titleOpts); } } else if (titleBlock) { - layout.removeBox(chart, titleBlock); + layouts.removeBox(chart, titleBlock); delete chart.titleBlock; } } diff --git a/test/specs/core.layoutService.tests.js b/test/specs/core.layouts.tests.js similarity index 97% rename from test/specs/core.layoutService.tests.js rename to test/specs/core.layouts.tests.js index a8673971b..19b3a14b2 100644 --- a/test/specs/core.layoutService.tests.js +++ b/test/specs/core.layouts.tests.js @@ -1,5 +1,14 @@ -// Tests of the scale service -describe('Test the layout service', function() { +describe('Chart.layouts', function() { + it('should be exposed through Chart.layouts', function() { + expect(Chart.layouts).toBeDefined(); + expect(typeof Chart.layouts).toBe('object'); + expect(Chart.layouts.defaults).toBeDefined(); + expect(Chart.layouts.addBox).toBeDefined(); + expect(Chart.layouts.removeBox).toBeDefined(); + expect(Chart.layouts.configure).toBeDefined(); + expect(Chart.layouts.update).toBeDefined(); + }); + // Disable tests which need to be rewritten based on changes introduced by // the following changes: https://github.com/chartjs/Chart.js/pull/2346 // using xit marks the test as pending: http://jasmine.github.io/2.0/introduction.html#section-Pending_Specs diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 08d5e79db..535b9af33 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,9 +1,9 @@ describe('Deprecations', function() { describe('Version 2.8.0', function() { describe('Chart.layoutService', function() { - it('should be defined and an alias of Chart.layout', function() { + it('should be defined and an alias of Chart.layouts', function() { expect(Chart.layoutService).toBeDefined(); - expect(Chart.layoutService).toBe(Chart.layout); + expect(Chart.layoutService).toBe(Chart.layouts); }); }); }); @@ -311,8 +311,8 @@ describe('Deprecations', function() { 'afterLayout' ]; - var override = Chart.layout.update; - Chart.layout.update = function() { + var override = Chart.layouts.update; + Chart.layouts.update = function() { sequence.push('layoutUpdate'); override.apply(this, arguments); }; From 1d5619d6d42867f4380527901f4a39a17095d98e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 11 Jan 2018 09:03:16 +0100 Subject: [PATCH 34/56] Fix GitBook error with the shared ESLint config (#5133) `gitbook-cli install` failed when trying to fetch eslint-config-chartjs because of the way it was installed (ie. using the GitHub repository URL). The shared config is now published on npmjs: https://www.npmjs.com/package/eslint-config-chartjs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 510c141cd..c7d21043b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "child-process-promise": "^2.2.1", "coveralls": "^3.0.0", "eslint": "^4.9.0", - "eslint-config-chartjs": "git+https://github.com/chartjs/eslint-config-chartjs.git", + "eslint-config-chartjs": "^0.1.0", "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", From 2f5a3e171be30465f107e78c4283a2111ed3d5b4 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 11 Jan 2018 12:51:03 +0100 Subject: [PATCH 35/56] Ignore package-lock.json (#5138) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cfb878a6d..0a65be9b2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /gh-pages /jsdoc /node_modules +/package-lock.json .DS_Store .idea .vscode From 2d7f0a46c3f58278f883cb829d9e8a109151559d Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 13 Jan 2018 14:23:50 +0100 Subject: [PATCH 36/56] Fix updating plugin options (#5144) Cached plugin descriptors hold a reference on the plugin options, which break if the plugin options object is replaced. That case happens when the user updates the plugin options with a new object, but also since the new config update logic (#4198) that now always clones the plugin options. The fix consists in explicitly invalidating that cache before updating the chart. --- src/core/core.controller.js | 4 ++++ src/core/core.plugins.js | 12 +++++++++++- test/specs/core.plugin.tests.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e32255edc..e29a5b076 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -376,6 +376,10 @@ module.exports = function(Chart) { updateConfig(me); + // plugins options references might have change, let's invalidate the cache + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + plugins._invalidate(me); + if (plugins.notify(me, 'beforeUpdate') === false) { return; } diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js index f5e8d10d8..f2fbcadec 100644 --- a/src/core/core.plugins.js +++ b/src/core/core.plugins.js @@ -121,7 +121,7 @@ module.exports = { * @private */ descriptors: function(chart) { - var cache = chart._plugins || (chart._plugins = {}); + var cache = chart.$plugins || (chart.$plugins = {}); if (cache.id === this._cacheId) { return cache.descriptors; } @@ -157,6 +157,16 @@ module.exports = { cache.descriptors = descriptors; cache.id = this._cacheId; return descriptors; + }, + + /** + * Invalidates cache for the given chart: descriptors hold a reference on plugin option, + * but in some cases, this reference can be changed by the user when updating options. + * https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + * @private + */ + _invalidate: function(chart) { + delete chart.$plugins; } }; diff --git a/test/specs/core.plugin.tests.js b/test/specs/core.plugin.tests.js index 387d78808..3a9e908a3 100644 --- a/test/specs/core.plugin.tests.js +++ b/test/specs/core.plugin.tests.js @@ -339,6 +339,39 @@ describe('Chart.plugins', function() { expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[1]).toEqual({a: 'foobar'}); + + delete Chart.defaults.global.plugins.a; + }); + + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + it('should invalidate cache when update plugin options', function() { + var plugin = {id: 'a', hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin], + options: { + plugins: { + a: { + foo: 'foo' + } + } + }, + }); + + spyOn(plugin, 'hook'); + + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({foo: 'foo'}); + + chart.options.plugins.a = {bar: 'bar'}; + chart.update(); + + plugin.hook.calls.reset(); + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({bar: 'bar'}); }); }); }); From 37ec8384d7f6ce9b07a822626d787d1327a4a235 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 13 Jan 2018 07:39:17 -0800 Subject: [PATCH 37/56] Format the label in the time scale tooltip (#5095) --- src/scales/scale.time.js | 29 ++++++++- test/specs/scale.time.tests.js | 109 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 90904f9e6..4892eea99 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -403,6 +403,27 @@ function ticksFromTimestamps(values, majorUnit) { return ticks; } +function determineLabelFormat(data, timeOpts) { + var i, momentDate, hasTime; + var ilen = data.length; + + // find the label with the most parts (milliseconds, minutes, etc.) + // format all labels with the same level of detail as the most specific label + for (i = 0; i < ilen; i++) { + momentDate = momentify(data[i], timeOpts); + if (momentDate.millisecond() !== 0) { + return 'MMM D, YYYY h:mm:ss.SSS a'; + } + if (momentDate.second() !== 0 || momentDate.minute() !== 0 || momentDate.hour() !== 0) { + hasTime = true; + } + } + if (hasTime) { + return 'MMM D, YYYY h:mm:ss a'; + } + return 'MMM D, YYYY'; +} + module.exports = function(Chart) { var defaultConfig = { @@ -621,6 +642,7 @@ module.exports = function(Chart) { me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); + me._labelFormat = determineLabelFormat(me._timestamps.data, timeOpts); return ticksFromTimestamps(ticks, me._majorUnit); }, @@ -636,10 +658,13 @@ module.exports = function(Chart) { label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - label = momentify(label, timeOpts).format(timeOpts.tooltipFormat); + return momentify(label, timeOpts).format(timeOpts.tooltipFormat); + } + if (typeof label === 'string') { + return label; } - return label; + return momentify(label, timeOpts).format(me._labelFormat); }, /** diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index e4c1739e6..964e25e08 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -584,6 +584,115 @@ describe('Time scale tests', function() { expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); }); + it('should get the correct label when time is specified as a string', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [{t: '2015-01-01T20:00:00', y: 10}, {t: '2015-01-02T21:00:00', y: 3}] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); + expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); + }); + + it('should get the correct label for a timestamp with milliseconds', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23.234'), y: 10}, + {t: +new Date('2018-01-09 06:17:43.426'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23.234 am'); + }); + + it('should get the correct label for a timestamp with time', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23'), y: 10}, + {t: +new Date('2018-01-09 06:17:43'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23 am'); + }); + + it('should get the correct label for a timestamp representing a date', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 00:00:00'), y: 10}, + {t: +new Date('2018-01-09 00:00:00'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018'); + }); + it('should get the correct pixel for only one data in the dataset', function() { var chart = window.acquireChart({ type: 'line', From e585c7505f33e7115a0d75b7d058e3c0d37a6952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bourgois?= Date: Sat, 13 Jan 2018 17:17:38 +0000 Subject: [PATCH 38/56] Log gulp error to Chart.js (#5143) * Log errors and skip for buildTask * Write gulp error to Chart.js + Add intentional error to core to check if travis fails * Remove unused require * Remove error + Proper require fs * Fix newline * Refactor * Put back browser errors * Use options * Fix intentional error * Use yargs + Refactor * remove space * Fefactor * Use booleans --- gulpfile.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 5a3093e44..09165c8b0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,10 +17,17 @@ var browserify = require('browserify'); var source = require('vinyl-source-stream'); var merge = require('merge-stream'); var collapse = require('bundle-collapser/plugin'); -var argv = require('yargs').argv +var yargs = require('yargs'); var path = require('path'); +var fs = require('fs'); var package = require('./package.json'); +var argv = yargs + .option('force-output', {default: false}) + .option('silent-errors', {default: false}) + .option('verbose', {default: false}) + .argv + var srcDir = './src/'; var outDir = './dist/'; @@ -34,6 +41,10 @@ var header = "/*!\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; +if (argv.verbose) { + util.log("Gulp running with options: " + JSON.stringify(argv, null, 2)); +} + gulp.task('bower', bowerTask); gulp.task('build', buildTask); gulp.task('package', packageTask); @@ -79,9 +90,25 @@ function bowerTask() { function buildTask() { + var errorHandler = function (err) { + if(argv.forceOutput) { + var browserError = 'console.error("Gulp: ' + err.toString() + '")'; + ['Chart', 'Chart.min', 'Chart.bundle', 'Chart.bundle.min'].forEach(function(fileName) { + fs.writeFileSync(outDir+fileName+'.js', browserError); + }); + } + if(argv.silentErrors) { + util.log(util.colors.red('[Error]'), err.toString()); + this.emit('end'); + } else { + throw err; + } + } + var bundled = browserify('./src/chart.js', { standalone: 'Chart' }) .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.bundle.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) @@ -96,6 +123,7 @@ function buildTask() { .ignore('moment') .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) From 6d58a6a8a8af64373605c86fe380a61250411e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bourgois?= Date: Tue, 16 Jan 2018 09:29:49 +0000 Subject: [PATCH 39/56] Add tests related to showLines for controller.scatter (#5150) --- test/specs/controller.scatter.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/specs/controller.scatter.test.js diff --git a/test/specs/controller.scatter.test.js b/test/specs/controller.scatter.test.js new file mode 100644 index 000000000..435750928 --- /dev/null +++ b/test/specs/controller.scatter.test.js @@ -0,0 +1,25 @@ +describe('Chart.controllers.scatter', function() { + describe('showLines option', function() { + it('should not draw a line if undefined', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 10, y: 15}], + label: 'dataset1' + }], + }, + options: {} + }); + + var meta = chart.getDatasetMeta(0); + spyOn(meta.dataset, 'draw'); + spyOn(meta.data[0], 'draw'); + + chart.update(); + + expect(meta.dataset.draw.calls.count()).toBe(0); + expect(meta.data[0].draw.calls.count()).toBe(1); + }); + }); +}); From 26c44cf7abfce2df221f9c9b568e1c5eae893bfb Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 12:12:33 -0500 Subject: [PATCH 40/56] Treat negative values in doughnut charts as positive (#5165) --- src/controllers/controller.doughnut.js | 2 +- test/specs/controller.doughnut.tests.js | 40 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 6028602c4..f7b36e989 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -273,7 +273,7 @@ module.exports = function(Chart) { calculateCircumference: function(value) { var total = this.getMeta().total; if (total > 0 && !isNaN(value)) { - return (Math.PI * 2.0) * (value / total); + return (Math.PI * 2.0) * (Math.abs(value) / total); } return 0; }, diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index 85e382ac3..a4d6526c9 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -205,6 +205,46 @@ describe('Chart.controllers.doughnut', function() { }); }); + it('should treat negative values as positive', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [-1, -3] + }], + labels: ['label0', 'label1'] + }, + options: { + legend: false, + title: false, + cutoutPercentage: 50, + rotation: Math.PI, + circumference: Math.PI * 0.5, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(2); + + // Only startAngle, endAngle and circumference should be different. + [ + {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, + {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} + ].forEach(function(expected, i) { + expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i]._model.startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i]._model.endAngle).toBeCloseTo(expected.e, 8); + }); + }); + it ('should draw all arcs', function() { var chart = window.acquireChart({ type: 'doughnut', From 9a0117ad492d2c02c4be5f3488f0d8d255fe5aa9 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:46:28 -0500 Subject: [PATCH 41/56] Responsive printing docs (#5167) --- docs/general/responsive.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/general/responsive.md b/docs/general/responsive.md index bf8e8fa7a..f410826b8 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -33,3 +33,15 @@ The chart can also be programmatically resized by modifying the container size: ```javascript chart.canvas.parentNode.style.height = '128px'; ``` + +## Printing Resizeable Charts + +CSS media queries allow changing styles when printing a page. The CSS applied from these media queries may cause charts to need to resize. However, the resize won't happen automatically. To support resizing charts when printing, one needs to hook the [onbeforeprint](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeprint) event and manually trigger resizing of each chart. + +```javascript +function beforePrintHandler () { + for (var id in Chart.instances) { + Chart.instances[id].resize() + } +} +``` From d668882971166ac11768ab659b2d5770642c33b3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:47:04 -0500 Subject: [PATCH 42/56] Tooltip label callback example (#5168) --- docs/configuration/tooltip.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 1be2c26bc..9990fc916 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -103,6 +103,32 @@ All functions are called with the same arguments: a [tooltip item](#tooltip-item | `footer` | `Array[tooltipItem], data` | Returns text to render as the footer of the tooltip. | `afterFooter` | `Array[tooltipItem], data` | Text to render after the footer section +### Label Callback + +The label callback can change the text that displays for a given data point. A common example to round data values; the following example rounds the data to two decimal places. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + callbacks: { + label: function(tooltipItem, data) { + var label = data.datasets[tooltipItem.datasetIndex].label || ''; + + if (label) { + label += ': '; + } + label += Math.round(tooltipItem.yLabel * 100) / 100; + return label; + } + } + } + } +}); +``` + ### Label Color Callback For example, to return a red box for each item in the tooltip you could do: From f82c8adf39382ff1989664a872a0f3ec21ae1265 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:47:25 -0500 Subject: [PATCH 43/56] Remove copy-pasta error in polar area and doughnut chart docs (#5169) --- docs/charts/doughnut.md | 1 - docs/charts/polar.md | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md index 35a7dbb56..cd9af9d56 100644 --- a/docs/charts/doughnut.md +++ b/docs/charts/doughnut.md @@ -55,7 +55,6 @@ The doughnut/pie chart allows a number of properties to be specified for each da | Name | Type | Description | ---- | ---- | ----------- -| `label` | `String` | The label for the dataset which appears in the legend and tooltips. | `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderWidth` | `Number[]` | The border width of the arcs in the dataset. diff --git a/docs/charts/polar.md b/docs/charts/polar.md index 29952cff6..8f403149e 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -46,7 +46,6 @@ The following options can be included in a polar area chart dataset to configure | Name | Type | Description | ---- | ---- | ----------- -| `label` | `String` | The label for the dataset which appears in the legend and tooltips. | `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderWidth` | `Number[]` | The border width of the arcs in the dataset. From 274fca68c97d956ac4608e19988cb0c84ffe6999 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:47:50 -0500 Subject: [PATCH 44/56] Update custom tooltip documentation and samples (#5166) --- docs/configuration/tooltip.md | 18 +++++++++++------- samples/tooltips/custom-line.html | 6 +++--- samples/tooltips/custom-pie.html | 6 +++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 9990fc916..93db40147 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -6,7 +6,7 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `enabled` | `Boolean` | `true` | Are tooltips enabled +| `enabled` | `Boolean` | `true` | Are on-canvas tooltips enabled | `custom` | `Function` | `null` | See [custom tooltip](#external-custom-tooltips) section. | `mode` | `String` | `'nearest'` | Sets which elements appear in the tooltip. [more...](../general/interactions/modes.md#interaction-modes). | `intersect` | `Boolean` | `true` | if true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times. @@ -191,6 +191,9 @@ var myPieChart = new Chart(ctx, { data: data, options: { tooltips: { + // Disable the on-canvas tooltip + enabled: false, + custom: function(tooltipModel) { // Tooltip Element var tooltipEl = document.getElementById('chartjs-tooltip'); @@ -199,7 +202,7 @@ var myPieChart = new Chart(ctx, { if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.id = 'chartjs-tooltip'; - tooltipEl.innerHTML = "
" + tooltipEl.innerHTML = "
"; document.body.appendChild(tooltipEl); } @@ -238,7 +241,7 @@ var myPieChart = new Chart(ctx, { var style = 'background:' + colors.backgroundColor; style += '; border-color:' + colors.borderColor; style += '; border-width: 2px'; - var span = ''; + var span = ''; innerHtml += '' + span + body + ''; }); innerHtml += ''; @@ -252,11 +255,12 @@ var myPieChart = new Chart(ctx, { // Display, position, and set styles for font tooltipEl.style.opacity = 1; + tooltipEl.style.position = 'absolute'; tooltipEl.style.left = position.left + tooltipModel.caretX + 'px'; tooltipEl.style.top = position.top + tooltipModel.caretY + 'px'; - tooltipEl.style.fontFamily = tooltipModel._fontFamily; - tooltipEl.style.fontSize = tooltipModel.fontSize; - tooltipEl.style.fontStyle = tooltipModel._fontStyle; + tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily; + tooltipEl.style.fontSize = tooltipModel.bodyFontSize + 'px'; + tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle; tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px'; } } @@ -264,7 +268,7 @@ var myPieChart = new Chart(ctx, { }); ``` -See `samples/tooltips/line-customTooltips.html` for examples on how to get started. +See [samples](http://www.chartjs.org/samples/) for examples on how to get started with custom tooltips. ## Tooltip Model The tooltip model contains parameters that can be used to render the tooltip. diff --git a/samples/tooltips/custom-line.html b/samples/tooltips/custom-line.html index 801b12df6..011762907 100644 --- a/samples/tooltips/custom-line.html +++ b/samples/tooltips/custom-line.html @@ -102,9 +102,9 @@ tooltipEl.style.opacity = 1; tooltipEl.style.left = positionX + tooltip.caretX + 'px'; tooltipEl.style.top = positionY + tooltip.caretY + 'px'; - tooltipEl.style.fontFamily = tooltip._fontFamily; - tooltipEl.style.fontSize = tooltip.fontSize; - tooltipEl.style.fontStyle = tooltip._fontStyle; + tooltipEl.style.fontFamily = tooltip._bodyFontFamily; + tooltipEl.style.fontSize = tooltip.bodyFontSize + 'px'; + tooltipEl.style.fontStyle = tooltip._bodyFontStyle; tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px'; }; diff --git a/samples/tooltips/custom-pie.html b/samples/tooltips/custom-pie.html index 9c3cd0aa6..7bfbc56f1 100644 --- a/samples/tooltips/custom-pie.html +++ b/samples/tooltips/custom-pie.html @@ -98,9 +98,9 @@ tooltipEl.style.opacity = 1; tooltipEl.style.left = positionX + tooltip.caretX + 'px'; tooltipEl.style.top = positionY + tooltip.caretY + 'px'; - tooltipEl.style.fontFamily = tooltip._fontFamily; - tooltipEl.style.fontSize = tooltip.fontSize; - tooltipEl.style.fontStyle = tooltip._fontStyle; + tooltipEl.style.fontFamily = tooltip._bodyFontFamily; + tooltipEl.style.fontSize = tooltip.bodyFontSize; + tooltipEl.style.fontStyle = tooltip._bodyFontStyle; tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px'; }; From 98ef3942d982869ee867e134df95008dd612caf3 Mon Sep 17 00:00:00 2001 From: Jonathan Quach Date: Sat, 27 Jan 2018 15:47:51 +0100 Subject: [PATCH 45/56] Fix variable name error on developer api documentation for (#5173) --- docs/developers/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developers/api.md b/docs/developers/api.md index a5a0a5133..976578d57 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -132,9 +132,9 @@ To get an item that was clicked on, `getElementAtEvent` can be used. ```javascript function clickHandler(evt) { - var item = myChart.getElementAtEvent(evt)[0]; + var firstPoint = myChart.getElementAtEvent(evt)[0]; - if (item) { + if (firstPoint) { var label = myChart.data.labels[firstPoint._index]; var value = myChart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index]; } From e61392a256727bb531e80abbb2d5716fdf7d31ed Mon Sep 17 00:00:00 2001 From: Jackson Haenchen Date: Sun, 28 Jan 2018 11:59:22 -0600 Subject: [PATCH 46/56] Don't draw tick across axis/border (#5178) --- src/core/core.scale.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index ffe13cbff..f79dbee80 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -705,10 +705,11 @@ module.exports = function(Chart) { var itemsToDraw = []; - var xTickStart = options.position === 'right' ? me.left : me.right - tl; - var xTickEnd = options.position === 'right' ? me.left + tl : me.right; - var yTickStart = options.position === 'bottom' ? me.top : me.bottom - tl; - var yTickEnd = options.position === 'bottom' ? me.top + tl : me.bottom; + var axisWidth = me.options.gridLines.lineWidth; + var xTickStart = options.position === 'right' ? me.right : me.right - axisWidth - tl; + var xTickEnd = options.position === 'right' ? me.right + tl : me.right; + var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; + var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; helpers.each(ticks, function(tick, index) { // autoskipper skipped this tick (#4635) @@ -764,7 +765,7 @@ module.exports = function(Chart) { ty1 = yTickStart; ty2 = yTickEnd; y1 = chartArea.top; - y2 = chartArea.bottom; + y2 = chartArea.bottom + axisWidth; } else { var isLeft = options.position === 'left'; var labelXOffset; @@ -790,7 +791,7 @@ module.exports = function(Chart) { tx1 = xTickStart; tx2 = xTickEnd; x1 = chartArea.left; - x2 = chartArea.right; + x2 = chartArea.right + axisWidth; ty1 = ty2 = y1 = y2 = yLineValue; } @@ -906,9 +907,9 @@ module.exports = function(Chart) { context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); var x1 = me.left; - var x2 = me.right; + var x2 = me.right + axisWidth; var y1 = me.top; - var y2 = me.bottom; + var y2 = me.bottom + axisWidth; var aliasPixel = helpers.aliasPixel(context.lineWidth); if (isHorizontal) { From c2681859531977fb83fee782f8aec6b50196d644 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 1 Feb 2018 03:35:08 +0100 Subject: [PATCH 47/56] Fix Slack invitation link (#5217) Setup a new Heroku app based on rauchg/slackin, using Slack legacy token from the Chart.js (chartjs.slack@...) user and reCAPTCHA from the same Google account. --- README.md | 2 +- docs/README.md | 2 +- docs/developers/contributing.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 10d2c1d8d..b68b39008 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chart.js -[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) +[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600)](https://chartjs-slack.herokuapp.com/) *Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) diff --git a/docs/README.md b/docs/README.md index 5a0e9f355..24ee8d499 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Chart.js -[![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=600)](https://chart-js-automation.herokuapp.com/) +[![slack](https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600)](https://chartjs-slack.herokuapp.com/) ## Installation diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index e6551d590..fa3511bb0 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -12,7 +12,7 @@ New contributions to the library are welcome, but we ask that you please follow # Joining the project - Active committers and contributors are invited to introduce yourself and request commit access to this project. We have a very active Slack community that you can join [here](https://chart-js-automation.herokuapp.com/). If you think you can help, we'd love to have you! + Active committers and contributors are invited to introduce yourself and request commit access to this project. We have a very active Slack community that you can join [here](https://chartjs-slack.herokuapp.com/). If you think you can help, we'd love to have you! # Building and Testing From 97ff45873e8e00af5e25d8adadb830fb15a272ef Mon Sep 17 00:00:00 2001 From: stockiNail Date: Fri, 2 Feb 2018 10:17:24 +0100 Subject: [PATCH 48/56] Add Charba GWT integration to extensions.md (#5225) --- docs/notes/extensions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 0998641ba..fe2c337f1 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -56,5 +56,8 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ### Java - Chart.java +### GWT (Google Web toolkit) + - Charba + ### Ember.js - ember-cli-chart From 182270ef9b1bc9fab1cefc89ce1e94a41f4d754f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bourgois?= Date: Sat, 3 Feb 2018 13:28:54 +0100 Subject: [PATCH 49/56] Setup HTML and JS linters for samples (#5195) --- .eslintrc.yml | 2 + .htmllintrc | 18 + gulpfile.js | 24 +- package.json | 3 +- samples/.eslintrc.yml | 9 + samples/advanced/data-labelling.html | 225 ++++++------ samples/advanced/progress-bar.html | 169 +++++---- samples/charts/area/analyser.js | 2 +- samples/charts/area/line-boundaries.html | 5 +- samples/charts/area/line-datasets.html | 9 +- samples/charts/area/line-stacked.html | 92 ++--- samples/charts/area/radar.html | 9 +- samples/charts/bar/horizontal.html | 246 +++++++------- samples/charts/bar/multi-axis.html | 192 +++++------ samples/charts/bar/stacked-group.html | 186 +++++----- samples/charts/bar/stacked.html | 180 +++++----- samples/charts/bar/vertical.html | 238 ++++++------- samples/charts/bubble.html | 340 +++++++++---------- samples/charts/combo-bar-line.html | 178 +++++----- samples/charts/doughnut.html | 236 ++++++------- samples/charts/line/basic.html | 278 +++++++-------- samples/charts/line/interpolation-modes.html | 158 ++++----- samples/charts/line/line-styles.html | 200 +++++------ samples/charts/line/multi-axis.html | 180 +++++----- samples/charts/line/point-sizes.html | 237 +++++++------ samples/charts/line/point-styles.html | 166 ++++----- samples/charts/line/skip-points.html | 166 ++++----- samples/charts/line/stepped.html | 140 ++++---- samples/charts/pie.html | 156 ++++----- samples/charts/polar-area.html | 206 +++++------ samples/charts/radar-skip-points.html | 188 +++++----- samples/charts/radar.html | 242 ++++++------- samples/charts/scatter/basic.html | 190 +++++------ samples/charts/scatter/multi-axis.html | 30 +- samples/index.html | 4 +- samples/legend/point-style.html | 208 ++++++------ samples/legend/positioning.html | 220 ++++++------ samples/scales/filtering-labels.html | 160 +++++---- samples/scales/gridlines-display.html | 220 ++++++------ samples/scales/gridlines-style.html | 116 +++---- samples/scales/linear/min-max-suggested.html | 110 +++--- samples/scales/linear/min-max.html | 106 +++--- samples/scales/linear/step-size.html | 298 ++++++++-------- samples/scales/logarithmic/line.html | 164 ++++----- samples/scales/logarithmic/scatter.html | 134 ++++---- samples/scales/multiline-labels.html | 148 ++++---- samples/scales/non-numeric-y.html | 123 ++++--- samples/scales/time/combo.html | 70 ++-- samples/scales/time/financial.html | 14 +- samples/scales/time/line-point-data.html | 47 +-- samples/scales/time/line.html | 38 +-- samples/scales/toggle-scale-type.html | 168 ++++----- samples/scriptable/bubble.html | 6 +- samples/tooltips/border.html | 7 +- samples/tooltips/callbacks.html | 190 +++++------ samples/tooltips/custom-line.html | 44 +-- samples/tooltips/custom-pie.html | 202 +++++------ samples/tooltips/custom-points.html | 56 +-- samples/tooltips/interactions.html | 10 +- samples/tooltips/positioning.html | 10 +- 60 files changed, 3904 insertions(+), 3869 deletions(-) create mode 100644 .htmllintrc create mode 100644 samples/.eslintrc.yml diff --git a/.eslintrc.yml b/.eslintrc.yml index 8f9b4af30..b0d9d5695 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,3 +3,5 @@ extends: chartjs env: browser: true node: true + +plugins: ['html'] diff --git a/.htmllintrc b/.htmllintrc new file mode 100644 index 000000000..a6b209703 --- /dev/null +++ b/.htmllintrc @@ -0,0 +1,18 @@ +{ + "indent-style": "tabs", + "attr-quote-style": "double", + "spec-char-escape": false, + "attr-bans": [ + "align", + "background", + "bgcolor", + "border", + "frameborder", + "longdesc", + "marginwidth", + "marginheight", + "scrolling" + ], + "tag-bans": [ "b", "i" ], + "id-class-style": false +} diff --git a/gulpfile.js b/gulpfile.js index 09165c8b0..21f19645f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,7 +3,6 @@ var concat = require('gulp-concat'); var connect = require('gulp-connect'); var eslint = require('gulp-eslint'); var file = require('gulp-file'); -var htmlv = require('gulp-html-validator'); var insert = require('gulp-insert'); var replace = require('gulp-replace'); var size = require('gulp-size'); @@ -20,6 +19,7 @@ var collapse = require('bundle-collapser/plugin'); var yargs = require('yargs'); var path = require('path'); var fs = require('fs'); +var htmllint = require('gulp-htmllint'); var package = require('./package.json'); var argv = yargs @@ -49,12 +49,13 @@ gulp.task('bower', bowerTask); gulp.task('build', buildTask); gulp.task('package', packageTask); gulp.task('watch', watchTask); -gulp.task('lint', lintTask); +gulp.task('lint', ['lint-html', 'lint-js']); +gulp.task('lint-html', lintHtmlTask); +gulp.task('lint-js', lintJsTask); gulp.task('docs', docsTask); -gulp.task('test', ['lint', 'validHTML', 'unittest']); +gulp.task('test', ['lint', 'unittest']); gulp.task('size', ['library-size', 'module-sizes']); gulp.task('server', serverTask); -gulp.task('validHTML', validHTMLTask); gulp.task('unittest', unittestTask); gulp.task('library-size', librarySizeTask); gulp.task('module-sizes', moduleSizesTask); @@ -153,8 +154,9 @@ function packageTask() { .pipe(gulp.dest(outDir)); } -function lintTask() { +function lintJsTask() { var files = [ + 'samples/**/*.html', 'samples/**/*.js', 'src/**/*.js', 'test/**/*.js' @@ -176,6 +178,13 @@ function lintTask() { .pipe(eslint.failAfterError()); } +function lintHtmlTask() { + return gulp.src('samples/**/*.html') + .pipe(htmllint({ + failOnError: true, + })); +} + function docsTask(done) { const script = require.resolve('gitbook-cli/bin/gitbook.js'); const cmd = process.execPath; @@ -189,11 +198,6 @@ function docsTask(done) { }); } -function validHTMLTask() { - return gulp.src('samples/*.html') - .pipe(htmlv()); -} - function startTest() { return [ {pattern: './test/fixtures/**/*.json', included: false}, diff --git a/package.json b/package.json index c7d21043b..03f3815cc 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,14 @@ "coveralls": "^3.0.0", "eslint": "^4.9.0", "eslint-config-chartjs": "^0.1.0", + "eslint-plugin-html": "^4.0.2", "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", "gulp-connect": "~5.0.0", "gulp-eslint": "^4.0.0", "gulp-file": "^0.3.0", - "gulp-html-validator": "^0.0.5", + "gulp-htmllint": "^0.0.15", "gulp-insert": "~0.5.0", "gulp-replace": "^0.6.1", "gulp-size": "~2.1.0", diff --git a/samples/.eslintrc.yml b/samples/.eslintrc.yml new file mode 100644 index 000000000..9573adbcd --- /dev/null +++ b/samples/.eslintrc.yml @@ -0,0 +1,9 @@ +globals: + $: true + Chart: true + Samples: true + moment: true + randomScalingFactor: true + +rules: + no-new: 0 diff --git a/samples/advanced/data-labelling.html b/samples/advanced/data-labelling.html index 17fc47c8f..52eb07b8f 100644 --- a/samples/advanced/data-labelling.html +++ b/samples/advanced/data-labelling.html @@ -3,130 +3,129 @@ - Labelling Data Points - - - + Labelling Data Points + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/advanced/progress-bar.html b/samples/advanced/progress-bar.html index d460bc88e..b97a998ea 100644 --- a/samples/advanced/progress-bar.html +++ b/samples/advanced/progress-bar.html @@ -1,96 +1,95 @@ - Animation Callbacks - - - + Animation Callbacks + + + -
- - -
-
-
- - + window.myLine.update(); + }); + - \ No newline at end of file + diff --git a/samples/charts/area/analyser.js b/samples/charts/area/analyser.js index e4ed8e90b..36fd56716 100644 --- a/samples/charts/area/analyser.js +++ b/samples/charts/area/analyser.js @@ -4,7 +4,7 @@ (function() { Chart.plugins.register({ - id: 'samples_filler_analyser', + id: 'samples-filler-analyser', beforeInit: function(chart, options) { this.element = document.getElementById(options.target); diff --git a/samples/charts/area/line-boundaries.html b/samples/charts/area/line-boundaries.html index edadc781c..7ac6883bb 100644 --- a/samples/charts/area/line-boundaries.html +++ b/samples/charts/area/line-boundaries.html @@ -94,15 +94,16 @@ }); }); - + // eslint-disable-next-line no-unused-vars function toggleSmooth(btn) { var value = btn.classList.toggle('btn-on'); Chart.helpers.each(Chart.instances, function(chart) { - chart.options.elements.line.tension = value? 0.4 : 0.000001; + chart.options.elements.line.tension = value ? 0.4 : 0.000001; chart.update(); }); } + // eslint-disable-next-line no-unused-vars function randomize() { var seed = utils.rand(); Chart.helpers.each(Chart.instances, function(chart) { diff --git a/samples/charts/area/line-datasets.html b/samples/charts/area/line-datasets.html index 7bc54e313..127726e7f 100644 --- a/samples/charts/area/line-datasets.html +++ b/samples/charts/area/line-datasets.html @@ -38,7 +38,7 @@ return utils.numbers(inputs); } - function generateLabels(config) { + function generateLabels() { return utils.months({count: inputs.count}); } @@ -122,7 +122,7 @@ filler: { propagate: false }, - samples_filler_analyser: { + 'samples-filler-analyser': { target: 'chart-analyser' } } @@ -134,18 +134,21 @@ options: options }); + // eslint-disable-next-line no-unused-vars function togglePropagate(btn) { var value = btn.classList.toggle('btn-on'); chart.options.plugins.filler.propagate = value; chart.update(); } + // eslint-disable-next-line no-unused-vars function toggleSmooth(btn) { var value = btn.classList.toggle('btn-on'); - chart.options.elements.line.tension = value? 0.4 : 0.000001; + chart.options.elements.line.tension = value ? 0.4 : 0.000001; chart.update(); } + // eslint-disable-next-line no-unused-vars function randomize() { chart.data.datasets.forEach(function(dataset) { dataset.data = generateData(); diff --git a/samples/charts/area/line-stacked.html b/samples/charts/area/line-stacked.html index 26ee6b18f..11a143b0b 100644 --- a/samples/charts/area/line-stacked.html +++ b/samples/charts/area/line-stacked.html @@ -7,9 +7,9 @@ @@ -26,70 +26,70 @@ - - + Horizontal Bar Chart + + + -
- -
- - - - - - + window.myHorizontalBar.update(); + }); + diff --git a/samples/charts/bar/multi-axis.html b/samples/charts/bar/multi-axis.html index 28755a701..c206866a4 100644 --- a/samples/charts/bar/multi-axis.html +++ b/samples/charts/bar/multi-axis.html @@ -2,107 +2,107 @@ - Bar Chart Multi Axis - - - + Bar Chart Multi Axis + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/stacked-group.html b/samples/charts/bar/stacked-group.html index 624992cfb..e3b734b2d 100644 --- a/samples/charts/bar/stacked-group.html +++ b/samples/charts/bar/stacked-group.html @@ -2,104 +2,104 @@ - Stacked Bar Chart with Groups - - - + Stacked Bar Chart with Groups + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/stacked.html b/samples/charts/bar/stacked.html index 46d1c4051..ca9f0cafb 100644 --- a/samples/charts/bar/stacked.html +++ b/samples/charts/bar/stacked.html @@ -2,101 +2,101 @@ - Stacked Bar Chart - - - + Stacked Bar Chart + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/vertical.html b/samples/charts/bar/vertical.html index 906b4624c..e9348b274 100644 --- a/samples/charts/bar/vertical.html +++ b/samples/charts/bar/vertical.html @@ -2,143 +2,143 @@ - Bar Chart - - - + Bar Chart + + + -
- -
- - - - - - + window.myBar.update(); + }); + diff --git a/samples/charts/bubble.html b/samples/charts/bubble.html index 0df09477f..092c1fb46 100644 --- a/samples/charts/bubble.html +++ b/samples/charts/bubble.html @@ -2,190 +2,190 @@ - Bubble Chart - - - + Bubble Chart + + + -
- -
- - - - - - + window.myChart.update(); + }); + diff --git a/samples/charts/combo-bar-line.html b/samples/charts/combo-bar-line.html index 0906029c4..c99894e22 100644 --- a/samples/charts/combo-bar-line.html +++ b/samples/charts/combo-bar-line.html @@ -2,100 +2,100 @@ - Combo Bar-Line Chart - - - + Combo Bar-Line Chart + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + chartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myMixedChart.update(); + }); + diff --git a/samples/charts/doughnut.html b/samples/charts/doughnut.html index b288fe129..446dcee4e 100644 --- a/samples/charts/doughnut.html +++ b/samples/charts/doughnut.html @@ -2,143 +2,143 @@ - Doughnut Chart - - - + Doughnut Chart + + + -
- -
- - - - - - + window.myDoughnut.update(); + }); + diff --git a/samples/charts/line/basic.html b/samples/charts/line/basic.html index db01ccf74..8028bee9c 100644 --- a/samples/charts/line/basic.html +++ b/samples/charts/line/basic.html @@ -2,162 +2,162 @@ - Line Chart - - - + Line Chart + + + -
- -
-
-
- - - - - - + window.myLine.update(); + }); + diff --git a/samples/charts/line/interpolation-modes.html b/samples/charts/line/interpolation-modes.html index 374da49c0..b11dd6819 100644 --- a/samples/charts/line/interpolation-modes.html +++ b/samples/charts/line/interpolation-modes.html @@ -2,102 +2,102 @@ - Line Chart - Cubic interpolation mode - - - + Line Chart - Cubic interpolation mode + + + -
- -
-
-
- - + diff --git a/samples/charts/line/line-styles.html b/samples/charts/line/line-styles.html index ed268e1fd..3ba9defcd 100644 --- a/samples/charts/line/line-styles.html +++ b/samples/charts/line/line-styles.html @@ -2,110 +2,110 @@ - Line Styles - - - + Line Styles + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/multi-axis.html b/samples/charts/line/multi-axis.html index 2ede74446..d3569e855 100644 --- a/samples/charts/line/multi-axis.html +++ b/samples/charts/line/multi-axis.html @@ -2,103 +2,103 @@ - Line Chart Multiple Axes - - - + Line Chart Multiple Axes + + + -
- -
- - + window.myLine.update(); + }); + diff --git a/samples/charts/line/point-sizes.html b/samples/charts/line/point-sizes.html index 823c6d3e4..53d3db46c 100644 --- a/samples/charts/line/point-sizes.html +++ b/samples/charts/line/point-sizes.html @@ -2,129 +2,128 @@ - Different Point Sizes - - - + Different Point Sizes + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/point-styles.html b/samples/charts/line/point-styles.html index 1985d394d..2ef46a9cf 100644 --- a/samples/charts/line/point-styles.html +++ b/samples/charts/line/point-styles.html @@ -2,95 +2,95 @@ - Line Chart - - - + Line Chart + + + -
-
- + var ctx = canvas.getContext('2d'); + var config = createConfig(pointStyle); + new Chart(ctx, config); + }); + }; + diff --git a/samples/charts/line/skip-points.html b/samples/charts/line/skip-points.html index 00aa81fe0..aedacce72 100644 --- a/samples/charts/line/skip-points.html +++ b/samples/charts/line/skip-points.html @@ -2,94 +2,94 @@ - Line Chart - - - + Line Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/stepped.html b/samples/charts/line/stepped.html index ccce87c2d..4ad9708eb 100644 --- a/samples/charts/line/stepped.html +++ b/samples/charts/line/stepped.html @@ -2,9 +2,9 @@ - Stepped Line Chart - - + Stepped Line Chart + + + Polar Area Chart + + + -
- -
- - - - + var colorNames = Object.keys(window.chartColors); + document.getElementById('addData').addEventListener('click', function() { + if (config.data.datasets.length > 0) { + config.data.labels.push('data #' + config.data.labels.length); + config.data.datasets.forEach(function(dataset) { + var colorName = colorNames[config.data.labels.length % colorNames.length]; + dataset.backgroundColor.push(window.chartColors[colorName]); + dataset.data.push(randomScalingFactor()); + }); + window.myPolarArea.update(); + } + }); + document.getElementById('removeData').addEventListener('click', function() { + config.data.labels.pop(); // remove the label first + config.data.datasets.forEach(function(dataset) { + dataset.backgroundColor.pop(); + dataset.data.pop(); + }); + window.myPolarArea.update(); + }); + diff --git a/samples/charts/radar-skip-points.html b/samples/charts/radar-skip-points.html index ab29b3a0e..ab042e2dc 100644 --- a/samples/charts/radar-skip-points.html +++ b/samples/charts/radar-skip-points.html @@ -2,108 +2,108 @@ - Radar Chart - - - + Radar Chart + + + -
- -
- - + window.myRadar.update(); + }); + diff --git a/samples/charts/radar.html b/samples/charts/radar.html index fbbb9d972..7eda09a7f 100644 --- a/samples/charts/radar.html +++ b/samples/charts/radar.html @@ -2,145 +2,145 @@ - Radar Chart - - - + Radar Chart + + + -
- -
- - - - - - + window.myRadar.update(); + }); + diff --git a/samples/charts/scatter/basic.html b/samples/charts/scatter/basic.html index c5e36e9d9..6ac227c3b 100644 --- a/samples/charts/scatter/basic.html +++ b/samples/charts/scatter/basic.html @@ -2,106 +2,106 @@ - Scatter Chart - - - + Scatter Chart + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + scatterChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return { + x: randomScalingFactor(), + y: randomScalingFactor() + }; + }); + }); + window.myScatter.update(); + }); + diff --git a/samples/charts/scatter/multi-axis.html b/samples/charts/scatter/multi-axis.html index cbf3bf73a..6c20b98f0 100644 --- a/samples/charts/scatter/multi-axis.html +++ b/samples/charts/scatter/multi-axis.html @@ -23,9 +23,9 @@ var color = Chart.helpers.color; var scatterChartData = { datasets: [{ - label: "My First dataset", - xAxisID: "x-axis-1", - yAxisID: "y-axis-1", + label: 'My First dataset', + xAxisID: 'x-axis-1', + yAxisID: 'y-axis-1', borderColor: window.chartColors.red, backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(), data: [{ @@ -51,9 +51,9 @@ y: randomScalingFactor(), }] }, { - label: "My Second dataset", - xAxisID: "x-axis-1", - yAxisID: "y-axis-2", + label: 'My Second dataset', + xAxisID: 'x-axis-1', + yAxisID: 'y-axis-2', borderColor: window.chartColors.blue, backgroundColor: color(window.chartColors.blue).alpha(0.2).rgbString(), data: [{ @@ -82,7 +82,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myScatter = Chart.Scatter(ctx, { data: scatterChartData, options: { @@ -95,22 +95,22 @@ }, scales: { xAxes: [{ - position: "bottom", + position: 'bottom', gridLines: { - zeroLineColor: "rgba(0,0,0,1)" + zeroLineColor: 'rgba(0,0,0,1)' } }], yAxes: [{ - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance display: true, - position: "left", - id: "y-axis-1", + position: 'left', + id: 'y-axis-1', }, { - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance display: true, - position: "right", + position: 'right', reverse: true, - id: "y-axis-2", + id: 'y-axis-2', // grid line settings gridLines: { diff --git a/samples/index.html b/samples/index.html index d855662f0..b9e13c364 100644 --- a/samples/index.html +++ b/samples/index.html @@ -47,8 +47,8 @@ var category = createCategory(item); var children = category.getElementsByClassName('items')[0]; - (item.items || []).forEach(function(item) { - children.appendChild(createEntry(item)); + (item.items || []).forEach(function(item2) { + children.appendChild(createEntry(item2)); }); categories.appendChild(category); diff --git a/samples/legend/point-style.html b/samples/legend/point-style.html index 727c7a6d8..b7acea4bd 100644 --- a/samples/legend/point-style.html +++ b/samples/legend/point-style.html @@ -2,115 +2,115 @@ - Legend Point Style - - - + Legend Point Style + + + -
-
- -
-
- -
-
- + window.onload = function() { + [{ + id: 'chart-legend-normal', + config: createConfig('red') + }, { + id: 'chart-legend-pointstyle', + config: createPointStyleConfig('blue') + }].forEach(function(details) { + var ctx = document.getElementById(details.id).getContext('2d'); + new Chart(ctx, details.config); + }); + }; + diff --git a/samples/legend/positioning.html b/samples/legend/positioning.html index 97bc70fa0..5bd2259cf 100644 --- a/samples/legend/positioning.html +++ b/samples/legend/positioning.html @@ -2,120 +2,120 @@ - Legend Positions - - - + Legend Positions + + + -
-
- -
-
- -
-
- -
-
- -
-
- + window.onload = function() { + [{ + id: 'chart-legend-top', + legendPosition: 'top', + color: 'red' + }, { + id: 'chart-legend-right', + legendPosition: 'right', + color: 'blue' + }, { + id: 'chart-legend-bottom', + legendPosition: 'bottom', + color: 'green' + }, { + id: 'chart-legend-left', + legendPosition: 'left', + color: 'yellow' + }].forEach(function(details) { + var ctx = document.getElementById(details.id).getContext('2d'); + var config = createConfig(details.legendPosition, details.color); + new Chart(ctx, config); + }); + }; + diff --git a/samples/scales/filtering-labels.html b/samples/scales/filtering-labels.html index 4af89025e..4b4b51724 100644 --- a/samples/scales/filtering-labels.html +++ b/samples/scales/filtering-labels.html @@ -2,92 +2,90 @@ - Chart with xAxis Filtering - - - + Chart with xAxis Filtering + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/gridlines-display.html b/samples/scales/gridlines-display.html index c21469329..8e30bdcdb 100644 --- a/samples/scales/gridlines-display.html +++ b/samples/scales/gridlines-display.html @@ -2,123 +2,123 @@ - Grid Lines Display Settings - - - + Grid Lines Display Settings + + + -
- + var ctx = canvas.getContext('2d'); + var config = createConfig(details.gridLines, details.title); + new Chart(ctx, config); + }); + }; + diff --git a/samples/scales/gridlines-style.html b/samples/scales/gridlines-style.html index f945cb266..d2c00ecdd 100644 --- a/samples/scales/gridlines-style.html +++ b/samples/scales/gridlines-style.html @@ -2,68 +2,68 @@ - Grid Lines Style Settings - - - + Grid Lines Style Settings + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/min-max-suggested.html b/samples/scales/linear/min-max-suggested.html index 18059548a..10d546ac2 100644 --- a/samples/scales/linear/min-max-suggested.html +++ b/samples/scales/linear/min-max-suggested.html @@ -2,66 +2,66 @@ - Suggested Min/Max Settings - - - + Suggested Min/Max Settings + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/min-max.html b/samples/scales/linear/min-max.html index 868bc7b1d..feafbd7a6 100644 --- a/samples/scales/linear/min-max.html +++ b/samples/scales/linear/min-max.html @@ -2,63 +2,63 @@ - Min/Max Settings - - - + Min/Max Settings + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/step-size.html b/samples/scales/linear/step-size.html index ced0b6c3c..fc0af2ee3 100644 --- a/samples/scales/linear/step-size.html +++ b/samples/scales/linear/step-size.html @@ -2,174 +2,174 @@ - Line Chart - - - + Line Chart + + + -
- -
-
-
- - - - - - + window.myLine.update(); + }); + + diff --git a/samples/scales/logarithmic/line.html b/samples/scales/logarithmic/line.html index 2c961abd2..72fd9b07d 100644 --- a/samples/scales/logarithmic/line.html +++ b/samples/scales/logarithmic/line.html @@ -2,96 +2,96 @@ - Logarithmic Line Chart - - - + Logarithmic Line Chart + + + -
- -
- - + window.myLine.update(); + }); + diff --git a/samples/scales/logarithmic/scatter.html b/samples/scales/logarithmic/scatter.html index a4bd577c2..5f266a5cd 100644 --- a/samples/scales/logarithmic/scatter.html +++ b/samples/scales/logarithmic/scatter.html @@ -2,29 +2,29 @@ - Scatter Chart - - - + Scatter Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myScatter = Chart.Scatter(ctx, { + data: scatterChartData, + options: { + title: { + display: true, + text: 'Chart.js Scatter Chart - Logarithmic X-Axis' + }, + scales: { + xAxes: [{ + type: 'logarithmic', + position: 'bottom', + ticks: { + userCallback: function(tick) { + var remain = tick / (Math.pow(10, Math.floor(Chart.helpers.log10(tick)))); + if (remain === 1 || remain === 2 || remain === 5) { + return tick.toString() + 'Hz'; + } + return ''; + }, + }, + scaleLabel: { + labelString: 'Frequency', + display: true, + } + }], + yAxes: [{ + type: 'linear', + ticks: { + userCallback: function(tick) { + return tick.toString() + 'dB'; + } + }, + scaleLabel: { + labelString: 'Voltage', + display: true + } + }] + } + } + }); + }; + diff --git a/samples/scales/multiline-labels.html b/samples/scales/multiline-labels.html index b7bb041e9..0f8af2a7d 100644 --- a/samples/scales/multiline-labels.html +++ b/samples/scales/multiline-labels.html @@ -2,85 +2,85 @@ - Line Chart - - - + Line Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/non-numeric-y.html b/samples/scales/non-numeric-y.html index 07e319b70..b3bfc5eec 100644 --- a/samples/scales/non-numeric-y.html +++ b/samples/scales/non-numeric-y.html @@ -2,72 +2,71 @@ - Line Chart - - - + Line Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/time/combo.html b/samples/scales/time/combo.html index ddcc5b326..d435e74c6 100644 --- a/samples/scales/time/combo.html +++ b/samples/scales/time/combo.html @@ -7,11 +7,11 @@ @@ -38,12 +38,12 @@ type: 'bar', data: { labels: [ - newDateString(0), - newDateString(1), - newDateString(2), - newDateString(3), - newDateString(4), - newDateString(5), + newDateString(0), + newDateString(1), + newDateString(2), + newDateString(3), + newDateString(4), + newDateString(5), newDateString(6) ], datasets: [{ @@ -52,12 +52,12 @@ backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], }, { @@ -66,12 +66,12 @@ backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], }, { @@ -81,23 +81,23 @@ borderColor: window.chartColors.green, fill: false, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], - }, ] + }] }, options: { - title: { - text:"Chart.js Combo Time Scale" - }, + title: { + text: 'Chart.js Combo Time Scale' + }, scales: { xAxes: [{ - type: "time", + type: 'time', display: true, time: { format: timeFormat, @@ -109,7 +109,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index 3d91dfa56..792eef553 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -18,7 +18,7 @@
-
+


Chart Type: @@ -33,10 +33,8 @@ } function randomBar(date, lastClose) { - var open = randomNumber(lastClose * .95, lastClose * 1.05); - var close = randomNumber(open * .95, open * 1.05); - var high = randomNumber(Math.max(open, close), Math.max(open, close) * 1.1); - var low = randomNumber(Math.min(open, close) * .9, Math.min(open, close)); + var open = randomNumber(lastClose * 0.95, lastClose * 1.05); + var close = randomNumber(open * 0.95, open * 1.05); return { t: date.valueOf(), y: close @@ -55,7 +53,7 @@ } } - var ctx = document.getElementById("chart1").getContext("2d"); + var ctx = document.getElementById('chart1').getContext('2d'); ctx.canvas.width = 1000; ctx.canvas.height = 300; var cfg = { @@ -63,7 +61,7 @@ data: { labels: labels, datasets: [{ - label: "CHRT - Chart.js Corporation", + label: 'CHRT - Chart.js Corporation', data: data, type: 'line', pointRadius: 0, @@ -94,7 +92,7 @@ document.getElementById('update').addEventListener('click', function() { var type = document.getElementById('type').value; - chart.config.data.datasets[0].type = type; + chart.config.data.datasets[0].type = type; chart.update(); }); diff --git a/samples/scales/time/line-point-data.html b/samples/scales/time/line-point-data.html index 3483c4545..e18be91f3 100644 --- a/samples/scales/time/line-point-data.html +++ b/samples/scales/time/line-point-data.html @@ -7,11 +7,11 @@ @@ -38,7 +38,7 @@ type: 'line', data: { datasets: [{ - label: "Dataset with string point data", + label: 'Dataset with string point data', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, fill: false, @@ -56,7 +56,7 @@ y: randomScalingFactor() }], }, { - label: "Dataset with date object point data", + label: 'Dataset with date object point data', backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, fill: false, @@ -77,24 +77,24 @@ }, options: { responsive: true, - title:{ - display:true, - text:"Chart.js Time Point Data" + title: { + display: true, + text: 'Chart.js Time Point Data' }, scales: { xAxes: [{ - type: "time", + type: 'time', display: true, scaleLabel: { display: true, labelString: 'Date' }, - ticks: { - major: { - fontStyle: "bold", - fontColor: "#FF0000" - } - } + ticks: { + major: { + fontStyle: 'bold', + fontColor: '#FF0000' + } + } }], yAxes: [{ display: true, @@ -108,7 +108,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; @@ -122,10 +122,15 @@ window.myLine.update(); }); + // TODO : fix issue with addData + // See https://github.com/chartjs/Chart.js/issues/5197 + // The Add Data button for this sample has no effect. + // An error is logged in the console. document.getElementById('addData').addEventListener('click', function() { if (config.data.datasets.length > 0) { - var numTicks = myLine.scales['x-axis-0'].ticksAsTimestamps.length; - var lastTime = numTicks ? moment(myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); + var numTicks = window.myLine.scales['x-axis-0'].ticksAsTimestamps.length; + var lastTime = numTicks ? moment(window.myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); + var newTime = lastTime .clone() .add(1, 'day') @@ -143,7 +148,7 @@ }); document.getElementById('removeData').addEventListener('click', function() { - config.data.datasets.forEach(function(dataset, datasetIndex) { + config.data.datasets.forEach(function(dataset) { dataset.data.pop(); }); diff --git a/samples/scales/time/line.html b/samples/scales/time/line.html index 0cca931e0..70a6978af 100644 --- a/samples/scales/time/line.html +++ b/samples/scales/time/line.html @@ -7,11 +7,11 @@ @@ -37,10 +37,6 @@ return moment().add(days, 'd').format(timeFormat); } - function newTimestamp(days) { - return moment().add(days, 'd').unix(); - } - var color = Chart.helpers.color; var config = { type: 'line', @@ -55,7 +51,7 @@ newDate(6) ], datasets: [{ - label: "My First dataset", + label: 'My First dataset', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, fill: false, @@ -69,7 +65,7 @@ randomScalingFactor() ], }, { - label: "My Second dataset", + label: 'My Second dataset', backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, fill: false, @@ -83,7 +79,7 @@ randomScalingFactor() ], }, { - label: "Dataset with point data", + label: 'Dataset with point data', backgroundColor: color(window.chartColors.green).alpha(0.5).rgbString(), borderColor: window.chartColors.green, fill: false, @@ -103,12 +99,12 @@ }] }, options: { - title:{ - text: "Chart.js Time Scale" - }, + title: { + text: 'Chart.js Time Scale' + }, scales: { xAxes: [{ - type: "time", + type: 'time', time: { format: timeFormat, // round: 'day' @@ -118,7 +114,7 @@ display: true, labelString: 'Date' } - }, ], + }], yAxes: [{ scaleLabel: { display: true, @@ -130,7 +126,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; @@ -152,7 +148,7 @@ 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 newColor = window.chartColors[colorName]; var newDataset = { label: 'Dataset ' + config.data.datasets.length, borderColor: newColor, @@ -173,7 +169,7 @@ config.data.labels.push(newDate(config.data.labels.length)); for (var index = 0; index < config.data.datasets.length; ++index) { - if (typeof config.data.datasets[index].data[0] === "object") { + if (typeof config.data.datasets[index].data[0] === 'object') { config.data.datasets[index].data.push({ x: newDate(config.data.datasets[index].data.length), y: randomScalingFactor(), @@ -195,7 +191,7 @@ document.getElementById('removeData').addEventListener('click', function() { config.data.labels.splice(-1, 1); // remove the label first - config.data.datasets.forEach(function(dataset, datasetIndex) { + config.data.datasets.forEach(function(dataset) { dataset.data.pop(); }); diff --git a/samples/scales/toggle-scale-type.html b/samples/scales/toggle-scale-type.html index b46687e82..92c943288 100644 --- a/samples/scales/toggle-scale-type.html +++ b/samples/scales/toggle-scale-type.html @@ -2,98 +2,98 @@ - Toggle Scale Type - - - + Toggle Scale Type + + + -
- -
- - + window.myLine.update(); + }); + diff --git a/samples/scriptable/bubble.html b/samples/scriptable/bubble.html index feaf1ecb8..a8ce1f474 100644 --- a/samples/scriptable/bubble.html +++ b/samples/scriptable/bubble.html @@ -24,7 +24,6 @@ var MIN_XY = -150; var MAX_XY = 100; - var presets = window.chartColors; var utils = Samples.utils; utils.srand(110); @@ -106,13 +105,15 @@ options: options }); + // eslint-disable-next-line no-unused-vars function randomize() { chart.data.datasets.forEach(function(dataset) { - dataset.data = generateData() + dataset.data = generateData(); }); chart.update(); } + // eslint-disable-next-line no-unused-vars function addDataset() { chart.data.datasets.push({ data: generateData() @@ -120,6 +121,7 @@ chart.update(); } + // eslint-disable-next-line no-unused-vars function removeDataset() { chart.data.datasets.shift(); chart.update(); diff --git a/samples/tooltips/border.html b/samples/tooltips/border.html index 25c649e54..0742be0c5 100644 --- a/samples/tooltips/border.html +++ b/samples/tooltips/border.html @@ -34,9 +34,9 @@ return { type: 'line', data: { - labels: ["January", "February", "March", "April", "May", "June", "July"], + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ - label: "Dataset", + label: 'Dataset', borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, data: [10, 30, 46, 2, 8, 50, 0], @@ -45,7 +45,7 @@ }, options: { responsive: true, - title:{ + title: { display: true, text: 'Sample tooltip with border' }, @@ -78,7 +78,6 @@ var ctx = canvas.getContext('2d'); var config = createConfig(); new Chart(ctx, config); - console.log(config); }; diff --git a/samples/tooltips/callbacks.html b/samples/tooltips/callbacks.html index 590edd1a2..0aa336bd5 100644 --- a/samples/tooltips/callbacks.html +++ b/samples/tooltips/callbacks.html @@ -2,106 +2,106 @@ - Tooltip Hooks - - - + Tooltip Hooks + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/tooltips/custom-line.html b/samples/tooltips/custom-line.html index 011762907..96cfac6fd 100644 --- a/samples/tooltips/custom-line.html +++ b/samples/tooltips/custom-line.html @@ -35,7 +35,7 @@
- +
- + Pie Chart with Custom Tooltips + + - + @@ -43,103 +43,103 @@
diff --git a/samples/tooltips/custom-points.html b/samples/tooltips/custom-points.html index 1bb64da4f..f779f62f9 100644 --- a/samples/tooltips/custom-points.html +++ b/samples/tooltips/custom-points.html @@ -41,13 +41,13 @@
diff --git a/samples/tooltips/positioning.html b/samples/tooltips/positioning.html index 696584b06..f98cd638c 100644 --- a/samples/tooltips/positioning.html +++ b/samples/tooltips/positioning.html @@ -34,15 +34,15 @@ return { type: 'line', data: { - labels: ["January", "February", "March", "April", "May", "June", "July"], + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ - label: "My First dataset", + label: 'My First dataset', borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, data: [10, 30, 46, 2, 8, 50, 0], fill: false, }, { - label: "My Second dataset", + label: 'My Second dataset', borderColor: window.chartColors.blue, backgroundColor: window.chartColors.blue, data: [7, 49, 46, 13, 25, 30, 22], @@ -51,7 +51,7 @@ }, options: { responsive: true, - title:{ + title: { display: true, text: 'Tooltip Position: ' + position }, @@ -78,7 +78,7 @@ var ctx = canvas.getContext('2d'); var config = createConfig(position); new Chart(ctx, config); - }) + }); }; From bba29e591604fed8c1e3bc4cf911273d4c5e0f42 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sun, 4 Feb 2018 03:27:50 -0800 Subject: [PATCH 50/56] Remove trailing spaces from docs (#5227) --- docs/axes/README.md | 8 ++++---- docs/axes/cartesian/time.md | 2 +- docs/charts/bar.md | 12 ++++++------ docs/charts/mixed.md | 6 +++--- docs/charts/radar.md | 10 +++++----- docs/configuration/legend.md | 2 +- docs/configuration/tooltip.md | 4 ++-- docs/developers/charts.md | 4 ++-- docs/general/colors.md | 2 +- docs/general/interactions/README.md | 2 +- docs/general/interactions/events.md | 2 +- docs/general/interactions/modes.md | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/axes/README.md b/docs/axes/README.md index b7de691ea..f65cd82d2 100644 --- a/docs/axes/README.md +++ b/docs/axes/README.md @@ -26,18 +26,18 @@ There are a number of config callbacks that can be used to change parameters in | Name | Arguments | Description | ---- | --------- | ----------- | `beforeUpdate` | `axis` | Callback called before the update process starts. -| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. +| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. | `afterSetDimensions` | `axis` | Callback that runs after dimensions are set. | `beforeDataLimits` | `axis` | Callback that runs before data limits are determined. | `afterDataLimits` | `axis` | Callback that runs after data limits are determined. | `beforeBuildTicks` | `axis` | Callback that runs before ticks are created. | `afterBuildTicks` | `axis` | Callback that runs after ticks are created. Useful for filtering ticks. | `beforeTickToLabelConversion` | `axis` | Callback that runs before ticks are converted into strings. -| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. +| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. | `beforeCalculateTickRotation` | `axis` | Callback that runs before tick rotation is determined. | `afterCalculateTickRotation` | `axis` | Callback that runs after tick rotation is determined. -| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. -| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. +| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. +| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. | `afterUpdate` | `axis` | Callback that runs at the end of the update process. ## Updating Axis Defaults diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index 9d15b5e26..cefe9c248 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -147,6 +147,6 @@ The `ticks.source` property controls the ticks generation * `'labels'`: generates ticks from user given `data.labels` values ONLY ### Parser -If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. +If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. If this is a function, it must return a moment.js object given the appropriate data value. diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 64515ed20..84704ba94 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -6,12 +6,12 @@ A bar chart provides a way of showing data values represented as vertical bars. "type": "bar", "data": { "labels": [ - "January", - "February", - "March", - "April", - "May", - "June", + "January", + "February", + "March", + "April", + "May", + "June", "July" ], "datasets": [{ diff --git a/docs/charts/mixed.md b/docs/charts/mixed.md index e7f43a219..9a51eae86 100644 --- a/docs/charts/mixed.md +++ b/docs/charts/mixed.md @@ -41,9 +41,9 @@ At this point we have a chart rendering how we'd like. It's important to note th "type": "bar", "data": { "labels": [ - "January", - "February", - "March", + "January", + "February", + "March", "April" ], "datasets": [{ diff --git a/docs/charts/radar.md b/docs/charts/radar.md index ac908315c..b8a41c838 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -8,9 +8,9 @@ They are often useful for comparing the points of two or more different data set "type": "radar", "data": { "labels": [ - "Eating", - "Drinking", - "Sleeping", + "Eating", + "Drinking", + "Sleeping", "Designing", "Coding", "Cycling", @@ -94,7 +94,7 @@ The style of point. Options are: * 'circle' * 'cross' * 'crossRot' -* 'dash'. +* 'dash'. * 'line' * 'rect' * 'rectRounded' @@ -127,7 +127,7 @@ It is common to want to apply a configuration setting to all created radar chart ## Data Structure -The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. +The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. ```javascript data: [20, 10] diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 52bee8687..6867b762d 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -10,7 +10,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob | `display` | `Boolean` | `true` | is the legend shown | `position` | `String` | `'top'` | Position of the legend. [more...](#position) | `fullWidth` | `Boolean` | `true` | Marks that this box should take the full width of the canvas (pushing down other boxes). This is unlikely to need to be changed in day-to-day use. -| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item +| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item | `onHover` | `Function` | | A callback that is called when a 'mousemove' event is registered on top of a label item | `reverse` | `Boolean` | `false` | Legend will show datasets in reverse order. | `labels` | `Object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 93db40147..b591dc1d1 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -63,9 +63,9 @@ Example: Chart.Tooltip.positioners.custom = function(elements, eventPosition) { /** @type {Chart.Tooltip} */ var tooltip = this; - + /* ... */ - + return { x: 0, y: 0 diff --git a/docs/developers/charts.md b/docs/developers/charts.md index f4c9a9746..e1267423b 100644 --- a/docs/developers/charts.md +++ b/docs/developers/charts.md @@ -75,7 +75,7 @@ The built in controller types are: For example, to derive a new chart type that extends from a bubble chart, you would do the following. ```javascript -// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. +// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. // We look for the defaults by doing Chart.defaults[chartType] // It looks like a bug exists when the defaults don't exist Chart.defaults.derivedBubble = Chart.defaults.bubble; @@ -102,7 +102,7 @@ var custom = Chart.controllers.bubble.extend({ // Stores the controller so that the chart initialization routine can look it up with // Chart.controllers[type] -Chart.controllers.derivedBubble = custom; +Chart.controllers.derivedBubble = custom; // Now we can create and use our new chart type new Chart(ctx, { diff --git a/docs/general/colors.md b/docs/general/colors.md index d2d270081..64ab84c66 100644 --- a/docs/general/colors.md +++ b/docs/general/colors.md @@ -6,7 +6,7 @@ You can also pass a [CanvasGradient](https://developer.mozilla.org/en-US/docs/We ## Patterns and Gradients -An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string colour. +An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string colour. For example, if you wanted to fill a dataset with a pattern from an image you could do the following. diff --git a/docs/general/interactions/README.md b/docs/general/interactions/README.md index 585e65582..532cab6f0 100644 --- a/docs/general/interactions/README.md +++ b/docs/general/interactions/README.md @@ -1,6 +1,6 @@ # Interactions -The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. To configure which events trigger chart interactions, see [events](./events.md#events). +The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. To configure which events trigger chart interactions, see [events](./events.md#events). | Name | Type | Default | Description | ---- | ---- | ------- | ----------- diff --git a/docs/general/interactions/events.md b/docs/general/interactions/events.md index 8e21f7502..d5d05138c 100644 --- a/docs/general/interactions/events.md +++ b/docs/general/interactions/events.md @@ -7,7 +7,7 @@ The following properties define how the chart interacts with events. | `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). | `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 -## Event Option +## Event Option For example, to have the chart only respond to click events, you could do ```javascript var chart = new Chart(ctx, { diff --git a/docs/general/interactions/modes.md b/docs/general/interactions/modes.md index a46f047e7..3e9f7982f 100644 --- a/docs/general/interactions/modes.md +++ b/docs/general/interactions/modes.md @@ -41,7 +41,7 @@ Finds the first item that intersects the point and returns it. Behaves like 'nea 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, in the x direction, is used to determine the 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, in the x direction, is used to determine the index. ```javascript var chart = new Chart(ctx, { From 584d1c646c84f3d49c865ab69e774cc6541027a4 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 9 Feb 2018 17:20:06 -0500 Subject: [PATCH 51/56] Fix label vertical alignment on vertical scales (#5248) --- src/core/core.scale.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index f79dbee80..1996c6c89 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -857,11 +857,15 @@ module.exports = function(Chart) { var label = itemToDraw.label; if (helpers.isArray(label)) { - for (var i = 0, y = 0; i < label.length; ++i) { + var lineCount = label.length; + var lineHeight = tickFont.size * 1.5; + var y = me.isHorizontal() ? 0 : -lineHeight * (lineCount - 1) / 2; + + for (var i = 0; i < lineCount; ++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 += (tickFont.size * 1.5); + y += lineHeight; } } else { context.fillText(label, 0, 0); From d6ce5c0772bda8e4331df1d8e867fde7e6ee0bbe Mon Sep 17 00:00:00 2001 From: Laura Cressman Date: Wed, 14 Feb 2018 21:22:19 -0500 Subject: [PATCH 52/56] Support multiple font colors for radial chart labels (#5240) * Support multiple font colors in array * Address linting error --- docs/axes/radial/linear.md | 2 +- src/scales/scale.radialLinear.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index adebe77cc..14009be6a 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -104,7 +104,7 @@ The following options are used to configure the point labels that are shown on t | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. -| `fontColor` | `Color` | `'#666'` | Font color for point labels. +| `fontColor` | `Color/Color[]` | `'#666'` | Font color for point labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family to use when rendering labels. | `fontSize` | `Number` | 10 | font size in pixels | `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 9f59f1788..e36b4e135 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -237,7 +237,6 @@ module.exports = function(Chart) { function drawPointLabels(scale) { var ctx = scale.ctx; - var valueOrDefault = helpers.valueOrDefault; var opts = scale.options; var angleLineOpts = opts.angleLines; var pointLabelOpts = opts.pointLabels; @@ -267,7 +266,7 @@ module.exports = function(Chart) { var pointLabelPosition = scale.getPointPosition(i, outerDistance + 5); // Keep this in loop since we may support array properties here - var pointLabelFontColor = valueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); + var pointLabelFontColor = helpers.valueAtIndexOrDefault(pointLabelOpts.fontColor, i, globalDefaults.defaultFontColor); ctx.font = plFont.font; ctx.fillStyle = pointLabelFontColor; From be6660c63d10b54f4972170c4f3364b61bb9b9b3 Mon Sep 17 00:00:00 2001 From: Winter Zhong Date: Fri, 16 Feb 2018 08:25:49 +0800 Subject: [PATCH 53/56] Improve title of generated documentation (#5256) --- book.json | 1 + 1 file changed, 1 insertion(+) diff --git a/book.json b/book.json index 487fd095c..8b0f73970 100644 --- a/book.json +++ b/book.json @@ -1,5 +1,6 @@ { "root": "./docs", + "title": "Chart.js documentation", "author": "chartjs", "gitbook": "3.2.2", "plugins": ["-lunr", "-search", "search-plus", "anchorjs", "chartjs", "ga"], From c90cf2ebcdce88d90086c32397a9d8355c985695 Mon Sep 17 00:00:00 2001 From: Wilson Lin Date: Tue, 20 Feb 2018 04:29:10 -0800 Subject: [PATCH 54/56] Make both README.md and installation.md clearer (#5274) Address the ambiguity of "Selecting the Correct Build" section --- README.md | 18 +++++++++++++++--- docs/getting-started/installation.md | 6 +++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b68b39008..f914944e1 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,23 @@ To install via bower: bower install chart.js --save ``` -#### Selecting the Correct Build +### Selecting the Correct Build -Chart.js provides two different builds that are available for your use. The `Chart.js` and `Chart.min.js` files include Chart.js and the accompanying color parsing library. If this version is used and you require the use of the time axis, [Moment.js](http://momentjs.com/) will need to be included before Chart.js. +Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. -The `Chart.bundle.js` and `Chart.bundle.min.js` builds include Moment.js in a single file. This version should be used if you require time axes and want a single file to include, select this version. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. +#### Stand-Alone Build +Files: +* `dist/Chart.js` +* `dist/Chart.min.js` + +The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. + +#### Bundled Build +Files: +* `dist/Chart.bundle.js` +* `dist/Chart.bundle.min.js` + +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. ## Documentation diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 613d7aa7a..ccc02ddc4 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -40,18 +40,18 @@ If you download or clone the repository, you must [build](../developers/contribu # Selecting the Correct Build -Chart.js provides two different builds that are available for your use. +Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. ## Stand-Alone Build Files: * `dist/Chart.js` * `dist/Chart.min.js` -This version only includes Chart.js. If this version is used and you require the use of the time axis, [Moment.js](http://momentjs.com/) will need to be included before Chart.js. +The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. ## Bundled Build Files: * `dist/Chart.bundle.js` * `dist/Chart.bundle.min.js` -The bundled version includes Moment.js built into the same file. This version should be used if you wish to use time axes and want a single file to include. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. From c2a5b1237635875b20c46d47577355ced733db14 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Wed, 21 Feb 2018 00:44:01 +0100 Subject: [PATCH 55/56] Bugfix: Improve polyfill function of log10 to return whole powers of 10 (#5275) * Bugfix: Improve polyfill function of log10 to return whole powers of 10 as integer values, as it caused endless loop in IE11 in the tick creation loop. * Compare floating-point numbers directly instead of using unnecessary division. --- src/core/core.helpers.js | 8 +++++++- test/specs/core.helpers.tests.js | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index bdce895cf..347fd958e 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -159,7 +159,13 @@ module.exports = function(Chart) { return Math.log10(x); } : function(x) { - return Math.log(x) / Math.LN10; + var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10. + // Check for whole powers of 10, + // which due to floating point rounding error should be corrected. + var powerOf10 = Math.round(exponent); + var isPowerOf10 = x === Math.pow(10, powerOf10); + + return isPowerOf10 ? powerOf10 : exponent; }; helpers.toRadians = function(degrees) { return degrees * (Math.PI / 180); diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 44446d38a..eb96ef8c7 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -194,8 +194,12 @@ describe('Core helper tests', function() { it('should do a log10 operation', function() { expect(helpers.log10(0)).toBe(-Infinity); - expect(helpers.log10(1)).toBe(0); - expect(helpers.log10(1000)).toBeCloseTo(3, 1e-9); + + // Check all allowed powers of 10, which should return integer values + var maxPowerOf10 = Math.floor(helpers.log10(Number.MAX_VALUE)); + for (var i = 0; i < maxPowerOf10; i += 1) { + expect(helpers.log10(Math.pow(10, i))).toBe(i); + } }); it('should correctly determine if two numbers are essentially equal', function() { From ac088a04abf671a217aa03a65699524486ceab06 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 1 Mar 2018 22:20:53 +0100 Subject: [PATCH 56/56] Bump version to 2.7.2 (#5307) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03f3815cc..a2e6c494a 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.7.1", + "version": "2.7.2", "license": "MIT", "main": "src/chart.js", "repository": {