Search code examples
javascriptd3.jslabeld3fc

How to use d3fc-label-label.js on a map?


I'm trying to position labels on map overlapping-free by using using d3fc-label-label.js in combination with d3.js. While labeling the map by basic d3 functions works well, the approach with the help of d3fc-label-label.js (heavily inspired by this example) produces a map with all the labels placed in top left corner.

Here's the javascript part that does the job

var width = 1300,
    height = 960;

var projection = d3.geoMercator()
  .scale(500)
  // Center the Map to middle of shown area
  .center([10.0, 50.5])
  .translate([width / 2, height / 2]);

// ??
var path = d3.geoPath()
  .projection(projection)
  .pointRadius(2);

// Set svg width & height
var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);
// var g = svg.append("g");

d3.json("europe_wgs84.geojson", function(error, map_data) {
  if (error) return console.error(error);
  // var places = topojson.feature(map_data, map_data.objects.places);

  // "path" instead of ".subunit"
  svg.selectAll("path")
    .data(map_data.features)
  .enter().append("path")
    .attr("d", path)
    .attr("class", function(d) { return "label " + d.id})

  var labelPadding = 2;

  // the component used to render each label
  var textLabel = fc.layoutTextLabel()
    .padding(labelPadding)
    //.value(function(d) { return map_data.properties.iso; });
    .value(function(d) { return d.properties.iso; });

  // use simulate annealing to find minimum overlapping text label positions
  var strategy = fc.layoutGreedy();

  // create the layout that positions the labels
  var labels = fc.layoutLabel(strategy)
      .size(function(_, i, g) {
          // measure the label and add the required padding
          var textSize = d3.select(g[i])
              .select('text')
              .node()
              .getBBox();
          return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
      })
      .position(function(d) { return projection(d.geometry.coordinates); })
      .component(textLabel);

  // render!
  svg.datum(map_data.features)
       .call(labels);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>

See the gist that includes the data and a HTML file.

I would guess the issue is related to append the labels correctly to path of the map. Sadly, I haven't figured it out and would greatly appreciate any help!


Solution

  • I believe the problem lies in the fact that you are not passing single coordinates as the label's position.

    layoutLabel.position(accessor)

    Specifies the position for each item in the associated array. The accessor function is invoked exactly once per datum, and should return the position as an array of two values, [x, y].

    In the example you show, that you are basing the design on, the variable places contains point geometries, it is to these points that labels are appended. Looking in the topojson we find places looking like:

    "places":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[5868,5064],"properties":{"name":"Ayr"}},{"type":"Point","coordinates":[7508,6637],"properties":{"name":"Aberdeen"}},{"type":"Point","coordinates":[6609,5933],"properties":{"name":"Perth"}},...
    

    Note that geometries.coordinates of each point contains one coordinate. However, in your code, d.geometry.coordinates contains an array of coordinates as it contains the boundary points of the entire path of each feature. This will cause errors in label placement. Instead, you might want to use path.centroid(d), this will return a single coordinate that is at the center of each country/region/path. Placement might not be perfect, as an extreme example, a series of countries arranged as concentric rings will have the same centroid. Here is a basic block showing placement using path.centroid (this shows only the placement - not the formatting of the labels as I'm not familiar with this library extension).


    If you were wondering why the linked example's regional labels appear nicely, in the example each region has a label appended at its centroid, bypassing d3fc-label-layout altogether:

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