Skip to content

Instantly share code, notes, and snippets.

@erikhazzard
Created August 2, 2013 01:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save erikhazzard/6136753 to your computer and use it in GitHub Desktop.
Save erikhazzard/6136753 to your computer and use it in GitHub Desktop.
Quintiles Stacked
{"description":"Quintiles Stacked","endpoint":"","display":"svg","public":true,"require":[],"fileconfigs":{"inlet.js":{"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/cZXCdLP.png"}
// ---------------------------------------------------------------------------
// Negative / Positive Stacked bar 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)
var rand = Math.ceil(Math.random() * 10);
for(var i=0; i < 5; i++){
stack = [];
for(var j=0; j < Math.ceil(Math.random() * 10); 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;
};
// ---------------------------------------------------------------------------
// Stacked Chart
// ---------------------------------------------------------------------------
var StackedChart = function StackedChart(config){
// Config
// --------------------------------------
// default values
config = config || {};
var width = config.width || 700;
var height = config.height || 500;
var margin = config.margin || {
top: 10, right: 10,
bottom: 10, left: 40
};
var data = config.data || [];
var duration = config.duration || 700;
// color scale
var color;
// assumes an SVG element already exists
// (note: attributes will be reset in chart())
var svg = d3.select('svg').attr({
width: width,
height: height
});
// keep track of first draw, used so the axes don't transition
var isFirstDraw = true;
var chartLeftMargin = 10;
// Setup groups
// ----------------------------------
var groups = {};
groups.chart = svg.append('g').attr({ 'class': 'chart' });
// Axes
groups.axes = groups.chart.append('g').attr({ 'class': 'axes' });
groups.xAxis = groups.axes.append('g')
.attr({'class': 'axis x'});
groups.yAxis = groups.axes.append('g')
.attr({'class': 'axis y'});
// Chart
groups.stackChartWrapper = groups.chart.append('g')
.attr({
'class': 'stackChartWrapper',
transform: 'translate(' + [chartLeftMargin, 0] + ')'
});
// add group for zero line
groups.zeroLine = groups.chart.append('g').attr({ 'class': 'zeroLine' });
// Annotations
groups.annotations = groups.chart.append('g')
.attr({ 'class': 'annotations' });
// add a text element (will be updated during interactions)
var annotationText = groups.annotations.append('svg:text')
.attr({
'class': 'annotationText',
x: 0,
y: 0
}).text('');
// Styles
// ----------------------------------
var styles = {
stackRect: {
fill: '#f4f4f4',
stroke: '#f4f4f4'
},
colors: {
positive: '#aed8ca',
negative: '#efc5b0'
}
};
// --------------------------------------
// Setup scales and axes
// --------------------------------------
var xScale;
var yScale;
var bandPadding = 0.1;
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.left, width - (margin.left + margin.right)], bandPadding);
// 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.top + margin.bottom), margin.bottom])
// nice it so we get nice round values
.nice();
// Update color scale
var yDomain = yScale.domain();
color = d3.scale.linear()
.domain([yDomain[0], 0, yDomain[1]])
.range(['#d7301f', '#fee8c8', '#7ce07d']);
// store value of the zero bar position so we don't have to recalculate
helpers.y0 = yScale(0);
};
// Setup Axes
// --------------------------------------
var updateAxis = function(){
// Update the x and y axis
var yAxis = d3.svg.axis()
.tickSize(-width - (margin.left + margin.right))
.scale(yScale)
.ticks(6)
.orient("left");
var axesDuration = duration;
if(isFirstDraw){ axesDuration = 0; }
// use the axes group defined above
groups.yAxis
.transition()
.duration(axesDuration)
.attr({
transform: "translate (" + [ xScale(margin.left), 0 ] + ")"
})
.call(yAxis);
// Draw a zero line
var zeroLine = groups.zeroLine.selectAll('.zeroLine')
.data([{}]);
zeroLine.enter().append('svg:line')
.attr({
'class': 'zeroLine'
});
zeroLine.attr({
y1: helpers.y0,
y2: helpers.y0,
x1: xScale(margin.left),
x2: width - (margin.left + margin.right) + xScale(margin.left)
});
};
// -----------------------------------------------------------------------
// Update Chart
// -----------------------------------------------------------------------
var updateStackGroups = function(){
// setup a group for each stack
// ------------------------------
var stackGroups = groups.stackChartWrapper.selectAll('.stackWrapper')
.data(data);
groups.stackGroups = stackGroups;
// ** Enter ** stacked groups
stackGroups.enter().append('svg:g')
.attr({
'class': function(d,i){
return 'stackWrapper group' + i;
}
})
// --------------------------
// ** INTERACTION **
// --------------------------
.on('mouseenter', function(d,i){
var $sel = d3.select(this);
//// Fade out all other bars
//groups.stackChartWrapper.selectAll('.stackWrapper')
//.style({ opacity: 0.5 });
// fade out labels
groups.axes.selectAll('text')
.transition().duration(duration/2)
.style({ opacity: 0.08 });
$sel.style({ opacity: 1 });
// Hide the net rect for this item
// NET rect
var netRect = $sel.select('.netRect');
$sel.select('.netRect')
.transition().duration(duration)
.style({ opacity: 0 })
// Make net bar as big as outline bar
// (NOTE : Gets reset to normal y / height on mouseleave
.attr({
y: helpers.stackY,
height: helpers.stackHeight
});
// Show the individual stack rects
// STACKS
$sel.selectAll('.stackRectsGroup')
.transition().duration(duration)
.style({ opacity: 1 });
// Update position of each stack item
$sel.selectAll('.stackRect')
.transition().duration(duration)
.attr({
y: function(d,i){ return yScale(d.y0); },
height: function(d,i){ return yScale(0) - yScale(d.size); }
})
.style({ opacity: 1 });
// Show net circle
$sel.selectAll('.netDot')
.style({ opacity: 1 });
// Draw line from net bar to axis
$sel.selectAll('.netLine')
.attr({
x1: function(){
// make the line extend across axis on left side
// a bit
return helpers.yAxisLocation() - 9; },
x2: helpers.netMidX,
opacity: 1
});
// Update annotation
annotationText.attr({
x: function(){ return helpers.yAxisLocation(); },
y: function(){ return helpers.netSumY(d,i) + 4; }
}).text( function(){ return helpers.getGroupSum(d); });
})
// MOUSE LEAVE
.on('mouseleave', function(d,i){
var $sel = d3.select(this);
//// Reset everything to normal
//groups.stackChartWrapper.selectAll('.stackWrapper')
//.style({ opacity: 1 });
groups.axes.selectAll('text')
.transition().duration(duration/2)
.style({ opacity: 1 });
stackGroups.selectAll('.outlineRect')
.style({ opacity: 1 });
// Hide the net rect for this item
// NET rect
var netRect = $sel.select('.netRect');
netRect.transition().duration(duration)
.attr({
y: helpers.netY,
height: helpers.netHeight
})
.style({ opacity: 1 });
// Shrink down the individual stack rects
$sel.selectAll('.stackRect')
.transition().duration(duration)
.style({ opacity: 0 });
//// NOTE: if we want stack items to come from center,
//set their properties like this:
//.attr({
//y: parseInt(netRect.attr('y'),10) +
//(parseInt(netRect.attr('height'),10) / 2),
//height: 0
//});
// reset net line
$sel.selectAll('.netLine')
.attr({
x1: helpers.netMidX,
x2: helpers.netMidX,
opacity: 1
});
$sel.selectAll('.netDot')
.style({ opacity: 0 });
});
// ** Exit ** stacked groups
stackGroups.exit().transition().duration(duration)
.style({ opacity: 0 })
.remove();
};
// Invisible Interaction Rects
// ----------------------------------
var updateInteractionRects = function(){
// Creates and updates rects placed behind everything
// to aid interaction
var stackGroups = groups.stackGroups;
// Setup Invisible interaction rect for each group
// ------------------------------
var interactionRects = stackGroups.selectAll('.interactionRect')
.data(function(d, i){
// Wrap each array of group items in an array so only
// one item is added below for each rect
return [d];
});
// ** Enter ** interaction bars
var interactionPadding = 35;
interactionRects.enter().append('rect')
.attr({
'class': function(d,i){
return 'interactionRect interaction' + i;
},
x: helpers.stackX,
y: helpers.y0,
height: 0,
width: xScale.rangeBand()
}).style({ opacity: 0 });
// ** Update **
// Add some padding so there's a larger interaciton radius
interactionRects.attr({
y: function(d,i){
return helpers.stackY(d,i) - interactionPadding;
},
height: function(d,i){
return helpers.stackHeight(d,i) + (interactionPadding * 2);
},
x: function(d,i){
return helpers.stackX(d,i) - 3;
},
// no gap between stacks
width: xScale.rangeBand() + (xScale.rangeBand() * bandPadding) + 2
});
// ** Exit ** interaction bars
interactionRects.exit().remove();
};
// Greyed out outline rects
// ----------------------------------
var updateOutlineRects = function(){
// Creates and updates the rect representing the combined stacks
var stackGroups = groups.stackGroups;
// Setup Invisible interaction rect for each group
// ------------------------------
var outlineRects = stackGroups.selectAll('.outlineRect')
.data(function(d, i){
// Wrap each array of group items in an array so only
// one item is added below for each rect
return [d];
});
// ** Enter ** interaction bars
outlineRects.enter().append('rect')
.attr({
'class': function(d,i){
return 'outlineRect outline' + i;
},
x: helpers.stackX,
y: helpers.y0,
height: 0,
width: xScale.rangeBand()
});
// ** Update **
outlineRects.transition().duration(duration)
.attr({
y: helpers.stackY,
height: helpers.stackHeight,
x: helpers.stackX,
width: xScale.rangeBand()
});
// ** Exit ** outline bars
outlineRects.exit().transition().duration(duration)
.style({ opacity: 0 })
.remove();
};
// Rects for net impact
// ----------------------------------
var updateNetRects = function(){
// Draws net impact rects for each stacked group
//
var stackGroups = groups.stackGroups;
// Net Bars
// ------------------------------
var netRects = stackGroups.selectAll('.netRect')
.data(function(d, i){
// Wrap each array of group items in an array so only
// one item is added below for each rect
return [d];
});
// ** Enter **
// Draw bars for the net impact
netRects.enter().append('svg:rect')
.attr({
'class': function(d,i){
return 'netRect net' + i;
},
width: xScale.rangeBand(),
x: helpers.stackX,
y: yScale(0),
height: 0
})
.style({
fill: helpers.colorFill,
stroke: helpers.colorStroke,
'stroke-width': '2px'
});
// ** Update **
netRects.transition().duration(duration)
.attr({
width: xScale.rangeBand(),
x: helpers.stackX,
y: helpers.netY,
height: helpers.netHeight
});
// ** Exit **
netRects.exit().transition().duration(duration)
.attr({ height: 0 })
.style({ opacity: 0 })
.remove();
};
// Rects for individual stack items
// ----------------------------------
var updateStackRects = function(){
// This function is called to:
// 1. initially create the stacked bars
// 2. update stacked bars on all subsequent calls
// Setup groups
var stackRectsGroup = groups.stackGroups
.selectAll('.stackRectsGroup')
.data(function(d){
// return a single array of the stack items.
return [d];
});
// ** Enter - group **
stackRectsGroup.enter()
.append('g')
.attr({
'class': function(d,i){
return 'stackRectsGroup stackRectsGroup' + i;
}
}).style({ opacity: 0 });
// ** Update - group **
stackRectsGroup.style({ opacity: 0 });
// ** Exit - group **
stackRectsGroup.exit().transition().duration(duration)
.style({ opacity: 0 })
.remove();
// Setup individual stack items
// ------------------------------
stackRects = stackRectsGroup.selectAll('.stackRect')
.data(function(d){ return d; });
// ** Enter **
stackRects.enter().append('svg:rect')
.attr({ 'class': 'stackRect' })
.style({
fill: helpers.colorFill,
stroke: '#ffffff',
'stroke-width': '2px'
})
// Stack Item Interaction
.on('mouseenter', function(d, i){
// TODO: Show tooltip
console.log('>>> Current item: ', d, 'Index: ', i);
});
// ** Update **
// No duration, since these are hidden at first
stackRects.attr({
// Position in middle of net rect
x: function(d){
return xScale(d.group);
},
width: xScale.rangeBand(),
y: function(d,i){ return yScale(d.y0); },
height: function(d,i){ return yScale(0) - yScale(d.size); }
////NOTE: If we want stack rects to start in middle of net bar,
////use this:
//y: function(d,i,dataIndex){
//return helpers.netY(data[dataIndex]) +
//(helpers.netHeight(data[dataIndex]) / 2);
//},
//height: 0
});
//** Exit **
// handles if an individual stacked item is removed
stackRects.exit().transition()
.duration(duration)
.attr({ height: 0 })
.style({ opacity: 0 })
.remove();
};
// Lines from net impact to axis
// ----------------------------------
var updateNetLines = function(){
// Draws a line from the net bar to the axis
//
var stackGroups = groups.stackGroups;
// Net Bars
// ------------------------------
var netLines = stackGroups.selectAll('.netLine')
.data(function(d, i){ return [d]; });
// ** Enter **
// Draw line to the axis
netLines.enter().append('svg:line')
.attr({
'class': function(d,i){
return 'netLine netLine' + i;
},
x1: 0,
x2: 0,
y1: helpers.netSumY,
y2: helpers.netSumY
})
.style({
});
// ** Update **
// No need to transition, lines only show on interaction
//
// * note * the x1 property will be transitioned to the axis
// on interaction (returned from calling helpers.yAxisLocation)
netLines.attr({
x1: helpers.netMidX,
x2: helpers.netMidX,
y1: helpers.netSumY,
y2: helpers.netSumY
});
// ** Exit **
netLines.exit()
.attr({ height: 0 })
.style({ opacity: 0 })
.remove();
};
// Lines from net impact to axis
// ----------------------------------
var updateNetDots = function(){
// Draws a line from the net bar to the axis
//
var stackGroups = groups.stackGroups;
// Net Bars
// ------------------------------
var netDots = stackGroups.selectAll('.netDot')
.data(function(d, i){ return [d]; });
// ** Enter **
// Draw line to the axis
netDots.enter().append('svg:circle')
.attr({
'class': function(d,i){
return 'netDot netDot' + i;
},
cx: helpers.netMidX,
cy: helpers.netSumY,
r: 6
}).style({
opacity: 0
});
// ** Update **
// No need to transition, lines only show on interaction
//
// * note * the x1 property will be transitioned to the axis
// on interaction (returned from calling helpers.yAxisLocation)
netDots.attr({
cx: helpers.netMidX,
cy: helpers.netSumY
});
// ** Exit **
netDots.exit()
.attr({ height: 0 })
.style({ opacity: 0 })
.remove();
};
// ----------------------------------
// Update Chart stack sub components
// ----------------------------------
var updateStacks = function updateStacks(){
// Main update chart function. Sets up and updates all components of the
// stacked chart
//
// Overview of the SVG structure
// 1. Group for each 'stack'
// 2. Invisible rect behind everything for interaction
// 3. Rect representing range of pos / neg items
// 4.1 Rect repesenting net impact (hidden on mouse over)
// 4.2 Rects for each policy (shown on mouse over)
//
// Group controls mouse interaction behavior
updateStackGroups();
updateInteractionRects();
updateOutlineRects();
updateNetRects();
updateStackRects();
updateNetLines();
updateNetDots();
};
// -----------------------------------------------------------------------
// Main Chart Function
// -----------------------------------------------------------------------
var chart = function chart(){
// Update the svg properties
svg.attr({
height: height,
width: width
});
// When this is called, disable any mouse interaction when things are
// transitioning
var blocker = svg.append('svg:rect')
.attr({
x: 0,
y: 0,
width: width,
height: height
})
.style({ opacity: 0 });
setTimeout(function(){
// remove the blocking rect after transitions are finished
blocker.remove();
}, duration);
// Format data
data = helpers.setupStack(data);
// Setup axes, chart, etc
updateScales();
updateAxis();
updateStacks();
// we're not on the first draw anymore
isFirstDraw = false;
return chart;
};
// ----------------------------------
// Helper functions
// ----------------------------------
var helpers = {};
helpers.stackX= function(d,i){
//Takes in either an array of stack objects or a single stack item and
//returns the x coordinate for it
if(d instanceof Array){ d = d[0]; }
var x = xScale(d.group);
return x;
};
helpers.stackY = function(d,i){
return yScale(d3.max(d, function(datum){
return datum.y0;
}));
};
helpers.stackHeight = function(d,i){
var size = d3.sum(d, function(datum){
return datum.size;
});
return yScale(0) - yScale(size);
};
// Net Positions
// ----------------------------------
helpers.netMidX = function(d,i){
// Mid point of x for passed in datum
// parameters: d {Object} array of stack items
return xScale(d[0].group) + (xScale.rangeBand()/2);
};
helpers.netEndX = function(d,i){
// Mid point of x for passed in datum
// parameters: d {Object} array of stack items
return xScale(d[0].group) + xScale.rangeBand();
};
helpers.netY = function(d,i){
// Returns where the starting y value should be
// parameters: d {Object} array of stack items
var y = yScale(helpers.getGroupSum(d));
// if the net impact is negative, y needs to start at the
// zero line
if(y > helpers.y0){
y = yScale(0);
}
return y;
};
helpers.netHeight = function(d,i){
// Height is calculated by either y0 - y (total height)
// or y - y0
// parameters: d {Object} array of stack items
var y = yScale(helpers.getGroupSum(d));
var height = helpers.y0 - y;
if( y > helpers.y0){
height = y - helpers.y0;
}
return height;
};
helpers.netSumY = function(d){
// returns the y value for sum of net impact of the passed in stack
// parameters: d {Object} array of stack items
// * note * this is called for the net line and net dot to position
// the y value
return yScale(helpers.getGroupSum(d));
};
// Axis location
helpers.yAxisLocation = function(){
return xScale(data[0][0].group) - chartLeftMargin;
};
// Colors
// ----------------------------------
helpers.colorFill = function(d,i){
var color = styles.colors.positive;
if(d instanceof Array){
var sum = helpers.getGroupSum(d);
if(sum<0){ color = styles.colors.negative; }
} else {
if(d.value<0){ color = styles.colors.negative; }
}
return color;
};
helpers.colorStroke = function(d,i){
var color = helpers.colorFill(d,i);
color = d3.rgb(color).hsl().darker();
return color;
};
// Aggregate / Group functions
// ----------------------------------
helpers.getGroupSum = function(d){
// Sum the negative and positives for a passed in stack group
var neg = 0;
var pos = 0;
var len=d.length;
var curVal = 0;
for(var i=0; i<len; i++){
curVal = d[i].value;
if(curVal < 0){ neg += curVal; }
else if(curVal >= 0){ pos += curVal; }
}
return pos + neg;
};
helpers.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
stackRect = origData[i][j];
stackRect.size = Math.abs(stackRect.value);
// If the value is negative, we want to place the bar under
// the 0 line
if (stackRect.value < 0) {
stackRect.y0 = baseNegative;
baseNegative -= stackRect.size;
} else {
basePositive += stackRect.size;
stackRect.y0 = basePositive;
}
}
}
//// To see the format of the data:
//console.log(JSON.stringify(origData, null, 4));
return origData;
};
// ----------------------------------
// Chart Getter / setters
// ----------------------------------
chart.width = function(value) {
if (!arguments.length){ return width; }
width = value;
return chart;
};
chart.height = function(value) {
if (!arguments.length){ return height; }
height = value;
return chart;
};
chart.margin = function(value) {
if (!arguments.length){ return margin; }
margin = value;
return chart;
};
chart.data = function(value) {
if (!arguments.length){ return data; }
data = value;
return chart;
};
chart.duration = function(value) {
if (!arguments.length){ return duration; }
duration= value;
return chart;
};
return chart;
};
// ---------------------------------------------------------------------------
// Create the chart
// ---------------------------------------------------------------------------
var chart = StackedChart({
// pass in some data
data: getData(),
// transition duration
duration: 500
});
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())();
//}, 4000);
.taxes, .services {
stroke: #ffffff;
stroke-width: 2px;
}
/* give outer paths a stroke so there isn't a white border */
.outer path {
stroke: rgba(255,255,255,1);
stroke-width: 1px;
}
.innerActive {
stroke: #343434 !important;
stroke-width: 3px;
}
.bar.positive {
fill: steelblue;
}
.bar.negative {
fill: brown;
}
.axis text {
font: 10px sans-serif;
}
.axis.y path,
.axis.y line {
fill: none;
stroke: #909090;
shape-rendering: crispEdges;
}
.axis.y .tick line {
stroke: #eaeaea;
}
.line {
fill: none;
stroke: #b0b0b0;
stroke-width: 2px;
}
.lineDot {
fill: #ffffff;
stroke: #a0a0a0;
stroke-width: 4px;
}
.netLine {
stroke: #505050;
stroke-width: 1px;
}
.outlineRect {
fill: #f4f4f4;
stroke: #f0f0f0;
}
.netDot {
fill: #cdcdcd;
stroke: #ffffff;
stroke-width: 3px;
}
.zeroLine {
stroke: #343434;
stroke-width: 1px;
}
.annotationText {
font-family: arial;
font-size: 1em;
text-anchor: end;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment