Skip to content

Instantly share code, notes, and snippets.

@Saigesp
Last active October 26, 2018 17:59
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 Saigesp/f2605e92dff3a976d97ae6027d07f6b5 to your computer and use it in GitHub Desktop.
Save Saigesp/f2605e92dff3a976d97ae6027d07f6b5 to your computer and use it in GitHub Desktop.
D3v4 Stacked barchart
f2605e92dff3a976d97ae6027d07f6b5

Stacked bar chart

D3 implementation of stacked barchart

See the demo and more charts from d3graphs repository

Features:

  • Object oriented approach
  • Responsive
  • Resizable

Requires:

  • D3 v4+

Default options:

{
    margin: {top: 10, right: 10, bottom: 10, left: 40},
    key: 'key',
    keys: [],
    colors: [],
    labels: true,
    fontsize: '12px',
    yscaleformat: '.0f',
    greyColorStart: 240,
    greyColorStep: 15,
    label_space: 150,
    currentkey: false,
}
class StackedBarChart {
constructor(selection, data, config = {}) {
let self = this;
this.selection = selection;
this.data = data;
// Graph configuration
this.cfg = {
margin: {top: 10, right: 10, bottom: 10, left: 40},
key: 'key',
keys: [],
colors: [],
labels: true,
fontsize: '12px',
yscaleformat: '.0f',
greyColorStart: 240,
greyColorStep: 15,
label_space: 150,
currentkey: false,
};
Object.keys(config).forEach(function(key) {
if(config[key] instanceof Object && config[key] instanceof Array === false){
Object.keys(config[key]).forEach(function(sk) {
self.cfg[key][sk] = config[key][sk];
});
} else self.cfg[key] = config[key];
});
this.cfg.width = parseInt(this.selection.node().offsetWidth) - this.cfg.margin.left - this.cfg.margin.right,
this.cfg.height = parseInt(this.selection.node().offsetHeight)- this.cfg.margin.top - this.cfg.margin.bottom;
this.yScale = d3.scaleLinear().rangeRound([0, this.cfg.height]);
this.yAScale = d3.scaleLinear().rangeRound([this.cfg.height, 0]);
this.cfg.separation = this.cfg.width / this.data.length;
if(!this.cfg.labels) this.cfg.label_space = 0;
this.cfg.height = this.cfg.height + this.cfg.label_space;
this.cfg.greyColorMin = this.cfg.greyColorStart - (this.cfg.keys.length * this.cfg.greyColorStep)
window.addEventListener("resize", function(){self.resize()});
this.initGraph();
}
initGraph() {
var self = this;
this.data.forEach(function(d){
d.total = 0;
self.cfg.keys.forEach(function(p){
d.total += d[p]
})
})
this.data.sort(function(x, y){
return d3.descending(x.total, y.total);
})
this.yScale.domain([0, d3.max(self.data, function(d){ return d.total;})]);
this.yAScale.domain(self.yScale.domain())
this.svg = this.selection.append('svg')
.attr("class", "chart barchar barchart-stacked")
.attr("viewBox", "0 0 "+(this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)+" "+(this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom))
.attr("width", this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)
.attr("height", this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom);
this.g = this.svg.append("g")
.attr("transform", "translate(" + (self.cfg.margin.left) + "," + (self.cfg.margin.top) + ")");
this.yGrid = this.g.append("g")
.attr("class", "grid grid--y")
.call(self.make_y_gridlines()
.tickSize(-this.cfg.width)
.tickFormat("")
.ticks(3, self.cfg.yscaleformat));
this.g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(self.yAScale)
.ticks(3, self.cfg.yscaleformat));
this.itemg = this.g.selectAll('.itemg')
.data(this.data)
.enter().append('g')
.attr('class', 'itemg')
.attr('transform', function(d, i){
return 'translate('+(i*self.cfg.separation)+','+(self.cfg.height-self.cfg.label_space)+')';
})
this.cfg.keys.forEach(function(provider, n){
self.itemg.append('rect')
.attr('width', self.cfg.separation-1)
.attr('height', function(d){
return self.yScale(d[provider]);
})
.attr('x', 0)
.attr('y', function(d){
var pt = 0;
for(var i = n; i >= 0; i--){
pt = pt - d[self.cfg.keys[i]];
}
return self.yScale(pt);
})
.attr('fill', function(d){
var greyColor = self.cfg.greyColorMin + (self.cfg.greyColorStep*n);
return d.name == self.cfg.currentkey || self.cfg.currentkey == '*' ? self.cfg.colors[n] : 'rgb('+greyColor+','+greyColor+','+greyColor+')'
})
.append("title")
.text(function(d) { return d[provider]});
})
if(this.cfg.labels){
var text = this.itemg.append('text')
.attr('y', (self.cfg.separation/2) + 4)
.attr('x', -5)
.attr('class', 'label')
.attr("transform", "rotate(-90)")
.attr('text-anchor', 'end')
.style('font-size', self.cfg.fontsize)
.style('font-weight', function(d){ return d.name == self.cfg.currentkey ? '700' : '100'; })
.style('cursor', 'pointer')
.text(function(d){ return d[self.cfg.key]; })
}
}
make_y_gridlines() {
return d3.axisLeft(this.yAScale);
}
// Data functions
setData(data){
this.data = data;
}
getData(){
return this.data;
}
resize(){
var self = this;
this.cfg.width = parseInt(this.selection.node().offsetWidth) - this.cfg.margin.left - this.cfg.margin.right;
this.cfg.separation = this.cfg.width / this.data.length;
this.svg.attr("viewBox", "0 0 "+(this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)+" "+(this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom))
.attr("width", this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)
this.itemg.attr('transform', function(d, i){
return 'translate('+(i*self.cfg.separation)+','+(self.cfg.height-self.cfg.label_space)+')';
})
this.itemg.selectAll('rect').attr('width', self.cfg.separation-1);
this.itemg.selectAll('text').attr('y', (self.cfg.separation/2) + 4);
this.yGrid.call(self.make_y_gridlines()
.tickSize(-this.cfg.width)
.tickFormat("")
.ticks(3, self.cfg.yscaleformat))
}
};
<html>
<head>
<meta charset="utf-8">
</head>
<style>
.chart .grid line {
opacity: 0.1;
}
.chart .grid path {
fill: transparent;
stroke: transparent;
}
.label {
font-family: sans-serif;
font-size: 12px;
cursor: default;
}
.chart .source {
fill: #7a7a7a;
font-size: 10px;
}
.chart .source a {
text-decoration: underline;
}
.chart .title {
font-weight: 700;
}
</style>
<body>
<div id="bars" style="width: 100%; height: 360px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="d3.stackedbar.js"></script>
<script>
d3.json('regiondata.json', function(data) {
var stacked = new StackedBarChart(d3.select('#bars'), data.properties.cities, {
key: 'name',
yscaleformat: '.0s',
currentkey:'*',
keys: ['airbnb_totals', 'homeaw_totals', 'houset_totals', 'onlyap_totals'],
colors: ['#FF5F6B', '#2E87E3', '#88C8F3', '#4CAF50'],
})
})
</script>
</body>
</html>
{
"_id": "0",
"properties":
{
"cities": [
{
"airbnb_totals": 3486,
"houset_totals": 58,
"homeaw_totals": 568,
"name": "Granada",
"onlyap_totals": 134,
"_id": "5992312a8a46554f807ad627"
},
{
"airbnb_totals": 2181,
"houset_totals": 195,
"homeaw_totals": 884,
"name": "Benalm\u00e1dena",
"onlyap_totals": 145,
"_id": "599231398a46554f807ad78f"
},
{
"airbnb_totals": 2255,
"houset_totals": 375,
"homeaw_totals": 1017,
"name": "Estepona",
"onlyap_totals": 121,
"_id": "5992313a8a46554f807ad795"
},
{
"airbnb_totals": 5762,
"houset_totals": 27,
"homeaw_totals": 1999,
"name": "M\u00e1laga",
"onlyap_totals": 474,
"_id": "5992313a8a46554f807ad79a"
},
{
"airbnb_totals": 5721,
"houset_totals": 385,
"homeaw_totals": 2354,
"name": "Marbella",
"onlyap_totals": 321,
"_id": "5992313a8a46554f807ad79c"
},
{
"airbnb_totals": 2557,
"houset_totals": 387,
"homeaw_totals": 1373,
"name": "Mijas",
"onlyap_totals": 209,
"_id": "5992313a8a46554f807ad79d"
},
{
"airbnb_totals": 6973,
"houset_totals": 597,
"homeaw_totals": 2021,
"name": "Sevilla",
"onlyap_totals": 176,
"_id": "5992313d8a46554f807ad7da"
},
{
"airbnb_totals": 24029,
"houset_totals": 1282,
"homeaw_totals": 5093,
"name": "Barcelona",
"onlyap_totals": 1483,
"_id": "599231f58a46554f807ae8fb"
},
{
"airbnb_totals": 2171,
"houset_totals": 42,
"homeaw_totals": 1470,
"name": "Salou",
"onlyap_totals": 448,
"_id": "5992320e8a46554f807aeb52"
},
{
"airbnb_totals": 22909,
"houset_totals": 801,
"homeaw_totals": 3693,
"name": "Madrid",
"onlyap_totals": 872,
"_id": "599232178a46554f807aec1b"
},
{
"airbnb_totals": 3732,
"houset_totals": 249,
"homeaw_totals": 642,
"name": "Alacant",
"onlyap_totals": 66,
"_id": "599232288a46554f807aedcb"
},
{
"airbnb_totals": 2913,
"houset_totals": 0,
"homeaw_totals": 1063,
"name": "Torrevieja",
"onlyap_totals": 174,
"_id": "5992322a8a46554f807aedfc"
},
{
"airbnb_totals": 2597,
"houset_totals": 137,
"homeaw_totals": 1806,
"name": "D\u00e9nia",
"onlyap_totals": 662,
"_id": "5992322c8a46554f807aee20"
},
{
"airbnb_totals": 8120,
"houset_totals": 259,
"homeaw_totals": 1112,
"name": "Valencia",
"onlyap_totals": 266,
"_id": "599232358a46554f807aef06"
},
{
"airbnb_totals": 2283,
"houset_totals": 565,
"homeaw_totals": 1183,
"name": "La Oliva",
"onlyap_totals": 92,
"_id": "599232618a46554f807af2f5"
},
{
"airbnb_totals": 2691,
"houset_totals": 211,
"homeaw_totals": 869,
"name": "Las Palmas",
"onlyap_totals": 119,
"_id": "599232618a46554f807af2f6"
},
{
"airbnb_totals": 2411,
"houset_totals": 435,
"homeaw_totals": 1326,
"name": "San Bartolom\u00e9",
"onlyap_totals": 352,
"_id": "599232628a46554f807af2fb"
},
{
"airbnb_totals": 3371,
"houset_totals": 399,
"homeaw_totals": 1456,
"name": "Adeje",
"onlyap_totals": 125,
"_id": "599232648a46554f807af30c"
},
{
"airbnb_totals": 3144,
"houset_totals": 428,
"homeaw_totals": 1211,
"name": "Arona",
"onlyap_totals": 208,
"_id": "599232658a46554f807af311"
},
{
"airbnb_totals": 2206,
"houset_totals": 0,
"homeaw_totals": 782,
"name": "Donostia",
"onlyap_totals": 173,
"_id": "599232738a46554f807af449"
}],
"countries": [
{
"airbnb_totals": 308857,
"houset_totals": 22109,
"homeaw_totals": 144306,
"name": "Spain",
"onlyap_totals": 24543,
"_id": "599216cb8a4655339b819813"
},
{
"airbnb_totals": 71786,
"houset_totals": 6172,
"homeaw_totals": 40231,
"name": "Portugal",
"onlyap_totals": 3778,
"_id": "5b06cfc88a4655394ee2b210"
},
{
"airbnb_totals": 722,
"houset_totals": 24,
"homeaw_totals": 280,
"name": "Andorra",
"onlyap_totals": 174,
"_id": "5b8be507bbef6076ab44b381"
}]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment