41

I'm using D3.js. I'd like to find an SVG equivalent to this CSS class, which adds ellipses if text flows out of its containing div:

.ai-ellipsis {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  -o-text-overflow: ellipsis;
  -moz-binding: url(<q>assets/xml/ellipsis.xml#ellipsis</q>);
}

This is my SVG:

<g class="bar" transform="translate(0,39)">
    <text class="label" x="-3" y="6.5" dy=".35em" text-anchor="start">Construction</text>    
    <rect height="13" width="123"></rect>
</g>

It's generated as follows:

barEnter.append("text").attr("class", "label")
        .attr("x", -3).attr("y", function() { return y.rangeBand() / 2})
        .attr("dy", ".35em").attr("text-anchor", "start")
        .text(function(d) {
            return d.Name;
        });

Currently the text is overflowing and overlapping the rect element.

Is there any way I can say "if text is more than a certain width, crop it and add ellipses"?

Rishikesh Raje
  • 8,556
  • 2
  • 16
  • 31
Richard
  • 62,943
  • 126
  • 334
  • 542
  • Once you implemented your solution, in terms of improved UX, you may consider providing an _alternative to show the original value_, in case the truncated text is not obviously easy to understand or distinguish from other truncated texts. e.g. use tooltips – Ricardo Jun 11 '19 at 01:03

6 Answers6

81

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
  • 4
    Great solution, my only nit-pick is the width and padding aren't defined in the variable section. – patorjk May 19 '15 at 20:51
  • 6
    @patorjk this would be solved by having `wrap()` returning a function like: `function wrap(width, padding) { return function() { /* original function */ } }`. Usage would be `text.append('tspan').text(function(d) { return d.name; }).each(wrap(100, 5));` – wvengen Jun 12 '15 at 10:48
  • 1
    if speed is required binary search could be used to determine correct length of text ... – user2846569 May 05 '16 at 10:50
  • 5
    Great answer. I'd tweak it further by using a unicode elipsis character instead of 3 dots. Use … in HTML,or \u2026 in JS – Neil Myatt Mar 29 '17 at 12:27
  • should be the accepted answer as the question indicates d3 is available and assumably prefered. – marcel-k Dec 19 '17 at 10:24
  • `width` and `padding` is not defined – Edgar Quintero Feb 27 '20 at 01:19
17

I am not aware of an equivalent CSS class for SVG, but you can use foreignObject to embed HTML in SVG. This gives you access to this functionality and is more flexible in general (e.g. you can do automatic line breaking easily).

See here for a complete example.

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
9

This function does not depend on d3:

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 + "...";
    }
  }
}
N-ate
  • 6,051
  • 2
  • 40
  • 48
c9s
  • 1,888
  • 19
  • 15
2
function trimText(text, threshold) {
    if (text.length <= threshold) return text;
    return text.substr(0, threshold).concat("...");
}

Use this function to set the SVG node text. The value for the threshold (eg. 20) depends on you. This means that you will display up to 20 characters from your node text. All the texts grater than 20 characters will be trim and display "..." at the end of the trim text.

Usage eg. :

var self = this;
nodeText.text(x => self.trimText(x.name, 20)) // nodeText is the text element of the SVG node
C. Draghici
  • 157
  • 5
  • I don't like the solution very much. It doesn't answer the question HOW to calculate the threshold. But at least I would consider the "ellipses" in the threshold length: return text.substr(0, threshold-3).concat("..."); – Christian Feb 23 '20 at 21:47
  • This would work if all characters were the same width. But 10 w's are much wider than 20 i's or spaces. So, it potentially leaves a lot of white space. – Craig Apr 27 '21 at 20:13
  • Also I would add a trimEnd and the threshold is kinda used wrong here. Here is my solution: const transformToShortedString = (string, maxLength) => { if (string.length > maxLength) { return `${string.substring(0, maxLength - 3).trimEnd()}...`; } return string; }; – juliushuck Apr 24 '23 at 01:48
0

Mauro Colella's excellent answer, in Typescript:

export const svgTextEllipsis = (textNode: SVGTextElement, padding = 0) => {
  const d3Node = d3.select(textNode);
  const targetWidth = Number(d3Node.attr("width")) - padding;
  const initialText = d3Node.text();
  const precision = 25;
  const maxIterations = 3;

  let textWidth = d3Node.node()?.getComputedTextLength() ?? 0;
  let textLength = initialText.length;
  let text = initialText;
  let i = 0;

  if (textWidth < targetWidth) return;

  while (
    i < maxIterations &&
    text.length > 0 &&
    Math.abs(targetWidth - textWidth) > precision
  ) {
    text =
      textWidth >= targetWidth
        ? text.slice(0, -textLength * 0.15)
        : initialText.slice(0, textLength * 1.15);
    d3Node.text(`${text}…`);
    textWidth = d3Node.node()?.getComputedTextLength() ?? 0;
    textLength = text.length;
    i += 1;
  }

  d3Node.text(d3Node.text().replace(/…+/, "…"));
};

Flonk
  • 133
  • 6
-1

If you write CSS it won't work on . Instead of that write logic and append '...' in string.

Akhil A
  • 59
  • 3