});
return base;
},
- merge = helpers.merge = function(base, master) {
- //Merge properties in left object over to a shallow clone of object right.
- var args = Array.prototype.slice.call(arguments, 0);
- args.unshift({});
- return extend.apply(null, args);
- },
// Need a special merge function to chart configs since they are now grouped
configMerge = helpers.configMerge = function(_base) {
var base = clone(_base);
helpers.each(value, function(valueObj, index) {
if (index < baseArray.length) {
- baseArray[index] = helpers.configMerge(baseArray[index], valueObj);
+ if (typeof baseArray[index] == 'object' && baseArray[index] !== null && typeof valueObj == 'object' && valueObj !== null) {
+ // Two objects are coming together. Do a merge of them.
+ baseArray[index] = helpers.configMerge(baseArray[index], valueObj);
+ } else {
+ // Just overwrite in this case since there is nothing to merge
+ baseArray[index] = valueObj;
+ }
} else {
baseArray.push(valueObj); // nothing to merge
}
return base;
},
getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault = function(value, index, defaultValue) {
- if (!value) {
+ if (value === undefined || value === null) {
return defaultValue;
}
- if (helpers.isArray(value) && index < value.length) {
- return value[index];
+ if (helpers.isArray(value)) {
+ return index < value.length ? value[index] : defaultValue;
}
return value;
},
findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) {
// Default to start of the array
- if (!startIndex) {
+ if (startIndex === undefined || startIndex === null) {
startIndex = -1;
}
for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
},
findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) {
// Default to end of the array
- if (!startIndex) {
+ if (startIndex === undefined || startIndex === null) {
startIndex = arrayToSearch.length;
}
for (var i = startIndex - 1; i >= 0; i--) {
return Math.log(x) / Math.LN10;
}
},
- cap = helpers.cap = function(valueToCap, maxValue, minValue) {
- if (isNumber(maxValue)) {
- if (valueToCap > maxValue) {
- return maxValue;
- }
- } else if (isNumber(minValue)) {
- if (valueToCap < minValue) {
- return minValue;
- }
- }
- return valueToCap;
- },
getDecimalPlaces = helpers.getDecimalPlaces = function(num) {
if (num % 1 !== 0 && isNumber(num)) {
var s = num.toString();
},
nextItem = helpers.nextItem = function(collection, index, loop) {
if (loop) {
- return collection[index + 1] || collection[0];
+ return index >= collection.length - 1 ? collection[0] : collection[index + 1];
}
- return collection[index + 1] || collection[collection.length - 1];
+
+ return index >= collection.length - 1 ? collection[collection.length - 1] : collection[index + 1];
},
previousItem = helpers.previousItem = function(collection, index, loop) {
if (loop) {
- return collection[index - 1] || collection[collection.length - 1];
- }
- return collection[index - 1] || collection[0];
- },
- calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val) {
- return Math.floor(Math.log(val) / Math.LN10);
- },
- calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly) {
-
- //Set a minimum step of two - a point at the top of the graph, and a point at the base
- var minSteps = 2,
- maxSteps = Math.floor(drawingSize / (textSize * 1.5)),
- skipFitting = (minSteps >= maxSteps);
-
- var maxValue = max(valuesArray),
- minValue = min(valuesArray);
-
- // We need some degree of seperation here to calculate the scales if all the values are the same
- // Adding/minusing 0.5 will give us a range of 1.
- if (maxValue === minValue) {
- maxValue += 0.5;
- // So we don't end up with a graph with a negative start value if we've said always start from zero
- if (minValue >= 0.5 && !startFromZero) {
- minValue -= 0.5;
- } else {
- // Make up a whole number above the values
- maxValue += 0.5;
- }
- }
-
- var valueRange = Math.abs(maxValue - minValue),
- rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
- graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
- graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
- graphRange = graphMax - graphMin,
- stepValue = Math.pow(10, rangeOrderOfMagnitude),
- numberOfSteps = Math.round(graphRange / stepValue);
-
- //If we have more space on the graph we'll use it to give more definition to the data
- while ((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
- if (numberOfSteps > maxSteps) {
- stepValue *= 2;
- numberOfSteps = Math.round(graphRange / stepValue);
- // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
- if (numberOfSteps % 1 !== 0) {
- skipFitting = true;
- }
- }
- //We can fit in double the amount of scale points on the scale
- else {
- //If user has declared ints only, and the step value isn't a decimal
- if (integersOnly && rangeOrderOfMagnitude >= 0) {
- //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
- if (stepValue / 2 % 1 === 0) {
- stepValue /= 2;
- numberOfSteps = Math.round(graphRange / stepValue);
- }
- //If it would make it a float break out of the loop
- else {
- break;
- }
- }
- //If the scale doesn't have to be an int, make the scale more granular anyway.
- else {
- stepValue /= 2;
- numberOfSteps = Math.round(graphRange / stepValue);
- }
-
- }
+ return index <= 0 ? collection[collection.length - 1] : collection[index - 1];
}
-
- if (skipFitting) {
- numberOfSteps = minSteps;
- stepValue = graphRange / numberOfSteps;
- }
- return {
- steps: numberOfSteps,
- stepValue: stepValue,
- min: graphMin,
- max: graphMin + (numberOfSteps * stepValue)
- };
-
+ return index <= 0 ? collection[0] : collection[index - 1];
},
// Implementation of the nice number algorithm used in determining where axis labels will go
niceNum = helpers.niceNum = function(range, round) {
return tmpl(templateString, valuesObject);
},
/* jshint ignore:end */
- generateLabels = helpers.generateLabels = function(templateString, numberOfSteps, graphMin, stepValue) {
- var labelsArray = new Array(numberOfSteps);
- if (templateString) {
- each(labelsArray, function(val, index) {
- labelsArray[index] = template(templateString, {
- value: (graphMin + (stepValue * (index + 1)))
- });
- });
- }
- return labelsArray;
- },
//--Animation methods
//Easing functions adapted from Robert Penner's easing equations
//http://www.robertpenner.com/easing/
+describe('Core helper tests', function() {
+
+ var helpers;
+
+ beforeAll(function() {
+ helpers = window.Chart.helpers;
+ });
+
+ it('Should iterate over an array and pass the extra data to that function', function() {
+ var testData = [0, 9, "abc"];
+ var scope = {}; // fake out the scope and ensure that 'this' is the correct thing
+
+ helpers.each(testData, function(item, index) {
+ expect(item).not.toBe(undefined);
+ expect(index).not.toBe(undefined);
+
+ expect(testData[index]).toBe(item);
+ expect(this).toBe(scope);
+ }, scope);
+
+ // Reverse iteration
+ var iterated = [];
+ helpers.each(testData, function(item, index) {
+ expect(item).not.toBe(undefined);
+ expect(index).not.toBe(undefined);
+
+ expect(testData[index]).toBe(item);
+ expect(this).toBe(scope);
+
+ iterated.push(item);
+ }, scope, true);
+
+ expect(iterated.slice().reverse()).toEqual(testData);
+ });
+
+ it('Should iterate over properties in an object', function() {
+ var testData = {
+ myProp1: 'abc',
+ myProp2: 276,
+ myProp3: ['a', 'b']
+ };
+
+ helpers.each(testData, function(value, key) {
+ if (key === 'myProp1') {
+ expect(value).toBe('abc');
+ } else if (key === 'myProp2') {
+ expect(value).toBe(276);
+ } else if (key === 'myProp3') {
+ expect(value).toEqual(['a', 'b']);
+ } else {
+ expect(false).toBe(true);
+ }
+ });
+ });
+
+ it('should not error when iterating over a null object', function() {
+ expect(function() {
+ helpers.each(undefined);
+ }).not.toThrow();
+ });
+
+ it('Should clone an object', function() {
+ var testData = {
+ myProp1: 'abc',
+ myProp2: ['a', 'b'],
+ myProp3: {
+ myProp4: 5,
+ myProp5: [1, 2]
+ }
+ };
+
+ var clone = helpers.clone(testData);
+ expect(clone).toEqual(testData);
+ expect(clone).not.toBe(testData);
+
+ expect(clone.myProp2).not.toBe(testData.myProp2);
+ expect(clone.myProp3).not.toBe(testData.myProp3);
+ expect(clone.myProp3.myProp5).not.toBe(testData.myProp3.myProp5);
+ });
+
+ it('should extend an object', function() {
+ var original = {
+ myProp1: 'abc',
+ myProp2: 56
+ };
+
+ var extension = {
+ myProp3: [2, 5, 6],
+ myProp2: 0
+ };
+
+ helpers.extend(original, extension);
+
+ expect(original).toEqual({
+ myProp1: 'abc',
+ myProp2: 0,
+ myProp3: [2, 5, 6],
+ });
+ });
+
+ it('Should merge a normal config without scales', function() {
+ var baseConfig = {
+ valueProp: 5,
+ arrayProp: [1, 2, 3, 4, 5, 6],
+ objectProp: {
+ prop1: 'abc',
+ prop2: 56
+ }
+ };
+
+ var toMerge = {
+ valueProp2: null,
+ arrayProp: ['a', 'c'],
+ objectProp: {
+ prop1: 'c',
+ prop3: 'prop3'
+ }
+ };
+
+ var merged = helpers.configMerge(baseConfig, toMerge);
+ expect(merged).toEqual({
+ valueProp: 5,
+ valueProp2: null,
+ arrayProp: ['a', 'c', 3, 4, 5, 6],
+ objectProp: {
+ prop1: 'c',
+ prop2: 56,
+ prop3: 'prop3'
+ }
+ });
+ });
+
+ it('should merge arrays containing objects', function() {
+ var baseConfig = {
+ arrayProp: [{
+ prop1: 'abc',
+ prop2: 56
+ }],
+ };
+
+ var toMerge = {
+ arrayProp: [{
+ prop1: 'myProp1',
+ prop3: 'prop3'
+ }, 2, {
+ prop1: 'myProp1'
+ }],
+ };
+
+ var merged = helpers.configMerge(baseConfig, toMerge);
+ expect(merged).toEqual({
+ arrayProp: [{
+ prop1: 'myProp1',
+ prop2: 56,
+ prop3: 'prop3'
+ },
+ 2, {
+ prop1: 'myProp1'
+ }],
+ });
+ });
+
+ it ('Should merge scale configs', function() {
+ var baseConfig = {
+ scales: {
+ prop1: {
+ abc: 123,
+ def: '456'
+ },
+ prop2: 777,
+ yAxes: [{
+ type: 'linear',
+ }, {
+ type: 'log'
+ }]
+ }
+ };
+
+ var toMerge = {
+ scales: {
+ prop1: {
+ def: 'bbb',
+ ghi: 78
+ },
+ prop2: null,
+ yAxes: [{
+ type: 'linear',
+ axisProp: 456
+ }, {
+ // pulls in linear default config since axis type changes
+ type: 'linear',
+ position: 'right'
+ }, {
+ // Pulls in linear default config since axis not in base
+ type: 'linear'
+ }]
+ }
+ };
+
+ var merged = helpers.configMerge(baseConfig, toMerge);
+ expect(merged).toEqual({
+ scales: {
+ prop1: {
+ abc: 123,
+ def: 'bbb',
+ ghi: 78
+ },
+ prop2: null,
+ yAxes: [{
+ type: 'linear',
+ axisProp: 456
+ }, {
+ type: 'linear',
+ display: true,
+ position: "right",
+ gridLines: {
+ show: true,
+ color: "rgba(0, 0, 0, 0.1)",
+ lineWidth: 1,
+ drawOnChartArea: true,
+ drawTicks: true,
+ zeroLineWidth: 1,
+ zeroLineColor: "rgba(0,0,0,0.25)",
+ },
+ reverse: false,
+ beginAtZero: false,
+ override: null,
+ labels: {
+ show: true,
+ mirror: false,
+ padding: 10,
+ template: "<%=value.toLocaleString()%>",
+ fontSize: 12,
+ fontStyle: "normal",
+ fontColor: "#666",
+ fontFamily: "Helvetica Neue"
+ }
+ }, {
+ type: 'linear',
+ display: true,
+ position: "left",
+ gridLines: {
+ show: true,
+ color: "rgba(0, 0, 0, 0.1)",
+ lineWidth: 1,
+ drawOnChartArea: true,
+ drawTicks: true,
+ zeroLineWidth: 1,
+ zeroLineColor: "rgba(0,0,0,0.25)",
+ },
+ reverse: false,
+ beginAtZero: false,
+ override: null,
+ labels: {
+ show: true,
+ mirror: false,
+ padding: 10,
+ template: "<%=value.toLocaleString()%>",
+ fontSize: 12,
+ fontStyle: "normal",
+ fontColor: "#666",
+ fontFamily: "Helvetica Neue"
+ }
+ }]
+ }
+ });
+ });
+
+ it ('should get value or default', function() {
+ expect(helpers.getValueAtIndexOrDefault(98, 0, 56)).toBe(98);
+ expect(helpers.getValueAtIndexOrDefault(0, 0, 56)).toBe(0);
+ expect(helpers.getValueAtIndexOrDefault(undefined, undefined, 56)).toBe(56);
+ expect(helpers.getValueAtIndexOrDefault([1, 2, 3], 1, 100)).toBe(2);
+ expect(helpers.getValueAtIndexOrDefault([1, 2, 3], 3, 100)).toBe(100);
+ });
+
+ it ('should filter an array', function() {
+ var data = [-10, 0, 6, 0, 7];
+ var callback = function(item) { return item > 2};
+ expect(helpers.where(data, callback)).toEqual([6, 7]);
+ expect(helpers.findNextWhere(data, callback)).toEqual(6);
+ expect(helpers.findNextWhere(data, callback, 2)).toBe(7);
+ expect(helpers.findNextWhere(data, callback, 4)).toBe(undefined);
+ expect(helpers.findPreviousWhere(data, callback)).toBe(7);
+ expect(helpers.findPreviousWhere(data, callback, 3)).toBe(6);
+ expect(helpers.findPreviousWhere(data, callback, 0)).toBe(undefined);
+ });
+
+ it ('Should get the correct sign', function() {
+ expect(helpers.sign(0)).toBe(0);
+ expect(helpers.sign(10)).toBe(1);
+ expect(helpers.sign(-5)).toBe(-1);
+ });
+
+ it ('should do a log10 operation', function() {
+ expect(helpers.log10(0)).toBe(-Infinity);
+ expect(helpers.log10(1)).toBe(0);
+ expect(helpers.log10(1000)).toBe(3);
+ });
+
+ it ('Should generate ids', function() {
+ expect(helpers.uid()).toBe('chart-0');
+ expect(helpers.uid()).toBe('chart-1');
+ expect(helpers.uid()).toBe('chart-2');
+ expect(helpers.uid()).toBe('chart-3');
+ });
+
+ it ('should detect a number', function() {
+ expect(helpers.isNumber(123)).toBe(true);
+ expect(helpers.isNumber('123')).toBe(true);
+ expect(helpers.isNumber(null)).toBe(false);
+ expect(helpers.isNumber(NaN)).toBe(false);
+ expect(helpers.isNumber(undefined)).toBe(false);
+ expect(helpers.isNumber('cbc')).toBe(false);
+ });
+
+ it ('should convert between radians and degrees', function() {
+ expect(helpers.toRadians(180)).toBe(Math.PI);
+ expect(helpers.toRadians(90)).toBe(0.5 * Math.PI);
+ expect(helpers.toDegrees(Math.PI)).toBe(180);
+ expect(helpers.toDegrees(Math.PI * 3 /2)).toBe(270);
+ });
+
+ it ('should get an angle from a point', function() {
+ var center = {
+ x: 0,
+ y: 0
+ };
+
+ expect(helpers.getAngleFromPoint(center, {x: 0, y: 10})).toEqual({
+ angle: Math.PI / 2,
+ distance: 10,
+ });
+
+ expect(helpers.getAngleFromPoint(center, {x: Math.sqrt(2), y: Math.sqrt(2)})).toEqual({
+ angle: Math.PI / 4,
+ distance: 2
+ });
+
+ expect(helpers.getAngleFromPoint(center, {x: -1.0 * Math.sqrt(2), y: -1.0 * Math.sqrt(2)})).toEqual({
+ angle: Math.PI * 1.25,
+ distance: 2
+ });
+ });
+
+ it ('should spline curves', function() {
+ expect(helpers.splineCurve({x: 0, y: 0}, {x: 1, y: 1}, { x: 2, y: 0}, 0)).toEqual({
+ previous: {
+ x: 1,
+ y: 1,
+ },
+ next: {
+ x: 1,
+ y: 1,
+ }
+ });
+
+ expect(helpers.splineCurve({x: 0, y: 0}, {x: 1, y: 1}, { x: 2, y: 0}, 1)).toEqual({
+ previous: {
+ x: 0,
+ y: 1,
+ },
+ next: {
+ x: 2,
+ y: 1,
+ }
+ });
+ });
+
+ it ('should get the next or previous item in an array', function() {
+ var testData = [0, 1, 2];
+
+ expect(helpers.nextItem(testData, 0, false)).toEqual(1);
+ expect(helpers.nextItem(testData, 2, false)).toEqual(2);
+ expect(helpers.nextItem(testData, 2, true)).toEqual(0);
+ expect(helpers.nextItem(testData, 1, true)).toEqual(2);
+ expect(helpers.nextItem(testData, -1, false)).toEqual(0);
+
+ expect(helpers.previousItem(testData, 0, false)).toEqual(0);
+ expect(helpers.previousItem(testData, 0, true)).toEqual(2);
+ expect(helpers.previousItem(testData, 2, false)).toEqual(1);
+ expect(helpers.previousItem(testData, 1, true)).toEqual(0);
+
+ });
+});
\ No newline at end of file