3

I have a seemingly simple d3.js problem. I am creating a tree from a set of json data. The tree is composed of labels that are composed of a rectangle container that wrap around some text. I would like to change the width of the rectangle according to the length of the text. I understand I should be doing something like this one, but I am struggling to understand how.

Here is my JS code (stripped down of most unnecessary frills):

var rectW = 140, rectH = 40;

// Declare the nodes.
var node = draw.selectAll('g.node')
               .data(nodes, function(d) { return d.id; });

// Enter the nodes.
var nodeLabel = node.enter().append('g')
                    .attr('transform', function(d) { return 'translate(' + source.x0 + ',' + source.y0 + ')'; });

var nodeRect = nodeLabel.append('rect')
                        .attr('width', rectW)
                        .attr('height', rectH);

var nodeText = nodeLabel.append('text')
                        .attr('x', rectW / 2)
                        .attr('y', rectH / 2)
                        .text(function (d) { return d.name; });

As you can see, I create an SVG group to which I append both the container rectangle and the contained text. Now, I would like to retrieve the length of each text element, and use it to change the width of the corresponding rectangle element. How can I do that? I tried with every possible combination of D3 directives I could think of, but my knowledge of the library is not enough advanced to suit my purposes.

UPDATE Thanks to Geraldo Furtado's answer, I managed to fix this issue by adding the following:

// This arranges the width of the rectangles
nodeRect.attr("width", function() {
    return this.nextSibling.getComputedTextLength() + 20;
})
// This repositions texts to be at the center of the rectangle
nodeText.attr('x', function() {
    return (this.getComputedTextLength() + 20) /2;
})
alecive
  • 177
  • 1
  • 14

2 Answers2

2

This is the current structure of your nodes:

<g>
    <rect></rect>
    <text></text>
</g>

That being the case, the texts are the nextSiblings of the rectangles. Therefore, all you need to get the length of the texts is using nextSibling in the rectangle selection:

nodeRect.attr("width", function() {
    return this.nextSibling.getComputedTextLength() + rectW
})

Here I'm adding rectW to keep the same padding on the left and right, since you're putting the texts to start at rectW / 2.

If you're not sure about the relationship between the texts and rectangles (who is the first child, who is the last child...), you can go up, select the group element and then select the text inside it:

nodeRect.attr("width", function() {
    return d3.select(this.parentNode).select("text").node().getComputedTextLength() + rectW
})

Here is a basic demo:

var data = [{
    name: "some text",
    x: 10,
    y: 10
  },
  {
    name: "A very very very long text here",
    x: 100,
    y: 50
  },
  {
    name: "Another text, this time longer than the previous one",
    x: 25,
    y: 100
  },
  {
    name: "some short text here",
    x: 220,
    y: 150
  }
];

var svg = d3.select("svg");

var rectW = 140,
  rectH = 30;

var node = svg.selectAll(null)
  .data(data);

var nodeLabel = node.enter().append('g')
  .attr('transform', function(d) {
    return 'translate(' + d.x + ',' + d.y + ')';
  });

var nodeRect = nodeLabel.append('rect')
  .attr('width', rectW)
  .attr('height', rectH)
  .style("fill", "none")
  .style("stroke", "gray")

var nodeText = nodeLabel.append('text')
  .attr('x', rectW / 2)
  .attr('y', rectH / 2)
  .style("dominant-baseline", "central")
  .text(function(d) {
    return d.name;
  });

nodeRect.attr("width", function() {
  return this.nextSibling.getComputedTextLength() + rectW
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>

For storing the computed width and using it later on, you can set another property. For instance:

nodeRect.attr("width", function(d) {
    return d.rectWidth = this.nextSibling.getComputedTextLength() + rectW
});

Here is the demo, look at the console:

var data = [{
    name: "some text",
    x: 10,
    y: 10
  },
  {
    name: "A very very very long text here",
    x: 100,
    y: 50
  },
  {
    name: "Another text, this time longer than the previous one",
    x: 25,
    y: 100
  },
  {
    name: "some short text here",
    x: 220,
    y: 150
  }
];

var svg = d3.select("svg");

var rectW = 140,
  rectH = 30;

var node = svg.selectAll(null)
  .data(data);

var nodeLabel = node.enter().append('g')
  .attr('transform', function(d) {
    return 'translate(' + d.x + ',' + d.y + ')';
  });

var nodeRect = nodeLabel.append('rect')
  .attr('width', rectW)
  .attr('height', rectH)
  .style("fill", "none")
  .style("stroke", "gray")

var nodeText = nodeLabel.append('text')
  .attr('x', rectW / 2)
  .attr('y', rectH / 2)
  .style("dominant-baseline", "central")
  .text(function(d) {
    return d.name;
  });

nodeRect.attr("width", function(d) {
  return d.rectWidth = this.nextSibling.getComputedTextLength() + rectW
});

nodeLabel.each(function(d) {
  console.log(d)
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Thats great! I also managed to center the position of the text by adding `nodeText.attr('x', function() {return (this.getComputedTextLength() + 20) /2;})`. Now everything else is misaligned though, because it was referring to rectangles with a fixed `rectW` width. Is there any way I can store this variable width (as an array of sorts) and use it later on when needed? I have updated my first question to clarify this. – alecive May 16 '18 at 00:51
  • @alecive your new question is not exactly clear, but I just edited my answer to show you how you can store the new variable. You can also use `d3.local`, but that's more complicated. Also, please don't update your question: next time, if you have a new issue, please post *another* question, linking the previous one... after all, it's free! – Gerardo Furtado May 16 '18 at 01:01
  • 1
    sorry for that, I don't know how to use StackOverflow properly.. I'll try out your solution, and I will open a new question if I still have issues! – alecive May 16 '18 at 01:06
  • @alecive Yes, please. At SO we really try to avoid new questions in the comments, because the answerer has to edit the answer to accomodate the new question, and then the whole Q/A pair becomes a mess. When posting a new question based on another one, just write *"as a follow up of my previous question..."*, and then link the previous question. However, pay attention to the fact that you have to provide all the relevant details in the new question, that is, it has to be answerable without having to click on the link. – Gerardo Furtado May 16 '18 at 01:18
  • 1
    Duly noted. I already removed my follow-up question from the first one. Thanks again for the help! – alecive May 16 '18 at 01:25
0

You can add the following line at the end of your script, after the text has been set:

nodeRect
.transition()
.attr("width",function(){
return Math.max(rectW,nodeText.node().getComputedTextLength())
}).delay(0).duration(500)

I used Math.max to set it to rectW if its large enough or expand if necessary. You can perhaps adapt that part.

ibrahim tanyalcin
  • 5,643
  • 3
  • 16
  • 22