]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Move and rewrite time helpers (#4549)
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Sun, 23 Jul 2017 15:41:12 +0000 (17:41 +0200)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sun, 23 Jul 2017 15:41:12 +0000 (11:41 -0400)
Move time helpers back into time scale, remove the `Chart.helpers.time namespace` and attempt to make the auto generation logic a bit simpler. The generate method doesn't anymore enforce min/max, the calling code needs to clamp timestamps if needed.

src/core/core.ticks.js
src/helpers/helpers.time.js [deleted file]
src/helpers/index.js
src/scales/scale.time.js
test/specs/scale.time.tests.js

index 11f44142c47ff36840aef6ff4989b89df5347722..eb7be8714c550412701ce1185e4757799c72aa2d 100644 (file)
@@ -139,9 +139,7 @@ module.exports = {
                        ticks.push(lastTick);
 
                        return ticks;
-               },
-
-               time: helpers.time.generateTicks
+               }
        },
 
        /**
diff --git a/src/helpers/helpers.time.js b/src/helpers/helpers.time.js
deleted file mode 100644 (file)
index 5bbcf3e..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-'use strict';
-
-var moment = require('moment');
-moment = typeof moment === 'function' ? moment : window.moment;
-
-var helpers = require('./helpers.core');
-
-var interval = {
-       millisecond: {
-               size: 1,
-               steps: [1, 2, 5, 10, 20, 50, 100, 250, 500]
-       },
-       second: {
-               size: 1000,
-               steps: [1, 2, 5, 10, 30]
-       },
-       minute: {
-               size: 60000,
-               steps: [1, 2, 5, 10, 30]
-       },
-       hour: {
-               size: 3600000,
-               steps: [1, 2, 3, 6, 12]
-       },
-       day: {
-               size: 86400000,
-               steps: [1, 2, 5]
-       },
-       week: {
-               size: 604800000,
-               maxStep: 4
-       },
-       month: {
-               size: 2.628e9,
-               maxStep: 3
-       },
-       quarter: {
-               size: 7.884e9,
-               maxStep: 4
-       },
-       year: {
-               size: 3.154e10,
-               maxStep: false
-       }
-};
-
-/**
- * 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
- */
-function generateTicksNiceRange(options, dataRange, niceRange) {
-       var ticks = [];
-       if (options.maxTicks) {
-               var stepSize = options.stepSize;
-               var startTick = helpers.isNullOrUndef(options.min) ? niceRange.min : options.min;
-               var majorUnit = options.majorUnit;
-               var majorUnitStart = majorUnit ? moment(startTick).add(1, majorUnit).startOf(majorUnit) : startTick;
-               var startRange = majorUnitStart.valueOf() - startTick;
-               var stepValue = interval[options.unit].size * stepSize;
-               var startFraction = startRange % stepValue;
-               var alignedTick = startTick;
-
-               // first tick
-               if (startFraction && majorUnit && !options.timeOpts.round && !options.timeOpts.isoWeekday && helpers.isNullOrUndef(options.min)) {
-                       alignedTick += startFraction - stepValue;
-                       ticks.push(alignedTick);
-               } else {
-                       ticks.push(startTick);
-               }
-
-               // generate remaining ticks
-               var cur = moment(alignedTick);
-               var realMax = helpers.isNullOrUndef(options.max) ? niceRange.max : options.max;
-               while (cur.add(stepSize, options.unit).valueOf() < realMax) {
-                       ticks.push(cur.valueOf());
-               }
-
-               // last tick
-               if (helpers.isNullOrUndef(options.max)) {
-                       ticks.push(cur.valueOf());
-               } else {
-                       ticks.push(realMax);
-               }
-       }
-       return ticks;
-}
-
-/**
- * @namespace Chart.helpers.time;
- */
-module.exports = {
-       /**
-        * Helper function to parse time to a moment object
-        * @param axis {TimeAxis} the time axis
-        * @param label {Date|string|number|Moment} The thing to parse
-        * @return {Moment} parsed time
-        */
-       parseTime: function(axis, label) {
-               var timeOpts = axis.options.time;
-               if (typeof timeOpts.parser === 'string') {
-                       return moment(label, timeOpts.parser);
-               }
-               if (typeof timeOpts.parser === 'function') {
-                       return timeOpts.parser(label);
-               }
-               if (typeof label.getMonth === 'function' || typeof label === 'number') {
-                       // Date objects
-                       return moment(label);
-               }
-               if (label.isValid && label.isValid()) {
-                       // Moment support
-                       return label;
-               }
-               var format = timeOpts.format;
-               if (typeof format !== 'string' && format.call) {
-                       // Custom parsing (return an instance of moment)
-                       console.warn('options.time.format is deprecated and replaced by options.time.parser.');
-                       return format(label);
-               }
-               // Moment format parsing
-               return moment(label, format);
-       },
-
-       /**
-        * Figure out which is the best unit for the scale
-        * @param minUnit {String} minimum unit to use
-        * @param min {Number} scale minimum
-        * @param max {Number} scale maximum
-        * @return {String} the unit to use
-        */
-       determineUnit: function(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 = 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;
-                       }
-               }
-
-               return unit;
-       },
-
-       /**
-        * Determine major unit accordingly to passed unit
-        * @param unit {String} relative unit
-        * @return {String} major unit
-        */
-       determineMajorUnit: function(unit) {
-               var units = Object.keys(interval);
-               var unitIndex = units.indexOf(unit);
-               while (unitIndex < units.length) {
-                       var majorUnit = units[++unitIndex];
-                       // exclude 'week' and 'quarter' units
-                       if (majorUnit !== 'week' && majorUnit !== 'quarter') {
-                               return majorUnit;
-                       }
-               }
-
-               return null;
-       },
-
-       /**
-        * Determines how we scale the unit
-        * @param min {Number} the scale minimum
-        * @param max {Number} the scale maximum
-        * @param unit {String} the unit determined by the {@see determineUnit} method
-        * @return {Number} the axis step size as a multiple of unit
-        */
-       determineStepSize: function(min, max, unit, maxTicks) {
-               // Using our unit, figure out what we need to scale as
-               var unitDefinition = interval[unit];
-               var unitSizeInMilliSeconds = unitDefinition.size;
-               var sizeInUnits = Math.ceil((max - min) / unitSizeInMilliSeconds);
-               var multiplier = 1;
-               var range = max - min;
-
-               if (unitDefinition.steps) {
-                       // Have an array of steps
-                       var numSteps = unitDefinition.steps.length;
-                       for (var i = 0; i < numSteps && sizeInUnits > maxTicks; i++) {
-                               multiplier = unitDefinition.steps[i];
-                               sizeInUnits = Math.ceil(range / (unitSizeInMilliSeconds * multiplier));
-                       }
-               } else {
-                       while (sizeInUnits > maxTicks && maxTicks > 0) {
-                               ++multiplier;
-                               sizeInUnits = Math.ceil(range / (unitSizeInMilliSeconds * multiplier));
-                       }
-               }
-
-               return multiplier;
-       },
-
-       /**
-        * @function generateTicks
-        * @param options {ITimeGeneratorOptions} the options for generation
-        * @param dataRange {IRange} the data range
-        * @return {Number[]} ticks
-        */
-       generateTicks: function(options, dataRange) {
-               var niceMin;
-               var niceMax;
-               var isoWeekday = options.timeOpts.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(options.unit).valueOf();
-                       niceMax = moment(dataRange.max).startOf(options.unit);
-                       if (dataRange.max - niceMax > 0) {
-                               niceMax.add(1, options.unit);
-                       }
-                       niceMax = niceMax.valueOf();
-               }
-               return generateTicksNiceRange(options, dataRange, {
-                       min: niceMin,
-                       max: niceMax
-               });
-       }
-};
index 632b772076a8016350b44780d3284d171b49ad2b..60c199cad55db2159ca56cacdbefe4869c96de7e 100644 (file)
@@ -4,4 +4,3 @@ module.exports = require('./helpers.core');
 module.exports.easing = require('./helpers.easing');
 module.exports.canvas = require('./helpers.canvas');
 module.exports.options = require('./helpers.options');
