Skip to content

D3 Simulation - Conexion de puntos por proximidad

Posted on:January 5, 2023

D3 Simulation

Canvas o Svg?

El primer planteamiento es si usar canvas o svg. Ambos tienen sus ventajas y desventajas. Por un lado Canvas es más eficiente ya que no tenemos que trabajar con d3-select para todos los elementos. Tambien nos evita tener en variables las lineas que conectan los circulos. Sin embargo implementar el hover y un fade-in fade-out es más complicado. Svg por otro nos soluciona facilmente el control del hover y los fades de entrada y salida, pero sin embargo para un gran número de elementos es costoso, pues debe tener n elementos en el DOM y manejar sus atributos

En este caso Canvas es una mejor solucion, pues queremos tener un gran número de elementos

Implementación

Para su implementación usaremos d3@6.

definimos un height y un width

Para crear las particulas:

const particles = new Array(n);
  for (let i = 0; i < n; ++i) {
    particles[i] = {
      x: Math.random() * width,
      y: Math.random() * height,
      radius: Math.abs(d3.randomNormal(2, 3)()),
      vx: Math.sign(Math.random() - 0.5) * d3.randomNormal(1, 1)(),
      vy: Math.sign(Math.random() - 0.5) * d3.randomNormal(1, 1)()
    };
  }

Como queremos que nuestras simulación no pare nunca ponemos la velocityDecay y el alphaDecay a 0.

const simulation = d3
  .forceSimulation(particles)
  .velocityDecay(0)
  .alphaDecay(0)

En este caso quería que las bolas pasaran de un lado a otro como si fuese un portal. Para hacer esto he hecho una fuerza que si sobrepasa el tamaño del width o height entonces aplica el módulo. De esta forma nos garantiza que siempre estará el circulo dentro del canvas:

function mod(n, m) {
  if (n > 0) return n % m;
  else return (n + m * (Math.trunc(n / m) + 1)) % m;
}
function forceBorderModule(width, height) {
  var _nodes = [];
  function force(alpha) {
    _nodes.forEach((n) => {
      n.x = mod(n.x, width);
      n.y = mod(n.y, height);
    });
  }
  function initialize(nodes) {
    _nodes = nodes;
  }
  force.initialize = initialize;
  return force;
}

Por lo que nuestra simulación queda así:

const simulation =d3
  .forceSimulation(particles)
  .alphaDecay(0)
  .velocityDecay(0)
  .force("module", forceBorderModule(width, height));

Para dibujar:

function drawCircle(n){
  context.beginPath();
  context.arc(n.x, n.y, n.radius, 0, 2 * Math.PI, false);
  context.stroke();
}
simulation.on("tick", tick);
function tick() {
  context.clearRect(0, 0, width, height);
  particles.forEach((n) => drawCircle(m))
}

Para identificar que circulos están cerca de otro circulo vamos a usar el (algoritmo de quadtree)[https://en.wikipedia.org/wiki/Quadtree]. Se basa en trocear el espacio buscando aquellos elementos dentro de ese espacio. Si encuentra más de un elemento en un trozo, lo vuelve a trocear hasta encontrar únicamente uno. d3 nos facilita de una librería para relaizar esto:

const tree = d3
  .quadtree()
  .x((n) => n.x)
  .y((n) => n.y)
  .addAll(particles);
tree.visit((visited, x0, y0, x1, y1) => {
  // visited.data elemento, si hay
  // x0,y0,x1,y1 rectangulo en el que se encuentra
})

Por lo que para encontrar los elementos que se encuentran a una distancia r definimos:

const radius = 40
function drawCircle(n){
 context.beginPath();
 context.arc(n.x, n.y, n.radius, 0, 2 * Math.PI, false);
 context.stroke();
}
function drawLine(n1,n2){
 context.beginPath();
 context.moveTo(n1.x, n1.y);
 context.lineTo(n2.x, n2.y);
 context.stroke();
}
simulation.on("tick", tick);
function tick() {
 context.clearRect(0, 0, width, height);
 particles.forEach((n) => {
     tree.visit((visited, x0, y0, x1, y1) => {
       if (visited.data && visited.data !== n) {
         const d = Math.hypot(n.x - visited.data.x, n.y - visited.data.y);
         if (d < radius) {
           drawLine(n,visited.data)
         }
       }
     });
     drawCircle(n)
 })
}

Codigo completo