Reputation: 15070
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.
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
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
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
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
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