diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 7be1f1125b4..fa523fa5dff 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -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++) { @@ -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); @@ -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; @@ -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({ @@ -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]; @@ -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, @@ -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); diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index f1a04df0b05..6c0792ff5ed 100755 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -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']); }); @@ -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']); }); @@ -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']); }); }); @@ -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']); }); @@ -255,6 +254,7 @@ 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']); @@ -262,16 +262,16 @@ describe('Time scale tests', function() { 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); @@ -279,11 +279,69 @@ describe('Time scale tests', function() { }); 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() { @@ -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({