Fruchterman and Reingold Algorithm

const jiggle = () => (Math.random() - 0.5) * 1e-4;
const initDisplacements = (disp) => {
  for (let d of disp) {
    d.x = 0;
    d.y = 0;
    d.d = 0;
  }
};
const distance = (n1, n2) => {
  let dx = n2.x - n1.x;
  let dy = n2.y - n1.y;
  if (dx === 0) dx = jiggle();
  if (dy === 0) dy = jiggle();
  const l = Math.sqrt(dx * dx + dy * dy);
  return { x: dx / l, y: dy / l, d: l };
};
const attractingForce = (K, nodes, e, disp) => {
  const d = distance(nodes[e.source], nodes[e.target]); // vector pointing from n1 to n2
  const delta = d.d;
  const fa = (delta * Math.abs(delta)) / K;
  disp[e.source].x += fa * d.x;
  disp[e.source].y += fa * d.y;
  disp[e.target].x -= fa * d.x;
  disp[e.target].y -= fa * d.y;
};
const repulsiveForce = (K, nodes, i, j, disp) => {
  const d = distance(nodes[i], nodes[j]); // vector pointing from nodes[i] to nodes[j]
  const fr = (K * K) / d.d;
  disp[i].x -= fr * d.x;
  disp[i].y -= fr * d.y;
  disp[j].x += fr * d.x;
  disp[j].y += fr * d.y;
};
const step = (K, nodes, edges, bbox, disp, alpha) => {
  initDisplacements(disp);
  // compute displacements from repelling forces
  nodes.forEach((n, i) => {
    for (let j = i + 1; j < nodes.length; j++) {
      repulsiveForce(K, nodes, i, j, disp);
    }
  });
  // compute displacements from attracting forces
  edges.forEach((e) => {
    attractingForce(K, nodes, e, disp);
  });
  // update nodes position
  nodes.forEach(function (n, i) {
    const dx = disp[n.id].x;
    const dy = disp[n.id].y;
    const d = Math.sqrt(dx * dx + dy * dy);
    n.x += (dx / d) * Math.min(alpha, d);
    n.y += (dy / d) * Math.min(alpha, d);
    if (n.x < bbox.xmin) n.x = bbox.xmin;
    if (n.x > bbox.xmax) n.x = bbox.xmax;
    if (n.y < bbox.ymin) n.y = bbox.ymin;
    if (n.y > bbox.ymax) n.y = bbox.ymax;
  });
};
const fruchtermanReingold = (width, height, nodes, edges, bbox) => {
  const alphaMin = 15;
  const nrIterations = 200;
  const C = 0.46;
  const K = C * Math.sqrt((width * height) / nodes.length);
  const alphaDecay = (1 + alphaMin) / nrIterations;
  let alpha = 500;
  const disp = [];
  nodes.forEach((n, index) => disp.push({ d: 0, x: 0, y: 0, id: index }));
  let counter = 0;
  while (alpha >= alphaMin) {
    // one simulation step
    step(K, nodes, edges, bbox, disp, alpha);
    // cooling
    alpha -= alphaDecay;
    counter++;
  }
  return counter;
};