block by harrystevens 4fba7a62b0ff302ef49768198d4c54c6

Circle Pack Update Pattern

Full Screen

Using D3’s general update pattern to create a randomly updating circle pack.

index.html

<!DOCTYPE html>
<html>
  <head>
    <style>
      body {
        margin: 0;
      }
      svg {
        margin: 0 auto;
        display: table;
      }
      .ancestor-circle {
        fill-opacity: .25;
        stroke-width: 2px;
      }
      .leaf-circle {
        fill-opacity: .8;
        stroke-width: 1.5px;
      }
    </style>
  </head>
  <body>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script>
      const margin = {left: 0, right: 0, top: 0, bottom: 0},
          duration = 1000,
          categories = "abcdefg".split(""),
          colors = {};

      categories.forEach((d, i) => {
        colors[d] = d3.schemeSet2[i];
      });

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

      const pack = d3.pack()
          .padding(d => d.height * 5);

      let root = d3.hierarchy(makeData())
          .sum(d => d.value)
          .sort((a, b) => a.value - b.value);

      draw();
      d3.interval(_ => {
        root = d3.hierarchy(makeData())
          .sum(d => d.value)
          .sort((a, b) => a.value - b.value);

        draw();
      }, duration * 2);
      onresize = _ => draw(true);

      function draw(resizing){
        const diameter = Math.min(innerWidth, innerHeight),
            width = diameter - margin.left - margin.right,
            height = diameter - margin.top - margin.bottom;

        svg
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom);

        g
            .attr("transform", `translate(${margin.left}, ${margin.top})`);

        pack
            .size([width, height]);

        const node = pack(root),
            leaves = node.leaves(),
            ancestors = node.ancestors();

        const ancestorCircles = g.selectAll(".ancestor-circle")
            .data(ancestors[0].children, d => d.data.name);

        if (resizing){
          ancestorCircles
              .attr("cx", d => d.x)
              .attr("cy", d => d.y)
              .attr("r", d => d.r);
        }
        else {
          ancestorCircles
            .transition().duration(duration)
              .attr("cx", d => d.x)
              .attr("cy", d => d.y)
              .attr("r", d => d.r);          
        }

        ancestorCircles.enter().append("circle")
            .style("fill", d => colors[d.data.name])
            .style("stroke", d => colors[d.data.name])
            .attr("class", "ancestor-circle")
            .attr("cx", d => d.x)
            .attr("cy", d => d.y)
            .attr("r", 1e-6)
          .transition().duration(duration / 2)
            .attr("r", d => d.r);

        const leafCircles = g.selectAll(".leaf-circle")
            .data(leaves, d => d.data.name);

        if (resizing){
          leafCircles.exit()
              .attr("r", 1e-6)
              .remove();

          leafCircles
              .attr("cx", d => d.x)
              .attr("cy", d => d.y)
              .attr("r", d => d.r);
        }
        else {
          leafCircles.exit()
            .transition().duration(duration / 2)
              .attr("r", 1e-6)
              .remove();

          leafCircles
            .transition().duration(duration)
              .attr("cx", d => d.x)
              .attr("cy", d => d.y)
              .attr("r", d => d.r);
        }

        leafCircles.enter().append("circle")
            .style("fill", d => colors[d.parent.data.name])
            .style("stroke", d => colors[d.parent.data.name])
            .attr("class", "leaf-circle")
            .attr("cx", d => d.x)
            .attr("cy", d => d.y)
            .attr("r", 1e-6)
          .transition().duration(duration)
            .attr("r", d => d.r);
      }

      // Generate random hierarchical data
      function makeData(){
        return {
          name: "root",
          children: categories.map(name => {
            return {
              name,
              children: d3.range(randBetween(5, 10)).map((d, i) => {
                return {
                  name: `${name}${i}`,
                  value: randBetween(10, 100)
                }
              })
            }
          })
        };
      }

      function randBetween(min, max){
        return Math.floor(Math.random() * (max - min + 1) + min);
      }
    </script>
  </body>
</html>