block by harrystevens 6f001c74bff49ae083d2752af4e95dff

Grouped Force

Full Screen

Use d3.scaleBand and d3-force to group nodes in a force simulation. The number of columns is dependent upon the width of the browser window.

index.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
    }
  </style>
</head>
<body>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script>
    let firstDraw = true, simulation = null, tickCount = 0;
    const groups = "abcdefghijkl".split("").map(letter => ({
      letter,
      data: d3.range(0, randBetween(5, 10)).map((d, i) => ({
        id: `${letter}-${i}`,
        value: randBetween(2, 20)
      }))
    }));
    const flat = flatten(groups.map(d => d.data));

    const xAccessor = d => x(d.col) + x.bandwidth();
    const yAccessor = d => y(d.row) + y.bandwidth() / 2;

    const x = d3.scaleBand();
    const y = d3.scaleBand();
    const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"];

    let width, height, cols;

    const svg = d3.select("body").append("svg");

    const nodes = svg.selectAll("g")
        .data(flat)
      .enter().append("g");

    const circles = nodes.append("circle")
        .attr("r", d => d.value);

    draw();
    addEventListener("resize", draw);
    function draw(){
      tickCount = 0;
      if (simulation) simulation.stop();

      width = innerWidth;
      height = innerHeight;
      cols = width < 600 ? 3 : 6;

      svg
          .attr("width", width)
          .attr("height", height);

      groups.forEach((d, i) => {
        d.row = Math.floor(i / cols);
        d.col = i % cols;
        d.data.forEach(d0 => {
          d0.row = d.row;
          d0.col = d.col;
        });        
      });

      x
          .domain(d3.range(0, cols + 1))
          .range([0, width]);

      y
          .domain(d3.range(0, d3.max(flat, d => d.row) + 1))
          .range([0, height]);

      circles
          .style("fill", d => colors[d.col]);

      simulation = d3.forceSimulation(flat)
          .force("x", d3.forceX(firstDraw ? width / 2 : xAccessor))
          .force("y", d3.forceY(firstDraw ? height / 2 : yAccessor))
          .force("collide", d3.forceCollide(d => d.value + 1))
          .alphaTarget(.5)
          .on("tick", tick);
          
      // Initialize with 50 ticks
      if (firstDraw){
        simulation.stop();
        for (let i = 0; i < 50; i++) simulation.tick();
        simulation.restart();
        firstDraw = false;
      }

    }

    function tick(){
      tickCount++;

      if (tickCount === 1){
        simulation
            .force("x", d3.forceX(xAccessor))
            .force("y", d3.forceY(yAccessor))      
      }

      nodes
          .attr("transform", d => `translate(${d.x}, ${d.y})`);
    }

    function flatten(arr){
      return [].concat.apply([], arr);
    }
    function randBetween(min, max){
      return Math.floor(Math.random() * (max - min + 1) + min);
    }
  </script>

</body>
</html>