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 ...
Data from this block.
xxxxxxxxxx
<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>
Updated missing url https://raw.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.1/build/d3-weighted-voronoi.js to https://raw.githack.com/kcnarf/d3-weighted-voronoi/v1.0.1/build/d3-weighted-voronoi.js
Updated missing url https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v1.2.0/build/d3-voronoi-map.js to https://rawcdn.githack.com/kcnarf/d3-voronoi-map/v1.2.0/build/d3-voronoi-map.js
Updated missing url https://rawcdn.githack.com/Kcnarf/d3-voronoi-treemap/v1.1.0/build/d3-voronoi-treemap.js to https://rawcdn.githack.com/kcnarf/d3-voronoi-treemap/v1.1.0/build/d3-voronoi-treemap.js
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.3/seedrandom.min.js
https://raw.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.1/build/d3-weighted-voronoi.js
https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v1.2.0/build/d3-voronoi-map.js
https://rawcdn.githack.com/Kcnarf/d3-voronoi-treemap/v1.1.0/build/d3-voronoi-treemap.js
https://unpkg.com/flubber@0.3.0