File: ia\thematics\BreaksCalculator.js
/**
* Calculates breaks for various types of classification.
*
* @author J Clare
* @class ia.BreaksCalculator
* @constructor
* @param {Number[]} data The data array.
*/
ia.BreaksCalculator = function(data)
{
this._stats = new ia.Statistics();
this._data = new Array();
this._functions = new Object();
this.sdSize = 1;
this.errorMessage = "Legend error defaulting to equal interval.";
this.addFunction(ia.Thematic.CONTINUOUS, ia.Thematic.CONTINUOUS);
this.addFunction(ia.Thematic.EQUAL_INTERVAL, ia.Thematic.EQUAL_INTERVAL);
this.addFunction(ia.Thematic.QUANTILE, ia.Thematic.QUANTILE);
this.addFunction(ia.Thematic.NATURAL, ia.Thematic.NATURAL);
this.addFunction(ia.Thematic.STANDARD_DEVIATION, ia.Thematic.STANDARD_DEVIATION);
if (data !== undefined) this.setData(data);
};
/**
* The size of the interval to use for standard deviation calculators.
*
* @property sdSize
* @type Number
* @default 1
*/
ia.BreaksCalculator.prototype.sdSize;
/**
* The error message that appears when a calculator breaks and defaults to equal interval.
*
* @property errorMessage
* @type String
* @default "Legend error defaulting to equal interval."
*/
ia.BreaksCalculator.prototype.errorMessage;
/**
* Adds a function. The function receives the number of classes as
* a parameter and must return the calculated breaks.
*
* @method addFunction
* @param {String} classifierName The name of the classifier.
* @param {Function} fnc The function.
*/
ia.BreaksCalculator.prototype.addFunction = function(classifierName, fnc)
{
this._functions[classifierName] = fnc;
};
/**
* Gets the statistics.
*
* @method getStats
* @return {ia.Statistics} The statistics.
*/
ia.BreaksCalculator.prototype.getStats = function()
{
return this._stats;
};
/**
* Gets the data.
*
* @method getData
* @return {Number[]} The data.
*/
ia.BreaksCalculator.prototype.getData = function()
{
return this._data;
};
/**
* Sets the data.
*
* @method setData
* @param {Number[]} value The data.
*/
ia.BreaksCalculator.prototype.setData = function(value)
{
this._data = value;
this._stats.setData(value);
};
/**
* Classifies a set of data values by setting the value ranges
* in each class to be equal in size.
*
* @method getBreaks
* @param {Number} numberOfClasses The number of classes to use in the classification.
* @param {String} name The name of the classification to use.
* @return {Number[]} The class breaks as an array of size (numberOfClasses + 1).
* Returns null if unable to calculate the breaks.
* The lower and upper boundaries are included in the breaks.
*/
ia.BreaksCalculator.prototype.getBreaks = function(numberOfClasses, classificationType)
{
if (numberOfClasses === null) numberOfClasses = 5;
if (classificationType === null) classificationType = "equalInterval";
var breaks;
try
{
if (this._stats.range === 0 && classificationType !== 'customClassifier')
{
breaks = [this._stats.minValue];
}
else
{
var fnc = this._functions[classificationType];
if (fnc !== undefined)
{
if (fnc === ia.Thematic.CONTINUOUS) breaks = this.continuous(numberOfClasses);
else if (fnc === ia.Thematic.EQUAL_INTERVAL) breaks = this.equalIntervals(numberOfClasses);
else if (fnc === ia.Thematic.QUANTILE) breaks = this.quantiles(numberOfClasses);
else if (fnc === ia.Thematic.NATURAL) breaks = this.natural(numberOfClasses);
else if (fnc === ia.Thematic.STANDARD_DEVIATION) breaks = this.standardDeviation(numberOfClasses);
else breaks = fnc.call(null, numberOfClasses);
}
}
}
catch (error)
{
// Default to equal interval.
breaks = this.equalIntervals(numberOfClasses);
}
if (breaks)
{
// Final check is to remove any breaks that may have been repeated.
var unique = breaks.filter(function (itm,i,a) {return i === a.indexOf(itm);});
breaks = unique.concat();
}
return breaks;
};
/**
* Classifies a set of data values by continuous values.
*
* @method continuous
* @param {Number} numberOfClasses The number of classes to use in the classification
* which is irrelevant in this case.
* @return {Number[]} The array of continuous values.
*/
ia.BreaksCalculator.prototype.continuous = function(numberOfClasses)
{
if (numberOfClasses === null) numberOfClasses = 5;
if (this._stats.range > 0)
{
return this._stats.unique;
}
else
{
return null;
}
};
/**
* Classifies a set of data values by setting the value ranges
* in each class to be equal in size.
*
* @method equalIntervals
* @param {Number} numberOfClasses The number of classes to use in the classification.
* @return {Number[]} The class breaks as an array of size (numberOfClasses + 1).
* Returns null if unable to calculate the breaks.
* The lower and upper boundaries are included in the breaks.
*/
ia.BreaksCalculator.prototype.equalIntervals = function(numberOfClasses)
{
if (numberOfClasses === null) numberOfClasses = 5;
if (this._stats.range > 0)
{
var nBreaks = numberOfClasses + 1;
var breakArray = new Array(nBreaks);
var classSize = this._stats.range / numberOfClasses;
breakArray[0] = this._stats.minValue;
for (var i = 1; i < numberOfClasses; i++)
{
breakArray[i] = parseFloat(this._stats.minValue) + (i * classSize);
}
breakArray[nBreaks-1] = this._stats.maxValue;
return breakArray;
}
else
{
return null;
}
}
/**
* Classifies a set of data values into classes with an equal number of units in each class.
*
* @method quantiles
* @param {Number} numberOfClasses The number of classes to use in the classification.
* @return {Number[]} The class breaks as an array of size (numberOfClasses + 1).
* Returns null if unable to calculate the breaks.
* The lower and upper boundaries are included in the breaks.
*/
ia.BreaksCalculator.prototype.quantiles = function(numberOfClasses)
{
if (numberOfClasses === null) numberOfClasses = 5;
var invalidBreaks = false;
var breakArray = this._quantileFunc(numberOfClasses, this._stats.sorted);
var n = breakArray.length;
for (var j = 0; j < n-1; j++)
{
if (breakArray[j] >= breakArray[j+1])
{
invalidBreaks = true;
break;
}
}
while (invalidBreaks)
{
breakArray = this._quantileFunc(numberOfClasses, this._stats.unique);
n = breakArray.length;
if (n === 1) invalidBreaks = false;
else
{
for (var j = 0; j < n-1; j++)
{
if (breakArray[j] >= breakArray[j+1])
{
numberOfClasses--;
invalidBreaks = true;
break;
}
else invalidBreaks = false;
}
}
}
return breakArray;
};
/**
* Classifies a set of data values into classes with an equal number of units in each class.
*
* @method _quantileFunc
* @param {Number} numberOfClasses The number of classes to use in the classification.
* @param {Number[]} a The array of numbers to use - this._stats.sorted or this._stats.unique.
* @return {Number[]} The class breaks as an array of size (numberOfClasses + 1).
* Returns null if unable to calculate the breaks.
* The lower and upper boundaries are included in the breaks.
* @private
*/
ia.BreaksCalculator.prototype._quantileFunc = function(numberOfClasses, a)
{
if (this._stats.range > 0)
{
if (this._stats.unique.length < numberOfClasses)
numberOfClasses = this._stats.unique.length;
var nBreaks = numberOfClasses + 1;
var breakArray = new Array(nBreaks);
var percentileIncr = 1 / numberOfClasses;
breakArray[0] = this._stats.minValue;
for (var i = 1; i < numberOfClasses; i++)
{
var p = ia.round((percentileIncr*i), 2)
//var p = percentileIncr*i;
breakArray[i] = this._stats.getPercentile(p, a);
}
breakArray[nBreaks-1] = this._stats.maxValue;
return breakArray;
}
else
{
return null;
}
};
/**
* Classifies a set of data values into classes with a variable
* number of values in each class. Class breaks are placed where
* there are gaps between clusters of values using Jenks natural
* breaks classification. Uses the continuous array of the
* <code>data</code> object for optimization.
*
* <p>Further information on Jenks classification can be found in:
* Jenks, George F. 1967. "The Data Model Concept in Statistical Mapping",
* International Yearbook of Cartography 7: 186-190.</p>
*
* @method natural
* @param {Number} numberOfClasses The number of classes to use in the classification.
* @return {Number[]} The class breaks as an array of size (numberOfClasses + 1).
* Returns null if unable to calculate the breaks.
* The lower and upper boundaries are included in the breaks.
*/
ia.BreaksCalculator.prototype.natural = function(numberOfClasses)
{
if (numberOfClasses === null) numberOfClasses = 5;
if (this._stats.range > 0)
{
// Get the number of observations.
var n = this._stats.unique.length;
if (this._stats.unique.length < numberOfClasses)
numberOfClasses = this._stats.unique.length;
// Create two matrices.
// The first contains indexes to the continuous array, the second variances.
var indexMatrix = new Array(n+1);
var varianceMatrix = new Array(n+1);
// Build and initialise the matrices.
for (var i = 1; i <= n; i++)
{
indexMatrix[i] = new Array(numberOfClasses+1);
varianceMatrix[i] = new Array(numberOfClasses+1);
for (var j = 1; j <= numberOfClasses; j++)
{
// First set of breaks initialised with 0.
if (j === 1)
{
indexMatrix[i][j] = 1;
varianceMatrix[i][j] = 0;
}
else
{
// Rest of variance matrix initialised with infinity.
varianceMatrix[i][j] = Infinity;
}
}
}
var variance;
var noOfObservations;
var sum;
var mean;
var meanSquared;
var observationSquared;
var sumOfObservationsSquared;
var index;
var prevIndex;
var observation;
for (i = 2; i <= n; i++)
{
// Reset the this._stats for the next class.
variance = 0
noOfObservations = 0;
sum = 0;
mean = 0;
meanSquared = 0;
observationSquared = 0;
sumOfObservationsSquared = 0;
// Builds and tests variance for increasing class sizes on each loop.
for (var l = 1; l <= i; l++)
{
// Get the index.
index = i - l + 1;
// Get the observation.
observation = this._stats.unique[index-1];
// Increase observations.
noOfObservations++;
// Mean calculation.
sum += parseFloat(observation);
mean = sum / noOfObservations;
meanSquared = mean * mean;
// Calculate variance within this class.
observationSquared = observation * observation;
sumOfObservationsSquared += parseFloat(observationSquared);
variance = (sumOfObservationsSquared / noOfObservations) - meanSquared;
// Compare against the variance for the previous class.
prevIndex = index - 1;
if (prevIndex !== 0)
{
for (j = 2; j <= numberOfClasses; j++)
{
// Stick with the previous variance if this one is bigger.
if (varianceMatrix[i][j] >= (varianceMatrix[prevIndex][j-1] + variance))
{
indexMatrix[i][j] = index;
varianceMatrix[i][j] = varianceMatrix[prevIndex][j-1] + variance;
};
};
};
};
indexMatrix[i][1] = 1;
varianceMatrix[i][1] = variance;
};
// Get the indices of the natural breaks.
var k = n;
var indexList = new Array(numberOfClasses);
indexList[numberOfClasses - 1] = n - 1;
for (j = numberOfClasses; j >= 2; j--)
{
index = indexMatrix[k][j] - 2;
indexList[j-2] = index;
k = indexMatrix[k][j] - 1;
};
// Get the breaks using the indices list.
var breakArray = new Array();
breakArray[0] = this._stats.minValue;
for (i = 0; i <= (indexList.length - 2); i++)
{
var value = this._stats.unique[indexList[i]];
breakArray[breakArray.length] = value;
}
breakArray[breakArray.length] = this._stats.maxValue;
return breakArray;
}
else
{
return null;
}
};
/**
* Classifies a set of data values by finding the mean value,
* and then placing class breaks above and below the mean at
* intervals of generally 0.25, 0.5 or 1 standard deviation.
* Makes use of the <code>sdSize</code> property to
* determine the size of the standard deviation
*
* <p>The standard deviation classifier differs from other
* classifiers in that the number of standard deviations
* above and below the mean are specified rather than the
* number of classes.</p>
*
* @method standardDeviation
* @param {Number} numberOfStandardDeviations The number of standard deviations above and below the mean.
* @return {Number[]} The class breaks as an array of size (numberOfStandardDeviations + 1).
* The lower and upper boundaries are included in the breaks.
* Returns null if unable to calculate the breaks.
* @see #sdSize
*/
ia.BreaksCalculator.prototype.standardDeviation = function(numberOfClasses)
{
if (numberOfClasses === null) numberOfClasses = 5;
if (this._stats.range > 0)
{
// Multiply by 2 to get the actual number of classes.
var numberOfStandardDeviations = 4;
var breakValue;
var breakArray = new Array(numberOfStandardDeviations + 1);
var meanBreak = Math.ceil(breakArray.length/2);
var mean = this._stats.mean;
var sd = this._stats.standardDeviation * this.sdSize;
breakArray[0] = -Infinity;
for (var i = 1; i <= (numberOfStandardDeviations + 1); i++)
{
if (i === meanBreak) breakValue = mean; // Center break is set to mean value.
else
{
// Number of standard deviations above / below the mean.
var noOfSDsFromMean = i - meanBreak;
breakValue = mean + (sd * noOfSDsFromMean);
}
breakArray[i] = breakValue;
}
breakArray[breakArray.length] = Infinity;
return breakArray;
}
else
{
return null;
}
};