This example is an extension of Mike Bostock’s tutorial Lets Make a Map that implements automatic label placement using collision detection. This is a proof of concept more than anything, not polished in any way.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Automatic Label Placement</title>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<style>
.subunit.SCT { fill: #ddc; }
.subunit.WLS { fill: #cdd; }
.subunit.NIR { fill: #cdc; }
.subunit.ENG { fill: #dcd; }
.subunit.IRL,
.subunit-label.IRL {
display: none;
}
.subunit-boundary {
fill: none;
stroke: #777;
stroke-dasharray: 2,2;
stroke-linejoin: round;
}
.subunit-boundary.IRL {
stroke: #aaa;
}
.place,
.place-label {
fill: #444;
}
text {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 24px;
pointer-events: none;
}
</style>
</head>
<body>
<script>
var width = 960,
height = 1160;
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([50, 60])
.scale(1200 * 5)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection)
.pointRadius(2);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("uk.json", function(error, uk) {
var subunits = topojson.feature(uk, uk.objects.subunits),
places = topojson.feature(uk, uk.objects.places);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id; })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a !== b && a.id !== "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary");
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a === b && a.id === "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary IRL");
svg.append("path")
.datum(places)
.attr("d", path)
.attr("class", "place");
svg.selectAll(".place-label")
.data(topojson.feature(uk, uk.objects.places).features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.geometry.coordinates) + ")"; })
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start" : "end"; })
.text(function(d) { return d.properties.name; });
arrangeLabels();
});
function arrangeLabels() {
var move = 1;
while(move > 0) {
move = 0;
svg.selectAll(".place-label")
.each(function() {
var that = this,
a = this.getBoundingClientRect();
svg.selectAll(".place-label")
.each(function() {
if(this != that) {
var b = this.getBoundingClientRect();
if((Math.abs(a.left - b.left) * 2 < (a.width + b.width)) &&
(Math.abs(a.top - b.top) * 2 < (a.height + b.height))) {
// overlap, move labels
var dx = (Math.max(0, a.right - b.left) +
Math.min(0, a.left - b.right)) * 0.01,
dy = (Math.max(0, a.bottom - b.top) +
Math.min(0, a.top - b.bottom)) * 0.02,
tt = d3.transform(d3.select(this).attr("transform")),
to = d3.transform(d3.select(that).attr("transform"));
move += Math.abs(dx) + Math.abs(dy);
to.translate = [ to.translate[0] + dx, to.translate[1] + dy ];
tt.translate = [ tt.translate[0] - dx, tt.translate[1] - dy ];
d3.select(this).attr("transform", "translate(" + tt.translate + ")");
d3.select(that).attr("transform", "translate(" + to.translate + ")");
a = this.getBoundingClientRect();
}
}
});
});
}
}
</script>
</body>
</html>