diff --git a/.codeclimate.yml b/.codeclimate.yml index d865954a2..ee3a5fdac 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -6,6 +6,7 @@ engines: - javascript eslint: enabled: true + channel: "eslint-3" fixme: enabled: true ratings: diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 426edad8d..7d57bbd0c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,9 +1,40 @@ + -Example of issue on an interactive website such as the following: -- http://jsbin.com/ -- http://jsfiddle.net/ -- http://codepen.io/pen/ -- Premade template: http://codepen.io/pen?template=JXVYzq \ No newline at end of file + + +## Expected Behavior + + + +## Current Behavior + + + +## Possible Solution + + + +## Steps to Reproduce (for bugs) + + +1. +2. +3. +4. + +## Context + + + +## Environment + +* Chart.js version: +* Browser name and version: +* Link to your project: diff --git a/.gitignore b/.gitignore index 478edb5ec..8ef0139ea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ .DS_Store .idea +.vscode bower.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8b96cbe2..6ec102439 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Reporting bugs Well structured, detailed bug reports are hugely valuable for the project. -Guidlines for reporting bugs: +Guidelines for reporting bugs: - Check the issue search to see if it has already been reported - Isolate the problem to a simple test case diff --git a/README.md b/README.md index 49d09c017..daec17a46 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## Installation -You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest) or just use these [Chart.js CDN](https://cdnjs.com/libraries/Chart.js) links. +You can download the latest version of Chart.js from the [GitHub releases](https://github.com/chartjs/Chart.js/releases/latest) or use a [Chart.js CDN](https://cdnjs.com/libraries/Chart.js). To install via npm: @@ -16,7 +16,16 @@ To install via npm: npm install chart.js --save ``` -To Install via bower, please follow [these instructions](http://www.chartjs.org/docs/#getting-started-installation). +To install via bower: +```bash +bower install chart.js --save +``` + +#### 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. + +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. ## Documentation diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md index 910ea5bea..b7eb09dfc 100644 --- a/docs/00-Getting-Started.md +++ b/docs/00-Getting-Started.md @@ -71,6 +71,7 @@ To create a chart, we need to instantiate the `Chart` class. To do this, we need var ctx = document.getElementById("myChart"); var ctx = document.getElementById("myChart").getContext("2d"); var ctx = $("#myChart"); +var ctx = "myChart"; ``` Once you have the element or context, you're ready to instantiate a pre-defined chart-type or create your own! diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 43e762ba4..a46922088 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -36,13 +36,13 @@ This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https:// Chart.js merges the options object passed to the chart with the global configuration using chart type defaults and scales defaults appropriately. This way you can be as specific as you would like in your individual chart configuration, while still changing the defaults for all chart types where applicable. The global general options are defined in `Chart.defaults.global`. The defaults for each chart type are discussed in the documentation for that chart type. -The following example would set the hover mode to 'single' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation. +The following example would set the hover mode to 'nearest' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation. ```javascript -Chart.defaults.global.hover.mode = 'single'; +Chart.defaults.global.hover.mode = 'nearest'; -// Hover mode is set to single because it was not overridden here -var chartInstanceHoverModeSingle = new Chart(ctx, { +// Hover mode is set to nearest because it was not overridden here +var chartInstanceHoverModeNearest = new Chart(ctx, { type: 'line', data: data, }); @@ -54,7 +54,7 @@ var chartInstanceDifferentHoverMode = new Chart(ctx, { options: { hover: { // Overrides the global setting - mode: 'label' + mode: 'index' } } }) @@ -83,7 +83,16 @@ maintainAspectRatio | Boolean | true | Maintain the original canvas aspect ratio events | Array[String] | `["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"]` | Events that the chart should listen to for tooltips and hovering onClick | Function | null | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed an array of active elements legendCallback | Function | ` function (chart) { }` | Function to generate a legend. Receives the chart object to generate a legend from. Default implementation returns an HTML string. -onResize | Function | null | Called when a resize occurs. Gets passed two arguemnts: the chart instance and the new size. +onResize | Function | null | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. + +### Layout Configuration + +The layout configuration is passed into the `options.layout` namespace. The global options for the chart layout is defined in `Chart.defaults.global.layout`. + +Name | Type | Default | Description +--- | --- | --- | --- +padding | Number or Object | 0 | The padding to add inside the chart. If this value is a number, it is applied to all sides of the chart (left, top, right, bottom). If this value is an object, the `left` property defines the left padding. Similarly the `right`, `top`, and `bottom` properties can also be specified. + ### Title Configuration @@ -130,6 +139,7 @@ fullWidth | Boolean | true | Marks that this box should take the full width of t onClick | Function | `function(event, legendItem) {}` | A callback that is called when a 'click' event is registered on top of a label item onHover | Function | `function(event, legendItem) {}` | A callback that is called when a 'mousemove' event is registered on top of a label item labels |Object|-| See the [Legend Label Configuration](#chart-configuration-legend-label-configuration) section below. +reverse | Boolean | false | Legend will show datasets in reverse order #### Legend Label Configuration @@ -145,7 +155,6 @@ fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Fon padding | Number | 10 | Padding between rows of colored boxes generateLabels: | Function | `function(chart) { }` | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#chart-configuration-legend-item-interface) for details. usePointStyle | Boolean | false | Label style will match corresponding point style (size is based on fontSize, boxWidth is not used in this case). -reverse | Boolean | false | Legend will show datasets in reverse order #### Legend Item Interface @@ -200,7 +209,7 @@ var chartInstance = new Chart(ctx, { fontColor: 'rgb(255, 99, 132)' } } - } +} }); ``` @@ -212,8 +221,11 @@ Name | Type | Default | Description --- | --- | --- | --- enabled | Boolean | true | Are tooltips enabled custom | Function | null | See [section](#advanced-usage-external-tooltips) below -mode | String | 'single' | Sets which elements appear in the tooltip. Acceptable options are `'single'`, `'label'` or `'x-axis'`.
 
`single` highlights the closest element.
 
`label` highlights elements in all datasets at the same `X` value.
 
