diff --git a/src/core/core.date.js b/src/core/core.date.js new file mode 100644 index 00000000000..6d17622398a --- /dev/null +++ b/src/core/core.date.js @@ -0,0 +1,135 @@ +'use strict'; + +var moment, luxon, DateTime; +try { + moment = require('moment'); // eslint-disable-line global-require + moment = typeof moment === 'function' ? moment : window.moment; +} catch (mte) { + try { + luxon = require('luxon'); // eslint-disable-line global-require + luxon = (luxon && luxon.DateTime) ? luxon : window.luxon; + DateTime = luxon && luxon.DateTime; + } catch (lxe) { + throw new Error('Chart.js - Neither Moment.js no Luxon could be found! You must include one of these date libraries to use the time scale. For more info, see https://www.chartjs.org/docs/latest/getting-started/installation.html'); + } +} + +var helpers = require('../helpers/index'); + +var CDate = function(date) { + this.underlying = date; + this.valid = true; + this.initialize.apply(this, arguments); +}; + +helpers.extend(CDate.prototype, { + + initialize: function(value) { + if (value === undefined) { + value = new Date(); + } + + if (luxon) { + if (typeof value === 'number') { + this.underlying = DateTime.fromMillis(value); + return; + } + if (value instanceof Date) { + this.underlying = DateTime.fromJSDate(value); + return; + } + if (value instanceof luxon) { + this.underlying = value; + return; + } + this.valid = false; + return + } + + if (!(value instanceof moment)) { + value = moment(value); + } + + this.underlying = value; + }, + + /** + * @private + */ + _underlying: function() { + return luxon ? this.underlying : this.underlying.clone(); + }, + + isValid: function() { + return this.isValid && (luxon ? this.underlying.isValid : this.underlying.isValid()); + }, + + millisecond: function() { + return luxon ? this.underlying.millisecond : this.underlying.millisecond(); + }, + + second: function() { + return luxon ? this.underlying.second : this.underlying.second(); + }, + + minute: function() { + return luxon ? this.underlying.minute : this.underlying.minute(); + }, + + hour: function() { + return luxon ? this.underlying.hour : this.underlying.hour(); + }, + + isoWeekday: function(value) { + return this._underlying().isoWeekday(value); + }, + + startOf: function(value) { + return this._underlying().startOf(value); + }, + + endOf: function(value) { + return this._underlying().endOf(value); + }, + + valueOf: function() { + return this.underlying.valueOf(); + }, + + toFormat: function(format) { + return luxon ? this.underlying.toFormat(format) : this.underlying.format(format); + }, + + add: function(value, type) { + if (luxon) { + var arg = {}; + arg[type] = value; + this.underlying.plus(arg); + } + return this._underlying().add(value, type); + }, + + diff: function(other, unit) { + other = new CDate(other).underlying; + var duration = luxon ? this.underlying.diff(other) + : moment.duration(this.underlying.diff(other)); + return duration.as(unit); + } + +}); + +module.exports = { + CDate: CDate, + moment: moment, + defaultDisplayFormats: { + millisecond: 'h:mm:ss.SSS a', // 11:20:01.123 AM, + second: 'h:mm:ss a', // 11:20:01 AM + minute: 'h:mm a', // 11:20 AM + hour: luxon ? 'ha' : 'hA', // 5PM + day: luxon ? 'MMM d' : 'MMM D', // Sep 4 + week: luxon ? 'WW' : 'll', // Week 46, or maybe "[W]WW - YYYY" ? + month: luxon ? 'MMM yyyy' : 'MMM YYYY', // Sept 2015 + quarter: luxon ? '\'Q\'q - yyyy' : '[Q]Q - YYYY', // Q3 - 2015 + year: luxon ? 'yyyy' : 'YYYY' // 2015 + } +}; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 61e4aae1b91..ebc554cf157 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -1,22 +1,10 @@ /* global window: false */ 'use strict'; -var moment, luxon, DateTime; -try { - moment = require('moment'); // eslint-disable-line global-require - moment = typeof moment === 'function' ? moment : window.moment; -} catch (mte) { - try { - luxon = require('luxon'); // eslint-disable-line global-require - luxon = (luxon && luxon.DateTime) ? luxon : window.luxon; - DateTime = luxon && luxon.DateTime; - } catch (lxe) { - throw new Error('Chart.js - Neither Moment.js no Luxon could be found! You must include one of these date libraries to use the time scale. For more info, see https://www.chartjs.org/docs/latest/getting-started/installation.html'); - } -} - var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var dateWrapper = require('../core/core.date'); +var CDate = dateWrapper.CDate; var Scale = require('../core/core.scale'); var scaleService = require('../core/core.scaleService'); @@ -190,39 +178,25 @@ function interpolate(table, skey, sval, tkey) { return prev[tkey] + offset; } -function millisToDate(value) { - return luxon ? DateTime.fromMillis(value) : moment(value); -} - /** - * Convert the given value to a moment object using the given time options. + * Convert the given value to an internal date representation using the given time options. * @see http://momentjs.com/docs/#/parsing/ */ function createDate(value, options) { - if (luxon) { - if (typeof value === 'number') { - return DateTime.fromMillis(value); - } - if (value instanceof Date) { - return DateTime.fromJSDate(value); - } - throw 'Input must be either a Date or milliseconds since epoch, but got ' + value; - } - var parser = options.parser; - var format = options.parser || options.format; + if (dateWrapper.moment) { + 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 (typeof parser === 'function') { + return new CDate(parser(value)); + } - if (!(value instanceof moment)) { - value = moment(value); + if (typeof value === 'string' && typeof format === 'string') { + return new CDate(dateWrapper.moment(value, format)); + } } + value = new CDate(value); if (value.isValid()) { return value; } @@ -230,14 +204,10 @@ function createDate(value, options) { // 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 new CDate(format(value)); } - return value; -} - -function isValid(date) { - return luxon ? date.isValid : date.isValid(); + return new CDate(value); } function parse(input, scale) { @@ -247,12 +217,12 @@ function parse(input, scale) { var options = scale.options.time; var value = createDate(scale.getRightValue(input), options); - if (!isValid(value)) { + if (!value.isValid()) { return null; } if (options.round) { - value.startOf(options.round); + value = value.startOf(options.round); } return value.valueOf(); @@ -306,14 +276,12 @@ function determineUnitForAutoTicks(minUnit, min, max, capacity) { * Figures out what unit to format a set of ticks with */ function determineUnitForFormatting(ticks, minUnit, min, max) { - var duration = luxon ? createDate(max).diff(createDate(min)) - : moment.duration(moment(max).diff(moment(min))); var ilen = UNITS.length; var i, unit; for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) { unit = UNITS[i]; - if (INTERVALS[unit].common && duration.as(unit) >= ticks.length) { + if (INTERVALS[unit].common && new CDate(max).diff(min, unit) >= ticks.length) { return unit; } } @@ -343,8 +311,8 @@ function generate(min, max, capacity, options) { var weekday = minor === 'week' ? timeOpts.isoWeekday : false; var majorTicksEnabled = options.ticks.major.enabled; var interval = INTERVALS[minor]; - var first = moment(min); - var last = moment(max); + var first = new CDate(min); + var last = new CDate(max); var ticks = []; var time; @@ -364,20 +332,20 @@ function generate(min, max, capacity, options) { // Make sure that the last tick include max if (last < max) { - last.add(1, minor); + last = last.add(1, minor); } - time = moment(first); + time = new CDate(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.startOf(major); - time.add(~~((first - time) / (interval.size * stepSize)) * stepSize, minor); + time = time.startOf(major); + time = time.add(~~((first - time) / (interval.size * stepSize)) * stepSize, minor); } - for (; time < last; time.add(stepSize, minor)) { + for (; time < last; time = time.add(stepSize, minor)) { ticks.push(+time); } @@ -423,7 +391,7 @@ function ticksFromTimestamps(values, majorUnit) { for (i = 0, ilen = values.length; i < ilen; ++i) { value = values[i]; - major = majorUnit ? value === millisToDate(value).startOf(majorUnit).valueOf() : false; + major = majorUnit ? value === new CDate(value).startOf(majorUnit).valueOf() : false; ticks.push({ value: value, @@ -442,20 +410,11 @@ function determineLabelFormat(data, timeOpts) { // format all labels with the same level of detail as the most specific label for (i = 0; i < ilen; i++) { date = createDate(data[i], timeOpts); - if (luxon) { - if (date.millisecond !== 0) { - return 'MMM D, YYYY h:mm:ss.SSS a'; - } - if (date.second !== 0 || date.minute !== 0 || date.hour !== 0) { - hasTime = true; - } - } else { - if (date.millisecond() !== 0) { - return 'MMM D, YYYY h:mm:ss.SSS a'; - } - if (date.second() !== 0 || date.minute() !== 0 || date.hour() !== 0) { - hasTime = true; - } + if (date.millisecond() !== 0) { + return 'MMM D, YYYY h:mm:ss.SSS a'; + } + if (date.second() !== 0 || date.minute() !== 0 || date.hour() !== 0) { + hasTime = true; } } if (hasTime) { @@ -497,17 +456,7 @@ module.exports = function() { minUnit: 'millisecond', // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/ - displayFormats: { - millisecond: 'h:mm:ss.SSS a', // 11:20:01.123 AM, - second: 'h:mm:ss a', // 11:20:01 AM - minute: 'h:mm a', // 11:20 AM - hour: luxon ? 'ha' : 'hA', // 5PM - day: luxon ? 'MMM d' : 'MMM D', // Sep 4 - week: luxon ? 'WW' : 'll', // Week 46, or maybe "[W]WW - YYYY" ? - month: luxon ? 'MMM yyyy' : 'MMM YYYY', // Sept 2015 - quarter: luxon ? '\'Q\'q - yyyy' : '[Q]Q - YYYY', // Q3 - 2015 - year: luxon ? 'yyyy' : 'YYYY' // 2015 - }, + displayFormats: dateWrapper.defaultDisplayFormats, }, ticks: { autoSkip: false, @@ -615,8 +564,8 @@ module.exports = function() { max = parse(timeOpts.max, me) || max; // In case there is no valid min/max, set limits based on unit time option - min = min === MAX_INTEGER ? +moment().startOf(unit) : min; - max = max === MIN_INTEGER ? +moment().endOf(unit) + 1 : max; + min = min === MAX_INTEGER ? new CDate().startOf(unit).valueOf() : min; + max = max === MIN_INTEGER ? new CDate().endOf(unit).valueOf() + 1 : max; // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); @@ -695,13 +644,13 @@ module.exports = function() { label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - return createDate(label, timeOpts).format(timeOpts.tooltipFormat); + return createDate(label, timeOpts).toFormat(timeOpts.tooltipFormat); } if (typeof label === 'string') { return label; } - return createDate(label, timeOpts).format(me._labelFormat); + return createDate(label, timeOpts).toFormat(me._labelFormat); }, /** @@ -716,11 +665,11 @@ module.exports = function() { var minorFormat = formats[me._unit]; var majorUnit = me._majorUnit; var majorFormat = formats[majorUnit]; - var majorTime = (luxon ? tick : tick.clone()).startOf(majorUnit).valueOf(); + var majorTime = tick.startOf(majorUnit).valueOf(); var majorTickOpts = options.ticks.major; var major = majorTickOpts.enabled && majorUnit && majorFormat && time === majorTime; var format = formatOverride ? formatOverride : major ? majorFormat : minorFormat; - var label = luxon ? tick.toFormat(format) : tick.format(format); + var label = tick.toFormat(format); var tickOpts = major ? majorTickOpts : options.ticks.minor; var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); @@ -732,7 +681,7 @@ module.exports = function() { var i, ilen; for (i = 0, ilen = ticks.length; i < ilen; ++i) { - labels.push(this.tickFormatFunction(millisToDate(ticks[i].value), i, ticks)); + labels.push(this.tickFormatFunction(new CDate(ticks[i].value), i, ticks)); } return labels; @@ -781,7 +730,7 @@ module.exports = function() { var pos = (size ? (pixel - start) / size : 0) * (me._offsets.left + 1 + me._offsets.left) - me._offsets.right; var time = interpolate(me._table, 'pos', pos, 'time'); - return moment(time); + return new CDate(time); }, /** @@ -808,7 +757,7 @@ module.exports = function() { var formatOverride = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation - var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, [], formatOverride); + var exampleLabel = me.tickFormatFunction(new CDate(exampleTime), 0, [], formatOverride); var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height;