Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active October 31, 2016 16:48
Show Gist options
  • Save HarryStevens/dea8fdcdae7401ab486bfc7fcc9f6882 to your computer and use it in GitHub Desktop.
Save HarryStevens/dea8fdcdae7401ab486bfc7fcc9f6882 to your computer and use it in GitHub Desktop.
Normalized Stacked Bar Chart
license: gpl-3.0

Create a normalized stacked bar chart in d3 (without the stack layout).

The CSV file can contain any number of groups and up to 8 categories. It must not have a total row at the bottom; that row is calculated with Javascript.

Libraries: d3.js, underscore.js, jQuery

group cat_a cat_b cat_c cat_d cat_e cat_f cat_g cat_h
Andrew 9577 942 471 2355 632 324 123 2355
Matthew 10362 1256 157 314 754 217 235 3611
Mark 9420 157 2669 1099 274 364 123 2355
Luke 9106 314 2512 471 123 127 234 3297
John 7693 157 3768 942 964 876 234 3140
Jimmy 6594 785 3611 1099 227 164 264 3611
Bill 6437 3297 1256 471 532 653 278 4239
Bob 5495 2512 2983 942 346 164 274 3768
Kate 7493 372 4830 1632 274 123 174 1537
Alice 3513 631 145 1473 227 126 237 1573
Sally 8574 2534 537 214 254 763 143 2500
Mary 2583 253 8459 203 126 643 2187 4832
Jenny 2365 2305 2805 234 123 129 347 204
Barbara 1305 3854 2035 2034 854 743 165 123
<html>
<head>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="legend"></div>
<div class="chart"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="scripts.js"></script>
</body>
</html>
var barHeight = 25, barPad = 3, textDy = 16, width = 600;
var margin = {top:0,bottom:0,left:60,right:0}
var svg = d3.select(".chart").append("svg")
.attr("width", width);
var x = d3.scaleLinear()
.range([0,width - margin.left])
.domain([0,100]);
var colorArray = ['#66c2a5','#fc8d62','#8da0cb','#e78ac3','#a6d854','#ffd92f','#e5c494','#b3b3b3'];
d3.csv("data.csv",types,function(error,data){
var keys = [], legKeys = [];
_.keys(data[0]).forEach(function(d,i){
if (i > 0){
d.includes('pct') ? keys.push(d) : legKeys.push(d);
}
});
data = addTotal(data, legKeys);
// create the legend
$('.legend').css('margin-left', margin.left);
legKeys.forEach(function(legKey,i){
$('.legend').append('<div class="swatch" style="background:' + colorArray[i] + '"></div>' + legKey);
});
// sort the data, putting the total at the end
var sorted = _.sortBy(data,keys[0]).reverse(), total = _.where(sorted,{group:'total'}), rem = _.reject(sorted,{group:'total'}), dat = [];
rem.forEach(function(r){ dat.push(r); });
dat.push(total[0]);
data = dat;
// now that the data is ready, calculate the height
var height = (data.length * barHeight) + (barPad * 3);
d3.select('svg').attr("height", height);
svg.selectAll('.label')
.data(data)
.enter().append('text')
.attr('class', function(d){
var label;
d.group == 'total' ? label = 'label strong' : label = 'label';
return label;
})
.attr('x', margin.left-5)
.attr('y', function(d,i){ return pad(d,i); })
.attr('dy', textDy)
.attr('text-anchor', 'end')
.text(function(d){ return d.group.charAt(0).toUpperCase() + d.group.slice(1); });
keys.forEach(function(key,i){
svg.selectAll('.bar .' + key)
.data(data)
.enter().append('rect')
.attr('class', 'bar ' + key)
.attr('width', function(d,i){ return x(d[key]); })
.attr('height', barHeight - barPad)
.attr('x', function(d){ return widths(keys,key,d); })
.attr('y', function(d,i){ return pad(d,i); });
d3.selectAll('.' + key)
.style('fill', colorArray[i]);
svg.selectAll('.bar-label .' + key)
.data(data)
.enter().append('text')
.attr('class', 'bar-label ' + key)
.attr('x', function(d){
var barX;
key !== keys[keys.length-1] ? barX = widths(keys,key,d) + 5 : barX = width - 5;
return barX;
})
.attr('dy', textDy)
.attr('y', function(d,i){ return pad(d,i); })
.attr('text-anchor', function(d){
var ta;
key == keys[keys.length-1] ? ta = 'end' : ta = 'start';
return ta;
})
.text(function(d){
var text;
d[key] < 5 ? text = '' : text = Math.round(d[key]) + pct(d, data[0].group, data[data.length-1].group);
return text;
});
});
});
function types(d){
var keys = _.keys(d);
var arr = [];
keys.forEach(function(k,i){
if (i!==0){
arr.push(+d[k]);
d[k] = +d[k];
}
});
var sum = arr.reduce(function(a,b){return a+b}, 0);
keys.forEach(function(k,i){
i !== 0 ? d[k + 'pct'] = +d[k]/sum*100 : null;
});
return d;
}
function pad(d,i){
var barI;
d.group == 'total' ? barI = i * barHeight + (barPad * 2) : barI = i * barHeight;
return barI;
}
function pct(d, first, last){
var extra;
d.group == first || d.group == last ? extra = '%' : extra = '';
return extra;
}
function widths(keys,key,d){
if (key == keys[0]) {
return margin.left;
} else if (key == keys[1]){
return margin.left + x(d[keys[0]]);
} else if (key == keys[2]){
return margin.left + x(d[keys[0]]) + x(d[keys[1]]);
} else if (key == keys[3]) {
return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]);
} else if (key == keys[4]) {
return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]);
} else if (key == keys[5]) {
return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]) + x(d[keys[4]]);
} else if (key == keys[6]) {
return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]) + x(d[keys[4]]) + x(d[keys[5]]);
} else if (key == keys[7]) {
return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]) + x(d[keys[4]]) + x(d[keys[5]]) + x(d[keys[6]]);
}
}
function addTotal(data,keys){
var totObj = {};
var arrA = [];
keys.forEach(function(key){
var arrB = [];
data.forEach(function(d){
arrB.push(d[key]);
})
var sumB = arrB.reduce(function(a,b){ return a+b }, 0);
totObj[key] = sumB;
arrA.push(sumB);
})
var sumA = arrA.reduce(function(a,b){ return a+b }, 0);
keys.forEach(function(key){
totObj[key + 'pct'] = totObj[key] / sumA * 100;
});
totObj.group = 'total';
data.push(totObj);
return data;
}
body {
font-family: "Helvetica Neue", sans-serif;
margin: 0 auto;
display: table;
}
.label {
font-size: .9em;
}
.label.strong {
font-weight: 900;
}
.bar-label {
font-size: .8em;
fill: #fff;
}
.legend {
font-size: .9em;
margin-bottom: 10px;
}
.swatch {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 4px;
margin-left: 8px;
}
.swatch:first-of-type {
margin-left: 0px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment