]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Make autoskip aware of major ticks (#6509)
authorBen McCann <322311+benmccann@users.noreply.github.com>
Sat, 19 Oct 2019 20:13:04 +0000 (13:13 -0700)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sat, 19 Oct 2019 20:13:04 +0000 (16:13 -0400)
* Make autoskip aware of major ticks
* Address review comments
* Fix codeclimate warning
* Add test for major and minor tick autoskipping
* Revert change for determining _majorUnit and fix sample

samples/scales/time/financial.html
src/core/core.scale.js
src/scales/scale.time.js
test/specs/scale.time.tests.js

index 084bacc36229e412309d801d3404995b45089c57..c4eb040423f5ef3e6841a894aef6d48099477ee3 100644 (file)
@@ -76,7 +76,7 @@
                        var now = moment();
                        var data = [];
                        var lessThanDay = unitLessThanDay();
-                       for (; data.length < 60 && date.isBefore(now); date = date.clone().add(1, unit).startOf(unit)) {
+                       for (; data.length < 600 && date.isBefore(now); date = date.clone().add(1, unit).startOf(unit)) {
                                if (outsideMarketHours(date)) {
                                        if (!lessThanDay || !beforeNineThirty(date)) {
                                                date = date.clone().add(date.isoWeekday() >= 5 ? 8 - date.isoWeekday() : 1, 'day');
                                }]
                        },
                        options: {
+                               animation: {
+                                       duration: 0
+                               },
                                scales: {
                                        xAxes: [{
                                                type: 'time',
                                                distribution: 'series',
                                                ticks: {
+                                                       major: {
+                                                               enabled: true,
+                                                               fontStyle: 'bold'
+                                                       },
                                                        source: 'data',
-                                                       autoSkip: true
+                                                       autoSkip: true,
+                                                       autoSkipPadding: 75,
+                                                       maxRotation: 0,
+                                                       sampleSize: 100
+                                               },
+                                               afterBuildTicks: function(scale, ticks) {
+                                                       var majorUnit = scale._majorUnit;
+                                                       var firstTick = ticks[0];
+                                                       var i, ilen, val, tick, currMajor, lastMajor;
+
+                                                       val = moment(ticks[0].value);
+                                                       if ((majorUnit === 'minute' && val.second() === 0)
+                                                                       || (majorUnit === 'hour' && val.minute() === 0)
+                                                                       || (majorUnit === 'day' && val.hour() === 9)
+                                                                       || (majorUnit === 'month' && val.date() <= 3 && val.isoWeekday() === 1)
+                                                                       || (majorUnit === 'year' && val.month() === 0)) {
+                                                               firstTick.major = true;
+                                                       } else {
+                                                               firstTick.major = false;
+                                                       }
+                                                       lastMajor = val.get(majorUnit);
+
+                                                       for (i = 1, ilen = ticks.length; i < ilen; i++) {
+                                                               tick = ticks[i];
+                                                               val = moment(tick.value);
+                                                               currMajor = val.get(majorUnit);
+                                                               tick.major = currMajor !== lastMajor;
+                                                               lastMajor = currMajor;
+                                                       }
+                                                       return ticks;
                                                }
                                        }],
                                        yAxes: [{
index 95a5f5c5f6ca69341ec87bd5f278ca1da73bcd25..6a5f7bf012b90c54bae2b38a95009ba2f73c912d 100644 (file)
@@ -214,6 +214,109 @@ function parseTickFontOptions(options) {
        return {minor: minor, major: major};
 }
 
+function nonSkipped(ticksToFilter) {
+       var filtered = [];
+       var item, index, len;
+       for (index = 0, len = ticksToFilter.length; index < len; ++index) {
+               item = ticksToFilter[index];
+               if (typeof item._index !== 'undefined') {
+                       filtered.push(item);
+               }
+       }
+       return filtered;
+}
+
+function getEvenSpacing(arr) {
+       var len = arr.length;
+       var i, diff;
+
+       if (len < 2) {
+               return false;
+       }
+
+       for (diff = arr[0], i = 1; i < len; ++i) {
+               if (arr[i] - arr[i - 1] !== diff) {
+                       return false;
+               }
+       }
+       return diff;
+}
+
+function calculateSpacing(majorIndices, ticks, axisLength, ticksLimit) {
+       var evenMajorSpacing = getEvenSpacing(majorIndices);
+       var spacing = (ticks.length - 1) / ticksLimit;
+       var factors, factor, i, ilen;
+
+       // If the major ticks are evenly spaced apart, place the minor ticks
+       // so that they divide the major ticks into even chunks
+       if (!evenMajorSpacing) {
+               return Math.max(spacing, 1);
+       }
+
+       factors = helpers.math._factorize(evenMajorSpacing);
+       for (i = 0, ilen = factors.length - 1; i < ilen; i++) {
+               factor = factors[i];
+               if (factor > spacing) {
+                       return factor;
+               }
+       }
+       return Math.max(spacing, 1);
+}
+
+function getMajorIndices(ticks) {
+       var result = [];
+       var i, ilen;
+       for (i = 0, ilen = ticks.length; i < ilen; i++) {
+               if (ticks[i].major) {
+                       result.push(i);
+               }
+       }
+       return result;
+}
+
+function skipMajors(ticks, majorIndices, spacing) {
+       var count = 0;
+       var next = majorIndices[0];
+       var i, tick;
+
+       spacing = Math.ceil(spacing);
+       for (i = 0; i < ticks.length; i++) {
+               tick = ticks[i];
+               if (i === next) {
+                       tick._index = i;
+                       count++;
+                       next = majorIndices[count * spacing];
+               } else {
+                       delete tick.label;
+               }
+       }
+}
+
+function skip(ticks, spacing, majorStart, majorEnd) {
+       var start = valueOrDefault(majorStart, 0);
+       var end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length);
+       var count = 0;
+       var length, i, tick, next;
+
+       spacing = Math.ceil(spacing);
+       if (majorEnd) {
+               length = majorEnd - majorStart;
+               spacing = length / Math.floor(length / spacing);
+       }
+
+       next = start;
+       for (i = Math.max(start, 0); i < end; i++) {
+               tick = ticks[i];
+               if (i === next) {
+                       tick._index = i;
+                       count++;
+                       next = Math.round(start + count * spacing);
+               } else {
+                       delete tick.label;
+               }
+       }
+}
+
 var Scale = Element.extend({
 
        zeroLineIndex: 0,
@@ -364,7 +467,7 @@ var Scale = Element.extend({
                me.afterFit();
 
                // Auto-skip
-               me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(ticks) : ticks;
+               me._ticksToDraw = tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto') ? me._autoSkip(ticks) : ticks;
 
                if (samplingEnabled) {
                        // Generate labels using all non-skipped ticks
@@ -848,40 +951,34 @@ var Scale = Element.extend({
         */
        _autoSkip: function(ticks) {
                var me = this;
-               var optionTicks = me.options.ticks;
-               var tickCount = ticks.length;
-               var skipRatio = false;
-               var maxTicks = optionTicks.maxTicksLimit;
-
-               // Total space needed to display all ticks. First and last ticks are
-               // drawn as their center at end of axis, so tickCount-1
-               var ticksLength = me._tickSize() * (tickCount - 1);
-
+               var tickOpts = me.options.ticks;
                var axisLength = me._length;
-               var result = [];
-               var i, tick;
-
-               if (ticksLength > axisLength) {
-                       skipRatio = 1 + Math.floor(ticksLength / axisLength);
-               }
-
-               // if they defined a max number of optionTicks,
-               // increase skipRatio until that number is met
-               if (tickCount > maxTicks) {
-                       skipRatio = Math.max(skipRatio, 1 + Math.floor(tickCount / maxTicks));
+               var ticksLimit = tickOpts.maxTicksLimit || axisLength / me._tickSize() + 1;
+               var majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : [];
+               var numMajorIndices = majorIndices.length;
+               var first = majorIndices[0];
+               var last = majorIndices[numMajorIndices - 1];
+               var i, ilen, spacing, avgMajorSpacing;
+
+               // If there are too many major ticks to display them all
+               if (numMajorIndices > ticksLimit) {
+                       skipMajors(ticks, majorIndices, numMajorIndices / ticksLimit);
+                       return nonSkipped(ticks);
                }
 
-               for (i = 0; i < tickCount; i++) {
-                       tick = ticks[i];
+               spacing = calculateSpacing(majorIndices, ticks, axisLength, ticksLimit);
 
-                       if (skipRatio <= 1 || i % skipRatio === 0) {
-                               tick._index = i;
-                               result.push(tick);
-                       } else {
-                               delete tick.label;
+               if (numMajorIndices > 0) {
+                       for (i = 0, ilen = numMajorIndices - 1; i < ilen; i++) {
+                               skip(ticks, spacing, majorIndices[i], majorIndices[i + 1]);
                        }
+                       avgMajorSpacing = numMajorIndices > 1 ? (last - first) / (numMajorIndices - 1) : null;
+                       skip(ticks, spacing, helpers.isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first);
+                       skip(ticks, spacing, last, helpers.isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing);
+                       return nonSkipped(ticks);
                }
-               return result;
+               skip(ticks, spacing);
+               return nonSkipped(ticks);
        },
 
        /**
@@ -955,7 +1052,7 @@ var Scale = Element.extend({
                var alignBorderValue = function(pixel) {
                        return alignPixel(chart, pixel, axisWidth);
                };
-               var borderValue, i, tick, label, lineValue, alignedLineValue;
+               var borderValue, i, tick, lineValue, alignedLineValue;
                var tx1, ty1, tx2, ty2, x1, y1, x2, y2, lineWidth, lineColor, borderDash, borderDashOffset;
 
                if (position === 'top') {
@@ -986,10 +1083,9 @@ var Scale = Element.extend({
 
                for (i = 0; i < ticksLength; ++i) {
                        tick = ticks[i] || {};
-                       label = tick.label;
 
                        // autoskipper skipped this tick (#4635)
-                       if (isNullOrUndef(label) && i < ticks.length) {
+                       if (isNullOrUndef(tick.label) && i < ticks.length) {
                                continue;
                        }
 
index 9b29916c8d309edf8e92533806bc2fa3043050c3..38428c8ffd78ad18eea78e3fd4cf78bdf4955aad 100644 (file)
@@ -5,8 +5,8 @@ var defaults = require('../core/core.defaults');
 var helpers = require('../helpers/index');
 var Scale = require('../core/core.scale');
 
+var resolve = helpers.options.resolve;
 var valueOrDefault = helpers.valueOrDefault;
-var factorize = helpers.math._factorize;
 
 // Integer constants are from the ES6 spec.
 var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991;
@@ -16,42 +16,42 @@ var INTERVALS = {
        millisecond: {
                common: true,
                size: 1,
-               steps: factorize(1000)
+               steps: 1000
        },
        second: {
                common: true,
                size: 1000,
-               steps: factorize(60)
+               steps: 60
        },
        minute: {
                common: true,
                size: 60000,
-               steps: factorize(60)
+               steps: 60
        },
        hour: {
                common: true,
                size: 3600000,
-               steps: factorize(24)
+               steps: 24
        },
        day: {
                common: true,
                size: 86400000,
-               steps: factorize(10)
+               steps: 30
        },
        week: {
                common: false,
                size: 604800000,
-               steps: factorize(4)
+               steps: 4
        },
        month: {
                common: true,
                size: 2.628e9,
-               steps: factorize(12)
+               steps: 12
        },
        quarter: {
                common: false,
                size: 7.884e9,
-               steps: factorize(4)
+               steps: 4
        },
        year: {
                common: true,
@@ -248,31 +248,6 @@ function parse(scale, input) {
        return value;
 }
 
-/**
- * Returns the number of unit to skip to be able to display up to `capacity` number of ticks
- * in `unit` for the given `min` / `max` range and respecting the interval steps constraints.
- */
-function determineStepSize(min, max, unit, capacity) {
-       var range = max - min;
-       var interval = INTERVALS[unit];
-       var milliseconds = interval.size;
-       var steps = interval.steps;
-       var i, ilen, factor;
-
-       if (!steps) {
-               return Math.ceil(range / (capacity * milliseconds));
-       }
-
-       for (i = 0, ilen = steps.length; i < ilen; ++i) {
-               factor = steps[i];
-               if (Math.ceil(range / (milliseconds * factor)) <= capacity) {
-                       break;
-               }
-       }
-
-       return factor;
-}
-
 /**
  * Figures out what unit results in an appropriate number of auto-generated ticks
  */
@@ -282,7 +257,7 @@ function determineUnitForAutoTicks(minUnit, min, max, capacity) {
 
        for (i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) {
                interval = INTERVALS[UNITS[i]];
-               factor = interval.steps ? interval.steps[interval.steps.length - 1] : MAX_INTEGER;
+               factor = interval.steps ? interval.steps / 2 : MAX_INTEGER;
 
                if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) {
                        return UNITS[i];
@@ -296,10 +271,9 @@ function determineUnitForAutoTicks(minUnit, min, max, capacity) {
  * Figures out what unit to format a set of ticks with
  */
 function determineUnitForFormatting(scale, ticks, minUnit, min, max) {
-       var ilen = UNITS.length;
        var i, unit;
 
-       for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) {
+       for (i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) {
                unit = UNITS[i];
                if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= ticks.length - 1) {
                        return unit;
@@ -319,7 +293,7 @@ function determineMajorUnit(unit) {
 
 /**
  * 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`.
+ * `minor` unit 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.
  */
@@ -328,51 +302,33 @@ function generate(scale, min, max, capacity) {
        var options = scale.options;
        var timeOpts = options.time;
        var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity);
-       var major = determineMajorUnit(minor);
-       var stepSize = valueOrDefault(timeOpts.stepSize, timeOpts.unitStepSize);
+       var stepSize = resolve([timeOpts.stepSize, timeOpts.unitStepSize, 1]);
        var weekday = minor === 'week' ? timeOpts.isoWeekday : false;
-       var majorTicksEnabled = options.ticks.major.enabled;
-       var interval = INTERVALS[minor];
        var first = min;
-       var last = 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 = +adapter.startOf(first, 'isoWeek', weekday);
-               last = +adapter.startOf(last, 'isoWeek', weekday);
        }
 
-       // Align first/last ticks on unit
+       // Align first ticks on unit
        first = +adapter.startOf(first, weekday ? 'day' : minor);
-       last = +adapter.startOf(last, weekday ? 'day' : minor);
 
-       // Make sure that the last tick include max
-       if (last < max) {
-               last = +adapter.add(last, 1, minor);
+       // Prevent browser from freezing in case user options request millions of milliseconds
+       if (adapter.diff(max, min, minor) > 100000 * stepSize) {
+               throw min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor;
        }
 
-       time = first;
-
-       if (majorTicksEnabled && major && !weekday && !timeOpts.round) {
-               // Align the first tick on the previous `minor` unit aligned on the `major` unit:
-               // we first aligned time on the previous `major` unit then add the number of full
-               // stepSize there is between first and the previous major time.
-               time = +adapter.startOf(time, major);
-               time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor);
+       for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) {
+               ticks.push(time);
        }
 
-       for (; time < last; time = +adapter.add(time, stepSize, minor)) {
-               ticks.push(+time);
+       if (time === max || options.bounds === 'ticks') {
+               ticks.push(time);
        }
 
-       ticks.push(+time);
-
        return ticks;
 }
 
@@ -609,18 +565,17 @@ module.exports = Scale.extend({
                var timeOpts = options.time;
                var timestamps = me._timestamps;
                var ticks = [];
+               var capacity = me.getLabelCapacity(min);
+               var source = options.ticks.source;
+               var distribution = options.distribution;
                var i, ilen, timestamp;
 
-               switch (options.ticks.source) {
-               case 'data':
+               if (source === 'data' || (source === 'auto' && distribution === 'series')) {
                        timestamps = timestamps.data;
-                       break;
-               case 'labels':
+               } else if (source === 'labels') {
                        timestamps = timestamps.labels;
-                       break;
-               case 'auto':
-               default:
-                       timestamps = generate(me, min, max, me.getLabelCapacity(min), options);
+               } else {
+                       timestamps = generate(me, min, max, capacity, options);
                }
 
                if (options.bounds === 'ticks' && timestamps.length) {
@@ -645,8 +600,9 @@ module.exports = Scale.extend({
 
                // PRIVATE
                me._unit = timeOpts.unit || determineUnitForFormatting(me, ticks, timeOpts.minUnit, me.min, me.max);
-               me._majorUnit = determineMajorUnit(me._unit);
-               me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution);
+               me._majorUnit = !options.ticks.major.enabled || me._unit === 'year' ? undefined
+                       : determineMajorUnit(me._unit);
+               me._table = buildLookupTable(me._timestamps.data, min, max, distribution);
                me._offsets = computeOffsets(me._table, ticks, min, max, options);
 
                if (options.ticks.reverse) {
@@ -690,11 +646,10 @@ module.exports = Scale.extend({
                var majorFormat = formats[majorUnit];
                var tick = ticks[index];
                var tickOpts = options.ticks;
-               var majorTickOpts = tickOpts.major;
-               var major = majorTickOpts.enabled && majorUnit && majorFormat && tick && tick.major;
+               var major = majorUnit && majorFormat && tick && tick.major;
                var label = adapter.format(time, format ? format : major ? majorFormat : minorFormat);
-               var nestedTickOpts = major ? majorTickOpts : tickOpts.minor;
-               var formatter = helpers.options.resolve([
+               var nestedTickOpts = major ? tickOpts.major : tickOpts.minor;
+               var formatter = resolve([
                        nestedTickOpts.callback,
                        nestedTickOpts.userCallback,
                        tickOpts.callback,
index 0829252129750b5851578add2ad83093b490509e..593a846b0f918a4428cad8cde00328f85546349f 100755 (executable)
@@ -617,21 +617,22 @@ describe('Time scale tests', function() {
                });
 
                it('should build the correct ticks', function() {
-                       // Where 'correct' is a two year spacing.
-                       expect(getTicksLabels(this.scale)).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2019']);
+                       expect(getTicksLabels(this.scale)).toEqual(['2005', '2006', '2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018']);
                });
 
                it('should have ticks with accurate labels', function() {
                        var scale = this.scale;
                        var ticks = scale.getTicks();
-                       var pixelsPerYear = scale.width / 14;
+                       // pixelsPerTick is an aproximation which assumes same number of milliseconds per year (not true)
+                       // we use a threshold of 1 day so that we still match these values
+                       var pixelsPerTick = scale.width / (ticks.length - 1);
 
                        for (var i = 0; i < ticks.length - 1; i++) {
-                               var offset = 2 * pixelsPerYear * i;
+                               var offset = pixelsPerTick * i;
                                expect(scale.getValueForPixel(scale.left + offset)).toBeCloseToTime({
                                        value: moment(ticks[i].label + '-01-01'),
                                        unit: 'day',
-                                       threshold: 0.5,
+                                       threshold: 1,
                                });
                        }
                });
@@ -700,10 +701,9 @@ describe('Time scale tests', function() {
                it('should get the correct labels for ticks', function() {
                        var scale = this.scale;
 
-                       expect(scale._ticks.map(function(tick) {
-                               return tick.major;
-                       })).toEqual([true, false, false, false, false, false, true]);
-                       expect(scale.ticks).toEqual(['<8:00:00>', '<8:00:10>', '<8:00:20>', '<8:00:30>', '<8:00:40>', '<8:00:50>', '<8:01:00>']);
+                       expect(scale.ticks.length).toEqual(61);
+                       expect(scale.ticks[0]).toEqual('<8:00:00>');
+                       expect(scale.ticks[scale.ticks.length - 1]).toEqual('<8:01:00>');
                });
 
                it('should update ticks.callback correctly', function() {
@@ -714,7 +714,9 @@ describe('Time scale tests', function() {
                                return '{' + value + '}';
                        };
                        chart.update();
-                       expect(scale.ticks).toEqual(['{8:00:00}', '{8:00:10}', '{8:00:20}', '{8:00:30}', '{8:00:40}', '{8:00:50}', '{8:01:00}']);
+                       expect(scale.ticks.length).toEqual(61);
+                       expect(scale.ticks[0]).toEqual('{8:00:00}');
+                       expect(scale.ticks[scale.ticks.length - 1]).toEqual('{8:01:00}');
                });
        });
 
@@ -760,10 +762,10 @@ describe('Time scale tests', function() {
                it('should get the correct labels for major and minor ticks', function() {
                        var scale = this.scale;
 
-                       expect(scale._ticks.map(function(tick) {
-                               return tick.major;
-                       })).toEqual([true, false, false, false, false, false, true]);
-                       expect(scale.ticks).toEqual(['[[8:00 pm]]', '(8:00:10 pm)', '(8:00:20 pm)', '(8:00:30 pm)', '(8:00:40 pm)', '(8:00:50 pm)', '[[8:01 pm]]']);
+                       expect(scale.ticks.length).toEqual(61);
+                       expect(scale.ticks[0]).toEqual('[[8:00 pm]]');
+                       expect(scale.ticks[Math.floor(scale.ticks.length / 2)]).toEqual('(8:00:30 pm)');
+                       expect(scale.ticks[scale.ticks.length - 1]).toEqual('[[8:01 pm]]');
                });
 
                it('should only use ticks.minor callback if ticks.major.enabled is false', function() {
@@ -772,7 +774,9 @@ describe('Time scale tests', function() {
 
                        chart.options.scales.xAxes[0].ticks.major.enabled = false;
                        chart.update();
-                       expect(scale.ticks).toEqual(['(8:00:00 pm)', '(8:00:10 pm)', '(8:00:20 pm)', '(8:00:30 pm)', '(8:00:40 pm)', '(8:00:50 pm)', '(8:01:00 pm)']);
+                       expect(scale.ticks.length).toEqual(61);
+                       expect(scale.ticks[0]).toEqual('(8:00:00 pm)');
+                       expect(scale.ticks[scale.ticks.length - 1]).toEqual('(8:01:00 pm)');
                });
 
                it('should use ticks.callback if ticks.major.callback is omitted', function() {
@@ -781,7 +785,9 @@ describe('Time scale tests', function() {
 
                        chart.options.scales.xAxes[0].ticks.major.callback = undefined;
                        chart.update();
-                       expect(scale.ticks).toEqual(['<8:00 pm>', '(8:00:10 pm)', '(8:00:20 pm)', '(8:00:30 pm)', '(8:00:40 pm)', '(8:00:50 pm)', '<8:01 pm>']);
+                       expect(scale.ticks.length).toEqual(61);
+                       expect(scale.ticks[0]).toEqual('<8:00 pm>');
+                       expect(scale.ticks[scale.ticks.length - 1]).toEqual('<8:01 pm>');
                });
 
                it('should use ticks.callback if ticks.minor.callback is omitted', function() {
@@ -790,7 +796,10 @@ describe('Time scale tests', function() {
 
                        chart.options.scales.xAxes[0].ticks.minor.callback = undefined;
                        chart.update();
-                       expect(scale.ticks).toEqual(['[[8:00 pm]]', '<8:00:10 pm>', '<8:00:20 pm>', '<8:00:30 pm>', '<8:00:40 pm>', '<8:00:50 pm>', '[[8:01 pm]]']);
+                       expect(scale.ticks.length).toEqual(61);
+                       expect(scale.ticks[0]).toEqual('[[8:00 pm]]');
+                       expect(scale.ticks[Math.floor(scale.ticks.length / 2)]).toEqual('<8:00:30 pm>');
+                       expect(scale.ticks[scale.ticks.length - 1]).toEqual('[[8:01 pm]]');
                });
        });
 
@@ -1766,6 +1775,52 @@ describe('Time scale tests', function() {
                });
        });
 
+       it('should handle autoskip with major and minor ticks', function() {
+               var date = moment('Jan 01 1990', 'MMM DD YYYY');
+               var data = [];
+               for (var i = 0; i < 60; i++) {
+                       data.push({x: date.valueOf(), y: Math.random()});
+                       date = date.clone().add(1, 'month');
+               }
+
+               var chart = window.acquireChart({
+                       type: 'line',
+                       data: {
+                               datasets: [{
+                                       xAxisID: 'xScale0',
+                                       data: data
+                               }],
+                       },
+                       options: {
+                               scales: {
+                                       xAxes: [{
+                                               id: 'xScale0',
+                                               type: 'time',
+                                               ticks: {
+                                                       major: {
+                                                               enabled: true
+                                                       },
+                                                       source: 'data',
+                                                       autoSkip: true
+                                               }
+                                       }],
+                               }
+                       }
+               });
+
+               var scale = chart.scales.xScale0;
+
+               var labels = scale._ticksToDraw.map(function(t) {
+                       return t.label;
+               });
+               expect(labels).toEqual([
+                       '1990', 'Apr 1990', 'Jul 1990', 'Oct 1990',
+                       '1991', 'Apr 1991', 'Jul 1991', 'Oct 1991',
+                       '1992', 'Apr 1992', 'Jul 1992', 'Oct 1992',
+                       '1993', 'Apr 1993', 'Jul 1993', 'Oct 1993',
+                       '1994', 'Apr 1994', 'Jul 1994', 'Oct 1994']);
+       });
+
        describe('Deprecations', function() {
                describe('options.time.displayFormats', function() {
                        it('should generate defaults from adapter presets', function() {