Skip to content

Instantly share code, notes, and snippets.

@Saigesp Saigesp/.block
Last active Oct 26, 2018

Embed
What would you like to do?
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
You can’t perform that action at this time.