An implementation of Danny Holten‘s hierarchical edge bundling algorithm in D3, showing dependencies between classes in a software class hierarchy. Dependencies are bundled according to the parent packages. This example uses two layouts: a radial d3.layout.treemap to position the tree nodes, and d3.layout.bundle to group the dependencies into spline bundles.
Compare to this radial layout.
<!DOCTYPE html>
<meta charset="utf-8">
<title>Hierarchical Edge Bundling (Treemap)</title>
<style>
.cell {
border: solid 1px white;
font: 10px sans-serif;
line-height: 12px;
overflow: hidden;
position: absolute;
text-indent: 2px;
}
.link {
stroke: #000;
stroke-opacity: .5;
fill: none;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500;
var fill = d3.scale.ordinal()
.range(["#f0f0f0", "#d9d9d9", "#bdbdbd"]);
var stroke = d3.scale.linear()
.domain([0, 1e4])
.range(["brown", "steelblue"]);
var treemap = d3.layout.treemap()
.size([width, height])
.value(function(d) { return d.size; });
var bundle = d3.layout.bundle();
var div = d3.select("body").append("div")
.style("position", "relative")
.style("width", width + "px")
.style("height", height + "px");
var line = d3.svg.line()
.interpolate("bundle")
.tension(.85)
.x(function(d) { return d.x + d.dx / 2; })
.y(function(d) { return d.y + d.dy / 2; });
d3.json("readme-flare-imports.json", function(error, classes) {
if (error) throw error;
var nodes = treemap.nodes(packages.root(classes)),
links = packages.imports(nodes);
div.selectAll(".cell")
.data(nodes)
.enter().append("div")
.attr("class", "cell")
.style("background-color", function(d) { return d.children ? fill(d.key) : null; })
.call(cell)
.text(function(d) { return d.children ? null : d.key; });
div.append("svg")
.attr("width", width)
.attr("height", height)
.style("position", "absolute")
.selectAll(".link")
.data(bundle(links))
.enter().append("path")
.attr("class", "link")
.attr("d", line)
.style("stroke", function(d) { return stroke(d[0].value); });
});
function cell() {
this.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return d.dx - 1 + "px"; })
.style("height", function(d) { return d.dy - 1 + "px"; });
}
var packages = {
// Lazily construct the package hierarchy from class names.
root: function(classes) {
var map = {};
function find(name, data) {
var node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}
classes.forEach(function(d) {
find(d.name, d);
});
return map[""];
},
// Return a list of imports for the given array of nodes.
imports: function(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.imports) d.imports.forEach(function(i) {
imports.push({source: map[d.name], target: map[i]});
});
});
return imports;
}
};
</script>