Wrapping SVG text without SVG 2
By: Carys Mills
Tags: Web Development, JavaScript, SVG, Web Design, D3js

Many kinds of wrapping are difficult.

Who knew wrapping and hyphenating text could be so difficult?

For HTML text, there are built-in CSS properties to help. But it’s not so easy when you’re working with SVG. Although there’s support for word wrapping in SVG 2, the spec is still just a W3C candidate recommendation.

For now, if you’re creating dynamic SVGs with text, it’s up to you to handle long strings. I recently found myself in that position while making a new data visualization with d3.js, and I came up with three functions to help.

Together, the functions determine where a string should be split, add hyphens or some other character to break the string and return an array of the split words. While I’d love for others to make use of these functions, keep in mind that not all languages support the same kind of wrapping and hyphenation.

The first function measures the width of a string and takes an optional string to specify the font weight, size and family. The function uses canvas to draw the text and measure it.

function getTextWidth(text, font = "500 12px sans-serif") {

const canvas = document.createElement("canvas");

const context = canvas.getContext("2d");

context.font = font;

return context.measureText(text).width;

}

The second function breaks an individual word, by looping through each character and determining whether it can be added to the current line or if the word should be broken. It returns both the broken, hyphenated strings and any leftover parts of the word, in case there’s space on the line for another word.

function breakString(word, maxWidth, hyphenCharacter='-') {

const characters = word.split("");

const lines = [];

let currentLine = ""; characters.forEach((character, index) => {

const nextLine = `${currentLine}${character}`;

const lineWidth = getTextWidth(nextLine); if (lineWidth >= maxWidth) {

const currentCharacter = index + 1;

const isLastLine = characters.length === currentCharacter;

const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`;

lines.push(isLastLine ? nextLine : hyphenatedNextLine);

currentLine = "";

} else {

currentLine = nextLine;

}

}); return { hyphenatedStrings: lines, remainingWord: currentLine };

}

The last function brings the other two together and determines how to break an entire label. It takes the entire string as a parameter, as well as the maximum width for the string.

function wrapLabel(label, maxWidth) {

const words = label.split(" ");

const completedLines = [];

let nextLine = ""; words.forEach((word, index) => {

const wordLength = getTextWidth(`${word} `);

const nextLineLength = getTextWidth(nextLine); if (wordLength > maxWidth) {

const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth);

completedLines.push(nextLine, ...hyphenatedStrings); nextLine = remainingWord;

} else if (nextLineLength + wordLength >= maxWidth) {

completedLines.push(nextLine);

nextLine = word;

} else {

nextLine = [nextLine, word].filter(Boolean).join(" ");

}

const currentWord = index + 1;

const isLastWord = currentWord === words.length; if (isLastWord) {

completedLines.push(nextLine);

}

}); return completedLines.filter(line => line !== "");

}

For my use case, I used the wrapLabel function to return an array of the strings I should display. I then rendered each string by mapping through the array and returning a tspan for each line.

const label = wrapLabel("supercalifragilisticexpialidocious", 20); // becomes ["supe-", "rcali-", "frag-", "ilistic-", "expi-", "alido-", "ciou-", "s"] const wrappedText = label.map((word, index) => (

<tspan x={0} dy={index === 0 ? 0 : 14}>

{word}

</tspan>

));

Until SVG 2 is released, I hope you find these functions useful.

That’s a wrap!