Skip to content

Instantly share code, notes, and snippets.

@jasondavies
Forked from mbostock/.block
Last active Jun 13, 2019
Embed
What would you like to do?
Vertical Bullet Charts
(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",
reverse = false,
vertical = false,
ranges = bulletRanges,
markers = bulletMarkers,
measures = bulletMeasures,
width = 380,
height = 30,
xAxis = d3.svg.axis();
// 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),
extentX,
extentY;
var wrap = g.select("g.wrap");
if (wrap.empty()) wrap = g.append("g").attr("class", "wrap");
if (vertical) {
extentX = height, extentY = width;
wrap.attr("transform", "rotate(90)translate(0," + -width + ")");
} else {
extentX = width, extentY = height;
wrap.attr("transform", null);
}
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain([0, Math.max(rangez[0], markerz[0], measurez[0])])
.range(reverse ? [extentX, 0] : [0, extentX]);
// 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 = wrap.selectAll("rect.range")
.data(rangez);
range.enter().append("rect")
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w0)
.attr("height", extentY)
.attr("x", reverse ? x0 : 0)
d3.transition(range)
.attr("x", reverse ? x1 : 0)
.attr("width", w1)
.attr("height", extentY);
// Update the measure rects.
var measure = wrap.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w0)
.attr("height", extentY / 3)
.attr("x", reverse ? x0 : 0)
.attr("y", extentY / 3);
d3.transition(measure)
.attr("width", w1)
.attr("height", extentY / 3)
.attr("x", reverse ? x1 : 0)
.attr("y", extentY / 3);
// Update the marker lines.
var marker = wrap.selectAll("line.marker")
.data(markerz);
marker.enter().append("line")
.attr("class", "marker")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", extentY / 6)
.attr("y2", extentY * 5 / 6);
d3.transition(marker)
.attr("x1", x1)
.attr("x2", x1)
.attr("y1", extentY / 6)
.attr("y2", extentY * 5 / 6);
var axis = g.selectAll("g.axis").data([0]);
axis.enter().append("g").attr("class", "axis");
axis.attr("transform", vertical ? null : "translate(0," + extentY + ")")
.call(xAxis.scale(x1));
});
d3.timer.flush();
}
// left, right, top, bottom
bullet.orient = function(_) {
if (!arguments.length) return orient;
orient = _ + "";
reverse = orient == "right" || orient == "bottom";
xAxis.orient((vertical = orient == "top" || orient == "bottom") ? "left" : "bottom");
return bullet;
};
// ranges (bad, satisfactory, good)
bullet.ranges = function(_) {
if (!arguments.length) return ranges;
ranges = _;
return bullet;
};
// markers (previous, goal)
bullet.markers = function(_) {
if (!arguments.length) return markers;
markers = _;
return bullet;
};
// measures (actual, forecast)
bullet.measures = function(_) {
if (!arguments.length) return measures;
measures = _;
return bullet;
};
bullet.width = function(_) {
if (!arguments.length) return width;
width = +_;
return bullet;
};
bullet.height = function(_) {
if (!arguments.length) return height;
height = +_;
return bullet;
};
return d3.rebind(bullet, xAxis, "tickFormat");
};
function bulletRanges(d) {
return d.ranges;
}
function bulletMarkers(d) {
return d.markers;
}
function bulletMeasures(d) {
return d.measures;
}
function bulletWidth(x) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d) - x0);
};
}
})();
[
{"title":"Revenue","subtitle":"US$, in thousands","ranges":[150,225,300],"measures":[220,270],"markers":[250]},
{"title":"Profit","subtitle":"%","ranges":[20,25,30],"measures":[21,23],"markers":[26]},
{"title":"Order Size","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":"Satisfaction","subtitle":"out of 5","ranges":[3.5,4.25,5],"measures":[3.2,4.7],"markers":[4.4]}
]
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
padding-top: 40px;
position: relative;
width: 960px;
}
button {
position: absolute;
right: 10px;
top: 10px;
}
.bullet { font: 10px sans-serif; }
.bullet .marker { stroke: #000; stroke-width: 2px; }
.bullet .axis line, .bullet .axis path { stroke: #666; stroke-width: .5px; fill: none; }
.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; }
</style>
<button>Update</button>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="bullet.js"></script>
<script>
var margin = {top: 5, right: 40, bottom: 50, left: 120},
width = 192 - margin.left - margin.right,
height = 450 - margin.top - margin.bottom;
var chart = d3.bullet()
.orient("bottom")
.width(width)
.height(height);
d3.json("bullets.json", function(error, data) {
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "bullet")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart);
var title = svg.append("g")
.style("text-anchor", "end")
.attr("transform", "translate(" + width + "," + (height + 20) + ")");
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
title.append("text")
.attr("class", "subtitle")
.attr("dy", "1em")
.text(function(d) { return d.subtitle; });
d3.selectAll("button").on("click", function() {
svg.datum(randomize).transition().duration(1000).call(chart);
});
});
function randomize(d) {
if (!d.randomizer) d.randomizer = randomizer(d);
d.ranges = d.ranges.map(d.randomizer);
d.markers = d.markers.map(d.randomizer);
d.measures = d.measures.map(d.randomizer);
return d;
}
function randomizer(d) {
var k = d3.max(d.ranges) * .2;
return function(d) {
return Math.max(0, d + k * (Math.random() - .5));
};
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment