Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active July 2, 2019 10:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kcnarf/5d4f70d906bc2cc84cd8d7b2559a49c0 to your computer and use it in GitHub Desktop.
Save Kcnarf/5d4f70d906bc2cc84cd8d7b2559a49c0 to your computer and use it in GitHub Desktop.
Voronoï <-> Voronoï treemap transition with flubber
license: lgpl-3.0

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.

{
"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"}
]
}
]
}
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment