This is a t-SNE representation of an array of (random) circles : red, green, blue, opacity and radius are 5 independent dimensions.
The t-SNE is computed by a javascript worker, using science.ai’s implementation.
A force is created that tries to:
follow the positions given by t-SNE
collide the centers.
The result is displayed with an Urquhart graph.
Forked from t-SNE, a force, and voronoi.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<script>
const maxIter = 500,
width = 400,
x = d3.scaleLinear()
.domain([-200, 200])
.range([0, width]),
area = d3.area()
.x((d,i) => i)
.y0(width+90)
.y1((d,i) => width + 90 - parseInt(3 * d||0));
const data = d3.range(300).map(d => [Math.random(), Math.random(), Math.random(), Math.random(), Math.random()]);
let diagram;
const canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", width)
.on('mousemove', function() {
let m = d3.mouse(this);
let f = diagram.find(m[0], m[1], 30);
if (f) {
let d = data[f.index];
tip
.html('['+f.index+'] = ' + d.map(d => Math.round(1000*d)/10).join(', '))
.style('border', 'solid 2px ' + d3.rgb(d[0]*255,d[1]*255,d[2]*255, 0.3 + 0.7 * d[3]).toString())
.style('left', m[0] + 'px')
.style('top', m[1] + 'px')
;
}
else
tip .style('top', '-1000px')
});
const tip = d3.select("body").append("div")
.style('position', 'absolute');
const context = canvas.node().getContext("2d");
const voronoi = d3.voronoi()
.x(function (d) {
return d.x;
})
.y(function (d) {
return d.y;
});
d3.queue()
.defer(d3.text, 'tsne.min.js')
.defer(d3.text, 'worker.js')
.await(function (err, t, w) {
const worker = new Worker(window.URL.createObjectURL(new Blob([t + w], {
type: "text/javascript"
})));
let pos = data.map(d => [Math.random() - 0.5, Math.random() - 0.5]);
let costs = [];
let s = 0, c = 1, a = 0;
const forcetsne = d3.forceSimulation(data)
.alphaDecay(0)
.alpha(0.1)
.force('tsne', function(alpha){
data.forEach((d,i) => {
d.x += alpha * (150*pos[i][0] - d.x);
d.y += alpha * (150*pos[i][1] - d.y);
});
})
.force('collide', d3.forceCollide().radius(d => 1 + 2 + 8 * d[4]))
.on('tick', function () {
let nodes = data.map((d,i) => {
let node = {x: x(d.x), y: x(d.y), r: 2 + 8 * d[4], color: d3.rgb(d[0]*255,d[1]*255,d[2]*255, 0.3 + 0.7 * d[3]).toString()};
node.index = i;
return node;
});
diagram = voronoi(nodes);
let links = urquhart(diagram);
// debug: show costs graph
// cost.attr('d', area(costs));
context.clearRect(0, 0, width, width);
context.beginPath();
for (var i = 0, n = links.length; i < n; ++i) {
var link = links[i],
dx = link.source.x - link.target.x,
dy = link.source.y - link.target.y;
context.moveTo(link.source.x, link.source.y);
context.lineTo(link.target.x, link.target.y);
}
context.strokeStyle = "#eee";
context.lineWidth = 6;
context.stroke();
context.beginPath();
for (var i = 0, n = links.length; i < n; ++i) {
var link = links[i],
dx = link.source.x - link.target.x,
dy = link.source.y - link.target.y;
context.moveTo(link.source.x, link.source.y);
context.lineTo(link.target.x, link.target.y);
}
context.strokeStyle = "#000";
context.lineWidth = 0.5;
context.stroke();
for (var i = 0, n = nodes.length; i < n; ++i) {
var node = nodes[i];
context.beginPath();
context.moveTo(node.x, node.y);
context.arc(node.x, node.y, 2+3*node.r/4, 0, 2*Math.PI);
context.lineWidth = 1;
context.fillStyle = node.color;
context.fill();
}
});
worker.onmessage = function (e) {
if (e.data.pos) pos = e.data.pos;
if (e.data.iterations) {
costs[e.data.iterations] = e.data.cost;
}
if (e.data.stop) {
console.log("stopped with message", e.data);
forcetsne.alphaDecay(0.02);
worker.terminate();
}
};
worker.postMessage({
nIter: maxIter,
// dim: 2,
perplexity: 20.0,
// earlyExaggeration: 4.0,
// learningRate: 100.0,
metric: 'manhattan', //'euclidean',
data: data
});
});
function urquhart(diagram) {
var urquhart = d3.map();
diagram.links()
.forEach(function (link) {
var v = d3.extent([link.source.index, link.target.index]);
urquhart.set(v, link);
});
urquhart._remove = [];
diagram.triangles()
.forEach(function (t) {
var l = 0,
length = 0,
i = "bleh",
v;
for (var j = 0; j < 3; j++) {
var a = t[j],
b = t[(j + 1) % 3];
v = d3.extent([a.index, b.index]);
length = (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
if (length > l) {
l = length;
i = v;
}
}
urquhart._remove.push(i);
});
urquhart._remove.forEach(function (i) {
if (urquhart.has(i)) urquhart.remove(i);
});
return urquhart.values();
}
</script>
</body>
self.onmessage = function(e){
let msg = e.data,
currcost = 100;
let model = new TSNE({
dim: msg.dim || 2,
perplexity: msg.perplexity || 100.0,
earlyExaggeration: msg.earlyExaggeration || 4.0,
learningRate: msg.learningRate || 100.0,
nIter: msg.nIter || 500,
metric: msg.metric || 'euclidean'
});
model.init({
data: msg.data,
type: 'dense'
});
model.on('progressData', function(pos){
self.postMessage({pos: model.getOutputScaled()});
});
model.on('progressIter', function (iter) {
currcost = currcost * 0.9 + iter[1];
self.postMessage({
iterations: iter[0],
cost: iter[1],
stop: currcost < 20
});
});
let run = model.run();
self.postMessage({
err: run[0],
iterations: run[1],
stop: true
});
};