diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 041be7062af..c117963c0bc 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -34,7 +34,7 @@ * [Category](axes/cartesian/category.md) * [Linear](axes/cartesian/linear.md) * [Logarithmic](axes/cartesian/logarithmic.md) - * [Time](axes/cartesian/time.md) + * [Time and TimeSeries](axes/cartesian/time.md) * [Radial](axes/radial/README.md) * [Linear](axes/radial/linear.md) * [Labelling](axes/labelling.md) diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index dc4a2bb42d0..424246cf116 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -92,6 +92,23 @@ var chart = new Chart(ctx, { ``` ## Parser -If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. +If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. If this is a function, it must return a moment.js object given the appropriate data value. + +# TimeSeries Cartesian Axis + +An other option to display dates is TimeSeries scale. Unlike time scale it shows all points from all datasets with same distance by x axes. + +## Configuration Options + +Configuration options for timeseries scale are very similar to time scale options. The difference is that timeseries don't have tick generation options like 'min', 'max' and 'stepSize'. These options extend the [common tick configuration](README.md#tick-configuration). + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `displayFormats` | `Object` | | Sets how different time units are displayed. [more...](#display-formats) +| `parser` | `String` or `Function` | | Custom parser for dates. [more...](#parser) +| `round` | `String` | `false` | If defined, dates will be rounded to the start of this unit. See [Time Units](#scales-time-units) below for the allowed units. +| `tooltipFormat` | `String` | | The moment js format string to use for the tooltip. +| `unit` | `String` | `false` | If defined, will force the unit to be a certain type. See [Time Units](#scales-time-units) section below for details. +| `minUnit` | `String` | `millisecond` | The minimum display format to be used for a time unit. diff --git a/samples/samples.js b/samples/samples.js index 235685bf2fa..fc922449519 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -115,6 +115,9 @@ }, { title: 'Combo', path: 'scales/time/combo.html' + }, { + title: 'Line (timeseries scale)', + path: 'scales/time/line-timeseries.html' }] }, { title: 'Scale options', diff --git a/samples/scales/time/line-timeseries.html b/samples/scales/time/line-timeseries.html new file mode 100644 index 00000000000..61766e70a45 --- /dev/null +++ b/samples/scales/time/line-timeseries.html @@ -0,0 +1,167 @@ + + + + + Line Chart + + + + + + + +
+ +
+
+
+ + + + + + + + + diff --git a/src/chart.js b/src/chart.js index 8026cc856b7..3996f24fbd0 100644 --- a/src/chart.js +++ b/src/chart.js @@ -28,11 +28,13 @@ require('./elements/element.point')(Chart); require('./elements/element.rectangle')(Chart); require('./scales/scale.linearbase')(Chart); +require('./scales/scale.timebase')(Chart); require('./scales/scale.category')(Chart); require('./scales/scale.linear')(Chart); require('./scales/scale.logarithmic')(Chart); require('./scales/scale.radialLinear')(Chart); require('./scales/scale.time')(Chart); +require('./scales/scale.timeseries')(Chart); // Controllers must be loaded after elements // See Chart.core.datasetController.dataElementType diff --git a/src/helpers/helpers.time.js b/src/helpers/helpers.time.js index f3e6e96b1c4..ce55bd4bb60 100644 --- a/src/helpers/helpers.time.js +++ b/src/helpers/helpers.time.js @@ -219,7 +219,6 @@ module.exports = function(Chart) { max: niceMax }); } - }; }; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 14621ba7a8b..6cf38654938 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -5,7 +5,6 @@ var moment = require('moment'); moment = typeof(moment) === 'function' ? moment : window.moment; module.exports = function(Chart) { - var helpers = Chart.helpers; var timeHelpers = helpers.time; @@ -39,16 +38,7 @@ module.exports = function(Chart) { } }; - var TimeScale = Chart.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(); - - Chart.Scale.prototype.initialize.call(this); - }, + var TimeScale = Chart.TimeScaleBase.extend({ determineDataLimits: function() { var me = this; var timeOpts = me.options.time; @@ -115,6 +105,7 @@ module.exports = function(Chart) { me.dataMax = dataMax; me._parsedData = parsedData; }, + buildTicks: function() { var me = this; var timeOpts = me.options.time; @@ -166,61 +157,7 @@ module.exports = function(Chart) { me.max = helpers.max(me.ticks); me.min = helpers.min(me.ticks); }, - // Get tooltip label - getLabelForIndex: function(index, datasetIndex) { - var me = this; - var label = me.chart.data.labels && index < me.chart.data.labels.length ? me.chart.data.labels[index] : ''; - var value = me.chart.data.datasets[datasetIndex].data[index]; - - if (value !== null && typeof value === 'object') { - label = me.getRightValue(value); - } - // Format nicely - if (me.options.time.tooltipFormat) { - label = timeHelpers.parseTime(me, label).format(me.options.time.tooltipFormat); - } - - return label; - }, - // Function to format an individual tick mark - tickFormatFunction: function(tick, index, ticks) { - var formattedTick; - var tickClone = tick.clone(); - var tickTimestamp = tick.valueOf(); - var major = false; - var tickOpts; - if (this.majorUnit && this.majorDisplayFormat && tickTimestamp === tickClone.startOf(this.majorUnit).valueOf()) { - // format as major unit - formattedTick = tick.format(this.majorDisplayFormat); - tickOpts = this.options.ticks.major; - major = true; - } else { - // format as minor (base) unit - formattedTick = tick.format(this.displayFormat); - tickOpts = this.options.ticks.minor; - } - - var callback = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); - - if (callback) { - return { - value: callback(formattedTick, index, ticks), - major: major - }; - } - return { - value: formattedTick, - major: major - }; - }, - convertTicksToLabels: function() { - var me = this; - me.ticksAsTimestamps = me.ticks; - me.ticks = me.ticks.map(function(tick) { - return moment(tick); - }).map(me.tickFormatFunction, me); - }, getPixelForOffset: function(offset) { var me = this; var epochWidth = me.max - me.min; @@ -234,6 +171,7 @@ module.exports = function(Chart) { var heightOffset = (me.height * decimal); return me.top + Math.round(heightOffset); }, + getPixelForValue: function(value, index, datasetIndex) { var me = this; var offset = null; @@ -256,39 +194,18 @@ module.exports = function(Chart) { return me.getPixelForOffset(offset); } }, + getPixelForTick: function(index) { return this.getPixelForOffset(this.ticksAsTimestamps[index]); }, + getValueForPixel: function(pixel) { var me = this; var innerDimension = me.isHorizontal() ? me.width : me.height; var offset = (pixel - (me.isHorizontal() ? me.left : me.top)) / innerDimension; return moment(me.min + (offset * (me.max - me.min))); }, - // Crude approximation of what the label width might be - getLabelWidth: function(label) { - var me = this; - var ticks = me.options.ticks; - - var tickLabelWidth = me.ctx.measureText(label).width; - var cosRotation = Math.cos(helpers.toRadians(ticks.maxRotation)); - var sinRotation = Math.sin(helpers.toRadians(ticks.maxRotation)); - var tickFontSize = helpers.valueOrDefault(ticks.fontSize, Chart.defaults.global.defaultFontSize); - return (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation); - }, - getLabelCapacity: function(exampleTime) { - var me = this; - - me.displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation - var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []).value; - var tickLabelWidth = me.getLabelWidth(exampleLabel); - - var innerWidth = me.isHorizontal() ? me.width : me.height; - var labelCapacity = innerWidth / tickLabelWidth; - - return labelCapacity; - } }); - Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig); + Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig); }; diff --git a/src/scales/scale.timebase.js b/src/scales/scale.timebase.js new file mode 100644 index 00000000000..bb9f8fd4164 --- /dev/null +++ b/src/scales/scale.timebase.js @@ -0,0 +1,100 @@ +/* global window: false */ +'use strict'; + +var moment = require('moment'); +moment = typeof(moment) === 'function' ? moment : window.moment; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + var timeHelpers = helpers.time; + + Chart.TimeScaleBase = Chart.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(); + + Chart.Scale.prototype.initialize.call(this); + }, + // Get tooltip label + getLabelForIndex: function(index, datasetIndex) { + var me = this; + var chartData = me.chart.data; + var label = chartData.labels && index < chartData.labels.length ? chartData.labels[index] : ''; + var value = chartData.datasets[datasetIndex].data[index]; + + if (helpers.isObject(value)) { + label = me.getRightValue(value); + } + + // Format nicely + if (me.options.time.tooltipFormat) { + label = timeHelpers.parseTime(me, label).format(me.options.time.tooltipFormat); + } + + return label; + }, + // Function to format an individual tick mark + tickFormatFunction: function(tick, index, ticks) { + var formattedTick; + var tickClone = tick.clone(); + var tickTimestamp = tick.valueOf(); + var major = false; + var tickOpts; + if (this.majorUnit && this.majorDisplayFormat && tickTimestamp === tickClone.startOf(this.majorUnit).valueOf()) { + // format as major unit + formattedTick = tick.format(this.majorDisplayFormat); + tickOpts = this.options.ticks.major; + major = true; + } else { + // format as minor (base) unit + formattedTick = tick.format(this.displayFormat); + tickOpts = this.options.ticks.minor; + } + + var callback = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); + + return { + value: callback ? callback(formattedTick, index, ticks) : formattedTick, + major: major + }; + }, + convertTicksToLabels: function() { + var me = this; + me.ticksAsTimestamps = me.ticks; + + var formattedTicks = []; + for (var tickIndex = 0; tickIndex < me.ticks.length; tickIndex++) { + formattedTicks.push(me.tickFormatFunction(moment(me.ticks[tickIndex]))); + } + + me.ticks = formattedTicks; + }, + // Crude approximation of what the label width might be + getLabelWidth: function(label) { + var me = this; + var ticks = me.options.ticks; + + var tickLabelWidth = me.ctx.measureText(label).width; + var cosRotation = Math.cos(helpers.toRadians(ticks.maxRotation)); + var sinRotation = Math.sin(helpers.toRadians(ticks.maxRotation)); + var tickFontSize = helpers.valueOrDefault(ticks.fontSize, Chart.defaults.global.defaultFontSize); + return (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation); + }, + getLabelCapacity: function(exampleTime) { + var me = this; + + me.displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation + var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []).value; + var tickLabelWidth = me.getLabelWidth(exampleLabel); + + var innerWidth = me.isHorizontal() ? me.width : me.height; + var labelCapacity = innerWidth / tickLabelWidth; + + return labelCapacity; + } + }); +}; diff --git a/src/scales/scale.timeseries.js b/src/scales/scale.timeseries.js new file mode 100644 index 00000000000..24b6152852c --- /dev/null +++ b/src/scales/scale.timeseries.js @@ -0,0 +1,196 @@ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + var timeHelpers = helpers.time; + + // Default config for a timeseries scale + var defaultConfig = { + position: 'bottom', + + time: { + parser: false, // false == a pattern string from http://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment + format: false, // DEPRECATED false == date objects, moment object, callback or a pattern string from http://momentjs.com/docs/#/parsing/string-format/ + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + 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:ss a', // 11:20:01 AM + hour: 'MMM D, hA', // Sept 4, 5PM + day: 'll', // Sep 4 2015 + week: 'll', // Week 46, or maybe "[W]WW - YYYY" ? + month: 'MMM YYYY', // Sept 2015 + quarter: '[Q]Q - YYYY', // Q3 + year: 'YYYY' // 2015 + }, + }, + ticks: { + autoSkip: true + } + }; + + function arrayUnique(arr) { + var result = []; + for (var i = 0; i < arr.length; i++) { + if (result.indexOf(arr[i]) === -1) { + result.push(arr[i]); + } + } + return result; + } + + var TimeSeriesScale = Chart.TimeScaleBase.extend({ + /** + * Internal function to get the correct labels. If data.xLabels or data.yLabels are defined, use those + * else fall back to data.labels + * @private + */ + getLabels: function() { + var data = this.chart.data; + return data.xLabels || data.labels; + }, + + determineDataLimits: function() { + var me = this; + var timeOpts = me.options.time; + var chartData = me.chart.data; + var parsedData = { + labels: [], + datasets: [] + }; + + var labels = me.getLabels(); + if (labels) { + for (var labelIndex = 0; labelIndex < labels.length; labelIndex++) { + var label = labels[labelIndex]; + var labelMoment = timeHelpers.parseTime(me, label); + + if (labelMoment.isValid()) { + // We need to round the time + if (timeOpts.round) { + labelMoment.startOf(timeOpts.round); + } + // Store this value for later + parsedData.labels[labelIndex] = labelMoment.valueOf(); + } + } + } + + if (chartData.datasets) { + for (var datasetIndex = 0; datasetIndex < chartData.datasets.length; datasetIndex++) { + var dataset = chartData.datasets[datasetIndex]; + var timestamps = []; + + 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 + for (var dataIndex = 0; dataIndex < dataset.data.length; dataIndex++) { + var value = dataset.data[dataIndex]; + var dataMoment = timeHelpers.parseTime(me, me.getRightValue(value)); + + if (dataMoment.isValid()) { + if (timeOpts.round) { + dataMoment.startOf(timeOpts.round); + } + + timestamps[dataIndex] = dataMoment.valueOf(); + } + } + } else { + // We have no x coordinates, so use the ones from the labels + timestamps = parsedData.labels.slice(); + } + + parsedData.datasets[datasetIndex] = timestamps; + } + } + + var allTimestamps = parsedData.labels; + for (var parsedDatasetIndex = 0; parsedDatasetIndex < parsedData.datasets.length; parsedDatasetIndex++) { + allTimestamps = allTimestamps.concat(parsedData.datasets[parsedDatasetIndex]); + } + + allTimestamps = arrayUnique(allTimestamps).sort(function(a, b) { + return a - b; + }); + + parsedData.allTimestamps = allTimestamps; + + me._parsedData = parsedData; + }, + + buildTicks: function() { + var me = this; + var timeOpts = me.options.time; + + var allTimestamps = me._parsedData.allTimestamps; + var dataMin = allTimestamps[0]; + var dataMax = allTimestamps[allTimestamps.length - 1]; + + var maxTicks = me.getLabelCapacity(dataMin); + var unit = timeOpts.unit || timeHelpers.determineUnit(timeOpts.minUnit, dataMin, dataMax, maxTicks); + me.displayFormat = timeOpts.displayFormats[unit]; + + me._tickTimestamps = allTimestamps; + me.ticks = allTimestamps; + }, + + // Used to get data value locations. Value can either be an index or a numerical value + getPixelForValue: function(value, index, datasetIndex, includeOffset) { + var me = this; + + var offsetAmt = Math.max((me.ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + + if (typeof datasetIndex === 'number') { + var timestamp = me._parsedData.datasets[datasetIndex][index]; + var indexByTimestamp = me._tickTimestamps.indexOf(timestamp); + index = indexByTimestamp !== -1 ? indexByTimestamp : index; + } + + var valueWidth = me.width / offsetAmt; + var widthOffset = valueWidth * index; + + if (me.options.gridLines.offsetGridLines && includeOffset || me.ticks.length === 1 && includeOffset) { + widthOffset += (valueWidth / 2); + } + + return me.left + Math.round(widthOffset); + }, + getPixelForTick: function(index, includeOffset) { + if (this.ticks.length === 1) { + includeOffset = true; + } + return this.getPixelForValue(this.ticks[index], index, null, includeOffset); + }, + getValueForPixel: function(pixel) { + var me = this; + var value; + var offsetAmt = Math.max((me.ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var valueDimension = me.width / offsetAmt; + + pixel -= me.left; + + if (me.options.gridLines.offsetGridLines) { + pixel -= (valueDimension / 2); + } + + if (pixel <= 0) { + value = 0; + } else { + value = Math.round(pixel / valueDimension); + } + + return value; + }, + getBasePixel: function() { + return this.bottom; + } + }); + + Chart.scaleService.registerScaleType('timeseries', TimeSeriesScale, defaultConfig); + +}; diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index d63fc59bf0e..2cdc58481a0 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -108,7 +108,7 @@ describe('Category scale tests', function() { expect(scale.ticks).toEqual(mockData.xLabels); }); - it('Should generate ticks from the data xLabels', function() { + it('Should generate ticks from the data yLabels', function() { var scaleID = 'myScale'; var mockData = { diff --git a/test/specs/scale.timeseries.tests.js b/test/specs/scale.timeseries.tests.js new file mode 100644 index 00000000000..a14659513c2 --- /dev/null +++ b/test/specs/scale.timeseries.tests.js @@ -0,0 +1,374 @@ +// Time scale tests +describe('TimeSeries scale tests', function() { + function createScale(data, options) { + var scaleID = 'myScale'; + var mockContext = window.createMockContext(); + var Constructor = Chart.scaleService.getScaleConstructor('timeseries'); + var scale = new Constructor({ + ctx: mockContext, + options: options, + chart: { + data: data + }, + id: scaleID + }); + + scale.update(400, 50); + return scale; + } + + function getTicksValues(ticks) { + return ticks.map(function(tick) { + return tick.value; + }); + } + + beforeEach(function() { + // Need a time matcher for getValueFromPixel + jasmine.addMatchers({ + toBeCloseToTime: function() { + return { + compare: function(actual, expected) { + var result = false; + + var diff = actual.diff(expected.value, expected.unit, true); + result = Math.abs(diff) < (expected.threshold !== undefined ? expected.threshold : 0.01); + + return { + pass: result + }; + } + }; + } + }); + }); + + it('Should load moment.js as a dependency', function() { + expect(window.moment).not.toBe(undefined); + }); + + it('Should register the constructor with the scale service', function() { + var Constructor = Chart.scaleService.getScaleConstructor('timeseries'); + expect(Constructor).not.toBe(undefined); + expect(typeof Constructor).toBe('function'); + }); + + it('Should have the correct default config', function() { + var defaultConfig = Chart.scaleService.getScaleDefaults('timeseries'); + expect(defaultConfig).toEqual({ + display: true, + gridLines: { + color: 'rgba(0, 0, 0, 0.1)', + drawBorder: true, + drawOnChartArea: true, + drawTicks: true, + tickMarkLength: 10, + lineWidth: 1, + offsetGridLines: false, + display: true, + zeroLineColor: 'rgba(0,0,0,0.25)', + zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, + borderDash: [], + borderDashOffset: 0.0 + }, + position: 'bottom', + scaleLabel: { + labelString: '', + display: false + }, + ticks: { + beginAtZero: false, + minRotation: 0, + maxRotation: 50, + mirror: false, + padding: 0, + reverse: false, + display: true, + callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below, + autoSkip: true, + autoSkipPadding: 0, + labelOffset: 0, + minor: {}, + major: {}, + }, + time: { + parser: false, + format: false, + unit: false, + round: false, + minUnit: 'millisecond', + displayFormats: { + millisecond: 'h:mm:ss.SSS a', // 11:20:01.123 AM + second: 'h:mm:ss a', // 11:20:01 AM + minute: 'h:mm:ss a', // 11:20:01 AM + hour: 'MMM D, hA', // Sept 4, 5PM + day: 'll', // Sep 4 2015 + week: 'll', // Week 46, or maybe "[W]WW - YYYY" ? + month: 'MMM YYYY', // Sept 2015 + quarter: '[Q]Q - YYYY', // Q3 + year: 'YYYY' // 2015 + } + } + }); + + // Is this actually a function + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); + }); +// + describe('when given inputs of different types', function() { + // Helper to build date objects + function newDateFromRef(days) { + return moment('01/01/2015 12:00', 'DD/MM/YYYY HH:mm').add(days, 'd').toDate(); + } + + it('should accept labels as strings', function() { + var mockData = { + labels: ['2015-01-01T12:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days + }; + + var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('timeseries')); + scale.update(1000, 200); + expect(getTicksValues(scale.ticks)).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 10, 2015']); + }); + + it('should accept labels as date objects', function() { + var mockData = { + labels: [newDateFromRef(0), newDateFromRef(1), newDateFromRef(2), newDateFromRef(4), newDateFromRef(6), newDateFromRef(7), newDateFromRef(9)], // days + }; + var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('timeseries')); + scale.update(1000, 200); + expect(getTicksValues(scale.ticks)).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 10, 2015']); + }); + + it('should accept data as xy points', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [{ + x: newDateFromRef(0), + y: 1 + }, { + x: newDateFromRef(1), + y: 10 + }, { + x: newDateFromRef(2), + y: 0 + }, { + x: newDateFromRef(4), + y: 5 + }, { + x: newDateFromRef(6), + y: 77 + }, { + x: newDateFromRef(7), + y: 9 + }, { + x: newDateFromRef(9), + y: 5 + }] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'timeseries', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + xScale.update(800, 200); + expect(getTicksValues(xScale.ticks)).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 10, 2015']); + }); + }); + + it('should allow custom time parsers', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [{ + x: 375068900, + y: 1 + }] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'timeseries', + position: 'bottom', + time: { + unit: 'day', + round: true, + parser: function(label) { + return moment.unix(label); + } + } + }], + } + } + }); + + // Counts down because the lines are drawn top to bottom + var xScale = chart.scales.xScale0; + + // Counts down because the lines are drawn top to bottom + expect(getTicksValues(xScale.ticks)[0]).toEqualOneOf(['Nov 19, 1981', 'Nov 20, 1981', 'Nov 21, 1981']); // handle time zone changes + }); + + it('should build ticks using the config unit', function() { + var mockData = { + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('timeseries')); + config.time.unit = 'hour'; + + var scale = createScale(mockData, config); + scale.update(2500, 200); + expect(getTicksValues(scale.ticks)).toEqual(['Jan 1, 8PM', 'Jan 2, 9PM']); + }); + + it('build ticks honoring the minUnit', function() { + var mockData = { + labels: ['2015-01-01T20:00:00', '2016-01-02T21:00:00'], // days + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('timeseries')); + config.time.minUnit = 'year'; + + var scale = createScale(mockData, config); + expect(getTicksValues(scale.ticks)).toEqual(['2015', '2016']); + }); + + it('should build ticks using the config diff', function() { + var mockData = { + labels: ['2015-01-01T20:00:00', '2015-02-02T21:00:00', '2015-02-21T01:00:00'], // days + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); + config.time.unit = 'week'; + config.time.round = 'week'; + + var scale = createScale(mockData, config); + scale.update(800, 200); + + // last date is feb 15 because we round to start of week + expect(getTicksValues(scale.ticks)).toEqual(['Dec 28, 2014', 'Feb 1, 2015', 'Feb 15, 2015']); + }); + + it('should get the correct label for a data value', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [null, 10, 3] + }], + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); + expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); + expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); + }); + + it ('Should get the correct pixel for a value', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: [10, 5, 0, 25, 78] + }], + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00'] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'timeseries', + position: 'bottom' + }], + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } + }); + + var xScale = chart.scales.xScale0; + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(42); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(42); + expect(xScale.getValueForPixel(33)).toBe(0); + + expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(470); + expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(470); + expect(xScale.getValueForPixel(487)).toBe(4); + + xScale.options.gridLines.offsetGridLines = true; + + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(42); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(85); + expect(xScale.getValueForPixel(33)).toBe(0); + expect(xScale.getValueForPixel(78)).toBe(0); + + expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(384); + expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(427); + expect(xScale.getValueForPixel(410)).toBe(4); + expect(xScale.getValueForPixel(433)).toBe(4); + }); + + it('does not create a negative width chart when hidden', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }] + }, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + min: moment().subtract(1, 'months'), + max: moment(), + } + }], + }, + responsive: true, + }, + }, { + wrapper: { + style: 'display: none', + }, + }); + expect(chart.scales['y-axis-0'].width).toEqual(0); + expect(chart.scales['y-axis-0'].maxWidth).toEqual(0); + expect(chart.width).toEqual(0); + }); +});