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.
On the left: the Urquhart graph eliminates links that have the longest projected distances;
On the right: using the original distances. (This algorithm doesn't connect dots just because they're projected together.)
I find it interesting that sites that are far apart (on the edge of the graph) are closer in the original space than it seems on the projected space. Also, closely related colours in the center are more connected.
Forked from t-SNE, a force, and voronoi.
forked from Fil's block: t-SNE and Urquhart w/distances study
xxxxxxxxxx
<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 = null,
distances = null;
const canvas1 = 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 canvas2 = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", width)
const tip = d3.select("body").append("div")
.style('position', 'absolute');
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 links1 = urquhart(diagram);
draw(canvas1, nodes, links1);
let links2 = urquhart(diagram, (a, b) => !distances ? 0 : distances.data[b.index + distances.stride[0] * a.index]);
draw(canvas2, nodes, links2);
// debug: show costs graph
// cost.attr('d', area(costs));
});
function draw(canvas, nodes, links){
let context = canvas.node().getContext("2d");
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.log) console.log.apply(this, e.data.log);
if (e.data.pos) pos = e.data.pos;
if (e.data.distances) distances = e.data.distances;
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: 'euclidean', //'manhattan', //'euclidean',
data: data
});
});
function urquhart(diagram, distance) {
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 = -1,
v;
for (var j = 0; j < 3; j++) {
var a = t[j],
b = t[(j + 1) % 3];
v = d3.extent([a.index, b.index]);
if (!distance) {
length = (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
} else {
length = distance(a, b);
}
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>
https://d3js.org/d3.v4.min.js