Skip to content

Instantly share code, notes, and snippets.

@ramnathv
Last active August 29, 2015 14:09
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 ramnathv/2434c62872af2b2e6495 to your computer and use it in GitHub Desktop.
Save ramnathv/2434c62872af2b2e6495 to your computer and use it in GitHub Desktop.
Financial Statement Bullet Graph
.link {
fill: none;
stroke: #aaa;
stroke-opacity: 0.6;
shape-rendering: crispEdges;
}
.bullet { font: 10px sans-serif; }
.bullet .marker { stroke: #000; stroke-width: 2px; }
.bullet .tick line { stroke: #666; stroke-width: .5px; }
.bullet .range.s0 { fill: #eee; }
.bullet .range.s1 { fill: #ddd; }
.bullet .range.s2 { fill: #ccc; }
.bullet .measure.s0 { fill: lightsteelblue; }
.bullet .measure.s1 { fill: steelblue; }
.bullet .title { font-size: 14px; font-weight: bold; }
.bullet .subtitle { fill: #999; }
.title {
font-weight: bold;
font-family: "Helvetica";
}
(function() {
// Chart design based on the recommendations of Stephen Few. Implementation
// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
// http://projects.instantcognition.com/protovis/bulletchart/
d3.bullet = function() {
var orient = "left", // TODO top & bottom
reverse = false,
duration = 0,
ranges = bulletRanges,
markers = bulletMarkers,
measures = bulletMeasures,
width = 380,
height = 30,
tickFormat = null;
ticks = 3;
// For each small multiple…
function bullet(g) {
g.each(function(d, i) {
var rangez = ranges.call(this, d, i).slice().sort(d3.descending),
markerz = markers.call(this, d, i).slice().sort(d3.descending),
measurez = measures.call(this, d, i).slice().sort(d3.descending),
g = d3.select(this);
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain([0, Math.max(rangez[0], markerz[0], measurez[0])])
.range(reverse ? [width, 0] : [0, width]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
.range(x1.range());
// Stash the new scale.
this.__chart__ = x1;
// Derive width-scales from the x-scales.
var w0 = bulletWidth(x0),
w1 = bulletWidth(x1);
// Update the range rects.
var range = g.selectAll("rect.range")
.data(rangez);
range.enter().append("rect")
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w0)
.attr("height", height)
.attr("x", reverse ? x0 : 0)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
range.transition()
.duration(duration)
.attr("x", reverse ? x1 : 0)
.attr("width", w1)
.attr("height", height);
// Update the measure rects.
var measure = g.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w0)
.attr("height", height / 3)
.attr("x", reverse ? x0 : 0)
.attr("y", height / 3)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
measure.transition()
.duration(duration)
.attr("width", w1)
.attr("height", height / 3)
.attr("x", reverse ? x1 : 0)
.attr("y", height / 3);
// Update the marker lines.
var marker = g.selectAll("line.marker")
.data(markerz);
marker.enter().append("line")
.attr("class", "marker")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", height / 6)
.attr("y2", height * 5 / 6)
.transition()
.duration(duration)
.attr("x1", x1)
.attr("x2", x1);
marker.transition()
.duration(duration)
.attr("x1", x1)
.attr("x2", x1)
.attr("y1", height / 6)
.attr("y2", height * 5 / 6);
// Compute the tick format.
var format = tickFormat || x1.tickFormat(8);
// Update the tick groups.
var tick = g.selectAll("g.tick")
.data(x1.ticks(ticks), function(d) {
return this.textContent || format(d);
});
// Initialize the ticks with the old scale, x0.
var tickEnter = tick.enter().append("g")
.attr("class", "tick")
.attr("transform", bulletTranslate(x0))
.style("opacity", 1e-6);
tickEnter.append("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickEnter.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1em")
.attr("y", height * 7 / 6)
.text(format);
// Transition the entering ticks to the new scale, x1.
tickEnter.transition()
.duration(duration)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1);
// Transition the updating ticks to the new scale, x1.
var tickUpdate = tick.transition()
.duration(duration)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1);
tickUpdate.select("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickUpdate.select("text")
.attr("y", height * 7 / 6);
// Transition the exiting ticks to the new scale, x1.
tick.exit().transition()
.duration(duration)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1e-6)
.remove();
});
d3.timer.flush();
}
// left, right, top, bottom
bullet.orient = function(x) {
if (!arguments.length) return orient;
orient = x;
reverse = orient == "right" || orient == "bottom";
return bullet;
};
// ranges (bad, satisfactory, good)
bullet.ranges = function(x) {
if (!arguments.length) return ranges;
ranges = x;
return bullet;
};
// markers (previous, goal)
bullet.markers = function(x) {
if (!arguments.length) return markers;
markers = x;
return bullet;
};
// measures (actual, forecast)
bullet.measures = function(x) {
if (!arguments.length) return measures;
measures = x;
return bullet;
};
bullet.width = function(x) {
if (!arguments.length) return width;
width = x;
return bullet;
};
bullet.height = function(x) {
if (!arguments.length) return height;
height = x;
return bullet;
};
bullet.tickFormat = function(x) {
if (!arguments.length) return tickFormat;
tickFormat = x;
return bullet;
};
bullet.ticks = function(x) {
if (!arguments.length) return ticks;
ticks = x;
return bullet;
};
bullet.duration = function(x) {
if (!arguments.length) return duration;
duration = x;
return bullet;
};
return bullet;
};
function bulletRanges(d) {
return d.ranges;
}
function bulletMarkers(d) {
return d.markers;
}
function bulletMeasures(d) {
return d.measures;
}
function bulletTranslate(x) {
return function(d) {
return "translate(" + x(d) + ",0)";
};
}
function bulletWidth(x) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d) - x0);
};
}
})();
{"description":"Financial Statement Bullet Graph","endpoint":"","display":"div","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},"app.css":{"default":true,"vim":false,"emacs":false,"fontSize":12},"bullets.js":{"default":true,"vim":false,"emacs":false,"fontSize":12}},"fullscreen":false,"play":false,"loop":false,"restart":false,"autoinit":true,"pause":true,"loop_type":"pingpong","bv":false,"nclones":15,"clone_opacity":0.4,"duration":3000,"ease":"linear","dt":0.01,"ajax-caching":true,"thumbnail":"http://i.imgur.com/nHiP26s.png"}
function make_tree(x, root){
function isObject(item) {
return (typeof item === "object" && !Array.isArray(item) && item !== null);
}
// modified version of d3.entries
function make_children(n){
return Object.keys(n).map(function(e){
return {name: e, children: n[e]}
})
}
function to_tree(x){
x.forEach(function(x_){
if (isObject(x_.children) && !x_.children.isleaf){
x_.children = to_tree(make_children(x_.children))
} else {
x_.value = x_.children
delete(x_.children)
}
return x
})
return x
}
return {
name: root,
children: to_tree(make_children(x))
}
}
function elbow(d) {
return "M" + d.source.y + "," + d.source.x
+ "H" + d.target.y + "V" + d.target.x
+ (d.target.children ? "" : "h" + 60);
}
function elbow2(d, offset) {
return "M" + (offset - d.source.y) + "," + d.source.x
+ "H" + (offset - d.target.y) + "V" + d.target.x
+ (d.target.children ? "" : "h" + -20);
}
var data2 = {
"Gross Profit":{
"Gross Sales": {x: 1, y: 2, isleaf: true},
"COGS": {y: 3, isleaf: true}
},
"Total Expenses":{
"Selling Expenses":1,
"G&A": 4
}
}
var data = {
"name": "A",
"children": [
{"name": "B"},
{"name": "C",
"children": [
{"name": "D"},
{"name": "E"}
]
}
]
}
data = make_tree(data2, "Income")
display = d3.select("#display")
display.append("h4")
.style("margin-left", "82px")
.text("Financial Statement Bullet Chart")
/* Figure out width, height, dimensions settings */
var canvas = d3.select("#display").append("svg")
.attr("width", 600)
.attr("height", 500)
.append("g")
.attr("transform", "translate(420, 0)")
var tree = d3.layout.tree()
.size([450, 350])
var nodes = tree.nodes(data).reverse()
//console.log(nodes)
data3 = [
{"title":"Gross Sales","subtitle":"US$, in thousands","ranges":[200,250,300],"measures":[260,270],"markers":[250]},
{"title":"Profit","subtitle":"%","ranges":[20,25,30],"measures":[21,23],"markers":[26]},
{"title":"Interest and Taxes","subtitle":"US$, average","ranges":[350,500,600],"measures":[100,320],"markers":[550]},
{"title":"New Customers","subtitle":"count","ranges":[1400,2000,2500],"measures":[1000,1650],"markers":[2100]},
{"title":"Gross Profit","subtitle":"out of 5","ranges":[3.5,4.25,5],"measures":[3.2,4.7],"markers":[4.4]},
{"title":"Gross Income","subtitle":"out of 5","ranges":[3.5,4.25,5],"measures":[3.2,4.7],"markers":[4.4]},
{"title":"Net Income","subtitle":"count","ranges":[1400,2000,2500],"measures":[1000,1650],"markers":[2100]}
]
nodes = nodes.map(function(d, i){
d.bullet = data3[i]
return d
})
var links = tree.links(nodes)
console.log(nodes)
var node = canvas.selectAll(".node")
.data(nodes, function(d){return d.name}).enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d){
return "translate(" + (- d.y) + "," + d.x + ")"
})
/*
node.append("circle")
.attr("cx", f("y"))
.attr("cy", f("x"))
.attr("r", 5)
*/
/*
var diagonal = d3.svg.diagonal()
.projection(function(d){return [339 - d.y, d.x]})
*/
var chart = d3.bullet()
.width(135)
.height(20)
.ticks(5)
//.duration(750)
var link = canvas.selectAll(".link")
.data(links).enter()
.append("path")
.attr("class", "link")
.attr("d", function(d){return elbow2(d, chart.width())})
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1)
function f(x){
return function(d){
return d[x]
}
}
canvas.selectAll(".node")
//.data(data3)
.append("text")
.attr("transform", "translate(12, -16)")
.attr("class", "title")
.html(function(d, i){return d.name})
.style("font-size", "12px")
canvas.selectAll(".node")
.data(data3)
.append("g")
.attr("transform", "translate(12, -10)")
.attr("class", "bullet")
.each(function(d, i){
d3.select(this).call(chart.ticks(4))
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment