Skip to content

Instantly share code, notes, and snippets.

@ricardo-marino
Last active February 2, 2017 15:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ricardo-marino/ca2db3457f82dbb10a8753ecba8c0029 to your computer and use it in GitHub Desktop.
Save ricardo-marino/ca2db3457f82dbb10a8753ecba8c0029 to your computer and use it in GitHub Desktop.
Animated Stacked Bars Chart
height: 700

Animated stacked bar plot

I wanted to try the transition effects to build a good-looking histogram in d3.v4. I also wanted the possibility of "zooming in" to observe local patterns for a specific cluster, and the click + transition behavior in d3 does it perfectly. It took me a while to get the good transition effect, and to reproduce the name transition for the legend rectangles and text, but I like the final result.

The option to change to percentage is very useful if classes are not equilibrated. In this case, it does not do a lot.

The structure of the code is extremely modular, no global variables were used. The original context of this project was to create an angular.js directive, and modularity is the angular way.

I am still trying to fix the transition effect for upper bars in percentage view, and will soon update this block.

Data: Brazilian population statistics and projections to 2020.

Source: IBGE

year 0-4 5-9 10-14 15-19 20-24 25-29 30-34 35-39 40-44 45-49 50-54 55-59 60-64 65-69 70-74 75-79 80-84 85-89 90+
2000 17314510 17273758 17518796 18097555 16346745 14468665 13436025 12497963 10779160 8928759 7105097 5445582 4513522 3493661 2696950 1759132 994067 493932 284467
2001 17333637 17252925 17402474 18119828 16738956 14751901 13572135 12699486 11103110 9245644 7404596 5652053 4611832 3589547 2756503 1832671 1031252 496283 290396
2002 17309347 17241043 17329890 18006076 17146912 15074831 13712393 12860762 11430728 9561223 7704810 5900595 4697462 3706117 2805444 1912831 1072847 507823 294994
2003 17240518 17236778 17291927 17806344 17525805 15429819 13870533 12994475 11748126 9876810 8006479 6179657 4787311 3833079 2853004 1993863 1119013 525567 300000
2004 17126610 17237068 17266480 17602425 17809769 15805016 14065051 13120828 12034508 10195079 8312316 6470614 4906585 3954731 2912039 2068284 1170440 547520 306124
2005 16975813 17230352 17240164 17447357 17960346 16188242 14306505 13254354 12278037 10516466 8623561 6761728 5069578 4063480 2989210 2133020 1226862 572304 313427
2006 16748883 17254039 17220180 17332309 17983683 16579582 14590689 13393467 12482076 10839702 8937019 7054866 5271797 4162692 3082622 2190158 1287905 600073 323395
2007 16518269 17234647 17209088 17260627 17871791 16986197 14913958 13536501 12646465 11166621 9249416 7349075 5513487 4250785 3194031 2239350 1354364 630986 337097
2008 16284481 17170904 17205581 17223245 17674357 17363699 15268717 13697042 12783678 11483586 9561999 7644993 5783963 4343115 3314471 2287859 1421793 665093 353863
2009 16049820 17062172 17206586 17198226 17472598 17646823 15643256 13893386 12913450 11770215 9877343 7945147 6065924 4462513 3430343 2345842 1484515 702752 373058
2010 15816957 16916587 17200577 17172257 17319107 17797553 16025477 14135911 13050164 12014841 10195824 8250688 6348447 4621978 3535046 2418507 1540149 743640 394087
2011 15587805 16695060 17225082 17153030 17205781 17822613 16415898 14420686 13192312 12220897 10516455 8558690 6633208 4817408 3631693 2504505 1590517 787832 417546
2012 15363958 16469622 17206777 17143273 17136273 17714321 16821840 14744294 13338350 12388253 10841091 8866088 6919282 5048950 3718984 2605448 1635653 836135 443870
2013 15147056 16240430 17144166 17141387 17101326 17521645 17199263 15099170 13501597 12528960 11156334 9174062 7207232 5306850 3810343 2713799 1680744 885459 472891
2014 14938133 16009509 17036612 17144248 17079105 17325091 17483642 15473785 13700143 12662331 11442231 9485114 7499452 5575432 3925728 2818329 1733123 931944 504610
2015 14737740 15779109 16892243 17140200 17056423 17176808 17637407 15856255 13944226 12802397 11687344 9799612 7797050 5844703 4076511 2913596 1796449 973943 538633
2016 14545488 15551873 16672044 17166564 17040654 17068601 17666798 16246942 14230010 12947682 11894903 10116412 8097251 6116344 4259166 3002421 1869824 1012885 575570
2017 14360778 15329961 16447927 17150098 17034257 17003793 17563875 16652990 14554024 13096707 12064747 10437153 8397098 6389645 4473931 3084038 1954490 1049081 616336
2018 14182966 15114823 16220015 17089362 17035512 16973092 17377213 17030859 14908841 13262537 12208578 10748841 8697681 6665075 4712250 3169590 2044693 1085738 659136
2019 14011332 14907522 15990363 16983733 17041319 16954854 17186554 17316692 15283177 13462945 12345155 11032142 9001331 6944755 4960344 3275690 2131881 1127421 701803
2020 13845258 14708594 15761172 16841311 17040111 16935971 17043597 17473250 15665301 13708027 12488105 11276016 9308355 7229599 5209414 3411743 2212046 1176296 743209
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<p> <input type="checkbox" id="myCheckbox"> View in percentage </p>
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var dataFile = 'brazil_demographics.csv'
d3.csv(dataFile, function(error, data){
var input = {'data': data, 'width': 960, 'height': 600},
canvas = setUpSvgCanvas(input);
drawBars(input, canvas);
})
function drawBars(input, canvas) {
var params = {'input': input, 'canvas': canvas};
initialize(params);
update(params);
}
function initialize(params) {
// unpacking params
var canvas = params.canvas,
input = params.input;
// unpacking canvas
var svg = canvas.svg,
margin = canvas.margin,
width = params.width = canvas.width,
height = params.height = canvas.height;
// processing Data and extracting binNames and clusterNames
var formattedData = formatData(input.data),
blockData = params.blockData = formattedData.blockData,
binNames = params.binNames = formattedData.binNames,
clusterNames = params.clusterNames = formattedData.clusterNames;
// initialize color
var color = setUpColors().domain(clusterNames);
// initialize scales and axis
var scales = initializeScales(width, height),
x = scales.x,
y = params.y = scales.y;
x.domain(binNames);
y.domain([0, d3.max(blockData, function(d) { return d.y1; })]);
initializeAxis(svg, x, y, height, width);
// initialize bars
var bar = params.bar = svg.selectAll('.bar')
.data(blockData)
.enter().append('g')
.attr('class', 'bar');
bar.append('rect')
.attr('x', function(d) { return x(d.x);})
.attr('y', function(d) {return y(0);})
.attr('width', x.bandwidth())
.attr('height', 0)
.attr('fill', function(d){ return color(d.cluster);});
// heights is a dictionary to store bar height by cluster
// this hierarchy is important for animation purposes
// each bar above the chosen bar must collapse to the top of the
// selected bar, this function defines this top
params.heights = setUpHeights(clusterNames, blockData);
// defining max of each bin to convert later to percentage
params.maxPerBin = setUpMax(clusterNames, blockData);
// variable to store chosen cluster when bar is clicked
var chosen = params.chosen = {
cluster: null
};
// initialize legend
var legend = params.legend = svg.selectAll('.legend')
.data(clusterNames)
.enter().append('g')
.attr('class', 'legend');
legend.append('rect')
.attr('x', width + margin.right - 18)
.attr('y', function(d, i) {return 20 * (clusterNames.length - 1 - i);})
.attr('height', 18)
.attr('width', 18)
.attr('fill', function(d){ return color(d);})
.on('click', function(d){
chosen.cluster = chosen.cluster === d ? null : d;
update(params);
});
legend.append('text')
.attr('x', width + margin.right - 25)
.attr('y', function(d, i) { return 20 * (clusterNames.length - 1 - i) ;})
.text(function(d) {return d;})
.attr('dy', '.95em')
.style('text-anchor', 'end');
// initialize checkbox options
d3.select("#myCheckbox").on("change",function(){update(params);});
params.view = false;
}
function update(params){
// retrieving params to avoid putting params.x everywhere
var svg = params.canvas.svg,
margin = params.canvas.margin,
y = params.y,
blockData = params.blockData,
heights = params.heights,
chosen = params.chosen,
width = params.width,
height = params.height,
bar = params.bar,
clusterNames = params.clusterNames,
binNames = params.binNames,
legend = params.legend,
maxPerBin = params.maxPerBin,
view = params.view;
var transDuration = 700;
// re-scaling data if view is changed to percentage
// and re-scaling back if normal view is selected
var newView = d3.select("#myCheckbox").property("checked");
if(newView){
if(view != newView){
blockData.forEach(function (d){
d.y0 /= maxPerBin[d.x];
d.y1 /= maxPerBin[d.x];
d.height /= maxPerBin[d.x];
});
heights = setUpHeights(clusterNames, blockData);
}
}
else{
if(view != newView){
blockData.forEach(function (d){
d.y0 *= maxPerBin[d.x];
d.y1 *= maxPerBin[d.x];
d.height *= maxPerBin[d.x];
});
heights = setUpHeights(clusterNames, blockData);
}
}
params.view = newView;
// update Y axis
if(chosen.cluster == null){
y.domain([0, d3.max(blockData, function(d) { return d.y1; })]);
}
else{
y.domain([0, d3.max(heights[chosen.cluster])]);
}
if(newView){
y.domain([0, 1]);
}
var axisY = d3.axisLeft(y)
.tickSize(-width);
if(newView){
axisY.tickFormat(d3.format(".0%"));
}
svg.selectAll('.axisY')
.transition()
.duration(transDuration)
.call(axisY);
// update legend
legend.selectAll('rect')
.transition()
.duration(transDuration)
.attr('height', function(d) {return choice(chosen.cluster, d, 18, 18, 0);})
.attr('y', function(d) {
var i = clusterNames.indexOf(d);
if (i > clusterNames.indexOf(chosen.cluster)){
return choice(chosen.cluster, d, 20 * (clusterNames.length - 1 - i) , 0, 0);
}
else {
return choice(chosen.cluster, d, 20 * (clusterNames.length - 1 - i) , 0, 18);
}
});
legend.selectAll('text')
.transition()
.duration(transDuration)
.attr('y', function(d) {
var i = clusterNames.indexOf(d);
if (i > clusterNames.indexOf(chosen.cluster)){
return choice(chosen.cluster, d, 20 * (clusterNames.length - 1 - i) , 0, 0);
}
else {
return choice(chosen.cluster, d, 20 * (clusterNames.length - 1 - i) , 0, 18);
}
})
.style('font-size' ,function(d, i) {return choice(chosen.cluster, d, '16px', '16px', '0px');})
.attr('x', function(d) {return choice(chosen.cluster, d,
width + margin.right - 25,
width + margin.right - 25,
width + margin.right - 25 - this.getComputedTextLength()/2);});
// update bars
bar.selectAll('rect')
.on('click', function(d){
chosen.cluster = chosen.cluster === d.cluster ? null : d.cluster;
update(params);
})
.transition()
.duration(transDuration)
.attr('y', function(d) {
return choice(chosen.cluster, d.cluster,
y(d.y1),
y(d.height),
myHeight(chosen, d, clusterNames, binNames, y, heights));})
.attr('height', function(d) {
return choice(chosen.cluster, d.cluster,
height - y(d.height),
height - y(d.height),
0);});
}
// heights is a dictionary to store bar height by cluster
// this hierarchy is important for animation purposes
function setUpHeights(clusterNames, blockData) {
var heights = {};
clusterNames.forEach(function(cluster) {
var clusterVec = [];
blockData.filter(function (d){ return d.cluster == cluster;}).forEach(function(d) {
clusterVec.push(d.height);
});
heights[cluster] = clusterVec;
});
return heights;
}
// getting the max value of each bin, to convert back and forth to percentage
function setUpMax(clusterNames, blockData){
var lastClusterElements = blockData.filter(function(d){return d.cluster == clusterNames[clusterNames.length - 1]})
var maxDict = {};
lastClusterElements.forEach(function(d) {
maxDict[d.x] = d.y1;
});
return maxDict;
}
// custom function to provide correct animation effect
// bars should fade into the top of the remaining bar
function myHeight(chosen, d, clusterNames, binNames, y, heights){
if(chosen.cluster == null){
return 0;
}
if(clusterNames.indexOf(chosen.cluster) > clusterNames.indexOf(d.cluster)){
return y(0);
}
else {
return y(heights[chosen.cluster][binNames.indexOf(d.x)]);
}
}
// handy function to play the update game with the bars and legend
function choice(variable, target, nullCase, targetCase, notTargetCase){
switch(variable) {
case null:
return nullCase;
case target:
return targetCase;
default:
return notTargetCase;
}
}
function initializeScales(width, height){
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(0.5);
var y = d3.scaleLinear()
.range([height, 0]);
return {
x: x,
y: y
};
}
function initializeAxis(svg, x, y, height, width){
var yAxis = d3.axisLeft(y)
.tickSize(-width);
svg.append('g')
.attr('class', 'axisY')
.call(yAxis);
svg.append('g')
.attr('class', 'axisX')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x));
}
function setUpSvgCanvas(input) {
// Set up the svg canvas
var margin = {top: 20, right: 80, bottom: 20, left: 80},
width = input.width - margin.left -margin.right,
height = input.height - margin.top -margin.bottom;
var svg = d3.select('svg')
.attr('width', width + margin.left + margin.right )
.attr('height', height + margin.top +margin.bottom )
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
return {
svg: svg,
margin: margin,
width: width,
height: height
};
}
function setUpColors() {
return d3.scaleOrdinal(d3.schemeCategory20);
}
// formatting Data to a more d3-friendly format
// extracting binNames and clusterNames
function formatData(data){
var clusterNames = d3.keys(data[0]).filter(function(key) {return key !== 'year'; });
var binNames = [];
var blockData = [];
for(var i = 0; i < data.length; i++){
var y = 0;
binNames.push(data[i].year);
for(var j = 0; j < clusterNames.length; j++){
var height = parseFloat(data[i][clusterNames[j]]);
var block = {'y0': parseFloat(y),
'y1': parseFloat(y) + parseFloat(height),
'height': height,
'x': data[i].year,
'cluster': clusterNames[j]};
y += parseFloat(data[i][clusterNames[j]]);
blockData.push(block);
}
}
return {
blockData: blockData,
binNames: binNames,
clusterNames: clusterNames
};
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment