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) {
if (this.options.stacked) {
var valuesPerType = {};
+ var hasNegativeValues = false;
helpers.each(this.chart.data.datasets, function(dataset) {
if (valuesPerType[dataset.type] === undefined) {
positiveValues[index] = 100;
} else {
if (value < 0) {
+ hasNegativeValues = true;
negativeValues[index] += value;
} else {
positiveValues[index] += value;
}, 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);
} 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
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;
},
});
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
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;
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();
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;
// 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)) {
}
}
- 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) {
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) {
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';
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';
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';
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';
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';
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';
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
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';
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';
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';
};
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({
};
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');
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';
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({
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');
+
});
});