Skip to content

Instantly share code, notes, and snippets.

@guypursey
Forked from mbostock/.block
Last active September 21, 2023 09:51
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save guypursey/f47d8cd11a8ff24854305505dbbd8c07 to your computer and use it in GitHub Desktop.
Save guypursey/f47d8cd11a8ff24854305505dbbd8c07 to your computer and use it in GitHub Desktop.
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
Copy link

thanks!

@isaacalves
Copy link

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
Copy link

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
Copy link

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
Copy link

bborad 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