Scatter plot with color coding according to Mahalanobis distance.
Created based on @veltman's block, and
calculating bi-variate normal
Scatter plot with color coding according to Mahalanobis distance.
Created based on @veltman's block, and
calculating bi-variate normal
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<head> | |
<link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet"> | |
<style> | |
body { | |
font-size: 20px; | |
font-family: 'Raleway', sans-serif; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<div style="margin-left: 0px; margin-top: 2px;"> | |
Select correlation level: <br/> | |
<span class="leftlabel">-1</span> | |
<input id ="range1" type="range" min="-99" max="99" value="70" style="width: 400px; margin-right: 10px;"/> | |
<span class="rightlabel">1</span> | |
<br /> | |
Current level: <span id="corr">0.7</span> | |
</div> | |
<script> | |
var margin = 25, | |
width = 400 - 2 * margin, | |
height = 400 - 2 * margin; | |
var svg = d3.select("body").append("svg") | |
.attr("id", "chart_svg") | |
.attr("width", width + 2 * margin) | |
.attr("height", height + 2 * margin) | |
.append("g") | |
.attr("transform", "translate(" + margin + " " + margin + ")"); | |
var x = d3.scaleLinear() | |
.range([0, width]); | |
var y = d3.scaleLinear() | |
.range([height, 0]); | |
var xAxis = d3.axisBottom(x).ticks(12), | |
yAxis = d3.axisLeft(y).ticks(12 * height / width); | |
//---------------------------------- Create Bi-variate Normal ------------------------------------- | |
// source: http://ryan-j-smith.github.io/D3-Examples/html/plotRandomCov.html | |
//------------------------------------------------------------------------------------------------- | |
var mu = [0, 0]; | |
var sig = [[1, 0.7], | |
[0.7, 1]]; | |
//2-D Cholesky decomposition of sigma | |
function chol2d(sig) { | |
var a, b, c; | |
a = Math.sqrt(sig[0][0]); | |
b = sig[0][1] / a; | |
c = Math.sqrt(sig[1][1] - b * b); | |
return [ | |
[a, 0], | |
[b, c] | |
]; | |
} | |
function randomData(samples) { | |
var data = [], | |
random = d3.randomNormal(); | |
//Perform cholesky decomposition | |
var sqrtSig = chol2d(sig); | |
for (i = 0; i < samples; i++) { | |
var x = [random(), random()]; | |
var y = [0, 0]; | |
data.push({ | |
x: mu[0] + sqrtSig[0][0] * x[0] + sqrtSig[0][1] * x[1], | |
y: mu[1] + sqrtSig[1][0] * x[0] + sqrtSig[1][1] * x[1] | |
}); | |
} | |
return data; | |
} | |
function calculate_mahalanobis(ds) { | |
arr = []; | |
b = []; | |
for (var j = 0; j < ds.length; j++) { | |
b = []; | |
b.push(ds[j]["x"]); | |
b.push(ds[j]["y"]); | |
arr.push(b); | |
} | |
var columns = transpose(arr), | |
means = columns.map(mean), | |
invertedCovariance = invert(cov(columns, means)); | |
var deltas = arr.map(function (row, i) { | |
return row.map(function (value, i) { | |
return value - means[i]; | |
}); | |
}); | |
deltas.map(function (row, i) { | |
m = Math.sqrt( | |
multiply(multiply(row, invertedCovariance), row) | |
); | |
arr[i].push(m) | |
}); | |
data = ds.map(function (d, i) { | |
return { | |
x: +d.x, | |
y: +d.y, | |
m: arr[i][2] | |
}; | |
}); | |
return data; | |
} | |
var ds = randomData(2000); | |
var data = calculate_mahalanobis(ds); | |
x.domain(d3.extent(data, function (d) { return d.x; })).nice(); | |
y.domain(d3.extent(data, function (d) { return d.y; })).nice(); | |
var maxDistance = 8; | |
var step = d3.scaleLinear() | |
.domain([1, 3]) | |
.range([0,6]); | |
var color = d3.scaleLinear() | |
.domain([1, 1.5, 2, 2.5, 3, 3.5, 6]) | |
.range(['#1a9850', '#66bd63', '#a6d96a', '#fdae61', '#f46d43', '#d73027', '#d73027']) | |
.interpolate(d3.interpolateHcl); //interpolateHsl interpolateHcl interpolateRgb | |
['#d73027', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#d9ef8b', '#a6d96a', '#66bd63', '#1a9850'] | |
svg.selectAll("circle") | |
.data(data) | |
.enter() | |
.append("circle") | |
.attr("class", "dot") | |
.attr("r", 3) | |
.attr("cx", function(d){return x(d.x);}) | |
.attr("cy", function(d){return y(d.y);}) | |
.attr("fill", function(d){ | |
return color(d.m); | |
}); | |
svg.append("g") | |
.attr("class", "x axis ") | |
.attr('id', "axis--x") | |
.attr("transform", "translate(0," + height + ")") | |
.call(xAxis); | |
svg.append("g") | |
.attr("class", "y axis") | |
.attr('id', "axis--y") | |
.call(yAxis); | |
//-------------------------------------------- math.js -------------------------------------------- | |
// source: https://github.com/veltman/mahalanobis/blob/master/src/math.js | |
//------------------------------------------------------------------------------------------------- | |
function mean(arr) { | |
return sum(arr) / arr.length; | |
} | |
function sum(arr) { | |
return arr.reduce(function(a, b){ | |
return a + b; | |
}); | |
} | |
function isNumeric(n) { | |
return typeof n === "number" && !isNaN(n); | |
} | |
//------------------------------------------- matrix.js ------------------------------------------- | |
// source: https://github.com/veltman/mahalanobis/blob/master/src/matrix.js | |
//------------------------------------------------------------------------------------------------- | |
function dot(a, b) { | |
if (a.length !== b.length) { | |
throw new TypeError("Vectors are of different sizes"); | |
} | |
return sum(a.map(function(x, i){ | |
return x * b[i]; | |
})); | |
} | |
function multiply(a, b) { | |
var aSize = a.every(isNumeric) ? 1 : a.length, | |
bSize = b.every(isNumeric) ? 1 : b.length; | |
if (aSize === 1) { | |
if (bSize === 1) { | |
return dot(a, b); | |
} | |
return b.map(function(row){ | |
return dot(a, row); | |
}); | |
} | |
if (bSize === 1) { | |
return a.map(function(row){ | |
return dot(row, b); | |
}); | |
} | |
return a.map(function(x){ | |
return transpose(b).map(function(y){ | |
return dot(x, y); | |
}); | |
}); | |
} | |
function transpose(matrix) { | |
return matrix[0].map(function(d, i){ | |
return matrix.map(function(row){ | |
return row[i]; | |
}); | |
}); | |
} | |
function cov(columns, means) { | |
return columns.map(function(c1, i){ | |
return columns.map(function(c2, j){ | |
var terms = c1.map(function(x, k){ | |
return (x - means[i]) * (c2[k] - means[j]); | |
}); | |
return sum(terms) / (c1.length - 1); | |
}); | |
}); | |
} | |
function invert(matrix) { | |
var size = matrix.length, | |
base, | |
swap, | |
augmented; | |
// Augment w/ identity matrix | |
augmented = matrix.map(function(row,i){ | |
return row.slice(0).concat(row.slice(0).map(function(d,j){ | |
return j === i ? 1 : 0; | |
})); | |
}); | |
// Process each row | |
for (var r = 0; r < size; r++) { | |
base = augmented[r][r]; | |
// Zero on diagonal, swap with a lower row | |
if (!base) { | |
for (var rr = r + 1; rr < size; rr++) { | |
if (augmented[rr][r]) { | |
// swap | |
swap = augmented[rr]; | |
augmented[rr] = augmented[r]; | |
augmented[r] = swap; | |
base = augmented[r][r]; | |
break; | |
} | |
} | |
if (!base) { | |
throw new RangeError("Matrix not invertable."); | |
} | |
} | |
// 1 on the diagonal | |
for (var c = 0; c < size * 2; c++) { | |
augmented[r][c] = augmented[r][c] / base; | |
} | |
// Zeroes elsewhere | |
for (var q = 0; q < size; q++) { | |
if (q !== r) { | |
base = augmented[q][r]; | |
for (var p = 0; p < size * 2; p++) { | |
augmented[q][p] -= base * augmented[r][p]; | |
} | |
} | |
} | |
} | |
return augmented.map(function(row){ | |
return row.slice(size); | |
}); | |
} | |
//----------------------------------------- mahalanobis.js ---------------------------------------- | |
// source: https://github.com/veltman/mahalanobis/blob/master/build/mahalanobis.js | |
//------------------------------------------------------------------------------------------------- | |
function mahalanobis(data, cov, loc_est) { | |
var deltas = row.map(function (d, i) { | |
return d - means[i]; | |
}); | |
} | |
d3.select("#range1").on("change", function () { | |
d3.select("#corr").html(d3.select("#range1").property("value") / 100); | |
sig = [[1, d3.select("#range1").property("value") / 100], | |
[d3.select("#range1").property("value") / 100, 1]]; | |
ds = randomData(2000); | |
data = calculate_mahalanobis(ds); | |
x.domain(d3.extent(data, function (d) { return d.x; })).nice(); | |
y.domain(d3.extent(data, function (d) { return d.y; })).nice(); | |
var t = svg.transition().duration(750); | |
svg.select("#axis--x").transition(t).call(xAxis); | |
svg.select("#axis--y").transition(t).call(yAxis); | |
svg.selectAll(".dot").data(data) | |
svg.selectAll("circle").transition(t) | |
.attr("cx", function (d) { return x(d.x); }) | |
.attr("cy", function (d) { return y(d.y); }) | |
.attr("fill", function (d) { | |
return color(d.m); | |
}); | |
}); | |
</script> |