block by HarryStevens 545ca9d50cb9abbd68bfee526b0541f9

Treemap Update Pattern

Full Screen

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

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      body {
        margin: 0;
        font-family: "Helvetica Neue", sans-serif;
      }
    </style>
  </head>
  <body>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script>
      const margin = {top: 0, right: 0, bottom: 0, left: 0},
          aspect = .85,
          minHeight = 400,
          duration = 1000,
          categories = "abcdef".split(""),
          colors = {};

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

      const treemap = d3.treemap()
          .padding(1)
          .round(true);

      const svg = d3.select("body").append("svg");
    
      const g = svg.append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

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

      draw();
      onresize = _ => draw(true);

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

        draw();
      }, duration * 2);

      function draw(resizing){
        width = innerWidth - margin.left - margin.right;
        let baseHeight = innerWidth * aspect;
        baseHeight = baseHeight < minHeight ? minHeight : baseHeight > innerHeight ? innerHeight : baseHeight;
        height = baseHeight - 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})`);

        treemap.size([width, height]);

        const leaves = treemap(root).leaves();
      
        const rects = svg.selectAll(".rect")
            .data(leaves, d => d.data.name);

        if (resizing){
          rects.exit().remove();

          rects
              .attr("transform", d => `translate(${d.x0},${d.y0})`)
              .attr("width", d => d.x1 - d.x0)
              .attr("height", d => d.y1 - d.y0);
        } else {
          rects.exit()
              .style("opacity", 1)
            .transition().duration(duration)
              .style("opacity", 1e-6)
              .remove();

          rects.transition().duration(duration)
              .attr("transform", d => `translate(${d.x0},${d.y0})`)
              .attr("width", d => d.x1 - d.x0)
              .attr("height", d => d.y1 - d.y0);
        }

        rects.enter().append("rect")
            .attr("class", "rect")
            .style("fill", d => colors[d.parent.data.name])
            .attr("transform", d => `translate(${d.x0},${d.y0})`)
            .attr("width", d => d.x1 - d.x0)
            .attr("height", d => d.y1 - d.y0)
            .style("opacity", 1e-6)
          .transition().duration(duration)
            .style("opacity", 1);

        const labels = svg.selectAll(".label")
            .data(leaves.filter(f => f.x1 - f.x0 > 60 && f.y1 - f.y0 > 30), d => d.data.name);

        if (resizing){
          labels.exit().remove();

          labels
              .html(d => `<tspan style='font-weight: 500'>${d.data.name}</tspan><tspan dx=10>${d.data.value}</tspan>`)
              .attr("transform", d => `translate(${d.x0}, ${d.y0})`);          
        } else {
          labels.exit()
              .style("opacity", 1)
            .transition().duration(duration)
              .style("opacity", 1e-6)
              .remove();

          labels
              .html(d => `<tspan style='font-weight: 500'>${d.data.name}</tspan><tspan dx=10>${d.data.value}</tspan>`)
            .transition().duration(duration)
              .attr("transform", d => `translate(${d.x0}, ${d.y0})`); 
        }
        
        labels.enter().append("text")
            .attr("class", "label")
            .attr("dy", 16)
            .attr("dx", 5)
            .attr("transform", d => `translate(${d.x0}, ${d.y0})`)
            .html(d => `<tspan style='font-weight: 500'>${d.data.name}</tspan><tspan dx=10>${d.data.value}</tspan>`)
            .style("opacity", 1e-6)
          .transition().duration(duration)
            .style("opacity", 1);
      }

      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>