parent
c7b739a2ce
commit
82cf81699f
Can't render this file because it is too large.
|
|
Can't render this file because it is too large.
|
|
@ -0,0 +1,548 @@ |
|||||||
|
var PCA = (function () { |
||||||
|
/** |
||||||
|
* The first step is to subtract the mean and center data |
||||||
|
*
|
||||||
|
* @param {Array} matrix - data in an mXn matrix format |
||||||
|
* @returns
|
||||||
|
*/ |
||||||
|
function computeDeviationMatrix(matrix) { |
||||||
|
var unit = unitSquareMatrix(matrix.length); |
||||||
|
return subtract(matrix, scale(multiply(unit, matrix), 1 / matrix.length)); |
||||||
|
} |
||||||
|
/** |
||||||
|
* Computes variance from deviation |
||||||
|
*
|
||||||
|
* @param {Array} deviation - data minus mean as calculated from computeDeviationMatrix |
||||||
|
* @returns
|
||||||
|
*/ |
||||||
|
function computeDeviationScores(deviation) { |
||||||
|
var devSumOfSquares = multiply(transpose(deviation), deviation); |
||||||
|
return devSumOfSquares; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Calculates the var covar square matrix using either population or sample |
||||||
|
*
|
||||||
|
* @param {Array} devSumOfSquares
|
||||||
|
* @param {boolean} sample - true/false whether data is from sample or not |
||||||
|
* @returns
|
||||||
|
*/ |
||||||
|
function computeVarianceCovariance(devSumOfSquares, sample) { |
||||||
|
var varianceCovariance; |
||||||
|
if (sample) |
||||||
|
varianceCovariance = scale(devSumOfSquares, 1 / (devSumOfSquares.length - 1)); |
||||||
|
else |
||||||
|
varianceCovariance = scale(devSumOfSquares, 1 / (devSumOfSquares.length)); |
||||||
|
return varianceCovariance; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Matrix is the deviation sum of squares as computed earlier |
||||||
|
*
|
||||||
|
* @param {Array} matrix - output of computeDeviationScores |
||||||
|
* @returns
|
||||||
|
*/ |
||||||
|
function computeSVD(matrix) { |
||||||
|
var result = svd(matrix); |
||||||
|
var eigenvectors = result.U; |
||||||
|
var eigenvalues = result.S; |
||||||
|
var results = eigenvalues.map(function (value, i) { |
||||||
|
var obj = {}; |
||||||
|
obj.eigenvalue = value; |
||||||
|
obj.vector = eigenvectors.map(function (vector, j) { |
||||||
|
return -1 * vector[i]; //HACK prevent completely negative vectors
|
||||||
|
}); |
||||||
|
return obj; |
||||||
|
}); |
||||||
|
return results; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Get reduced dataset after removing some dimensions |
||||||
|
*
|
||||||
|
* @param {Array} data - initial matrix started out with |
||||||
|
* @param {rest} vectors - eigenvectors selected as part of process |
||||||
|
* @returns
|
||||||
|
*/ |
||||||
|
function computeAdjustedData(data, ...vectorObjs) { |
||||||
|
//FIXME no need to transpose vectors since they're already in row normal form
|
||||||
|
var vectors = vectorObjs.map(function(v){return v.vector}); |
||||||
|
var matrixMinusMean = computeDeviationMatrix(data); |
||||||
|
var adjustedData = multiply(vectors, transpose(matrixMinusMean)); |
||||||
|
var unit = unitSquareMatrix(data.length); |
||||||
|
var avgData = scale(multiply(unit, data), -1 / data.length); //NOTE get the averages to add back
|
||||||
|
|
||||||
|
var formattedAdjustData = formatData(adjustedData, 2); |
||||||
|
return { |
||||||
|
adjustedData: adjustedData, |
||||||
|
formattedAdjustedData: formattedAdjustData, |
||||||
|
avgData: avgData, |
||||||
|
selectedVectors: vectors |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get original data set from reduced data set (decompress) |
||||||
|
* @param {*} adjustedData = formatted or unformatted adjusted data |
||||||
|
* @param {*} vectors = selectedVectors |
||||||
|
* @param {*} avgData = avgData |
||||||
|
*/ |
||||||
|
function computeOriginalData(adjustedData, vectors, avgData) { |
||||||
|
var originalWithoutMean = transpose(multiply(transpose(vectors), adjustedData)); |
||||||
|
var originalWithMean = subtract(originalWithoutMean, avgData); |
||||||
|
var formattedData = formatData(originalWithMean, 2); |
||||||
|
return { |
||||||
|
originalData: originalWithMean, |
||||||
|
formattedOriginalData: formattedData |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get percentage explained, or loss |
||||||
|
* @param {*} vectors
|
||||||
|
* @param {*} selected
|
||||||
|
*/ |
||||||
|
function computePercentageExplained(vectors, ...selected) { |
||||||
|
var total = vectors.map(function (v) { |
||||||
|
return v.eigenvalue |
||||||
|
}).reduce(function (a, b) { |
||||||
|
return a + b; |
||||||
|
}); |
||||||
|
var explained = selected.map(function (v) { |
||||||
|
return v.eigenvalue |
||||||
|
}).reduce(function (a, b) { |
||||||
|
return a + b; |
||||||
|
}); |
||||||
|
return (explained / total); |
||||||
|
} |
||||||
|
|
||||||
|
function getEigenVectors(data) { |
||||||
|
return computeSVD(computeVarianceCovariance(computeDeviationScores(computeDeviationMatrix(data)), false)); |
||||||
|
} |
||||||
|
|
||||||
|
function analyseTopResult(data) { |
||||||
|
var eigenVectors = getEigenVectors(data); |
||||||
|
var sorted = eigenVectors.sort(function (a, b) { |
||||||
|
return b.eigenvalue - a.eigenvalue; |
||||||
|
}); |
||||||
|
console.log('Sorted Vectors', sorted); |
||||||
|
var selected = sorted[0].vector; |
||||||
|
return computeAdjustedData(data, selected); |
||||||
|
} |
||||||
|
|
||||||
|
function formatData(data, precision) { |
||||||
|
var TEN = Math.pow(10, precision || 2); |
||||||
|
return data.map(function (d, i) { |
||||||
|
return d.map(function (n) { |
||||||
|
return Math.round(n * TEN) / TEN; |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
/** |
||||||
|
* Multiplies AxB, where A and B are matrices of nXm and mXn dimensions |
||||||
|
* @param {} a
|
||||||
|
* @param {*} b
|
||||||
|
*/ |
||||||
|
function multiply(a, b) { |
||||||
|
if (!a[0] || !b[0] || !a.length || !b.length) { |
||||||
|
throw new Error('Both A and B should be matrices'); |
||||||
|
} |
||||||
|
|
||||||
|
if (b.length !== a[0].length) { |
||||||
|
throw new Error('Columns in A should be the same as the number of rows in B'); |
||||||
|
} |
||||||
|
var product = []; |
||||||
|
|
||||||
|
for (var i = 0; i < a.length; i++) { |
||||||
|
product[i] = []; //initialize a new row
|
||||||
|
for (var j = 0; j < b[0].length; j++) { |
||||||
|
for (var k = 0; k < a[0].length; k++) { |
||||||
|
(product[i])[j] = !!(product[i])[j] ? (product[i])[j] + (a[i])[k] * (b[k])[j] : (a[i])[k] * (b[k])[j]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return product; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Utility function to subtract matrix b from a |
||||||
|
*
|
||||||
|
* @param {any} a
|
||||||
|
* @param {any} b
|
||||||
|
* @returns
|
||||||
|
*/ |
||||||
|
function subtract(a, b) { |
||||||
|
if (!(a.length === b.length && a[0].length === b[0].length)) |
||||||
|
throw new Error('Both A and B should have the same dimensions'); |
||||||
|
var result = []; |
||||||
|
for (var i = 0; i < a.length; i++) { |
||||||
|
result[i] = []; |
||||||
|
for (var j = 0; j < b[0].length; j++) { |
||||||
|
(result[i])[j] = (a[i])[j] - (b[i])[j]; |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Multiplies a matrix into a factor |
||||||
|
*
|
||||||
|
* @param {any} matrix
|
||||||
|
* @param {any} factor
|
||||||
|
* @returns
|
||||||
|
*/ |
||||||
|
function scale(matrix, factor) { |
||||||
|
var result = []; |
||||||
|
for (var i = 0; i < matrix.length; i++) { |
||||||
|
result[i] = []; |
||||||
|
for (var j = 0; j < matrix[0].length; j++) { |
||||||
|
(result[i])[j] = (matrix[i])[j] * factor; |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates a unit square matrix |
||||||
|
* @param {*} rows = number of rows to fill |
||||||
|
*/ |
||||||
|
function unitSquareMatrix(rows) { |
||||||
|
var result = []; |
||||||
|
for (var i = 0; i < rows; i++) { |
||||||
|
result[i] = []; |
||||||
|
for (var j = 0; j < rows; j++) { |
||||||
|
(result[i])[j] = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Transposes a matrix, converts rows to columns |
||||||
|
* @param {*} matrix
|
||||||
|
*/ |
||||||
|
function transpose(matrix) { |
||||||
|
var operated = clone(matrix); |
||||||
|
return operated[0].map(function (m, c) { |
||||||
|
return matrix.map(function (r) { |
||||||
|
return r[c]; |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
/** |
||||||
|
* Deep Clones a matrix |
||||||
|
* @param {*} arr
|
||||||
|
*/ |
||||||
|
function clone(arr) { |
||||||
|
var string = JSON.stringify(arr); |
||||||
|
var result = JSON.parse(string); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Compute the thin SVD from G. H. Golub and C. Reinsch, Numer. Math. 14, 403-420 (1970) |
||||||
|
* From the Numeric JS Implementation Copyright (C) 2011 by Sébastien Loisel |
||||||
|
* The C implementation from which this has been taken may be found here: http://www.public.iastate.edu/~dicook/JSS/paper/code/svd.c
|
||||||
|
* @param {*} A = m*n matrix |
||||||
|
*/ |
||||||
|
function svd(A) { |
||||||
|
var temp; |
||||||
|
var prec = Math.pow(2, -52) // assumes double prec
|
||||||
|
var tolerance = 1.e-64 / prec; |
||||||
|
var itmax = 50; |
||||||
|
var c = 0; |
||||||
|
var i = 0; |
||||||
|
var j = 0; |
||||||
|
var k = 0; |
||||||
|
var l = 0; |
||||||
|
var u = clone(A); |
||||||
|
var m = u.length; |
||||||
|
var n = u[0].length; |
||||||
|
|
||||||
|
if (m < n) throw "Need more rows than columns" |
||||||
|
|
||||||
|
var e = new Array(n); //vector1
|
||||||
|
var q = new Array(n); //vector2
|
||||||
|
for (i = 0; i < n; i++) e[i] = q[i] = 0.0; |
||||||
|
var v = rep([n, n], 0); |
||||||
|
|
||||||
|
function pythag(a, b) { |
||||||
|
a = Math.abs(a) |
||||||
|
b = Math.abs(b) |
||||||
|
if (a > b) |
||||||
|
return a * Math.sqrt(1.0 + (b * b / a / a)) |
||||||
|
else if (b == 0.0) |
||||||
|
return a |
||||||
|
return b * Math.sqrt(1.0 + (a * a / b / b)) |
||||||
|
} |
||||||
|
|
||||||
|
//rep function
|
||||||
|
function rep(s, v, k) { |
||||||
|
if (typeof k === "undefined") { |
||||||
|
k = 0; |
||||||
|
} |
||||||
|
var n = s[k], |
||||||
|
ret = Array(n), |
||||||
|
i; |
||||||
|
if (k === s.length - 1) { |
||||||
|
for (i = n - 2; i >= 0; i -= 2) { |
||||||
|
ret[i + 1] = v; |
||||||
|
ret[i] = v; |
||||||
|
} |
||||||
|
if (i === -1) { |
||||||
|
ret[0] = v; |
||||||
|
} |
||||||
|
return ret; |
||||||
|
} |
||||||
|
for (i = n - 1; i >= 0; i--) { |
||||||
|
ret[i] = rep(s, v, k + 1); |
||||||
|
} |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
//Householder's reduction to bidiagonal form
|
||||||
|
|
||||||
|
var f = 0.0; |
||||||
|
var g = 0.0; |
||||||
|
var h = 0.0; |
||||||
|
var x = 0.0; |
||||||
|
var y = 0.0; |
||||||
|
var z = 0.0; |
||||||
|
var s = 0.0; |
||||||
|
|
||||||
|
for (i = 0; i < n; i++) { |
||||||
|
e[i] = g; //vector
|
||||||
|
s = 0.0; //sum
|
||||||
|
l = i + 1; //stays i+1
|
||||||
|
for (j = i; j < m; j++) |
||||||
|
s += (u[j][i] * u[j][i]); |
||||||
|
if (s <= tolerance) |
||||||
|
g = 0.0; |
||||||
|
else { |
||||||
|
f = u[i][i]; |
||||||
|
g = Math.sqrt(s); |
||||||
|
if (f >= 0.0) g = -g; |
||||||
|
h = f * g - s |
||||||
|
u[i][i] = f - g; |
||||||
|
for (j = l; j < n; j++) { |
||||||
|
s = 0.0 |
||||||
|
for (k = i; k < m; k++) |
||||||
|
s += u[k][i] * u[k][j] |
||||||
|
f = s / h |
||||||
|
for (k = i; k < m; k++) |
||||||
|
u[k][j] += f * u[k][i] |
||||||
|
} |
||||||
|
} |
||||||
|
q[i] = g |
||||||
|
s = 0.0 |
||||||
|
for (j = l; j < n; j++) |
||||||
|
s = s + u[i][j] * u[i][j] |
||||||
|
if (s <= tolerance) |
||||||
|
g = 0.0 |
||||||
|
else { |
||||||
|
f = u[i][i + 1] |
||||||
|
g = Math.sqrt(s) |
||||||
|
if (f >= 0.0) g = -g |
||||||
|
h = f * g - s |
||||||
|
u[i][i + 1] = f - g; |
||||||
|
for (j = l; j < n; j++) e[j] = u[i][j] / h |
||||||
|
for (j = l; j < m; j++) { |
||||||
|
s = 0.0 |
||||||
|
for (k = l; k < n; k++) |
||||||
|
s += (u[j][k] * u[i][k]) |
||||||
|
for (k = l; k < n; k++) |
||||||
|
u[j][k] += s * e[k] |
||||||
|
} |
||||||
|
} |
||||||
|
y = Math.abs(q[i]) + Math.abs(e[i]) |
||||||
|
if (y > x) |
||||||
|
x = y |
||||||
|
} |
||||||
|
|
||||||
|
// accumulation of right hand transformations
|
||||||
|
for (i = n - 1; i != -1; i += -1) { |
||||||
|
if (g != 0.0) { |
||||||
|
h = g * u[i][i + 1] |
||||||
|
for (j = l; j < n; j++) |
||||||
|
v[j][i] = u[i][j] / h //u is array, v is square of columns
|
||||||
|
for (j = l; j < n; j++) { |
||||||
|
s = 0.0 |
||||||
|
for (k = l; k < n; k++) |
||||||
|
s += u[i][k] * v[k][j] |
||||||
|
for (k = l; k < n; k++) |
||||||
|
v[k][j] += (s * v[k][i]) |
||||||
|
} |
||||||
|
} |
||||||
|
for (j = l; j < n; j++) { |
||||||
|
v[i][j] = 0; |
||||||
|
v[j][i] = 0; |
||||||
|
} |
||||||
|
v[i][i] = 1; |
||||||
|
g = e[i] |
||||||
|
l = i |
||||||
|
} |
||||||
|
|
||||||
|
// accumulation of left hand transformations
|
||||||
|
for (i = n - 1; i != -1; i += -1) { |
||||||
|
l = i + 1 |
||||||
|
g = q[i] |
||||||
|
for (j = l; j < n; j++) |
||||||
|
u[i][j] = 0; |
||||||
|
if (g != 0.0) { |
||||||
|
h = u[i][i] * g |
||||||
|
for (j = l; j < n; j++) { |
||||||
|
s = 0.0 |
||||||
|
for (k = l; k < m; k++) s += u[k][i] * u[k][j]; |
||||||
|
f = s / h |
||||||
|
for (k = i; k < m; k++) u[k][j] += f * u[k][i]; |
||||||
|
} |
||||||
|
for (j = i; j < m; j++) u[j][i] = u[j][i] / g; |
||||||
|
} else |
||||||
|
for (j = i; j < m; j++) u[j][i] = 0; |
||||||
|
u[i][i] += 1; |
||||||
|
} |
||||||
|
|
||||||
|
// diagonalization of the bidiagonal form
|
||||||
|
prec = prec * x |
||||||
|
for (k = n - 1; k != -1; k += -1) { |
||||||
|
for (var iteration = 0; iteration < itmax; iteration++) { // test f splitting
|
||||||
|
var test_convergence = false |
||||||
|
for (l = k; l != -1; l += -1) { |
||||||
|
if (Math.abs(e[l]) <= prec) { |
||||||
|
test_convergence = true |
||||||
|
break |
||||||
|
} |
||||||
|
if (Math.abs(q[l - 1]) <= prec) |
||||||
|
break |
||||||
|
} |
||||||
|
if (!test_convergence) { // cancellation of e[l] if l>0
|
||||||
|
c = 0.0 |
||||||
|
s = 1.0 |
||||||
|
var l1 = l - 1 |
||||||
|
for (i = l; i < k + 1; i++) { |
||||||
|
f = s * e[i] |
||||||
|
e[i] = c * e[i] |
||||||
|
if (Math.abs(f) <= prec) |
||||||
|
break |
||||||
|
g = q[i] |
||||||
|
h = pythag(f, g) |
||||||
|
q[i] = h |
||||||
|
c = g / h |
||||||
|
s = -f / h |
||||||
|
for (j = 0; j < m; j++) { |
||||||
|
y = u[j][l1] |
||||||
|
z = u[j][i] |
||||||
|
u[j][l1] = y * c + (z * s) |
||||||
|
u[j][i] = -y * s + (z * c) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// test f convergence
|
||||||
|
z = q[k] |
||||||
|
if (l == k) { //convergence
|
||||||
|
if (z < 0.0) { //q[k] is made non-negative
|
||||||
|
q[k] = -z |
||||||
|
for (j = 0; j < n; j++) |
||||||
|
v[j][k] = -v[j][k] |
||||||
|
} |
||||||
|
break //break out of iteration loop and move on to next k value
|
||||||
|
} |
||||||
|
if (iteration >= itmax - 1) |
||||||
|
throw 'Error: no convergence.' |
||||||
|
// shift from bottom 2x2 minor
|
||||||
|
x = q[l] |
||||||
|
y = q[k - 1] |
||||||
|
g = e[k - 1] |
||||||
|
h = e[k] |
||||||
|
f = ((y - z) * (y + z) + (g - h) * (g + h)) / (2.0 * h * y) |
||||||
|
g = pythag(f, 1.0) |
||||||
|
if (f < 0.0) |
||||||
|
f = ((x - z) * (x + z) + h * (y / (f - g) - h)) / x |
||||||
|
else |
||||||
|
f = ((x - z) * (x + z) + h * (y / (f + g) - h)) / x |
||||||
|
// next QR transformation
|
||||||
|
c = 1.0 |
||||||
|
s = 1.0 |
||||||
|
for (i = l + 1; i < k + 1; i++) { |
||||||
|
g = e[i] |
||||||
|
y = q[i] |
||||||
|
h = s * g |
||||||
|
g = c * g |
||||||
|
z = pythag(f, h) |
||||||
|
e[i - 1] = z |
||||||
|
c = f / z |
||||||
|
s = h / z |
||||||
|
f = x * c + g * s |
||||||
|
g = -x * s + g * c |
||||||
|
h = y * s |
||||||
|
y = y * c |
||||||
|
for (j = 0; j < n; j++) { |
||||||
|
x = v[j][i - 1] |
||||||
|
z = v[j][i] |
||||||
|
v[j][i - 1] = x * c + z * s |
||||||
|
v[j][i] = -x * s + z * c |
||||||
|
} |
||||||
|
z = pythag(f, h) |
||||||
|
q[i - 1] = z |
||||||
|
c = f / z |
||||||
|
s = h / z |
||||||
|
f = c * g + s * y |
||||||
|
x = -s * g + c * y |
||||||
|
for (j = 0; j < m; j++) { |
||||||
|
y = u[j][i - 1] |
||||||
|
z = u[j][i] |
||||||
|
u[j][i - 1] = y * c + z * s |
||||||
|
u[j][i] = -y * s + z * c |
||||||
|
} |
||||||
|
} |
||||||
|
e[l] = 0.0 |
||||||
|
e[k] = f |
||||||
|
q[k] = x |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (i = 0; i < q.length; i++) |
||||||
|
if (q[i] < prec) q[i] = 0 |
||||||
|
|
||||||
|
//sort eigenvalues
|
||||||
|
for (i = 0; i < n; i++) { |
||||||
|
for (j = i - 1; j >= 0; j--) { |
||||||
|
if (q[j] < q[i]) { |
||||||
|
c = q[j] |
||||||
|
q[j] = q[i] |
||||||
|
q[i] = c |
||||||
|
for (k = 0; k < u.length; k++) { |
||||||
|
temp = u[k][i]; |
||||||
|
u[k][i] = u[k][j]; |
||||||
|
u[k][j] = temp; |
||||||
|
} |
||||||
|
for (k = 0; k < v.length; k++) { |
||||||
|
temp = v[k][i]; |
||||||
|
v[k][i] = v[k][j]; |
||||||
|
v[k][j] = temp; |
||||||
|
} |
||||||
|
i = j |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
U: u, |
||||||
|
S: q, |
||||||
|
V: v |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
computeDeviationScores: computeDeviationScores, |
||||||
|
computeDeviationMatrix: computeDeviationMatrix, |
||||||
|
computeSVD: computeSVD, |
||||||
|
computePercentageExplained: computePercentageExplained, |
||||||
|
computeOriginalData: computeOriginalData, |
||||||
|
computeVarianceCovariance: computeVarianceCovariance, |
||||||
|
computeAdjustedData: computeAdjustedData, |
||||||
|
getEigenVectors: getEigenVectors, |
||||||
|
analyseTopResult: analyseTopResult, |
||||||
|
transpose: transpose, |
||||||
|
multiply: multiply, |
||||||
|
clone: clone, |
||||||
|
scale: scale |
||||||
|
} |
||||||
|
})(); |
||||||
|
|
||||||
|
if(typeof module !== 'undefined') |
||||||
|
module.exports = PCA; |
@ -1,107 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright (C) 2015 Jose F. Maldonado |
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
||||||
* If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/ |
|
||||||
|
|
||||||
// Verify if the namespace is not already defined.
|
|
||||||
if(typeof SaVaGe !== 'object') SaVaGe = {}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a SVG element representing a toggle switch. |
|
||||||
*
|
|
||||||
* The parameter must be an object with the following attributes: |
|
||||||
* 'container' (a selector of the element where the element must be appended), |
|
||||||
* 'value' (a boolean indicating the initial value of the switch, by default 'false'), |
|
||||||
* 'height' (a number indicating the height, in pixels, of the element, by default 50), |
|
||||||
* 'width' (a number indicating the width, in pixels, of the element, by default 80), |
|
||||||
* 'radius' (a number indicating the radius of the lever), |
|
||||||
* 'border' (a number indicating the width, in pixels, the distance between the element background and the switch's button, by default 5), |
|
||||||
* 'duration' (a number indicating the number of milliseconds that the toggle animation must last, by default 250), |
|
||||||
* 'colors' (an object with the attributes 'backLeft', 'foreLeft', 'backRight' and 'foreRight' indicating the colors of the element in each state) and |
|
||||||
* 'onChange' (a callback function which is invoked every time that the element is clicked). |
|
||||||
*
|
|
||||||
* The object returned by this function contains the methods: |
|
||||||
* 'svg', and instance of the SVG object created with D3.js, |
|
||||||
* 'getValue()', for get the current state of the switch, |
|
||||||
* 'serValue(newVal)', for change the state of the switch and |
|
||||||
* 'remove()', for remove the element from the document. |
|
||||||
*
|
|
||||||
* @param {object} params An collection of values for customize the element. |
|
||||||
* @returns {object} An object with methods for manipulate the element. |
|
||||||
*/ |
|
||||||
SaVaGe.ToggleSwitch = function(params) { |
|
||||||
// Verify parameters.
|
|
||||||
if(typeof params !== 'object') params = {}; |
|
||||||
if(typeof params.container !== 'string') params.container = "body"; |
|
||||||
if(typeof params.value !== 'boolean') params.value = false; |
|
||||||
if(typeof params.height !== 'number') params.height = 50; |
|
||||||
if(typeof params.width !== 'number' || params.width < params.height) params.width = parseInt(params.height*1.6, 10); |
|
||||||
if(typeof params.radius !== 'number') params.radius = params.height/2 - 4; |
|
||||||
if(typeof params.duration !== 'number') params.duration = 250; |
|
||||||
if(typeof params.colors !== 'object') params.colors = {}; |
|
||||||
if(params.colors.backLeft === undefined) params.colors.backLeft = "lightgray"; |
|
||||||
if(params.colors.foreLeft === undefined) params.colors.foreLeft = "white"; |
|
||||||
if(params.colors.backRight === undefined) params.colors.backRight = "#88f"; |
|
||||||
if(params.colors.foreRight === undefined) params.colors.foreRight = "white"; |
|
||||||
|
|
||||||
// Define internal variables.
|
|
||||||
var atRight = params.value; |
|
||||||
|
|
||||||
// Calculate SVG dimensions and position offset.
|
|
||||||
var svgHeight = params.height; |
|
||||||
var svgWidth = params.width; |
|
||||||
var offsetX = 0; |
|
||||||
var offsetY = 0; |
|
||||||
if(params.radius*2 > params.height) { |
|
||||||
svgHeight = params.radius*2; |
|
||||||
svgWidth = parseInt(params.width + (params.radius*2 - params.height), 10); |
|
||||||
offsetX = parseInt((svgWidth - params.width)/2, 10); |
|
||||||
offsetY = parseInt((svgWidth - params.width)/2, 10); |
|
||||||
} |
|
||||||
|
|
||||||
// Create widget.
|
|
||||||
var svg = d3.select(params.container).append("svg") |
|
||||||
.attr("width", svgWidth) |
|
||||||
.attr("height", svgHeight) |
|
||||||
.style("cursor", "pointer"); |
|
||||||
var rect = svg.append("rect") |
|
||||||
.attr("x", offsetX) |
|
||||||
.attr("y", offsetY) |
|
||||||
.attr("rx", params.height/2) |
|
||||||
.attr("ry", params.height/2) |
|
||||||
.style("fill", atRight? params.colors.backRight : params.colors.backLeft) |
|
||||||
.attr("width", params.width) |
|
||||||
.attr("height", params.height); |
|
||||||
var circle = svg.append("circle") |
|
||||||
.attr("cx", (atRight? (params.width-params.height/2) : (params.height/2)) + offsetX) |
|
||||||
.attr("cy", params.height/2 + offsetY) |
|
||||||
.attr("r", params.radius) |
|
||||||
.style("fill", atRight? params.colors.foreRight : params.colors.foreLeft); |
|
||||||
|
|
||||||
// Define internal functions.
|
|
||||||
var setAtRight = function(newValue) { |
|
||||||
atRight = newValue; |
|
||||||
circle.transition().duration(params.duration) |
|
||||||
.attr("cx", (atRight? (params.width-params.height/2) : (params.height/2)) + offsetX) |
|
||||||
.style("fill", atRight? params.colors.foreRight : params.colors.foreLeft); |
|
||||||
rect.transition().duration(params.duration).style("fill", atRight? params.colors.backRight : params.colors.backLeft); |
|
||||||
|
|
||||||
}; |
|
||||||
|
|
||||||
// Define result's object.
|
|
||||||
var res = { |
|
||||||
'svg' : svg, |
|
||||||
'getValue': function() { return atRight; }, |
|
||||||
'setValue': setAtRight, |
|
||||||
'remove': function() { svg.remove(); } |
|
||||||
}; |
|
||||||
|
|
||||||
// Define click listener.
|
|
||||||
svg.on('click', function(data, index){ |
|
||||||
setAtRight(!atRight); |
|
||||||
if(typeof params.onChange === 'function') params.onChange(res); |
|
||||||
});
|
|
||||||
|
|
||||||
return res; |
|
||||||
}; |
|
Loading…
Reference in new issue