Skip to content

Commit

Permalink
Attempt to calculate a sensible number of ticks
Browse files Browse the repository at this point in the history
Estimate based on expected label size.

Try not to create overlapping ticks.
  • Loading branch information
Thomas Redston authored and tredston committed Feb 16, 2017
1 parent e5cd916 commit 7190f04
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 28 deletions.
76 changes: 56 additions & 20 deletions src/scales/scale.time.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ module.exports = function(Chart) {
* @param max {Number} scale maximum
* @return {String} the unit to use
*/
function determineUnit(minUnit, min, max) {
function determineUnit(minUnit, min, max, maxTicks) {
var units = Object.keys(time);
var maxTicks = 11;
var unit;

for (var i = units.indexOf(minUnit); i < units.length; i++) {
Expand Down Expand Up @@ -144,9 +143,8 @@ 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 unitSizeInMilliSeconds = unitDefinition.size;
var sizeInUnits = Math.ceil((max - min) / unitSizeInMilliSeconds);
Expand All @@ -169,15 +167,38 @@ module.exports = function(Chart) {
}

/**
* @function Chart.Ticks.generators.time
* Helper for generating axis labels.
* @param generationOptions {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(generationOptions, dataRange, niceRange) {
var ticks = [];
var stepSize = generationOptions.stepSize;
ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceRange.min);
var cur = moment(niceRange.min);
while (cur.add(stepSize, generationOptions.unit).valueOf() < niceRange.max) {
ticks.push(cur.valueOf());
}
var realMax = generationOptions.max || niceRange.max;
var minSpacing = (dataRange.max - dataRange.min) / generationOptions.maxTicks;
var lastSpacing = realMax - ticks[ticks.length - 1];
if (lastSpacing >= minSpacing) {
ticks.push(realMax);
} else {
ticks[ticks.length - 1] = realMax;
}
return ticks;
}

/**
* @function Chart.Ticks.generators.time
* @param generationOptions {ITimeGeneratorOptions} the options for generation
* @param dataRange {IRange} the data range
* @return {Number[]} ticks
*/
Chart.Ticks.generators.time = function(generationOptions, dataRange) {
var niceMin;
var niceMax;
var isoWeekday = generationOptions.isoWeekday;
Expand All @@ -196,16 +217,10 @@ module.exports = function(Chart) {
}
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(generationOptions, dataRange, {
min: niceMin,
max: niceMax
});
};

var TimeScale = Chart.Scale.extend({
Expand Down Expand Up @@ -302,12 +317,13 @@ module.exports = function(Chart) {
maxTimestamp = parseTime(me, timeOpts.max).valueOf();
}

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

Expand All @@ -316,11 +332,10 @@ module.exports = function(Chart) {
stepSize = timeOpts.stepSize;
} else {
// Auto determine step size
stepSize = determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit);
stepSize = determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit, maxTicks);
}

var timeGeneratorOptions = {
maxTicks: 11,
maxTicks: maxTicks,
min: minTimestamp,
max: maxTimestamp,
stepSize: stepSize,
Expand Down Expand Up @@ -419,6 +434,27 @@ 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 tickLabelWidth = me.ctx.measureText(label).width;
var cosRotation = Math.cos(helpers.toRadians(me.options.ticks.maxRotation));
var sinRotation = Math.sin(helpers.toRadians(me.options.ticks.maxRotation));
var tickFontSize = helpers.getValueOrDefault(me.options.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/scale.time.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,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 @@ -131,8 +130,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 @@ -177,8 +175,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 @@ -230,6 +228,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 @@ -255,35 +254,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 @@ -378,6 +436,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 7190f04

Please sign in to comment.