Skip to content

Commit

Permalink
Hierarchical edge bundling improvements.
Browse files Browse the repository at this point in the history
The input to the layout is now an array of edges to bundle, rather than nodes.
This eliminates the need for an `outgoing` accessor, since the links are passed
to the bundle layout directly.

The svg line generator now supports a beta (straightening; bundle strength)
parameter. I haven't decided if this is the right place or the right name for
it, but it seems like a reasonable starting point. I'm not happy with the cos &
sin needed to produce radial lines (both here and for the diagonal projection in
other examples), but I don't have a good alternative yet.

This commit also tries to make the construction of the links from the layout
nodes a bit easier to follow. The previous code was used another intermediate
representation, and I think it's cleaner to construct the default format
expected by the layouts. However, there's still a good chunk of code required to
massage the JSON format into a node hierarchy and array of dependencies, so I'd
like to find a way to simplify that, too.
  • Loading branch information
mbostock committed Jun 22, 2011
1 parent 65a2370 commit 0aba070
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 189 deletions.
29 changes: 27 additions & 2 deletions d3.js
Expand Up @@ -2718,11 +2718,12 @@ d3.svg.line = function() {
y = d3_svg_lineY,
interpolate = "linear",
interpolator = d3_svg_lineInterpolators[interpolate],
tension = .7;
tension = .7,
beta = 1;

function line(d) {
return d.length < 1 ? null
: "M" + interpolator(d3_svg_linePoints(this, d, x, y), tension);
: "M" + interpolator(d3_svg_lineStraighten(d3_svg_linePoints(this, d, x, y), beta), tension);
}

line.x = function(v) {
Expand All @@ -2749,9 +2750,33 @@ d3.svg.line = function() {
return line;
};

line.beta = function(v) {
if (!arguments.length) return beta;
beta = v;
return line;
};

return line;
};

function d3_svg_lineStraighten(points, beta) {
var n = points.length - 1,
x0 = points[0][0],
y0 = points[0][1],
dx = points[n][0] - x0,
dy = points[n][1] - y0,
i = -1,
p,
t;
while (++i <= n) {
p = points[i];
t = i / n;
p[0] = beta * p[0] + (1 - beta) * (x0 + t * dx);
p[1] = beta * p[1] + (1 - beta) * (y0 + t * dy);
}
return points;
}

// Converts the specified array of data into an array of points
// (x-y tuples), by evaluating the specified `x` and `y` functions on each
// data point. The `this` context of the evaluated functions is the specified
Expand Down
45 changes: 11 additions & 34 deletions d3.layout.js
Expand Up @@ -4,42 +4,26 @@
// the parent hierarchy to the least common ancestor, and then back down to the
// destination node.
d3.layout.bundle = function() {
var beta = .85,
outgoing = d3_layout_bundleOutgoing;

function bundle(nodes) {
return function(links) {
var splines = [],
i = -1,
n = nodes.length;
while (++i < n) {
var node = nodes[i],
// TODO cache outgoing?
targets = outgoing.call(this, node, i);
for (var j = 0; j < targets.length; j++) {
splines.push(d3_layout_bundleSpline(node, targets[j]));
}
}
n = links.length;
while (++i < n) splines.push(d3_layout_bundleSpline(links[i]));
return splines;
};

bundle.outgoing = function(x) {
if (!arguments.length) return outgoing;
outgoing = x;
return bundle;
};

return bundle;
};

function d3_layout_bundleSpline(start, end) {
var lca = d3_layout_bundleLeastCommonAncestor(start, end),
function d3_layout_bundleSpline(link) {
var start = link.source,
end = link.target,
lca = d3_layout_bundleLeastCommonAncestor(start, end),
points = [start];
while (start != lca) {
while (start !== lca) {
start = start.parent;
points.push(start);
}
var k = points.length;
while (end != lca) {
while (end !== lca) {
points.splice(k, 0, end);
end = end.parent;
}
Expand All @@ -59,26 +43,19 @@ function d3_layout_bundleAncestors(node) {
}

function d3_layout_bundleLeastCommonAncestor(a, b) {
if (a == b) {
return a;
}
if (a === b) return a;
var aNodes = d3_layout_bundleAncestors(a),
bNodes = d3_layout_bundleAncestors(b),
aNode = aNodes.pop(),
bNode = bNodes.pop(),
sharedNode = null;

while (aNode == bNode) {
while (aNode === bNode) {
sharedNode = aNode;
aNode = aNodes.pop();
bNode = bNodes.pop();
}
return sharedNode;
}

function d3_layout_bundleOutgoing(d, i) {
return d.outgoing;
}
d3.layout.chord = function() {
var chord = {},
chords,
Expand Down
2 changes: 1 addition & 1 deletion d3.layout.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions d3.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/bundle/bundle-radial.html
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Hierarchical Edge Bundling</title>
<title>Hierarchical Edge Bundling (Radial Tree)</title>
<script type="text/javascript" src="../../d3.js"></script>
<script type="text/javascript" src="../../d3.layout.js"></script>
<link type="text/css" rel="stylesheet" href="bundle.css"/>
Expand Down
74 changes: 40 additions & 34 deletions examples/bundle/bundle-radial.js
@@ -1,23 +1,16 @@
var r = 960 / 2,
stroke = d3.scale.linear().domain([0, 1e4]).range(["black", "steelblue"]),
nodeMap = {};
stroke = d3.scale.linear().domain([0, 1e4]).range(["brown", "steelblue"]);

var cluster = d3.layout.cluster()
.size([360, r - 120])
.sort(null)
.value(function(d) { return d.value.size; })
.children(function(d) { return isNaN(d.value.size) ? d3.entries(d.value) : null; });
.value(function(d) { return d.size; });

var bundle = d3.layout.bundle()
.outgoing(function(d) {
if (!d.data.value.imports) return [];
return d.data.value.imports.map(function(d) {
return nodeMap[d];
});
});
var bundle = d3.layout.bundle();

var line = d3.svg.line()
.interpolate("basis")
.beta(.85)
.x(function(d) {
var r = d.y, a = (d.x - 90) / 180 * Math.PI;
return r * Math.cos(a);
Expand All @@ -33,43 +26,56 @@ var vis = d3.select("#chart").append("svg:svg")
.append("svg:g")
.attr("transform", "translate(" + r + "," + r + ")");

d3.json("dependency-data.json", function(json) {
var tree = {};
json.forEach(function(d) {
var path = d.name.split("."),
last = path.length - 1,
node = tree;
path.forEach(function(part, i) {
if (i === last) {
node[part] = d;
return;
}
if (!(part in node)) {
node[part] = {};
d3.json("dependency-data.json", function(classes) {
var map = {},
links = [];

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);
}
node = node[part];
});
}
return node;
}

// Lazily construct the package hierarchy from class names.
classes.forEach(function(d) {
find(d.name, d);
});
var nodes = cluster(d3.entries(tree)[0]);

nodes.forEach(function(node) {
if (node.data.value.name) nodeMap[node.data.value.name] = node;
// Compute the cluster layout, starting at the root node!
var nodes = cluster(map[""]);

// Store a reference from class data object to the layout node.
nodes.forEach(function(d) {
d.data.node = d;
});

var link = vis.selectAll("path.link")
.data(bundle(nodes))
// For each import, construct a link from the source to target node.
classes.forEach(function(d) {
d.imports.forEach(function(i) {
links.push({source: map[d.name].node, target: map[i].node});
});
});

vis.selectAll("path.link")
.data(bundle(links))
.enter().append("svg:path")
.style("stroke", function(d) { return stroke(d[0].value); })
.attr("class", "link")
.attr("d", line);

var node = vis.selectAll("g.node")
vis.selectAll("g.node")
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })

node.append("svg:text")
.append("svg:text")
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
Expand Down
2 changes: 1 addition & 1 deletion examples/bundle/bundle-treemap.css
Expand Up @@ -9,6 +9,6 @@

.link {
stroke: #000;
stroke-width: .5px;
stroke-opacity: .5;
fill: none;
}
13 changes: 3 additions & 10 deletions examples/bundle/bundle-treemap.html
Expand Up @@ -2,21 +2,14 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Treemap</title>
<title>Hierarchical Edge Bundling (Treemap)</title>
<script type="text/javascript" src="../../d3.js"></script>
<script type="text/javascript" src="../../d3.layout.js"></script>
<script type="text/javascript" src="../../lib/colorbrewer/colorbrewer.js"></script>
<link type="text/css" rel="stylesheet" href="bundle-treemap.css"/>
<link type="text/css" rel="stylesheet" href="../button.css"/>
</head>
<body>
<div id="chart">
<button id="size" class="first active">
Size
</button
><button id="count" class="last">
Count
</button><p>
</div>
<div id="chart"></div>
<script type="text/javascript" src="bundle-treemap.js"></script>
</body>
</html>

0 comments on commit 0aba070

Please sign in to comment.