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 Urquhart graph given by t-SNE
collide the centers.
Forked from t-SNE, a force, and voronoi.
forked from Fil's block: t-SNE, a force, and Urquhart graph
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 = 500,
x = d3.scaleLinear()
.domain([-width/2, width/2])
.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(400).map(d => [Math.random(), Math.random(), Math.random(), Math.random(), Math.random()]);
let nodes;
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 = { index: d3.scan(nodes.map(
d => (d.x - m[0])*(d.x - m[0]) + (d.y - m[1])*(d.y - m[1])
)) }//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');
function north(v, direction = 'north') {
var strength = 1;
var north = function(alpha){
return;
data.forEach(d => {
switch(direction) {
case 'north':
d.vy -= alpha * strength * v(d);
break;
case 'south':
d.vy += alpha * strength * v(d);
break;
case 'east':
d.vx += alpha * strength * v(d);
break;
case 'west':
d.vx -= alpha * strength * v(d);
break;
}
});
}
north.strength = function(_){
return (arguments.length) ? (strength = +_, north) : strength;
}
return north;
}
function east(v) {
return north(v, 'east');
}
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;
var diagram = null;
var links = [];
const forcetsne = d3.forceSimulation(data)
.alphaDecay(0)
.alpha(0.1)
/*
.force('tsne', function(alpha){
alpha /= 3000;
alpha = 0;
data.forEach((d,i) => {
d.x += alpha * (150*pos[i][0] - d.x);
d.y += alpha * (150*pos[i][1] - d.y);
});
})
*/
//.force("link", d3.forceLink(links))
.force('manybody', d3.forceManyBody().strength(_ => -10))
.force('x', d3.forceX(_ => 0))
.force('y', d3.forceY(_ => 0))
.force('north', north(d => d[4] - 0.5).strength(-10)) // small is north
.force('east', east(d => (d[0] / (d[0] + d[1] + d[2])) - 1/3).strength(10)) // red is east
.force('collide', d3.forceCollide().radius(d => 3 + 8 * d[4]))
.on('tick', function () {
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;
});
context.clearRect(0, 0, width, width);
context.beginPath();
links.forEach(link => {
//console.log(link.source.index, link.target.index)
context.moveTo(nodes[link.source.index].x, nodes[link.source.index].y);
context.lineTo(nodes[link.target.index].x, nodes[link.target.index].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;
diagram = voronoi(pos.map((t, i) => {
t.index = i;
t.x = t[0];
t.y = t[1];
return t;
}));
links = urquhart(diagram);
forcetsne.force("link", d3.forceLink(
links.map(l => ({
source: l.source.index,
target: l.target.index,
}))
).distance(_ => 0)
.iterations(1)
)
}
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>
https://d3js.org/d3.v4.min.js