From: Evert Timberg Date: Sat, 23 Jan 2016 17:44:55 +0000 (-0500) Subject: Add some tests for scales. Cleaned up some minor bugs in the time scale. Wrote better... X-Git-Tag: v2.0.0~60^2~7 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=c312835eb19db36eae92760c9592e49d91076493;p=thirdparty%2FChart.js.git Add some tests for scales. Cleaned up some minor bugs in the time scale. Wrote better helpers for `helpers.min` and `helpers.max` --- diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 7d677fd27..b25954777 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -265,10 +265,22 @@ return !isNaN(parseFloat(n)) && isFinite(n); }; helpers.max = function(array) { - return Math.max.apply(Math, array); + return array.reduce(function(max, value) { + if (!isNaN(value)) { + return Math.max(max, value); + } else { + return max; + } + }, Number.MIN_VALUE); }; helpers.min = function(array) { - return Math.min.apply(Math, array); + return array.reduce(function(min, value) { + if (!isNaN(value)) { + return Math.min(min, value); + } else { + return min; + } + }, Number.MAX_VALUE); }; helpers.sign = function(x) { if (Math.sign) { diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 3e0de4c1e..01393d76b 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -43,6 +43,7 @@ if (this.options.stacked) { var valuesPerType = {}; + var hasNegativeValues = false; helpers.each(this.chart.data.datasets, function(dataset) { if (valuesPerType[dataset.type] === undefined) { @@ -71,6 +72,7 @@ positiveValues[index] = 100; } else { if (value < 0) { + hasNegativeValues = true; negativeValues[index] += value; } else { positiveValues[index] += value; @@ -81,9 +83,9 @@ }, this); helpers.each(valuesPerType, function(valuesForType) { - var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); + var values = hasNegativeValues ? valuesForType.positiveValues.concat(valuesForType.negativeValues) : valuesForType.positiveValues; var minVal = helpers.min(values); - var maxVal = helpers.max(values); + var maxVal = helpers.max(values) this.min = this.min === null ? minVal : Math.min(this.min, minVal); this.max = this.max === null ? maxVal : Math.max(this.max, maxVal); }, this); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 10b406c18..8e94d8eb5 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -172,7 +172,7 @@ } else { var innerWidth = this.width - (this.paddingLeft + this.paddingRight); pixel = this.left + (innerWidth / range * (helpers.log10(newVal) - helpers.log10(this.start))); - return pixel + this.paddingLeft; + pixel += this.paddingLeft; } } else { // Bottom - top since pixels increase downard on a screen @@ -180,10 +180,11 @@ pixel = this.top + this.paddingTop; } else { var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - return (this.bottom - this.paddingBottom) - (innerHeight / range * (helpers.log10(newVal) - helpers.log10(this.start))); + pixel = (this.bottom - this.paddingBottom) - (innerHeight / range * (helpers.log10(newVal) - helpers.log10(this.start))); } } + return pixel; }, }); diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 7ecdfa215..beda63dae 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -185,9 +185,6 @@ getLabelForIndex: function(index, datasetIndex) { return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); }, - getCircumference: function() { - return ((Math.PI * 2) / this.getValueCount()); - }, fit: function() { /* * Right, this is really confusing and there is a lot of maths going on here diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index e70ece9ca..b2dffa762 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -86,17 +86,8 @@ scaleLabelMoments.push(labelMoment); }, this); - if (this.options.time.min) { - this.firstTick = this.parseTime(this.options.time.min); - } else { - this.firstTick = moment.min.call(this, scaleLabelMoments); - } - - if (this.options.time.max) { - this.lastTick = this.parseTime(this.options.time.max); - } else { - this.lastTick = moment.max.call(this, scaleLabelMoments); - } + this.firstTick = moment.min.call(this, scaleLabelMoments); + this.lastTick = moment.max.call(this, scaleLabelMoments); } else { this.firstTick = null; this.lastTick = null; @@ -125,6 +116,15 @@ this.labelMoments.push(momentsForDataset); }, this); + // Set these after we've done all the data + if (this.options.time.min) { + this.firstTick = this.parseTime(this.options.time.min); + } + + if (this.options.time.max) { + this.lastTick = this.parseTime(this.options.time.max); + } + // We will modify these, so clone for later this.firstTick = (this.firstTick || moment()).clone(); this.lastTick = (this.lastTick || moment()).clone(); @@ -141,7 +141,7 @@ this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit, true)); } else { // Determine the smallest needed unit of the time - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var innerWidth = this.isHorizontal() ? this.width - (this.paddingLeft + this.paddingRight) : this.height - (this.paddingTop + this.paddingBottom); var labelCapacity = innerWidth / (this.options.ticks.fontSize + 10); var buffer = this.options.time.round ? 0 : 2; @@ -156,7 +156,6 @@ // 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 - //var canScaleUnit = ; this.unitScale = 1; if (helpers.isArray(unitDefinition.steps) && Math.ceil(this.tickRange / labelCapacity) < helpers.max(unitDefinition.steps)) { @@ -185,8 +184,21 @@ } } - this.firstTick.startOf(this.tickUnit); - this.lastTick.endOf(this.tickUnit); + var roundedStart; + + // Only round the first tick if we have no hard minimum + if (!this.options.time.min) { + this.firstTick.startOf(this.tickUnit); + roundedStart = this.firstTick; + } else { + roundedStart = this.firstTick.clone().startOf(this.tickUnit); + } + + // Only round the last tick if we have no hard maximum + if (!this.options.time.max) { + this.lastTick.endOf(this.tickUnit); + } + this.smallestLabelSeparation = this.width; helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { @@ -200,18 +212,24 @@ this.displayFormat = this.options.time.displayFormat; } + // first tick. will have been rounded correctly if options.time.min is not specified + this.ticks.push(this.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 = 0; i <= this.tickRange; ++i) { + for (var i = 1; i < this.tickRange; ++i) { if (i % this.unitScale === 0) { - this.ticks.push(this.firstTick.clone().add(i, this.tickUnit)); - } else if (i === this.tickRange) { - // Expand out the last one if not an exact multiple - this.tickRange = Math.ceil(this.tickRange / this.unitScale) * this.unitScale; - this.ticks.push(this.firstTick.clone().add(this.tickRange, this.tickUnit)); - this.lastTick = this.ticks[this.ticks.length - 1].clone(); - break; + this.ticks.push(roundedStart.clone().add(i, this.tickUnit)); } } + + // Always show the right tick + if (this.options.time.max) { + this.ticks.push(this.lastTick.clone()); + } else { + this.tickRange = Math.ceil(this.tickRange / this.unitScale) * this.unitScale; + this.ticks.push(this.firstTick.clone().add(this.tickRange, this.tickUnit)); + this.lastTick = this.ticks[this.ticks.length - 1].clone(); + } }, // Get tooltip label getLabelForIndex: function(index, datasetIndex) { diff --git a/test/scale.category.tests.js b/test/scale.category.tests.js index e844e9aee..935ed604e 100644 --- a/test/scale.category.tests.js +++ b/test/scale.category.tests.js @@ -78,6 +78,33 @@ describe('Category scale tests', function() { expect(scale.ticks).toEqual(mockData.labels); }); + it ('should get the correct label for the index', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + var Constructor = Chart.scaleService.getScaleConstructor('category'); + var scale = new Constructor({ + ctx: {}, + options: config, + chart: { + data: mockData + }, + id: scaleID + }); + + scale.buildTicks(); + + expect(scale.getLabelForIndex(1)).toBe('tick2'); + }); + it ('Should get the correct pixel for a value when horizontal', function() { var scaleID = 'myScale'; diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 9f093ce68..e21794fc8 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -169,6 +169,48 @@ describe('Linear Scale', function() { expect(scale.max).toBe(80); }); + it('Should correctly determine the max & min data values ignoring data that is NaN', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [null, 90, NaN, undefined, 45, 30] + }] + }; + + var options = Chart.scaleService.getScaleDefaults('linear'); + var Constructor = Chart.scaleService.getScaleConstructor('linear'); + var scale = new Constructor({ + ctx: {}, + options: options, // use default config for scale + chart: { + data: mockData + }, + id: scaleID + }); + + expect(scale).not.toEqual(undefined); // must construct + expect(scale.min).toBe(undefined); // not yet set + expect(scale.max).toBe(undefined); + + // Set arbitrary width and height for now + scale.width = 50; + scale.height = 400; + + scale.determineDataLimits(); + scale.buildTicks(); + expect(scale.min).toBe(30); + expect(scale.max).toBe(90); + + // Scale is now stacked + options.stacked = true; + + scale.determineDataLimits(); + expect(scale.min).toBe(30); + expect(scale.max).toBe(90); + }); + it('Should correctly determine the max & min for scatter data', function() { var scaleID = 'myScale'; @@ -233,6 +275,50 @@ describe('Linear Scale', function() { expect(horizontalScale.max).toBe(100); }); + it('Should correctly get the label for the given index', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [{ + x: 10, + y: 100 + }, { + x: -10, + y: 0 + }, { + x: 0, + y: 0 + }, { + x: 99, + y: 7 + }] + }] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('linear')); + var Constructor = Chart.scaleService.getScaleConstructor('linear'); + var scale = new Constructor({ + ctx: {}, + options: config, + chart: { + data: mockData + }, + id: scaleID + }); + + // Set arbitrary width and height for now + scale.width = 50; + scale.height = 400; + + scale.determineDataLimits(); + scale.buildTicks(); + + expect(scale.getLabelForIndex(3, 0)).toBe(7) + }); + it('Should correctly determine the min and max data values when stacked mode is turned on', function() { var scaleID = 'myScale'; diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index fac226a53..b3bda00c7 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -159,6 +159,45 @@ describe('Logarithmic Scale tests', function() { expect(scale.max).toBe(5000); }); + it('Should correctly determine the max & min data values when there is NaN data', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [undefined, 10, null, 5, 5000, NaN, 78, 450] + }] + }; + + var mockContext = window.createMockContext(); + var options = Chart.scaleService.getScaleDefaults('logarithmic'); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: mockContext, + options: options, // use default config for scale + chart: { + data: mockData + }, + id: scaleID + }); + + expect(scale).not.toEqual(undefined); // must construct + expect(scale.min).toBe(undefined); // not yet set + expect(scale.max).toBe(undefined); + + scale.update(400, 400); + expect(scale.min).toBe(1); + expect(scale.max).toBe(5000); + + // Turn on stacked mode since it uses it's own + options.stacked = true; + + scale.update(400, 400); + expect(scale.min).toBe(1); + expect(scale.max).toBe(5000); + }); + + it('Should correctly determine the max & min for scatter data', function() { var scaleID = 'myScale'; @@ -495,6 +534,38 @@ describe('Logarithmic Scale tests', function() { expect(scale.ticks).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16']); }); + it('Should correctly get the correct label for a data item', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 5000, 78, 450] + }, { + yAxisID: 'second scale', + data: [1, 1000, 10, 100], + }, { + yAxisID: scaleID, + data: [150] + }] + }; + + var mockContext = window.createMockContext(); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: mockContext, + options: Chart.scaleService.getScaleDefaults('logarithmic'), // use default config for scale + chart: { + data: mockData, + }, + id: scaleID + }); + + scale.update(400, 400); + + expect(scale.getLabelForIndex(0, 2)).toBe(150); + }); + it('Should get the correct pixel value for a point', function() { var scaleID = 'myScale'; @@ -533,6 +604,7 @@ describe('Logarithmic Scale tests', function() { expect(verticalScale.getPixelForValue(80, 0, 0)).toBe(5); // top + paddingTop expect(verticalScale.getPixelForValue(1, 0, 0)).toBe(105); // bottom - paddingBottom expect(verticalScale.getPixelForValue(10, 0, 0)).toBeCloseTo(52.4, 1e-4); // halfway + expect(verticalScale.getPixelForValue(0, 0, 0)).toBe(5); // 0 is invalid. force it on top var horizontalConfig = Chart.helpers.clone(config); horizontalConfig.position = 'bottom'; @@ -560,5 +632,6 @@ describe('Logarithmic Scale tests', function() { expect(horizontalScale.getPixelForValue(80, 0, 0)).toBe(105); // right - paddingRight expect(horizontalScale.getPixelForValue(1, 0, 0)).toBe(5); // left + paddingLeft expect(horizontalScale.getPixelForValue(10, 0, 0)).toBeCloseTo(57.5, 1e-4); // halfway + expect(horizontalScale.getPixelForValue(0, 0, 0)).toBe(5); // 0 is invalid, put it on the left. }); }); \ No newline at end of file diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index 61c59021c..e9ae118b2 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -163,6 +163,33 @@ describe('Test the radial linear scale', function() { expect(scale.max).toBe(200); }); + it('Should correctly determine the max & min data values when there is NaN data', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [50, 60, NaN, 70, null, undefined] + }], + labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + }; + + var mockContext = window.createMockContext(); + var Constructor = Chart.scaleService.getScaleConstructor('radialLinear'); + var scale = new Constructor({ + ctx: mockContext, + options: Chart.scaleService.getScaleDefaults('radialLinear'), // use default config for scale + chart: { + data: mockData + }, + id: scaleID, + }); + + scale.update(200, 300); + expect(scale.min).toBe(50); + expect(scale.max).toBe(70); + }); + it('Should ensure that the scale has a max and min that are not equal', function() { var scaleID = 'myScale'; @@ -427,6 +454,38 @@ describe('Test the radial linear scale', function() { expect(scale.yCenter).toBe(155); }); + it('should correctly get the label for a given data index', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 0, 25, 78] + }], + labels: ['point1', 'point2', 'point3', 'point4', 'point5'] // used in radar charts which use the same scales + }; + + var mockContext = window.createMockContext(); + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('radialLinear')); + var Constructor = Chart.scaleService.getScaleConstructor('radialLinear'); + var scale = new Constructor({ + ctx: mockContext, + options: config, + chart: { + data: mockData + }, + id: scaleID, + }); + + scale.left = 10; + scale.right = 210; + scale.top = 5; + scale.bottom = 305; + scale.update(200, 300); + + expect(scale.getLabelForIndex(1, 0)).toBe(5); + }); + it('should get the correct distance from the center point', function() { var scaleID = 'myScale'; diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index ed3810dc8..6b16ecd63 100644 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -125,6 +125,57 @@ describe('Time scale tests', function() { expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 9, 2015', 'Jan 11, 2015', 'Jan 13, 2015']); }); + 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(); + } + + var scaleID = 'myScale'; + var mockData = { + datasets: [{ + 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 + }], // 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.update(400, 50); + + // Counts down because the lines are drawn top to bottom + expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 9, 2015', 'Jan 11, 2015', 'Jan 13, 2015']); + }); + it('should build ticks using the config unit', function() { var scaleID = 'myScale'; @@ -133,7 +184,7 @@ describe('Time scale tests', function() { }; var mockContext = window.createMockContext(); - var config = Chart.scaleService.getScaleDefaults('time'); + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); config.time.unit = 'hour'; var Constructor = Chart.scaleService.getScaleConstructor('time'); var scale = new Constructor({ @@ -158,7 +209,7 @@ describe('Time scale tests', function() { }; var mockContext = window.createMockContext(); - var config = Chart.scaleService.getScaleDefaults('time'); + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); config.time.unit = 'week'; config.time.round = 'week'; var Constructor = Chart.scaleService.getScaleConstructor('time'); @@ -176,6 +227,31 @@ describe('Time scale tests', function() { 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'; + + 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" + 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); + expect(scale.ticks).toEqual(['Jan 1, 4AM', 'Jan 1, 4PM', 'Jan 2, 4AM', 'Jan 2, 4PM', 'Jan 3, 4AM', 'Jan 3, 4PM', 'Jan 4, 4AM', 'Jan 4, 4PM', 'Jan 5, 4AM', 'Jan 5, 6AM']); + }); + it('should get the correct pixel for a value', function() { var scaleID = 'myScale'; @@ -197,20 +273,19 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); scale.update(400, 50); expect(scale.width).toBe(400); - expect(scale.height).toBe(28); + expect(scale.height).toBe(50); scale.left = 0; scale.right = 400; scale.top = 10; scale.bottom = 38; - expect(scale.getPixelForValue('', 0, 0)).toBe(63); - expect(scale.getPixelForValue('', 6, 0)).toBe(342); + expect(scale.getPixelForValue('', 0, 0)).toBe(128); + expect(scale.getPixelForValue('', 6, 0)).toBe(380); - var verticalScaleConfig = Chart.scaleService.getScaleDefaults('time'); + var verticalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); verticalScaleConfig.position = "left"; var verticalScale = new Constructor({ @@ -229,7 +304,42 @@ describe('Time scale tests', function() { verticalScale.right = 50; verticalScale.bottom = 400; - expect(verticalScale.getPixelForValue('', 0, 0)).toBe(6); - expect(verticalScale.getPixelForValue('', 6, 0)).toBe(394); + expect(verticalScale.getPixelForValue('', 0, 0)).toBe(38); + expect(verticalScale.getPixelForValue('', 6, 0)).toBe(375); + }); + + it('should get the correct label for a data value', 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 + datasets: [{ + data: [], + }] + }; + + 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.update(400, 50); + + expect(scale.width).toBe(400); + expect(scale.height).toBe(50); + scale.left = 0; + scale.right = 400; + scale.top = 10; + scale.bottom = 38; + + expect(scale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); + expect(scale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); + }); });