-module.exports.time = require('./helpers.time');
index 2ffb6f9d2fb2aa6244db37c0df8d68fd58711a89..94bd7a82c7f241df062a3e7c1bbd40e84a6e387f 100644 (file)
@@ -7,6 +7,59 @@ moment = typeof moment === 'function' ? moment : window.moment;
 var defaults = require('../core/core.defaults');
 var helpers = require('../helpers/index');
 
+// Integer constants are from the ES6 spec.
+var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991;
+var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
+
+var INTERVALS = {
+       millisecond: {
+               major: true,
+               size: 1,
+               steps: [1, 2, 5, 10, 20, 50, 100, 250, 500]
+       },
+       second: {
+               major: true,
+               size: 1000,
+               steps: [1, 2, 5, 10, 30]
+       },
+       minute: {
+               major: true,
+               size: 60000,
+               steps: [1, 2, 5, 10, 30]
+       },
+       hour: {
+               major: true,
+               size: 3600000,
+               steps: [1, 2, 3, 6, 12]
+       },
+       day: {
+               major: true,
+               size: 86400000,
+               steps: [1, 2, 5]
+       },
+       week: {
+               major: false,
+               size: 604800000,
+               steps: [1, 2, 3, 4]
+       },
+       month: {
+               major: true,
+               size: 2.628e9,
+               steps: [1, 2, 3]
+       },
+       quarter: {
+               major: false,
+               size: 7.884e9,
+               steps: [1, 2, 3, 4]
+       },
+       year: {
+               major: true,
+               size: 3.154e10
+       }
+};
+
+var UNITS = Object.keys(INTERVALS);
+
 function sorter(a, b) {
        return a - b;
 }
