From c312835eb19db36eae92760c9592e49d91076493 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 23 Jan 2016 12:44:55 -0500 Subject: [PATCH 1/8] Add some tests for scales. Cleaned up some minor bugs in the time scale. Wrote better helpers for `helpers.min` and `helpers.max` --- src/core/core.helpers.js | 16 +++- src/scales/scale.linear.js | 6 +- src/scales/scale.logarithmic.js | 5 +- src/scales/scale.radialLinear.js | 3 - src/scales/scale.time.js | 64 ++++++++++------ test/scale.category.tests.js | 27 +++++++ test/scale.linear.tests.js | 86 +++++++++++++++++++++ test/scale.logarithmic.tests.js | 73 ++++++++++++++++++ test/scale.radialLinear.tests.js | 59 ++++++++++++++ test/scale.time.tests.js | 128 ++++++++++++++++++++++++++++--- 10 files changed, 426 insertions(+), 41 deletions(-) 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'); + }); }); From 0ed39c9fd702a0f281206137b01131080be8712b Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 24 Jan 2016 09:21:10 -0500 Subject: [PATCH 2/8] Fix error in math helpers. --- src/core/core.helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index b25954777..65bdd1304 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -271,7 +271,7 @@ } else { return max; } - }, Number.MIN_VALUE); + }, Number.NEGATIVE_INFINITY); }; helpers.min = function(array) { return array.reduce(function(min, value) { @@ -280,7 +280,7 @@ } else { return min; } - }, Number.MAX_VALUE); + }, Number.POSITIVE_INFINITY); }; helpers.sign = function(x) { if (Math.sign) { From 68ab74a46d12b20e33820d72d7737ea19e5c4394 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 24 Jan 2016 09:22:28 -0500 Subject: [PATCH 3/8] Fix linear scale stacked mode --- src/scales/scale.linear.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 01393d76b..8c2778509 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -43,6 +43,7 @@ if (this.options.stacked) { var valuesPerType = {}; + var hasPositiveValues = false; var hasNegativeValues = false; helpers.each(this.chart.data.datasets, function(dataset) { @@ -75,6 +76,7 @@ hasNegativeValues = true; negativeValues[index] += value; } else { + hasPositiveValues = true; positiveValues[index] += value; } } @@ -83,7 +85,7 @@ }, this); helpers.each(valuesPerType, function(valuesForType) { - var values = hasNegativeValues ? valuesForType.positiveValues.concat(valuesForType.negativeValues) : valuesForType.positiveValues; + var values = hasPositiveValues ? hasNegativeValues ? valuesForType.positiveValues.concat(valuesForType.negativeValues) : valuesForType.positiveValues : valuesForType.negativeValues; var minVal = helpers.min(values); var maxVal = helpers.max(values) this.min = this.min === null ? minVal : Math.min(this.min, minVal); From d0b67c603b456698dbcbf75b211f580f9d10843e Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 24 Jan 2016 10:58:30 -0500 Subject: [PATCH 4/8] Line and bar test updates --- src/controllers/controller.line.js | 2 - test/controller.bar.tests.js | 146 +++++++++++ test/controller.line.tests.js | 375 +++++++++++++++++++++++++++++ 3 files changed, 521 insertions(+), 2 deletions(-) diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 4d2df3bb6..bef0cde44 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -223,8 +223,6 @@ } else { return yScale.getPixelForValue(sumPos + value); } - - return yScale.getPixelForValue(value); } return yScale.getPixelForValue(value); diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index 76634e941..64b4eb927 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -378,6 +378,152 @@ describe('Bar controller tests', function() { expect(bar2._model.y).toBe(37); }); + it('should update elements when the scales are stacked', function() { + var data = { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID', + bar: true + }, { + data: [10, 15, 0, -4], + label: 'dataset2', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID', + bar: true + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }; + var mockContext = window.createMockContext(); + + var VerticalScaleConstructor = Chart.scaleService.getScaleConstructor('linear'); + var verticalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('linear')); + verticalScaleConfig = Chart.helpers.scaleMerge(verticalScaleConfig, Chart.defaults.bar.scales.yAxes[0]); + verticalScaleConfig.stacked = true; + var yScale = new VerticalScaleConstructor({ + ctx: mockContext, + options: verticalScaleConfig, + chart: { + data: data + }, + id: 'firstYScaleID' + }); + + // Update ticks & set physical dimensions + var verticalSize = yScale.update(50, 200); + yScale.top = 0; + yScale.left = 0; + yScale.right = verticalSize.width; + yScale.bottom = verticalSize.height; + + var HorizontalScaleConstructor = Chart.scaleService.getScaleConstructor('category'); + var horizontalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + horizontalScaleConfig = Chart.helpers.scaleMerge(horizontalScaleConfig, Chart.defaults.bar.scales.xAxes[0]); + horizontalScaleConfig.stacked = true; + var xScale = new HorizontalScaleConstructor({ + ctx: mockContext, + options: horizontalScaleConfig, + chart: { + data: data + }, + id: 'firstXScaleID' + }); + + // Update ticks & set physical dimensions + var horizontalSize = xScale.update(200, 50); + xScale.left = yScale.right; + xScale.top = yScale.bottom; + xScale.right = horizontalSize.width + xScale.left; + xScale.bottom = horizontalSize.height + xScale.top; + + var chart = { + data: data, + config: { + type: 'bar' + }, + options: { + elements: { + rectangle: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2, + } + }, + scales: { + xAxes: [{ + id: 'firstXScaleID' + }], + yAxes: [{ + id: 'firstYScaleID' + }] + } + }, + scales: { + firstXScaleID: xScale, + firstYScaleID: yScale, + } + }; + + var controller0 = new Chart.controllers.bar(chart, 0); + var controller1 = new Chart.controllers.bar(chart, 1); + + controller0.buildOrUpdateElements(); + controller0.update(); + controller1.buildOrUpdateElements(); + controller1.update(); + + expect(chart.data.datasets[0].metaData[0]._model).toEqual(jasmine.objectContaining({ + x: 106, + y: 60, + base: 113, + width: 30.400000000000002 + })); + expect(chart.data.datasets[0].metaData[1]._model).toEqual(jasmine.objectContaining({ + x: 144, + y: 167, + base: 113, + width: 30.400000000000002 + })); + expect(chart.data.datasets[0].metaData[2]._model).toEqual(jasmine.objectContaining({ + x: 183, + y: 60, + base: 113, + width: 30.400000000000002 + })); + expect(chart.data.datasets[0].metaData[3]._model).toEqual(jasmine.objectContaining({ + x: 222, + y: 167, + base: 113, + width: 30.400000000000002 + })); + + expect(chart.data.datasets[1].metaData[0]._model).toEqual(jasmine.objectContaining({ + x: 106, + y: 6, + base: 60, + width: 30.400000000000002 + })); + expect(chart.data.datasets[1].metaData[1]._model).toEqual(jasmine.objectContaining({ + x: 144, + y: 33, + base: 113, + width: 30.400000000000002 + })); + expect(chart.data.datasets[1].metaData[2]._model).toEqual(jasmine.objectContaining({ + x: 183, + y: 60, + base: 60, + width: 30.400000000000002 + })); + expect(chart.data.datasets[1].metaData[3]._model).toEqual(jasmine.objectContaining({ + x: 222, + y: 189, + base: 167, + width: 30.400000000000002 + })); + }); + it ('should draw all bars', function() { var data = { datasets: [{}, { diff --git a/test/controller.line.tests.js b/test/controller.line.tests.js index 3cf921c14..40c7c2bb0 100644 --- a/test/controller.line.tests.js +++ b/test/controller.line.tests.js @@ -544,6 +544,381 @@ describe('Line controller tests', function() { }); }); + it('should update elements when the y scale is stacked', function() { + var data = { + datasets: [{ + data: [10, 15, -4, -4], + label: 'dataset2', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID', + type: 'line' + }, { + data: [20, 20, 30, -30], + label: 'dataset1', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID', + type: 'line' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }; + var mockContext = window.createMockContext(); + + var VerticalScaleConstructor = Chart.scaleService.getScaleConstructor('linear'); + var verticalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('linear')); + verticalScaleConfig = Chart.helpers.scaleMerge(verticalScaleConfig, Chart.defaults.line.scales.yAxes[0]); + verticalScaleConfig.stacked = true; + var yScale = new VerticalScaleConstructor({ + ctx: mockContext, + options: verticalScaleConfig, + chart: { + data: data + }, + id: 'firstYScaleID' + }); + + // Update ticks & set physical dimensions + var verticalSize = yScale.update(50, 200); + yScale.top = 0; + yScale.left = 0; + yScale.right = verticalSize.width; + yScale.bottom = verticalSize.height; + + var HorizontalScaleConstructor = Chart.scaleService.getScaleConstructor('category'); + var horizontalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + horizontalScaleConfig = Chart.helpers.scaleMerge(horizontalScaleConfig, Chart.defaults.line.scales.xAxes[0]); + var xScale = new HorizontalScaleConstructor({ + ctx: mockContext, + options: horizontalScaleConfig, + chart: { + data: data + }, + id: 'firstXScaleID' + }); + + // Update ticks & set physical dimensions + var horizontalSize = xScale.update(200, 50); + xScale.left = yScale.right; + xScale.top = yScale.bottom; + xScale.right = horizontalSize.width + xScale.left; + xScale.bottom = horizontalSize.height + xScale.top; + + + var chart = { + chartArea: { + bottom: 200, + left: xScale.left, + right: xScale.left + 200, + top: 0 + }, + data: data, + config: { + type: 'line' + }, + options: { + showLines: true, + elements: { + line: { + backgroundColor: 'rgb(255, 0, 0)', + borderCapStyle: 'round', + borderColor: 'rgb(0, 255, 0)', + borderDash: [], + borderDashOffset: 0.1, + borderJoinStyle: 'bevel', + borderWidth: 1.2, + fill: true, + tension: 0, + }, + point: { + backgroundColor: Chart.defaults.global.defaultColor, + borderWidth: 1, + borderColor: Chart.defaults.global.defaultColor, + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1, + radius: 3, + pointStyle: 'circle' + } + }, + scales: { + xAxes: [{ + id: 'firstXScaleID' + }], + yAxes: [{ + id: 'firstYScaleID' + }] + } + }, + scales: { + firstXScaleID: xScale, + firstYScaleID: yScale, + } + }; + + var controller = new Chart.controllers.line(chart, 0); + controller.update(); + + // Line element + expect(chart.data.datasets[0].metaDataset._model).toEqual(jasmine.objectContaining({ + scaleTop: 0, + scaleBottom: 200, + scaleZero: 100, + })); + + expect(chart.data.datasets[0].metaData[0]._model).toEqual(jasmine.objectContaining({ + // Point + x: 91, + y: 30, + })); + + expect(chart.data.datasets[0].metaData[1]._model).toEqual(jasmine.objectContaining({ + // Point + x: 141, + y: 18, + })); + + expect(chart.data.datasets[0].metaData[2]._model).toEqual(jasmine.objectContaining({ + // Point + x: 192, + y: 109, + })); + + expect(chart.data.datasets[0].metaData[3]._model).toEqual(jasmine.objectContaining({ + // Point + x: 242, + y: 180, + })); + }); + + it('should find the correct scale zero when the data is all positive', function() { + var data = { + datasets: [{ + data: [10, 15, 20, 20], + label: 'dataset2', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID', + type: 'line' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }; + var mockContext = window.createMockContext(); + + var VerticalScaleConstructor = Chart.scaleService.getScaleConstructor('linear'); + var verticalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('linear')); + verticalScaleConfig = Chart.helpers.scaleMerge(verticalScaleConfig, Chart.defaults.line.scales.yAxes[0]); + verticalScaleConfig.stacked = true; + var yScale = new VerticalScaleConstructor({ + ctx: mockContext, + options: verticalScaleConfig, + chart: { + data: data + }, + id: 'firstYScaleID' + }); + + // Update ticks & set physical dimensions + var verticalSize = yScale.update(50, 200); + yScale.top = 0; + yScale.left = 0; + yScale.right = verticalSize.width; + yScale.bottom = verticalSize.height; + + var HorizontalScaleConstructor = Chart.scaleService.getScaleConstructor('category'); + var horizontalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + horizontalScaleConfig = Chart.helpers.scaleMerge(horizontalScaleConfig, Chart.defaults.line.scales.xAxes[0]); + var xScale = new HorizontalScaleConstructor({ + ctx: mockContext, + options: horizontalScaleConfig, + chart: { + data: data + }, + id: 'firstXScaleID' + }); + + // Update ticks & set physical dimensions + var horizontalSize = xScale.update(200, 50); + xScale.left = yScale.right; + xScale.top = yScale.bottom; + xScale.right = horizontalSize.width + xScale.left; + xScale.bottom = horizontalSize.height + xScale.top; + + + var chart = { + chartArea: { + bottom: 200, + left: xScale.left, + right: xScale.left + 200, + top: 0 + }, + data: data, + config: { + type: 'line' + }, + options: { + showLines: true, + elements: { + line: { + backgroundColor: 'rgb(255, 0, 0)', + borderCapStyle: 'round', + borderColor: 'rgb(0, 255, 0)', + borderDash: [], + borderDashOffset: 0.1, + borderJoinStyle: 'bevel', + borderWidth: 1.2, + fill: true, + tension: 0, + }, + point: { + backgroundColor: Chart.defaults.global.defaultColor, + borderWidth: 1, + borderColor: Chart.defaults.global.defaultColor, + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1, + radius: 3, + pointStyle: 'circle' + } + }, + scales: { + xAxes: [{ + id: 'firstXScaleID' + }], + yAxes: [{ + id: 'firstYScaleID' + }] + } + }, + scales: { + firstXScaleID: xScale, + firstYScaleID: yScale, + } + }; + + var controller = new Chart.controllers.line(chart, 0); + controller.update(); + + // Line element + expect(chart.data.datasets[0].metaDataset._model).toEqual(jasmine.objectContaining({ + scaleTop: 0, + scaleBottom: 200, + scaleZero: 194, // yScale.min is the 0 point + })); + }); + + it('should find the correct scale zero when the data is all negative', function() { + var data = { + datasets: [{ + data: [-10, -15, -20, -20], + label: 'dataset2', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID', + type: 'line' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }; + var mockContext = window.createMockContext(); + + var VerticalScaleConstructor = Chart.scaleService.getScaleConstructor('linear'); + var verticalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('linear')); + verticalScaleConfig = Chart.helpers.scaleMerge(verticalScaleConfig, Chart.defaults.line.scales.yAxes[0]); + verticalScaleConfig.stacked = true; + var yScale = new VerticalScaleConstructor({ + ctx: mockContext, + options: verticalScaleConfig, + chart: { + data: data + }, + id: 'firstYScaleID' + }); + + // Update ticks & set physical dimensions + var verticalSize = yScale.update(50, 200); + yScale.top = 0; + yScale.left = 0; + yScale.right = verticalSize.width; + yScale.bottom = verticalSize.height; + + var HorizontalScaleConstructor = Chart.scaleService.getScaleConstructor('category'); + var horizontalScaleConfig = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); + horizontalScaleConfig = Chart.helpers.scaleMerge(horizontalScaleConfig, Chart.defaults.line.scales.xAxes[0]); + var xScale = new HorizontalScaleConstructor({ + ctx: mockContext, + options: horizontalScaleConfig, + chart: { + data: data + }, + id: 'firstXScaleID' + }); + + // Update ticks & set physical dimensions + var horizontalSize = xScale.update(200, 50); + xScale.left = yScale.right; + xScale.top = yScale.bottom; + xScale.right = horizontalSize.width + xScale.left; + xScale.bottom = horizontalSize.height + xScale.top; + + + var chart = { + chartArea: { + bottom: 200, + left: xScale.left, + right: xScale.left + 200, + top: 0 + }, + data: data, + config: { + type: 'line' + }, + options: { + showLines: true, + elements: { + line: { + backgroundColor: 'rgb(255, 0, 0)', + borderCapStyle: 'round', + borderColor: 'rgb(0, 255, 0)', + borderDash: [], + borderDashOffset: 0.1, + borderJoinStyle: 'bevel', + borderWidth: 1.2, + fill: true, + tension: 0, + }, + point: { + backgroundColor: Chart.defaults.global.defaultColor, + borderWidth: 1, + borderColor: Chart.defaults.global.defaultColor, + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1, + radius: 3, + pointStyle: 'circle' + } + }, + scales: { + xAxes: [{ + id: 'firstXScaleID' + }], + yAxes: [{ + id: 'firstYScaleID' + }] + } + }, + scales: { + firstXScaleID: xScale, + firstYScaleID: yScale, + } + }; + + var controller = new Chart.controllers.line(chart, 0); + controller.update(); + + // Line element + expect(chart.data.datasets[0].metaDataset._model).toEqual(jasmine.objectContaining({ + scaleTop: 0, + scaleBottom: 200, + scaleZero: 6, // yScale.max is the zero point + })); + }); + it ('should fall back to the line styles for points', function() { var data = { datasets: [{ From 6aa2933ec5555e803f676ee8d0e0ca09de1e7386 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 24 Jan 2016 10:59:19 -0500 Subject: [PATCH 5/8] Bubble controller tests --- samples/bubble.html | 2 +- src/charts/Chart.Bubble.js | 35 -- src/controllers/controller.bubble.js | 13 +- test/controller.bubble.tests.js | 836 +++++++++++++++++++++++++++ 4 files changed, 848 insertions(+), 38 deletions(-) create mode 100644 test/controller.bubble.tests.js diff --git a/samples/bubble.html b/samples/bubble.html index 5df2c9ba4..34f4acba9 100644 --- a/samples/bubble.html +++ b/samples/bubble.html @@ -2,7 +2,7 @@ - Bar Chart + Bubble Chart