block by Kcnarf 5d4f70d906bc2cc84cd8d7b2559a49c0

Voronoï <-> Voronoï treemap transition with flubber

Full Screen

This block (a continuation of a previous one) experiments the use of @veltman‘s flubber d3 plugin in order to transition back and forth between 2 Voronoï treemaps (computed thanks to @kcnarf‘s d3-voronoi-treemap plugin).

The current result is not as satifying as expected, because (sometimes) some cells may move to one place to another one, making the overall animation not smooth/stable at all. This phenomenon appears almost everytime for inner cells. This has to be investigate …

Acknowledgments to :

Data from this block.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>d3-voronoi-treemap</title>
    <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.3/seedrandom.min.js"></script>
    <script src="https://raw.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.1/build/d3-weighted-voronoi.js"></script>
    <script src="https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v1.2.0/build/d3-voronoi-map.js"></script>
    <script src="https://rawcdn.githack.com/Kcnarf/d3-voronoi-treemap/v1.1.0/build/d3-voronoi-treemap.js"></script>
    <script src="https://unpkg.com/flubber@0.3.0"></script>
    <style>
      path {
        stroke: white;
        stroke-width: 1px;
      }

      .control {
        position: absolute;
      }
      .control.top {
        top: 5px;
      }
      .control.bottom {
        bottom: 5px;
      }
      .control.left {
        left: 5px;
      }
      .control.right {
        right: 5px;
      }
      .control.right div {
        text-align: right;
      }
      .control.left div {
        text-align: left;
      }
      .control .separator {
        height: 5px;
      }
    </style>
  </head>
  <body>
    <svg></svg>
    <div class="control bottom right">
      <div>
        Show inner cells
        <input id="showInnerCells" type="checkbox" name="showInnerCell" onchange="InnerCellVisibilityUpdated()" />
      </div>
    </div>
    <script>
      //begin: global data
      const weightAlteringAmplitude = 10;
      let svg, hierarchy, hierarchy2;
      //end: global data
      
      //begin: drawing conf.
      var width = 960,
          height = 500,
          radius = 200,
          showInnerCells = false;
      //end: drawing conf.
      
      
      //begin: user interaction handlers
      function InnerCellVisibilityUpdated() {
        showInnerCells = d3.select('#showInnerCells').node().checked;
        restart();
      }
      //end: user interaction handlers
      
      svg = d3
        .select('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + [width / 2 - radius, height / 2 - radius] + ')');

      d3.json('globalEconomyByGDP.json', function(error, rootData) {
        if (error) throw error;

        //create a second hierarchy, a copy of the first one
        //d3-voronoi-treemap add attributes and pointers to/from polygons and original data
        //in this block, we transition between 2 sets of weights, so wee need 2 hierarchies
        const clonedRootData = JSON.parse(JSON.stringify(rootData));

        //compute 2 hierarchies
        hierarchy = d3.hierarchy(rootData);
        hierarchy2 = d3.hierarchy(clonedRootData);
        
        //add some extra data to hierarchy
        hierarchy.id = 1;
        hierarchy2.id = 2;

        //begin: sort data so that Voronoï cells stay more or less at the same place
        //DOES NOT WORK :-(; seems OK for top-level cells, but not for nested ones; don't know why
        hierarchy.sum(function(d) {
          return d.weight;
        });
        hierarchy2.sum(function(d) {
          return d.weight;
        });
        //below, cf. https://github.com/d3/d3-hierarchy#node_sort
        const sorter = function(a, b) {
          return b.value - a.value;
        };
        hierarchy.sort(sorter);
        hierarchy2.sort(sorter);
        //end: sort data so that Voronoï cells stay more or less at the same place

        //compute slightly different weights between the 2 hierarchies
        alterWeights(hierarchy, weightAlteringAmplitude);
        alterWeights(hierarchy2, weightAlteringAmplitude);

        //sum up weights for each node of the hierarchy; needed by Voronoï treemap
        hierarchy.sum((d) => d.alteredWeight );
        hierarchy2.sum((d) => d.alteredWeight );

        //computation of the 2 Voronoï tessellations
        computeVoronoi(hierarchy);
        computeVoronoi(hierarchy2);
        
        restart();
      });
      
      function restart() {
        //computation of the 2 sets of polygons we will transition back and forth
        var voroPolies1 = getPolygons(hierarchy, showInnerCells);
        var voroPolies2 = getPolygons(hierarchy2, showInnerCells);
        
        const animationPairs = computeAnimationPairs(voroPolies1, voroPolies2)
        svg.selectAll('path').remove();
        svg
          .selectAll('path')
          .data(animationPairs)
          .enter()
          .append('path')
          .style('fill', function(d) {
          return d.color;
        })
          .call(animate, true);
      }

      //definition of the Flubber's interpolator between each pair of cells
      function computeAnimationPairs(voroPolies1, voroPolies2) {
        var paired = d3
        .nest()
        .key(function(d) {
          return d.name;
        })
        .key(function(d) {
          return 'fromVoroPolies' + d.hierarchyIndex;
        })
        .rollup(values => values[0])
        .object(voroPolies1.concat(voroPolies2));

        return d3.values(paired).map(function(value) {
          return {
            color: value.fromVoroPolies1.color,
            interpolator: flubber.interpolate(value.fromVoroPolies1.polygon, value.fromVoroPolies2.polygon)
          };
        });
      }

      //use of fubber's interpolation (depending on elapsed time)
      function animate(cells, direction) {
        cells
          .attr('d', function(d) {
            return d.interpolator(direction ? 0 : 1);
          })
          .transition()
          .delay(500)
          .duration(1000)
          .attrTween('d', function(d) {
            return direction
              ? d.interpolator
              : function(t) {
                  return d.interpolator(1 - t);
                };
          })
          .filter(function(d, i) {
            return !i;
          })
          .on('end', function() {
            cells.call(animate, !direction);
          });
      }
      
      function computeVoronoi(hierarchy) {
        //for reproducibility purpose, use (and reset) a seedable pseudo random number generator
        var myseededprng = new Math.seedrandom('my seed'); // (from seedrandom's doc) Use "new" to create a local prng without altering Math.random
        d3
          .voronoiTreemap()
          .prng(myseededprng) 
          .clip(
            d3.range(0, 2 * Math.PI, Math.PI / 30).map(function(a) {
              return [radius + radius * Math.cos(a), radius + radius * Math.sin(a)];
            })
          )(hierarchy);
      }

      //get top-level polygons, or leaf polygons
      function getPolygons(hierarchy, showInnerCells) {
        if (showInnerCells) {
          return hierarchy.leaves().map(function(d) {
            return {
              name: d.data.name,
              polygon: d.polygon,
              color: d.parent.data.color,
              hierarchyIndex: hierarchy.id
            };
          });
        } else {
          //show top-level cells
          return hierarchy.children.map(function(d) {
            return {
              name: d.data.name,
              polygon: d.polygon,
              color: d.data.color,
              hierarchyIndex: hierarchy.id
            };
          });
        }
      }

      //slightly modify weights to obtain 2 slightly similar Voronoï tesselations
      function alterWeights(hierarchy, alteringAmplitude) {
        hierarchy.each(n => {
          n.data.alteredWeight = n.data.weight + alteringAmplitude * Math.random();
        });
      }
    </script>
  </body>
