mtmacdonald
mtmacdonald

Reputation: 15070

Calculate width of text before drawing the text

I want to display a rect with a text label next to it. The width of the rect should stretch to the width of the svg container, less the width of the the text, which is dynamic and can be of any variable length.

JSFiddle

var text = 'Foobar';
var textWidth = 50; //how to calculate this?
var plotWidth = 400;
var barWidth = plotWidth-textWidth;

var plot = d3.select(container)
        .insert("svg")
        .attr('width', plotWidth)
        .attr('height', 50);

plot.append("rect")
    .style("fill", "steelblue")
    .attr("x", 0)
    .attr("width", barWidth)
    .attr("y", 0)
    .attr("height", 50);

plot.append("text")
    .attr("x", barWidth)
    .attr("y", 28)
    .text(text);

How do I calculate the width of the text using D3, before it is drawn? Or how do I otherwise position and size elements that depend on the dimensions of variable length text?

Upvotes: 37

Views: 36674

Answers (4)

djvg
djvg

Reputation: 14255

It is also possible to use getComputedTextLength() or getBBox() inside a d3 selection.attr.

Here's a minimal example that calculates x and y coordinates based on the size of the text content:

d3.select('svg').append('text')
  .text('some text')
    .attr('x', function() {return this.getComputedTextLength();})
    .attr('y', (d, i, nodes) => nodes[i].getBBox().height);
svg {
  border: 1px dashed black;
}

svg text {
  fill: black;
  text-anchor: start;
  dominant-baseline: hanging;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200" height="100"></svg>

Note that two function styles are used, to illustrate the following:

In a normal function (), you can use either nodes[i] or this to access the current node.

However, in arrow functions =>, we must use nodes[i], because using this raises an error:

this.getComputedTextLength is not a function

Also see d3-selection issue 97 and this SO answer.

Upvotes: 1

Pythonic
Pythonic

Reputation: 2131

I had a similar problem in a complex chart with lots of interactions between elements and text, which required knowing the text width before displaying any element.

I resorted to creating a dummy text to grab its width and immediately removing it. Note the last line of code of the function in each.

var textData = ['a', 'b', 'c']    // your text here

var textWidth = []

svg.append('g')
    .selectAll('.dummyText')     // declare a new CSS class 'dummyText'
    .data(textData)
    .enter()                     // create new element
    .append("text")              // add element to class
    .attr("font-family", "sans-serif")
    .attr("font-size", "14px")
    //.attr("opacity", 0.0)      // not really necessary
    .text(function(d) { return d})
    .each(function(d,i) {
        var thisWidth = this.getComputedTextLength()
        textWidth.push(thisWidth)
        this.remove() // remove them just after displaying them
    })

console.log(textWidth) // this array contains the on-screen width of each text element

Upvotes: 23

TxRegex
TxRegex

Reputation: 2425

I know you asked about D3, but this might be a native solution to your issue.

The HTML5 canvas 2D context has some built-in functionality to measure text. You might be able to tap into that to measure text for other APIs like SVG. If it's not 100% accurate, surely it's proportional to the correct answer.

var BrowserText = (function () {
    var canvas = document.createElement('canvas'),
        context = canvas.getContext('2d');

    /**
     * Measures the rendered width of arbitrary text given the font size and font face
     * @param {string} text The text to measure
     * @param {number} fontSize The font size in pixels
     * @param {string} fontFace The font face ("Arial", "Helvetica", etc.)
     * @returns {number} The width of the text
     **/
    function getWidth(text, fontSize, fontFace) {
        context.font = fontSize + 'px ' + fontFace;
        return context.measureText(text).width;
    }

    return {
        getWidth: getWidth
    };
})();

// Then call it like this:
console.log(BrowserText.getWidth('hello world', 22, 'Arial')); // 105.166015625
console.log(BrowserText.getWidth('hello world', 22)); // 100.8154296875

Upvotes: 32

Henry S
Henry S

Reputation: 3112

Here's a working example based on using getBBox().width getComputedTextLength():

Edit: Updating the answer to use getComputedTextLength due to performance concerns (see comment)

http://jsfiddle.net/henbox/jzkj29nv/27/

var text_element = plot.select("text");
var textWidth = text_element.node().getComputedTextLength()

I've also switched to using text-anchor: end; CSS for the text, so you don't need to calculate the start position of the text (just pass in the end)

Upvotes: 12

Related Questions