From 1a1b5b468ef91197f239bb24c701eff4a5933f85 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Fri, 25 May 2018 21:05:11 -0700 Subject: [PATCH] Allow using either moment or luxon in pluggable fashion --- docs/getting-started/installation.md | 54 ++++++------ gulpfile.js | 2 + package.json | 6 +- samples/samples.js | 5 +- samples/scales/time/financial-luxon.html | 101 +++++++++++++++++++++++ src/scales/scale.time.js | 93 ++++++++++++++------- 6 files changed, 207 insertions(+), 54 deletions(-) create mode 100644 samples/scales/time/financial-luxon.html diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index ccc02ddc4b9..4d4e04e0dd8 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,7 +1,14 @@ # Installation -Chart.js can be installed via npm or bower. It is recommended to get Chart.js this way. -## npm +Chart.js can be installed as a dependency of your application via [npm](https://www.npmjs.com/package/chart.js) or [bower](https://libraries.io/bower/chartjs). It is recommended to get Chart.js this way. + +## Including as a dependency in your build + +### Optional Chart.js dependencies + +If you are using the time scale you will need to include either [Moment.js](https://momentjs.com) or [Luxon](https://moment.github.io/luxon/docs/manual/install.html#node) in your dependencies. If you do not include Moment.js you will receive a warning that it is missing from your build. You may ignore this warning if you are not using the time scale or have included Luxon. + +### npm [![npm](https://img.shields.io/npm/v/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) [![npm](https://img.shields.io/npm/dm/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) @@ -9,14 +16,33 @@ Chart.js can be installed via npm or bower. It is recommended to get Chart.js th npm install chart.js --save ``` -## Bower +### Bower [![bower](https://img.shields.io/bower/v/chartjs.svg?style=flat-square&maxAge=600)](https://libraries.io/bower/chartjs) ```bash bower install chart.js --save ``` -## CDN +## Pre-built scripts + +Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. + +### Stand-Alone Build +Files: +* `dist/Chart.js` +* `dist/Chart.min.js` + +The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. + +### Bundled Build +Files: +* `dist/Chart.bundle.js` +* `dist/Chart.bundle.min.js` + +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatibility issues. + +You can get these builds from the CDNs below. + ### CDNJS [![cdnjs](https://img.shields.io/cdnjs/v/Chart.js.svg?style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) @@ -31,27 +57,9 @@ Chart.js built files are also available through [jsDelivr](http://www.jsdelivr.c https://www.jsdelivr.com/package/npm/chart.js?path=dist -## Github +### Github [![github](https://img.shields.io/github/release/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://github.com/chartjs/Chart.js/releases/latest) You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). If you download or clone the repository, you must [build](../developers/contributing.md#building-and-testing) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. - -# Selecting the Correct Build - -Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. - -## Stand-Alone Build -Files: -* `dist/Chart.js` -* `dist/Chart.min.js` - -The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. - -## Bundled Build -Files: -* `dist/Chart.bundle.js` -* `dist/Chart.bundle.min.js` - -The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. diff --git a/gulpfile.js b/gulpfile.js index 24c01665ae4..63f207cd6ec 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -107,6 +107,7 @@ function buildTask() { } var bundled = browserify('./src/chart.js', { standalone: 'Chart' }) + .ignore('luxon') .plugin(collapse) .bundle() .on('error', errorHandler) @@ -121,6 +122,7 @@ function buildTask() { .pipe(gulp.dest(outDir)); var nonBundled = browserify('./src/chart.js', { standalone: 'Chart' }) + .ignore('luxon') .ignore('moment') .plugin(collapse) .bundle() diff --git a/package.json b/package.json index a2e6c494af1..05554d0f0b2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", "karma-jasmine-html-reporter": "^0.2.2", + "moment": "^2.10.2", + "luxon": "^1.2.1", "merge-stream": "^1.0.1", "pixelmatch": "^4.0.2", "vinyl-source-stream": "^1.1.0", @@ -51,7 +53,9 @@ "main": "Chart.js" }, "dependencies": { - "chartjs-color": "^2.1.0", + "chartjs-color": "^2.1.0" + }, + "peerDependencies": { "moment": "^2.10.2" } } diff --git a/samples/samples.js b/samples/samples.js index b818827c6d2..7a0056ad62c 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -113,8 +113,11 @@ title: 'Line (point data)', path: 'scales/time/line-point-data.html' }, { - title: 'Time Series', + title: 'Time Series - Moment', path: 'scales/time/financial.html' + }, { + title: 'Time Series - Luxon', + path: 'scales/time/financial-luxon.html' }, { title: 'Combo', path: 'scales/time/combo.html' diff --git a/samples/scales/time/financial-luxon.html b/samples/scales/time/financial-luxon.html new file mode 100644 index 00000000000..1cae2012d3b --- /dev/null +++ b/samples/scales/time/financial-luxon.html @@ -0,0 +1,101 @@ + + + + + Line Chart + + + + + + + +
+ +
+
+
+ Chart Type: + + + + + + diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index dfd708b2345..125251b7ae3 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -1,8 +1,19 @@ /* global window: false */ 'use strict'; -var moment = require('moment'); -moment = typeof moment === 'function' ? moment : window.moment; +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'); @@ -179,11 +190,24 @@ 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. * @see http://momentjs.com/docs/#/parsing/ */ -function momentify(value, options) { +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; @@ -212,14 +236,18 @@ function momentify(value, options) { return value; } +function isValid(date) { + return luxon ? date.isValid : date.isValid(); +} + function parse(input, scale) { if (helpers.isNullOrUndef(input)) { return null; } var options = scale.options.time; - var value = momentify(scale.getRightValue(input), options); - if (!value.isValid()) { + var value = createDate(scale.getRightValue(input), options); + if (!isValid(value)) { return null; } @@ -278,7 +306,8 @@ 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 = moment.duration(moment(max).diff(moment(min))); + var duration = luxon ? createDate(max).diff(createDate(min)) + : moment.duration(moment(max).diff(moment(min))); var ilen = UNITS.length; var i, unit; @@ -394,7 +423,7 @@ function ticksFromTimestamps(values, majorUnit) { for (i = 0, ilen = values.length; i < ilen; ++i) { value = values[i]; - major = majorUnit ? value === +moment(value).startOf(majorUnit) : false; + major = majorUnit ? value === millisToDate(value).startOf(majorUnit).valueOf() : false; ticks.push({ value: value, @@ -406,18 +435,27 @@ function ticksFromTimestamps(values, majorUnit) { } function determineLabelFormat(data, timeOpts) { - var i, momentDate, hasTime; + var i, date, hasTime; var ilen = data.length; // find the label with the most parts (milliseconds, minutes, etc.) // format all labels with the same level of detail as the most specific label for (i = 0; i < ilen; i++) { - momentDate = momentify(data[i], timeOpts); - if (momentDate.millisecond() !== 0) { - return 'MMM D, YYYY h:mm:ss.SSS a'; - } - if (momentDate.second() !== 0 || momentDate.minute() !== 0 || momentDate.hour() !== 0) { - hasTime = true; + 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 (hasTime) { @@ -463,12 +501,12 @@ module.exports = function() { 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: 'hA', // 5PM - day: 'MMM D', // Sep 4 - week: 'll', // Week 46, or maybe "[W]WW - YYYY" ? - month: 'MMM YYYY', // Sept 2015 - quarter: '[Q]Q - YYYY', // Q3 - year: 'YYYY' // 2015 + 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 }, }, ticks: { @@ -492,10 +530,6 @@ module.exports = function() { var TimeScale = Scale.extend({ initialize: function() { - if (!moment) { - throw new Error('Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com'); - } - this.mergeTicksOptions(); Scale.prototype.initialize.call(this); @@ -660,13 +694,13 @@ module.exports = function() { label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - return momentify(label, timeOpts).format(timeOpts.tooltipFormat); + return createDate(label, timeOpts).format(timeOpts.tooltipFormat); } if (typeof label === 'string') { return label; } - return momentify(label, timeOpts).format(me._labelFormat); + return createDate(label, timeOpts).format(me._labelFormat); }, /** @@ -681,10 +715,11 @@ module.exports = function() { var minorFormat = formats[me._unit]; var majorUnit = me._majorUnit; var majorFormat = formats[majorUnit]; - var majorTime = tick.clone().startOf(majorUnit).valueOf(); + var majorTime = (luxon ? tick : tick.clone()).startOf(majorUnit).valueOf(); var majorTickOpts = options.ticks.major; var major = majorTickOpts.enabled && majorUnit && majorFormat && time === majorTime; - var label = tick.format(formatOverride ? formatOverride : major ? majorFormat : minorFormat); + var format = formatOverride ? formatOverride : major ? majorFormat : minorFormat; + var label = luxon ? tick.toFormat(format) : tick.format(format); var tickOpts = major ? majorTickOpts : options.ticks.minor; var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); @@ -696,7 +731,7 @@ module.exports = function() { var i, ilen; for (i = 0, ilen = ticks.length; i < ilen; ++i) { - labels.push(this.tickFormatFunction(moment(ticks[i].value), i, ticks)); + labels.push(this.tickFormatFunction(millisToDate(ticks[i].value), i, ticks)); } return labels;