1

I am trying to add some simple country labels to a D3 vector map which is layered on top of a D3-tile raster map. The labels are being created in as expected, but I am not able to project them properly on the map. The projection in D3-tile is a bit messed up (by which I mean it doesn't work like on a 'normal' vector map, and I don't understand it).

I have created a jsfiddle where I create the maps and then try to project them so that they move around with user interaction.

Bit of code that fails to achieve this is here:

  d3.selectAll(".country_labels")   
    .attr("transform", function(d) {return "translate(" + path.centroid(d) + ")"})  

UPDATE

I suspect my issue on this question is similar to the one I raised earlier today on here. I also note that a similar-ish question was raised here too.

I have made some progress and put together this new fiddle. The labels are now all on the map, but floating around the gulf of guinea, close to geocoordinates [0,0]. To me, this means they may have been projected properly but that the zoom has not functioned as expected. The issue here is that there are three separate types of coordinates in this script:

  1. Geocoordinates - these are the starting point and always fixed
  2. The 'd3-tile' coordinates. The ones that fit within a single pixel, and therefore always very close to zero
  3. Pixel coordinates - these correspond to the actual coordinates on the screen
Noobster
  • 1,024
  • 1
  • 13
  • 28

1 Answers1

1

This is similar to your other question, just it is on the forward projection & zoom rather than the inverts. (I started writing this before the update, but had to run, I'll continue with your original code).

As with the paths, you append your labels as expected:

country_labels.selectAll("text")
  .data(collection.features)
  .enter().append("text")
  .attr("x", function(d){return path.centroid(d)[0];})
  .attr("y", function(d){return path.centroid(d)[1];})
  .attr("dx", -40)
  .text(function(d){ return d.properties.name })
  .style("fill", "#aeaeaf") 
  .style("font-size", "15px")

There is one concern here, as the projection of most d3-tile examples, including yours, use a d3-projection scale of 1/tau, the world is projected within the space of 1 pixel, so the dx value is equal to 40 worlds, this won't work when applying the zoom, so let's drop that part

Now you are appending the features more or less just like the paths, but the issue is in the zoom handling:

d3.selectAll(".country_labels") 
  .attr("transform", function(d) {return "translate(" + path.centroid(d) + ")"})    

The paths are given a similar treatment:

vector
    .attr("transform", "translate(" + [transform.x, transform.y] + ")scale(" + transform.k + ")")
    .style("stroke-width", 1 / transform.k); 

But there are a couple differences here:

  1. you are applying a different transform (scale and translate) to the paths as compared to the text: for the text there is no reference to the current zoom transform, instead, you only use the projection, which is anchored at 0,0 with all features lying within an area of one pixel (and anchored at 0,0 will have its baseline at y=0, the text will be largely out of view). If you inspect the svg, you'll see the text, just in the wrong spot.

  2. The paths have a reduced stroke width as one zooms in (as we are zooming the svg, the stroke width itself increases), the same would apply for text, so even if the text was correctly positioned, it would be very very large (more than most any screen holding the browser).

One way we can address this is we apply the zoom transform on the x/y coordinates of the text, not the element itself (which would scale the text size as well, this way we don't need to resize the text at all):

country_labels.selectAll("text") .attr("x", function(d){return transform.apply(path.centroid(d))[0];}) .attr("y", function(d){return transform.apply(path.centroid(d))1;})

Like with the inversion from svg pixel to lat/long, we go through the same motions, but in reverse order: apply the projection, then apply the zoom.

Here's an updated fiddle.


However, I have bad news - the labels are positioned exactly where you are telling them to be positioned now. But they aren't where you want them to be (how's the saying go, the best thing about programming is that the code does exactly what you tell it, the worst thing about programming is that the code does exactly what you tell it?).

You are using path centroids to place labels, this works sometimes for some features, but it doesn't work all the time. Take the United States for example, the centroid of the US using a Mercator projection isn't in the United States because it is between Alaska and the lower 48 states (sorry Hawaii, you don't have much pull here). The centroid of Canada is partly in the Arctic Ocean, and in many datasets (not this one surprisingly), France is labelled in the middle of the Atlantic because of French Guiana, when using centroids as the text anchor.

You can improve the visual appearance slightly by using .style("text-anchor","middle"), which at least centers labels where the are (very useful for smaller or equitorial countries), but ultimately centroid placement isn't ideal.

I'll just finish with: Annotations are the bane of cartography.

But, there is hope, here's one of the more promising futures I've seen.

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Andrew - your answer is perfect, once again. I am sure I don't just speak for myself when I say you are really shedding heaps of light onto D3 tiles. Quick question - how might you amend your fiddle to recalculate all the label placements once the transitions (zoom-to-bound or user panning/zooming) is complete? Depending on browser I find that labels can really be a drag on performance and make the animations judder. – Noobster Jun 17 '18 at 10:36
  • Thanks also for your discussion on centroid placement - I have found that really useful, and I also marvelled at the link you shared. – Noobster Jun 17 '18 at 10:37
  • (Sorry fo the delay, been on and off the computer here), Yeah, transitioning that many labels could be a performance draw. One option in particular comes to mind: using d3.geoContains and a dynamically generated geojson that represents the viewport (find the lat/long of each corner of the svg for the current and next zoom state) to see what labels don't need to be drawn at all and only draw and transition those that are needed. That would reduce the number of transitioning elements and should speed things up. – Andrew Reid Jun 18 '18 at 04:16
  • I will look d3.geoContains - I have not heard of this before! The solution I had in mind for now is more simple ... Simply to disable all the labels upon user interaction / click to bounded zoom and to re-enable them once the transition is complete. That way the labels are calculated just once and at the end of the transition. – Noobster Jun 18 '18 at 04:35
  • That is probably the easiest way, I was going to suggest it but got caught up in the other option. – Andrew Reid Jun 18 '18 at 04:38
  • The problem with the way the script is set up is that labels are recalculated every single time the zoom is invoked, which in this case is roughly 35 times each time you click on a country. So `.on("end", function(){...}` will not work here. My second ideas was to pass the scale calculated in the zoom to bounding box function and to pass that on to the zoom function. That way I could write 'if transform.k = scale, then run labels'... but I am not able to pass that variable. And it doesn't seem to be included anywhere in the zoom function. – Noobster Jun 18 '18 at 04:42
  • New question raised here: https://stackoverflow.com/questions/50926489/d3-v4-label-marker-placement-after-last-zoom-transformation-only – Noobster Jun 19 '18 at 10:45