Refactor time scale methods into a common location

This commit is contained in:
Ben McCann 2017-05-04 21:34:23 -07:00 committed by Evert Timberg
parent 04ab523dd4
commit 1cbba830fd
4 changed files with 211 additions and 191 deletions

View File

@ -4,6 +4,8 @@
var Chart = require('./core/core.js')(); var Chart = require('./core/core.js')();
require('./core/core.helpers')(Chart); require('./core/core.helpers')(Chart);
require('./helpers/helpers.time')(Chart);
require('./platforms/platform.js')(Chart); require('./platforms/platform.js')(Chart);
require('./core/core.canvasHelpers')(Chart); require('./core/core.canvasHelpers')(Chart);
require('./core/core.element')(Chart); require('./core/core.element')(Chart);

View File

@ -141,7 +141,9 @@ module.exports = function(Chart) {
ticks.push(lastTick); ticks.push(lastTick);
return ticks; return ticks;
} },
time: helpers.time.generateTicks
}, },
/** /**

196
src/helpers/helpers.time.js Normal file
View File

@ -0,0 +1,196 @@
'use strict';
var moment = require('moment');
moment = typeof(moment) === 'function' ? moment : window.moment;
module.exports = function(Chart) {
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;
ticks.push(options.min !== undefined ? options.min : niceRange.min);
var cur = moment(niceRange.min);
while (cur.add(stepSize, options.unit).valueOf() < niceRange.max) {
ticks.push(cur.valueOf());
}
var realMax = options.max || niceRange.max;
if (ticks[ticks.length - 1] !== realMax) {
ticks.push(realMax);
}
}
return ticks;
}
Chart.helpers = Chart.helpers || {};
Chart.helpers.time = {
/**
* 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;
},
/**
* 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.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
});
}
};
};

View File

@ -7,44 +7,7 @@ moment = typeof(moment) === 'function' ? moment : window.moment;
module.exports = function(Chart) { module.exports = function(Chart) {
var helpers = Chart.helpers; var helpers = Chart.helpers;
var interval = { var timeHelpers = helpers.time;
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
}
};
var defaultConfig = { var defaultConfig = {
position: 'bottom', position: 'bottom',
@ -76,149 +39,6 @@ module.exports = function(Chart) {
} }
}; };
/**
* 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
*/
function parseTime(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
*/
function determineUnit(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;
}
/**
* 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
*/
function determineStepSize(min, max, unit, maxTicks) {
// Using our unit, figoure 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;
}
/**
* 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 generateTicks(options, dataRange, niceRange) {
var ticks = [];
if (options.maxTicks) {
var stepSize = options.stepSize;
ticks.push(options.min !== undefined ? options.min : niceRange.min);
var cur = moment(niceRange.min);
while (cur.add(stepSize, options.unit).valueOf() < niceRange.max) {
ticks.push(cur.valueOf());
}
var realMax = options.max || niceRange.max;
if (ticks[ticks.length - 1] !== realMax) {
ticks.push(realMax);
}
}
return ticks;
}
/**
* @function Chart.Ticks.generators.time
* @param options {ITimeGeneratorOptions} the options for generation
* @param dataRange {IRange} the data range
* @return {Number[]} ticks
*/
Chart.Ticks.generators.time = function(options, dataRange) {
var niceMin;
var niceMax;
var isoWeekday = options.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 generateTicks(options, dataRange, {
min: niceMin,
max: niceMax
});
};
var TimeScale = Chart.Scale.extend({ var TimeScale = Chart.Scale.extend({
initialize: function() { initialize: function() {
if (!moment) { if (!moment) {
@ -244,7 +64,7 @@ module.exports = function(Chart) {
var timestamp; var timestamp;
helpers.each(chartData.labels, function(label, labelIndex) { helpers.each(chartData.labels, function(label, labelIndex) {
var labelMoment = parseTime(me, label); var labelMoment = timeHelpers.parseTime(me, label);
if (labelMoment.isValid()) { if (labelMoment.isValid()) {
// We need to round the time // We need to round the time
@ -267,7 +87,7 @@ module.exports = function(Chart) {
if (typeof dataset.data[0] === 'object' && dataset.data[0] !== null && me.chart.isDatasetVisible(datasetIndex)) { if (typeof dataset.data[0] === 'object' && dataset.data[0] !== null && me.chart.isDatasetVisible(datasetIndex)) {
// We have potential point data, so we need to parse this // We have potential point data, so we need to parse this
helpers.each(dataset.data, function(value, dataIndex) { helpers.each(dataset.data, function(value, dataIndex) {
var dataMoment = parseTime(me, me.getRightValue(value)); var dataMoment = timeHelpers.parseTime(me, me.getRightValue(value));
if (dataMoment.isValid()) { if (dataMoment.isValid()) {
if (timeOpts.round) { if (timeOpts.round) {
@ -302,7 +122,7 @@ module.exports = function(Chart) {
var dataMax = me.dataMax; var dataMax = me.dataMax;
if (timeOpts.min) { if (timeOpts.min) {
var minMoment = parseTime(me, timeOpts.min); var minMoment = timeHelpers.parseTime(me, timeOpts.min);
if (timeOpts.round) { if (timeOpts.round) {
minMoment.round(timeOpts.round); minMoment.round(timeOpts.round);
} }
@ -310,15 +130,15 @@ module.exports = function(Chart) {
} }
if (timeOpts.max) { if (timeOpts.max) {
maxTimestamp = parseTime(me, timeOpts.max).valueOf(); maxTimestamp = timeHelpers.parseTime(me, timeOpts.max).valueOf();
} }
var maxTicks = me.getLabelCapacity(minTimestamp || dataMin); var maxTicks = me.getLabelCapacity(minTimestamp || dataMin);
var unit = timeOpts.unit || determineUnit(timeOpts.minUnit, minTimestamp || dataMin, maxTimestamp || dataMax, maxTicks); var unit = timeOpts.unit || timeHelpers.determineUnit(timeOpts.minUnit, minTimestamp || dataMin, maxTimestamp || dataMax, maxTicks);
me.displayFormat = timeOpts.displayFormats[unit]; me.displayFormat = timeOpts.displayFormats[unit];
var stepSize = timeOpts.stepSize || determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit, maxTicks); var stepSize = timeOpts.stepSize || timeHelpers.determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit, maxTicks);
me.ticks = Chart.Ticks.generators.time({ me.ticks = timeHelpers.generateTicks({
maxTicks: maxTicks, maxTicks: maxTicks,
min: minTimestamp, min: minTimestamp,
max: maxTimestamp, max: maxTimestamp,
@ -347,7 +167,7 @@ module.exports = function(Chart) {
// Format nicely // Format nicely
if (me.options.time.tooltipFormat) { if (me.options.time.tooltipFormat) {
label = parseTime(me, label).format(me.options.time.tooltipFormat); label = timeHelpers.parseTime(me, label).format(me.options.time.tooltipFormat);
} }
return label; return label;
@ -393,7 +213,7 @@ module.exports = function(Chart) {
if (offset === null) { if (offset === null) {
if (!value || !value.isValid) { if (!value || !value.isValid) {
// not already a moment object // not already a moment object
value = parseTime(me, me.getRightValue(value)); value = timeHelpers.parseTime(me, me.getRightValue(value));
} }
if (value && value.isValid && value.isValid()) { if (value && value.isValid && value.isValid()) {