`'x-axis'` also highlights elements in all datasets at the same `X` value, but activates when hovering anywhere within the vertical slice of the x-axis representing that `X` value. +mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details +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. +position | String | 'average' | The mode for positioning the tooltip. 'average' mode will place the tooltip at the average position of the items displayed in the tooltip. 'nearest' will place the tooltip at the position of the element closest to the event position. New modes can be defined by adding functions to the Chart.Tooltip.positioners map. itemSort | Function | undefined | Allows sorting of [tooltip items](#chart-configuration-tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart. +filter | Function | undefined | Allows filtering of [tooltip items](#chart-configuration-tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.filter](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). This function can also accept a second parameter that is the data object passed to the chart. backgroundColor | Color | 'rgba(0,0,0,0.8)' | Background color of the tooltip titleFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for tooltip title inherited from global font family titleFontSize | Number | 12 | Font size for tooltip title inherited from global font size @@ -237,6 +249,7 @@ yPadding | Number | 6 | Padding to add on top and bottom of tooltip caretSize | Number | 5 | Size, in px, of the tooltip arrow cornerRadius | Number | 6 | Radius of tooltip corner curves multiKeyBackground | Color | "#fff" | Color to draw behind the colored boxes when multiple items are in the tooltip +displayColors | Boolean | true | if true, color boxes are shown in the tooltip callbacks | Object | | See the [callbacks section](#chart-configuration-tooltip-callbacks) below #### Tooltip Callbacks @@ -253,12 +266,13 @@ afterTitle | `Array[tooltipItem], data` | Text to render after the title beforeBody | `Array[tooltipItem], data` | Text to render before the body section beforeLabel | `tooltipItem, data` | Text to render before an individual label label | `tooltipItem, data` | Text to render for an individual item in the tooltip -labelColor | `tooltipItem, chartInstace` | Returns the colors to render for the tooltip item. Return as an object containing two parameters: `borderColor` and `backgroundColor`. +labelColor | `tooltipItem, chartInstance` | Returns the colors to render for the tooltip item. Return as an object containing two parameters: `borderColor` and `backgroundColor`. afterLabel | `tooltipItem, data` | Text to render after an individual label afterBody | `Array[tooltipItem], data` | Text to render after the body section beforeFooter | `Array[tooltipItem], data` | Text to render before the footer section footer | `Array[tooltipItem], data` | Text to render as the footer afterFooter | `Array[tooltipItem], data` | Text to render after the footer section +dataPoints | `Array[tooltipItem]` | List of matching point informations. #### Tooltip Item Interface @@ -276,7 +290,13 @@ The tooltip items passed to the tooltip callbacks implement the following interf datasetIndex: Number, // Index of this data item in the dataset - index: Number + index: Number, + + // X position of matching point + x: Number, + + // Y position of matching point + y: Number, } ``` @@ -286,10 +306,28 @@ The hover configuration is passed into the `options.hover` namespace. The global Name | Type | Default | Description --- | --- | --- | --- -mode | String | 'single' | Sets which elements hover. Acceptable options are `'single'`, `'label'`, `'x-axis'`, or `'dataset'`.
 
`single` highlights the closest element.
 
`label` highlights elements in all datasets at the same `X` value.
 
`'x-axis'` also highlights elements in all datasets at the same `X` value, but activates when hovering anywhere within the vertical slice of the x-axis representing that `X` value.
 
`dataset` highlights the closest dataset. +mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details +intersect | Boolean | true | if true, the hover mode only applies when the mouse position intersects an item on the chart animationDuration | Number | 400 | Duration in milliseconds it takes to animate hover style changes onHover | Function | null | Called when any of the events fire. Called in the context of the chart and passed an array of active elements (bars, points, etc) +### Interaction Modes +When configuring interaction with the graph via hover or tooltips, a number of different modes are available. + +The following table details the modes and how they behave in conjunction with the `intersect` setting + +Mode | Behaviour +--- | --- +point | Finds all of the items that intersect the point +nearest | Gets the item that is nearest to the point. The nearest item is determined based on the distance to the center of the chart item (point, bar). If 2 or more items are at the same distance, the one with the smallest area is used. If `intersect` is true, this is only triggered when the mouse position intersects an item in the graph. This is very useful for combo charts where points are hidden behind bars. +single (deprecated) | Finds the first item that intersects the point and returns it. Behaves like 'nearest' mode with intersect = true. +label (deprecated) | See `'index'` mode +index | Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. +x-axis (deprecated) | Behaves like `'index'` mode with `intersect = true` +dataset | Finds items in the same dataset. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. +x | Returns all items that would intersect based on the `X` coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts +y | Returns all items that would intersect based on the `Y` coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts. + ### Animation Configuration The following animation options are available. The global options for are defined in `Chart.defaults.global.animation`. @@ -305,7 +343,7 @@ onComplete | Function | none | Callback called at the end of an animation. Passe The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed an object that implements the following interface. An example usage of these callbacks can be found on [Github](https://github.com/chartjs/Chart.js/blob/master/samples/AnimationCallbacks/progress-bar.html). This sample displays a progress bar showing how far along the animation is. -```javscript +```javascript { // Chart object chartInstance, @@ -345,7 +383,7 @@ The animation object passed to the callbacks is of type `Chart.Animation`. The o The global options for elements are defined in `Chart.defaults.global.elements`. -Options can be configured for four different types of elements; arc, lines, points, and rectangles. When set, these options apply to all objects of that type unless specifically overridden by the configuration attached to a dataset. +Options can be configured for four different types of elements: arc, lines, points, and rectangles. When set, these options apply to all objects of that type unless specifically overridden by the configuration attached to a dataset. #### Arc Configuration @@ -372,7 +410,7 @@ borderDash | Array | `[]` | Default line dash. See [MDN](https://developer.mozil borderDashOffset | Number | 0.0 | Default line dash offset. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) borderJoinStyle | String | 'miter' | Default line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) capBezierPoints | Boolean | true | If true, bezier control points are kept inside the chart. If false, no restriction is enforced. -fill | Boolean | true | If true, the line is filled. +fill | Boolean or String | true | If true, the fill is assumed to be to zero. String values are 'zero', 'top', and 'bottom' to fill to different locations. If `false`, no fill is added stepped | Boolean | false | If true, the line is shown as a stepped line and 'tension' will be ignored #### Point Configuration @@ -451,7 +489,7 @@ var chartData = { ### Mixed Chart Types -When creating a chart, you have the option to overlay different chart types on top of eachother as separate datasets. +When creating a chart, you have the option to overlay different chart types on top of each other as separate datasets. To do this, you must set a `type` for each dataset individually. You can create mixed chart types with bar and line chart types. diff --git a/docs/02-Scales.md b/docs/02-Scales.md index c776355eb..ae7758326 100644 --- a/docs/02-Scales.md +++ b/docs/02-Scales.md @@ -59,7 +59,7 @@ offsetGridLines | Boolean | false | If true, labels are shifted to be between gr #### Scale Title Configuration -The grid line configuration is nested under the scale configuration in the `scaleLabel` key. It defines options for the scale title. +The scale label configuration is nested under the scale configuration in the `scaleLabel` key. It defines options for the scale title. Name | Type | Default | Description --- | --- | --- | --- @@ -72,11 +72,12 @@ fontStyle | String | "normal" | Font style for the scale title, follows CSS font #### Tick Configuration -The grid line configuration is nested under the scale configuration in the `ticks` key. It defines options for the tick marks that are generated by the axis. +The tick configuration is nested under the scale configuration in the `ticks` key. It defines options for the tick marks that are generated by the axis. Name | Type | Default | Description --- | --- | --- | --- autoSkip | Boolean | true | If true, automatically calculates how many labels that can be shown and hides labels accordingly. Turn it off to show all labels no matter what +autoSkipPadding | Number | 0 | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. *Note: Only applicable to horizontal scales.* callback | Function | `function(value) { return helpers.isArray(value) ? value : '' + value; }` | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](#scales-creating-custom-tick-formats) section below. display | Boolean | true | If true, show the ticks. fontColor | Color | "#666" | Font color for the tick labels. @@ -301,7 +302,7 @@ var chartInstance = new Chart(ctx, { ### Radial Linear Scale -The radial linear scale is used specifically for the radar and polar are chart types. It overlays the chart area, rather than being positioned on one of the edges. +The radial linear scale is used specifically for the radar and polar area chart types. It overlays the chart area, rather than being positioned on one of the edges. #### Configuration Options diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 3a361bcff..062bf59e8 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -28,19 +28,27 @@ myLineChart.destroy(); Triggers an update of the chart. This can be safely called after replacing the entire data object. This will update all scales, legends, and then re-render the chart. ```javascript -// duration is the time for the animation of the redraw in miliseconds -// lazy is a boolean. if true, the animation can be interupted by other animations +// duration is the time for the animation of the redraw in milliseconds +// lazy is a boolean. if true, the animation can be interrupted by other animations myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's value of 'March' to be 50 myLineChart.update(); // Calling update now animates the position of March from 90 to 50. ``` +#### .reset() + +Reset the chart to it's state before the initial animation. A new animation can then be triggered using `update`. + +```javascript +myLineChart.reset(); +``` + #### .render(duration, lazy) Triggers a redraw of all chart elements. Note, this does not update elements for new data. Use `.update()` in that case. ```javascript -// duration is the time for the animation of the redraw in miliseconds -// lazy is a boolean. if true, the animation can be interupted by other animations +// duration is the time for the animation of the redraw in milliseconds +// lazy is a boolean. if true, the animation can be interrupted by other animations myLineChart.render(duration, lazy); ``` @@ -165,6 +173,8 @@ var myPieChart = new Chart(ctx, { // tooltip.text // tooltip.x // tooltip.y + // tooltip.caretX + // tooltip.caretY // etc... } } @@ -216,12 +226,12 @@ Scale instances are given the following properties during the fitting process. { left: Number, // left edge of the scale bounding box right: Number, // right edge of the bounding box' - top: Number, + top: Number, bottom: Number, width: Number, // the same as right - left height: Number, // the same as bottom - top - // Margin on each side. Like css, this is outside the bounding box. + // Margin on each side. Like css, this is outside the bounding box. margins: { left: Number, right: Number, @@ -238,7 +248,7 @@ Scale instances are given the following properties during the fitting process. ``` #### Scale Interface -To work with Chart.js, custom scale types must implement the following interface. +To work with Chart.js, custom scale types must implement the following interface. ```javascript { @@ -273,10 +283,10 @@ To work with Chart.js, custom scale types must implement the following interface Optionally, the following methods may also be overwritten, but an implementation is already provided by the `Chart.Scale` base class. ```javascript - // Transform the ticks array of the scale instance into strings. The default implementation simply calls this.options.ticks.callback(numericalTick, index, ticks); + // Transform the ticks array of the scale instance into strings. The default implementation simply calls this.options.ticks.callback(numericalTick, index, ticks); convertTicksToLabels: function() {}, - // Determine how much the labels will rotate by. The default implementation will only rotate labels if the scale is horizontal. + // Determine how much the labels will rotate by. The default implementation will only rotate labels if the scale is horizontal. calculateTickRotation: function() {}, // Fits the scale into the canvas. @@ -293,7 +303,7 @@ Optionally, the following methods may also be overwritten, but an implementation The Core.Scale base class also has some utility functions that you may find useful. ```javascript -{ +{ // Returns true if the scale instance is horizontal isHorizontal: function() {}, @@ -363,7 +373,7 @@ The following methods may optionally be overridden by derived dataset controller // chart types using a single scale linkScales: function() {}, - // Called by the main chart controller when an update is triggered. The default implementation handles the number of data points changing and creating elements appropriately. + // Called by the main chart controller when an update is triggered. The default implementation handles the number of data points changing and creating elements appropriately. buildOrUpdateElements: function() {} } ``` @@ -432,7 +442,7 @@ Plugins should derive from Chart.PluginBase and implement the following interfac ### Building Chart.js -Chart.js uses gulp to build the library into a single JavaScript file. +Chart.js uses gulp to build the library into a single JavaScript file. Firstly, we need to ensure development dependencies are installed. With node and npm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: diff --git a/docs/10-Notes.md b/docs/10-Notes.md index 85cc6507a..fd2125e17 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -49,31 +49,31 @@ Library Features | Feature | Chart.js | D3 | HighCharts | Chartist | | ------- | -------- | --- | ---------- | -------- | -| Completely Free | ✓ | ✓ | | ✓ | -| Canvas | ✓ | | | | -| SVG | | ✓ | ✓ | ✓ | -| Built-in Charts | ✓ | | ✓ | ✓ | -| 8+ Chart Types | ✓ | ✓ | ✓ | | -| Extendable to Custom Charts | ✓ | ✓ | | | -| Supports Modern Browsers | ✓ | ✓ | ✓ | ✓ | -| Extensive Documentation | ✓ | ✓ | ✓ | ✓ | -| Open Source | ✓ | ✓ | ✓ | ✓ | +| Completely Free | ✓ | ✓ | | ✓ | +| Canvas | ✓ | | | | +| SVG | | ✓ | ✓ | ✓ | +| Built-in Charts | ✓ | | ✓ | ✓ | +| 8+ Chart Types | ✓ | ✓ | ✓ | | +| Extendable to Custom Charts | ✓ | ✓ | | | +| Supports Modern Browsers | ✓ | ✓ | ✓ | ✓ | +| Extensive Documentation | ✓ | ✓ | ✓ | ✓ | +| Open Source | ✓ | ✓ | ✓ | ✓ | Built in Chart Types | Type | Chart.js | HighCharts | Chartist | | ---- | -------- | ---------- | -------- | -| Combined Types | ✓ | ✓ | | -| Line | ✓ | ✓ | ✓ | -| Bar | ✓ | ✓ | ✓ | -| Horizontal Bar | ✓ | ✓ | ✓ | -| Pie/Doughnut | ✓ | ✓ | ✓ | -| Polar Area | ✓ | ✓ | | -| Radar | ✓ | | | -| Scatter | ✓ | ✓ | ✓ | -| Bubble | ✓ | | | -| Gauges | | ✓ | | -| Maps (Heat/Tree/etc.) | | ✓ | | +| Combined Types | ✓ | ✓ | | +| Line | ✓ | ✓ | ✓ | +| Bar | ✓ | ✓ | ✓ | +| Horizontal Bar | ✓ | ✓ | ✓ | +| Pie/Doughnut | ✓ | ✓ | ✓ | +| Polar Area | ✓ | ✓ | | +| Radar | ✓ | | | +| Scatter | ✓ | ✓ | ✓ | +| Bubble | ✓ | | | +| Gauges | | ✓ | | +| Maps (Heat/Tree/etc.) | | ✓ | | ### Popular Plugins diff --git a/gulpfile.js b/gulpfile.js index b3dab98d1..1ce312d90 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,10 +17,12 @@ 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 package = require('./package.json'); var srcDir = './src/'; var outDir = './dist/'; +var testDir = './test/'; var header = "/*!\n" + " * Chart.js\n" + @@ -33,17 +35,11 @@ var header = "/*!\n" + " */\n"; var preTestFiles = [ - './node_modules/moment/min/moment.min.js', + './node_modules/moment/min/moment.min.js' ]; var testFiles = [ - './test/mockContext.js', - './test/*.js', - - // Disable tests which need to be rewritten based on changes introduced by - // the following changes: https://github.com/chartjs/Chart.js/pull/2346 - '!./test/core.layoutService.tests.js', - '!./test/defaultConfig.tests.js' + './test/*.js' ]; gulp.task('bower', bowerTask); @@ -133,6 +129,7 @@ function packageTask() { function lintTask() { var files = [ srcDir + '**/*.js', + testDir + '**/*.js' ]; // NOTE(SB) codeclimate has 'complexity' and 'max-statements' eslint rules way too strict @@ -142,7 +139,22 @@ function lintTask() { rules: { 'complexity': [1, 6], 'max-statements': [1, 30] - } + }, + globals: [ + 'Chart', + 'acquireChart', + 'afterAll', + 'afterEach', + 'beforeAll', + 'beforeEach', + 'describe', + 'expect', + 'it', + 'jasmine', + 'moment', + 'spyOn', + 'xit' + ] }; return gulp.src(files) @@ -157,10 +169,13 @@ function validHTMLTask() { } function startTest() { - var files = ['./src/**/*.js']; - Array.prototype.unshift.apply(files, preTestFiles); - Array.prototype.push.apply(files, testFiles); - return files; + return [].concat(preTestFiles).concat([ + './src/**/*.js', + './test/mockContext.js' + ]).concat( + argv.inputs? + argv.inputs.split(';'): + testFiles); } function unittestTask() { diff --git a/package.json b/package.json index e901b26c3..b8dba1874 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.3.0", + "version": "2.4.0", "license": "MIT", "main": "src/chart.js", "repository": { @@ -17,7 +17,7 @@ "gulp": "3.9.x", "gulp-concat": "~2.1.x", "gulp-connect": "~2.0.5", - "gulp-eslint": "^2.0.0", + "gulp-eslint": "^3.0.0", "gulp-file": "^0.3.0", "gulp-html-validator": "^0.0.2", "gulp-insert": "~0.5.0", @@ -38,7 +38,9 @@ "karma-jasmine": "^0.3.6", "karma-jasmine-html-reporter": "^0.1.8", "merge-stream": "^1.0.0", - "vinyl-source-stream": "^1.1.0" + "vinyl-source-stream": "^1.1.0", + "watchify": "^3.7.0", + "yargs": "^5.0.0" }, "spm": { "main": "Chart.js" diff --git a/samples/AnimationCallbacks/progress-bar.html b/samples/AnimationCallbacks/progress-bar.html deleted file mode 100644 index 7d79d6c1d..000000000 --- a/samples/AnimationCallbacks/progress-bar.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - Animation Callbacks - - - - - - -
- - -
-
-
- - - - - - - - - \ No newline at end of file diff --git a/samples/animation/progress-bar.html b/samples/animation/progress-bar.html new file mode 100644 index 000000000..b0c495a02 --- /dev/null +++ b/samples/animation/progress-bar.html @@ -0,0 +1,96 @@ + + + + Animation Callbacks + + + + + + +
+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/samples/bar-multi-axis.html b/samples/bar-multi-axis.html deleted file mode 100644 index 9c8ec3642..000000000 --- a/samples/bar-multi-axis.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - Bar Chart Multi Axis - - - - - - -
- -
- - - - - diff --git a/samples/bar-stacked.html b/samples/bar-stacked.html deleted file mode 100644 index e85a72b26..000000000 --- a/samples/bar-stacked.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - Stacked Bar Chart - - - - - - -
- -
- - - - - diff --git a/samples/bar-horizontal.html b/samples/bar/bar-horizontal.html similarity index 63% rename from samples/bar-horizontal.html rename to samples/bar/bar-horizontal.html index 6d9fd54e6..affcc3349 100644 --- a/samples/bar-horizontal.html +++ b/samples/bar/bar-horizontal.html @@ -3,8 +3,8 @@ Horizontal Bar Chart - - + + + + + +
+ +
+ + + + + diff --git a/samples/bar/bar-stacked.html b/samples/bar/bar-stacked.html new file mode 100644 index 000000000..f80b1b6ed --- /dev/null +++ b/samples/bar/bar-stacked.html @@ -0,0 +1,102 @@ + + + + + Stacked Bar Chart + + + + + + +
+ +
+ + + + + diff --git a/samples/bar.html b/samples/bar/bar.html similarity index 57% rename from samples/bar.html rename to samples/bar/bar.html index d595bae49..d2fc4acac 100644 --- a/samples/bar.html +++ b/samples/bar/bar.html @@ -3,8 +3,8 @@ Bar Chart - - + + - - - -
- -
- - - - - diff --git a/samples/data_labelling.html b/samples/data_labelling.html new file mode 100644 index 000000000..939e517a9 --- /dev/null +++ b/samples/data_labelling.html @@ -0,0 +1,132 @@ + + + + + + Labelling Data Points + + + + + + +
+ +
+ + + + + diff --git a/samples/different-point-sizes.html b/samples/different-point-sizes.html deleted file mode 100644 index 926eecfba..000000000 --- a/samples/different-point-sizes.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
-
-
- - - - - - - diff --git a/samples/doughnut.html b/samples/doughnut.html index 73c96ad80..51b98a82a 100644 --- a/samples/doughnut.html +++ b/samples/doughnut.html @@ -4,7 +4,7 @@ Doughnut Chart - + + + + +
+
+ +
+
+ +
+
+ + + + diff --git a/samples/legend/positions.html b/samples/legend/positions.html new file mode 100644 index 000000000..97bc70fa0 --- /dev/null +++ b/samples/legend/positions.html @@ -0,0 +1,121 @@ + + + + + Legend Positions + + + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + diff --git a/samples/line-customTooltips.html b/samples/line-customTooltips.html deleted file mode 100644 index 6695cb663..000000000 --- a/samples/line-customTooltips.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - Line Chart with Custom Tooltips - - - - - - -
- -
- - - - diff --git a/samples/line-legend.html b/samples/line-legend.html deleted file mode 100644 index 92e5e5b5a..000000000 --- a/samples/line-legend.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
-
-
- - - - - - - - - diff --git a/samples/line-logarithmic.html b/samples/line-logarithmic.html deleted file mode 100644 index 290ea43ea..000000000 --- a/samples/line-logarithmic.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
- - - - - - - - - diff --git a/samples/line-multiline-labels.html b/samples/line-multiline-labels.html deleted file mode 100644 index 3fd0de5e1..000000000 --- a/samples/line-multiline-labels.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
-
-
- - - - - - - - - - diff --git a/samples/line-stacked-area.html b/samples/line-stacked-area.html deleted file mode 100644 index 88f14e2bd..000000000 --- a/samples/line-stacked-area.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
-
-
- - - - - - - - - diff --git a/samples/line-stepped.html b/samples/line-stepped.html deleted file mode 100644 index e618a698c..000000000 --- a/samples/line-stepped.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
-
-
- - - - - - - - - - diff --git a/samples/line-x-axis-filter.html b/samples/line-x-axis-filter.html deleted file mode 100644 index e90575df5..000000000 --- a/samples/line-x-axis-filter.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - Chart with xAxis Filtering - - - - - - -
- -
-
-
- - - - - - - - - diff --git a/samples/line.html b/samples/line.html deleted file mode 100644 index ffca9df3b..000000000 --- a/samples/line.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
-
-
- - - - - - - - - - diff --git a/samples/line/different-point-sizes.html b/samples/line/different-point-sizes.html new file mode 100644 index 000000000..ed52cf0ae --- /dev/null +++ b/samples/line/different-point-sizes.html @@ -0,0 +1,130 @@ + + + + + Different Point Sizes + + + + + + +
+ +
+ + + + diff --git a/samples/line-cubicInterpolationMode.html b/samples/line/interpolation-modes.html similarity index 77% rename from samples/line-cubicInterpolationMode.html rename to samples/line/interpolation-modes.html index 97dac46ca..ff75210a6 100644 --- a/samples/line-cubicInterpolationMode.html +++ b/samples/line/interpolation-modes.html @@ -3,8 +3,8 @@ Line Chart - Cubic interpolation mode - - + + + + + +
+ +
+ + + + diff --git a/samples/line/line-stacked-area.html b/samples/line/line-stacked-area.html new file mode 100644 index 000000000..9621c14d5 --- /dev/null +++ b/samples/line/line-stacked-area.html @@ -0,0 +1,183 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + + + + + diff --git a/samples/line/line-stepped.html b/samples/line/line-stepped.html new file mode 100644 index 000000000..ff1bf2a6e --- /dev/null +++ b/samples/line/line-stepped.html @@ -0,0 +1,93 @@ + + + + + Stepped Line Chart + + + + + + +
+ +
+ + + + diff --git a/samples/line/line-styles.html b/samples/line/line-styles.html new file mode 100644 index 000000000..0ac3aabdb --- /dev/null +++ b/samples/line/line-styles.html @@ -0,0 +1,111 @@ + + + + + Line Styles + + + + + + +
+ +
+ + + + diff --git a/samples/line-skip-points.html b/samples/line/line.html similarity index 59% rename from samples/line-skip-points.html rename to samples/line/line.html index 2d760d2e1..68ae7e5f6 100644 --- a/samples/line-skip-points.html +++ b/samples/line/line.html @@ -3,10 +3,10 @@ Line Chart - - + + + + + +
+
+ + + + diff --git a/samples/pie-customTooltips.html b/samples/pie-customTooltips.html deleted file mode 100644 index 2f5916539..000000000 --- a/samples/pie-customTooltips.html +++ /dev/null @@ -1,156 +0,0 @@ - - - - - Pie Chart with Custom Tooltips - - - - - - - -
- -
-
- -
- -
- - - - - - diff --git a/samples/pie.html b/samples/pie.html index b376b5abd..c299af961 100644 --- a/samples/pie.html +++ b/samples/pie.html @@ -4,12 +4,12 @@ Pie Chart - + -
- +
+
@@ -18,12 +18,6 @@ var randomScalingFactor = function() { return Math.round(Math.random() * 100); }; - var randomColorFactor = function() { - return Math.round(Math.random() * 255); - }; - var randomColor = function(opacity) { - return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')'; - }; var config = { type: 'pie', @@ -37,49 +31,20 @@ randomScalingFactor(), ], backgroundColor: [ - "#F7464A", - "#46BFBD", - "#FDB45C", - "#949FB1", - "#4D5360", - ], - }, { - data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - ], - backgroundColor: [ - "#F7464A", - "#46BFBD", - "#FDB45C", - "#949FB1", - "#4D5360", - ], - }, { - data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - ], - backgroundColor: [ - "#F7464A", - "#46BFBD", - "#FDB45C", - "#949FB1", - "#4D5360", + window.chartColors.red, + window.chartColors.orange, + window.chartColors.yellow, + window.chartColors.green, + window.chartColors.blue, ], + label: 'Dataset 1' }], labels: [ "Red", - "Green", + "Orange", "Yellow", - "Grey", - "Dark Grey" + "Green", + "Blue" ] }, options: { @@ -92,27 +57,37 @@ window.myPie = new Chart(ctx, config); }; - $('#randomizeData').click(function() { - $.each(config.data.datasets, function(i, piece) { - $.each(piece.data, function(j, value) { - config.data.datasets[i].data[j] = randomScalingFactor(); - config.data.datasets[i].backgroundColor[j] = randomColor(0.7); + document.getElementById('randomizeData').addEventListener('click', function() { + config.data.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); }); }); + window.myPie.update(); }); - $('#addDataset').click(function() { + var colorNames = Object.keys(window.chartColors); + document.getElementById('addDataset').addEventListener('click', function() { var newDataset = { - backgroundColor: [randomColor(0.7), randomColor(0.7), randomColor(0.7), randomColor(0.7), randomColor(0.7)], - data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()] + backgroundColor: [], + data: [], + label: 'New dataset ' + config.data.datasets.length, }; + for (var index = 0; index < config.data.labels.length; ++index) { + newDataset.data.push(randomScalingFactor()); + + var colorName = colorNames[index % colorNames.length];; + var newColor = window.chartColors[colorName]; + newDataset.backgroundColor.push(newColor); + } + config.data.datasets.push(newDataset); window.myPie.update(); }); - $('#removeDataset').click(function() { + document.getElementById('removeDataset').addEventListener('click', function() { config.data.datasets.splice(0, 1); window.myPie.update(); }); diff --git a/samples/polar-area.html b/samples/polar-area.html index ae21e1d44..48fe984af 100644 --- a/samples/polar-area.html +++ b/samples/polar-area.html @@ -4,7 +4,7 @@ Polar Area Chart - + - - - -
- -
- - - - - - - - - diff --git a/samples/radar/radar-skip-points.html b/samples/radar/radar-skip-points.html new file mode 100644 index 000000000..ab29b3a0e --- /dev/null +++ b/samples/radar/radar-skip-points.html @@ -0,0 +1,109 @@ + + + + + Radar Chart + + + + + + +
+ +
+ + + + + diff --git a/samples/radar.html b/samples/radar/radar.html similarity index 52% rename from samples/radar.html rename to samples/radar/radar.html index 4203cff15..507d5bb47 100644 --- a/samples/radar.html +++ b/samples/radar/radar.html @@ -3,8 +3,8 @@ Radar Chart - - + + + + + +
+ + + + diff --git a/samples/scales/filtering-labels.html b/samples/scales/filtering-labels.html new file mode 100644 index 000000000..4af89025e --- /dev/null +++ b/samples/scales/filtering-labels.html @@ -0,0 +1,93 @@ + + + + + Chart with xAxis Filtering + + + + + + +
+ +
+ + + + diff --git a/samples/scales/gridlines.html b/samples/scales/gridlines.html new file mode 100644 index 000000000..76bb1f007 --- /dev/null +++ b/samples/scales/gridlines.html @@ -0,0 +1,69 @@ + + + + + Suggested Min/Max Settings + + + + + + +
+ +
+ + + + diff --git a/samples/line-non-numeric-y.html b/samples/scales/line-non-numeric-y.html similarity index 67% rename from samples/line-non-numeric-y.html rename to samples/scales/line-non-numeric-y.html index c0b74a150..07e319b70 100644 --- a/samples/line-non-numeric-y.html +++ b/samples/scales/line-non-numeric-y.html @@ -3,8 +3,8 @@ Line Chart - - + + + + + +
+ +
+ + + + diff --git a/samples/scales/linear/step-size.html b/samples/scales/linear/step-size.html new file mode 100644 index 000000000..d38f3e1f0 --- /dev/null +++ b/samples/scales/linear/step-size.html @@ -0,0 +1,116 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + + + + + diff --git a/samples/scales/linear/suggested-min-max-settings.html b/samples/scales/linear/suggested-min-max-settings.html new file mode 100644 index 000000000..18059548a --- /dev/null +++ b/samples/scales/linear/suggested-min-max-settings.html @@ -0,0 +1,67 @@ + + + + + Suggested Min/Max Settings + + + + + + +
+ +
+ + + + diff --git a/samples/scales/logarithmic/line-logarithmic.html b/samples/scales/logarithmic/line-logarithmic.html new file mode 100644 index 000000000..2c961abd2 --- /dev/null +++ b/samples/scales/logarithmic/line-logarithmic.html @@ -0,0 +1,97 @@ + + + + + Logarithmic Line Chart + + + + + + +
+ +
+ + + + + diff --git a/samples/scatter-logX.html b/samples/scales/logarithmic/scatter-logX.html similarity index 76% rename from samples/scatter-logX.html rename to samples/scales/logarithmic/scatter-logX.html index 8c7fe2ad0..a4bd577c2 100644 --- a/samples/scatter-logX.html +++ b/samples/scales/logarithmic/scatter-logX.html @@ -3,8 +3,8 @@ Scatter Chart - - + + + + + +
+ +
+ + + + diff --git a/samples/timeScale/combo-time-scale.html b/samples/scales/time/combo-time-scale.html similarity index 52% rename from samples/timeScale/combo-time-scale.html rename to samples/scales/time/combo-time-scale.html index f6c178a7d..873e40d1a 100644 --- a/samples/timeScale/combo-time-scale.html +++ b/samples/scales/time/combo-time-scale.html @@ -4,8 +4,8 @@ Line Chart - Combo Time Scale - - + + - - - -
-
- -
-
- - - - - diff --git a/samples/scatter.html b/samples/scatter.html deleted file mode 100644 index af026e7b1..000000000 --- a/samples/scatter.html +++ /dev/null @@ -1,177 +0,0 @@ - - - - - Scatter Chart - - - - - - -
-
- -
-
- - - - - diff --git a/samples/scatter/scatter-multi-axis.html b/samples/scatter/scatter-multi-axis.html new file mode 100644 index 000000000..aedcd3daa --- /dev/null +++ b/samples/scatter/scatter-multi-axis.html @@ -0,0 +1,139 @@ + + + + + Scatter Chart Multi Axis + + + + + + +
+ +
+ + + + + diff --git a/samples/scatter/scatter.html b/samples/scatter/scatter.html new file mode 100644 index 000000000..b8ac32b8f --- /dev/null +++ b/samples/scatter/scatter.html @@ -0,0 +1,107 @@ + + + + + Scatter Chart + + + + + + +
+ +
+ + + + + diff --git a/samples/tooltip-hooks.html b/samples/tooltip-hooks.html deleted file mode 100644 index 88a660515..000000000 --- a/samples/tooltip-hooks.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - Line Chart - - - - - - -
- -
-
-
- - - - - - - - - diff --git a/samples/tooltips/dataPoints-customTooltips.html b/samples/tooltips/dataPoints-customTooltips.html new file mode 100644 index 000000000..b9e981675 --- /dev/null +++ b/samples/tooltips/dataPoints-customTooltips.html @@ -0,0 +1,125 @@ + + + + + Custom Tooltips using Data Points + + + + + + + +
+ +
+
+
+ + + + diff --git a/samples/tooltips/interaction-modes.html b/samples/tooltips/interaction-modes.html new file mode 100644 index 000000000..17cacf42a --- /dev/null +++ b/samples/tooltips/interaction-modes.html @@ -0,0 +1,125 @@ + + + + + Tooltip Interaction Modes + + + + + + +
+
+ + + + diff --git a/samples/tooltips/line-customTooltips.html b/samples/tooltips/line-customTooltips.html new file mode 100644 index 000000000..97f543b26 --- /dev/null +++ b/samples/tooltips/line-customTooltips.html @@ -0,0 +1,165 @@ + + + + + Line Chart with Custom Tooltips + + + + + + +
+ +
+ + + + diff --git a/samples/tooltips/pie-customTooltips.html b/samples/tooltips/pie-customTooltips.html new file mode 100644 index 000000000..4eedd6a3a --- /dev/null +++ b/samples/tooltips/pie-customTooltips.html @@ -0,0 +1,146 @@ + + + + + Pie Chart with Custom Tooltips + + + + + + + +
+ +
+ +
+
+
+ + + + + diff --git a/samples/tooltips/position-modes.html b/samples/tooltips/position-modes.html new file mode 100644 index 000000000..696584b06 --- /dev/null +++ b/samples/tooltips/position-modes.html @@ -0,0 +1,86 @@ + + + + + Tooltip Interaction Modes + + + + + + +
+
+ + + + diff --git a/samples/tooltips/tooltip-callbacks.html b/samples/tooltips/tooltip-callbacks.html new file mode 100644 index 000000000..590edd1a2 --- /dev/null +++ b/samples/tooltips/tooltip-callbacks.html @@ -0,0 +1,107 @@ + + + + + Tooltip Hooks + + + + + + +
+ +
+ + + + diff --git a/samples/utils.js b/samples/utils.js new file mode 100644 index 000000000..34965ce66 --- /dev/null +++ b/samples/utils.js @@ -0,0 +1,13 @@ +window.chartColors = { + red: 'rgb(255, 99, 132)', + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(75, 192, 192)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(231,233,237)' +}; + +window.randomScalingFactor = function() { + return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100); +} \ No newline at end of file diff --git a/src/chart.js b/src/chart.js index a12890aa0..2c5e62826 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,9 +12,11 @@ require('./core/core.datasetController')(Chart); require('./core/core.layoutService')(Chart); require('./core/core.scaleService')(Chart); require('./core/core.plugin.js')(Chart); +require('./core/core.ticks.js')(Chart); require('./core/core.scale')(Chart); require('./core/core.title')(Chart); require('./core/core.legend')(Chart); +require('./core/core.interaction')(Chart); require('./core/core.tooltip')(Chart); require('./elements/element.arc')(Chart); diff --git a/src/charts/Chart.Radar.js b/src/charts/Chart.Radar.js index 1648e9fac..d17bd5d81 100644 --- a/src/charts/Chart.Radar.js +++ b/src/charts/Chart.Radar.js @@ -3,7 +3,6 @@ module.exports = function(Chart) { Chart.Radar = function(context, config) { - config.options = Chart.helpers.configMerge({aspectRatio: 1}, config.options); config.type = 'radar'; return new Chart(context, config); diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index d9fa7bfeb..e9a85aabd 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -69,31 +69,29 @@ module.exports = function(Chart) { var custom = rectangle.custom || {}; var dataset = me.getDataset(); - helpers.extend(rectangle, { - // Utility - _xScale: xScale, - _yScale: yScale, - _datasetIndex: me.index, - _index: index, + rectangle._xScale = xScale; + rectangle._yScale = yScale; + rectangle._datasetIndex = me.index; + rectangle._index = index; - // Desired view properties - _model: { - x: me.calculateBarX(index, me.index), - y: reset ? scaleBase : me.calculateBarY(index, me.index), + var ruler = me.getRuler(index); + rectangle._model = { + x: me.calculateBarX(index, me.index, ruler), + y: reset ? scaleBase : me.calculateBarY(index, me.index), - // Tooltip - label: me.chart.data.labels[index], - datasetLabel: dataset.label, + // Tooltip + label: me.chart.data.labels[index], + datasetLabel: dataset.label, + + // Appearance + base: reset ? scaleBase : me.calculateBarBase(me.index, index), + width: me.calculateBarWidth(ruler), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), + borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, + borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) + }; - // Appearance - base: reset ? scaleBase : me.calculateBarBase(me.index, index), - width: me.calculateBarWidth(index), - backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), - borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, - borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), - borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) - } - }); rectangle.pivot(); }, @@ -160,12 +158,11 @@ module.exports = function(Chart) { }; }, - calculateBarWidth: function(index) { + calculateBarWidth: function(ruler) { var xScale = this.getScaleForId(this.getMeta().xAxisID); if (xScale.options.barThickness) { return xScale.options.barThickness; } - var ruler = this.getRuler(index); return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth; }, @@ -184,13 +181,11 @@ module.exports = function(Chart) { return barIndex; }, - calculateBarX: function(index, datasetIndex) { + calculateBarX: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); var barIndex = me.getBarIndex(datasetIndex); - - var ruler = me.getRuler(index); var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; @@ -242,12 +237,16 @@ module.exports = function(Chart) { draw: function(ease) { var me = this; var easingDecimal = ease || 1; - helpers.each(me.getMeta().data, function(rectangle, index) { - var d = me.getDataset().data[index]; + var metaData = me.getMeta().data; + var dataset = me.getDataset(); + var i, len; + + for (i = 0, len = metaData.length; i < len; ++i) { + var d = dataset.data[i]; if (d !== null && d !== undefined && !isNaN(d)) { - rectangle.transition(easingDecimal).draw(); + metaData[i].transition(easingDecimal).draw(); } - }, me); + } }, setHoverStyle: function(rectangle) { @@ -342,103 +341,84 @@ module.exports = function(Chart) { var dataset = me.getDataset(); var rectangleElementOptions = me.chart.options.elements.rectangle; - helpers.extend(rectangle, { - // Utility - _xScale: xScale, - _yScale: yScale, - _datasetIndex: me.index, - _index: index, + rectangle._xScale = xScale; + rectangle._yScale = yScale; + rectangle._datasetIndex = me.index; + rectangle._index = index; - // Desired view properties - _model: { - x: reset ? scaleBase : me.calculateBarX(index, me.index), - y: me.calculateBarY(index, me.index), + var ruler = me.getRuler(index); + rectangle._model = { + x: reset ? scaleBase : me.calculateBarX(index, me.index), + y: me.calculateBarY(index, me.index, ruler), - // Tooltip - label: me.chart.data.labels[index], - datasetLabel: dataset.label, + // Tooltip + label: me.chart.data.labels[index], + datasetLabel: dataset.label, - // Appearance - base: reset ? scaleBase : me.calculateBarBase(me.index, index), - height: me.calculateBarHeight(index), - backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), - borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, - borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), - borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) - }, + // Appearance + base: reset ? scaleBase : me.calculateBarBase(me.index, index), + height: me.calculateBarHeight(ruler), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), + borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, + borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) + }; + rectangle.draw = function() { + var ctx = this._chart.ctx; + var vm = this._view; - draw: function() { - var ctx = this._chart.ctx; - var vm = this._view; + var halfHeight = vm.height / 2, + topY = vm.y - halfHeight, + bottomY = vm.y + halfHeight, + right = vm.base - (vm.base - vm.x), + halfStroke = vm.borderWidth / 2; - var halfHeight = vm.height / 2, - topY = vm.y - halfHeight, - bottomY = vm.y + halfHeight, - right = vm.base - (vm.base - vm.x), - halfStroke = vm.borderWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - topY += halfStroke; - bottomY -= halfStroke; - right += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [vm.base, bottomY], - [vm.base, topY], - [right, topY], - [right, bottomY] - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); - if (startCorner === -1) { - startCorner = 0; - } - - function cornerAt(cornerIndex) { - return corners[(startCorner + cornerIndex) % 4]; - } - - // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); - for (var i = 1; i < 4; i++) { - ctx.lineTo.apply(ctx, cornerAt(i)); - } - - ctx.fill(); - if (vm.borderWidth) { - ctx.stroke(); - } - }, - - inRange: function(mouseX, mouseY) { - var vm = this._view; - var inRange = false; - - if (vm) { - if (vm.x < vm.base) { - inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base); - } else { - inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x); - } - } - - return inRange; + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (vm.borderWidth) { + topY += halfStroke; + bottomY -= halfStroke; + right += halfStroke; } - }); + + ctx.beginPath(); + + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; + + // Corner points, from bottom-left to bottom-right clockwise + // | 1 2 | + // | 0 3 | + var corners = [ + [vm.base, bottomY], + [vm.base, topY], + [right, topY], + [right, bottomY] + ]; + + // Find first (starting) corner with fallback to 'bottom' + var borders = ['bottom', 'left', 'top', 'right']; + var startCorner = borders.indexOf(vm.borderSkipped, 0); + if (startCorner === -1) { + startCorner = 0; + } + + function cornerAt(cornerIndex) { + return corners[(startCorner + cornerIndex) % 4]; + } + + // Draw rectangle from 'startCorner' + ctx.moveTo.apply(ctx, cornerAt(0)); + for (var i = 1; i < 4; i++) { + ctx.lineTo.apply(ctx, cornerAt(i)); + } + + ctx.fill(); + if (vm.borderWidth) { + ctx.stroke(); + } + }; rectangle.pivot(); }, @@ -505,13 +485,12 @@ module.exports = function(Chart) { }; }, - calculateBarHeight: function(index) { + calculateBarHeight: function(ruler) { var me = this; var yScale = me.getScaleForId(me.getMeta().yAxisID); if (yScale.options.barThickness) { return yScale.options.barThickness; } - var ruler = me.getRuler(index); return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight; }, @@ -548,13 +527,11 @@ module.exports = function(Chart) { return xScale.getPixelForValue(value); }, - calculateBarY: function(index, datasetIndex) { + calculateBarY: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); var barIndex = me.getBarIndex(datasetIndex); - - var ruler = me.getRuler(index); var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0; diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index b1c6474e0..abe807b3b 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -31,7 +31,7 @@ module.exports = function(Chart) { label: function(tooltipItem, data) { var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; var dataPoint = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; - return datasetLabel + ': (' + dataPoint.x + ', ' + dataPoint.y + ', ' + dataPoint.r + ')'; + return datasetLabel + ': (' + tooltipItem.xLabel + ', ' + tooltipItem.yLabel + ', ' + dataPoint.r + ')'; } } } diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index e0f378dae..3dcd33e9f 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -102,7 +102,19 @@ module.exports = function(Chart) { return ''; }, label: function(tooltipItem, data) { - return data.labels[tooltipItem.index] + ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + var dataLabel = data.labels[tooltipItem.index]; + var value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + + if (helpers.isArray(dataLabel)) { + // show value on first line of multiline label + // need to clone because we are changing the value + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + + return dataLabel; } } } diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index bfc061af2..59d097b1b 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -34,19 +34,6 @@ module.exports = function(Chart) { dataElementType: Chart.elements.Point, - addElementAndReset: function(index) { - var me = this; - var options = me.chart.options; - var meta = me.getMeta(); - - Chart.DatasetController.prototype.addElementAndReset.call(me, index); - - // Make sure bezier control points are updated - if (lineEnabled(me.getDataset(), options) && meta.dataset._model.tension !== 0) { - me.updateBezierControlPoints(); - } - }, - update: function(reset) { var me = this; var meta = me.getMeta(); @@ -78,7 +65,7 @@ module.exports = function(Chart) { // Appearance // The default behavior of lines is to break at null values, according // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 - // This option gives linse the ability to span gaps + // This option gives lines the ability to span gaps spanGaps: dataset.spanGaps ? dataset.spanGaps : options.spanGaps, tension: custom.tension ? custom.tension : helpers.getValueOrDefault(dataset.lineTension, lineElementOptions.tension), backgroundColor: custom.backgroundColor ? custom.backgroundColor : (dataset.backgroundColor || lineElementOptions.backgroundColor), diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index b27daf417..78234ea59 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -32,11 +32,11 @@ module.exports = function(Chart) { if (datasets.length) { for (var i = 0; i < datasets[0].data.length; ++i) { - text.push('
  • '); + text.push('
  • '); if (labels[i]) { text.push(labels[i]); } - text.push('
  • '); + text.push(''); } } diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 3c050595a..cd62da476 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -5,6 +5,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; Chart.defaults.radar = { + aspectRatio: 1, scale: { type: 'radialLinear' }, @@ -23,13 +24,6 @@ module.exports = function(Chart) { linkScales: helpers.noop, - addElementAndReset: function(index) { - Chart.DatasetController.prototype.addElementAndReset.call(this, index); - - // Make sure bezier control points are updated - this.updateBezierControlPoints(); - }, - update: function(reset) { var me = this; var meta = me.getMeta(); @@ -78,7 +72,6 @@ module.exports = function(Chart) { me.updateElement(point, index, reset); }, me); - // Update bezier control points me.updateBezierControlPoints(); }, diff --git a/src/core/core.animation.js b/src/core/core.animation.js index 7ccb37493..dcb9c2ee0 100644 --- a/src/core/core.animation.js +++ b/src/core/core.animation.js @@ -27,6 +27,14 @@ module.exports = function(Chart) { animations: [], dropFrames: 0, request: null, + + /** + * @function Chart.animationService.addAnimation + * @param chartInstance {ChartController} the chart to animate + * @param animationObject {IAnimation} the animation that we will animate + * @param duration {Number} length of animation in ms + * @param lazy {Boolean} if true, the chart is not marked as animating to enable more responsive interactions + */ addAnimation: function(chartInstance, animationObject, duration, lazy) { var me = this; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 30ba13418..bdc98ee04 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,6 +3,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; @@ -14,39 +15,228 @@ module.exports = function(Chart) { Chart.controllers = {}; /** - * @class Chart.Controller - * The main controller of a chart. + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * TODO(SB) Move this method in the upcoming core.platform class. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns {Number} Size in pixels or undefined if unknown. */ - Chart.Controller = function(instance) { + function readUsedSize(element, property) { + var value = helpers.getStyle(element, property); + var matches = value && value.match(/(\d+)px/); + return matches? Number(matches[1]) : undefined; + } - this.chart = instance; - this.config = instance.config; - this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {}); - this.id = helpers.uid(); + /** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + * TODO(SB) Move this method in the upcoming core.platform class. + */ + function initCanvas(canvas, config) { + var style = canvas.style; - Object.defineProperty(this, 'data', { - get: function() { - return this.config.data; + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + var renderHeight = canvas.getAttribute('height'); + var renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas._chartjs = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + + if (renderWidth === null || renderWidth === '') { + var displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (renderHeight === null || renderHeight === '') { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (config.options.aspectRatio || 2); + } else { + var displayHeight = readUsedSize(canvas, 'height'); + if (displayWidth !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; + } + + /** + * Restores the canvas initial state, such as render/display sizes and style. + * TODO(SB) Move this method in the upcoming core.platform class. + */ + function releaseCanvas(canvas) { + if (!canvas._chartjs) { + return; + } + + var initial = canvas._chartjs.initial; + ['height', 'width'].forEach(function(prop) { + var value = initial[prop]; + if (value === undefined || value === null) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); } }); - // Add the chart instance to the global namespace - Chart.instances[this.id] = this; + helpers.each(initial.style || {}, function(value, key) { + canvas.style[key] = value; + }); - if (this.options.responsive) { - // Silent resize before chart draws - this.resize(true); + // The canvas render size might have been changed (and thus the state stack discarded), + // we can't use save() and restore() to restore the initial state. So make sure that at + // least the canvas context is reset to the default state by setting the canvas width. + // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html + canvas.width = canvas.width; + + delete canvas._chartjs; + } + + /** + * TODO(SB) Move this method in the upcoming core.platform class. + */ + function acquireContext(item, config) { + if (typeof item === 'string') { + item = document.getElementById(item); + } else if (item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; } - this.initialize(); + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } - return this; + if (item instanceof HTMLCanvasElement) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + var context = item.getContext && item.getContext('2d'); + if (context instanceof CanvasRenderingContext2D) { + initCanvas(item, config); + return context; + } + } + + return null; + } + + /** + * Initializes the given config with global and chart default values. + */ + function initConfig(config) { + config = config || {}; + + // Do NOT use configMerge() for the data object because this method merges arrays + // and so would change references to labels and datasets, preventing data updates. + var data = config.data = config.data || {}; + data.datasets = data.datasets || []; + data.labels = data.labels || []; + + config.options = helpers.configMerge( + Chart.defaults.global, + Chart.defaults[config.type], + config.options || {}); + + return config; + } + + /** + * @class Chart.Controller + * The main controller of a chart. + */ + Chart.Controller = function(item, config, instance) { + var me = this; + + config = initConfig(config); + + var context = acquireContext(item, config); + var canvas = context && context.canvas; + var height = canvas && canvas.height; + var width = canvas && canvas.width; + + instance.ctx = context; + instance.canvas = canvas; + instance.config = config; + instance.width = width; + instance.height = height; + instance.aspectRatio = height? width / height : null; + + me.id = helpers.uid(); + me.chart = instance; + me.config = config; + me.options = config.options; + me._bufferedRender = false; + + // Add the chart instance to the global namespace + Chart.instances[me.id] = me; + + Object.defineProperty(me, 'data', { + get: function() { + return me.config.data; + } + }); + + if (!context || !canvas) { + // The given item is not a compatible context2d element, let's return before finalizing + // the chart initialization but after setting basic chart / controller properties that + // can help to figure out that the chart is not valid (e.g chart.canvas !== null); + // https://github.com/chartjs/Chart.js/issues/2807 + console.error("Failed to create chart: can't acquire context from the given item"); + return me; + } + + helpers.retinaScale(instance); + + // Responsiveness is currently based on the use of an iframe, however this method causes + // performance issues and could be troublesome when used with ad blockers. So make sure + // that the user is still able to create a chart without iframe when responsive is false. + // See https://github.com/chartjs/Chart.js/issues/2210 + if (me.options.responsive) { + helpers.addResizeListener(canvas.parentNode, function() { + me.resize(); + }); + + // Initial resize before chart draws (must be silent to preserve initial animations). + me.resize(true); + } + + me.initialize(); + + return me; }; helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ { - initialize: function() { var me = this; + // Before init plugin notification Chart.plugins.notify('beforeInit', [me]); @@ -74,7 +264,7 @@ module.exports = function(Chart) { }, stop: function() { - // Stops any current animation loop occuring + // Stops any current animation loop occurring Chart.animationService.cancelAnimation(this); return this; }, @@ -82,19 +272,23 @@ module.exports = function(Chart) { resize: function(silent) { var me = this; var chart = me.chart; + var options = me.options; var canvas = chart.canvas; - var newWidth = helpers.getMaximumWidth(canvas); - var aspectRatio = chart.aspectRatio; - var newHeight = (me.options.maintainAspectRatio && isNaN(aspectRatio) === false && isFinite(aspectRatio) && aspectRatio !== 0) ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas); + var aspectRatio = (options.maintainAspectRatio && chart.aspectRatio) || null; - var sizeChanged = chart.width !== newWidth || chart.height !== newHeight; + // the canvas render width and height will be casted to integers so make sure that + // the canvas display style uses the same integer values to avoid blurring effect. + var newWidth = Math.floor(helpers.getMaximumWidth(canvas)); + var newHeight = Math.floor(aspectRatio? newWidth / aspectRatio : helpers.getMaximumHeight(canvas)); - if (!sizeChanged) { - return me; + if (chart.width === newWidth && chart.height === newHeight) { + return; } canvas.width = chart.width = newWidth; canvas.height = chart.height = newHeight; + canvas.style.width = newWidth + 'px'; + canvas.style.height = newHeight + 'px'; helpers.retinaScale(chart); @@ -111,8 +305,6 @@ module.exports = function(Chart) { me.stop(); me.update(me.options.responsiveAnimationDuration); } - - return me; }, ensureScalesHaveIDs: function() { @@ -222,6 +414,11 @@ module.exports = function(Chart) { return newControllers; }, + /** + * Reset the elements of all datasets + * @method resetElements + * @private + */ resetElements: function() { var me = this; helpers.each(me.data.datasets, function(dataset, datasetIndex) { @@ -229,6 +426,15 @@ module.exports = function(Chart) { }, me); }, + /** + * Resets the chart back to it's state before the initial animation + * @method reset + */ + reset: function() { + this.resetElements(); + this.tooltip.initialize(); + }, + update: function(animationDuration, lazy) { var me = this; Chart.plugins.notify('beforeUpdate', [me]); @@ -246,7 +452,7 @@ module.exports = function(Chart) { Chart.layoutService.update(me, me.chart.width, me.chart.height); - // Apply changes to the dataets that require the scales to have been calculated i.e BorderColor chages + // Apply changes to the datasets that require the scales to have been calculated i.e BorderColor changes Chart.plugins.notify('afterScaleUpdate', [me]); // Can only reset the new controllers after the scales have been updated @@ -259,7 +465,14 @@ module.exports = function(Chart) { // Do this before render so that any plugins that need final scale updates can use it Chart.plugins.notify('afterUpdate', [me]); - me.render(animationDuration, lazy); + if (me._bufferedRender) { + me._bufferedRequest = { + lazy: lazy, + duration: animationDuration + }; + } else { + me.render(animationDuration, lazy); + } }, /** @@ -371,125 +584,28 @@ module.exports = function(Chart) { // Get the single element that was clicked on // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw getElementAtEvent: function(e) { - var me = this; - var eventPosition = helpers.getRelativePosition(e, me.chart); - var elementsArray = []; - - helpers.each(me.data.datasets, function(dataset, datasetIndex) { - if (me.isDatasetVisible(datasetIndex)) { - var meta = me.getDatasetMeta(datasetIndex); - helpers.each(meta.data, function(element) { - if (element.inRange(eventPosition.x, eventPosition.y)) { - elementsArray.push(element); - return elementsArray; - } - }); - } - }); - - return elementsArray.slice(0, 1); + return Chart.Interaction.modes.single(this, e); }, getElementsAtEvent: function(e) { - var me = this; - var eventPosition = helpers.getRelativePosition(e, me.chart); - var elementsArray = []; - - var found = function() { - if (me.data.datasets) { - for (var i = 0; i < me.data.datasets.length; i++) { - var meta = me.getDatasetMeta(i); - if (me.isDatasetVisible(i)) { - for (var j = 0; j < meta.data.length; j++) { - if (meta.data[j].inRange(eventPosition.x, eventPosition.y)) { - return meta.data[j]; - } - } - } - } - } - }.call(me); - - if (!found) { - return elementsArray; - } - - helpers.each(me.data.datasets, function(dataset, datasetIndex) { - if (me.isDatasetVisible(datasetIndex)) { - var meta = me.getDatasetMeta(datasetIndex), - element = meta.data[found._index]; - if (element && !element._view.skip) { - elementsArray.push(element); - } - } - }, me); - - return elementsArray; + return Chart.Interaction.modes.label(this, e, {intersect: true}); }, getElementsAtXAxis: function(e) { - var me = this; - var eventPosition = helpers.getRelativePosition(e, me.chart); - var elementsArray = []; - - var found = function() { - if (me.data.datasets) { - for (var i = 0; i < me.data.datasets.length; i++) { - var meta = me.getDatasetMeta(i); - if (me.isDatasetVisible(i)) { - for (var j = 0; j < meta.data.length; j++) { - if (meta.data[j].inLabelRange(eventPosition.x, eventPosition.y)) { - return meta.data[j]; - } - } - } - } - } - }.call(me); - - if (!found) { - return elementsArray; - } - - helpers.each(me.data.datasets, function(dataset, datasetIndex) { - if (me.isDatasetVisible(datasetIndex)) { - var meta = me.getDatasetMeta(datasetIndex); - var index = helpers.findIndex(meta.data, function(it) { - return found._model.x === it._model.x; - }); - if (index !== -1 && !meta.data[index]._view.skip) { - elementsArray.push(meta.data[index]); - } - } - }, me); - - return elementsArray; + return Chart.Interaction.modes['x-axis'](this, e, {intersect: true}); }, - getElementsAtEventForMode: function(e, mode) { - var me = this; - switch (mode) { - case 'single': - return me.getElementAtEvent(e); - case 'label': - return me.getElementsAtEvent(e); - case 'dataset': - return me.getDatasetAtEvent(e); - case 'x-axis': - return me.getElementsAtXAxis(e); - default: - return e; + getElementsAtEventForMode: function(e, mode, options) { + var method = Chart.Interaction.modes[mode]; + if (typeof method === 'function') { + return method(this, e, options); } + + return []; }, getDatasetAtEvent: function(e) { - var elementsArray = this.getElementAtEvent(e); - - if (elementsArray.length > 0) { - elementsArray = this.getDatasetMeta(elementsArray[0]._datasetIndex).data; - } - - return elementsArray; + return Chart.Interaction.modes.dataset(this, e); }, getDatasetMeta: function(datasetIndex) { @@ -539,24 +655,28 @@ module.exports = function(Chart) { destroy: function() { var me = this; - me.stop(); - me.clear(); - helpers.unbindEvents(me, me.events); - helpers.removeResizeListener(me.chart.canvas.parentNode); - - // Reset canvas height/width attributes var canvas = me.chart.canvas; - canvas.width = me.chart.width; - canvas.height = me.chart.height; + var meta, i, ilen; - // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here - if (me.chart.originalDevicePixelRatio !== undefined) { - me.chart.ctx.scale(1 / me.chart.originalDevicePixelRatio, 1 / me.chart.originalDevicePixelRatio); + me.stop(); + + // dataset controllers need to cleanup associated data + for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + meta = me.getDatasetMeta(i); + if (meta.controller) { + meta.controller.destroy(); + meta.controller = null; + } } - // Reset to the old style since it may have been changed by the device pixel ratio changes - canvas.style.width = me.chart.originalCanvasStyleWidth; - canvas.style.height = me.chart.originalCanvasStyleHeight; + if (canvas) { + helpers.unbindEvents(me, me.events); + helpers.removeResizeListener(canvas.parentNode); + helpers.clear(me.chart); + releaseCanvas(canvas); + me.chart.canvas = null; + me.chart.ctx = null; + } Chart.plugins.notify('destroy', [me]); @@ -575,6 +695,7 @@ module.exports = function(Chart) { _data: me.data, _options: me.options.tooltips }, me); + me.tooltip.initialize(); }, bindEvents: function() { @@ -588,20 +709,6 @@ module.exports = function(Chart) { var method = enabled? 'setHoverStyle' : 'removeHoverStyle'; var element, i, ilen; - switch (mode) { - case 'single': - elements = [elements[0]]; - break; - case 'label': - case 'dataset': - case 'x-axis': - // elements = elements; - break; - default: - // unsupported mode - return; - } - for (i=0, ilen=elements.length; i 0) { + return; + } + + arrayEvents.forEach(function(key) { + delete array[key]; + }); + + delete array._chartjs; + } // Base class for all dataset controllers (line, bar, etc) Chart.DatasetController = function(chart, datasetIndex) { @@ -65,6 +135,15 @@ module.exports = function(Chart) { this.update(true); }, + /** + * @private + */ + destroy: function() { + if (this._data) { + unlistenArrayEvents(this._data, this); + } + }, + createMetaDataset: function() { var me = this; var type = me.datasetElementType; @@ -92,45 +171,50 @@ module.exports = function(Chart) { var i, ilen; for (i=0, ilen=data.length; i numMetaData) { - // Add new elements - for (var index = numMetaData; index < numData; ++index) { - this.addElementAndReset(index); + // In order to correctly handle data addition/deletion animation (an thus simulate + // real-time charts), we need to monitor these data modifications and synchronize + // the internal meta data accordingly. + if (me._data !== data) { + if (me._data) { + // This case happens when the user replaced the data array instance. + unlistenArrayEvents(me._data, me); } + + listenArrayEvents(data, me); + me._data = data; } + + // Re-sync meta data in case the user replaced the data array or if we missed + // any updates and so make sure that we handle number of datapoints changing. + me.resyncElements(); }, - update: noop, + update: helpers.noop, draw: function(ease) { var easingDecimal = ease || 1; - helpers.each(this.getMeta().data, function(element) { - element.transition(easingDecimal).draw(); - }); + var i, len; + var metaData = this.getMeta().data; + for (i = 0, len = metaData.length; i < len; ++i) { + metaData[i].transition(easingDecimal).draw(); + } }, removeHoverStyle: function(element, elementOpts) { @@ -156,8 +240,69 @@ module.exports = function(Chart) { model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : valueOrDefault(dataset.hoverBackgroundColor, index, getHoverColor(model.backgroundColor)); model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : valueOrDefault(dataset.hoverBorderColor, index, getHoverColor(model.borderColor)); model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : valueOrDefault(dataset.hoverBorderWidth, index, model.borderWidth); - } + }, + /** + * @private + */ + resyncElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data; + var numMeta = meta.data.length; + var numData = data.length; + + if (numData < numMeta) { + meta.data.splice(numData, numMeta - numData); + } else if (numData > numMeta) { + me.insertElements(numMeta, numData - numMeta); + } + }, + + /** + * @private + */ + insertElements: function(start, count) { + for (var i=0; i 0) { + items = chart.getDatasetMeta(items[0]._datasetIndex).data; + } + + return items; + }, + + /** + * @function Chart.Interaction.modes.x-axis + * @deprecated since version 2.4.0. Use index mode and intersect == true + */ + 'x-axis': function(chart, e) { + return indexMode(chart, e, true); + }, + + /** + * Point mode returns all elements that hit test based on the event position + * of the event + * @function Chart.Interaction.modes.intersect + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + point: function(chart, e) { + var position = helpers.getRelativePosition(e, chart.chart); + return getIntersectItems(chart, position); + }, + + /** + * nearest mode returns the element closest to the point + * @function Chart.Interaction.modes.intersect + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + nearest: function(chart, e, options) { + var position = helpers.getRelativePosition(e, chart.chart); + var nearestItems = getNearestItems(chart, position, options.intersect); + + // We have multiple items at the same distance from the event. Now sort by smallest + if (nearestItems.length > 1) { + nearestItems.sort(function(a, b) { + var sizeA = a.getArea(); + var sizeB = b.getArea(); + var ret = sizeA - sizeB; + + if (ret === 0) { + // if equal sort by dataset index + ret = a._datasetIndex - b._datasetIndex; + } + + return ret; + }); + } + + // Return only 1 item + return nearestItems.slice(0, 1); + }, + + /** + * x mode returns the elements that hit-test at the current x coordinate + * @function Chart.Interaction.modes.x + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + x: function(chart, e, options) { + var position = helpers.getRelativePosition(e, chart.chart); + var items = []; + var intersectsItem = false; + + parseVisibleItems(chart, function(element) { + if (element.inXRange(position.x)) { + items.push(element); + } + + if (element.inRange(position.x, position.y)) { + intersectsItem = true; + } + }); + + // If we want to trigger on an intersect and we don't have any items + // that intersect the position, return nothing + if (options.intersect && !intersectsItem) { + items = []; + } + return items; + }, + + /** + * y mode returns the elements that hit-test at the current y coordinate + * @function Chart.Interaction.modes.y + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + y: function(chart, e, options) { + var position = helpers.getRelativePosition(e, chart.chart); + var items = []; + var intersectsItem = false; + + parseVisibleItems(chart, function(element) { + if (element.inYRange(position.y)) { + items.push(element); + } + + if (element.inRange(position.x, position.y)) { + intersectsItem = true; + } + }); + + // If we want to trigger on an intersect and we don't have any items + // that intersect the position, return nothing + if (options.intersect && !intersectsItem) { + items = []; + } + return items; + } + } + }; +}; diff --git a/src/core/core.js b/src/core/core.js index 577db878e..cea3cf30b 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -3,63 +3,9 @@ module.exports = function() { // Occupy the global variable of Chart, and create a simple base class - var Chart = function(context, config) { - var me = this; - var helpers = Chart.helpers; - me.config = config || { - data: { - datasets: [] - } - }; - - // Support a jQuery'd canvas element - if (context.length && context[0].getContext) { - context = context[0]; - } - - // Support a canvas domnode - if (context.getContext) { - context = context.getContext('2d'); - } - - me.ctx = context; - me.canvas = context.canvas; - - context.canvas.style.display = context.canvas.style.display || 'block'; - - // Figure out what the size of the chart will be. - // If the canvas has a specified width and height, we use those else - // we look to see if the canvas node has a CSS width and height. - // If there is still no height, fill the parent container - me.width = context.canvas.width || parseInt(helpers.getStyle(context.canvas, 'width'), 10) || helpers.getMaximumWidth(context.canvas); - me.height = context.canvas.height || parseInt(helpers.getStyle(context.canvas, 'height'), 10) || helpers.getMaximumHeight(context.canvas); - - me.aspectRatio = me.width / me.height; - - if (isNaN(me.aspectRatio) || isFinite(me.aspectRatio) === false) { - // If the canvas has no size, try and figure out what the aspect ratio will be. - // Some charts prefer square canvases (pie, radar, etc). If that is specified, use that - // else use the canvas default ratio of 2 - me.aspectRatio = config.aspectRatio !== undefined ? config.aspectRatio : 2; - } - - // Store the original style of the element so we can set it back - me.originalCanvasStyleWidth = context.canvas.style.width; - me.originalCanvasStyleHeight = context.canvas.style.height; - - // High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - helpers.retinaScale(me); - me.controller = new Chart.Controller(me); - - // Always bind this so that if the responsive state changes we still work - helpers.addResizeListener(context.canvas.parentNode, function() { - if (me.controller && me.controller.config.options.responsive) { - me.controller.resize(); - } - }); - - return me.controller ? me.controller : me; - + var Chart = function(item, config) { + this.controller = new Chart.Controller(item, config, this); + return this.controller; }; // Globally expose the defaults to allow for user updating/changing @@ -71,7 +17,8 @@ module.exports = function() { events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], hover: { onHover: null, - mode: 'single', + mode: 'nearest', + intersect: true, animationDuration: 400 }, onClick: null, @@ -106,5 +53,4 @@ module.exports = function() { Chart.Chart = Chart; return Chart; - }; diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index 7336deac0..220a25344 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -32,8 +32,26 @@ module.exports = function(Chart) { return; } - var xPadding = 0; - var yPadding = 0; + var layoutOptions = chartInstance.options.layout; + var padding = layoutOptions ? layoutOptions.padding : null; + + var leftPadding = 0; + var rightPadding = 0; + var topPadding = 0; + var bottomPadding = 0; + + if (!isNaN(padding)) { + // options.layout.padding is a number. assign to all + leftPadding = padding; + rightPadding = padding; + topPadding = padding; + bottomPadding = padding; + } else { + leftPadding = padding.left || 0; + rightPadding = padding.right || 0; + topPadding = padding.top || 0; + bottomPadding = padding.bottom || 0; + } var leftBoxes = helpers.where(chartInstance.boxes, function(box) { return box.options.position === 'left'; @@ -99,8 +117,8 @@ module.exports = function(Chart) { // 9. Tell any axes that overlay the chart area the positions of the chart area // Step 1 - var chartWidth = width - (2 * xPadding); - var chartHeight = height - (2 * yPadding); + var chartWidth = width - leftPadding - rightPadding; + var chartHeight = height - topPadding - bottomPadding; var chartAreaWidth = chartWidth / 2; // min 50% var chartAreaHeight = chartHeight / 2; // min 50% @@ -140,10 +158,10 @@ module.exports = function(Chart) { // be if the axes are drawn at their minimum sizes. // Steps 5 & 6 - var totalLeftBoxesWidth = xPadding; - var totalRightBoxesWidth = xPadding; - var totalTopBoxesHeight = yPadding; - var totalBottomBoxesHeight = yPadding; + var totalLeftBoxesWidth = leftPadding; + var totalRightBoxesWidth = rightPadding; + var totalTopBoxesHeight = topPadding; + var totalBottomBoxesHeight = bottomPadding; // Function to fit a box function fitBox(box) { @@ -213,10 +231,10 @@ module.exports = function(Chart) { 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 = xPadding; - totalRightBoxesWidth = xPadding; - totalTopBoxesHeight = yPadding; - totalBottomBoxesHeight = yPadding; + totalLeftBoxesWidth = leftPadding; + totalRightBoxesWidth = rightPadding; + totalTopBoxesHeight = topPadding; + totalBottomBoxesHeight = bottomPadding; helpers.each(leftBoxes, function(box) { totalLeftBoxesWidth += box.width; @@ -265,13 +283,13 @@ module.exports = function(Chart) { } // Step 7 - Position the boxes - var left = xPadding; - var top = yPadding; + var left = leftPadding; + var top = topPadding; function placeBox(box) { if (box.isHorizontal()) { - box.left = box.options.fullWidth ? xPadding : totalLeftBoxesWidth; - box.right = box.options.fullWidth ? width - xPadding : totalLeftBoxesWidth + maxChartAreaWidth; + box.left = box.options.fullWidth ? leftPadding : totalLeftBoxesWidth; + box.right = box.options.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; box.top = top; box.bottom = top + box.height; diff --git a/src/core/core.legend.js b/src/core/core.legend.js index e82c06ea3..9ea5787db 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -64,6 +64,18 @@ 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; + } + Chart.Legend = Chart.Element.extend({ initialize: function(config) { @@ -76,7 +88,7 @@ module.exports = function(Chart) { this.doughnutMode = false; }, - // These methods are ordered by lifecyle. Utilities then follow. + // 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 @@ -204,11 +216,9 @@ module.exports = function(Chart) { ctx.textBaseline = 'top'; helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = labelOpts.usePointStyle ? - fontSize * Math.sqrt(2) : - labelOpts.boxWidth; - + 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; @@ -236,10 +246,7 @@ module.exports = function(Chart) { var itemHeight = fontSize + vPadding; helpers.each(me.legendItems, function(legendItem, i) { - // If usePointStyle is set, multiple boxWidth by 2 since it represents - // the radius and not truly the width - var boxWidth = labelOpts.usePointStyle ? 2 * labelOpts.boxWidth : labelOpts.boxWidth; - + var boxWidth = getBoxWidth(labelOpts, fontSize); var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; // If too tall, go to new column @@ -280,7 +287,7 @@ module.exports = function(Chart) { return this.options.position === 'top' || this.options.position === 'bottom'; }, - // Actualy draw the legend on the canvas + // Actually draw the legend on the canvas draw: function() { var me = this; var opts = me.options; @@ -308,7 +315,7 @@ module.exports = function(Chart) { ctx.fillStyle = fontColor; // render in correct colour ctx.font = labelFont; - var boxWidth = labelOpts.boxWidth, + var boxWidth = getBoxWidth(labelOpts, fontSize), hitboxes = me.legendHitBoxes; // current position @@ -334,7 +341,7 @@ module.exports = function(Chart) { } if (opts.labels && opts.labels.usePointStyle) { - // Recalulate x and y for drawPoint() because its expecting + // 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; @@ -385,9 +392,7 @@ module.exports = function(Chart) { var itemHeight = fontSize + labelOpts.padding; helpers.each(me.legendItems, function(legendItem, i) { var textWidth = ctx.measureText(legendItem.text).width, - width = labelOpts.usePointStyle ? - fontSize + (fontSize / 2) + textWidth : - boxWidth + (fontSize / 2) + textWidth, + width = boxWidth + (fontSize / 2) + textWidth, x = cursor.x, y = cursor.y; @@ -421,11 +426,17 @@ module.exports = function(Chart) { } }, - // Handle an event + /** + * Handle an event + * @private + * @param e {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) { @@ -453,14 +464,18 @@ module.exports = function(Chart) { // Touching an element if (type === 'click') { opts.onClick.call(me, e, me.legendItems[i]); + changed = true; break; } else if (type === 'mousemove') { opts.onHover.call(me, e, me.legendItems[i]); + changed = true; break; } } } } + + return changed; } }); diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index b675fbe31..fe6ac31f1 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -40,7 +40,7 @@ module.exports = function(Chart) { }, /** - * Remove all registered p^lugins. + * Remove all registered plugins. * @since 2.1.5 */ clear: function() { @@ -57,7 +57,7 @@ module.exports = function(Chart) { }, /** - * Returns all registered plugin intances. + * Returns all registered plugin instances. * @returns {Array} array of plugin objects. * @since 2.1.5 */ diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 123da3184..5f35b999f 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -46,15 +46,13 @@ module.exports = function(Chart) { autoSkipPadding: 0, labelOffset: 0, // We pass through arrays to be rendered as multiline labels, we convert Others to strings here. - callback: function(value) { - return helpers.isArray(value) ? value : '' + value; - } + callback: Chart.Ticks.formatters.values } }; Chart.Scale = Chart.Element.extend({ - // These methods are ordered by lifecyle. Utilities then follow. + // These methods are ordered by lifecycle. Utilities then follow. // Any function defined here is inherited by all scale types. // Any function can be extended by the scale type @@ -169,13 +167,8 @@ module.exports = function(Chart) { convertTicksToLabels: function() { var me = this; // Convert ticks to strings - me.ticks = me.ticks.map(function(numericalTick, index, ticks) { - if (me.options.ticks.userCallback) { - return me.options.ticks.userCallback(numericalTick, index, ticks); - } - return me.options.ticks.callback(numericalTick, index, ticks); - }, - me); + var tickOpts = me.options.ticks; + me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback); }, afterTickToLabelConversion: function() { helpers.callCallback(this.options.afterTickToLabelConversion, [this]); @@ -398,8 +391,8 @@ module.exports = function(Chart) { if (rawValue === null || typeof(rawValue) === 'undefined') { return NaN; } - // isNaN(object) returns true, so make sure NaN is checking for a number - if (typeof(rawValue) === 'number' && isNaN(rawValue)) { + // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values + if (typeof(rawValue) === 'number' && !isFinite(rawValue)) { return NaN; } // If it is in fact an object, dive in one more level @@ -470,7 +463,7 @@ module.exports = function(Chart) { 0); }, - // Actualy draw the scale on the canvas + // Actually draw the scale on the canvas // @param {rectangle} chartArea : the area of the chart to draw full grid lines on draw: function(chartArea) { var me = this; diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js new file mode 100644 index 000000000..f11f1192b --- /dev/null +++ b/src/core/core.ticks.js @@ -0,0 +1,202 @@ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + + /** + * Namespace to hold static tick generation functions + * @namespace Chart.Ticks + */ + Chart.Ticks = { + /** + * 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) { + var minMaxDeltaDivisibleByStepSize = ((generationOptions.max - generationOptions.min) % generationOptions.stepSize) === 0; + if (minMaxDeltaDivisibleByStepSize) { + 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 getValueOrDefault = helpers.getValueOrDefault; + + // 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 = getValueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); + + while (tickVal < dataRange.max) { + ticks.push(tickVal); + + var exp; + var significand; + + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.round(dataRange.minNotZero / Math.pow(10, exp)); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; + } + + if (significand === 10) { + significand = 1; + ++exp; + } + + tickVal = significand * Math.pow(10, exp); + } + + var lastTick = getValueOrDefault(generationOptions.max, tickVal); + ticks.push(lastTick); + + return ticks; + } + }, + + /** + * Namespace to hold formatters for different types of ticks + * @namespace Chart.Ticks.formatters + */ + formatters: { + /** + * Formatter for value labels + * @method Chart.Ticks.formatters.values + * @param value the value to display + * @return {String|Array} the label to display + */ + values: function(value) { + return helpers.isArray(value) ? value : '' + value; + }, + + /** + * Formatter for linear numeric ticks + * @method Chart.Ticks.formatters.linear + * @param tickValue {Number} the value to be formatted + * @param index {Number} the position of the tickValue parameter in the ticks array + * @param ticks {Array} the list of ticks being converted + * @return {String} string representation of the tickValue parameter + */ + linear: function(tickValue, index, ticks) { + // If we have lots of ticks, don't use the ones + var delta = ticks.length > 3 ? ticks[2] - ticks[1] : ticks[1] - ticks[0]; + + // If we have a number like 2.5 as the delta, figure out how many decimal places we need + if (Math.abs(delta) > 1) { + if (tickValue !== Math.floor(tickValue)) { + // not an integer + delta = tickValue - Math.floor(tickValue); + } + } + + var logDelta = helpers.log10(Math.abs(delta)); + var tickString = ''; + + if (tickValue !== 0) { + var numDecimal = -1 * Math.floor(logDelta); + numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places + tickString = tickValue.toFixed(numDecimal); + } else { + tickString = '0'; // never show decimal places for 0 + } + + return tickString; + }, + + logarithmic: function(tickValue, index, ticks) { + var remain = tickValue / (Math.pow(10, Math.floor(helpers.log10(tickValue)))); + + if (tickValue === 0) { + return '0'; + } else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === ticks.length - 1) { + return tickValue.toExponential(); + } + return ''; + } + } + }; +}; diff --git a/src/core/core.title.js b/src/core/core.title.js index 0a8c1a5aa..a7663258a 100644 --- a/src/core/core.title.js +++ b/src/core/core.title.js @@ -28,7 +28,7 @@ module.exports = function(Chart) { me.legendHitBoxes = []; }, - // These methods are ordered by lifecyle. Utilities then follow. + // These methods are ordered by lifecycle. Utilities then follow. beforeUpdate: function() { var chartOpts = this.chart.options; @@ -139,7 +139,7 @@ module.exports = function(Chart) { return pos === 'top' || pos === 'bottom'; }, - // Actualy draw the title block on the canvas + // Actually draw the title block on the canvas draw: function() { var me = this, ctx = me.ctx, @@ -158,7 +158,8 @@ module.exports = function(Chart) { top = me.top, left = me.left, bottom = me.bottom, - right = me.right; + right = me.right, + maxWidth; ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour ctx.font = titleFont; @@ -167,9 +168,11 @@ module.exports = function(Chart) { if (me.isHorizontal()) { titleX = left + ((right - left) / 2); // midpoint of the width titleY = top + ((bottom - top) / 2); // midpoint of the height + maxWidth = right - left; } else { titleX = opts.position === 'left' ? left + (fontSize / 2) : right - (fontSize / 2); titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); } @@ -178,7 +181,7 @@ module.exports = function(Chart) { ctx.rotate(rotation); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText(opts.text, 0, 0); + ctx.fillText(opts.text, 0, 0, maxWidth); ctx.restore(); } } diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 5edfd0075..d99f2302f 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -4,10 +4,20 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + /** + * Helper method to merge the opacity into a color + */ + function mergeOpacity(colorString, opacity) { + var color = helpers.color(colorString); + return color.alpha(opacity * color.alpha()).rgbaString(); + } + Chart.defaults.global.tooltips = { enabled: true, custom: null, - mode: 'single', + mode: 'nearest', + position: 'average', + intersect: true, backgroundColor: 'rgba(0,0,0,0.8)', titleFontStyle: 'bold', titleSpacing: 2, @@ -24,11 +34,10 @@ module.exports = function(Chart) { footerAlign: 'left', yPadding: 6, xPadding: 6, - yAlign: 'center', - xAlign: 'center', caretSize: 5, cornerRadius: 6, multiKeyBackground: '#fff', + displayColors: true, callbacks: { // Args are: (tooltipItems, data) beforeTitle: helpers.noop, @@ -96,40 +105,7 @@ module.exports = function(Chart) { return base; } - function getAveragePosition(elements) { - if (!elements.length) { - return false; - } - - var i, len; - var xPositions = []; - var yPositions = []; - - for (i = 0, len = elements.length; i < len; ++i) { - var el = elements[i]; - if (el && el.hasValue()) { - var pos = el.tooltipPosition(); - xPositions.push(pos.x); - yPositions.push(pos.y); - } - } - - var x = 0, - y = 0; - for (i = 0; i < xPositions.length; ++i) { - if (xPositions[i]) { - x += xPositions[i]; - y += yPositions[i]; - } - } - - return { - x: Math.round(x / xPositions.length), - y: Math.round(y / xPositions.length) - }; - } - - // Private helper to create a tooltip iteam model + // Private helper to create a tooltip item model // @param element : the chart element (point, arc, bar) to create the tooltip item for // @return : new tooltip item function createTooltipItem(element) { @@ -142,59 +118,255 @@ module.exports = function(Chart) { xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '', yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '', index: index, - datasetIndex: datasetIndex + datasetIndex: datasetIndex, + x: element._model.x, + y: element._model.y + }; + } + + /** + * Helper to get the reset model for the tooltip + * @param tooltipOpts {Object} the tooltip options + */ + function getBaseModel(tooltipOpts) { + var globalDefaults = Chart.defaults.global; + var getValueOrDefault = helpers.getValueOrDefault; + + return { + // Positioning + xPadding: tooltipOpts.xPadding, + yPadding: tooltipOpts.yPadding, + xAlign: tooltipOpts.xAlign, + yAlign: tooltipOpts.yAlign, + + // Body + bodyFontColor: tooltipOpts.bodyFontColor, + _bodyFontFamily: getValueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), + _bodyFontStyle: getValueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), + _bodyAlign: tooltipOpts.bodyAlign, + bodyFontSize: getValueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), + bodySpacing: tooltipOpts.bodySpacing, + + // Title + titleFontColor: tooltipOpts.titleFontColor, + _titleFontFamily: getValueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), + _titleFontStyle: getValueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), + titleFontSize: getValueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), + _titleAlign: tooltipOpts.titleAlign, + titleSpacing: tooltipOpts.titleSpacing, + titleMarginBottom: tooltipOpts.titleMarginBottom, + + // Footer + footerFontColor: tooltipOpts.footerFontColor, + _footerFontFamily: getValueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), + _footerFontStyle: getValueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), + footerFontSize: getValueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), + _footerAlign: tooltipOpts.footerAlign, + footerSpacing: tooltipOpts.footerSpacing, + footerMarginTop: tooltipOpts.footerMarginTop, + + // Appearance + caretSize: tooltipOpts.caretSize, + cornerRadius: tooltipOpts.cornerRadius, + backgroundColor: tooltipOpts.backgroundColor, + opacity: 0, + legendColorBackground: tooltipOpts.multiKeyBackground, + displayColors: tooltipOpts.displayColors + }; + } + + /** + * Get the size of the tooltip + */ + function getTooltipSize(tooltip, model) { + var ctx = tooltip._chart.ctx; + + var height = model.yPadding * 2; // Tooltip Padding + var width = 0; + + // Count of all lines in the body + var body = model.body; + var combinedBodyLength = body.reduce(function(count, bodyItem) { + return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length; + }, 0); + combinedBodyLength += model.beforeBody.length + model.afterBody.length; + + var titleLineCount = model.title.length; + var footerLineCount = model.footer.length; + var titleFontSize = model.titleFontSize, + bodyFontSize = model.bodyFontSize, + footerFontSize = model.footerFontSize; + + height += titleLineCount * titleFontSize; // Title Lines + height += titleLineCount ? (titleLineCount - 1) * model.titleSpacing : 0; // Title Line Spacing + height += titleLineCount ? model.titleMarginBottom : 0; // Title's bottom Margin + height += combinedBodyLength * bodyFontSize; // Body Lines + height += combinedBodyLength ? (combinedBodyLength - 1) * model.bodySpacing : 0; // Body Line Spacing + height += footerLineCount ? model.footerMarginTop : 0; // Footer Margin + height += footerLineCount * (footerFontSize); // Footer Lines + height += footerLineCount ? (footerLineCount - 1) * model.footerSpacing : 0; // Footer Line Spacing + + // Title width + var widthPadding = 0; + var maxLineWidth = function(line) { + width = Math.max(width, ctx.measureText(line).width + widthPadding); + }; + + ctx.font = helpers.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily); + helpers.each(model.title, maxLineWidth); + + // Body width + ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); + helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth); + + // Body lines may include some extra width due to the color box + widthPadding = model.displayColors ? (bodyFontSize + 2) : 0; + helpers.each(body, function(bodyItem) { + helpers.each(bodyItem.before, maxLineWidth); + helpers.each(bodyItem.lines, maxLineWidth); + helpers.each(bodyItem.after, maxLineWidth); + }); + + // Reset back to 0 + widthPadding = 0; + + // Footer width + ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); + helpers.each(model.footer, maxLineWidth); + + // Add padding + width += 2 * model.xPadding; + + return { + width: width, + height: height + }; + } + + /** + * Helper to get the alignment of a tooltip given the size + */ + function determineAlignment(tooltip, size) { + var model = tooltip._model; + var chart = tooltip._chart; + var chartArea = tooltip._chartInstance.chartArea; + var xAlign = 'center'; + var yAlign = 'center'; + + if (model.y < size.height) { + yAlign = 'top'; + } else if (model.y > (chart.height - size.height)) { + yAlign = 'bottom'; + } + + var lf, rf; // functions to determine left, right alignment + var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart + var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges + var midX = (chartArea.left + chartArea.right) / 2; + var midY = (chartArea.top + chartArea.bottom) / 2; + + if (yAlign === 'center') { + lf = function(x) { + return x <= midX; + }; + rf = function(x) { + return x > midX; + }; + } else { + lf = function(x) { + return x <= (size.width / 2); + }; + rf = function(x) { + return x >= (chart.width - (size.width / 2)); + }; + } + + olf = function(x) { + return x + size.width > chart.width; + }; + orf = function(x) { + return x - size.width < 0; + }; + yf = function(y) { + return y <= midY ? 'top' : 'bottom'; + }; + + if (lf(model.x)) { + xAlign = 'left'; + + // Is tooltip too wide and goes over the right side of the chart.? + if (olf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } + } else if (rf(model.x)) { + xAlign = 'right'; + + // Is tooltip too wide and goes outside left edge of canvas? + if (orf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } + } + + var opts = tooltip._options; + return { + xAlign: opts.xAlign ? opts.xAlign : xAlign, + yAlign: opts.yAlign ? opts.yAlign : yAlign + }; + } + + /** + * @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment + */ + function getBackgroundPoint(vm, size, alignment) { + // Background Position + var x = vm.x; + var y = vm.y; + + var caretSize = vm.caretSize, + caretPadding = vm.caretPadding, + cornerRadius = vm.cornerRadius, + xAlign = alignment.xAlign, + yAlign = alignment.yAlign, + paddingAndSize = caretSize + caretPadding, + radiusAndPadding = cornerRadius + caretPadding; + + if (xAlign === 'right') { + x -= size.width; + } else if (xAlign === 'center') { + x -= (size.width / 2); + } + + if (yAlign === 'top') { + y += paddingAndSize; + } else if (yAlign === 'bottom') { + y -= size.height + paddingAndSize; + } else { + y -= (size.height / 2); + } + + if (yAlign === 'center') { + if (xAlign === 'left') { + x += paddingAndSize; + } else if (xAlign === 'right') { + x -= paddingAndSize; + } + } else if (xAlign === 'left') { + x -= radiusAndPadding; + } else if (xAlign === 'right') { + x += radiusAndPadding; + } + + return { + x: x, + y: y }; } Chart.Tooltip = Chart.Element.extend({ initialize: function() { - var me = this; - var globalDefaults = Chart.defaults.global; - var tooltipOpts = me._options; - var getValueOrDefault = helpers.getValueOrDefault; - - helpers.extend(me, { - _model: { - // Positioning - xPadding: tooltipOpts.xPadding, - yPadding: tooltipOpts.yPadding, - xAlign: tooltipOpts.xAlign, - yAlign: tooltipOpts.yAlign, - - // Body - bodyFontColor: tooltipOpts.bodyFontColor, - _bodyFontFamily: getValueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), - _bodyFontStyle: getValueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), - _bodyAlign: tooltipOpts.bodyAlign, - bodyFontSize: getValueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), - bodySpacing: tooltipOpts.bodySpacing, - - // Title - titleFontColor: tooltipOpts.titleFontColor, - _titleFontFamily: getValueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), - _titleFontStyle: getValueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), - titleFontSize: getValueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), - _titleAlign: tooltipOpts.titleAlign, - titleSpacing: tooltipOpts.titleSpacing, - titleMarginBottom: tooltipOpts.titleMarginBottom, - - // Footer - footerFontColor: tooltipOpts.footerFontColor, - _footerFontFamily: getValueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), - _footerFontStyle: getValueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), - footerFontSize: getValueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), - _footerAlign: tooltipOpts.footerAlign, - footerSpacing: tooltipOpts.footerSpacing, - footerMarginTop: tooltipOpts.footerMarginTop, - - // Appearance - caretSize: tooltipOpts.caretSize, - cornerRadius: tooltipOpts.cornerRadius, - backgroundColor: tooltipOpts.backgroundColor, - opacity: 0, - legendColorBackground: tooltipOpts.multiKeyBackground - } - }); + this._model = getBaseModel(this._options); }, // Get the title @@ -271,25 +443,55 @@ module.exports = function(Chart) { update: function(changed) { var me = this; var opts = me._options; - var model = me._model; + + // Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition + // that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time + // which breaks any animations. + var existingModel = me._model; + var model = me._model = getBaseModel(opts); var active = me._active; var data = me._data; var chartInstance = me._chartInstance; + // In the case where active.length === 0 we need to keep these at existing values for good animations + var alignment = { + xAlign: existingModel.xAlign, + yAlign: existingModel.yAlign + }; + var backgroundPoint = { + x: existingModel.x, + y: existingModel.y + }; + var tooltipSize = { + width: existingModel.width, + height: existingModel.height + }; + var tooltipPosition = { + x: existingModel.caretX, + y: existingModel.caretY + }; + var i, len; if (active.length) { model.opacity = 1; - var labelColors = [], - tooltipPosition = getAveragePosition(active); + var labelColors = []; + tooltipPosition = Chart.Tooltip.positioners[opts.position](active, me._eventPosition); var tooltipItems = []; for (i = 0, len = active.length; i < len; ++i) { tooltipItems.push(createTooltipItem(active[i])); } + // If the user provided a filter function, use it to modify the tooltip items + if (opts.filter) { + tooltipItems = tooltipItems.filter(function(a) { + return opts.filter(a, data); + }); + } + // If the user provided a sorting function, use it to modify the tooltip items if (opts.itemSort) { tooltipItems = tooltipItems.sort(function(a, b) { @@ -297,209 +499,55 @@ module.exports = function(Chart) { }); } - // If there is more than one item, show color items - if (active.length > 1) { - helpers.each(tooltipItems, function(tooltipItem) { - labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance)); - }); - } - - // Build the Text Lines - helpers.extend(model, { - title: me.getTitle(tooltipItems, data), - beforeBody: me.getBeforeBody(tooltipItems, data), - body: me.getBody(tooltipItems, data), - afterBody: me.getAfterBody(tooltipItems, data), - footer: me.getFooter(tooltipItems, data), - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - caretPadding: helpers.getValueOrDefault(tooltipPosition.padding, 2), - labelColors: labelColors + // Determine colors for boxes + helpers.each(tooltipItems, function(tooltipItem) { + labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance)); }); - // We need to determine alignment of - var tooltipSize = me.getTooltipSize(model); - me.determineAlignment(tooltipSize); // Smart Tooltip placement to stay on the canvas + // Build the Text Lines + model.title = me.getTitle(tooltipItems, data); + model.beforeBody = me.getBeforeBody(tooltipItems, data); + model.body = me.getBody(tooltipItems, data); + model.afterBody = me.getAfterBody(tooltipItems, data); + model.footer = me.getFooter(tooltipItems, data); - helpers.extend(model, me.getBackgroundPoint(model, tooltipSize)); + // Initial positioning and colors + model.x = Math.round(tooltipPosition.x); + model.y = Math.round(tooltipPosition.y); + model.caretPadding = helpers.getValueOrDefault(tooltipPosition.padding, 2); + model.labelColors = labelColors; + + // data points + model.dataPoints = tooltipItems; + + // We need to determine alignment of the tooltip + tooltipSize = getTooltipSize(this, model); + alignment = determineAlignment(this, tooltipSize); + // Final Size and Position + backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment); } else { - me._model.opacity = 0; + model.opacity = 0; } + model.xAlign = alignment.xAlign; + model.yAlign = alignment.yAlign; + model.x = backgroundPoint.x; + model.y = backgroundPoint.y; + model.width = tooltipSize.width; + model.height = tooltipSize.height; + + // Point where the caret on the tooltip points to + model.caretX = tooltipPosition.x; + model.caretY = tooltipPosition.y; + + me._model = model; + if (changed && opts.custom) { opts.custom.call(me, model); } return me; }, - getTooltipSize: function(vm) { - var ctx = this._chart.ctx; - - var size = { - height: vm.yPadding * 2, // Tooltip Padding - width: 0 - }; - - // Count of all lines in the body - var body = vm.body; - var combinedBodyLength = body.reduce(function(count, bodyItem) { - return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length; - }, 0); - combinedBodyLength += vm.beforeBody.length + vm.afterBody.length; - - var titleLineCount = vm.title.length; - var footerLineCount = vm.footer.length; - var titleFontSize = vm.titleFontSize, - bodyFontSize = vm.bodyFontSize, - footerFontSize = vm.footerFontSize; - - size.height += titleLineCount * titleFontSize; // Title Lines - size.height += (titleLineCount - 1) * vm.titleSpacing; // Title Line Spacing - size.height += titleLineCount ? vm.titleMarginBottom : 0; // Title's bottom Margin - size.height += combinedBodyLength * bodyFontSize; // Body Lines - size.height += combinedBodyLength ? (combinedBodyLength - 1) * vm.bodySpacing : 0; // Body Line Spacing - size.height += footerLineCount ? vm.footerMarginTop : 0; // Footer Margin - size.height += footerLineCount * (footerFontSize); // Footer Lines - size.height += footerLineCount ? (footerLineCount - 1) * vm.footerSpacing : 0; // Footer Line Spacing - - // Title width - var widthPadding = 0; - var maxLineWidth = function(line) { - size.width = Math.max(size.width, ctx.measureText(line).width + widthPadding); - }; - - ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); - helpers.each(vm.title, maxLineWidth); - - // Body width - ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - helpers.each(vm.beforeBody.concat(vm.afterBody), maxLineWidth); - - // Body lines may include some extra width due to the color box - widthPadding = body.length > 1 ? (bodyFontSize + 2) : 0; - helpers.each(body, function(bodyItem) { - helpers.each(bodyItem.before, maxLineWidth); - helpers.each(bodyItem.lines, maxLineWidth); - helpers.each(bodyItem.after, maxLineWidth); - }); - - // Reset back to 0 - widthPadding = 0; - - // Footer width - ctx.font = helpers.fontString(footerFontSize, vm._footerFontStyle, vm._footerFontFamily); - helpers.each(vm.footer, maxLineWidth); - - // Add padding - size.width += 2 * vm.xPadding; - - return size; - }, - determineAlignment: function(size) { - var me = this; - var model = me._model; - var chart = me._chart; - var chartArea = me._chartInstance.chartArea; - - if (model.y < size.height) { - model.yAlign = 'top'; - } else if (model.y > (chart.height - size.height)) { - model.yAlign = 'bottom'; - } - - var lf, rf; // functions to determine left, right alignment - var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart - var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges - var midX = (chartArea.left + chartArea.right) / 2; - var midY = (chartArea.top + chartArea.bottom) / 2; - - if (model.yAlign === 'center') { - lf = function(x) { - return x <= midX; - }; - rf = function(x) { - return x > midX; - }; - } else { - lf = function(x) { - return x <= (size.width / 2); - }; - rf = function(x) { - return x >= (chart.width - (size.width / 2)); - }; - } - - olf = function(x) { - return x + size.width > chart.width; - }; - orf = function(x) { - return x - size.width < 0; - }; - yf = function(y) { - return y <= midY ? 'top' : 'bottom'; - }; - - if (lf(model.x)) { - model.xAlign = 'left'; - - // Is tooltip too wide and goes over the right side of the chart.? - if (olf(model.x)) { - model.xAlign = 'center'; - model.yAlign = yf(model.y); - } - } else if (rf(model.x)) { - model.xAlign = 'right'; - - // Is tooltip too wide and goes outside left edge of canvas? - if (orf(model.x)) { - model.xAlign = 'center'; - model.yAlign = yf(model.y); - } - } - }, - getBackgroundPoint: function(vm, size) { - // Background Position - var pt = { - x: vm.x, - y: vm.y - }; - - var caretSize = vm.caretSize, - caretPadding = vm.caretPadding, - cornerRadius = vm.cornerRadius, - xAlign = vm.xAlign, - yAlign = vm.yAlign, - paddingAndSize = caretSize + caretPadding, - radiusAndPadding = cornerRadius + caretPadding; - - if (xAlign === 'right') { - pt.x -= size.width; - } else if (xAlign === 'center') { - pt.x -= (size.width / 2); - } - - if (yAlign === 'top') { - pt.y += paddingAndSize; - } else if (yAlign === 'bottom') { - pt.y -= size.height + paddingAndSize; - } else { - pt.y -= (size.height / 2); - } - - if (yAlign === 'center') { - if (xAlign === 'left') { - pt.x += paddingAndSize; - } else if (xAlign === 'right') { - pt.x -= paddingAndSize; - } - } else if (xAlign === 'left') { - pt.x -= radiusAndPadding; - } else if (xAlign === 'right') { - pt.x += radiusAndPadding; - } - - return pt; - }, drawCaret: function(tooltipPoint, size, opacity) { var vm = this._view; var ctx = this._chart.ctx; @@ -555,8 +603,7 @@ module.exports = function(Chart) { } } - var bgColor = helpers.color(vm.backgroundColor); - ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); + ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); @@ -574,8 +621,7 @@ module.exports = function(Chart) { var titleFontSize = vm.titleFontSize, titleSpacing = vm.titleSpacing; - var titleFontColor = helpers.color(vm.titleFontColor); - ctx.fillStyle = titleFontColor.alpha(opacity * titleFontColor.alpha()).rgbString(); + ctx.fillStyle = mergeOpacity(vm.titleFontColor, opacity); ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); var i, len; @@ -597,8 +643,7 @@ module.exports = function(Chart) { ctx.textAlign = vm._bodyAlign; ctx.textBaseline = 'top'; - var bodyFontColor = helpers.color(vm.bodyFontColor); - var textColor = bodyFontColor.alpha(opacity * bodyFontColor.alpha()).rgbString(); + var textColor = mergeOpacity(vm.bodyFontColor, opacity); ctx.fillStyle = textColor; ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); @@ -612,7 +657,7 @@ module.exports = function(Chart) { // Before body lines helpers.each(vm.beforeBody, fillLineOfText); - var drawColorBoxes = body.length > 1; + var drawColorBoxes = vm.displayColors; xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0; // Draw body lines now @@ -623,15 +668,15 @@ module.exports = function(Chart) { // Draw Legend-like boxes if needed if (drawColorBoxes) { // Fill a white rect so that colours merge nicely if the opacity is < 1 - ctx.fillStyle = helpers.color(vm.legendColorBackground).alpha(opacity).rgbaString(); + ctx.fillStyle = mergeOpacity(vm.legendColorBackground, opacity); ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); // Border - ctx.strokeStyle = helpers.color(vm.labelColors[i].borderColor).alpha(opacity).rgbaString(); + ctx.strokeStyle = mergeOpacity(vm.labelColors[i].borderColor, opacity); ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); // Inner square - ctx.fillStyle = helpers.color(vm.labelColors[i].backgroundColor).alpha(opacity).rgbaString(); + ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); ctx.fillStyle = textColor; @@ -659,8 +704,7 @@ module.exports = function(Chart) { ctx.textAlign = vm._footerAlign; ctx.textBaseline = 'top'; - var footerFontColor = helpers.color(vm.footerFontColor); - ctx.fillStyle = footerFontColor.alpha(opacity * footerFontColor.alpha()).rgbString(); + ctx.fillStyle = mergeOpacity(vm.footerFontColor, opacity); ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); helpers.each(footer, function(line) { @@ -669,6 +713,11 @@ module.exports = function(Chart) { }); } }, + drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { + ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); + helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius); + ctx.fill(); + }, draw: function() { var ctx = this._chart.ctx; var vm = this._view; @@ -677,7 +726,10 @@ module.exports = function(Chart) { return; } - var tooltipSize = this.getTooltipSize(vm); + var tooltipSize = { + width: vm.width, + height: vm.height + }; var pt = { x: vm.x, y: vm.y @@ -688,10 +740,7 @@ module.exports = function(Chart) { if (this._options.enabled) { // Draw Background - var bgColor = helpers.color(vm.backgroundColor); - ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); - helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius); - ctx.fill(); + this.drawBackground(pt, vm, ctx, tooltipSize, opacity); // Draw Caret this.drawCaret(pt, tooltipSize, opacity); @@ -709,6 +758,120 @@ module.exports = function(Chart) { // Footer this.drawFooter(pt, vm, ctx, opacity); } + }, + + /** + * Handle an event + * @private + * @param e {Event} the event to handle + * @returns {Boolean} true if the tooltip changed + */ + handleEvent: function(e) { + var me = this; + var options = me._options; + var changed = false; + + me._lastActive = me._lastActive || []; + + // Find Active Elements for tooltips + if (e.type === 'mouseout') { + me._active = []; + } else { + me._active = me._chartInstance.getElementsAtEventForMode(e, options.mode, options); + } + + // Remember Last Actives + changed = !helpers.arrayEquals(me._active, me._lastActive); + me._lastActive = me._active; + + if (options.enabled || options.custom) { + me._eventPosition = helpers.getRelativePosition(e, me._chart); + + 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); + } + + return changed; } }); + + /** + * @namespace Chart.Tooltip.positioners + */ + Chart.Tooltip.positioners = { + /** + * Average mode places the tooltip at the average position of the elements shown + * @function Chart.Tooltip.positioners.average + * @param elements {ChartElement[]} the elements being displayed in the tooltip + * @returns {Point} tooltip position + */ + average: function(elements) { + if (!elements.length) { + return false; + } + + var i, len; + var x = 0; + var y = 0; + var count = 0; + + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var pos = el.tooltipPosition(); + x += pos.x; + y += pos.y; + ++count; + } + } + + return { + x: Math.round(x / count), + y: Math.round(y / count) + }; + }, + + /** + * Gets the tooltip position nearest of the item nearest to the event position + * @function Chart.Tooltip.positioners.nearest + * @param elements {Chart.Element[]} the tooltip elements + * @param eventPosition {Point} the position of the event in canvas coordinates + * @returns {Point} the tooltip position + */ + nearest: function(elements, eventPosition) { + var x = eventPosition.x; + var y = eventPosition.y; + + var nearestElement; + var minDistance = Number.POSITIVE_INFINITY; + var i, len; + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var center = el.getCenterPoint(); + var d = helpers.distanceBetweenPoints(eventPosition, center); + + if (d < minDistance) { + minDistance = d; + nearestElement = el; + } + } + } + + if (nearestElement) { + var tp = nearestElement.tooltipPosition(); + x = tp.x; + y = tp.y; + } + + return { + x: x, + y: y + }; + } + }; }; diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index b114e6f04..a61d19a3a 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -52,6 +52,19 @@ module.exports = function(Chart) { } return false; }, + getCenterPoint: function() { + var vm = this._view; + var halfAngle = (vm.startAngle + vm.endAngle) / 2; + var halfRadius = (vm.innerRadius + vm.outerRadius) / 2; + return { + x: vm.x + Math.cos(halfAngle) * halfRadius, + y: vm.y + Math.sin(halfAngle) * halfRadius + }; + }, + getArea: function() { + var vm = this._view; + return Math.PI * ((vm.endAngle - vm.startAngle) / (2 * Math.PI)) * (Math.pow(vm.outerRadius, 2) - Math.pow(vm.innerRadius, 2)); + }, tooltipPosition: function() { var vm = this._view; diff --git a/src/elements/element.line.js b/src/elements/element.line.js index d81e7861c..b63ece78c 100644 --- a/src/elements/element.line.js +++ b/src/elements/element.line.js @@ -15,7 +15,7 @@ module.exports = function(Chart) { borderDashOffset: 0.0, borderJoinStyle: 'miter', capBezierPoints: true, - fill: true // do we fill in the area between the line and its base axis + fill: true, // do we fill in the area between the line and its base axis }; Chart.elements.Line = Chart.Element.extend({ @@ -23,9 +23,18 @@ module.exports = function(Chart) { var me = this; var vm = me._view; var spanGaps = vm.spanGaps; - var scaleZero = vm.scaleZero; + var fillPoint = vm.scaleZero; var loop = me._loop; + // Handle different fill modes for cartesian lines + if (!loop) { + if (vm.fill === 'top') { + fillPoint = vm.scaleTop; + } else if (vm.fill === 'bottom') { + fillPoint = vm.scaleBottom; + } + } + var ctx = me._chart.ctx; ctx.save(); @@ -71,9 +80,9 @@ module.exports = function(Chart) { // First point moves to it's starting position no matter what if (index === 0) { if (loop) { - ctx.moveTo(scaleZero.x, scaleZero.y); + ctx.moveTo(fillPoint.x, fillPoint.y); } else { - ctx.moveTo(currentVM.x, scaleZero); + ctx.moveTo(currentVM.x, fillPoint); } if (!currentVM.skip) { @@ -87,9 +96,9 @@ module.exports = function(Chart) { // Only do this if this is the first point that is skipped if (!spanGaps && lastDrawnIndex === (index - 1)) { if (loop) { - ctx.lineTo(scaleZero.x, scaleZero.y); + ctx.lineTo(fillPoint.x, fillPoint.y); } else { - ctx.lineTo(previous._view.x, scaleZero); + ctx.lineTo(previous._view.x, fillPoint); } } } else { @@ -102,7 +111,7 @@ module.exports = function(Chart) { } else if (loop) { ctx.lineTo(currentVM.x, currentVM.y); } else { - ctx.lineTo(currentVM.x, scaleZero); + ctx.lineTo(currentVM.x, fillPoint); ctx.lineTo(currentVM.x, currentVM.y); } } else { @@ -115,7 +124,7 @@ module.exports = function(Chart) { } if (!loop && lastDrawnIndex !== -1) { - ctx.lineTo(points[lastDrawnIndex]._view.x, scaleZero); + ctx.lineTo(points[lastDrawnIndex]._view.x, fillPoint); } ctx.fillStyle = vm.backgroundColor || globalDefaults.defaultColor; diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 474ef252f..fe3a6696b 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -18,14 +18,35 @@ module.exports = function(Chart) { hoverBorderWidth: 1 }; + function xRange(mouseX) { + var vm = this._view; + return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + } + + function yRange(mouseY) { + var vm = this._view; + return vm ? (Math.pow(mouseY - vm.y, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + } + Chart.elements.Point = Chart.Element.extend({ inRange: function(mouseX, mouseY) { var vm = this._view; return vm ? ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(vm.hitRadius + vm.radius, 2)) : false; }, - inLabelRange: function(mouseX) { + + inLabelRange: xRange, + inXRange: xRange, + inYRange: yRange, + + getCenterPoint: function() { var vm = this._view; - return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return { + x: vm.x, + y: vm.y + }; + }, + getArea: function() { + return Math.PI * Math.pow(this._view.radius, 2); }, tooltipPosition: function() { var vm = this._view; diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index 6752228c8..427916791 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -11,6 +11,44 @@ module.exports = function(Chart) { borderSkipped: 'bottom' }; + function isVertical(bar) { + return bar._view.width !== undefined; + } + + /** + * Helper function to get the bounds of the bar regardless of the orientation + * @private + * @param bar {Chart.Element.Rectangle} the bar + * @return {Bounds} bounds of the bar + */ + function getBarBounds(bar) { + var vm = bar._view; + var x1, x2, y1, y2; + + if (isVertical(bar)) { + // vertical + var halfWidth = vm.width / 2; + x1 = vm.x - halfWidth; + x2 = vm.x + halfWidth; + y1 = Math.min(vm.y, vm.base); + y2 = Math.max(vm.y, vm.base); + } else { + // horizontal bar + var halfHeight = vm.height / 2; + x1 = Math.min(vm.x, vm.base); + x2 = Math.max(vm.x, vm.base); + y1 = vm.y - halfHeight; + y2 = vm.y + halfHeight; + } + + return { + left: x1, + top: y1, + right: x2, + bottom: y2 + }; + } + Chart.elements.Rectangle = Chart.Element.extend({ draw: function() { var ctx = this._chart.ctx; @@ -57,9 +95,12 @@ module.exports = function(Chart) { } // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); + var corner = cornerAt(0); + ctx.moveTo(corner[0], corner[1]); + for (var i = 1; i < 4; i++) { - ctx.lineTo.apply(ctx, cornerAt(i)); + corner = cornerAt(i); + ctx.lineTo(corner[0], corner[1]); } ctx.fill(); @@ -72,16 +113,56 @@ module.exports = function(Chart) { return vm.base - vm.y; }, inRange: function(mouseX, mouseY) { - var vm = this._view; - return vm ? - (vm.y < vm.base ? - (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base) : - (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y)) : - false; + var inRange = false; + + if (this._view) { + var bounds = getBarBounds(this); + inRange = mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom; + } + + return inRange; }, - inLabelRange: function(mouseX) { + inLabelRange: function(mouseX, mouseY) { + var me = this; + if (!me._view) { + return false; + } + + var inRange = false; + var bounds = getBarBounds(me); + + if (isVertical(me)) { + inRange = mouseX >= bounds.left && mouseX <= bounds.right; + } else { + inRange = mouseY >= bounds.top && mouseY <= bounds.bottom; + } + + return inRange; + }, + inXRange: function(mouseX) { + var bounds = getBarBounds(this); + return mouseX >= bounds.left && mouseX <= bounds.right; + }, + inYRange: function(mouseY) { + var bounds = getBarBounds(this); + return mouseY >= bounds.top && mouseY <= bounds.bottom; + }, + getCenterPoint: function() { var vm = this._view; - return vm ? (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) : false; + var x, y; + if (isVertical(this)) { + x = vm.x; + y = (vm.y + vm.base) / 2; + } else { + x = (vm.x + vm.base) / 2; + y = vm.y; + } + + return {x: x, y: y}; + }, + getArea: function() { + var vm = this._view; + return vm.width * Math.abs(vm.y - vm.base); }, tooltipPosition: function() { var vm = this._view; diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 7e1b3d409..fb90e6333 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -10,7 +10,7 @@ module.exports = function(Chart) { var DatasetScale = Chart.Scale.extend({ /** - * Internal function to get the correct labels. If data.xLabels or data.yLabels are defined, use tose + * Internal function to get the correct labels. If data.xLabels or data.yLabels are defined, use those * else fall back to data.labels * @private */ diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index f2700a3a4..e8afa84f8 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -7,31 +7,7 @@ module.exports = function(Chart) { var defaultConfig = { position: 'left', ticks: { - callback: function(tickValue, index, ticks) { - // If we have lots of ticks, don't use the ones - var delta = ticks.length > 3 ? ticks[2] - ticks[1] : ticks[1] - ticks[0]; - - // If we have a number like 2.5 as the delta, figure out how many decimal places we need - if (Math.abs(delta) > 1) { - if (tickValue !== Math.floor(tickValue)) { - // not an integer - delta = tickValue - Math.floor(tickValue); - } - } - - var logDelta = helpers.log10(Math.abs(delta)); - var tickString = ''; - - if (tickValue !== 0) { - var numDecimal = -1 * Math.floor(logDelta); - numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places - tickString = tickValue.toFixed(numDecimal); - } else { - tickString = '0'; // never show decimal places for 0 - } - - return tickString; - } + callback: Chart.Ticks.formatters.linear } }; diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 37caa0359..dc700f304 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -22,7 +22,7 @@ module.exports = function(Chart) { // move the top up to 0 me.max = 0; } else if (minSign > 0 && maxSign > 0) { - // move the botttom down to 0 + // move the bottom down to 0 me.min = 0; } } @@ -53,49 +53,22 @@ module.exports = function(Chart) { buildTicks: function() { var me = this; var opts = me.options; - var ticks = me.ticks = []; var tickOpts = opts.ticks; - var getValueOrDefault = helpers.getValueOrDefault; // 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 - + // the graph. Make sure we always have at least 2 ticks var maxTicks = me.getTickLimit(); - - // Make sure we always have at least 2 ticks maxTicks = Math.max(2, maxTicks); - // 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; - var fixedStepSizeSet = (tickOpts.fixedStepSize && tickOpts.fixedStepSize > 0) || (tickOpts.stepSize && tickOpts.stepSize > 0); - if (fixedStepSizeSet) { - spacing = getValueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize); - } else { - var niceRange = helpers.niceNum(me.max - me.min, false); - spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - } - var niceMin = Math.floor(me.min / spacing) * spacing; - var niceMax = Math.ceil(me.max / spacing) * spacing; - 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(tickOpts.min !== undefined ? tickOpts.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - ticks.push(niceMin + (j * spacing)); - } - ticks.push(tickOpts.max !== undefined ? tickOpts.max : niceMax); + var numericGeneratorOptions = { + maxTicks: maxTicks, + min: tickOpts.min, + max: tickOpts.max, + stepSize: helpers.getValueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) + }; + var ticks = me.ticks = Chart.Ticks.generators.linear(numericGeneratorOptions, me); me.handleDirectionalChanges(); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 50417528a..9d8745940 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -9,16 +9,7 @@ module.exports = function(Chart) { // label settings ticks: { - callback: function(value, index, arr) { - var remain = value / (Math.pow(10, Math.floor(helpers.log10(value)))); - - if (value === 0) { - return '0'; - } else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === arr.length - 1) { - return value.toExponential(); - } - return ''; - } + callback: Chart.Ticks.formatters.logarithmic } }; @@ -124,43 +115,12 @@ module.exports = function(Chart) { var me = this; var opts = me.options; var tickOpts = opts.ticks; - var getValueOrDefault = helpers.getValueOrDefault; - // Reset the ticks array. Later on, we will draw a grid line at these positions - // The array simply contains the numerical value of the spots where ticks will be - var ticks = me.ticks = []; - - // 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 = getValueOrDefault(tickOpts.min, Math.pow(10, Math.floor(helpers.log10(me.min)))); - - while (tickVal < me.max) { - ticks.push(tickVal); - - var exp; - var significand; - - if (tickVal === 0) { - exp = Math.floor(helpers.log10(me.minNotZero)); - significand = Math.round(me.minNotZero / Math.pow(10, exp)); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; - } - - if (significand === 10) { - significand = 1; - ++exp; - } - - tickVal = significand * Math.pow(10, exp); - } - - var lastTick = getValueOrDefault(tickOpts.max, tickVal); - ticks.push(lastTick); + var generationOptions = { + min: tickOpts.min, + max: tickOpts.max + }; + var ticks = me.ticks = Chart.Ticks.generators.logarithmic(generationOptions, me); if (!me.isHorizontal()) { // We are in a vertical orientation. The top value is the highest. So reverse the array @@ -218,7 +178,7 @@ module.exports = function(Chart) { pixel += paddingLeft; } } else { - // Bottom - top since pixels increase downard on a screen + // Bottom - top since pixels increase downward on a screen innerDimension = me.height - (paddingTop + paddingBottom); if (start === 0 && !tickOpts.reverse) { range = helpers.log10(me.end) - helpers.log10(me.minNotZero); diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 361ad3cba..cc7c6557d 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -31,7 +31,9 @@ module.exports = function(Chart) { backdropPaddingY: 2, // Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2 + backdropPaddingX: 2, + + callback: Chart.Ticks.formatters.linear }, pointLabels: { diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 5a5638cef..c69bcf21e 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -264,7 +264,7 @@ module.exports = function(Chart) { me.unitScale = 1; if (helpers.isArray(unitDefinition.steps) && Math.ceil(me.scaleSizeInUnits / labelCapacity) < helpers.max(unitDefinition.steps)) { - // Use one of the prefedined steps + // Use one of the predefined steps for (var idx = 0; idx < unitDefinition.steps.length; ++idx) { if (unitDefinition.steps[idx] >= Math.ceil(me.scaleSizeInUnits / labelCapacity)) { me.unitScale = helpers.getValueOrDefault(me.options.time.unitStepSize, unitDefinition.steps[idx]); diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index 3ef9cabdf..a77eea22e 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -5,8 +5,8 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [] } + {data: []}, + {data: []} ], labels: [] } @@ -30,8 +30,8 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [] } + {data: []}, + {data: []} ], labels: [] }, @@ -57,10 +57,10 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [], type: 'line' }, - { data: [], hidden: true }, - { data: [] }, - { data: [] } + {data: [], type: 'line'}, + {data: [], hidden: true}, + {data: []}, + {data: []} ], labels: [] } @@ -75,10 +75,10 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [], hidden: true }, - { data: [], type: 'line' }, - { data: [] } + {data: []}, + {data: [], hidden: true}, + {data: [], type: 'line'}, + {data: []} ], labels: [] } @@ -94,8 +94,8 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [10, 15, 0, -4] } + {data: []}, + {data: [10, 15, 0, -4]} ], labels: [] } @@ -154,8 +154,9 @@ describe('Bar controller tests', function() { expect(meta.data.length).toBe(2); - [ { x: 122, y: 484 }, - { x: 234, y: 32 } + [ + {x: 122, y: 484}, + {x: 234, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(1); expect(meta.data[i]._index).toBe(i); @@ -251,10 +252,11 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { b: 290, w: 91, x: 95, y: 161 }, - { b: 290, w: 91, x: 209, y: 419 }, - { b: 290, w: 91, x: 322, y: 161 }, - { b: 290, w: 91, x: 436, y: 419 } + [ + {b: 290, w: 91, x: 95, y: 161}, + {b: 290, w: 91, x: 209, y: 419}, + {b: 290, w: 91, x: 322, y: 161}, + {b: 290, w: 91, x: 436, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -264,10 +266,11 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); - [ { b: 161, w: 91, x: 95, y: 32 }, - { b: 290, w: 91, x: 209, y: 97 }, - { b: 161, w: 91, x: 322, y: 161 }, - { b: 419, w: 91, x: 436, y: 471 } + [ + {b: 161, w: 91, x: 95, y: 32}, + {b: 290, w: 91, x: 209, y: 97}, + {b: 161, w: 91, x: 322, y: 161}, + {b: 419, w: 91, x: 436, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -305,10 +308,11 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { b: 290, w: 91, x: 95, y: 161 }, - { b: 290, w: 91, x: 209, y: 419 }, - { b: 290, w: 91, x: 322, y: 161 }, - { b: 290, w: 91, x: 436, y: 419 } + [ + {b: 290, w: 91, x: 95, y: 161}, + {b: 290, w: 91, x: 209, y: 419}, + {b: 290, w: 91, x: 322, y: 161}, + {b: 290, w: 91, x: 436, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -318,10 +322,11 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); - [ { b: 161, w: 91, x: 95, y: 32 }, - { b: 290, w: 91, x: 209, y: 97 }, - { b: 161, w: 91, x: 322, y: 161 }, - { b: 419, w: 91, x: 436, y: 471 } + [ + {b: 161, w: 91, x: 95, y: 32}, + {b: 290, w: 91, x: 209, y: 97}, + {b: 161, w: 91, x: 322, y: 161}, + {b: 419, w: 91, x: 436, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); diff --git a/test/controller.bubble.tests.js b/test/controller.bubble.tests.js index ec61a0b50..977b2d517 100644 --- a/test/controller.bubble.tests.js +++ b/test/controller.bubble.tests.js @@ -133,10 +133,11 @@ describe('Bubble controller tests', function() { var meta = chart.getDatasetMeta(0); - [ { r: 5, x: 38, y: 32 }, - { r: 1, x: 189, y: 484 }, - { r: 2, x: 341, y: 461 }, - { r: 1, x: 492, y: 32 } + [ + {r: 5, x: 38, y: 32}, + {r: 1, x: 189, y: 484}, + {r: 2, x: 341, y: 461}, + {r: 1, x: 492, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._model.radius).toBe(expected.r); expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); diff --git a/test/controller.doughnut.tests.js b/test/controller.doughnut.tests.js index 0d31653a7..d9319bb80 100644 --- a/test/controller.doughnut.tests.js +++ b/test/controller.doughnut.tests.js @@ -78,10 +78,11 @@ describe('Doughnut controller tests', function() { expect(meta.data.length).toBe(4); - [ { c: 0 }, - { c: 0 }, - { c: 0, }, - { c: 0 } + [ + {c: 0}, + {c: 0}, + {c: 0}, + {c: 0} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -96,14 +97,15 @@ describe('Doughnut controller tests', function() { borderColor: 'rgb(0, 0, 255)', borderWidth: 2 })); - }) + }); chart.update(); - [ { c: 1.7453292519, s: -1.5707963267, e: 0.1745329251 }, - { c: 2.0943951023, s: 0.1745329251, e: 2.2689280275 }, - { c: 0, s: 2.2689280275, e: 2.2689280275 }, - { c: 2.4434609527, s: 2.2689280275, e: 4.7123889803 } + [ + {c: 1.7453292519, s: -1.5707963267, e: 0.1745329251}, + {c: 2.0943951023, s: 0.1745329251, e: 2.2689280275}, + {c: 0, s: 2.2689280275, e: 2.2689280275}, + {c: 2.4434609527, s: 2.2689280275, e: 4.7123889803} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -118,7 +120,7 @@ describe('Doughnut controller tests', function() { borderColor: 'rgb(0, 0, 255)', borderWidth: 2 })); - }) + }); // Change the amount of data and ensure that arcs are updated accordingly chart.data.datasets[1].data = [1, 2]; // remove 2 elements from dataset 0 @@ -172,17 +174,18 @@ describe('Doughnut controller tests', function() { 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 } + [ + {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.x).toBeCloseToPixel(495); expect(meta.data[i]._model.y).toBeCloseToPixel(511); expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(478); expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(359); - expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c,8); + 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() { diff --git a/test/controller.line.tests.js b/test/controller.line.tests.js index 9afc9cec0..3c66b268e 100644 --- a/test/controller.line.tests.js +++ b/test/controller.line.tests.js @@ -165,12 +165,12 @@ describe('Line controller tests', function() { var chart = window.acquireChart({ type: 'line', data: { - datasets: [{ - data: [10, 15, 0, -4], - label: 'dataset', - xAxisID: 'firstXScaleID', - yAxisID: 'firstYScaleID' - }], + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID' + }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { @@ -202,8 +202,9 @@ describe('Line controller tests', function() { expect(meta.data.length).toBe(2); - [ { x: 44, y: 484 }, - { x: 193, y: 32 } + [ + {x: 44, y: 484}, + {x: 193, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(0); expect(meta.data[i]._index).toBe(i); @@ -223,23 +224,23 @@ describe('Line controller tests', function() { expect(meta.data.length).toBe(3); // should add a new meta data item }); - it('should correctly calculate x scale for label and point', function(){ - var chart = window.acquireChart({ + it('should correctly calculate x scale for label and point', function() { + var chart = window.acquireChart({ type: 'line', - data: { - labels: ["One"], - datasets: [{ - data: [1], - }] + data: { + labels: ['One'], + datasets: [{ + data: [1], + }] + }, + options: { + hover: { + mode: 'single' }, - options: { - hover: { - mode: 'single' - }, scales: { yAxes: [{ ticks: { - beginAtZero:true + beginAtZero: true } }] } @@ -252,7 +253,7 @@ describe('Line controller tests', function() { expect(point._model.x).toBeCloseToPixel(267); // 2 points - chart.data.labels = ["One", "Two"]; + chart.data.labels = ['One', 'Two']; chart.data.datasets[0].data = [1, 2]; chart.update(); @@ -262,7 +263,7 @@ describe('Line controller tests', function() { expect(points[1]._model.x).toBeCloseToPixel(498); // 3 points - chart.data.labels = ["One", "Two", "Three"]; + chart.data.labels = ['One', 'Two', 'Three']; chart.data.datasets[0].data = [1, 2, 3]; chart.update(); @@ -273,7 +274,7 @@ describe('Line controller tests', function() { expect(points[2]._model.x).toBeCloseToPixel(493); // 4 points - chart.data.labels = ["One", "Two", "Three", "Four"]; + chart.data.labels = ['One', 'Two', 'Three', 'Four']; chart.data.datasets[0].data = [1, 2, 3, 4]; chart.update(); @@ -309,24 +310,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 38, y: 161 }, - { x: 189, y: 419 }, - { x: 341, y: 161 }, - { x: 492, y: 419 } + [ + {x: 38, y: 161}, + {x: 189, y: 419}, + {x: 341, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 38, y: 32 }, - { x: 189, y: 97 }, - { x: 341, y: 161 }, - { x: 492, y: 471 } + [ + {x: 38, y: 32}, + {x: 189, y: 97}, + {x: 341, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); @@ -362,24 +365,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 76, y: 161 }, - { x: 215, y: 419 }, - { x: 353, y: 161 }, - { x: 492, y: 419 } + [ + {x: 76, y: 161}, + {x: 215, y: 419}, + {x: 353, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 76, y: 32 }, - { x: 215, y: 97 }, - { x: 353, y: 161 }, - { x: 492, y: 471 } + [ + {x: 76, y: 32}, + {x: 215, y: 97}, + {x: 353, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); @@ -432,24 +437,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 38, y: 161 }, - { x: 189, y: 419 }, - { x: 341, y: 161 }, - { x: 492, y: 419 } + [ + {x: 38, y: 161}, + {x: 189, y: 419}, + {x: 341, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 38, y: 32 }, - { x: 189, y: 97 }, - { x: 341, y: 161 }, - { x: 492, y: 471 } + [ + {x: 38, y: 32}, + {x: 189, y: 97}, + {x: 341, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); @@ -478,24 +485,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 38, y: 161 }, - { x: 189, y: 419 }, - { x: 341, y: 161 }, - { x: 492, y: 419 } + [ + {x: 38, y: 161}, + {x: 189, y: 419}, + {x: 341, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 38, y: 32 }, - { x: 189, y: 97 }, - { x: 341, y: 161 }, - { x: 492, y: 471 } + [ + {x: 38, y: 32}, + {x: 189, y: 97}, + {x: 341, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); diff --git a/test/controller.polarArea.tests.js b/test/controller.polarArea.tests.js index 648117a4c..3bd95eb23 100644 --- a/test/controller.polarArea.tests.js +++ b/test/controller.polarArea.tests.js @@ -2,14 +2,14 @@ describe('Polar area controller tests', function() { it('should be constructed', function() { var chart = window.acquireChart({ - type: 'polarArea', - data: { - datasets: [ - { data: [] }, - { data: [] } - ], - labels: [] - } + type: 'polarArea', + data: { + datasets: [ + {data: []}, + {data: []} + ], + labels: [] + } }); var meta = chart.getDatasetMeta(1); @@ -28,8 +28,8 @@ describe('Polar area controller tests', function() { type: 'polarArea', data: { datasets: [ - { data: [] }, - { data: [10, 15, 0, -4] } + {data: []}, + {data: [10, 15, 0, -4]} ], labels: [] } @@ -45,14 +45,14 @@ describe('Polar area controller tests', function() { it('should draw all elements', function() { var chart = window.acquireChart({ - type: 'polarArea', - data: { - datasets: [{ - data: [10, 15, 0, -4], - label: 'dataset2' - }], - labels: ['label1', 'label2', 'label3', 'label4'] - } + type: 'polarArea', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } }); var meta = chart.getDatasetMeta(0); @@ -95,10 +95,11 @@ describe('Polar area controller tests', function() { var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); - [ { o: 156, s: -0.5 * Math.PI, e: 0 }, - { o: 211, s: 0, e: 0.5 * Math.PI }, - { o: 45, s: 0.5 * Math.PI, e: Math.PI }, - { o: 0, s: Math.PI, e: 1.5 * Math.PI } + [ + {o: 156, s: -0.5 * Math.PI, e: 0}, + {o: 211, s: 0, e: 0.5 * Math.PI}, + {o: 45, s: 0.5 * Math.PI, e: Math.PI}, + {o: 0, s: Math.PI, e: 1.5 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -176,10 +177,11 @@ describe('Polar area controller tests', function() { var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); - [ { o: 156, s: 0, e: 0.5 * Math.PI }, - { o: 211, s: 0.5 * Math.PI, e: Math.PI }, - { o: 45, s: Math.PI, e: 1.5 * Math.PI }, - { o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI } + [ + {o: 156, s: 0, e: 0.5 * Math.PI}, + {o: 211, s: 0.5 * Math.PI, e: Math.PI}, + {o: 45, s: Math.PI, e: 1.5 * Math.PI}, + {o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -230,7 +232,7 @@ describe('Polar area controller tests', function() { expect(meta.data[0] instanceof Chart.elements.Arc).toBe(true); expect(meta.data[1] instanceof Chart.elements.Arc).toBe(true); - // add 3 items + // add 3 items chart.data.labels = ['label1', 'label2', 'label3', 'label4', 'label5']; chart.data.datasets[0].data = [1, 2, 3, 4, 5]; chart.update(); diff --git a/test/controller.radar.tests.js b/test/controller.radar.tests.js index 421ff9eb3..449d295c9 100644 --- a/test/controller.radar.tests.js +++ b/test/controller.radar.tests.js @@ -32,8 +32,6 @@ describe('Radar controller tests', function() { } }); - var controller = new Chart.controllers.radar(chart, 0); - var meta = chart.getDatasetMeta(0); expect(meta.dataset instanceof Chart.elements.Line).toBe(true); // line element expect(meta.data.length).toBe(4); // 4 points created @@ -130,10 +128,10 @@ describe('Radar controller tests', function() { })); [ - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -157,10 +155,10 @@ describe('Radar controller tests', function() { meta.controller.update(); [ - { x: 256, y: 133, cppx: 246, cppy: 133, cpnx: 272, cpny: 133 }, - { x: 464, y: 272, cppx: 464, cppy: 264, cpnx: 464, cpny: 278 }, - { x: 256, y: 272, cppx: 276.9, cppy: 272, cpnx: 250.4, cpny: 272 }, - { x: 200, y: 272, cppx: 200, cppy: 275, cpnx: 200, cpny: 261 }, + {x: 256, y: 133, cppx: 246, cppy: 133, cpnx: 272, cpny: 133}, + {x: 464, y: 272, cppx: 464, cppy: 264, cpnx: 464, cpny: 278}, + {x: 256, y: 272, cppx: 276.9, cppy: 272, cpnx: 250.4, cpny: 272}, + {x: 200, y: 272, cppx: 200, cppy: 275, cpnx: 200, cpny: 261}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -218,10 +216,10 @@ describe('Radar controller tests', function() { // Since tension is now 0, we don't care about the control points [ - { x: 256, y: 133 }, - { x: 464, y: 272 }, - { x: 256, y: 272 }, - { x: 200, y: 272 }, + {x: 256, y: 133}, + {x: 464, y: 272}, + {x: 256, y: 272}, + {x: 200, y: 272}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -454,4 +452,4 @@ describe('Radar controller tests', function() { expect(point._model.borderWidth).toBe(5.5); expect(point._model.radius).toBe(4.4); }); -}); \ No newline at end of file +}); diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js new file mode 100644 index 000000000..cc99b0d7f --- /dev/null +++ b/test/core.controller.tests.js @@ -0,0 +1,833 @@ +describe('Chart.Controller', function() { + + function waitForResize(chart, callback) { + var resizer = chart.chart.canvas.parentNode._chartjs.resizer; + var content = resizer.contentWindow || resizer; + var state = content.document.readyState || 'complete'; + var handler = function() { + Chart.helpers.removeEvent(content, 'load', handler); + Chart.helpers.removeEvent(content, 'resize', handler); + setTimeout(callback, 50); + }; + + Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler); + } + + describe('context acquisition', function() { + var canvasId = 'chartjs-canvas'; + + beforeEach(function() { + var canvas = document.createElement('canvas'); + canvas.setAttribute('id', canvasId); + window.document.body.appendChild(canvas); + }); + + afterEach(function() { + document.getElementById(canvasId).remove(); + }); + + // see https://github.com/chartjs/Chart.js/issues/2807 + it('should gracefully handle invalid item', function() { + var chart = new Chart('foobar'); + + expect(chart).not.toBeValidChart(); + + chart.destroy(); + }); + + it('should accept a DOM element id', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvasId); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas element', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvas); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas context2D', function() { + var canvas = document.getElementById(canvasId); + var context = canvas.getContext('2d'); + var chart = new Chart(context); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(context); + + chart.destroy(); + }); + + it('should accept an array containing canvas', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart([canvas]); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + }); + + describe('config initialization', function() { + it('should create missing config.data properties', function() { + var chart = acquireChart({}); + var data = chart.data; + + expect(data instanceof Object).toBeTruthy(); + expect(data.labels instanceof Array).toBeTruthy(); + expect(data.labels.length).toBe(0); + expect(data.datasets instanceof Array).toBeTruthy(); + expect(data.datasets.length).toBe(0); + }); + + it('should not alter config.data references', function() { + var ds0 = {data: [10, 11, 12, 13]}; + var ds1 = {data: [20, 21, 22, 23]}; + var datasets = [ds0, ds1]; + var labels = [0, 1, 2, 3]; + var data = {labels: labels, datasets: datasets}; + + var chart = acquireChart({ + type: 'line', + data: data + }); + + expect(chart.data).toBe(data); + expect(chart.data.labels).toBe(labels); + expect(chart.data.datasets).toBe(datasets); + expect(chart.data.datasets[0]).toBe(ds0); + expect(chart.data.datasets[1]).toBe(ds1); + expect(chart.data.datasets[0].data).toBe(ds0.data); + expect(chart.data.datasets[1].data).toBe(ds1.data); + }); + + it('should initialize config with default options', function() { + var callback = function() {}; + + var defaults = Chart.defaults; + defaults.global.responsiveAnimationDuration = 42; + defaults.global.hover.onHover = callback; + defaults.line.hover.mode = 'x-axis'; + defaults.line.spanGaps = true; + + var chart = acquireChart({ + type: 'line' + }); + + var options = chart.options; + expect(options.defaultFontSize).toBe(defaults.global.defaultFontSize); + expect(options.showLines).toBe(defaults.line.showLines); + expect(options.spanGaps).toBe(true); + expect(options.responsiveAnimationDuration).toBe(42); + expect(options.hover.onHover).toBe(callback); + expect(options.hover.mode).toBe('x-axis'); + }); + + it('should override default options', function() { + var defaults = Chart.defaults; + defaults.global.responsiveAnimationDuration = 42; + defaults.line.hover.mode = 'x-axis'; + defaults.line.spanGaps = true; + + var chart = acquireChart({ + type: 'line', + options: { + responsiveAnimationDuration: 4242, + spanGaps: false, + hover: { + mode: 'dataset', + }, + title: { + position: 'bottom' + } + } + }); + + var options = chart.options; + expect(options.responsiveAnimationDuration).toBe(4242); + expect(options.spanGaps).toBe(false); + expect(options.hover.mode).toBe('dataset'); + expect(options.title.position).toBe('bottom'); + }); + }); + + describe('config.options.aspectRatio', function() { + it('should use default "global" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 620px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 620, dh: 310, + rw: 620, rh: 310, + }); + }); + + it('should use default "chart" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + type: 'doughnut', + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 425px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 425, dh: 425, + rw: 425, rh: 425, + }); + }); + + it('should use "user" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 405px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 405, dh: 135, + rw: 405, rh: 135, + }); + }); + + it('should not apply aspect ratio when height specified', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 400px; height: 410px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 400, dh: 410, + rw: 400, rh: 410, + }); + }); + }); + + describe('config.options.responsive: false', function() { + it('should use default canvas size for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + }); + + it('should use canvas attributes for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '', + width: 305, + height: 245, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 305, dh: 245, + rw: 305, rh: 245, + }); + }); + + it('should use canvas style for render and display sizes (if no attributes)', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 345, rh: 125, + }); + }); + + it('should use attributes for the render size and style for the display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px;', + width: 165, + height: 85, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 165, rh: 85, + }); + }); + + it('should not inject the resizer element', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }); + + var wrapper = chart.chart.canvas.parentNode; + expect(wrapper.childNodes.length).toBe(1); + expect(wrapper.firstChild.tagName).toBe('CANVAS'); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: false)', function() { + it('should fill parent width and height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + }); + + it('should resize the canvas when parent width changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.width = '455px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 350, + rw: 455, rh: 350, + }); + + wrapper.style.width = '150px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 350, + rw: 150, rh: 350, + }); + + done(); + }); + }); + }); + + it('should resize the canvas when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '455px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 455, + rw: 300, rh: 455, + }); + + wrapper.style.height = '150px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + done(); + }); + }); + }); + + it('should not include parent padding when resizing the canvas', function(done) { + var chart = acquireChart({ + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'padding: 50px; width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '355px'; + wrapper.style.width = '455px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + done(); + }); + }); + + it('should resize the canvas when the canvas display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'display: none;' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + var canvas = chart.chart.canvas; + canvas.style.display = 'block'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + done(); + }); + }); + + it('should resize the canvas when the wrapper display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'display: none; width: 460px; height: 380px' + } + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.display = 'block'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 460, dh: 380, + rw: 460, rh: 380, + }); + + done(); + }); + }); + + // https://github.com/chartjs/Chart.js/issues/3521 + it('should resize the canvas after the wrapper has been re-attached to the DOM', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + var parent = wrapper.parentNode; + parent.removeChild(wrapper); + parent.appendChild(wrapper); + wrapper.style.height = '355px'; + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 355, + rw: 320, rh: 355, + }); + + parent.removeChild(wrapper); + wrapper.style.width = '455px'; + parent.appendChild(wrapper); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + done(); + }); + }); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: true)', function() { + it('should fill parent width and use aspect ratio to calculate height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 490, + rw: 300, rh: 490, + }); + }); + + it('should resize the canvas with correct aspect ratio when parent width changes', function(done) { + var chart = acquireChart({ + type: 'line', // AR == 2 + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.width = '450px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 450, dh: 225, + rw: 450, rh: 225, + }); + + wrapper.style.width = '150px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 75, + rw: 150, rh: 75, + }); + + done(); + }); + }); + }); + + it('should not resize the canvas when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '455px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + wrapper.style.height = '150px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + done(); + }); + }); + }); + }); + + describe('Retina scale (a.k.a. device pixel ratio)', function() { + beforeEach(function() { + this.devicePixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 3; + }); + + afterEach(function() { + window.devicePixelRatio = this.devicePixelRatio; + }); + + // see https://github.com/chartjs/Chart.js/issues/3575 + it ('should scale the render size but not the "implicit" display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + width: 320, + height: 240, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 240, + rw: 960, rh: 720, + }); + }); + + it ('should scale the render size but not the "explicit" display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 320px; height: 240px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 240, + rw: 960, rh: 720, + }); + }); + }); + + describe('controller.destroy', function() { + it('should reset context to default values', function() { + var chart = acquireChart({}); + var context = chart.chart.ctx; + + chart.destroy(); + + // https://www.w3.org/TR/2dcontext/#conformance-requirements + Chart.helpers.each({ + fillStyle: '#000000', + font: '10px sans-serif', + lineJoin: 'miter', + lineCap: 'butt', + lineWidth: 1, + miterLimit: 10, + shadowBlur: 0, + shadowColor: 'rgba(0, 0, 0, 0)', + shadowOffsetX: 0, + shadowOffsetY: 0, + strokeStyle: '#000000', + textAlign: 'start', + textBaseline: 'alphabetic' + }, function(value, key) { + expect(context[key]).toBe(value); + }); + }); + + it('should restore canvas initial values', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + width: 180, + style: 'width: 512px; height: 480px' + }, + wrapper: { + style: 'width: 450px; height: 450px; position: relative' + } + }); + + var canvas = chart.chart.canvas; + var wrapper = canvas.parentNode; + wrapper.style.width = '475px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 475, dh: 450, + rw: 475, rh: 450, + }); + + chart.destroy(); + + expect(canvas.getAttribute('width')).toBe('180'); + expect(canvas.getAttribute('height')).toBe(null); + expect(canvas.style.width).toBe('512px'); + expect(canvas.style.height).toBe('480px'); + expect(canvas.style.display).toBe(''); + + done(); + }); + }); + + it('should remove the resizer element when responsive: true', function() { + var chart = acquireChart({ + options: { + responsive: true + } + }); + + var wrapper = chart.chart.canvas.parentNode; + var resizer = wrapper.firstChild; + + expect(wrapper.childNodes.length).toBe(2); + expect(resizer.tagName).toBe('IFRAME'); + + chart.destroy(); + + expect(wrapper.childNodes.length).toBe(1); + expect(wrapper.firstChild.tagName).toBe('CANVAS'); + }); + }); + + describe('controller.reset', function() { + it('should reset the chart elements', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 0] + }] + }, + options: { + responsive: true + } + }); + + var meta = chart.getDatasetMeta(0); + + // 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[2]._model.y).toBe(32); + expect(meta.data[3]._model.y).toBe(484); + + chart.reset(); + + // For a line chart, the animation state is the bottom + expect(meta.data[0]._model.y).toBe(484); + expect(meta.data[1]._model.y).toBe(484); + expect(meta.data[2]._model.y).toBe(484); + expect(meta.data[3]._model.y).toBe(484); + }); + }); +}); diff --git a/test/core.datasetController.tests.js b/test/core.datasetController.tests.js new file mode 100644 index 000000000..5ab9dd5f3 --- /dev/null +++ b/test/core.datasetController.tests.js @@ -0,0 +1,188 @@ +describe('Chart.DatasetController', function() { + it('should listen for dataset data insertions or removals', function() { + var data = [0, 1, 2, 3, 4, 5]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var controller = chart.getDatasetMeta(0).controller; + var methods = [ + 'onDataPush', + 'onDataPop', + 'onDataShift', + 'onDataSplice', + 'onDataUnshift' + ]; + + methods.forEach(function(method) { + spyOn(controller, method); + }); + + data.push(6, 7, 8); + data.push(9); + data.pop(); + data.shift(); + data.shift(); + data.shift(); + data.splice(1, 4, 10, 11); + data.unshift(12, 13, 14, 15); + data.unshift(16, 17); + + [2, 1, 3, 1, 2].forEach(function(expected, index) { + expect(controller[methods[index]].calls.count()).toBe(expected); + }); + }); + + it('should synchronize metadata when data are inserted or removed', function() { + var data = [0, 1, 2, 3, 4, 5]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var meta = chart.getDatasetMeta(0); + var first, second, last; + + first = meta.data[0]; + last = meta.data[5]; + data.push(6, 7, 8); + data.push(9); + expect(meta.data.length).toBe(10); + expect(meta.data[0]).toBe(first); + expect(meta.data[5]).toBe(last); + + last = meta.data[9]; + data.pop(); + expect(meta.data.length).toBe(9); + expect(meta.data[0]).toBe(first); + expect(meta.data.indexOf(last)).toBe(-1); + + last = meta.data[8]; + data.shift(); + data.shift(); + data.shift(); + expect(meta.data.length).toBe(6); + expect(meta.data.indexOf(first)).toBe(-1); + expect(meta.data[5]).toBe(last); + + first = meta.data[0]; + second = meta.data[1]; + last = meta.data[5]; + data.splice(1, 4, 10, 11); + expect(meta.data.length).toBe(4); + expect(meta.data[0]).toBe(first); + expect(meta.data[3]).toBe(last); + expect(meta.data.indexOf(second)).toBe(-1); + + data.unshift(12, 13, 14, 15); + data.unshift(16, 17); + expect(meta.data.length).toBe(10); + expect(meta.data[6]).toBe(first); + expect(meta.data[9]).toBe(last); + }); + + it('should re-synchronize metadata when the data object reference changes', function() { + var data0 = [0, 1, 2, 3, 4, 5]; + var data1 = [6, 7, 8]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data0 + }] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(6); + + chart.data.datasets[0].data = data1; + chart.update(); + + expect(meta.data.length).toBe(3); + + data1.push(9, 10, 11); + expect(meta.data.length).toBe(6); + }); + + it('should re-synchronize metadata when data are unusually altered', function() { + var data = [0, 1, 2, 3, 4, 5]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(6); + + data.length = 2; + chart.update(); + + expect(meta.data.length).toBe(2); + + data.length = 42; + chart.update(); + + expect(meta.data.length).toBe(42); + }); + + it('should cleanup attached properties when the reference changes or when the chart is destroyed', function() { + var data0 = [0, 1, 2, 3, 4, 5]; + var data1 = [6, 7, 8]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data0 + }] + } + }); + + var hooks = ['push', 'pop', 'shift', 'splice', 'unshift']; + + expect(data0._chartjs).toBeDefined(); + hooks.forEach(function(hook) { + expect(data0[hook]).not.toBe(Array.prototype[hook]); + }); + + expect(data1._chartjs).not.toBeDefined(); + hooks.forEach(function(hook) { + expect(data1[hook]).toBe(Array.prototype[hook]); + }); + + chart.data.datasets[0].data = data1; + chart.update(); + + expect(data0._chartjs).not.toBeDefined(); + hooks.forEach(function(hook) { + expect(data0[hook]).toBe(Array.prototype[hook]); + }); + + expect(data1._chartjs).toBeDefined(); + hooks.forEach(function(hook) { + expect(data1[hook]).not.toBe(Array.prototype[hook]); + }); + + chart.destroy(); + + expect(data1._chartjs).not.toBeDefined(); + hooks.forEach(function(hook) { + expect(data1[hook]).toBe(Array.prototype[hook]); + }); + }); +}); diff --git a/test/core.element.tests.js b/test/core.element.tests.js index 6443de099..7d194562e 100644 --- a/test/core.element.tests.js +++ b/test/core.element.tests.js @@ -25,9 +25,9 @@ describe('Core element tests', function() { element._model.numberProp = 100; element._model.numberProp2 = 250; element._model._underscoreProp = 200; - element._model.stringProp = 'def' + element._model.stringProp = 'def'; element._model.newStringProp = 'newString'; - element._model.colorProp = 'rgb(255, 255, 0)' + element._model.colorProp = 'rgb(255, 255, 0)'; element.transition(0.25); expect(element._view).toEqual({ @@ -42,4 +42,4 @@ describe('Core element tests', function() { colorProp: 'rgb(64, 64, 0)', }); }); -}); \ No newline at end of file +}); diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index 8ca5bb59c..4a7e9f555 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -7,7 +7,7 @@ describe('Core helper tests', function() { }); it('should iterate over an array and pass the extra data to that function', function() { - var testData = [0, 9, "abc"]; + var testData = [0, 9, 'abc']; var scope = {}; // fake out the scope and ensure that 'this' is the correct thing helpers.each(testData, function(item, index) { @@ -121,7 +121,7 @@ describe('Core helper tests', function() { expect(merged).toEqual({ valueProp: 5, valueProp2: null, - arrayProp: ['a', 'c', 3, 4, 5, 6], + arrayProp: ['a', 'c'], objectProp: { prop1: 'c', prop2: 56, @@ -130,37 +130,6 @@ describe('Core helper tests', function() { }); }); - it('should merge arrays containing objects', function() { - var baseConfig = { - arrayProp: [{ - prop1: 'abc', - prop2: 56 - }], - }; - - var toMerge = { - arrayProp: [{ - prop1: 'myProp1', - prop3: 'prop3' - }, 2, { - prop1: 'myProp1' - }], - }; - - var merged = helpers.configMerge(baseConfig, toMerge); - expect(merged).toEqual({ - arrayProp: [{ - prop1: 'myProp1', - prop2: 56, - prop3: 'prop3' - }, - 2, { - prop1: 'myProp1' - } - ], - }); - }); - it('should merge scale configs', function() { var baseConfig = { scales: { @@ -214,7 +183,7 @@ describe('Core helper tests', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label @@ -222,12 +191,12 @@ describe('Core helper tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "right", + position: 'right', scaleLabel: { labelString: '', display: false, @@ -250,7 +219,7 @@ describe('Core helper tests', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label, @@ -258,12 +227,12 @@ describe('Core helper tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "left", + position: 'left', scaleLabel: { labelString: '', display: false, @@ -302,7 +271,7 @@ describe('Core helper tests', function() { it('should filter an array', function() { var data = [-10, 0, 6, 0, 7]; var callback = function(item) { - return item > 2 + return item > 2; }; expect(helpers.where(data, callback)).toEqual([6, 7]); expect(helpers.findNextWhere(data, callback)).toEqual(6); @@ -431,19 +400,19 @@ describe('Core helper tests', function() { it('should spline curves with monotone cubic interpolation', function() { var dataPoints = [ - { _model: { x: 0, y: 0, skip: false } }, - { _model: { x: 3, y: 6, skip: false } }, - { _model: { x: 9, y: 6, skip: false } }, - { _model: { x: 12, y: 60, skip: false } }, - { _model: { x: 15, y: 60, skip: false } }, - { _model: { x: 18, y: 120, skip: false } }, - { _model: { x: null, y: null, skip: true } }, - { _model: { x: 21, y: 180, skip: false } }, - { _model: { x: 24, y: 120, skip: false } }, - { _model: { x: 27, y: 125, skip: false } }, - { _model: { x: 30, y: 105, skip: false } }, - { _model: { x: 33, y: 110, skip: false } }, - { _model: { x: 36, y: 170, skip: false } } + {_model: {x: 0, y: 0, skip: false}}, + {_model: {x: 3, y: 6, skip: false}}, + {_model: {x: 9, y: 6, skip: false}}, + {_model: {x: 12, y: 60, skip: false}}, + {_model: {x: 15, y: 60, skip: false}}, + {_model: {x: 18, y: 120, skip: false}}, + {_model: {x: null, y: null, skip: true}}, + {_model: {x: 21, y: 180, skip: false}}, + {_model: {x: 24, y: 120, skip: false}}, + {_model: {x: 27, y: 125, skip: false}}, + {_model: {x: 30, y: 105, skip: false}}, + {_model: {x: 33, y: 110, skip: false}}, + {_model: {x: 36, y: 170, skip: false}} ]; helpers.splineCurveMonotone(dataPoints); expect(dataPoints).toEqual([{ @@ -611,13 +580,13 @@ describe('Core helper tests', function() { it('should return the width of the longest text in an Array and 2D Array', function() { var context = window.createMockContext(); var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; - var arrayOfThings_1D = ['FooBar','Bar']; - var arrayOfThings_2D = [['FooBar_1','Bar_2'],'Foo_1']; - + var arrayOfThings1D = ['FooBar', 'Bar']; + var arrayOfThings2D = [['FooBar_1', 'Bar_2'], 'Foo_1']; - // Regardless 'FooBar' is the longest label it should return (charcters * 10) - expect(helpers.longestText(context, font, arrayOfThings_1D, {})).toEqual(60); - expect(helpers.longestText(context, font, arrayOfThings_2D, {})).toEqual(80); + + // Regardless 'FooBar' is the longest label it should return (characters * 10) + expect(helpers.longestText(context, font, arrayOfThings1D, {})).toEqual(60); + expect(helpers.longestText(context, font, arrayOfThings2D, {})).toEqual(80); // We check to make sure we made the right calls to the canvas. expect(context.getCalls()).toEqual([{ name: 'measureText', @@ -660,14 +629,14 @@ describe('Core helper tests', function() { }); it('count look at all the labels and return maximum number of lines', function() { - var context = window.createMockContext(); - var arrayOfThings_1 = ['Foo','Bar']; - var arrayOfThings_2 = [['Foo','Bar'],'Foo']; - var arrayOfThings_3 = [['Foo','Bar','Boo'],['Foo','Bar'],'Foo']; + window.createMockContext(); + var arrayOfThings1 = ['Foo', 'Bar']; + var arrayOfThings2 = [['Foo', 'Bar'], 'Foo']; + var arrayOfThings3 = [['Foo', 'Bar', 'Boo'], ['Foo', 'Bar'], 'Foo']; - expect(helpers.numberOfLabelLines(arrayOfThings_1)).toEqual(1); - expect(helpers.numberOfLabelLines(arrayOfThings_2)).toEqual(2); - expect(helpers.numberOfLabelLines(arrayOfThings_3)).toEqual(3); + expect(helpers.numberOfLabelLines(arrayOfThings1)).toEqual(1); + expect(helpers.numberOfLabelLines(arrayOfThings2)).toEqual(2); + expect(helpers.numberOfLabelLines(arrayOfThings3)).toEqual(3); }); it('should draw a rounded rectangle', function() { @@ -707,14 +676,14 @@ describe('Core helper tests', function() { }, { name: 'closePath', args: [] - }]) + }]); }); it ('should get the maximum width and height for a node', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); @@ -731,14 +700,14 @@ describe('Core helper tests', function() { it ('should get the maximum width of a node that has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-width style var innerDiv = document.createElement('div'); - innerDiv.style.maxWidth = "150px"; + innerDiv.style.maxWidth = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(150); @@ -749,14 +718,14 @@ describe('Core helper tests', function() { it ('should get the maximum height of a node that has a max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-height style var innerDiv = document.createElement('div'); - innerDiv.style.maxHeight = "150px"; + innerDiv.style.maxHeight = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -767,14 +736,14 @@ describe('Core helper tests', function() { it ('should get the maximum width of a node when the parent has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-width style var parentDiv = document.createElement('div'); - parentDiv.style.maxWidth = "150px"; + parentDiv.style.maxWidth = '150px'; div.appendChild(parentDiv); // Create the div we want to get the max size for @@ -789,19 +758,19 @@ describe('Core helper tests', function() { it ('should get the maximum height of a node when the parent has a max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-height style var parentDiv = document.createElement('div'); - parentDiv.style.maxHeight = "150px"; + parentDiv.style.maxHeight = '150px'; div.appendChild(parentDiv); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); - innerDiv.style.height = "300px"; // make it large + innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -812,14 +781,14 @@ describe('Core helper tests', function() { it ('should get the maximum width of a node that has a percentage max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-width style var innerDiv = document.createElement('div'); - innerDiv.style.maxWidth = "50%"; + innerDiv.style.maxWidth = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(100); @@ -830,14 +799,14 @@ describe('Core helper tests', function() { it ('should get the maximum height of a node that has a percentage max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-height style var innerDiv = document.createElement('div'); - innerDiv.style.maxHeight = "50%"; + innerDiv.style.maxHeight = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -848,14 +817,14 @@ describe('Core helper tests', function() { it ('should get the maximum width of a node when the parent has a percentage max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-width style var parentDiv = document.createElement('div'); - parentDiv.style.maxWidth = "50%"; + parentDiv.style.maxWidth = '50%'; div.appendChild(parentDiv); // Create the div we want to get the max size for @@ -870,18 +839,18 @@ describe('Core helper tests', function() { it ('should get the maximum height of a node when the parent has a percentage max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-height style var parentDiv = document.createElement('div'); - parentDiv.style.maxHeight = "50%"; + parentDiv.style.maxHeight = '50%'; div.appendChild(parentDiv); var innerDiv = document.createElement('div'); - innerDiv.style.height = "300px"; // make it large + innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -922,7 +891,7 @@ describe('Core helper tests', function() { expect(backgroundColor instanceof CanvasPattern).toBe(true); done(); - } + }; }); it('should return a modified version of color when called with a color', function() { diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js new file mode 100644 index 000000000..7d8e339ca --- /dev/null +++ b/test/core.interaction.tests.js @@ -0,0 +1,797 @@ +// Tests of the interaction handlers in Core.Interaction + +// Test the rectangle element +describe('Core.Interaction', function() { + describe('point mode', function() { + it ('should return all items under the point', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 20, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + var point = meta0.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.point(chartInstance, evt); + expect(elements).toEqual([point, meta1.data[1]]); + }); + + it ('should return an empty array when no items are found', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 20, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event at (0, 0) + var node = chartInstance.chart.canvas; + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: 0, + clientY: 0, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.point(chartInstance, evt); + expect(elements).toEqual([]); + }); + }); + + describe('index mode', function() { + it ('should return all items at the same index', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + var point = meta0.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.index(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([point, meta1.data[1]]); + }); + + it ('should return all items at the same index when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.index(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([meta0.data[0], meta1.data[0]]); + }); + }); + + describe('dataset mode', function() { + it ('should return all items in the dataset of the first item found', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(0); + var point = meta.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.dataset(chartInstance, evt, {intersect: true}); + expect(elements).toEqual(meta.data); + }); + + it ('should return all items in the dataset of the first item found when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.dataset(chartInstance, evt, {intersect: false}); + + var meta = chartInstance.getDatasetMeta(1); + expect(elements).toEqual(meta.data); + }); + }); + + describe('nearest mode', function() { + it ('should return the nearest item', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(1); + var node = chartInstance.chart.canvas; + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: 0, + clientY: 0, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([meta.data[0]]); + }); + + it ('should return the smallest item if more than 1 are at the same distance', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 5, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return the lowest dataset index if size and area are the same', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([meta0.data[1]]); + }); + }); + + describe('nearest intersect mode', function() { + it ('should return the nearest item', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(1); + var point = meta.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._view.x + 15, + clientY: rect.top + point._view.y, + currentTarget: node + }; + + // Nothing intersects so find nothing + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([]); + + evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._view.x, + clientY: rect.top + point._view.y, + currentTarget: node + }; + elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([point]); + }); + + it ('should return the nearest item even if 2 intersect', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 39, 30], + pointRadius: [5, 30, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return the smallest item if more than 1 are at the same distance', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 5, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return the item at the lowest dataset index if distance and area are the same', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([meta0.data[1]]); + }); + }); + + describe('x mode', function() { + it('should return items at the same x value when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + + evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x + 20, // out of range + clientY: rect.top, + currentTarget: node + }; + + elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([]); + }); + + it('should return items at the same x value when intersect is true', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([]); // we don't intersect anything + + evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); + }); + + describe('y mode', function() { + it('should return items at the same y value when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top + pt.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); + + evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y + 20, // out of range + currentTarget: node + }; + + elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: false}); + expect(elements).toEqual([]); + }); + + it('should return items at the same y value when intersect is true', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top + pt.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([]); // we don't intersect anything + + evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: true}); + expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); + }); + }); +}); diff --git a/test/core.layoutService.tests.js b/test/core.layoutService.tests.js index 860b4b649..951ea234e 100644 --- a/test/core.layoutService.tests.js +++ b/test/core.layoutService.tests.js @@ -1,11 +1,14 @@ // Tests of the scale service describe('Test the layout service', function() { - it('should fit a simple chart with 2 scales', function() { + // 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 + xit('should fit a simple chart with 2 scales', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ - { data: [10, 5, 0, 25, 78, -10] } + {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, @@ -48,12 +51,12 @@ describe('Test the layout service', function() { expect(chart.scales.yScale.labelRotation).toBeCloseTo(0); }); - it('should fit scales that are in the top and right positions', function() { + xit('should fit scales that are in the top and right positions', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ - { data: [10, 5, 0, 25, 78, -10] } + {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, @@ -121,10 +124,10 @@ describe('Test the layout service', function() { expect(chart.scale.right).toBeCloseToPixel(512); expect(chart.scale.top).toBeCloseToPixel(32); expect(chart.scale.width).toBeCloseToPixel(512); - expect(chart.scale.height).toBeCloseToPixel(480) + expect(chart.scale.height).toBeCloseToPixel(480); }); - it('should fit multiple axes in the same position', function() { + xit('should fit multiple axes in the same position', function() { var chart = window.acquireChart({ type: 'bar', data: { @@ -185,7 +188,7 @@ describe('Test the layout service', function() { expect(chart.scales.yScale2.labelRotation).toBeCloseTo(0); }); - it ('should fix a full width box correctly', function() { + xit ('should fix a full width box correctly', function() { var chart = window.acquireChart({ type: 'bar', data: { @@ -239,4 +242,152 @@ describe('Test the layout service', function() { expect(chart.scales.yScale.right).toBeCloseToPixel(45); expect(chart.scales.yScale.top).toBeCloseToPixel(60); }); + + describe('padding settings', function() { + it('should apply a single padding to all dimensions', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale', + type: 'category', + display: false + }], + yAxes: [{ + id: 'yScale', + type: 'linear', + display: false + }] + }, + legend: { + display: false + }, + title: { + display: false + }, + layout: { + padding: 10 + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(140); + expect(chart.chartArea.left).toBeCloseToPixel(10); + expect(chart.chartArea.right).toBeCloseToPixel(240); + expect(chart.chartArea.top).toBeCloseToPixel(10); + }); + + it('should apply padding in all positions', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale', + type: 'category', + display: false + }], + yAxes: [{ + id: 'yScale', + type: 'linear', + display: false + }] + }, + legend: { + display: false + }, + title: { + display: false + }, + layout: { + padding: { + left: 5, + right: 15, + top: 8, + bottom: 12 + } + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(138); + expect(chart.chartArea.left).toBeCloseToPixel(5); + expect(chart.chartArea.right).toBeCloseToPixel(235); + expect(chart.chartArea.top).toBeCloseToPixel(8); + }); + + it('should default to 0 padding if no dimensions specified', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale', + type: 'category', + display: false + }], + yAxes: [{ + id: 'yScale', + type: 'linear', + display: false + }] + }, + legend: { + display: false + }, + title: { + display: false + }, + layout: { + padding: {} + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(150); + expect(chart.chartArea.left).toBeCloseToPixel(0); + expect(chart.chartArea.right).toBeCloseToPixel(250); + expect(chart.chartArea.top).toBeCloseToPixel(0); + }); + }); }); diff --git a/test/core.legend.tests.js b/test/core.legend.tests.js index caa35883a..6d539fd1e 100644 --- a/test/core.legend.tests.js +++ b/test/core.legend.tests.js @@ -118,20 +118,21 @@ describe('Legend block tests', function() { expect(chart.legend.legendHitBoxes.length).toBe(3); - [ { h: 12, l: 101, t: 10, w: 93 }, - { h: 12, l: 205, t: 10, w: 93 }, - { h: 12, l: 308, t: 10, w: 93 } + [ + {h: 12, l: 101, t: 10, w: 93}, + {h: 12, l: 205, t: 10, w: 93}, + {h: 12, l: 308, t: 10, w: 93} ].forEach(function(expected, i) { expect(chart.legend.legendHitBoxes[i].height).toBeCloseToPixel(expected.h); expect(chart.legend.legendHitBoxes[i].left).toBeCloseToPixel(expected.l); expect(chart.legend.legendHitBoxes[i].top).toBeCloseToPixel(expected.t); expect(chart.legend.legendHitBoxes[i].width).toBeCloseToPixel(expected.w); - }) + }); // NOTE(SB) We should get ride of the following tests and use image diff instead. // For now, as discussed with Evert Timberg, simply comment out. // See http://humblesoftware.github.io/js-imagediff/test.html - /*chart.legend.ctx = window.createMockContext(); + /* chart.legend.ctx = window.createMockContext(); chart.update(); expect(chart.legend.ctx .getCalls()).toEqual([{ diff --git a/test/core.plugin.tests.js b/test/core.plugin.tests.js index 0d4186af8..59a71b1c5 100644 --- a/test/core.plugin.tests.js +++ b/test/core.plugin.tests.js @@ -75,24 +75,52 @@ describe('Chart.plugins', function() { }; Chart.plugins.register(myplugin); - Chart.plugins.notify('trigger', [{ count: 10 }]); + Chart.plugins.notify('trigger', [{count: 10}]); expect(myplugin.count).toBe(10); }); it('should return TRUE if no plugin explicitly returns FALSE', function() { - Chart.plugins.register({ check: function() {} }); - Chart.plugins.register({ check: function() { return; } }); - Chart.plugins.register({ check: function() { return null; } }); - Chart.plugins.register({ check: function() { return 42 } }); + Chart.plugins.register({ + check: function() {} + }); + Chart.plugins.register({ + check: function() { + return; + } + }); + Chart.plugins.register({ + check: function() { + return null; + } + }); + Chart.plugins.register({ + check: function() { + return 42; + } + }); var res = Chart.plugins.notify('check'); expect(res).toBeTruthy(); }); it('should return FALSE if no plugin explicitly returns FALSE', function() { - Chart.plugins.register({ check: function() {} }); - Chart.plugins.register({ check: function() { return; } }); - Chart.plugins.register({ check: function() { return false; } }); - Chart.plugins.register({ check: function() { return 42 } }); + Chart.plugins.register({ + check: function() {} + }); + Chart.plugins.register({ + check: function() { + return; + } + }); + Chart.plugins.register({ + check: function() { + return false; + } + }); + Chart.plugins.register({ + check: function() { + return 42; + } + }); var res = Chart.plugins.notify('check'); expect(res).toBeFalsy(); }); diff --git a/test/core.scaleService.tests.js b/test/core.scaleService.tests.js index ca0eaf20d..aacac9879 100644 --- a/test/core.scaleService.tests.js +++ b/test/core.scaleService.tests.js @@ -15,7 +15,7 @@ describe('Test the scale service', function() { expect(Chart.scaleService.getScaleDefaults(type)).toEqual(jasmine.objectContaining({ testProp: true })); - + Chart.scaleService.updateScaleDefaults(type, { testProp: 'red', newProp: 42 diff --git a/test/core.title.tests.js b/test/core.title.tests.js index ce1c437ea..704605278 100644 --- a/test/core.title.tests.js +++ b/test/core.title.tests.js @@ -14,14 +14,14 @@ describe('Title block tests', function() { fontStyle: 'bold', padding: 10, text: '' - }) + }); }); it('should update correctly', function() { var chart = {}; var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; var title = new Chart.Title({ chart: chart, @@ -50,7 +50,7 @@ describe('Title block tests', function() { var chart = {}; var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; options.position = 'left'; var title = new Chart.Title({ @@ -81,7 +81,7 @@ describe('Title block tests', function() { var context = window.createMockContext(); var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; var title = new Chart.Title({ chart: chart, @@ -118,7 +118,7 @@ describe('Title block tests', function() { args: [0] }, { name: 'fillText', - args: ['My title', 0, 0] + args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] @@ -130,7 +130,7 @@ describe('Title block tests', function() { var context = window.createMockContext(); var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; options.position = 'left'; var title = new Chart.Title({ @@ -168,7 +168,7 @@ describe('Title block tests', function() { args: [-0.5 * Math.PI] }, { name: 'fillText', - args: ['My title', 0, 0] + args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] @@ -201,10 +201,10 @@ describe('Title block tests', function() { args: [0.5 * Math.PI] }, { name: 'fillText', - args: ['My title', 0, 0] + args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] }]); }); -}); \ No newline at end of file +}); diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 21d25ebaa..1a2d8fb0b 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -1,117 +1,209 @@ // Test the rectangle element -describe('tooltip tests', function() { - it('Should display in label mode', function() { - var chartInstance = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - label: 'Dataset 1', - data: [10, 20, 30], - pointHoverBorderColor: 'rgb(255, 0, 0)', - pointHoverBackgroundColor: 'rgb(0, 255, 0)' - }, { - label: 'Dataset 2', - data: [40, 40, 40], - pointHoverBorderColor: 'rgb(0, 0, 255)', - pointHoverBackgroundColor: 'rgb(0, 255, 255)' - }], - labels: ['Point 1', 'Point 2', 'Point 3'] - }, - options: { - tooltips: { - mode: 'label' +describe('Core.Tooltip', function() { + describe('index mode', function() { + it('Should only use x distance when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'index', + intersect: false + } } - } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(0); + var point = meta.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: 0 + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + var globalDefaults = Chart.defaults.global; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xPadding: 6, + yPadding: 6, + xAlign: 'left', + yAlign: 'center', + + // Body + bodyFontColor: '#fff', + _bodyFontFamily: globalDefaults.defaultFontFamily, + _bodyFontStyle: globalDefaults.defaultFontStyle, + _bodyAlign: 'left', + bodyFontSize: globalDefaults.defaultFontSize, + bodySpacing: 2, + + // Title + titleFontColor: '#fff', + _titleFontFamily: globalDefaults.defaultFontFamily, + _titleFontStyle: 'bold', + titleFontSize: globalDefaults.defaultFontSize, + _titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFontColor: '#fff', + _footerFontFamily: globalDefaults.defaultFontFamily, + _footerFontStyle: 'bold', + footerFontSize: globalDefaults.defaultFontSize, + _footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + opacity: 1, + legendColorBackground: '#fff', + displayColors: true, + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 1: 20'], + after: [] + }, { + before: [], + lines: ['Dataset 2: 40'], + after: [] + }], + afterBody: [], + footer: [], + caretPadding: 2, + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }, { + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + })); + + expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.y).toBeCloseToPixel(155); }); - // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(0); - var point = meta.data[1]; + it('Should only display if intersecting if intersect is set', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'index', + intersect: true + } + } + }); - var node = chartInstance.chart.canvas; - var rect = node.getBoundingClientRect(); + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(0); + var point = meta.data[1]; - var evt = new MouseEvent('mousemove', { - view: window, - bubbles: true, - cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: 0 + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + var globalDefaults = Chart.defaults.global; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xPadding: 6, + yPadding: 6, + + // Body + bodyFontColor: '#fff', + _bodyFontFamily: globalDefaults.defaultFontFamily, + _bodyFontStyle: globalDefaults.defaultFontStyle, + _bodyAlign: 'left', + bodyFontSize: globalDefaults.defaultFontSize, + bodySpacing: 2, + + // Title + titleFontColor: '#fff', + _titleFontFamily: globalDefaults.defaultFontFamily, + _titleFontStyle: 'bold', + titleFontSize: globalDefaults.defaultFontSize, + _titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFontColor: '#fff', + _footerFontFamily: globalDefaults.defaultFontFamily, + _footerFontStyle: 'bold', + footerFontSize: globalDefaults.defaultFontSize, + _footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + opacity: 0, + legendColorBackground: '#fff', + displayColors: true, + })); }); - - // Manully trigger rather than having an async test - node.dispatchEvent(evt); - - // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; - var globalDefaults = Chart.defaults.global; - - expect(tooltip._view).toEqual(jasmine.objectContaining({ - // Positioning - xPadding: 6, - yPadding: 6, - xAlign: 'left', - yAlign: 'center', - - // Body - bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: 'left', - bodyFontSize: globalDefaults.defaultFontSize, - bodySpacing: 2, - - // Title - titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', - titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: 'left', - titleSpacing: 2, - titleMarginBottom: 6, - - // Footer - footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', - footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: 'left', - footerSpacing: 2, - footerMarginTop: 6, - - // Appearance - caretSize: 5, - cornerRadius: 6, - backgroundColor: 'rgba(0,0,0,0.8)', - opacity: 1, - legendColorBackground: '#fff', - - // Text - title: ['Point 2'], - beforeBody: [], - body: [{ - before: [], - lines: ['Dataset 1: 20'], - after: [] - }, { - before: [], - lines: ['Dataset 2: 40'], - after: [] - }], - afterBody: [], - footer: [], - caretPadding: 2, - labelColors: [{ - borderColor: 'rgb(255, 0, 0)', - backgroundColor: 'rgb(0, 255, 0)' - }, { - borderColor: 'rgb(0, 0, 255)', - backgroundColor: 'rgb(0, 255, 255)' - }] - })); - - expect(tooltip._view.x).toBeCloseToPixel(269); - expect(tooltip._view.y).toBeCloseToPixel(155); }); it('Should display in single mode', function() { @@ -153,7 +245,7 @@ describe('tooltip tests', function() { clientY: rect.top + point._model.y }); - // Manully trigger rather than having an async test + // Manually trigger rather than having an async test node.dispatchEvent(evt); // Check and see if tooltip was displayed @@ -199,6 +291,7 @@ describe('tooltip tests', function() { backgroundColor: 'rgba(0,0,0,0.8)', opacity: 1, legendColorBackground: '#fff', + displayColors: true, // Text title: ['Point 2'], @@ -211,7 +304,10 @@ describe('tooltip tests', function() { afterBody: [], footer: [], caretPadding: 2, - labelColors: [] + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }] })); expect(tooltip._view.x).toBeCloseToPixel(269); @@ -246,7 +342,7 @@ describe('tooltip tests', function() { return 'title'; }, afterTitle: function() { - return 'afterTitle' + return 'afterTitle'; }, beforeBody: function() { return 'beforeBody'; @@ -270,7 +366,7 @@ describe('tooltip tests', function() { return 'footer'; }, afterFooter: function() { - return 'afterFooter' + return 'afterFooter'; } } } @@ -292,7 +388,7 @@ describe('tooltip tests', function() { clientY: rect.top + point._model.y }); - // Manully trigger rather than having an async test + // Manually trigger rather than having an async test node.dispatchEvent(evt); // Check and see if tooltip was displayed @@ -398,9 +494,6 @@ describe('tooltip tests', function() { var meta0 = chartInstance.getDatasetMeta(0); var point0 = meta0.data[1]; - var meta1 = chartInstance.getDatasetMeta(1); - var point1 = meta1.data[1]; - var node = chartInstance.chart.canvas; var rect = node.getBoundingClientRect(); @@ -412,7 +505,7 @@ describe('tooltip tests', function() { clientY: rect.top + point0._model.y }); - // Manully trigger rather than having an async test + // Manually trigger rather than having an async test node.dispatchEvent(evt); // Check and see if tooltip was displayed @@ -449,4 +542,136 @@ describe('tooltip tests', function() { expect(tooltip._view.x).toBeCloseToPixel(269); expect(tooltip._view.y).toBeCloseToPixel(155); }); + + it('should filter items from the tooltip using the callback', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)', + tooltipHidden: true + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'label', + filter: function(tooltipItem, data) { + // For testing purposes remove the first dataset that has a tooltipHidden property + return !data.datasets[tooltipItem.datasetIndex].tooltipHidden; + } + } + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var point0 = meta0.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point0._model.x, + clientY: rect.top + point0._model.y + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xAlign: 'left', + yAlign: 'center', + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 2: 40'], + after: [] + }], + afterBody: [], + footer: [], + labelColors: [{ + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + })); + }); + + it('Should have dataPoints', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'single' + } + } + }); + + // Trigger an event over top of the + var pointIndex = 1; + var datasetIndex = 0; + var meta = chartInstance.getDatasetMeta(datasetIndex); + var point = meta.data[pointIndex]; + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + + expect(tooltip._view instanceof Object).toBe(true); + expect(tooltip._view.dataPoints instanceof Array).toBe(true); + expect(tooltip._view.dataPoints.length).toEqual(1); + expect(tooltip._view.dataPoints[0].index).toEqual(pointIndex); + expect(tooltip._view.dataPoints[0].datasetIndex).toEqual(datasetIndex); + expect(tooltip._view.dataPoints[0].xLabel).toEqual( + chartInstance.config.data.labels[pointIndex] + ); + expect(tooltip._view.dataPoints[0].yLabel).toEqual( + chartInstance.config.data.datasets[datasetIndex].data[pointIndex] + ); + expect(tooltip._view.dataPoints[0].x).toBeCloseToPixel(point._model.x); + expect(tooltip._view.dataPoints[0].y).toBeCloseToPixel(point._model.y); + }); }); diff --git a/test/defaultConfig.tests.js b/test/defaultConfig.tests.js index 0123f420f..0e8599014 100644 --- a/test/defaultConfig.tests.js +++ b/test/defaultConfig.tests.js @@ -1,293 +1,296 @@ // Test the bubble chart default config -describe("Test the bubble chart default config", function() { - it('should reutrn correct tooltip strings', function() { - var config = Chart.defaults.bubble; +describe('Default Configs', function() { + describe('Bubble Chart', function() { + it('should return correct tooltip strings', function() { + var config = Chart.defaults.bubble; + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + label: 'My dataset', + data: [{ + x: 10, + y: 12, + r: 5 + }] + }] + }, + options: config + }); - // Title is always blank - expect(config.tooltips.callbacks.title()).toBe(''); + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[0]]; + chart.tooltip.update(); - // Item label - var data = { - datasets: [{ - label: 'My dataset', - data: [{ - x: 10, - y: 12, - r: 5 - }] - }] - }; - - var tooltipItem = { - datasetIndex: 0, - index: 0 - }; - - expect(config.tooltips.callbacks.label(tooltipItem, data)).toBe('My dataset: (10, 12, 5)'); - }); -}); - -describe('Test the doughnut chart default config', function() { - it('should return correct tooltip strings', function() { - var config = Chart.defaults.doughnut; - - // Title is always blank - expect(config.tooltips.callbacks.title()).toBe(''); - - // Item label - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, 30], - }] - }; - - var tooltipItem = { - datasetIndex: 0, - index: 1 - }; - - expect(config.tooltips.callbacks.label(tooltipItem, data)).toBe('label2: 20'); + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['My dataset: (10, 12, 5)'], + after: [] + }]); + }); }); - it('should return the correct html legend', function() { - var config = Chart.defaults.doughnut; + describe('Doughnut Chart', function() { + it('should return correct tooltip strings', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, 30], + }] + }, + options: config + }); - var chart = { - id: 'mychart', - data: { - labels: ['label1', 'label2'], - datasets: [{ - data: [10, 20], - backgroundColor: ['red', 'green'] - }] - } - }; - var expectedLegend = '
    • label1
    • label2
    '; + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); - expect(config.legendCallback(chart)).toBe(expectedLegend); - }); + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['label2: 20'], + after: [] + }]); + }); - it('should return correct legend label objects', function() { - var config = Chart.defaults.doughnut; - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'], - metaData: [{}, {}, {}] - }] - }; + it('should return correct tooltip string for a multiline label', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', ['row1', 'row2', 'row3'], 'label3'], + datasets: [{ + data: [10, 20, 30], + }] + }, + options: config + }); - var expected = [{ - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label2', - fillStyle: 'green', - hidden: false, - index: 1, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label3', - fillStyle: 'blue', - hidden: true, - index: 2, - strokeStyle: '#000', - lineWidth: 2 - }]; + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); - var chart = { - data: data, - options: { - elements: { - arc: { + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: [ + 'row1: 20', + 'row2', + 'row3' + ], + after: [] + }]); + }); + + it('should return the correct html legend', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2'], + datasets: [{ + data: [10, 20], + backgroundColor: ['red', 'green'] + }] + }, + options: config + }); + + var expectedLegend = '
    • label1
    • label2
    '; + expect(chart.generateLegend()).toBe(expectedLegend); + }); + + it('should return correct legend label objects', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' - } - } - } - }; - expect(config.legend.labels.generateLabels.call({ chart: chart }, data)).toEqual(expected); - }); + }] + }, + options: config + }); - it('should hide the correct arc when a legend item is clicked', function() { - var config = Chart.defaults.doughnut; + var expected = [{ + text: 'label1', + fillStyle: 'red', + hidden: false, + index: 0, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label2', + fillStyle: 'green', + hidden: false, + index: 1, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label3', + fillStyle: 'blue', + hidden: true, + index: 2, + strokeStyle: '#000', + lineWidth: 2 + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); - var legendItem = { - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0 - }; - - var chart = { - data: { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'] - }] - }, - update: function() {} - }; - - spyOn(chart, 'update'); - var scope = { - chart: chart - }; - - config.legend.onClick.call(scope, null, legendItem); - - expect(chart.data.datasets[0].metaHiddenData).toEqual([10]); - expect(chart.data.datasets[0].data).toEqual([NaN, 20, NaN]); - - expect(chart.update).toHaveBeenCalled(); - - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); - - // Should not toggle index 2 since there was never data for it - legendItem.index = 2; - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); - }); -}); - -describe('Test the polar area chart default config', function() { - it('should return correct tooltip strings', function() { - var config = Chart.defaults.polarArea; - - // Title is always blank - expect(config.tooltips.callbacks.title()).toBe(''); - - // Item label - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, 30], - }] - }; - - var tooltipItem = { - datasetIndex: 0, - index: 1, - yLabel: 20 - }; - - expect(config.tooltips.callbacks.label(tooltipItem, data)).toBe('label2: 20'); - }); - - it('should return the correct html legend', function() { - var config = Chart.defaults.polarArea; - - var chart = { - id: 'mychart', - data: { - labels: ['label1', 'label2'], - datasets: [{ - data: [10, 20], - backgroundColor: ['red', 'green'] - }] - } - }; - var expectedLegend = '
    • label1
    • label2
    '; - - expect(config.legendCallback(chart)).toBe(expectedLegend); - }); - - it('should return correct legend label objects', function() { - var config = Chart.defaults.polarArea; - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'], - metaData: [{}, {}, {}] - }] - }; - - var expected = [{ - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label2', - fillStyle: 'green', - hidden: false, - index: 1, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label3', - fillStyle: 'blue', - hidden: true, - index: 2, - strokeStyle: '#000', - lineWidth: 2 - }]; - - var chart = { - data: data, - options: { - elements: { - arc: { + it('should hide the correct arc when a legend item is clicked', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' - } - } - } - }; - expect(config.legend.labels.generateLabels.call({ chart: chart }, data)).toEqual(expected); + }] + }, + options: config + }); + var meta = chart.getDatasetMeta(0); + + spyOn(chart, 'update').and.callThrough(); + + var legendItem = chart.legend.legendItems[0]; + config.legend.onClick.call(chart.legend, null, legendItem); + + expect(meta.data[0].hidden).toBe(true); + expect(chart.update).toHaveBeenCalled(); + + config.legend.onClick.call(chart.legend, null, legendItem); + expect(meta.data[0].hidden).toBe(false); + }); }); - it('should hide the correct arc when a legend item is clicked', function() { - var config = Chart.defaults.polarArea; + describe('Polar Area Chart', function() { + it('should return correct tooltip strings', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, 30], + }] + }, + options: config + }); - var legendItem = { - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0 - }; + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); - var chart = { - data: { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'] - }] - }, - update: function() {} - }; + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['label2: 20'], + after: [] + }]); + }); - spyOn(chart, 'update'); - var scope = { - chart: chart - }; + it('should return the correct html legend', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2'], + datasets: [{ + data: [10, 20], + backgroundColor: ['red', 'green'] + }] + }, + options: config + }); - config.legend.onClick.call(scope, null, legendItem); + var expectedLegend = '
    • label1
    • label2
    '; + expect(chart.generateLegend()).toBe(expectedLegend); + }); - expect(chart.data.datasets[0].metaHiddenData).toEqual([10]); - expect(chart.data.datasets[0].data).toEqual([NaN, 20, NaN]); + it('should return correct legend label objects', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + options: config + }); - expect(chart.update).toHaveBeenCalled(); + var expected = [{ + text: 'label1', + fillStyle: 'red', + hidden: false, + index: 0, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label2', + fillStyle: 'green', + hidden: false, + index: 1, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label3', + fillStyle: 'blue', + hidden: true, + index: 2, + strokeStyle: '#000', + lineWidth: 2 + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); + it('should hide the correct arc when a legend item is clicked', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + options: config + }); + var meta = chart.getDatasetMeta(0); - // Should not toggle index 2 since there was never data for it - legendItem.index = 2; - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); + spyOn(chart, 'update').and.callThrough(); + + var legendItem = chart.legend.legendItems[0]; + config.legend.onClick.call(chart.legend, null, legendItem); + + expect(meta.data[0].hidden).toBe(true); + expect(chart.update).toHaveBeenCalled(); + + config.legend.onClick.call(chart.legend, null, legendItem); + expect(meta.data[0].hidden).toBe(false); + }); }); }); diff --git a/test/element.arc.tests.js b/test/element.arc.tests.js index 4ba854575..b2caaadaa 100644 --- a/test/element.arc.tests.js +++ b/test/element.arc.tests.js @@ -60,6 +60,46 @@ describe('Arc element tests', function() { expect(pos.y).toBeCloseTo(0.5); }); + it ('should get the area', function() { + var arc = new Chart.elements.Arc({ + _datasetIndex: 2, + _index: 1 + }); + + // Mock out the view as if the controller put it there + arc._view = { + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: Math.sqrt(2), + }; + + expect(arc.getArea()).toBeCloseTo(0.5 * Math.PI, 6); + }); + + it ('should get the center', function() { + var arc = new Chart.elements.Arc({ + _datasetIndex: 2, + _index: 1 + }); + + // Mock out the view as if the controller put it there + arc._view = { + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: Math.sqrt(2), + }; + + var center = arc.getCenterPoint(); + expect(center.x).toBeCloseTo(0.5, 6); + expect(center.y).toBeCloseTo(0.5, 6); + }); + it ('should draw correctly with no border', function() { var mockContext = window.createMockContext(); var arc = new Chart.elements.Arc({ @@ -173,4 +213,4 @@ describe('Arc element tests', function() { args: [] }]); }); -}); \ No newline at end of file +}); diff --git a/test/element.line.tests.js b/test/element.line.tests.js index 38a56fe3e..0a2a6eced 100644 --- a/test/element.line.tests.js +++ b/test/element.line.tests.js @@ -527,6 +527,318 @@ describe('Line element tests', function() { expect(mockContext.getCalls()).toEqual(expected); }); + it('should draw with fillMode top', function() { + var mockContext = window.createMockContext(); + + // Create our points + var points = []; + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 0, + _view: { + x: 0, + y: 10, + controlPointNextX: 0, + controlPointNextY: 10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 1, + _view: { + x: 5, + y: 0, + controlPointPreviousX: 5, + controlPointPreviousY: 0, + controlPointNextX: 5, + controlPointNextY: 0 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 2, + _view: { + x: 15, + y: -10, + controlPointPreviousX: 15, + controlPointPreviousY: -10, + controlPointNextX: 15, + controlPointNextY: -10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 3, + _view: { + x: 19, + y: -5, + controlPointPreviousX: 19, + controlPointPreviousY: -5, + controlPointNextX: 19, + controlPointNextY: -5 + } + })); + + var line = new Chart.elements.Line({ + _datasetindex: 2, + _chart: { + ctx: mockContext, + }, + _children: points, + // Need to provide some settings + _view: { + fill: 'top', + scaleZero: 2, // for filling lines + scaleTop: -2, + scaleBottom: 10, + tension: 0.0, // no bezier curve for now + + borderCapStyle: 'round', + borderColor: 'rgb(255, 255, 0)', + borderDash: [2, 2], + borderDashOffset: 1.5, + borderJoinStyle: 'bevel', + borderWidth: 4, + backgroundColor: 'rgb(0, 0, 0)' + } + }); + + line.draw(); + + var expected = [{ + name: 'save', + args: [] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, -2] + }, { + name: 'lineTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'lineTo', + args: [19, -2] + }, { + name: 'setFillStyle', + args: ['rgb(0, 0, 0)'] + }, { + name: 'closePath', + args: [] + }, { + name: 'fill', + args: [] + }, { + name: 'setLineCap', + args: ['round'] + }, { + name: 'setLineDash', + args: [ + [2, 2] + ] + }, { + name: 'setLineDashOffset', + args: [1.5] + }, { + name: 'setLineJoin', + args: ['bevel'] + }, { + name: 'setLineWidth', + args: [4] + }, { + name: 'setStrokeStyle', + args: ['rgb(255, 255, 0)'] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]; + expect(mockContext.getCalls()).toEqual(expected); + }); + + it('should draw with fillMode bottom', function() { + var mockContext = window.createMockContext(); + + // Create our points + var points = []; + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 0, + _view: { + x: 0, + y: 10, + controlPointNextX: 0, + controlPointNextY: 10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 1, + _view: { + x: 5, + y: 0, + controlPointPreviousX: 5, + controlPointPreviousY: 0, + controlPointNextX: 5, + controlPointNextY: 0 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 2, + _view: { + x: 15, + y: -10, + controlPointPreviousX: 15, + controlPointPreviousY: -10, + controlPointNextX: 15, + controlPointNextY: -10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 3, + _view: { + x: 19, + y: -5, + controlPointPreviousX: 19, + controlPointPreviousY: -5, + controlPointNextX: 19, + controlPointNextY: -5 + } + })); + + var line = new Chart.elements.Line({ + _datasetindex: 2, + _chart: { + ctx: mockContext, + }, + _children: points, + // Need to provide some settings + _view: { + fill: 'bottom', + scaleZero: 2, // for filling lines + scaleTop: -2, + scaleBottom: 10, + tension: 0.0, // no bezier curve for now + + borderCapStyle: 'round', + borderColor: 'rgb(255, 255, 0)', + borderDash: [2, 2], + borderDashOffset: 1.5, + borderJoinStyle: 'bevel', + borderWidth: 4, + backgroundColor: 'rgb(0, 0, 0)' + } + }); + + line.draw(); + + var expected = [{ + name: 'save', + args: [] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, 10] + }, { + name: 'lineTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'lineTo', + args: [19, 10] + }, { + name: 'setFillStyle', + args: ['rgb(0, 0, 0)'] + }, { + name: 'closePath', + args: [] + }, { + name: 'fill', + args: [] + }, { + name: 'setLineCap', + args: ['round'] + }, { + name: 'setLineDash', + args: [ + [2, 2] + ] + }, { + name: 'setLineDashOffset', + args: [1.5] + }, { + name: 'setLineJoin', + args: ['bevel'] + }, { + name: 'setLineWidth', + args: [4] + }, { + name: 'setStrokeStyle', + args: ['rgb(255, 255, 0)'] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]; + expect(mockContext.getCalls()).toEqual(expected); + }); + it('should skip points correctly', function() { var mockContext = window.createMockContext(); @@ -2268,4 +2580,4 @@ describe('Line element tests', function() { args: [] }]); }); -}); \ No newline at end of file +}); diff --git a/test/element.point.tests.js b/test/element.point.tests.js index c257f7375..61fe3d09a 100644 --- a/test/element.point.tests.js +++ b/test/element.point.tests.js @@ -64,6 +64,36 @@ describe('Point element tests', function() { }); }); + it('should get the correct area', function() { + var point = new Chart.elements.Point({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + point._view = { + radius: 2, + }; + + expect(point.getArea()).toEqual(Math.PI * 4); + }); + + it('should get the correct center point', function() { + var point = new Chart.elements.Point({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + point._view = { + radius: 2, + x: 10, + y: 10 + }; + + expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); + }); + it ('should draw correctly', function() { var mockContext = window.createMockContext(); var point = new Chart.elements.Point({ @@ -245,7 +275,7 @@ describe('Point element tests', function() { }, { name: 'lineTo', args: [12, 15], - },{ + }, { name: 'closePath', args: [], }, { @@ -317,7 +347,7 @@ describe('Point element tests', function() { }, { name: 'lineTo', args: [12, 15], - },{ + }, { name: 'moveTo', args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] }, { @@ -471,4 +501,4 @@ describe('Point element tests', function() { expect(mockContext.getCalls()).toEqual([]); }); -}); \ No newline at end of file +}); diff --git a/test/element.rectangle.tests.js b/test/element.rectangle.tests.js index 8c28970b1..b833862bd 100644 --- a/test/element.rectangle.tests.js +++ b/test/element.rectangle.tests.js @@ -132,6 +132,40 @@ describe('Rectangle element tests', function() { }); }); + it ('should get the correct area', function() { + var rectangle = new Chart.elements.Rectangle({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + rectangle._view = { + base: 0, + width: 4, + x: 10, + y: 15 + }; + + expect(rectangle.getArea()).toEqual(60); + }); + + it ('should get the center', function() { + var rectangle = new Chart.elements.Rectangle({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + rectangle._view = { + base: 0, + width: 4, + x: 10, + y: 15 + }; + + expect(rectangle.getCenterPoint()).toEqual({x: 10, y: 7.5}); + }); + it ('should draw correctly', function() { var mockContext = window.createMockContext(); var rectangle = new Chart.elements.Rectangle({ @@ -242,10 +276,10 @@ describe('Rectangle element tests', function() { }]); }); - function testBorderSkipped (borderSkipped, expectedDrawCalls) { + function testBorderSkipped(borderSkipped, expectedDrawCalls) { var mockContext = window.createMockContext(); var rectangle = new Chart.elements.Rectangle({ - _chart: { ctx: mockContext } + _chart: {ctx: mockContext} }); // Attach a view object as if we were the controller @@ -257,47 +291,47 @@ describe('Rectangle element tests', function() { x: 10, y: 15, }; - + rectangle.draw(); - var drawCalls = rectangle._view.ctx.getCalls().splice(4, 4); + var drawCalls = rectangle._view.ctx.getCalls().splice(4, 4); expect(drawCalls).toEqual(expectedDrawCalls); } - + it ('should draw correctly respecting "borderSkipped" == "bottom"', function() { testBorderSkipped ('bottom', [ - { name: 'moveTo', args: [8, 0] }, - { name: 'lineTo', args: [8, 15] }, - { name: 'lineTo', args: [12, 15] }, - { name: 'lineTo', args: [12, 0] }, + {name: 'moveTo', args: [8, 0]}, + {name: 'lineTo', args: [8, 15]}, + {name: 'lineTo', args: [12, 15]}, + {name: 'lineTo', args: [12, 0]}, ]); }); it ('should draw correctly respecting "borderSkipped" == "left"', function() { testBorderSkipped ('left', [ - { name: 'moveTo', args: [8, 15] }, - { name: 'lineTo', args: [12, 15] }, - { name: 'lineTo', args: [12, 0] }, - { name: 'lineTo', args: [8, 0] }, + {name: 'moveTo', args: [8, 15]}, + {name: 'lineTo', args: [12, 15]}, + {name: 'lineTo', args: [12, 0]}, + {name: 'lineTo', args: [8, 0]}, ]); }); it ('should draw correctly respecting "borderSkipped" == "top"', function() { testBorderSkipped ('top', [ - { name: 'moveTo', args: [12, 15] }, - { name: 'lineTo', args: [12, 0] }, - { name: 'lineTo', args: [8, 0] }, - { name: 'lineTo', args: [8, 15] }, + {name: 'moveTo', args: [12, 15]}, + {name: 'lineTo', args: [12, 0]}, + {name: 'lineTo', args: [8, 0]}, + {name: 'lineTo', args: [8, 15]}, ]); }); it ('should draw correctly respecting "borderSkipped" == "right"', function() { testBorderSkipped ('right', [ - { name: 'moveTo', args: [12, 0] }, - { name: 'lineTo', args: [8, 0] }, - { name: 'lineTo', args: [8, 15] }, - { name: 'lineTo', args: [12, 15] }, + {name: 'moveTo', args: [12, 0]}, + {name: 'lineTo', args: [8, 0]}, + {name: 'lineTo', args: [8, 15]}, + {name: 'lineTo', args: [12, 15]}, ]); }); -}); \ No newline at end of file +}); diff --git a/test/mockContext.js b/test/mockContext.js index 9590b5360..d065f2a7a 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -1,3 +1,5 @@ +/* eslint guard-for-in: 1 */ +/* eslint camelcase: 1 */ (function() { // Code from http://stackoverflow.com/questions/4406864/html-canvas-unit-testing var Context = function() { @@ -13,44 +15,56 @@ // Define properties here so that we can record each time they are set Object.defineProperties(this, { - "fillStyle": { - 'get': function() { return this._fillStyle; }, - 'set': function(style) { + fillStyle: { + get: function() { + return this._fillStyle; + }, + set: function(style) { this._fillStyle = style; this.record('setFillStyle', [style]); } }, - 'lineCap': { - 'get': function() { return this._lineCap; }, - 'set': function(cap) { + lineCap: { + get: function() { + return this._lineCap; + }, + set: function(cap) { this._lineCap = cap; this.record('setLineCap', [cap]); } }, - 'lineDashOffset': { - 'get': function() { return this._lineDashOffset; }, - 'set': function(offset) { + lineDashOffset: { + get: function() { + return this._lineDashOffset; + }, + set: function(offset) { this._lineDashOffset = offset; this.record('setLineDashOffset', [offset]); } }, - 'lineJoin': { - 'get': function() { return this._lineJoin; }, - 'set': function(join) { + lineJoin: { + get: function() { + return this._lineJoin; + }, + set: function(join) { this._lineJoin = join; this.record('setLineJoin', [join]); } }, - 'lineWidth': { - 'get': function() { return this._lineWidth; }, - 'set': function (width) { + lineWidth: { + get: function() { + return this._lineWidth; + }, + set: function(width) { this._lineWidth = width; this.record('setLineWidth', [width]); } }, - 'strokeStyle': { - 'get': function() { return this._strokeStyle; }, - 'set': function(style) { + strokeStyle: { + get: function() { + return this._strokeStyle; + }, + set: function(style) { this._strokeStyle = style; this.record('setStrokeStyle', [style]); } @@ -70,33 +84,35 @@ fill: function() {}, fillRect: function() {}, fillText: function() {}, - lineTo: function(x, y) {}, + lineTo: function() {}, measureText: function(text) { // return the number of characters * fixed size - return text ? { width: text.length * 10 } : {width: 0}; + return text ? {width: text.length * 10} : {width: 0}; }, - moveTo: function(x, y) {}, + moveTo: function() {}, quadraticCurveTo: function() {}, restore: function() {}, rotate: function() {}, save: function() {}, setLineDash: function() {}, stroke: function() {}, - strokeRect: function(x, y, w, h) {}, - setTransform: function(a, b, c, d, e, f) {}, - translate: function(x, y) {}, + strokeRect: function() {}, + setTransform: function() {}, + translate: function() {}, }; // attach methods to the class itself - var scope = this; - var addMethod = function(name, method) { - scope[methodName] = function() { - scope.record(name, arguments); - return method.apply(scope, arguments); - }; - } + var me = this; + var methodName; - for (var methodName in methods) { + var addMethod = function(name, method) { + me[methodName] = function() { + me.record(name, arguments); + return method.apply(me, arguments); + }; + }; + + for (methodName in methods) { var method = methods[methodName]; addMethod(methodName, method); @@ -108,11 +124,11 @@ name: methodName, args: Array.prototype.slice.call(args) }); - }, + }; Context.prototype.getCalls = function() { return this._calls; - } + }; Context.prototype.resetCalls = function() { this._calls = []; @@ -136,10 +152,10 @@ result = (diff <= (A > B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine } - return { pass: result }; + return {pass: result}; } - } - }; + }; + } function toEqualOneOf() { return { @@ -158,10 +174,86 @@ }; } + function toBeValidChart() { + return { + compare: function(actual) { + var chart = actual && actual.chart; + var message = null; + + if (!(actual instanceof Chart.Controller)) { + message = 'Expected ' + actual + ' to be an instance of Chart.Controller'; + } else if (!(chart instanceof Chart)) { + message = 'Expected chart to be an instance of Chart'; + } else if (!(chart.canvas instanceof HTMLCanvasElement)) { + message = 'Expected canvas to be an instance of HTMLCanvasElement'; + } else if (!(chart.ctx instanceof CanvasRenderingContext2D)) { + message = 'Expected context to be an instance of CanvasRenderingContext2D'; + } else if (typeof chart.height !== 'number' || !isFinite(chart.height)) { + message = 'Expected height to be a strict finite number'; + } else if (typeof chart.width !== 'number' || !isFinite(chart.width)) { + message = 'Expected width to be a strict finite number'; + } + + return { + message: message? message : 'Expected ' + actual + ' to be valid chart', + pass: !message + }; + } + }; + } + + function toBeChartOfSize() { + return { + compare: function(actual, expected) { + var res = toBeValidChart().compare(actual); + if (!res.pass) { + return res; + } + + var message = null; + var chart = actual.chart; + var canvas = chart.ctx.canvas; + var style = getComputedStyle(canvas); + var pixelRatio = window.devicePixelRatio; + var dh = parseInt(style.height, 10); + var dw = parseInt(style.width, 10); + var rh = canvas.height; + var rw = canvas.width; + var orh = rh / pixelRatio; + var orw = rw / pixelRatio; + + // sanity checks + if (chart.height !== orh) { + message = 'Expected chart height ' + chart.height + ' to be equal to original render height ' + orh; + } else if (chart.width !== orw) { + message = 'Expected chart width ' + chart.width + ' to be equal to original render width ' + orw; + } + + // validity checks + if (dh !== expected.dh) { + message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh; + } else if (dw !== expected.dw) { + message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw; + } else if (rh !== expected.rh) { + message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh; + } else if (rw !== expected.rw) { + message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw; + } + + return { + message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected, + pass: !message + }; + } + }; + } + beforeEach(function() { jasmine.addMatchers({ toBeCloseToPixel: toBeCloseToPixel, - toEqualOneOf: toEqualOneOf + toEqualOneOf: toEqualOneOf, + toBeValidChart: toBeValidChart, + toBeChartOfSize: toBeChartOfSize }); }); @@ -178,13 +270,13 @@ * @param {boolean} options.persistent - If true, the chart will not be released after the spec. */ function acquireChart(config, options) { - var wrapper = document.createElement("div"); - var canvas = document.createElement("canvas"); + var wrapper = document.createElement('div'); + var canvas = document.createElement('canvas'); var chart, key; options = options || {}; - options.canvas = options.canvas || { height: 512, width: 512 }; - options.wrapper = options.wrapper || { class: 'chartjs-wrapper' }; + options.canvas = options.canvas || {height: 512, width: 512}; + options.wrapper = options.wrapper || {class: 'chartjs-wrapper'}; for (key in options.canvas) { if (options.canvas.hasOwnProperty(key)) { @@ -207,23 +299,24 @@ wrapper.appendChild(canvas); window.document.body.appendChild(wrapper); - chart = new Chart(canvas.getContext("2d"), config); - chart.__test_persistent = options.persistent; + chart = new Chart(canvas.getContext('2d'), config); + chart._test_persistent = options.persistent; + chart._test_wrapper = wrapper; charts[chart.id] = chart; return chart; } function releaseChart(chart) { - chart.chart.canvas.parentNode.remove(); + chart.destroy(); + chart._test_wrapper.remove(); delete charts[chart.id]; - delete chart; } afterEach(function() { // Auto releasing acquired charts for (var id in charts) { var chart = charts[id]; - if (!chart.__test_persistent) { + if (!chart._test_persistent) { releaseChart(chart); } } @@ -255,4 +348,4 @@ '.chartjs-wrapper {' + 'position: absolute' + '}'); -})(); +}()); diff --git a/test/scale.category.tests.js b/test/scale.category.tests.js index 5307ba652..13955d35b 100644 --- a/test/scale.category.tests.js +++ b/test/scale.category.tests.js @@ -13,7 +13,7 @@ describe('Category scale tests', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label @@ -21,12 +21,12 @@ describe('Category scale tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "bottom", + position: 'bottom', scaleLabel: { labelString: '', display: false @@ -287,8 +287,8 @@ describe('Category scale tests', function() { var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); config.gridLines.offsetGridLines = true; - config.ticks.min = "tick2"; - config.ticks.max = "tick4"; + config.ticks.min = 'tick2'; + config.ticks.max = 'tick4'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ @@ -349,7 +349,7 @@ describe('Category scale tests', function() { var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); config.gridLines.offsetGridLines = true; - config.position = "left"; + config.position = 'left'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ ctx: mockContext, @@ -414,9 +414,9 @@ describe('Category scale tests', function() { var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); config.gridLines.offsetGridLines = true; - config.ticks.min = "tick2"; - config.ticks.max = "tick4"; - config.position = "left"; + config.ticks.min = 'tick2'; + config.ticks.max = 'tick4'; + config.position = 'left'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 659ccc727..7102a6f4a 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -11,7 +11,7 @@ describe('Linear Scale', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label @@ -19,12 +19,12 @@ describe('Linear Scale', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "left", + position: 'left', scaleLabel: { labelString: '', display: false, @@ -156,9 +156,9 @@ describe('Linear Scale', function() { data: { datasets: [{ yAxisID: 'yScale0', - data: [null, 90, NaN, undefined, 45, 30] + data: [null, 90, NaN, undefined, 45, 30, Infinity, -Infinity] }], - labels: ['a', 'b', 'c', 'd', 'e', 'f'] + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] }, options: { scales: { @@ -486,6 +486,38 @@ describe('Linear Scale', function() { expect(chart.scales.yScale0.ticks[chart.scales.yScale0.ticks.length - 1]).toBe('-1010'); }); + it('Should use min, max and stepSize to create fixed spaced ticks', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [10, 3, 6, 8, 3, 1] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear', + ticks: { + min: 1, + max: 11, + stepSize: 2 + } + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min).toBe(1); + expect(chart.scales.yScale0.max).toBe(11); + expect(chart.scales.yScale0.ticks).toEqual(['11', '9', '7', '5', '3', '1']); + }); + + it('should forcibly include 0 in the range if the beginAtZero option is used', function() { var chart = window.acquireChart({ type: 'bar', diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index be6350341..9636a0b37 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -10,7 +10,7 @@ describe('Logarithmic Scale tests', function() { expect(defaultConfig).toEqual({ display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, @@ -18,12 +18,12 @@ describe('Logarithmic Scale tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "left", + position: 'left', scaleLabel: { labelString: '', display: false, @@ -78,14 +78,14 @@ describe('Logarithmic Scale tests', function() { id: 'yScale1', type: 'logarithmic' }, - { - id: 'yScale2', - type: 'logarithmic' - }, - { - id: 'yScale3', - type: 'logarithmic' - }] + { + id: 'yScale2', + type: 'logarithmic' + }, + { + id: 'yScale3', + type: 'logarithmic' + }] } } }); @@ -125,7 +125,7 @@ describe('Logarithmic Scale tests', function() { data: ['20', '0', '150', '1800', '3040'] }, { yAxisID: 'yScale3', - data: ['67', '0.0004', '0', '820', '0.001'] + data: ['67', '0.0004', '0', '820', '0.001'] }], labels: ['a', 'b', 'c', 'd', 'e'] }, @@ -141,10 +141,10 @@ describe('Logarithmic Scale tests', function() { id: 'yScale2', type: 'logarithmic' }, - { - id: 'yScale3', - type: 'logarithmic' - }] + { + id: 'yScale3', + type: 'logarithmic' + }] } } }); @@ -185,8 +185,8 @@ describe('Logarithmic Scale tests', function() { data: [20, 0, 7400, 14, 291] }, { yAxisID: 'yScale2', - data: [6, 0.0007, 9, 890, 60000], - hidden: true + data: [6, 0.0007, 9, 890, 60000], + hidden: true }], labels: ['a', 'b', 'c', 'd', 'e'] }, @@ -224,7 +224,7 @@ describe('Logarithmic Scale tests', function() { data: [undefined, 10, null, 5, 5000, NaN, 78, 450] }, { yAxisID: 'yScale0', - data: [undefined, 28, null, 1000, 500, NaN, 50, 42] + data: [undefined, 28, null, 1000, 500, NaN, 50, 42, Infinity, -Infinity] }, { yAxisID: 'yScale1', data: [undefined, 30, null, 9400, 0, NaN, 54, 836] @@ -232,7 +232,7 @@ describe('Logarithmic Scale tests', function() { yAxisID: 'yScale1', data: [undefined, 0, null, 800, 9, NaN, 894, 21] }], - labels: ['a', 'b', 'c', 'd', 'e', 'f' ,'g'] + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] }, options: { scales: { @@ -269,10 +269,10 @@ describe('Logarithmic Scale tests', function() { data: { datasets: [{ data: [ - { x: 10, y: 100 }, - { x: 2, y: 6 }, - { x: 65, y: 121 }, - { x: 99, y: 7 } + {x: 10, y: 100}, + {x: 2, y: 6}, + {x: 65, y: 121}, + {x: 99, y: 7} ] }] }, @@ -304,10 +304,10 @@ describe('Logarithmic Scale tests', function() { data: { datasets: [{ data: [ - { x: 7, y: 950 }, - { x: 289, y: 0 }, - { x: 0, y: 8 }, - { x: 23, y: 0.04 } + {x: 7, y: 950}, + {x: 289, y: 0}, + {x: 0, y: 8}, + {x: 23, y: 0.04} ] }] }, @@ -718,9 +718,9 @@ describe('Logarithmic Scale tests', function() { var xScale = chart.scales.xScale; expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight - expect(xScale.getPixelForValue( 1, 0, 0)).toBeCloseToPixel(48); // left + paddingLeft + expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(48); // left + paddingLeft expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(283); // halfway - expect(xScale.getPixelForValue( 0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. expect(xScale.getValueForPixel(495)).toBeCloseTo(80, 1e-4); expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); @@ -728,9 +728,9 @@ describe('Logarithmic Scale tests', function() { var yScale = chart.scales.yScale; expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue( 1, 0, 0)).toBeCloseToPixel(456); // bottom - paddingBottom + expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(456); // bottom - paddingBottom expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(234); // halfway - expect(yScale.getPixelForValue( 0, 0, 0)).toBeCloseToPixel(32); // 0 is invalid. force it on top + expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); // 0 is invalid. force it on top expect(yScale.getValueForPixel(32)).toBeCloseTo(80, 1e-4); expect(yScale.getValueForPixel(456)).toBeCloseTo(1, 1e-4); diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index 61b6d9689..7ac95ac39 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -11,13 +11,13 @@ describe('Test the radial linear scale', function() { expect(defaultConfig).toEqual({ angleLines: { display: true, - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', lineWidth: 1 }, animate: true, display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, @@ -25,7 +25,7 @@ describe('Test the radial linear scale', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 @@ -35,13 +35,13 @@ describe('Test the radial linear scale', function() { fontSize: 10, callback: defaultConfig.pointLabels.callback, // make this nicer, then check explicitly below }, - position: "chartArea", + position: 'chartArea', scaleLabel: { labelString: '', display: false, }, ticks: { - backdropColor: "rgba(255,255,255,0.75)", + backdropColor: 'rgba(255,255,255,0.75)', backdropPaddingY: 2, backdropPaddingX: 2, beginAtZero: false, @@ -73,7 +73,7 @@ describe('Test the radial linear scale', function() { }, { data: [150] }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: {} @@ -93,7 +93,7 @@ describe('Test the radial linear scale', function() { }, { data: ['150'] }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: {} @@ -116,7 +116,7 @@ describe('Test the radial linear scale', function() { data: [1000], hidden: true }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: {} @@ -132,9 +132,9 @@ describe('Test the radial linear scale', function() { type: 'radar', data: { datasets: [{ - data: [50, 60, NaN, 70, null, undefined] + data: [50, 60, NaN, 70, null, undefined, Infinity, -Infinity] }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'] }, options: { scales: {} @@ -176,7 +176,7 @@ describe('Test the radial linear scale', function() { datasets: [{ data: [1, 1, 1, 2, 1, 0] }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scale: { @@ -199,7 +199,7 @@ describe('Test the radial linear scale', function() { datasets: [{ data: [1, 1, 1, 2, 1, 0] }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scale: { @@ -223,7 +223,7 @@ describe('Test the radial linear scale', function() { datasets: [{ data: [20, 30, 40, 50] }], - labels: ['lablel1', 'label2', 'label3', 'label4'] + labels: ['label1', 'label2', 'label3', 'label4'] }, options: { scale: { @@ -259,7 +259,7 @@ describe('Test the radial linear scale', function() { datasets: [{ data: [10, 5, 0, 25, 78] }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5'] + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scale: { @@ -426,19 +426,19 @@ describe('Test the radial linear scale', function() { var radToNearestDegree = function(rad) { return Math.round((360 * rad) / (2 * Math.PI)); - } + }; var slice = 72; // (360 / 5) - for(var i = 0; i < 5; i++) { + for (var i = 0; i < 5; i++) { expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe(15 + (slice * i) - 90); } chart.options.startAngle = 0; chart.update(); - for(var i = 0; i < 5; i++) { - expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe((slice * i) - 90); + for (var x = 0; x < 5; x++) { + expect(radToNearestDegree(chart.scale.getIndexAngle(x))).toBe((slice * x) - 90); } }); }); diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index 5179191ba..896ff00d4 100755 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -15,7 +15,7 @@ describe('Time scale tests', function() { pass: result }; } - } + }; } }); }); @@ -35,7 +35,7 @@ describe('Time scale tests', function() { expect(defaultConfig).toEqual({ display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, @@ -43,12 +43,12 @@ describe('Time scale tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "bottom", + position: 'bottom', scaleLabel: { labelString: '', display: false @@ -75,15 +75,15 @@ describe('Time scale tests', function() { displayFormat: false, minUnit: 'millisecond', displayFormats: { - 'millisecond': 'h:mm:ss.SSS a', // 11:20:01.123 AM - 'second': 'h:mm:ss a', // 11:20:01 AM - 'minute': 'h:mm:ss a', // 11:20:01 AM - 'hour': 'MMM D, hA', // Sept 4, 5PM - 'day': 'll', // Sep 4 2015 - 'week': 'll', // Week 46, or maybe "[W]WW - YYYY" ? - 'month': 'MMM YYYY', // Sept 2015 - 'quarter': '[Q]Q - YYYY', // Q3 - 'year': 'YYYY' // 2015 + millisecond: 'h:mm:ss.SSS a', // 11:20:01.123 AM + second: 'h:mm:ss a', // 11:20:01 AM + minute: 'h:mm:ss a', // 11:20:01 AM + hour: 'MMM D, hA', // Sept 4, 5PM + day: 'll', // Sep 4 2015 + week: 'll', // Week 46, or maybe "[W]WW - YYYY" ? + month: 'MMM YYYY', // Sept 2015 + quarter: '[Q]Q - YYYY', // Q3 + year: 'YYYY' // 2015 } } }); @@ -96,7 +96,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00", "2015-01-03T22:00:00", "2015-01-05T23:00:00", "2015-01-07T03:00", "2015-01-08T10:00", "2015-01-10T12:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }; var mockContext = window.createMockContext(); @@ -110,11 +110,11 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual([ 'Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015' ]); + expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015']); }); it('should build ticks using date objects', function() { @@ -142,7 +142,7 @@ describe('Time scale tests', function() { scale.update(400, 50); // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual([ 'Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015' ]); + expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015']); }); it('should build ticks when the data is xy points', function() { @@ -198,7 +198,7 @@ describe('Time scale tests', function() { // Counts down because the lines are drawn top to bottom var xScale = chart.scales.xScale0; - expect(xScale.ticks).toEqual([ 'Jan 1, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 9, 2015', 'Jan 11, 2015' ]); + expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 9, 2015', 'Jan 11, 2015']); }); it('should allow custom time parsers', function() { @@ -223,7 +223,7 @@ describe('Time scale tests', function() { time: { unit: 'day', round: true, - parser: function customTimeParser(label) { + parser: function(label) { return moment.unix(label); } } @@ -248,7 +248,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days }; var mockContext = window.createMockContext(); @@ -264,7 +264,7 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); expect(scale.ticks).toEqual(['Jan 1, 8PM', 'Jan 1, 9PM', 'Jan 1, 10PM', 'Jan 1, 11PM', 'Jan 2, 12AM', 'Jan 2, 1AM', 'Jan 2, 2AM', 'Jan 2, 3AM', 'Jan 2, 4AM', 'Jan 2, 5AM', 'Jan 2, 6AM', 'Jan 2, 7AM', 'Jan 2, 8AM', 'Jan 2, 9AM', 'Jan 2, 10AM', 'Jan 2, 11AM', 'Jan 2, 12PM', 'Jan 2, 1PM', 'Jan 2, 2PM', 'Jan 2, 3PM', 'Jan 2, 4PM', 'Jan 2, 5PM', 'Jan 2, 6PM', 'Jan 2, 7PM', 'Jan 2, 8PM', 'Jan 2, 9PM']); }); @@ -273,7 +273,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days }; var mockContext = window.createMockContext(); @@ -289,7 +289,7 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015']); }); @@ -298,7 +298,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-02-02T21:00:00", "2015-02-21T01:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-02-02T21:00:00', '2015-02-21T01:00:00'], // days }; var mockContext = window.createMockContext(); @@ -315,7 +315,7 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); // last date is feb 15 because we round to start of week @@ -326,13 +326,13 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T20:00:00", "2015-01-03T20:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], // days }; var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); - config.time.min = "2015-01-01T04:00:00"; - config.time.max = "2015-01-05T06:00:00" + config.time.min = '2015-01-01T04:00:00'; + config.time.max = '2015-01-05T06:00:00'; var Constructor = Chart.scaleService.getScaleConstructor('time'); var scale = new Constructor({ ctx: mockContext, @@ -344,7 +344,7 @@ describe('Time scale tests', function() { }); scale.update(400, 50); - expect(scale.ticks).toEqual([ 'Jan 1, 2015', 'Jan 5, 2015' ]); + expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 5, 2015']); }); it('Should use the isoWeekday option', function() { @@ -352,9 +352,9 @@ describe('Time scale tests', function() { var mockData = { labels: [ - "2015-01-01T20:00:00", // Thursday - "2015-01-02T20:00:00", // Friday - "2015-01-03T20:00:00" // Saturday + '2015-01-01T20:00:00', // Thursday + '2015-01-02T20:00:00', // Friday + '2015-01-03T20:00:00' // Saturday ] }; @@ -374,7 +374,7 @@ describe('Time scale tests', function() { }); scale.update(400, 50); - expect(scale.ticks).toEqual([ 'Dec 31, 2014', 'Jan 7, 2015' ]); + expect(scale.ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']); }); it('should get the correct pixel for a value', function() { @@ -386,7 +386,7 @@ describe('Time scale tests', function() { yAxisID: 'yScale0', data: [] }], - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00", "2015-01-03T22:00:00", "2015-01-05T23:00:00", "2015-01-07T03:00", "2015-01-08T10:00", "2015-01-10T12:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, options: { scales: { @@ -430,7 +430,7 @@ describe('Time scale tests', function() { yAxisID: 'yScale0', data: [] }], - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00", "2015-01-03T22:00:00", "2015-01-05T23:00:00", "2015-01-07T03:00", "2015-01-08T10:00", "2015-01-10T12:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, options: { scales: { @@ -457,7 +457,7 @@ describe('Time scale tests', function() { var chart = window.acquireChart({ type: 'line', data: { - labels: ["2016-05-27"], + labels: ['2016-05-27'], datasets: [{ xAxisID: 'xScale0', data: [5] @@ -468,7 +468,7 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', display: true, - type: "time" + type: 'time' }] } } @@ -485,11 +485,11 @@ describe('Time scale tests', function() { }); }); - it("should not throw an error if the datasetIndex is out of bounds", function() { + it('should not throw an error if the datasetIndex is out of bounds', function() { var chart = window.acquireChart({ type: 'line', data: { - labels: ["2016-06-26"], + labels: ['2016-06-26'], datasets: [{ xAxisID: 'xScale0', data: [5] @@ -500,7 +500,7 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', display: true, - type: "time", + type: 'time', }] } } @@ -514,11 +514,11 @@ describe('Time scale tests', function() { expect(getOutOfBoundLabelMoment).not.toThrow(); }); - it("should not throw an error if the datasetIndex or index are null", function() { + it('should not throw an error if the datasetIndex or index are null', function() { var chart = window.acquireChart({ type: 'line', data: { - labels: ["2016-06-26"], + labels: ['2016-06-26'], datasets: [{ xAxisID: 'xScale0', data: [5] @@ -529,7 +529,7 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', display: true, - type: "time", + type: 'time', }] } }