Skip to content

Instantly share code, notes, and snippets.

@e9t
Forked from MoritzStefaner/.block
Last active August 29, 2015 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save e9t/9d2ed06ad15e1af00b78 to your computer and use it in GitHub Desktop.
Save e9t/9d2ed06ad15e1af00b78 to your computer and use it in GitHub Desktop.
Force based labels for fixed nodes

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, original nodes are fixed, and force layout governs the label placement.

Modified from Moritz Stefaner's Force-based label placement.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Force based labels for fixed nodes</title>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<style>
svg circle {
opacity: .5;
}
</style>
</head>
<body>
<script>
var outerWidth = 960,
outerHeight = 500,
padding = {top: 60, right: 60, bottom: 60, left: 60};
var width = outerWidth - padding.left - padding.right,
height = outerHeight - padding.top - padding.bottom;
// randomly generate points, o.w., input your own
var points = [], npoints = 50;
for(var i=0; i < npoints; i++)
points.push({x: Math.random()*width, y: Math.random()*height, label: "node" + i});
// create label nodes with forced layout
var labels = [],
labelLinks = [];
for(var i = 0; i < points.length; i++) {
var node = {
label: points[i].label,
x: points[i].x,
y: points[i].y
};
labels.push({node : node }); labels.push({node : node }); // push twice
labelLinks.push({ source : i * 2, target : i * 2 + 1, weight : 1, x: 100 });
};
var force = d3.layout.force()
.nodes(labels)
.links(labelLinks)
.gravity(0)
.linkDistance(0)
.linkStrength(8)
.charge(-100)
.size([width, height])
.on("tick", tick);
function tick() {
circleNode.call(updateNode);
labelNode.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,
diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = Math.min(0, b.width * (diffX - dist) / (dist * 2));
var shiftY = 5;
this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
labelNode.call(updateNode);
}
// draw svg
var svg = d3.select("body")
.append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight)
.append("g")
.attr("transform", "translate(" + padding.left + "," + padding.top + ")");
var circleNode = svg.selectAll("circle")
.data(points)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", "#555")
.style("stroke-width", 3);
var labelNode = svg.selectAll("g")
.data(force.nodes())
.enter().append("g")
.attr("class", "labelNode");
labelNode.append("circle")
.attr("r", 0)
.style("fill", "red");
labelNode.append("text")
.text(function(d, i) { return i % 2 == 0 ? "" : d.node.label })
.style("fill", "#555")
.style("font-size", 12);
// Update nodes
var updateNode = function() {
this.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
force.start();
/* // comment out for no animations
var niters = 50;
for (var i = niters; i > 0; --i) force.tick();
force.stop();
//*/
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment