67

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?

bdesham
  • 15,430
  • 13
  • 79
  • 123
  • Is this a duplicate question? http://stackoverflow.com/questions/4991171/auto-line-wrapping-in-svg-text – Paul Armstrong Nov 06 '12 at 01:46
  • 1
    @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 Nov 06 '12 at 02:07

6 Answers6

92

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.

bdesham
  • 15,430
  • 13
  • 79
  • 123
  • 6
    This was exactly what I needed, thank you. For those who might be in a similar situation, I should point out that `d` here is a data point and *not* the string you're formatting, so `.split()` (for me at least) required a change to `d.description.split("\n");`. – Daniel Quinn Mar 06 '13 at 18:06
  • Works beautifully. For guys working on this,make sure you have the right elements. – Nevin Madhukar K Aug 13 '14 at 11:13
  • 2
    Another improvement to this beyond @DanielQuinn's suggestion to split on newlines: use `d3.select(this).text()` to grab the actual element text instead of the bound data value. This matters for 2 reasons: first, your axis values could be numbers rather than strings (this code fails in that case) and second, my improvement allows the custom text to be inserted by a `tickFormat` function on the axis generator. – Factor Mystic Apr 20 '15 at 00:14
  • 1
    For anyone else who uses this, found I needed to set the variable words to d.toString().split(' '). –  Jan 07 '16 at 12:32
  • This worked for me using v4. I added a pipe to the date format because that's where I wanted my newline to occur: `var dateFormat = d3.timeFormat("%b %e|%H:%M"); ... var el = d3.select(this); var words = el.text().split('|'); el.text('');` – utdrmac Nov 13 '17 at 16:50
10

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.

Ilya Boyandin
  • 3,069
  • 25
  • 23
7

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.

chrisjordanme
  • 612
  • 8
  • 11
5

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);
            }
        }
    });
}
John Duskin
  • 355
  • 5
  • 12
1

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
Nav
  • 19,885
  • 27
  • 92
  • 135
-1

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(

Metalstorm
  • 2,940
  • 3
  • 26
  • 22