66
votes

I'm using D3 to generate a bar chart (I adapted the code from this example). The labels I'm using on the x-axis are a couple of words long each, and since this makes all of the labels overlap I need to break these labels across lines. (It'll be fine if I can replace all of the spaces in each label with newlines.)

I originally tried this by replacing the spaces with literal newlines (&#xA;) and setting xml:space="preserve" on the labels' <text> elements. Unfortunately, it turns out that SVG doesn't respect this property. Next I tried to wrap each word in a <tspan> that I could later style. I passed each label through this function:

function (text) {
    return '<tspan>' + text.replace(/ /g, '</tspan><tspan>') + '</tspan>';
}

but this just puts literal <tspan>s into the output. How can I wrap my text labels in tspans (or do something else) so that my labels don't overlap?

6
Is this a duplicate question? stackoverflow.com/questions/4991171/…Paul Armstrong
@PaulArmstrong not really, OP needs to insert tspan's not autowrap text with foreignObject (which is an overkill and unsupported by IE9 (and 10?).methodofaction

6 Answers

89
votes

I ended up using the following code to break each x-axis label across lines:

var insertLinebreaks = function (d) {
    var el = d3.select(this);
    var words = d.split(' ');
    el.text('');

    for (var i = 0; i < words.length; i++) {
        var tspan = el.append('tspan').text(words[i]);
        if (i > 0)
            tspan.attr('x', 0).attr('dy', '15');
    }
};

svg.selectAll('g.x.axis g text').each(insertLinebreaks);

Note that this assumes that the labels have already been created. (If you follow the canonical histogram example then the labels will have been set up in just the way you need.) There also isn't any real line-breaking logic present; the function converts every space into a newline. This fits my purposes fine but you may need to edit the split() line to be smarter about how it partitions the parts of the string into lines.

10
votes

SVG text element does not support text-wrapping, so there are two options:

  • split the text into multiple SVG text elements
  • use an overlay HTML div on top of the SVG

See Mike Bostock's comment on this here.

6
votes

Something I've found to be useful is using a 'foreignObject' tag instead of text or tspan elements. This allows for the simple embedding of HTML, allowing for words to break naturally. The caveat being the overall dimensions of the object meeting specific needs:

var myLabel = svg.append('foreignObject')
    .attr({
        height: 50,
        width: 100, // dimensions determined based on need
        transform: 'translate(0,0)' // put it where you want it...
     })
     .html('<div class"style-me"><p>My label or other text</p></div>');

Whatever elements you place inside of this object can later be obtained using d3.select/selectAll to update text values dynamically as well.

4
votes

Having looked around I found that Mike Bostock has provided a solution enabling you to wrap text round.

http://bl.ocks.org/mbostock/7555321

To implement it on my code (I'm using collapsed tree diagram). I simply copied the "wrap" method.

Then appended the following

    // Standard code for a node    
    nodeEnter.append("text")
        .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
        .attr("dy", ".35em")
        .text(function(d) { return d.text; })
        // New added line to call the function to wrap after a given width
        .call(wrap, 40);

I don't see any reason this should not work for a force-directed, bar or any other pattern

Amendment :

I've modified the wrap function to the following for anyone who reads this and is using collapisible graph. The change in the "x" attribute sets the allignment correctly, incrementing linenumber was performed on a separate line as issues were noted in the original code and "y" has been hard set to zero otherwise issues would occur in which the line spacing increased with each line.

function wrap(text, width) {
    text.each(function() {
        var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        y = text.attr("y"),
        dy = parseFloat(text.attr("dy")),
        lineHeight = 1.1, // ems
        tspan = text.text(null).append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", y).attr("dy", dy + "em");     
        while (word = words.pop()) {
            line.push(word);
            tspan.text(line.join(" "));
            var textWidth = tspan.node().getComputedTextLength();
            if (tspan.node().getComputedTextLength() > width) {
                line.pop();
                tspan.text(line.join(" "));
                line = [word];
                ++lineNumber;
                tspan = text.append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", 0).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
            }
        }
    });
}
0
votes

There's also this answer on wrapping long labels.

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.bar {
  fill: steelblue;
}

.bar:hover {
  fill: brown;
}

.title {
  font: bold 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.axis {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>

var margin = {top: 80, right: 180, bottom: 80, left: 180},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1, .3);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(8, "%");

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.tsv("data.tsv", type, function(error, data) {
  x.domain(data.map(function(d) { return d.name; }));
  y.domain([0, d3.max(data, function(d) { return d.value; })]);

  svg.append("text")
      .attr("class", "title")
      .attr("x", x(data[0].name))
      .attr("y", -26)
      .text("Why Are We Leaving Facebook?");

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis)
    .selectAll(".tick text")
      .call(wrap, x.rangeBand());

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis);

  svg.selectAll(".bar")
      .data(data)
    .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.name); })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); });
});

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

function type(d) {
  d.value = +d.value;
  return d;
}

</script>

and the data file "data.tsv":

name    value
Family in feud with Zuckerbergs .17
Committed 671 birthdays to memory   .19
Ex is doing too well    .10
High school friends all dead now    .15
Discovered how to “like” things mentally    .27
Not enough politics .12
-1
votes

use <tspan>

and in nv.d3

nv.models.axis = function() {

...

      .select('text')
            .attr('dy', '0em')
            .attr('y', -axis.tickPadding())
            .attr('text-anchor', 'middle')
            .text(function(d,i) {
              var v = fmt(d);
              return ('' + v).match('NaN') ? '' : v;
            });

change all occurrences of .text( to .html(