Skip to content

Instantly share code, notes, and snippets.

@anaeliaovalle
Last active March 16, 2017 07:02
Show Gist options
  • Save anaeliaovalle/60a7ed1f4e55a9052caab3c69668a306 to your computer and use it in GitHub Desktop.
Save anaeliaovalle/60a7ed1f4e55a9052caab3c69668a306 to your computer and use it in GitHub Desktop.
D3.js Boxplot with Axes and Labels
license: mit

A box-and-whisker plot with axes. Based on Mike Bostock's implementation. Instead of using individual svg elements as in Mike's implementation, here all boxplots are rendered with in one root element. This makes it easy to add axes.

Further differences between the two implementations are:

  • visibility of boxplot labels can be switched with the labels variable
  • CSV files are supported in which each column is an independent variable and each row contains measurements for all variables (see data.csv)
  • transitions are not used here but can be easily added again

forked from jensgrubert's block: D3.js Boxplot with Axes and Labels

(function() {
// Inspired by http://informationandvisualization.de/blog/box-plot
d3.box = function() {
var width = 1,
height = 1,
duration = 0,
domain = null,
value = Number,
whiskers = boxWhiskers,
quartiles = boxQuartiles,
showLabels = true, // whether or not to show text labels
numBars = 8,
curBar = 1,
tickFormat = null;
// For each small multiple…
function box(g) {
g.each(function(data, i) {
//d = d.map(value).sort(d3.ascending);
//var boxIndex = data[0];
//var boxIndex = 1;
var d = data[1].sort(d3.ascending);
var g = d3.select(this),
n = d.length,
min = d[0],
max = d[n - 1];
// Compute quartiles. Must return exactly 3 elements.
var quartileData = d.quartiles = quartiles(d);
// console.log(quartileData);
// Compute whiskers. Must return exactly 2 elements, or null.
var whiskerIndices = whiskers && whiskers.call(this, d, i),
whiskerData = whiskerIndices && whiskerIndices.map(function(i) {
console.log(d);
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", 10)
.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(outlierIndices, Number);
outlier.enter().insert("circle", "text")
.attr("class", "outlier")
.attr("r", 3)
.attr("cx", width / 2)
.attr("cy", function(i) { return x0(d[i]); })
.style("opacity", 1e-6)
.style("fill", "orange")
.transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1);
outlier.transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1);
outlier.exit().transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[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;
};
function boxWhiskers(d) {
return [0, d.length - 1];
}
function boxQuartiles(d) {
return [
d3.quantile(d, .25),
d3.quantile(d, .5),
d3.quantile(d, .75)
];
}
})();
Alarms Fire Hazard Citizen_Assist Rescue Vehicle_Related Other Medical_Incident
3.91 5.80 3.89 7.86 4.50 6.27 12.43 7.95
4.34 4.05 3.86 4.82 7.25 5.67 5.93 7.33
4.13 4.41 5.56 4.15 5.00 6.50 7.45 7.87
3.88 3.51 7.00 3.86 7.00 5.37 6.93 6.57
3.95 4.21 3.88 4.67 7.00 4.84 4.85 7.70
4.41 4.43 3.00 5.00 5 7.00 8.06 7.90
4.39 4.30 5.75 5.00 4.33 8.44 7.60 7.39
4.11 4.51 4.86 6.14 3.60 7.47 5.52 8.31
4.13 3.45 4.75 12.23 5 5.33 11.25 7.62
4.44 4.30 6.71 5.40 4.50 7.20 9.56 7.73
3.97 3.66 5.00 3.17 4.33 5.06 5.70 6.59
3.97 4.96 4.45 4.00 5.00 7.63 9.83 6.88
4.35 3.92 4.40 5.25 5.50 5.97 7.75 7.06
3.73 4.23 3.67 4.40 4.00 7.12 6.90 7.73
4.91 3.94 4.53 5.53 5.00 6.63 6.44 7.43
4.36 6.18 4.38 5.38 14.88 9.61 9.83 7.98
4.40 4.39 4.40 3.67 11.00 7.36 5.33 7.65
3.95 4.26 5.83 3.78 36.11 6.82 6.86 6.51
3.90 3.62 4.50 4.91 7.00 4.36 7.64 7.14
3.73 4.41 5.00 5.55 6.38 6.83 6.00 6.81
4.36 6.51 4.00 4.43 4.83 8.94 8.81 7.36
3.70 4.83 4.09 6.33 13.20 6.19 7.06 7.70
4.13 4.71 4.67 6.13 6.80 7.74 8.50 7.79
3.78 3.09 7.33 6.40 7.50 3.92 6.33 7.02
3.92 3.19 3.67 4.18 6.00 8.20 7.17 6.85
4.03 4.21 5.00 3.00 4.00 6.00 5.78 6.39
4.00 3.47 3.50 7.26 5.00 9.67 5.44 6.70
3.89 4.44 3.79 4.17 3.75 6.12 5.17 6.91
4.44 4.27 4.36 5.14 12.00 8.06 7.18 7.55
4.16 5.04 5.00 8.43 5.00 5.47 6.08 7.36
4.71 4.02 3.67 4.56 3.67 5.75 4.15 6.56
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Avenir"
}
.box {
font: 1px "Avenir";
}
.box line,
.box rect,
.box circle {
fill: orange;
stroke: #000000;
stroke-width: 1.2px;
}
.box .center {
stroke-dasharray: 2,3;
}
.box .outlier {
fill: none;
stroke: #000;
}
.axis {
font: 12px "Avenir";
}
.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="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: 112, left: 50};
var width = 800 - margin.left - margin.right;
var height = 400 - margin.top - margin.bottom;
var min = Infinity,
max = -Infinity;
// parse in the data
d3.csv("data.csv", function(error, csv) {
// using an array of arrays with
// data[n][2]
// where n = number of columns in the csv file
// data[i][0] = name of the ith column
// data[i][1] = array of values of ith column
var data = [];
data[0] = [];
data[1] = [];
data[2] = [];
data[3] = [];
data[4] = [];
data[5] = [];
data[6] = [];
data[7] = [];
// add more rows if your csv file has more columns
// add here the header of the csv file
data[0][0] = "Alarms";
data[1][0] = "Fire";
data[2][0] = "Hazard";
data[3][0] = "Citzen_Assist";
data[4][0] = "Rescue";
data[5][0] = "Vehicle_Related";
data[6][0] = "Other";
data[7][0] = "Medical_Incident";
// add more rows if your csv file has more columns
data[0][1] = [];
data[1][1] = [];
data[2][1] = [];
data[3][1] = [];
data[4][1] = [];
data[5][1] = [];
data[6][1] = [];
data[7][1] = [];
csv.forEach(function(x) {
var v1 = Math.floor(x.Alarms),
v2 = Math.floor(x.Fire),
v3 = Math.floor(x.Hazard),
v4 = Math.floor(x.Citizen_Assist),
v5 = Math.floor(x.Rescue),
v6 = Math.floor(x.Vehicle_Related),
v7 = Math.floor(x.Other),
v8 = Math.floor(x.Medical_Incident);
// add more variables if your csv file has more columns
arr = [v1,v2,v3,v4,v5,v6,v7,v8]
// var rowMax = Math.max(v1, Math.max(v2, Math.max(v3, Math.max(v4,Math.max(v5, Math.max(v6, Math.max(v7,v8)))))));
// var rowMin = Math.min(v1, Math.min(v2, Math.min(v3, Math.min(v4,Math.min(v5, Math.min(v6, Math.min(v7,v8)))))));
var rowMax = Math.max(v1,v2,v3,v4,v5,v6,v7,v8);
var rowMin = Math.min(v1,v2,v3,v4,v5,v6,v7,v8);
data[0][1].push(v1);
data[1][1].push(v2);
data[2][1].push(v3);
data[3][1].push(v4);
data[4][1].push(v5);
data[5][1].push(v6);
data[6][1].push(v7);
data[7][1].push(v8);
// add more rows if your csv file has more columns
if (rowMax > max) max = rowMax;
if (rowMin < min) min = rowMin;
max = 15
console.log(data);
});
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(data.map(function(d) { return d[0] } ) )
.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(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + 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("SFFD Response Times by Call Type in Dec 2016");
// draw y axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text") // and text1
.attr("transform", "rotate(-90)")
.attr("y", -35)
.attr("x", -28)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("font-size", "13px")
.style("font-weight", "bold")
.text("Response Time [Minutes]");
// 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", 40 )
.attr("dy", ".71em")
.style("text-anchor", "middle")
.style("font-size", "13px")
.style("font-weight", "bold")
.text("Call Type");
});
// 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