block by fil 2dc296af097cd26242b52105e06763d8

t-SNE Voronoi

Full Screen

This is a t-SNE representation of a 4-dimensional array of (random) colours. The t-SNE is computed by a javascript worker, using science.ai’s implementation. For eye-candy the end result is displayed as a Voronoi diagram.

Forked from Fil‘s block: t-SNE worker

Built with blockbuilder.org, so double-thank-you to @enjalot.

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 width = 400,
    x = d3.scaleLinear()
    .domain([-1, 1])
    .range([0, width]),
    area = d3.area()
       .x((d,i) => i)
       .y0(width+90)
       .y1((d,i) => width + 90 - parseInt(3 * d||0));


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

        const data = d3.range(200).map(d => [Math.random(), Math.random(), Math.random(), Math.random()]);

        const svg = d3.select('body').append('svg')
            .attr('width', width + 100)
            .attr('height', width + 100);

        const polygons = svg.selectAll('path')
            .data(data)
            .enter()
            .append('path')
            .attr('d', null)
            .attr('stroke-width', 0.3)
            .attr('fill', d => d3.rgb(d[0] * 256, d[1] * 256, d[2] * 256))
            .attr('stroke', d => d3.rgb(d[0] * 256, d[1] * 256, d[2] * 256))
            .attr('opacity', d => 0.5 + 0.5 * d[3]);

        const cost = svg.append('path')
            .attr('fill', '#aaa');            

        let pos = data.map(d => [Math.random() - 0.5, Math.random() - 0.5]);
        let costs = [];

        const voronoi = d3.voronoi().extent([[-1, -1], [1, 1]]);

        const timer = d3.timer(function () {
            let p = voronoi.polygons(pos);
            polygons.attr('d', (d, i) => p[i] ? "M" + (p[i]).map(d => d.map(x)).join("L") + "Z" : null);
          cost.attr('d', area(costs));
        });

        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.iterations >= 399) timer.stop();
            }
        };
        worker.postMessage({
            nIter: 400,
            // dim: 2,
            perplexity: 20.0,
            // earlyExaggeration: 4.0,
            // learningRate: 100.0,
            // metric: 'euclidean',
            data: data
        });

    });
  </script>
</body>

worker.js

self.onmessage = function(e){
  var msg = e.data;

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

 model.on('progressIter', function (iter) {
  self.postMessage({
    iterations: iter[0],
    cost: iter[1]
  });
});
 
  model.run();
};