public
Last active

Force-based label placement

  • Download Gist
index.html
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<meta charset="utf-8">
<title>Force based label placement</title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?2.6.0"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.layout.js?2.6.0"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.geom.js?2.6.0"></script>
</head>
<body>
<script type="text/javascript" charset="utf-8">
var w = 960, h = 500;
 
var labelDistance = 0;
 
var vis = d3.select("body").append("svg:svg").attr("width", w).attr("height", h);
 
var nodes = [];
var labelAnchors = [];
var labelAnchorLinks = [];
var links = [];
 
for(var i = 0; i < 30; i++) {
var node = {
label : "node " + i
};
nodes.push(node);
labelAnchors.push({
node : node
});
labelAnchors.push({
node : node
});
};
 
for(var i = 0; i < nodes.length; i++) {
for(var j = 0; j < i; j++) {
if(Math.random() > .95)
links.push({
source : i,
target : j,
weight : Math.random()
});
}
labelAnchorLinks.push({
source : i * 2,
target : i * 2 + 1,
weight : 1
});
};
 
var force = d3.layout.force().size([w, h]).nodes(nodes).links(links).gravity(1).linkDistance(50).charge(-3000).linkStrength(function(x) {
return x.weight * 10
});
 
 
force.start();
 
var force2 = d3.layout.force().nodes(labelAnchors).links(labelAnchorLinks).gravity(0).linkDistance(0).linkStrength(8).charge(-100).size([w, h]);
force2.start();
 
var link = vis.selectAll("line.link").data(links).enter().append("svg:line").attr("class", "link").style("stroke", "#CCC");
 
var node = vis.selectAll("g.node").data(force.nodes()).enter().append("svg:g").attr("class", "node");
node.append("svg:circle").attr("r", 5).style("fill", "#555").style("stroke", "#FFF").style("stroke-width", 3);
node.call(force.drag);
 
 
var anchorLink = vis.selectAll("line.anchorLink").data(labelAnchorLinks)//.enter().append("svg:line").attr("class", "anchorLink").style("stroke", "#999");
 
var anchorNode = vis.selectAll("g.anchorNode").data(force2.nodes()).enter().append("svg:g").attr("class", "anchorNode");
anchorNode.append("svg:circle").attr("r", 0).style("fill", "#FFF");
anchorNode.append("svg:text").text(function(d, i) {
return i % 2 == 0 ? "" : d.node.label
}).style("fill", "#555").style("font-family", "Arial").style("font-size", 12);
 
var updateLink = function() {
this.attr("x1", function(d) {
return d.source.x;
}).attr("y1", function(d) {
return d.source.y;
}).attr("x2", function(d) {
return d.target.x;
}).attr("y2", function(d) {
return d.target.y;
});
 
}
 
var updateNode = function() {
this.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
 
}
 
 
force.on("tick", function() {
 
force2.start();
 
node.call(updateNode);
 
anchorNode.each(function(d, i) {
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.childNodes[1].getBBox();
 
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
 
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
 
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 5;
this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
 
 
anchorNode.call(updateNode);
 
link.call(updateLink);
anchorLink.call(updateLink);
 
});
 
</script>
</body>
</html>
readme.mkd
Markdown

A small demo of a pleasant, yet simple label placement algorithm for densely packed visualizations. The basic idea is to have labels orbit around their target node at a fixed distance, but repeal each other, so that they don't overlap, and orient themselves to the outside of clusters. To support that, labels on the right of their target node are left-aligned, and labels on the left of their target node are right-aligned; in between, we interpolate. In this example, one force layout governs the node placement, and the second one the label placement, but of course, the node placement could be computed by any other algorithm.

I first used this label placement in the MPG research networks project and owe the basic idea to Cedric Kiefer.

Dear Moritz,

This is really useful!
I have also tried using d3 force layouts for label placement (with a lesser degree of success ;) ).
I had a question on Stackoverflow regarding this a while ago:
http://stackoverflow.com/questions/5833695/bubble-chart-label-placement-algorithm-preferably-in-javascript

Before playing around with your code, what do you think about using it for scatter chart label placement?

Thanks!

Hi,

thanks! I can see this labeling strategy work in a variety of settings, why not try it for the scatterplot..

Hello. This demo looks like it could do exactly what I'm looking for... Being new to Javascript, I'm not sure how to rewrite this demo to load data from a file like the Force Directed Graph does. Can anybody come up with any hints? Thanks for making this available.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.