[ Launch: treemap ] 5513664 by enoex
See Previous Inlet [ Gist ]
-
-
Save erikhazzard/5513664 to your computer and use it in GitHub Desktop.
treemap - stacked bars - custom layout
This file contains 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
{"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} |
This file contains 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
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