Create a gist now

Instantly share code, notes, and snippets.

@mbostock /.block
Last active Apr 4, 2018

What would you like to do?
Wrapping Long Labels
license: gpl-3.0

This example, using satirical data from The Onion, demonstrates how to wrap long axis labels to fit on multiple lines.

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
<!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="//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>
@jimkang

This comment has been minimized.

Show comment Hide comment
@jimkang

jimkang Dec 8, 2013

There's a problem with wrap() if you use it multiple times on the same element. It can be avoided if you use the data as the source of the text rather than textSelection.text(). I tried out this change here: https://gist.github.com/jimkang/7864867

There's also an extended explanation.

jimkang commented Dec 8, 2013

There's a problem with wrap() if you use it multiple times on the same element. It can be avoided if you use the data as the source of the text rather than textSelection.text(). I tried out this change here: https://gist.github.com/jimkang/7864867

There's also an extended explanation.

@jml6m

This comment has been minimized.

Show comment Hide comment
@jml6m

jml6m Nov 2, 2014

With a small change to the wrap function, you can have this code work on labels for horizontal charts as well. Note that the vertical alignment for multi-line labels will not be centered.

https://gist.github.com/jml6m/02b06ffc269982fa987f

jml6m commented Nov 2, 2014

With a small change to the wrap function, you can have this code work on labels for horizontal charts as well. Note that the vertical alignment for multi-line labels will not be centered.

https://gist.github.com/jml6m/02b06ffc269982fa987f

@enactdev

This comment has been minimized.

Show comment Hide comment
@enactdev

enactdev Jan 15, 2015

I edited this to make it responsive. https://gist.github.com/enactdev/a647e60b209e67602304

enactdev commented Jan 15, 2015

I edited this to make it responsive. https://gist.github.com/enactdev/a647e60b209e67602304

@DougManuel

This comment has been minimized.

Show comment Hide comment
@DougManuel

DougManuel Nov 21, 2016

wrap() could be a candidate for a general d3js module. It strikes me as a potentially common issue for D3js users -- not only for axis labels, but tooltips, etc.

Not sure exactly what a wrap module would look like as a general d3js module. I modified the code in this gist for my project (irregular histogram) by adding width attribute to the text element and then retrieving that property in wrap var width = parseFloat(text.attr("width")). I also needed to retrieve the x within the function, in addition to y and dy. I expect that these parameters of text. could all be optional function parameters.

(I am also planning on modifying the code in this gist so that there isn't an empty line when first word is wider than width. I'd welcome advice on how best to incorporate this modification into the gist code).

DougManuel commented Nov 21, 2016

wrap() could be a candidate for a general d3js module. It strikes me as a potentially common issue for D3js users -- not only for axis labels, but tooltips, etc.

Not sure exactly what a wrap module would look like as a general d3js module. I modified the code in this gist for my project (irregular histogram) by adding width attribute to the text element and then retrieving that property in wrap var width = parseFloat(text.attr("width")). I also needed to retrieve the x within the function, in addition to y and dy. I expect that these parameters of text. could all be optional function parameters.

(I am also planning on modifying the code in this gist so that there isn't an empty line when first word is wider than width. I'd welcome advice on how best to incorporate this modification into the gist code).

@pistou

This comment has been minimized.

Show comment Hide comment
@pistou

pistou Jul 11, 2017

There is an issue with you wrap function.
If you use it on a non-displayed chart (one that you can toggle for instance), the legend won't be wrapped.

This comes from getComputedTextLength() that will return 0 if the text is not displayed.

The (ugly) solution I found was to clone my text element and use that clone to compute the original legend.

