]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
New time scale `ticks.bounds` option (#4556)
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Tue, 25 Jul 2017 08:12:53 +0000 (10:12 +0200)
committerGitHub <noreply@github.com>
Tue, 25 Jul 2017 08:12:53 +0000 (10:12 +0200)
`ticks.bounds` (`'data'`(default)|`'label'`): `data` preserves the data range while `labels` ensures that all labels are visible. This option is bypassed by the min/max time options.

Remove the useless time scale `_model` object containing private members: instead, make these members private (prefixed by `_`) part of the scale.

src/scales/scale.time.js
test/specs/global.deprecations.tests.js
test/specs/scale.time.tests.js

index 94bd7a82c7f241df062a3e7c1bbd40e84a6e387f..9b88feecc53dd9197321e12b01059362d552f200 100644 (file)
@@ -336,8 +336,33 @@ module.exports = function(Chart) {
                },
                ticks: {
                        autoSkip: false,
-                       mode: 'linear',   // 'linear|series'
-                       source: 'auto'    // 'auto|labels'
+
+                       /**
+                        * Ticks distribution along the scale:
+                        * - 'linear': ticks and data are spread according to their time (distances can vary),
+                        * - 'series': ticks and data are spread at the same distance from each other.
+                        * @see https://github.com/chartjs/Chart.js/pull/4507
+                        * @since 2.7.0
+                        */
+                       mode: 'linear',
+
+                       /**
+                        * Ticks generation input values:
+                        * - 'labels': generates ticks from user given `data.labels` values ONLY.
+                        * - 'auto': generates "optimal" ticks based on scale size and time options.
+                        * @see https://github.com/chartjs/Chart.js/pull/4507
+                        * @since 2.7.0
+                        */
+                       source: 'auto',
+
+                       /**
+                        * Ticks boundary strategy (bypassed by min/max time options)
+                        * - `data`: make sure data are fully visible, labels outside are removed
+                        * - `labels`: make sure labels are fully visible, data outside are truncated
+                        * @see https://github.com/chartjs/Chart.js/pull/4556
+                        * @since 2.7.0
+                        */
+                       bounds: 'data'
                }
        };
 
@@ -383,8 +408,8 @@ module.exports = function(Chart) {
                        var chart = me.chart;
                        var options = me.options;
                        var datasets = chart.data.datasets || [];
-                       var min = MAX_INTEGER;
-                       var max = MIN_INTEGER;
+                       var min = parse(options.time.min, me) || MAX_INTEGER;
+                       var max = parse(options.time.max, me) || MIN_INTEGER;
                        var timestamps = [];
                        var labels = [];
                        var i, j, ilen, jlen, data, timestamp;
@@ -420,29 +445,25 @@ module.exports = function(Chart) {
                                }
                        }
 
-                       // Enforce limits with user min/max options
-                       min = parse(options.time.min, me) || min;
-                       max = parse(options.time.max, me) || max;
-
                        // In case there is no valid min/max, let's use today limits
                        min = min === MAX_INTEGER ? +moment().startOf('day') : min;
                        max = max === MIN_INTEGER ? +moment().endOf('day') + 1 : max;
 
-                       me._model = {
-                               datasets: timestamps,
-                               horizontal: me.isHorizontal(),
-                               labels: labels.sort(sorter),    // Sort labels **after** data have been converted
-                               min: Math.min(min, max),        // Make sure that max is **strictly** higher ...
-                               max: Math.max(min + 1, max),    // ... than min (required by the lookup table)
-                               table: []
-                       };
+                       // Make sure that max is strictly higher than min (required by the lookup table)
+                       me.min = Math.min(min, max);
+                       me.max = Math.max(min + 1, max);
+
+                       // PRIVATE
+                       me._datasets = timestamps;
+                       me._horizontal = me.isHorizontal();
+                       me._labels = labels.sort(sorter);    // Sort labels **after** data have been converted
+                       me._table = [];
                },
 
                buildTicks: function() {
                        var me = this;
-                       var model = me._model;
-                       var min = model.min;
-                       var max = model.max;
+                       var min = me.min;
+                       var max = me.max;
                        var timeOpts = me.options.time;
                        var ticksOpts = me.options.ticks;
                        var formats = timeOpts.displayFormats;
@@ -458,14 +479,19 @@ module.exports = function(Chart) {
                                        || determineStepSize(min, max, unit, capacity);
 
                                timestamps = generate(min, max, unit, majorUnit, stepSize, timeOpts);
-
-                               // Expand min/max to the generated ticks
-                               min = helpers.isNullOrUndef(timeOpts.min) && timestamps.length ? timestamps[0] : min;
-                               max = helpers.isNullOrUndef(timeOpts.max) && timestamps.length ? timestamps[timestamps.length - 1] : max;
                        } else {
-                               timestamps = model.labels;
+                               timestamps = me._labels;
+                       }
+
+                       if (ticksOpts.bounds === 'labels' && timestamps.length) {
+                               min = timestamps[0];
+                               max = timestamps[timestamps.length - 1];
                        }
 
+                       // Enforce limits with user min/max options
+                       min = parse(timeOpts.min, me) || min;
+                       max = parse(timeOpts.max, me) || max;
+
                        // Remove ticks outside the min/max range
                        for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
                                timestamp = timestamps[i];
@@ -477,12 +503,13 @@ module.exports = function(Chart) {
                        me.ticks = ticks;
                        me.min = min;
                        me.max = max;
-                       me.unit = unit;
-                       me.majorUnit = majorUnit;
-                       me.displayFormat = formats[unit];
-                       me.majorDisplayFormat = formats[majorUnit];
 
-                       model.table = buildLookupTable(ticks, min, max, ticksOpts.mode === 'linear');
+                       // PRIVATE
+                       me._unit = unit;
+                       me._majorUnit = majorUnit;
+                       me._displayFormat = formats[unit];
+                       me._majorDisplayFormat = formats[majorUnit];
+                       me._table = buildLookupTable(ticks, min, max, ticksOpts.mode === 'linear');
                },
 
                getLabelForIndex: function(index, datasetIndex) {
@@ -510,11 +537,11 @@ module.exports = function(Chart) {
                        var me = this;
                        var options = me.options;
                        var time = tick.valueOf();
-                       var majorUnit = me.majorUnit;
-                       var majorFormat = me.majorDisplayFormat;
-                       var majorTime = tick.clone().startOf(me.majorUnit).valueOf();
+                       var majorUnit = me._majorUnit;
+                       var majorFormat = me._majorDisplayFormat;
+                       var majorTime = tick.clone().startOf(me._majorUnit).valueOf();
                        var major = majorUnit && majorFormat && time === majorTime;
-                       var formattedTick = tick.format(major ? majorFormat : me.displayFormat);
+                       var formattedTick = tick.format(major ? majorFormat : me._displayFormat);
                        var tickOpts = major ? options.ticks.major : options.ticks.minor;
                        var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback);
 
@@ -543,10 +570,9 @@ module.exports = function(Chart) {
                 */
                getPixelForOffset: function(time) {
                        var me = this;
-                       var model = me._model;
-                       var size = model.horizontal ? me.width : me.height;
-                       var start = model.horizontal ? me.left : me.top;
-                       var pos = interpolate(model.table, 'time', time, 'pos');
+                       var size = me._horizontal ? me.width : me.height;
+                       var start = me._horizontal ? me.left : me.top;
+                       var pos = interpolate(me._table, 'time', time, 'pos');
 
                        return start + size * pos;
                },
@@ -556,7 +582,7 @@ module.exports = function(Chart) {
                        var time = null;
 
                        if (index !== undefined && datasetIndex !== undefined) {
-                               time = me._model.datasets[datasetIndex][index];
+                               time = me._datasets[datasetIndex][index];
                        }
 
                        if (time === null) {
@@ -576,11 +602,10 @@ module.exports = function(Chart) {
 
                getValueForPixel: function(pixel) {
                        var me = this;
-                       var model = me._model;
-                       var size = model.horizontal ? me.width : me.height;
-                       var start = model.horizontal ? me.left : me.top;
+                       var size = me._horizontal ? me.width : me.height;
+                       var start = me._horizontal ? me.left : me.top;
                        var pos = size ? (pixel - start) / size : 0;
-                       var time = interpolate(model.table, 'pos', pos, 'time');
+                       var time = interpolate(me._table, 'pos', pos, 'time');
 
                        return moment(time);
                },
@@ -607,7 +632,7 @@ module.exports = function(Chart) {
                getLabelCapacity: function(exampleTime) {
                        var me = this;
 
-                       me.displayFormat = me.options.time.displayFormats.millisecond;  // Pick the longest format for guestimation
+                       me._displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation
 
                        var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []).value;
                        var tickLabelWidth = me.getLabelWidth(exampleLabel);
index 7bc07ebde0b0419b9c14666c6b9da91da7018b11..6b6c984898b144be36ce8cd62847fb28a84b6955 100644 (file)
@@ -268,6 +268,9 @@ describe('Deprecations', function() {
                                                                time: {
                                                                        unit: 'hour',
                                                                        unitStepSize: 2
+                                                               },
+                                                               ticks: {
+                                                                       bounds: 'labels'
                                                                }
                                                        }]
                                                }
index 87621d6b3ee23bdb2d8893d63808b7fedd747e38..fd5b03491e5a2efa7825727f3966f07b40b157ff 100755 (executable)
@@ -92,6 +92,7 @@ describe('Time scale tests', function() {
                                mirror: false,
                                mode: 'linear',
                                source: 'auto',
+                               bounds: 'data',
                                padding: 0,
                                reverse: false,
                                display: true,
@@ -144,7 +145,8 @@ describe('Time scale tests', function() {
                        scale.update(1000, 200);
                        var ticks = getTicksValues(scale.ticks);
 
-                       expect(ticks).toEqual(['Jan 2015', 'Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11']);
+                       // `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
+                       expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
                });
 
                it('should accept labels as date objects', function() {
@@ -155,7 +157,8 @@ describe('Time scale tests', function() {
                        scale.update(1000, 200);
                        var ticks = getTicksValues(scale.ticks);
 
-                       expect(ticks).toEqual(['Jan 2015', 'Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11']);
+                       // `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
+                       expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
                });
 
                it('should accept data as xy points', function() {
@@ -203,7 +206,8 @@ describe('Time scale tests', function() {
                        xScale.update(800, 200);
                        var ticks = getTicksValues(xScale.ticks);
 
-                       expect(ticks).toEqual(['Jan 2015', 'Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11']);
+                       // `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
+                       expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
                });
 
                it('should accept data as ty points', function() {
@@ -251,7 +255,8 @@ describe('Time scale tests', function() {
                        tScale.update(800, 200);
                        var ticks = getTicksValues(tScale.ticks);
 
-                       expect(ticks).toEqual(['Jan 2015', 'Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10', 'Jan 11']);
+                       // `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
+                       expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
                });
        });
 
@@ -259,12 +264,10 @@ describe('Time scale tests', function() {
                var chart = window.acquireChart({
                        type: 'line',
                        data: {
+                               labels: ['foo', 'bar'],
                                datasets: [{
                                        xAxisID: 'xScale0',
-                                       data: [{
-                                               x: 375068900,
-                                               y: 1
-                                       }]
+                                       data: [0, 1]
                                }],
                        },
                        options: {
@@ -277,8 +280,13 @@ describe('Time scale tests', function() {
                                                        unit: 'day',
                                                        round: true,
                                                        parser: function(label) {
-                                                               return moment.unix(label);
+                                                               return label === 'foo' ?
+                                                                       moment(946771200000) :  // 02/01/2000 @ 12:00am (UTC)
+                                                                       moment(1462665600000);  // 05/08/2016 @ 12:00am (UTC)
                                                        }
+                                               },
+                                               ticks: {
+                                                       source: 'labels'
                                                }
                                        }],
                                }
@@ -289,8 +297,8 @@ describe('Time scale tests', function() {
                var xScale = chart.scales.xScale0;
 
                // Counts down because the lines are drawn top to bottom
-               expect(xScale.ticks[0].value).toEqualOneOf(['Nov 19', 'Nov 20', 'Nov 21']); // handle time zone changes
-               expect(xScale.ticks[1].value).toEqualOneOf(['Nov 19', 'Nov 20', 'Nov 21']); // handle time zone changes
+               expect(xScale.ticks[0].value).toBe('Jan 2');
+               expect(xScale.ticks[1].value).toBe('May 8');
        });
 
        it('should build ticks using the config unit', function() {
@@ -313,8 +321,14 @@ describe('Time scale tests', function() {
                        labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days
                };
 
-               var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time'));
-               config.time.minUnit = 'day';
+               var config = Chart.helpers.mergeIf({
+                       time: {
+                               minUnit: 'day'
+                       },
+                       ticks: {
+                               bounds: 'labels'
+                       }
+               }, Chart.scaleService.getScaleDefaults('time'));
 
                var scale = createScale(mockData, config);
                var ticks = getTicksValues(scale.ticks);
@@ -327,9 +341,15 @@ describe('Time scale tests', function() {
                        labels: ['2015-01-01T20:00:00', '2015-02-02T21:00:00', '2015-02-21T01:00:00'], // days
                };
 
-               var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time'));
-               config.time.unit = 'week';
-               config.time.round = 'week';
+               var config = Chart.helpers.mergeIf({
+                       time: {
+                               unit: 'week',
+                               round: 'week'
+                       },
+                       ticks: {
+                               bounds: 'labels'
+                       }
+               }, Chart.scaleService.getScaleDefaults('time'));
 
                var scale = createScale(mockData, config);
                scale.update(800, 200);
@@ -345,9 +365,15 @@ describe('Time scale tests', function() {
                                labels: ['2015-01-01T20:00:00', '2015-01-01T21:00:00'],
                        };
 
-                       var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time'));
-                       config.time.unit = 'hour';
-                       config.time.stepSize = 2;
+                       var config = Chart.helpers.mergeIf({
+                               time: {
+                                       unit: 'hour',
+                                       stepSize: 2
+                               },
+                               ticks: {
+                                       bounds: 'labels'
+                               }
+                       }, Chart.scaleService.getScaleDefaults('time'));
 
                        var scale = createScale(mockData, config);
                        scale.update(2500, 200);
@@ -394,10 +420,16 @@ describe('Time scale tests', function() {
                        ]
                };
 
-               var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time'));
-               config.time.unit = 'week';
-               // Wednesday
-               config.time.isoWeekday = 3;
+               var config = Chart.helpers.mergeIf({
+                       time: {
+                               unit: 'week',
+                               isoWeekday: 3 // Wednesday
+                       },
+                       ticks: {
+                               bounds: 'labels'
+                       }
+               }, Chart.scaleService.getScaleDefaults('time'));
+
                var scale = createScale(mockData, config);
                var ticks = getTicksValues(scale.ticks);
 
@@ -405,50 +437,64 @@ describe('Time scale tests', function() {
        });
 
        describe('when rendering several days', function() {
-               var chart = window.acquireChart({
-                       type: 'line',
-                       data: {
-                               datasets: [{
-                                       xAxisID: 'xScale0',
-                                       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
-                       },
-                       options: {
-                               scales: {
-                                       xAxes: [{
-                                               id: 'xScale0',
-                                               type: 'time',
-                                               position: 'bottom'
+               beforeEach(function() {
+                       this.chart = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               xAxisID: 'xScale0',
+                                               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'
+                                       ]
+                               },
+                               options: {
+                                       scales: {
+                                               xAxes: [{
+                                                       id: 'xScale0',
+                                                       type: 'time',
+                                                       position: 'bottom'
+                                               }],
+                                       }
                                }
-                       }
-               });
+                       });
 
-               var xScale = chart.scales.xScale0;
+                       this.scale = this.chart.scales.xScale0;
+               });
 
                it('should be bounded by the nearest week beginnings', function() {
-                       expect(xScale.getValueForPixel(xScale.left)).toBeGreaterThan(moment(chart.data.labels[0]).startOf('week'));
-                       expect(xScale.getValueForPixel(xScale.right)).toBeLessThan(moment(chart.data.labels[chart.data.labels.length - 1]).add(1, 'week').endOf('week'));
+                       var chart = this.chart;
+                       var scale = this.scale;
+                       expect(scale.getValueForPixel(scale.left)).toBeGreaterThan(moment(chart.data.labels[0]).startOf('week'));
+                       expect(scale.getValueForPixel(scale.right)).toBeLessThan(moment(chart.data.labels[chart.data.labels.length - 1]).add(1, 'week').endOf('week'));
                });
 
                it('should convert between screen coordinates and times', function() {
-                       var timeRange = moment(xScale.max).valueOf() - moment(xScale.min).valueOf();
-                       var msPerPix = timeRange / xScale.width;
-                       var firstPointOffsetMs = moment(chart.config.data.labels[0]).valueOf() - xScale.min;
-                       var firstPointPixel = xScale.left + firstPointOffsetMs / msPerPix;
-                       var lastPointOffsetMs = moment(chart.config.data.labels[chart.config.data.labels.length - 1]).valueOf() - xScale.min;
-                       var lastPointPixel = xScale.left + lastPointOffsetMs / msPerPix;
-
-                       expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(firstPointPixel);
-                       expect(xScale.getPixelForValue(chart.data.labels[0])).toBeCloseToPixel(firstPointPixel);
-                       expect(xScale.getValueForPixel(firstPointPixel)).toBeCloseToTime({
+                       var chart = this.chart;
+                       var scale = this.scale;
+                       var timeRange = moment(scale.max).valueOf() - moment(scale.min).valueOf();
+                       var msPerPix = timeRange / scale.width;
+                       var firstPointOffsetMs = moment(chart.config.data.labels[0]).valueOf() - scale.min;
+                       var firstPointPixel = scale.left + firstPointOffsetMs / msPerPix;
+                       var lastPointOffsetMs = moment(chart.config.data.labels[chart.config.data.labels.length - 1]).valueOf() - scale.min;
+                       var lastPointPixel = scale.left + lastPointOffsetMs / msPerPix;
+
+                       expect(scale.getPixelForValue('', 0, 0)).toBeCloseToPixel(firstPointPixel);
+                       expect(scale.getPixelForValue(chart.data.labels[0])).toBeCloseToPixel(firstPointPixel);
+                       expect(scale.getValueForPixel(firstPointPixel)).toBeCloseToTime({
                                value: moment(chart.data.labels[0]),
                                unit: 'hour',
                        });
 
-                       expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(lastPointPixel);
-                       expect(xScale.getValueForPixel(lastPointPixel)).toBeCloseToTime({
+                       expect(scale.getPixelForValue('', 6, 0)).toBeCloseToPixel(lastPointPixel);
+                       expect(scale.getValueForPixel(lastPointPixel)).toBeCloseToTime({
                                value: moment(chart.data.labels[6]),
                                unit: 'hour'
                        });
@@ -456,51 +502,58 @@ describe('Time scale tests', function() {
        });
 
        describe('when rendering several years', function() {
-               var chart = window.acquireChart({
-                       type: 'line',
-                       data: {
-                               labels: ['2005-07-04', '2017-01-20'],
-                       },
-                       options: {
-                               scales: {
-                                       xAxes: [{
-                                               id: 'xScale0',
-                                               type: 'time',
-                                               position: 'bottom'
-                                       }],
+               beforeEach(function() {
+                       this.chart = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       labels: ['2005-07-04', '2017-01-20'],
+                               },
+                               options: {
+                                       scales: {
+                                               xAxes: [{
+                                                       id: 'xScale0',
+                                                       type: 'time',
+                                                       position: 'bottom',
+                                                       ticks: {
+                                                               bounds: 'labels'
+                                                       }
+                                               }],
+                                       }
                                }
-                       }
-               });
-
-               var xScale = chart.scales.xScale0;
-               xScale.update(800, 200);
+                       });
 
-               var step = xScale.ticks[1].time - xScale.ticks[0].time;
-               var stepsAmount = Math.floor((xScale.max - xScale.min) / step);
+                       this.scale = this.chart.scales.xScale0;
+                       this.scale.update(800, 200);
+               });
 
                it('should be bounded by nearest step\'s year start and end', function() {
-                       expect(xScale.getValueForPixel(xScale.left)).toBeCloseToTime({
-                               value: moment(xScale.min).startOf('year'),
+                       var scale = this.scale;
+                       var step = scale.ticks[1].time - scale.ticks[0].time;
+                       var stepsAmount = Math.floor((scale.max - scale.min) / step);
+
+                       expect(scale.getValueForPixel(scale.left)).toBeCloseToTime({
+                               value: moment(scale.min).startOf('year'),
                                unit: 'hour',
                        });
-                       expect(xScale.getValueForPixel(xScale.right)).toBeCloseToTime({
-                               value: moment(xScale.min + step * stepsAmount).endOf('year'),
+                       expect(scale.getValueForPixel(scale.right)).toBeCloseToTime({
+                               value: moment(scale.min + step * stepsAmount).endOf('year'),
                                unit: 'hour',
                        });
                });
 
                it('should build the correct ticks', function() {
                        // Where 'correct' is a two year spacing.
-                       expect(getTicksValues(xScale.ticks)).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2019']);
+                       expect(getTicksValues(this.scale.ticks)).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2019']);
                });
 
                it('should have ticks with accurate labels', function() {
-                       var ticks = xScale.ticks;
-                       var pixelsPerYear = xScale.width / 14;
+                       var scale = this.scale;
+                       var ticks = scale.ticks;
+                       var pixelsPerYear = scale.width / 14;
 
                        for (var i = 0; i < ticks.length - 1; i++) {
                                var offset = 2 * pixelsPerYear * i;
-                               expect(xScale.getValueForPixel(xScale.left + offset)).toBeCloseToTime({
+                               expect(scale.getValueForPixel(scale.left + offset)).toBeCloseToTime({
                                        value: moment(ticks[i].value + '-01-01'),
                                        unit: 'day',
                                        threshold: 0.5,
@@ -638,7 +691,7 @@ describe('Time scale tests', function() {
 
                                expect(scale.min).toEqual(+moment('2012', 'YYYY'));
                                expect(scale.max).toEqual(+moment('2051', 'YYYY'));
-                               expect(getTicksValues(this.chart.scales.x.ticks)).toEqual([
+                               expect(getTicksValues(scale.ticks)).toEqual([
                                        '2017', '2019', '2020', '2025', '2042']);
                        });
                        it ('should remove ticks that are not inside the min and max time range', function() {
@@ -652,7 +705,7 @@ describe('Time scale tests', function() {
 
                                expect(scale.min).toEqual(+moment('2022', 'YYYY'));
                                expect(scale.max).toEqual(+moment('2032', 'YYYY'));
-                               expect(getTicksValues(this.chart.scales.x.ticks)).toEqual([
+                               expect(getTicksValues(scale.ticks)).toEqual([
                                        '2025']);
                        });
                        it ('should not duplicate ticks if min and max are the labels limits', function() {
@@ -666,7 +719,7 @@ describe('Time scale tests', function() {
 
                                expect(scale.min).toEqual(+moment('2017', 'YYYY'));
                                expect(scale.max).toEqual(+moment('2042', 'YYYY'));
-                               expect(getTicksValues(this.chart.scales.x.ticks)).toEqual([
+                               expect(getTicksValues(scale.ticks)).toEqual([
                                        '2017', '2019', '2020', '2025', '2042']);
                        });
                        it ('should correctly handle empty `data.labels`', function() {
@@ -678,7 +731,7 @@ describe('Time scale tests', function() {
 
                                expect(scale.min).toEqual(+moment().startOf('day'));
                                expect(scale.max).toEqual(+moment().endOf('day') + 1);
-                               expect(getTicksValues(this.chart.scales.x.ticks)).toEqual([]);
+                               expect(getTicksValues(scale.ticks)).toEqual([]);
                        });
                });
        });
@@ -834,4 +887,155 @@ describe('Time scale tests', function() {
                        });
                });
        });
+
+       describe('when ticks.bounds', function() {
+               describe('is "data"', function() {
+                       it ('should preserve the data range', function() {
+                               var chart = window.acquireChart({
+                                       type: 'line',
+                                       data: {
+                                               labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'],
+                                               datasets: [{data: [0, 1, 2, 3, 4, 5]}]
+                                       },
+                                       options: {
+                                               scales: {
+                                                       xAxes: [{
+                                                               id: 'x',
+                                                               type: 'time',
+                                                               time: {
+                                                                       parser: 'MM/DD HH:mm',
+                                                                       unit: 'day'
+                                                               },
+                                                               ticks: {
+                                                                       bounds: 'data'
+                                                               }
+                                                       }],
+                                                       yAxes: [{
+                                                               display: false
+                                                       }]
+                                               }
+                                       }
+                               });
+
+                               var scale = chart.scales.x;
+
+                               expect(scale.min).toEqual(+moment('02/20 08:00', 'MM/DD HH:mm'));
+                               expect(scale.max).toEqual(+moment('02/23 11:00', 'MM/DD HH:mm'));
+                               expect(scale.getPixelForValue('02/20 08:00')).toBeCloseToPixel(scale.left);
+                               expect(scale.getPixelForValue('02/23 11:00')).toBeCloseToPixel(scale.left + scale.width);
+                               expect(getTicksValues(scale.ticks)).toEqual([
+                                       'Feb 21', 'Feb 22', 'Feb 23']);
+                       });
+               });
+
+               describe('is "labels"', function() {
+                       it('should preserve the label range', function() {
+                               var chart = window.acquireChart({
+                                       type: 'line',
+                                       data: {
+                                               labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'],
+                                               datasets: [{data: [0, 1, 2, 3, 4, 5]}]
+                                       },
+                                       options: {
+                                               scales: {
+                                                       xAxes: [{
+                                                               id: 'x',
+                                                               type: 'time',
+                                                               time: {
+                                                                       parser: 'MM/DD HH:mm',
+                                                                       unit: 'day'
+                                                               },
+                                                               ticks: {
+                                                                       bounds: 'labels'
+                                                               }
+                                                       }],
+                                                       yAxes: [{
+                                                               display: false
+                                                       }]
+                                               }
+                                       }
+                               });
+
+                               var scale = chart.scales.x;
+                               var ticks = scale.ticks;
+
+                               expect(scale.min).toEqual(ticks[0].time);
+                               expect(scale.max).toEqual(ticks[ticks.length - 1].time);
+                               expect(scale.getPixelForValue('02/20 08:00')).toBeCloseToPixel(77);
+                               expect(scale.getPixelForValue('02/23 11:00')).toBeCloseToPixel(412);
+                               expect(getTicksValues(scale.ticks)).toEqual([
+                                       'Feb 20', 'Feb 21', 'Feb 22', 'Feb 23', 'Feb 24']);
+                       });
+               });
+       });
+
+       describe('when time.min and/or time.max are defined', function() {
+               ['auto', 'labels'].forEach(function(source) {
+                       ['data', 'labels'].forEach(function(bounds) {
+                               describe('and source is "' + source + '" and bounds "' + bounds + '"', function() {
+                                       beforeEach(function() {
+                                               this.chart = window.acquireChart({
+                                                       type: 'line',
+                                                       data: {
+                                                               labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'],
+                                                               datasets: [{data: [0, 1, 2, 3, 4, 5]}]
+                                                       },
+                                                       options: {
+                                                               scales: {
+                                                                       xAxes: [{
+                                                                               id: 'x',
+                                                                               type: 'time',
+                                                                               time: {
+                                                                                       parser: 'MM/DD HH:mm',
+                                                                                       unit: 'day'
+                                                                               },
+                                                                               ticks: {
+                                                                                       source: source,
+                                                                                       bounds: bounds
+                                                                               }
+                                                                       }],
+                                                                       yAxes: [{
+                                                                               display: false
+                                                                       }]
+                                                               }
+                                                       }
+                                               });
+                                       });
+
+                                       it ('should expand scale to the min/max range', function() {
+                                               var chart = this.chart;
+                                               var scale = chart.scales.x;
+                                               var options = chart.options.scales.xAxes[0];
+                                               var min = '02/19 07:00';
+                                               var max = '02/24 08:00';
+
+                                               options.time.min = min;
+                                               options.time.max = max;
+                                               chart.update();
+
+                                               expect(scale.min).toEqual(+moment(min, 'MM/DD HH:mm'));
+                                               expect(scale.max).toEqual(+moment(max, 'MM/DD HH:mm'));
+                                               expect(scale.getPixelForValue(min)).toBeCloseToPixel(scale.left);
+                                               expect(scale.getPixelForValue(max)).toBeCloseToPixel(scale.left + scale.width);
+                                       });
+                                       it ('should shrink scale to the min/max range', function() {
+                                               var chart = this.chart;
+                                               var scale = chart.scales.x;
+                                               var options = chart.options.scales.xAxes[0];
+                                               var min = '02/21 07:00';
+                                               var max = '02/22 20:00';
+
+                                               options.time.min = min;
+                                               options.time.max = max;
+                                               chart.update();
+
+                                               expect(scale.min).toEqual(+moment(min, 'MM/DD HH:mm'));
+                                               expect(scale.max).toEqual(+moment(max, 'MM/DD HH:mm'));
+                                               expect(scale.getPixelForValue(min)).toBeCloseToPixel(scale.left);
+                                               expect(scale.getPixelForValue(max)).toBeCloseToPixel(scale.left + scale.width);
+                                       });
+                               });
+                       });
+               });
+       });
 });