Skip to content

Instantly share code, notes, and snippets.

@erikhazzard
Created May 3, 2013 20:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erikhazzard/5513664 to your computer and use it in GitHub Desktop.
Save erikhazzard/5513664 to your computer and use it in GitHub Desktop.
treemap - stacked bars - custom layout
{"editor_editor":{"coffee":false,"vim":false,"emacs":false,"width":600,"height":300,"hide":false},"description":"treemap - stacked bars - custom layout","endpoint":"","display":"svg","public":true,"require":[],"fileconfigs":{"_.md":{"default":true,"vim":false,"emacs":false,"fontSize":12},"config.json":{"default":true,"vim":false,"emacs":false,"fontSize":12},"inlet.js":{"default":true,"vim":false,"emacs":false,"fontSize":12},"inlet.svg":{"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}
var stackedData = {
"effects": {
"taxes": [
{
"new": -200,
"baseline": -400,
"title": "income-tax"
},
{
"new": -10,
"baseline": -30,
"title": "capital-gains-tax"
},
{
"new": 300,
"baseline": -100,
"title": "sales-tax"
},
{
"new": 200,
"baseline": 0,
"title": "gas-tax"
}
],
"spending": [
{
"new": 800,
"baseline": 0,
"title": "education"
},
{
"new": -800,
"baseline": 0,
"title": "education2"
}
]
}
};
var processData = function(inputData){
//Takes in data in the format of
// { effects:
// {
// taxes: [
// {new: x, baseline: y, title: 'title'}, ...
// ],
// effects: [
// {new: x, baseline: y, title: 'title'}, ...
// ]
// }
// }
//
//Returns data in format of
// {
// totals: {
// total: taxes.total + spending.total,
// magnitude: taxes.totalMagnitude + spending.totalMagnitude,
// },
//
// taxes: {
// totals: {
// total: ∑ positive + ∑ negative,
// magnitude: ∑ positive + ∑ Math.abs(negative),
//
// positive: ∑ positive,
// positiveMagnitude: ∑ Math.absolute(positive),
//
// negative: ∑ negative,
// negativeMagnitude: ∑ Math.absolute(negative),
// }
//
// positive: [{
// title: 'title',
// magnitude: Math.abs(new - baseline),
// value: new - baseline
// }, ... ],
// negative: [{
// title: 'title',
// magnitude: Math.abs(new - baseline),
// value: new - baseline
// }, ...]
// },
//
// spending:
// SAME STRUCTURE AS TAXES
// }
//Setup base structure
var outputData = {
totals: {
total: 0,
magnitude: 0
},
taxes: {
totals: {
total: 0,
magnitude: 0,
positive: 0,
positiveMagnitude: 0,
negative: 0,
negativeMagnitude: 0
},
positive: [],
negative: []
},
spending: {
totals: {
total: 0,
magnitude: 0,
positive: 0,
positiveMagnitude: 0,
negative: 0,
negativeMagnitude: 0
},
positive: [],
negative: []
}
};
var category = '', iCategory;
for(iCategory=0; iCategory<2; iCategory++){
//get current category
category = ['spending', 'taxes'][iCategory];
//category is either 'spending' or 'taxes'
var i=0, len, curItem, curValue, curMagnitude, itemKey;
//Parse the input data
for(i=0, len=inputData.effects[category].length; i<len; i++){
//Look at the new - baseline to get the magnitude and value
curItem = inputData.effects[category][i];
//Get value, update totals
curValue = curItem['new'] - curItem.baseline;
curMagnitude = Math.abs(curValue);
//total
outputData.totals.total += curValue;
outputData.totals.magnitude += curMagnitude;
//category
outputData[category].totals.total += curValue;
outputData[category].totals.magnitude += curMagnitude;
//Positive / Negative items
itemKey = 'positive';
if(curValue < 0){
itemKey = 'negative';
}
//Add actual item
outputData[category][itemKey].push({
value: curValue,
magnitude: curMagnitude,
title: curItem.title,
newValue: curItem['new'],
baselineValue: curItem.baseline
});
//update totals
outputData[category].totals[itemKey] += curValue;
outputData[category].totals[itemKey + 'Magnitude'] += curMagnitude;
}
}
//run calculation on taxes and spending
return outputData;
};
//---------------------------------------
//Some config vars
//---------------------------------------
var DURATION = 1500;
var svgWidth = 940,
svgHeight = 560;
var padding = {
bottom: 100,
left: 100,
right: 0,
top: 100
};
var width = svgWidth - (padding.left + padding.right),
height = svgHeight- (padding.top + padding.bottom);
//---------------------------------------
//Setup svg
//---------------------------------------
var chart = d3.select('svg').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight)
.append('g')
.attr({
'class': 'impactGroup',
transform: 'translate(' + [
padding.left, padding.top
] + ')'
});
//---------------------------------------
//setup total bar width / height scale
//---------------------------------------
//NOTE: These domains will be updated
var totalXScaleTaxes = d3.scale.linear()
.domain([0, 1000])
.rangeRound([0, height - 10]);
var totalXScaleSpending = d3.scale.linear()
.domain([0, 1000])
.rangeRound([0, height - 10]);
var totalYScale = d3.scale.linear()
.domain([0, 10])
.rangeRound([0, width])
.clamp(true);
//---------------------------------------
//Color config
//---------------------------------------
var colorConfig = {
positive: ['#aed8ca', '#6dbaac'],
negative: ['#efc5b0', '#e37e4c']
};
var yPadding = 4;
var xPadding = 2;
//=======================================
//
//Setup stack objects
//
//=======================================
//Base stack object
var StackObject = function(options){
options = options || {};
//default options
this.width = 0;
this.height = 0;
//used to store the previous transition height
this.previousHeight = 0;
this.sign = 'positive'; // or 'negative'
this.category = '';
//extend / override properties based on passed in options
_.extend(this, options);
return this;
};
//position / size functions
StackObject.prototype.getHeight = function(){
return totalYScale(this.height);
};
StackObject.prototype.getY = function(){
return 0;
};
StackObject.prototype.getWidth = function(){
//NOTE: This must be overriden
return totalXScaleTaxes(this.width);
};
StackObject.prototype.getX = function(){
return 0;
};
StackObject.prototype.color = function(){
return d3.scale.linear()
.clamp(true)
//TODO: use data max
.domain([0, 1000])
.range(colorConfig[this.sign]);
};
//---------------------------------------
//TAXES
//---------------------------------------
var stackObjects = {};
stackObjects.TaxesPositive = new StackObject({
category: 'taxes',
group: chart.append('svg:g').attr({
'class': 'groupTaxesPositive'
}),
getWidth: function(){
return totalXScaleTaxes(this.width);
},
//NOTE: Put this in the negative section to show negatives on the bottom
getX: function(){
return stackObjects.TaxesNegative.getWidth() + xPadding;
},
//width - NOTE:these will be updated based on data
height: 400,
width: 700
});
stackObjects.TaxesNegative = new StackObject({
category: 'taxes',
group: chart.append('svg:g').attr({
'class': 'groupTaxesNegative',
transform: 'translate(0, 205)'
}),
sign: 'negative',
getWidth: function(){
return totalXScaleTaxes(this.width);
},
height: 600,
width: 300
});
//---------------------------------------
//SPENDING
//---------------------------------------
stackObjects.SpendingPositive = new StackObject({
category: 'spending',
group: chart.append('svg:g').attr({
'class': 'group2',
transform: 'translate(0, 205)'
}),
getWidth: function(){
return totalXScaleSpending(this.width);
},
//width
height: 600,
width: 600,
//note: put this in negative to show negative as primary
getY: function(){
return stackObjects.TaxesPositive.getHeight() + yPadding;
},
getX: function(){
return stackObjects.SpendingNegative.getWidth() + xPadding;
}
});
stackObjects.SpendingNegative = new StackObject({
category: 'spending',
group: chart.append('svg:g').attr({
'class': 'group2',
transform: 'translate(0, 205)'
}),
sign: 'negative',
getWidth: function(){
return totalXScaleSpending(this.width);
},
//width
height: 600,
width: 400,
//note: put in positive to show positive as secondary
getY: function(){
return stackObjects.TaxesPositive.getHeight() + yPadding;
}
});
//=======================================
//Side bar setup
//=======================================
var sideBracketGroup = chart.append('svg:g').attr({
'class': 'sideBracket',
transform: 'translate(-10, 0)'
});
//=======================================
//
//Update / Draw stacked bar
//
//=======================================
function updateStack(stackObject, data, rawData) {
//Draws or updates the stacked bars
// parameters:
// stackObject: {Object} containing properties of the stack bar
// data: {Object} data for the stacked bar
//
//NOTE: y0 is the previous y value, y1 is the current data value
//setup scales
var yScale = d3.scale.linear()
.rangeRound([stackObject.getWidth(), 0]);
var color = stackObject.color();
var stackGroup = stackObject.group;
var total = 0;
//Setup stacked data
var y0 = 0;
_.each(data, function(d) {
d.y0 = y0;
d.y1 = y0 += +d.magnitude;
total += d.y1;
});
data.sort(function(a, b) { return b.total - a.total; });
//Setup scale domains
yScale.domain([0,
rawData[stackObject.category].totals[stackObject.sign + 'Magnitude']]);
//get height of the bars (based on spending - taxes)
var barHeight = stackObject.getHeight();
//update y position of group
stackObject.group.transition().duration(DURATION).attr({
transform: 'translate(' + [
stackObject.getY(),
stackObject.getX()
] + ')'
});
// DATA JOIN
// Join new data with old elements, if any.
var policy = stackGroup.selectAll('.policy')
.data(data);
//flip it
//stackGroup.attr({ transform: 'translate(' + [width, 0] + ') scale(-1 1)' });
//enter
//-----------------------------------
policy.enter().append('rect').attr({
'class': 'policy',
'y': height,
'width': stackObject.previousHeight
}).style({
opacity: 1,
'fill': function(d,i) {
return color(d.y1);
},
stroke: '#ffffff'
}).on('mouseover', function(d,i){
console.log(d);
d3.select(this).style({
fill: d3.rgb(color(d.y1)).hsl().brighter(0.7)
});
}).on('mouseout', function(d,i){
d3.select(this).style({
fill: color(d.y1)
});
});
//update
policy.style({
opacity: 1
})
.transition()
.duration(DURATION)
.attr({
'width': barHeight,
'y': function(d) { return yScale(d.y1); },
'height': function(d) { return yScale(d.y0) - yScale(d.y1); }
}).style({
'fill': function(d,i) {
return color(d.y1);
}
});
//EXIT
policy.exit()
.transition()
.duration(DURATION)
.attr({
'height': barHeight,
'width': function(d) {
return 0;
},
'x': function(d) {
return 0;
}
}).style({
opacity: 0.1
})
.remove();
//Update the previous height
stackObject.previousHeight = barHeight;
return true;
}
//=======================================
//
//Update / Draw stacked bar
//
//=======================================
var updateSideBracket = function(data){
//This draws and updates the side bars next to the chart, showing revenue
// and spending titles
var titles = ['Revenue', 'Spending'];
//-----------------------------------
// brackets
//-----------------------------------
var bracketGroup = sideBracketGroup.selectAll('.bracketGroup')
.data(data);
bracketGroup.enter()
.append('svg:g').attr({ 'class': 'bracketGroup' });
//Get y1 and y2 positions
var getY1 = function(d,i){
//Update y vals based on index
var y = 0;
if(i===1){ y = totalYScale(data[0]) + yPadding;}
return y;
};
var getY2 = function(d,i){
var y = totalYScale(data[0]);
if(i===1){ y = totalYScale(data[0]) + totalYScale(d) + yPadding; }
return y;
};
//-----------------------------------
//bracket - horizontal enter
//-----------------------------------
var bracketStyle = {
fill: 'none',
opacity: 1,
'stroke-width': '2px',
stroke: '#343434'
};
var bracketWidth = 14;
var bracket = bracketGroup.selectAll('.yBracket')
.data(data);
//draw line - main bar ( | )
bracket.enter().append('svg:line')
.attr({
'class': 'yBracket',
y1: 0,
y2: 20,
x1: 0,
x2: 0
}).style(bracketStyle);
//update
bracket.transition()
.duration(DURATION)
.attr({
y1: getY1,
y2: getY2
});
//EXIT
bracket.exit().remove();
//-----------------------------------
//bracket - horizontal top tick
//-----------------------------------
var bracketX1 = bracketGroup.selectAll('.x1Bracket')
.data(data);
//draw line - main bar ( | )
bracketX1.enter().append('svg:line')
.attr({
'class': 'x1Bracket',
y1: 0,
y2: 0,
x1: -1,
x2: bracketWidth
}).style(bracketStyle);
//update
bracketX1.transition()
.duration(DURATION)
.attr({
y1: getY1,
y2: getY1
});
//EXIT
bracket.exit().remove();
//-----------------------------------
//text
//-----------------------------------
var bracketText = bracketGroup.selectAll('.bracketText')
.data(data);
bracketText.enter().append('text').attr({
'class': 'bracketText',
y: function(d,i){
var y = 0;
if(i===1){ y = totalYScale(data[0]) + yPadding; }
return y + ( totalYScale(d) / 2 ) - 4;
},
x: function(d,i){
return (((titles[i] + '').length * -3) / 2) - 60;
}
}).text(function(d,i){
return titles[i];
});
bracketText.style({
fill: '#a0a0a0',
opacity: 1
})
.transition()
.duration(DURATION)
.attr({
y: function(d,i){
var y = 0;
if(i===1){ y = totalYScale(data[0]) + yPadding; }
return y + ( totalYScale(d) / 2 ) + 7;
}
});
bracketText.exit().remove();
};
//=======================================
//
//Setup chart
//
//=======================================
//Draw and update two bars
var drawBars = function(){
//Set 'global' data props
var data = processData(stackedData);
//set up total Y value
var yTotal = data.totals.magnitude;
totalYScale.domain([0, yTotal]);
//set heights for bars
stackObjects.TaxesPositive.height = data.taxes.totals.magnitude;
stackObjects.TaxesNegative.height = data.taxes.totals.magnitude;
stackObjects.SpendingPositive.height = data.spending.totals.magnitude;
stackObjects.SpendingNegative.height = data.spending.totals.magnitude;
//set widths
totalXScaleSpending.domain([0, data.spending.totals.magnitude]);
totalXScaleTaxes.domain([0, data.taxes.totals.magnitude]);
//spending
stackObjects.SpendingPositive.width = data.spending.totals.positiveMagnitude;
stackObjects.SpendingNegative.width = data.spending.totals.positiveMagnitude;
//taxes
stackObjects.TaxesPositive.width = data.taxes.totals.positiveMagnitude;
stackObjects.TaxesNegative.width = data.taxes.totals.negativeMagnitude;
//Update stacked bar - dummy data
var updateStackedBar = function(stack){
updateStack(
stack,
_.clone(data[stack.category][stack.sign]),
data
);
};
//Draw a all the stacked bars
updateStackedBar(stackObjects.SpendingPositive);
updateStackedBar(stackObjects.SpendingNegative);
updateStackedBar(stackObjects.TaxesPositive);
updateStackedBar(stackObjects.TaxesNegative);
//Draw the side bars
updateSideBracket([ data.taxes.totals.magnitude, data.spending.totals.magnitude ]);
};
drawBars();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment