Automatic Label Placement
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.
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="http://d3js.org/d3.v3.min.js"></script> | |
| <script src="http://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> |
Lars,
This is an awesome piece of code. I have a quick and ignorant question. In the example above you generate the UK map by pulling data from a JSON array. I am guessing this is legacy from the Bostock example.
Is there any way we could get the above to work, working directly from an SVG picture?
Kind regards and thanks,
GBdeJ