24

I'm drawing text labels in SVG. I have a fixed width available (say 200px). When the text is too long, how can I trim it ?

The ideal solution would also add ellipsis (...) where the text is cut. But I can also live without it.

Robert Longson
  • 118,664
  • 26
  • 252
  • 242
Blacksad
  • 14,906
  • 15
  • 70
  • 81

8 Answers8

34

Using d3 library

a wrapper function for overflowing text:

    function wrap() {
        var self = d3.select(this),
            textLength = self.node().getComputedTextLength(),
            text = self.text();
        while (textLength > (width - 2 * padding) && text.length > 0) {
            text = text.slice(0, -1);
            self.text(text + '...');
            textLength = self.node().getComputedTextLength();
        }
    } 

usage:

text.append('tspan').text(function(d) { return d.name; }).each(wrap);
user2846569
  • 2,752
  • 2
  • 23
  • 24
30

One way to do this is to use a textPath element, since all characters that fall off the path will be clipped away automatically. See the text-path examples from the SVG testsuite.

Another way is to use CSS3 text-overflow on svg text elements, an example here. Opera 11 supports that, but you'll likely find that the other browsers support it only on html elements at this time.

You can also measure the text strings and insert the ellipsis yourself with script, I'd suggest using the getSubStringLength method on the text element, increasing the nchars parameter until you find a length that is suitable.

Erik Dahlström
  • 59,452
  • 12
  • 120
  • 139
  • 2
    The CSS3 example is not working for me. Neither the text-overflow or the text path seem to clip the overflow. Has anything changed? – David Meza Dec 20 '16 at 14:42
  • @DavidMeza sadly 'text-overflow' (still) isn't supported inside svg text elements, https://bugs.chromium.org/p/chromium/issues/detail?id=366550 – Erik Dahlström Dec 20 '16 at 15:24
  • @ErikDahlström Thank you, the first method worked great! I fiddled with it a bit as on my first attempt wasn't using quite unique values in the `xlink:href` - `id` associations. – Robert Monfera Apr 10 '17 at 15:33
  • Just note that for `textPath[xlink:href]` to work on a document with a `base[href]`, you'll need to use an absolute href to the path – André Werlang Apr 13 '17 at 19:47
18

Implementing Erik's 3rd suggestion I came up with something like this:

//places textString in textObj, adds an ellipsis if text can't fit in width
function placeTextWithEllipsis(textObj,textString,width){
    textObj.textContent=textString;

    //ellipsis is needed
    if (textObj.getSubStringLength(0,textString.length)>=width){
        for (var x=textString.length-3;x>0;x-=3){
            if (textObj.getSubStringLength(0,x)<=width){
                textObj.textContent=textString.substring(0,x)+"...";
                return;
            }
        }
        textObj.textContent="..."; //can't place at all
    }
}

Seems to do the trick :)

OpherV
  • 6,787
  • 6
  • 36
  • 55
  • This worked for me, but you have to make sure the node is already rendered in the dom. So beware when dynamically add nodes. – borisrorsvort Jun 26 '14 at 13:05
  • @borisrorsvort actually I encountered the same problem. Have you come up with a solution for running this on non-rendered nodes? – OpherV Jun 30 '14 at 11:33
  • At the end of the classical update function I loop all the nodes then using your function. Then when new nodes comes in I rerun the update method. https://gist.github.com/borisrorsvort/cc6ada7e4cbfc7f959b9 – borisrorsvort Jul 01 '14 at 14:14
6

@user2846569 show me how to do it ( yes, using d3.js ). But, I have to make some little changes to work:


         function wrap( d ) {
                var self = d3.select(this),
                    textLength = self.node().getComputedTextLength(),
                    text = self.text();
                while ( ( textLength > self.attr('width') )&& text.length > 0) {
                    text = text.slice(0, -1);
                    self.text(text + '...');
                    textLength = self.node().getComputedTextLength();
                }
            }
            svg.append('text')
                .append('tspan')
                .text(function(d) { return d; }) 
                .attr('width', 200 )
                .each( wrap );
Thiago Mata
  • 2,825
  • 33
  • 32
  • @user2846569's solution has function vars that don't exist. Use this modified solution instead. – lsu_guy Jul 31 '16 at 07:29
4

