12

In an SVG graph I create node elements consisting of a rectangle and some text. The amount of text can differ significantly, hence I'd like to set the width of the rect based on the width of the text.

Here's the creation of the rectangles with D3.js (using fixed width and height values):

var rects = nodeEnter.append("rect")
    .attr("width", rectW)
    .attr("height", rectH);

followed by the text element:

var nodeText = nodeEnter.append("text")
    .attr("class", "node-text")
    .attr("y", rectH / 2)
    .attr("dy", ".35em")
    .text(function (d) {
        return d.data.name;
    });
nodeText // The bounding box is valid not before the node addition happened actually.
    .attr("x", function (d) {
        return (rectW - this.getBBox().width) / 2;
    });

As you can see, currently I center the text in the available space. Then I tried to set the widths of the rects based on their text, but I never get both, the rect element and the text HTML element (for getBBox()) at the same time. Here's one of my attempts:

rects.attr("width",
        d => this.getBBox().width + 20
    );

but obviously this is wrong as it refers to rects not the text.

What's the correct approach here?

isherwood
  • 58,414
  • 16
  • 114
  • 157
Mike Lischke
  • 48,925
  • 16
  • 119
  • 181

3 Answers3

11

I would use getComputedTextLength to measure the text. I don't know if there is an equivalent for this in D3.js My answer is using plain javascript and is assuming that the rect and the text center is {x:50,y:25 } and you are using text{dominant-baseline:middle;text-anchor:middle;}

let text_length = txt.getComputedTextLength();

rct.setAttributeNS(null,"width",text_length )
rct.setAttributeNS(null,"x",(50 - text_length/2) )
svg{border:1px solid}
text{dominant-baseline:middle;text-anchor:middle;}
<svg viewBox="0 0 100 50">
  <rect x="25" y="12.5" width="50" height="25" stroke="black" fill="none" id="rct" />
  <text x="50" y="25" id="txt">Test text</text>
</svg>

Alternatively instead of txt.getComputedTextLength() you may use txt.textLength.baseVal.value

enxaneta
  • 31,608
  • 5
  • 29
  • 42
7

The solution is pretty simple when you remember that the this binding in the attr() call refers to the associated HTML (SVG) element:

rects.attr("width",
    d => this.parentNode.childNodes[1].getComputedTextLength() + 20
);

The rect is the first element in a list of SVG elements that make up the displayed node. The text for that node is at index 1 (as follows from the append calls).

LightninBolt74
  • 211
  • 4
  • 11
Mike Lischke
  • 48,925
  • 16
  • 119
  • 181
2

Normally I would comment, but I don't have enough reputation points. The accepted answer has the right idea, but it doesn't work, how he coded it. The first problem is, he uses an arrow function instead of an anonymus function. In arrow functions, this has a different scope. So use an anonymus function here.

The second problem is the order of rect and text, as you can see in the source code, in the question. Since rect is appended before text, the parent node doesn't have the child text yet. So you have to just append the rect, then append the text and set its attrs and then set the attrs of rect. So the solution is:

var rects = nodeEnter.append("rect")

var nodeText = nodeEnter.append("text")
    .attr("class", "node-text")
    .attr("y", rectH / 2)
    .attr("dy", ".35em")
    .text(function (d) {
        return d.data.name;
    });
nodeText // The bounding box is valid not before the node addition happened actually.
    .attr("x", function (d) {
        return (rectW - this.getBBox().width) / 2;
    });

rect
    .attr('width', function () {
        return this.parentNode.childNodes[1].getComputedTextLength();
    })
    .attr("height", rectH);

Note: If you don't need the parameter d, you don't have to accept it, like I did.

Rip
  • 73
  • 2
  • 8
  • 1
    The trouble is, it's my own answer to my question and I posted it because it works exactly like written. The arrow function is essential (and you should probably always use that, unless you have a specific reason not to do), because it binds `this` not to varying contexts (depending on the caller) but always to the owner class/function (which is what you mostly want). – Mike Lischke Aug 29 '20 at 18:07
  • I tried your example and it didn't work that way. I first had to apply my fixes, so I posted it for the case, other have the same problems as I had – Rip Aug 29 '20 at 18:10
  • And I cannot imagine, that in your example the dom/javascript didn't complain, that the child `text` wasn't appended yet. – Rip Aug 29 '20 at 18:13
  • It's also worth noting, that d3 itself uses anonymus functions. – Rip Aug 29 '20 at 18:14