Skip to content

Instantly share code, notes, and snippets.

@aholachek
Last active May 24, 2017 07:59
Show Gist options
  • Save aholachek/fb0c1cd7ea9707bc8ff55a82402c54b1 to your computer and use it in GitHub Desktop.
Save aholachek/fb0c1cd7ea9707bc8ff55a82402c54b1 to your computer and use it in GitHub Desktop.
Animated Grouped/Stacked Bar Transitions
/*eslint no-undef: 0*/
function createChart (svg, data) {
// //normalize data
// Object.keys(data).forEach((d)=>{
// ["0", "1", "2", "3", "4", "5", "6"].forEach(k=>{
// if (d[k] === undefined) d[k] =
// })
// })
var colors = ['#98abc5', '#8a89a6', '#7b6888', '#6b486b', '#a05d56', '#d0743c', '#ff8c00']
svg = d3.select(svg)
var margin = {top: 20, right: 20, bottom: 30, left: 40}
var width = 960 - margin.left - margin.right
var height = 500 - margin.top - margin.bottom
var g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1)
var x1 = d3.scaleBand()
.padding(0.05)
var y = d3.scaleLinear()
.rangeRound([height, 0])
var z = d3.scaleOrdinal()
.range(colors)
// check each subset of data for possible sections, since not all subsets have every possible section.
var nameKeys = data[Object.keys(data)[0]].map(function (obj) { return obj.name; })
var valueKeys = ["1", "2", "3", "4", "5", "6"]
//fill in empty data entries
Object.keys(data).forEach(function (d){
data[d].forEach(function (section){
valueKeys.forEach(function (k){
if (section.values[k] === undefined) { section.values[k] = 0 }
})
})
})
x0.domain(nameKeys)
x1.domain(valueKeys).rangeRound([0, x0.bandwidth()])
var barContainer = g.append('g')
var xAxis = g.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x0))
var yAxis = g.append('g')
.attr('class', 'axis')
yAxis
.append('text')
.attr('x', 2)
.attr('y', y(y.ticks().pop()) + 0.5)
.attr('dy', '0.32em')
.attr('font-weight', 'bold')
.attr('text-anchor', 'start')
.text('Prop Value')
var legend = g.append('g')
.attr('font-size', 10)
.attr('text-anchor', 'end')
legend.append('text')
.text('Loan Grade')
.attr('x', width - 19)
.style('font-weight', 'bold')
.attr('dy', -10)
.attr('dx', 20)
var legendEnter = legend
.selectAll('g')
.data(valueKeys)
.enter().append('g')
.attr('transform', function (d, i) { return 'translate(0,' + i * 20 + ')' })
legendEnter.append('rect')
.attr('x', width - 19)
.attr('width', 19)
.attr('height', 19)
.attr('fill', z)
legendEnter.append('text')
.attr('x', width - 24)
.attr('y', 9.5)
.attr('dy', '0.32em')
.text(function (d) { return d; })
var stack = d3.stack()
.keys(valueKeys)
function updateChart (data, chartType) {
if ( chartType === void 0 ) chartType='group';
if (chartType === 'group'){
//find max value of a section
var maxValue = d3.max(data.map(function (d) { return Object.values(d.values); }).reduce(function (a, b) { return a.concat(b); }, []))
y.domain([0, maxValue]).nice()
yAxis.transition()
.call(d3.axisLeft(y))
var barsWithData = barContainer
.selectAll('g')
.data(data)
barsWithData.exit().remove()
var bars = barsWithData
.enter()
.append('g')
.attr('transform', function (d) { return 'translate(' + x0(d.name) + ',0)' })
.merge(barsWithData)
.selectAll('rect')
.data(function (d) {
return Object.keys(d.values).map(function (k) { return ({ key: k, value: d.values[k] }); })
})
bars.exit().transition().style('opacity', 0).remove()
bars
.enter()
.append('rect')
.attr('fill', function (d) {
return z(d.key)
})
// start y at height (0) so animation in looks like bars are growing upwards
.attr('y', height)
.merge(bars)
.transition()
.attr('width', x1.bandwidth())
.attr('x', function (d) { return x1(d.key) })
.attr('y', function (d) { return y(d.value); })
.attr('height', function (d) { return height - y(d.value); })
}
// ========================================================
// show stacked view
// ========================================================
else if (chartType === 'stack'){
//find max value of a section
var maxValue$1 = d3.max(
data.map(function (d) { return Object.values(d.values); })
.map(function (valueArray){
return valueArray.reduce(function (a,b){ return a+ b; })
})
)
y.domain([0, maxValue$1]).nice()
yAxis.transition()
.call(d3.axisLeft(y))
//add data for missing bars
var seriesFlipped = stack(data.map(function (d){
var defaultData = {}
valueKeys.forEach(function (k){ return defaultData[k] = 0; })
return Object.assign(defaultData, d.values)
}))
var series = []
//need to reorient the series
//we want a list of groups, not a list of rects from each level
seriesFlipped[0].forEach(function (col, i){
var arr = []
seriesFlipped.forEach(function (row, index2){
//mimic the key from the grouped data format
row[i].key = index2 + 1 + ''
arr.push(row[i])
})
series.push(arr)
})
var barSections = barContainer
.selectAll('g')
.data(series)
var bars$1 = barSections
.enter()
.append('g')
.merge(barSections)
.attr('transform', function (d,i){console.log(x0(nameKeys[i])); return 'translate(' + x0(nameKeys[i]) + ',0)'} )
.selectAll('rect')
.data(function (d){ return d; }, function (d){ return d.key; })
var enterBars = bars$1.enter().append('rect')
.attr('fill', function (d){ return z(d.key); })
bars$1.exit().transition().style('opacity', 0).remove()
enterBars
.merge(bars$1)
.transition()
.delay(function (d,i){ return i * 50; })
.attr('width', x0.bandwidth())
.attr("y", function(d) {return y(d[1]) })
.attr("x", 0)
.attr("height", function(d) { return y(d[0]) - y(d[1]) })
}
}
return {
updateChart: updateChart
}
}
d3.json('./data.json', function(error, data){
//start with the first year selected
var chart = createChart(document.querySelector('svg'), data)
// append the input controls
var fieldset1 = d3.select('.controls').append('fieldset')
fieldset1.append('legend').text('Year')
Object.keys(data).forEach(function (year, index ){
var label = fieldset1.append('label')
label
.append('input')
.attr('type', 'radio')
.attr('name', 'year')
.attr('value', year)
.attr('checked', function(){
if (index === 0) { return true }
return null
})
label.append('span')
.text(year)
label.on('click', function(){
chart.updateChart(data[year], document.querySelector('input[name="graphType"]:checked').value)
})
})
var fieldset2 = d3.select('.controls').append('fieldset')
var types = ['group', 'stack']
fieldset2.append('legend').text('Graph Layout')
types.forEach(function (graphType, index){
var label = fieldset2.append('label')
label.append('input')
.attr('type', 'radio')
.attr('name', 'graphType')
.attr('value', graphType)
.attr('checked', function(){
if (index === 0) { return true }
return null
})
.on('click', function (){
chart.updateChart(data[document.querySelector('input[name="year"]:checked').value], graphType)
})
label.append('span')
.text(graphType)
})
// render initial chart
chart.updateChart(data[Object.keys(data)[0]])
})
{
"2007": [{
"name": "< 680",
"values": {
"2": 0.001496073,
"3": 0.126923915,
"4": 0.561200924,
"5": 0.759201816,
"6": 0.893993805
}
}, {
"name": "680-719",
"values": {
"1": 0.001278119,
"2": 0.362579479,
"3": 0.726073687,
"4": 0.360936984,
"5": 0.208819673,
"6": 0.106006195
}
}, {
"name": "720-759",
"values": {
"1": 0.292816973,
"2": 0.454058097,
"3": 0.114257685,
"4": 0.04486968,
"5": 0.03197851
}
}, {
"name": "760-799",
"values": {
"1": 0.577965235,
"2": 0.150698167,
"3": 0.032744713,
"4": 0.032992412
}
}, {
"name": " > 799",
"values": {
"1": 0.127939673,
"2": 0.031168184
}
}],
"2008": [{
"name": "< 680",
"values": {
"3": 0.143311078,
"4": 0.535939085,
"5": 0.723523376,
"6": 0.908838126
}
}, {
"name": "680-719",
"values": {
"1": 0.004053058,
"2": 0.389601129,
"3": 0.755033279,
"4": 0.432991983,
"5": 0.26300794,
"6": 0.080496991
}
}, {
"name": "720-759",
"values": {
"1": 0.493578271,
"2": 0.465804997,
"3": 0.09097742,
"4": 0.02912365,
"5": 0.013468684,
"6": 0.010664883
}
}, {
"name": "760-799",
"values": {
"1": 0.426650174,
"2": 0.139293212,
"3": 0.00653134,
"4": 0.001945282
}
}, {
"name": " > 799",
"values": {
"1": 0.075718497,
"2": 0.005300662,
"3": 0.004146883
}
}],
"2009": [{
"name": "< 680",
"values": {
"2": 0.001972273,
"3": 0.061021424,
"4": 0.347816086,
"5": 0.61161081,
"6": 0.570951163
}
}, {
"name": "680-719",
"values": {
"1": 0.018202872,
"2": 0.293262861,
"3": 0.758535486,
"4": 0.513630234,
"5": 0.31274635,
"6": 0.350945594
}
}, {
"name": "720-759",
"values": {
"1": 0.483340134,
"2": 0.536493435,
"3": 0.147870432,
"4": 0.099536218,
"5": 0.061560763,
"6": 0.065444695
}
}, {
"name": "760-799",
"values": {
"1": 0.426668429,
"2": 0.136695924,
"3": 0.030099845,
"4": 0.039017462,
"5": 0.014082076
}
}, {
"name": " > 799",
"values": {
"1": 0.071788565,
"2": 0.031575507,
"3": 0.002472813,
"6": 0.012658548
}
}],
"2010": [{
"name": "< 680",
"values": {
"2": 0.000565286,
"3": 0.101189409,
"4": 0.322693727,
"5": 0.460132061,
"6": 0.768736028
}
}, {
"name": "680-719",
"values": {
"1": 0.027164145,
"2": 0.299532019,
"3": 0.663901042,
"4": 0.619509592,
"5": 0.522816687,
"6": 0.220272267
}
}, {
"name": "720-759",
"values": {
"1": 0.529838118,
"2": 0.540840914,
"3": 0.212721062,
"4": 0.053869384,
"5": 0.014355666,
"6": 0.007426827
}
}, {
"name": "760-799",
"values": {
"1": 0.4036741,
"2": 0.141882155,
"3": 0.018212217,
"4": 0.003484646,
"5": 0.002695586,
"6": 0.003564877
}
}, {
"name": " > 799",
"values": {
"1": 0.039323637,
"2": 0.017179627,
"3": 0.00397627,
"4": 0.000442651
}
}],
"2011": [{
"name": "< 680",
"values": {
"2": 0.015057048,
"3": 0.116486357,
"4": 0.224473482,
"5": 0.289184506,
"6": 0.637609798
}
}, {
"name": "680-719",
"values": {
"1": 0.067014875,
"2": 0.386027401,
"3": 0.416716479,
"4": 0.568125117,
"5": 0.61540113,
"6": 0.348747984
}
}, {
"name": "720-759",
"values": {
"1": 0.539048116,
"2": 0.432319314,
"3": 0.384647352,
"4": 0.186473585,
"5": 0.088463897,
"6": 0.013642218
}
}, {
"name": "760-799",
"values": {
"1": 0.347640699,
"2": 0.151553784,
"3": 0.075187561,
"4": 0.01983747,
"5": 0.006950466
}
}, {
"name": " > 799",
"values": {
"1": 0.04629631,
"2": 0.015042453,
"3": 0.006962251,
"4": 0.001090345
}
}],
"2012": [{
"name": "< 680",
"values": {
"1": 0.005421439,
"2": 0.063906321,
"3": 0.283911481,
"4": 0.425256631,
"5": 0.450367247,
"6": 0.759740613
}
}, {
"name": "680-719",
"values": {
"1": 0.144858351,
"2": 0.634091318,
"3": 0.567030199,
"4": 0.535311115,
"5": 0.526155018,
"6": 0.234408449
}
}, {
"name": "720-759",
"values": {
"1": 0.545049517,
"2": 0.238328877,
"3": 0.133471737,
"4": 0.037855184,
"5": 0.022462245,
"6": 0.005850939
}
}, {
"name": "760-799",
"values": {
"1": 0.242711679,
"2": 0.052358647,
"3": 0.013872764,
"4": 0.001577071,
"5": 0.00073054
}
}, {
"name": " > 799",
"values": {
"1": 0.061959014,
"2": 0.011314837,
"3": 0.00171382,
"5": 0.00028495
}
}],
"2013": [{
"name": "< 680",
"values": {
"1": 0.023571756,
"2": 0.195151221,
"3": 0.310355648,
"4": 0.434717223,
"5": 0.461840072,
"6": 0.548137738
}
}, {
"name": "680-719",
"values": {
"1": 0.382264916,
"2": 0.600494954,
"3": 0.56921187,
"4": 0.497816621,
"5": 0.492143478,
"6": 0.422219252
}
}, {
"name": "720-759",
"values": {
"1": 0.400716441,
"2": 0.170888523,
"3": 0.105929433,
"4": 0.060732611,
"5": 0.040102649,
"6": 0.02732842
}
}, {
"name": "760-799",
"values": {
"1": 0.151039722,
"2": 0.027613396,
"3": 0.012099796,
"4": 0.005199786,
"5": 0.005058964,
"6": 0.002209775
}
}, {
"name": " > 799",
"values": {
"1": 0.042407165,
"2": 0.005851907,
"3": 0.002403253,
"4": 0.001533759,
"5": 0.000854837,
"6": 0.000104815
}
}],
"2014": [{
"name": "< 680",
"values": {
"1": 0.09100394,
"2": 0.260715312,
"3": 0.357812121,
"4": 0.428166968,
"5": 0.500877119,
"6": 0.582926869
}
}, {
"name": "680-719",
"values": {
"1": 0.40991467,
"2": 0.512238814,
"3": 0.50450076,
"4": 0.492149704,
"5": 0.451578706,
"6": 0.38127527
}
}, {
"name": "720-759",
"values": {
"1": 0.343033612,
"2": 0.181768549,
"3": 0.117484927,
"4": 0.072160131,
"5": 0.042532396,
"6": 0.030031709
}
}, {
"name": "760-799",
"values": {
"1": 0.126384859,
"2": 0.037901088,
"3": 0.017083752,
"4": 0.006185321,
"5": 0.004293123,
"6": 0.004388759
}
}, {
"name": " > 799",
"values": {
"1": 0.029662918,
"2": 0.007376236,
"3": 0.00311844,
"4": 0.001337875,
"5": 0.000718656,
"6": 0.001377393
}
}],
"2015": [{
"name": "< 680",
"values": {
"1": 0.070046383,
"2": 0.270228942,
"3": 0.382762751,
"4": 0.452456944,
"5": 0.481655394,
"6": 0.530177643
}
}, {
"name": "680-719",
"values": {
"1": 0.454732716,
"2": 0.497872156,
"3": 0.487494901,
"4": 0.463295654,
"5": 0.446155064,
"6": 0.409546578
}
}, {
"name": "720-759",
"values": {
"1": 0.314743806,
"2": 0.183511918,
"3": 0.109837217,
"4": 0.07301078,
"5": 0.063317006,
"6": 0.051229672
}
}, {
"name": "760-799",
"values": {
"1": 0.123455357,
"2": 0.039320465,
"3": 0.01683621,
"4": 0.010105941,
"5": 0.007550441,
"6": 0.008328697
}
}, {
"name": " > 799",
"values": {
"1": 0.037021739,
"2": 0.00906652,
"3": 0.003068921,
"4": 0.001130681,
"5": 0.001322096,
"6": 0.000717411
}
}],
"2016": [{
"name": "< 680",
"values": {
"1": 0.032102761,
"2": 0.282392528,
"3": 0.387682619,
"4": 0.452072231,
"5": 0.485884013,
"6": 0.516687305
}
}, {
"name": "680-719",
"values": {
"1": 0.451597134,
"2": 0.489041153,
"3": 0.483377285,
"4": 0.457223451,
"5": 0.441015531,
"6": 0.419706296
}
}, {
"name": "720-759",
"values": {
"1": 0.332516374,
"2": 0.179264739,
"3": 0.108623205,
"4": 0.075675392,
"5": 0.06315108,
"6": 0.05555666
}
}, {
"name": "760-799",
"values": {
"1": 0.140067131,
"2": 0.039911557,
"3": 0.017053532,
"4": 0.013692641,
"5": 0.008405617,
"6": 0.007302824
}
}, {
"name": " > 799",
"values": {
"1": 0.043716599,
"2": 0.009390023,
"3": 0.003263359,
"4": 0.001336285,
"5": 0.00154376,
"6": 0.000746914
}
}]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
}
label {
margin-right: 1rem;
}
fieldset {
border: none;
}
legend {
font-weight: bold;
}
</style>
</head>
<body>
<h1></h1>
<svg width="960" height="500"/>
<div class="controls"></div>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js" charset="utf-8"></script>
<script src=".script-compiled.js"></script>
<script>
// change frame height
d3.select(self.frameElement).style('height', '660px');
</script>
</body>
</html>
/*eslint no-undef: 0*/
function createChart (svg, data) {
const colors = ['#98abc5', '#8a89a6', '#7b6888', '#6b486b', '#a05d56', '#d0743c', '#ff8c00']
svg = d3.select(svg)
const margin = {top: 20, right: 20, bottom: 30, left: 40}
const width = 960 - margin.left - margin.right
const height = 500 - margin.top - margin.bottom
const g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1)
var x1 = d3.scaleBand()
.padding(0.05)
var y = d3.scaleLinear()
.rangeRound([height, 0])
var z = d3.scaleOrdinal()
.range(colors)
// check each subset of data for possible sections, since not all subsets have every possible section.
let nameKeys = data[Object.keys(data)[0]].map(obj =>obj.name)
let valueKeys = ["1", "2", "3", "4", "5", "6"]
//fill in empty data entries
Object.keys(data).forEach((d)=>{
data[d].forEach(section=>{
valueKeys.forEach(k=>{
if (section.values[k] === undefined) section.values[k] = 0
})
})
})
x0.domain(nameKeys)
x1.domain(valueKeys).rangeRound([0, x0.bandwidth()])
const barContainer = g.append('g')
const xAxis = g.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x0))
const yAxis = g.append('g')
.attr('class', 'axis')
yAxis
.append('text')
.attr('x', 2)
.attr('y', y(y.ticks().pop()) + 0.5)
.attr('dy', '0.32em')
.attr('font-weight', 'bold')
.attr('text-anchor', 'start')
.text('Prop Value')
var legend = g.append('g')
.attr('font-size', 10)
.attr('text-anchor', 'end')
legend.append('text')
.text('Loan Grade')
.attr('x', width - 19)
.style('font-weight', 'bold')
.attr('dy', -10)
.attr('dx', 20)
var legendEnter = legend
.selectAll('g')
.data(valueKeys)
.enter().append('g')
.attr('transform', function (d, i) { return 'translate(0,' + i * 20 + ')' })
legendEnter.append('rect')
.attr('x', width - 19)
.attr('width', 19)
.attr('height', 19)
.attr('fill', z)
legendEnter.append('text')
.attr('x', width - 24)
.attr('y', 9.5)
.attr('dy', '0.32em')
.text(d => d)
const stack = d3.stack()
.keys(valueKeys)
// updates both the year + the chart type (group or stacked)
function updateChart (data, chartType='group') {
// ========================================================
// show grouped view
// ========================================================
if (chartType === 'group'){
//find max value of a section
const maxValue = d3.max(data.map((d) => Object.values(d.values)).reduce((a, b) => a.concat(b), []))
y.domain([0, maxValue]).nice()
yAxis.transition()
.call(d3.axisLeft(y))
const barsWithData = barContainer
.selectAll('g')
.data(data)
barsWithData.exit().remove()
const bars = barsWithData
.enter()
.append('g')
.attr('transform', function (d) { return 'translate(' + x0(d.name) + ',0)' })
.merge(barsWithData)
.selectAll('rect')
.data(function (d) {
return Object.keys(d.values).map(k => ({ key: k, value: d.values[k] }))
})
bars.exit().transition().style('opacity', 0).remove()
bars
.enter()
.append('rect')
.attr('fill', function (d) {
return z(d.key)
})
// start y at height (0) so animation in looks like bars are growing upwards
.attr('y', height)
.merge(bars)
.transition()
.attr('width', x1.bandwidth())
.attr('x', function (d) { return x1(d.key) })
.attr('y', d => y(d.value))
.attr('height', d => height - y(d.value))
}
// ========================================================
// show stacked view
// ========================================================
else if (chartType === 'stack'){
//find max value of a section
const maxValue = d3.max(
data.map((d) => Object.values(d.values))
.map((valueArray)=>{
return valueArray.reduce((a,b)=> a+ b)
})
)
y.domain([0, maxValue]).nice()
yAxis.transition()
.call(d3.axisLeft(y))
//add data for missing bars
const seriesFlipped = stack(data.map(d=>{
const defaultData = {}
valueKeys.forEach(k=> defaultData[k] = 0)
return Object.assign(defaultData, d.values)
}))
const series = []
//need to reorient the series
//we want a list of groups, not a list of rects from each level
seriesFlipped[0].forEach((col, i)=>{
const arr = []
seriesFlipped.forEach((row, index2)=>{
//mimic the key from the grouped data format
row[i].key = index2 + 1 + ''
arr.push(row[i])
})
series.push(arr)
})
const barSections = barContainer
.selectAll('g')
.data(series)
const bars = barSections
.enter()
.append('g')
.merge(barSections)
.attr('transform', (d,i)=> {console.log(x0(nameKeys[i])); return 'translate(' + x0(nameKeys[i]) + ',0)'} )
.selectAll('rect')
.data(d=>d, (d)=> d.key)
const enterBars = bars.enter().append('rect')
.attr('fill', (d)=> z(d.key))
bars.exit().transition().style('opacity', 0).remove()
enterBars
.merge(bars)
.transition()
.delay((d,i)=> i * 50)
.attr('width', x0.bandwidth())
.attr("y", function(d) {return y(d[1]) })
.attr("x", 0)
.attr("height", function(d) { return y(d[0]) - y(d[1]) })
}
}
return {
updateChart
}
}
d3.json('./data.json', function(error, data){
//start with the first year selected
const chart = createChart(document.querySelector('svg'), data)
// append the input controls
const fieldset1 = d3.select('.controls').append('fieldset')
fieldset1.append('legend').text('Year')
Object.keys(data).forEach((year, index )=>{
const label = fieldset1.append('label')
label
.append('input')
.attr('type', 'radio')
.attr('name', 'year')
.attr('value', year)
.attr('checked', function(){
if (index === 0) return true
return null
})
label.append('span')
.text(year)
label.on('click', function(){
chart.updateChart(data[year], document.querySelector('input[name="graphType"]:checked').value)
})
})
const fieldset2 = d3.select('.controls').append('fieldset')
const types = ['group', 'stack']
fieldset2.append('legend').text('Graph Layout')
types.forEach((graphType, index)=>{
const label = fieldset2.append('label')
label.append('input')
.attr('type', 'radio')
.attr('name', 'graphType')
.attr('value', graphType)
.attr('checked', function(){
if (index === 0) return true
return null
})
.on('click', ()=>{
chart.updateChart(data[document.querySelector('input[name="year"]:checked').value], graphType)
})
label.append('span')
.text(graphType)
})
// render initial chart
chart.updateChart(data[Object.keys(data)[0]])
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment