block by pnavarrc 5913636

Automatic Label Placement

Full Screen

Automatic Label Placement

This example is an extension of Mike Bostock’s tutorial Lets Make a Map that implements automatic label placement using the force layout and multiple foci. The centroid of each feature will define a foci of the force. This foci will attract the label that correspond to that feature (and ignore the others). The labels will repel themselves to avoid overlapping.

References

index.html

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

        .subunit-label {
            fill: #777;
            fill-opacity: .5;
            font-size: 20px;
            font-weight: 300;
            text-anchor: middle;
        }

        .place,
        .place-label {
            fill: #444;
        }

        text {
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
            font-size: 10px;
            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.selectAll(".subunit-label")
            .data(subunits.features)
            .enter().append("text")
            .attr("class", function(d) { return "subunit-label " + d.id; })
            .attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
            .attr("dy", ".35em")
            .text(function(d) { return d.properties.name; });

        svg.append("path")
            .datum(places)
            .attr("d", path)
            .attr("class", "place");

        // Place and label location
        var foci = [],
            labels = [];

        // Store the projected coordinates of the places for the foci and the labels
        places.features.forEach(function(d, i) {
            var c = projection(d.geometry.coordinates)
            foci.push({x: c[0], y: c[1]});
            labels.push({x: c[0], y: c[1], label: d.properties.name})
        });

        // Create the force layout with a slightly weak charge
        var force = d3.layout.force()
            .nodes(labels)
            .charge(-20)
            .gravity(0)
            .chargeDistance(width / 8)
            .size([width, height]);

        // Append the place labels, setting their initial positions to
        // the feature's centroid
        var placeLabels = svg.selectAll('.place-label')
            .data(labels)
            .enter()
            .append('text')
            .attr('class', 'place-label')
            .attr('x', function(d) { return d.x; })
            .attr('y', function(d) { return d.y; })
            .attr('text-anchor', 'middle')
            .text(function(d) { return d.label; });

        force.on("tick", function(e) {
            var k = .1 * e.alpha;
            labels.forEach(function(o, j) {
                // The change in the position is proportional to the distance
                // between the label and the corresponding place (foci)
                o.y += (foci[j].y - o.y) * k;
                o.x += (foci[j].x - o.x) * k;
            });

            // Update the position of the text element
            svg.selectAll("text.place-label")
                .attr("x", function(d) { return d.x; })
                .attr("y", function(d) { return d.y; });
        });

        force.start();
    });

</script>

</body>
</html>