Skip to content

Instantly share code, notes, and snippets.

@erikhazzard
Created July 30, 2013 21:14
Show Gist options
  • Save erikhazzard/6117033 to your computer and use it in GitHub Desktop.
Save erikhazzard/6117033 to your computer and use it in GitHub Desktop.
Negative Stacked Bar Charts
{"description":"Negative Stacked Bar Charts","endpoint":"","display":"svg","public":true,"require":[],"fileconfigs":{"inlet.js":{"default":true,"vim":false,"emacs":false,"fontSize":12},"_.md":{"default":true,"vim":false,"emacs":false,"fontSize":12},"config.json":{"default":true,"vim":false,"emacs":false,"fontSize":12},"style.css":{"default":true,"vim":false,"emacs":false,"fontSize":12}},"fullscreen":false,"play":false,"loop":false,"restart":false,"autoinit":true,"pause":true,"loop_type":"period","bv":false,"nclones":15,"clone_opacity":0.4,"duration":3000,"ease":"linear","dt":0.01,"thumbnail":"http://i.imgur.com/TEELsAg.png"}
// ---------------------------------------------------------------------------
// Negative / Positive Stacked bar chart
// In the 'reusable' chart pattern: http://bost.ocks.org/mike/chart/
// ---------------------------------------------------------------------------
//
var getData = function getData(){
// returns some dummy data
// *note*: In this example, the data is expected to be in the format of
// an array with sub arrays. Each sub array represents a group of stacked
// bars, each item in each sub array is an object representing an individual
// "stacked" item
//
// You can change the StackedChart function to work with your data format by
// modifying the `setupStack` function.
var dummyData = [];
var stack = null;
// Make some random data
// (*note* use Math.ceil so 0 is never generated)
for(var i=0; i < Math.ceil(Math.random() * 10); i++){
stack = [];
for(var j=0; j < Math.ceil(Math.random() * 5); j++){
stack.push({
value: Math.round(-100 + Math.random() * 200) + 1,
name: i + '-' + j,
group: 'Group ' + i
});
}
dummyData.push(stack);
}
//// To see structure of data:
// console.log( JSON.stringify( dummyData, null, 4 ) );
return dummyData;
};
var StackedChart = function StackedChart(config){
// Setup SVG
// --------------------------------------
// default values
config = config || {};
var width = config.width || 500;
var height = config.height || 500;
var margin = config.margin || 20;
var data = config.data || [];
var duration = config.duration || 700;
// color scale
var color = d3.scale.category20c();
// assumes an SVG element already exists
// (note: attributes will be reset in myChart())
var svg = d3.select('svg').attr({
width: width,
height: height
});
// Setup groups
var chartGroup = svg.append('g');
var xAxisGroup = svg.append('g').attr('class','axis x');
var yAxisGroup = svg.append('g').attr('class','axis y');
// Setup scales and axes
// --------------------------------------
var xScale;
var yScale;
var updateScales = function updateScales(){
// setup an ordinal scale for the x axis. The input domain will be an
// array of group names (from the data)
xScale = d3.scale.ordinal().domain(data.map(function(datum,i){
// We'll always have at least element in the datum array
return datum[0].group;
}))
.rangeRoundBands([margin, width - margin], 0.1);
// Setup a linear scale for the y axis
var merged = d3.merge(data);
yScale = d3.scale.linear().domain([
// the min data should be the base y minus the size
d3.min(merged, function(d){ return d.y0-d.size; }),
// y0 will contain the highest value
d3.max(merged, function(d){ return d.y0; })
])
.range([height - margin, margin])
// nice it so we get nice round values
.nice();
};
var updateAxis = function(){
// Update the x and y axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickSize(6, 0);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
// use the axes group defined above
xAxisGroup.transition()
.duration(duration)
.attr({
transform: "translate (" + [
0, yScale(0) ] + ")"
})
.call(xAxis);
yAxisGroup.transition()
.duration(duration)
.attr({
transform: "translate (" + [
xScale(margin), 0 ] + ")"
})
.call(yAxis);
};
// Update bars
// --------------------------------------
var updateBars = function(){
// This function is called to:
// 1. initially create the stacked bars
// 2. update stacked bars on all subsequent calls
// Setup groups
var barGroups = chartGroup.selectAll('.barGroup').data(data);
// create groups
barGroups.enter().append('svg:g').attr({
'class': 'barGroup'
});
// ** exit groups **
// handles if an entire group is removed
barGroups.exit().transition()
.duration(duration)
.style({ opacity: 0 })
.remove();
// secondly, setup the stacked groups
var bars = barGroups.selectAll('.bar')
.data(function(d){
return d;
});
// ** Enter **
bars.enter().append('svg:rect')
.attr({
'class': 'bar',
width: xScale.rangeBand(),
x: function(d,i){
// Pass in the index to the xScale
return xScale(d.group);
},
y: function(d,i){
return yScale(d.y0);
},
height: function(d,i){
return (yScale(0) - yScale(d.size)) / 2;
}
})
.style({
fill: function(d,i){
return color(i);
},
opacity: 0
})
// interaction
.on('mouseenter', function(d, i){
console.log('Current item: ', d, 'Index: ', i);
});
// ** Update **
bars.transition()
.duration(duration)
.attr({
width: xScale.rangeBand(),
x: function(d,i){
// Pass in the index to the xScale
return xScale(d.group);
},
y: function(d,i){
return yScale(d.y0);
},
height: function(d,i){
return yScale(0) - yScale(d.size);
}
})
.style({
opacity: 1
});
//** Exit **
// handles if an individual stacked item is removed
bars.exit().transition()
.duration(duration)
.attr({ height: 0 })
.style({ opacity: 0 })
.remove();
return bars;
};
// --------------------------------------
// Update / Draw Chart
// --------------------------------------
var myChart = function myChart(){
// Update the svg properties
svg.attr({
height: height,
width: width
});
// Format data
data = setupStack(data);
// Update all parts of the chart
updateScales();
updateAxis();
// Update bars
updateBars();
return myChart;
};
// Helper functions
// ----------------------------------
var setupStack = function setupStack(origData){
// Formats the passed in data object to be in a format our
// chart can consume.
// *note*: This will modify the passed in object. If you don't want
// this behavior, you can clone the object (e.g., use underscore's
// clone method: origData = _.clone(origData)
//
// The setup data will be an array of arrays, each object in the
// subarray being an object representing an individual "stack". There
// are two added properties, `y0` and `size`, which specify the y
// position and `height` of the stack item. When created the bars,
// use these properties to position and size the bar
//
// setup some variables
var len = origData.length;
var i=0; j=0, d=null;
var basePositive=0, baseNegative=0;
for(i=0;i<len;i++){ // loop through each stacked group
// reset bases for each new group
basePositive = 0;
baseNegative = 0;
for(j=0; j<origData[i].length; j++){ // loop through each stack
stackItem = origData[i][j];
stackItem.size = Math.abs(stackItem.value);
// If the value is negative, we want to place the bar under
// the 0 line
if (stackItem.value < 0) {
stackItem.y0 = baseNegative;
baseNegative -= stackItem.size;
} else {
basePositive += stackItem.size;
stackItem.y0 = basePositive;
}
}
}
//// To see the format of the data:
//console.log(JSON.stringify(origData, null, 4));
return origData;
};
// Config Chart
// ----------------------------------
myChart.width = function(value) {
if (!arguments.length){ return width; }
width = value;
return myChart;
};
myChart.height = function(value) {
if (!arguments.length){ return height; }
height = value;
return myChart;
};
myChart.margin = function(value) {
if (!arguments.length){ return margin; }
margin = value;
return myChart;
};
myChart.data = function(value) {
if (!arguments.length){ return data; }
data = value;
return myChart;
};
myChart.duration = function(value) {
if (!arguments.length){ return duration; }
duration= value;
return myChart;
};
return myChart;
};
// --------------------------------------
// Create the chart
// --------------------------------------
var chart = StackedChart({
// pass in some data
data: getData(),
// transition duration
duration: 900
});
chart();
// further calls to chart() will update it
setInterval(function(){
// we can also change any exposed property
//chart.margin(Math.random() * 20 )
//.height(Math.random() * 800 )
//.width(Math.random() * 800 )
//.data( getData() )
//();
// pass in new data then generate chart
chart.data(getData())();
}, 1800);
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment