diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7d57bbd0c0c..e6ead94ecb3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,9 @@ diff --git a/.htmllintrc b/.htmllintrc index a6b2097031b..1ab933490de 100644 --- a/.htmllintrc +++ b/.htmllintrc @@ -1,5 +1,6 @@ { "indent-style": "tabs", + "line-end-style": false, "attr-quote-style": "double", "spec-char-escape": false, "attr-bans": [ diff --git a/README.md b/README.md index f914944e101..d7e966ca784 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ 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. +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 compatibility issues. The Moment.js version in the bundled build is private to Chart.js so if you want to use Moment.js yourself, it's better to use Chart.js (non bundled) and import Moment.js manually. ## Documentation diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8591dad86ae..15265bd2b5a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,6 +6,7 @@ * [Integration](getting-started/integration.md) * [Usage](getting-started/usage.md) * [General](general/README.md) + * [Accessibility](general/accessibility.md) * [Responsive](general/responsive.md) * [Pixel Ratio](general/device-pixel-ratio.md) * [Interactions](general/interactions/README.md) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 8518da9f250..1bf35b79f16 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -15,7 +15,7 @@ All of the included cartesian axes support a number of common options. | -----| ---- | --------| ----------- | `type` | `String` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. | `position` | `String` | | Position of the axis in the chart. Possible values are: `'top'`, `'left'`, `'bottom'`, `'right'` -| `offset` | `Boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` in the bar chart by default. +| `offset` | `Boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` for a category scale in a bar chart by default. | `id` | `String` | | The ID is used to link datasets and scale axes together. [more...](#axis-id) | `gridLines` | `Object` | | Grid line configuration. [more...](../styling.md#grid-line-configuration) | `scaleLabel` | `Object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration) 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 cefe9c24806..fcda85d6173 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -6,7 +6,7 @@ The time scale is used to display times and dates. When building its ticks, it w ### Input Data -The x-axis data points may additionally be specified via the `t` attribute when using the time scale. +The x-axis data points may additionally be specified via the `t` or `x` attribute when using the time scale. data: [{ x: new Date(), @@ -64,6 +64,7 @@ var chart = new Chart(ctx, { options: { scales: { xAxes: [{ + type: 'time', time: { unit: 'month' } diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index 14009be6a5c..e19a33d75fd 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) @@ -75,14 +76,12 @@ This example sets up a chart with a y axis that creates ticks at `0, 0.5, 1, 1.5 ```javascript let options = { - scales: { - yAxes: [{ - ticks: { - max: 5, - min: 0, - stepSize: 0.5 - } - }] + scale: { + ticks: { + max: 5, + min: 0, + stepSize: 0.5 + } } }; ``` diff --git a/docs/axes/styling.md b/docs/axes/styling.md index f60afd9bb68..12a170f3e54 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -9,6 +9,7 @@ The grid line configuration is nested under the scale configuration in the `grid | Name | Type | Default | Description | -----| ---- | --------| ----------- | `display` | `Boolean` | `true` | If false, do not display grid lines for this axis. +| `circular` | `Boolean` | `false` | If true, gridlines are circular (on radar chart only) | `color` | `Color/Color[]` | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. | `borderDash` | `Number[]` | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `borderDashOffset` | `Number` | `0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) @@ -21,7 +22,7 @@ The grid line configuration is nested under the scale configuration in the `grid | `zeroLineColor` | Color | `'rgba(0, 0, 0, 0.25)'` | Stroke color of the grid line for the first index (index 0). | `zeroLineBorderDash` | `Number[]` | `[]` | Length and spacing of dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `zeroLineBorderDashOffset` | `Number` | `0` | Offset for line dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -| `offsetGridLines` | `Boolean` | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` in the bar chart by default. +| `offsetGridLines` | `Boolean` | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` for a category scale in a bar chart by default. ## Tick Configuration The tick configuration is nested under the scale configuration in the `ticks` key. It defines options for the tick marks that are generated by the axis. diff --git a/docs/charts/README.md b/docs/charts/README.md index 26677b04d76..dda7e343811 100644 --- a/docs/charts/README.md +++ b/docs/charts/README.md @@ -4,9 +4,10 @@ Chart.js comes with built-in chart types: * [line](./line.md) * [bar](./bar.md) * [radar](./radar.md) -* [polar area](./polar.md) * [doughnut and pie](./doughnut.md) +* [polar area](./polar.md) * [bubble](./bubble.md) +* [scatter](./scatter.md) [Area charts](area.md) can be built from a line or radar chart using the dataset `fill` option. diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 84704ba941b..e4d146b2c67 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -95,12 +95,19 @@ The bar chart defines the following configuration options. These options are mer | ---- | ---- | ------- | ----------- | `barPercentage` | `Number` | `0.9` | Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. [more...](#barpercentage-vs-categorypercentage) | `categoryPercentage` | `Number` | `0.8` | Percent (0-1) of the available width each category should be within the sample width. [more...](#barpercentage-vs-categorypercentage) -| `barThickness` | `Number` | | Manually set width of each bar in pixels. If not set, the base sample widths are calculated automatically so that they take the full available widths without overlap. Then, the bars are sized using `barPercentage` and `categoryPercentage`. +| `barThickness` | `Number/String` | | Manually set width of each bar in pixels. If set to `'flex'`, it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. [more...](#barthickness) | `maxBarThickness` | `Number` | | Set this to ensure that bars are not sized thicker than this. | `gridLines.offsetGridLines` | `Boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) +### barThickness +If this value is a number, it is applied to the width of each bar, in pixels. When this is enforced, `barPercentage` and `categoryPercentage` are ignored. + +If set to `'flex'`, the base sample widths are calculated automatically based on the previous and following samples so that they take the full available widths without overlap. Then, bars are sized using `barPercentage` and `categoryPercentage`. There is no gap when the percentage options are 1. This mode generates bars with different widths when data are not evenly spaced. + +If not set (default), the base sample widths are calculated using the smallest interval that prevents bar overlapping, and bars are sized using `barPercentage` and `categoryPercentage`. This mode always generates bars equally sized. + ### offsetGridLines -If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a bar chart while false for other charts by default. +If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a category scale in a bar chart while false for other scales or chart types by default. This setting applies to the axis configuration. If axes are added to the chart, this setting will need to be set for each new axis. diff --git a/docs/charts/bubble.md b/docs/charts/bubble.md index 4cb2ee6994f..e9f8a7216b1 100644 --- a/docs/charts/bubble.md +++ b/docs/charts/bubble.md @@ -51,6 +51,7 @@ The bubble chart allows a number of properties to be specified for each dataset. | [`hitRadius`](#interactions) | `Number` | Yes | Yes | `1` | [`label`](#labeling) | `String` | - | - | `undefined` | [`pointStyle`](#styling) | `String` | Yes | Yes | `circle` +| [`rotation`](#styling) | `Number` | Yes | Yes | `0` | [`radius`](#styling) | `Number` | Yes | Yes | `3` ### Labeling @@ -67,6 +68,7 @@ The style of each bubble can be controlled with the following properties: | `borderColor` | bubble border color | `borderWidth` | bubble border width (in pixels) | `pointStyle` | bubble [shape style](../configuration/elements#point-styles) +| `rotation` | bubble rotation (in degrees) | `radius` | bubble radius (in pixels) All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. diff --git a/docs/charts/line.md b/docs/charts/line.md index 90471e462dd..db0245b1e0c 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -63,6 +63,7 @@ All point* properties can be specified as an array. If these are set to an array | `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. | `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered. | `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](../configuration/elements#point-styles) +| `pointRotation` | `Number/Number[]` | The rotation of the point in degrees. | `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events. | `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered. | `pointHoverBorderColor` | `Color/Color[]` | Point border color when hovered. diff --git a/docs/charts/radar.md b/docs/charts/radar.md index b8a41c8384c..947e15a3102 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -82,6 +82,7 @@ All point* properties can be specified as an array. If these are set to an array | `pointBorderColor` | `Color/Color[]` | The border color for points. | `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. | `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered. +| `pointRotation` | `Number/Number[]` | The rotation of the point in degrees. | `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](#pointstyle) | `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events. | `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered. diff --git a/docs/configuration/elements.md b/docs/configuration/elements.md index 5375a7e9f1f..148b5f39a54 100644 --- a/docs/configuration/elements.md +++ b/docs/configuration/elements.md @@ -19,6 +19,7 @@ Global point options: `Chart.defaults.global.elements.point` | -----| ---- | --------| ----------- | `radius` | `Number` | `3` | Point radius. | [`pointStyle`](#point-styles) | `String` | `circle` | Point style. +| `rotation` | `Number` | `0` | Point rotation (in degrees). | `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point fill color. | `borderWidth` | `Number` | `1` | Point stroke width. | `borderColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point stroke color. diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 6867b762d6f..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`. diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index b591dc1d13e..566bc5696bc 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -256,12 +256,13 @@ var myPieChart = new Chart(ctx, { // Display, position, and set styles for font tooltipEl.style.opacity = 1; tooltipEl.style.position = 'absolute'; - tooltipEl.style.left = position.left + tooltipModel.caretX + 'px'; - tooltipEl.style.top = position.top + tooltipModel.caretY + 'px'; + tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px'; + tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px'; tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily; tooltipEl.style.fontSize = tooltipModel.bodyFontSize + 'px'; tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle; tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px'; + tooltipEl.style.pointerEvents = 'none'; } } } diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index fa3511bb09a..551ac71ca97 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -37,6 +37,7 @@ The following commands are now available from the repository root: > gulp lint // perform code linting (ESLint) > gulp test // perform code linting and run unit tests > gulp docs // build the documentation in ./dist/docs +> gulp docs --watch // starts the gitbook live reloaded server ``` More information can be found in [gulpfile.js](https://github.com/chartjs/Chart.js/blob/master/gulpfile.js). diff --git a/docs/general/accessibility.md b/docs/general/accessibility.md new file mode 100644 index 00000000000..a59df93475a --- /dev/null +++ b/docs/general/accessibility.md @@ -0,0 +1,39 @@ +# Accessible Charts + +Chart.js charts are rendered on user provided `canvas` elements. Thus, it is up to the user to create the `canvas` element in a way that is accessible. The `canvas` element has support in all browsers and will render on screen but the `canvas` content will not be accessible to screen readers. + +With `canvas`, the accessibility has to be added with `ARIA` attributes on the `canvas` element or added using internal fallback content placed within the opening and closing canvas tags. + +This [website](http://pauljadam.com/demos/canvas.html) has a more detailed explanation of `canvas` accessibility as well as in depth examples. + +## Examples + +These are some examples of **accessible** `canvas` elements. + +By setting the `role` and `aria-label`, this `canvas` now has an accessible name. + +```html + +``` + +This `canvas` element has a text alternative via fallback content. + +```html + +

