mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Move and rewrite time helpers (#4549)
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.
This commit is contained in:
parent
e7445a5f00
commit
48d6face28
@ -139,9 +139,7 @@ module.exports = {
|
||||
ticks.push(lastTick);
|
||||
|
||||
return ticks;
|
||||
},
|
||||
|
||||
time: helpers.time.generateTicks
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
|
||||
@ -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,33 +159,156 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
function determineUnit(minUnit, min, max, capacity) {
|
||||
var ilen = UNITS.length;
|
||||
var i, interval, factor;
|
||||
|
||||
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 timeHelpers = helpers.time;
|
||||
|
||||
// Integer constants are from the ES6 spec.
|
||||
var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991;
|
||||
var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
|
||||
|
||||
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);
|
||||
|| 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
|
||||
});
|
||||
timestamps = generate(min, max, unit, majorUnit, stepSize, timeOpts);
|
||||
|
||||
// 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;
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user