geotheory
geotheory

Reputation: 23690

Wrapping long text in d3.js

I want to wrap long text elements to a width. The example here is taken from Bostock's wrap function, but seems to have 2 problems: firstly the result of wrap has not inherited the element's x value (texts are shifted left); secondly it's wrapping on the same line, and lineHeight argument has no effect.

Grateful for suggestions. http://jsfiddle.net/geotheory/bk87ja3g/

var svg = d3.select("body").append("svg")
    .attr("width", 300)
    .attr("height", 300)
    .style("background-color", '#ddd');

dat = ["Ukip has peaked, but no one wants to admit it - Nigel Farage now resembles every other politician", 
       "Ashley Judd isn't alone: most women who talk about sport on Twitter face abuse",
       "I'm on list to be a Mars One astronaut - but I won't see the red planet"];

svg.selectAll("text").data(dat).enter().append("text")
    .attr('x', 25)
    .attr('y', function(d, i){ return 30 + i * 90; })
    .text(function(d){ return d; })
    .call(wrap, 250);

function wrap(text, width) {
    text.each(function() {
        var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 1,
        lineHeight = 1.2, // 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);
            }
        }
    });
}

Upvotes: 3

Views: 5283

Answers (2)

Mark
Mark

Reputation: 108567

Bostock's original function assumes that the text element has an initial dy set. It also drops any x attribute on the text. Finally, you changed the wrap function to start at lineNumber = 1, that needs to be 0.

Refactoring a bit:

function wrap(text, width) {
    text.each(function() {
        var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0, //<-- 0!
        lineHeight = 1.2, // ems
        x = text.attr("x"), //<-- include the x!
        y = text.attr("y"),
        dy = text.attr("dy") ? text.attr("dy") : 0; //<-- null check
        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);
            }
        }
    });
}

Updated fiddle.

Upvotes: 3

Lars Kotthoff
Lars Kotthoff

Reputation: 109292

The problem is this line:

dy = parseFloat(text.attr("dy"))

In the example you've linked to, dy is set on the text elements, but not in your case. So you're getting NaN there which in turn causes the dy for the tspan to be NaN. Fix by assigning 0 to dy if NaN:

dy = parseFloat(text.attr("dy")) || 0

Complete demo here.

Upvotes: 3

Related Questions