Skip to content

Instantly share code, notes, and snippets.

@jugglinmike
Last active December 19, 2015 19:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jugglinmike/6004102 to your computer and use it in GitHub Desktop.
Save jugglinmike/6004102 to your computer and use it in GitHub Desktop.
Bullet Charts with d3.chart
(function() {
"use strict";
d3.json("bullets.json", function(error, data) {
var myChart = d3.select("body").chart("Bullets", {
seriesCount: data.length
});
myChart.margin({ top: 5, right: 40, bottom: 20, left: 120 })
.width(960)
.height(50)
.duration(1000);
myChart.draw(data);
d3.selectAll("button").on("click", function() {
data.forEach(randomize);
myChart.draw(data);
});
});
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));
};
}
})();
// Chart design based on original implementation by Mike Bostock:
// http://bl.ocks.org/mbostock/4061961
d3.chart("Bullet", {
initialize: function() {
this.xScale = d3.scale.linear();
this.base.classed("bullet", true);
this.titleGroup = this.base.append("g")
.style("text-anchor", "end");
this.title = this.titleGroup.append("text")
.attr("class", "title");
this.subtitle = this.titleGroup.append("text")
.attr("class", "subtitle")
.attr("dy", "1em");
this._margin = { top: 0, right: 0, bottom: 0, left: 0 };
// Default configuration
this.duration(0);
this.markers(function(d) {
return d.markers;
});
this.measures(function(d) {
return d.measures;
});
this.width(380);
this.height(30);
this.tickFormat(this.xScale.tickFormat(8));
this.orient("left"); // TODO top & bottom
this.ranges(function(d) {
return d.ranges;
});
this.layer("ranges", this.base.append("g").classed("ranges", true), {
dataBind: function(data) {
// This layer operates on "ranges" data
data = data.ranges;
return this.selectAll("rect.range").data(data);
},
insert: function() {
return this.append("rect");
},
events: {
enter: function() {
var chart = this.chart();
this.attr("class", function(d, i) { return "range s" + i; })
.attr("width", chart.xScale)
.attr("height", chart.height())
.attr("x", this.chart()._reverse ? chart.xScale : 0);
},
"merge:transition": function() {
var chart = this.chart();
this.duration(chart.duration())
.attr("width", chart.xScale)
.attr("x", chart._reverse ? chart.xScale : 0);
},
exit: function() {
this.remove();
}
}
});
this.layer("measures", this.base.append("g").classed("measures", true), {
dataBind: function(data) {
// This layer operates on "measures" data
data = data.measures;
return this.selectAll("rect.measure").data(data);
},
insert: function() {
return this.append("rect");
},
events: {
enter: function() {
var chart = this.chart();
var hy = chart.height() / 3;
this.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", chart.xScale)
.attr("height", hy)
.attr("x", chart._reverse ? chart.xScale : 0)
.attr("y", hy);
},
"merge:transition": function() {
var chart = this.chart();
this.duration(chart.duration())
.attr("width", chart.xScale)
.attr("x", chart.reverse ? chart.xScale : 0);
}
}
});
this.layer("markers", this.base.append("g").classed("markers", true), {
dataBind: function(data) {
// This layer operates on "markers" data
data = data.markers;
return this.selectAll("line.marker").data(data);
},
insert: function() {
return this.append("line");
},
events: {
enter: function() {
var chart = this.chart();
var height = chart.height();
this.attr("class", "marker")
.attr("x1", chart.xScale)
.attr("x2", chart.xScale)
.attr("y1", height / 6)
.attr("y2", height * 5 / 6);
},
"merge:transition": function() {
var chart = this.chart();
var height = chart.height();
this.duration(chart.duration())
.attr("x1", chart.xScale)
.attr("x2", chart.xScale)
.attr("y1", height / 6)
.attr("y2", height * 5 / 6);
}
}
});
this.layer("ticks", this.base.append("g").classed("ticks", true), {
dataBind: function() {
var format = this.chart().tickFormat();
return this.selectAll("g.tick").data(this.chart().xScale.ticks(8), function(d) {
return this.textContent || format(d);
});
},
insert: function() {
var tick = this.append("g").attr("class", "tick");
var chart = this.chart();
var height = chart.height();
var format = chart.tickFormat();
tick.append("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tick.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1em")
.attr("y", height * 7 / 6)
.text(format);
return tick;
},
events: {
enter: function() {
var chart = this.chart();
this.attr("transform", function(d) {
return "translate(" + chart.xScale(d) + ",0)";
})
.style("opacity", 1e-6);
},
"merge:transition": function() {
var chart = this.chart();
var height = chart.height();
this.duration(chart.duration())
.attr("transform", function(d) {
return "translate(" + chart.xScale(d) + ",0)";
})
.style("opacity", 1);
this.select("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
this.select("text")
.attr("y", height * 7 / 6);
},
"exit:transition": function() {
var chart = this.chart()
this.duration(chart.duration())
.attr("transform", function(d) {
return "translate(" + chart.xScale(d) + ",0)";
})
.style("opacity", 1e-6)
.remove();
}
}
});
d3.timer.flush();
},
transform: function(data) {
// Copy data before sorting
var newData = {
title: data.title,
subtitle: data.subtitle,
randomizer: data.randomizer,
ranges: data.ranges.slice().sort(d3.descending),
measures: data.measures.slice().sort(d3.descending),
markers: data.markers.slice().sort(d3.descending)
};
this.xScale.domain([0, Math.max(newData.ranges[0], newData.measures[0], newData.markers[0])]);
this.title.text(data.title);
this.subtitle.text(data.subtitle);
return newData;
},
// left, right, top, bottom
orient: function(x) {
if (!arguments.length) return this._orient;
this._orient = x;
this._reverse = this._orient == "right" || this._orient == "bottom";
return this;
},
// ranges (bad, satisfactory, good)
ranges: function(x) {
if (!arguments.length) return this._ranges;
this._ranges = x;
return this;
},
// markers (previous, goal)
markers: function(x) {
if (!arguments.length) return this._markers;
this._markers = x;
return this;
},
// measures (actual, forecast)
measures: function(x) {
if (!arguments.length) return this._measures;
this._measures = x;
return this;
},
width: function(x) {
var margin;
if (!arguments.length) {
return this._width;
}
margin = this.margin();
x -= margin.left + margin.right
this._width = x;
this.xScale.range(this._reverse ? [x, 0] : [0, x]);
this.base.attr("width", x);
return this;
},
height: function(x) {
var margin;
if (!arguments.length) {
return this._height;
}
margin = this.margin();
x -= margin.top + margin.bottom;
this._height = x;
this.base.attr("height", x);
this.titleGroup.attr("transform", "translate(-6," + x / 2 + ")");
return this;
},
margin: function(margin) {
if (!margin) {
return this._margin;
}
["top", "right", "bottom", "left"].forEach(function(dimension) {
if (dimension in margin) {
this._margin[dimension] = margin[dimension];
}
}, this);
this.base.attr("transform", "translate(" + this._margin.left + "," +
this._margin.top + ")")
return this;
},
tickFormat: function(x) {
if (!arguments.length) return this._tickFormat;
this._tickFormat = x;
return this;
},
duration: function(x) {
if (!arguments.length) return this._duration;
this._duration = x;
return this;
}
});
// Chart design based on original implementation by Mike Bostock:
// http://bl.ocks.org/mbostock/4061961
d3.chart("Bullets", {
initialize: function(options) {
var mixins = this.mixins = [];
var idx, len, mixin;
if (options && options.seriesCount) {
for (idx = 0, len = options.seriesCount; idx < len; ++idx) {
this._addSeries(idx);
}
}
},
_addSeries: function(idx) {
var mixin = this.mixin("Bullet", this.base.append("svg").append("g"));
// Cache the prototype's implementation of `transform` so that it may
// be invoked from the overriding implementation. This is admittedly a
// bit of a hack, and it may point to a future improvement for d3.chart
var t = mixin.transform;
mixin.transform = function(data) {
return t.call(mixin, data[idx]);
};
this.mixins.push(mixin);
},
width: function(width) {
if (!arguments.length) {
return this._width;
}
this._width = width;
this.base.attr("width", width);
this.base.selectAll("svg").attr("width", width);
this.mixins.forEach(function(mixin) {
mixin.width(width);
});
return this;
},
height: function(height) {
if (!arguments.length) {
return this._height;
}
this._height = height;
this.base.selectAll("svg").attr("height", height);
this.mixins.forEach(function(mixin) {
mixin.height(height);
});
return this;
},
duration: function(duration) {
if (!arguments.length) {
return this._duration;
}
this._duration = duration;
this.mixins.forEach(function(mixin) {
mixin.duration(duration);
});
},
margin: function(margin) {
this.mixins.forEach(function(mixin) {
mixin.margin(margin);
});
return this;
}
});
[
{"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>
<html>
<head>
<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 .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; }
</style>
<script src="http://d3js.org/d3.v3.js"></script>
<script src="http://misoproject.com/js/d3.chart.js"></script>
</head>
<body>
<button>Update</button>
<script src="bullet-chart.js"></script>
<script src="bullets-chart.js"></script>
<script src="bullet-app.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment