diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index ea9daea2350..d37c63315d1 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. The Moment.js version in the bundled build is private to Chart.js so if you want to use Moment.js yourself, it's better to use Chart.js (non bundled) and import Moment.js manually. + +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](https://www.jsdelivr. 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](https://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. The Moment.js version in the bundled build is private to Chart.js so if you want to use Moment.js yourself, it's better to use Chart.js (non bundled) and import Moment.js manually. diff --git a/package.json b/package.json index bf913bf0aee..6543eedbd66 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "karma-jasmine": "^2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "karma-rollup-preprocessor": "^6.1.1", + "moment": "^2.10.2", + "luxon": "^1.2.1", "merge-stream": "^1.0.1", "pixelmatch": "^4.0.2", "rollup": "^0.67.4", @@ -58,7 +60,9 @@ "yargs": "^12.0.5" }, "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 6edc6282423..60514a59981 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..42d6f3dbf6d --- /dev/null +++ b/samples/scales/time/financial-luxon.html @@ -0,0 +1,105 @@ + + + + + Line Chart + + + + + + + +
+ +
+
+
+ Chart Type: + + + + + + diff --git a/src/helpers/helpers.date.js b/src/helpers/helpers.date.js new file mode 100644 index 00000000000..b92e62f921f --- /dev/null +++ b/src/helpers/helpers.date.js @@ -0,0 +1,6 @@ +'use strict'; + +var luxonHelpers = require('./helpers.luxon'); +var momentHelpers = require('./helpers.moment'); + +module.exports = momentHelpers.moment ? momentHelpers : (luxonHelpers.luxon ? luxonHelpers : undefined); diff --git a/src/helpers/helpers.luxon.js b/src/helpers/helpers.luxon.js new file mode 100644 index 00000000000..8a83f9af19d --- /dev/null +++ b/src/helpers/helpers.luxon.js @@ -0,0 +1,96 @@ +'use strict'; + +var luxon, DateTime; +try { + luxon = require('luxon'); // eslint-disable-line global-require + luxon = (luxon && luxon.DateTime) ? luxon : window.luxon; + DateTime = luxon.DateTime; +} catch (lxe) { + // swallow the error here +} + + +module.exports = { + + luxon: luxon, + + createDate: function(value) { + if (value === undefined) { + value = new Date(); + } + + if (typeof value === 'number') { + return DateTime.fromMillis(value); + } + if (value instanceof Date) { + return DateTime.fromJSDate(value); + } + if (value instanceof DateTime) { + return value; + } + return undefined; + }, + + isValid: function(date) { + return date.isValid; + }, + + millisecond: function(date) { + return date.millisecond; + }, + + second: function(date) { + return date.second; + }, + + minute: function(date) { + return date.minute; + }, + + hour: function(date) { + return date.hour; + }, + + isoWeekday: function(date, value) { + return date.isoWeekday(value); + }, + + startOf: function(date, value) { + return date.startOf(value); + }, + + endOf: function(date, value) { + return date.endOf(value); + }, + + valueOf: function(date) { + return date.valueOf(); + }, + + toFormat: function(date, format) { + return date.toFormat(format); + }, + + add: function(date, value, type) { + var arg = {}; + arg[type] = value; + return date.plus(arg); + }, + + diff: function(date, other, unit) { + var duration = date.diff(other); + return duration.as(unit); + }, + + 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: 'ha', // 5PM + day: 'MMM d', // Sep 4 + week: 'WW', // Week 46, or maybe "[W]WW - YYYY" ? + month: 'MMM yyyy', // Sept 2015 + quarter: '\'Q\'q - yyyy', // Q3 - 2015 + year: 'yyyy' // 2015 + } +}; diff --git a/src/helpers/helpers.moment.js b/src/helpers/helpers.moment.js new file mode 100644 index 00000000000..2c6c14639a1 --- /dev/null +++ b/src/helpers/helpers.moment.js @@ -0,0 +1,87 @@ +'use strict'; + +var moment; +try { + moment = require('moment'); // eslint-disable-line global-require + moment = typeof moment === 'function' ? moment : window.moment; +} catch (mte) { + // swallow the error here +} + +module.exports = { + + moment: moment, + + createDate: function(value) { + if (value === undefined) { + return moment(); + } + + if (value instanceof moment) { + return value; + } + + return moment(value); + }, + + isValid: function(date) { + return date.isValid(); + }, + + millisecond: function(date) { + return date.millisecond(); + }, + + second: function(date) { + return date.second(); + }, + + minute: function(date) { + return date.minute(); + }, + + hour: function(date) { + return date.hour(); + }, + + isoWeekday: function(date, value) { + return date.clone().isoWeekday(value); + }, + + startOf: function(date, value) { + return date.clone().startOf(value); + }, + + endOf: function(date, value) { + return date.clone().endOf(value); + }, + + valueOf: function(date) { + return date.valueOf(); + }, + + toFormat: function(date, format) { + return date.format(format); + }, + + add: function(date, value, type) { + return date.clone().add(value, type); + }, + + diff: function(date, other, unit) { + var duration = moment.duration(date.clone().diff(other)); + return duration.as(unit); + }, + + 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: '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 - 2015 + year: 'YYYY' // 2015 + } +}; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index a2892bfd6cd..8d9bc01508a 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -1,9 +1,9 @@ /* global window: false */ 'use strict'; -var moment = require('moment'); var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var dateHelpers = require('../helpers/helpers.date'); var Scale = require('../core/core.scale'); var scaleService = require('../core/core.scaleService'); @@ -178,36 +178,32 @@ function interpolate(table, skey, sval, tkey) { } /** - * Convert the given value to a moment object using the given time options. - * @see https://momentjs.com/docs/#/parsing/ + * Convert the given value to a date object using the given time options. */ -function momentify(value, options) { +function createDate(value, options) { var parser = options.parser; var format = options.parser || options.format; if (typeof parser === 'function') { - return parser(value); + return dateHelpers.createDate(parser(value)); } - if (typeof value === 'string' && typeof format === 'string') { - return moment(value, format); + if (dateHelpers.moment && typeof value === 'string' && typeof format === 'string') { + return dateHelpers.moment(value, format); } - if (!(value instanceof moment)) { - value = moment(value); - } - - if (value.isValid()) { + value = dateHelpers.createDate(value); + if (value && dateHelpers.isValid(value)) { 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 dateHelpers.createDate(format(value)); } - return value; + return dateHelpers.createDate(value); } function parse(input, scale) { @@ -216,13 +212,13 @@ function parse(input, scale) { } var options = scale.options.time; - var value = momentify(scale.getRightValue(input), options); - if (!value.isValid()) { + var value = createDate(scale.getRightValue(input), options); + if (!dateHelpers.isValid(value)) { return null; } if (options.round) { - value.startOf(options.round); + value = dateHelpers.startOf(value, options.round); } return value.valueOf(); @@ -276,13 +272,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 = 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 && dateHelpers.diff(dateHelpers.createDate(max), dateHelpers.createDate(min), unit) >= ticks.length) { return unit; } } @@ -312,8 +307,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 = dateHelpers.createDate(min); + var last = dateHelpers.createDate(max); var ticks = []; var time; @@ -323,30 +318,30 @@ function generate(min, max, capacity, options) { // For 'week' unit, handle the first day of week option if (weekday) { - first = first.isoWeekday(weekday); - last = last.isoWeekday(weekday); + first = dateHelpers.isoWeekday(first, weekday); + last = dateHelpers.isoWeekday(last, weekday); } // Align first/last ticks on unit - first = first.startOf(weekday ? 'day' : minor); - last = last.startOf(weekday ? 'day' : minor); + first = dateHelpers.startOf(first, weekday ? 'day' : minor); + last = dateHelpers.startOf(last, weekday ? 'day' : minor); // Make sure that the last tick include max if (last < max) { - last.add(1, minor); + last = dateHelpers.add(last, 1, minor); } - time = moment(first); + time = dateHelpers.createDate(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 = dateHelpers.startOf(time, major); + time = dateHelpers.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor); } - for (; time < last; time.add(stepSize, minor)) { + for (; time < last; time = dateHelpers.add(time, stepSize, minor)) { ticks.push(+time); } @@ -392,7 +387,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 === dateHelpers.startOf(dateHelpers.createDate(value), majorUnit).valueOf() : false; ticks.push({ value: value, @@ -404,17 +399,17 @@ 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) { + date = createDate(data[i], timeOpts); + if (dateHelpers.millisecond(date) !== 0) { return 'MMM D, YYYY h:mm:ss.SSS a'; } - if (momentDate.second() !== 0 || momentDate.minute() !== 0 || momentDate.hour() !== 0) { + if (dateHelpers.second(date) !== 0 || dateHelpers.minute(date) !== 0 || dateHelpers.hour(date) !== 0) { hasTime = true; } } @@ -455,19 +450,7 @@ module.exports = function() { displayFormat: false, // DEPRECATED isoWeekday: false, // override week start day - see https://momentjs.com/docs/#/get-set/iso-weekday/ minUnit: 'millisecond', - - // defaults to unit's corresponding unitFormat below or override using pattern string from https://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: '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 - }, + displayFormats: dateHelpers.defaultDisplayFormats }, ticks: { autoSkip: false, @@ -490,8 +473,8 @@ 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'); + if (!dateHelpers) { + 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'); } this.mergeTicksOptions(); @@ -579,8 +562,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 ? dateHelpers.startOf(dateHelpers.createDate(), unit).valueOf() : min; + max = max === MIN_INTEGER ? dateHelpers.endOf(dateHelpers.createDate(), unit).valueOf() + 1 : max; // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); @@ -663,13 +646,13 @@ module.exports = function() { label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - return momentify(label, timeOpts).format(timeOpts.tooltipFormat); + return dateHelpers.toFormat(createDate(label, timeOpts), timeOpts.tooltipFormat); } if (typeof label === 'string') { return label; } - return momentify(label, timeOpts).format(me._labelFormat); + return dateHelpers.toFormat(createDate(label, timeOpts), me._labelFormat); }, /** @@ -684,10 +667,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 = dateHelpers.startOf(tick, 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 = dateHelpers.toFormat(tick, format); var tickOpts = major ? majorTickOpts : options.ticks.minor; var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); @@ -699,7 +683,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(dateHelpers.createDate(ticks[i].value), i, ticks)); } return labels; @@ -750,7 +734,7 @@ module.exports = function() { var pos = (size ? (pixel - start) / size : 0) * (me._offsets.start + 1 + me._offsets.start) - me._offsets.end; var time = interpolate(me._table, 'pos', pos, 'time'); - return moment(time); + return dateHelpers.createDate(time); }, /** @@ -777,7 +761,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(dateHelpers.createDate(exampleTime), 0, [], formatOverride); var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height;