The linearGradient element can be used to produce a pure SVG solution. This example fades out the truncated text (no ellipsis):

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  <defs>
    <linearGradient gradientUnits="userSpaceOnUse" x1="0" x2="200" y1="0" y2="0" id="truncateText">
      <stop offset="90%" stop-opacity="1" />
      <stop offset="100%" stop-opacity="0" />
    </linearGradient>
    <linearGradient id="truncateLegendText0" gradientTransform="translate(0)" xlink:href="#truncateText" />
    <linearGradient id="truncateLegendText1" gradientTransform="translate(200)" xlink:href="#truncateText" />
  </defs>

  <text fill="url(#truncateLegendText0)" font-size="50" x="0" y="50">0123456789</text>
  <text fill="url(#truncateLegendText1)" font-size="50" x="200" y="150">0123456789</text>
  
</svg>

(I had to use linear gradients to solve this because the SVG renderer I was using does not support the textPath solution.)

Gabriel Deal
  • 935
  • 1
  • 8
  • 19
  • This worked better than any JS options as I was using Angular. Angular was interferring and causing a horrible infinite loop flicker of truncated and full text – t0mmyw Mar 26 '21 at 11:13
3

Try this one, I use this function in my chart library:

function textEllipsis(el, text, width) {
  if (typeof el.getSubStringLength !== "undefined") {
    el.textContent = text;
    var len = text.length;
    while (el.getSubStringLength(0, len--) > width) {}
    el.textContent = text.slice(0, len) + "...";
  } else if (typeof el.getComputedTextLength !== "undefined") {
    while (el.getComputedTextLength() > width) {
      text = text.slice(0,-1);
      el.textContent = text + "...";
    }
  } else {
    // the last fallback
    while (el.getBBox().width > width) {
      text = text.slice(0,-1);
      // we need to update the textContent to update the boundary width
      el.textContent = text + "...";
    }
  }
}
c9s
  • 1,888
  • 19
  • 15
1

My approach was similar to OpherV's, but I tried doing this using JQuery

function getWidthOfText(text, fontSize, fontFamily) {
    var span = $('<span></span>');
    span.css({
       'font-family': fontFamily,
        'font-size' : fontSize
    }).text(text);
    $('body').append(span);
    var w = span.width();
    span.remove();
    return w;
}

function getStringForSize(text, size, fontSize, fontFamily) {
    var curSize = getWidthOfText(text, fontSize, fontFamily);
    if(curSize > size)
    {
        var curText = text.substring(0,text.length-5) + '...';
        return getStringForSize(curText, size, fontSize, fontFamily);
    }
    else
    {
        return text;
    }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Now when calling getStringForSize('asdfasdfasdfasdfasdfasdf', 110, '13px','OpenSans-Light') you'll get "asdfasdfasdfasd..."

Umashankar
  • 694
  • 7
  • 21
Elektropepi
  • 1,115
  • 1
  • 13
  • 22
1

There is several variants using d3 and loops for search smaller text that fit. This can be achieved without loops and it work faster. textNode - d3 node.

clipText(textNode, maxWidth, postfix) {
        const textWidth = textNode.getComputedTextLength();       
        if (textWidth > maxWidth) {
            let text = textNode.textContent;
            const newLength = Math.round(text.length * (1 - (textWidth - maxWidth) / textWidth));
            text = text.substring(0, newLength);
            textNode.textContent = text.trim() + postfix;
        }
    }
sepulka
  • 405
  • 4
  • 15
  • 1
    This doesn't work in general, because characters can have different widths. In this case the reduced length cannot be obtained via proportional scaling. It is a good alternative though for mono-spaced fonts. – bluenote10 Jan 17 '21 at 22:38
  • @bluenote10, you're right, but I think difference in 1-2-3 symbols are not principal in most cases. For example I use it to trim too long titles (more than 30 symbols) on chart. I start with solution that use loop, but it add noticeable delay in chart update, if I have a lot of titles (normal situation for my application). I think speed is more important than accuracy in javascript applications in most cases. – sepulka Jan 19 '21 at 07:31
  • For symbols yes, but what if the string contains a total of 10 IIIIIIIIII vs 10 WWWWWWWWWW? It simply depends on the use case. If your designer wants all strings to be as close in length as possible you have to measure the text. Also note that you can internally cache these string length measurements, which will give excellent performance in many situations as well. – bluenote10 Jan 19 '21 at 08:00
  • @bluenote10, you're fully right, but I think you describe rare situations, in most cases functions like this used for regular text. Anyway you describe disadvantages of my method, this help if anybody try to use it, thank you. – sepulka Jan 22 '21 at 07:50