user235236
user235236

Reputation: 467

Wrapping Text in D3

I would like to get the text to wrap on the following D3 tree so that instead of

Foo is not a long word

each line is wrapped to

Foo is
not a
long word

I have tried making the text a 'foreignObject' rather than a text object and the text does indeed wrap, but it doesn't move on the tree animation and is all grouped in the upper left hand corner.

Code located at

http://jsfiddle.net/mikeyai/X43X5/1/

Javascript:

var width = 960,
    height = 500;

var tree = d3.layout.tree()
    .size([width - 20, height - 20]);

var root = {},
    nodes = tree(root);

root.parent = root;
root.px = root.x;
root.py = root.y;

var diagonal = d3.svg.diagonal();

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("transform", "translate(10,10)");

var node = svg.selectAll(".node"),
    link = svg.selectAll(".link");

var duration = 750,
    timer = setInterval(update, duration);

function update() {
  if (nodes.length >= 500) return clearInterval(timer);

  // Add a new node to a random parent.
  var n = {id: nodes.length},
      p = nodes[Math.random() * nodes.length | 0];
  if (p.children) p.children.push(n); else p.children = [n];
  nodes.push(n);

  // Recompute the layout and data join.
  node = node.data(tree.nodes(root), function(d) { return d.id; });
  link = link.data(tree.links(nodes), function(d) { return d.source.id + "-" + d.target.id; });

  // Add entering nodes in the parent’s old position.
  node.enter().append("text")
      .attr("class", "node")
      .attr("x", function(d) { return d.parent.px; })
      .attr("y", function(d) { return d.parent.py; })
        .text('Foo is not a long word');

  // Add entering links in the parent’s old position.
  link.enter().insert("path", ".node")
      .attr("class", "link")
      .attr("d", function(d) {
        var o = {x: d.source.px, y: d.source.py};
        return diagonal({source: o, target: o});
      });

  // Transition nodes and links to their new positions.
  var t = svg.transition()
      .duration(duration);

  t.selectAll(".link")
      .attr("d", diagonal);

  t.selectAll(".node")
      .attr("x", function(d) { return d.px = d.x; })
      .attr("y", function(d) { return d.py = d.y; });
}

Upvotes: 28

Views: 55571

Answers (6)

Ashish Pandey
Ashish Pandey

Reputation: 1

To wrap text you can you use this function

function wrapText(text, width, clientWidth, boundingRect) {
  text.each(function () {
    const textNode = d3.select(this);
    let words = textNode.text().split(/\s+/).reverse();
    words = words.filter((q) => q);
    const lineHeight = 1.1; // Adjust this value to set the line height
    const y = textNode.attr("y");
    const x = textNode.attr("x"); // const dy = parseFloat(textNode.attr("dy"));
    const dy = 0;
    let tspan = textNode
      .text(null)
      .append("tspan")
      .attr("x", x)
      .attr("y", y)
      .attr("dy", dy + "em");
    let word;
    let line = [];
    let lineNumber = 0;
    while ((word = words.pop())) {
      line.push(word);
      tspan.text(line.join(" "));
      // Create a temporary tspan to calculate text length without modifying the content
      const tempTspan = textNode.append("tspan").text(line.join(" "));
      const textLength = tempTspan.node().getComputedTextLength();
      tempTspan.remove();
      if (parseFloat(x) + textLength > clientWidth && textLength < width) {
        tspan.attr("x", boundingRect.width.toString());
      }
      if (parseFloat(x) < boundingRect.left && textLength < width) {
        tspan.attr("x", boundingRect.left.toString());
      }
      if (textLength > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        if (parseFloat(x) + textLength > clientWidth) {
          tspan = textNode
            .append("tspan")
            .attr("x", boundingRect.width.toString())
            .attr("y", y)
            .attr("dy", ++lineNumber * lineHeight + dy + "em")
            .text(word);
        } else if (parseFloat(x) < boundingRect.left) {
          tspan = textNode
            .append("tspan")
            .attr("x", boundingRect.left.toString())
            .attr("y", y)
            .attr("dy", ++lineNumber * lineHeight + dy + "em")
            .text(word);
        } else {
          tspan = textNode
            .append("tspan")
            .attr("x", x)
            .attr("y", y)
            .attr("dy", ++lineNumber * lineHeight + dy + "em")
            .text(word);
        }
      }
    }
  });
}

Inside the function, it uses the text.each() method to iterate over each text element in the selection and apply the wrapping logic individually.

It uses D3's d3.select(this) to select the current text element within the iteration.

It splits the content of the text element into words using the .split(/\s+/) method. The /\s+/ regular expression is used to split the text by whitespace (spaces and newlines). The reverse() method is then called to reverse the order of the words.

