From da417d819c9829ed28607e721721f577eebf9f49 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 25 Jul 2017 10:12:53 +0200 Subject: [PATCH] New time scale `ticks.bounds` option (#4556) `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 | 111 ++++--- test/specs/global.deprecations.tests.js | 3 + test/specs/scale.time.tests.js | 376 ++++++++++++++++++------ 3 files changed, 361 insertions(+), 129 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 94bd7a82c7f..9b88feecc53 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -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); diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 7bc07ebde0b..6b6c984898b 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -268,6 +268,9 @@ describe('Deprecations', function() { time: { unit: 'hour', unitStepSize: 2 + }, + ticks: { + bounds: 'labels' } }] } diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 87621d6b3ee..fd5b03491e5 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -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); + }); + }); + }); + }); + }); });