Created
March 25, 2016 01:35
-
-
Save luhn/4027b68469cff084ce23 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
// Inspired by http://informationandvisualization.de/blog/box-plot | |
d3.box = function() { | |
var width = 1, | |
height = 1, | |
duration = 0, | |
domain = null, | |
value = Number; | |
showLabels = true, // whether or not to show text labels | |
numBars = 4, | |
curBar = 1, | |
tickFormat = null; | |
// For each small multiple… | |
function box(g) { | |
g.each(function(d, i) { | |
//d = d.map(value).sort(d3.ascending); | |
//var boxIndex = data[0]; | |
//var boxIndex = 1; | |
// var d = data[1].sort(d3.ascending); | |
// console.log(boxIndex); | |
//console.log(d); | |
var g = d3.select(this), | |
min = d.lower_whisker, | |
max = d.upper_whisker; | |
// Compute quartiles. Must return exactly 3 elements. | |
var quartileData = d.quartiles = [ | |
d.lower_quartile, d.median, d.upper_quartile | |
]; | |
var whiskerData = [ | |
d.lower_whisker, d.upper_whisker | |
]; | |
var outliers = d.outliers; | |
/* | |
// Compute whiskers. Must return exactly 2 elements, or null. | |
var whiskerIndices = whiskers && whiskers.call(this, d, i), | |
whiskerData = whiskerIndices && whiskerIndices.map(function(i) { return d[i]; }); | |
// Compute outliers. If no whiskers are specified, all data are "outliers". | |
// We compute the outliers as indices, so that we can join across transitions! | |
var outlierIndices = whiskerIndices | |
? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) | |
: d3.range(n); | |
*/ | |
// Compute the new x-scale. | |
var x1 = d3.scale.linear() | |
.domain(domain && domain.call(this, d, i) || [min, max]) | |
.range([height, 0]); | |
// Retrieve the old x-scale, if this is an update. | |
var x0 = this.__chart__ || d3.scale.linear() | |
.domain([0, Infinity]) | |
// .domain([0, max]) | |
.range(x1.range()); | |
// Stash the new scale. | |
this.__chart__ = x1; | |
// Note: the box, median, and box tick elements are fixed in number, | |
// so we only have to handle enter and update. In contrast, the outliers | |
// and other elements are variable, so we need to exit them! Variable | |
// elements also fade in and out. | |
// Update center line: the vertical line spanning the whiskers. | |
var center = g.selectAll("line.center") | |
.data(whiskerData ? [whiskerData] : []); | |
//vertical line | |
center.enter().insert("line", "rect") | |
.attr("class", "center") | |
.attr("x1", width / 2) | |
.attr("y1", function(d) { return x0(d[0]); }) | |
.attr("x2", width / 2) | |
.attr("y2", function(d) { return x0(d[1]); }) | |
.style("opacity", 1e-6) | |
.transition() | |
.duration(duration) | |
.style("opacity", 1) | |
.attr("y1", function(d) { return x1(d[0]); }) | |
.attr("y2", function(d) { return x1(d[1]); }); | |
center.transition() | |
.duration(duration) | |
.style("opacity", 1) | |
.attr("y1", function(d) { return x1(d[0]); }) | |
.attr("y2", function(d) { return x1(d[1]); }); | |
center.exit().transition() | |
.duration(duration) | |
.style("opacity", 1e-6) | |
.attr("y1", function(d) { return x1(d[0]); }) | |
.attr("y2", function(d) { return x1(d[1]); }) | |
.remove(); | |
// Update innerquartile box. | |
var box = g.selectAll("rect.box") | |
.data([quartileData]); | |
box.enter().append("rect") | |
.attr("class", "box") | |
.attr("x", 0) | |
.attr("y", function(d) { return x0(d[2]); }) | |
.attr("width", width) | |
.attr("height", function(d) { return x0(d[0]) - x0(d[2]); }) | |
.transition() | |
.duration(duration) | |
.attr("y", function(d) { return x1(d[2]); }) | |
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); | |
box.transition() | |
.duration(duration) | |
.attr("y", function(d) { return x1(d[2]); }) | |
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); | |
// Update median line. | |
var medianLine = g.selectAll("line.median") | |
.data([quartileData[1]]); | |
medianLine.enter().append("line") | |
.attr("class", "median") | |
.attr("x1", 0) | |
.attr("y1", x0) | |
.attr("x2", width) | |
.attr("y2", x0) | |
.transition() | |
.duration(duration) | |
.attr("y1", x1) | |
.attr("y2", x1); | |
medianLine.transition() | |
.duration(duration) | |
.attr("y1", x1) | |
.attr("y2", x1); | |
// Update whiskers. | |
var whisker = g.selectAll("line.whisker") | |
.data(whiskerData || []); | |
whisker.enter().insert("line", "circle, text") | |
.attr("class", "whisker") | |
.attr("x1", 0) | |
.attr("y1", x0) | |
.attr("x2", 0 + width) | |
.attr("y2", x0) | |
.style("opacity", 1e-6) | |
.transition() | |
.duration(duration) | |
.attr("y1", x1) | |
.attr("y2", x1) | |
.style("opacity", 1); | |
whisker.transition() | |
.duration(duration) | |
.attr("y1", x1) | |
.attr("y2", x1) | |
.style("opacity", 1); | |
whisker.exit().transition() | |
.duration(duration) | |
.attr("y1", x1) | |
.attr("y2", x1) | |
.style("opacity", 1e-6) | |
.remove(); | |
// Update outliers. | |
var outlier = g.selectAll("circle.outlier") | |
.data(outliers, Number); | |
outlier.enter().insert("circle", "text") | |
.attr("class", "outlier") | |
.attr("r", 5) | |
.attr("cx", width / 2) | |
.attr("cy", function(i) { return x0(i); }) | |
.style("opacity", 1e-6) | |
.transition() | |
.duration(duration) | |
.attr("cy", function(i) { return x1(i); }) | |
.style("opacity", 1); | |
outlier.transition() | |
.duration(duration) | |
.attr("cy", function(i) { return x1(i); }) | |
.style("opacity", 1); | |
outlier.exit().transition() | |
.duration(duration) | |
.attr("cy", function(i) { return x1(i); }) | |
.style("opacity", 1e-6) | |
.remove(); | |
// Compute the tick format. | |
var format = tickFormat || x1.tickFormat(8); | |
// Update box ticks. | |
var boxTick = g.selectAll("text.box") | |
.data(quartileData); | |
if(showLabels == true) { | |
boxTick.enter().append("text") | |
.attr("class", "box") | |
.attr("dy", ".3em") | |
.attr("dx", function(d, i) { return i & 1 ? 6 : -6 }) | |
.attr("x", function(d, i) { return i & 1 ? + width : 0 }) | |
.attr("y", x0) | |
.attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; }) | |
.text(format) | |
.transition() | |
.duration(duration) | |
.attr("y", x1); | |
} | |
boxTick.transition() | |
.duration(duration) | |
.text(format) | |
.attr("y", x1); | |
// Update whisker ticks. These are handled separately from the box | |
// ticks because they may or may not exist, and we want don't want | |
// to join box ticks pre-transition with whisker ticks post-. | |
var whiskerTick = g.selectAll("text.whisker") | |
.data(whiskerData || []); | |
if(showLabels == true) { | |
whiskerTick.enter().append("text") | |
.attr("class", "whisker") | |
.attr("dy", ".3em") | |
.attr("dx", 6) | |
.attr("x", width) | |
.attr("y", x0) | |
.text(format) | |
.style("opacity", 1e-6) | |
.transition() | |
.duration(duration) | |
.attr("y", x1) | |
.style("opacity", 1); | |
} | |
whiskerTick.transition() | |
.duration(duration) | |
.text(format) | |
.attr("y", x1) | |
.style("opacity", 1); | |
whiskerTick.exit().transition() | |
.duration(duration) | |
.attr("y", x1) | |
.style("opacity", 1e-6) | |
.remove(); | |
}); | |
d3.timer.flush(); | |
} | |
box.width = function(x) { | |
if (!arguments.length) return width; | |
width = x; | |
return box; | |
}; | |
box.height = function(x) { | |
if (!arguments.length) return height; | |
height = x; | |
return box; | |
}; | |
box.tickFormat = function(x) { | |
if (!arguments.length) return tickFormat; | |
tickFormat = x; | |
return box; | |
}; | |
box.duration = function(x) { | |
if (!arguments.length) return duration; | |
duration = x; | |
return box; | |
}; | |
box.domain = function(x) { | |
if (!arguments.length) return domain; | |
domain = x == null ? x : d3.functor(x); | |
return box; | |
}; | |
box.value = function(x) { | |
if (!arguments.length) return value; | |
value = x; | |
return box; | |
}; | |
box.whiskers = function(x) { | |
if (!arguments.length) return whiskers; | |
whiskers = x; | |
return box; | |
}; | |
box.showLabels = function(x) { | |
if (!arguments.length) return showLabels; | |
showLabels = x; | |
return box; | |
}; | |
box.quartiles = function(x) { | |
if (!arguments.length) return quartiles; | |
quartiles = x; | |
return box; | |
}; | |
return box; | |
}; | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<!-- Created from http://bl.ocks.org/jensgrubert/7789216 --> | |
<!-- Allows using pre-computed quartiles, whiskers, etc. for rendering boxplots --> | |
<style> | |
body { | |
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | |
} | |
.box { | |
font: 10px sans-serif; | |
} | |
.box line, | |
.box rect, | |
.box circle { | |
fill: steelblue; | |
stroke: #000; | |
stroke-width: 1px; | |
} | |
.box .center { | |
stroke-dasharray: 3,3; | |
} | |
.box .outlier { | |
fill: none; | |
stroke: #000; | |
} | |
.axis { | |
font: 12px sans-serif; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.x.axis path { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
</style> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="box.js"></script> | |
<script> | |
var labels = true; // show the text labels beside individual boxplots? | |
var margin = {top: 30, right: 50, bottom: 70, left: 50}; | |
var width = 800 - margin.left - margin.right; | |
var height = 400 - margin.top - margin.bottom; | |
var min = -10000, | |
max = 75000; | |
var chart = d3.box() | |
.whiskers(iqr(1.5)) | |
.height(height) | |
.domain([min, max]) | |
.showLabels(labels); | |
var svg = d3.select("body").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.attr("class", "box") | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
// the x-axis | |
var x = d3.scale.ordinal() | |
.domain(['Overall', '2-top', '4-top', '6-top', '8-top', '9+']) // Plug: X-axis values | |
.rangeRoundBands([0 , width], 0.7, 0.3); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("bottom"); | |
// the y-axis | |
var y = d3.scale.linear() | |
.domain([min, max]) | |
.range([height + margin.top, 0 + margin.top]); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient("left"); | |
// draw the boxplots | |
svg.selectAll(".box") | |
.data([ | |
// Plug: data | |
// Notice that I had to add the "label" property here | |
{"label": "Overall", "lower_quartile":-3385.1860359999996,"median":45107.467772,"upper_quartile":59222.38894125,"lower_whisker":-5745.132464,"outliers":[-8000],"upper_whisker":75517.738427}, | |
{"label": "2-top", "lower_quartile":1000,"median":30000,"upper_quartile":59222.38894125,"lower_whisker":-5745.132464,"outliers":[73000, 71000],"upper_whisker":70517.738427}, | |
{"label": "4-top", "lower_quartile":-3385.1860359999996,"median":45107.467772,"upper_quartile":59222.38894125,"lower_whisker":-5745.132464,"outliers":[73000, 71000],"upper_whisker":70517.738427}, | |
{"label": "6-top", "lower_quartile":-3385.1860359999996,"median":45107.467772,"upper_quartile":59222.38894125,"lower_whisker":-5745.132464,"outliers":[73000, 71000],"upper_whisker":70517.738427}, | |
{"label": "8-top", "lower_quartile":-3385.1860359999996,"median":45107.467772,"upper_quartile":59222.38894125,"lower_whisker":-5745.132464,"outliers":[73000, 71000],"upper_whisker":70517.738427}, | |
{"label": "9+", "lower_quartile":-3385.1860359999996,"median":45107.467772,"upper_quartile":59222.38894125,"lower_whisker":-5745.132464,"outliers":[73000, 71000],"upper_whisker":70517.738427}, | |
]) | |
.enter().append("g") | |
.attr("transform", function(d) { console.log(d); return "translate(" + x(d.label) + "," + margin.top + ")"; } ) | |
.call(chart.width(x.rangeBand())); | |
// add a title | |
svg.append("text") | |
.attr("x", (width / 2)) | |
.attr("y", 0 + (margin.top / 2)) | |
.attr("text-anchor", "middle") | |
.style("font-size", "18px") | |
//.style("text-decoration", "underline") | |
.text("Difference between actual and estimated wait times"); // Plug: title | |
// draw y axis | |
svg.append("g") | |
.attr("class", "y axis") | |
.call(yAxis) | |
.append("text") // and text1 | |
.attr("transform", "rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.style("text-anchor", "end") | |
.style("font-size", "16px") | |
.text("Actual vs estimated (minutes)"); // Plug: Y-axis label | |
// draw x axis | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + (height + margin.top + 10) + ")") | |
.call(xAxis) | |
.append("text") // text label for the x axis | |
.attr("x", (width / 2) ) | |
.attr("y", 10 ) | |
.attr("dy", ".71em") | |
.style("text-anchor", "middle") | |
.style("font-size", "16px") | |
.text("Party size"); // Plug: X-axis label | |
// Returns a function to compute the interquartile range. | |
function iqr(k) { | |
return function(d, i) { | |
var q1 = d.quartiles[0], | |
q3 = d.quartiles[2], | |
iqr = (q3 - q1) * k, | |
i = -1, | |
j = d.length; | |
while (d[++i] < q1 - iqr); | |
while (d[--j] > q3 + iqr); | |
return [i, j]; | |
}; | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment