Skip to content

Instantly share code, notes, and snippets.

@mgold
Created September 3, 2014 02:03
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 mgold/0f1d3667b74ea5616d9f to your computer and use it in GitHub Desktop.
Save mgold/0f1d3667b74ea5616d9f to your computer and use it in GitHub Desktop.
Testing d3.bullet

Testing each option to d3.bullet, taken from d3-plugins and slightly revised. This shows that all of the options still work, and some of them work in edge cases where they didn't before. Also shown are cases that do not yet work. This is not an automated test suite; determining success or failure must be done manually (optically).

/* bullet.js
CSS for SVG renderings of bullet charts, faithful to Stephen Few's design spec
http://www.perceptualedge.com/articles/misc/Bullet_Graph_Design_Spec.pdf
*/
.bullet .marker { stroke-width: 2px }
.bullet .marker.s0 { stroke: #000000 }
.bullet .marker.s1 { stroke: #404040 }
.bullet .measure.s0 { fill: #000000 }
/* 3% black */
.bullet .range.s0.t5 { fill: #F7F7F7 }
/* 10% black */
.bullet .range.s0.t2, .bullet .range.s0.t3, .bullet .range.s0.t4, .bullet .range.s1.t5 { fill: #E5E5E5 }
/* 20% black */
.bullet .range.s1.t4, .bullet .range.s2.t5 { fill: #CCCCCC }
/* 25% black */
.bullet .range.s1.t3 { fill: #BFBFBF }
/* 35% black */
.bullet .range.s1.t2, .bullet .range.s2.t4, .bullet .range.s3.t5 { fill: #A6A6A6 }
/* 40% black */
.bullet .range.s2.t3 { fill: #999999 }
/* 50% black */
.bullet .range.s3.t4, .bullet .range.s4.t5 { fill: #7F7F7F }
(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 = 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 = 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");
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], d3.max(markerz), 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+" t"+range.size(); })
.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 marker lines.
var marker = wrap.selectAll("line.marker")
.data(markerz);
marker.enter().append("line")
.attr("class", function(d, i) { return "marker s"+i; })
.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);
// 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();
}
// 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 bulletWidth(x) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d) - x0);
};
}
})();
<!DOCTYPE html>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="bullet.css" />
<body></body>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
padding-top: 40px;
position: relative;
width: 960px;
}
.bullet { font: 10px sans-serif; }
.bullet .axis line, .bullet .axis path { stroke: #666; stroke-width: .5px; fill: none; }
.bullet .measure.s0 { fill: steelblue; }
.bullet .measure.s1 { fill: lightsteelblue; }
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="bullet.js"></script>
<script>
var margin = {top: 5, right: 40, bottom: 30, left: 40},
width = 450 - margin.left - margin.right,
height = 60 - margin.top - margin.bottom;
var chart = d3.bullet()
.orient("left")
.width(width)
.height(height)
.markers([85])
.measures([92])
function base_chart(){
return d3.select("body").append("svg")
.attr("class", "bullet")
.attr("width", chart.width() + margin.left + margin.right)
.attr("height", chart.height() + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
}
d3.select("body").append("h2").text("Passing tests showing correctness")
d3.select("body").append("p").text("Testing 2 to 5 ranges")
base_chart().call(chart.ranges([90,100]))
base_chart().call(chart.ranges([80,90,100]))
base_chart().call(chart.ranges([70,80,90,100]))
base_chart().call(chart.ranges([65,70,80,90,100]))
d3.select("body").append("p").text("Testing ability to specify accessors as functions")
chart.markers(function(){return [85]})
.measures(function(){return [92]})
.ranges(function(){return [80,90,100]})
base_chart().call(chart)
d3.select("body").append("p").text("Testing 2 markers")
chart.markers([85, 75])
base_chart().call(chart)
d3.select("body").append("p").text("Testing 2 measures")
d3.select("body").append("p").text("Measure 2 may undershoot or overshoot measure 1, which below is always 85")
chart.markers([89])
base_chart().call(chart.measures([85,92]))
base_chart().call(chart.measures([85,70]))
chart.orient("right")
base_chart().call(chart.measures([85,92]))
base_chart().call(chart.measures([85,70]))
d3.select("body").append("p").text("Testing alternate orientations")
chart.measures([92])
base_chart().call(chart.orient("right"))
chart.width(height).height(width)
base_chart().call(chart.orient("top"))
base_chart().call(chart.orient("bottom"))
chart.width(width).height(height).orient("left")
d3.select("body").append("h2").text("Failing tests (things I'd like to add/fix later)")
d3.select("body").append("p").text("Chart does not render properly when missing markers, measures, and ranges (also range length 1)")
base_chart().call(chart.markers([]))
chart.markers([92])
base_chart().call(chart.measures([]))
chart.measures([85])
base_chart().call(chart.ranges([]))
base_chart().call(chart.ranges([100]))
chart.ranges([80,90,100])
d3.select("body").append("p").text("Ranges that don't start at 0 (90%-100%, negative only, zero in middle)")
d3.select("body").append("p").text("This means that we can no longer implictly start at 0 (big breaking change)")
d3.select("body").append("p").text("Ranges that don't include 0 should render the measure as an X not a bar")
base_chart().call(chart.ranges([90,95, 100]).markers([98]).measures([94]))
base_chart().call(chart.ranges([-10,-40, -80]).markers([-5]).measures([-33]))
base_chart().call(chart.ranges([-10, 0, 10]).markers([-5]).measures([-3]))
</script>
@nmeirik
Copy link

nmeirik commented Jun 20, 2018

Do you have any plans to implement support for negative ranges? Is there anything I can do to help?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment