From fe46561dfc23be9ba4892183ab891364388293f3 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 27 Jul 2017 06:41:09 +0200 Subject: [PATCH] New time scale `ticks.source: 'data'` option (#4568) This new option value generates ticks from data (including labels from {t|x|y} data objects). --- src/scales/scale.time.js | 41 +++++++++---- test/specs/scale.time.tests.js | 106 ++++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 28 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 9b88feecc53..4d9cc6c4cf8 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -260,12 +260,13 @@ function determineMajorUnit(unit) { } /** - * Generates timestamps between min and max, rounded to the `minor` unit, aligned on - * the `major` unit, spaced with `stepSize` and using the given scale time `options`. + * Generates a maximum of `capacity` timestamps between min and max, rounded to the + * `minor` unit, aligned on the `major` unit and using the given scale time `options`. * Important: this method can return ticks outside the min and max range, it's the * responsibility of the calling code to clamp values if needed. */ -function generate(min, max, minor, major, stepSize, options) { +function generate(min, max, minor, major, capacity, options) { + var stepSize = helpers.valueOrDefault(options.stepSize, options.unitStepSize); var weekday = minor === 'week' ? options.isoWeekday : false; var interval = INTERVALS[minor]; var first = moment(min); @@ -273,6 +274,10 @@ function generate(min, max, minor, major, stepSize, options) { var ticks = []; var time; + if (!stepSize) { + stepSize = determineStepSize(min, max, minor, capacity); + } + // For 'week' unit, handle the first day of week option if (weekday) { first = first.isoWeekday(weekday); @@ -348,8 +353,9 @@ module.exports = function(Chart) { /** * 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. + * - 'data': generates ticks from data (including labels from data {t|x|y} objects). + * - 'labels': generates ticks from user given `data.labels` values ONLY. * @see https://github.com/chartjs/Chart.js/pull/4507 * @since 2.7.0 */ @@ -472,15 +478,22 @@ module.exports = function(Chart) { var majorUnit = determineMajorUnit(unit); var timestamps = []; var ticks = []; - var i, ilen, timestamp, stepSize; - - if (ticksOpts.source === 'auto') { - stepSize = helpers.valueOrDefault(timeOpts.stepSize, timeOpts.unitStepSize) - || determineStepSize(min, max, unit, capacity); + var hash = {}; + var i, ilen, timestamp; - timestamps = generate(min, max, unit, majorUnit, stepSize, timeOpts); - } else { + switch (ticksOpts.source) { + case 'data': + for (i = 0, ilen = me._datasets.length; i < ilen; ++i) { + timestamps.push.apply(timestamps, me._datasets[i]); + } + timestamps.sort(sorter); + break; + case 'labels': timestamps = me._labels; + break; + case 'auto': + default: + timestamps = generate(min, max, unit, majorUnit, capacity, timeOpts); } if (ticksOpts.bounds === 'labels' && timestamps.length) { @@ -492,10 +505,12 @@ module.exports = function(Chart) { min = parse(timeOpts.min, me) || min; max = parse(timeOpts.max, me) || max; - // Remove ticks outside the min/max range + // Remove ticks outside the min/max range and duplicated entries for (i = 0, ilen = timestamps.length; i < ilen; ++i) { timestamp = timestamps[i]; - if (timestamp >= min && timestamp <= max) { + if (timestamp >= min && timestamp <= max && !hash[timestamp]) { + // hash is used to efficiently detect timestamp duplicates + hash[timestamp] = true; ticks.push(timestamp); } } diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index fd5b03491e5..e46db676a03 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -281,8 +281,8 @@ describe('Time scale tests', function() { round: true, parser: function(label) { return label === 'foo' ? - moment(946771200000) : // 02/01/2000 @ 12:00am (UTC) - moment(1462665600000); // 05/08/2016 @ 12:00am (UTC) + moment('2000/01/02', 'YYYY/MM/DD') : + moment('2016/05/08', 'YYYY/MM/DD'); } }, ticks: { @@ -694,19 +694,86 @@ describe('Time scale tests', function() { 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() { + it ('should not duplicate ticks if min and max are the labels limits', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.xAxes[0]; - options.time.min = '2022'; - options.time.max = '2032'; + options.time.min = '2017'; + options.time.max = '2042'; chart.update(); - expect(scale.min).toEqual(+moment('2022', 'YYYY')); - expect(scale.max).toEqual(+moment('2032', 'YYYY')); + expect(scale.min).toEqual(+moment('2017', 'YYYY')); + expect(scale.max).toEqual(+moment('2042', 'YYYY')); expect(getTicksValues(scale.ticks)).toEqual([ - '2025']); + '2017', '2019', '2020', '2025', '2042']); + }); + it ('should correctly handle empty `data.labels`', function() { + var chart = this.chart; + var scale = chart.scales.x; + + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('day')); + expect(scale.max).toEqual(+moment().endOf('day') + 1); + expect(getTicksValues(scale.ticks)).toEqual([]); + }); + }); + + describe('is "data"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [ + {data: [0, 1, 2, 3, 4, 5]}, + {data: [ + {t: '2018', y: 6}, + {t: '2020', y: 7}, + {t: '2043', y: 8} + ]} + ] + }, + options: { + scales: { + xAxes: [{ + id: 'x', + type: 'time', + time: { + parser: 'YYYY' + }, + ticks: { + source: 'data' + } + }] + } + } + }); + }); + + it ('should generate ticks from "datasets.data"', function() { + var scale = this.chart.scales.x; + + expect(scale.min).toEqual(+moment('2017', 'YYYY')); + expect(scale.max).toEqual(+moment('2043', 'YYYY')); + expect(getTicksValues(scale.ticks)).toEqual([ + '2017', '2018', '2019', '2020', '2025', '2042', '2043']); + }); + it ('should not add ticks for min and max if they extend the labels range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.min = '2012'; + options.time.max = '2051'; + chart.update(); + + expect(scale.min).toEqual(+moment('2012', 'YYYY')); + expect(scale.max).toEqual(+moment('2051', 'YYYY')); + expect(getTicksValues(scale.ticks)).toEqual([ + '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); it ('should not duplicate ticks if min and max are the labels limits', function() { var chart = this.chart; @@ -714,13 +781,13 @@ describe('Time scale tests', function() { var options = chart.options.scales.xAxes[0]; options.time.min = '2017'; - options.time.max = '2042'; + options.time.max = '2043'; chart.update(); expect(scale.min).toEqual(+moment('2017', 'YYYY')); - expect(scale.max).toEqual(+moment('2042', 'YYYY')); + expect(scale.max).toEqual(+moment('2043', 'YYYY')); expect(getTicksValues(scale.ticks)).toEqual([ - '2017', '2019', '2020', '2025', '2042']); + '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); it ('should correctly handle empty `data.labels`', function() { var chart = this.chart; @@ -729,9 +796,10 @@ describe('Time scale tests', function() { chart.data.labels = []; chart.update(); - expect(scale.min).toEqual(+moment().startOf('day')); - expect(scale.max).toEqual(+moment().endOf('day') + 1); - expect(getTicksValues(scale.ticks)).toEqual([]); + expect(scale.min).toEqual(+moment('2018', 'YYYY')); + expect(scale.max).toEqual(+moment('2043', 'YYYY')); + expect(getTicksValues(scale.ticks)).toEqual([ + '2018', '2020', '2043']); }); }); }); @@ -970,7 +1038,7 @@ describe('Time scale tests', function() { }); describe('when time.min and/or time.max are defined', function() { - ['auto', 'labels'].forEach(function(source) { + ['auto', 'data', 'labels'].forEach(function(source) { ['data', 'labels'].forEach(function(bounds) { describe('and source is "' + source + '" and bounds "' + bounds + '"', function() { beforeEach(function() { @@ -1017,6 +1085,10 @@ describe('Time scale tests', function() { 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); + scale.ticks.forEach(function(tick) { + expect(tick.time >= +moment(min, 'MM/DD HH:mm')).toBeTruthy(); + expect(tick.time <= +moment(max, 'MM/DD HH:mm')).toBeTruthy(); + }); }); it ('should shrink scale to the min/max range', function() { var chart = this.chart; @@ -1033,6 +1105,10 @@ describe('Time scale tests', function() { 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); + scale.ticks.forEach(function(tick) { + expect(tick.time >= +moment(min, 'MM/DD HH:mm')).toBeTruthy(); + expect(tick.time <= +moment(max, 'MM/DD HH:mm')).toBeTruthy(); + }); }); }); });