@@ -106,32 +159,155 @@ function interpolate(table, skey, sval, tkey) {
        return prev[tkey] + offset;
 }
 
+/**
+ * Convert the given value to a moment object using the given time options.
+ * @see http://momentjs.com/docs/#/parsing/
+ */
+function momentify(value, options) {
+       var parser = options.parser;
+       var format = options.parser || options.format;
+
+       if (typeof parser === 'function') {
+               return parser(value);
+       }
+
+       if (typeof value === 'string' && typeof format === 'string') {
+               return moment(value, format);
+       }
+
+       if (!(value instanceof moment)) {
+               value = moment(value);
+       }
+
+       if (value.isValid()) {
+               return value;
+       }
+
+       // Labels are in an incompatible moment format and no `parser` has been provided.
+       // The user might still use the deprecated `format` option to convert his inputs.
+       if (typeof format === 'function') {
+               return format(value);
+       }
+
+       return value;
+}
+
 function parse(input, scale) {
        if (helpers.isNullOrUndef(input)) {
                return null;
        }
 
-       var round = scale.options.time.round;
-       var value = scale.getRightValue(input);
-       var time = value.isValid ? value : helpers.time.parseTime(scale, value);
-       if (!time || !time.isValid()) {
+       var options = scale.options.time;
+       var value = momentify(scale.getRightValue(input), options);
+       if (!value.isValid()) {
                return null;
        }
 
-       if (round) {
-               time.startOf(round);
+       if (options.round) {
+               value.startOf(options.round);
        }
 
-       return time.valueOf();
+       return value.valueOf();
 }
 
-module.exports = function(Chart) {
+/**
+ * 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 || 1) * milliseconds));
+       }
+
+       for (i = 0, ilen = steps.length; i < ilen; ++i) {
+               factor = steps[i];
+               if (Math.ceil(range / (milliseconds * factor)) <= capacity) {
+                       break;
+               }
+       }
+
+       return factor;
+}
 
-       var timeHelpers = helpers.time;
+function determineUnit(minUnit, min, max, capacity) {
+       var ilen = UNITS.length;
+       var i, interval, factor;
 
-       // Integer constants are from the ES6 spec.
-       var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991;
-       var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
+       for (i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) {
+               interval = INTERVALS[UNITS[i]];
+               factor = interval.steps ? interval.steps[interval.steps.length - 1] : MAX_INTEGER;
+
+               if (Math.ceil((max - min) / (factor * interval.size)) <= capacity) {
+                       return UNITS[i];
+               }
+       }
+
+       return UNITS[ilen - 1];
+}
+
+function determineMajorUnit(unit) {
+       for (var i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) {
+               if (INTERVALS[UNITS[i]].major) {
+                       return UNITS[i];
+               }
+       }
+}
+
+/**
+ * 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`.
+ * 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) {
+       var weekday = minor === 'week' ? options.isoWeekday : false;
+       var interval = INTERVALS[minor];
+       var first = moment(min);
+       var last = moment(max);
+       var ticks = [];
+       var time;
+
+       // For 'week' unit, handle the first day of week option
+       if (weekday) {
+               first = first.isoWeekday(weekday);
+               last = last.isoWeekday(weekday);
+       }
+
+       // Align first/last ticks on unit
+       first = first.startOf(weekday ? 'day' : minor);
+       last = last.startOf(weekday ? 'day' : minor);
+
+       // Make sure that the last tick include max
+       if (last < max) {
+               last.add(1, minor);
+       }
+
+       time = moment(first);
+
+       if (major && !weekday && !options.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.startOf(major);
+               time.add(~~((first - time) / (interval.size * stepSize)) * stepSize, minor);
+       }
+
+       for (; time < last; time.add(stepSize, minor)) {
+               ticks.push(+time);
+       }
+
+       ticks.push(+time);
+
+       return ticks;
+}
+
+module.exports = function(Chart) {
 
        var defaultConfig = {
                position: 'bottom',
@@ -165,6 +341,10 @@ module.exports = function(Chart) {
                }
        };
 
+       Chart.Ticks.generators.time = function(opts, range) {
+               return generate(range.min, range.max, opts.unit, opts.majorUnit, opts.stepSize, opts.timeOpts);
+       };
+
        var TimeScale = Chart.Scale.extend({
                initialize: function() {
                        if (!moment) {
@@ -176,6 +356,18 @@ module.exports = function(Chart) {
                        Chart.Scale.prototype.initialize.call(this);
                },
 
+               update: function() {
+                       var me = this;
+                       var options = me.options;
+
+                       // DEPRECATIONS: output a message only one time per update
+                       if (options.time && options.time.format) {
+                               console.warn('options.time.format is deprecated and replaced by options.time.parser.');
+                       }
+
+                       return Chart.Scale.prototype.update.apply(me, arguments);
+               },
+
                /**
                 * Allows data to be referenced via 't' attribute
                 */
@@ -242,8 +434,6 @@ module.exports = function(Chart) {
                                labels: labels.sort(sorter),    // Sort labels **after** data have been converted
                                min: Math.min(min, max),        // Make sure that max is **strictly** higher ...
                                max: Math.max(min + 1, max),    // ... than min (required by the lookup table)
-                               offset: null,
-                               size: null,
                                table: []
                        };
                },
@@ -257,38 +447,31 @@ module.exports = function(Chart) {
                        var ticksOpts = me.options.ticks;
                        var formats = timeOpts.displayFormats;
                        var capacity = me.getLabelCapacity(min);
-                       var unit = timeOpts.unit || timeHelpers.determineUnit(timeOpts.minUnit, min, max, capacity);
-                       var majorUnit = timeHelpers.determineMajorUnit(unit);
+                       var unit = timeOpts.unit || determineUnit(timeOpts.minUnit, min, max, capacity);
+                       var majorUnit = determineMajorUnit(unit);
+                       var timestamps = [];
                        var ticks = [];
                        var i, ilen, timestamp, stepSize;
 
-                       if (ticksOpts.source === 'labels') {
-                               for (i = 0, ilen = model.labels.length; i < ilen; ++i) {
-                                       timestamp = model.labels[i];
-                                       if (timestamp >= min && timestamp <= max) {
-                                               ticks.push(timestamp);
-                                       }
-                               }
-                       } else {
+                       if (ticksOpts.source === 'auto') {
                                stepSize = helpers.valueOrDefault(timeOpts.stepSize, timeOpts.unitStepSize)
-                                       || timeHelpers.determineStepSize(min, max, unit, capacity);
-
-                               ticks = timeHelpers.generateTicks({
-                                       maxTicks: capacity,
-                                       min: parse(timeOpts.min, me),
-                                       max: parse(timeOpts.max, me),
-                                       stepSize: stepSize,
-                                       majorUnit: majorUnit,
-                                       unit: unit,
-                                       timeOpts: timeOpts
-                               }, {
-                                       min: min,
-                                       max: max
-                               });
-
-                               // Recompute min/max, the ticks generation might have changed them (BUG?)
-                               min = ticks.length ? ticks[0] : min;
-                               max = ticks.length ? ticks[ticks.length - 1] : max;
+                                       || determineStepSize(min, max, unit, capacity);
+
+                               timestamps = generate(min, max, unit, majorUnit, stepSize, timeOpts);
+
+                               // Expand min/max to the generated ticks
+                               min = helpers.isNullOrUndef(timeOpts.min) && timestamps.length ? timestamps[0] : min;
+                               max = helpers.isNullOrUndef(timeOpts.max) && timestamps.length ? timestamps[timestamps.length - 1] : max;
+                       } else {
+                               timestamps = model.labels;
+                       }
+
+                       // Remove ticks outside the min/max range
+                       for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
+                               timestamp = timestamps[i];
+                               if (timestamp >= min && timestamp <= max) {
+                                       ticks.push(timestamp);
+                               }
                        }
 
                        me.ticks = ticks;
@@ -313,7 +496,7 @@ module.exports = function(Chart) {
                                label = me.getRightValue(value);
                        }
                        if (timeOpts.tooltipFormat) {
-                               label = timeHelpers.parseTime(me, label).format(timeOpts.tooltipFormat);
+                               label = momentify(label, timeOpts).format(timeOpts.tooltipFormat);
                        }
 
                        return label;
@@ -430,7 +613,7 @@ module.exports = function(Chart) {
                        var tickLabelWidth = me.getLabelWidth(exampleLabel);
                        var innerWidth = me.isHorizontal() ? me.width : me.height;
 
-                       return innerWidth / tickLabelWidth;
+                       return Math.floor(innerWidth / tickLabelWidth);
                }
        });
 
index 590921101c63d364b0ee734ea8ef3422658d4ddc..87621d6b3ee23bdb2d8893d63808b7fedd747e38 100755 (executable)
@@ -372,7 +372,7 @@ describe('Time scale tests', function() {
                        config.time.min = '2014-12-29T04:00:00';
 
                        var scale = createScale(mockData, config);
-                       expect(scale.ticks[0].value).toEqual('Dec 29');
+                       expect(scale.ticks[0].value).toEqual('Dec 31');
                });
 
                it('should use the max option', function() {