|
1 // Inspired by http://informationandvisualization.de/blog/box-plot |
|
2 d3.chart.box = function() { |
|
3 var width = 1, |
|
4 height = 1, |
|
5 duration = 0, |
|
6 domain = null, |
|
7 value = Number, |
|
8 whiskers = d3_chart_boxWhiskers, |
|
9 quartiles = d3_chart_boxQuartiles, |
|
10 tickFormat = null; |
|
11 |
|
12 // For each small multiple… |
|
13 function box(g) { |
|
14 g.each(function(d, i) { |
|
15 d = d.map(value).sort(d3.ascending); |
|
16 var g = d3.select(this), |
|
17 n = d.length, |
|
18 min = d[0], |
|
19 max = d[n - 1]; |
|
20 |
|
21 // Compute quartiles. Must return exactly 3 elements. |
|
22 var quartileData = d.quartiles = quartiles(d); |
|
23 |
|
24 // Compute whiskers. Must return exactly 2 elements, or null. |
|
25 var whiskerIndices = whiskers && whiskers.call(this, d, i), |
|
26 whiskerData = whiskerIndices && whiskerIndices.map(function(i) { return d[i]; }); |
|
27 |
|
28 // Compute outliers. If no whiskers are specified, all data are "outliers". |
|
29 // We compute the outliers as indices, so that we can join across transitions! |
|
30 var outlierIndices = whiskerIndices |
|
31 ? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) |
|
32 : d3.range(n); |
|
33 |
|
34 // Compute the new x-scale. |
|
35 var x1 = d3.scale.linear() |
|
36 .domain(domain && domain.call(this, d, i) || [min, max]) |
|
37 .range([height, 0]); |
|
38 |
|
39 // Retrieve the old x-scale, if this is an update. |
|
40 var x0 = this.__chart__ || d3.scale.linear() |
|
41 .domain([0, Infinity]) |
|
42 .range(x1.range()); |
|
43 |
|
44 // Stash the new scale. |
|
45 this.__chart__ = x1; |
|
46 |
|
47 // Note: the box, median, and box tick elements are fixed in number, |
|
48 // so we only have to handle enter and update. In contrast, the outliers |
|
49 // and other elements are variable, so we need to exit them! Variable |
|
50 // elements also fade in and out. |
|
51 |
|
52 // Update center line: the vertical line spanning the whiskers. |
|
53 var center = g.selectAll("line.center") |
|
54 .data(whiskerData ? [whiskerData] : []); |
|
55 |
|
56 center.enter().insert("svg:line", "rect") |
|
57 .attr("class", "center") |
|
58 .attr("x1", width / 2) |
|
59 .attr("y1", function(d) { return x0(d[0]); }) |
|
60 .attr("x2", width / 2) |
|
61 .attr("y2", function(d) { return x0(d[1]); }) |
|
62 .style("opacity", 1e-6) |
|
63 .transition() |
|
64 .duration(duration) |
|
65 .style("opacity", 1) |
|
66 .attr("y1", function(d) { return x1(d[0]); }) |
|
67 .attr("y2", function(d) { return x1(d[1]); }); |
|
68 |
|
69 center.transition() |
|
70 .duration(duration) |
|
71 .style("opacity", 1) |
|
72 .attr("y1", function(d) { return x1(d[0]); }) |
|
73 .attr("y2", function(d) { return x1(d[1]); }); |
|
74 |
|
75 center.exit().transition() |
|
76 .duration(duration) |
|
77 .style("opacity", 1e-6) |
|
78 .attr("y1", function(d) { return x1(d[0]); }) |
|
79 .attr("y2", function(d) { return x1(d[1]); }) |
|
80 .remove(); |
|
81 |
|
82 // Update innerquartile box. |
|
83 var box = g.selectAll("rect.box") |
|
84 .data([quartileData]); |
|
85 |
|
86 box.enter().append("svg:rect") |
|
87 .attr("class", "box") |
|
88 .attr("x", 0) |
|
89 .attr("y", function(d) { return x0(d[2]); }) |
|
90 .attr("width", width) |
|
91 .attr("height", function(d) { return x0(d[0]) - x0(d[2]); }) |
|
92 .transition() |
|
93 .duration(duration) |
|
94 .attr("y", function(d) { return x1(d[2]); }) |
|
95 .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); |
|
96 |
|
97 box.transition() |
|
98 .duration(duration) |
|
99 .attr("y", function(d) { return x1(d[2]); }) |
|
100 .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); |
|
101 |
|
102 // Update median line. |
|
103 var medianLine = g.selectAll("line.median") |
|
104 .data([quartileData[1]]); |
|
105 |
|
106 medianLine.enter().append("svg:line") |
|
107 .attr("class", "median") |
|
108 .attr("x1", 0) |
|
109 .attr("y1", x0) |
|
110 .attr("x2", width) |
|
111 .attr("y2", x0) |
|
112 .transition() |
|
113 .duration(duration) |
|
114 .attr("y1", x1) |
|
115 .attr("y2", x1); |
|
116 |
|
117 medianLine.transition() |
|
118 .duration(duration) |
|
119 .attr("y1", x1) |
|
120 .attr("y2", x1); |
|
121 |
|
122 // Update whiskers. |
|
123 var whisker = g.selectAll("line.whisker") |
|
124 .data(whiskerData || []); |
|
125 |
|
126 whisker.enter().insert("svg:line", "circle, text") |
|
127 .attr("class", "whisker") |
|
128 .attr("x1", 0) |
|
129 .attr("y1", x0) |
|
130 .attr("x2", width) |
|
131 .attr("y2", x0) |
|
132 .style("opacity", 1e-6) |
|
133 .transition() |
|
134 .duration(duration) |
|
135 .attr("y1", x1) |
|
136 .attr("y2", x1) |
|
137 .style("opacity", 1); |
|
138 |
|
139 whisker.transition() |
|
140 .duration(duration) |
|
141 .attr("y1", x1) |
|
142 .attr("y2", x1) |
|
143 .style("opacity", 1); |
|
144 |
|
145 whisker.exit().transition() |
|
146 .duration(duration) |
|
147 .attr("y1", x1) |
|
148 .attr("y2", x1) |
|
149 .style("opacity", 1e-6) |
|
150 .remove(); |
|
151 |
|
152 // Update outliers. |
|
153 var outlier = g.selectAll("circle.outlier") |
|
154 .data(outlierIndices, Number); |
|
155 |
|
156 outlier.enter().insert("svg:circle", "text") |
|
157 .attr("class", "outlier") |
|
158 .attr("r", 5) |
|
159 .attr("cx", width / 2) |
|
160 .attr("cy", function(i) { return x0(d[i]); }) |
|
161 .style("opacity", 1e-6) |
|
162 .transition() |
|
163 .duration(duration) |
|
164 .attr("cy", function(i) { return x1(d[i]); }) |
|
165 .style("opacity", 1); |
|
166 |
|
167 outlier.transition() |
|
168 .duration(duration) |
|
169 .attr("cy", function(i) { return x1(d[i]); }) |
|
170 .style("opacity", 1); |
|
171 |
|
172 outlier.exit().transition() |
|
173 .duration(duration) |
|
174 .attr("cy", function(i) { return x1(d[i]); }) |
|
175 .style("opacity", 1e-6) |
|
176 .remove(); |
|
177 |
|
178 // Compute the tick format. |
|
179 var format = tickFormat || x1.tickFormat(8); |
|
180 |
|
181 // Update box ticks. |
|
182 var boxTick = g.selectAll("text.box") |
|
183 .data(quartileData); |
|
184 |
|
185 boxTick.enter().append("svg:text") |
|
186 .attr("class", "box") |
|
187 .attr("dy", ".3em") |
|
188 .attr("dx", function(d, i) { return i & 1 ? 6 : -6 }) |
|
189 .attr("x", function(d, i) { return i & 1 ? width : 0 }) |
|
190 .attr("y", x0) |
|
191 .attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; }) |
|
192 .text(format) |
|
193 .transition() |
|
194 .duration(duration) |
|
195 .attr("y", x1); |
|
196 |
|
197 boxTick.transition() |
|
198 .duration(duration) |
|
199 .text(format) |
|
200 .attr("y", x1); |
|
201 |
|
202 // Update whisker ticks. These are handled separately from the box |
|
203 // ticks because they may or may not exist, and we want don't want |
|
204 // to join box ticks pre-transition with whisker ticks post-. |
|
205 var whiskerTick = g.selectAll("text.whisker") |
|
206 .data(whiskerData || []); |
|
207 |
|
208 whiskerTick.enter().append("svg:text") |
|
209 .attr("class", "whisker") |
|
210 .attr("dy", ".3em") |
|
211 .attr("dx", 6) |
|
212 .attr("x", width) |
|
213 .attr("y", x0) |
|
214 .text(format) |
|
215 .style("opacity", 1e-6) |
|
216 .transition() |
|
217 .duration(duration) |
|
218 .attr("y", x1) |
|
219 .style("opacity", 1); |
|
220 |
|
221 whiskerTick.transition() |
|
222 .duration(duration) |
|
223 .text(format) |
|
224 .attr("y", x1) |
|
225 .style("opacity", 1); |
|
226 |
|
227 whiskerTick.exit().transition() |
|
228 .duration(duration) |
|
229 .attr("y", x1) |
|
230 .style("opacity", 1e-6) |
|
231 .remove(); |
|
232 }); |
|
233 d3.timer.flush(); |
|
234 } |
|
235 |
|
236 box.width = function(x) { |
|
237 if (!arguments.length) return width; |
|
238 width = x; |
|
239 return box; |
|
240 }; |
|
241 |
|
242 box.height = function(x) { |
|
243 if (!arguments.length) return height; |
|
244 height = x; |
|
245 return box; |
|
246 }; |
|
247 |
|
248 box.tickFormat = function(x) { |
|
249 if (!arguments.length) return tickFormat; |
|
250 tickFormat = x; |
|
251 return box; |
|
252 }; |
|
253 |
|
254 box.duration = function(x) { |
|
255 if (!arguments.length) return duration; |
|
256 duration = x; |
|
257 return box; |
|
258 }; |
|
259 |
|
260 box.domain = function(x) { |
|
261 if (!arguments.length) return domain; |
|
262 domain = x == null ? x : d3.functor(x); |
|
263 return box; |
|
264 }; |
|
265 |
|
266 box.value = function(x) { |
|
267 if (!arguments.length) return value; |
|
268 value = x; |
|
269 return box; |
|
270 }; |
|
271 |
|
272 box.whiskers = function(x) { |
|
273 if (!arguments.length) return whiskers; |
|
274 whiskers = x; |
|
275 return box; |
|
276 }; |
|
277 |
|
278 box.quartiles = function(x) { |
|
279 if (!arguments.length) return quartiles; |
|
280 quartiles = x; |
|
281 return box; |
|
282 }; |
|
283 |
|
284 return box; |
|
285 }; |
|
286 |
|
287 function d3_chart_boxWhiskers(d) { |
|
288 return [0, d.length - 1]; |
|
289 } |
|
290 |
|
291 function d3_chart_boxQuartiles(d) { |
|
292 return [ |
|
293 d3.quantile(d, .25), |
|
294 d3.quantile(d, .5), |
|
295 d3.quantile(d, .75) |
|
296 ]; |
|
297 } |