Skip to content

Commit

Permalink
New time scale ticks.source: 'data' option (chartjs#4568)
Browse files Browse the repository at this point in the history
This new option value generates ticks from data (including labels from {t|x|y} data objects).
  • Loading branch information
simonbrunel authored and yofreke committed Dec 30, 2017
1 parent f1f9978 commit fe46561
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 28 deletions.
41 changes: 28 additions & 13 deletions src/scales/scale.time.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,19 +260,24 @@ 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);
var last = moment(max);
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);
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
}
Expand Down
106 changes: 91 additions & 15 deletions test/specs/scale.time.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -694,33 +694,100 @@ 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;
var scale = chart.scales.x;
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;
Expand All @@ -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']);
});
});
});
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand All @@ -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();
});
});
});
});
Expand Down

0 comments on commit fe46561

Please sign in to comment.