Skip to content

Instantly share code, notes, and snippets.

@dawaldron
Last active October 6, 2018 04:44
Show Gist options
  • Save dawaldron/df07e00da18115300f929e8c1a7d4ff7 to your computer and use it in GitHub Desktop.
Save dawaldron/df07e00da18115300f929e8c1a7d4ff7 to your computer and use it in GitHub Desktop.
d3 stacked bar bump chart
year country money
1970 United States 1000
1980 United States 950
1990 United States 800
2000 United States 700
1970 China 500
1980 China 700
1990 China 900
2000 China 1300
1970 Canada 1100
1980 Canada 900
1990 Canada 700
2000 Canada 500
1970 Mexico 600
1980 Mexico 650
1990 Mexico 750
2000 Mexico 1000
<!DOCTYPE html>
<style>
body {
font-family: sans-serif;
}
</style>
<div id="chart" style="width:600px"></div>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script>
// margins and dimensions
var divwidth = +d3.select("#chart").style("width").replace("px",""),
margin = {top:40, right:20, bottom:20, left:110},
width = divwidth - margin.right - margin.left,
height = divwidth * .5 - margin.top - margin.bottom,
svg = d3.select("#chart").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom),
g = svg.append("g")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data.csv", function(error, data) {
if (error) throw error;
// nest the data by year
var nestedData = d3.nest()
.key(function(d) { return d.year; })
.entries(data);
// calculate ranks and cumulative dollars within each year
nestedData.forEach(function(d) {
d.values.sort(function(a,b) { return b.money - a.money; });
var cumsum = 0;
d.values.forEach(function(v, i) {
v.money = +v.money;
v.rank = i + 1;
cumsum = cumsum + v.money;
v.cumsum = cumsum;
return(v);
});
return(d);
});
// scale for columns
var x = d3.scaleBand()
.domain(nestedData.map(function(d) { return d.key; }))
.range([0, width])
.padding(.2);
// scale for stacked rectangles
var yspacing = 5;
var y = d3.scaleLinear()
.domain([0, d3.max(nestedData, function(d) { return d3.sum(d.values, function(v) { return +v.money; }); })])
.range([0, height]);
// color palette
var color = d3.scaleOrdinal()
.domain(data.map(function(d) { return d.country; }))
.range(['#66c2a5','#fc8d62','#8da0cb','#e78ac3']);
// create group for each year
var pane = g.selectAll("g.pane")
.data(nestedData)
.enter()
.append("g")
.attr("class", "pane")
.attr("transform", function(d) { return "translate(" + x(d.key) + ",0)"; });
// group for each rectangle
var pill = pane.selectAll("g.pill")
.data(function(d) { return d.values; })
.enter()
.append("g")
.attr("class", "pill")
.attr("transform", function(v) { return "translate(0," + (y(v.cumsum - v.money) + yspacing) + ")"; });
// rectangles
pill.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", x.bandwidth())
.attr("height", function(v) { return y(v.money) - yspacing; })
.style("fill", function(v) { return color(v.country); });
// data labels
pill.append("text")
.attr("x", x.bandwidth() / 2)
.attr("y", function(v) { return y(v.money) / 2; })
.attr("dy", 3)
.style("text-anchor", "middle")
.text(function(v) { return d3.format("$,.0f")(v.money); });
// unnest function from https://bl.ocks.org/SpaceActuary/723b26e187e6bbc2608f
var unnest = function(data, children){
var out = [];
data.forEach(function(d,i) {
d_keys = Object.keys(d);
values = d[children];
values.forEach(function(v){
d_keys.forEach(function(k){
if (k != children) { v[k] = d[k]; }
});
out.push(v);
});
});
return out;
};
// unnest the data (keeps the calculated ranks and dollars)
var unnestedData = unnest(nestedData, "values");
// nest by country now
var renestedData = d3.nest()
.key(function(d) { return d.country; })
.entries(unnestedData);
// get previous year values needed for trapezoid coordinates
renestedData.forEach(function(d) {
d.values.sort(function(a,b) { return a.year - b.year; });
var prevyear = "0";
var prevmoney = 0;
var prevcumsum = 0;
d.values.forEach(function(v, i) {
v.country = d.key;
v.prevmoney = prevmoney;
v.prevcumsum = prevcumsum;
v.prevyear = prevyear;
prevmoney = v.money;
prevcumsum = v.cumsum;
prevyear = v.year;
return(v);
});
return(d);
});
// add the trapezoids
var connectorSet = g.selectAll("g.connector")
.data(renestedData)
.enter()
.append("g")
.attr("class", "connector");
connectorSet.selectAll("polygon")
.data(function(d) { return d.values.slice(1); })
.enter()
.append("polygon")
.attr("points", function(v) { return (x(v.prevyear) + x.bandwidth()) + "," + (y(v.prevcumsum - v.prevmoney) + yspacing) + " " +
(x(v.year)) + "," + (y(v.cumsum - v.money) + yspacing) + " " +
(x(v.year)) + "," + (y(v.cumsum)) + " " +
(x(v.prevyear) + x.bandwidth()) + "," + (y(v.prevcumsum)); })
.style("fill", function(v) { return color(v.country); })
.style("fill-opacity", .5);
// add labels using first year's data
g.selectAll("text.label")
.data(nestedData[0].values)
.enter()
.append("text")
.attr("class", "label")
.attr("x", 12)
.attr("y", function(d) { return y(d.cumsum - d.money / 2); })
.attr("dy", 8)
.style("text-anchor", "end")
.style("fill", function(d) { return color(d.country); })
.text(function(d) { return d.country; });
// add x axis
var xAxis = g.append("g")
.call(d3.axisTop(x));
xAxis.selectAll("path, line").remove();
xAxis.selectAll("text").style("font-size", "14px");
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment