}
/**
- * 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);
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);
/**
* 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
*/
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) {
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);
}
}
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: {
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;
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;
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']);
});
});
});
});
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() {
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;
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();
+ });
});
});
});