Skip to content
This repository has been archived by the owner on Nov 7, 2018. It is now read-only.

Improvements and docs for bullet charts #104

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions bullet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,75 @@ Implementation based on work by
[Clint Ivy](http://projects.instantcognition.com/protovis/bulletchart/),
Jamie Love of [N-Squared Software](http://www.nsquaredsoftware.com/) and
[Jason Davies](http://www.jasondavies.com/).

##Documentation

A bullet chart consists of _ranges_, _markers_, and _measures_. A range
indicates the satisfaction of a measure and is shown as the background colors,
typically shades of gray. A marker indicates a target or previous value and is
shown as a vertical bar. A measure is the actual value being shown and is the
central bar, typically blue. Bullet charts also have an
[axis](https://github.com/mbostock/d3/wiki/SVG-Axes). Titles and subtitles are
not automatically generated.

Currently, the range of values must start at 0 and may not include negative numbers.

<a name="bullet" href="#bullet">#</a> d3.<b>bullet</b>()
Create a new bullet chart.

<a name="_bullet" href="#_bullet">#</a> <b>bullet</b>(<i>selection</i>)
Apply the axis to a selection or transition. The selection must contain an
`svg` or `g` element. Typically passed as the argument to
[selection.call](https://github.com/mbostock/d3/wiki/Selections#call).

<a name="orient" href="#orient">#</a> bullet.<b>orient</b>([<i>orientation</i>])
Get or set the orientation of the bullet chart. The supported orientations are:
`"top"`, `"bottom"`, `"left"`, and `"right"`. The default orientation is `"left"`.

<a name="width" href="#width">#</a> bullet.<b>width</b>([<i>width</i>])
<a name="height" href="#height">#</a> bullet.<b>height</b>([<i>height</i>])
Get or set the width or height of the bullet chart, not including the axis.
Width and height are always horizontal and vertical, respectively, regardless
of _orient_. The default width is 380 and default height is 30.

<a name="ranges" href="#ranges">#</a> bullet.<b>ranges</b>([<i>ranges</i>])
Get or set the ranges of the bullet chart. _ranges_ should be an array of
thresholds between ranges (the first range implicitly starts at 0), or a
function that returns the array. The function will be passed the current datum
`d` and the current index `i`, with the `this` context as the current DOM
element. The default is a function that returns `d.ranges`.

Ranges are rendered as `rect` elements with the `range` class and two others.
The index is given by `s0`, `s1`, and so on. `s0` is always the largest range
and has the lowest z-order. If there are three ranges, all three will have the
`t3` class, indicating the total number of ranges. This is useful for styling
bullet charts with varying numbers of ranges with different gradients.

Stephen Few recommends at most five ranges, and ideally three.

<a name="markers" href="#markers">#</a> bullet.<b>markers</b>([<i>markers</i>])
Get or set the markers of the bullet chart. _markers_ should be an array of
marker values, or a function that returns the array. The function will be
passed the current datum `d` and the current index `i`, with the `this` context
as the current DOM element. The default is a function that returns `d.markers`.

Markers are rendered as `line` elements with the `marker` class and another
indicating the index in the original data: `s0`, `s1`, and so on.

Stephen Few suggests that multiple markers can be used to indicate the target
value and a prior comparable value, e.g. from last year. He advises against
more than two markers, and that one is usually sufficient.

<a name="measures" href="#measures">#</a> bullet.<b>measures</b>([<i>measures</i>])
Get or set the measures of the bullet chart. _measures_ should be an array of
values, or a function that returns the array. The function will be passed the
current datum `d` and the current index `i`, with the `this` context as the
current DOM element. The default is a function that returns `d.measures`.

Measures are rendered as `rect` elements with the `measure` class and another
indicating the index: `s0`, `s1`, and so on. `s0` always corresponds to the
first measure in the original array, even though the bars will be z-ordered to
put the largest on the bottom.

The first measure is the primary datapoint displayed by the chart. Stephen Few
recommends a second measure for a forecasted value, if appropriate.
72 changes: 33 additions & 39 deletions bullet/bullet.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@ d3.bullet = function() {
var orient = "left",
reverse = false,
vertical = false,
ranges = bulletRanges,
markers = bulletMarkers,
measures = bulletMeasures,
ranges = function(d){return d.ranges},
markers = function(d){return d.markers},
measures = function(d){return d.measures},
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),
var rangez = typeof ranges === "function" ? ranges.call(this, d, i) : ranges,
markerz = typeof markers === "function" ? markers.call(this, d, i) : markers,
measurez = typeof measures === "function" ? measures.call(this, d, i) : measures,
g = d3.select(this),
extentX,
extentY;

rangez = rangez.slice().sort(d3.descending);
markerz = markerz.slice();
measurez = measurez.slice();

var wrap = g.select("g.wrap");
if (wrap.empty()) wrap = g.append("g").attr("class", "wrap");

Expand All @@ -32,12 +36,12 @@ d3.bullet = function() {
wrap.attr("transform", "rotate(90)translate(0," + -width + ")");
} else {
extentX = width, extentY = height;
wrap.attr("transform", "translate(0)");
wrap.attr("transform", null);
}

// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain([0, Math.max(rangez[0], markerz[0], measurez[0])])
.domain([0, Math.max(rangez[0], d3.max(markerz), d3.max(measurez))])
.range(reverse ? [extentX, 0] : [0, extentX]);

// Retrieve the old x-scale, if this is an update.
Expand All @@ -57,7 +61,7 @@ d3.bullet = function() {
.data(rangez);

range.enter().append("rect")
.attr("class", function(d, i) { return "range s" + i; })
.attr("class", function(d, i) { return "range s"+i+" t"+range.size(); })
.attr("width", w0)
.attr("height", extentY)
.attr("x", reverse ? x0 : 0)
Expand All @@ -67,29 +71,12 @@ d3.bullet = function() {
.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("class", function(d, i) { return "marker s"+i; })
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", extentY / 6)
Expand All @@ -101,8 +88,27 @@ d3.bullet = function() {
.attr("y1", extentY / 6)
.attr("y2", extentY * 5 / 6);

// 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; })
.sort(d3.descending)
.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);

var axis = g.selectAll("g.axis").data([0]);
axis.enter().append("g").attr("class", "axis");
if (!vertical) axis.attr("transform", "translate(0,"+extentY+")")
axis.call(xAxis.scale(x1));
});
d3.timer.flush();
Expand Down Expand Up @@ -153,18 +159,6 @@ d3.bullet = function() {
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) {
Expand Down