A simple example of a waterfall chart using d3, showing customer retention on a week by week basis for the past month.
d3 Waterfall Chart
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
period | start of period | new | returned | went inactive | |
---|---|---|---|---|---|
Four Weeks Ago | 100 | 20 | 30 | 40 | |
Three Weeks Ago | 110 | 30 | 20 | 30 | |
Two Weeks Ago | 130 | 40 | 25 | 40 | |
One Week Ago | 155 | 30 | 55 | 50 | |
Current Week | 190 | 0 | 0 | 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
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font: 10px sans-serif; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.x.axis path { | |
display: none; | |
} | |
</style> | |
<script src="http://code.jquery.com/jquery-latest.js"></script> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script> | |
var margin = {top: 20, right: 20, bottom: 30, left: 40}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var x0 = d3.scale.ordinal() | |
.rangeRoundBands([0, width], 0, .1); | |
var x1 = d3.scale.ordinal(); | |
var y = d3.scale.linear() | |
.range([height, 0]); | |
var color = d3.scale.ordinal() | |
.range(["#2DBAD4", "#33CC33", "#00E6B8", "#C9081D"]); | |
var xAxis = d3.svg.axis() | |
.scale(x0) | |
.orient("bottom"); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left") | |
.tickFormat(d3.format(".2s")); | |
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.csv("data.csv", function(error, data) { | |
var categories = d3.keys(data[0]).filter(function(key) { | |
return key !== "period"; | |
}); | |
data.forEach(function(d) { | |
//first calculate the starting height for each category | |
total = 0 | |
totals = {} | |
for (var i=0; i<categories.length; i++) { | |
if (i == 3) { | |
total = total - +d[categories[i]]; | |
totals[categories[i]] = total; | |
} | |
else { | |
totals[categories[i]] = total; | |
total += +d[categories[i]]; | |
} | |
} | |
//then map category name, value, and starting height to each observation | |
d.formatted = categories.map(function(category) { | |
return { | |
name: category, | |
value: +d[category], | |
baseHeight: +totals[category] | |
}; | |
}); | |
}); | |
x0.domain(data.map(function(d) { return d.period; })); | |
x1.domain(categories).rangeRoundBands([0, x0.rangeBand()]); | |
y.domain([0, d3.max(data, function(d) { | |
return d3.max(d.formatted, function(d) { | |
return d.value + d.baseHeight; }); })]); | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + height + ")") | |
.call(xAxis); | |
svg.append("g") | |
.attr("class", "y axis") | |
.call(yAxis) | |
.append("text") | |
.attr("transform", "rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.style("text-anchor", "end") | |
.text("Customers"); | |
var state = svg.selectAll(".state") | |
.data(data) | |
.enter().append("g") | |
.attr("class", "g") | |
.attr("transform", function(d) { | |
return "translate(" + x0(d.period) + ",0)"; | |
}); | |
state.selectAll("rect") | |
.data(function(d) { return d.formatted; }) | |
.enter().append("rect") | |
.attr("width", x1.rangeBand()) | |
.attr("x", function(d) { return x1(d.name); }) | |
.attr("y", function(d) { return y(d.value + d.baseHeight); }) | |
.attr("height", function(d) { return height - y(d.value); }) | |
.style("fill", function(d) { return color(d.name); }); | |
var legend = svg.selectAll(".legend") | |
.data(categories.slice().reverse()) | |
.enter().append("g") | |
.attr("class", "legend") | |
.attr("transform", function(d, i) { | |
return "translate(0," + i * 20 + ")"; | |
}); | |
legend.append("rect") | |
.attr("x", width - 18) | |
.attr("width", 18) | |
.attr("height", 18) | |
.style("fill", color); | |
legend.append("text") | |
.attr("x", width - 24) | |
.attr("y", 9) | |
.attr("dy", ".35em") | |
.style("text-anchor", "end") | |
.text(function(d) { return d; }); | |
}); | |
</script> |
Thanks for this. If anyone is looking for a v5 compatible example: https://gist.github.com/jacoduplessis/29209d40299e13d9a1aa09ce0eecf02f
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for that example!
On line 53 and 54, why is there an
append()
with atransform translate
? Why does it need this in order to show the vertical axis?Why I ask:
I've wrote a similar code, based on this, and that exact bit causes problems when I try to export the SVG as an image, for some reason (when getting a string from the SVG, encoding and setting as the source of an image element, the
onload()
event for the image does not fire)If I remove those lines of code, the image export suddenly works, but then I can't see the vertical axis of my chart. I'm at the moment clueless why both of these things happen.
I "unchained" those two lines of code, like this, but nothing happens, the transform is not applied and the vertical axis not shown.
Why that does not work?