function wrap(text, width) {
    text.each(function() {
        var text = d3.select(this);
        
        // clone
        let clone = document.createElement('div');
        clone.setAttribute('style', "position:absolute; visibility:hidden; width:0; height:0;");
        document.body.appendChild(clone);
        
        let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        clone.appendChild(svg);
        
        let elt = text.node().cloneNode(true);
        svg.appendChild(elt);
        
        
        var cloneText = d3.select(elt), // cloned text
            words = cloneText.text().split(/\s+/).reverse(),
            word,
            line = [],
            lineNumber = 0,
            lineHeight = 1.4,
            y = cloneText.attr("y"),
            x = cloneText.attr("x"),
            dy = parseFloat(cloneText.attr("dy")),
            tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em"),
            cloneTspan = cloneText.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em"); // equivalent of tspan from the clone

        while (word = words.pop()) {
            line.push(word);
            tspan.text(line.join(" "));
            cloneTspan.text(line.join(" ")); // keep them equivalent
            
            if (cloneTspan.node().getComputedTextLength() > width) {
                line.pop();
                tspan.text(line.join(" "));
                cloneTspan.text(line.join(" ")); // keep them equivalent
                line = [word];
                
                lineNumber++;
                tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
                cloneTspan = cloneText.append("tspan").attr("x", x).attr("y", y).attr("dy", lineNumber * lineHeight + dy + "em").text(word); // keep them equivalent
            }
        }
        
        document.body.removeChild(clone); // remove the clone to not pollute the page
    });
}

pistou commented Jul 11, 2017

There is an issue with you wrap function.
If you use it on a non-displayed chart (one that you can toggle for instance), the legend won't be wrapped.

This comes from getComputedTextLength() that will return 0 if the text is not displayed.

The (ugly) solution I found was to clone my text element and use that clone to compute the original legend.

function wrap(text, width) {
    text.each(function() {
        var text = d3.select(this);
        
        // clone
        let clone = document.createElement('div');
        clone.setAttribute('style', "position:absolute; visibility:hidden; width:0; height:0;");
        document.body.appendChild(clone);
        
        let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        clone.appendChild(svg);
        
        let elt = text.node().cloneNode(true);
        svg.appendChild(elt);
        
        
        var cloneText = d3.select(elt), // cloned text
            words = cloneText.text().split(/\s+/).reverse(),
            word,
            line = [],
            lineNumber = 0,
            lineHeight = 1.4,
            y = cloneText.attr("y"),
            x = cloneText.attr("x"),
            dy = parseFloat(cloneText.attr("dy")),
            tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em"),
            cloneTspan = cloneText.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em"); // equivalent of tspan from the clone

        while (word = words.pop()) {
            line.push(word);
            tspan.text(line.join(" "));
            cloneTspan.text(line.join(" ")); // keep them equivalent
            
            if (cloneTspan.node().getComputedTextLength() > width) {
                line.pop();
                tspan.text(line.join(" "));
                cloneTspan.text(line.join(" ")); // keep them equivalent
                line = [word];
                
                lineNumber++;
                tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
                cloneTspan = cloneText.append("tspan").attr("x", x).attr("y", y).attr("dy", lineNumber * lineHeight + dy + "em").text(word); // keep them equivalent
            }
        }
        
        document.body.removeChild(clone); // remove the clone to not pollute the page
    });
}
@praveenuics

This comment has been minimized.

Show comment Hide comment
@praveenuics

praveenuics Oct 22, 2017

Is there any such link where i can find the wrap function in typescript, please. i am working with d3v4 in angular2

Is there any such link where i can find the wrap function in typescript, please. i am working with d3v4 in angular2

@clchow

This comment has been minimized.

Show comment Hide comment
@clchow

clchow Oct 31, 2017

I changed the variable definition for words to use the data for that text element instead of pulling the rendered text, because I need to recall this text wrapper for showing on hover details when I zoom in and out of a timeline. Not sure if there is a better way but it seems to work for me.

clchow commented Oct 31, 2017

I changed the variable definition for words to use the data for that text element instead of pulling the rendered text, because I need to recall this text wrapper for showing on hover details when I zoom in and out of a timeline. Not sure if there is a better way but it seems to work for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment