Blacksad
Blacksad

Reputation: 15422

Trimming text to a given pixel width in SVG

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.

Upvotes: 25

Views: 29941

Answers (9)

Sideways S
Sideways S

Reputation: 727

I'm surprised that none of the prior answers reference <clip-path>, as I just used it and it truncated the text to the pixel in 100% straightforward SVG. It sounds like the OP might prefer the current #2 answer here, as it uses <textPath> which truncates at letter boundaries, at least in my testing on Chrome.

Here is a snippet based on the MDN docs page example for <clip-path> (you might have to scroll the results to see the truncation):

<svg viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
  <clipPath id="myClip" clipPathUnits="userSpaceOnUse">
    <rect width="20" height="20"/>
  </clipPath>
  <rect x="0.5" y="0.5" width="24" height="24" stroke="green" fill="none"/>
  <text x="1" y="15" fill="blue" font-size="12" clip-path="url(#myClip)">Truncation at the pixel</text>
</svg>

Upvotes: 0

Elektropepi
Elektropepi

Reputation: 1165

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..."

Upvotes: 1

sepulka
sepulka

Reputation: 405

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;
        }
    }

Upvotes: 1

Gabriel D
Gabriel D

Reputation: 1025

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.)

Upvotes: 4

c9s
c9s

Reputation: 1917

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 + "...";
    }
  }
}

Upvotes: 3

Thiago Mata
Thiago Mata

Reputation: 2959

@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 );

Upvotes: 6

user2846569
user2846569

Reputation: 2832

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);

Upvotes: 36

OpherV
OpherV

Reputation: 6937

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 :)

Upvotes: 19

Erik Dahlstr&#246;m
Erik Dahlstr&#246;m

Reputation: 60966

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.

Upvotes: 32

Related Questions