Created
December 16, 2011 22:45
Dynamic Stacked Bar Chart using d3.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>Dynamic Stacked Bar Chart using d3.js</title> | |
<script src="http://mbostock.github.com/d3/d3.v2.js"></script> | |
<style> | |
rect.a { | |
fill: green; | |
} | |
rect.b { | |
fill: orange; | |
} | |
rect.c { | |
fill: red; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="graph" class="aBarchart" style="width:1000px; height:200px;"></div> | |
<script> | |
// capture the height/width defined in the div so we only have it defined in one place | |
var chartHeight = parseInt(document.getElementById('graph').style.height); | |
var chartWidth = parseInt(document.getElementById('graph').style.width); | |
// TODO we need a ceiling value | |
var ceiling = 200; | |
// Y scale will fit values from 0-10 within pixels 0 - height | |
var y = d3.scale.linear().domain([0, ceiling]).range([0, chartHeight]); | |
/** | |
* Create an empty shell of a chart that bars can be added to | |
*/ | |
function displayStackedChart(chartId) { | |
// create an SVG element inside the div that fills 100% of the div | |
var vis = d3.select("#" + chartId).append("svg:svg").attr("width", "100%").attr("height", "100%") | |
// transform down to simulate making the origin bottom-left instead of top-left | |
// we will then need to always make Y values negative | |
.append("g").attr("class","barChart").attr("transform", "translate(0, " + chartHeight + ")"); | |
} | |
/* the property names on the data objects that we'll get data from */ | |
var propertyNames = ["a", "b", "c"]; | |
/** | |
* Add or update a bar of data in the given chart | |
* | |
* The data object expects to have an 'id' property to identify itself (id == a single bar) | |
* and have object properties with numerical values for each property in the 'propertyNames' array. | |
*/ | |
function addData(chartId, data) { | |
// if data already exists for this data ID, update it instead of adding it | |
var existingBarNode = document.querySelectorAll("#" + chartId + "_" + data.id); | |
if(existingBarNode.length > 0) { | |
var existingBar = d3.select(existingBarNode.item()); | |
// reset the decay since we received an update | |
existingBar.transition().duration(100) | |
.attr("style", "opacity:1.0"); | |
// update the data on each data point defined by 'propertyNames' | |
for(index in propertyNames) { | |
existingBar.select("rect." + propertyNames[index]) | |
.transition().ease("linear").duration(300) | |
.attr("y", barY(data, propertyNames[index])) | |
.attr("height", barHeight(data, propertyNames[index])); | |
} | |
} else { | |
// it's new data so add a bar | |
var barDimensions = updateBarWidthsAndPlacement(chartId); | |
// select the chart and add the new bar | |
var barGroup = d3.select("#" + chartId).selectAll("g.barChart") | |
.append("g") | |
.attr("class", "bar") | |
.attr("id", chartId + "_" + data.id) | |
.attr("style", "opacity:1.0"); | |
// now add each data point to the stack of this bar | |
for(index in propertyNames) { | |
barGroup.append("rect") | |
.attr("class", propertyNames[index]) | |
.attr("width", (barDimensions.barWidth-1)) | |
.attr("x", function () { return (barDimensions.numBars-1) * barDimensions.barWidth;}) | |
.attr("y", barY(data, propertyNames[index])) | |
.attr("height", barHeight(data, propertyNames[index])); | |
} | |
// setup an interval timer for this bar that will decay the coloring | |
barGroup.styleInterval = setInterval(function() { | |
var theBar = document.getElementById(chartId + "_" + data.id); | |
if(theBar == undefined) { | |
clearInterval(barGroup.styleInterval); | |
} else { | |
if(theBar.style.opacity > 0.2) { | |
theBar.style.opacity = theBar.style.opacity - 0.05; | |
} | |
} | |
}, 1000); | |
//console.log("set interval: " + barGroup.styleInterval); | |
} | |
} | |
/** | |
* Remove a bar of data in the given chart | |
* | |
* The data object expects to have an 'id' property to identify itself (id == a single bar) | |
* and have object properties with numerical values for each property in the 'propertyNames' array. | |
*/ | |
function removeData(chartId, barId) { | |
var existingBarNode = document.querySelectorAll("#" + chartId + "_" + barId); | |
if(existingBarNode.length > 0) { | |
// bar exists so we'll remove it | |
var barGroup = d3.select(existingBarNode.item()); | |
barGroup | |
.transition().duration(200) | |
.remove(); | |
} | |
} | |
/** | |
* Update the bar widths and x positions based on the number of bars. | |
* @returns {barWidth: X, numBars:Y} | |
*/ | |
function updateBarWidthsAndPlacement(chartId) { | |
/** | |
* Since we dynamically add/remove bars we can't use data indexes but must determine how | |
* many bars we have already in the graph to calculate x-axis placement | |
*/ | |
var numBars = document.querySelectorAll("#" + chartId + " g.bar").length + 1; | |
// determine what the width of all bars should be | |
var barWidth = chartWidth/numBars; | |
if(barWidth > 50) { | |
barWidth=50; | |
} | |
// reset the width and x position of each bar to fit | |
var barNodes = document.querySelectorAll(("#" + chartId + " g.barChart g.bar")); | |
for(var i=0; i < barNodes.length; i++) { | |
d3.select(barNodes.item(i)).selectAll("rect") | |
//.transition().duration(10) // animation makes the display choppy, so leaving it out | |
.attr("x", i * barWidth) | |
.attr("width", (barWidth-1)); | |
} | |
return {"barWidth":barWidth, "numBars":numBars}; | |
} | |
/* | |
* Function to calculate the Y position of a bar | |
*/ | |
function barY(data, propertyOfDataToDisplay) { | |
/* | |
* Determine the baseline by summing the previous values in the data array. | |
* There may be a cleaner way of doing this with d3.layout.stack() but it | |
* wasn't obvious how to do so while playing with it. | |
*/ | |
var baseline = 0; | |
for(var j=0; j < index; j++) { | |
baseline = baseline + data[propertyNames[j]]; | |
} | |
// make the y value negative 'height' instead of 0 due to origin moved to bottom-left | |
return -y(baseline + data[propertyOfDataToDisplay]); | |
} | |
/* | |
* Function to calculate height of a bar | |
*/ | |
function barHeight(data, propertyOfDataToDisplay) { | |
return data[propertyOfDataToDisplay]; | |
} | |
// used to populate random data for testing | |
function randomInt(magnitude) { | |
return Math.floor(Math.random()*magnitude); | |
} | |
/* initialize the chart without any data */ | |
displayStackedChart("graph"); | |
/* kick off a continual interval timer to simulate the ongoing addition and update of data */ | |
setInterval(function() { | |
addData("graph", {"id":"v"+randomInt(150), "a":(randomInt(50)+100), "b":randomInt(50), "c":randomInt(40)}); | |
}, 20); | |
/* kick off a continual interval timer to simulate the occasional removal of data */ | |
setInterval(function() { | |
// we want removals to be somewhat bursty, so we'll randomize how many we remove | |
var numToRemove = randomInt(20); | |
for(var r=0; r<numToRemove; r++) { | |
removeData("graph", "v"+randomInt(150)); | |
} | |
}, 5000); | |
</script> | |
</body> | |
</html> |
Updated the d3.js link to http://mbostock.github.com/d3/d3.v2.js
great example. created a fiddle to try it out: http://jsfiddle.net/3YNNJ/2/
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Blog post at: http://benjchristensen.com/2011/12/16/dynamic-stacked-bar-chart-using-d3-js/
View at: http://bl.ocks.org/1488375