Skip to content

Commit

Permalink
Re-implement tick generation
Browse files Browse the repository at this point in the history
As in v2.5
  • Loading branch information
tredston committed Apr 1, 2017
1 parent d0aef0a commit 531ecf3
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 54 deletions.
116 changes: 70 additions & 46 deletions src/scales/scale.time.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ moment = typeof(moment) === 'function' ? moment : window.moment;
module.exports = function(Chart) {

var helpers = Chart.helpers;
var time = {
var interval = {
millisecond: {
size: 1,
steps: [1, 2, 5, 10, 20, 50, 100, 250, 500]
Expand Down Expand Up @@ -115,15 +115,14 @@ module.exports = function(Chart) {
* @param max {Number} scale maximum
* @return {String} the unit to use
*/
function determineUnit(minUnit, min, max) {
var units = Object.keys(time);
var maxTicks = 11;
function determineUnit(minUnit, min, max, maxTicks) {
var units = Object.keys(interval);
var unit;
var numUnits = units.length;

for (var i = units.indexOf(minUnit); i < numUnits; i++) {
unit = units[i];
var unitDetails = time[unit];
var unitDetails = interval[unit];
var steps = (unitDetails.steps && unitDetails.steps[unitDetails.steps.length - 1]) || unitDetails.maxStep;
if (steps === undefined || Math.ceil((max - min) / (steps * unitDetails.size)) <= maxTicks) {
break;
Expand All @@ -140,10 +139,9 @@ module.exports = function(Chart) {
* @param unit {String} the unit determined by the {@see determineUnit} method
* @return {Number} the axis step size as a multiple of unit
*/
function determineStepSize(min, max, unit) {
function determineStepSize(min, max, unit, maxTicks) {
// Using our unit, figoure out what we need to scale as
var maxTicks = 11; // eventually configure this
var unitDefinition = time[unit];
var unitDefinition = interval[unit];
var unitSizeInMilliSeconds = unitDefinition.size;
var sizeInUnits = Math.ceil((max - min) / unitSizeInMilliSeconds);
var multiplier = 1;
Expand All @@ -166,43 +164,60 @@ module.exports = function(Chart) {
}

/**
* @function Chart.Ticks.generators.time
* @param generationOptions {ITimeGeneratorOptions} the options for generation
* Helper for generating axis labels.
* @param options {ITimeGeneratorOptions} the options for generation
* @param dataRange {IRange} the data range
* @param niceRange {IRange} the pretty range to display
* @return {Number[]} ticks
*/
Chart.Ticks.generators.time = function(generationOptions, dataRange) {
function generateTicks(options, dataRange, niceRange) {
var ticks = [];
var stepSize = generationOptions.stepSize;
if (options.maxTicks) {
var stepSize = options.stepSize;
ticks.push(options.min !== undefined ? options.min : niceRange.min);
var cur = moment(niceRange.min);
while (cur.add(stepSize, options.unit).valueOf() < niceRange.max) {
ticks.push(cur.valueOf());
}
var realMax = options.max || niceRange.max;
var minSpacing = (dataRange.max - dataRange.min) / options.maxTicks;
var lastSpacing = realMax - ticks[ticks.length - 1];
if (ticks[ticks.length - 1] !== realMax) {
ticks.push(realMax);
}
}
return ticks;
}

/**
* @function Chart.Ticks.generators.time
* @param options {ITimeGeneratorOptions} the options for generation
* @param dataRange {IRange} the data range
* @return {Number[]} ticks
*/
Chart.Ticks.generators.time = function(options, dataRange) {
var niceMin;
var niceMax;
var isoWeekday = generationOptions.isoWeekday;
if (generationOptions.unit === 'week' && isoWeekday !== false) {
var isoWeekday = options.isoWeekday;
if (options.unit === 'week' && isoWeekday !== false) {
niceMin = moment(dataRange.min).startOf('isoWeek').isoWeekday(isoWeekday).valueOf();
niceMax = moment(dataRange.max).startOf('isoWeek').isoWeekday(isoWeekday);
if (dataRange.max - niceMax > 0) {
niceMax.add(1, 'week');
}
niceMax = niceMax.valueOf();
} else {
niceMin = moment(dataRange.min).startOf(generationOptions.unit).valueOf();
niceMax = moment(dataRange.max).startOf(generationOptions.unit);
niceMin = moment(dataRange.min).startOf(options.unit).valueOf();
niceMax = moment(dataRange.max).startOf(options.unit);
if (dataRange.max - niceMax > 0) {
niceMax.add(1, generationOptions.unit);
niceMax.add(1, options.unit);
}
niceMax = niceMax.valueOf();
}

// Put the values into the ticks array
ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin);
var cur = moment(niceMin);
while (cur.add(stepSize, generationOptions.unit).valueOf() < niceMax) {
ticks.push(cur.valueOf());
}
ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax);

return ticks;
return generateTicks(options, dataRange, {
min: niceMin,
max: niceMax
});
};

var TimeScale = Chart.Scale.extend({
Expand Down Expand Up @@ -299,32 +314,19 @@ module.exports = function(Chart) {
maxTimestamp = parseTime(me, timeOpts.max).valueOf();
}

var unit;
if (timeOpts.unit) {
unit = timeOpts.unit;
} else {
// Auto Determine Unit
unit = determineUnit(timeOpts.minUnit, minTimestamp || dataMin, maxTimestamp || dataMax);
}
var maxTicks = me.getLabelCapacity(minTimestamp || dataMin);
var unit = timeOpts.unit || determineUnit(timeOpts.minUnit, minTimestamp || dataMin, maxTimestamp || dataMax, maxTicks);
me.displayFormat = timeOpts.displayFormats[unit];

var stepSize;
if (timeOpts.stepSize) {
stepSize = timeOpts.stepSize;
} else {
// Auto determine step size
stepSize = determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit);
}

var timeGeneratorOptions = {
maxTicks: 11,
var stepSize = timeOpts.stepSize || determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit, maxTicks);
var ticks = me.ticks = Chart.Ticks.generators.time({
maxTicks: maxTicks,
min: minTimestamp,
max: maxTimestamp,
stepSize: stepSize,
unit: unit,
isoWeekday: timeOpts.isoWeekday
};
var ticks = me.ticks = Chart.Ticks.generators.time(timeGeneratorOptions, {
}, {
min: dataMin,
max: dataMax
});
Expand Down Expand Up @@ -413,6 +415,28 @@ module.exports = function(Chart) {
var offset = (pixel - (me.isHorizontal() ? me.left : me.top)) / innerDimension;
return moment(me.min + (offset * (me.max - me.min)));
},
// Crude approximation of what the label width might be
getLabelWidth: function(label) {
var me = this;
var ticks = me.options.ticks;

var tickLabelWidth = me.ctx.measureText(label).width;
var cosRotation = Math.cos(helpers.toRadians(ticks.maxRotation));
var sinRotation = Math.sin(helpers.toRadians(ticks.maxRotation));
var tickFontSize = helpers.getValueOrDefault(ticks.fontSize, Chart.defaults.global.defaultFontSize);
return (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation);
},
getLabelCapacity: function(exampleTime) {
var me = this;

me.displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation
var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []);
var tickLabelWidth = me.getLabelWidth(exampleLabel);

var innerWidth = me.isHorizontal() ? me.width : me.height;
var labelCapacity = innerWidth / tickLabelWidth;
return labelCapacity;
}
});
Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig);

Expand Down
75 changes: 67 additions & 8 deletions test/specs/scale.time.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,11 @@ describe('Time scale tests', function() {

it('should accept labels as strings', function() {
var mockData = {
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
labels: ['2015-01-01T12: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
};

var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('time'));

// Counts down because the lines are drawn top to bottom
scale.update(1000, 200);
expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']);
});

Expand All @@ -133,8 +132,7 @@ describe('Time scale tests', function() {
labels: [newDateFromRef(0), newDateFromRef(1), newDateFromRef(2), newDateFromRef(4), newDateFromRef(6), newDateFromRef(7), newDateFromRef(9)], // days
};
var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('time'));

// Counts down because the lines are drawn top to bottom
scale.update(1000, 200);
expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']);
});

Expand Down Expand Up @@ -179,8 +177,8 @@ describe('Time scale tests', function() {
}
});

// Counts down because the lines are drawn top to bottom
var xScale = chart.scales.xScale0;
xScale.update(800, 200);
expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']);
});
});
Expand Down Expand Up @@ -232,6 +230,7 @@ describe('Time scale tests', function() {
config.time.unit = 'hour';

var scale = createScale(mockData, config);
scale.update(2500, 200);
expect(scale.ticks).toEqual(['Jan 1, 8PM', 'Jan 1, 9PM', 'Jan 1, 10PM', 'Jan 1, 11PM', 'Jan 2, 12AM', 'Jan 2, 1AM', 'Jan 2, 2AM', 'Jan 2, 3AM', 'Jan 2, 4AM', 'Jan 2, 5AM', 'Jan 2, 6AM', 'Jan 2, 7AM', 'Jan 2, 8AM', 'Jan 2, 9AM', 'Jan 2, 10AM', 'Jan 2, 11AM', 'Jan 2, 12PM', 'Jan 2, 1PM', 'Jan 2, 2PM', 'Jan 2, 3PM', 'Jan 2, 4PM', 'Jan 2, 5PM', 'Jan 2, 6PM', 'Jan 2, 7PM', 'Jan 2, 8PM', 'Jan 2, 9PM']);
});

Expand All @@ -257,35 +256,94 @@ describe('Time scale tests', function() {
config.time.round = 'week';

var scale = createScale(mockData, config);
scale.update(800, 200);

// last date is feb 15 because we round to start of week
expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 1, 2015', 'Feb 8, 2015', 'Feb 15, 2015']);
});

describe('when specifying limits', function() {
var mockData = {
labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], // days
labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'],
};

var config;
beforeEach(function() {
config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time'));
config.time.unit = 'day';
});

it('should use the min option', function() {
config.time.unit = 'day';
config.time.min = '2014-12-29T04:00:00';

var scale = createScale(mockData, config);
expect(scale.ticks[0]).toEqual('Dec 29, 2014');
});

it('should use the max option', function() {
config.time.unit = 'day';
config.time.max = '2015-01-05T06:00:00';

var scale = createScale(mockData, config);
expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 5, 2015');
});

it('should not have overlapping ticks', function() {
var data = {
labels: ['2015-01-01', '2015-06-01'],
};
config.time.max = '2015-06-02';

var scale = createScale(data, config);
scale.update(1000, 200);

for (var i = 0; i < scale.ticks.length - 1; i++) {
var firstPixel = scale.getPixelForTick(i);
var secondPixel = scale.getPixelForTick(i + 1);
var spacing = secondPixel - firstPixel;
var firstWidth = scale.getLabelWidth(scale.ticks[i]);
var secondWidth = scale.getLabelWidth(scale.ticks[i + 1]);
expect(spacing).toBeGreaterThan((secondWidth / 2) + (firstWidth / 2));
}
});
});

it('should not have overlapping ticks', function() {
// Regression test for: https://github.com/chartjs/Chart.js/issues/2249
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [{
xAxisID: 'xScale0',
data: [{
x: 1383846626377,
y: 66
}, {
x: 1471515416119,
y: 46
}]
}]
},
options: {
scales: {
xAxes: [{
id: 'xScale0',
type: 'time',
}]
}
}
});
var scale = chart.scales.xScale0;
scale.update(24633, 160);

for (var i = 0; i < scale.ticks.length - 1; i++) {
var firstPixel = scale.getPixelForTick(i);
var secondPixel = scale.getPixelForTick(i + 1);
var spacing = secondPixel - firstPixel;
var firstWidth = scale.getLabelWidth(scale.ticks[i]);
var secondWidth = scale.getLabelWidth(scale.ticks[i + 1]);
expect(spacing).toBeGreaterThan((secondWidth / 2) + (firstWidth / 2));
}
});

it('Should use the isoWeekday option', function() {
Expand Down Expand Up @@ -380,6 +438,7 @@ describe('Time scale tests', function() {
});

var xScale = chart.scales.xScale0;
xScale.update(800, 200);

it('should be bounded by nearest year starts', function() {
expect(xScale.getValueForPixel(xScale.left)).toBeCloseToTime({
Expand Down

0 comments on commit 531ecf3

Please sign in to comment.