</html>

globalEconomyByGDP.json

{
  "name": "world",
  "children": [
    {
      "name": "Asia",
      "color": "#f58321",
      "children": [
        {"name": "China", "weight": 14.84, "code": "CN"},
        {"name": "Japan", "weight": 5.91, "code": "JP"},
        {"name": "India", "weight": 2.83, "code": "IN"},
        {"name": "South Korea", "weight": 1.86, "code": "KR"},
        {"name": "Russia", "weight": 1.8, "code": "RU"},
        {"name": "Indonesia", "weight": 1.16, "code": "ID"},
        {"name": "Turkey", "weight": 0.97, "code": "TR"},
        {"name": "Saudi Arabia", "weight": 0.87, "code": "SA"},
        {"name": "Iran", "weight": 0.57, "code": "IR"},
        {"name": "Thaïland", "weight": 0.53, "code": "TH"},
        {"name": "United Arab Emirates", "weight": 0.5, "code": "AE"},
        {"name": "Hong Kong", "weight": 0.42, "code": "HK"},
        {"name": "Israel", "weight": 0.4, "code": "IL"},
        {"name": "Malasya", "weight": 0.4, "code": "MY"},
        {"name": "Singapore", "weight": 0.39, "code": "SG"},
        {"name": "Philippines", "weight": 0.39, "code": "PH"}
      ]
    },
    {
      "name": "North America",
      "color": "#ef1621",
      "children": [
        {"name": "United States", "weight": 24.32, "code": "US"},
        {"name": "Canada", "weight": 2.09, "code": "CA"},
        {"name": "Mexico", "weight": 1.54, "code": "MX"}
      ]
    },
    {
      "name": "Europe",
      "color": "#77bc45",
      "children": [
        {"name": "Germany", "weight": 4.54, "code": "DE"},
        {"name": "United Kingdom", "weight": 3.85, "code": "UK"},
        {"name": "France", "weight": 3.26, "code": "FR"},
        {"name": "Italy", "weight": 2.46, "code": "IT"},
        {"name": "Spain", "weight": 1.62, "code": "ES"},
        {"name": "Netherlands", "weight": 1.01, "code": "NL"},
        {"name": "Switzerland", "weight": 0.9, "code": "CH"},
        {"name": "Sweden", "weight": 0.67, "code": "SE"},
        {"name": "Poland", "weight": 0.64, "code": "PL"},
        {"name": "Belgium", "weight": 0.61, "code": "BE"},
        {"name": "Norway", "weight": 0.52, "code": "NO"},
        {"name": "Austria", "weight": 0.51, "code": "AT"},
        {"name": "Denmark", "weight": 0.4, "code": "DK"},
        {"name": "Ireland", "weight": 0.38, "code": "IE"}
      ]
    },
    {
      "name": "South America",
      "color": "#4aaaea",
      "children": [
        {"name": "Brazil", "weight": 2.39, "code": "BR"},
        {"name": "Argentina", "weight": 0.79, "code": "AR"},
        {"name": "Venezuela", "weight": 0.5, "code": "VE"},
        {"name": "Colombia", "weight": 0.39, "code": "CO"}
      ]
    },
    {
      "name": "Australia",
      "color": "#00acad",
      "children": [
        {"name": "Australia", "weight": 1.81, "code": "AU"}
      ]
    },
    {
      "name": "Africa",
      "color": "#f575a3",
      "children": [
        {"name": "Nigeria", "weight": 0.65, "code": "NG"},
        {"name": "Egypt", "weight": 0.45, "code": "EG"},
        {"name": "South Africa", "weight": 0.42, "code": "ZA"}
      ]
    },
    {"name": "Rest of the World",
      "color": "#592c94",
      "children": [
        {"name": "Rest of the World", "weight": 9.41, "code": "RotW"}
      ]
    }
  ]
}