]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add normalized option (#7538)
authorBen McCann <322311+benmccann@users.noreply.github.com>
Tue, 7 Jul 2020 11:50:53 +0000 (04:50 -0700)
committerGitHub <noreply@github.com>
Tue, 7 Jul 2020 11:50:53 +0000 (07:50 -0400)
Add normalized option to time scales

docs/docs/axes/cartesian/timeseries.md
docs/docs/general/performance.md
src/controllers/controller.bar.js
src/controllers/controller.line.js
src/core/core.controller.js
src/core/core.scale.js
src/scales/scale.time.js
src/scales/scale.timeseries.js
test/specs/scale.time.tests.js

index 986c4dbffb41317b0cd5365fa80950f455a0fe19..1ec50a20569d34705d33eccc52bdef787f0a4e05 100644 (file)
@@ -2,7 +2,7 @@
 title: Time Series Axis
 ---
 
-The time series scale extends from the time scale and supports all the same options. However, for the time series scale, each data point is spread equidistant. Also, the data indices are expected to be unique, sorted, and consistent across datasets.
+The time series scale extends from the time scale and supports all the same options. However, for the time series scale, each data point is spread equidistant.
 
 ## Example
 
index f3a9ee48da4cb49a14e863728d8819c04ab066af..499b29110c5f17e58ef3c7b0d7b8fa83581b74b6 100644 (file)
@@ -4,6 +4,24 @@ title: Performance
 
 Chart.js charts are rendered on `canvas` elements, which makes rendering quite fast. For large datasets or performance sensitive applications, you may wish to consider the tips below.
 
+## Data structure and format
+
+### Parsing
+
+Provide prepared data in the internal format accepted by the dataset and scales and set `parsing: false`. See [Data structures](data-structures.md) for more information.
+
+### Data normalization
+
+Chart.js is fastest if you provide data with indices that are unique, sorted, and consistent across datasets and provide the `normalized: true` option to let Chart.js know that you have done so. Even without this option, it can sometimes still be faster to provide sorted data.
+
+### Decimation
+
+Decimating your data will achieve the best results. When there is a lot of data to display on the graph, it doesn't make sense to show tens of thousands of data points on a graph that is only a few hundred pixels wide.
+
+There are many approaches to data decimation and selection of an algorithm will depend on your data and the results you want to achieve. For instance, [min/max](https://digital.ni.com/public.nsf/allkb/F694FFEEA0ACF282862576020075F784) decimation will preserve peaks in your data but could require up to 4 points for each pixel. This type of decimation would work well for a very noisy signal where you need to see data peaks.
+
+Line charts are able to do [automatic data decimation during draw](#automatic-data-decimation-during-draw), when certain conditions are met. You should still consider decimating data yourself before passing it in for maximum performance since the automatic decimation occurs late in the chart life cycle.
+
 ## Tick Calculation
 
 ### Rotation
@@ -30,10 +48,6 @@ new Chart(ctx, {
 });
 ```
 
-## Provide ordered data
-
-If the data is unordered, Chart.js needs to sort it. This can be slow in some cases, so its always a good idea to provide ordered data.
-
 ## Specify `min` and `max` for scales
 
 If you specify the `min` and `max`, the scale does not have to compute the range from the data.
@@ -59,19 +73,7 @@ new Chart(ctx, {
 });
 ```
 
-## Data structure and format
-
-Provide prepared data in the internal format accepted by the dataset and scales and set `parsing: false`. See [Data structures](data-structures.md) for more information.
-
-## Data Decimation
-
-Decimating your data will achieve the best results. When there is a lot of data to display on the graph, it doesn't make sense to show tens of thousands of data points on a graph that is only a few hundred pixels wide.
-
-There are many approaches to data decimation and selection of an algorithm will depend on your data and the results you want to achieve. For instance, [min/max](https://digital.ni.com/public.nsf/allkb/F694FFEEA0ACF282862576020075F784) decimation will preserve peaks in your data but could require up to 4 points for each pixel. This type of decimation would work well for a very noisy signal where you need to see data peaks.
-
-Line charts are able to do [automatic data decimation during draw](#automatic-data-decimation-during-draw), when certain conditions are met. You should still consider decimating data yourself before passing it in for maximum performance since the automatic decimation occurs late in the chart life cycle.
-
-## Render Chart.js in a web worker (Chrome only)
+## Parallel rendering with web workers (Chrome only)
 
 Chome (in version 69) added the ability to [transfer rendering control of a canvas](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/transferControlToOffscreen) to a web worker. Web workers can use the [OffscreenCanvas API](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) to render from a web worker onto canvases in the DOM. Chart.js is a canvas-based library and supports rendering in a web worker - just pass an OffscreenCanvas into the Chart constructor instead of a Canvas element. Note that as of today, this API is only supported in Chrome.
 
@@ -220,7 +222,7 @@ new Chart(ctx, {
 });
 ```
 
-### When transpiling with Babel, cosider using `loose` mode
+## When transpiling with Babel, cosider using `loose` mode
 
 Babel 7.9 changed the way classes are constructed. It is slow, unless used with `loose` mode.
 [More information](https://github.com/babel/babel/issues/11356)
index 3608120294d568848f342d9c420b9ae7588a80f6..146e95e0199fdd2c34874f90c790a1ba9f6bae79 100644 (file)
@@ -355,7 +355,7 @@ export default class BarController extends DatasetController {
                let i, ilen;
 
                for (i = 0, ilen = meta.data.length; i < ilen; ++i) {
-                       pixels.push(iScale.getPixelForValue(me.getParsed(i)[iScale.axis]));
+                       pixels.push(iScale.getPixelForValue(me.getParsed(i)[iScale.axis], i));
                }
 
                // Note: a potential optimization would be to skip computing this
index 873dc281fe4700a336bf43aaeca4c2f2536e3024..a44f811156b1ac928fc0eda38769b1b2294a5457 100644 (file)
@@ -49,8 +49,8 @@ export default class LineController extends DatasetController {
                        const index = start + i;
                        const point = points[i];
                        const parsed = me.getParsed(index);
-                       const x = xScale.getPixelForValue(parsed.x);
-                       const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(_stacked ? me.applyStack(yScale, parsed) : parsed.y);
+                       const x = xScale.getPixelForValue(parsed.x, index);
+                       const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(_stacked ? me.applyStack(yScale, parsed) : parsed.y, index);
                        const properties = {
                                x,
                                y,
index e36a514a451e03f373f6ae64c8ffc48adebe047a..103b054c4ecc54038cee2f0ecfbd15cc814cf99b 100644 (file)
@@ -451,7 +451,7 @@ class Chart {
                                scales[scale.id] = scale;
                        }
 
-                       scale.init(scaleOptions);
+                       scale.init(scaleOptions, options);
 
                        // TODO(SB): I think we should be able to remove this custom case (options.scale)
                        // and consider it as a regular scale part of the "scales"" map only! This would
index 1212f187b232ae24ed24d134695c28c7a267f622..d08ac4ffcd3c1bb811ff6fab13711ee51297b745 100644 (file)
@@ -938,9 +938,10 @@ export default class Scale extends Element {
         * Returns the location of the given data point. Value can either be an index or a numerical value
         * The coordinate (0, 0) is at the upper-left corner of the canvas
         * @param {*} value
+        * @param {number} [index]
         * @return {number}
         */
-       getPixelForValue(value) { // eslint-disable-line no-unused-vars
+       getPixelForValue(value, index) { // eslint-disable-line no-unused-vars
                return NaN;
        }
 
index 888f424c58d8057bf92a049285cee0c4b6dd505f..99f5b9412526acfce0db871473f9c59eaee18889 100644 (file)
@@ -217,13 +217,13 @@ export default class TimeScale extends Scale {
                this._unit = 'day';
                /** @type {Unit=} */
                this._majorUnit = undefined;
-               /** @type {object} */
                this._offsets = {};
+               this._normalized = false;
        }
 
-       init(options) {
-               const time = options.time || (options.time = {});
-               const adapter = this._adapter = new adapters._date(options.adapters.date);
+       init(scaleOpts, opts) {
+               const time = scaleOpts.time || (scaleOpts.time = {});
+               const adapter = this._adapter = new adapters._date(scaleOpts.adapters.date);
 
                // Backward compatibility: before introducing adapter, `displayFormats` was
                // supposed to contain *all* unit/string pairs but this can't be resolved
@@ -231,7 +231,9 @@ export default class TimeScale extends Scale {
                // missing formats on update
                mergeIf(time.displayFormats, adapter.formats());
 
-               super.init(options);
+               super.init(scaleOpts);
+
+               this._normalized = opts.normalized;
        }
 
        /**
@@ -574,13 +576,15 @@ export default class TimeScale extends Scale {
 
                const metas = me.getMatchingVisibleMetas();
 
+               if (me._normalized && metas.length) {
+                       return (me._cache.data = metas[0].controller.getAllParsedValues(me));
+               }
+
                for (i = 0, ilen = metas.length; i < ilen; ++i) {
                        timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(me));
                }
 
-               // We can not assume data is in order or unique - not even for single dataset
-               // It seems to be somewhat faster to do sorting first
-               return (me._cache.data = _arrayUnique(timestamps.sort(sorter)));
+               return (me._cache.data = me.normalize(timestamps));
        }
 
        /**
@@ -600,8 +604,16 @@ export default class TimeScale extends Scale {
                        timestamps.push(parse(me, labels[i]));
                }
 
-               // We could assume labels are in order and unique - but let's not
-               return (me._cache.labels = _arrayUnique(timestamps.sort(sorter)));
+               return (me._cache.labels = me._normalized ? timestamps : me.normalize(timestamps));
+       }
+
+       /**
+        * @param {number[]} values
+        * @protected
+        */
+       normalize(values) {
+               // It seems to be somewhat faster to do sorting first
+               return _arrayUnique(values.sort(sorter));
        }
 }
 
index a1cce266f29b041ed2b18ee79879fa1c24882c65..232b49d22098845ce6a6046752fe0251ce3adffe 100644 (file)
@@ -1,37 +1,34 @@
 import TimeScale from './scale.time';
-import {_arrayUnique, _lookupByKey} from '../helpers/helpers.collection';
+import {_lookup} from '../helpers/helpers.collection';
+import {isNullOrUndef} from '../helpers/helpers.core';
 
 /**
- * Linearly interpolates the given source `value` using the table items `skey` values and
- * returns the associated `tkey` value. For example, interpolate(table, 'time', 42, 'pos')
- * returns the position for a timestamp equal to 42. If value is out of bounds, values at
- * index [0, 1] or [n - 1, n] are used for the interpolation.
+ * Linearly interpolates the given source `val` using the table. If value is out of bounds, values
+ * at index [0, 1] or [n - 1, n] are used for the interpolation.
  * @param {object} table
- * @param {string} skey
- * @param {number} sval
- * @param {string} tkey
+ * @param {number} val
+ * @param {boolean} [reverse] lookup time based on position instead of vice versa
  * @return {object}
  */
-function interpolate(table, skey, sval, tkey) {
-       const {lo, hi} = _lookupByKey(table, skey, sval);
+function interpolate(table, val, reverse) {
+       let prevSource, nextSource, prevTarget, nextTarget;
 
        // Note: the lookup table ALWAYS contains at least 2 items (min and max)
-       const prev = table[lo];
-       const next = table[hi];
-
-       const span = next[skey] - prev[skey];
-       const ratio = span ? (sval - prev[skey]) / span : 0;
-       const offset = (next[tkey] - prev[tkey]) * ratio;
+       if (reverse) {
+               prevSource = Math.floor(val);
+               nextSource = Math.ceil(val);
+               prevTarget = table[prevSource];
+               nextTarget = table[nextSource];
+       } else {
+               const result = _lookup(table, val);
+               prevTarget = result.lo;
+               nextTarget = result.hi;
+               prevSource = table[prevTarget];
+               nextSource = table[nextTarget];
+       }
 
-       return prev[tkey] + offset;
-}
-
-/**
- * @param {number} a
- * @param {number} b
- */
-function sorter(a, b) {
-       return a - b;
+       const span = nextSource - prevSource;
+       return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget;
 }
 
 class TimeSeriesScale extends TimeScale {
@@ -44,6 +41,8 @@ class TimeSeriesScale extends TimeScale {
 
                /** @type {object[]} */
                this._table = [];
+               /** @type {number} */
+               this._maxIndex = undefined;
        }
 
        /**
@@ -53,6 +52,7 @@ class TimeSeriesScale extends TimeScale {
                const me = this;
                const timestamps = me._getTimestampsForTable();
                me._table = me.buildLookupTable(timestamps);
+               me._maxIndex = me._table.length - 1;
                super.initOffsets(timestamps);
        }
 
@@ -77,9 +77,8 @@ class TimeSeriesScale extends TimeScale {
                        ];
                }
 
-               const table = [];
                const items = [min];
-               let i, ilen, prev, curr, next;
+               let i, ilen, curr;
 
                for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
                        curr = timestamps[i];
@@ -90,18 +89,7 @@ class TimeSeriesScale extends TimeScale {
 
                items.push(max);
 
-               for (i = 0, ilen = items.length; i < ilen; ++i) {
-                       next = items[i + 1];
-                       prev = items[i - 1];
-                       curr = items[i];
-
-                       // only add points that breaks the scale linearity
-                       if (prev === undefined || next === undefined || Math.round((next + prev) / 2) !== curr) {
-                               table.push({time: curr, pos: i / (ilen - 1)});
-                       }
-               }
-
-               return table;
+               return items;
        }
 
        /**
@@ -122,7 +110,7 @@ class TimeSeriesScale extends TimeScale {
                if (data.length && label.length) {
                        // If combining labels and data (data might not contain all labels),
                        // we need to recheck uniqueness and sort
-                       timestamps = _arrayUnique(data.concat(label).sort(sorter));
+                       timestamps = me.normalize(data.concat(label));
                } else {
                        timestamps = data.length ? data : label;
                }
@@ -133,48 +121,23 @@ class TimeSeriesScale extends TimeScale {
 
        /**
         * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)
+        * @param {number} [index]
         * @return {number}
         */
-       getDecimalForValue(value) {
-               return interpolate(this._table, 'time', value, 'pos');
-       }
-
-       /**
-        * @return {number[]}
-        * @protected
-        */
-       getDataTimestamps() {
+       getPixelForValue(value, index) {
                const me = this;
-               const timestamps = me._cache.data || [];
-
-               if (timestamps.length) {
-                       return timestamps;
-               }
-
-               const metas = me.getMatchingVisibleMetas();
-               return (me._cache.data = metas.length ? metas[0].controller.getAllParsedValues(me) : []);
+               const offsets = me._offsets;
+               const pos = me._normalized && me._maxIndex > 0 && !isNullOrUndef(index)
+                       ? index / me._maxIndex : me.getDecimalForValue(value);
+               return me.getPixelForDecimal((offsets.start + pos) * offsets.factor);
        }
 
        /**
-        * @return {number[]}
-        * @protected
+        * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)
+        * @return {number}
         */
-       getLabelTimestamps() {
-               const me = this;
-               const timestamps = me._cache.labels || [];
-               let i, ilen;
-
-               if (timestamps.length) {
-                       return timestamps;
-               }
-
-               const labels = me.getLabels();
-               for (i = 0, ilen = labels.length; i < ilen; ++i) {
-                       timestamps.push(me.parse(labels[i]));
-               }
-
-               // We could assume labels are in order and unique - but let's not
-               return (me._cache.labels = timestamps);
+       getDecimalForValue(value) {
+               return interpolate(this._table, value) / this._maxIndex;
        }
 
        /**
@@ -184,8 +147,8 @@ class TimeSeriesScale extends TimeScale {
        getValueForPixel(pixel) {
                const me = this;
                const offsets = me._offsets;
-               const pos = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
-               return interpolate(me._table, 'pos', pos, 'time');
+               const decimal = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
+               return interpolate(me._table, decimal * this._maxIndex, true);
        }
 }
 
index db7fb9c3948cd5398178dfc3f31a481413e31a5d..1a4ab6cc80b7475df66edb886ce099b687429ee4 100644 (file)
@@ -712,144 +712,147 @@ describe('Time scale tests', function() {
                });
        });
 
-       describe('when scale type', function() {
-               describe('is "timeseries"', function() {
-                       beforeEach(function() {
-                               this.chart = window.acquireChart({
-                                       type: 'line',
-                                       data: {
-                                               labels: ['2017', '2019', '2020', '2025', '2042'],
-                                               datasets: [{data: [0, 1, 2, 3, 4, 5]}]
-                                       },
-                                       options: {
-                                               scales: {
-                                                       x: {
-                                                               type: 'timeseries',
-                                                               time: {
-                                                                       parser: 'YYYY'
+       [true, false].forEach(function(normalized) {
+               describe('when normalized is ' + normalized + ' and scale type', function() {
+                       describe('is "timeseries"', function() {
+                               beforeEach(function() {
+                                       this.chart = window.acquireChart({
+                                               type: 'line',
+                                               data: {
+                                                       labels: ['2017', '2019', '2020', '2025', '2042'],
+                                                       datasets: [{data: [0, 1, 2, 3, 4]}]
+                                               },
+                                               options: {
+                                                       normalized,
+                                                       scales: {
+                                                               x: {
+                                                                       type: 'timeseries',
+                                                                       time: {
+                                                                               parser: 'YYYY'
+                                                                       },
+                                                                       ticks: {
+                                                                               source: 'labels'
+                                                                       }
                                                                },
-                                                               ticks: {
-                                                                       source: 'labels'
+                                                               y: {
+                                                                       display: false
                                                                }
-                                                       },
-                                                       y: {
-                                                               display: false
                                                        }
                                                }
-                                       }
+                                       });
                                });
-                       });
 
-                       it ('should space data out with the same gap, whatever their time values', function() {
-                               var scale = this.chart.scales.x;
-                               var start = scale.left;
-                               var slice = scale.width / 4;
-
-                               expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(start);
-                               expect(scale.getPixelForValue(moment('2019').valueOf())).toBeCloseToPixel(start + slice);
-                               expect(scale.getPixelForValue(moment('2020').valueOf())).toBeCloseToPixel(start + slice * 2);
-                               expect(scale.getPixelForValue(moment('2025').valueOf())).toBeCloseToPixel(start + slice * 3);
-                               expect(scale.getPixelForValue(moment('2042').valueOf())).toBeCloseToPixel(start + slice * 4);
-                       });
-                       it ('should add a step before if scale.min is before the first data', function() {
-                               var chart = this.chart;
-                               var scale = chart.scales.x;
-                               var options = chart.options.scales.x;
+                               it ('should space data out with the same gap, whatever their time values', function() {
+                                       var scale = this.chart.scales.x;
+                                       var start = scale.left;
+                                       var slice = scale.width / 4;
+
+                                       expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start);
+                                       expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice);
+                                       expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * 2);
+                                       expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * 3);
+                                       expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * 4);
+                               });
+                               it ('should add a step before if scale.min is before the first data', function() {
+                                       var chart = this.chart;
+                                       var scale = chart.scales.x;
+                                       var options = chart.options.scales.x;
 
-                               options.min = '2012';
-                               chart.update();
+                                       options.min = '2012';
+                                       chart.update();
 
-                               var start = scale.left;
-                               var slice = scale.width / 5;
+                                       var start = scale.left;
+                                       var slice = scale.width / 5;
 
-                               expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(start + slice);
-                               expect(scale.getPixelForValue(moment('2042').valueOf())).toBeCloseToPixel(start + slice * 5);
-                       });
-                       it ('should add a step after if scale.max is after the last data', function() {
-                               var chart = this.chart;
-                               var scale = chart.scales.x;
-                               var options = chart.options.scales.x;
+                                       expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(start + slice);
+                                       expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(start + slice * 5);
+                               });
+                               it ('should add a step after if scale.max is after the last data', function() {
+                                       var chart = this.chart;
+                                       var scale = chart.scales.x;
+                                       var options = chart.options.scales.x;
 
-                               options.max = '2050';
-                               chart.update();
+                                       options.max = '2050';
+                                       chart.update();
 
-                               var start = scale.left;
-                               var slice = scale.width / 5;
+                                       var start = scale.left;
+                                       var slice = scale.width / 5;
 
-                               expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(start);
-                               expect(scale.getPixelForValue(moment('2042').valueOf())).toBeCloseToPixel(start + slice * 4);
-                       });
-                       it ('should add steps before and after if scale.min/max are outside the data range', function() {
-                               var chart = this.chart;
-                               var scale = chart.scales.x;
-                               var options = chart.options.scales.x;
+                                       expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start);
+                                       expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * 4);
+                               });
+                               it ('should add steps before and after if scale.min/max are outside the data range', function() {
+                                       var chart = this.chart;
+                                       var scale = chart.scales.x;
+                                       var options = chart.options.scales.x;
 
-                               options.min = '2012';
-                               options.max = '2050';
-                               chart.update();
+                                       options.min = '2012';
+                                       options.max = '2050';
+                                       chart.update();
 
-                               var start = scale.left;
-                               var slice = scale.width / 6;
+                                       var start = scale.left;
+                                       var slice = scale.width / 6;
 
-                               expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(start + slice);
-                               expect(scale.getPixelForValue(moment('2042').valueOf())).toBeCloseToPixel(start + slice * 5);
+                                       expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(start + slice);
+                                       expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(start + slice * 5);
+                               });
                        });
-               });
-               describe('is "time"', function() {
-                       beforeEach(function() {
-                               this.chart = window.acquireChart({
-                                       type: 'line',
-                                       data: {
-                                               labels: ['2017', '2019', '2020', '2025', '2042'],
-                                               datasets: [{data: [0, 1, 2, 3, 4, 5]}]
-                                       },
-                                       options: {
-                                               scales: {
-                                                       x: {
-                                                               type: 'time',
-                                                               time: {
-                                                                       parser: 'YYYY'
+                       describe('is "time"', function() {
+                               beforeEach(function() {
+                                       this.chart = window.acquireChart({
+                                               type: 'line',
+                                               data: {
+                                                       labels: ['2017', '2019', '2020', '2025', '2042'],
+                                                       datasets: [{data: [0, 1, 2, 3, 4, 5]}]
+                                               },
+                                               options: {
+                                                       scales: {
+                                                               x: {
+                                                                       type: 'time',
+                                                                       time: {
+                                                                               parser: 'YYYY'
+                                                                       },
+                                                                       ticks: {
+                                                                               source: 'labels'
+                                                                       }
                                                                },
-                                                               ticks: {
-                                                                       source: 'labels'
+                                                               y: {
+                                                                       display: false
                                                                }
-                                                       },
-                                                       y: {
-                                                               display: false
                                                        }
                                                }
-                                       }
+                                       });
                                });
-                       });
 
-                       it ('should space data out with a gap relative to their time values', function() {
-                               var scale = this.chart.scales.x;
-                               var start = scale.left;
-                               var slice = scale.width / (2042 - 2017);
-
-                               expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(start);
-                               expect(scale.getPixelForValue(moment('2019').valueOf())).toBeCloseToPixel(start + slice * (2019 - 2017));
-                               expect(scale.getPixelForValue(moment('2020').valueOf())).toBeCloseToPixel(start + slice * (2020 - 2017));
-                               expect(scale.getPixelForValue(moment('2025').valueOf())).toBeCloseToPixel(start + slice * (2025 - 2017));
-                               expect(scale.getPixelForValue(moment('2042').valueOf())).toBeCloseToPixel(start + slice * (2042 - 2017));
-                       });
-                       it ('should take in account scale min and max if outside the ticks range', function() {
-                               var chart = this.chart;
-                               var scale = chart.scales.x;
-                               var options = chart.options.scales.x;
+                               it ('should space data out with a gap relative to their time values', function() {
+                                       var scale = this.chart.scales.x;
+                                       var start = scale.left;
+                                       var slice = scale.width / (2042 - 2017);
+
+                                       expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start);
+                                       expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice * (2019 - 2017));
+                                       expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * (2020 - 2017));
+                                       expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * (2025 - 2017));
+                                       expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * (2042 - 2017));
+                               });
+                               it ('should take in account scale min and max if outside the ticks range', function() {
+                                       var chart = this.chart;
+                                       var scale = chart.scales.x;
+                                       var options = chart.options.scales.x;
 
-                               options.min = '2012';
-                               options.max = '2050';
-                               chart.update();
+                                       options.min = '2012';
+                                       options.max = '2050';
+                                       chart.update();
 
-                               var start = scale.left;
-                               var slice = scale.width / (2050 - 2012);
+                                       var start = scale.left;
+                                       var slice = scale.width / (2050 - 2012);
 
-                               expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(start + slice * (2017 - 2012));
-                               expect(scale.getPixelForValue(moment('2019').valueOf())).toBeCloseToPixel(start + slice * (2019 - 2012));
-                               expect(scale.getPixelForValue(moment('2020').valueOf())).toBeCloseToPixel(start + slice * (2020 - 2012));
-                               expect(scale.getPixelForValue(moment('2025').valueOf())).toBeCloseToPixel(start + slice * (2025 - 2012));
-                               expect(scale.getPixelForValue(moment('2042').valueOf())).toBeCloseToPixel(start + slice * (2042 - 2012));
+                                       expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start + slice * (2017 - 2012));
+                                       expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice * (2019 - 2012));
+                                       expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * (2020 - 2012));
+                                       expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * (2025 - 2012));
+                                       expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * (2042 - 2012));
+                               });
                        });
                });
        });