Search code examples
javascriptd3.jssvgword-wrap

Wrap text function not creating new lines


I have a d3 chart with a title. The title is the chart. However, on small screens the text needs to be wrap as not to over flow.

However the Bl.ocks text wrap method is not creating new lines and instead creating a new tspan for every word and then placing each text/tspan on top of one another

Here is a jsfiddle of the issue: jsfiddle here

Here is the relevant code

var chartTitle = svg.append("text")
                .attr("y", -15 )
                .attr("text-anchor", "start")  
                .text("This is a very long chart title that should be wrapped!")
                .attr('class','chartTitle')
                      .call(wrap, width/2);



        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")),
                  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);
                }
              }
            });
          }

CSS

.chartTitle{
  font-size:20px;
}

Solution

  • That wrap function, originally written by Mike Bostock (D3 creator) for wrapping labels, uses the text dy attribute (which is automatically created by the axis generator). That being said, the problem with your code is that there is no dy attribute in that text element.

    You can simply tweak the function or just set a zero dy attribute. Here is a demo using the latter:

    var svg = d3.select("body")
      .append("svg");
    
    var chartTitle = svg.append("text")
      .attr("y", 20)
      .attr("dy", 0)//set the dy attribute
      .attr("text-anchor", "start")
      .text("This is a very long chart title that should be wrapped!")
      .attr('class', 'chartTitle')
      .call(wrap, 300 / 2);
    
    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")),
          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);
          }
        }
      });
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>