block by mbostock 4341134

Hierarchical Edge Bundling

Full Screen

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.

index.html

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