Moritz Stefaner’s Block 1377729
Updated October 24, 2018

Force-based label placement

Open

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.

index.html#

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

LICENSE#

This block appears to have no license. Please contact the author to request a license.