|
<!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> |