59

I am starting with d3.js, and am trying to create a row of nodes each of which contains a centered number label.

I am able to produce the desired result visually, but the way I did it is hardly optimal as it involves hard-coding the x-y coordinates for each text element. Below is the code:

var svg_w = 800;
var svg_h = 400;
var svg = d3.select("body")
    .append("svg")
    .attr("width", svg_w)
    .attr("weight", svg_h);

var dataset = [];
for (var i = 0; i < 6; i++) {
    var datum = 10 + Math.round(Math.random() * 20);
    dataset.push(datum);
}

var nodes = svg.append("g")
               .attr("class", "nodes")
               .selectAll("circle")
               .data(dataset)
               .enter()
               .append("circle")
               .attr("class", "node")
               .attr("cx", function(d, i) {
                   return (i * 70) + 50;
               })
               .attr("cy", svg_h / 2)
               .attr("r", 20);

var labels = svg.append("g")
                .attr("class", "labels")
                .selectAll("text")
                .data(dataset)
                .enter()
                .append("text")
                .attr("dx", function(d, i) {
                    return (i * 70) + 42
                })
                .attr("dy", svg_h / 2 + 5)
                .text(function(d) {
                    return d;
                });

The node class is custom CSS class I've defined separately for the circle elements, whereas classes nodes and labels are not explicitly defined and they are borrowed from this answer.

As seen, the positioning of each text label is hard-coded so that it appears at the center of the each node. Obviously, this is not the right solution.

My question is that how should I correctly associate each text label with each node circle dynamically so that if the positioning of a label changes along with that of a circle automatically. Conceptual explanation is extremely welcome with code example.

Community
  • 1
  • 1
skyork
  • 7,113
  • 18
  • 63
  • 103
  • 1
    Text anchors appear to not work directly in D3 yet, but did you tried something as simple as CSS text-align? This should do the trick! http://www.w3schools.com/cssref/pr_text_text-align.asp Also try to take a look at posts like these from the d3 Google Group: https://groups.google.com/forum/#!searchin/d3-js/text-align/d3-js/M6a-97ajkWs/rHJV4_WrhX0J%5B1-25%5D – paxRoman Aug 08 '12 at 08:39
  • 1
    Where are you getting the idea that text anchors do not work in d3? – Brant Olsen Aug 08 '12 at 13:56
  • few months ago they did not worked...thanks for clarifying :) nice to see a simple approach to this problem – paxRoman Aug 08 '12 at 14:51

3 Answers3

70

The text-anchor attribute works as expected on an svg element created by D3. However, you need to append the text and the circle into a common g element to ensure that the text and the circle are centered with one another.

To do this, you can change your nodes variable to:

var nodes = svg.append("g")
           .attr("class", "nodes")
           .selectAll("circle")
           .data(dataset)
           .enter()
           // Add one g element for each data node here.
           .append("g")
           // Position the g element like the circle element used to be.
           .attr("transform", function(d, i) {
             // Set d.x and d.y here so that other elements can use it. d is 
             // expected to be an object here.
             d.x = i * 70 + 50,
             d.y = svg_h / 2;
             return "translate(" + d.x + "," + d.y + ")"; 
           });

Note that the dataset is now a list of objects so that d.y and d.x can be used instead of just a list of strings.

Then, replace your circle and text append code with the following:

// Add a circle element to the previously added g element.
nodes.append("circle")
      .attr("class", "node")
      .attr("r", 20);

// Add a text element to the previously added g element.
nodes.append("text")
     .attr("text-anchor", "middle")
     .text(function(d) {
       return d.name;
      });

Now, instead of changing the position of the circle you change the position of the g element which moves both the circle and the text.

Here is a JSFiddle showing centered text on circles.

If you want to have your text be in a separate g element so that it always appears on top, then use the d.x and d.y values set in the first g element's creation to transform the text.

var text = svg.append("svg:g").selectAll("g")
         .data(force.nodes())
         .enter().append("svg:g");

text.append("svg:text")
    .attr("text-anchor", "middle")
    .text(function(d) { return d.name; });

text.attr("transform",  function(d) {
      return "translate(" + d.x + "," + d.y + ")"; 
    });
Brant Olsen
  • 5,628
  • 5
  • 36
  • 53
  • thanks for the answer! It seems that your approach is similar to the first approach outlined in this [answer](http://stackoverflow.com/questions/11102795/d3-node-labeling/11109803#11109803), which means that each label will be drawn *on top only for its circle*, but will be drawn *beneath other circles*. Is it possible to do it using the two-data-join approach outlined in the same answer? (which was what I modelled my attempt on). Thanks! – skyork Aug 08 '12 at 14:35
  • I made an update for this. The two main things you need to change are making your data a list of objects instead of a list of strings so that `y` and `x` can be stored for each string, and putting your text inside a `g` element so that the `g` element can be transformed. – Brant Olsen Aug 08 '12 at 14:52
  • if I understood your updated code above correctly, you first create a `g` elements and inside which you create a series of `g` elements for the data in the array, and then you create `text` elements inside each of these `g` element? What confuses me a little is `text.append("svg:text")...`, does `text` here represents the series of `g` elements? If so, how does D3 create a `text` element for *each of these `g` elements*? The syntax is a bit misleading. – skyork Aug 08 '12 at 17:04
  • 1
    `text` does represent the series of `g` elements. Under the covers of D3, the `append` methods knows to do a `each` when given a list of elements. Thus the `append` is applied to each and every `g` element in the list. – Brant Olsen Aug 08 '12 at 17:13
  • See https://github.com/mbostock/d3/wiki/Selections#wiki-each for more information on `each`. Specifically see that it says nearly every other operator uses `each` under the covers. – Brant Olsen Aug 08 '12 at 17:15
  • 4
    just a further observation: with only `.attr("text-anchor", "middle")` for each `text` element, the label is at the middle horizontally but slightly off vertically. I fixed this by adding `attr("y", ".3em")` (borrowed from examples at d3.js website), which seems to work well even for arbitrary size of node circle. However, what exactly this additional attribute does eludes my understanding. Sure, it does something to the y-coordinate of each `text` element, but why `.3em` in particular? It seems almost magical to me... – skyork Aug 08 '12 at 23:01
42

The best answer came from the asker himself:

just a further observation: with only .attr("text-anchor", "middle") for each text element, the label is at the middle horizontally but slightly off vertically. I fixed this by adding attr("y", ".3em") (borrowed from examples at d3.js website), which seems to work well even for arbitrary size of node circle. However, what exactly this additional attribute does eludes my understanding. Sure, it does something to the y-coordinate of each text element, but why .3em in particular? It seems almost magical to me...

Just add .attr("text-anchor", "middle") to each text element.

Example:

node.append("text")
    .attr("x", 0)
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .text(function(d) { return d.name; });
Community
  • 1
  • 1
tacone
  • 11,371
  • 8
  • 43
  • 60
4

This page describes what's going on under the svg hood when it comes to text elements. Understanding the underlying machinery and data structures helped me get a better handle on how I had to modify my code to get it working.