Skip to content

Instantly share code, notes, and snippets.

@guypursey guypursey/.block forked from mbostock/.block
Last active Jul 1, 2019

Embed
What would you like to do?
Wrapping long labels with D3 v4 (sample data)
license: gpl-3.0

This is a fork of Mike Bostock's Block/Gist showing how to wrap long labels in D3. However, I've adapted it to use D3 v4 (latest version of D3 at time of writing).

This demonstration uses satirical data from The Onion.

The changes to Bostock's code are not substantial.

  • Some method names and usages had to change to be compatible with D3 v4.
  • I decluttered most of the JavaScript semicolons.
  • I replaced many of the pure function declarations with easier-to-read arrow functions.
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.v4.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.scaleBand()
.range([0, width])
.paddingInner(.1)
.paddingOuter(.3)
var y = d3.scaleLinear()
.range([height, 0])
var xAxis = d3.axisBottom(x)
var yAxis = d3.axisLeft(y)
.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", d => { d.value = +d.value; return d }, function(error, data) {
x.domain(data.map(d => d.name))
y.domain([0, d3.max(data, d => 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.bandwidth())
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", d => x(d.name))
.attr("width", x.bandwidth())
.attr("y", d => y(d.value))
.attr("height", d => 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)
}
}
})
}
</script>
@bentedder

This comment has been minimized.

Copy link

commented Mar 19, 2018

thanks!

@isaacalves

This comment has been minimized.

Copy link

commented May 3, 2018

Is it possible to return a value from wrap() (and/or call()?) and pass it onwards? or update the data array? For instance, the number of lines the text has been broken to so it can be used to position other elements?

@isaacalves

This comment has been minimized.

Copy link

commented May 3, 2018

I solved my problem by chaining a call() method, that returns an array with the text nodes:

          .call(wrap, x.rangeBand())
          .call(function(d, i){
            let textArray = d[0];
            for (let i = 0; i < textArray.length; i++) {
              if (textArray[i].hasChildNodes()) {
                data[i].textNumLines = textArray[i].childNodes.length;
              }
              let textLabelY = parseInt(textArray[i].getAttribute('y'), 10);
              data[i].textLabelY = textLabelY;
            }
          });
@KievDevel

This comment has been minimized.

Copy link

commented Mar 27, 2019

Hello!

I'd suggest to modify line 105 check with:
if (tspan.node().getComputedTextLength() > width && line.length > 1) {

Current code will create empty lead tspan if first word on the list has bigger width, than it can take.

Thanks! Hope it will help someone :)

@bborad

This comment has been minimized.

Copy link

commented May 23, 2019

wrap function seems to move label on right (only when there are multiple lines)
image

Any suggestions on how to fix this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.