It filters out any empty words using .filter((q) => q). This step is useful for cases where there are consecutive spaces or leading/trailing spaces in the text, as it ensures that empty words are removed.

The lineHeight variable is used to set the desired line height for wrapped text. You can adjust this value to control the spacing between lines.

The current y and x attributes of the text element are retrieved using .attr("y") and .attr("x"), respectively. These attributes define the initial position of the text element.

The dy variable is set to 0, which means there is no initial offset along the y-axis for the text.

A temporary tspan element is created within the text element using .append("tspan"). This tspan element is used to calculate the length of the text without actually modifying the content.

The text content of the text element is set to null using .text(null), effectively clearing the content.

The tspan element's x, y, and dy attributes are set to the initial position and offset values.

At this point, the setup for wrapping is complete, and the function is ready to process the words and wrap the text.

The rest of the code inside the while loop is responsible for the actual text wrapping logic. It iterates over each word in the words array and appends it to the current tspan element while measuring its length.

If the length of the current tspan exceeds the specified width, a new line is started by creating a new tspan element. If the text length is greater than the available space on the right or left side of the container, the x attribute of the tspan element is adjusted to fit the text within the available space.

By the end of the function, the text content has been wrapped, and multiple tspan elements may have been created to accommodate the wrapped lines.

Upvotes: 0

Kerwin Sneijders
Kerwin Sneijders

Reputation: 814

You can also use a plain HTML element inside your SVG by using a foreignObject.

For example, I used the following to append an HTML div to my svg object.

svg.append("foreignObject")
    .attr("width", blockWidth)
    .attr("height", blockHeight)
    .append("xhtml:div")
    .style("color", "#000")
    .style("text-align", "center")
    .style("width", "100%")
    .style("height", "100%")
    .style("padding", "5px")
    .style("font-size", `${fontSize}px`)
    .style("overflow-y", "auto")
    .html("The text to display")

Result:

<foreignObject width="200" height="50">
    <div style="color: rgb(0, 0, 0); text-align: center; width: 100%; height: 100%; padding: 5px; font-size: 12px; overflow-y: auto;">
        The text to display
    </div>
</foreignObject>

You could also use a .attr('class', 'classname') instead of all the .style(...) calls and insert styles through a stylesheet if your styles are static.

Source (and more info/options) from this SO answer.

Upvotes: 2

brandones
brandones

Reputation: 1937

If you're using React, a library you can use for this is @visx/text. It exposes a more powerful SVG text element that supports a width parameter.

import { Text } from '@visx/text';

const App = () => (
  <svg>
    <Text width={20}>Foo is not a long word</Text>
  </svg>
);

Upvotes: 3

mdml
mdml

Reputation: 22882

You can modify Mike Bostock's "Wrapping Long Labels" example to add <tspan> elements to your <text> nodes. There are two major changes required to add wrapped text to your nodes. I didn't delve into having the text update its position during transitions, but it shouldn't be too hard to add.

The first is to add a function wrap, based off of the function in the above example. wrap will take care of adding <tspan> elements to make your text fit within a certain width:

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
            x = text.attr("x"),
            y = text.attr("y"),
            dy = 0, //parseFloat(text.attr("dy")),
            tspan = text.text(null)
                        .append("tspan")
                        .attr("x", x)
                        .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", x)
                            .attr("y", y)
                            .attr("dy", ++lineNumber * lineHeight + dy + "em")
                            .text(word);
            }
        }
    });
}

The second change is that instead of setting the text of each node, you need to call wrap for each node:

// Add entering nodes in the parent’s old position.
node.enter().append("text")
    .attr("class", "node")
    .attr("x", function (d) { return d.parent.px; })
    .attr("y", function (d) { return d.parent.py; })
    .text("Foo is not a long word")
    .call(wrap, 30); // wrap the text in <= 30 pixels

Upvotes: 45

Kristina Darroch
Kristina Darroch

Reputation: 171

This is a way to text wrap using d3 plus. It's really easy for me and works in all browsers as of now

d3plus.textwrap()
    .container(d3.select("#intellectual"))
    .shape('square')
    .width(370)
    .height(55)
    .resize(true)
    .draw();      

Upvotes: 0

user879121
user879121

Reputation:

Another option, if you're willing to add another JS lib, is to use D3plus, a D3 addon. It has built-in text wrapping functionality. It even supports padding and resizing text to fill the available space.

d3plus.textwrap()
  .container(d3.select("#rectWrap"))
  .draw();

I've used it. It sure beats calculating the wrapping yourself.

There's another d3 plugin available for text wrapping but I've never used it so I can't speak to it's usefulness.

Upvotes: 3

Related Questions