From 5426daa757be7cdc79228f3bcd85bf5d18061176 Mon Sep 17 00:00:00 2001 From: Max Goldstein Date: Sun, 31 Aug 2014 17:55:45 -0400 Subject: [PATCH 1/6] Bugfix: Render axis below horizontal plots. To reproduce: go to the vertical example, and change the orientation from "bottom" to "left". Observe the axis tick marks above the plot, reaching down into it. --- bullet/bullet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bullet/bullet.js b/bullet/bullet.js index 8288b0e..64e9a3c 100644 --- a/bullet/bullet.js +++ b/bullet/bullet.js @@ -103,6 +103,7 @@ d3.bullet = function() { 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(); From 9a09392368943ec62fe6fc64cc57d3d59a698d65 Mon Sep 17 00:00:00 2001 From: Max Goldstein Date: Sun, 31 Aug 2014 17:57:32 -0400 Subject: [PATCH 2/6] Render measurements after (above) markers. It's how Stephen Few wants it, from the design spec: Whenever the featured measure intersects a comparative measure, the comparative measure should usually appear behind the featured measure. --- bullet/bullet.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/bullet/bullet.js b/bullet/bullet.js index 64e9a3c..87067a6 100644 --- a/bullet/bullet.js +++ b/bullet/bullet.js @@ -67,23 +67,6 @@ 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); @@ -101,6 +84,23 @@ 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; }) + .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+")") From 8e7312fc9f2baf5e880b7b90fd1038d7118cada4 Mon Sep 17 00:00:00 2001 From: Max Goldstein Date: Sun, 31 Aug 2014 17:59:10 -0400 Subject: [PATCH 3/6] Inline default accessors to anonymous functions. Also, no need to specify a transform that does nothing. Mostly a style thing. --- bullet/bullet.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/bullet/bullet.js b/bullet/bullet.js index 87067a6..6057687 100644 --- a/bullet/bullet.js +++ b/bullet/bullet.js @@ -7,9 +7,9 @@ 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(); @@ -32,7 +32,7 @@ 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. @@ -154,18 +154,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) { From f3744626b6d09c3f8be57336cd0b06df4ab10e58 Mon Sep 17 00:00:00 2001 From: Max Goldstein Date: Sun, 31 Aug 2014 18:08:41 -0400 Subject: [PATCH 4/6] Add more classes to marker, range If I have multiple markers, I want the second to be less prominent. If I have charts with a varying number of ranges, I also need to know the total number of ranges. --- bullet/bullet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bullet/bullet.js b/bullet/bullet.js index 6057687..d8427ea 100644 --- a/bullet/bullet.js +++ b/bullet/bullet.js @@ -57,7 +57,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) @@ -72,7 +72,7 @@ d3.bullet = function() { .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) From 7ef61b5d9caccdff86b7395fddd97a47f3cbbbbd Mon Sep 17 00:00:00 2001 From: Max Goldstein Date: Sun, 31 Aug 2014 18:13:38 -0400 Subject: [PATCH 5/6] Don't always sort; allow explicit arrays. Rather than call ranges, markers, and measures, see if they're a function first. Sorting the markers is unhelpful: they won't occlude each other and it's helpful to be able to style the first marker more prominently. Sorting the measures is unhelpful for the same reason, we want to always know s0 is the actual value and s1 is the forecast. However, for rendering, we sort them to prevent occlusion. --- bullet/bullet.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bullet/bullet.js b/bullet/bullet.js index d8427ea..3c242f1 100644 --- a/bullet/bullet.js +++ b/bullet/bullet.js @@ -17,13 +17,17 @@ d3.bullet = function() { // 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"); @@ -37,7 +41,7 @@ d3.bullet = function() { // 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. @@ -90,6 +94,7 @@ d3.bullet = function() { 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) From a4aae5014c2f2ecdfec46a821fc1e26ea15a6f1d Mon Sep 17 00:00:00 2001 From: Max Goldstein Date: Sun, 31 Aug 2014 18:17:15 -0400 Subject: [PATCH 6/6] Add documentation to README. Note that this documention reflects the changes I have made. --- bullet/README.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/bullet/README.md b/bullet/README.md index 800be42..05b6a00 100644 --- a/bullet/README.md +++ b/bullet/README.md @@ -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. + +# d3.bullet() +Create a new bullet chart. + +# bullet(selection) +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). + +# bullet.orient([orientation]) +Get or set the orientation of the bullet chart. The supported orientations are: +`"top"`, `"bottom"`, `"left"`, and `"right"`. The default orientation is `"left"`. + +# bullet.width([width]) +# bullet.height([height]) +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. + +# bullet.ranges([ranges]) +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. + +# bullet.markers([markers]) +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. + +# bullet.measures([measures]) +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.