From 01127a6219982d76cb3774dabbd3ec87051aea03 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 23 Oct 2016 09:07:57 -0400 Subject: [PATCH 1/6] Make parseTime private --- src/scales/scale.time.js | 49 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index e922734fb00..f76db1ee89c 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -68,6 +68,31 @@ module.exports = function(Chart) { } }; + 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); + } + // Date objects + if (typeof label.getMonth === 'function' || typeof label === 'number') { + return moment(label); + } + // Moment support + if (label.isValid && label.isValid()) { + return label; + } + // Custom parsing (return an instance of moment) + if (typeof timeOpts.format !== 'string' && timeOpts.format.call) { + console.warn('options.time.format is deprecated and replaced by options.time.parser. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale'); + return timeOpts.format(label); + } + // Moment format parsing + return moment(label, timeOpts.format); + } + var TimeScale = Chart.Scale.extend({ initialize: function() { if (!moment) { @@ -381,30 +406,6 @@ module.exports = function(Chart) { offset *= me.scaleSizeInUnits; return me.firstTick.clone().add(moment.duration(offset, me.tickUnit).asSeconds(), 'seconds'); }, - parseTime: function(label) { - var me = this; - if (typeof me.options.time.parser === 'string') { - return moment(label, me.options.time.parser); - } - if (typeof me.options.time.parser === 'function') { - return me.options.time.parser(label); - } - // Date objects - if (typeof label.getMonth === 'function' || typeof label === 'number') { - return moment(label); - } - // Moment support - if (label.isValid && label.isValid()) { - return label; - } - // Custom parsing (return an instance of moment) - if (typeof me.options.time.format !== 'string' && me.options.time.format.call) { - console.warn('options.time.format is deprecated and replaced by options.time.parser. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale'); - return me.options.time.format(label); - } - // Moment format parsing - return moment(label, me.options.time.format); - } }); Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig); From 8a2f45379cacedc98bb1adc2a7e9c00bc8c8302f Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 23 Oct 2016 13:26:09 -0400 Subject: [PATCH 2/6] start on fixing time scale --- src/scales/scale.time.js | 489 ++++++++++++++++++++------------------- 1 file changed, 249 insertions(+), 240 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index f76db1ee89c..03e436f4ccd 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -8,34 +8,42 @@ module.exports = function(Chart) { var helpers = Chart.helpers; var time = { - units: [{ - name: 'millisecond', + millisecond: { + size: 1, steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] - }, { - name: 'second', + }, + second: { + size: 1000, steps: [1, 2, 5, 10, 30] - }, { - name: 'minute', + }, + minute: { + size: 60000, steps: [1, 2, 5, 10, 30] - }, { - name: 'hour', + }, + hour: { + size: 3600000, steps: [1, 2, 3, 6, 12] - }, { - name: 'day', + }, + day: { + size: 86400000, steps: [1, 2, 5] - }, { - name: 'week', + }, + week: { + size: 604800000, maxStep: 4 - }, { - name: 'month', + }, + month: { + size: 2.628e9, maxStep: 3 - }, { - name: 'quarter', + }, + quarter: { + size: 7.884e9, maxStep: 4 - }, { - name: 'year', + }, + year: { + size: 3.154e10, maxStep: false - }] + } }; var defaultConfig = { @@ -61,31 +69,33 @@ module.exports = function(Chart) { month: 'MMM YYYY', // Sept 2015 quarter: '[Q]Q - YYYY', // Q3 year: 'YYYY' // 2015 - } + }, }, ticks: { autoSkip: false } }; + /** + * 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') { + } else if (typeof timeOpts.parser === 'function') { return timeOpts.parser(label); - } - // Date objects - if (typeof label.getMonth === 'function' || typeof label === 'number') { + } else if (typeof label.getMonth === 'function' || typeof label === 'number') { + // Date objects return moment(label); - } - // Moment support - if (label.isValid && label.isValid()) { + } else if (label.isValid && label.isValid()) { + // Moment support return label; - } - // Custom parsing (return an instance of moment) - if (typeof timeOpts.format !== 'string' && timeOpts.format.call) { + } else if (typeof timeOpts.format !== 'string' && timeOpts.format.call) { + // Custom parsing (return an instance of moment) console.warn('options.time.format is deprecated and replaced by options.time.parser. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale'); return timeOpts.format(label); } @@ -93,246 +103,243 @@ module.exports = function(Chart) { return moment(label, timeOpts.format); } - 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'); + /** + * Figure out which is the best unit for the scale + * @param min {Number} scale minimum + * @param max {Number} scale maximum + * @return {String} the unit to use + */ + function determineUnit(min, max) { + var units = Object.keys(time); + var maxTicks = 11; + var unit; + + for (var i = 0; i < units.length; i++) { + unit = units[i]; + var unitDetails = time[unit]; + var fits = false; + + if (unitDetails.steps) { + fits = Math.ceil((max - min) / (unitDetails.steps[unitDetails.steps.length - 1] * unitDetails.size)) <= maxTicks; + } else if (unitDetails.maxStep) { + fits = Math.ceil((max - min) / (unitDetails.maxStep * unitDetails.size)) <= maxTicks; + } else { + // No limit on the multiplier so it always fits + fits = true; } - Chart.Scale.prototype.initialize.call(this); - }, - getLabelDiff: function(datasetIndex, index) { - var me = this; - if (datasetIndex === null || index === null) { - return null; + if (fits) { + break; } + } - if (me.labelDiffs === undefined) { - me.buildLabelDiffs(); + 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 in milliseconds + */ + function determineStepSize(min, max, unit) { + // Using our unit, figoure out what we need to scale as + var maxTicks = 11; // eventually configure this + var unitDefinition = time[unit]; + var unitSizeInMilliSeconds = unitDefinition.size; + var sizeInUnits = Math.ceil((max - min) % unitSizeInMilliSeconds); + var multiplier = 1; + + if (unitDefinition.steps) { + // Have an array of steps + for (var i = 0; i < unitDefinition.steps.length && sizeInUnits > maxTicks; i++) { + multiplier = unitDefinition.steps[i]; + sizeInUnits = Math.ceil((max - min) / (unitSizeInMilliSeconds * multiplier)); + } + } else { + while (sizeInUnits > maxTicks) { + ++multiplier; + sizeInUnits = Math.ceil((max - min) / (unitSizeInMilliSeconds * multiplier)); } + } + + return unitSizeInMilliSeconds * multiplier; + } - if (typeof me.labelDiffs[datasetIndex] !== 'undefined') { - return me.labelDiffs[datasetIndex][index]; + /** + * @function Chart.Ticks.generators.time + * @param generationOptions {ITimeGeneratorOptions} the options for generation + * @param dataRange {IRange} the data range + * @return {Number[]} ticks + */ + Chart.Ticks.generators.time = function(generationOptions, dataRange) { + var ticks = []; + var spacing = generationOptions.stepSize; + var baseSpacing = generationOptions.baseSize; + var niceMin = Math.floor(dataRange.min / baseSpacing) * baseSpacing; + var niceMax = Math.ceil(dataRange.max / baseSpacing) * baseSpacing; + + // If min, max and stepSize is set and they make an evenly spaced scale use it. + if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + var minMaxDeltaDivisableByStepSize = ((generationOptions.max - generationOptions.min) % generationOptions.stepSize) === 0; + if (minMaxDeltaDivisableByStepSize) { + niceMin = generationOptions.min; + niceMax = generationOptions.max; } + } - return null; - }, - getMomentStartOf: function(tick) { - var me = this; - if (me.options.time.unit === 'week' && me.options.time.isoWeekday !== false) { - return tick.clone().startOf('isoWeek').isoWeekday(me.options.time.isoWeekday); + var numSpaces = (niceMax - niceMin) / spacing; + // If very close to our rounded value, use it. + if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + + // Put the values into the ticks array + ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); + for (var j = 1; j < numSpaces; ++j) { + ticks.push(niceMin + (j * spacing)); + } + ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); + + return ticks; + }; + + 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'); } - return tick.clone().startOf(me.tickUnit); + + Chart.Scale.prototype.initialize.call(this); }, determineDataLimits: function() { var me = this; - me.labelMoments = []; + var timeOpts = me.options.time; - function appendLabel(array, label) { - var labelMoment = me.parseTime(label); - if (labelMoment.isValid()) { - if (me.options.time.round) { - labelMoment.startOf(me.options.time.round); - } - array.push(labelMoment); - } - } + // We store the data range as unix millisecond timestamps so dataMin and dataMax will always be integers. + var dataMin = Number.MAX_SAFE_INTEGER; + var dataMax = Number.MIN_SAFE_INTEGER; - // Only parse these once. If the dataset does not have data as x,y pairs, we will use - // these - var scaleLabelMoments = []; - if (me.chart.data.labels && me.chart.data.labels.length > 0) { - helpers.each(me.chart.data.labels, function(label) { - appendLabel(scaleLabelMoments, label); - }, me); + var chartData = me.chart.data; + var parsedData = { + labels: [], + datasets: [] + }; - me.firstTick = moment.min(scaleLabelMoments); - me.lastTick = moment.max(scaleLabelMoments); - } else { - me.firstTick = null; - me.lastTick = null; - } + var timestamp; - helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { - var momentsForDataset = []; + if (chartData.labels && chartData.labels.length) { + for (var i = 0; i < chartData.labels.length; i++) { + var labelMoment = parseTime(me, chartData.labels[i]); + + if (labelMoment.isValid()) { + // We need to round the time + if (timeOpts.round) { + labelMoment.startOf(timeOpts.round); + } - if (typeof dataset.data[0] === 'object' && dataset.data[0] !== null) { - helpers.each(dataset.data, function(value) { - appendLabel(momentsForDataset, me.getRightValue(value)); - }, me); + timestamp = labelMoment.valueOf(); + dataMin = Math.min(timestamp, dataMin); + dataMax = Math.max(timestamp, dataMax); - if (me.chart.isDatasetVisible(datasetIndex)) { - // May have gone outside the scale ranges, make sure we keep the first and last ticks updated - var min = moment.min(momentsForDataset); - var max = moment.max(momentsForDataset); - me.firstTick = me.firstTick !== null ? moment.min(me.firstTick, min) : min; - me.lastTick = me.lastTick !== null ? moment.max(me.lastTick, max) : max; + // Store this value for later + parsedData.labels[i] = timestamp; } - } else { - // We have no labels. Use the ones from the scale - momentsForDataset = scaleLabelMoments; } - - me.labelMoments.push(momentsForDataset); - }, me); - - // Set these after we've done all the data - if (me.options.time.min) { - me.firstTick = me.parseTime(me.options.time.min); } - if (me.options.time.max) { - me.lastTick = me.parseTime(me.options.time.max); - } - - // We will modify these, so clone for later - me.firstTick = (me.firstTick || moment()).clone(); - me.lastTick = (me.lastTick || moment()).clone(); - }, - buildLabelDiffs: function() { - var me = this; - - me.labelDiffs = me.labelMoments.map(function(datasetLabels) { - return datasetLabels.map(function(label) { - return label.diff(me.firstTick, me.tickUnit, true); - }); - }); - }, - buildTicks: function() { - var me = this; - - me.ctx.save(); - var tickFontSize = helpers.getValueOrDefault(me.options.ticks.fontSize, Chart.defaults.global.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(me.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(me.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - me.ctx.font = tickLabelFont; - - me.ticks = []; - me.unitScale = 1; // How much we scale the unit by, ie 2 means 2x unit per step - me.scaleSizeInUnits = 0; // How large the scale is in the base unit (seconds, minutes, etc) - - // Set unit override if applicable - if (me.options.time.unit) { - me.tickUnit = me.options.time.unit || 'day'; - me.displayFormat = me.options.time.displayFormats[me.tickUnit]; - me.scaleSizeInUnits = me.lastTick.diff(me.firstTick, me.tickUnit, true); - me.unitScale = helpers.getValueOrDefault(me.options.time.unitStepSize, 1); - } else { - // Determine the smallest needed unit of the time - var innerWidth = me.isHorizontal() ? me.width : me.height; - - // Crude approximation of what the label length might be - var tempFirstLabel = me.tickFormatFunction(me.firstTick, 0, []); - var tickLabelWidth = me.ctx.measureText(tempFirstLabel).width; - var cosRotation = Math.cos(helpers.toRadians(me.options.ticks.maxRotation)); - var sinRotation = Math.sin(helpers.toRadians(me.options.ticks.maxRotation)); - tickLabelWidth = (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation); - var labelCapacity = innerWidth / (tickLabelWidth); - - // Start as small as possible - me.tickUnit = me.options.time.minUnit; - me.scaleSizeInUnits = me.lastTick.diff(me.firstTick, me.tickUnit, true); - me.displayFormat = me.options.time.displayFormats[me.tickUnit]; - - var unitDefinitionIndex = 0; - var unitDefinition = time.units[unitDefinitionIndex]; - - // While we aren't ideal and we don't have units left - while (unitDefinitionIndex < time.units.length) { - // Can we scale this unit. If `false` we can scale infinitely - me.unitScale = 1; - - if (helpers.isArray(unitDefinition.steps) && Math.ceil(me.scaleSizeInUnits / labelCapacity) < helpers.max(unitDefinition.steps)) { - // Use one of the predefined steps - for (var idx = 0; idx < unitDefinition.steps.length; ++idx) { - if (unitDefinition.steps[idx] >= Math.ceil(me.scaleSizeInUnits / labelCapacity)) { - me.unitScale = helpers.getValueOrDefault(me.options.time.unitStepSize, unitDefinition.steps[idx]); - break; + var datasets = chartData.datasets; + for (var datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { + var dataset = datasets[datasetIndex]; + if (me.chart.isDatasetVisible(datasetIndex) && dataset.data.length) { + var timestamps = []; + + if (typeof dataset.data[0] === 'object' && dataset.data[0] !== null) { + // We have potential point data, so we need to parse this + for (var dataIndex = 0; dataIndex < dataset.data.length; dataIndex++) { + var dataMoment = parseTime(me, me.getRightValue(dataset.data[dataIndex])); + + if (dataMoment.isValid()) { + if (timeOpts.round) { + dataMoment.startOf(timeOpts.round); + } + + timestamp = dataMoment.valueOf(); + dataMin = Math.min(timestamp, dataMin); + dataMax = Math.max(timestamp, dataMax); + timestamps[dataIndex] = timestamp; } } - - break; - } else if ((unitDefinition.maxStep === false) || (Math.ceil(me.scaleSizeInUnits / labelCapacity) < unitDefinition.maxStep)) { - // We have a max step. Scale this unit - me.unitScale = helpers.getValueOrDefault(me.options.time.unitStepSize, Math.ceil(me.scaleSizeInUnits / labelCapacity)); - break; } else { - // Move to the next unit up - ++unitDefinitionIndex; - unitDefinition = time.units[unitDefinitionIndex]; - - me.tickUnit = unitDefinition.name; - var leadingUnitBuffer = me.firstTick.diff(me.getMomentStartOf(me.firstTick), me.tickUnit, true); - var trailingUnitBuffer = me.getMomentStartOf(me.lastTick.clone().add(1, me.tickUnit)).diff(me.lastTick, me.tickUnit, true); - me.scaleSizeInUnits = me.lastTick.diff(me.firstTick, me.tickUnit, true) + leadingUnitBuffer + trailingUnitBuffer; - me.displayFormat = me.options.time.displayFormats[unitDefinition.name]; + // We have no x coordinates, so use the ones from the labels + timestamps = parsedData.labels.slice(); } + + parsedData.datasets[datasetIndex] = timestamps; } } - var roundedStart; + me.dataMin = dataMin; + me.dataMax = dataMax; + me._parsedData = parsedData; + }, + buildTicks: function() { + var me = this; + var timeOpts = me.options.time; - // Only round the first tick if we have no hard minimum - if (!me.options.time.min) { - me.firstTick = me.getMomentStartOf(me.firstTick); - roundedStart = me.firstTick; - } else { - roundedStart = me.getMomentStartOf(me.firstTick); - } + var minTimestamp; + var maxTimestamp; + var dataMin = me.dataMin; + var dataMax = me.dataMax; - // Only round the last tick if we have no hard maximum - if (!me.options.time.max) { - var roundedEnd = me.getMomentStartOf(me.lastTick); - var delta = roundedEnd.diff(me.lastTick, me.tickUnit, true); - if (delta < 0) { - // Do not use end of because we need me to be in the next time unit - me.lastTick = me.getMomentStartOf(me.lastTick.add(1, me.tickUnit)); - } else if (delta >= 0) { - me.lastTick = roundedEnd; + if (timeOpts.min) { + var minMoment = parseTime(me, timeOpts.min); + if (timeOpts.round) { + minMoment.round(timeOpts.round); } - - me.scaleSizeInUnits = me.lastTick.diff(me.firstTick, me.tickUnit, true); + minTimestamp = minMoment.valueOf(); } - // Tick displayFormat override - if (me.options.time.displayFormat) { - me.displayFormat = me.options.time.displayFormat; + if (timeOpts.max) { + maxTimestamp = parseTime(me, timeOpts.max).valueOf(); } - // first tick. will have been rounded correctly if options.time.min is not specified - me.ticks.push(me.firstTick.clone()); - - // For every unit in between the first and last moment, create a moment and add it to the ticks tick - for (var i = me.unitScale; i <= me.scaleSizeInUnits; i += me.unitScale) { - var newTick = roundedStart.clone().add(i, me.tickUnit); - - // Are we greater than the max time - if (me.options.time.max && newTick.diff(me.lastTick, me.tickUnit, true) >= 0) { - break; - } - - me.ticks.push(newTick); + var unit; + if (timeOpts.unit) { + unit = timeOpts.unit; + } else { + // Auto Determine Unit + unit = determineUnit(minTimestamp || dataMin, maxTimestamp || dataMax); } + me.displayFormat = timeOpts.displayFormats[unit]; - // Always show the right tick - var diff = me.ticks[me.ticks.length - 1].diff(me.lastTick, me.tickUnit); - if (diff !== 0 || me.scaleSizeInUnits === 0) { - // this is a weird case. If the option is the same as the end option, we can't just diff the times because the tick was created from the roundedStart - // but the last tick was not rounded. - if (me.options.time.max) { - me.ticks.push(me.lastTick.clone()); - me.scaleSizeInUnits = me.lastTick.diff(me.ticks[0], me.tickUnit, true); - } else { - me.ticks.push(me.lastTick.clone()); - me.scaleSizeInUnits = me.lastTick.diff(me.firstTick, me.tickUnit, true); - } + var stepSize; + if (timeOpts.stepSize) { + stepSize = timeOpts.stepSize; + } else { + // Auto determine step size + stepSize = determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit); } - me.ctx.restore(); + var timeGeneratorOptions = { + maxTicks: 11, + min: minTimestamp, + max: maxTimestamp, + stepSize: stepSize, + baseSize: time[unit].size + }; + var ticks = me.ticks = Chart.Ticks.generators.time(timeGeneratorOptions, { + min: dataMin, + max: dataMax + }); - // Invalidate label diffs cache - me.labelDiffs = undefined; + // At this point, we need to update our max and min given the tick values since we have expanded the + // range of the scale + me.max = helpers.max(ticks); + me.min = helpers.min(ticks); }, // Get tooltip label getLabelForIndex: function(index, datasetIndex) { @@ -346,7 +353,7 @@ module.exports = function(Chart) { // Format nicely if (me.options.time.tooltipFormat) { - label = me.parseTime(label).format(me.options.time.tooltipFormat); + label = parseTime(me, label).format(me.options.time.tooltipFormat); } return label; @@ -364,28 +371,31 @@ module.exports = function(Chart) { }, convertTicksToLabels: function() { var me = this; - me.tickMoments = me.ticks; - me.ticks = me.ticks.map(me.tickFormatFunction, me); + me._ticksAsTimestamps = me.ticks; + me.ticks = me.ticks.map(function(tick) { + return moment(tick); + }).map(me.tickFormatFunction, me); }, getPixelForValue: function(value, index, datasetIndex) { var me = this; var offset = null; if (index !== undefined && datasetIndex !== undefined) { - offset = me.getLabelDiff(datasetIndex, index); + offset = me._parsedData.datasets[datasetIndex][index]; } if (offset === null) { if (!value || !value.isValid) { // not already a moment object - value = me.parseTime(me.getRightValue(value)); + value = parseTime(me, me.getRightValue(value)); } + if (value && value.isValid && value.isValid()) { - offset = value.diff(me.firstTick, me.tickUnit, true); + offset = value.valueOf(); } } if (offset !== null) { - var decimal = offset !== 0 ? offset / me.scaleSizeInUnits : offset; + var decimal = (offset - me.min) / (me.max - me.min); if (me.isHorizontal()) { var valueOffset = (me.width * decimal); @@ -397,14 +407,13 @@ module.exports = function(Chart) { } }, getPixelForTick: function(index) { - return this.getPixelForValue(this.tickMoments[index], null, null); + return this.getPixelForValue(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; - offset *= me.scaleSizeInUnits; - return me.firstTick.clone().add(moment.duration(offset, me.tickUnit).asSeconds(), 'seconds'); + return moment(me.min + (offset * (me.max - me.min))); }, }); Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig); From f0b6a9c2e7561a0b9f43c1f81a191d4a49b040fb Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Wed, 18 Jan 2017 16:53:17 +0000 Subject: [PATCH 3/6] Reimplement existing functionality --- samples/scales/time/line-point-data.html | 3 +- src/scales/scale.time.js | 174 ++++++++++++----------- test/specs/scale.time.tests.js | 38 +++-- 3 files changed, 125 insertions(+), 90 deletions(-) diff --git a/samples/scales/time/line-point-data.html b/samples/scales/time/line-point-data.html index d880515f5a1..1eeefa3ada0 100644 --- a/samples/scales/time/line-point-data.html +++ b/samples/scales/time/line-point-data.html @@ -119,7 +119,8 @@ document.getElementById('addData').addEventListener('click', function() { if (config.data.datasets.length > 0) { - var lastTime = myLine.scales['x-axis-0'].labelMoments[0].length ? myLine.scales['x-axis-0'].labelMoments[0][myLine.scales['x-axis-0'].labelMoments[0].length - 1] : moment(); + var numTicks = myLine.scales['x-axis-0'].ticksAsTimestamps.length; + var lastTime = numTicks ? moment(myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); var newTime = lastTime .clone() .add(1, 'day') diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 03e436f4ccd..f30965d6516 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -86,49 +86,46 @@ module.exports = function(Chart) { var timeOpts = axis.options.time; if (typeof timeOpts.parser === 'string') { return moment(label, timeOpts.parser); - } else if (typeof timeOpts.parser === 'function') { + } + if (typeof timeOpts.parser === 'function') { return timeOpts.parser(label); - } else if (typeof label.getMonth === 'function' || typeof label === 'number') { + } + if (typeof label.getMonth === 'function' || typeof label === 'number') { // Date objects return moment(label); - } else if (label.isValid && label.isValid()) { + } + if (label.isValid && label.isValid()) { // Moment support return label; - } else if (typeof timeOpts.format !== 'string' && timeOpts.format.call) { + } + 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. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale'); - return timeOpts.format(label); + console.warn('options.time.format is deprecated and replaced by options.time.parser.'); + return format(label); } // Moment format parsing - return moment(label, timeOpts.format); + 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(min, max) { + function determineUnit(minUnit, min, max) { var units = Object.keys(time); var maxTicks = 11; var unit; + var numUnits = units.length; - for (var i = 0; i < units.length; i++) { + for (var i = units.indexOf(minUnit); i < numUnits; i++) { unit = units[i]; var unitDetails = time[unit]; - var fits = false; - - if (unitDetails.steps) { - fits = Math.ceil((max - min) / (unitDetails.steps[unitDetails.steps.length - 1] * unitDetails.size)) <= maxTicks; - } else if (unitDetails.maxStep) { - fits = Math.ceil((max - min) / (unitDetails.maxStep * unitDetails.size)) <= maxTicks; - } else { - // No limit on the multiplier so it always fits - fits = true; - } - - if (fits) { + var steps = (unitDetails.steps && unitDetails.steps[unitDetails.steps.length - 1]) || unitDetails.maxStep; + if (steps === undefined || Math.ceil((max - min) / (steps * unitDetails.size)) <= maxTicks) { break; } } @@ -148,12 +145,13 @@ module.exports = function(Chart) { var maxTicks = 11; // eventually configure this var unitDefinition = time[unit]; var unitSizeInMilliSeconds = unitDefinition.size; - var sizeInUnits = Math.ceil((max - min) % unitSizeInMilliSeconds); + var sizeInUnits = Math.ceil((max - min) / unitSizeInMilliSeconds); var multiplier = 1; if (unitDefinition.steps) { // Have an array of steps - for (var i = 0; i < unitDefinition.steps.length && sizeInUnits > maxTicks; i++) { + var numSteps = unitDefinition.steps.length; + for (var i = 0; i < numSteps && sizeInUnits > maxTicks; i++) { multiplier = unitDefinition.steps[i]; sizeInUnits = Math.ceil((max - min) / (unitSizeInMilliSeconds * multiplier)); } @@ -177,8 +175,23 @@ module.exports = function(Chart) { var ticks = []; var spacing = generationOptions.stepSize; var baseSpacing = generationOptions.baseSize; - var niceMin = Math.floor(dataRange.min / baseSpacing) * baseSpacing; - var niceMax = Math.ceil(dataRange.max / baseSpacing) * baseSpacing; + + var niceMin; + var niceMax; + var isoWeekday = generationOptions.isoWeekday; + if (generationOptions.unit === 'week' && isoWeekday !== false) { + niceMin = moment(dataRange.min).startOf('isoWeek').isoWeekday(isoWeekday).valueOf(); + niceMax = moment(dataRange.max).startOf('isoWeek').isoWeekday(isoWeekday).valueOf(); + if (dataRange.max - niceMax > 0) { + niceMax += baseSpacing; + } + } else { + niceMin = moment(dataRange.min).startOf(generationOptions.unit).valueOf(); + niceMax = moment(dataRange.max).startOf(generationOptions.unit).valueOf(); + if (dataRange.max - niceMax > 0) { + niceMax += baseSpacing; + } + } // If min, max and stepSize is set and they make an evenly spaced scale use it. if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { @@ -231,56 +244,50 @@ module.exports = function(Chart) { var timestamp; - if (chartData.labels && chartData.labels.length) { - for (var i = 0; i < chartData.labels.length; i++) { - var labelMoment = parseTime(me, chartData.labels[i]); + helpers.each(chartData.labels, function(label, labelIndex) { + var labelMoment = parseTime(me, label); - if (labelMoment.isValid()) { - // We need to round the time - if (timeOpts.round) { - labelMoment.startOf(timeOpts.round); - } + if (labelMoment.isValid()) { + // We need to round the time + if (timeOpts.round) { + labelMoment.startOf(timeOpts.round); + } - timestamp = labelMoment.valueOf(); - dataMin = Math.min(timestamp, dataMin); - dataMax = Math.max(timestamp, dataMax); + timestamp = labelMoment.valueOf(); + dataMin = Math.min(timestamp, dataMin); + dataMax = Math.max(timestamp, dataMax); - // Store this value for later - parsedData.labels[i] = timestamp; - } + // Store this value for later + parsedData.labels[labelIndex] = timestamp; } - } + }); + + helpers.each(chartData.datasets, function(dataset, 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 + helpers.each(dataset.data, function(value, dataIndex) { + var dataMoment = parseTime(me, me.getRightValue(value)); - var datasets = chartData.datasets; - for (var datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { - var dataset = datasets[datasetIndex]; - if (me.chart.isDatasetVisible(datasetIndex) && dataset.data.length) { - var timestamps = []; - - if (typeof dataset.data[0] === 'object' && dataset.data[0] !== null) { - // We have potential point data, so we need to parse this - for (var dataIndex = 0; dataIndex < dataset.data.length; dataIndex++) { - var dataMoment = parseTime(me, me.getRightValue(dataset.data[dataIndex])); - - if (dataMoment.isValid()) { - if (timeOpts.round) { - dataMoment.startOf(timeOpts.round); - } - - timestamp = dataMoment.valueOf(); - dataMin = Math.min(timestamp, dataMin); - dataMax = Math.max(timestamp, dataMax); - timestamps[dataIndex] = timestamp; + if (dataMoment.isValid()) { + if (timeOpts.round) { + dataMoment.startOf(timeOpts.round); } - } - } else { - // We have no x coordinates, so use the ones from the labels - timestamps = parsedData.labels.slice(); - } - parsedData.datasets[datasetIndex] = timestamps; + timestamp = dataMoment.valueOf(); + dataMin = Math.min(timestamp, dataMin); + dataMax = Math.max(timestamp, dataMax); + timestamps[dataIndex] = timestamp; + } + }); + } else { + // We have no x coordinates, so use the ones from the labels + timestamps = parsedData.labels.slice(); } - } + + parsedData.datasets[datasetIndex] = timestamps; + }); me.dataMin = dataMin; me.dataMax = dataMax; @@ -312,7 +319,7 @@ module.exports = function(Chart) { unit = timeOpts.unit; } else { // Auto Determine Unit - unit = determineUnit(minTimestamp || dataMin, maxTimestamp || dataMax); + unit = determineUnit(timeOpts.minUnit, minTimestamp || dataMin, maxTimestamp || dataMax); } me.displayFormat = timeOpts.displayFormats[unit]; @@ -329,7 +336,9 @@ module.exports = function(Chart) { min: minTimestamp, max: maxTimestamp, stepSize: stepSize, - baseSize: time[unit].size + unit: unit, + baseSize: time[unit].size, + isoWeekday: timeOpts.isoWeekday }; var ticks = me.ticks = Chart.Ticks.generators.time(timeGeneratorOptions, { min: dataMin, @@ -371,11 +380,24 @@ module.exports = function(Chart) { }, convertTicksToLabels: function() { var me = this; - me._ticksAsTimestamps = me.ticks; + 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; + var decimal = epochWidth ? (offset - me.min) / epochWidth : 0; + + if (me.isHorizontal()) { + var valueOffset = (me.width * decimal); + return me.left + Math.round(valueOffset); + } + + var heightOffset = (me.height * decimal); + return me.top + Math.round(heightOffset); + }, getPixelForValue: function(value, index, datasetIndex) { var me = this; var offset = null; @@ -395,19 +417,11 @@ module.exports = function(Chart) { } if (offset !== null) { - var decimal = (offset - me.min) / (me.max - me.min); - - if (me.isHorizontal()) { - var valueOffset = (me.width * decimal); - return me.left + Math.round(valueOffset); - } - - var heightOffset = (me.height * decimal); - return me.top + Math.round(heightOffset); + return me.getPixelForOffset(offset); } }, getPixelForTick: function(index) { - return this.getPixelForValue(this._ticksAsTimestamps[index]); + return this.getPixelForOffset(this.ticksAsTimestamps[index]); }, getValueForPixel: function(pixel) { var me = this; diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 1aa3fa2bf2f..ab719fd5836 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -116,7 +116,7 @@ describe('Time scale tests', function() { scale.update(400, 50); // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015']); + expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); }); it('should build ticks using date objects', function() { @@ -144,7 +144,7 @@ describe('Time scale tests', function() { scale.update(400, 50); // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015']); + expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); }); it('should build ticks when the data is xy points', function() { @@ -200,7 +200,7 @@ describe('Time scale tests', function() { // Counts down because the lines are drawn top to bottom var xScale = chart.scales.xScale0; - expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 9, 2015', 'Jan 11, 2015']); + expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); }); it('should allow custom time parsers', function() { @@ -335,6 +335,7 @@ describe('Time scale tests', function() { var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); config.time.min = '2015-01-01T04:00:00'; config.time.max = '2015-01-05T06:00:00'; + config.time.unit = 'day'; var Constructor = Chart.scaleService.getScaleConstructor('time'); var scale = new Constructor({ ctx: mockContext, @@ -346,7 +347,8 @@ describe('Time scale tests', function() { }); scale.update(400, 50); - expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 5, 2015']); + expect(scale.ticks[0]).toEqual('Jan 1, 2015'); + expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 5, 2015'); }); it('Should use the isoWeekday option', function() { @@ -408,16 +410,34 @@ describe('Time scale tests', function() { var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(71); - expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(452); - expect(xScale.getPixelForValue('2015-01-01T20:00:00')).toBeCloseToPixel(71); + expect(xScale.getValueForPixel(xScale.left)).toBeCloseToTime({ + value: moment('2015-01-01'), + unit: 'hour', + threshold: 0.01 + }); + expect(xScale.getValueForPixel(xScale.right)).toBeCloseToTime({ + value: moment('2015-01-11'), + unit: 'hour', + threshold: 0.01 + }); + + var timeRange = moment('2015-01-11').valueOf() - moment('2015-01-01').valueOf(); + var msInHour = 3600000; + var firstLabelAlong = 20 * msInHour / timeRange; + var firstLabelPixel = xScale.left + (xScale.width * firstLabelAlong); + var lastLabelAlong = (timeRange - (12 * msInHour)) / timeRange; + var lastLabelPixel = xScale.left + (xScale.width * lastLabelAlong); + + expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(firstLabelPixel); + expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(lastLabelPixel); + expect(xScale.getPixelForValue(chart.data.labels[0])).toBeCloseToPixel(firstLabelPixel); - expect(xScale.getValueForPixel(71)).toBeCloseToTime({ + expect(xScale.getValueForPixel(firstLabelPixel)).toBeCloseToTime({ value: moment(chart.data.labels[0]), unit: 'hour', threshold: 0.75 }); - expect(xScale.getValueForPixel(452)).toBeCloseToTime({ + expect(xScale.getValueForPixel(lastLabelPixel)).toBeCloseToTime({ value: moment(chart.data.labels[6]), unit: 'hour' }); From 191fbf06d22df0720a049befa3fb9babc735314f Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Sat, 21 Jan 2017 02:24:00 +0000 Subject: [PATCH 4/6] Tidy tests --- test/specs/scale.time.tests.js | 346 ++++++++++++--------------------- 1 file changed, 129 insertions(+), 217 deletions(-) diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index ab719fd5836..bf281c2e9ff 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -1,5 +1,22 @@ // Time scale tests describe('Time scale tests', function() { + function createScale(data, options) { + var scaleID = 'myScale'; + var mockContext = window.createMockContext(); + var Constructor = Chart.scaleService.getScaleConstructor('time'); + var scale = new Constructor({ + ctx: mockContext, + options: options, + chart: { + data: data + }, + id: scaleID + }); + + scale.update(400, 50); + return scale; + } + beforeEach(function() { // Need a time matcher for getValueFromPixel jasmine.addMatchers({ @@ -9,7 +26,7 @@ describe('Time scale tests', function() { var result = false; var diff = actual.diff(expected.value, expected.unit, true); - result = Math.abs(diff) < (expected.threshold !== undefined ? expected.threshold : 0.5); + result = Math.abs(diff) < (expected.threshold !== undefined ? expected.threshold : 0.01); return { pass: result @@ -94,113 +111,78 @@ describe('Time scale tests', function() { expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); - it('should build ticks using days', function() { - var scaleID = 'myScale'; - - var mockData = { - 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 - }; - - var mockContext = window.createMockContext(); - var Constructor = Chart.scaleService.getScaleConstructor('time'); - var scale = new Constructor({ - ctx: mockContext, - options: Chart.scaleService.getScaleDefaults('time'), // use default config for scale - chart: { - data: mockData - }, - id: scaleID - }); - - // scale.buildTicks(); - scale.update(400, 50); - - // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); - }); - - it('should build ticks using date objects', 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(); } - var scaleID = 'myScale'; - var mockData = { - labels: [newDateFromRef(0), newDateFromRef(1), newDateFromRef(2), newDateFromRef(4), newDateFromRef(6), newDateFromRef(7), newDateFromRef(9)], // days - }; + it('should accept labels as strings', function() { + var mockData = { + 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 + }; - var mockContext = window.createMockContext(); - var Constructor = Chart.scaleService.getScaleConstructor('time'); - var scale = new Constructor({ - ctx: mockContext, - options: Chart.scaleService.getScaleDefaults('time'), // use default config for scale - chart: { - data: mockData - }, - id: scaleID - }); + var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('time')); - scale.update(400, 50); + // Counts down because the lines are drawn top to bottom + expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); + }); - // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 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('time')); - it('should build ticks when the data is xy points', 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(); - } + // Counts down because the lines are drawn top to bottom + expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); + }); - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - xAxisID: 'xScale0', - yAxisID: 'yScale0', - 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: 'time', - position: 'bottom' + 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 + }] }], - yAxes: [{ - id: 'yScale0', - type: 'linear' - }] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } } - } - }); + }); - // Counts down because the lines are drawn top to bottom - var xScale = chart.scales.xScale0; - expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); + // Counts down because the lines are drawn top to bottom + var xScale = chart.scales.xScale0; + expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); + }); }); it('should allow custom time parsers', function() { @@ -209,7 +191,6 @@ describe('Time scale tests', function() { data: { datasets: [{ xAxisID: 'xScale0', - yAxisID: 'yScale0', data: [{ x: 375068900, y: 1 @@ -230,10 +211,6 @@ describe('Time scale tests', function() { } } }], - yAxes: [{ - id: 'yScale0', - type: 'linear' - }] } } }); @@ -247,113 +224,71 @@ describe('Time scale tests', function() { }); it('should build ticks using the config unit', function() { - var scaleID = 'myScale'; - var mockData = { labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days }; - var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); config.time.unit = 'hour'; - var Constructor = Chart.scaleService.getScaleConstructor('time'); - var scale = new Constructor({ - ctx: mockContext, - options: config, // use default config for scale - chart: { - data: mockData - }, - id: scaleID - }); - // scale.buildTicks(); - scale.update(400, 50); + var scale = createScale(mockData, config); expect(scale.ticks).toEqual(['Jan 1, 8PM', 'Jan 1, 9PM', 'Jan 1, 10PM', 'Jan 1, 11PM', 'Jan 2, 12AM', 'Jan 2, 1AM', 'Jan 2, 2AM', 'Jan 2, 3AM', 'Jan 2, 4AM', 'Jan 2, 5AM', 'Jan 2, 6AM', 'Jan 2, 7AM', 'Jan 2, 8AM', 'Jan 2, 9AM', 'Jan 2, 10AM', 'Jan 2, 11AM', 'Jan 2, 12PM', 'Jan 2, 1PM', 'Jan 2, 2PM', 'Jan 2, 3PM', 'Jan 2, 4PM', 'Jan 2, 5PM', 'Jan 2, 6PM', 'Jan 2, 7PM', 'Jan 2, 8PM', 'Jan 2, 9PM']); }); it('build ticks honoring the minUnit', function() { - var scaleID = 'myScale'; - var mockData = { labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days }; - var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); config.time.minUnit = 'day'; - var Constructor = Chart.scaleService.getScaleConstructor('time'); - var scale = new Constructor({ - ctx: mockContext, - options: config, // use default config for scale - chart: { - data: mockData - }, - id: scaleID - }); - // scale.buildTicks(); - scale.update(400, 50); + var scale = createScale(mockData, config); expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015']); }); it('should build ticks using the config diff', function() { - var scaleID = 'myScale'; - var mockData = { labels: ['2015-01-01T20:00:00', '2015-02-02T21:00:00', '2015-02-21T01:00:00'], // days }; - var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); config.time.unit = 'week'; config.time.round = 'week'; - var Constructor = Chart.scaleService.getScaleConstructor('time'); - var scale = new Constructor({ - ctx: mockContext, - options: config, // use default config for scale - chart: { - data: mockData - }, - id: scaleID - }); - // scale.buildTicks(); - scale.update(400, 50); + var scale = createScale(mockData, config); // last date is feb 15 because we round to start of week expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 1, 2015', 'Feb 8, 2015', 'Feb 15, 2015']); }); - it('Should use the min and max options', function() { - var scaleID = 'myScale'; - + describe('when specifying limits', function() { var mockData = { labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], // days }; - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); - config.time.min = '2015-01-01T04:00:00'; - config.time.max = '2015-01-05T06:00:00'; - config.time.unit = 'day'; - var Constructor = Chart.scaleService.getScaleConstructor('time'); - var scale = new Constructor({ - ctx: mockContext, - options: config, // use default config for scale - chart: { - data: mockData - }, - id: scaleID + var config; + beforeEach(function() { + config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); + config.time.unit = 'day'; }); - scale.update(400, 50); - expect(scale.ticks[0]).toEqual('Jan 1, 2015'); - expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 5, 2015'); + it('should use the min option', function() { + config.time.min = '2014-12-29T04:00:00'; + + var scale = createScale(mockData, config); + expect(scale.ticks[0]).toEqual('Dec 29, 2014'); + }); + + it('should use the max option', function() { + config.time.max = '2015-01-05T06:00:00'; + + var scale = createScale(mockData, config); + expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 5, 2015'); + }); }); it('Should use the isoWeekday option', function() { - var scaleID = 'myScale'; - var mockData = { labels: [ '2015-01-01T20:00:00', // Thursday @@ -362,32 +297,20 @@ describe('Time scale tests', function() { ] }; - var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); config.time.unit = 'week'; // Wednesday config.time.isoWeekday = 3; - var Constructor = Chart.scaleService.getScaleConstructor('time'); - var scale = new Constructor({ - ctx: mockContext, - options: config, // use default config for scale - chart: { - data: mockData - }, - id: scaleID - }); - - scale.update(400, 50); + var scale = createScale(mockData, config); expect(scale.ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']); }); - it('should get the correct pixel for a value', function() { + describe('when rendering several days', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'xScale0', - yAxisID: 'yScale0', data: [] }], 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 @@ -399,47 +322,43 @@ describe('Time scale tests', function() { type: 'time', position: 'bottom' }], - yAxes: [{ - id: 'yScale0', - type: 'linear', - position: 'left' - }] } } }); var xScale = chart.scales.xScale0; - expect(xScale.getValueForPixel(xScale.left)).toBeCloseToTime({ - value: moment('2015-01-01'), - unit: 'hour', - threshold: 0.01 + it('should be bounded by the nearest day beginnings', function() { + expect(xScale.getValueForPixel(xScale.left)).toBeCloseToTime({ + value: moment(chart.data.labels[0]).startOf('day'), + unit: 'hour', + }); + expect(xScale.getValueForPixel(xScale.right)).toBeCloseToTime({ + value: moment(chart.data.labels[chart.data.labels.length - 1]).endOf('day'), + unit: 'hour', + }); }); - expect(xScale.getValueForPixel(xScale.right)).toBeCloseToTime({ - value: moment('2015-01-11'), - unit: 'hour', - threshold: 0.01 - }); - - var timeRange = moment('2015-01-11').valueOf() - moment('2015-01-01').valueOf(); - var msInHour = 3600000; - var firstLabelAlong = 20 * msInHour / timeRange; - var firstLabelPixel = xScale.left + (xScale.width * firstLabelAlong); - var lastLabelAlong = (timeRange - (12 * msInHour)) / timeRange; - var lastLabelPixel = xScale.left + (xScale.width * lastLabelAlong); - expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(firstLabelPixel); - expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(lastLabelPixel); - expect(xScale.getPixelForValue(chart.data.labels[0])).toBeCloseToPixel(firstLabelPixel); - - expect(xScale.getValueForPixel(firstLabelPixel)).toBeCloseToTime({ - value: moment(chart.data.labels[0]), - unit: 'hour', - threshold: 0.75 - }); - expect(xScale.getValueForPixel(lastLabelPixel)).toBeCloseToTime({ - value: moment(chart.data.labels[6]), - unit: 'hour' + it('should convert between screen coordinates and times', function() { + var timeRange = moment('2015-01-11').valueOf() - moment('2015-01-01').valueOf(); + var msInHour = 3600000; + var firstLabelAlong = 20 * msInHour / timeRange; + var firstLabelPixel = xScale.left + (xScale.width * firstLabelAlong); + var lastLabelAlong = (timeRange - (12 * msInHour)) / timeRange; + var lastLabelPixel = xScale.left + (xScale.width * lastLabelAlong); + + expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(firstLabelPixel); + expect(xScale.getPixelForValue(chart.data.labels[0])).toBeCloseToPixel(firstLabelPixel); + expect(xScale.getValueForPixel(firstLabelPixel)).toBeCloseToTime({ + value: moment(chart.data.labels[0]), + unit: 'hour', + }); + + expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(lastLabelPixel); + expect(xScale.getValueForPixel(lastLabelPixel)).toBeCloseToTime({ + value: moment(chart.data.labels[6]), + unit: 'hour' + }); }); }); @@ -449,7 +368,6 @@ describe('Time scale tests', function() { data: { datasets: [{ xAxisID: 'xScale0', - yAxisID: 'yScale0', 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 @@ -461,11 +379,6 @@ describe('Time scale tests', function() { type: 'time', position: 'bottom' }], - yAxes: [{ - id: 'yScale0', - type: 'linear', - position: 'left' - }] } } }); @@ -504,7 +417,6 @@ describe('Time scale tests', function() { expect(xScale.getValueForPixel(62)).toBeCloseToTime({ value: moment(chart.data.labels[0]), unit: 'day', - threshold: 0.75 }); }); }); From d0aef0a4e6a4671f2c71f4af05c00dce87825615 Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Wed, 25 Jan 2017 15:42:21 +0000 Subject: [PATCH 5/6] Fix labels for non-linearly sized units Months, quarters and years have non-constant numbers of seconds. A scale that's linear WRT milliseconds produces incorrect tick labels due to the label formatting losing precision (eg year labels lose month and day so a label of 2016-12-32 displays as 2016 instead of 2017). --- src/scales/scale.time.js | 40 ++++++++------------------- test/specs/scale.time.tests.js | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index f30965d6516..ec0d2693bbd 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -138,7 +138,7 @@ module.exports = function(Chart) { * @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 in milliseconds + * @return {Number} the axis step size as a multiple of unit */ function determineStepSize(min, max, unit) { // Using our unit, figoure out what we need to scale as @@ -162,7 +162,7 @@ module.exports = function(Chart) { } } - return unitSizeInMilliSeconds * multiplier; + return multiplier; } /** @@ -173,47 +173,32 @@ module.exports = function(Chart) { */ Chart.Ticks.generators.time = function(generationOptions, dataRange) { var ticks = []; - var spacing = generationOptions.stepSize; - var baseSpacing = generationOptions.baseSize; + var stepSize = generationOptions.stepSize; var niceMin; var niceMax; var isoWeekday = generationOptions.isoWeekday; if (generationOptions.unit === 'week' && isoWeekday !== false) { niceMin = moment(dataRange.min).startOf('isoWeek').isoWeekday(isoWeekday).valueOf(); - niceMax = moment(dataRange.max).startOf('isoWeek').isoWeekday(isoWeekday).valueOf(); + niceMax = moment(dataRange.max).startOf('isoWeek').isoWeekday(isoWeekday); if (dataRange.max - niceMax > 0) { - niceMax += baseSpacing; + niceMax.add(1, 'week'); } + niceMax = niceMax.valueOf(); } else { niceMin = moment(dataRange.min).startOf(generationOptions.unit).valueOf(); - niceMax = moment(dataRange.max).startOf(generationOptions.unit).valueOf(); + niceMax = moment(dataRange.max).startOf(generationOptions.unit); if (dataRange.max - niceMax > 0) { - niceMax += baseSpacing; + niceMax.add(1, generationOptions.unit); } - } - - // If min, max and stepSize is set and they make an evenly spaced scale use it. - if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - var minMaxDeltaDivisableByStepSize = ((generationOptions.max - generationOptions.min) % generationOptions.stepSize) === 0; - if (minMaxDeltaDivisableByStepSize) { - niceMin = generationOptions.min; - niceMax = generationOptions.max; - } - } - - var numSpaces = (niceMax - niceMin) / spacing; - // If very close to our rounded value, use it. - if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { - numSpaces = Math.round(numSpaces); - } else { - numSpaces = Math.ceil(numSpaces); + niceMax = niceMax.valueOf(); } // Put the values into the ticks array ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - ticks.push(niceMin + (j * spacing)); + var cur = moment(niceMin); + while (cur.add(stepSize, generationOptions.unit).valueOf() < niceMax) { + ticks.push(cur.valueOf()); } ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); @@ -337,7 +322,6 @@ module.exports = function(Chart) { max: maxTimestamp, stepSize: stepSize, unit: unit, - baseSize: time[unit].size, isoWeekday: timeOpts.isoWeekday }; var ticks = me.ticks = Chart.Ticks.generators.time(timeGeneratorOptions, { diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index bf281c2e9ff..a96b9f486ff 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -362,6 +362,56 @@ describe('Time scale tests', function() { }); }); + describe('when rendering several years', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2005-07-04', '2017-01-20'], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + + it('should be bounded by nearest year starts', function() { + expect(xScale.getValueForPixel(xScale.left)).toBeCloseToTime({ + value: moment(chart.data.labels[0]).startOf('year'), + unit: 'hour', + }); + expect(xScale.getValueForPixel(xScale.right)).toBeCloseToTime({ + value: moment(chart.data.labels[chart.data.labels - 1]).endOf('year'), + unit: 'hour', + }); + }); + + it('should build the correct ticks', function() { + // Where 'correct' is a two year spacing, except the last tick which is the year end of the last point. + expect(xScale.ticks).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2018']); + }); + + it('should have ticks with accurate labels', function() { + var ticks = xScale.ticks; + var pixelsPerYear = xScale.width / 13; + + for (var i = 0; i < ticks.length - 1; i++) { + var offset = 2 * pixelsPerYear * i; + expect(xScale.getValueForPixel(xScale.left + offset)).toBeCloseToTime({ + value: moment(ticks[i] + '-01-01'), + unit: 'day', + threshold: 0.5, + }); + } + }); + }); + it('should get the correct label for a data value', function() { var chart = window.acquireChart({ type: 'line', From 9e65c41f2b8c0fa1490069df33f3ddaea1ccc36c Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Sat, 1 Apr 2017 18:10:56 +0100 Subject: [PATCH 6/6] Re-implement tick generation As in v2.5 --- src/scales/scale.time.js | 114 ++++++++++++++++++++------------- test/specs/scale.time.tests.js | 18 +++--- 2 files changed, 78 insertions(+), 54 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index ec0d2693bbd..567454d2951 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -7,7 +7,7 @@ moment = typeof(moment) === 'function' ? moment : window.moment; module.exports = function(Chart) { var helpers = Chart.helpers; - var time = { + var interval = { millisecond: { size: 1, steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] @@ -115,15 +115,14 @@ module.exports = function(Chart) { * @param max {Number} scale maximum * @return {String} the unit to use */ - function determineUnit(minUnit, min, max) { - var units = Object.keys(time); - var maxTicks = 11; + 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 = time[unit]; + 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; @@ -140,10 +139,9 @@ module.exports = function(Chart) { * @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) { + function determineStepSize(min, max, unit, maxTicks) { // Using our unit, figoure out what we need to scale as - var maxTicks = 11; // eventually configure this - var unitDefinition = time[unit]; + var unitDefinition = interval[unit]; var unitSizeInMilliSeconds = unitDefinition.size; var sizeInUnits = Math.ceil((max - min) / unitSizeInMilliSeconds); var multiplier = 1; @@ -166,19 +164,40 @@ module.exports = function(Chart) { } /** - * @function Chart.Ticks.generators.time - * @param generationOptions {ITimeGeneratorOptions} the options for generation + * 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 */ - Chart.Ticks.generators.time = function(generationOptions, dataRange) { + function generateTicks(options, dataRange, niceRange) { var ticks = []; - var stepSize = generationOptions.stepSize; + 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 = generationOptions.isoWeekday; - if (generationOptions.unit === 'week' && isoWeekday !== false) { + 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) { @@ -186,23 +205,17 @@ module.exports = function(Chart) { } niceMax = niceMax.valueOf(); } else { - niceMin = moment(dataRange.min).startOf(generationOptions.unit).valueOf(); - niceMax = moment(dataRange.max).startOf(generationOptions.unit); + niceMin = moment(dataRange.min).startOf(options.unit).valueOf(); + niceMax = moment(dataRange.max).startOf(options.unit); if (dataRange.max - niceMax > 0) { - niceMax.add(1, generationOptions.unit); + niceMax.add(1, options.unit); } niceMax = niceMax.valueOf(); } - - // Put the values into the ticks array - ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); - var cur = moment(niceMin); - while (cur.add(stepSize, generationOptions.unit).valueOf() < niceMax) { - ticks.push(cur.valueOf()); - } - ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); - - return ticks; + return generateTicks(options, dataRange, { + min: niceMin, + max: niceMax + }); }; var TimeScale = Chart.Scale.extend({ @@ -299,32 +312,19 @@ module.exports = function(Chart) { maxTimestamp = parseTime(me, timeOpts.max).valueOf(); } - var unit; - if (timeOpts.unit) { - unit = timeOpts.unit; - } else { - // Auto Determine Unit - unit = determineUnit(timeOpts.minUnit, minTimestamp || dataMin, maxTimestamp || dataMax); - } + var maxTicks = me.getLabelCapacity(minTimestamp || dataMin); + var unit = timeOpts.unit || determineUnit(timeOpts.minUnit, minTimestamp || dataMin, maxTimestamp || dataMax, maxTicks); me.displayFormat = timeOpts.displayFormats[unit]; - var stepSize; - if (timeOpts.stepSize) { - stepSize = timeOpts.stepSize; - } else { - // Auto determine step size - stepSize = determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit); - } - - var timeGeneratorOptions = { - maxTicks: 11, + var stepSize = timeOpts.stepSize || determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit, maxTicks); + var ticks = me.ticks = Chart.Ticks.generators.time({ + maxTicks: maxTicks, min: minTimestamp, max: maxTimestamp, stepSize: stepSize, unit: unit, isoWeekday: timeOpts.isoWeekday - }; - var ticks = me.ticks = Chart.Ticks.generators.time(timeGeneratorOptions, { + }, { min: dataMin, max: dataMax }); @@ -413,6 +413,28 @@ module.exports = function(Chart) { 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.getValueOrDefault(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, []); + 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); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index a96b9f486ff..076acfbf01a 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -119,12 +119,11 @@ describe('Time scale tests', function() { it('should accept labels as strings', function() { var mockData = { - 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 + 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('time')); - - // Counts down because the lines are drawn top to bottom + scale.update(1000, 200); expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); }); @@ -133,8 +132,7 @@ describe('Time scale tests', function() { labels: [newDateFromRef(0), newDateFromRef(1), newDateFromRef(2), newDateFromRef(4), newDateFromRef(6), newDateFromRef(7), newDateFromRef(9)], // days }; var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('time')); - - // Counts down because the lines are drawn top to bottom + scale.update(1000, 200); expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); }); @@ -179,8 +177,8 @@ describe('Time scale tests', function() { } }); - // Counts down because the lines are drawn top to bottom var xScale = chart.scales.xScale0; + xScale.update(800, 200); expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015', 'Jan 4, 2015', 'Jan 5, 2015', 'Jan 6, 2015', 'Jan 7, 2015', 'Jan 8, 2015', 'Jan 9, 2015', 'Jan 10, 2015', 'Jan 11, 2015']); }); }); @@ -232,6 +230,7 @@ describe('Time scale tests', function() { config.time.unit = 'hour'; var scale = createScale(mockData, config); + scale.update(2500, 200); expect(scale.ticks).toEqual(['Jan 1, 8PM', 'Jan 1, 9PM', 'Jan 1, 10PM', 'Jan 1, 11PM', 'Jan 2, 12AM', 'Jan 2, 1AM', 'Jan 2, 2AM', 'Jan 2, 3AM', 'Jan 2, 4AM', 'Jan 2, 5AM', 'Jan 2, 6AM', 'Jan 2, 7AM', 'Jan 2, 8AM', 'Jan 2, 9AM', 'Jan 2, 10AM', 'Jan 2, 11AM', 'Jan 2, 12PM', 'Jan 2, 1PM', 'Jan 2, 2PM', 'Jan 2, 3PM', 'Jan 2, 4PM', 'Jan 2, 5PM', 'Jan 2, 6PM', 'Jan 2, 7PM', 'Jan 2, 8PM', 'Jan 2, 9PM']); }); @@ -257,6 +256,7 @@ describe('Time scale tests', function() { 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(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 1, 2015', 'Feb 8, 2015', 'Feb 15, 2015']); @@ -264,16 +264,16 @@ describe('Time scale tests', function() { describe('when specifying limits', function() { var mockData = { - labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], }; var config; beforeEach(function() { config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); - config.time.unit = 'day'; }); it('should use the min option', function() { + config.time.unit = 'day'; config.time.min = '2014-12-29T04:00:00'; var scale = createScale(mockData, config); @@ -281,6 +281,7 @@ describe('Time scale tests', function() { }); it('should use the max option', function() { + config.time.unit = 'day'; config.time.max = '2015-01-05T06:00:00'; var scale = createScale(mockData, config); @@ -380,6 +381,7 @@ describe('Time scale tests', function() { }); var xScale = chart.scales.xScale0; + xScale.update(800, 200); it('should be bounded by nearest year starts', function() { expect(xScale.getValueForPixel(xScale.left)).toBeCloseToTime({