This example, using satirical data from The Onion, demonstrates how to wrap long axis labels to fit on multiple lines.
Last active
August 20, 2024 14:19
-
-
Save mbostock/7555321 to your computer and use it in GitHub Desktop.
Wrapping Long Labels
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
Re: hidden text. In my case, the text was hidden in a 'tab-pane.' My solution, also ugly, was to remove the class immediately before the getComputedTextLength() calculation and then restore the class immediately after. Would love to hear if there is a better way to do this now.
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
x = text.attr("x"),
y = text.attr("y"),
dy = parseFloat(text.attr("dy") || 0),
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
var tabs = d3.selectAll('div.tab-pane').classed('tab-pane', false); // remove tab-pane class
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); // add 'x'
}
}
tabs.classed('tab-pane', true); // add tab-pane class back
});
}
Original idea here: https://stackoverflow.com/questions/27886734/how-to-word-wrap-legend-labels-in-d3.
Small bug I found for anyone else who runs into this:
If the max width of the tick is less than the width of the first word, it seems the code as-written will append an empty <tspan>
at the top of the <text>
. To fix this, just add an additional check in if
conditional on line 107 that line.length > 1
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.