144

So here is what I have:

<path class="..." onmousemove="show_tooltip(event,'very long text 
    \\\n I would like to linebreak')" onmouseout="hide_tooltip()" d="..."/>

<rect class="tooltip_bg" id="tooltip_bg" ... />
<text class="tooltip" id="tooltip" ...>Tooltip</text>

<script>
<![CDATA[
function show_tooltip(e,text) {
    var tt = document.getElementById('tooltip');
    var bg = document.getElementById('tooltip_bg');

    // set position ...

    tt.textContent=text;

    bg.setAttribute('width',tt.getBBox().width+10);
    bg.setAttribute('height',tt.getBBox().height+6);

    // set visibility ...
}
...

Now my very long tooltip text doesn't have a linebreak, even though if I use alert(); it shows me that the text actually DOES have two lines. (It contains a "\" though, how do I remove that one by the way?)
I can't get CDATA to work anywhere.

sollniss
  • 1,895
  • 2
  • 19
  • 36

6 Answers6

192

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.

Sergiu Dumitriu
  • 11,455
  • 3
  • 39
  • 62
  • 2
    And how do I recognize where to put the `s`? Replace? Split? – sollniss May 22 '13 at 21:43
  • 2
    Tried it out `var tspan = document.createElement('tspan') tspan.setAttribute('x','0'); tspan.setAttribute('dy','1.2em'); tspan.textContent = text; tt.appendChild(tspan);` doesn't show any text at all. – sollniss May 22 '13 at 22:20
  • 2
    Would you care to elaborate on why the *x='0' dy='1.2em'* is needed? It does work, indeed, just like you said. However, I was expecting it to work even without those attributes. Instead, nothing's displayed... Also, I'mnot entirely clear on **why** the linebreak occurs at all. It's not like we've set up the width of the container to something fix, so that it can impose line breaking, have we? – Konrad Viltersten Jun 28 '16 at 11:10
  • 4
    `x=0` is an absolute coordinate: move the text fragment to the origin of the [*current coordinate system*](https://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace). The `transform` attribute on the `g` element defines a new current coordinate system, and assuming that the text is left-aligned, the tspan is moved to the left. This acts like a carriage return instruction. `dy=1.2em` is a *relative* coordinate: move the text fragment by this amount, relative to the current text fragment. This acts like a line-feed instruction. Combined, you get a CR/LF. – Sergiu Dumitriu Jun 28 '16 at 12:00
  • Not tried this yet: Could you also do this without the group? very long textI would like to linebreak ?? – Richard Oct 02 '17 at 20:17
  • Following on from my prior comment: The first line of the textThe second line of the textThe third line of the text Seems to work OK – Richard Oct 03 '17 at 14:23
  • 1
    @Richard It does work, however it has the slight disadvantage that you must compute the coordinates for each line yourself. `x=0 dy=1.2em` is much simpler, it's the same for each line, and doesn't depend on the font size. Another advantage is that if you want to move the whole text, you can just update one pair of values in the group's `transform` attribute, instead of updating the coordinates of every tspan. – Sergiu Dumitriu Oct 11 '17 at 15:22
  • Good points. Still - it's implemented now and works OK - have a goosey: https://www.facebook.com/rgraph/posts/1667831796602006 – Richard Oct 11 '17 at 18:54
36

I suppese you alredy managed to solve it, but if someone is looking for similar solution then this worked for me:

 g.append('svg:text')
  .attr('x', 0)
  .attr('y', 30)
  .attr('class', 'id')
  .append('svg:tspan')
  .attr('x', 0)
  .attr('dy', 5)
  .text(function(d) { return d.name; })
  .append('svg:tspan')
  .attr('x', 0)
  .attr('dy', 20)
  .text(function(d) { return d.sname; })
  .append('svg:tspan')
  .attr('x', 0)
  .attr('dy', 20)
  .text(function(d) { return d.idcode; })

There are 3 lines separated with linebreak.

Kristīne Glode
  • 1,409
  • 4
  • 16
  • 22
  • 31
    FWIW: looks like the OP was using pure JavaScript; this answer appears to be leveraging [D3](http://d3js.org/). – Ben Mosher Sep 15 '14 at 15:08
  • I am using D3, and your approach worked for me. Thanks for posting it. I found that I needed to delete the old tspans first before appended new ones, like this: focus.selectAll("tspan").remove(); – Darren Parker Dec 14 '15 at 19:38
  • 1
    Beware with this approach that it nests the tags since it chains .append(). This can cause some minor headaches with CSS depending on what you want to do. – seneyr Feb 22 '17 at 16:56
  • See [here](http://jarrettmeyer.com/2018/06/05/svg-multiline-text-with-tspan) for an approach that avoids the nesting described by @seneyr – maltem-za Nov 30 '18 at 12:38
  • with modern d3 one can utilize .call() for each of the append groups to avoid nesting – Simon May 06 '22 at 18:44
22

With the tspan solution, let's say you don't know in advance where to put your line breaks: you can use this nice function, that I found here: http://bl.ocks.org/mbostock/7555321

That automatically does line breaks for long text svg for a given width in pixel.

function wrap(text, width) {
  text.each(function() {
    var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.1, // ems
        y = text.attr("y"),
        dy = parseFloat(text.attr("dy")) || 0,
        tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
    while (word = words.pop()) {
      line.push(word);
      tspan.text(line.join(" "));
      if (tspan.node().getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
      }
    }
  });
}
Elias Dorneles
  • 22,556
  • 11
  • 85
  • 107
steco
  • 1,303
  • 13
  • 16
16

use HTML instead of javascript

limitation: the SVG renderer must support HTML rendering

for example, inkscape cannot render such SVG files

<html>
  <head><style> * { margin: 0; padding: 0; } </style></head>
  <body>
    <h1>svg foreignObject to embed html</h1>

    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 300 300"
      x="0" y="0" height="300" width="300"
    >

      <circle
        r="142" cx="150" cy="150"
        fill="none" stroke="#000000" stroke-width="2"
      />

      <foreignObject
        x="50" y="50" width="200" height="200"
      >
        <div
          xmlns="http://www.w3.org/1999/xhtml"
          style="
            width: 196px; height: 196px;
            border: solid 2px #000000;
            font-size: 32px;
            overflow: auto; /* scroll */
          "
        >
          <p>this is html in svg 1</p>
          <p>this is html in svg 2</p>
          <p>this is html in svg 3</p>
          <p>this is html in svg 4</p>
        </div>
      </foreignObject>

    </svg>

</body></html>
milahu
  • 2,447
  • 1
  • 18
  • 25
10

I think this does what you want:

function ShowTooltip(evt, mouseovertext){
    // Make tooltip text        
    var tooltip_text = tt.childNodes.item(1);
    var words = mouseovertext.split("\\\n");
    var max_length = 0;

    for (var i=0; i<3; i++){
        tooltip_text.childNodes.item(i).firstChild.data = i<words.length ?  words[i] : " ";
        length = tooltip_text.childNodes.item(i).getComputedTextLength();
        if (length > max_length) {max_length = length;}
    }

    var x = evt.clientX + 14 + max_length/2;
    var y = evt.clientY + 29;
    tt.setAttributeNS(null,"transform", "translate(" + x + " " + y + ")")

    // Make tooltip background
    bg.setAttributeNS(null,"width", max_length+15);
    bg.setAttributeNS(null,"height", words.length*15+6);
    bg.setAttributeNS(null,"x",evt.clientX+8);
    bg.setAttributeNS(null,"y",evt.clientY+14);

    // Show everything
    tt.setAttributeNS(null,"visibility","visible");
    bg.setAttributeNS(null,"visibility","visible");
}

It splits the text on \\\n and for each puts each fragment in a tspan. Then it calculates the size of the box required based on the longest length of text and the number of lines. You will also need to change the tooltip text element to contain three tspans:

<g id="tooltip" visibility="hidden">
    <text><tspan>x</tspan><tspan x="0" dy="15">x</tspan><tspan x="0" dy="15">x</tspan></text>
</g>

This assumes that you never have more than three lines. If you want more than three lines you can add more tspans and increase the length of the for loop.

Peter Collingridge
  • 10,849
  • 3
  • 44
  • 61
2

I have adapted a bit the solution by @steco, switching the dependency from d3 to jquery and adding the height of the text element as parameter

function wrap(text, width, height) {
  text.each(function(idx,elem) {
    var text = $(elem);
    text.attr("dy",height);
        var words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.1, // ems
        y = text.attr("y"),
        dy = parseFloat( text.attr("dy") ),
        tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
    while (word = words.pop()) {
      line.push(word);
      tspan.text(line.join(" "));
      if (elem.getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
      }
    }
  });
}
Machado
  • 8,965
  • 6
  • 43
  • 46
loretoparisi
  • 15,724
  • 11
  • 102
  • 146