Hello Fallback World

+
+``` + +These are some bad examples of **inaccessible** `canvas` elements. + +This `canvas` element does not have an accessible name or role. + +```html + +``` + +This `canvas` element has inaccessible fallback content. + +```html +Your browser does not support the canvas element. +``` diff --git a/docs/general/responsive.md b/docs/general/responsive.md index f410826b84f..fd92aa494f0 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -16,6 +16,7 @@ Chart.js provides a [few options](#configuration-options) to enable responsivene | `responsive` | `Boolean` | `true` | Resizes the chart canvas when its container does ([important note...](#important-note)). | `responsiveAnimationDuration` | `Number` | `0` | Duration in milliseconds it takes to animate to new size after a resize event. | `maintainAspectRatio` | `Boolean` | `true` | Maintain the original canvas aspect ratio `(width / height)` when resizing. +| `aspectRatio` | `Number` | `2` | Canvas aspect ratio (i.e. `width / height`, a value of 1 representing a square canvas). Note that this option is ignored if the height is explicitly defined either as attribute or via the style. | `onResize` | `Function` | `null` | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. ## Important Note diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index ccc02ddc4b9..00b19b059e4 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -54,4 +54,4 @@ 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. +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. The Moment.js version in the bundled build is private to Chart.js so if you want to use Moment.js yourself, it's better to use Chart.js (non bundled) and import Moment.js manually. diff --git a/docs/getting-started/integration.md b/docs/getting-started/integration.md index 173448f58b4..95ccbc46d0a 100644 --- a/docs/getting-started/integration.md +++ b/docs/getting-started/integration.md @@ -2,13 +2,6 @@ Chart.js can be integrated with plain JavaScript or with different module loaders. The examples below show how to load Chart.js in different systems. -## ES6 Modules - -```javascript -import Chart from 'chart.js'; -var myChart = new Chart(ctx, {...}); -``` - ## Script Tag ```html @@ -18,6 +11,13 @@ var myChart = new Chart(ctx, {...}); ``` +## Webpack + +```javascript +import Chart from 'chart.js'; +var myChart = new Chart(ctx, {...}); +``` + ## Common JS ```javascript diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index fe2c337f156..97f70e8fadd 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -18,6 +18,8 @@ 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-style - Provides more styling options. - chartjs-plugin-waterfall - Enables easy use of waterfall charts. - chartjs-plugin-zoom - Enables zooming and panning on charts. @@ -55,6 +57,7 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ### Java - Chart.java + - Wicked-Charts ### GWT (Google Web toolkit) - Charba diff --git a/gulpfile.js b/gulpfile.js index 21f19645f81..f6795fa7f56 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -20,7 +20,7 @@ var yargs = require('yargs'); var path = require('path'); var fs = require('fs'); var htmllint = require('gulp-htmllint'); -var package = require('./package.json'); +var pkg = require('./package.json'); var argv = yargs .option('force-output', {default: false}) @@ -69,11 +69,11 @@ gulp.task('default', ['build', 'watch']); */ function bowerTask() { var json = JSON.stringify({ - name: package.name, - description: package.description, - homepage: package.homepage, - license: package.license, - version: package.version, + name: pkg.name, + description: pkg.description, + homepage: pkg.homepage, + license: pkg.license, + version: pkg.version, main: outDir + "Chart.js", ignore: [ '.github', @@ -112,11 +112,11 @@ function buildTask() { .on('error', errorHandler) .pipe(source('Chart.bundle.js')) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(gulp.dest(outDir)) .pipe(streamify(uglify())) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(streamify(concat('Chart.bundle.min.js'))) .pipe(gulp.dest(outDir)); @@ -127,11 +127,11 @@ function buildTask() { .on('error', errorHandler) .pipe(source('Chart.js')) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(gulp.dest(outDir)) .pipe(streamify(uglify())) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(streamify(concat('Chart.min.js'))) .pipe(gulp.dest(outDir)); @@ -190,7 +190,7 @@ function docsTask(done) { const cmd = process.execPath; exec([cmd, script, 'install', './'].join(' ')).then(() => { - return exec([cmd, script, 'build', './', './dist/docs'].join(' ')); + return exec([cmd, script, argv.watch ? 'serve' : 'build', './', './dist/docs'].join(' ')); }).catch((err) => { console.error(err.stdout); }).then(() => { diff --git a/karma.conf.js b/karma.conf.js index 5601cbd7212..5bb73ef03a0 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,9 +3,27 @@ module.exports = function(karma) { var args = karma.args || {}; var config = { - browsers: ['Firefox'], frameworks: ['browserify', 'jasmine'], reporters: ['progress', 'kjhtml'], + browsers: ['chrome', 'firefox'], + + // Explicitly disable hardware acceleration to make image + // diff more stable when ran on Travis and dev machine. + // https://github.com/chartjs/Chart.js/pull/5629 + customLaunchers: { + chrome: { + base: 'Chrome', + flags: [ + '--disable-accelerated-2d-canvas' + ] + }, + firefox: { + base: 'Firefox', + prefs: { + 'layers.acceleration.disabled': true + } + } + }, preprocessors: { './test/jasmine.index.js': ['browserify'], @@ -19,21 +37,12 @@ module.exports = function(karma) { // These settings deal with browser disconnects. We had seen test flakiness from Firefox // [Firefox 56.0.0 (Linux 0.0.0)]: Disconnected (1 times), because no message in 10000 ms. // https://github.com/jasmine/jasmine/issues/1327#issuecomment-332939551 - browserNoActivityTimeout: 60000, browserDisconnectTolerance: 3 }; // https://swizec.com/blog/how-to-run-javascript-tests-in-chrome-on-travis/swizec/6647 if (process.env.TRAVIS) { - config.browsers.push('chrome_travis_ci'); - config.customLaunchers = { - chrome_travis_ci: { - base: 'Chrome', - flags: ['--no-sandbox'] - } - }; - } else { - config.browsers.push('Chrome'); + config.customLaunchers.chrome.flags.push('--no-sandbox'); } if (args.coverage) { diff --git a/package.json b/package.json index a2e6c494af1..30e365f7467 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.2", + "version": "2.7.3", "license": "MIT", "main": "src/chart.js", "repository": { diff --git a/samples/advanced/progress-bar.html b/samples/advanced/progress-bar.html index b97a998ea95..b9c72446e59 100644 --- a/samples/advanced/progress-bar.html +++ b/samples/advanced/progress-bar.html @@ -28,6 +28,7 @@ data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ + label: 'My First dataset', fill: false, borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, diff --git a/samples/charts/bar/horizontal.html b/samples/charts/bar/horizontal.html index 6450e014d92..e174ad2ca35 100644 --- a/samples/charts/bar/horizontal.html +++ b/samples/charts/bar/horizontal.html @@ -102,7 +102,7 @@ var colorName = colorNames[horizontalBarChartData.datasets.length % colorNames.length]; var dsColor = window.chartColors[colorName]; var newDataset = { - label: 'Dataset ' + horizontalBarChartData.datasets.length, + label: 'Dataset ' + (horizontalBarChartData.datasets.length + 1), backgroundColor: color(dsColor).alpha(0.5).rgbString(), borderColor: dsColor, data: [] @@ -130,7 +130,7 @@ }); document.getElementById('removeDataset').addEventListener('click', function() { - horizontalBarChartData.datasets.splice(0, 1); + horizontalBarChartData.datasets.pop(); window.myHorizontalBar.update(); }); diff --git a/samples/charts/bar/vertical.html b/samples/charts/bar/vertical.html index e9348b274fd..5127d4937c2 100644 --- a/samples/charts/bar/vertical.html +++ b/samples/charts/bar/vertical.html @@ -95,7 +95,7 @@ var colorName = colorNames[barChartData.datasets.length % colorNames.length]; var dsColor = window.chartColors[colorName]; var newDataset = { - label: 'Dataset ' + barChartData.datasets.length, + label: 'Dataset ' + (barChartData.datasets.length + 1), backgroundColor: color(dsColor).alpha(0.5).rgbString(), borderColor: dsColor, borderWidth: 1, @@ -125,7 +125,7 @@ }); document.getElementById('removeDataset').addEventListener('click', function() { - barChartData.datasets.splice(0, 1); + barChartData.datasets.pop(); window.myBar.update(); }); diff --git a/samples/charts/doughnut.html b/samples/charts/doughnut.html index 446dcee4e2e..8466970a403 100644 --- a/samples/charts/doughnut.html +++ b/samples/charts/doughnut.html @@ -23,6 +23,7 @@ + diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index 792eef55306..b261b898661 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -56,12 +56,16 @@ var ctx = document.getElementById('chart1').getContext('2d'); ctx.canvas.width = 1000; ctx.canvas.height = 300; + + var color = Chart.helpers.color; var cfg = { type: 'bar', data: { labels: labels, datasets: [{ label: 'CHRT - Chart.js Corporation', + backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), + borderColor: window.chartColors.red, data: data, type: 'line', pointRadius: 0, diff --git a/samples/scales/time/line-point-data.html b/samples/scales/time/line-point-data.html index e18be91f3d8..604a49b4f1a 100644 --- a/samples/scales/time/line-point-data.html +++ b/samples/scales/time/line-point-data.html @@ -121,27 +121,16 @@ window.myLine.update(); }); - - // TODO : fix issue with addData - // See https://github.com/chartjs/Chart.js/issues/5197 - // The Add Data button for this sample has no effect. - // An error is logged in the console. document.getElementById('addData').addEventListener('click', function() { if (config.data.datasets.length > 0) { - var numTicks = window.myLine.scales['x-axis-0'].ticksAsTimestamps.length; - var lastTime = numTicks ? moment(window.myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); - - var newTime = lastTime - .clone() - .add(1, 'day') - .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(); } 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.bar.js b/src/controllers/controller.bar.js index ff2b56ae5a0..cadae3fdb6c 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -461,29 +461,6 @@ module.exports = function(Chart) { helpers.canvas.unclipArea(chart.ctx); }, - - setHoverStyle: function(rectangle) { - var dataset = this.chart.data.datasets[rectangle._datasetIndex]; - var index = rectangle._index; - var custom = rectangle.custom || {}; - var model = rectangle._model; - - model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.valueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); - model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.valueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.getHoverColor(model.borderColor)); - model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.valueAtIndexOrDefault(dataset.hoverBorderWidth, index, model.borderWidth); - }, - - removeHoverStyle: function(rectangle) { - var dataset = this.chart.data.datasets[rectangle._datasetIndex]; - var index = rectangle._index; - var custom = rectangle.custom || {}; - var model = rectangle._model; - var rectangleElementOptions = this.chart.options.elements.rectangle; - - model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.valueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor); - model.borderColor = custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor); - model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth); - } }); Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 65c6fae01b8..f14e512300f 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -87,6 +87,7 @@ module.exports = function(Chart) { borderWidth: options.borderWidth, hitRadius: options.hitRadius, pointStyle: options.pointStyle, + rotation: options.rotation, radius: reset ? 0 : options.radius, skip: custom.skip || isNaN(x) || isNaN(y), x: x, @@ -102,26 +103,18 @@ module.exports = function(Chart) { setHoverStyle: function(point) { var model = point._model; var options = point._options; - + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; model.backgroundColor = helpers.valueOrDefault(options.hoverBackgroundColor, helpers.getHoverColor(options.backgroundColor)); model.borderColor = helpers.valueOrDefault(options.hoverBorderColor, helpers.getHoverColor(options.borderColor)); model.borderWidth = helpers.valueOrDefault(options.hoverBorderWidth, options.borderWidth); model.radius = options.radius + options.hoverRadius; }, - /** - * @protected - */ - removeHoverStyle: function(point) { - var model = point._model; - var options = point._options; - - model.backgroundColor = options.backgroundColor; - model.borderColor = options.borderColor; - model.borderWidth = options.borderWidth; - model.radius = options.radius; - }, - /** * @private */ @@ -154,7 +147,8 @@ module.exports = function(Chart) { 'hoverBorderWidth', 'hoverRadius', 'hitRadius', - 'pointStyle' + 'pointStyle', + 'rotation' ]; for (i = 0, ilen = keys.length; i < ilen; ++i) { @@ -173,7 +167,6 @@ module.exports = function(Chart) { dataset.radius, options.radius ], context, index); - return values; } }); diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index f7b36e989a9..eb759fe60b8 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -229,8 +229,14 @@ module.exports = function(Chart) { }); var model = arc._model; + // Resets the visual styles - this.removeHoverStyle(arc); + var custom = arc.custom || {}; + var valueOrDefault = helpers.valueAtIndexOrDefault; + var elementOpts = this.chart.options.elements.arc; + model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor); + model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor); + model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth); // Set correct angles if not resetting if (!reset || !animationOpts.animateRotate) { @@ -246,10 +252,6 @@ module.exports = function(Chart) { arc.pivot(); }, - removeHoverStyle: function(arc) { - Chart.DatasetController.prototype.removeHoverStyle.call(this, arc, this.chart.options.elements.arc); - }, - calculateTotal: function() { var dataset = this.getDataset(); var meta = this.getMeta(); diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 9d9206d5cb2..4a18bdadb3c 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -148,6 +148,19 @@ module.exports = function(Chart) { return borderWidth; }, + getPointRotation: function(point, index) { + var pointRotation = this.chart.options.elements.point.rotation; + var dataset = this.getDataset(); + var custom = point.custom || {}; + + if (!isNaN(custom.rotation)) { + pointRotation = custom.rotation; + } else if (!isNaN(dataset.pointRotation) || helpers.isArray(dataset.pointRotation)) { + pointRotation = helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointRotation); + } + return pointRotation; + }, + updateElement: function(point, index, reset) { var me = this; var meta = me.getMeta(); @@ -185,6 +198,7 @@ module.exports = function(Chart) { // Appearance radius: custom.radius || helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointOptions.radius), pointStyle: custom.pointStyle || helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointOptions.pointStyle), + rotation: me.getPointRotation(point, index), backgroundColor: me.getPointBackgroundColor(point, index), borderColor: me.getPointBorderColor(point, index), borderWidth: me.getPointBorderWidth(point, index), @@ -283,15 +297,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) { @@ -299,35 +321,24 @@ module.exports = function(Chart) { } }, - setHoverStyle: function(point) { + setHoverStyle: function(element) { // Point - var dataset = this.chart.data.datasets[point._datasetIndex]; - var index = point._index; - var custom = point.custom || {}; - var model = point._model; + var dataset = this.chart.data.datasets[element._datasetIndex]; + var index = element._index; + var custom = element.custom || {}; + var model = element._model; + + element.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; - model.radius = custom.hoverRadius || helpers.valueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); model.backgroundColor = custom.hoverBackgroundColor || helpers.valueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); model.borderColor = custom.hoverBorderColor || helpers.valueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.getHoverColor(model.borderColor)); model.borderWidth = custom.hoverBorderWidth || helpers.valueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, model.borderWidth); + model.radius = custom.hoverRadius || helpers.valueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); }, - - removeHoverStyle: function(point) { - var me = this; - var dataset = me.chart.data.datasets[point._datasetIndex]; - var index = point._index; - var custom = point.custom || {}; - var model = point._model; - - // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { - dataset.pointRadius = dataset.radius; - } - - model.radius = custom.radius || helpers.valueAtIndexOrDefault(dataset.pointRadius, index, me.chart.options.elements.point.radius); - model.backgroundColor = me.getPointBackgroundColor(point, index); - model.borderColor = me.getPointBorderColor(point, index); - model.borderWidth = me.getPointBorderWidth(point, index); - } }); }; diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index e59aedfb98a..663a9534d55 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -117,25 +117,47 @@ module.exports = function(Chart) { linkScales: helpers.noop, update: function(reset) { + var me = this; + var dataset = me.getDataset(); + var meta = me.getMeta(); + var start = me.chart.options.startAngle || 0; + var starts = me._starts = []; + var angles = me._angles = []; + var i, ilen, angle; + + me._updateRadius(); + + meta.count = me.countVisibleElements(); + + for (i = 0, ilen = dataset.data.length; i < ilen; i++) { + starts[i] = start; + angle = me._computeAngle(i); + angles[i] = angle; + start += angle; + } + + helpers.each(meta.data, function(arc, index) { + me.updateElement(arc, index, reset); + }); + }, + + /** + * @private + */ + _updateRadius: function() { var me = this; var chart = me.chart; var chartArea = chart.chartArea; - var meta = me.getMeta(); var opts = chart.options; var arcOpts = opts.elements.arc; var minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + chart.outerRadius = Math.max((minSize - arcOpts.borderWidth / 2) / 2, 0); chart.innerRadius = Math.max(opts.cutoutPercentage ? (chart.outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); me.outerRadius = chart.outerRadius - (chart.radiusLength * me.index); me.innerRadius = me.outerRadius - chart.radiusLength; - - meta.count = me.countVisibleElements(); - - helpers.each(meta.data, function(arc, index) { - me.updateElement(arc, index, reset); - }); }, updateElement: function(arc, index, reset) { @@ -147,25 +169,14 @@ module.exports = function(Chart) { var scale = chart.scale; var labels = chart.data.labels; - var circumference = me.calculateCircumference(dataset.data[index]); var centerX = scale.xCenter; var centerY = scale.yCenter; - // If there is NaN data before us, we need to calculate the starting angle correctly. - // We could be way more efficient here, but its unlikely that the polar area chart will have a lot of data - var visibleCount = 0; - var meta = me.getMeta(); - for (var i = 0; i < index; ++i) { - if (!isNaN(dataset.data[i]) && !meta.data[i].hidden) { - ++visibleCount; - } - } - // var negHalfPI = -0.5 * Math.PI; var datasetStartAngle = opts.startAngle; var distance = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); - var startAngle = datasetStartAngle + (circumference * visibleCount); - var endAngle = startAngle + (arc.hidden ? 0 : circumference); + var startAngle = me._starts[index]; + var endAngle = startAngle + (arc.hidden ? 0 : me._angles[index]); var resetRadius = animationOpts.animateScale ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); @@ -188,13 +199,16 @@ module.exports = function(Chart) { }); // Apply border and fill style - me.removeHoverStyle(arc); + var elementOpts = this.chart.options.elements.arc; + var custom = arc.custom || {}; + var valueOrDefault = helpers.valueAtIndexOrDefault; + var model = arc._model; - arc.pivot(); - }, + model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor); + model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor); + model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth); - removeHoverStyle: function(arc) { - Chart.DatasetController.prototype.removeHoverStyle.call(this, arc, this.chart.options.elements.arc); + arc.pivot(); }, countVisibleElements: function() { @@ -211,12 +225,31 @@ module.exports = function(Chart) { return count; }, - calculateCircumference: function(value) { + /** + * @private + */ + _computeAngle: function(index) { + var me = this; var count = this.getMeta().count; - if (count > 0 && !isNaN(value)) { - return (2 * Math.PI) / count; + var dataset = me.getDataset(); + var meta = me.getMeta(); + + if (isNaN(dataset.data[index]) || meta.data[index].hidden) { + return 0; } - return 0; + + // Scriptable options + var context = { + chart: me.chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + return helpers.options.resolve([ + me.chart.options.elements.arc.angle, + (2 * Math.PI) / count + ], context, index); } }); }; diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 5de4e4ede0a..89717157a37 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -106,6 +106,7 @@ module.exports = function(Chart) { borderColor: custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor), borderWidth: custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth), pointStyle: custom.pointStyle ? custom.pointStyle : helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointElementOptions.pointStyle), + rotation: custom.rotation ? custom.rotation : helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointElementOptions.rotation), // Tooltip hitRadius: custom.hitRadius ? custom.hitRadius : helpers.valueAtIndexOrDefault(dataset.pointHitRadius, index, pointElementOptions.hitRadius) @@ -146,23 +147,17 @@ module.exports = function(Chart) { var index = point._index; var model = point._model; + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + model.radius = custom.hoverRadius ? custom.hoverRadius : helpers.valueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.valueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.valueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.getHoverColor(model.borderColor)); model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.valueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, model.borderWidth); }, - - removeHoverStyle: function(point) { - var dataset = this.chart.data.datasets[point._datasetIndex]; - var custom = point.custom || {}; - var index = point._index; - var model = point._model; - var pointElementOptions = this.chart.options.elements.point; - - model.radius = custom.radius ? custom.radius : helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointElementOptions.radius); - model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.valueAtIndexOrDefault(dataset.pointBackgroundColor, index, pointElementOptions.backgroundColor); - model.borderColor = custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor); - model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth); - } }); }; 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..48365fb224d 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; }, @@ -177,7 +181,7 @@ module.exports = function(Chart) { // the canvas render width and height will be casted to integers so make sure that // the canvas display style uses the same integer values to avoid blurring effect. - // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collased + // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed var newWidth = Math.max(0, Math.floor(helpers.getMaximumWidth(canvas))); var newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas))); @@ -203,7 +207,9 @@ module.exports = function(Chart) { } me.stop(); - me.update(me.options.responsiveAnimationDuration); + me.update({ + duration: me.options.responsiveAnimationDuration + }); } }, @@ -275,7 +281,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 +313,7 @@ module.exports = function(Chart) { me.scales = scales; - Chart.scaleService.addScalesToLayout(this); + scaleService.addScalesToLayout(this); }, buildOrUpdateControllers: function() { @@ -519,7 +525,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 +541,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; @@ -557,6 +563,10 @@ module.exports = function(Chart) { me.transition(easingValue); + if (me.width <= 0 || me.height <= 0) { + return; + } + if (plugins.notify(me, 'beforeDraw', [easingValue]) === false) { return; } @@ -775,7 +785,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, @@ -876,7 +886,10 @@ module.exports = function(Chart) { // We only need to render at this point. Updating will cause scales to be // recomputed generating flicker & using more memory than necessary. - me.render(me.options.hover.animationDuration, true); + me.render({ + duration: me.options.hover.animationDuration, + lazy: true + }); } me._bufferedRender = false; diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index ee6158c35b1..4928f98913e 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -238,16 +238,9 @@ module.exports = function(Chart) { } }, - removeHoverStyle: function(element, elementOpts) { - var dataset = this.chart.data.datasets[element._datasetIndex]; - var index = element._index; - var custom = element.custom || {}; - var valueOrDefault = helpers.valueAtIndexOrDefault; - var model = element._model; - - model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor); - model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor); - model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth); + removeHoverStyle: function(element) { + helpers.merge(element._model, element.$previousStyle || {}); + delete element.$previousStyle; }, setHoverStyle: function(element) { @@ -258,6 +251,12 @@ module.exports = function(Chart) { var getHoverColor = helpers.getHoverColor; var model = element._model; + element.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth + }; + model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : valueOrDefault(dataset.hoverBackgroundColor, index, getHoverColor(model.backgroundColor)); model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : valueOrDefault(dataset.hoverBorderColor, index, getHoverColor(model.borderColor)); model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : valueOrDefault(dataset.hoverBorderWidth, index, model.borderWidth); diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 347fd958e83..5cd1d8f6e91 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); @@ -382,7 +383,7 @@ module.exports = function(Chart) { helpers.getRelativePosition = function(evt, chart) { var mouseX, mouseY; var e = evt.originalEvent || evt; - var canvas = evt.currentTarget || evt.srcElement; + var canvas = evt.target || evt.srcElement; var boundingRect = canvas.getBoundingClientRect(); var touches = e.touches; @@ -449,7 +450,7 @@ module.exports = function(Chart) { // @see http://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser function getConstraintDimension(domNode, maxStyle, percentageProperty) { var view = document.defaultView; - var parentNode = domNode.parentNode; + var parentNode = helpers._getParentNode(domNode); var constrainedNode = view.getComputedStyle(domNode)[maxStyle]; var constrainedContainer = view.getComputedStyle(parentNode)[maxStyle]; var hasCNode = isConstrainedValue(constrainedNode); @@ -472,27 +473,49 @@ 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); + }; + /** + * @private + */ + helpers._getParentNode = function(domNode) { + var parent = domNode.parentNode; + if (parent && parent.host) { + parent = parent.host; + } + return parent; + }; helpers.getMaximumWidth = function(domNode) { - var container = domNode.parentNode; + var container = helpers._getParentNode(domNode); 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); }; helpers.getMaximumHeight = function(domNode) { - var container = domNode.parentNode; + var container = helpers._getParentNode(domNode); if (!container) { 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); }; @@ -502,7 +525,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 1996c6c894d..78ede6985f6 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -89,848 +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. - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; + 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; } + } - // 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; + } + + // 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]); + }, - labelRotation++; - labelWidth = cosRotation * originalLabelWidth; + // + + 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(); + + 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); } + } - // 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; - } + // Value is good, return it + return rawValue; + }, - 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; + /** + * 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; } - if (isHorizontal) { - skipRatio = false; + 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))); + }, - 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))); - } + /** + * 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); + }, - // 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)); - } - } + /** + * 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()); + }, - for (i = 0; i < tickCount; i++) { - tick = ticks[i]; + getBaseValue: function() { + var me = this; + var min = me.min; + var max = me.max; - // 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 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; + } + + if (isHorizontal) { + skipRatio = false; + + 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))); } - 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; + + // 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 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.right : me.right - axisWidth - tl; - var xTickEnd = options.position === 'right' ? me.right + tl : me.right; - var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; - var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; - - helpers.each(ticks, function(tick, index) { - // autoskipper skipped this tick (#4635) - if (helpers.isNullOrUndef(tick.label)) { - return; - } + for (i = 0; i < tickCount; i++) { + tick = ticks[i]; - 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); - } + // 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; + }, - // 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; - } + // 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 xLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); - if (xLineValue < me.left) { - lineColor = 'rgba(0,0,0,0)'; - } - xLineValue += helpers.aliasPixel(lineWidth); + 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; + } - labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) + 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; + + if (isHorizontal) { + var labelYOffset = tl + tickPadding; - tx1 = tx2 = x1 = x2 = xLineValue; - ty1 = yTickStart; - ty2 = yTickEnd; - y1 = chartArea.top; - y2 = chartArea.bottom + axisWidth; + 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 + axisWidth; - 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)) { - 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 { - 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 + 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; + 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.ticks.js b/src/core/core.ticks.js index 3898842a49a..f63e525983a 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -46,9 +46,15 @@ module.exports = { var tickString = ''; if (tickValue !== 0) { - var numDecimal = -1 * Math.floor(logDelta); - numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places - tickString = tickValue.toFixed(numDecimal); + var maxTick = Math.max(Math.abs(ticks[0]), Math.abs(ticks[ticks.length - 1])); + if (maxTick < 1e-4) { // all ticks are small numbers; use scientific notation + var logTick = helpers.log10(Math.abs(tickValue)); + tickString = tickValue.toExponential(Math.floor(logTick) - Math.floor(logDelta)); + } else { + var numDecimal = -1 * Math.floor(logDelta); + numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places + tickString = tickValue.toFixed(numDecimal); + } } else { tickString = '0'; // never show decimal places for 0 } diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 9b09d760443..c529812cc61 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -96,853 +96,878 @@ 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; +} + +/** + * Returns array of strings split by newline + * @param {String} value - The value to split by newline. + * @returns {Array} value if newline present - Returned from String split() method + * @function + */ +function splitNewlines(str) { + if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) { + return str.split('\n'); + } + return str; +} + + +// 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; + }; + rf = function(x) { + return x > midX; }; - orf = function(x) { - return x - size.width - model.caretSize - model.caretPadding < 0; + } else { + lf = function(x) { + return x <= (size.width / 2); }; - yf = function(y) { - return y <= midY ? 'top' : 'bottom'; + 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 + }; +} + +/** + * Helper to build before and after body lines + */ +function getBeforeAfterBodyLines(callback) { + return pushOrConcat([], splitNewlines(callback)); +} + +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, splitNewlines(beforeTitle)); + lines = pushOrConcat(lines, splitNewlines(title)); + lines = pushOrConcat(lines, splitNewlines(afterTitle)); + + return lines; + }, + + // Args are: (tooltipItem, data) + getBeforeBody: function() { + return getBeforeAfterBodyLines(this._options.callbacks.beforeBody.apply(this, arguments)); + }, + + // 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, splitNewlines(callbacks.beforeLabel.call(me, tooltipItem, data))); + pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.after, splitNewlines(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() { + return getBeforeAfterBodyLines(this._options.callbacks.afterBody.apply(this, arguments)); + }, + + // 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, splitNewlines(beforeFooter)); + lines = pushOrConcat(lines, splitNewlines(footer)); + lines = pushOrConcat(lines, splitNewlines(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; } - }, - drawBody: function(pt, vm, ctx, opacity) { - var bodyFontSize = vm.bodyFontSize; - var bodySpacing = vm.bodySpacing; - var body = vm.body; + } 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; - 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 eab5b31d453..2bcdc88f0f8 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -68,39 +68,22 @@ module.exports = Element.extend({ var model = this._model; var ctx = this._chart.ctx; var pointStyle = vm.pointStyle; + var rotation = vm.rotation; 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; } - ctx.strokeStyle = vm.borderColor || defaultColor; - ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); - ctx.fillStyle = vm.backgroundColor || defaultColor; - - // Cliping for Points. - // going out from inner charArea? - if ((chartArea !== undefined) && ((model.x < chartArea.left) || (chartArea.right * errMargin < model.x) || (model.y < chartArea.top) || (chartArea.bottom * errMargin < model.y))) { - // Point fade out - if (model.x < chartArea.left) { - ratio = (x - model.x) / (chartArea.left - model.x); - } else if (chartArea.right * errMargin < model.x) { - ratio = (model.x - x) / (model.x - chartArea.right); - } else if (model.y < chartArea.top) { - ratio = (y - model.y) / (chartArea.top - model.y); - } else if (chartArea.bottom * errMargin < model.y) { - ratio = (model.y - y) / (model.y - chartArea.bottom); - } - ratio = Math.round(ratio * 100) / 100; - ctx.strokeStyle = color(ctx.strokeStyle).alpha(ratio).rgbString(); - ctx.fillStyle = color(ctx.fillStyle).alpha(ratio).rgbString(); + // Clipping for Points. + if (chartArea === undefined || (model.x >= chartArea.left && chartArea.right * errMargin >= model.x && model.y >= chartArea.top && chartArea.bottom * errMargin >= model.y)) { + ctx.strokeStyle = vm.borderColor || defaultColor; + ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); + ctx.fillStyle = vm.backgroundColor || defaultColor; + helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y, rotation); } - - helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y); } }); diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 13b110268b9..60fb6e1a299 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -27,25 +27,30 @@ var exports = module.exports = { */ roundedRect: function(ctx, x, y, width, height, radius) { if (radius) { - var rx = Math.min(radius, width / 2); - var ry = Math.min(radius, height / 2); - - ctx.moveTo(x + rx, y); - ctx.lineTo(x + width - rx, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + ry); - ctx.lineTo(x + width, y + height - ry); - ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); - ctx.lineTo(x + rx, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - ry); - ctx.lineTo(x, y + ry); - ctx.quadraticCurveTo(x, y, x + rx, y); + // NOTE(SB) `epsilon` helps to prevent minor artifacts appearing + // on Chrome when `r` is exactly half the height or the width. + var epsilon = 0.0000001; + var r = Math.min(radius, (height / 2) - epsilon, (width / 2) - epsilon); + + ctx.moveTo(x + r, y); + ctx.lineTo(x + width - r, y); + ctx.arcTo(x + width, y, x + width, y + r, r); + ctx.lineTo(x + width, y + height - r); + ctx.arcTo(x + width, y + height, x + width - r, y + height, r); + ctx.lineTo(x + r, y + height); + ctx.arcTo(x, y + height, x, y + height - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.moveTo(x, y); } else { ctx.rect(x, y, width, height); } }, - drawPoint: function(ctx, style, radius, x, y) { + drawPoint: function(ctx, style, radius, x, y, rotation) { var type, edgeLength, xOffset, yOffset, height, size; + rotation = rotation || 0; if (style && typeof style === 'object') { type = style.toString(); @@ -59,97 +64,88 @@ var exports = module.exports = { return; } + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotation * Math.PI / 180); + ctx.beginPath(); + switch (style) { // Default includes circle default: - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.arc(0, 0, radius, 0, Math.PI * 2); ctx.closePath(); - ctx.fill(); break; case 'triangle': - ctx.beginPath(); edgeLength = 3 * radius / Math.sqrt(3); height = edgeLength * Math.sqrt(3) / 2; - ctx.moveTo(x - edgeLength / 2, y + height / 3); - ctx.lineTo(x + edgeLength / 2, y + height / 3); - ctx.lineTo(x, y - 2 * height / 3); + ctx.moveTo(-edgeLength / 2, height / 3); + ctx.lineTo(edgeLength / 2, height / 3); + ctx.lineTo(0, -2 * height / 3); ctx.closePath(); - ctx.fill(); break; case 'rect': size = 1 / Math.SQRT2 * radius; - ctx.beginPath(); - ctx.fillRect(x - size, y - size, 2 * size, 2 * size); - ctx.strokeRect(x - size, y - size, 2 * size, 2 * size); + ctx.rect(-size, -size, 2 * size, 2 * size); break; case 'rectRounded': var offset = radius / Math.SQRT2; - var leftX = x - offset; - var topY = y - offset; + var leftX = -offset; + var topY = -offset; var sideSize = Math.SQRT2 * radius; - ctx.beginPath(); - this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius / 2); - ctx.closePath(); - ctx.fill(); + + // NOTE(SB) the rounded rect implementation changed to use `arcTo` + // instead of `quadraticCurveTo` since it generates better results + // when rect is almost a circle. 0.425 (instead of 0.5) produces + // results visually closer to the previous impl. + this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius * 0.425); break; case 'rectRot': size = 1 / Math.SQRT2 * radius; - ctx.beginPath(); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y - size); + ctx.moveTo(-size, 0); + ctx.lineTo(0, size); + ctx.lineTo(size, 0); + ctx.lineTo(0, -size); ctx.closePath(); - ctx.fill(); break; case 'cross': - ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); - ctx.closePath(); + ctx.moveTo(0, radius); + ctx.lineTo(0, -radius); + ctx.moveTo(-radius, 0); + ctx.lineTo(radius, 0); break; case 'crossRot': - ctx.beginPath(); xOffset = Math.cos(Math.PI / 4) * radius; yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); - ctx.closePath(); + ctx.moveTo(-xOffset, -yOffset); + ctx.lineTo(xOffset, yOffset); + ctx.moveTo(-xOffset, yOffset); + ctx.lineTo(xOffset, -yOffset); break; case 'star': - ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(0, radius); + ctx.lineTo(0, -radius); + ctx.moveTo(-radius, 0); + ctx.lineTo(radius, 0); xOffset = Math.cos(Math.PI / 4) * radius; yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); - ctx.closePath(); + ctx.moveTo(-xOffset, -yOffset); + ctx.lineTo(xOffset, yOffset); + ctx.moveTo(-xOffset, yOffset); + ctx.lineTo(xOffset, -yOffset); break; case 'line': - ctx.beginPath(); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); - ctx.closePath(); + ctx.moveTo(-radius, 0); + ctx.lineTo(radius, 0); break; case 'dash': - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + radius, y); - ctx.closePath(); + ctx.moveTo(0, 0); + ctx.lineTo(radius, 0); break; } + ctx.fill(); ctx.stroke(); + ctx.restore(); }, clipArea: function(ctx, area) { @@ -210,5 +206,4 @@ helpers.clear = exports.clear; helpers.drawRoundedRectangle = function(ctx) { ctx.beginPath(); exports.roundedRect.apply(exports, arguments); - ctx.closePath(); }; 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..7408eb9f989 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,18 +15,28 @@ 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; // If min, max and stepSize is set and they make an evenly spaced scale use it. - if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + if (!helpers.isNullOrUndef(generationOptions.min) && !helpers.isNullOrUndef(generationOptions.max) && generationOptions.stepSize) { // If very close to our whole number, use it. if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { niceMin = generationOptions.min; @@ -41,9 +52,9 @@ function generateTicks(generationOptions, dataRange) { numSpaces = Math.ceil(numSpaces); } - var precision = 1; + precision = 1; if (spacing < 1) { - precision = Math.pow(10, spacing.toString().length - 2); + precision = Math.pow(10, 1 - Math.floor(helpers.log10(spacing))); niceMin = Math.round(niceMin * precision) / precision; niceMax = Math.round(niceMax * 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 e36b4e13539..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) { @@ -524,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..2496a487c3e 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; @@ -20,12 +22,12 @@ var INTERVALS = { second: { common: true, size: 1000, - steps: [1, 2, 5, 10, 30] + steps: [1, 2, 5, 10, 15, 30] }, minute: { common: true, size: 60000, - steps: [1, 2, 5, 10, 30] + steps: [1, 2, 5, 10, 15, 30] }, hour: { common: true, @@ -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/controller.bubble/point-style.json b/test/fixtures/controller.bubble/point-style.json new file mode 100644 index 00000000000..a645075168b --- /dev/null +++ b/test/fixtures/controller.bubble/point-style.json @@ -0,0 +1,129 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3}, + {"x": 1, "y": 3}, + {"x": 2, "y": 3}, + {"x": 3, "y": 3}, + {"x": 4, "y": 3}, + {"x": 5, "y": 3}, + {"x": 6, "y": 3}, + {"x": 7, "y": 3}, + {"x": 8, "y": 3}, + {"x": 9, "y": 3} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "data": [ + {"x": 0, "y": 2}, + {"x": 1, "y": 2}, + {"x": 2, "y": 2}, + {"x": 3, "y": 2}, + {"x": 4, "y": 2}, + {"x": 5, "y": 2}, + {"x": 6, "y": 2}, + {"x": 7, "y": 2}, + {"x": 8, "y": 2}, + {"x": 9, "y": 2} + ], + "backgroundColor": "transparent", + "borderColor": "0000ff", + "borderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "data": [ + {"x": 0, "y": 1}, + {"x": 1, "y": 1}, + {"x": 2, "y": 1}, + {"x": 3, "y": 1}, + {"x": 4, "y": 1}, + {"x": 5, "y": 1}, + {"x": 6, "y": 1}, + {"x": 7, "y": 1}, + {"x": 8, "y": 1}, + {"x": 9, "y": 1} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{ + "display": false, + "ticks": { + "min": 0, + "max": 4 + } + }] + }, + "elements": { + "line": { + "borderColor": "transparent", + "borderWidth": 0, + "fill": false + }, + "point": { + "radius": 16 + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bubble/point-style.png b/test/fixtures/controller.bubble/point-style.png new file mode 100644 index 00000000000..f1d3b8168c0 Binary files /dev/null and b/test/fixtures/controller.bubble/point-style.png differ diff --git a/test/fixtures/controller.line/point-style.json b/test/fixtures/controller.line/point-style.json new file mode 100644 index 00000000000..ef295ad5914 --- /dev/null +++ b/test/fixtures/controller.line/point-style.json @@ -0,0 +1,98 @@ +{ + "config": { + "type": "line", + "data": { + "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "datasets": [{ + "borderColor": "transparent", + "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "transparent", + "pointBorderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "pointBackgroundColor": "transparent", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{ + "display": false, + "ticks": { + "min": 0, + "max": 4 + } + }] + }, + "elements": { + "line": { + "fill": false + }, + "point": { + "radius": 16 + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/point-style.png b/test/fixtures/controller.line/point-style.png new file mode 100644 index 00000000000..f177fbfe15e Binary files /dev/null and b/test/fixtures/controller.line/point-style.png differ diff --git a/test/fixtures/controller.polarArea/angle-array.json b/test/fixtures/controller.polarArea/angle-array.json new file mode 100644 index 00000000000..f8fce27b555 --- /dev/null +++ b/test/fixtures/controller.polarArea/angle-array.json @@ -0,0 +1,34 @@ +{ + "config": { + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [11, 16, 21, 7, 10], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)", + "rgba(255, 159, 64, 0.8)" + ] + }] + }, + "options": { + "elements": { + "arc": { + "angle": [ + 1.0566, 1.7566, 1.0566, 2.1566, 0.2566 + ] + } + }, + "responsive": false, + "legend": false, + "title": false, + "scale": { + "display": false + } + } + } +} diff --git a/test/fixtures/controller.polarArea/angle-array.png b/test/fixtures/controller.polarArea/angle-array.png new file mode 100644 index 00000000000..9594be93ac2 Binary files /dev/null and b/test/fixtures/controller.polarArea/angle-array.png differ diff --git a/test/fixtures/controller.polarArea/angle-undefined.json b/test/fixtures/controller.polarArea/angle-undefined.json new file mode 100644 index 00000000000..ef83f1b8772 --- /dev/null +++ b/test/fixtures/controller.polarArea/angle-undefined.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [11, 16, 21, 7, 10], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)", + "rgba(255, 159, 64, 0.8)" + ] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scale": { + "display": false + } + } + } +} diff --git a/test/fixtures/controller.polarArea/angle-undefined.png b/test/fixtures/controller.polarArea/angle-undefined.png new file mode 100644 index 00000000000..df9b4bf7909 Binary files /dev/null and b/test/fixtures/controller.polarArea/angle-undefined.png differ diff --git a/test/fixtures/controller.radar/point-style.json b/test/fixtures/controller.radar/point-style.json new file mode 100644 index 00000000000..913c1cb87cd --- /dev/null +++ b/test/fixtures/controller.radar/point-style.json @@ -0,0 +1,95 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "datasets": [{ + "borderColor": "transparent", + "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "transparent", + "pointBorderWidth": 0, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "pointBackgroundColor": "transparent", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scale": { + "display": false, + "ticks": { + "min": 0, + "max": 3 + } + }, + "elements": { + "line": { + "fill": false + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 512, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.radar/point-style.png b/test/fixtures/controller.radar/point-style.png new file mode 100644 index 00000000000..562cb620b05 Binary files /dev/null and b/test/fixtures/controller.radar/point-style.png differ 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/fixtures/element.point/point-style-circle.json b/test/fixtures/element.point/point-style-circle.json new file mode 100644 index 00000000000..714d421a049 --- /dev/null +++ b/test/fixtures/element.point/point-style-circle.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "circle" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-circle.png b/test/fixtures/element.point/point-style-circle.png new file mode 100644 index 00000000000..d7b9bf531b2 Binary files /dev/null and b/test/fixtures/element.point/point-style-circle.png differ diff --git a/test/fixtures/element.point/point-style-cross-rot.json b/test/fixtures/element.point/point-style-cross-rot.json new file mode 100644 index 00000000000..e81b8b73113 --- /dev/null +++ b/test/fixtures/element.point/point-style-cross-rot.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "crossRot" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-cross-rot.png b/test/fixtures/element.point/point-style-cross-rot.png new file mode 100644 index 00000000000..3f6d1df091c Binary files /dev/null and b/test/fixtures/element.point/point-style-cross-rot.png differ diff --git a/test/fixtures/element.point/point-style-cross.json b/test/fixtures/element.point/point-style-cross.json new file mode 100644 index 00000000000..5eb99441fce --- /dev/null +++ b/test/fixtures/element.point/point-style-cross.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "cross" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-cross.png b/test/fixtures/element.point/point-style-cross.png new file mode 100644 index 00000000000..ecf3cda77e8 Binary files /dev/null and b/test/fixtures/element.point/point-style-cross.png differ diff --git a/test/fixtures/element.point/point-style-dash.json b/test/fixtures/element.point/point-style-dash.json new file mode 100644 index 00000000000..16f059eef0d --- /dev/null +++ b/test/fixtures/element.point/point-style-dash.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "dash" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-dash.png b/test/fixtures/element.point/point-style-dash.png new file mode 100644 index 00000000000..9c381d83d54 Binary files /dev/null and b/test/fixtures/element.point/point-style-dash.png differ diff --git a/test/fixtures/element.point/point-style-line.json b/test/fixtures/element.point/point-style-line.json new file mode 100644 index 00000000000..eda6d98f62c --- /dev/null +++ b/test/fixtures/element.point/point-style-line.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "line" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-line.png b/test/fixtures/element.point/point-style-line.png new file mode 100644 index 00000000000..2911fea9775 Binary files /dev/null and b/test/fixtures/element.point/point-style-line.png differ diff --git a/test/fixtures/element.point/point-style-rect-rot.json b/test/fixtures/element.point/point-style-rect-rot.json new file mode 100644 index 00000000000..85875655e1c --- /dev/null +++ b/test/fixtures/element.point/point-style-rect-rot.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "rectRot" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect-rot.png b/test/fixtures/element.point/point-style-rect-rot.png new file mode 100644 index 00000000000..09c0adac3d3 Binary files /dev/null and b/test/fixtures/element.point/point-style-rect-rot.png differ diff --git a/test/fixtures/element.point/point-style-rect-rounded.json b/test/fixtures/element.point/point-style-rect-rounded.json new file mode 100644 index 00000000000..6d65b46d820 --- /dev/null +++ b/test/fixtures/element.point/point-style-rect-rounded.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "rectRounded" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect-rounded.png b/test/fixtures/element.point/point-style-rect-rounded.png new file mode 100644 index 00000000000..a58e9e62361 Binary files /dev/null and b/test/fixtures/element.point/point-style-rect-rounded.png differ diff --git a/test/fixtures/element.point/point-style-rect.json b/test/fixtures/element.point/point-style-rect.json new file mode 100644 index 00000000000..5e2be596366 --- /dev/null +++ b/test/fixtures/element.point/point-style-rect.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "rect" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect.png b/test/fixtures/element.point/point-style-rect.png new file mode 100644 index 00000000000..493c9a69272 Binary files /dev/null and b/test/fixtures/element.point/point-style-rect.png differ diff --git a/test/fixtures/element.point/point-style-star.json b/test/fixtures/element.point/point-style-star.json new file mode 100644 index 00000000000..58ea577d44a --- /dev/null +++ b/test/fixtures/element.point/point-style-star.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "star" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-star.png b/test/fixtures/element.point/point-style-star.png new file mode 100644 index 00000000000..b5b58c0a486 Binary files /dev/null and b/test/fixtures/element.point/point-style-star.png differ diff --git a/test/fixtures/element.point/point-style-triangle.json b/test/fixtures/element.point/point-style-triangle.json new file mode 100644 index 00000000000..e7657f1af25 --- /dev/null +++ b/test/fixtures/element.point/point-style-triangle.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "triangle" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-triangle.png b/test/fixtures/element.point/point-style-triangle.png new file mode 100644 index 00000000000..d2cc5c6f1e8 Binary files /dev/null and b/test/fixtures/element.point/point-style-triangle.png differ diff --git a/test/jasmine.context.js b/test/jasmine.context.js index 8f4171fee4d..3497c721918 100644 --- a/test/jasmine.context.js +++ b/test/jasmine.context.js @@ -75,6 +75,7 @@ Context.prototype._initMethods = function() { var me = this; var methods = { arc: function() {}, + arcTo: function() {}, beginPath: function() {}, bezierCurveTo: function() {}, clearRect: function() {}, diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index 096b2e8e116..7957b6ab5db 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -1372,13 +1372,21 @@ describe('Chart.controllers.bar', function() { var meta = chart.getDatasetMeta(1); var bar = meta.data[0]; + var helpers = window.Chart.helpers; // Change default chart.options.elements.rectangle.backgroundColor = 'rgb(128, 128, 128)'; chart.options.elements.rectangle.borderColor = 'rgb(15, 15, 15)'; chart.options.elements.rectangle.borderWidth = 3.14; - // Remove to defaults + chart.update(); + expect(bar._model.backgroundColor).toBe('rgb(128, 128, 128)'); + expect(bar._model.borderColor).toBe('rgb(15, 15, 15)'); + expect(bar._model.borderWidth).toBe(3.14); + meta.controller.setHoverStyle(bar); + expect(bar._model.backgroundColor).toBe(helpers.getHoverColor('rgb(128, 128, 128)')); + expect(bar._model.borderColor).toBe(helpers.getHoverColor('rgb(15, 15, 15)')); + expect(bar._model.borderWidth).toBe(3.14); meta.controller.removeHoverStyle(bar); expect(bar._model.backgroundColor).toBe('rgb(128, 128, 128)'); expect(bar._model.borderColor).toBe('rgb(15, 15, 15)'); @@ -1389,6 +1397,14 @@ describe('Chart.controllers.bar', function() { chart.data.datasets[1].borderColor = ['rgb(9, 9, 9)', 'rgb(0, 0, 0)']; chart.data.datasets[1].borderWidth = [2.5, 5]; + chart.update(); + expect(bar._model.backgroundColor).toBe('rgb(255, 255, 255)'); + expect(bar._model.borderColor).toBe('rgb(9, 9, 9)'); + expect(bar._model.borderWidth).toBe(2.5); + meta.controller.setHoverStyle(bar); + expect(bar._model.backgroundColor).toBe(helpers.getHoverColor('rgb(255, 255, 255)')); + expect(bar._model.borderColor).toBe(helpers.getHoverColor('rgb(9, 9, 9)')); + expect(bar._model.borderWidth).toBe(2.5); meta.controller.removeHoverStyle(bar); expect(bar._model.backgroundColor).toBe('rgb(255, 255, 255)'); expect(bar._model.borderColor).toBe('rgb(9, 9, 9)'); @@ -1401,6 +1417,14 @@ describe('Chart.controllers.bar', function() { borderWidth: 1.5 }; + chart.update(); + expect(bar._model.backgroundColor).toBe('rgb(255, 0, 0)'); + expect(bar._model.borderColor).toBe('rgb(0, 255, 0)'); + expect(bar._model.borderWidth).toBe(1.5); + meta.controller.setHoverStyle(bar); + expect(bar._model.backgroundColor).toBe(helpers.getHoverColor('rgb(255, 0, 0)')); + expect(bar._model.borderColor).toBe(helpers.getHoverColor('rgb(0, 255, 0)')); + expect(bar._model.borderWidth).toBe(1.5); meta.controller.removeHoverStyle(bar); expect(bar._model.backgroundColor).toBe('rgb(255, 0, 0)'); expect(bar._model.borderColor).toBe('rgb(0, 255, 0)'); diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index 4799883ddfb..2597b9ae641 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -1,4 +1,6 @@ describe('Chart.controllers.bubble', function() { + describe('auto', jasmine.specsFromFixtures('controller.bubble')); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'bubble', diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index a4d6526c912..b1969e2817a 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -355,6 +355,8 @@ describe('Chart.controllers.doughnut', function() { var meta = chart.getDatasetMeta(0); var arc = meta.data[0]; + chart.update(); + meta.controller.setHoverStyle(arc); meta.controller.removeHoverStyle(arc); expect(arc._model.backgroundColor).toBe('rgb(255, 0, 0)'); expect(arc._model.borderColor).toBe('rgb(0, 0, 255)'); @@ -365,6 +367,8 @@ describe('Chart.controllers.doughnut', function() { chart.data.datasets[0].borderColor = 'rgb(18, 18, 18)'; chart.data.datasets[0].borderWidth = 1.56; + chart.update(); + meta.controller.setHoverStyle(arc); meta.controller.removeHoverStyle(arc); expect(arc._model.backgroundColor).toBe('rgb(9, 9, 9)'); expect(arc._model.borderColor).toBe('rgb(18, 18, 18)'); @@ -375,6 +379,8 @@ describe('Chart.controllers.doughnut', function() { chart.data.datasets[0].borderColor = ['rgb(18, 18, 18)']; chart.data.datasets[0].borderWidth = [0.1, 1.56]; + chart.update(); + meta.controller.setHoverStyle(arc); meta.controller.removeHoverStyle(arc); expect(arc._model.backgroundColor).toBe('rgb(255, 255, 255)'); expect(arc._model.borderColor).toBe('rgb(18, 18, 18)'); @@ -387,6 +393,8 @@ describe('Chart.controllers.doughnut', function() { borderWidth: 3.14159, }; + chart.update(); + meta.controller.setHoverStyle(arc); meta.controller.removeHoverStyle(arc); expect(arc._model.backgroundColor).toBe('rgb(7, 7, 7)'); expect(arc._model.borderColor).toBe('rgb(17, 17, 17)'); diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index 31c0763a8fe..ed8f4ec5c80 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -1,4 +1,6 @@ describe('Chart.controllers.line', function() { + describe('auto', jasmine.specsFromFixtures('controller.line')); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'line', @@ -703,6 +705,7 @@ describe('Chart.controllers.line', function() { chart.options.elements.point.radius = 1.01; meta.controller.removeHoverStyle(point); + chart.update(); expect(point._model.backgroundColor).toBe('rgb(45, 46, 47)'); expect(point._model.borderColor).toBe('rgb(50, 51, 52)'); expect(point._model.borderWidth).toBe(10.1); @@ -715,6 +718,7 @@ describe('Chart.controllers.line', function() { chart.data.datasets[0].pointBorderWidth = 2.1; meta.controller.removeHoverStyle(point); + chart.update(); expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); expect(point._model.borderWidth).toBe(2.1); @@ -726,6 +730,7 @@ describe('Chart.controllers.line', function() { chart.data.datasets[0].radius = 20; meta.controller.removeHoverStyle(point); + chart.update(); expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); expect(point._model.borderWidth).toBe(2.1); @@ -740,6 +745,7 @@ describe('Chart.controllers.line', function() { }; meta.controller.removeHoverStyle(point); + chart.update(); expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); expect(point._model.borderWidth).toBe(5.5); diff --git a/test/specs/controller.polarArea.tests.js b/test/specs/controller.polarArea.tests.js index 050bd68b80f..11e0be0b7d3 100644 --- a/test/specs/controller.polarArea.tests.js +++ b/test/specs/controller.polarArea.tests.js @@ -1,3 +1,5 @@ +describe('auto', jasmine.specsFromFixtures('controller.polarArea')); + describe('Chart.controllers.polarArea', function() { it('should be constructed', function() { var chart = window.acquireChart({ @@ -330,7 +332,14 @@ describe('Chart.controllers.polarArea', function() { chart.options.elements.arc.borderColor = 'rgb(50, 51, 52)'; chart.options.elements.arc.borderWidth = 10.1; + meta.controller.setHoverStyle(arc); + chart.update(); + expect(arc._model.backgroundColor).toBe('rgb(45, 46, 47)'); + expect(arc._model.borderColor).toBe('rgb(50, 51, 52)'); + expect(arc._model.borderWidth).toBe(10.1); + meta.controller.removeHoverStyle(arc); + chart.update(); expect(arc._model.backgroundColor).toBe('rgb(45, 46, 47)'); expect(arc._model.borderColor).toBe('rgb(50, 51, 52)'); expect(arc._model.borderWidth).toBe(10.1); @@ -341,6 +350,7 @@ describe('Chart.controllers.polarArea', function() { chart.data.datasets[0].borderWidth = 2.1; meta.controller.removeHoverStyle(arc); + chart.update(); expect(arc._model.backgroundColor).toBe('rgb(77, 79, 81)'); expect(arc._model.borderColor).toBe('rgb(123, 125, 127)'); expect(arc._model.borderWidth).toBe(2.1); @@ -353,6 +363,7 @@ describe('Chart.controllers.polarArea', function() { }; meta.controller.removeHoverStyle(arc); + chart.update(); expect(arc._model.backgroundColor).toBe('rgb(0, 0, 0)'); expect(arc._model.borderColor).toBe('rgb(10, 10, 10)'); expect(arc._model.borderWidth).toBe(5.5); diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index df1001420c4..2bc0a2e0d24 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -1,4 +1,6 @@ describe('Chart.controllers.radar', function() { + describe('auto', jasmine.specsFromFixtures('controller.radar')); + it('Should be constructed', function() { var chart = window.acquireChart({ type: 'radar', @@ -410,6 +412,7 @@ describe('Chart.controllers.radar', function() { chart.options.elements.point.radius = 1.01; meta.controller.removeHoverStyle(point); + chart.update(); expect(point._model.backgroundColor).toBe('rgb(45, 46, 47)'); expect(point._model.borderColor).toBe('rgb(50, 51, 52)'); expect(point._model.borderWidth).toBe(10.1); @@ -422,6 +425,7 @@ describe('Chart.controllers.radar', function() { chart.data.datasets[0].pointBorderWidth = 2.1; meta.controller.removeHoverStyle(point); + chart.update(); expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); expect(point._model.borderWidth).toBe(2.1); @@ -436,6 +440,7 @@ describe('Chart.controllers.radar', function() { }; meta.controller.removeHoverStyle(point); + chart.update(); expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); expect(point._model.borderWidth).toBe(5.5); diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index eb96ef8c745..30baaf5acc1 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -568,6 +568,31 @@ describe('Core helper tests', function() { document.body.removeChild(div); }); + it ('should get the maximum width and height for a node in a ShadowRoot', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + if (!div.attachShadow) { + // Shadow DOM is not natively supported + return; + } + + var shadow = div.attachShadow({mode: 'closed'}); + + // Create the div we want to get the max size for + var innerDiv = document.createElement('div'); + shadow.appendChild(innerDiv); + + expect(helpers.getMaximumWidth(innerDiv)).toBe(200); + expect(helpers.getMaximumHeight(innerDiv)).toBe(300); + + document.body.removeChild(div); + }); + it ('should get the maximum width of a node that has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); @@ -746,6 +771,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/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index 8878d9d9dbc..9d858a49e10 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -949,4 +949,152 @@ describe('Core.Tooltip', function() { } } }); + + it('Should split newlines into separate lines in user callbacks', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'label', + callbacks: { + beforeTitle: function() { + return 'beforeTitle\nnewline'; + }, + title: function() { + return 'title\nnewline'; + }, + afterTitle: function() { + return 'afterTitle\nnewline'; + }, + beforeBody: function() { + return 'beforeBody\nnewline'; + }, + beforeLabel: function() { + return 'beforeLabel\nnewline'; + }, + label: function() { + return 'label'; + }, + afterLabel: function() { + return 'afterLabel\nnewline'; + }, + afterBody: function() { + return 'afterBody\nnewline'; + }, + beforeFooter: function() { + return 'beforeFooter\nnewline'; + }, + footer: function() { + return 'footer\nnewline'; + }, + afterFooter: function() { + return 'afterFooter\nnewline'; + }, + labelTextColor: function() { + return 'labelTextColor'; + } + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + var node = chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var globalDefaults = Chart.defaults.global; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xPadding: 6, + yPadding: 6, + xAlign: 'center', + yAlign: 'top', + + // Body + bodyFontColor: '#fff', + _bodyFontFamily: globalDefaults.defaultFontFamily, + _bodyFontStyle: globalDefaults.defaultFontStyle, + _bodyAlign: 'left', + bodyFontSize: globalDefaults.defaultFontSize, + bodySpacing: 2, + + // Title + titleFontColor: '#fff', + _titleFontFamily: globalDefaults.defaultFontFamily, + _titleFontStyle: 'bold', + titleFontSize: globalDefaults.defaultFontSize, + _titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFontColor: '#fff', + _footerFontFamily: globalDefaults.defaultFontFamily, + _footerFontStyle: 'bold', + footerFontSize: globalDefaults.defaultFontSize, + _footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + opacity: 1, + legendColorBackground: '#fff', + + // Text + title: ['beforeTitle', 'newline', 'title', 'newline', 'afterTitle', 'newline'], + beforeBody: ['beforeBody', 'newline'], + body: [{ + before: ['beforeLabel', 'newline'], + lines: ['label'], + after: ['afterLabel', 'newline'] + }, { + before: ['beforeLabel', 'newline'], + lines: ['label'], + after: ['afterLabel', 'newline'] + }], + afterBody: ['afterBody', 'newline'], + footer: ['beforeFooter', 'newline', 'footer', 'newline', 'afterFooter', 'newline'], + caretPadding: 2, + labelTextColors: ['labelTextColor', 'labelTextColor'], + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }, { + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + })); + }); }); diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index f09b912d3c0..8d1227003b9 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -1,6 +1,6 @@ -// Test the point element +describe('Chart.elements.Point', function() { + describe('auto', jasmine.specsFromFixtures('element.point')); -describe('Point element tests', function() { it ('Should be constructed', function() { var point = new Chart.elements.Point({ _datasetIndex: 2, @@ -94,7 +94,7 @@ describe('Point element tests', function() { expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); }); - it ('should draw correctly', function() { + it ('should draw correctly with default settings if necessary', function() { var mockContext = window.createMockContext(); var point = new Chart.elements.Point({ _datasetIndex: 2, @@ -107,11 +107,7 @@ describe('Point element tests', function() { // Attach a view object as if we were the controller point._view = { radius: 2, - pointStyle: 'circle', hitRadius: 3, - borderColor: 'rgba(1, 2, 3, 1)', - borderWidth: 6, - backgroundColor: 'rgba(0, 255, 0)', x: 10, y: 15, ctx: mockContext @@ -121,374 +117,28 @@ describe('Point element tests', function() { expect(mockContext.getCalls()).toEqual([{ name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'arc', - args: [10, 15, 2, 0, 2 * Math.PI] - }, { - name: 'closePath', - args: [], - }, { - name: 'fill', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'triangle'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [10 - 3 * 2 / Math.sqrt(3) / 2, 15 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3] - }, { - name: 'lineTo', - args: [10 + 3 * 2 / Math.sqrt(3) / 2, 15 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], - }, { - name: 'lineTo', - args: [10, 15 - 2 * 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], - }, { - name: 'closePath', - args: [], - }, { - name: 'fill', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'rect'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'fillRect', - args: [10 - 1 / Math.SQRT2 * 2, 15 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] - }, { - name: 'strokeRect', - args: [10 - 1 / Math.SQRT2 * 2, 15 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] - }, { - name: 'stroke', - args: [] - }]); - - var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); - var drawRoundedRectangle = Chart.helpers.canvas.roundedRect; - var offset = point._view.radius / Math.SQRT2; - Chart.helpers.canvas.roundedRect = drawRoundedRectangleSpy; - mockContext.resetCalls(); - point._view.pointStyle = 'rectRounded'; - point.draw(); - - expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( - mockContext, - 10 - offset, - 15 - offset, - Math.SQRT2 * 2, - Math.SQRT2 * 2, - 2 / 2 - ); - expect(mockContext.getCalls()).toContain( - jasmine.objectContaining({ - name: 'fill', - args: [], - }) - ); - - Chart.helpers.canvas.roundedRect = drawRoundedRectangle; - mockContext.resetCalls(); - point._view.pointStyle = 'rectRot'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [10 - 1 / Math.SQRT2 * 2, 15] - }, { - name: 'lineTo', - args: [10, 15 + 1 / Math.SQRT2 * 2] - }, { - name: 'lineTo', - args: [10 + 1 / Math.SQRT2 * 2, 15], - }, { - name: 'lineTo', - args: [10, 15 - 1 / Math.SQRT2 * 2], - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'cross'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [10, 17] - }, { - name: 'lineTo', - args: [10, 13], - }, { - name: 'moveTo', - args: [8, 15], - }, { - name: 'lineTo', - args: [12, 15], - }, { - name: 'closePath', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'crossRot'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] - }, { - name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2], - }, { - name: 'closePath', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'star'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [10, 17] - }, { - name: 'lineTo', - args: [10, 13], - }, { - name: 'moveTo', - args: [8, 15], - }, { - name: 'lineTo', - args: [12, 15], - }, { - name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] - }, { - name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2], - }, { - name: 'closePath', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'line'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [8, 15] - }, { - name: 'lineTo', - args: [12, 15], - }, { - name: 'closePath', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'dash'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] + args: ['rgba(0,0,0,0.1)'] }, { name: 'setLineWidth', - args: [6] + args: [1] }, { name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] + args: ['rgba(0,0,0,0.1)'] }, { - name: 'beginPath', + name: 'save', args: [] }, { - name: 'moveTo', + name: 'translate', args: [10, 15] }, { - name: 'lineTo', - args: [12, 15], - }, { - name: 'closePath', - args: [], - }, { - name: 'stroke', - args: [] - }]); - - }); - - it ('should draw correctly with default settings if necessary', function() { - var mockContext = window.createMockContext(); - var point = new Chart.elements.Point({ - _datasetIndex: 2, - _index: 1, - _chart: { - ctx: mockContext, - } - }); - - // Attach a view object as if we were the controller - point._view = { - radius: 2, - hitRadius: 3, - x: 10, - y: 15, - ctx: mockContext - }; - - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'setLineWidth', - args: [1] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] + name: 'rotate', + args: [0] }, { name: 'beginPath', args: [] }, { name: 'arc', - args: [10, 15, 2, 0, 2 * Math.PI] + args: [0, 0, 2, 0, 2 * Math.PI] }, { name: 'closePath', args: [], @@ -498,6 +148,9 @@ describe('Point element tests', function() { }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); }); 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.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 535b9af3307..11d25d1d9b6 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -113,7 +113,6 @@ describe('Deprecations', function() { var calls = ctx.getCalls(); expect(calls[0]).toEqual({name: 'beginPath', args: []}); - expect(calls[calls.length - 1]).toEqual({name: 'closePath', args: []}); expect(Chart.helpers.canvas.roundedRect).toHaveBeenCalledWith(ctx, 10, 20, 30, 40, 5); }); }); 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/helpers.canvas.tests.js b/test/specs/helpers.canvas.tests.js index 81326529b49..1a342c1cb3b 100644 --- a/test/specs/helpers.canvas.tests.js +++ b/test/specs/helpers.canvas.tests.js @@ -30,13 +30,15 @@ describe('Chart.helpers.canvas', function() { expect(context.getCalls()).toEqual([ {name: 'moveTo', args: [15, 20]}, {name: 'lineTo', args: [35, 20]}, - {name: 'quadraticCurveTo', args: [40, 20, 40, 25]}, + {name: 'arcTo', args: [40, 20, 40, 25, 5]}, {name: 'lineTo', args: [40, 55]}, - {name: 'quadraticCurveTo', args: [40, 60, 35, 60]}, + {name: 'arcTo', args: [40, 60, 35, 60, 5]}, {name: 'lineTo', args: [15, 60]}, - {name: 'quadraticCurveTo', args: [10, 60, 10, 55]}, + {name: 'arcTo', args: [10, 60, 10, 55, 5]}, {name: 'lineTo', args: [10, 25]}, - {name: 'quadraticCurveTo', args: [10, 20, 15, 20]} + {name: 'arcTo', args: [10, 20, 15, 20, 5]}, + {name: 'closePath', args: []}, + {name: 'moveTo', args: [10, 20]} ]); }); it('should optimize path if radius is 0', function() { diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 60dd07698c0..380feaac09d 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -212,6 +212,31 @@ describe('Linear Scale', function() { expect(chart.scales.yScale0.max).toBe(90); }); + it('Should correctly determine the max & min data values for small numbers', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [-1e-8, 3e-8, -4e-8, 6e-8] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min * 1e8).toBeCloseTo(-4); + expect(chart.scales.yScale0.max * 1e8).toBeCloseTo(6); + }); + it('Should correctly determine the max & min for scatter data', function() { var chart = window.acquireChart({ type: 'line', @@ -548,6 +573,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({ @@ -905,4 +990,60 @@ describe('Linear Scale', function() { expect(chart.scales['x-axis-0'].min).toEqual(20); expect(chart.scales['x-axis-0'].max).toEqual(21); }); + + it('min settings should be used if set to zero', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'dataset 1', + backgroundColor: '#382765', + data: [2500, 2000, 1500] + }] + }; + + var chart = window.acquireChart({ + type: 'horizontalBar', + data: barData, + options: { + scales: { + xAxes: [{ + ticks: { + min: 0, + max: 3000 + } + }] + } + } + }); + + expect(chart.scales['x-axis-0'].min).toEqual(0); + }); + + it('max settings should be used if set to zero', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'dataset 1', + backgroundColor: '#382765', + data: [-2500, -2000, -1500] + }] + }; + + var chart = window.acquireChart({ + type: 'horizontalBar', + data: barData, + options: { + scales: { + xAxes: [{ + ticks: { + min: -3000, + max: 0 + } + }] + } + } + }); + + expect(chart.scales['x-axis-0'].max).toEqual(0); + }); });