diff --git a/.eslintrc.yml b/.eslintrc.yml index 8f9b4af3023..b0d9d5695a9 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 00000000000..1ab933490de --- /dev/null +++ b/.htmllintrc @@ -0,0 +1,19 @@ +{ + "indent-style": "tabs", + "line-end-style": false, + "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/README.md b/README.md index 10d2c1d8d7d..f914944e101 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) @@ -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/book.json b/book.json index 487fd095c03..8b0f73970df 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"], diff --git a/docs/README.md b/docs/README.md index 5a0e9f35579..24ee8d49982 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/axes/README.md b/docs/axes/README.md index b7de691eae9..f65cd82d29a 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/linear.md b/docs/axes/cartesian/linear.md index 1d0292074a8..3f68b79ff1d 100644 --- a/docs/axes/cartesian/linear.md +++ b/docs/axes/cartesian/linear.md @@ -12,6 +12,7 @@ The following options are provided by the linear scale. They are all located in | `min` | `Number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](#axis-range-settings) | `max` | `Number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](#axis-range-settings) | `maxTicksLimit` | `Number` | `11` | Maximum number of ticks and gridlines to show. +| `precision` | `Number` | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. | `stepSize` | `Number` | | User defined fixed step size for the scale. [more...](#step-size) | `suggestedMax` | `Number` | | Adjustment used when calculating the maximum data value. [more...](#axis-range-settings) | `suggestedMin` | `Number` | | Adjustment used when calculating the minimum data value. [more...](#axis-range-settings) diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index 9d15b5e2674..cefe9c24806 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/axes/radial/linear.md b/docs/axes/radial/linear.md index adebe77cc61..594901db335 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -27,6 +27,7 @@ The following options are provided by the linear scale. They are all located in | `min` | `Number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](#axis-range-settings) | `max` | `Number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](#axis-range-settings) | `maxTicksLimit` | `Number` | `11` | Maximum number of ticks and gridlines to show. +| `precision` | `Number` | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. | `stepSize` | `Number` | | User defined fixed step size for the scale. [more...](#step-size) | `suggestedMax` | `Number` | | Adjustment used when calculating the maximum data value. [more...](#axis-range-settings) | `suggestedMin` | `Number` | | Adjustment used when calculating the minimum data value. [more...](#axis-range-settings) @@ -104,7 +105,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/docs/charts/bar.md b/docs/charts/bar.md index 64515ed20d8..84704ba941b 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 e7f43a2193b..9a51eae860f 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 ac908315cf9..b8a41c8384c 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 52bee8687c0..d2a3a88b3b6 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -1,6 +1,6 @@ # Legend Configuration -The chart legend displays data about the datasets that area appearing on the chart. +The chart legend displays data about the datasets that are appearing on the chart. ## Configuration options The legend configuration is passed into the `options.legend` namespace. The global options for the chart legend is defined in `Chart.defaults.global.legend`. @@ -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 93db40147b7..b591dc1d13e 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/api.md b/docs/developers/api.md index a5a0a513396..976578d572a 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]; } diff --git a/docs/developers/charts.md b/docs/developers/charts.md index f4c9a97469a..e1267423bf0 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/developers/contributing.md b/docs/developers/contributing.md index e6551d590d1..fa3511bb09a 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 diff --git a/docs/general/colors.md b/docs/general/colors.md index d2d270081e6..64ab84c66cf 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 585e6558204..532cab6f080 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 8e21f75024b..d5d05138cf7 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 a46f047e70b..3e9f7982f9e 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, { diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 613d7aa7a6c..ccc02ddc4b9 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. diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 0998641bacc..ae141d96e92 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -18,6 +18,7 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co - chartjs-plugin-deferred - Defers initial chart update until chart scrolls into viewport. - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. - chartjs-plugin-stacked100 - Draws 100% stacked bar chart. + - chartjs-plugin-streaming - Enables to create live streaming charts. - chartjs-plugin-waterfall - Enables easy use of waterfall charts. - chartjs-plugin-zoom - Enables zooming and panning on charts. @@ -56,5 +57,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 diff --git a/gulpfile.js b/gulpfile.js index 09165c8b041..21f19645f81 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 c7d21043b1a..a2e6c494af1 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": { @@ -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 00000000000..9573adbcde6 --- /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 17fc47c8f99..52eb07b8fd3 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 d460bc88e2a..b97a998ea95 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 e4ed8e90b24..36fd5671692 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 edadc781c54..7ac6883bb12 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 7bc54e3138a..127726e7fb7 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 26ee6b18f8f..11a143b0b63 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 + + + -
- -
- - - - - - +
+ +
+ + + + + + diff --git a/samples/charts/bar/multi-axis.html b/samples/charts/bar/multi-axis.html index 28755a701a5..c206866a410 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 624992cfb4a..e3b734b2d17 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 46d1c4051f3..ca9f0cafb71 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 906b4624c39..e9348b274fd 100644 --- a/samples/charts/bar/vertical.html +++ b/samples/charts/bar/vertical.html @@ -2,143 +2,143 @@ - Bar Chart - - - + Bar Chart + + + -
- -
- - - - - - +
+ +
+ + + + + + diff --git a/samples/charts/bubble.html b/samples/charts/bubble.html index 0df09477f5c..092c1fb464b 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 0906029c41d..c99894e2212 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 b288fe1293f..8466970a403 100644 --- a/samples/charts/doughnut.html +++ b/samples/charts/doughnut.html @@ -2,143 +2,156 @@ - Doughnut Chart - - - + Doughnut Chart + + + -
- -
- - - - - - +
+ +
+ + + + + + + diff --git a/samples/charts/line/basic.html b/samples/charts/line/basic.html index db01ccf743d..8028bee9c51 100644 --- a/samples/charts/line/basic.html +++ b/samples/charts/line/basic.html @@ -2,162 +2,162 @@ - Line Chart - - - + Line Chart + + + -
- -
-
-
- - - - - - +
+ +
+
+
+ + + + + + diff --git a/samples/charts/line/interpolation-modes.html b/samples/charts/line/interpolation-modes.html index 374da49c083..b11dd681972 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 ed268e1fdb4..3ba9defcdaf 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 2ede74446ae..d3569e85525 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 823c6d3e41c..53d3db46cbb 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 1985d394d23..2ef46a9cf35 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 00aa81fe045..aedacce7244 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 ccce87c2dcf..4ad9708ebd1 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 ab29b3a0e34..ab042e2dcd6 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 fbbb9d972bc..7eda09a7fb4 100644 --- a/samples/charts/radar.html +++ b/samples/charts/radar.html @@ -2,145 +2,145 @@ - Radar Chart - - - + Radar Chart + + + -
- -
- - - - - - +
+ +
+ + + + + + diff --git a/samples/charts/scatter/basic.html b/samples/charts/scatter/basic.html index c5e36e9d99c..6ac227c3b95 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 cbf3bf73a8a..6c20b98f0de 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 d855662f093..b9e13c364c4 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 727c7a6d85c..b7acea4bd84 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 97bc70fa047..5bd2259cfb1 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 4af89025eb4..4b4b51724e4 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 c21469329b8..8e30bdcdbf4 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 f945cb266d4..d2c00ecdda2 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 18059548a7f..10d546ac297 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 868bc7b1db5..feafbd7a62c 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 ced0b6c3ca3..fc0af2ee3e0 100644 --- a/samples/scales/linear/step-size.html +++ b/samples/scales/linear/step-size.html @@ -2,174 +2,174 @@ - Line Chart - - - + Line Chart + + + -
- -
-
-
- - - - - - +
+ +
+
+
+ + + + + + diff --git a/samples/scales/logarithmic/line.html b/samples/scales/logarithmic/line.html index 2c961abd2e1..72fd9b07dba 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 a4bd577c273..5f266a5cda1 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 b7bb041e973..0f8af2a7df1 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 07e319b7090..b3bfc5eec92 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 ddcc5b326bf..d435e74c69c 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 3d91dfa56b7..792eef55306 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 3483c454535..604a49b4f1a 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); }; @@ -121,29 +121,23 @@ window.myLine.update(); }); - 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 newTime = lastTime - .clone() - .add(1, 'day') - .format('MM/DD/YYYY HH:mm'); - - for (var index = 0; index < config.data.datasets.length; ++index) { - config.data.datasets[index].data.push({ - x: newTime, - y: randomScalingFactor() - }); - } + config.data.datasets[0].data.push({ + x: newDateString(config.data.datasets[0].data.length + 2), + y: randomScalingFactor() + }); + config.data.datasets[1].data.push({ + x: newDate(config.data.datasets[1].data.length + 2), + y: randomScalingFactor() + }); window.myLine.update(); } }); 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 0cca931e009..70a6978af18 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 b46687e823a..92c943288e3 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 feaf1ecb84c..a8ce1f4743c 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 25c649e548e..0742be0c5f5 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 590edd1a236..0aa336bd5f3 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 011762907c4..96cfac6fd3c 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 1bb64da4fa5..f779f62f9c7 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 696584b0677..f98cd638ced 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); - }) + }); }; diff --git a/src/chart.js b/src/chart.js index a958e343ff8..1f6982ce47e 100644 --- a/src/chart.js +++ b/src/chart.js @@ -8,6 +8,8 @@ Chart.helpers = require('./helpers/index'); // @todo dispatch these helpers into appropriated helpers/helpers.* file and write unit tests! require('./core/core.helpers')(Chart); +Chart.Animation = require('./core/core.animation'); +Chart.animationService = require('./core/core.animations'); Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); @@ -15,14 +17,13 @@ Chart.Interaction = require('./core/core.interaction'); Chart.layouts = require('./core/core.layouts'); Chart.platform = require('./platforms/platform'); Chart.plugins = require('./core/core.plugins'); +Chart.Scale = require('./core/core.scale'); +Chart.scaleService = require('./core/core.scaleService'); Chart.Ticks = require('./core/core.ticks'); +Chart.Tooltip = require('./core/core.tooltip'); -require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); -require('./core/core.scaleService')(Chart); -require('./core/core.scale')(Chart); -require('./core/core.tooltip')(Chart); require('./scales/scale.linearbase')(Chart); require('./scales/scale.category')(Chart); @@ -49,7 +50,7 @@ require('./charts/Chart.PolarArea')(Chart); require('./charts/Chart.Radar')(Chart); require('./charts/Chart.Scatter')(Chart); -// Loading built-it plugins +// Loading built-in plugins var plugins = require('./plugins'); for (var k in plugins) { if (plugins.hasOwnProperty(k)) { diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 9d9206d5cb2..7aacf2d23e2 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -283,15 +283,23 @@ module.exports = function(Chart) { var points = meta.data || []; var area = chart.chartArea; var ilen = points.length; + var halfBorderWidth; var i = 0; - helpers.canvas.clipArea(chart.ctx, area); - if (lineEnabled(me.getDataset(), chart.options)) { + halfBorderWidth = (meta.dataset._model.borderWidth || 0) / 2; + + helpers.canvas.clipArea(chart.ctx, { + left: area.left, + right: area.right, + top: area.top - halfBorderWidth, + bottom: area.bottom + halfBorderWidth + }); + meta.dataset.draw(); - } - helpers.canvas.unclipArea(chart.ctx); + helpers.canvas.unclipArea(chart.ctx); + } // Draw the points for (; i < ilen; ++i) { diff --git a/src/core/core.animation.js b/src/core/core.animation.js index af746588e60..8b2f4dd2ade 100644 --- a/src/core/core.animation.js +++ b/src/core/core.animation.js @@ -1,172 +1,43 @@ -/* global window: false */ 'use strict'; -var defaults = require('./core.defaults'); var Element = require('./core.element'); -var helpers = require('../helpers/index'); -defaults._set('global', { - animation: { - duration: 1000, - easing: 'easeOutQuart', - onProgress: helpers.noop, - onComplete: helpers.noop - } -}); - -module.exports = function(Chart) { - - Chart.Animation = Element.extend({ - chart: null, // the animation associated chart instance - currentStep: 0, // the current animation step - numSteps: 60, // default number of steps - easing: '', // the easing to use for this animation - render: null, // render function used by the animation service - - onAnimationProgress: null, // user specified callback to fire on each step of the animation - onAnimationComplete: null, // user specified callback to fire when the animation finishes - }); - - Chart.animationService = { - frameDuration: 17, - animations: [], - dropFrames: 0, - request: null, - - /** - * @param {Chart} chart - The chart to animate. - * @param {Chart.Animation} animation - The animation that we will animate. - * @param {Number} duration - The animation duration in ms. - * @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions - */ - addAnimation: function(chart, animation, duration, lazy) { - var animations = this.animations; - var i, ilen; - - animation.chart = chart; - - if (!lazy) { - chart.animating = true; - } - - for (i = 0, ilen = animations.length; i < ilen; ++i) { - if (animations[i].chart === chart) { - animations[i] = animation; - return; - } - } - - animations.push(animation); - - // If there are no animations queued, manually kickstart a digest, for lack of a better word - if (animations.length === 1) { - this.requestAnimationFrame(); - } - }, - - cancelAnimation: function(chart) { - var index = helpers.findIndex(this.animations, function(animation) { - return animation.chart === chart; - }); - - if (index !== -1) { - this.animations.splice(index, 1); - chart.animating = false; - } - }, - - requestAnimationFrame: function() { - var me = this; - if (me.request === null) { - // Skip animation frame requests until the active one is executed. - // This can happen when processing mouse events, e.g. 'mousemove' - // and 'mouseout' events will trigger multiple renders. - me.request = helpers.requestAnimFrame.call(window, function() { - me.request = null; - me.startDigest(); - }); - } - }, - - /** - * @private - */ - startDigest: function() { - var me = this; - var startTime = Date.now(); - var framesToDrop = 0; +var exports = module.exports = Element.extend({ + chart: null, // the animation associated chart instance + currentStep: 0, // the current animation step + numSteps: 60, // default number of steps + easing: '', // the easing to use for this animation + render: null, // render function used by the animation service - if (me.dropFrames > 1) { - framesToDrop = Math.floor(me.dropFrames); - me.dropFrames = me.dropFrames % 1; - } - - me.advance(1 + framesToDrop); - - var endTime = Date.now(); - - me.dropFrames += (endTime - startTime) / me.frameDuration; - - // Do we have more stuff to animate? - if (me.animations.length > 0) { - me.requestAnimationFrame(); - } - }, - - /** - * @private - */ - advance: function(count) { - var animations = this.animations; - var animation, chart; - var i = 0; - - while (i < animations.length) { - animation = animations[i]; - chart = animation.chart; - - animation.currentStep = (animation.currentStep || 0) + count; - animation.currentStep = Math.min(animation.currentStep, animation.numSteps); - - helpers.callback(animation.render, [chart, animation], chart); - helpers.callback(animation.onAnimationProgress, [animation], chart); - - if (animation.currentStep >= animation.numSteps) { - helpers.callback(animation.onAnimationComplete, [animation], chart); - chart.animating = false; - animations.splice(i, 1); - } else { - ++i; - } - } - } - }; - - /** - * Provided for backward compatibility, use Chart.Animation instead - * @prop Chart.Animation#animationObject - * @deprecated since version 2.6.0 - * @todo remove at version 3 - */ - Object.defineProperty(Chart.Animation.prototype, 'animationObject', { - get: function() { - return this; - } - }); + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes +}); - /** - * Provided for backward compatibility, use Chart.Animation#chart instead - * @prop Chart.Animation#chartInstance - * @deprecated since version 2.6.0 - * @todo remove at version 3 - */ - Object.defineProperty(Chart.Animation.prototype, 'chartInstance', { - get: function() { - return this.chart; - }, - set: function(value) { - this.chart = value; - } - }); +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart.Animation instead + * @prop Chart.Animation#animationObject + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ +Object.defineProperty(exports.prototype, 'animationObject', { + get: function() { + return this; + } +}); -}; +/** + * Provided for backward compatibility, use Chart.Animation#chart instead + * @prop Chart.Animation#chartInstance + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ +Object.defineProperty(exports.prototype, 'chartInstance', { + get: function() { + return this.chart; + }, + set: function(value) { + this.chart = value; + } +}); diff --git a/src/core/core.animations.js b/src/core/core.animations.js new file mode 100644 index 00000000000..6853cb8736d --- /dev/null +++ b/src/core/core.animations.js @@ -0,0 +1,129 @@ +/* global window: false */ +'use strict'; + +var defaults = require('./core.defaults'); +var helpers = require('../helpers/index'); + +defaults._set('global', { + animation: { + duration: 1000, + easing: 'easeOutQuart', + onProgress: helpers.noop, + onComplete: helpers.noop + } +}); + +module.exports = { + frameDuration: 17, + animations: [], + dropFrames: 0, + request: null, + + /** + * @param {Chart} chart - The chart to animate. + * @param {Chart.Animation} animation - The animation that we will animate. + * @param {Number} duration - The animation duration in ms. + * @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions + */ + addAnimation: function(chart, animation, duration, lazy) { + var animations = this.animations; + var i, ilen; + + animation.chart = chart; + + if (!lazy) { + chart.animating = true; + } + + for (i = 0, ilen = animations.length; i < ilen; ++i) { + if (animations[i].chart === chart) { + animations[i] = animation; + return; + } + } + + animations.push(animation); + + // If there are no animations queued, manually kickstart a digest, for lack of a better word + if (animations.length === 1) { + this.requestAnimationFrame(); + } + }, + + cancelAnimation: function(chart) { + var index = helpers.findIndex(this.animations, function(animation) { + return animation.chart === chart; + }); + + if (index !== -1) { + this.animations.splice(index, 1); + chart.animating = false; + } + }, + + requestAnimationFrame: function() { + var me = this; + if (me.request === null) { + // Skip animation frame requests until the active one is executed. + // This can happen when processing mouse events, e.g. 'mousemove' + // and 'mouseout' events will trigger multiple renders. + me.request = helpers.requestAnimFrame.call(window, function() { + me.request = null; + me.startDigest(); + }); + } + }, + + /** + * @private + */ + startDigest: function() { + var me = this; + var startTime = Date.now(); + var framesToDrop = 0; + + if (me.dropFrames > 1) { + framesToDrop = Math.floor(me.dropFrames); + me.dropFrames = me.dropFrames % 1; + } + + me.advance(1 + framesToDrop); + + var endTime = Date.now(); + + me.dropFrames += (endTime - startTime) / me.frameDuration; + + // Do we have more stuff to animate? + if (me.animations.length > 0) { + me.requestAnimationFrame(); + } + }, + + /** + * @private + */ + advance: function(count) { + var animations = this.animations; + var animation, chart; + var i = 0; + + while (i < animations.length) { + animation = animations[i]; + chart = animation.chart; + + animation.currentStep = (animation.currentStep || 0) + count; + animation.currentStep = Math.min(animation.currentStep, animation.numSteps); + + helpers.callback(animation.render, [chart, animation], chart); + helpers.callback(animation.onAnimationProgress, [animation], chart); + + if (animation.currentStep >= animation.numSteps) { + helpers.callback(animation.onAnimationComplete, [animation], chart); + chart.animating = false; + animations.splice(i, 1); + } else { + ++i; + } + } + } +}; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e29a5b0769c..d27967d3941 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -1,11 +1,15 @@ 'use strict'; +var Animation = require('./core.animation'); +var animations = require('./core.animations'); var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); var layouts = require('./core.layouts'); var platform = require('../platforms/platform'); var plugins = require('./core.plugins'); +var scaleService = require('../core/core.scaleService'); +var Tooltip = require('./core.tooltip'); module.exports = function(Chart) { @@ -164,7 +168,7 @@ module.exports = function(Chart) { stop: function() { // Stops any current animation loop occurring - Chart.animationService.cancelAnimation(this); + animations.cancelAnimation(this); return this; }, @@ -275,7 +279,7 @@ module.exports = function(Chart) { scale.ctx = me.ctx; scale.chart = me; } else { - var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); + var scaleClass = scaleService.getScaleConstructor(scaleType); if (!scaleClass) { return; } @@ -307,7 +311,7 @@ module.exports = function(Chart) { me.scales = scales; - Chart.scaleService.addScalesToLayout(this); + scaleService.addScalesToLayout(this); }, buildOrUpdateControllers: function() { @@ -519,7 +523,7 @@ module.exports = function(Chart) { }; if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { - var animation = new Chart.Animation({ + var animation = new Animation({ numSteps: (duration || animationOptions.duration) / 16.66, // 60 fps easing: config.easing || animationOptions.easing, @@ -535,12 +539,12 @@ module.exports = function(Chart) { onAnimationComplete: onComplete }); - Chart.animationService.addAnimation(me, animation, duration, lazy); + animations.addAnimation(me, animation, duration, lazy); } else { me.draw(); // See https://github.com/chartjs/Chart.js/issues/3781 - onComplete(new Chart.Animation({numSteps: 0, chart: me})); + onComplete(new Animation({numSteps: 0, chart: me})); } return me; @@ -775,7 +779,7 @@ module.exports = function(Chart) { initToolTip: function() { var me = this; - me.tooltip = new Chart.Tooltip({ + me.tooltip = new Tooltip({ _chart: me, _chartInstance: me, // deprecated, backward compatibility _data: me.data, diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index bdce895cf70..64e4180df4e 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -5,8 +5,9 @@ var color = require('chartjs-color'); var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); +var scaleService = require('../core/core.scaleService'); -module.exports = function(Chart) { +module.exports = function() { // -- Basic js utility methods @@ -21,7 +22,7 @@ module.exports = function(Chart) { target[key] = helpers.scaleMerge(tval, sval); } else if (key === 'scale') { // used in polar area & radar charts since there is only one scale - target[key] = helpers.merge(tval, [Chart.scaleService.getScaleDefaults(sval.type), sval]); + target[key] = helpers.merge(tval, [scaleService.getScaleDefaults(sval.type), sval]); } else { helpers._merger(key, target, source, options); } @@ -51,7 +52,7 @@ module.exports = function(Chart) { if (!target[key][i].type || (scale.type && scale.type !== target[key][i].type)) { // new/untyped scale or type changed: let's apply the new defaults // then merge source scale to correctly overwrite the defaults. - helpers.merge(target[key][i], [Chart.scaleService.getScaleDefaults(type), scale]); + helpers.merge(target[key][i], [scaleService.getScaleDefaults(type), scale]); } else { // scales type are the same helpers.merge(target[key][i], scale); @@ -159,7 +160,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); @@ -466,15 +473,25 @@ module.exports = function(Chart) { helpers.getConstraintHeight = function(domNode) { return getConstraintDimension(domNode, 'max-height', 'clientHeight'); }; + /** + * @private + */ + helpers._calculatePadding = function(container, padding, parentDimension) { + padding = helpers.getStyle(container, padding); + + return padding.indexOf('%') > -1 ? parentDimension / parseInt(padding, 10) : parseInt(padding, 10); + }; helpers.getMaximumWidth = function(domNode) { var container = domNode.parentNode; if (!container) { return domNode.clientWidth; } - var paddingLeft = parseInt(helpers.getStyle(container, 'padding-left'), 10); - var paddingRight = parseInt(helpers.getStyle(container, 'padding-right'), 10); - var w = container.clientWidth - paddingLeft - paddingRight; + var clientWidth = container.clientWidth; + var paddingLeft = helpers._calculatePadding(container, 'padding-left', clientWidth); + var paddingRight = helpers._calculatePadding(container, 'padding-right', clientWidth); + + var w = clientWidth - paddingLeft - paddingRight; var cw = helpers.getConstraintWidth(domNode); return isNaN(cw) ? w : Math.min(w, cw); }; @@ -484,9 +501,11 @@ module.exports = function(Chart) { return domNode.clientHeight; } - var paddingTop = parseInt(helpers.getStyle(container, 'padding-top'), 10); - var paddingBottom = parseInt(helpers.getStyle(container, 'padding-bottom'), 10); - var h = container.clientHeight - paddingTop - paddingBottom; + var clientHeight = container.clientHeight; + var paddingTop = helpers._calculatePadding(container, 'padding-top', clientHeight); + var paddingBottom = helpers._calculatePadding(container, 'padding-bottom', clientHeight); + + var h = clientHeight - paddingTop - paddingBottom; var ch = helpers.getConstraintHeight(domNode); return isNaN(ch) ? h : Math.min(h, ch); }; @@ -496,7 +515,7 @@ module.exports = function(Chart) { document.defaultView.getComputedStyle(el, null).getPropertyValue(property); }; helpers.retinaScale = function(chart, forceRatio) { - var pixelRatio = chart.currentDevicePixelRatio = forceRatio || window.devicePixelRatio || 1; + var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1; if (pixelRatio === 1) { return; } diff --git a/src/core/core.scale.js b/src/core/core.scale.js index ffe13cbff82..78ede6985f6 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -89,843 +89,846 @@ function getLineValue(scale, index, offsetGridLines) { return lineValue; } -module.exports = function(Chart) { +function computeTextSize(context, tick, font) { + return helpers.isArray(tick) ? + helpers.longestText(context, font, tick) : + context.measureText(tick).width; +} - function computeTextSize(context, tick, font) { - return helpers.isArray(tick) ? - helpers.longestText(context, font, tick) : - context.measureText(tick).width; - } +function parseFontOptions(options) { + var valueOrDefault = helpers.valueOrDefault; + var globalDefaults = defaults.global; + var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); + var style = valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle); + var family = valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily); + + return { + size: size, + style: style, + family: family, + font: helpers.fontString(size, style, family) + }; +} - function parseFontOptions(options) { - var valueOrDefault = helpers.valueOrDefault; - var globalDefaults = defaults.global; - var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); - var style = valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle); - var family = valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily); +function parseLineHeight(options) { + return helpers.options.toLineHeight( + helpers.valueOrDefault(options.lineHeight, 1.2), + helpers.valueOrDefault(options.fontSize, defaults.global.defaultFontSize)); +} +module.exports = Element.extend({ + /** + * Get the padding needed for the scale + * @method getPadding + * @private + * @returns {Padding} the necessary padding + */ + getPadding: function() { + var me = this; return { - size: size, - style: style, - family: family, - font: helpers.fontString(size, style, family) + left: me.paddingLeft || 0, + top: me.paddingTop || 0, + right: me.paddingRight || 0, + bottom: me.paddingBottom || 0 }; - } + }, - function parseLineHeight(options) { - return helpers.options.toLineHeight( - helpers.valueOrDefault(options.lineHeight, 1.2), - helpers.valueOrDefault(options.fontSize, defaults.global.defaultFontSize)); - } + /** + * Returns the scale tick objects ({label, major}) + * @since 2.7 + */ + getTicks: function() { + return this._ticks; + }, - Chart.Scale = Element.extend({ - /** - * Get the padding needed for the scale - * @method getPadding - * @private - * @returns {Padding} the necessary padding - */ - getPadding: function() { - var me = this; - return { - left: me.paddingLeft || 0, - top: me.paddingTop || 0, - right: me.paddingRight || 0, - bottom: me.paddingBottom || 0 + // These methods are ordered by lifecyle. Utilities then follow. + // Any function defined here is inherited by all scale types. + // Any function can be extended by the scale type + + mergeTicksOptions: function() { + var ticks = this.options.ticks; + if (ticks.minor === false) { + ticks.minor = { + display: false }; - }, - - /** - * Returns the scale tick objects ({label, major}) - * @since 2.7 - */ - getTicks: function() { - return this._ticks; - }, - - // These methods are ordered by lifecyle. Utilities then follow. - // Any function defined here is inherited by all scale types. - // Any function can be extended by the scale type - - mergeTicksOptions: function() { - var ticks = this.options.ticks; - if (ticks.minor === false) { - ticks.minor = { - display: false - }; - } - if (ticks.major === false) { - ticks.major = { - display: false - }; - } - for (var key in ticks) { - if (key !== 'major' && key !== 'minor') { - if (typeof ticks.minor[key] === 'undefined') { - ticks.minor[key] = ticks[key]; - } - if (typeof ticks.major[key] === 'undefined') { - ticks.major[key] = ticks[key]; - } + } + if (ticks.major === false) { + ticks.major = { + display: false + }; + } + for (var key in ticks) { + if (key !== 'major' && key !== 'minor') { + if (typeof ticks.minor[key] === 'undefined') { + ticks.minor[key] = ticks[key]; } - } - }, - beforeUpdate: function() { - helpers.callback(this.options.beforeUpdate, [this]); - }, - update: function(maxWidth, maxHeight, margins) { - var me = this; - var i, ilen, labels, label, ticks, tick; - - // 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 = helpers.extend({ - left: 0, - right: 0, - top: 0, - bottom: 0 - }, margins); - me.longestTextCache = me.longestTextCache || {}; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - - // Data min/max - me.beforeDataLimits(); - me.determineDataLimits(); - me.afterDataLimits(); - - // Ticks - `this.ticks` is now DEPRECATED! - // Internal ticks are now stored as objects in the PRIVATE `this._ticks` member - // and must not be accessed directly from outside this class. `this.ticks` being - // around for long time and not marked as private, we can't change its structure - // without unexpected breaking changes. If you need to access the scale ticks, - // use scale.getTicks() instead. - - me.beforeBuildTicks(); - - // New implementations should return an array of objects but for BACKWARD COMPAT, - // we still support no return (`this.ticks` internally set by calling this method). - ticks = me.buildTicks() || []; - - me.afterBuildTicks(); - - me.beforeTickToLabelConversion(); - - // New implementations should return the formatted tick labels but for BACKWARD - // COMPAT, we still support no return (`this.ticks` internally changed by calling - // this method and supposed to contain only string values). - labels = me.convertTicksToLabels(ticks) || me.ticks; - - me.afterTickToLabelConversion(); - - me.ticks = labels; // BACKWARD COMPATIBILITY - - // IMPORTANT: from this point, we consider that `this.ticks` will NEVER change! - - // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`) - for (i = 0, ilen = labels.length; i < ilen; ++i) { - label = labels[i]; - tick = ticks[i]; - if (!tick) { - ticks.push(tick = { - label: label, - major: false - }); - } else { - tick.label = label; + if (typeof ticks.major[key] === 'undefined') { + ticks.major[key] = ticks[key]; } } + } + }, + beforeUpdate: function() { + helpers.callback(this.options.beforeUpdate, [this]); + }, - me._ticks = ticks; + update: function(maxWidth, maxHeight, margins) { + var me = this; + var i, ilen, labels, label, ticks, tick; - // Tick Rotation - me.beforeCalculateTickRotation(); - me.calculateTickRotation(); - me.afterCalculateTickRotation(); - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); - return me.minSize; + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = helpers.extend({ + left: 0, + right: 0, + top: 0, + bottom: 0 + }, margins); + me.longestTextCache = me.longestTextCache || {}; - }, - afterUpdate: function() { - helpers.callback(this.options.afterUpdate, [this]); - }, + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); - // + // Data min/max + me.beforeDataLimits(); + me.determineDataLimits(); + me.afterDataLimits(); - beforeSetDimensions: function() { - helpers.callback(this.options.beforeSetDimensions, [this]); - }, - 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; + // Ticks - `this.ticks` is now DEPRECATED! + // Internal ticks are now stored as objects in the PRIVATE `this._ticks` member + // and must not be accessed directly from outside this class. `this.ticks` being + // around for long time and not marked as private, we can't change its structure + // without unexpected breaking changes. If you need to access the scale ticks, + // use scale.getTicks() instead. + + me.beforeBuildTicks(); + + // New implementations should return an array of objects but for BACKWARD COMPAT, + // we still support no return (`this.ticks` internally set by calling this method). + ticks = me.buildTicks() || []; + + me.afterBuildTicks(); + + me.beforeTickToLabelConversion(); + + // New implementations should return the formatted tick labels but for BACKWARD + // COMPAT, we still support no return (`this.ticks` internally changed by calling + // this method and supposed to contain only string values). + labels = me.convertTicksToLabels(ticks) || me.ticks; - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; + me.afterTickToLabelConversion(); + + me.ticks = labels; // BACKWARD COMPATIBILITY + + // IMPORTANT: from this point, we consider that `this.ticks` will NEVER change! + + // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`) + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + tick = ticks[i]; + if (!tick) { + ticks.push(tick = { + label: label, + major: false + }); + } else { + tick.label = label; } + } - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; - }, - afterSetDimensions: function() { - helpers.callback(this.options.afterSetDimensions, [this]); - }, - - // Data limits - beforeDataLimits: function() { - helpers.callback(this.options.beforeDataLimits, [this]); - }, - determineDataLimits: helpers.noop, - afterDataLimits: function() { - helpers.callback(this.options.afterDataLimits, [this]); - }, + me._ticks = ticks; + // Tick Rotation + me.beforeCalculateTickRotation(); + me.calculateTickRotation(); + me.afterCalculateTickRotation(); + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // - beforeBuildTicks: function() { - helpers.callback(this.options.beforeBuildTicks, [this]); - }, - buildTicks: helpers.noop, - afterBuildTicks: function() { - helpers.callback(this.options.afterBuildTicks, [this]); - }, - - beforeTickToLabelConversion: function() { - helpers.callback(this.options.beforeTickToLabelConversion, [this]); - }, - convertTicksToLabels: function() { - var me = this; - // Convert ticks to strings - var tickOpts = me.options.ticks; - me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback, this); - }, - afterTickToLabelConversion: function() { - helpers.callback(this.options.afterTickToLabelConversion, [this]); - }, + me.afterUpdate(); - // + return me.minSize; - beforeCalculateTickRotation: function() { - helpers.callback(this.options.beforeCalculateTickRotation, [this]); - }, - calculateTickRotation: function() { - var me = this; - var context = me.ctx; - var tickOpts = me.options.ticks; - var labels = labelsFromTicks(me._ticks); - - // Get the width of each grid by calculating the difference - // between x offsets between 0 and 1. - var tickFont = parseFontOptions(tickOpts); - context.font = tickFont.font; - - var labelRotation = tickOpts.minRotation || 0; - - if (labels.length && me.options.display && me.isHorizontal()) { - var originalLabelWidth = helpers.longestText(context, tickFont.font, labels, me.longestTextCache); - var labelWidth = originalLabelWidth; - var cosRotation, sinRotation; - - // Allow 3 pixels x2 padding either side for label readability - var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; - - // Max label rotation can be set or default to 90 - also act as a loop counter - while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { - var angleRadians = helpers.toRadians(labelRotation); - cosRotation = Math.cos(angleRadians); - sinRotation = Math.sin(angleRadians); - - if (sinRotation * originalLabelWidth > me.maxHeight) { - // go back one step - labelRotation--; - break; - } + }, + afterUpdate: function() { + helpers.callback(this.options.afterUpdate, [this]); + }, + + // + + beforeSetDimensions: function() { + helpers.callback(this.options.beforeSetDimensions, [this]); + }, + 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 position before calculating rotation + me.top = 0; + me.bottom = me.height; + } - labelRotation++; - labelWidth = cosRotation * originalLabelWidth; + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + }, + afterSetDimensions: function() { + helpers.callback(this.options.afterSetDimensions, [this]); + }, + + // Data limits + beforeDataLimits: function() { + helpers.callback(this.options.beforeDataLimits, [this]); + }, + determineDataLimits: helpers.noop, + afterDataLimits: function() { + helpers.callback(this.options.afterDataLimits, [this]); + }, + + // + beforeBuildTicks: function() { + helpers.callback(this.options.beforeBuildTicks, [this]); + }, + buildTicks: helpers.noop, + afterBuildTicks: function() { + helpers.callback(this.options.afterBuildTicks, [this]); + }, + + beforeTickToLabelConversion: function() { + helpers.callback(this.options.beforeTickToLabelConversion, [this]); + }, + convertTicksToLabels: function() { + var me = this; + // Convert ticks to strings + var tickOpts = me.options.ticks; + me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback, this); + }, + afterTickToLabelConversion: function() { + helpers.callback(this.options.afterTickToLabelConversion, [this]); + }, + + // + + beforeCalculateTickRotation: function() { + helpers.callback(this.options.beforeCalculateTickRotation, [this]); + }, + calculateTickRotation: function() { + var me = this; + var context = me.ctx; + var tickOpts = me.options.ticks; + var labels = labelsFromTicks(me._ticks); + + // Get the width of each grid by calculating the difference + // between x offsets between 0 and 1. + var tickFont = parseFontOptions(tickOpts); + context.font = tickFont.font; + + var labelRotation = tickOpts.minRotation || 0; + + if (labels.length && me.options.display && me.isHorizontal()) { + var originalLabelWidth = helpers.longestText(context, tickFont.font, labels, me.longestTextCache); + var labelWidth = originalLabelWidth; + var cosRotation, sinRotation; + + // Allow 3 pixels x2 padding either side for label readability + var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; + + // Max label rotation can be set or default to 90 - also act as a loop counter + while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { + var angleRadians = helpers.toRadians(labelRotation); + cosRotation = Math.cos(angleRadians); + sinRotation = Math.sin(angleRadians); + + if (sinRotation * originalLabelWidth > me.maxHeight) { + // go back one step + labelRotation--; + break; } + + labelRotation++; + labelWidth = cosRotation * originalLabelWidth; } + } - me.labelRotation = labelRotation; - }, - afterCalculateTickRotation: function() { - helpers.callback(this.options.afterCalculateTickRotation, [this]); - }, + me.labelRotation = labelRotation; + }, + afterCalculateTickRotation: function() { + helpers.callback(this.options.afterCalculateTickRotation, [this]); + }, - // + // - beforeFit: function() { - helpers.callback(this.options.beforeFit, [this]); - }, - fit: function() { - var me = this; - // Reset - var minSize = me.minSize = { - width: 0, - height: 0 - }; + beforeFit: function() { + helpers.callback(this.options.beforeFit, [this]); + }, + fit: function() { + var me = this; + // Reset + var minSize = me.minSize = { + width: 0, + height: 0 + }; - var labels = labelsFromTicks(me._ticks); + var labels = labelsFromTicks(me._ticks); - var opts = me.options; - var tickOpts = opts.ticks; - var scaleLabelOpts = opts.scaleLabel; - var gridLineOpts = opts.gridLines; - var display = opts.display; - var isHorizontal = me.isHorizontal(); + var opts = me.options; + var tickOpts = opts.ticks; + var scaleLabelOpts = opts.scaleLabel; + var gridLineOpts = opts.gridLines; + var display = opts.display; + var isHorizontal = me.isHorizontal(); - var tickFont = parseFontOptions(tickOpts); - var tickMarkLength = opts.gridLines.tickMarkLength; + var tickFont = parseFontOptions(tickOpts); + var tickMarkLength = opts.gridLines.tickMarkLength; - // Width - if (isHorizontal) { - // subtract the margins to line up with the chartArea if we are a full width scale - minSize.width = me.isFullWidth() ? me.maxWidth - me.margins.left - me.margins.right : me.maxWidth; - } else { - minSize.width = display && gridLineOpts.drawTicks ? tickMarkLength : 0; - } + // Width + if (isHorizontal) { + // subtract the margins to line up with the chartArea if we are a full width scale + minSize.width = me.isFullWidth() ? me.maxWidth - me.margins.left - me.margins.right : me.maxWidth; + } else { + minSize.width = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + } + + // height + if (isHorizontal) { + minSize.height = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + } else { + minSize.height = me.maxHeight; // fill all the height + } + + // Are we showing a title for the scale? + if (scaleLabelOpts.display && display) { + var scaleLabelLineHeight = parseLineHeight(scaleLabelOpts); + var scaleLabelPadding = helpers.options.toPadding(scaleLabelOpts.padding); + var deltaHeight = scaleLabelLineHeight + scaleLabelPadding.height; - // height if (isHorizontal) { - minSize.height = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + minSize.height += deltaHeight; } else { - minSize.height = me.maxHeight; // fill all the height + minSize.width += deltaHeight; } + } - // Are we showing a title for the scale? - if (scaleLabelOpts.display && display) { - var scaleLabelLineHeight = parseLineHeight(scaleLabelOpts); - var scaleLabelPadding = helpers.options.toPadding(scaleLabelOpts.padding); - var deltaHeight = scaleLabelLineHeight + scaleLabelPadding.height; + // Don't bother fitting the ticks if we are not showing them + if (tickOpts.display && display) { + var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, labels, me.longestTextCache); + var tallestLabelHeightInLines = helpers.numberOfLabelLines(labels); + var lineSpace = tickFont.size * 0.5; + var tickPadding = me.options.ticks.padding; - if (isHorizontal) { - minSize.height += deltaHeight; + if (isHorizontal) { + // A horizontal axis is more constrained by the height. + me.longestLabelWidth = largestTextWidth; + + var angleRadians = helpers.toRadians(me.labelRotation); + var cosRotation = Math.cos(angleRadians); + var sinRotation = Math.sin(angleRadians); + + // TODO - improve this calculation + var labelHeight = (sinRotation * largestTextWidth) + + (tickFont.size * tallestLabelHeightInLines) + + (lineSpace * (tallestLabelHeightInLines - 1)) + + lineSpace; // padding + + minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); + + me.ctx.font = tickFont.font; + var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.font); + var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.font); + + // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned + // which means that the right padding is dominated by the font height + if (me.labelRotation !== 0) { + me.paddingLeft = opts.position === 'bottom' ? (cosRotation * firstLabelWidth) + 3 : (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges + me.paddingRight = opts.position === 'bottom' ? (cosRotation * lineSpace) + 3 : (cosRotation * lastLabelWidth) + 3; } else { - minSize.width += deltaHeight; + me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges + me.paddingRight = lastLabelWidth / 2 + 3; } - } - - // Don't bother fitting the ticks if we are not showing them - if (tickOpts.display && display) { - var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, labels, me.longestTextCache); - var tallestLabelHeightInLines = helpers.numberOfLabelLines(labels); - var lineSpace = tickFont.size * 0.5; - var tickPadding = me.options.ticks.padding; - - if (isHorizontal) { - // A horizontal axis is more constrained by the height. - me.longestLabelWidth = largestTextWidth; - - var angleRadians = helpers.toRadians(me.labelRotation); - var cosRotation = Math.cos(angleRadians); - var sinRotation = Math.sin(angleRadians); - - // TODO - improve this calculation - var labelHeight = (sinRotation * largestTextWidth) - + (tickFont.size * tallestLabelHeightInLines) - + (lineSpace * (tallestLabelHeightInLines - 1)) - + lineSpace; // padding - - minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); - - me.ctx.font = tickFont.font; - var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.font); - var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.font); - - // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned - // which means that the right padding is dominated by the font height - if (me.labelRotation !== 0) { - me.paddingLeft = opts.position === 'bottom' ? (cosRotation * firstLabelWidth) + 3 : (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges - me.paddingRight = opts.position === 'bottom' ? (cosRotation * lineSpace) + 3 : (cosRotation * lastLabelWidth) + 3; - } else { - me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - me.paddingRight = lastLabelWidth / 2 + 3; - } + } else { + // A vertical axis is more constrained by the width. Labels are the + // dominant factor here, so get that length first and account for padding + if (tickOpts.mirror) { + largestTextWidth = 0; } else { - // A vertical axis is more constrained by the width. Labels are the - // dominant factor here, so get that length first and account for padding - if (tickOpts.mirror) { - largestTextWidth = 0; - } else { - // use lineSpace for consistency with horizontal axis - // tickPadding is not implemented for horizontal - largestTextWidth += tickPadding + lineSpace; - } + // use lineSpace for consistency with horizontal axis + // tickPadding is not implemented for horizontal + largestTextWidth += tickPadding + lineSpace; + } - minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); + minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); - me.paddingTop = tickFont.size / 2; - me.paddingBottom = tickFont.size / 2; - } + me.paddingTop = tickFont.size / 2; + me.paddingBottom = tickFont.size / 2; } + } - me.handleMargins(); - - me.width = minSize.width; - me.height = minSize.height; - }, - - /** - * Handle margins and padding interactions - * @private - */ - handleMargins: function() { - var me = this; - if (me.margins) { - me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); - me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); - me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); - me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); - } - }, - - afterFit: function() { - helpers.callback(this.options.afterFit, [this]); - }, - - // Shared Methods - isHorizontal: function() { - return this.options.position === 'top' || this.options.position === 'bottom'; - }, - isFullWidth: function() { - return (this.options.fullWidth); - }, - - // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not - getRightValue: function(rawValue) { - // Null and undefined values first - if (helpers.isNullOrUndef(rawValue)) { - return NaN; - } - // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values - if (typeof rawValue === 'number' && !isFinite(rawValue)) { - return NaN; - } - // If it is in fact an object, dive in one more level - if (rawValue) { - if (this.isHorizontal()) { - if (rawValue.x !== undefined) { - return this.getRightValue(rawValue.x); - } - } else if (rawValue.y !== undefined) { - return this.getRightValue(rawValue.y); - } - } + me.handleMargins(); - // Value is good, return it - return rawValue; - }, - - /** - * Used to get the value to display in the tooltip for the data at the given index - * @param index - * @param datasetIndex - */ - getLabelForIndex: helpers.noop, - - /** - * Returns the location of the given data point. Value can either be an index or a numerical value - * The coordinate (0, 0) is at the upper-left corner of the canvas - * @param value - * @param index - * @param datasetIndex - */ - getPixelForValue: helpers.noop, - - /** - * Used to get the data value from a given pixel. This is the inverse of getPixelForValue - * The coordinate (0, 0) is at the upper-left corner of the canvas - * @param pixel - */ - getValueForPixel: helpers.noop, - - /** - * Returns the location of the tick at the given index - * The coordinate (0, 0) is at the upper-left corner of the canvas - */ - getPixelForTick: function(index) { - var me = this; - var offset = me.options.offset; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var tickWidth = innerWidth / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); - var pixel = (tickWidth * index) + me.paddingLeft; - - if (offset) { - pixel += tickWidth / 2; - } + me.width = minSize.width; + me.height = minSize.height; + }, - var finalVal = me.left + Math.round(pixel); - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; - } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - return me.top + (index * (innerHeight / (me._ticks.length - 1))); - }, - - /** - * Utility for getting the pixel location of a percentage of scale - * The coordinate (0, 0) is at the upper-left corner of the canvas - */ - getPixelForDecimal: function(decimal) { - var me = this; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueOffset = (innerWidth * decimal) + me.paddingLeft; - - var finalVal = me.left + Math.round(valueOffset); - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; - } - return me.top + (decimal * me.height); - }, - - /** - * Returns the pixel for the minimum chart value - * The coordinate (0, 0) is at the upper-left corner of the canvas - */ - getBasePixel: function() { - return this.getPixelForValue(this.getBaseValue()); - }, - - getBaseValue: function() { - var me = this; - var min = me.min; - var max = me.max; - - return me.beginAtZero ? 0 : - min < 0 && max < 0 ? max : - min > 0 && max > 0 ? min : - 0; - }, - - /** - * Returns a subset of ticks to be plotted to avoid overlapping labels. - * @private - */ - _autoSkip: function(ticks) { - var skipRatio; - var me = this; - var isHorizontal = me.isHorizontal(); - var optionTicks = me.options.ticks.minor; - var tickCount = ticks.length; - var labelRotationRadians = helpers.toRadians(me.labelRotation); - var cosRotation = Math.cos(labelRotationRadians); - var longestRotatedLabel = me.longestLabelWidth * cosRotation; - var result = []; - var i, tick, shouldSkip; - - // figure out the maximum number of gridlines to show - var maxTicks; - if (optionTicks.maxTicksLimit) { - maxTicks = optionTicks.maxTicksLimit; - } + /** + * Handle margins and padding interactions + * @private + */ + handleMargins: function() { + var me = this; + if (me.margins) { + me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); + me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); + me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); + me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); + } + }, - if (isHorizontal) { - skipRatio = false; + afterFit: function() { + helpers.callback(this.options.afterFit, [this]); + }, - if ((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount > (me.width - (me.paddingLeft + me.paddingRight))) { - skipRatio = 1 + Math.floor(((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount) / (me.width - (me.paddingLeft + me.paddingRight))); - } + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, + isFullWidth: function() { + return (this.options.fullWidth); + }, - // if they defined a max number of optionTicks, - // increase skipRatio until that number is met - if (maxTicks && tickCount > maxTicks) { - skipRatio = Math.max(skipRatio, Math.floor(tickCount / maxTicks)); + // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not + getRightValue: function(rawValue) { + // Null and undefined values first + if (helpers.isNullOrUndef(rawValue)) { + return NaN; + } + // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values + if (typeof rawValue === 'number' && !isFinite(rawValue)) { + return NaN; + } + // If it is in fact an object, dive in one more level + if (rawValue) { + if (this.isHorizontal()) { + if (rawValue.x !== undefined) { + return this.getRightValue(rawValue.x); } + } else if (rawValue.y !== undefined) { + return this.getRightValue(rawValue.y); } + } - for (i = 0; i < tickCount; i++) { - tick = ticks[i]; + // Value is good, return it + return rawValue; + }, - // Since we always show the last tick,we need may need to hide the last shown one before - shouldSkip = (skipRatio > 1 && i % skipRatio > 0) || (i % skipRatio === 0 && i + skipRatio >= tickCount); - if (shouldSkip && i !== tickCount - 1) { - // leave tick in place but make sure it's not displayed (#4635) - delete tick.label; - } - result.push(tick); - } - return result; - }, - - // Actually draw the scale on the canvas - // @param {rectangle} chartArea : the area of the chart to draw full grid lines on - draw: function(chartArea) { - var me = this; - var options = me.options; - if (!options.display) { - return; + /** + * Used to get the value to display in the tooltip for the data at the given index + * @param index + * @param datasetIndex + */ + getLabelForIndex: helpers.noop, + + /** + * Returns the location of the given data point. Value can either be an index or a numerical value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param value + * @param index + * @param datasetIndex + */ + getPixelForValue: helpers.noop, + + /** + * Used to get the data value from a given pixel. This is the inverse of getPixelForValue + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param pixel + */ + getValueForPixel: helpers.noop, + + /** + * Returns the location of the tick at the given index + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getPixelForTick: function(index) { + var me = this; + var offset = me.options.offset; + if (me.isHorizontal()) { + var innerWidth = me.width - (me.paddingLeft + me.paddingRight); + var tickWidth = innerWidth / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); + var pixel = (tickWidth * index) + me.paddingLeft; + + if (offset) { + pixel += tickWidth / 2; } - var context = me.ctx; - var globalDefaults = defaults.global; - var optionTicks = options.ticks.minor; - var optionMajorTicks = options.ticks.major || optionTicks; - var gridLines = options.gridLines; - var scaleLabel = options.scaleLabel; + var finalVal = me.left + Math.round(pixel); + finalVal += me.isFullWidth() ? me.margins.left : 0; + return finalVal; + } + var innerHeight = me.height - (me.paddingTop + me.paddingBottom); + return me.top + (index * (innerHeight / (me._ticks.length - 1))); + }, + + /** + * Utility for getting the pixel location of a percentage of scale + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getPixelForDecimal: function(decimal) { + var me = this; + if (me.isHorizontal()) { + var innerWidth = me.width - (me.paddingLeft + me.paddingRight); + var valueOffset = (innerWidth * decimal) + me.paddingLeft; + + var finalVal = me.left + Math.round(valueOffset); + finalVal += me.isFullWidth() ? me.margins.left : 0; + return finalVal; + } + return me.top + (decimal * me.height); + }, - var isRotated = me.labelRotation !== 0; - var isHorizontal = me.isHorizontal(); + /** + * Returns the pixel for the minimum chart value + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getBasePixel: function() { + return this.getPixelForValue(this.getBaseValue()); + }, - var ticks = optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); - var tickFontColor = helpers.valueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); - var tickFont = parseFontOptions(optionTicks); - var majorTickFontColor = helpers.valueOrDefault(optionMajorTicks.fontColor, globalDefaults.defaultFontColor); - var majorTickFont = parseFontOptions(optionMajorTicks); + getBaseValue: function() { + var me = this; + var min = me.min; + var max = me.max; - var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; + return me.beginAtZero ? 0 : + min < 0 && max < 0 ? max : + min > 0 && max > 0 ? min : + 0; + }, - var scaleLabelFontColor = helpers.valueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); - var scaleLabelFont = parseFontOptions(scaleLabel); - var scaleLabelPadding = helpers.options.toPadding(scaleLabel.padding); - var labelRotationRadians = helpers.toRadians(me.labelRotation); + /** + * Returns a subset of ticks to be plotted to avoid overlapping labels. + * @private + */ + _autoSkip: function(ticks) { + var skipRatio; + var me = this; + var isHorizontal = me.isHorizontal(); + var optionTicks = me.options.ticks.minor; + var tickCount = ticks.length; + var labelRotationRadians = helpers.toRadians(me.labelRotation); + var cosRotation = Math.cos(labelRotationRadians); + var longestRotatedLabel = me.longestLabelWidth * cosRotation; + var result = []; + var i, tick, shouldSkip; + + // figure out the maximum number of gridlines to show + var maxTicks; + if (optionTicks.maxTicksLimit) { + maxTicks = optionTicks.maxTicksLimit; + } - var itemsToDraw = []; + if (isHorizontal) { + skipRatio = false; - 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; + if ((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount > (me.width - (me.paddingLeft + me.paddingRight))) { + skipRatio = 1 + Math.floor(((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount) / (me.width - (me.paddingLeft + me.paddingRight))); + } - helpers.each(ticks, function(tick, index) { - // autoskipper skipped this tick (#4635) - if (helpers.isNullOrUndef(tick.label)) { - return; - } + // if they defined a max number of optionTicks, + // increase skipRatio until that number is met + if (maxTicks && tickCount > maxTicks) { + skipRatio = Math.max(skipRatio, Math.floor(tickCount / maxTicks)); + } + } - var label = tick.label; - var lineWidth, lineColor, borderDash, borderDashOffset; - if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) { - // Draw the first index specially - lineWidth = gridLines.zeroLineWidth; - lineColor = gridLines.zeroLineColor; - borderDash = gridLines.zeroLineBorderDash; - borderDashOffset = gridLines.zeroLineBorderDashOffset; - } else { - lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, index); - lineColor = helpers.valueAtIndexOrDefault(gridLines.color, index); - borderDash = helpers.valueOrDefault(gridLines.borderDash, globalDefaults.borderDash); - borderDashOffset = helpers.valueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); - } + for (i = 0; i < tickCount; i++) { + tick = ticks[i]; - // Common properties - var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY; - var textAlign = 'middle'; - var textBaseline = 'middle'; - var tickPadding = optionTicks.padding; - - if (isHorizontal) { - var labelYOffset = tl + tickPadding; - - if (options.position === 'bottom') { - // bottom - textBaseline = !isRotated ? 'top' : 'middle'; - textAlign = !isRotated ? 'center' : 'right'; - labelY = me.top + labelYOffset; - } else { - // top - textBaseline = !isRotated ? 'bottom' : 'middle'; - textAlign = !isRotated ? 'center' : 'left'; - labelY = me.bottom - labelYOffset; - } + // Since we always show the last tick,we need may need to hide the last shown one before + shouldSkip = (skipRatio > 1 && i % skipRatio > 0) || (i % skipRatio === 0 && i + skipRatio >= tickCount); + if (shouldSkip && i !== tickCount - 1) { + // leave tick in place but make sure it's not displayed (#4635) + delete tick.label; + } + result.push(tick); + } + return result; + }, - var xLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); - if (xLineValue < me.left) { - lineColor = 'rgba(0,0,0,0)'; - } - xLineValue += helpers.aliasPixel(lineWidth); + // Actually draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + var me = this; + var options = me.options; + if (!options.display) { + return; + } + + var context = me.ctx; + var globalDefaults = defaults.global; + var optionTicks = options.ticks.minor; + var optionMajorTicks = options.ticks.major || optionTicks; + var gridLines = options.gridLines; + var scaleLabel = options.scaleLabel; + + var isRotated = me.labelRotation !== 0; + var isHorizontal = me.isHorizontal(); + + var ticks = optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); + var tickFontColor = helpers.valueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); + var tickFont = parseFontOptions(optionTicks); + var majorTickFontColor = helpers.valueOrDefault(optionMajorTicks.fontColor, globalDefaults.defaultFontColor); + var majorTickFont = parseFontOptions(optionMajorTicks); + + var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; + + var scaleLabelFontColor = helpers.valueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); + var scaleLabelFont = parseFontOptions(scaleLabel); + var scaleLabelPadding = helpers.options.toPadding(scaleLabel.padding); + var labelRotationRadians = helpers.toRadians(me.labelRotation); + + var itemsToDraw = []; + + var axisWidth = me.options.gridLines.lineWidth; + var xTickStart = options.position === 'right' ? me.left : me.right - axisWidth - tl; + var xTickEnd = options.position === 'right' ? me.left + 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) + if (helpers.isNullOrUndef(tick.label)) { + return; + } + + var label = tick.label; + var lineWidth, lineColor, borderDash, borderDashOffset; + if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) { + // Draw the first index specially + lineWidth = gridLines.zeroLineWidth; + lineColor = gridLines.zeroLineColor; + borderDash = gridLines.zeroLineBorderDash; + borderDashOffset = gridLines.zeroLineBorderDashOffset; + } else { + lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, index); + lineColor = helpers.valueAtIndexOrDefault(gridLines.color, index); + borderDash = helpers.valueOrDefault(gridLines.borderDash, globalDefaults.borderDash); + borderDashOffset = helpers.valueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); + } + + // Common properties + var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY; + var textAlign = 'middle'; + var textBaseline = 'middle'; + var tickPadding = optionTicks.padding; - labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) + if (isHorizontal) { + var labelYOffset = tl + tickPadding; - tx1 = tx2 = x1 = x2 = xLineValue; - ty1 = yTickStart; - ty2 = yTickEnd; - y1 = chartArea.top; - y2 = chartArea.bottom; + if (options.position === 'bottom') { + // bottom + textBaseline = !isRotated ? 'top' : 'middle'; + textAlign = !isRotated ? 'center' : 'right'; + labelY = me.top + labelYOffset; } else { - var isLeft = options.position === 'left'; - var labelXOffset; - - if (optionTicks.mirror) { - textAlign = isLeft ? 'left' : 'right'; - labelXOffset = tickPadding; - } else { - textAlign = isLeft ? 'right' : 'left'; - labelXOffset = tl + tickPadding; - } + // top + textBaseline = !isRotated ? 'bottom' : 'middle'; + textAlign = !isRotated ? 'center' : 'left'; + labelY = me.bottom - labelYOffset; + } - labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; + var xLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); + if (xLineValue < me.left) { + lineColor = 'rgba(0,0,0,0)'; + } + xLineValue += helpers.aliasPixel(lineWidth); - var yLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); - if (yLineValue < me.top) { - lineColor = 'rgba(0,0,0,0)'; - } - yLineValue += helpers.aliasPixel(lineWidth); + labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - labelY = me.getPixelForTick(index) + optionTicks.labelOffset; + tx1 = tx2 = x1 = x2 = xLineValue; + ty1 = yTickStart; + ty2 = yTickEnd; + y1 = chartArea.top; + y2 = chartArea.bottom + axisWidth; + } else { + var isLeft = options.position === 'left'; + var labelXOffset; - tx1 = xTickStart; - tx2 = xTickEnd; - x1 = chartArea.left; - x2 = chartArea.right; - ty1 = ty2 = y1 = y2 = yLineValue; + if (optionTicks.mirror) { + textAlign = isLeft ? 'left' : 'right'; + labelXOffset = tickPadding; + } else { + textAlign = isLeft ? 'right' : 'left'; + labelXOffset = tl + tickPadding; } - itemsToDraw.push({ - tx1: tx1, - ty1: ty1, - tx2: tx2, - ty2: ty2, - x1: x1, - y1: y1, - x2: x2, - y2: y2, - labelX: labelX, - labelY: labelY, - glWidth: lineWidth, - glColor: lineColor, - glBorderDash: borderDash, - glBorderDashOffset: borderDashOffset, - rotation: -1 * labelRotationRadians, - label: label, - major: tick.major, - textBaseline: textBaseline, - textAlign: textAlign - }); - }); + labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; - // Draw all of the tick labels, tick marks, and grid lines at the correct places - helpers.each(itemsToDraw, function(itemToDraw) { - if (gridLines.display) { - context.save(); - context.lineWidth = itemToDraw.glWidth; - context.strokeStyle = itemToDraw.glColor; - if (context.setLineDash) { - context.setLineDash(itemToDraw.glBorderDash); - context.lineDashOffset = itemToDraw.glBorderDashOffset; - } + var yLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); + if (yLineValue < me.top) { + lineColor = 'rgba(0,0,0,0)'; + } + yLineValue += helpers.aliasPixel(lineWidth); - context.beginPath(); + labelY = me.getPixelForTick(index) + optionTicks.labelOffset; - if (gridLines.drawTicks) { - context.moveTo(itemToDraw.tx1, itemToDraw.ty1); - context.lineTo(itemToDraw.tx2, itemToDraw.ty2); - } + tx1 = xTickStart; + tx2 = xTickEnd; + x1 = chartArea.left; + x2 = chartArea.right + axisWidth; + ty1 = ty2 = y1 = y2 = yLineValue; + } - if (gridLines.drawOnChartArea) { - context.moveTo(itemToDraw.x1, itemToDraw.y1); - context.lineTo(itemToDraw.x2, itemToDraw.y2); - } + itemsToDraw.push({ + tx1: tx1, + ty1: ty1, + tx2: tx2, + ty2: ty2, + x1: x1, + y1: y1, + x2: x2, + y2: y2, + labelX: labelX, + labelY: labelY, + glWidth: lineWidth, + glColor: lineColor, + glBorderDash: borderDash, + glBorderDashOffset: borderDashOffset, + rotation: -1 * labelRotationRadians, + label: label, + major: tick.major, + textBaseline: textBaseline, + textAlign: textAlign + }); + }); - context.stroke(); - context.restore(); + // Draw all of the tick labels, tick marks, and grid lines at the correct places + helpers.each(itemsToDraw, function(itemToDraw) { + if (gridLines.display) { + context.save(); + context.lineWidth = itemToDraw.glWidth; + context.strokeStyle = itemToDraw.glColor; + if (context.setLineDash) { + context.setLineDash(itemToDraw.glBorderDash); + context.lineDashOffset = itemToDraw.glBorderDashOffset; } - if (optionTicks.display) { - // Make sure we draw text in the correct color and font - context.save(); - context.translate(itemToDraw.labelX, itemToDraw.labelY); - context.rotate(itemToDraw.rotation); - context.font = itemToDraw.major ? majorTickFont.font : tickFont.font; - context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; - context.textBaseline = itemToDraw.textBaseline; - context.textAlign = itemToDraw.textAlign; - - var label = itemToDraw.label; - if (helpers.isArray(label)) { - for (var i = 0, y = 0; i < label.length; ++i) { - // We just make sure the multiline element is a string here.. - context.fillText('' + label[i], 0, y); - // apply same lineSpacing as calculated @ L#320 - y += (tickFont.size * 1.5); - } - } else { - context.fillText(label, 0, 0); - } - context.restore(); + context.beginPath(); + + if (gridLines.drawTicks) { + context.moveTo(itemToDraw.tx1, itemToDraw.ty1); + context.lineTo(itemToDraw.tx2, itemToDraw.ty2); } - }); - if (scaleLabel.display) { - // Draw the scale label - var scaleLabelX; - var scaleLabelY; - var rotation = 0; - var halfLineHeight = parseLineHeight(scaleLabel) / 2; - - if (isHorizontal) { - scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' - ? me.bottom - halfLineHeight - scaleLabelPadding.bottom - : me.top + halfLineHeight + scaleLabelPadding.top; - } else { - var isLeft = options.position === 'left'; - scaleLabelX = isLeft - ? me.left + halfLineHeight + scaleLabelPadding.top - : me.right - halfLineHeight - scaleLabelPadding.top; - scaleLabelY = me.top + ((me.bottom - me.top) / 2); - rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; + if (gridLines.drawOnChartArea) { + context.moveTo(itemToDraw.x1, itemToDraw.y1); + context.lineTo(itemToDraw.x2, itemToDraw.y2); } - context.save(); - context.translate(scaleLabelX, scaleLabelY); - context.rotate(rotation); - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.fillStyle = scaleLabelFontColor; // render in correct colour - context.font = scaleLabelFont.font; - context.fillText(scaleLabel.labelString, 0, 0); + context.stroke(); context.restore(); } - if (gridLines.drawBorder) { - // Draw the line at the edge of the axis - context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); - context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); - var x1 = me.left; - var x2 = me.right; - var y1 = me.top; - var y2 = me.bottom; - - var aliasPixel = helpers.aliasPixel(context.lineWidth); - if (isHorizontal) { - y1 = y2 = options.position === 'top' ? me.bottom : me.top; - y1 += aliasPixel; - y2 += aliasPixel; + if (optionTicks.display) { + // Make sure we draw text in the correct color and font + context.save(); + context.translate(itemToDraw.labelX, itemToDraw.labelY); + context.rotate(itemToDraw.rotation); + context.font = itemToDraw.major ? majorTickFont.font : tickFont.font; + context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; + context.textBaseline = itemToDraw.textBaseline; + context.textAlign = itemToDraw.textAlign; + + var label = itemToDraw.label; + if (helpers.isArray(label)) { + 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 += lineHeight; + } } else { - x1 = x2 = options.position === 'left' ? me.right : me.left; - x1 += aliasPixel; - x2 += aliasPixel; + context.fillText(label, 0, 0); } + context.restore(); + } + }); - context.beginPath(); - context.moveTo(x1, y1); - context.lineTo(x2, y2); - context.stroke(); + if (scaleLabel.display) { + // Draw the scale label + var scaleLabelX; + var scaleLabelY; + var rotation = 0; + var halfLineHeight = parseLineHeight(scaleLabel) / 2; + + if (isHorizontal) { + scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width + scaleLabelY = options.position === 'bottom' + ? me.bottom - halfLineHeight - scaleLabelPadding.bottom + : me.top + halfLineHeight + scaleLabelPadding.top; + } else { + var isLeft = options.position === 'left'; + scaleLabelX = isLeft + ? me.left + halfLineHeight + scaleLabelPadding.top + : me.right - halfLineHeight - scaleLabelPadding.top; + scaleLabelY = me.top + ((me.bottom - me.top) / 2); + rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; } + + context.save(); + context.translate(scaleLabelX, scaleLabelY); + context.rotate(rotation); + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = scaleLabelFontColor; // render in correct colour + context.font = scaleLabelFont.font; + context.fillText(scaleLabel.labelString, 0, 0); + context.restore(); } - }); -}; + + if (gridLines.drawBorder) { + // Draw the line at the edge of the axis + context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); + context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); + var x1 = me.left; + var x2 = me.right + axisWidth; + var y1 = me.top; + var y2 = me.bottom + axisWidth; + + var aliasPixel = helpers.aliasPixel(context.lineWidth); + if (isHorizontal) { + y1 = y2 = options.position === 'top' ? me.bottom : me.top; + y1 += aliasPixel; + y2 += aliasPixel; + } else { + x1 = x2 = options.position === 'left' ? me.right : me.left; + x1 += aliasPixel; + x2 += aliasPixel; + } + + context.beginPath(); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.stroke(); + } + } +}); diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index f2ea01d329a..fe945382003 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -4,43 +4,40 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var layouts = require('./core.layouts'); -module.exports = function(Chart) { +module.exports = { + // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then + // use the new chart options to grab the correct scale + constructors: {}, + // Use a registration function so that we can move to an ES6 map when we no longer need to support + // old browsers - Chart.scaleService = { - // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then - // use the new chart options to grab the correct scale - constructors: {}, - // Use a registration function so that we can move to an ES6 map when we no longer need to support - // old browsers - - // Scale config defaults - defaults: {}, - registerScaleType: function(type, scaleConstructor, scaleDefaults) { - this.constructors[type] = scaleConstructor; - this.defaults[type] = helpers.clone(scaleDefaults); - }, - getScaleConstructor: function(type) { - return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; - }, - getScaleDefaults: function(type) { - // Return the scale defaults merged with the global settings so that we always use the latest ones - return this.defaults.hasOwnProperty(type) ? helpers.merge({}, [defaults.scale, this.defaults[type]]) : {}; - }, - updateScaleDefaults: function(type, additions) { - var me = this; - if (me.defaults.hasOwnProperty(type)) { - me.defaults[type] = helpers.extend(me.defaults[type], additions); - } - }, - addScalesToLayout: function(chart) { - // Adds each scale to the chart.boxes array to be sized accordingly - helpers.each(chart.scales, function(scale) { - // Set ILayoutItem parameters for backwards compatibility - scale.fullWidth = scale.options.fullWidth; - scale.position = scale.options.position; - scale.weight = scale.options.weight; - layouts.addBox(chart, scale); - }); + // Scale config defaults + defaults: {}, + registerScaleType: function(type, scaleConstructor, scaleDefaults) { + this.constructors[type] = scaleConstructor; + this.defaults[type] = helpers.clone(scaleDefaults); + }, + getScaleConstructor: function(type) { + return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; + }, + getScaleDefaults: function(type) { + // Return the scale defaults merged with the global settings so that we always use the latest ones + return this.defaults.hasOwnProperty(type) ? helpers.merge({}, [defaults.scale, this.defaults[type]]) : {}; + }, + updateScaleDefaults: function(type, additions) { + var me = this; + if (me.defaults.hasOwnProperty(type)) { + me.defaults[type] = helpers.extend(me.defaults[type], additions); } - }; + }, + addScalesToLayout: function(chart) { + // Adds each scale to the chart.boxes array to be sized accordingly + helpers.each(chart.scales, function(scale) { + // Set ILayoutItem parameters for backwards compatibility + scale.fullWidth = scale.options.fullWidth; + scale.position = scale.options.position; + scale.weight = scale.options.weight; + layouts.addBox(chart, scale); + }); + } }; diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 9b09d760443..3f9490f8546 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -96,853 +96,859 @@ defaults._set('global', { } }); -module.exports = function(Chart) { - +var positioners = { /** - * Helper method to merge the opacity into a color - */ - function mergeOpacity(colorString, opacity) { - var color = helpers.color(colorString); - return color.alpha(opacity * color.alpha()).rgbaString(); - } + * Average mode places the tooltip at the average position of the elements shown + * @function Chart.Tooltip.positioners.average + * @param elements {ChartElement[]} the elements being displayed in the tooltip + * @returns {Point} tooltip position + */ + average: function(elements) { + if (!elements.length) { + return false; + } - // Helper to push or concat based on if the 2nd parameter is an array or not - function pushOrConcat(base, toPush) { - if (toPush) { - if (helpers.isArray(toPush)) { - // base = base.concat(toPush); - Array.prototype.push.apply(base, toPush); - } else { - base.push(toPush); + var i, len; + var x = 0; + var y = 0; + var count = 0; + + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var pos = el.tooltipPosition(); + x += pos.x; + y += pos.y; + ++count; } } - return base; - } - - // Private helper to create a tooltip item model - // @param element : the chart element (point, arc, bar) to create the tooltip item for - // @return : new tooltip item - function createTooltipItem(element) { - var xScale = element._xScale; - var yScale = element._yScale || element._scale; // handle radar || polarArea charts - var index = element._index; - var datasetIndex = element._datasetIndex; - return { - xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '', - yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '', - index: index, - datasetIndex: datasetIndex, - x: element._model.x, - y: element._model.y + x: Math.round(x / count), + y: Math.round(y / count) }; - } + }, /** - * Helper to get the reset model for the tooltip - * @param tooltipOpts {Object} the tooltip options + * Gets the tooltip position nearest of the item nearest to the event position + * @function Chart.Tooltip.positioners.nearest + * @param elements {Chart.Element[]} the tooltip elements + * @param eventPosition {Point} the position of the event in canvas coordinates + * @returns {Point} the tooltip position */ - function getBaseModel(tooltipOpts) { - var globalDefaults = defaults.global; - var valueOrDefault = helpers.valueOrDefault; - - return { - // Positioning - xPadding: tooltipOpts.xPadding, - yPadding: tooltipOpts.yPadding, - xAlign: tooltipOpts.xAlign, - yAlign: tooltipOpts.yAlign, + nearest: function(elements, eventPosition) { + var x = eventPosition.x; + var y = eventPosition.y; + var minDistance = Number.POSITIVE_INFINITY; + var i, len, nearestElement; + + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var center = el.getCenterPoint(); + var d = helpers.distanceBetweenPoints(eventPosition, center); + + if (d < minDistance) { + minDistance = d; + nearestElement = el; + } + } + } - // Body - bodyFontColor: tooltipOpts.bodyFontColor, - _bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), - _bodyFontStyle: valueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), - _bodyAlign: tooltipOpts.bodyAlign, - bodyFontSize: valueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), - bodySpacing: tooltipOpts.bodySpacing, - - // Title - titleFontColor: tooltipOpts.titleFontColor, - _titleFontFamily: valueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), - _titleFontStyle: valueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), - titleFontSize: valueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), - _titleAlign: tooltipOpts.titleAlign, - titleSpacing: tooltipOpts.titleSpacing, - titleMarginBottom: tooltipOpts.titleMarginBottom, + if (nearestElement) { + var tp = nearestElement.tooltipPosition(); + x = tp.x; + y = tp.y; + } - // Footer - footerFontColor: tooltipOpts.footerFontColor, - _footerFontFamily: valueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), - _footerFontStyle: valueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), - footerFontSize: valueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), - _footerAlign: tooltipOpts.footerAlign, - footerSpacing: tooltipOpts.footerSpacing, - footerMarginTop: tooltipOpts.footerMarginTop, - - // Appearance - caretSize: tooltipOpts.caretSize, - cornerRadius: tooltipOpts.cornerRadius, - backgroundColor: tooltipOpts.backgroundColor, - opacity: 0, - legendColorBackground: tooltipOpts.multiKeyBackground, - displayColors: tooltipOpts.displayColors, - borderColor: tooltipOpts.borderColor, - borderWidth: tooltipOpts.borderWidth + return { + x: x, + y: y }; } +}; - /** - * Get the size of the tooltip - */ - function getTooltipSize(tooltip, model) { - var ctx = tooltip._chart.ctx; - - var height = model.yPadding * 2; // Tooltip Padding - var width = 0; - - // Count of all lines in the body - var body = model.body; - var combinedBodyLength = body.reduce(function(count, bodyItem) { - return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length; - }, 0); - combinedBodyLength += model.beforeBody.length + model.afterBody.length; - - var titleLineCount = model.title.length; - var footerLineCount = model.footer.length; - var titleFontSize = model.titleFontSize; - var bodyFontSize = model.bodyFontSize; - var footerFontSize = model.footerFontSize; - - height += titleLineCount * titleFontSize; // Title Lines - height += titleLineCount ? (titleLineCount - 1) * model.titleSpacing : 0; // Title Line Spacing - height += titleLineCount ? model.titleMarginBottom : 0; // Title's bottom Margin - height += combinedBodyLength * bodyFontSize; // Body Lines - height += combinedBodyLength ? (combinedBodyLength - 1) * model.bodySpacing : 0; // Body Line Spacing - height += footerLineCount ? model.footerMarginTop : 0; // Footer Margin - height += footerLineCount * (footerFontSize); // Footer Lines - height += footerLineCount ? (footerLineCount - 1) * model.footerSpacing : 0; // Footer Line Spacing - - // Title width - var widthPadding = 0; - var maxLineWidth = function(line) { - width = Math.max(width, ctx.measureText(line).width + widthPadding); - }; - - ctx.font = helpers.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily); - helpers.each(model.title, maxLineWidth); +/** + * Helper method to merge the opacity into a color + */ +function mergeOpacity(colorString, opacity) { + var color = helpers.color(colorString); + return color.alpha(opacity * color.alpha()).rgbaString(); +} + +// Helper to push or concat based on if the 2nd parameter is an array or not +function pushOrConcat(base, toPush) { + if (toPush) { + if (helpers.isArray(toPush)) { + // base = base.concat(toPush); + Array.prototype.push.apply(base, toPush); + } else { + base.push(toPush); + } + } - // Body width - ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); - helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth); + return base; +} + +// Private helper to create a tooltip item model +// @param element : the chart element (point, arc, bar) to create the tooltip item for +// @return : new tooltip item +function createTooltipItem(element) { + var xScale = element._xScale; + var yScale = element._yScale || element._scale; // handle radar || polarArea charts + var index = element._index; + var datasetIndex = element._datasetIndex; + + return { + xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '', + yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '', + index: index, + datasetIndex: datasetIndex, + x: element._model.x, + y: element._model.y + }; +} + +/** + * Helper to get the reset model for the tooltip + * @param tooltipOpts {Object} the tooltip options + */ +function getBaseModel(tooltipOpts) { + var globalDefaults = defaults.global; + var valueOrDefault = helpers.valueOrDefault; + + return { + // Positioning + xPadding: tooltipOpts.xPadding, + yPadding: tooltipOpts.yPadding, + xAlign: tooltipOpts.xAlign, + yAlign: tooltipOpts.yAlign, + + // Body + bodyFontColor: tooltipOpts.bodyFontColor, + _bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), + _bodyFontStyle: valueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), + _bodyAlign: tooltipOpts.bodyAlign, + bodyFontSize: valueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), + bodySpacing: tooltipOpts.bodySpacing, + + // Title + titleFontColor: tooltipOpts.titleFontColor, + _titleFontFamily: valueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), + _titleFontStyle: valueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), + titleFontSize: valueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), + _titleAlign: tooltipOpts.titleAlign, + titleSpacing: tooltipOpts.titleSpacing, + titleMarginBottom: tooltipOpts.titleMarginBottom, + + // Footer + footerFontColor: tooltipOpts.footerFontColor, + _footerFontFamily: valueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), + _footerFontStyle: valueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), + footerFontSize: valueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), + _footerAlign: tooltipOpts.footerAlign, + footerSpacing: tooltipOpts.footerSpacing, + footerMarginTop: tooltipOpts.footerMarginTop, + + // Appearance + caretSize: tooltipOpts.caretSize, + cornerRadius: tooltipOpts.cornerRadius, + backgroundColor: tooltipOpts.backgroundColor, + opacity: 0, + legendColorBackground: tooltipOpts.multiKeyBackground, + displayColors: tooltipOpts.displayColors, + borderColor: tooltipOpts.borderColor, + borderWidth: tooltipOpts.borderWidth + }; +} + +/** + * Get the size of the tooltip + */ +function getTooltipSize(tooltip, model) { + var ctx = tooltip._chart.ctx; + + var height = model.yPadding * 2; // Tooltip Padding + var width = 0; + + // Count of all lines in the body + var body = model.body; + var combinedBodyLength = body.reduce(function(count, bodyItem) { + return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length; + }, 0); + combinedBodyLength += model.beforeBody.length + model.afterBody.length; + + var titleLineCount = model.title.length; + var footerLineCount = model.footer.length; + var titleFontSize = model.titleFontSize; + var bodyFontSize = model.bodyFontSize; + var footerFontSize = model.footerFontSize; + + height += titleLineCount * titleFontSize; // Title Lines + height += titleLineCount ? (titleLineCount - 1) * model.titleSpacing : 0; // Title Line Spacing + height += titleLineCount ? model.titleMarginBottom : 0; // Title's bottom Margin + height += combinedBodyLength * bodyFontSize; // Body Lines + height += combinedBodyLength ? (combinedBodyLength - 1) * model.bodySpacing : 0; // Body Line Spacing + height += footerLineCount ? model.footerMarginTop : 0; // Footer Margin + height += footerLineCount * (footerFontSize); // Footer Lines + height += footerLineCount ? (footerLineCount - 1) * model.footerSpacing : 0; // Footer Line Spacing + + // Title width + var widthPadding = 0; + var maxLineWidth = function(line) { + width = Math.max(width, ctx.measureText(line).width + widthPadding); + }; - // Body lines may include some extra width due to the color box - widthPadding = model.displayColors ? (bodyFontSize + 2) : 0; - helpers.each(body, function(bodyItem) { - helpers.each(bodyItem.before, maxLineWidth); - helpers.each(bodyItem.lines, maxLineWidth); - helpers.each(bodyItem.after, maxLineWidth); - }); + ctx.font = helpers.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily); + helpers.each(model.title, maxLineWidth); - // Reset back to 0 - widthPadding = 0; + // Body width + ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); + helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth); - // Footer width - ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); - helpers.each(model.footer, maxLineWidth); + // Body lines may include some extra width due to the color box + widthPadding = model.displayColors ? (bodyFontSize + 2) : 0; + helpers.each(body, function(bodyItem) { + helpers.each(bodyItem.before, maxLineWidth); + helpers.each(bodyItem.lines, maxLineWidth); + helpers.each(bodyItem.after, maxLineWidth); + }); - // Add padding - width += 2 * model.xPadding; + // Reset back to 0 + widthPadding = 0; - return { - width: width, - height: height - }; - } + // Footer width + ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); + helpers.each(model.footer, maxLineWidth); - /** - * Helper to get the alignment of a tooltip given the size - */ - function determineAlignment(tooltip, size) { - var model = tooltip._model; - var chart = tooltip._chart; - var chartArea = tooltip._chart.chartArea; - var xAlign = 'center'; - var yAlign = 'center'; - - if (model.y < size.height) { - yAlign = 'top'; - } else if (model.y > (chart.height - size.height)) { - yAlign = 'bottom'; - } + // Add padding + width += 2 * model.xPadding; - var lf, rf; // functions to determine left, right alignment - var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart - var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges - var midX = (chartArea.left + chartArea.right) / 2; - var midY = (chartArea.top + chartArea.bottom) / 2; + return { + width: width, + height: height + }; +} + +/** + * Helper to get the alignment of a tooltip given the size + */ +function determineAlignment(tooltip, size) { + var model = tooltip._model; + var chart = tooltip._chart; + var chartArea = tooltip._chart.chartArea; + var xAlign = 'center'; + var yAlign = 'center'; + + if (model.y < size.height) { + yAlign = 'top'; + } else if (model.y > (chart.height - size.height)) { + yAlign = 'bottom'; + } - if (yAlign === 'center') { - lf = function(x) { - return x <= midX; - }; - rf = function(x) { - return x > midX; - }; - } else { - lf = function(x) { - return x <= (size.width / 2); - }; - rf = function(x) { - return x >= (chart.width - (size.width / 2)); - }; - } + var lf, rf; // functions to determine left, right alignment + var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart + var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges + var midX = (chartArea.left + chartArea.right) / 2; + var midY = (chartArea.top + chartArea.bottom) / 2; - olf = function(x) { - return x + size.width + model.caretSize + model.caretPadding > chart.width; + if (yAlign === 'center') { + lf = function(x) { + return x <= midX; }; - orf = function(x) { - return x - size.width - model.caretSize - model.caretPadding < 0; + rf = function(x) { + return x > midX; }; - yf = function(y) { - return y <= midY ? 'top' : 'bottom'; + } else { + lf = function(x) { + return x <= (size.width / 2); }; + rf = function(x) { + return x >= (chart.width - (size.width / 2)); + }; + } - if (lf(model.x)) { - xAlign = 'left'; + olf = function(x) { + return x + size.width + model.caretSize + model.caretPadding > chart.width; + }; + orf = function(x) { + return x - size.width - model.caretSize - model.caretPadding < 0; + }; + yf = function(y) { + return y <= midY ? 'top' : 'bottom'; + }; - // Is tooltip too wide and goes over the right side of the chart.? - if (olf(model.x)) { - xAlign = 'center'; - yAlign = yf(model.y); - } - } else if (rf(model.x)) { - xAlign = 'right'; + if (lf(model.x)) { + xAlign = 'left'; - // Is tooltip too wide and goes outside left edge of canvas? - if (orf(model.x)) { - xAlign = 'center'; - yAlign = yf(model.y); - } + // Is tooltip too wide and goes over the right side of the chart.? + if (olf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); } + } else if (rf(model.x)) { + xAlign = 'right'; - var opts = tooltip._options; - return { - xAlign: opts.xAlign ? opts.xAlign : xAlign, - yAlign: opts.yAlign ? opts.yAlign : yAlign - }; + // Is tooltip too wide and goes outside left edge of canvas? + if (orf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } } - /** - * @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment - */ - function getBackgroundPoint(vm, size, alignment, chart) { - // Background Position - var x = vm.x; - var y = vm.y; - - var caretSize = vm.caretSize; - var caretPadding = vm.caretPadding; - var cornerRadius = vm.cornerRadius; - var xAlign = alignment.xAlign; - var yAlign = alignment.yAlign; - var paddingAndSize = caretSize + caretPadding; - var radiusAndPadding = cornerRadius + caretPadding; - - if (xAlign === 'right') { - x -= size.width; - } else if (xAlign === 'center') { - x -= (size.width / 2); - if (x + size.width > chart.width) { - x = chart.width - size.width; - } - if (x < 0) { - x = 0; - } + var opts = tooltip._options; + return { + xAlign: opts.xAlign ? opts.xAlign : xAlign, + yAlign: opts.yAlign ? opts.yAlign : yAlign + }; +} + +/** + * @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment + */ +function getBackgroundPoint(vm, size, alignment, chart) { + // Background Position + var x = vm.x; + var y = vm.y; + + var caretSize = vm.caretSize; + var caretPadding = vm.caretPadding; + var cornerRadius = vm.cornerRadius; + var xAlign = alignment.xAlign; + var yAlign = alignment.yAlign; + var paddingAndSize = caretSize + caretPadding; + var radiusAndPadding = cornerRadius + caretPadding; + + if (xAlign === 'right') { + x -= size.width; + } else if (xAlign === 'center') { + x -= (size.width / 2); + if (x + size.width > chart.width) { + x = chart.width - size.width; } - - if (yAlign === 'top') { - y += paddingAndSize; - } else if (yAlign === 'bottom') { - y -= size.height + paddingAndSize; - } else { - y -= (size.height / 2); + if (x < 0) { + x = 0; } + } - if (yAlign === 'center') { - if (xAlign === 'left') { - x += paddingAndSize; - } else if (xAlign === 'right') { - x -= paddingAndSize; - } - } else if (xAlign === 'left') { - x -= radiusAndPadding; + if (yAlign === 'top') { + y += paddingAndSize; + } else if (yAlign === 'bottom') { + y -= size.height + paddingAndSize; + } else { + y -= (size.height / 2); + } + + if (yAlign === 'center') { + if (xAlign === 'left') { + x += paddingAndSize; } else if (xAlign === 'right') { - x += radiusAndPadding; + x -= paddingAndSize; } - - return { - x: x, - y: y - }; + } else if (xAlign === 'left') { + x -= radiusAndPadding; + } else if (xAlign === 'right') { + x += radiusAndPadding; } - Chart.Tooltip = Element.extend({ - initialize: function() { - this._model = getBaseModel(this._options); - this._lastActive = []; - }, - - // Get the title - // Args are: (tooltipItem, data) - getTitle: function() { - var me = this; - var opts = me._options; - var callbacks = opts.callbacks; - - var beforeTitle = callbacks.beforeTitle.apply(me, arguments); - var title = callbacks.title.apply(me, arguments); - var afterTitle = callbacks.afterTitle.apply(me, arguments); - - var lines = []; - lines = pushOrConcat(lines, beforeTitle); - lines = pushOrConcat(lines, title); - lines = pushOrConcat(lines, afterTitle); - - return lines; - }, - - // Args are: (tooltipItem, data) - getBeforeBody: function() { - var lines = this._options.callbacks.beforeBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; - }, - - // Args are: (tooltipItem, data) - getBody: function(tooltipItems, data) { - var me = this; - var callbacks = me._options.callbacks; - var bodyItems = []; + return { + x: x, + y: y + }; +} + +var exports = module.exports = Element.extend({ + initialize: function() { + this._model = getBaseModel(this._options); + this._lastActive = []; + }, + + // Get the title + // Args are: (tooltipItem, data) + getTitle: function() { + var me = this; + var opts = me._options; + var callbacks = opts.callbacks; + + var beforeTitle = callbacks.beforeTitle.apply(me, arguments); + var title = callbacks.title.apply(me, arguments); + var afterTitle = callbacks.afterTitle.apply(me, arguments); + + var lines = []; + lines = pushOrConcat(lines, beforeTitle); + lines = pushOrConcat(lines, title); + lines = pushOrConcat(lines, afterTitle); + + return lines; + }, + + // Args are: (tooltipItem, data) + getBeforeBody: function() { + var lines = this._options.callbacks.beforeBody.apply(this, arguments); + return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; + }, + + // Args are: (tooltipItem, data) + getBody: function(tooltipItems, data) { + var me = this; + var callbacks = me._options.callbacks; + var bodyItems = []; + + helpers.each(tooltipItems, function(tooltipItem) { + var bodyItem = { + before: [], + lines: [], + after: [] + }; + pushOrConcat(bodyItem.before, callbacks.beforeLabel.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.after, callbacks.afterLabel.call(me, tooltipItem, data)); - helpers.each(tooltipItems, function(tooltipItem) { - var bodyItem = { - before: [], - lines: [], - after: [] - }; - pushOrConcat(bodyItem.before, callbacks.beforeLabel.call(me, tooltipItem, data)); - pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data)); - pushOrConcat(bodyItem.after, callbacks.afterLabel.call(me, tooltipItem, data)); + bodyItems.push(bodyItem); + }); - bodyItems.push(bodyItem); - }); + return bodyItems; + }, + + // Args are: (tooltipItem, data) + getAfterBody: function() { + var lines = this._options.callbacks.afterBody.apply(this, arguments); + return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; + }, + + // Get the footer and beforeFooter and afterFooter lines + // Args are: (tooltipItem, data) + getFooter: function() { + var me = this; + var callbacks = me._options.callbacks; + + var beforeFooter = callbacks.beforeFooter.apply(me, arguments); + var footer = callbacks.footer.apply(me, arguments); + var afterFooter = callbacks.afterFooter.apply(me, arguments); + + var lines = []; + lines = pushOrConcat(lines, beforeFooter); + lines = pushOrConcat(lines, footer); + lines = pushOrConcat(lines, afterFooter); + + return lines; + }, + + update: function(changed) { + var me = this; + var opts = me._options; + + // Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition + // that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time + // which breaks any animations. + var existingModel = me._model; + var model = me._model = getBaseModel(opts); + var active = me._active; + + var data = me._data; + + // In the case where active.length === 0 we need to keep these at existing values for good animations + var alignment = { + xAlign: existingModel.xAlign, + yAlign: existingModel.yAlign + }; + var backgroundPoint = { + x: existingModel.x, + y: existingModel.y + }; + var tooltipSize = { + width: existingModel.width, + height: existingModel.height + }; + var tooltipPosition = { + x: existingModel.caretX, + y: existingModel.caretY + }; - return bodyItems; - }, - - // Args are: (tooltipItem, data) - getAfterBody: function() { - var lines = this._options.callbacks.afterBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; - }, - - // Get the footer and beforeFooter and afterFooter lines - // Args are: (tooltipItem, data) - getFooter: function() { - var me = this; - var callbacks = me._options.callbacks; - - var beforeFooter = callbacks.beforeFooter.apply(me, arguments); - var footer = callbacks.footer.apply(me, arguments); - var afterFooter = callbacks.afterFooter.apply(me, arguments); - - var lines = []; - lines = pushOrConcat(lines, beforeFooter); - lines = pushOrConcat(lines, footer); - lines = pushOrConcat(lines, afterFooter); - - return lines; - }, - - update: function(changed) { - var me = this; - var opts = me._options; - - // Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition - // that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time - // which breaks any animations. - var existingModel = me._model; - var model = me._model = getBaseModel(opts); - var active = me._active; - - var data = me._data; - - // In the case where active.length === 0 we need to keep these at existing values for good animations - var alignment = { - xAlign: existingModel.xAlign, - yAlign: existingModel.yAlign - }; - var backgroundPoint = { - x: existingModel.x, - y: existingModel.y - }; - var tooltipSize = { - width: existingModel.width, - height: existingModel.height - }; - var tooltipPosition = { - x: existingModel.caretX, - y: existingModel.caretY - }; + var i, len; - var i, len; + if (active.length) { + model.opacity = 1; - if (active.length) { - model.opacity = 1; + var labelColors = []; + var labelTextColors = []; + tooltipPosition = positioners[opts.position].call(me, active, me._eventPosition); - var labelColors = []; - var labelTextColors = []; - tooltipPosition = Chart.Tooltip.positioners[opts.position].call(me, active, me._eventPosition); + var tooltipItems = []; + for (i = 0, len = active.length; i < len; ++i) { + tooltipItems.push(createTooltipItem(active[i])); + } - var tooltipItems = []; - for (i = 0, len = active.length; i < len; ++i) { - tooltipItems.push(createTooltipItem(active[i])); - } + // If the user provided a filter function, use it to modify the tooltip items + if (opts.filter) { + tooltipItems = tooltipItems.filter(function(a) { + return opts.filter(a, data); + }); + } - // If the user provided a filter function, use it to modify the tooltip items - if (opts.filter) { - tooltipItems = tooltipItems.filter(function(a) { - return opts.filter(a, data); - }); - } + // If the user provided a sorting function, use it to modify the tooltip items + if (opts.itemSort) { + tooltipItems = tooltipItems.sort(function(a, b) { + return opts.itemSort(a, b, data); + }); + } - // If the user provided a sorting function, use it to modify the tooltip items - if (opts.itemSort) { - tooltipItems = tooltipItems.sort(function(a, b) { - return opts.itemSort(a, b, data); - }); - } + // Determine colors for boxes + helpers.each(tooltipItems, function(tooltipItem) { + labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, me._chart)); + labelTextColors.push(opts.callbacks.labelTextColor.call(me, tooltipItem, me._chart)); + }); - // Determine colors for boxes - helpers.each(tooltipItems, function(tooltipItem) { - labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, me._chart)); - labelTextColors.push(opts.callbacks.labelTextColor.call(me, tooltipItem, me._chart)); - }); + // Build the Text Lines + model.title = me.getTitle(tooltipItems, data); + model.beforeBody = me.getBeforeBody(tooltipItems, data); + model.body = me.getBody(tooltipItems, data); + model.afterBody = me.getAfterBody(tooltipItems, data); + model.footer = me.getFooter(tooltipItems, data); + + // Initial positioning and colors + model.x = Math.round(tooltipPosition.x); + model.y = Math.round(tooltipPosition.y); + model.caretPadding = opts.caretPadding; + model.labelColors = labelColors; + model.labelTextColors = labelTextColors; + + // data points + model.dataPoints = tooltipItems; + + // We need to determine alignment of the tooltip + tooltipSize = getTooltipSize(this, model); + alignment = determineAlignment(this, tooltipSize); + // Final Size and Position + backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment, me._chart); + } else { + model.opacity = 0; + } - // Build the Text Lines - model.title = me.getTitle(tooltipItems, data); - model.beforeBody = me.getBeforeBody(tooltipItems, data); - model.body = me.getBody(tooltipItems, data); - model.afterBody = me.getAfterBody(tooltipItems, data); - model.footer = me.getFooter(tooltipItems, data); - - // Initial positioning and colors - model.x = Math.round(tooltipPosition.x); - model.y = Math.round(tooltipPosition.y); - model.caretPadding = opts.caretPadding; - model.labelColors = labelColors; - model.labelTextColors = labelTextColors; - - // data points - model.dataPoints = tooltipItems; - - // We need to determine alignment of the tooltip - tooltipSize = getTooltipSize(this, model); - alignment = determineAlignment(this, tooltipSize); - // Final Size and Position - backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment, me._chart); - } else { - model.opacity = 0; - } + model.xAlign = alignment.xAlign; + model.yAlign = alignment.yAlign; + model.x = backgroundPoint.x; + model.y = backgroundPoint.y; + model.width = tooltipSize.width; + model.height = tooltipSize.height; - model.xAlign = alignment.xAlign; - model.yAlign = alignment.yAlign; - model.x = backgroundPoint.x; - model.y = backgroundPoint.y; - model.width = tooltipSize.width; - model.height = tooltipSize.height; + // Point where the caret on the tooltip points to + model.caretX = tooltipPosition.x; + model.caretY = tooltipPosition.y; - // Point where the caret on the tooltip points to - model.caretX = tooltipPosition.x; - model.caretY = tooltipPosition.y; + me._model = model; - me._model = model; + if (changed && opts.custom) { + opts.custom.call(me, model); + } - if (changed && opts.custom) { - opts.custom.call(me, model); - } + return me; + }, - return me; - }, - drawCaret: function(tooltipPoint, size) { - var ctx = this._chart.ctx; - var vm = this._view; - var caretPosition = this.getCaretPosition(tooltipPoint, size, vm); - - ctx.lineTo(caretPosition.x1, caretPosition.y1); - ctx.lineTo(caretPosition.x2, caretPosition.y2); - ctx.lineTo(caretPosition.x3, caretPosition.y3); - }, - getCaretPosition: function(tooltipPoint, size, vm) { - var x1, x2, x3, y1, y2, y3; - var caretSize = vm.caretSize; - var cornerRadius = vm.cornerRadius; - var xAlign = vm.xAlign; - var yAlign = vm.yAlign; - var ptX = tooltipPoint.x; - var ptY = tooltipPoint.y; - var width = size.width; - var height = size.height; - - if (yAlign === 'center') { - y2 = ptY + (height / 2); - - if (xAlign === 'left') { - x1 = ptX; - x2 = x1 - caretSize; - x3 = x1; - - y1 = y2 + caretSize; - y3 = y2 - caretSize; - } else { - x1 = ptX + width; - x2 = x1 + caretSize; - x3 = x1; - - y1 = y2 - caretSize; - y3 = y2 + caretSize; - } - } else { - if (xAlign === 'left') { - x2 = ptX + cornerRadius + (caretSize); - x1 = x2 - caretSize; - x3 = x2 + caretSize; - } else if (xAlign === 'right') { - x2 = ptX + width - cornerRadius - caretSize; - x1 = x2 - caretSize; - x3 = x2 + caretSize; - } else { - x2 = vm.caretX; - x1 = x2 - caretSize; - x3 = x2 + caretSize; - } - if (yAlign === 'top') { - y1 = ptY; - y2 = y1 - caretSize; - y3 = y1; - } else { - y1 = ptY + height; - y2 = y1 + caretSize; - y3 = y1; - // invert drawing order - var tmp = x3; - x3 = x1; - x1 = tmp; - } - } - return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; - }, - drawTitle: function(pt, vm, ctx, opacity) { - var title = vm.title; + drawCaret: function(tooltipPoint, size) { + var ctx = this._chart.ctx; + var vm = this._view; + var caretPosition = this.getCaretPosition(tooltipPoint, size, vm); - if (title.length) { - ctx.textAlign = vm._titleAlign; - ctx.textBaseline = 'top'; + ctx.lineTo(caretPosition.x1, caretPosition.y1); + ctx.lineTo(caretPosition.x2, caretPosition.y2); + ctx.lineTo(caretPosition.x3, caretPosition.y3); + }, + getCaretPosition: function(tooltipPoint, size, vm) { + var x1, x2, x3, y1, y2, y3; + var caretSize = vm.caretSize; + var cornerRadius = vm.cornerRadius; + var xAlign = vm.xAlign; + var yAlign = vm.yAlign; + var ptX = tooltipPoint.x; + var ptY = tooltipPoint.y; + var width = size.width; + var height = size.height; - var titleFontSize = vm.titleFontSize; - var titleSpacing = vm.titleSpacing; + if (yAlign === 'center') { + y2 = ptY + (height / 2); - ctx.fillStyle = mergeOpacity(vm.titleFontColor, opacity); - ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); + if (xAlign === 'left') { + x1 = ptX; + x2 = x1 - caretSize; + x3 = x1; - var i, len; - for (i = 0, len = title.length; i < len; ++i) { - ctx.fillText(title[i], pt.x, pt.y); - pt.y += titleFontSize + titleSpacing; // Line Height and spacing + y1 = y2 + caretSize; + y3 = y2 - caretSize; + } else { + x1 = ptX + width; + x2 = x1 + caretSize; + x3 = x1; - if (i + 1 === title.length) { - pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing - } - } + y1 = y2 - caretSize; + y3 = y2 + caretSize; + } + } else { + if (xAlign === 'left') { + x2 = ptX + cornerRadius + (caretSize); + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else if (xAlign === 'right') { + x2 = ptX + width - cornerRadius - caretSize; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else { + x2 = vm.caretX; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } + if (yAlign === 'top') { + y1 = ptY; + y2 = y1 - caretSize; + y3 = y1; + } else { + y1 = ptY + height; + y2 = y1 + caretSize; + y3 = y1; + // invert drawing order + var tmp = x3; + x3 = x1; + x1 = tmp; } - }, - drawBody: function(pt, vm, ctx, opacity) { - var bodyFontSize = vm.bodyFontSize; - var bodySpacing = vm.bodySpacing; - var body = vm.body; + } + return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; + }, + + drawTitle: function(pt, vm, ctx, opacity) { + var title = vm.title; - ctx.textAlign = vm._bodyAlign; + if (title.length) { + ctx.textAlign = vm._titleAlign; ctx.textBaseline = 'top'; - ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - // Before Body - var xLinePadding = 0; - var fillLineOfText = function(line) { - ctx.fillText(line, pt.x + xLinePadding, pt.y); - pt.y += bodyFontSize + bodySpacing; - }; + var titleFontSize = vm.titleFontSize; + var titleSpacing = vm.titleSpacing; - // Before body lines - ctx.fillStyle = mergeOpacity(vm.bodyFontColor, opacity); - helpers.each(vm.beforeBody, fillLineOfText); - - var drawColorBoxes = vm.displayColors; - xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0; - - // Draw body lines now - helpers.each(body, function(bodyItem, i) { - var textColor = mergeOpacity(vm.labelTextColors[i], opacity); - ctx.fillStyle = textColor; - helpers.each(bodyItem.before, fillLineOfText); - - helpers.each(bodyItem.lines, function(line) { - // Draw Legend-like boxes if needed - if (drawColorBoxes) { - // Fill a white rect so that colours merge nicely if the opacity is < 1 - ctx.fillStyle = mergeOpacity(vm.legendColorBackground, opacity); - ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); - - // Border - ctx.lineWidth = 1; - ctx.strokeStyle = mergeOpacity(vm.labelColors[i].borderColor, opacity); - ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); - - // Inner square - ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); - ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); - ctx.fillStyle = textColor; - } + ctx.fillStyle = mergeOpacity(vm.titleFontColor, opacity); + ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); - fillLineOfText(line); - }); + var i, len; + for (i = 0, len = title.length; i < len; ++i) { + ctx.fillText(title[i], pt.x, pt.y); + pt.y += titleFontSize + titleSpacing; // Line Height and spacing - helpers.each(bodyItem.after, fillLineOfText); - }); + if (i + 1 === title.length) { + pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing + } + } + } + }, + + drawBody: function(pt, vm, ctx, opacity) { + var bodyFontSize = vm.bodyFontSize; + var bodySpacing = vm.bodySpacing; + var body = vm.body; + + ctx.textAlign = vm._bodyAlign; + ctx.textBaseline = 'top'; + ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + + // Before Body + var xLinePadding = 0; + var fillLineOfText = function(line) { + ctx.fillText(line, pt.x + xLinePadding, pt.y); + pt.y += bodyFontSize + bodySpacing; + }; - // Reset back to 0 for after body - xLinePadding = 0; + // Before body lines + ctx.fillStyle = mergeOpacity(vm.bodyFontColor, opacity); + helpers.each(vm.beforeBody, fillLineOfText); + + var drawColorBoxes = vm.displayColors; + xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0; + + // Draw body lines now + helpers.each(body, function(bodyItem, i) { + var textColor = mergeOpacity(vm.labelTextColors[i], opacity); + ctx.fillStyle = textColor; + helpers.each(bodyItem.before, fillLineOfText); + + helpers.each(bodyItem.lines, function(line) { + // Draw Legend-like boxes if needed + if (drawColorBoxes) { + // Fill a white rect so that colours merge nicely if the opacity is < 1 + ctx.fillStyle = mergeOpacity(vm.legendColorBackground, opacity); + ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); + + // Border + ctx.lineWidth = 1; + ctx.strokeStyle = mergeOpacity(vm.labelColors[i].borderColor, opacity); + ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); + + // Inner square + ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); + ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); + ctx.fillStyle = textColor; + } - // After body lines - helpers.each(vm.afterBody, fillLineOfText); - pt.y -= bodySpacing; // Remove last body spacing - }, - drawFooter: function(pt, vm, ctx, opacity) { - var footer = vm.footer; + fillLineOfText(line); + }); - if (footer.length) { - pt.y += vm.footerMarginTop; + helpers.each(bodyItem.after, fillLineOfText); + }); - ctx.textAlign = vm._footerAlign; - ctx.textBaseline = 'top'; + // Reset back to 0 for after body + xLinePadding = 0; - ctx.fillStyle = mergeOpacity(vm.footerFontColor, opacity); - ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); + // After body lines + helpers.each(vm.afterBody, fillLineOfText); + pt.y -= bodySpacing; // Remove last body spacing + }, - helpers.each(footer, function(line) { - ctx.fillText(line, pt.x, pt.y); - pt.y += vm.footerFontSize + vm.footerSpacing; - }); - } - }, - drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { - ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); - ctx.strokeStyle = mergeOpacity(vm.borderColor, opacity); - ctx.lineWidth = vm.borderWidth; - var xAlign = vm.xAlign; - var yAlign = vm.yAlign; - var x = pt.x; - var y = pt.y; - var width = tooltipSize.width; - var height = tooltipSize.height; - var radius = vm.cornerRadius; - - ctx.beginPath(); - ctx.moveTo(x + radius, y); - if (yAlign === 'top') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - if (yAlign === 'center' && xAlign === 'right') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - if (yAlign === 'bottom') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - if (yAlign === 'center' && xAlign === 'left') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); + drawFooter: function(pt, vm, ctx, opacity) { + var footer = vm.footer; - ctx.fill(); + if (footer.length) { + pt.y += vm.footerMarginTop; - if (vm.borderWidth > 0) { - ctx.stroke(); - } - }, - draw: function() { - var ctx = this._chart.ctx; - var vm = this._view; + ctx.textAlign = vm._footerAlign; + ctx.textBaseline = 'top'; - if (vm.opacity === 0) { - return; - } + ctx.fillStyle = mergeOpacity(vm.footerFontColor, opacity); + ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); - var tooltipSize = { - width: vm.width, - height: vm.height - }; - var pt = { - x: vm.x, - y: vm.y - }; + helpers.each(footer, function(line) { + ctx.fillText(line, pt.x, pt.y); + pt.y += vm.footerFontSize + vm.footerSpacing; + }); + } + }, + + drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { + ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); + ctx.strokeStyle = mergeOpacity(vm.borderColor, opacity); + ctx.lineWidth = vm.borderWidth; + var xAlign = vm.xAlign; + var yAlign = vm.yAlign; + var x = pt.x; + var y = pt.y; + var width = tooltipSize.width; + var height = tooltipSize.height; + var radius = vm.cornerRadius; + + ctx.beginPath(); + ctx.moveTo(x + radius, y); + if (yAlign === 'top') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + if (yAlign === 'center' && xAlign === 'right') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + if (yAlign === 'bottom') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + if (yAlign === 'center' && xAlign === 'left') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); - // IE11/Edge does not like very small opacities, so snap to 0 - var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; + ctx.fill(); - // Truthy/falsey value for empty tooltip - var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; + if (vm.borderWidth > 0) { + ctx.stroke(); + } + }, - if (this._options.enabled && hasTooltipContent) { - // Draw Background - this.drawBackground(pt, vm, ctx, tooltipSize, opacity); + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; - // Draw Title, Body, and Footer - pt.x += vm.xPadding; - pt.y += vm.yPadding; + if (vm.opacity === 0) { + return; + } - // Titles - this.drawTitle(pt, vm, ctx, opacity); + var tooltipSize = { + width: vm.width, + height: vm.height + }; + var pt = { + x: vm.x, + y: vm.y + }; - // Body - this.drawBody(pt, vm, ctx, opacity); + // IE11/Edge does not like very small opacities, so snap to 0 + var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; - // Footer - this.drawFooter(pt, vm, ctx, opacity); - } - }, - - /** - * Handle an event - * @private - * @param {IEvent} event - The event to handle - * @returns {Boolean} true if the tooltip changed - */ - handleEvent: function(e) { - var me = this; - var options = me._options; - var changed = false; - - me._lastActive = me._lastActive || []; - - // Find Active Elements for tooltips - if (e.type === 'mouseout') { - me._active = []; - } else { - me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); - } + // Truthy/falsey value for empty tooltip + var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; - // Remember Last Actives - changed = !helpers.arrayEquals(me._active, me._lastActive); + if (this._options.enabled && hasTooltipContent) { + // Draw Background + this.drawBackground(pt, vm, ctx, tooltipSize, opacity); - // Only handle target event on tooltip change - if (changed) { - me._lastActive = me._active; + // Draw Title, Body, and Footer + pt.x += vm.xPadding; + pt.y += vm.yPadding; - if (options.enabled || options.custom) { - me._eventPosition = { - x: e.x, - y: e.y - }; + // Titles + this.drawTitle(pt, vm, ctx, opacity); - me.update(true); - me.pivot(); - } - } + // Body + this.drawBody(pt, vm, ctx, opacity); - return changed; + // Footer + this.drawFooter(pt, vm, ctx, opacity); } - }); + }, /** - * @namespace Chart.Tooltip.positioners + * Handle an event + * @private + * @param {IEvent} event - The event to handle + * @returns {Boolean} true if the tooltip changed */ - Chart.Tooltip.positioners = { - /** - * Average mode places the tooltip at the average position of the elements shown - * @function Chart.Tooltip.positioners.average - * @param elements {ChartElement[]} the elements being displayed in the tooltip - * @returns {Point} tooltip position - */ - average: function(elements) { - if (!elements.length) { - return false; - } + handleEvent: function(e) { + var me = this; + var options = me._options; + var changed = false; - var i, len; - var x = 0; - var y = 0; - var count = 0; - - for (i = 0, len = elements.length; i < len; ++i) { - var el = elements[i]; - if (el && el.hasValue()) { - var pos = el.tooltipPosition(); - x += pos.x; - y += pos.y; - ++count; - } - } + me._lastActive = me._lastActive || []; - return { - x: Math.round(x / count), - y: Math.round(y / count) - }; - }, - - /** - * Gets the tooltip position nearest of the item nearest to the event position - * @function Chart.Tooltip.positioners.nearest - * @param elements {Chart.Element[]} the tooltip elements - * @param eventPosition {Point} the position of the event in canvas coordinates - * @returns {Point} the tooltip position - */ - nearest: function(elements, eventPosition) { - var x = eventPosition.x; - var y = eventPosition.y; - var minDistance = Number.POSITIVE_INFINITY; - var i, len, nearestElement; - - for (i = 0, len = elements.length; i < len; ++i) { - var el = elements[i]; - if (el && el.hasValue()) { - var center = el.getCenterPoint(); - var d = helpers.distanceBetweenPoints(eventPosition, center); - - if (d < minDistance) { - minDistance = d; - nearestElement = el; - } - } - } + // Find Active Elements for tooltips + if (e.type === 'mouseout') { + me._active = []; + } else { + me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); + } - if (nearestElement) { - var tp = nearestElement.tooltipPosition(); - x = tp.x; - y = tp.y; - } + // Remember Last Actives + changed = !helpers.arrayEquals(me._active, me._lastActive); - return { - x: x, - y: y - }; + // Only handle target event on tooltip change + if (changed) { + me._lastActive = me._active; + + if (options.enabled || options.custom) { + me._eventPosition = { + x: e.x, + y: e.y + }; + + me.update(true); + me.pivot(); + } } - }; -}; + + return changed; + } +}); + +/** + * @namespace Chart.Tooltip.positioners + */ +exports.positioners = positioners; + diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 2a4074971cb..5d5020913e1 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -71,41 +71,19 @@ module.exports = Element.extend({ var radius = vm.radius; var x = vm.x; var y = vm.y; - var color = helpers.color; var errMargin = 1.01; // 1.01 is margin for Accumulated error. (Especially Edge, IE.) - var ratio = 0; if (vm.skip) { return; } - var finalLineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); - ctx.strokeStyle = finalLineWidth === 0 ? - 'rgba(0,0,0,0)' : - vm.borderColor || defaultColor; - - // Will disregard any value that is not a number > 0 - ctx.lineWidth = finalLineWidth; - ctx.fillStyle = vm.backgroundColor || defaultColor; - - // Cliping for Points. - // going out from inner charArea? - if ((chartArea !== undefined) && ((model.x < chartArea.left) || (chartArea.right * errMargin < model.x) || (model.y < chartArea.top) || (chartArea.bottom * errMargin < model.y))) { - // Point fade out - if (model.x < chartArea.left) { - ratio = (x - model.x) / (chartArea.left - model.x); - } else if (chartArea.right * errMargin < model.x) { - ratio = (model.x - x) / (model.x - chartArea.right); - } else if (model.y < chartArea.top) { - ratio = (y - model.y) / (chartArea.top - model.y); - } else if (chartArea.bottom * errMargin < model.y) { - ratio = (model.y - y) / (model.y - chartArea.bottom); - } - ratio = Math.round(ratio * 100) / 100; - ctx.strokeStyle = color(ctx.strokeStyle).alpha(ratio).rgbString(); - ctx.fillStyle = color(ctx.fillStyle).alpha(ratio).rgbString(); + // Clipping for Points. + if (chartArea === undefined || (model.x >= chartArea.left && chartArea.right * errMargin >= model.x && model.y >= chartArea.top && chartArea.bottom * errMargin >= model.y)) { + var finalLineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); + ctx.strokeStyle = finalLineWidth === 0 ? 'rgba(0,0,0,0)' : vm.borderColor || defaultColor; + ctx.lineWidth = finalLineWidth; + ctx.fillStyle = vm.backgroundColor || defaultColor; + helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y); } - - helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y); } }); diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 5910ce88a51..dd8b01783bf 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -1,13 +1,16 @@ 'use strict'; -module.exports = function(Chart) { +var Scale = require('../core/core.scale'); +var scaleService = require('../core/core.scaleService'); + +module.exports = function() { // Default config for a category scale var defaultConfig = { position: 'bottom' }; - var DatasetScale = Chart.Scale.extend({ + var DatasetScale = Scale.extend({ /** * Internal function to get the correct labels. If data.xLabels or data.yLabels are defined, use those * else fall back to data.labels @@ -128,6 +131,5 @@ module.exports = function(Chart) { } }); - Chart.scaleService.registerScaleType('category', DatasetScale, defaultConfig); - + scaleService.registerScaleType('category', DatasetScale, defaultConfig); }; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index aa723004fb7..a980615c5d1 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -2,6 +2,7 @@ var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var scaleService = require('../core/core.scaleService'); var Ticks = require('../core/core.ticks'); module.exports = function(Chart) { @@ -186,6 +187,6 @@ module.exports = function(Chart) { return this.getPixelForValue(this.ticksAsNumbers[index]); } }); - Chart.scaleService.registerScaleType('linear', LinearScale, defaultConfig); + scaleService.registerScaleType('linear', LinearScale, defaultConfig); }; diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 3e4b5c083f3..ce53da7b98d 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -1,6 +1,7 @@ 'use strict'; var helpers = require('../helpers/index'); +var Scale = require('../core/core.scale'); /** * Generate a set of linear ticks @@ -14,12 +15,22 @@ function generateTicks(generationOptions, dataRange) { // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks // for details. + var factor; + var precision; 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); + + precision = generationOptions.precision; + if (precision !== undefined) { + // If the user specified a precision, round to that number of decimal places + factor = Math.pow(10, precision); + spacing = Math.ceil(spacing * factor) / factor; + } } var niceMin = Math.floor(dataRange.min / spacing) * spacing; var niceMax = Math.ceil(dataRange.max / spacing) * spacing; @@ -41,7 +52,7 @@ function generateTicks(generationOptions, dataRange) { numSpaces = Math.ceil(numSpaces); } - var precision = 1; + precision = 1; if (spacing < 1) { precision = Math.pow(10, spacing.toString().length - 2); niceMin = Math.round(niceMin * precision) / precision; @@ -56,17 +67,16 @@ function generateTicks(generationOptions, dataRange) { return ticks; } - module.exports = function(Chart) { var noop = helpers.noop; - Chart.LinearScaleBase = Chart.Scale.extend({ + Chart.LinearScaleBase = Scale.extend({ getRightValue: function(value) { if (typeof value === 'string') { return +value; } - return Chart.Scale.prototype.getRightValue.call(this, value); + return Scale.prototype.getRightValue.call(this, value); }, handleTickRangeOptions: function() { @@ -154,6 +164,7 @@ module.exports = function(Chart) { maxTicks: maxTicks, min: tickOpts.min, max: tickOpts.max, + precision: tickOpts.precision, stepSize: helpers.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) }; var ticks = me.ticks = generateTicks(numericGeneratorOptions, me); @@ -180,7 +191,7 @@ module.exports = function(Chart) { me.ticksAsNumbers = me.ticks.slice(); me.zeroLineIndex = me.ticks.indexOf(0); - Chart.Scale.prototype.convertTicksToLabels.call(me); + Scale.prototype.convertTicksToLabels.call(me); } }); }; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 74a210e4473..0365568e772 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -1,6 +1,8 @@ 'use strict'; var helpers = require('../helpers/index'); +var Scale = require('../core/core.scale'); +var scaleService = require('../core/core.scaleService'); var Ticks = require('../core/core.ticks'); /** @@ -66,7 +68,7 @@ module.exports = function(Chart) { } }; - var LogarithmicScale = Chart.Scale.extend({ + var LogarithmicScale = Scale.extend({ determineDataLimits: function() { var me = this; var opts = me.options; @@ -241,7 +243,7 @@ module.exports = function(Chart) { convertTicksToLabels: function() { this.tickValues = this.ticks.slice(); - Chart.Scale.prototype.convertTicksToLabels.call(this); + Scale.prototype.convertTicksToLabels.call(this); }, // Get the correct tooltip label getLabelForIndex: function(index, datasetIndex) { @@ -342,6 +344,6 @@ module.exports = function(Chart) { return value; } }); - Chart.scaleService.registerScaleType('logarithmic', LogarithmicScale, defaultConfig); + scaleService.registerScaleType('logarithmic', LogarithmicScale, defaultConfig); }; diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 9f59f17887a..b580e1da75b 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -2,6 +2,7 @@ var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var scaleService = require('../core/core.scaleService'); var Ticks = require('../core/core.ticks'); module.exports = function(Chart) { @@ -237,7 +238,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 +267,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; @@ -525,6 +525,6 @@ module.exports = function(Chart) { } } }); - Chart.scaleService.registerScaleType('radialLinear', LinearRadialScale, defaultConfig); + scaleService.registerScaleType('radialLinear', LinearRadialScale, defaultConfig); }; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 4892eea9996..dfd708b2345 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -6,6 +6,8 @@ moment = typeof moment === 'function' ? moment : window.moment; var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var Scale = require('../core/core.scale'); +var scaleService = require('../core/core.scaleService'); // Integer constants are from the ES6 spec. var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; @@ -424,7 +426,7 @@ function determineLabelFormat(data, timeOpts) { return 'MMM D, YYYY'; } -module.exports = function(Chart) { +module.exports = function() { var defaultConfig = { position: 'bottom', @@ -488,7 +490,7 @@ module.exports = function(Chart) { } }; - var TimeScale = Chart.Scale.extend({ + var TimeScale = Scale.extend({ initialize: function() { if (!moment) { throw new Error('Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com'); @@ -496,7 +498,7 @@ module.exports = function(Chart) { this.mergeTicksOptions(); - Chart.Scale.prototype.initialize.call(this); + Scale.prototype.initialize.call(this); }, update: function() { @@ -508,7 +510,7 @@ module.exports = function(Chart) { console.warn('options.time.format is deprecated and replaced by options.time.parser.'); } - return Chart.Scale.prototype.update.apply(me, arguments); + return Scale.prototype.update.apply(me, arguments); }, /** @@ -518,7 +520,7 @@ module.exports = function(Chart) { if (rawValue && rawValue.t !== undefined) { rawValue = rawValue.t; } - return Chart.Scale.prototype.getRightValue.call(this, rawValue); + return Scale.prototype.getRightValue.call(this, rawValue); }, determineDataLimits: function() { @@ -779,5 +781,5 @@ module.exports = function(Chart) { } }); - Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig); + scaleService.registerScaleType('time', TimeScale, defaultConfig); }; diff --git a/test/fixtures/core.scale/tick-drawing.json b/test/fixtures/core.scale/tick-drawing.json new file mode 100644 index 00000000000..7c9d6da7abe --- /dev/null +++ b/test/fixtures/core.scale/tick-drawing.json @@ -0,0 +1,79 @@ +{ + "config": { + "type": "horizontalBar", + "data": { + "labels": ["January", "February", "March", "April", "May", "June", "July"], + "datasets": [] + }, + "options": { + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "category", + "position": "top", + "id": "x-axis-1", + "ticks": { + "display": false + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }, { + "type": "category", + "position": "bottom", + "id": "x-axis-2", + "ticks": { + "display": false + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }], + "yAxes": [{ + "position": "left", + "id": "y-axis-1", + "type": "linear", + "ticks": { + "display": false, + "min": -100, + "max": 100 + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }, { + "type": "linear", + "id": "y-axis-2", + "position": "right", + "ticks": { + "display": false, + "min": 0, + "max": 50 + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/tick-drawing.png b/test/fixtures/core.scale/tick-drawing.png new file mode 100644 index 00000000000..fb80cd01232 Binary files /dev/null and b/test/fixtures/core.scale/tick-drawing.png differ diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 44446d38ab8..3c471b50510 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() { @@ -742,6 +746,36 @@ describe('Core helper tests', function() { expect(canvas.style.width).toBe('400px'); }); + it ('Should get padding of parent as number (pixels) when defined as percent (returns incorrectly in IE11)', function() { + + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '300px'; + div.style.height = '300px'; + document.body.appendChild(div); + + // Inner DIV to have 10% padding of parent + var innerDiv = document.createElement('div'); + + div.appendChild(innerDiv); + + var canvas = document.createElement('canvas'); + innerDiv.appendChild(canvas); + + // No padding + expect(helpers.getMaximumWidth(canvas)).toBe(300); + + // test with percentage + innerDiv.style.padding = '10%'; + expect(helpers.getMaximumWidth(canvas)).toBe(240); + + // test with pixels + innerDiv.style.padding = '10px'; + expect(helpers.getMaximumWidth(canvas)).toBe(280); + + document.body.removeChild(div); + }); + describe('Color helper', function() { function isColorInstance(obj) { return typeof obj === 'object' && obj.hasOwnProperty('values') && obj.values.hasOwnProperty('rgb'); diff --git a/test/specs/global.defaults.tests.js b/test/specs/global.defaults.tests.js index 0e859901434..a01284c1a0b 100644 --- a/test/specs/global.defaults.tests.js +++ b/test/specs/global.defaults.tests.js @@ -1,4 +1,3 @@ -// Test the bubble chart default config describe('Default Configs', function() { describe('Bubble Chart', function() { it('should return correct tooltip strings', function() { diff --git a/test/specs/global.namespace.tests.js b/test/specs/global.namespace.tests.js new file mode 100644 index 00000000000..49942fc9af3 --- /dev/null +++ b/test/specs/global.namespace.tests.js @@ -0,0 +1,46 @@ +describe('Chart namespace', function() { + describe('Chart', function() { + it('should a function (constructor)', function() { + expect(Chart instanceof Function).toBeTruthy(); + }); + it('should define "core" properties', function() { + expect(Chart instanceof Function).toBeTruthy(); + expect(Chart.Animation instanceof Object).toBeTruthy(); + expect(Chart.animationService instanceof Object).toBeTruthy(); + expect(Chart.defaults instanceof Object).toBeTruthy(); + expect(Chart.Element instanceof Object).toBeTruthy(); + expect(Chart.Interaction instanceof Object).toBeTruthy(); + expect(Chart.layouts instanceof Object).toBeTruthy(); + expect(Chart.plugins instanceof Object).toBeTruthy(); + expect(Chart.platform instanceof Object).toBeTruthy(); + expect(Chart.Scale instanceof Object).toBeTruthy(); + expect(Chart.scaleService instanceof Object).toBeTruthy(); + expect(Chart.Ticks instanceof Object).toBeTruthy(); + expect(Chart.Tooltip instanceof Object).toBeTruthy(); + expect(Chart.Tooltip.positioners instanceof Object).toBeTruthy(); + }); + }); + + describe('Chart.elements', function() { + it('should be an object', function() { + expect(Chart.elements instanceof Object).toBeTruthy(); + }); + it('should contains "elements" classes', function() { + expect(Chart.elements.Arc instanceof Function).toBeTruthy(); + expect(Chart.elements.Line instanceof Function).toBeTruthy(); + expect(Chart.elements.Point instanceof Function).toBeTruthy(); + expect(Chart.elements.Rectangle instanceof Function).toBeTruthy(); + }); + }); + + describe('Chart.helpers', function() { + it('should be an object', function() { + expect(Chart.helpers instanceof Object).toBeTruthy(); + }); + it('should contains "helpers" namespaces', function() { + expect(Chart.helpers.easing instanceof Object).toBeTruthy(); + expect(Chart.helpers.canvas instanceof Object).toBeTruthy(); + expect(Chart.helpers.options instanceof Object).toBeTruthy(); + }); + }); +}); diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 60dd07698c0..b42675d8a8d 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -548,6 +548,66 @@ describe('Linear Scale', function() { expect(chart.scales.yScale0.ticks).toEqual(['11', '9', '7', '5', '3', '1']); }); + describe('precision', function() { + it('Should create integer steps if precision is 0', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [0, 1, 2, 1, 0, 1] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear', + ticks: { + precision: 0 + } + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min).toBe(0); + expect(chart.scales.yScale0.max).toBe(2); + expect(chart.scales.yScale0.ticks).toEqual(['2', '1', '0']); + }); + + it('Should round the step size to the given number of decimal places', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [0, 0.001, 0.002, 0.003, 0, 0.001] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear', + ticks: { + precision: 2 + } + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min).toBe(0); + expect(chart.scales.yScale0.max).toBe(0.01); + expect(chart.scales.yScale0.ticks).toEqual(['0.01', '0']); + }); + }); + it('should forcibly include 0 in the range if the beginAtZero option is used', function() { var chart = window.acquireChart({