+++ /dev/null
-'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
- });
- }
-};
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;
}
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',
}
};
+ 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) {
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
*/
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: []
};
},
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;
label = me.getRightValue(value);
}
if (timeOpts.tooltipFormat) {
- label = timeHelpers.parseTime(me, label).format(timeOpts.tooltipFormat);
+ label = momentify(label, timeOpts).format(timeOpts.tooltipFormat);
}
return label;
var tickLabelWidth = me.getLabelWidth(exampleLabel);
var innerWidth = me.isHorizontal() ? me.width : me.height;
- return innerWidth / tickLabelWidth;
+ return Math.floor(innerWidth / tickLabelWidth);
}
});