block by fil e402e9c51ce77c21baedc2d1af933bc3

t-SNE, a force, and Urquhart graph

Full Screen

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:

The result is displayed with an Urquhart graph.

Forked from t-SNE, a force, and voronoi.

index.html

<!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>

worker.js

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
  });

};