diff --git a/.codeclimate.yml b/.codeclimate.yml index fcc885c82..0b8340feb 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,25 +1,19 @@ -engines: +version: "2" +plugins: duplication: enabled: true config: languages: - - javascript - eslint: - enabled: true - channel: "eslint-3" + - javascript fixme: enabled: true -ratings: - paths: - - "src/**/*.js" -exclude_paths: -- '.github/' -- 'dist/' -- 'test/' -- 'docs/' -- 'samples/' -- 'scripts/' -- '**.md' -- '**.json' -- 'gulpfile.js' -- 'karma.conf.js' +exclude_patterns: + - "dist/" + - "docs/" + - "samples/" + - "scripts/" + - "test/" + - "*.js" + - "*.json" + - "*.md" + - ".*" diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 6a31e4b6e..000000000 --- a/.eslintrc +++ /dev/null @@ -1,224 +0,0 @@ -ecmaFeatures: - modules: true - jsx: true - -env: - amd: true - browser: true - es6: true - jquery: true - node: true - -# http://eslint.org/docs/rules/ -rules: - # Possible Errors - no-cond-assign: 2 - no-console: [2, {allow: [warn, error]}] - no-constant-condition: 2 - no-control-regex: 2 - no-debugger: 2 - no-dupe-args: 2 - no-dupe-keys: 2 - no-duplicate-case: 2 - no-empty: 2 - no-empty-character-class: 2 - no-ex-assign: 2 - no-extra-boolean-cast: 2 - no-extra-parens: [2, functions] - no-extra-semi: 2 - no-func-assign: 2 - no-inner-declarations: [2, functions] - no-invalid-regexp: 2 - no-irregular-whitespace: 2 - no-negated-in-lhs: 2 - no-obj-calls: 2 - no-regex-spaces: 2 - no-sparse-arrays: 2 - no-unexpected-multiline: 2 - no-unreachable: 2 - use-isnan: 2 - valid-jsdoc: 0 - valid-typeof: 2 - - # Best Practices - accessor-pairs: 2 - array-callback-return: 0 - block-scoped-var: 0 - complexity: [2, 10] - consistent-return: 0 - curly: [2, all] - default-case: 2 - dot-location: 0 - dot-notation: 2 - eqeqeq: 2 - guard-for-in: 2 - no-alert: 2 - no-caller: 2 - no-case-declarations: 2 - no-div-regex: 2 - no-else-return: 2 - no-empty-pattern: 2 - no-eq-null: 2 - no-eval: 2 - no-extend-native: 2 - no-extra-bind: 2 - no-fallthrough: 2 - no-floating-decimal: 2 - no-implicit-coercion: 0 - no-implied-eval: 2 - no-invalid-this: 0 - no-iterator: 2 - no-labels: 2 - no-lone-blocks: 2 - no-loop-func: 2 - no-magic-number: 0 - no-multi-spaces: 2 - no-multi-str: 2 - no-native-reassign: 2 - no-new-func: 2 - no-new-wrappers: 2 - no-new: 2 - no-octal-escape: 2 - no-octal: 2 - no-proto: 2 - no-redeclare: 2 - no-return-assign: 2 - no-script-url: 2 - no-self-compare: 2 - no-sequences: 2 - no-throw-literal: 0 - no-unused-expressions: 2 - no-useless-call: 2 - no-useless-concat: 2 - no-void: 2 - no-warning-comments: 0 - no-with: 2 - radix: 2 - vars-on-top: 0 - wrap-iife: 2 - yoda: [1, never] - - # Strict - strict: 0 - - # Variables - init-declarations: 0 - no-catch-shadow: 2 - no-delete-var: 2 - no-label-var: 2 - no-shadow-restricted-names: 2 - no-shadow: 2 - no-undef-init: 2 - no-undef: 2 - no-undefined: 0 - no-unused-vars: 2 - no-use-before-define: 2 - - # Node.js and CommonJS - callback-return: 2 - global-require: 2 - handle-callback-err: 2 - no-mixed-requires: 0 - no-new-require: 0 - no-path-concat: 2 - no-process-exit: 2 - no-restricted-modules: 0 - no-sync: 0 - - # Stylistic Issues - array-bracket-spacing: [2, never] - block-spacing: 0 - brace-style: [2, 1tbs] - camelcase: 2 - comma-dangle: [2, only-multiline] - comma-spacing: 2 - comma-style: [2, last] - computed-property-spacing: [2, never] - consistent-this: [2, me] - eol-last: 2 - func-call-spacing: 0 - func-names: [2, never] - func-style: 0 - id-length: 0 - id-match: 0 - indent: [2, tab] - jsx-quotes: 0 - key-spacing: 2 - keyword-spacing: 2 - linebreak-style: 0 - lines-around-comment: 0 - max-depth: 0 - max-len: 0 - max-lines: 0 - max-nested-callbacks: 0 - max-params: 0 - max-statements-per-line: 0 - max-statements: [2, 30] - multiline-ternary: 0 - new-cap: 0 - new-parens: 2 - newline-after-var: 0 - newline-before-return: 0 - newline-per-chained-call: 0 - no-array-constructor: 0 - no-bitwise: 0 - no-continue: 0 - no-inline-comments: 0 - no-lonely-if: 2 - no-mixed-operators: 0 - no-mixed-spaces-and-tabs: 2 - no-multiple-empty-lines: [2, {max: 2}] - no-negated-condition: 0 - no-nested-ternary: 0 - no-new-object: 0 - no-plusplus: 0 - no-restricted-syntax: 0 - no-spaced-func: 0 - no-ternary: 0 - no-trailing-spaces: 2 - no-underscore-dangle: 0 - no-unneeded-ternary: 0 - no-whitespace-before-property: 2 - object-curly-newline: 0 - object-curly-spacing: [2, never] - object-property-newline: 0 - one-var-declaration-per-line: 2 - one-var: [2, {initialized: never}] - operator-assignment: 0 - operator-linebreak: 0 - padded-blocks: 0 - quote-props: [2, as-needed] - quotes: [2, single, {avoidEscape: true}] - require-jsdoc: 0 - semi-spacing: 2 - semi: [2, always] - sort-keys: 0 - sort-vars: 0 - space-before-blocks: [2, always] - space-before-function-paren: [2, never] - space-in-parens: [2, never] - space-infix-ops: 2 - space-unary-ops: [2, {words: true, nonwords: false}] - spaced-comment: [2, always] - unicode-bom: 0 - wrap-regex: 2 - - # ECMAScript 6 - arrow-body-style: 0 - arrow-parens: 0 - arrow-spacing: 0 - constructor-super: 0 - generator-star-spacing: 0 - no-arrow-condition: 0 - no-class-assign: 0 - no-const-assign: 0 - no-dupe-class-members: 0 - no-this-before-super: 0 - no-var: 0 - object-shorthand: 0 - prefer-arrow-callback: 0 - prefer-const: 0 - prefer-reflect: 0 - prefer-spread: 0 - prefer-template: 0 - require-yield: 0 diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 000000000..b0d9d5695 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,7 @@ +extends: chartjs + +env: + browser: true + node: true + +plugins: ['html'] diff --git a/.gitignore b/.gitignore index 53ce8fedb..0a65be9b2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,11 @@ /gh-pages /jsdoc /node_modules +/package-lock.json .DS_Store .idea .vscode bower.json *.log *.swp +*.stackdump diff --git a/.htmllintrc b/.htmllintrc new file mode 100644 index 000000000..a6b209703 --- /dev/null +++ b/.htmllintrc @@ -0,0 +1,18 @@ +{ + "indent-style": "tabs", + "attr-quote-style": "double", + "spec-char-escape": false, + "attr-bans": [ + "align", + "background", + "bgcolor", + "border", + "frameborder", + "longdesc", + "marginwidth", + "marginheight", + "scrolling" + ], + "tag-bans": [ "b", "i" ], + "id-class-style": false +} diff --git a/.travis.yml b/.travis.yml index 862d8b044..9b38ab3c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: - gulp docs - gulp package - gulp bower - - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls + - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls || true notifications: slack: chartjs:pcfCZR6ugg5TEcaLtmIfQYuA diff --git a/LICENSE.md b/LICENSE.md index 620db307e..29c941dcc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2017 Nick Downie +Copyright (c) 2018 Chart.js Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 767950b88..f914944e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chart.js -[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![codeclimate](https://img.shields.io/codeclimate/github/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) +[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600)](https://chartjs-slack.herokuapp.com/) *Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) @@ -19,11 +19,23 @@ To install via bower: bower install chart.js --save ``` -#### Selecting the Correct Build +### Selecting the Correct Build -Chart.js provides two different builds that are available for your use. The `Chart.js` and `Chart.min.js` files include Chart.js and the accompanying color parsing library. If this version is used and you require the use of the time axis, [Moment.js](http://momentjs.com/) will need to be included before Chart.js. +Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. -The `Chart.bundle.js` and `Chart.bundle.min.js` builds include Moment.js in a single file. This version should be used if you require time axes and want a single file to include, select this version. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. +#### Stand-Alone Build +Files: +* `dist/Chart.js` +* `dist/Chart.min.js` + +The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. + +#### Bundled Build +Files: +* `dist/Chart.bundle.js` +* `dist/Chart.bundle.min.js` + +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. ## Documentation diff --git a/book.json b/book.json index 487fd095c..8b0f73970 100644 --- a/book.json +++ b/book.json @@ -1,5 +1,6 @@ { "root": "./docs", + "title": "Chart.js documentation", "author": "chartjs", "gitbook": "3.2.2", "plugins": ["-lunr", "-search", "search-plus", "anchorjs", "chartjs", "ga"], diff --git a/docs/README.md b/docs/README.md index 5a0e9f355..24ee8d499 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Chart.js -[![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=600)](https://chart-js-automation.herokuapp.com/) +[![slack](https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600)](https://chartjs-slack.herokuapp.com/) ## Installation diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 606300900..8591dad86 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -7,6 +7,7 @@ * [Usage](getting-started/usage.md) * [General](general/README.md) * [Responsive](general/responsive.md) + * [Pixel Ratio](general/device-pixel-ratio.md) * [Interactions](general/interactions/README.md) * [Events](general/interactions/events.md) * [Modes](general/interactions/modes.md) diff --git a/docs/axes/README.md b/docs/axes/README.md index b7de691ea..f65cd82d2 100644 --- a/docs/axes/README.md +++ b/docs/axes/README.md @@ -26,18 +26,18 @@ There are a number of config callbacks that can be used to change parameters in | Name | Arguments | Description | ---- | --------- | ----------- | `beforeUpdate` | `axis` | Callback called before the update process starts. -| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. +| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. | `afterSetDimensions` | `axis` | Callback that runs after dimensions are set. | `beforeDataLimits` | `axis` | Callback that runs before data limits are determined. | `afterDataLimits` | `axis` | Callback that runs after data limits are determined. | `beforeBuildTicks` | `axis` | Callback that runs before ticks are created. | `afterBuildTicks` | `axis` | Callback that runs after ticks are created. Useful for filtering ticks. | `beforeTickToLabelConversion` | `axis` | Callback that runs before ticks are converted into strings. -| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. +| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. | `beforeCalculateTickRotation` | `axis` | Callback that runs before tick rotation is determined. | `afterCalculateTickRotation` | `axis` | Callback that runs after tick rotation is determined. -| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. -| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. +| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. +| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. | `afterUpdate` | `axis` | Callback that runs at the end of the update process. ## Updating Axis Defaults diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 64ce32073..8518da9f2 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -75,13 +75,13 @@ var myChart = new Chart(ctx, { data: { datasets: [{ data: [20, 50, 100, 75, 25, 0], - label: 'Left dataset' + label: 'Left dataset', // This binds the dataset to the left y axis yAxisID: 'left-y-axis' }, { - data: [0.1, 0.5, 1.0, 2.0, 1.5, 0] - label: 'Right dataset' + data: [0.1, 0.5, 1.0, 2.0, 1.5, 0], + label: 'Right dataset', // This binds the dataset to the right y axis yAxisID: 'right-y-axis', diff --git a/docs/axes/cartesian/linear.md b/docs/axes/cartesian/linear.md index 00f7aced1..1d0292074 100644 --- a/docs/axes/cartesian/linear.md +++ b/docs/axes/cartesian/linear.md @@ -20,7 +20,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -43,7 +43,7 @@ let chart = new Chart(ctx, { scales: { yAxes: [{ ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } }] diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index 9d15b5e26..cefe9c248 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -147,6 +147,6 @@ The `ticks.source` property controls the ticks generation * `'labels'`: generates ticks from user given `data.labels` values ONLY ### Parser -If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. +If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. If this is a function, it must return a moment.js object given the appropriate data value. diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index 1636781a8..14009be6a 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -36,7 +36,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -58,7 +58,7 @@ let chart = new Chart(ctx, { options: { scale: { ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } } @@ -104,7 +104,7 @@ The following options are used to configure the point labels that are shown on t | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. -| `fontColor` | `Color` | `'#666'` | Font color for point labels. +| `fontColor` | `Color/Color[]` | `'#666'` | Font color for point labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family to use when rendering labels. | `fontSize` | `Number` | 10 | font size in pixels | `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 1de31e01f..f60afd9bb 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -35,8 +35,8 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). | `reverse` | `Boolean` | `false` | Reverses order of tick labels. -| `minor` | `object` | `{}` | Minor ticks configuration. Ommited options are inherited from options above. -| `major` | `object` | `{}` | Major ticks configuration. Ommited options are inherited from options above. +| `minor` | `object` | `{}` | Minor ticks configuration. Omitted options are inherited from options above. +| `major` | `object` | `{}` | Major ticks configuration. Omitted options are inherited from options above. ## Minor Tick Configuration The minorTick configuration is nested under the ticks configuration in the `minor` key. It defines options for the minor tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. diff --git a/docs/charts/bar.md b/docs/charts/bar.md index cd5e8c9f3..84704ba94 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -6,12 +6,12 @@ A bar chart provides a way of showing data values represented as vertical bars. "type": "bar", "data": { "labels": [ - "January", - "February", - "March", - "April", - "May", - "June", + "January", + "February", + "March", + "April", + "May", + "June", "July" ], "datasets": [{ @@ -152,7 +152,7 @@ The `data` property of a dataset for a bar chart is specified as a an array of n data: [20, 10] ``` -You can also specify the dataset as x/y coordinates when using the [time scale](./time.md). +You can also specify the dataset as x/y coordinates when using the [time scale](../axes/cartesian/time.md#time-cartesian-axis). ```javascript data: [{x:'2016-12-25', y:20}, {x:'2016-12-26', y:10}] diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md index 35a7dbb56..cd9af9d56 100644 --- a/docs/charts/doughnut.md +++ b/docs/charts/doughnut.md @@ -55,7 +55,6 @@ The doughnut/pie chart allows a number of properties to be specified for each da | Name | Type | Description | ---- | ---- | ----------- -| `label` | `String` | The label for the dataset which appears in the legend and tooltips. | `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderWidth` | `Number[]` | The border width of the arcs in the dataset. diff --git a/docs/charts/line.md b/docs/charts/line.md index 40441b7af..90471e462 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -119,7 +119,7 @@ The `data` property of a dataset for a line chart can be passed in two formats. data: [20, 10] ``` -When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#Category Axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. +When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#category-cartesian-axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. ### Point[] diff --git a/docs/charts/mixed.md b/docs/charts/mixed.md index e7f43a219..9a51eae86 100644 --- a/docs/charts/mixed.md +++ b/docs/charts/mixed.md @@ -41,9 +41,9 @@ At this point we have a chart rendering how we'd like. It's important to note th "type": "bar", "data": { "labels": [ - "January", - "February", - "March", + "January", + "February", + "March", "April" ], "datasets": [{ diff --git a/docs/charts/polar.md b/docs/charts/polar.md index 29952cff6..8f403149e 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -46,7 +46,6 @@ The following options can be included in a polar area chart dataset to configure | Name | Type | Description | ---- | ---- | ----------- -| `label` | `String` | The label for the dataset which appears in the legend and tooltips. | `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderWidth` | `Number[]` | The border width of the arcs in the dataset. diff --git a/docs/charts/radar.md b/docs/charts/radar.md index ac908315c..b8a41c838 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -8,9 +8,9 @@ They are often useful for comparing the points of two or more different data set "type": "radar", "data": { "labels": [ - "Eating", - "Drinking", - "Sleeping", + "Eating", + "Drinking", + "Sleeping", "Designing", "Coding", "Cycling", @@ -94,7 +94,7 @@ The style of point. Options are: * 'circle' * 'cross' * 'crossRot' -* 'dash'. +* 'dash'. * 'line' * 'rect' * 'rectRounded' @@ -127,7 +127,7 @@ It is common to want to apply a configuration setting to all created radar chart ## Data Structure -The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. +The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. ```javascript data: [20, 10] diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index bce69af79..6867b762d 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -10,7 +10,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob | `display` | `Boolean` | `true` | is the legend shown | `position` | `String` | `'top'` | Position of the legend. [more...](#position) | `fullWidth` | `Boolean` | `true` | Marks that this box should take the full width of the canvas (pushing down other boxes). This is unlikely to need to be changed in day-to-day use. -| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item +| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item | `onHover` | `Function` | | A callback that is called when a 'mousemove' event is registered on top of a label item | `reverse` | `Boolean` | `false` | Legend will show datasets in reverse order. | `labels` | `Object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. @@ -164,3 +164,7 @@ var chart = new Chart(ctx, { } }); ``` + +Note that legendCallback is not called automatically and you must call `generateLegend()` yourself in code when creating a legend using this method. + + diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 51004843a..b591dc1d1 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -6,7 +6,7 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `enabled` | `Boolean` | `true` | Are tooltips enabled +| `enabled` | `Boolean` | `true` | Are on-canvas tooltips enabled | `custom` | `Function` | `null` | See [custom tooltip](#external-custom-tooltips) section. | `mode` | `String` | `'nearest'` | Sets which elements appear in the tooltip. [more...](../general/interactions/modes.md#interaction-modes). | `intersect` | `Boolean` | `true` | if true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times. @@ -30,7 +30,7 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | `footerFontSize` | `Number` | `12` | Footer font size | `footerFontStyle` | `String` | `'bold'` | Footer font style | `footerFontColor` | `Color` | `'#fff'` | Footer font color -| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each fotter line. +| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each footer line. | `footerMarginTop` | `Number` | `6` | Margin to add before drawing the footer. | `xPadding` | `Number` | `6` | Padding to add on left and right of tooltip. | `yPadding` | `Number` | `6` | Padding to add on top and bottom of tooltip. @@ -63,9 +63,9 @@ Example: Chart.Tooltip.positioners.custom = function(elements, eventPosition) { /** @type {Chart.Tooltip} */ var tooltip = this; - + /* ... */ - + return { x: 0, y: 0 @@ -103,6 +103,32 @@ All functions are called with the same arguments: a [tooltip item](#tooltip-item | `footer` | `Array[tooltipItem], data` | Returns text to render as the footer of the tooltip. | `afterFooter` | `Array[tooltipItem], data` | Text to render after the footer section +### Label Callback + +The label callback can change the text that displays for a given data point. A common example to round data values; the following example rounds the data to two decimal places. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + callbacks: { + label: function(tooltipItem, data) { + var label = data.datasets[tooltipItem.datasetIndex].label || ''; + + if (label) { + label += ': '; + } + label += Math.round(tooltipItem.yLabel * 100) / 100; + return label; + } + } + } + } +}); +``` + ### Label Color Callback For example, to return a red box for each item in the tooltip you could do: @@ -165,6 +191,9 @@ var myPieChart = new Chart(ctx, { data: data, options: { tooltips: { + // Disable the on-canvas tooltip + enabled: false, + custom: function(tooltipModel) { // Tooltip Element var tooltipEl = document.getElementById('chartjs-tooltip'); @@ -173,7 +202,7 @@ var myPieChart = new Chart(ctx, { if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.id = 'chartjs-tooltip'; - tooltipEl.innerHTML = "
" + tooltipEl.innerHTML = "
"; document.body.appendChild(tooltipEl); } @@ -212,7 +241,7 @@ var myPieChart = new Chart(ctx, { var style = 'background:' + colors.backgroundColor; style += '; border-color:' + colors.borderColor; style += '; border-width: 2px'; - var span = ''; + var span = ''; innerHtml += '' + span + body + ''; }); innerHtml += ''; @@ -226,11 +255,12 @@ var myPieChart = new Chart(ctx, { // Display, position, and set styles for font tooltipEl.style.opacity = 1; + tooltipEl.style.position = 'absolute'; tooltipEl.style.left = position.left + tooltipModel.caretX + 'px'; tooltipEl.style.top = position.top + tooltipModel.caretY + 'px'; - tooltipEl.style.fontFamily = tooltipModel._fontFamily; - tooltipEl.style.fontSize = tooltipModel.fontSize; - tooltipEl.style.fontStyle = tooltipModel._fontStyle; + tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily; + tooltipEl.style.fontSize = tooltipModel.bodyFontSize + 'px'; + tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle; tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px'; } } @@ -238,7 +268,7 @@ var myPieChart = new Chart(ctx, { }); ``` -See `samples/tooltips/line-customTooltips.html` for examples on how to get started. +See [samples](http://www.chartjs.org/samples/) for examples on how to get started with custom tooltips. ## Tooltip Model The tooltip model contains parameters that can be used to render the tooltip. diff --git a/docs/developers/api.md b/docs/developers/api.md index a5a0a5133..976578d57 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -132,9 +132,9 @@ To get an item that was clicked on, `getElementAtEvent` can be used. ```javascript function clickHandler(evt) { - var item = myChart.getElementAtEvent(evt)[0]; + var firstPoint = myChart.getElementAtEvent(evt)[0]; - if (item) { + if (firstPoint) { var label = myChart.data.labels[firstPoint._index]; var value = myChart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index]; } diff --git a/docs/developers/axes.md b/docs/developers/axes.md index 8d120195a..7d6c74e32 100644 --- a/docs/developers/axes.md +++ b/docs/developers/axes.md @@ -78,14 +78,14 @@ To work with Chart.js, custom scale types must implement the following interface // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param index: index into the ticks array - // @param includeOffset: if true, get the pixel halway between the given tick and the next + // @param includeOffset: if true, get the pixel halfway between the given tick and the next getPixelForTick: function(index, includeOffset) {}, // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param value : the value to get the pixel for // @param index : index into the data array of the value // @param datasetIndex : index of the dataset the value comes from - // @param includeOffset : if true, get the pixel halway between the given tick and the next + // @param includeOffset : if true, get the pixel halfway between the given tick and the next getPixelForValue: function(value, index, datasetIndex, includeOffset) {} // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) diff --git a/docs/developers/charts.md b/docs/developers/charts.md index f4c9a9746..e1267423b 100644 --- a/docs/developers/charts.md +++ b/docs/developers/charts.md @@ -75,7 +75,7 @@ The built in controller types are: For example, to derive a new chart type that extends from a bubble chart, you would do the following. ```javascript -// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. +// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. // We look for the defaults by doing Chart.defaults[chartType] // It looks like a bug exists when the defaults don't exist Chart.defaults.derivedBubble = Chart.defaults.bubble; @@ -102,7 +102,7 @@ var custom = Chart.controllers.bubble.extend({ // Stores the controller so that the chart initialization routine can look it up with // Chart.controllers[type] -Chart.controllers.derivedBubble = custom; +Chart.controllers.derivedBubble = custom; // Now we can create and use our new chart type new Chart(ctx, { diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index e6551d590..fa3511bb0 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -12,7 +12,7 @@ New contributions to the library are welcome, but we ask that you please follow # Joining the project - Active committers and contributors are invited to introduce yourself and request commit access to this project. We have a very active Slack community that you can join [here](https://chart-js-automation.herokuapp.com/). If you think you can help, we'd love to have you! + Active committers and contributors are invited to introduce yourself and request commit access to this project. We have a very active Slack community that you can join [here](https://chartjs-slack.herokuapp.com/). If you think you can help, we'd love to have you! # Building and Testing diff --git a/docs/developers/updates.md b/docs/developers/updates.md index 06550ce0c..b65757f85 100644 --- a/docs/developers/updates.md +++ b/docs/developers/updates.md @@ -1,6 +1,6 @@ # Updating Charts -It's pretty common to want to update charts after they've been created. When the chart data is changed, Chart.js will animate to the new data values. +It's pretty common to want to update charts after they've been created. When the chart data or options are changed, Chart.js will animate to the new data values and options. ## Adding or Removing Data @@ -14,9 +14,7 @@ function addData(chart, label, data) { }); chart.update(); } -``` -```javascript function removeData(chart) { chart.data.labels.pop(); chart.data.datasets.forEach((dataset) => { @@ -26,6 +24,78 @@ function removeData(chart) { } ``` +## Updating Options + +To update the options, mutating the options property in place or passing in a new options object are supported. + +- If the options are mutated in place, other option properties would be preserved, including those calculated by Chart.js. +- If created as a new object, it would be like creating a new chart with the options - old options would be discarded. + +```javascript +function updateConfigByMutating(chart) { + chart.options.title.text = 'new title'; + chart.update(); +} + +function updateConfigAsNewObject(chart) { + chart.options = { + responsive: true, + title:{ + display:true, + text: 'Chart.js' + }, + scales: { + xAxes: [{ + display: true + }], + yAxes: [{ + display: true + }] + } + } + chart.update(); +} +``` + +Scales can be updated separately without changing other options. +To update the scales, pass in an object containing all the customization including those unchanged ones. + +Variables referencing any one from `chart.scales` would be lost after updating scales with a new `id` or the changed `type`. + +```javascript +function updateScales(chart) { + var xScale = chart.scales['x-axis-0']; + var yScale = chart.scales['y-axis-0']; + chart.options.scales = { + xAxes: [{ + id: 'newId', + display: true + }], + yAxes: [{ + display: true, + type: 'logarithmic' + }] + } + chart.update(); + // need to update the reference + xScale = chart.scales['newId']; + yScale = chart.scales['y-axis-0']; +} +``` + +You can also update a specific scale either by specifying its index or id. + +```javascript +function updateScale(chart) { + chart.options.scales.yAxes[0] = { + type: 'logarithmic' + } + chart.update(); +} +``` + +Code sample for updating options can be found in [toggle-scale-type.html](../../samples/scales/toggle-scale-type.html). + ## Preventing Animations Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with a duration of `0`. This will render the chart synchronously and without an animation. \ No newline at end of file diff --git a/docs/general/colors.md b/docs/general/colors.md index d2d270081..64ab84c66 100644 --- a/docs/general/colors.md +++ b/docs/general/colors.md @@ -6,7 +6,7 @@ You can also pass a [CanvasGradient](https://developer.mozilla.org/en-US/docs/We ## Patterns and Gradients -An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string colour. +An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string colour. For example, if you wanted to fill a dataset with a pattern from an image you could do the following. diff --git a/docs/general/interactions/README.md b/docs/general/interactions/README.md index 585e65582..532cab6f0 100644 --- a/docs/general/interactions/README.md +++ b/docs/general/interactions/README.md @@ -1,6 +1,6 @@ # Interactions -The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. To configure which events trigger chart interactions, see [events](./events.md#events). +The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. To configure which events trigger chart interactions, see [events](./events.md#events). | Name | Type | Default | Description | ---- | ---- | ------- | ----------- diff --git a/docs/general/interactions/events.md b/docs/general/interactions/events.md index 8e21f7502..d5d05138c 100644 --- a/docs/general/interactions/events.md +++ b/docs/general/interactions/events.md @@ -7,7 +7,7 @@ The following properties define how the chart interacts with events. | `onHover` | `Function` | `null` | Called when any of the events fire. Called in the context of the chart and passed the event and an array of active elements (bars, points, etc). | `onClick` | `Function` | `null` | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed the event and an array of active elements -## Event Option +## Event Option For example, to have the chart only respond to click events, you could do ```javascript var chart = new Chart(ctx, { diff --git a/docs/general/interactions/modes.md b/docs/general/interactions/modes.md index a46f047e7..3e9f7982f 100644 --- a/docs/general/interactions/modes.md +++ b/docs/general/interactions/modes.md @@ -41,7 +41,7 @@ Finds the first item that intersects the point and returns it. Behaves like 'nea See `'index'` mode ## index -Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item, in the x direction, is used to determine the index. +Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item, in the x direction, is used to determine the index. ```javascript var chart = new Chart(ctx, { diff --git a/docs/general/responsive.md b/docs/general/responsive.md index bf8e8fa7a..f410826b8 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -33,3 +33,15 @@ The chart can also be programmatically resized by modifying the container size: ```javascript chart.canvas.parentNode.style.height = '128px'; ``` + +## Printing Resizeable Charts + +CSS media queries allow changing styles when printing a page. The CSS applied from these media queries may cause charts to need to resize. However, the resize won't happen automatically. To support resizing charts when printing, one needs to hook the [onbeforeprint](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeprint) event and manually trigger resizing of each chart. + +```javascript +function beforePrintHandler () { + for (var id in Chart.instances) { + Chart.instances[id].resize() + } +} +``` diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 9266b3099..bff52c72d 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -40,4 +40,4 @@ var chart = new Chart(ctx, { It's that easy to get started using Chart.js! From here you can explore the many options that can help you customise your charts with scales, tooltips, labels, colors, custom actions, and much more. -There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attatched to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file +There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attached to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index c8291d260..ccc02ddc4 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -36,22 +36,22 @@ https://www.jsdelivr.com/package/npm/chart.js?path=dist You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). -If you download or clone the repository, you must [build](../developers/contributing.md#building-chartjs) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. +If you download or clone the repository, you must [build](../developers/contributing.md#building-and-testing) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. # Selecting the Correct Build -Chart.js provides two different builds that are available for your use. +Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. ## Stand-Alone Build Files: * `dist/Chart.js` * `dist/Chart.min.js` -This version only includes Chart.js. If this version is used and you require the use of the time axis, [Moment.js](http://momentjs.com/) will need to be included before Chart.js. +The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. ## Bundled Build Files: * `dist/Chart.bundle.js` * `dist/Chart.bundle.min.js` -The bundled version includes Moment.js built into the same file. This version should be used if you wish to use time axes and want a single file to include. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 33e0e647e..fe2c337f1 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -18,13 +18,19 @@ 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-waterfall - Enables easy use of waterfall charts. - chartjs-plugin-zoom - Enables zooming and panning on charts. In addition, many plugins can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). ## Integrations -### Angular +### Angular (v2+) + + - emn178/angular2-chartjs + - valor-software/ng2-charts + +### Angular (v1) - angular-chart.js - tc-angular-chartjs - angular-chartjs @@ -49,3 +55,9 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ### Java - Chart.java + +### GWT (Google Web toolkit) + - Charba + +### Ember.js + - ember-cli-chart diff --git a/gulpfile.js b/gulpfile.js index bb42ea9d3..21f19645f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,7 +3,6 @@ var concat = require('gulp-concat'); var connect = require('gulp-connect'); var eslint = require('gulp-eslint'); var file = require('gulp-file'); -var htmlv = require('gulp-html-validator'); var insert = require('gulp-insert'); var replace = require('gulp-replace'); var size = require('gulp-size'); @@ -17,10 +16,18 @@ var browserify = require('browserify'); var source = require('vinyl-source-stream'); var merge = require('merge-stream'); var collapse = require('bundle-collapser/plugin'); -var argv = require('yargs').argv +var yargs = require('yargs'); var path = require('path'); +var fs = require('fs'); +var htmllint = require('gulp-htmllint'); var package = require('./package.json'); +var argv = yargs + .option('force-output', {default: false}) + .option('silent-errors', {default: false}) + .option('verbose', {default: false}) + .argv + var srcDir = './src/'; var outDir = './dist/'; @@ -29,21 +36,26 @@ var header = "/*!\n" + " * http://chartjs.org/\n" + " * Version: {{ version }}\n" + " *\n" + - " * Copyright 2017 Nick Downie\n" + + " * Copyright " + (new Date().getFullYear()) + " Chart.js Contributors\n" + " * Released under the MIT license\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; +if (argv.verbose) { + util.log("Gulp running with options: " + JSON.stringify(argv, null, 2)); +} + gulp.task('bower', bowerTask); gulp.task('build', buildTask); gulp.task('package', packageTask); gulp.task('watch', watchTask); -gulp.task('lint', lintTask); +gulp.task('lint', ['lint-html', 'lint-js']); +gulp.task('lint-html', lintHtmlTask); +gulp.task('lint-js', lintJsTask); gulp.task('docs', docsTask); -gulp.task('test', ['lint', 'validHTML', 'unittest']); +gulp.task('test', ['lint', 'unittest']); gulp.task('size', ['library-size', 'module-sizes']); gulp.task('server', serverTask); -gulp.task('validHTML', validHTMLTask); gulp.task('unittest', unittestTask); gulp.task('library-size', librarySizeTask); gulp.task('module-sizes', moduleSizesTask); @@ -79,9 +91,25 @@ function bowerTask() { function buildTask() { + var errorHandler = function (err) { + if(argv.forceOutput) { + var browserError = 'console.error("Gulp: ' + err.toString() + '")'; + ['Chart', 'Chart.min', 'Chart.bundle', 'Chart.bundle.min'].forEach(function(fileName) { + fs.writeFileSync(outDir+fileName+'.js', browserError); + }); + } + if(argv.silentErrors) { + util.log(util.colors.red('[Error]'), err.toString()); + this.emit('end'); + } else { + throw err; + } + } + var bundled = browserify('./src/chart.js', { standalone: 'Chart' }) .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.bundle.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) @@ -96,6 +124,7 @@ function buildTask() { .ignore('moment') .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) @@ -125,8 +154,9 @@ function packageTask() { .pipe(gulp.dest(outDir)); } -function lintTask() { +function lintJsTask() { var files = [ + 'samples/**/*.html', 'samples/**/*.js', 'src/**/*.js', 'test/**/*.js' @@ -148,6 +178,13 @@ function lintTask() { .pipe(eslint.failAfterError()); } +function lintHtmlTask() { + return gulp.src('samples/**/*.html') + .pipe(htmllint({ + failOnError: true, + })); +} + function docsTask(done) { const script = require.resolve('gitbook-cli/bin/gitbook.js'); const cmd = process.execPath; @@ -161,11 +198,6 @@ function docsTask(done) { }); } -function validHTMLTask() { - return gulp.src('samples/*.html') - .pipe(htmlv()); -} - function startTest() { return [ {pattern: './test/fixtures/**/*.json', included: false}, diff --git a/package.json b/package.json index a7a504887..a2e6c494a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.7.1", + "version": "2.7.2", "license": "MIT", "main": "src/chart.js", "repository": { @@ -10,30 +10,33 @@ "url": "https://github.com/chartjs/Chart.js.git" }, "devDependencies": { - "browserify": "^14.3.0", - "browserify-istanbul": "^2.0.0", - "bundle-collapser": "^1.2.1", + "browserify": "^14.5.0", + "browserify-istanbul": "^3.0.1", + "bundle-collapser": "^1.3.0", "child-process-promise": "^2.2.1", - "coveralls": "^2.13.1", - "gitbook-cli": "^2.3.0", + "coveralls": "^3.0.0", + "eslint": "^4.9.0", + "eslint-config-chartjs": "^0.1.0", + "eslint-plugin-html": "^4.0.2", + "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", "gulp-connect": "~5.0.0", - "gulp-eslint": "^3.0.1", + "gulp-eslint": "^4.0.0", "gulp-file": "^0.3.0", - "gulp-html-validator": "^0.0.5", + "gulp-htmllint": "^0.0.15", "gulp-insert": "~0.5.0", - "gulp-replace": "^0.5.4", + "gulp-replace": "^0.6.1", "gulp-size": "~2.1.0", "gulp-streamify": "^1.0.2", "gulp-uglify": "~3.0.x", "gulp-util": "~3.0.x", "gulp-zip": "~4.0.0", - "jasmine": "^2.6.0", - "jasmine-core": "^2.6.2", - "karma": "^1.7.0", + "jasmine": "^2.8.0", + "jasmine-core": "^2.8.0", + "karma": "^1.7.1", "karma-browserify": "^5.1.1", - "karma-chrome-launcher": "^2.1.1", + "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", @@ -42,13 +45,13 @@ "pixelmatch": "^4.0.2", "vinyl-source-stream": "^1.1.0", "watchify": "^3.9.0", - "yargs": "^8.0.1" + "yargs": "^9.0.1" }, "spm": { "main": "Chart.js" }, "dependencies": { - "chartjs-color": "~2.2.0", - "moment": "~2.18.0" + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" } } diff --git a/samples/.eslintrc.yml b/samples/.eslintrc.yml new file mode 100644 index 000000000..9573adbcd --- /dev/null +++ b/samples/.eslintrc.yml @@ -0,0 +1,9 @@ +globals: + $: true + Chart: true + Samples: true + moment: true + randomScalingFactor: true + +rules: + no-new: 0 diff --git a/samples/advanced/data-labelling.html b/samples/advanced/data-labelling.html index 17fc47c8f..52eb07b8f 100644 --- a/samples/advanced/data-labelling.html +++ b/samples/advanced/data-labelling.html @@ -3,130 +3,129 @@ - Labelling Data Points - - - + Labelling Data Points + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/advanced/progress-bar.html b/samples/advanced/progress-bar.html index d460bc88e..b97a998ea 100644 --- a/samples/advanced/progress-bar.html +++ b/samples/advanced/progress-bar.html @@ -1,96 +1,95 @@ - Animation Callbacks - - - + Animation Callbacks + + + -
- - -
-
-
- - + window.myLine.update(); + }); + - \ No newline at end of file + diff --git a/samples/charts/area/analyser.js b/samples/charts/area/analyser.js index e4ed8e90b..36fd56716 100644 --- a/samples/charts/area/analyser.js +++ b/samples/charts/area/analyser.js @@ -4,7 +4,7 @@ (function() { Chart.plugins.register({ - id: 'samples_filler_analyser', + id: 'samples-filler-analyser', beforeInit: function(chart, options) { this.element = document.getElementById(options.target); diff --git a/samples/charts/area/line-boundaries.html b/samples/charts/area/line-boundaries.html index edadc781c..7ac6883bb 100644 --- a/samples/charts/area/line-boundaries.html +++ b/samples/charts/area/line-boundaries.html @@ -94,15 +94,16 @@ }); }); - + // eslint-disable-next-line no-unused-vars function toggleSmooth(btn) { var value = btn.classList.toggle('btn-on'); Chart.helpers.each(Chart.instances, function(chart) { - chart.options.elements.line.tension = value? 0.4 : 0.000001; + chart.options.elements.line.tension = value ? 0.4 : 0.000001; chart.update(); }); } + // eslint-disable-next-line no-unused-vars function randomize() { var seed = utils.rand(); Chart.helpers.each(Chart.instances, function(chart) { diff --git a/samples/charts/area/line-datasets.html b/samples/charts/area/line-datasets.html index 7bc54e313..127726e7f 100644 --- a/samples/charts/area/line-datasets.html +++ b/samples/charts/area/line-datasets.html @@ -38,7 +38,7 @@ return utils.numbers(inputs); } - function generateLabels(config) { + function generateLabels() { return utils.months({count: inputs.count}); } @@ -122,7 +122,7 @@ filler: { propagate: false }, - samples_filler_analyser: { + 'samples-filler-analyser': { target: 'chart-analyser' } } @@ -134,18 +134,21 @@ options: options }); + // eslint-disable-next-line no-unused-vars function togglePropagate(btn) { var value = btn.classList.toggle('btn-on'); chart.options.plugins.filler.propagate = value; chart.update(); } + // eslint-disable-next-line no-unused-vars function toggleSmooth(btn) { var value = btn.classList.toggle('btn-on'); - chart.options.elements.line.tension = value? 0.4 : 0.000001; + chart.options.elements.line.tension = value ? 0.4 : 0.000001; chart.update(); } + // eslint-disable-next-line no-unused-vars function randomize() { chart.data.datasets.forEach(function(dataset) { dataset.data = generateData(); diff --git a/samples/charts/area/line-stacked.html b/samples/charts/area/line-stacked.html index 26ee6b18f..11a143b0b 100644 --- a/samples/charts/area/line-stacked.html +++ b/samples/charts/area/line-stacked.html @@ -7,9 +7,9 @@ @@ -26,70 +26,70 @@ - - + Horizontal Bar Chart + + + -
- -
- - - - - - + window.myHorizontalBar.update(); + }); + diff --git a/samples/charts/bar/multi-axis.html b/samples/charts/bar/multi-axis.html index 28755a701..c206866a4 100644 --- a/samples/charts/bar/multi-axis.html +++ b/samples/charts/bar/multi-axis.html @@ -2,107 +2,107 @@ - Bar Chart Multi Axis - - - + Bar Chart Multi Axis + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/stacked-group.html b/samples/charts/bar/stacked-group.html index 624992cfb..e3b734b2d 100644 --- a/samples/charts/bar/stacked-group.html +++ b/samples/charts/bar/stacked-group.html @@ -2,104 +2,104 @@ - Stacked Bar Chart with Groups - - - + Stacked Bar Chart with Groups + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/stacked.html b/samples/charts/bar/stacked.html index 46d1c4051..ca9f0cafb 100644 --- a/samples/charts/bar/stacked.html +++ b/samples/charts/bar/stacked.html @@ -2,101 +2,101 @@ - Stacked Bar Chart - - - + Stacked Bar Chart + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/vertical.html b/samples/charts/bar/vertical.html index 906b4624c..e9348b274 100644 --- a/samples/charts/bar/vertical.html +++ b/samples/charts/bar/vertical.html @@ -2,143 +2,143 @@ - Bar Chart - - - + Bar Chart + + + -
- -
- - - - - - + window.myBar.update(); + }); + diff --git a/samples/charts/bubble.html b/samples/charts/bubble.html index 0df09477f..092c1fb46 100644 --- a/samples/charts/bubble.html +++ b/samples/charts/bubble.html @@ -2,190 +2,190 @@ - Bubble Chart - - - + Bubble Chart + + + -
- -
- - - - - - + window.myChart.update(); + }); + diff --git a/samples/charts/combo-bar-line.html b/samples/charts/combo-bar-line.html index 0906029c4..c99894e22 100644 --- a/samples/charts/combo-bar-line.html +++ b/samples/charts/combo-bar-line.html @@ -2,100 +2,100 @@ - Combo Bar-Line Chart - - - + Combo Bar-Line Chart + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + chartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myMixedChart.update(); + }); + diff --git a/samples/charts/doughnut.html b/samples/charts/doughnut.html index b288fe129..446dcee4e 100644 --- a/samples/charts/doughnut.html +++ b/samples/charts/doughnut.html @@ -2,143 +2,143 @@ - Doughnut Chart - - - + Doughnut Chart + + + -
- -
- - - - - - + window.myDoughnut.update(); + }); + diff --git a/samples/charts/line/basic.html b/samples/charts/line/basic.html index db01ccf74..8028bee9c 100644 --- a/samples/charts/line/basic.html +++ b/samples/charts/line/basic.html @@ -2,162 +2,162 @@ - Line Chart - - - + Line Chart + + + -
- -
-
-
- - - - - - + window.myLine.update(); + }); + diff --git a/samples/charts/line/interpolation-modes.html b/samples/charts/line/interpolation-modes.html index 374da49c0..b11dd6819 100644 --- a/samples/charts/line/interpolation-modes.html +++ b/samples/charts/line/interpolation-modes.html @@ -2,102 +2,102 @@ - Line Chart - Cubic interpolation mode - - - + Line Chart - Cubic interpolation mode + + + -
- -
-
-
- - + diff --git a/samples/charts/line/line-styles.html b/samples/charts/line/line-styles.html index ed268e1fd..3ba9defcd 100644 --- a/samples/charts/line/line-styles.html +++ b/samples/charts/line/line-styles.html @@ -2,110 +2,110 @@ - Line Styles - - - + Line Styles + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/multi-axis.html b/samples/charts/line/multi-axis.html index 2ede74446..d3569e855 100644 --- a/samples/charts/line/multi-axis.html +++ b/samples/charts/line/multi-axis.html @@ -2,103 +2,103 @@ - Line Chart Multiple Axes - - - + Line Chart Multiple Axes + + + -
- -
- - + window.myLine.update(); + }); + diff --git a/samples/charts/line/point-sizes.html b/samples/charts/line/point-sizes.html index 823c6d3e4..53d3db46c 100644 --- a/samples/charts/line/point-sizes.html +++ b/samples/charts/line/point-sizes.html @@ -2,129 +2,128 @@ - Different Point Sizes - - - + Different Point Sizes + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/point-styles.html b/samples/charts/line/point-styles.html index 1985d394d..2ef46a9cf 100644 --- a/samples/charts/line/point-styles.html +++ b/samples/charts/line/point-styles.html @@ -2,95 +2,95 @@ - Line Chart - - - + Line Chart + + + -
-
- + var ctx = canvas.getContext('2d'); + var config = createConfig(pointStyle); + new Chart(ctx, config); + }); + }; + diff --git a/samples/charts/line/skip-points.html b/samples/charts/line/skip-points.html index 00aa81fe0..aedacce72 100644 --- a/samples/charts/line/skip-points.html +++ b/samples/charts/line/skip-points.html @@ -2,94 +2,94 @@ - Line Chart - - - + Line Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/stepped.html b/samples/charts/line/stepped.html index ccce87c2d..4ad9708eb 100644 --- a/samples/charts/line/stepped.html +++ b/samples/charts/line/stepped.html @@ -2,9 +2,9 @@ - Stepped Line Chart - - + Stepped Line Chart + + + Polar Area Chart + + + -
- -
- - - - + var colorNames = Object.keys(window.chartColors); + document.getElementById('addData').addEventListener('click', function() { + if (config.data.datasets.length > 0) { + config.data.labels.push('data #' + config.data.labels.length); + config.data.datasets.forEach(function(dataset) { + var colorName = colorNames[config.data.labels.length % colorNames.length]; + dataset.backgroundColor.push(window.chartColors[colorName]); + dataset.data.push(randomScalingFactor()); + }); + window.myPolarArea.update(); + } + }); + document.getElementById('removeData').addEventListener('click', function() { + config.data.labels.pop(); // remove the label first + config.data.datasets.forEach(function(dataset) { + dataset.backgroundColor.pop(); + dataset.data.pop(); + }); + window.myPolarArea.update(); + }); + diff --git a/samples/charts/radar-skip-points.html b/samples/charts/radar-skip-points.html index ab29b3a0e..ab042e2dc 100644 --- a/samples/charts/radar-skip-points.html +++ b/samples/charts/radar-skip-points.html @@ -2,108 +2,108 @@ - Radar Chart - - - + Radar Chart + + + -
- -
- - + window.myRadar.update(); + }); + diff --git a/samples/charts/radar.html b/samples/charts/radar.html index fbbb9d972..7eda09a7f 100644 --- a/samples/charts/radar.html +++ b/samples/charts/radar.html @@ -2,145 +2,145 @@ - Radar Chart - - - + Radar Chart + + + -
- -
- - - - - - + window.myRadar.update(); + }); + diff --git a/samples/charts/scatter/basic.html b/samples/charts/scatter/basic.html index c5e36e9d9..6ac227c3b 100644 --- a/samples/charts/scatter/basic.html +++ b/samples/charts/scatter/basic.html @@ -2,106 +2,106 @@ - Scatter Chart - - - + Scatter Chart + + + -
- -
- - + document.getElementById('randomizeData').addEventListener('click', function() { + scatterChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return { + x: randomScalingFactor(), + y: randomScalingFactor() + }; + }); + }); + window.myScatter.update(); + }); + diff --git a/samples/charts/scatter/multi-axis.html b/samples/charts/scatter/multi-axis.html index cbf3bf73a..6c20b98f0 100644 --- a/samples/charts/scatter/multi-axis.html +++ b/samples/charts/scatter/multi-axis.html @@ -23,9 +23,9 @@ var color = Chart.helpers.color; var scatterChartData = { datasets: [{ - label: "My First dataset", - xAxisID: "x-axis-1", - yAxisID: "y-axis-1", + label: 'My First dataset', + xAxisID: 'x-axis-1', + yAxisID: 'y-axis-1', borderColor: window.chartColors.red, backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(), data: [{ @@ -51,9 +51,9 @@ y: randomScalingFactor(), }] }, { - label: "My Second dataset", - xAxisID: "x-axis-1", - yAxisID: "y-axis-2", + label: 'My Second dataset', + xAxisID: 'x-axis-1', + yAxisID: 'y-axis-2', borderColor: window.chartColors.blue, backgroundColor: color(window.chartColors.blue).alpha(0.2).rgbString(), data: [{ @@ -82,7 +82,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myScatter = Chart.Scatter(ctx, { data: scatterChartData, options: { @@ -95,22 +95,22 @@ }, scales: { xAxes: [{ - position: "bottom", + position: 'bottom', gridLines: { - zeroLineColor: "rgba(0,0,0,1)" + zeroLineColor: 'rgba(0,0,0,1)' } }], yAxes: [{ - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance display: true, - position: "left", - id: "y-axis-1", + position: 'left', + id: 'y-axis-1', }, { - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance display: true, - position: "right", + position: 'right', reverse: true, - id: "y-axis-2", + id: 'y-axis-2', // grid line settings gridLines: { diff --git a/samples/index.html b/samples/index.html index d855662f0..b9e13c364 100644 --- a/samples/index.html +++ b/samples/index.html @@ -47,8 +47,8 @@ var category = createCategory(item); var children = category.getElementsByClassName('items')[0]; - (item.items || []).forEach(function(item) { - children.appendChild(createEntry(item)); + (item.items || []).forEach(function(item2) { + children.appendChild(createEntry(item2)); }); categories.appendChild(category); diff --git a/samples/legend/point-style.html b/samples/legend/point-style.html index 727c7a6d8..b7acea4bd 100644 --- a/samples/legend/point-style.html +++ b/samples/legend/point-style.html @@ -2,115 +2,115 @@ - Legend Point Style - - - + Legend Point Style + + + -
-
- -
-
- -
-
- + window.onload = function() { + [{ + id: 'chart-legend-normal', + config: createConfig('red') + }, { + id: 'chart-legend-pointstyle', + config: createPointStyleConfig('blue') + }].forEach(function(details) { + var ctx = document.getElementById(details.id).getContext('2d'); + new Chart(ctx, details.config); + }); + }; + diff --git a/samples/legend/positioning.html b/samples/legend/positioning.html index 97bc70fa0..5bd2259cf 100644 --- a/samples/legend/positioning.html +++ b/samples/legend/positioning.html @@ -2,120 +2,120 @@ - Legend Positions - - - + Legend Positions + + + -
-
- -
-
- -
-
- -
-
- -
-
- + window.onload = function() { + [{ + id: 'chart-legend-top', + legendPosition: 'top', + color: 'red' + }, { + id: 'chart-legend-right', + legendPosition: 'right', + color: 'blue' + }, { + id: 'chart-legend-bottom', + legendPosition: 'bottom', + color: 'green' + }, { + id: 'chart-legend-left', + legendPosition: 'left', + color: 'yellow' + }].forEach(function(details) { + var ctx = document.getElementById(details.id).getContext('2d'); + var config = createConfig(details.legendPosition, details.color); + new Chart(ctx, config); + }); + }; + diff --git a/samples/samples.js b/samples/samples.js index 2d11e9d48..b818827c6 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -136,6 +136,9 @@ }, { title: 'Non numeric Y Axis', path: 'scales/non-numeric-y.html' + }, { + title: 'Toggle Scale Type', + path: 'scales/toggle-scale-type.html' }] }, { title: 'Legend', diff --git a/samples/scales/filtering-labels.html b/samples/scales/filtering-labels.html index 4af89025e..4b4b51724 100644 --- a/samples/scales/filtering-labels.html +++ b/samples/scales/filtering-labels.html @@ -2,92 +2,90 @@ - Chart with xAxis Filtering - - - + Chart with xAxis Filtering + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/gridlines-display.html b/samples/scales/gridlines-display.html index 0c2dc114e..8e30bdcdb 100644 --- a/samples/scales/gridlines-display.html +++ b/samples/scales/gridlines-display.html @@ -2,123 +2,123 @@ - Suggested Min/Max Settings - - - + Grid Lines Display Settings + + + -
- + var ctx = canvas.getContext('2d'); + var config = createConfig(details.gridLines, details.title); + new Chart(ctx, config); + }); + }; + diff --git a/samples/scales/gridlines-style.html b/samples/scales/gridlines-style.html index 76bb1f007..d2c00ecdd 100644 --- a/samples/scales/gridlines-style.html +++ b/samples/scales/gridlines-style.html @@ -2,68 +2,68 @@ - Suggested Min/Max Settings - - - + Grid Lines Style Settings + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/min-max-suggested.html b/samples/scales/linear/min-max-suggested.html index 18059548a..10d546ac2 100644 --- a/samples/scales/linear/min-max-suggested.html +++ b/samples/scales/linear/min-max-suggested.html @@ -2,66 +2,66 @@ - Suggested Min/Max Settings - - - + Suggested Min/Max Settings + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/min-max.html b/samples/scales/linear/min-max.html index 868bc7b1d..feafbd7a6 100644 --- a/samples/scales/linear/min-max.html +++ b/samples/scales/linear/min-max.html @@ -2,63 +2,63 @@ - Min/Max Settings - - - + Min/Max Settings + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/step-size.html b/samples/scales/linear/step-size.html index ced0b6c3c..fc0af2ee3 100644 --- a/samples/scales/linear/step-size.html +++ b/samples/scales/linear/step-size.html @@ -2,174 +2,174 @@ - Line Chart - - - + Line Chart + + + -
- -
-
-
- - - - - - + window.myLine.update(); + }); + + diff --git a/samples/scales/logarithmic/line.html b/samples/scales/logarithmic/line.html index 2c961abd2..72fd9b07d 100644 --- a/samples/scales/logarithmic/line.html +++ b/samples/scales/logarithmic/line.html @@ -2,96 +2,96 @@ - Logarithmic Line Chart - - - + Logarithmic Line Chart + + + -
- -
- - + window.myLine.update(); + }); + diff --git a/samples/scales/logarithmic/scatter.html b/samples/scales/logarithmic/scatter.html index a4bd577c2..5f266a5cd 100644 --- a/samples/scales/logarithmic/scatter.html +++ b/samples/scales/logarithmic/scatter.html @@ -2,29 +2,29 @@ - Scatter Chart - - - + Scatter Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myScatter = Chart.Scatter(ctx, { + data: scatterChartData, + options: { + title: { + display: true, + text: 'Chart.js Scatter Chart - Logarithmic X-Axis' + }, + scales: { + xAxes: [{ + type: 'logarithmic', + position: 'bottom', + ticks: { + userCallback: function(tick) { + var remain = tick / (Math.pow(10, Math.floor(Chart.helpers.log10(tick)))); + if (remain === 1 || remain === 2 || remain === 5) { + return tick.toString() + 'Hz'; + } + return ''; + }, + }, + scaleLabel: { + labelString: 'Frequency', + display: true, + } + }], + yAxes: [{ + type: 'linear', + ticks: { + userCallback: function(tick) { + return tick.toString() + 'dB'; + } + }, + scaleLabel: { + labelString: 'Voltage', + display: true + } + }] + } + } + }); + }; + diff --git a/samples/scales/multiline-labels.html b/samples/scales/multiline-labels.html index b7bb041e9..0f8af2a7d 100644 --- a/samples/scales/multiline-labels.html +++ b/samples/scales/multiline-labels.html @@ -2,85 +2,85 @@ - Line Chart - - - + Line Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/non-numeric-y.html b/samples/scales/non-numeric-y.html index 07e319b70..b3bfc5eec 100644 --- a/samples/scales/non-numeric-y.html +++ b/samples/scales/non-numeric-y.html @@ -2,72 +2,71 @@ - Line Chart - - - + Line Chart + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/time/combo.html b/samples/scales/time/combo.html index ddcc5b326..d435e74c6 100644 --- a/samples/scales/time/combo.html +++ b/samples/scales/time/combo.html @@ -7,11 +7,11 @@ @@ -38,12 +38,12 @@ type: 'bar', data: { labels: [ - newDateString(0), - newDateString(1), - newDateString(2), - newDateString(3), - newDateString(4), - newDateString(5), + newDateString(0), + newDateString(1), + newDateString(2), + newDateString(3), + newDateString(4), + newDateString(5), newDateString(6) ], datasets: [{ @@ -52,12 +52,12 @@ backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], }, { @@ -66,12 +66,12 @@ backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], }, { @@ -81,23 +81,23 @@ borderColor: window.chartColors.green, fill: false, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], - }, ] + }] }, options: { - title: { - text:"Chart.js Combo Time Scale" - }, + title: { + text: 'Chart.js Combo Time Scale' + }, scales: { xAxes: [{ - type: "time", + type: 'time', display: true, time: { format: timeFormat, @@ -109,7 +109,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index 3d91dfa56..792eef553 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -18,7 +18,7 @@
-
+


Chart Type: @@ -33,10 +33,8 @@ } function randomBar(date, lastClose) { - var open = randomNumber(lastClose * .95, lastClose * 1.05); - var close = randomNumber(open * .95, open * 1.05); - var high = randomNumber(Math.max(open, close), Math.max(open, close) * 1.1); - var low = randomNumber(Math.min(open, close) * .9, Math.min(open, close)); + var open = randomNumber(lastClose * 0.95, lastClose * 1.05); + var close = randomNumber(open * 0.95, open * 1.05); return { t: date.valueOf(), y: close @@ -55,7 +53,7 @@ } } - var ctx = document.getElementById("chart1").getContext("2d"); + var ctx = document.getElementById('chart1').getContext('2d'); ctx.canvas.width = 1000; ctx.canvas.height = 300; var cfg = { @@ -63,7 +61,7 @@ data: { labels: labels, datasets: [{ - label: "CHRT - Chart.js Corporation", + label: 'CHRT - Chart.js Corporation', data: data, type: 'line', pointRadius: 0, @@ -94,7 +92,7 @@ document.getElementById('update').addEventListener('click', function() { var type = document.getElementById('type').value; - chart.config.data.datasets[0].type = type; + chart.config.data.datasets[0].type = type; chart.update(); }); diff --git a/samples/scales/time/line-point-data.html b/samples/scales/time/line-point-data.html index 3483c4545..e18be91f3 100644 --- a/samples/scales/time/line-point-data.html +++ b/samples/scales/time/line-point-data.html @@ -7,11 +7,11 @@ @@ -38,7 +38,7 @@ type: 'line', data: { datasets: [{ - label: "Dataset with string point data", + label: 'Dataset with string point data', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, fill: false, @@ -56,7 +56,7 @@ y: randomScalingFactor() }], }, { - label: "Dataset with date object point data", + label: 'Dataset with date object point data', backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, fill: false, @@ -77,24 +77,24 @@ }, options: { responsive: true, - title:{ - display:true, - text:"Chart.js Time Point Data" + title: { + display: true, + text: 'Chart.js Time Point Data' }, scales: { xAxes: [{ - type: "time", + type: 'time', display: true, scaleLabel: { display: true, labelString: 'Date' }, - ticks: { - major: { - fontStyle: "bold", - fontColor: "#FF0000" - } - } + ticks: { + major: { + fontStyle: 'bold', + fontColor: '#FF0000' + } + } }], yAxes: [{ display: true, @@ -108,7 +108,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; @@ -122,10 +122,15 @@ window.myLine.update(); }); + // TODO : fix issue with addData + // See https://github.com/chartjs/Chart.js/issues/5197 + // The Add Data button for this sample has no effect. + // An error is logged in the console. document.getElementById('addData').addEventListener('click', function() { if (config.data.datasets.length > 0) { - var numTicks = myLine.scales['x-axis-0'].ticksAsTimestamps.length; - var lastTime = numTicks ? moment(myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); + var numTicks = window.myLine.scales['x-axis-0'].ticksAsTimestamps.length; + var lastTime = numTicks ? moment(window.myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); + var newTime = lastTime .clone() .add(1, 'day') @@ -143,7 +148,7 @@ }); document.getElementById('removeData').addEventListener('click', function() { - config.data.datasets.forEach(function(dataset, datasetIndex) { + config.data.datasets.forEach(function(dataset) { dataset.data.pop(); }); diff --git a/samples/scales/time/line.html b/samples/scales/time/line.html index 0cca931e0..70a6978af 100644 --- a/samples/scales/time/line.html +++ b/samples/scales/time/line.html @@ -7,11 +7,11 @@ @@ -37,10 +37,6 @@ return moment().add(days, 'd').format(timeFormat); } - function newTimestamp(days) { - return moment().add(days, 'd').unix(); - } - var color = Chart.helpers.color; var config = { type: 'line', @@ -55,7 +51,7 @@ newDate(6) ], datasets: [{ - label: "My First dataset", + label: 'My First dataset', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, fill: false, @@ -69,7 +65,7 @@ randomScalingFactor() ], }, { - label: "My Second dataset", + label: 'My Second dataset', backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, fill: false, @@ -83,7 +79,7 @@ randomScalingFactor() ], }, { - label: "Dataset with point data", + label: 'Dataset with point data', backgroundColor: color(window.chartColors.green).alpha(0.5).rgbString(), borderColor: window.chartColors.green, fill: false, @@ -103,12 +99,12 @@ }] }, options: { - title:{ - text: "Chart.js Time Scale" - }, + title: { + text: 'Chart.js Time Scale' + }, scales: { xAxes: [{ - type: "time", + type: 'time', time: { format: timeFormat, // round: 'day' @@ -118,7 +114,7 @@ display: true, labelString: 'Date' } - }, ], + }], yAxes: [{ scaleLabel: { display: true, @@ -130,7 +126,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; @@ -152,7 +148,7 @@ var colorNames = Object.keys(window.chartColors); document.getElementById('addDataset').addEventListener('click', function() { var colorName = colorNames[config.data.datasets.length % colorNames.length]; - var newColor = window.chartColors[colorName] + var newColor = window.chartColors[colorName]; var newDataset = { label: 'Dataset ' + config.data.datasets.length, borderColor: newColor, @@ -173,7 +169,7 @@ config.data.labels.push(newDate(config.data.labels.length)); for (var index = 0; index < config.data.datasets.length; ++index) { - if (typeof config.data.datasets[index].data[0] === "object") { + if (typeof config.data.datasets[index].data[0] === 'object') { config.data.datasets[index].data.push({ x: newDate(config.data.datasets[index].data.length), y: randomScalingFactor(), @@ -195,7 +191,7 @@ document.getElementById('removeData').addEventListener('click', function() { config.data.labels.splice(-1, 1); // remove the label first - config.data.datasets.forEach(function(dataset, datasetIndex) { + config.data.datasets.forEach(function(dataset) { dataset.data.pop(); }); diff --git a/samples/scales/toggle-scale-type.html b/samples/scales/toggle-scale-type.html new file mode 100644 index 000000000..92c943288 --- /dev/null +++ b/samples/scales/toggle-scale-type.html @@ -0,0 +1,99 @@ + + + + + Toggle Scale Type + + + + + + +
+ +
+ + + + + diff --git a/samples/scriptable/bubble.html b/samples/scriptable/bubble.html index feaf1ecb8..a8ce1f474 100644 --- a/samples/scriptable/bubble.html +++ b/samples/scriptable/bubble.html @@ -24,7 +24,6 @@ var MIN_XY = -150; var MAX_XY = 100; - var presets = window.chartColors; var utils = Samples.utils; utils.srand(110); @@ -106,13 +105,15 @@ options: options }); + // eslint-disable-next-line no-unused-vars function randomize() { chart.data.datasets.forEach(function(dataset) { - dataset.data = generateData() + dataset.data = generateData(); }); chart.update(); } + // eslint-disable-next-line no-unused-vars function addDataset() { chart.data.datasets.push({ data: generateData() @@ -120,6 +121,7 @@ chart.update(); } + // eslint-disable-next-line no-unused-vars function removeDataset() { chart.data.datasets.shift(); chart.update(); diff --git a/samples/tooltips/border.html b/samples/tooltips/border.html index 25c649e54..0742be0c5 100644 --- a/samples/tooltips/border.html +++ b/samples/tooltips/border.html @@ -34,9 +34,9 @@ return { type: 'line', data: { - labels: ["January", "February", "March", "April", "May", "June", "July"], + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ - label: "Dataset", + label: 'Dataset', borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, data: [10, 30, 46, 2, 8, 50, 0], @@ -45,7 +45,7 @@ }, options: { responsive: true, - title:{ + title: { display: true, text: 'Sample tooltip with border' }, @@ -78,7 +78,6 @@ var ctx = canvas.getContext('2d'); var config = createConfig(); new Chart(ctx, config); - console.log(config); }; diff --git a/samples/tooltips/callbacks.html b/samples/tooltips/callbacks.html index 590edd1a2..0aa336bd5 100644 --- a/samples/tooltips/callbacks.html +++ b/samples/tooltips/callbacks.html @@ -2,106 +2,106 @@ - Tooltip Hooks - - - + Tooltip Hooks + + + -
- -
- + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/tooltips/custom-line.html b/samples/tooltips/custom-line.html index 801b12df6..96cfac6fd 100644 --- a/samples/tooltips/custom-line.html +++ b/samples/tooltips/custom-line.html @@ -35,7 +35,7 @@
- +
- + Pie Chart with Custom Tooltips + + - + @@ -43,103 +43,103 @@
diff --git a/samples/tooltips/custom-points.html b/samples/tooltips/custom-points.html index 1bb64da4f..f779f62f9 100644 --- a/samples/tooltips/custom-points.html +++ b/samples/tooltips/custom-points.html @@ -41,13 +41,13 @@
diff --git a/samples/tooltips/positioning.html b/samples/tooltips/positioning.html index 696584b06..f98cd638c 100644 --- a/samples/tooltips/positioning.html +++ b/samples/tooltips/positioning.html @@ -34,15 +34,15 @@ return { type: 'line', data: { - labels: ["January", "February", "March", "April", "May", "June", "July"], + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ - label: "My First dataset", + label: 'My First dataset', borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, data: [10, 30, 46, 2, 8, 50, 0], fill: false, }, { - label: "My Second dataset", + label: 'My Second dataset', borderColor: window.chartColors.blue, backgroundColor: window.chartColors.blue, data: [7, 49, 46, 13, 25, 30, 22], @@ -51,7 +51,7 @@ }, options: { responsive: true, - title:{ + title: { display: true, text: 'Tooltip Position: ' + position }, @@ -78,7 +78,7 @@ var ctx = canvas.getContext('2d'); var config = createConfig(position); new Chart(ctx, config); - }) + }); }; diff --git a/src/chart.js b/src/chart.js index df17bb92c..a958e343f 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,13 +12,14 @@ Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); +Chart.layouts = require('./core/core.layouts'); Chart.platform = require('./platforms/platform'); +Chart.plugins = require('./core/core.plugins'); +Chart.Ticks = require('./core/core.ticks'); -require('./core/core.plugin')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); -require('./core/core.layoutService')(Chart); require('./core/core.scaleService')(Chart); require('./core/core.scale')(Chart); require('./core/core.tooltip')(Chart); @@ -49,15 +50,12 @@ require('./charts/Chart.Radar')(Chart); require('./charts/Chart.Scatter')(Chart); // Loading built-it plugins -var plugins = []; - -plugins.push( - require('./plugins/plugin.filler')(Chart), - require('./plugins/plugin.legend')(Chart), - require('./plugins/plugin.title')(Chart) -); - -Chart.plugins.register(plugins); +var plugins = require('./plugins'); +for (var k in plugins) { + if (plugins.hasOwnProperty(k)) { + Chart.plugins.register(plugins[k]); + } +} Chart.platform.initialize(); @@ -68,6 +66,43 @@ if (typeof window !== 'undefined') { // DEPRECATIONS +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Legend + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Legend = plugins.legend._element; + +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Title + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Title = plugins.title._element; + +/** + * Provided for backward compatibility, use Chart.plugins instead + * @namespace Chart.pluginService + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.pluginService = Chart.plugins; + +/** + * Provided for backward compatibility, inheriting from Chart.PlugingBase has no + * effect, instead simply create/register plugins via plain JavaScript objects. + * @interface Chart.PluginBase + * @deprecated since version 2.5.0 + * @todo remove at version 3 + * @private + */ +Chart.PluginBase = Chart.Element.extend({}); + /** * Provided for backward compatibility, use Chart.helpers.canvas instead. * @namespace Chart.canvasHelpers @@ -76,3 +111,12 @@ if (typeof window !== 'undefined') { * @private */ Chart.canvasHelpers = Chart.helpers.canvas; + +/** + * Provided for backward compatibility, use Chart.layouts instead. + * @namespace Chart.layoutService + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +Chart.layoutService = Chart.layouts; diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index ae4aee45d..ff2b56ae5 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -95,6 +95,93 @@ defaults._set('horizontalBar', { } }); +/** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ +function computeMinSampleSize(scale, pixels) { + var min = scale.isHorizontal() ? scale.width : scale.height; + var ticks = scale.getTicks(); + var prev, curr, i, ilen; + + for (i = 1, ilen = pixels.length; i < ilen; ++i) { + min = Math.min(min, pixels[i] - pixels[i - 1]); + } + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + min = i > 0 ? Math.min(min, curr - prev) : min; + prev = curr; + } + + return min; +} + +/** + * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, + * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This + * mode currently always generates bars equally sized (until we introduce scriptable options?). + * @private + */ +function computeFitCategoryTraits(index, ruler, options) { + var thickness = options.barThickness; + var count = ruler.stackCount; + var curr = ruler.pixels[index]; + var size, ratio; + + if (helpers.isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * count; + ratio = 1; + } + + return { + chunk: size / count, + ratio: ratio, + start: curr - (size / 2) + }; +} + +/** + * Computes an "optimal" category that globally arranges bars side by side (no gap when + * percentage options are 1), based on the previous and following categories. This mode + * generates bars with different widths when data are not evenly spaced. + * @private + */ +function computeFlexCategoryTraits(index, ruler, options) { + var pixels = ruler.pixels; + var curr = pixels[index]; + var prev = index > 0 ? pixels[index - 1] : null; + var next = index < pixels.length - 1 ? pixels[index + 1] : null; + var percent = options.categoryPercentage; + var start, size; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale end extremity. + prev = curr - (next === null ? ruler.end - curr : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + + start = curr - ((curr - prev) / 2) * percent; + size = ((next - prev) / 2) * percent; + + return { + chunk: size / ruler.stackCount, + ratio: options.barPercentage, + start: start + }; +} + module.exports = function(Chart) { Chart.controllers.bar = Chart.DatasetController.extend({ @@ -201,10 +288,12 @@ module.exports = function(Chart) { }, /** - * Returns the effective number of stacks based on groups and bar visibility. + * Returns the stacks based on groups and bar visibility. + * @param {Number} [last] - The dataset index + * @returns {Array} The stack list * @private */ - getStackCount: function(last) { + _getStacks: function(last) { var me = this; var chart = me.chart; var scale = me.getIndexScale(); @@ -223,15 +312,33 @@ module.exports = function(Chart) { } } - return stacks.length; + return stacks; + }, + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + getStackCount: function() { + return this._getStacks().length; }, /** * Returns the stack index for the given dataset based on groups and bar visibility. + * @param {Number} [datasetIndex] - The dataset index + * @param {String} [name] - The stack name to find + * @returns {Number} The stack index * @private */ - getStackIndex: function(datasetIndex) { - return this.getStackCount(datasetIndex) - 1; + getStackIndex: function(datasetIndex, name) { + var stacks = this._getStacks(datasetIndex); + var index = (name !== undefined) + ? stacks.indexOf(name) + : -1; // indexOf returns -1 if element is not present + + return (index === -1) + ? stacks.length - 1 + : index; }, /** @@ -242,17 +349,22 @@ module.exports = function(Chart) { var scale = me.getIndexScale(); var stackCount = me.getStackCount(); var datasetIndex = me.index; - var pixels = []; var isHorizontal = scale.isHorizontal(); var start = isHorizontal ? scale.left : scale.top; var end = start + (isHorizontal ? scale.width : scale.height); - var i, ilen; + var pixels = []; + var i, ilen, min; for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { pixels.push(scale.getPixelForValue(null, i, datasetIndex)); } + min = helpers.isNullOrUndef(scale.options.barThickness) + ? computeMinSampleSize(scale, pixels) + : -1; + return { + min: min, pixels: pixels, start: start, end: end, @@ -312,50 +424,21 @@ module.exports = function(Chart) { calculateBarIndexPixels: function(datasetIndex, index, ruler) { var me = this; var options = ruler.scale.options; - var stackIndex = me.getStackIndex(datasetIndex); - var pixels = ruler.pixels; - var base = pixels[index]; - var length = pixels.length; - var start = ruler.start; - var end = ruler.end; - var leftSampleSize, rightSampleSize, leftCategorySize, rightCategorySize, fullBarSize, size; + var range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options) + : computeFitCategoryTraits(index, ruler, options); - if (length === 1) { - leftSampleSize = base > start ? base - start : end - base; - rightSampleSize = base < end ? end - base : base - start; - } else { - if (index > 0) { - leftSampleSize = (base - pixels[index - 1]) / 2; - if (index === length - 1) { - rightSampleSize = leftSampleSize; - } - } - if (index < length - 1) { - rightSampleSize = (pixels[index + 1] - base) / 2; - if (index === 0) { - leftSampleSize = rightSampleSize; - } - } - } - - leftCategorySize = leftSampleSize * options.categoryPercentage; - rightCategorySize = rightSampleSize * options.categoryPercentage; - fullBarSize = (leftCategorySize + rightCategorySize) / ruler.stackCount; - size = fullBarSize * options.barPercentage; - - size = Math.min( - helpers.valueOrDefault(options.barThickness, size), - helpers.valueOrDefault(options.maxBarThickness, Infinity)); - - base -= leftCategorySize; - base += fullBarSize * stackIndex; - base += (fullBarSize - size) / 2; + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + helpers.valueOrDefault(options.maxBarThickness, Infinity), + range.chunk * range.ratio); return { - size: size, - base: base, - head: base + size, - center: base + size / 2 + base: center - size / 2, + head: center + size / 2, + center: center, + size: size }; }, diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 6028602c4..f7b36e989 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -273,7 +273,7 @@ module.exports = function(Chart) { calculateCircumference: function(value) { var total = this.getMeta().total; if (total > 0 && !isNaN(value)) { - return (Math.PI * 2.0) * (value / total); + return (Math.PI * 2.0) * (Math.abs(value) / total); } return 0; }, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 9e4984a9a..e29a5b076 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,10 +3,11 @@ 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'); module.exports = function(Chart) { - var plugins = Chart.plugins; // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; @@ -45,17 +46,21 @@ module.exports = function(Chart) { function updateConfig(chart) { var newOptions = chart.options; - // Update Scale(s) with options - if (newOptions.scale) { - chart.scale.options = newOptions.scale; - } else if (newOptions.scales) { - newOptions.scales.xAxes.concat(newOptions.scales.yAxes).forEach(function(scaleOptions) { - chart.scales[scaleOptions.id].options = scaleOptions; - }); - } + helpers.each(chart.scales, function(scale) { + layouts.removeBox(chart, scale); + }); + newOptions = helpers.configMerge( + Chart.defaults.global, + Chart.defaults[chart.config.type], + newOptions); + + chart.options = chart.config.options = newOptions; + chart.ensureScalesHaveIDs(); + chart.buildOrUpdateScales(); // Tooltip chart.tooltip._options = newOptions.tooltips; + chart.tooltip.initialize(); } function positionIsHorizontal(position) { @@ -143,7 +148,7 @@ module.exports = function(Chart) { // Make sure scales have IDs and are built before we build any controllers. me.ensureScalesHaveIDs(); - me.buildScales(); + me.buildOrUpdateScales(); me.initToolTip(); // After init plugin notification @@ -223,11 +228,15 @@ module.exports = function(Chart) { /** * Builds a map of scale ID to scale object for future lookup. */ - buildScales: function() { + buildOrUpdateScales: function() { var me = this; var options = me.options; - var scales = me.scales = {}; + var scales = me.scales || {}; var items = []; + var updated = Object.keys(scales).reduce(function(obj, id) { + obj[id] = false; + return obj; + }, {}); if (options.scales) { items = items.concat( @@ -251,24 +260,35 @@ module.exports = function(Chart) { helpers.each(items, function(item) { var scaleOptions = item.options; + var id = scaleOptions.id; var scaleType = helpers.valueOrDefault(scaleOptions.type, item.dtype); - var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); - if (!scaleClass) { - return; - } if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { scaleOptions.position = item.dposition; } - var scale = new scaleClass({ - id: scaleOptions.id, - options: scaleOptions, - ctx: me.ctx, - chart: me - }); + updated[id] = true; + var scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + scale.options = scaleOptions; + scale.ctx = me.ctx; + scale.chart = me; + } else { + var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); + if (!scaleClass) { + return; + } + scale = new scaleClass({ + id: id, + type: scaleType, + options: scaleOptions, + ctx: me.ctx, + chart: me + }); + scales[scale.id] = scale; + } - scales[scale.id] = scale; scale.mergeTicksOptions(); // TODO(SB): I think we should be able to remove this custom case (options.scale) @@ -278,6 +298,14 @@ module.exports = function(Chart) { me.scale = scale; } }); + // clear up discarded scales + helpers.each(updated, function(hasUpdated, id) { + if (!hasUpdated) { + delete scales[id]; + } + }); + + me.scales = scales; Chart.scaleService.addScalesToLayout(this); }, @@ -301,6 +329,7 @@ module.exports = function(Chart) { if (meta.controller) { meta.controller.updateIndex(datasetIndex); + meta.controller.linkScales(); } else { var ControllerClass = Chart.controllers[meta.type]; if (ControllerClass === undefined) { @@ -347,6 +376,10 @@ module.exports = function(Chart) { updateConfig(me); + // plugins options references might have change, let's invalidate the cache + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + plugins._invalidate(me); + if (plugins.notify(me, 'beforeUpdate') === false) { return; } @@ -365,9 +398,11 @@ module.exports = function(Chart) { me.updateLayout(); // Can only reset the new controllers after the scales have been updated - helpers.each(newControllers, function(controller) { - controller.reset(); - }); + if (me.options.animation && me.options.animation.duration) { + helpers.each(newControllers, function(controller) { + controller.reset(); + }); + } me.updateDatasets(); @@ -405,7 +440,7 @@ module.exports = function(Chart) { return; } - Chart.layoutService.update(this, this.width, this.height); + layouts.update(this, this.width, this.height); /** * Provided for backward compatibility, use `afterLayout` instead. @@ -819,7 +854,15 @@ module.exports = function(Chart) { me._bufferedRequest = null; var changed = me.handleEvent(e); - changed |= tooltip && tooltip.handleEvent(e); + // for smooth tooltip animations issue #4989 + // the tooltip should be the source of change + // Animation check workaround: + // tooltip._start will be null when tooltip isn't animating + if (tooltip) { + changed = tooltip._start + ? tooltip.handleEvent(e) + : changed | tooltip.handleEvent(e); + } plugins.notify(me, 'afterEvent', [e]); diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 67dbe27f2..ee6158c35 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -111,10 +111,10 @@ module.exports = function(Chart) { var meta = me.getMeta(); var dataset = me.getDataset(); - if (meta.xAxisID === null) { + if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; } - if (meta.yAxisID === null) { + if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; } }, diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 713362cd8..347fd958e 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -159,7 +159,13 @@ module.exports = function(Chart) { return Math.log10(x); } : function(x) { - return Math.log(x) / Math.LN10; + var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10. + // Check for whole powers of 10, + // which due to floating point rounding error should be corrected. + var powerOf10 = Math.round(exponent); + var isPowerOf10 = x === Math.pow(10, powerOf10); + + return isPowerOf10 ? powerOf10 : exponent; }; helpers.toRadians = function(degrees) { return degrees * (Math.PI / 180); @@ -512,8 +518,10 @@ module.exports = function(Chart) { // If no style has been set on the canvas, the render size is used as display size, // making the chart visually bigger, so let's enforce it to the "correct" values. // See https://github.com/chartjs/Chart.js/issues/3575 - canvas.style.height = height + 'px'; - canvas.style.width = width + 'px'; + if (!canvas.style.height && !canvas.style.width) { + canvas.style.height = height + 'px'; + canvas.style.width = width + 'px'; + } }; // -- Canvas methods helpers.fontString = function(pixelSize, fontStyle, fontFamily) { diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js deleted file mode 100644 index 5649f113d..000000000 --- a/src/core/core.layoutService.js +++ /dev/null @@ -1,422 +0,0 @@ -'use strict'; - -var helpers = require('../helpers/index'); - -module.exports = function(Chart) { - - function filterByPosition(array, position) { - return helpers.where(array, function(v) { - return v.position === position; - }); - } - - function sortByWeight(array, reverse) { - array.forEach(function(v, i) { - v._tmpIndex_ = i; - return v; - }); - array.sort(function(a, b) { - var v0 = reverse ? b : a; - var v1 = reverse ? a : b; - return v0.weight === v1.weight ? - v0._tmpIndex_ - v1._tmpIndex_ : - v0.weight - v1.weight; - }); - array.forEach(function(v) { - delete v._tmpIndex_; - }); - } - - /** - * @interface ILayoutItem - * @prop {String} position - The position of the item in the chart layout. Possible values are - * 'left', 'top', 'right', 'bottom', and 'chartArea' - * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area - * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down - * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) - * @prop {Function} update - Takes two parameters: width and height. Returns size of item - * @prop {Function} getPadding - Returns an object with padding on the edges - * @prop {Number} width - Width of item. Must be valid after update() - * @prop {Number} height - Height of item. Must be valid after update() - * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update - * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update - * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update - * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update - */ - - // The layout service is very self explanatory. It's responsible for the layout within a chart. - // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need - // It is this service's responsibility of carrying out that layout. - Chart.layoutService = { - defaults: {}, - - /** - * Register a box to a chart. - * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. - * @param {Chart} chart - the chart to use - * @param {ILayoutItem} item - the item to add to be layed out - */ - addBox: function(chart, item) { - if (!chart.boxes) { - chart.boxes = []; - } - - // initialize item with default values - item.fullWidth = item.fullWidth || false; - item.position = item.position || 'top'; - item.weight = item.weight || 0; - - chart.boxes.push(item); - }, - - /** - * Remove a layoutItem from a chart - * @param {Chart} chart - the chart to remove the box from - * @param {Object} layoutItem - the item to remove from the layout - */ - removeBox: function(chart, layoutItem) { - var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; - if (index !== -1) { - chart.boxes.splice(index, 1); - } - }, - - /** - * Sets (or updates) options on the given `item`. - * @param {Chart} chart - the chart in which the item lives (or will be added to) - * @param {Object} item - the item to configure with the given options - * @param {Object} options - the new item options. - */ - configure: function(chart, item, options) { - var props = ['fullWidth', 'position', 'weight']; - var ilen = props.length; - var i = 0; - var prop; - - for (; i < ilen; ++i) { - prop = props[i]; - if (options.hasOwnProperty(prop)) { - item[prop] = options[prop]; - } - } - }, - - /** - * Fits boxes of the given chart into the given size by having each box measure itself - * then running a fitting algorithm - * @param {Chart} chart - the chart - * @param {Number} width - the width to fit into - * @param {Number} height - the height to fit into - */ - update: function(chart, width, height) { - if (!chart) { - return; - } - - var layoutOptions = chart.options.layout || {}; - var padding = helpers.options.toPadding(layoutOptions.padding); - var leftPadding = padding.left; - var rightPadding = padding.right; - var topPadding = padding.top; - var bottomPadding = padding.bottom; - - var leftBoxes = filterByPosition(chart.boxes, 'left'); - var rightBoxes = filterByPosition(chart.boxes, 'right'); - var topBoxes = filterByPosition(chart.boxes, 'top'); - var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); - var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); - - // Sort boxes by weight. A higher weight is further away from the chart area - sortByWeight(leftBoxes, true); - sortByWeight(rightBoxes, false); - sortByWeight(topBoxes, true); - sortByWeight(bottomBoxes, false); - - // Essentially we now have any number of boxes on each of the 4 sides. - // Our canvas looks like the following. - // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and - // B1 is the bottom axis - // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays - // These locations are single-box locations only, when trying to register a chartArea location that is already taken, - // an error will be thrown. - // - // |----------------------------------------------------| - // | T1 (Full Width) | - // |----------------------------------------------------| - // | | | T2 | | - // | |----|-------------------------------------|----| - // | | | C1 | | C2 | | - // | | |----| |----| | - // | | | | | - // | L1 | L2 | ChartArea (C0) | R1 | - // | | | | | - // | | |----| |----| | - // | | | C3 | | C4 | | - // | |----|-------------------------------------|----| - // | | | B1 | | - // |----------------------------------------------------| - // | B2 (Full Width) | - // |----------------------------------------------------| - // - // What we do to find the best sizing, we do the following - // 1. Determine the minimum size of the chart area. - // 2. Split the remaining width equally between each vertical axis - // 3. Split the remaining height equally between each horizontal axis - // 4. Give each layout the maximum size it can be. The layout will return it's minimum size - // 5. Adjust the sizes of each axis based on it's minimum reported size. - // 6. Refit each axis - // 7. Position each axis in the final location - // 8. Tell the chart the final location of the chart area - // 9. Tell any axes that overlay the chart area the positions of the chart area - - // Step 1 - var chartWidth = width - leftPadding - rightPadding; - var chartHeight = height - topPadding - bottomPadding; - var chartAreaWidth = chartWidth / 2; // min 50% - var chartAreaHeight = chartHeight / 2; // min 50% - - // Step 2 - var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); - - // Step 3 - var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); - - // Step 4 - var maxChartAreaWidth = chartWidth; - var maxChartAreaHeight = chartHeight; - var minBoxSizes = []; - - function getMinimumBoxSize(box) { - var minSize; - var isHorizontal = box.isHorizontal(); - - if (isHorizontal) { - minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); - maxChartAreaHeight -= minSize.height; - } else { - minSize = box.update(verticalBoxWidth, chartAreaHeight); - maxChartAreaWidth -= minSize.width; - } - - minBoxSizes.push({ - horizontal: isHorizontal, - minSize: minSize, - box: box, - }); - } - - helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); - - // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) - var maxHorizontalLeftPadding = 0; - var maxHorizontalRightPadding = 0; - var maxVerticalTopPadding = 0; - var maxVerticalBottomPadding = 0; - - helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { - if (horizontalBox.getPadding) { - var boxPadding = horizontalBox.getPadding(); - maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); - maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); - } - }); - - helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { - if (verticalBox.getPadding) { - var boxPadding = verticalBox.getPadding(); - maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); - maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); - } - }); - - // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could - // be if the axes are drawn at their minimum sizes. - // Steps 5 & 6 - var totalLeftBoxesWidth = leftPadding; - var totalRightBoxesWidth = rightPadding; - var totalTopBoxesHeight = topPadding; - var totalBottomBoxesHeight = bottomPadding; - - // Function to fit a box - function fitBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { - return minBox.box === box; - }); - - if (minBoxSize) { - if (box.isHorizontal()) { - var scaleMargin = { - left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), - right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), - top: 0, - bottom: 0 - }; - - // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends - // on the margin. Sometimes they need to increase in size slightly - box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); - } else { - box.update(minBoxSize.minSize.width, maxChartAreaHeight); - } - } - } - - // Update, and calculate the left and right margins for the horizontal boxes - helpers.each(leftBoxes.concat(rightBoxes), fitBox); - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - // Set the Left and Right margins for the horizontal boxes - helpers.each(topBoxes.concat(bottomBoxes), fitBox); - - // Figure out how much margin is on the top and bottom of the vertical boxes - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - function finalFitVerticalBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { - return minSize.box === box; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: totalTopBoxesHeight, - bottom: totalBottomBoxesHeight - }; - - if (minBoxSize) { - box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); - } - } - - // Let the left layout know the final margin - helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); - - // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) - totalLeftBoxesWidth = leftPadding; - totalRightBoxesWidth = rightPadding; - totalTopBoxesHeight = topPadding; - totalBottomBoxesHeight = bottomPadding; - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - // We may be adding some padding to account for rotated x axis labels - var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); - totalLeftBoxesWidth += leftPaddingAddition; - totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); - - var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); - totalTopBoxesHeight += topPaddingAddition; - totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); - - // Figure out if our chart area changed. This would occur if the dataset layout label rotation - // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do - // without calling `fit` again - var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; - var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; - - if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { - helpers.each(leftBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(rightBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(topBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - helpers.each(bottomBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - maxChartAreaHeight = newMaxChartAreaHeight; - maxChartAreaWidth = newMaxChartAreaWidth; - } - - // Step 7 - Position the boxes - var left = leftPadding + leftPaddingAddition; - var top = topPadding + topPaddingAddition; - - function placeBox(box) { - if (box.isHorizontal()) { - box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; - box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; - box.top = top; - box.bottom = top + box.height; - - // Move to next point - top = box.bottom; - - } else { - - box.left = left; - box.right = left + box.width; - box.top = totalTopBoxesHeight; - box.bottom = totalTopBoxesHeight + maxChartAreaHeight; - - // Move to next point - left = box.right; - } - } - - helpers.each(leftBoxes.concat(topBoxes), placeBox); - - // Account for chart width and height - left += maxChartAreaWidth; - top += maxChartAreaHeight; - - helpers.each(rightBoxes, placeBox); - helpers.each(bottomBoxes, placeBox); - - // Step 8 - chart.chartArea = { - left: totalLeftBoxesWidth, - top: totalTopBoxesHeight, - right: totalLeftBoxesWidth + maxChartAreaWidth, - bottom: totalTopBoxesHeight + maxChartAreaHeight - }; - - // Step 9 - helpers.each(chartAreaBoxes, function(box) { - box.left = chart.chartArea.left; - box.top = chart.chartArea.top; - box.right = chart.chartArea.right; - box.bottom = chart.chartArea.bottom; - - box.update(maxChartAreaWidth, maxChartAreaHeight); - }); - } - }; -}; diff --git a/src/core/core.layouts.js b/src/core/core.layouts.js new file mode 100644 index 000000000..b99612bbe --- /dev/null +++ b/src/core/core.layouts.js @@ -0,0 +1,419 @@ +'use strict'; + +var helpers = require('../helpers/index'); + +function filterByPosition(array, position) { + return helpers.where(array, function(v) { + return v.position === position; + }); +} + +function sortByWeight(array, reverse) { + array.forEach(function(v, i) { + v._tmpIndex_ = i; + return v; + }); + array.sort(function(a, b) { + var v0 = reverse ? b : a; + var v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0._tmpIndex_ - v1._tmpIndex_ : + v0.weight - v1.weight; + }); + array.forEach(function(v) { + delete v._tmpIndex_; + }); +} + +/** + * @interface ILayoutItem + * @prop {String} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {Function} update - Takes two parameters: width and height. Returns size of item + * @prop {Function} getPadding - Returns an object with padding on the edges + * @prop {Number} width - Width of item. Must be valid after update() + * @prop {Number} height - Height of item. Must be valid after update() + * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + +// The layout service is very self explanatory. It's responsible for the layout within a chart. +// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need +// It is this service's responsibility of carrying out that layout. +module.exports = { + defaults: {}, + + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {ILayoutItem} item - the item to add to be layed out + */ + addBox: function(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + + // initialize item with default values + item.fullWidth = item.fullWidth || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + + chart.boxes.push(item); + }, + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {Object} layoutItem - the item to remove from the layout + */ + removeBox: function(chart, layoutItem) { + var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {Object} item - the item to configure with the given options + * @param {Object} options - the new item options. + */ + configure: function(chart, item, options) { + var props = ['fullWidth', 'position', 'weight']; + var ilen = props.length; + var i = 0; + var prop; + + for (; i < ilen; ++i) { + prop = props[i]; + if (options.hasOwnProperty(prop)) { + item[prop] = options[prop]; + } + } + }, + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {Number} width - the width to fit into + * @param {Number} height - the height to fit into + */ + update: function(chart, width, height) { + if (!chart) { + return; + } + + var layoutOptions = chart.options.layout || {}; + var padding = helpers.options.toPadding(layoutOptions.padding); + var leftPadding = padding.left; + var rightPadding = padding.right; + var topPadding = padding.top; + var bottomPadding = padding.bottom; + + var leftBoxes = filterByPosition(chart.boxes, 'left'); + var rightBoxes = filterByPosition(chart.boxes, 'right'); + var topBoxes = filterByPosition(chart.boxes, 'top'); + var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); + var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); + + // Sort boxes by weight. A higher weight is further away from the chart area + sortByWeight(leftBoxes, true); + sortByWeight(rightBoxes, false); + sortByWeight(topBoxes, true); + sortByWeight(bottomBoxes, false); + + // Essentially we now have any number of boxes on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays + // These locations are single-box locations only, when trying to register a chartArea location that is already taken, + // an error will be thrown. + // + // |----------------------------------------------------| + // | T1 (Full Width) | + // |----------------------------------------------------| + // | | | T2 | | + // | |----|-------------------------------------|----| + // | | | C1 | | C2 | | + // | | |----| |----| | + // | | | | | + // | L1 | L2 | ChartArea (C0) | R1 | + // | | | | | + // | | |----| |----| | + // | | | C3 | | C4 | | + // | |----|-------------------------------------|----| + // | | | B1 | | + // |----------------------------------------------------| + // | B2 (Full Width) | + // |----------------------------------------------------| + // + // What we do to find the best sizing, we do the following + // 1. Determine the minimum size of the chart area. + // 2. Split the remaining width equally between each vertical axis + // 3. Split the remaining height equally between each horizontal axis + // 4. Give each layout the maximum size it can be. The layout will return it's minimum size + // 5. Adjust the sizes of each axis based on it's minimum reported size. + // 6. Refit each axis + // 7. Position each axis in the final location + // 8. Tell the chart the final location of the chart area + // 9. Tell any axes that overlay the chart area the positions of the chart area + + // Step 1 + var chartWidth = width - leftPadding - rightPadding; + var chartHeight = height - topPadding - bottomPadding; + var chartAreaWidth = chartWidth / 2; // min 50% + var chartAreaHeight = chartHeight / 2; // min 50% + + // Step 2 + var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); + + // Step 3 + var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); + + // Step 4 + var maxChartAreaWidth = chartWidth; + var maxChartAreaHeight = chartHeight; + var minBoxSizes = []; + + function getMinimumBoxSize(box) { + var minSize; + var isHorizontal = box.isHorizontal(); + + if (isHorizontal) { + minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); + maxChartAreaHeight -= minSize.height; + } else { + minSize = box.update(verticalBoxWidth, maxChartAreaHeight); + maxChartAreaWidth -= minSize.width; + } + + minBoxSizes.push({ + horizontal: isHorizontal, + minSize: minSize, + box: box, + }); + } + + helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); + + // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) + var maxHorizontalLeftPadding = 0; + var maxHorizontalRightPadding = 0; + var maxVerticalTopPadding = 0; + var maxVerticalBottomPadding = 0; + + helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { + if (horizontalBox.getPadding) { + var boxPadding = horizontalBox.getPadding(); + maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); + maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); + } + }); + + helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { + if (verticalBox.getPadding) { + var boxPadding = verticalBox.getPadding(); + maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); + maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); + } + }); + + // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could + // be if the axes are drawn at their minimum sizes. + // Steps 5 & 6 + var totalLeftBoxesWidth = leftPadding; + var totalRightBoxesWidth = rightPadding; + var totalTopBoxesHeight = topPadding; + var totalBottomBoxesHeight = bottomPadding; + + // Function to fit a box + function fitBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { + return minBox.box === box; + }); + + if (minBoxSize) { + if (box.isHorizontal()) { + var scaleMargin = { + left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), + right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), + top: 0, + bottom: 0 + }; + + // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends + // on the margin. Sometimes they need to increase in size slightly + box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); + } else { + box.update(minBoxSize.minSize.width, maxChartAreaHeight); + } + } + } + + // Update, and calculate the left and right margins for the horizontal boxes + helpers.each(leftBoxes.concat(rightBoxes), fitBox); + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + // Set the Left and Right margins for the horizontal boxes + helpers.each(topBoxes.concat(bottomBoxes), fitBox); + + // Figure out how much margin is on the top and bottom of the vertical boxes + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + + helpers.each(bottomBoxes, function(box) { + totalBottomBoxesHeight += box.height; + }); + + function finalFitVerticalBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { + return minSize.box === box; + }); + + var scaleMargin = { + left: 0, + right: 0, + top: totalTopBoxesHeight, + bottom: totalBottomBoxesHeight + }; + + if (minBoxSize) { + box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); + } + } + + // Let the left layout know the final margin + helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); + + // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) + totalLeftBoxesWidth = leftPadding; + totalRightBoxesWidth = rightPadding; + totalTopBoxesHeight = topPadding; + totalBottomBoxesHeight = bottomPadding; + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + helpers.each(bottomBoxes, function(box) { + totalBottomBoxesHeight += box.height; + }); + + // We may be adding some padding to account for rotated x axis labels + var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); + totalLeftBoxesWidth += leftPaddingAddition; + totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); + + var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); + totalTopBoxesHeight += topPaddingAddition; + totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); + + // Figure out if our chart area changed. This would occur if the dataset layout label rotation + // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do + // without calling `fit` again + var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; + var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; + + if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { + helpers.each(leftBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(rightBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(topBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + helpers.each(bottomBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + maxChartAreaHeight = newMaxChartAreaHeight; + maxChartAreaWidth = newMaxChartAreaWidth; + } + + // Step 7 - Position the boxes + var left = leftPadding + leftPaddingAddition; + var top = topPadding + topPaddingAddition; + + function placeBox(box) { + if (box.isHorizontal()) { + box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; + box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; + box.top = top; + box.bottom = top + box.height; + + // Move to next point + top = box.bottom; + + } else { + + box.left = left; + box.right = left + box.width; + box.top = totalTopBoxesHeight; + box.bottom = totalTopBoxesHeight + maxChartAreaHeight; + + // Move to next point + left = box.right; + } + } + + helpers.each(leftBoxes.concat(topBoxes), placeBox); + + // Account for chart width and height + left += maxChartAreaWidth; + top += maxChartAreaHeight; + + helpers.each(rightBoxes, placeBox); + helpers.each(bottomBoxes, placeBox); + + // Step 8 + chart.chartArea = { + left: totalLeftBoxesWidth, + top: totalTopBoxesHeight, + right: totalLeftBoxesWidth + maxChartAreaWidth, + bottom: totalTopBoxesHeight + maxChartAreaHeight + }; + + // Step 9 + helpers.each(chartAreaBoxes, function(box) { + box.left = chart.chartArea.left; + box.top = chart.chartArea.top; + box.right = chart.chartArea.right; + box.bottom = chart.chartArea.bottom; + + box.update(maxChartAreaWidth, maxChartAreaHeight); + }); + } +}; diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js deleted file mode 100644 index 0b7423a69..000000000 --- a/src/core/core.plugin.js +++ /dev/null @@ -1,395 +0,0 @@ -'use strict'; - -var defaults = require('./core.defaults'); -var Element = require('./core.element'); -var helpers = require('../helpers/index'); - -defaults._set('global', { - plugins: {} -}); - -module.exports = function(Chart) { - - /** - * The plugin service singleton - * @namespace Chart.plugins - * @since 2.1.0 - */ - Chart.plugins = { - /** - * Globally registered plugins. - * @private - */ - _plugins: [], - - /** - * This identifier is used to invalidate the descriptors cache attached to each chart - * when a global plugin is registered or unregistered. In this case, the cache ID is - * incremented and descriptors are regenerated during following API calls. - * @private - */ - _cacheId: 0, - - /** - * Registers the given plugin(s) if not already registered. - * @param {Array|Object} plugins plugin instance(s). - */ - register: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - if (p.indexOf(plugin) === -1) { - p.push(plugin); - } - }); - - this._cacheId++; - }, - - /** - * Unregisters the given plugin(s) only if registered. - * @param {Array|Object} plugins plugin instance(s). - */ - unregister: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - var idx = p.indexOf(plugin); - if (idx !== -1) { - p.splice(idx, 1); - } - }); - - this._cacheId++; - }, - - /** - * Remove all registered plugins. - * @since 2.1.5 - */ - clear: function() { - this._plugins = []; - this._cacheId++; - }, - - /** - * Returns the number of registered plugins? - * @returns {Number} - * @since 2.1.5 - */ - count: function() { - return this._plugins.length; - }, - - /** - * Returns all registered plugin instances. - * @returns {Array} array of plugin objects. - * @since 2.1.5 - */ - getAll: function() { - return this._plugins; - }, - - /** - * Calls enabled plugins for `chart` on the specified hook and with the given args. - * This method immediately returns as soon as a plugin explicitly returns false. The - * returned value can be used, for instance, to interrupt the current action. - * @param {Object} chart - The chart instance for which plugins should be called. - * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). - * @param {Array} [args] - Extra arguments to apply to the hook call. - * @returns {Boolean} false if any of the plugins return false, else returns true. - */ - notify: function(chart, hook, args) { - var descriptors = this.descriptors(chart); - var ilen = descriptors.length; - var i, descriptor, plugin, params, method; - - for (i = 0; i < ilen; ++i) { - descriptor = descriptors[i]; - plugin = descriptor.plugin; - method = plugin[hook]; - if (typeof method === 'function') { - params = [chart].concat(args || []); - params.push(descriptor.options); - if (method.apply(plugin, params) === false) { - return false; - } - } - } - - return true; - }, - - /** - * Returns descriptors of enabled plugins for the given chart. - * @returns {Array} [{ plugin, options }] - * @private - */ - descriptors: function(chart) { - var cache = chart._plugins || (chart._plugins = {}); - if (cache.id === this._cacheId) { - return cache.descriptors; - } - - var plugins = []; - var descriptors = []; - var config = (chart && chart.config) || {}; - var options = (config.options && config.options.plugins) || {}; - - this._plugins.concat(config.plugins || []).forEach(function(plugin) { - var idx = plugins.indexOf(plugin); - if (idx !== -1) { - return; - } - - var id = plugin.id; - var opts = options[id]; - if (opts === false) { - return; - } - - if (opts === true) { - opts = helpers.clone(defaults.global.plugins[id]); - } - - plugins.push(plugin); - descriptors.push({ - plugin: plugin, - options: opts || {} - }); - }); - - cache.descriptors = descriptors; - cache.id = this._cacheId; - return descriptors; - } - }; - - /** - * Plugin extension hooks. - * @interface IPlugin - * @since 2.1.0 - */ - /** - * @method IPlugin#beforeInit - * @desc Called before initializing `chart`. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterInit - * @desc Called after `chart` has been initialized and before the first update. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeUpdate - * @desc Called before updating `chart`. If any plugin returns `false`, the update - * is cancelled (and thus subsequent render(s)) until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart update. - */ - /** - * @method IPlugin#afterUpdate - * @desc Called after `chart` has been updated and before rendering. Note that this - * hook will not be called if the chart update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsUpdate - * @desc Called before updating the `chart` datasets. If any plugin returns `false`, - * the datasets update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} false to cancel the datasets update. - * @since version 2.1.5 - */ - /** - * @method IPlugin#afterDatasetsUpdate - * @desc Called after the `chart` datasets have been updated. Note that this hook - * will not be called if the datasets update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @since version 2.1.5 - */ - /** - * @method IPlugin#beforeDatasetUpdate - * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin - * returns `false`, the datasets update is cancelled until another `update` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetUpdate - * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note - * that this hook will not be called if the datasets update has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeLayout - * @desc Called before laying out `chart`. If any plugin returns `false`, - * the layout update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart layout. - */ - /** - * @method IPlugin#afterLayout - * @desc Called after the `chart` has been layed out. Note that this hook will not - * be called if the layout update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeRender - * @desc Called before rendering `chart`. If any plugin returns `false`, - * the rendering is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart rendering. - */ - /** - * @method IPlugin#afterRender - * @desc Called after the `chart` has been fully rendered (and animation completed). Note - * that this hook will not be called if the rendering has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDraw - * @desc Called before drawing `chart` at every animation frame specified by the given - * easing value. If any plugin returns `false`, the frame drawing is cancelled until - * another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart drawing. - */ - /** - * @method IPlugin#afterDraw - * @desc Called after the `chart` has been drawn for the specific easing value. Note - * that this hook will not be called if the drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsDraw - * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, - * the datasets drawing is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetsDraw - * @desc Called after the `chart` datasets have been drawn. Note that this hook - * will not be called if the datasets drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetDraw - * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets - * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing - * is cancelled until another `render` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetDraw - * @desc Called after the `chart` datasets at the given `args.index` have been drawn - * (datasets are drawn in the reverse order). Note that this hook will not be called - * if the datasets drawing has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeTooltipDraw - * @desc Called before drawing the `tooltip`. If any plugin returns `false`, - * the tooltip drawing is cancelled until another `render` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart tooltip drawing. - */ - /** - * @method IPlugin#afterTooltipDraw - * @desc Called after drawing the `tooltip`. Note that this hook will not - * be called if the tooltip drawing has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeEvent - * @desc Called before processing the specified `event`. If any plugin returns `false`, - * the event will be discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterEvent - * @desc Called after the `event` has been consumed. Note that this hook - * will not be called if the `event` has been previously discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#resize - * @desc Called after the chart as been resized. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} size - The new canvas display size (eq. canvas.style width & height). - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#destroy - * @desc Called after the chart as been destroyed. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - - /** - * Provided for backward compatibility, use Chart.plugins instead - * @namespace Chart.pluginService - * @deprecated since version 2.1.5 - * @todo remove at version 3 - * @private - */ - Chart.pluginService = Chart.plugins; - - /** - * Provided for backward compatibility, inheriting from Chart.PlugingBase has no - * effect, instead simply create/register plugins via plain JavaScript objects. - * @interface Chart.PluginBase - * @deprecated since version 2.5.0 - * @todo remove at version 3 - * @private - */ - Chart.PluginBase = Element.extend({}); -}; diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js new file mode 100644 index 000000000..f2fbcadec --- /dev/null +++ b/src/core/core.plugins.js @@ -0,0 +1,382 @@ +'use strict'; + +var defaults = require('./core.defaults'); +var helpers = require('../helpers/index'); + +defaults._set('global', { + plugins: {} +}); + +/** + * The plugin service singleton + * @namespace Chart.plugins + * @since 2.1.0 + */ +module.exports = { + /** + * Globally registered plugins. + * @private + */ + _plugins: [], + + /** + * This identifier is used to invalidate the descriptors cache attached to each chart + * when a global plugin is registered or unregistered. In this case, the cache ID is + * incremented and descriptors are regenerated during following API calls. + * @private + */ + _cacheId: 0, + + /** + * Registers the given plugin(s) if not already registered. + * @param {Array|Object} plugins plugin instance(s). + */ + register: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + if (p.indexOf(plugin) === -1) { + p.push(plugin); + } + }); + + this._cacheId++; + }, + + /** + * Unregisters the given plugin(s) only if registered. + * @param {Array|Object} plugins plugin instance(s). + */ + unregister: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + var idx = p.indexOf(plugin); + if (idx !== -1) { + p.splice(idx, 1); + } + }); + + this._cacheId++; + }, + + /** + * Remove all registered plugins. + * @since 2.1.5 + */ + clear: function() { + this._plugins = []; + this._cacheId++; + }, + + /** + * Returns the number of registered plugins? + * @returns {Number} + * @since 2.1.5 + */ + count: function() { + return this._plugins.length; + }, + + /** + * Returns all registered plugin instances. + * @returns {Array} array of plugin objects. + * @since 2.1.5 + */ + getAll: function() { + return this._plugins; + }, + + /** + * Calls enabled plugins for `chart` on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The + * returned value can be used, for instance, to interrupt the current action. + * @param {Object} chart - The chart instance for which plugins should be called. + * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] - Extra arguments to apply to the hook call. + * @returns {Boolean} false if any of the plugins return false, else returns true. + */ + notify: function(chart, hook, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; + + for (i = 0; i < ilen; ++i) { + descriptor = descriptors[i]; + plugin = descriptor.plugin; + method = plugin[hook]; + if (typeof method === 'function') { + params = [chart].concat(args || []); + params.push(descriptor.options); + if (method.apply(plugin, params) === false) { + return false; + } + } + } + + return true; + }, + + /** + * Returns descriptors of enabled plugins for the given chart. + * @returns {Array} [{ plugin, options }] + * @private + */ + descriptors: function(chart) { + var cache = chart.$plugins || (chart.$plugins = {}); + if (cache.id === this._cacheId) { + return cache.descriptors; + } + + var plugins = []; + var descriptors = []; + var config = (chart && chart.config) || {}; + var options = (config.options && config.options.plugins) || {}; + + this._plugins.concat(config.plugins || []).forEach(function(plugin) { + var idx = plugins.indexOf(plugin); + if (idx !== -1) { + return; + } + + var id = plugin.id; + var opts = options[id]; + if (opts === false) { + return; + } + + if (opts === true) { + opts = helpers.clone(defaults.global.plugins[id]); + } + + plugins.push(plugin); + descriptors.push({ + plugin: plugin, + options: opts || {} + }); + }); + + cache.descriptors = descriptors; + cache.id = this._cacheId; + return descriptors; + }, + + /** + * Invalidates cache for the given chart: descriptors hold a reference on plugin option, + * but in some cases, this reference can be changed by the user when updating options. + * https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + * @private + */ + _invalidate: function(chart) { + delete chart.$plugins; + } +}; + +/** + * Plugin extension hooks. + * @interface IPlugin + * @since 2.1.0 + */ +/** + * @method IPlugin#beforeInit + * @desc Called before initializing `chart`. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterInit + * @desc Called after `chart` has been initialized and before the first update. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeUpdate + * @desc Called before updating `chart`. If any plugin returns `false`, the update + * is cancelled (and thus subsequent render(s)) until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart update. + */ +/** + * @method IPlugin#afterUpdate + * @desc Called after `chart` has been updated and before rendering. Note that this + * hook will not be called if the chart update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsUpdate + * @desc Called before updating the `chart` datasets. If any plugin returns `false`, + * the datasets update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} false to cancel the datasets update. + * @since version 2.1.5 +*/ +/** + * @method IPlugin#afterDatasetsUpdate + * @desc Called after the `chart` datasets have been updated. Note that this hook + * will not be called if the datasets update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @since version 2.1.5 + */ +/** + * @method IPlugin#beforeDatasetUpdate + * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin + * returns `false`, the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetUpdate + * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note + * that this hook will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeLayout + * @desc Called before laying out `chart`. If any plugin returns `false`, + * the layout update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart layout. + */ +/** + * @method IPlugin#afterLayout + * @desc Called after the `chart` has been layed out. Note that this hook will not + * be called if the layout update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeRender + * @desc Called before rendering `chart`. If any plugin returns `false`, + * the rendering is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart rendering. + */ +/** + * @method IPlugin#afterRender + * @desc Called after the `chart` has been fully rendered (and animation completed). Note + * that this hook will not be called if the rendering has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDraw + * @desc Called before drawing `chart` at every animation frame specified by the given + * easing value. If any plugin returns `false`, the frame drawing is cancelled until + * another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart drawing. + */ +/** + * @method IPlugin#afterDraw + * @desc Called after the `chart` has been drawn for the specific easing value. Note + * that this hook will not be called if the drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsDraw + * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, + * the datasets drawing is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetsDraw + * @desc Called after the `chart` datasets have been drawn. Note that this hook + * will not be called if the datasets drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetDraw + * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets + * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing + * is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetDraw + * @desc Called after the `chart` datasets at the given `args.index` have been drawn + * (datasets are drawn in the reverse order). Note that this hook will not be called + * if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeTooltipDraw + * @desc Called before drawing the `tooltip`. If any plugin returns `false`, + * the tooltip drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart tooltip drawing. + */ +/** + * @method IPlugin#afterTooltipDraw + * @desc Called after drawing the `tooltip`. Note that this hook will not + * be called if the tooltip drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeEvent + * @desc Called before processing the specified `event`. If any plugin returns `false`, + * the event will be discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterEvent + * @desc Called after the `event` has been consumed. Note that this hook + * will not be called if the `event` has been previously discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#resize + * @desc Called after the chart as been resized. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} size - The new canvas display size (eq. canvas.style width & height). + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#destroy + * @desc Called after the chart as been destroyed. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ diff --git a/src/core/core.scale.js b/src/core/core.scale.js index ffe13cbff..1996c6c89 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -705,10 +705,11 @@ module.exports = function(Chart) { var itemsToDraw = []; - var xTickStart = options.position === 'right' ? me.left : me.right - tl; - var xTickEnd = options.position === 'right' ? me.left + tl : me.right; - var yTickStart = options.position === 'bottom' ? me.top : me.bottom - tl; - var yTickEnd = options.position === 'bottom' ? me.top + tl : me.bottom; + var axisWidth = me.options.gridLines.lineWidth; + var xTickStart = options.position === 'right' ? me.right : me.right - axisWidth - tl; + var xTickEnd = options.position === 'right' ? me.right + tl : me.right; + var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; + var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; helpers.each(ticks, function(tick, index) { // autoskipper skipped this tick (#4635) @@ -764,7 +765,7 @@ module.exports = function(Chart) { ty1 = yTickStart; ty2 = yTickEnd; y1 = chartArea.top; - y2 = chartArea.bottom; + y2 = chartArea.bottom + axisWidth; } else { var isLeft = options.position === 'left'; var labelXOffset; @@ -790,7 +791,7 @@ module.exports = function(Chart) { tx1 = xTickStart; tx2 = xTickEnd; x1 = chartArea.left; - x2 = chartArea.right; + x2 = chartArea.right + axisWidth; ty1 = ty2 = y1 = y2 = yLineValue; } @@ -856,11 +857,15 @@ module.exports = function(Chart) { var label = itemToDraw.label; if (helpers.isArray(label)) { - for (var i = 0, y = 0; i < label.length; ++i) { + var lineCount = label.length; + var lineHeight = tickFont.size * 1.5; + var y = me.isHorizontal() ? 0 : -lineHeight * (lineCount - 1) / 2; + + for (var i = 0; i < lineCount; ++i) { // We just make sure the multiline element is a string here.. context.fillText('' + label[i], 0, y); // apply same lineSpacing as calculated @ L#320 - y += (tickFont.size * 1.5); + y += lineHeight; } } else { context.fillText(label, 0, 0); @@ -906,9 +911,9 @@ module.exports = function(Chart) { context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); var x1 = me.left; - var x2 = me.right; + var x2 = me.right + axisWidth; var y1 = me.top; - var y2 = me.bottom; + var y2 = me.bottom + axisWidth; var aliasPixel = helpers.aliasPixel(context.lineWidth); if (isHorizontal) { diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index 23cabe610..f2ea01d32 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -2,6 +2,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); +var layouts = require('./core.layouts'); module.exports = function(Chart) { @@ -38,7 +39,7 @@ module.exports = function(Chart) { scale.fullWidth = scale.options.fullWidth; scale.position = scale.options.position; scale.weight = scale.options.weight; - Chart.layoutService.addBox(chart, scale); + layouts.addBox(chart, scale); }); } }; diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 2dbc27474..3898842a4 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -7,140 +7,6 @@ var helpers = require('../helpers/index'); * @namespace Chart.Ticks */ module.exports = { - /** - * Namespace to hold generators for different types of ticks - * @namespace Chart.Ticks.generators - */ - generators: { - /** - * Interface for the options provided to the numeric tick generator - * @interface INumericTickGenerationOptions - */ - /** - * The maximum number of ticks to display - * @name INumericTickGenerationOptions#maxTicks - * @type Number - */ - /** - * The distance between each tick. - * @name INumericTickGenerationOptions#stepSize - * @type Number - * @optional - */ - /** - * Forced minimum for the ticks. If not specified, the minimum of the data range is used to calculate the tick minimum - * @name INumericTickGenerationOptions#min - * @type Number - * @optional - */ - /** - * The maximum value of the ticks. If not specified, the maximum of the data range is used to calculate the tick maximum - * @name INumericTickGenerationOptions#max - * @type Number - * @optional - */ - - /** - * Generate a set of linear ticks - * @method Chart.Ticks.generators.linear - * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks - * @param dataRange {IRange} the range of the data - * @returns {Array} array of tick values - */ - linear: function(generationOptions, dataRange) { - var ticks = []; - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - var spacing; - if (generationOptions.stepSize && generationOptions.stepSize > 0) { - spacing = generationOptions.stepSize; - } else { - var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false); - spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true); - } - var niceMin = Math.floor(dataRange.min / spacing) * spacing; - var niceMax = Math.ceil(dataRange.max / spacing) * spacing; - - // If min, max and stepSize is set and they make an evenly spaced scale use it. - if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - // If very close to our whole number, use it. - if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { - niceMin = generationOptions.min; - niceMax = generationOptions.max; - } - } - - var numSpaces = (niceMax - niceMin) / spacing; - // If very close to our rounded value, use it. - if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { - numSpaces = Math.round(numSpaces); - } else { - numSpaces = Math.ceil(numSpaces); - } - - // Put the values into the ticks array - ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - ticks.push(niceMin + (j * spacing)); - } - ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); - - return ticks; - }, - - /** - * Generate a set of logarithmic ticks - * @method Chart.Ticks.generators.logarithmic - * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks - * @param dataRange {IRange} the range of the data - * @returns {Array} array of tick values - */ - logarithmic: function(generationOptions, dataRange) { - var ticks = []; - var valueOrDefault = helpers.valueOrDefault; - - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); - - var endExp = Math.floor(helpers.log10(dataRange.max)); - var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); - var exp, significand; - - if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); - significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); - - ticks.push(tickVal); - tickVal = significand * Math.pow(10, exp); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)); - } - - do { - ticks.push(tickVal); - - ++significand; - if (significand === 10) { - significand = 1; - ++exp; - } - - tickVal = significand * Math.pow(10, exp); - } while (exp < endExp || (exp === endExp && significand < endSignificand)); - - var lastTick = valueOrDefault(generationOptions.max, tickVal); - ticks.push(lastTick); - - return ticks; - } - }, - /** * Namespace to hold formatters for different types of ticks * @namespace Chart.Ticks.formatters diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 73460f8d1..9b09d7604 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -299,10 +299,10 @@ module.exports = function(Chart) { } olf = function(x) { - return x + size.width > chart.width; + return x + size.width + model.caretSize + model.caretPadding > chart.width; }; orf = function(x) { - return x - size.width < 0; + return x - size.width - model.caretSize - model.caretPadding < 0; }; yf = function(y) { return y <= midY ? 'top' : 'bottom'; @@ -336,7 +336,7 @@ module.exports = function(Chart) { /** * @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) { + function getBackgroundPoint(vm, size, alignment, chart) { // Background Position var x = vm.x; var y = vm.y; @@ -353,6 +353,12 @@ module.exports = function(Chart) { 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; + } } if (yAlign === 'top') { @@ -545,7 +551,7 @@ module.exports = function(Chart) { tooltipSize = getTooltipSize(this, model); alignment = determineAlignment(this, tooltipSize); // Final Size and Position - backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment); + backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment, me._chart); } else { model.opacity = 0; } @@ -617,7 +623,7 @@ module.exports = function(Chart) { x1 = x2 - caretSize; x3 = x2 + caretSize; } else { - x2 = ptX + (width / 2); + x2 = vm.caretX; x1 = x2 - caretSize; x3 = x2 + caretSize; } @@ -846,25 +852,19 @@ module.exports = function(Chart) { // Remember Last Actives changed = !helpers.arrayEquals(me._active, me._lastActive); - // If tooltip didn't change, do not handle the target event - if (!changed) { - return false; - } + // Only handle target event on tooltip change + if (changed) { + me._lastActive = me._active; - me._lastActive = me._active; + if (options.enabled || options.custom) { + me._eventPosition = { + x: e.x, + y: e.y + }; - if (options.enabled || options.custom) { - me._eventPosition = { - x: e.x, - y: e.y - }; - - var model = me._model; - me.update(true); - me.pivot(); - - // See if our tooltip position changed - changed |= (model.x !== me._model.x) || (model.y !== me._model.y); + me.update(true); + me.pivot(); + } } return changed; diff --git a/src/elements/element.point.js b/src/elements/element.point.js index d5116a259..eab5b31d4 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -24,12 +24,12 @@ defaults._set('global', { function xRange(mouseX) { var vm = this._view; - return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseX - vm.x) < vm.radius + vm.hitRadius) : false; } function yRange(mouseY) { var vm = this._view; - return vm ? (Math.pow(mouseY - vm.y, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; } module.exports = Element.extend({ diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 000000000..1cd981516 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = {}; +module.exports.filler = require('./plugin.filler'); +module.exports.legend = require('./plugin.legend'); +module.exports.title = require('./plugin.title'); diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js index cf0226542..eb8dad4c3 100644 --- a/src/plugins/plugin.filler.js +++ b/src/plugins/plugin.filler.js @@ -18,304 +18,301 @@ defaults._set('global', { } }); -module.exports = function() { +var mappers = { + dataset: function(source) { + var index = source.fill; + var chart = source.chart; + var meta = chart.getDatasetMeta(index); + var visible = meta && chart.isDatasetVisible(index); + var points = (visible && meta.dataset._children) || []; + var length = points.length || 0; - var mappers = { - dataset: function(source) { - var index = source.fill; - var chart = source.chart; - var meta = chart.getDatasetMeta(index); - var visible = meta && chart.isDatasetVisible(index); - var points = (visible && meta.dataset._children) || []; - var length = points.length || 0; + return !length ? null : function(point, i) { + return (i < length && points[i]._view) || null; + }; + }, - return !length ? null : function(point, i) { - return (i < length && points[i]._view) || null; + boundary: function(source) { + var boundary = source.boundary; + var x = boundary ? boundary.x : null; + var y = boundary ? boundary.y : null; + + return function(point) { + return { + x: x === null ? point.x : x, + y: y === null ? point.y : y, }; - }, + }; + } +}; - boundary: function(source) { - var boundary = source.boundary; - var x = boundary ? boundary.x : null; - var y = boundary ? boundary.y : null; +// @todo if (fill[0] === '#') +function decodeFill(el, index, count) { + var model = el._model || {}; + var fill = model.fill; + var target; - return function(point) { - return { - x: x === null ? point.x : x, - y: y === null ? point.y : y, - }; - }; - } - }; - - // @todo if (fill[0] === '#') - function decodeFill(el, index, count) { - var model = el._model || {}; - var fill = model.fill; - var target; - - if (fill === undefined) { - fill = !!model.backgroundColor; - } - - if (fill === false || fill === null) { - return false; - } - - if (fill === true) { - return 'origin'; - } - - target = parseFloat(fill, 10); - if (isFinite(target) && Math.floor(target) === target) { - if (fill[0] === '-' || fill[0] === '+') { - target = index + target; - } - - if (target === index || target < 0 || target >= count) { - return false; - } - - return target; - } - - switch (fill) { - // compatibility - case 'bottom': - return 'start'; - case 'top': - return 'end'; - case 'zero': - return 'origin'; - // supported boundaries - case 'origin': - case 'start': - case 'end': - return fill; - // invalid fill values - default: - return false; - } + if (fill === undefined) { + fill = !!model.backgroundColor; } - function computeBoundary(source) { - var model = source.el._model || {}; - var scale = source.el._scale || {}; - var fill = source.fill; - var target = null; - var horizontal; - - if (isFinite(fill)) { - return null; - } - - // Backward compatibility: until v3, we still need to support boundary values set on - // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and - // controllers might still use it (e.g. the Smith chart). - - if (fill === 'start') { - target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; - } else if (fill === 'end') { - target = model.scaleTop === undefined ? scale.top : model.scaleTop; - } else if (model.scaleZero !== undefined) { - target = model.scaleZero; - } else if (scale.getBasePosition) { - target = scale.getBasePosition(); - } else if (scale.getBasePixel) { - target = scale.getBasePixel(); - } - - if (target !== undefined && target !== null) { - if (target.x !== undefined && target.y !== undefined) { - return target; - } - - if (typeof target === 'number' && isFinite(target)) { - horizontal = scale.isHorizontal(); - return { - x: horizontal ? target : null, - y: horizontal ? null : target - }; - } - } - - return null; - } - - function resolveTarget(sources, index, propagate) { - var source = sources[index]; - var fill = source.fill; - var visited = [index]; - var target; - - if (!propagate) { - return fill; - } - - while (fill !== false && visited.indexOf(fill) === -1) { - if (!isFinite(fill)) { - return fill; - } - - target = sources[fill]; - if (!target) { - return false; - } - - if (target.visible) { - return fill; - } - - visited.push(fill); - fill = target.fill; - } - + if (fill === false || fill === null) { return false; } - function createMapper(source) { - var fill = source.fill; - var type = 'dataset'; + if (fill === true) { + return 'origin'; + } - if (fill === false) { - return null; + target = parseFloat(fill, 10); + if (isFinite(target) && Math.floor(target) === target) { + if (fill[0] === '-' || fill[0] === '+') { + target = index + target; } + if (target === index || target < 0 || target >= count) { + return false; + } + + return target; + } + + switch (fill) { + // compatibility + case 'bottom': + return 'start'; + case 'top': + return 'end'; + case 'zero': + return 'origin'; + // supported boundaries + case 'origin': + case 'start': + case 'end': + return fill; + // invalid fill values + default: + return false; + } +} + +function computeBoundary(source) { + var model = source.el._model || {}; + var scale = source.el._scale || {}; + var fill = source.fill; + var target = null; + var horizontal; + + if (isFinite(fill)) { + return null; + } + + // Backward compatibility: until v3, we still need to support boundary values set on + // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and + // controllers might still use it (e.g. the Smith chart). + + if (fill === 'start') { + target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; + } else if (fill === 'end') { + target = model.scaleTop === undefined ? scale.top : model.scaleTop; + } else if (model.scaleZero !== undefined) { + target = model.scaleZero; + } else if (scale.getBasePosition) { + target = scale.getBasePosition(); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } + + if (target !== undefined && target !== null) { + if (target.x !== undefined && target.y !== undefined) { + return target; + } + + if (typeof target === 'number' && isFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal ? target : null, + y: horizontal ? null : target + }; + } + } + + return null; +} + +function resolveTarget(sources, index, propagate) { + var source = sources[index]; + var fill = source.fill; + var visited = [index]; + var target; + + if (!propagate) { + return fill; + } + + while (fill !== false && visited.indexOf(fill) === -1) { if (!isFinite(fill)) { - type = 'boundary'; + return fill; } - return mappers[type](source); + target = sources[fill]; + if (!target) { + return false; + } + + if (target.visible) { + return fill; + } + + visited.push(fill); + fill = target.fill; } - function isDrawable(point) { - return point && !point.skip; + return false; +} + +function createMapper(source) { + var fill = source.fill; + var type = 'dataset'; + + if (fill === false) { + return null; } - function drawArea(ctx, curve0, curve1, len0, len1) { - var i; + if (!isFinite(fill)) { + type = 'boundary'; + } - if (!len0 || !len1) { + return mappers[type](source); +} + +function isDrawable(point) { + return point && !point.skip; +} + +function drawArea(ctx, curve0, curve1, len0, len1) { + var i; + + if (!len0 || !len1) { + return; + } + + // building first area curve (normal) + ctx.moveTo(curve0[0].x, curve0[0].y); + for (i = 1; i < len0; ++i) { + helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); + } + + // joining the two area curves + ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); + + // building opposite area curve (reverse) + for (i = len1 - 1; i > 0; --i) { + helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); + } +} + +function doFill(ctx, points, mapper, view, color, loop) { + var count = points.length; + var span = view.spanGaps; + var curve0 = []; + var curve1 = []; + var len0 = 0; + var len1 = 0; + var i, ilen, index, p0, p1, d0, d1; + + ctx.beginPath(); + + for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { + index = i % count; + p0 = points[index]._view; + p1 = mapper(p0, index, view); + d0 = isDrawable(p0); + d1 = isDrawable(p1); + + if (d0 && d1) { + len0 = curve0.push(p0); + len1 = curve1.push(p1); + } else if (len0 && len1) { + if (!span) { + drawArea(ctx, curve0, curve1, len0, len1); + len0 = len1 = 0; + curve0 = []; + curve1 = []; + } else { + if (d0) { + curve0.push(p0); + } + if (d1) { + curve1.push(p1); + } + } + } + } + + drawArea(ctx, curve0, curve1, len0, len1); + + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); +} + +module.exports = { + id: 'filler', + + afterDatasetsUpdate: function(chart, options) { + var count = (chart.data.datasets || []).length; + var propagate = options.propagate; + var sources = []; + var meta, i, el, source; + + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + el = meta.dataset; + source = null; + + if (el && el._model && el instanceof elements.Line) { + source = { + visible: chart.isDatasetVisible(i), + fill: decodeFill(el, i, count), + chart: chart, + el: el + }; + } + + meta.$filler = source; + sources.push(source); + } + + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source) { + continue; + } + + source.fill = resolveTarget(sources, i, propagate); + source.boundary = computeBoundary(source); + source.mapper = createMapper(source); + } + }, + + beforeDatasetDraw: function(chart, args) { + var meta = args.meta.$filler; + if (!meta) { return; } - // building first area curve (normal) - ctx.moveTo(curve0[0].x, curve0[0].y); - for (i = 1; i < len0; ++i) { - helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); - } + var ctx = chart.ctx; + var el = meta.el; + var view = el._view; + var points = el._children || []; + var mapper = meta.mapper; + var color = view.backgroundColor || defaults.global.defaultColor; - // joining the two area curves - ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); - - // building opposite area curve (reverse) - for (i = len1 - 1; i > 0; --i) { - helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); + if (mapper && color && points.length) { + helpers.canvas.clipArea(ctx, chart.chartArea); + doFill(ctx, points, mapper, view, color, el._loop); + helpers.canvas.unclipArea(ctx); } } - - function doFill(ctx, points, mapper, view, color, loop) { - var count = points.length; - var span = view.spanGaps; - var curve0 = []; - var curve1 = []; - var len0 = 0; - var len1 = 0; - var i, ilen, index, p0, p1, d0, d1; - - ctx.beginPath(); - - for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { - index = i % count; - p0 = points[index]._view; - p1 = mapper(p0, index, view); - d0 = isDrawable(p0); - d1 = isDrawable(p1); - - if (d0 && d1) { - len0 = curve0.push(p0); - len1 = curve1.push(p1); - } else if (len0 && len1) { - if (!span) { - drawArea(ctx, curve0, curve1, len0, len1); - len0 = len1 = 0; - curve0 = []; - curve1 = []; - } else { - if (d0) { - curve0.push(p0); - } - if (d1) { - curve1.push(p1); - } - } - } - } - - drawArea(ctx, curve0, curve1, len0, len1); - - ctx.closePath(); - ctx.fillStyle = color; - ctx.fill(); - } - - return { - id: 'filler', - - afterDatasetsUpdate: function(chart, options) { - var count = (chart.data.datasets || []).length; - var propagate = options.propagate; - var sources = []; - var meta, i, el, source; - - for (i = 0; i < count; ++i) { - meta = chart.getDatasetMeta(i); - el = meta.dataset; - source = null; - - if (el && el._model && el instanceof elements.Line) { - source = { - visible: chart.isDatasetVisible(i), - fill: decodeFill(el, i, count), - chart: chart, - el: el - }; - } - - meta.$filler = source; - sources.push(source); - } - - for (i = 0; i < count; ++i) { - source = sources[i]; - if (!source) { - continue; - } - - source.fill = resolveTarget(sources, i, propagate); - source.boundary = computeBoundary(source); - source.mapper = createMapper(source); - } - }, - - beforeDatasetDraw: function(chart, args) { - var meta = args.meta.$filler; - if (!meta) { - return; - } - - var ctx = chart.ctx; - var el = meta.el; - var view = el._view; - var points = el._children || []; - var mapper = meta.mapper; - var color = view.backgroundColor || defaults.global.defaultColor; - - if (mapper && color && points.length) { - helpers.canvas.clipArea(ctx, chart.chartArea); - doFill(ctx, points, mapper, view, color, el._loop); - helpers.canvas.unclipArea(ctx); - } - } - }; }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 71e0e4110..3f1559c30 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -3,6 +3,9 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layouts = require('../core/core.layouts'); + +var noop = helpers.noop; defaults._set('global', { legend: { @@ -79,489 +82,495 @@ defaults._set('global', { } }); -module.exports = function(Chart) { +/** + * Helper function to get the box width based on the usePointStyle option + * @param labelopts {Object} the label options on the legend + * @param fontSize {Number} the label font size + * @return {Number} width of the color box area + */ +function getBoxWidth(labelOpts, fontSize) { + return labelOpts.usePointStyle ? + fontSize * Math.SQRT2 : + labelOpts.boxWidth; +} - var layout = Chart.layoutService; - var noop = helpers.noop; +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Legend = Element.extend({ - /** - * Helper function to get the box width based on the usePointStyle option - * @param labelopts {Object} the label options on the legend - * @param fontSize {Number} the label font size - * @return {Number} width of the color box area - */ - function getBoxWidth(labelOpts, fontSize) { - return labelOpts.usePointStyle ? - fontSize * Math.SQRT2 : - labelOpts.boxWidth; - } + initialize: function(config) { + helpers.extend(this, config); - Chart.Legend = Element.extend({ + // Contains hit boxes for each dataset (in dataset order) + this.legendHitBoxes = []; - initialize: function(config) { - helpers.extend(this, config); + // Are we in doughnut mode which has a different data type + this.doughnutMode = false; + }, - // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; + // These methods are ordered by lifecycle. Utilities then follow. + // Any function defined here is inherited by all legend types. + // Any function can be extended by the legend type - // Are we in doughnut mode which has a different data type - this.doughnutMode = false; - }, + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; - // These methods are ordered by lifecycle. Utilities then follow. - // Any function defined here is inherited by all legend types. - // Any function can be extended by the legend type + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - }, - afterUpdate: noop, + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // + me.afterUpdate(); - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; + return me.minSize; + }, + afterUpdate: noop, - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } + // - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } - // + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; - beforeBuildLabels: noop, - buildLabels: function() { - var me = this; - var labelOpts = me.options.labels || {}; - var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, - if (labelOpts.filter) { - legendItems = legendItems.filter(function(item) { - return labelOpts.filter(item, me.chart.data); + // + + beforeBuildLabels: noop, + buildLabels: function() { + var me = this; + var labelOpts = me.options.labels || {}; + var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; + + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } + + if (me.options.reverse) { + legendItems.reverse(); + } + + me.legendItems = legendItems; + }, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var display = opts.display; + + var ctx = me.ctx; + + var globalDefault = defaults.global; + var valueOrDefault = helpers.valueOrDefault; + var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); + var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); + var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); + var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + + // Reset hit boxes + var hitboxes = me.legendHitBoxes = []; + + var minSize = me.minSize; + var isHorizontal = me.isHorizontal(); + + if (isHorizontal) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = display ? 10 : 0; + } else { + minSize.width = display ? 10 : 0; + minSize.height = me.maxHeight; // fill all the height + } + + // Increase sizes here + if (display) { + ctx.font = labelFont; + + if (isHorizontal) { + // Labels + + // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one + var lineWidths = me.lineWidths = [0]; + var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; + + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + helpers.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + + if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { + totalHeight += fontSize + (labelOpts.padding); + lineWidths[lineWidths.length] = me.left; + } + + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: width, + height: fontSize + }; + + lineWidths[lineWidths.length - 1] += width + labelOpts.padding; }); + + minSize.height += totalHeight; + + } else { + var vPadding = labelOpts.padding; + var columnWidths = me.columnWidths = []; + var totalWidth = labelOpts.padding; + var currentColWidth = 0; + var currentColHeight = 0; + var itemHeight = fontSize + vPadding; + + helpers.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + + // If too tall, go to new column + if (currentColHeight + itemHeight > minSize.height) { + totalWidth += currentColWidth + labelOpts.padding; + columnWidths.push(currentColWidth); // previous column width + + currentColWidth = 0; + currentColHeight = 0; + } + + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight; + + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: itemWidth, + height: fontSize + }; + }); + + totalWidth += currentColWidth; + columnWidths.push(currentColWidth); + minSize.width += totalWidth; } + } - if (me.options.reverse) { - legendItems.reverse(); - } + me.width = minSize.width; + me.height = minSize.height; + }, + afterFit: noop, - me.legendItems = legendItems; - }, - afterBuildLabels: noop, + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, - // - - beforeFit: noop, - fit: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var display = opts.display; + // Actually draw the legend on the canvas + draw: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var globalDefault = defaults.global; + var lineDefault = globalDefault.elements.line; + var legendWidth = me.width; + var lineWidths = me.lineWidths; + if (opts.display) { var ctx = me.ctx; - - var globalDefault = defaults.global; var valueOrDefault = helpers.valueOrDefault; + var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var cursor; - // Reset hit boxes - var hitboxes = me.legendHitBoxes = []; + // Canvas setup + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.strokeStyle = fontColor; // for strikethrough effect + ctx.fillStyle = fontColor; // render in correct colour + ctx.font = labelFont; - var minSize = me.minSize; + var boxWidth = getBoxWidth(labelOpts, fontSize); + var hitboxes = me.legendHitBoxes; + + // current position + var drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0) { + return; + } + + // Set the ctx for the box + ctx.save(); + + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); + ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); + ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); + ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); + ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); + var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); + + if (ctx.setLineDash) { + // IE 9 and 10 do not support line dash + ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); + } + + if (opts.labels && opts.labels.usePointStyle) { + // Recalculate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + var radius = fontSize * Math.SQRT2 / 2; + var offSet = radius / Math.SQRT2; + var centerX = x + offSet; + var centerY = y + offSet; + + // Draw pointStyle as legend symbol + helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); + } else { + // Draw box as legend symbol + if (!isLineWidthZero) { + ctx.strokeRect(x, y, boxWidth, fontSize); + } + ctx.fillRect(x, y, boxWidth, fontSize); + } + + ctx.restore(); + }; + var fillText = function(x, y, legendItem, textWidth) { + var halfFontSize = fontSize / 2; + var xLeft = boxWidth + halfFontSize + x; + var yMiddle = y + halfFontSize; + + ctx.fillText(legendItem.text, xLeft, yMiddle); + + if (legendItem.hidden) { + // Strikethrough the text if hidden + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.moveTo(xLeft, yMiddle); + ctx.lineTo(xLeft + textWidth, yMiddle); + ctx.stroke(); + } + }; + + // Horizontal var isHorizontal = me.isHorizontal(); - if (isHorizontal) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = display ? 10 : 0; + cursor = { + x: me.left + ((legendWidth - lineWidths[0]) / 2), + y: me.top + labelOpts.padding, + line: 0 + }; } else { - minSize.width = display ? 10 : 0; - minSize.height = me.maxHeight; // fill all the height + cursor = { + x: me.left + labelOpts.padding, + y: me.top + labelOpts.padding, + line: 0 + }; } - // Increase sizes here - if (display) { - ctx.font = labelFont; + var itemHeight = fontSize + labelOpts.padding; + helpers.each(me.legendItems, function(legendItem, i) { + var textWidth = ctx.measureText(legendItem.text).width; + var width = boxWidth + (fontSize / 2) + textWidth; + var x = cursor.x; + var y = cursor.y; if (isHorizontal) { - // Labels - - // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one - var lineWidths = me.lineWidths = [0]; - var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; - - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - - helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - - if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { - totalHeight += fontSize + (labelOpts.padding); - lineWidths[lineWidths.length] = me.left; - } - - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: width, - height: fontSize - }; - - lineWidths[lineWidths.length - 1] += width + labelOpts.padding; - }); - - minSize.height += totalHeight; - - } else { - var vPadding = labelOpts.padding; - var columnWidths = me.columnWidths = []; - var totalWidth = labelOpts.padding; - var currentColWidth = 0; - var currentColHeight = 0; - var itemHeight = fontSize + vPadding; - - helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - - // If too tall, go to new column - if (currentColHeight + itemHeight > minSize.height) { - totalWidth += currentColWidth + labelOpts.padding; - columnWidths.push(currentColWidth); // previous column width - - currentColWidth = 0; - currentColHeight = 0; - } - - // Get max width - currentColWidth = Math.max(currentColWidth, itemWidth); - currentColHeight += itemHeight; - - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: itemWidth, - height: fontSize - }; - }); - - totalWidth += currentColWidth; - columnWidths.push(currentColWidth); - minSize.width += totalWidth; - } - } - - me.width = minSize.width; - me.height = minSize.height; - }, - afterFit: noop, - - // Shared Methods - isHorizontal: function() { - return this.options.position === 'top' || this.options.position === 'bottom'; - }, - - // Actually draw the legend on the canvas - draw: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var globalDefault = defaults.global; - var lineDefault = globalDefault.elements.line; - var legendWidth = me.width; - var lineWidths = me.lineWidths; - - if (opts.display) { - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var cursor; - - // Canvas setup - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - ctx.lineWidth = 0.5; - ctx.strokeStyle = fontColor; // for strikethrough effect - ctx.fillStyle = fontColor; // render in correct colour - ctx.font = labelFont; - - var boxWidth = getBoxWidth(labelOpts, fontSize); - var hitboxes = me.legendHitBoxes; - - // current position - var drawLegendBox = function(x, y, legendItem) { - if (isNaN(boxWidth) || boxWidth <= 0) { - return; - } - - // Set the ctx for the box - ctx.save(); - - ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); - ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); - ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); - ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); - ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); - ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); - var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); - - if (ctx.setLineDash) { - // IE 9 and 10 do not support line dash - ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); - } - - if (opts.labels && opts.labels.usePointStyle) { - // Recalculate x and y for drawPoint() because its expecting - // x and y to be center of figure (instead of top left) - var radius = fontSize * Math.SQRT2 / 2; - var offSet = radius / Math.SQRT2; - var centerX = x + offSet; - var centerY = y + offSet; - - // Draw pointStyle as legend symbol - helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); - } else { - // Draw box as legend symbol - if (!isLineWidthZero) { - ctx.strokeRect(x, y, boxWidth, fontSize); - } - ctx.fillRect(x, y, boxWidth, fontSize); - } - - ctx.restore(); - }; - var fillText = function(x, y, legendItem, textWidth) { - var halfFontSize = fontSize / 2; - var xLeft = boxWidth + halfFontSize + x; - var yMiddle = y + halfFontSize; - - ctx.fillText(legendItem.text, xLeft, yMiddle); - - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(xLeft, yMiddle); - ctx.lineTo(xLeft + textWidth, yMiddle); - ctx.stroke(); - } - }; - - // Horizontal - var isHorizontal = me.isHorizontal(); - if (isHorizontal) { - cursor = { - x: me.left + ((legendWidth - lineWidths[0]) / 2), - y: me.top + labelOpts.padding, - line: 0 - }; - } else { - cursor = { - x: me.left + labelOpts.padding, - y: me.top + labelOpts.padding, - line: 0 - }; - } - - var itemHeight = fontSize + labelOpts.padding; - helpers.each(me.legendItems, function(legendItem, i) { - var textWidth = ctx.measureText(legendItem.text).width; - var width = boxWidth + (fontSize / 2) + textWidth; - var x = cursor.x; - var y = cursor.y; - - if (isHorizontal) { - if (x + width >= legendWidth) { - y = cursor.y += itemHeight; - cursor.line++; - x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); - } - } else if (y + itemHeight > me.bottom) { - x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top + labelOpts.padding; + if (x + width >= legendWidth) { + y = cursor.y += itemHeight; cursor.line++; + x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); } - - drawLegendBox(x, y, legendItem); - - hitboxes[i].left = x; - hitboxes[i].top = y; - - // Fill the actual label - fillText(x, y, legendItem, textWidth); - - if (isHorizontal) { - cursor.x += width + (labelOpts.padding); - } else { - cursor.y += itemHeight; - } - - }); - } - }, - - /** - * Handle an event - * @private - * @param {IEvent} event - The event to handle - * @return {Boolean} true if a change occured - */ - handleEvent: function(e) { - var me = this; - var opts = me.options; - var type = e.type === 'mouseup' ? 'click' : e.type; - var changed = false; - - if (type === 'mousemove') { - if (!opts.onHover) { - return; + } else if (y + itemHeight > me.bottom) { + x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; + y = cursor.y = me.top + labelOpts.padding; + cursor.line++; } - } else if (type === 'click') { - if (!opts.onClick) { - return; + + drawLegendBox(x, y, legendItem); + + hitboxes[i].left = x; + hitboxes[i].top = y; + + // Fill the actual label + fillText(x, y, legendItem, textWidth); + + if (isHorizontal) { + cursor.x += width + (labelOpts.padding); + } else { + cursor.y += itemHeight; } - } else { + + }); + } + }, + + /** + * Handle an event + * @private + * @param {IEvent} event - The event to handle + * @return {Boolean} true if a change occured + */ + handleEvent: function(e) { + var me = this; + var opts = me.options; + var type = e.type === 'mouseup' ? 'click' : e.type; + var changed = false; + + if (type === 'mousemove') { + if (!opts.onHover) { return; } + } else if (type === 'click') { + if (!opts.onClick) { + return; + } + } else { + return; + } - // Chart event already has relative position in it - var x = e.x; - var y = e.y; + // Chart event already has relative position in it + var x = e.x; + var y = e.y; - if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { - // See if we are touching one of the dataset boxes - var lh = me.legendHitBoxes; - for (var i = 0; i < lh.length; ++i) { - var hitBox = lh[i]; + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + // See if we are touching one of the dataset boxes + var lh = me.legendHitBoxes; + for (var i = 0; i < lh.length; ++i) { + var hitBox = lh[i]; - if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { - // Touching an element - if (type === 'click') { - // use e.native for backwards compatibility - opts.onClick.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } else if (type === 'mousemove') { - // use e.native for backwards compatibility - opts.onHover.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + // Touching an element + if (type === 'click') { + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, me.legendItems[i]); + changed = true; + break; + } else if (type === 'mousemove') { + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, me.legendItems[i]); + changed = true; + break; } } } - - return changed; } + + return changed; + } +}); + +function createNewLegendAndAttach(chart, legendOpts) { + var legend = new Legend({ + ctx: chart.ctx, + options: legendOpts, + chart: chart }); - function createNewLegendAndAttach(chart, legendOpts) { - var legend = new Chart.Legend({ - ctx: chart.ctx, - options: legendOpts, - chart: chart - }); + layouts.configure(chart, legend, legendOpts); + layouts.addBox(chart, legend); + chart.legend = legend; +} - layout.configure(chart, legend, legendOpts); - layout.addBox(chart, legend); - chart.legend = legend; - } +module.exports = { + id: 'legend', - return { - id: 'legend', + /** + * Backward compatibility: since 2.1.5, the legend is registered as a plugin, making + * Chart.Legend obsolete. To avoid a breaking change, we export the Legend as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Legend, - beforeInit: function(chart) { - var legendOpts = chart.options.legend; + beforeInit: function(chart) { + var legendOpts = chart.options.legend; - if (legendOpts) { + if (legendOpts) { + createNewLegendAndAttach(chart, legendOpts); + } + }, + + beforeUpdate: function(chart) { + var legendOpts = chart.options.legend; + var legend = chart.legend; + + if (legendOpts) { + helpers.mergeIf(legendOpts, defaults.global.legend); + + if (legend) { + layouts.configure(chart, legend, legendOpts); + legend.options = legendOpts; + } else { createNewLegendAndAttach(chart, legendOpts); } - }, - - beforeUpdate: function(chart) { - var legendOpts = chart.options.legend; - var legend = chart.legend; - - if (legendOpts) { - helpers.mergeIf(legendOpts, defaults.global.legend); - - if (legend) { - layout.configure(chart, legend, legendOpts); - legend.options = legendOpts; - } else { - createNewLegendAndAttach(chart, legendOpts); - } - } else if (legend) { - layout.removeBox(chart, legend); - delete chart.legend; - } - }, - - afterEvent: function(chart, e) { - var legend = chart.legend; - if (legend) { - legend.handleEvent(e); - } + } else if (legend) { + layouts.removeBox(chart, legend); + delete chart.legend; } - }; + }, + + afterEvent: function(chart, e) { + var legend = chart.legend; + if (legend) { + legend.handleEvent(e); + } + } }; diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index ebefed294..47588844d 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -3,6 +3,9 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layouts = require('../core/core.layouts'); + +var noop = helpers.noop; defaults._set('global', { title: { @@ -17,227 +20,233 @@ defaults._set('global', { } }); -module.exports = function(Chart) { +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Title = Element.extend({ + initialize: function(config) { + var me = this; + helpers.extend(me, config); - var layout = Chart.layoutService; - var noop = helpers.noop; + // Contains hit boxes for each dataset (in dataset order) + me.legendHitBoxes = []; + }, - Chart.Title = Element.extend({ - initialize: function(config) { - var me = this; - helpers.extend(me, config); + // These methods are ordered by lifecycle. Utilities then follow. - // Contains hit boxes for each dataset (in dataset order) - me.legendHitBoxes = []; - }, + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; - // These methods are ordered by lifecycle. Utilities then follow. + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - - }, - afterUpdate: noop, + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // + me.afterUpdate(); - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; + return me.minSize; - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } + }, + afterUpdate: noop, - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; + // - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; - // - - beforeBuildLabels: noop, - buildLabels: noop, - afterBuildLabels: noop, - - // - - beforeFit: noop, - fit: function() { - var me = this; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var display = opts.display; - var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); - var minSize = me.minSize; - var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; - - if (me.isHorizontal()) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = textSize; - } else { - minSize.width = textSize; - minSize.height = me.maxHeight; // fill all the height - } - - me.width = minSize.width; - me.height = minSize.height; - - }, - afterFit: noop, - - // Shared Methods - isHorizontal: function() { - var pos = this.options.position; - return pos === 'top' || pos === 'bottom'; - }, - - // Actually draw the title block on the canvas - draw: function() { - var me = this; - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var globalDefaults = defaults.global; - - if (opts.display) { - var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); - var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); - var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); - var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var offset = lineHeight / 2 + opts.padding; - var rotation = 0; - var top = me.top; - var left = me.left; - var bottom = me.bottom; - var right = me.right; - var maxWidth, titleX, titleY; - - ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour - ctx.font = titleFont; - - // Horizontal - if (me.isHorizontal()) { - titleX = left + ((right - left) / 2); // midpoint of the width - titleY = top + offset; - maxWidth = right - left; - } else { - titleX = opts.position === 'left' ? left + offset : right - offset; - titleY = top + ((bottom - top) / 2); - maxWidth = bottom - top; - rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); - } - - ctx.save(); - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - var text = opts.text; - if (helpers.isArray(text)) { - var y = 0; - for (var i = 0; i < text.length; ++i) { - ctx.fillText(text[i], 0, y, maxWidth); - y += lineHeight; - } - } else { - ctx.fillText(text, 0, 0, maxWidth); - } - - ctx.restore(); - } + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; } + + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, + + // + + beforeBuildLabels: noop, + buildLabels: noop, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var display = opts.display; + var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); + var minSize = me.minSize; + var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; + var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); + var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; + + if (me.isHorizontal()) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = textSize; + } else { + minSize.width = textSize; + minSize.height = me.maxHeight; // fill all the height + } + + me.width = minSize.width; + me.height = minSize.height; + + }, + afterFit: noop, + + // Shared Methods + isHorizontal: function() { + var pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + }, + + // Actually draw the title block on the canvas + draw: function() { + var me = this; + var ctx = me.ctx; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var globalDefaults = defaults.global; + + if (opts.display) { + var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); + var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); + var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); + var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); + var offset = lineHeight / 2 + opts.padding; + var rotation = 0; + var top = me.top; + var left = me.left; + var bottom = me.bottom; + var right = me.right; + var maxWidth, titleX, titleY; + + ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour + ctx.font = titleFont; + + // Horizontal + if (me.isHorizontal()) { + titleX = left + ((right - left) / 2); // midpoint of the width + titleY = top + offset; + maxWidth = right - left; + } else { + titleX = opts.position === 'left' ? left + offset : right - offset; + titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; + rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); + } + + ctx.save(); + ctx.translate(titleX, titleY); + ctx.rotate(rotation); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + var text = opts.text; + if (helpers.isArray(text)) { + var y = 0; + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], 0, y, maxWidth); + y += lineHeight; + } + } else { + ctx.fillText(text, 0, 0, maxWidth); + } + + ctx.restore(); + } + } +}); + +function createNewTitleBlockAndAttach(chart, titleOpts) { + var title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart: chart }); - function createNewTitleBlockAndAttach(chart, titleOpts) { - var title = new Chart.Title({ - ctx: chart.ctx, - options: titleOpts, - chart: chart - }); + layouts.configure(chart, title, titleOpts); + layouts.addBox(chart, title); + chart.titleBlock = title; +} - layout.configure(chart, title, titleOpts); - layout.addBox(chart, title); - chart.titleBlock = title; - } +module.exports = { + id: 'title', - return { - id: 'title', + /** + * Backward compatibility: since 2.1.5, the title is registered as a plugin, making + * Chart.Title obsolete. To avoid a breaking change, we export the Title as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Title, - beforeInit: function(chart) { - var titleOpts = chart.options.title; + beforeInit: function(chart) { + var titleOpts = chart.options.title; - if (titleOpts) { + if (titleOpts) { + createNewTitleBlockAndAttach(chart, titleOpts); + } + }, + + beforeUpdate: function(chart) { + var titleOpts = chart.options.title; + var titleBlock = chart.titleBlock; + + if (titleOpts) { + helpers.mergeIf(titleOpts, defaults.global.title); + + if (titleBlock) { + layouts.configure(chart, titleBlock, titleOpts); + titleBlock.options = titleOpts; + } else { createNewTitleBlockAndAttach(chart, titleOpts); } - }, - - beforeUpdate: function(chart) { - var titleOpts = chart.options.title; - var titleBlock = chart.titleBlock; - - if (titleOpts) { - helpers.mergeIf(titleOpts, defaults.global.title); - - if (titleBlock) { - layout.configure(chart, titleBlock, titleOpts); - titleBlock.options = titleOpts; - } else { - createNewTitleBlockAndAttach(chart, titleOpts); - } - } else if (titleBlock) { - Chart.layoutService.removeBox(chart, titleBlock); - delete chart.titleBlock; - } + } else if (titleBlock) { + layouts.removeBox(chart, titleBlock); + delete chart.titleBlock; } - }; + } }; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 94ebd12a5..aa723004f 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -170,11 +170,10 @@ module.exports = function(Chart) { if (me.isHorizontal()) { pixel = me.left + (me.width / range * (rightValue - start)); - return Math.round(pixel); + } else { + pixel = me.bottom - (me.height / range * (rightValue - start)); } - - pixel = me.bottom - (me.height / range * (rightValue - start)); - return Math.round(pixel); + return pixel; }, getValueForPixel: function(pixel) { var me = this; diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 0d9a33bf2..3e4b5c083 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -1,7 +1,61 @@ 'use strict'; var helpers = require('../helpers/index'); -var Ticks = require('../core/core.ticks'); + +/** + * Generate a set of linear ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + var spacing; + if (generationOptions.stepSize && generationOptions.stepSize > 0) { + spacing = generationOptions.stepSize; + } else { + var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false); + spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true); + } + var niceMin = Math.floor(dataRange.min / spacing) * spacing; + var niceMax = Math.ceil(dataRange.max / spacing) * spacing; + + // If min, max and stepSize is set and they make an evenly spaced scale use it. + if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + // If very close to our whole number, use it. + if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { + niceMin = generationOptions.min; + niceMax = generationOptions.max; + } + } + + var numSpaces = (niceMax - niceMin) / spacing; + // If very close to our rounded value, use it. + if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + + var precision = 1; + if (spacing < 1) { + precision = Math.pow(10, spacing.toString().length - 2); + niceMin = Math.round(niceMin * precision) / precision; + niceMax = Math.round(niceMax * precision) / precision; + } + ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); + for (var j = 1; j < numSpaces; ++j) { + ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); + } + ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); + + return ticks; +} + module.exports = function(Chart) { @@ -102,7 +156,7 @@ module.exports = function(Chart) { max: tickOpts.max, stepSize: helpers.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) }; - var ticks = me.ticks = Ticks.generators.linear(numericGeneratorOptions, me); + var ticks = me.ticks = generateTicks(numericGeneratorOptions, me); me.handleDirectionalChanges(); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 78aaf8171..74a210e44 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -3,6 +3,58 @@ var helpers = require('../helpers/index'); var Ticks = require('../core/core.ticks'); +/** + * Generate a set of logarithmic ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + var valueOrDefault = helpers.valueOrDefault; + + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); + + var endExp = Math.floor(helpers.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp, significand; + + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); + + ticks.push(tickVal); + tickVal = significand * Math.pow(10, exp); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)); + } + var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + + do { + ticks.push(tickVal); + + ++significand; + if (significand === 10) { + significand = 1; + ++exp; + precision = exp >= 0 ? 1 : precision; + } + + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; + } while (exp < endExp || (exp === endExp && significand < endSignificand)); + + var lastTick = valueOrDefault(generationOptions.max, tickVal); + ticks.push(lastTick); + + return ticks; +} + + module.exports = function(Chart) { var defaultConfig = { @@ -18,11 +70,9 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var opts = me.options; - var tickOpts = opts.ticks; var chart = me.chart; var data = chart.data; var datasets = data.datasets; - var valueOrDefault = helpers.valueOrDefault; var isHorizontal = me.isHorizontal(); function IDMatches(meta) { return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; @@ -68,27 +118,23 @@ module.exports = function(Chart) { helpers.each(dataset.data, function(rawValue, index) { var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } - values[index] = values[index] || 0; - - if (opts.relativePoints) { - values[index] = 100; - } else { - // Don't need to split positive and negative since the log scale can't handle a 0 crossing - values[index] += value; - } + values[index] += value; }); } }); helpers.each(valuesPerStack, function(valuesForType) { - var minVal = helpers.min(valuesForType); - var maxVal = helpers.max(valuesForType); - me.min = me.min === null ? minVal : Math.min(me.min, minVal); - me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + if (valuesForType.length > 0) { + var minVal = helpers.min(valuesForType); + var maxVal = helpers.max(valuesForType); + me.min = me.min === null ? minVal : Math.min(me.min, minVal); + me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + } }); } else { @@ -97,7 +143,8 @@ module.exports = function(Chart) { if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } @@ -121,6 +168,17 @@ module.exports = function(Chart) { }); } + // Common base implementation to handle ticks.min, ticks.max + this.handleTickRangeOptions(); + }, + handleTickRangeOptions: function() { + var me = this; + var opts = me.options; + var tickOpts = opts.ticks; + var valueOrDefault = helpers.valueOrDefault; + var DEFAULT_MIN = 1; + var DEFAULT_MAX = 10; + me.min = valueOrDefault(tickOpts.min, me.min); me.max = valueOrDefault(tickOpts.max, me.max); @@ -129,8 +187,25 @@ module.exports = function(Chart) { me.min = Math.pow(10, Math.floor(helpers.log10(me.min)) - 1); me.max = Math.pow(10, Math.floor(helpers.log10(me.max)) + 1); } else { - me.min = 1; - me.max = 10; + me.min = DEFAULT_MIN; + me.max = DEFAULT_MAX; + } + } + if (me.min === null) { + me.min = Math.pow(10, Math.floor(helpers.log10(me.max)) - 1); + } + if (me.max === null) { + me.max = me.min !== 0 + ? Math.pow(10, Math.floor(helpers.log10(me.min)) + 1) + : DEFAULT_MAX; + } + if (me.minNotZero === null) { + if (me.min > 0) { + me.minNotZero = me.min; + } else if (me.max < 1) { + me.minNotZero = Math.pow(10, Math.floor(helpers.log10(me.max))); + } else { + me.minNotZero = DEFAULT_MIN; } } }, @@ -138,17 +213,13 @@ module.exports = function(Chart) { var me = this; var opts = me.options; var tickOpts = opts.ticks; + var reverse = !me.isHorizontal(); var generationOptions = { min: tickOpts.min, max: tickOpts.max }; - var ticks = me.ticks = Ticks.generators.logarithmic(generationOptions, me); - - if (!me.isHorizontal()) { - // We are in a vertical orientation. The top value is the highest. So reverse the array - ticks.reverse(); - } + var ticks = me.ticks = generateTicks(generationOptions, me); // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale @@ -156,14 +227,16 @@ module.exports = function(Chart) { me.min = helpers.min(ticks); if (tickOpts.reverse) { - ticks.reverse(); - + reverse = !reverse; me.start = me.max; me.end = me.min; } else { me.start = me.min; me.end = me.max; } + if (reverse) { + ticks.reverse(); + } }, convertTicksToLabels: function() { this.tickValues = this.ticks.slice(); @@ -177,64 +250,94 @@ module.exports = function(Chart) { getPixelForTick: function(index) { return this.getPixelForValue(this.tickValues[index]); }, + /** + * Returns the value of the first tick. + * @param {Number} value - The minimum not zero value. + * @return {Number} The first tick value. + * @private + */ + _getFirstTickValue: function(value) { + var exp = Math.floor(helpers.log10(value)); + var significand = Math.floor(value / Math.pow(10, exp)); + + return significand * Math.pow(10, exp); + }, getPixelForValue: function(value) { var me = this; - var start = me.start; - var newVal = +me.getRightValue(value); - var opts = me.options; - var tickOpts = opts.ticks; - var innerDimension, pixel, range; + var reverse = me.options.ticks.reverse; + var log10 = helpers.log10; + var firstTickValue = me._getFirstTickValue(me.minNotZero); + var offset = 0; + var innerDimension, pixel, start, end, sign; - if (me.isHorizontal()) { - range = helpers.log10(me.end) - helpers.log10(start); // todo: if start === 0 - if (newVal === 0) { - pixel = me.left; - } else { - innerDimension = me.width; - pixel = me.left + (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); - } + value = +me.getRightValue(value); + if (reverse) { + start = me.end; + end = me.start; + sign = -1; + } else { + start = me.start; + end = me.end; + sign = 1; + } + if (me.isHorizontal()) { + innerDimension = me.width; + pixel = reverse ? me.right : me.left; } else { - // Bottom - top since pixels increase downward on a screen innerDimension = me.height; - if (start === 0 && !tickOpts.reverse) { - range = helpers.log10(me.end) - helpers.log10(me.minNotZero); - if (newVal === start) { - pixel = me.bottom; - } else if (newVal === me.minNotZero) { - pixel = me.bottom - innerDimension * 0.02; - } else { - pixel = me.bottom - innerDimension * 0.02 - (innerDimension * 0.98 / range * (helpers.log10(newVal) - helpers.log10(me.minNotZero))); - } - } else if (me.end === 0 && tickOpts.reverse) { - range = helpers.log10(me.start) - helpers.log10(me.minNotZero); - if (newVal === me.end) { - pixel = me.top; - } else if (newVal === me.minNotZero) { - pixel = me.top + innerDimension * 0.02; - } else { - pixel = me.top + innerDimension * 0.02 + (innerDimension * 0.98 / range * (helpers.log10(newVal) - helpers.log10(me.minNotZero))); - } - } else if (newVal === 0) { - pixel = tickOpts.reverse ? me.top : me.bottom; - } else { - range = helpers.log10(me.end) - helpers.log10(start); - innerDimension = me.height; - pixel = me.bottom - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); + sign *= -1; // invert, since the upper-left corner of the canvas is at pixel (0, 0) + pixel = reverse ? me.top : me.bottom; + } + if (value !== start) { + if (start === 0) { // include zero tick + offset = helpers.getValueOrDefault( + me.options.ticks.fontSize, + Chart.defaults.global.defaultFontSize + ); + innerDimension -= offset; + start = firstTickValue; } + if (value !== 0) { + offset += innerDimension / (log10(end) - log10(start)) * (log10(value) - log10(start)); + } + pixel += sign * offset; } return pixel; }, getValueForPixel: function(pixel) { var me = this; - var range = helpers.log10(me.end) - helpers.log10(me.start); - var value, innerDimension; + var reverse = me.options.ticks.reverse; + var log10 = helpers.log10; + var firstTickValue = me._getFirstTickValue(me.minNotZero); + var innerDimension, start, end, value; + if (reverse) { + start = me.end; + end = me.start; + } else { + start = me.start; + end = me.end; + } if (me.isHorizontal()) { innerDimension = me.width; - value = me.start * Math.pow(10, (pixel - me.left) * range / innerDimension); - } else { // todo: if start === 0 + value = reverse ? me.right - pixel : pixel - me.left; + } else { innerDimension = me.height; - value = Math.pow(10, (me.bottom - pixel) * range / innerDimension) / me.start; + value = reverse ? pixel - me.top : me.bottom - pixel; + } + if (value !== start) { + if (start === 0) { // include zero tick + var offset = helpers.getValueOrDefault( + me.options.ticks.fontSize, + Chart.defaults.global.defaultFontSize + ); + value -= offset; + innerDimension -= offset; + start = firstTickValue; + } + value *= log10(end) - log10(start); + value /= innerDimension; + value = Math.pow(10, log10(start) + value); } return value; } diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 9f59f1788..e36b4e135 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -237,7 +237,6 @@ module.exports = function(Chart) { function drawPointLabels(scale) { var ctx = scale.ctx; - var valueOrDefault = helpers.valueOrDefault; var opts = scale.options; var angleLineOpts = opts.angleLines; var pointLabelOpts = opts.pointLabels; @@ -267,7 +266,7 @@ module.exports = function(Chart) { var pointLabelPosition = scale.getPointPosition(i, outerDistance + 5); // Keep this in loop since we may support array properties here - var pointLabelFontColor = valueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); + var pointLabelFontColor = helpers.valueAtIndexOrDefault(pointLabelOpts.fontColor, i, globalDefaults.defaultFontColor); ctx.font = plFont.font; ctx.fillStyle = pointLabelFontColor; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 4fe39b810..4892eea99 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -240,7 +240,7 @@ function determineStepSize(min, max, unit, capacity) { var i, ilen, factor; if (!steps) { - return Math.ceil(range / ((capacity || 1) * milliseconds)); + return Math.ceil(range / (capacity * milliseconds)); } for (i = 0, ilen = steps.length; i < ilen; ++i) { @@ -403,6 +403,27 @@ function ticksFromTimestamps(values, majorUnit) { return ticks; } +function determineLabelFormat(data, timeOpts) { + var i, momentDate, hasTime; + var ilen = data.length; + + // find the label with the most parts (milliseconds, minutes, etc.) + // format all labels with the same level of detail as the most specific label + for (i = 0; i < ilen; i++) { + momentDate = momentify(data[i], timeOpts); + if (momentDate.millisecond() !== 0) { + return 'MMM D, YYYY h:mm:ss.SSS a'; + } + if (momentDate.second() !== 0 || momentDate.minute() !== 0 || momentDate.hour() !== 0) { + hasTime = true; + } + } + if (hasTime) { + return 'MMM D, YYYY h:mm:ss a'; + } + return 'MMM D, YYYY'; +} + module.exports = function(Chart) { var defaultConfig = { @@ -504,6 +525,7 @@ module.exports = function(Chart) { var me = this; var chart = me.chart; var timeOpts = me.options.time; + var unit = timeOpts.unit || 'day'; var min = MAX_INTEGER; var max = MIN_INTEGER; var timestamps = []; @@ -555,9 +577,9 @@ module.exports = function(Chart) { min = parse(timeOpts.min, me) || min; max = parse(timeOpts.max, me) || max; - // In case there is no valid min/max, let's use today limits - min = min === MAX_INTEGER ? +moment().startOf('day') : min; - max = max === MIN_INTEGER ? +moment().endOf('day') + 1 : max; + // In case there is no valid min/max, set limits based on unit time option + min = min === MAX_INTEGER ? +moment().startOf(unit) : min; + max = max === MIN_INTEGER ? +moment().endOf(unit) + 1 : max; // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); @@ -620,6 +642,7 @@ module.exports = function(Chart) { me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); + me._labelFormat = determineLabelFormat(me._timestamps.data, timeOpts); return ticksFromTimestamps(ticks, me._majorUnit); }, @@ -635,10 +658,13 @@ module.exports = function(Chart) { label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - label = momentify(label, timeOpts).format(timeOpts.tooltipFormat); + return momentify(label, timeOpts).format(timeOpts.tooltipFormat); + } + if (typeof label === 'string') { + return label; } - return label; + return momentify(label, timeOpts).format(me._labelFormat); }, /** @@ -748,7 +774,8 @@ module.exports = function(Chart) { var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height; - return Math.floor(innerWidth / tickLabelWidth); + var capacity = Math.floor(innerWidth / tickLabelWidth); + return capacity > 0 ? capacity : 1; } }); diff --git a/test/.eslintrc b/test/.eslintrc.yml similarity index 89% rename from test/.eslintrc rename to test/.eslintrc.yml index 8e8f899bf..9d98c4528 100644 --- a/test/.eslintrc +++ b/test/.eslintrc.yml @@ -11,3 +11,4 @@ globals: rules: # Best Practices complexity: 0 + max-statements: 0 diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.json b/test/fixtures/controller.bar/bar-thickness-absolute.json new file mode 100644 index 000000000..599b090d6 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-absolute.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2019", "2024", "2025"], + "datasets": [{ + "backgroundColor": "rgba(255, 99, 132, 0.5)", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": 128, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.png b/test/fixtures/controller.bar/bar-thickness-absolute.png new file mode 100644 index 000000000..40172b392 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-absolute.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.json b/test/fixtures/controller.bar/bar-thickness-flex-offset.json new file mode 100644 index 000000000..1776b07be --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex-offset.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.png b/test/fixtures/controller.bar/bar-thickness-flex-offset.png new file mode 100644 index 000000000..e20cc4eb4 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-flex-offset.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-flex.json b/test/fixtures/controller.bar/bar-thickness-flex.json new file mode 100644 index 000000000..0bef9db18 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex.png b/test/fixtures/controller.bar/bar-thickness-flex.png new file mode 100644 index 000000000..791a29d25 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-flex.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-max.json b/test/fixtures/controller.bar/bar-thickness-max.json new file mode 100644 index 000000000..3add5550b --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-max.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "maxBarThickness": 8, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-max.png b/test/fixtures/controller.bar/bar-thickness-max.png new file mode 100644 index 000000000..e22867dbd Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-max.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.json b/test/fixtures/controller.bar/bar-thickness-min-interval.json new file mode 100644 index 000000000..e5861685c --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-min-interval.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.png b/test/fixtures/controller.bar/bar-thickness-min-interval.png new file mode 100644 index 000000000..ae01a9b94 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-min-interval.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.json b/test/fixtures/controller.bar/bar-thickness-multiple.json new file mode 100644 index 000000000..fc39849ae --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-multiple.json @@ -0,0 +1,46 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.png b/test/fixtures/controller.bar/bar-thickness-multiple.png new file mode 100644 index 000000000..d38405292 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-multiple.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-no-overlap.json b/test/fixtures/controller.bar/bar-thickness-no-overlap.json new file mode 100644 index 000000000..3e9f1f5f5 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-no-overlap.json @@ -0,0 +1,46 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [ + {"y": "1", "t": "2016"}, + {"y": "2", "t": "2017"}, + {"y": "3", "t": "2017-08"}, + {"y": "4", "t": "2024"}, + {"y": "5", "t": "2030"} + ] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-no-overlap.png b/test/fixtures/controller.bar/bar-thickness-no-overlap.png new file mode 100644 index 000000000..1385a0853 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-no-overlap.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-offset.json b/test/fixtures/controller.bar/bar-thickness-offset.json new file mode 100644 index 000000000..b31eb739d --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-offset.json @@ -0,0 +1,47 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-offset.png b/test/fixtures/controller.bar/bar-thickness-offset.png new file mode 100644 index 000000000..8dcecac88 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-offset.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.json b/test/fixtures/controller.bar/bar-thickness-single-xy.json new file mode 100644 index 000000000..76caa37fa --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-single-xy.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [{"x": "2022", "y": 42}] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.png b/test/fixtures/controller.bar/bar-thickness-single-xy.png new file mode 100644 index 000000000..26171e531 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-single-xy.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-single.json b/test/fixtures/controller.bar/bar-thickness-single.json new file mode 100644 index 000000000..322af7547 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-single.json @@ -0,0 +1,43 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + }, + "time": { + "min": "2013" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-single.png b/test/fixtures/controller.bar/bar-thickness-single.png new file mode 100644 index 000000000..22314319e Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-single.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.json b/test/fixtures/controller.bar/bar-thickness-stacked.json new file mode 100644 index 000000000..aa2d82553 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-stacked.json @@ -0,0 +1,48 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "stacked": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "stacked": true, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.png b/test/fixtures/controller.bar/bar-thickness-stacked.png new file mode 100644 index 000000000..696829ee3 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-stacked.png differ diff --git a/test/jasmine.utils.js b/test/jasmine.utils.js index 7db35642e..2c2006a2c 100644 --- a/test/jasmine.utils.js +++ b/test/jasmine.utils.js @@ -118,9 +118,6 @@ function specFromFixture(description, inputs) { var chart = acquireChart(json.config, json.options); if (!inputs.png) { fail('Missing PNG comparison file for ' + inputs.json); - if (!json.debug) { - releaseChart(chart); - } done(); } diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index 315bdbd34..096b2e8e1 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -1,4 +1,6 @@ describe('Chart.controllers.bar', function() { + describe('auto', jasmine.specsFromFixtures('controller.bar')); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'bar', @@ -1630,84 +1632,4 @@ describe('Chart.controllers.bar', function() { }); }); }); - - describe('Bar thickness with a time scale', function() { - ['auto', 'data', 'labels'].forEach(function(source) { - ['series', 'linear'].forEach(function(distribution) { - describe('When ticks.source is "' + source + '", distribution is "' + distribution + '"', function() { - beforeEach(function() { - this.chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [{ - data: [1, 2, 3] - }, { - data: [1, 2, 3] - }], - labels: ['2017', '2018', '2020'] - }, - options: { - legend: false, - title: false, - scales: { - xAxes: [{ - id: 'x', - type: 'time', - time: { - unit: 'year', - parser: 'YYYY' - }, - ticks: { - source: source - }, - offset: true, - distribution: distribution - }], - yAxes: [{ - type: 'linear' - }] - } - } - }); - }); - - it('should correctly set bar width', function() { - var chart = this.chart; - var scale = chart.scales.x; - var options = chart.options.scales.xAxes[0]; - var categoryPercentage = options.categoryPercentage; - var barPercentage = options.barPercentage; - var firstInterval = scale.getPixelForValue('2018') - scale.getPixelForValue('2017'); - var firstExpected = firstInterval * categoryPercentage / 2 * barPercentage; - var lastInterval = scale.getPixelForValue('2020') - scale.getPixelForValue('2018'); - var lastExpected = lastInterval * categoryPercentage / 2 * barPercentage; - var i, ilen, meta; - - for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { - meta = chart.getDatasetMeta(i); - expect(meta.data[0]._model.width).toBeCloseToPixel(firstExpected); - expect(meta.data[1]._model.width).toBeCloseToPixel((firstExpected + lastExpected) / 2); - expect(meta.data[2]._model.width).toBeCloseToPixel(lastExpected); - } - }); - - it('should correctly set bar width if maxBarThickness is specified', function() { - var chart = this.chart; - var options = chart.options.scales.xAxes[0]; - var i, ilen, meta; - - options.maxBarThickness = 10; - chart.update(); - - for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { - meta = chart.getDatasetMeta(i); - expect(meta.data[0]._model.width).toBeCloseToPixel(10); - expect(meta.data[1]._model.width).toBeCloseToPixel(10); - expect(meta.data[2]._model.width).toBeCloseToPixel(10); - } - }); - }); - }); - }); - }); }); diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index 85e382ac3..a4d6526c9 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -205,6 +205,46 @@ describe('Chart.controllers.doughnut', function() { }); }); + it('should treat negative values as positive', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [-1, -3] + }], + labels: ['label0', 'label1'] + }, + options: { + legend: false, + title: false, + cutoutPercentage: 50, + rotation: Math.PI, + circumference: Math.PI * 0.5, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(2); + + // Only startAngle, endAngle and circumference should be different. + [ + {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, + {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} + ].forEach(function(expected, i) { + expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i]._model.startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i]._model.endAngle).toBeCloseTo(expected.e, 8); + }); + }); + it ('should draw all arcs', function() { var chart = window.acquireChart({ type: 'doughnut', diff --git a/test/specs/controller.scatter.test.js b/test/specs/controller.scatter.test.js new file mode 100644 index 000000000..435750928 --- /dev/null +++ b/test/specs/controller.scatter.test.js @@ -0,0 +1,25 @@ +describe('Chart.controllers.scatter', function() { + describe('showLines option', function() { + it('should not draw a line if undefined', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 10, y: 15}], + label: 'dataset1' + }], + }, + options: {} + }); + + var meta = chart.getDatasetMeta(0); + spyOn(meta.dataset, 'draw'); + spyOn(meta.data[0], 'draw'); + + chart.update(); + + expect(meta.dataset.draw.calls.count()).toBe(0); + expect(meta.data[0].draw.calls.count()).toBe(1); + }); + }); +}); diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index 3ec8da50b..1ca699bc1 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -759,8 +759,8 @@ describe('Chart', function() { // Verify that points are at their initial correct location, // then we will reset and see that they moved - expect(meta.data[0]._model.y).toBe(333); - expect(meta.data[1]._model.y).toBe(183); + expect(meta.data[0]._model.y).toBeCloseToPixel(333); + expect(meta.data[1]._model.y).toBeCloseToPixel(183); expect(meta.data[2]._model.y).toBe(32); expect(meta.data[3]._model.y).toBe(484); @@ -775,6 +775,38 @@ describe('Chart', function() { }); describe('config update', function() { + it ('should update options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + chart.options = { + responsive: false, + scales: { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + } + }; + chart.update(); + + var yScale = chart.scales['y-axis-0']; + expect(yScale.options.ticks.min).toBe(0); + expect(yScale.options.ticks.max).toBe(10); + }); + it ('should update scales options', function() { var chart = acquireChart({ type: 'line', @@ -798,6 +830,79 @@ describe('Chart', function() { expect(yScale.options.ticks.max).toBe(10); }); + it ('should update scales options from new object', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + var newScalesConfig = { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + }; + chart.options.scales = newScalesConfig; + + chart.update(); + + var yScale = chart.scales['y-axis-0']; + expect(yScale.options.ticks.min).toBe(0); + expect(yScale.options.ticks.max).toBe(10); + }); + + it ('should remove discarded scale', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true, + scales: { + yAxes: [{ + id: 'yAxis0', + ticks: { + min: 0, + max: 10 + } + }] + } + } + }); + + var newScalesConfig = { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + }; + chart.options.scales = newScalesConfig; + + chart.update(); + + var yScale = chart.scales.yAxis0; + expect(yScale).toBeUndefined(); + var newyScale = chart.scales['y-axis-0']; + expect(newyScale.options.ticks.min).toBe(0); + expect(newyScale.options.ticks.max).toBe(10); + }); + it ('should update tooltip options', function() { var chart = acquireChart({ type: 'line', diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 419428bf1..eb96ef8c7 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -194,8 +194,12 @@ describe('Core helper tests', function() { it('should do a log10 operation', function() { expect(helpers.log10(0)).toBe(-Infinity); - expect(helpers.log10(1)).toBe(0); - expect(helpers.log10(1000)).toBeCloseTo(3, 1e-9); + + // Check all allowed powers of 10, which should return integer values + var maxPowerOf10 = Math.floor(helpers.log10(Number.MAX_VALUE)); + for (var i = 0; i < maxPowerOf10; i += 1) { + expect(helpers.log10(Math.pow(10, i))).toBe(i); + } }); it('should correctly determine if two numbers are essentially equal', function() { @@ -725,6 +729,23 @@ describe('Core helper tests', function() { document.body.removeChild(div); }); + it ('should leave styled height and width on canvas if explicitly set', function() { + var chart = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'height: 400px; width: 400px;' + } + }); + + helpers.retinaScale(chart, true); + + var canvas = chart.canvas; + + expect(canvas.style.height).toBe('400px'); + expect(canvas.style.width).toBe('400px'); + }); + describe('Color helper', function() { function isColorInstance(obj) { return typeof obj === 'object' && obj.hasOwnProperty('values') && obj.values.hasOwnProperty('rgb'); diff --git a/test/specs/core.layoutService.tests.js b/test/specs/core.layouts.tests.js similarity index 92% rename from test/specs/core.layoutService.tests.js rename to test/specs/core.layouts.tests.js index b3d576426..19b3a14b2 100644 --- a/test/specs/core.layoutService.tests.js +++ b/test/specs/core.layouts.tests.js @@ -1,5 +1,14 @@ -// Tests of the scale service -describe('Test the layout service', function() { +describe('Chart.layouts', function() { + it('should be exposed through Chart.layouts', function() { + expect(Chart.layouts).toBeDefined(); + expect(typeof Chart.layouts).toBe('object'); + expect(Chart.layouts.defaults).toBeDefined(); + expect(Chart.layouts.addBox).toBeDefined(); + expect(Chart.layouts.removeBox).toBeDefined(); + expect(Chart.layouts.configure).toBeDefined(); + expect(Chart.layouts.update).toBeDefined(); + }); + // Disable tests which need to be rewritten based on changes introduced by // the following changes: https://github.com/chartjs/Chart.js/pull/2346 // using xit marks the test as pending: http://jasmine.github.io/2.0/introduction.html#section-Pending_Specs @@ -550,4 +559,34 @@ describe('Test the layout service', function() { expect(isOrderCorrect).toBe(true); }); }); + + describe('box sizing', function() { + it('should correctly compute y-axis width to fit labels', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['tick 1', 'tick 2', 'tick 3', 'tick 4', 'tick 5'], + datasets: [{ + data: [0, 2.25, 1.5, 1.25, 2.5] + }], + }, + options: { + legend: { + display: false, + }, + }, + }, { + canvas: { + height: 256, + width: 256 + } + }); + var yAxis = chart.scales['y-axis-0']; + + // issue #4441: y-axis labels partially hidden. + // minimum horizontal space required to fit labels + expect(yAxis.width).toBeCloseToPixel(33); + expect(yAxis.ticks).toEqual(['2.5', '2.0', '1.5', '1.0', '0.5', '0']); + }); + }); }); diff --git a/test/specs/core.plugin.tests.js b/test/specs/core.plugin.tests.js index 387d78808..3a9e908a3 100644 --- a/test/specs/core.plugin.tests.js +++ b/test/specs/core.plugin.tests.js @@ -339,6 +339,39 @@ describe('Chart.plugins', function() { expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[1]).toEqual({a: 'foobar'}); + + delete Chart.defaults.global.plugins.a; + }); + + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + it('should invalidate cache when update plugin options', function() { + var plugin = {id: 'a', hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin], + options: { + plugins: { + a: { + foo: 'foo' + } + } + }, + }); + + spyOn(plugin, 'hook'); + + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({foo: 'foo'}); + + chart.options.plugins.a = {bar: 'bar'}; + chart.update(); + + plugin.hook.calls.reset(); + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({bar: 'bar'}); }); }); }); diff --git a/test/specs/core.ticks.tests.js b/test/specs/core.ticks.tests.js new file mode 100644 index 000000000..01cecc546 --- /dev/null +++ b/test/specs/core.ticks.tests.js @@ -0,0 +1,96 @@ +describe('Test tick generators', function() { + // formatters are used as default config values so users want to be able to reference them + it('Should expose formatters api', function() { + expect(typeof Chart.Ticks).toBeDefined(); + expect(typeof Chart.Ticks.formatters).toBeDefined(); + expect(typeof Chart.Ticks.formatters.values).toBe('function'); + expect(typeof Chart.Ticks.formatters.linear).toBe('function'); + expect(typeof Chart.Ticks.formatters.logarithmic).toBe('function'); + }); + + it('Should generate linear spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + legend: { + display: false, + }, + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom', + ticks: { + callback: function(value) { + return value.toString(); + } + } + }], + yAxes: [{ + type: 'linear', + ticks: { + callback: function(value) { + return value.toString(); + } + } + }] + } + } + }); + + var xAxis = chart.scales['x-axis-0']; + var yAxis = chart.scales['y-axis-0']; + + expect(xAxis.ticks).toEqual(['-1', '-0.8', '-0.6', '-0.4', '-0.2', '0', '0.2', '0.4', '0.6', '0.8', '1']); + expect(yAxis.ticks).toEqual(['1', '0.8', '0.6', '0.4', '0.2', '0', '-0.2', '-0.4', '-0.6', '-0.8', '-1']); + }); + + it('Should generate logarithmic spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + legend: { + display: false, + }, + scales: { + xAxes: [{ + type: 'logarithmic', + position: 'bottom', + ticks: { + min: 0.1, + max: 1, + callback: function(value) { + return value.toString(); + } + } + }], + yAxes: [{ + type: 'logarithmic', + ticks: { + min: 0.1, + max: 1, + callback: function(value) { + return value.toString(); + } + } + }] + } + } + }); + + var xAxis = chart.scales['x-axis-0']; + var yAxis = chart.scales['y-axis-0']; + + expect(xAxis.ticks).toEqual(['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); + expect(yAxis.ticks).toEqual(['1', '0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.1']); + }); +}); diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index 632f8121d..8878d9d9d 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -764,7 +764,7 @@ describe('Core.Tooltip', function() { it('Should not update if active element has not changed', function() { var chart = window.acquireChart({ - type: 'bar', + type: 'line', data: { datasets: [{ label: 'Dataset 1', @@ -882,4 +882,71 @@ describe('Core.Tooltip', function() { expect(fn.calls.first().object instanceof Chart.Tooltip).toBe(true); }); }); + + it('Should avoid tooltip truncation in x axis if there is enough space to show tooltip without truncation', function() { + var chart = window.acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [ + 50, + 50 + ], + backgroundColor: [ + 'rgb(255, 0, 0)', + 'rgb(0, 255, 0)' + ], + label: 'Dataset 1' + }], + labels: [ + 'Red long tooltip text to avoid unnecessary loop steps', + 'Green long tooltip text to avoid unnecessary loop steps' + ] + }, + options: { + responsive: true, + animation: { + // without this slice center point is calculated wrong + animateRotate: false + } + } + }); + + // Trigger an event over top of the slice + for (var slice = 0; slice < 2; slice++) { + var meta = chart.getDatasetMeta(0); + var point = meta.data[slice].getCenterPoint(); + var tooltipPosition = meta.data[slice].tooltipPosition(); + var node = chart.canvas; + var rect = node.getBoundingClientRect(); + + var mouseMoveEvent = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }); + var mouseOutEvent = new MouseEvent('mouseout'); + + // Lets cycle while tooltip is narrower than chart area + var infiniteCycleDefense = 70; + for (var i = 0; i < infiniteCycleDefense; i++) { + chart.config.data.labels[slice] = chart.config.data.labels[slice] + 'l'; + chart.update(); + node.dispatchEvent(mouseOutEvent); + node.dispatchEvent(mouseMoveEvent); + var model = chart.tooltip._model; + expect(model.x).toBeGreaterThanOrEqual(0); + if (model.width <= chart.width) { + expect(model.x + model.width).toBeLessThanOrEqual(chart.width); + } + expect(model.caretX).toBe(tooltipPosition.x); + // if tooltip is longer than chart area then all tests done + if (model.width > chart.width) { + break; + } + } + } + }); }); diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index f1091464d..535b9af33 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,4 +1,13 @@ describe('Deprecations', function() { + describe('Version 2.8.0', function() { + describe('Chart.layoutService', function() { + it('should be defined and an alias of Chart.layouts', function() { + expect(Chart.layoutService).toBeDefined(); + expect(Chart.layoutService).toBe(Chart.layouts); + }); + }); + }); + describe('Version 2.7.0', function() { describe('Chart.Controller.update(duration, lazy)', function() { it('should add an animation with the provided options', function() { @@ -302,8 +311,8 @@ describe('Deprecations', function() { 'afterLayout' ]; - var override = Chart.layoutService.update; - Chart.layoutService.update = function() { + var override = Chart.layouts.update; + Chart.layouts.update = function() { sequence.push('layoutUpdate'); override.apply(this, arguments); }; @@ -372,5 +381,23 @@ describe('Deprecations', function() { expect(Chart.pluginService).toBe(Chart.plugins); }); }); + + describe('Chart.Legend', function() { + it('should be defined and an instance of Chart.Element', function() { + var legend = new Chart.Legend({}); + expect(Chart.Legend).toBeDefined(); + expect(legend).not.toBe(undefined); + expect(legend instanceof Chart.Element).toBeTruthy(); + }); + }); + + describe('Chart.Title', function() { + it('should be defined and an instance of Chart.Element', function() { + var title = new Chart.Title({}); + expect(Chart.Title).toBeDefined(); + expect(title).not.toBe(undefined); + expect(title instanceof Chart.Element).toBeTruthy(); + }); + }); }); }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index b950c3601..5b75069aa 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -1,10 +1,5 @@ // Test the rectangle element describe('Legend block tests', function() { - it('Should be constructed', function() { - var legend = new Chart.Legend({}); - expect(legend).not.toBe(undefined); - }); - it('should have the correct default config', function() { expect(Chart.defaults.global.legend).toEqual({ display: true, diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js index edeb32a8b..28786f054 100644 --- a/test/specs/plugin.title.tests.js +++ b/test/specs/plugin.title.tests.js @@ -1,11 +1,6 @@ // Test the rectangle element describe('Title block tests', function() { - it('Should be constructed', function() { - var title = new Chart.Title({}); - expect(title).not.toBe(undefined); - }); - it('Should have the correct default config', function() { expect(Chart.defaults.global.title).toEqual({ display: false, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 9640b1d3d..3d79e6cfc 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -690,92 +690,435 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale1.getLabelForIndex(0, 2)).toBe(150); }); - it('should get the correct pixel value for a point', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - xAxisID: 'xScale', // for the horizontal scale - yAxisID: 'yScale', - data: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}] - }], + describe('when', function() { + var data = [ + { + data: [1, 39], + stack: 'stack' }, - options: { - scales: { - xAxes: [{ - id: 'xScale', - type: 'logarithmic' - }], - yAxes: [{ - id: 'yScale', - type: 'logarithmic' - }] - } + { + data: [1, 39], + stack: 'stack' + }, + ]; + var dataWithEmptyStacks = [ + { + data: [] + }, + { + data: [] } - }); - - var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight - expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(37 + 6); // left + paddingLeft + lineSpace - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(278 + 6 / 2); // halfway - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(37 + 6); // 0 is invalid, put it on the left. - - expect(xScale.getValueForPixel(495)).toBeCloseToPixel(80); - expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(278)).toBeCloseTo(10, 1e-4); - - var yScale = chart.scales.yScale; - expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(246); // halfway - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // 0 is invalid. force it on bottom - - expect(yScale.getValueForPixel(32)).toBeCloseTo(80, 1e-4); - expect(yScale.getValueForPixel(484)).toBeCloseTo(1, 1e-4); - expect(yScale.getValueForPixel(246)).toBeCloseTo(10, 1e-4); - }); - - it('should get the correct pixel value for a point when 0 values are present', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [{ - yAxisID: 'yScale', - data: [0.063, 4, 0, 63, 10, 0.5] - }], - labels: [] + ].concat(data); + var config = [ + { + axis: 'y', + firstTick: 1, // start of the axis (minimum) + describe: 'all stacks are defined' }, - options: { - scales: { + { + axis: 'y', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'y', + scale: { yAxes: [{ - id: 'yScale', - type: 'logarithmic', ticks: { - reverse: false + min: 0 } }] - } + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + scale: { + yAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + firstTick: 1, + describe: 'all stacks are defined' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'x', + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not all stacks are defined and ticks.min: 0' + }, + ]; + config.forEach(function(setup) { + var scaleConfig = {}; + var type, chartStart, chartEnd; + + if (setup.axis === 'x') { + type = 'horizontalBar'; + chartStart = 'left'; + chartEnd = 'right'; + } else { + type = 'bar'; + chartStart = 'bottom'; + chartEnd = 'top'; } + scaleConfig[setup.axis + 'Axes'] = [{ + type: 'logarithmic' + }]; + Chart.helpers.extend(scaleConfig, setup.scale); + var description = 'dataset has stack option and ' + setup.describe + + ' and axis is "' + setup.axis + '";'; + describe(description, function() { + it('should define the correct axis limits', function() { + var chart = window.acquireChart({ + type: type, + data: { + labels: ['category 1', 'category 2'], + datasets: setup.data || data, + }, + options: { + scales: scaleConfig + } + }); + + var axisID = setup.axis + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = 80; // last tick (should be first available tick after: 2 * 39) + var start = chart.chartArea[chartStart]; + var end = chart.chartArea[chartEnd]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[chartEnd]; + end = chart.chartArea[chartStart]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); }); - - var yScale = chart.scales.yScale; - expect(yScale.getPixelForValue(70, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(0.063, 0, 0)).toBeCloseToPixel(475); // minNotZero 2% from range - expect(yScale.getPixelForValue(0.5, 0, 0)).toBeCloseToPixel(344); - expect(yScale.getPixelForValue(4, 0, 0)).toBeCloseToPixel(213); - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(155); - expect(yScale.getPixelForValue(63, 0, 0)).toBeCloseToPixel(38.5); - - chart.options.scales.yAxes[0].ticks.reverse = true; // Reverse mode - chart.update(); - - expect(yScale.getPixelForValue(70, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(0.063, 0, 0)).toBeCloseToPixel(41); // minNotZero 2% from range - expect(yScale.getPixelForValue(0.5, 0, 0)).toBeCloseToPixel(172); - expect(yScale.getPixelForValue(4, 0, 0)).toBeCloseToPixel(303); - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(361); - expect(yScale.getPixelForValue(63, 0, 0)).toBeCloseToPixel(477); }); + + describe('when', function() { + var config = [ + { + dataset: [], + firstTick: 1, // value of the first tick + lastTick: 10, // value of the last tick + describe: 'empty dataset, without ticks.min/max' + }, + { + dataset: [], + scale: {stacked: true}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, without ticks.min/max, with stacked: true' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'bar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'horizontalBar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + dataset: [], + scale: {ticks: {min: 1}}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, ticks.min: 1, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.max: 80, without ticks.min' + }, + { + dataset: [], + scale: {ticks: {max: 0.8}}, + firstTick: 0.01, + lastTick: 0.8, + describe: 'empty dataset, ticks.max: 0.8, without ticks.min' + }, + { + dataset: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 1, y: 1}, max point {x:78, y:78}' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' + } + ]; + axes.forEach(function(axis) { + var expectation = 'min = ' + setup.firstTick + ', max = ' + setup.lastTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); // 0 is invalid, put it at the start. + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); + }); + }); + + describe('when', function() { + var config = [ + { + dataset: [], + scale: {ticks: {min: 0}}, + firstTick: 1, // value of the first tick + lastTick: 10, // value of the last tick + describe: 'empty dataset, ticks.min: 0, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.min: 0, ticks.max: 80' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 0.8}}, + firstTick: 0.1, + lastTick: 0.8, + describe: 'empty dataset, ticks.min: 0, ticks.max: 0.8' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 1.2, y: 1.2}' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 6, + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 6.3, y: 6.3}' + }, + { + dataset: [{x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + scale: {ticks: {min: 0}}, + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 1.2, y: 1.2}, max point {x:78, y:78}, ticks.min: 0' + }, + { + dataset: [{x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], + scale: {ticks: {min: 0}}, + firstTick: 6, + lastTick: 80, + describe: 'dataset min point {x: 6.3, y: 6.3}, max point {x:78, y:78}, ticks.min: 0' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' + } + ]; + axes.forEach(function(axis) { + var expectation = 'min = 0, max = ' + setup.lastTick + ', first tick = ' + setup.firstTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var fontSize = chart.options.defaultFontSize; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + var sign = scale.isHorizontal() ? 1 : -1; + + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start + sign * fontSize); + + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start + sign * fontSize)).toBeCloseTo(firstTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start - sign * fontSize, 4); + + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start - sign * fontSize)).toBeCloseTo(firstTick, 4); + }); + }); + }); + }); + }); + }); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 19189040e..964e25e08 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -584,6 +584,115 @@ describe('Time scale tests', function() { expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); }); + it('should get the correct label when time is specified as a string', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [{t: '2015-01-01T20:00:00', y: 10}, {t: '2015-01-02T21:00:00', y: 3}] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); + expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); + }); + + it('should get the correct label for a timestamp with milliseconds', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23.234'), y: 10}, + {t: +new Date('2018-01-09 06:17:43.426'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23.234 am'); + }); + + it('should get the correct label for a timestamp with time', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23'), y: 10}, + {t: +new Date('2018-01-09 06:17:43'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23 am'); + }); + + it('should get the correct label for a timestamp representing a date', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 00:00:00'), y: 10}, + {t: +new Date('2018-01-09 00:00:00'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018'); + }); + it('should get the correct pixel for only one data in the dataset', function() { var chart = window.acquireChart({ type: 'line', @@ -703,7 +812,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2019', '2020', '2025', '2042']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -714,6 +823,19 @@ describe('Time scale tests', function() { expect(scale.max).toEqual(+moment().endOf('day') + 1); expect(getTicksLabels(scale)).toEqual([]); }); + it ('should correctly handle empty `data.labels` using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); describe('is "data"', function() { @@ -784,7 +906,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -796,6 +918,21 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2018', '2020', '2043']); }); + it ('should correctly handle empty `data.labels` and hidden datasets using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + var meta = chart.getDatasetMeta(1); + meta.hidden = true; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); });