How to Linebreak an Svg Text Within JavaScript

How to linebreak an svg text within javascript?

This is not something that SVG 1.1 supports. SVG 1.2 does have the textArea element, with automatic word wrapping, but it's not implemented in all browsers. SVG 2 does not plan on implementing textArea, but it does have auto-wrapped text.

However, given that you already know where your linebreaks should occur, you can break your text into multiple <tspan>s, each with x="0" and dy="1.4em" to simulate actual lines of text. For example:

<g transform="translate(123 456)"><!-- replace with your target upper left corner coordinates -->
<text x="0" y="0">
<tspan x="0" dy="1.2em">very long text</tspan>
<tspan x="0" dy="1.2em">I would like to linebreak</tspan>
</text>
</g>

Of course, since you want to do that from JavaScript, you'll have to manually create and insert each element into the DOM.

How to linebreak an svg text in javascript?

You need to specify the position (or offset) of each tspan element to give the impression of a line break -- they are really just text containers that you can position arbitrarily. This is going to be much easier if you wrap the text elements in g elements because then you can specify "absolute" coordinates (i.e. x and y) for the elements within. This will make moving the tspan elements to the start of the line easier.

The main code to add the elements would look like this.

text.append("text")       
.each(function (d) {
var arr = d.name.split(" ");
for (i = 0; i < arr.length; i++) {
d3.select(this).append("tspan")
.text(arr[i])
.attr("dy", i ? "1.2em" : 0)
.attr("x", 0)
.attr("text-anchor", "middle")
.attr("class", "tspan" + i);
}
});

I'm using .each(), which will call the function for each element and not expect a return value instead of the .text() you were using. The dy setting designates the line height and x set to 0 means that every new line will start at the beginning of the block.

Modified jsfiddle here, along with some other minor cleanups.

Line break in C3 generated SVG chart via JavaScript

Your question statement was a bit unprecise : You are using C3.js which will produce svg element.

So the markup returned was actually <tspan dx="0" dy=".71em" x="0">0<br>2014</tspan>.

C3 will use the textContent property of the tspan to append the text content returned by your function.

As already said in other questions, you can't add a line break into <tspan> elements.

So the solution is effectively to create a new tspan just under the other one, in the same <text> element.

Unfortunately, there is no way to get these precise elements except by looping through all others tspans, so this may sounds like a real hack but here is a script that will do what you want...

// get our svg doc
var svg = document.querySelector('svg');
// get our tspans element
var tspans = svg.querySelectorAll('tspan');
// transform it to an array so the clones don't add to the list
var ts = Array.prototype.slice.call(tspans);

for(var i = 0; i<ts.length; i++){
// get the content
var cont = ts[i].textContent.split('\n');
// that wasn't the good one...
if(cont.length<2) continue;
// create a clone
var clone = ts[i].cloneNode(1);
// set the text to the new line
clone.textContent = cont[1];
// space it a litlle bit more
clone.setAttribute('dy', '0.9em')
// set the good text to the upperline
ts[i].textContent = cont[0];
// append our clone
ts[i].parentNode.insertBefore(clone, ts[i].nextSibling)
};

var chart = c3.generate({    data: {        json: [{            date: '2014-01-01',            upload: 200,            download: 200,            total: 400        }, {            date: '2014-01-02',            upload: 100,            download: 300,            total: 400        }, {            date: '2014-02-01',            upload: 300,            download: 200,            total: 500        }, {            date: '2014-02-02',            upload: 400,            download: 100,            total: 500        }],        keys: {            x: 'date',            value: ['upload', 'download']        }    },    axis: {        x: {            type: 'timeseries',            tick: {                format: function (x) {                    if (x.getDate() === 1) {                        return x.getMonth() + '\n' + x.getFullYear();                                          }                }            }        }    }});// get our svg docvar svg  = document.querySelector('svg');// get our tspans elementvar tspans = svg.querySelectorAll('tspan');// transform it to an array so the clones don't add to the listvar ts = Array.prototype.slice.call(tspans);
for(var i = 0; i<ts.length; i++){ // get the content var cont = ts[i].textContent.split('\n'); // that wasn't the good one... if(cont.length<2) continue; // create a clone var clone = ts[i].cloneNode(1); // set the text to the new line clone.textContent = cont[1]; // space it a litlle bit more clone.setAttribute('dy', '0.9em') // set the good text to the upperline ts[i].textContent = cont[0]; // append our clone ts[i].parentNode.insertBefore(clone, ts[i].nextSibling)};
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script><script src="https://rawgit.com/masayuki0812/c3/master/c3.js"></script><link href="https://rawgit.com/masayuki0812/c3/master/c3.css" rel="stylesheet"><div id="chart"></div>

Proper way to change a SVG text without changing its children tspan

"Is there a property or any other method to specifically change the text content only?"

No, probably not. In fact, I think it might be for the better that such a method doesn't exist. Say for example that your <text> node looked like this:

<text>
Foo
<tspan>Bar</tspan>
Baz
</text>

And now we would call the unknown method to change the text into Hello world. What would the desired outcome be?

<text>
Hello world
<tspan>Bar</tspan>
</text>
<text>
<tspan>Bar</tspan>
Hello world
</text>

or even

<text>
Hello
<tspan>Bar</tspan>
world
</text>

Looking at it like this, it becomes clear that the "text content" can't be seen as a special case that exists apart from the node tree: just like the <tspan> in your example, there can be multiple text nodes in any order and quantity. It's therefore hard to justify a special method that only changes text nodes, without introducing method to only change all the <tspan>, <div> and so on as well.

Solutions

If you regard all the text nodes as some kind of typeless elements like this:

<text>
<text>Foo</text>
<tspan>Bar</tspan>
<text>Baz</text>
</text>

there is little question about how you would approach targeting and changing your desired element. Options include:

  • Grab it by index (like you already did). Judging from the fictional snippet above, it would be pretty safe to assume that Foo will indeed always be the first element. But just like with the :first selector in CSS, you might rather not want to depend so rigidly on a specific structure.
  • A class="text" attribute would be very clear, but then you'd need to wrap your text in an actual element (which might be the best solution, but I'm following your specs here).
  • Grab the first element that is of the right type, just like you would with any other element.

The snippet below would therefore be a perfectly valid way to alter the first text node in a list of children (written in a functional manner to keep it concise).

Array.from(document.querySelector("text").childNodes)
.find(e => e.nodeType === Node.TEXT_NODE)
.textContent = "New Foo";
<svg>
<text x="10" y="30">
Foo Bar Baz
<tspan x="30" dy="30">FooBar</tspan>
<tspan x="30" dy="30">FooBaz</tspan>
</text>
</svg>


Related Topics



Leave a reply



Submit