block by fil 0d0f6a01385eb4133fe40aff6e7ad159

d3 Voronoi Worker

Full Screen

This block implements two independent processes:

A SVG overlay tracks the mouse: it stays responsive even if the voronoi calculation is sluggish.

We watch the performance of both processes and display their fps rates. Tweak the data range and time intervals to see how they relate.

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; }
    div#fps,svg { position: fixed; top:0; left:0; color: white; }
  </style>
</head>

<body>
  <canvas width=960 height=500 />
  <script> 
const canvas = d3.select("canvas"),
      width = canvas.attr('width'),
      height = canvas.attr('height'),
      context = canvas.node().getContext("2d"),
      color = d3.scaleLinear().range(["brown", "steelblue"]);

const tracker = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "none")
.append('circle')
.attr("r", 10)
.attr("stroke", "white")
.attr("fill", "none");


    
const fpsdiv = d3.select('body').append('div').attr('id','fps'); 
let data = d3.range(1000)
    .map(d => [Math.random()*width, Math.random()*height, Math.random()]);

let voronoi, workervoronoi;

let fps = [0,0,0],
    last = performance.now();
setInterval(() => {
  fps[2]++;
  let now = performance.now();
  fps = fps.map(d => d * Math.pow(0.9, (now-last)/1000));
  //fpsdiv.html('fps: ' + fps.map(d => Math.round(d * 10 * 5 / fps[2])/10));
  fpsdiv.html('FPS<br>data: ' + Math.round(fps[0] * 10 * 5 / fps[2])/10 + '<br>' +  'image: ' + Math.round(fps[1] * 10 * 5 / fps[2])/10);
  last = now;
}, 200);

    
d3.interval(() => {
  data.forEach((d,i) => {
    if (i===0) return;
    data[i][0] += Math.random() - Math.random();
    data[i][1] += Math.random() - Math.random();
  });
  fps[0]++;
}, 100);

canvas.on('mousemove', function() {
   data[0] = d3.mouse(this);
  tracker
    .attr('cx', data[0][0])
    .attr('cy', data[0][1]);
});

d3.interval(draw, 100);

let radius = 10;

function draw() {
  if (typeof workervoronoi == 'function' && !workervoronoi.busy) {
    workervoronoi.busy = true;
    workervoronoi(data, function(polygons){
      polygons.forEach( (d) => {
        var c = d.data[2] ? color(d.data[2]) : 'white';
        context.beginPath();
        context.fillStyle = c;
        drawCell(d);
        context.fill();
      });
      workervoronoi.busy = false;
      fps[1]++;
    });
  }

}

    
createWorker('worker.js', (worker) => {
  workervoronoi = function(data, callback){
     worker.postMessage({
           data: data,
           extent: [[-1,-1], [width+1, height+1]],
        });

     worker.onmessage = function (e) {
        if (e.data.polygons) {
           callback(e.data.polygons);
        }
     }
  }
});


    
function drawCell(cell) {
  if (!cell) return false;
  context.moveTo(cell[0][0], cell[0][1]);
  for (var j = 1, m = cell.length; j < m; ++j) {
    context.lineTo(cell[j][0], cell[j][1]);
  }
  context.closePath();
  return true;
}

// this is for blockbuilder
function createWorker(s, cb) {
  d3.queue()
  .defer(d3.text, s)
  .awaitAll(function (err, scripts) {

  const worker = new Worker(window.URL.createObjectURL(new Blob(scripts, {
     type: "text/javascript"
  })));
  if (typeof cb == 'function') {
    cb(worker);
  }

  });
}

  
  </script>
</body>

worker.js

importScripts('https://d3js.org/d3.v4.min.js');

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

  let voronoi = d3.voronoi().extent(msg.extent);

  self.postMessage({
    polygons: voronoi.polygons(msg.data)
  });

};