1 | (function(){d3.chart = {}; |
---|
2 | // Inspired by http://informationandvisualization.de/blog/box-plot |
---|
3 | d3.chart.box = function() { |
---|
4 | var width = 1, |
---|
5 | height = 1, |
---|
6 | duration = 0, |
---|
7 | domain = null, |
---|
8 | value = Number, |
---|
9 | whiskers = d3_chart_boxWhiskers, |
---|
10 | quartiles = d3_chart_boxQuartiles, |
---|
11 | tickFormat = null; |
---|
12 | |
---|
13 | // For each small multiple⊠|
---|
14 | function box(g) { |
---|
15 | g.each(function(d, i) { |
---|
16 | d = d.map(value).sort(d3.ascending); |
---|
17 | var g = d3.select(this), |
---|
18 | n = d.length, |
---|
19 | min = d[0], |
---|
20 | max = d[n - 1]; |
---|
21 | |
---|
22 | // Compute quartiles. Must return exactly 3 elements. |
---|
23 | var quartileData = d.quartiles = quartiles(d); |
---|
24 | |
---|
25 | // Compute whiskers. Must return exactly 2 elements, or null. |
---|
26 | var whiskerIndices = whiskers && whiskers.call(this, d, i), |
---|
27 | whiskerData = whiskerIndices && whiskerIndices.map(function(i) { return d[i]; }); |
---|
28 | |
---|
29 | // Compute outliers. If no whiskers are specified, all data are "outliers". |
---|
30 | // We compute the outliers as indices, so that we can join across transitions! |
---|
31 | var outlierIndices = whiskerIndices |
---|
32 | ? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) |
---|
33 | : d3.range(n); |
---|
34 | |
---|
35 | // Compute the new x-scale. |
---|
36 | var x1 = d3.scale.linear() |
---|
37 | .domain(domain && domain.call(this, d, i) || [min, max]) |
---|
38 | .range([height, 0]); |
---|
39 | |
---|
40 | // Retrieve the old x-scale, if this is an update. |
---|
41 | var x0 = this.__chart__ || d3.scale.linear() |
---|
42 | .domain([0, Infinity]) |
---|
43 | .range(x1.range()); |
---|
44 | |
---|
45 | // Stash the new scale. |
---|
46 | this.__chart__ = x1; |
---|
47 | |
---|
48 | // Note: the box, median, and box tick elements are fixed in number, |
---|
49 | // so we only have to handle enter and update. In contrast, the outliers |
---|
50 | // and other elements are variable, so we need to exit them! Variable |
---|
51 | // elements also fade in and out. |
---|
52 | |
---|
53 | // Update center line: the vertical line spanning the whiskers. |
---|
54 | var center = g.selectAll("line.center") |
---|
55 | .data(whiskerData ? [whiskerData] : []); |
---|
56 | |
---|
57 | center.enter().insert("svg:line", "rect") |
---|
58 | .attr("class", "center") |
---|
59 | .attr("x1", width / 2) |
---|
60 | .attr("y1", function(d) { return x0(d[0]); }) |
---|
61 | .attr("x2", width / 2) |
---|
62 | .attr("y2", function(d) { return x0(d[1]); }) |
---|
63 | .style("opacity", 1e-6) |
---|
64 | .transition() |
---|
65 | .duration(duration) |
---|
66 | .style("opacity", 1) |
---|
67 | .attr("y1", function(d) { return x1(d[0]); }) |
---|
68 | .attr("y2", function(d) { return x1(d[1]); }); |
---|
69 | |
---|
70 | center.transition() |
---|
71 | .duration(duration) |
---|
72 | .style("opacity", 1) |
---|
73 | .attr("y1", function(d) { return x1(d[0]); }) |
---|
74 | .attr("y2", function(d) { return x1(d[1]); }); |
---|
75 | |
---|
76 | center.exit().transition() |
---|
77 | .duration(duration) |
---|
78 | .style("opacity", 1e-6) |
---|
79 | .attr("y1", function(d) { return x1(d[0]); }) |
---|
80 | .attr("y2", function(d) { return x1(d[1]); }) |
---|
81 | .remove(); |
---|
82 | |
---|
83 | // Update innerquartile box. |
---|
84 | var box = g.selectAll("rect.box") |
---|
85 | .data([quartileData]); |
---|
86 | |
---|
87 | box.enter().append("svg:rect") |
---|
88 | .attr("class", "box") |
---|
89 | .attr("x", 0) |
---|
90 | .attr("y", function(d) { return x0(d[2]); }) |
---|
91 | .attr("width", width) |
---|
92 | .attr("height", function(d) { return x0(d[0]) - x0(d[2]); }) |
---|
93 | .transition() |
---|
94 | .duration(duration) |
---|
95 | .attr("y", function(d) { return x1(d[2]); }) |
---|
96 | .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); |
---|
97 | |
---|
98 | box.transition() |
---|
99 | .duration(duration) |
---|
100 | .attr("y", function(d) { return x1(d[2]); }) |
---|
101 | .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); |
---|
102 | |
---|
103 | // Update median line. |
---|
104 | var medianLine = g.selectAll("line.median") |
---|
105 | .data([quartileData[1]]); |
---|
106 | |
---|
107 | medianLine.enter().append("svg:line") |
---|
108 | .attr("class", "median") |
---|
109 | .attr("x1", 0) |
---|
110 | .attr("y1", x0) |
---|
111 | .attr("x2", width) |
---|
112 | .attr("y2", x0) |
---|
113 | .transition() |
---|
114 | .duration(duration) |
---|
115 | .attr("y1", x1) |
---|
116 | .attr("y2", x1); |
---|
117 | |
---|
118 | medianLine.transition() |
---|
119 | .duration(duration) |
---|
120 | .attr("y1", x1) |
---|
121 | .attr("y2", x1); |
---|
122 | |
---|
123 | // Update whiskers. |
---|
124 | var whisker = g.selectAll("line.whisker") |
---|
125 | .data(whiskerData || []); |
---|
126 | |
---|
127 | whisker.enter().insert("svg:line", "circle, text") |
---|
128 | .attr("class", "whisker") |
---|
129 | .attr("x1", 0) |
---|
130 | .attr("y1", x0) |
---|
131 | .attr("x2", width) |
---|
132 | .attr("y2", x0) |
---|
133 | .style("opacity", 1e-6) |
---|
134 | .transition() |
---|
135 | .duration(duration) |
---|
136 | .attr("y1", x1) |
---|
137 | .attr("y2", x1) |
---|
138 | .style("opacity", 1); |
---|
139 | |
---|
140 | whisker.transition() |
---|
141 | .duration(duration) |
---|
142 | .attr("y1", x1) |
---|
143 | .attr("y2", x1) |
---|
144 | .style("opacity", 1); |
---|
145 | |
---|
146 | whisker.exit().transition() |
---|
147 | .duration(duration) |
---|
148 | .attr("y1", x1) |
---|
149 | .attr("y2", x1) |
---|
150 | .style("opacity", 1e-6) |
---|
151 | .remove(); |
---|
152 | |
---|
153 | // Update outliers. |
---|
154 | var outlier = g.selectAll("circle.outlier") |
---|
155 | .data(outlierIndices, Number); |
---|
156 | |
---|
157 | outlier.enter().insert("svg:circle", "text") |
---|
158 | .attr("class", "outlier") |
---|
159 | .attr("r", 5) |
---|
160 | .attr("cx", width / 2) |
---|
161 | .attr("cy", function(i) { return x0(d[i]); }) |
---|
162 | .style("opacity", 1e-6) |
---|
163 | .transition() |
---|
164 | .duration(duration) |
---|
165 | .attr("cy", function(i) { return x1(d[i]); }) |
---|
166 | .style("opacity", 1); |
---|
167 | |
---|
168 | outlier.transition() |
---|
169 | .duration(duration) |
---|
170 | .attr("cy", function(i) { return x1(d[i]); }) |
---|
171 | .style("opacity", 1); |
---|
172 | |
---|
173 | outlier.exit().transition() |
---|
174 | .duration(duration) |
---|
175 | .attr("cy", function(i) { return x1(d[i]); }) |
---|
176 | .style("opacity", 1e-6) |
---|
177 | .remove(); |
---|
178 | |
---|
179 | // Compute the tick format. |
---|
180 | var format = tickFormat || x1.tickFormat(8); |
---|
181 | |
---|
182 | // Update box ticks. |
---|
183 | var boxTick = g.selectAll("text.box") |
---|
184 | .data(quartileData); |
---|
185 | |
---|
186 | boxTick.enter().append("svg:text") |
---|
187 | .attr("class", "box") |
---|
188 | .attr("dy", ".3em") |
---|
189 | .attr("dx", function(d, i) { return i & 1 ? 6 : -6 }) |
---|
190 | .attr("x", function(d, i) { return i & 1 ? width : 0 }) |
---|
191 | .attr("y", x0) |
---|
192 | .attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; }) |
---|
193 | .text(format) |
---|
194 | .transition() |
---|
195 | .duration(duration) |
---|
196 | .attr("y", x1); |
---|
197 | |
---|
198 | boxTick.transition() |
---|
199 | .duration(duration) |
---|
200 | .text(format) |
---|
201 | .attr("y", x1); |
---|
202 | |
---|
203 | // Update whisker ticks. These are handled separately from the box |
---|
204 | // ticks because they may or may not exist, and we want don't want |
---|
205 | // to join box ticks pre-transition with whisker ticks post-. |
---|
206 | var whiskerTick = g.selectAll("text.whisker") |
---|
207 | .data(whiskerData || []); |
---|
208 | |
---|
209 | whiskerTick.enter().append("svg:text") |
---|
210 | .attr("class", "whisker") |
---|
211 | .attr("dy", ".3em") |
---|
212 | .attr("dx", 6) |
---|
213 | .attr("x", width) |
---|
214 | .attr("y", x0) |
---|
215 | .text(format) |
---|
216 | .style("opacity", 1e-6) |
---|
217 | .transition() |
---|
218 | .duration(duration) |
---|
219 | .attr("y", x1) |
---|
220 | .style("opacity", 1); |
---|
221 | |
---|
222 | whiskerTick.transition() |
---|
223 | .duration(duration) |
---|
224 | .text(format) |
---|
225 | .attr("y", x1) |
---|
226 | .style("opacity", 1); |
---|
227 | |
---|
228 | whiskerTick.exit().transition() |
---|
229 | .duration(duration) |
---|
230 | .attr("y", x1) |
---|
231 | .style("opacity", 1e-6) |
---|
232 | .remove(); |
---|
233 | }); |
---|
234 | d3.timer.flush(); |
---|
235 | } |
---|
236 | |
---|
237 | box.width = function(x) { |
---|
238 | if (!arguments.length) return width; |
---|
239 | width = x; |
---|
240 | return box; |
---|
241 | }; |
---|
242 | |
---|
243 | box.height = function(x) { |
---|
244 | if (!arguments.length) return height; |
---|
245 | height = x; |
---|
246 | return box; |
---|
247 | }; |
---|
248 | |
---|
249 | box.tickFormat = function(x) { |
---|
250 | if (!arguments.length) return tickFormat; |
---|
251 | tickFormat = x; |
---|
252 | return box; |
---|
253 | }; |
---|
254 | |
---|
255 | box.duration = function(x) { |
---|
256 | if (!arguments.length) return duration; |
---|
257 | duration = x; |
---|
258 | return box; |
---|
259 | }; |
---|
260 | |
---|
261 | box.domain = function(x) { |
---|
262 | if (!arguments.length) return domain; |
---|
263 | domain = x == null ? x : d3.functor(x); |
---|
264 | return box; |
---|
265 | }; |
---|
266 | |
---|
267 | box.value = function(x) { |
---|
268 | if (!arguments.length) return value; |
---|
269 | value = x; |
---|
270 | return box; |
---|
271 | }; |
---|
272 | |
---|
273 | box.whiskers = function(x) { |
---|
274 | if (!arguments.length) return whiskers; |
---|
275 | whiskers = x; |
---|
276 | return box; |
---|
277 | }; |
---|
278 | |
---|
279 | box.quartiles = function(x) { |
---|
280 | if (!arguments.length) return quartiles; |
---|
281 | quartiles = x; |
---|
282 | return box; |
---|
283 | }; |
---|
284 | |
---|
285 | return box; |
---|
286 | }; |
---|
287 | |
---|
288 | function d3_chart_boxWhiskers(d) { |
---|
289 | return [0, d.length - 1]; |
---|
290 | } |
---|
291 | |
---|
292 | function d3_chart_boxQuartiles(d) { |
---|
293 | return [ |
---|
294 | d3.quantile(d, .25), |
---|
295 | d3.quantile(d, .5), |
---|
296 | d3.quantile(d, .75) |
---|
297 | ]; |
---|
298 | } |
---|
299 | // Chart design based on the recommendations of Stephen Few. Implementation |
---|
300 | // based on the work of Clint Ivy, Jamie Love, and Jason Davies. |
---|
301 | // http://projects.instantcognition.com/protovis/bulletchart/ |
---|
302 | d3.chart.bullet = function() { |
---|
303 | var orient = "left", // TODO top & bottom |
---|
304 | reverse = false, |
---|
305 | duration = 0, |
---|
306 | ranges = d3_chart_bulletRanges, |
---|
307 | markers = d3_chart_bulletMarkers, |
---|
308 | measures = d3_chart_bulletMeasures, |
---|
309 | width = 380, |
---|
310 | height = 30, |
---|
311 | tickFormat = null; |
---|
312 | |
---|
313 | // For each small multiple⊠|
---|
314 | function bullet(g) { |
---|
315 | g.each(function(d, i) { |
---|
316 | var rangez = ranges.call(this, d, i).slice().sort(d3.descending), |
---|
317 | markerz = markers.call(this, d, i).slice().sort(d3.descending), |
---|
318 | measurez = measures.call(this, d, i).slice().sort(d3.descending), |
---|
319 | g = d3.select(this); |
---|
320 | |
---|
321 | // Compute the new x-scale. |
---|
322 | var x1 = d3.scale.linear() |
---|
323 | .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) |
---|
324 | .range(reverse ? [width, 0] : [0, width]); |
---|
325 | |
---|
326 | // Retrieve the old x-scale, if this is an update. |
---|
327 | var x0 = this.__chart__ || d3.scale.linear() |
---|
328 | .domain([0, Infinity]) |
---|
329 | .range(x1.range()); |
---|
330 | |
---|
331 | // Stash the new scale. |
---|
332 | this.__chart__ = x1; |
---|
333 | |
---|
334 | // Derive width-scales from the x-scales. |
---|
335 | var w0 = d3_chart_bulletWidth(x0), |
---|
336 | w1 = d3_chart_bulletWidth(x1); |
---|
337 | |
---|
338 | // Update the range rects. |
---|
339 | var range = g.selectAll("rect.range") |
---|
340 | .data(rangez); |
---|
341 | |
---|
342 | range.enter().append("svg:rect") |
---|
343 | .attr("class", function(d, i) { return "range s" + i; }) |
---|
344 | .attr("width", w0) |
---|
345 | .attr("height", height) |
---|
346 | .attr("x", reverse ? x0 : 0) |
---|
347 | .transition() |
---|
348 | .duration(duration) |
---|
349 | .attr("width", w1) |
---|
350 | .attr("x", reverse ? x1 : 0); |
---|
351 | |
---|
352 | range.transition() |
---|
353 | .duration(duration) |
---|
354 | .attr("x", reverse ? x1 : 0) |
---|
355 | .attr("width", w1) |
---|
356 | .attr("height", height); |
---|
357 | |
---|
358 | // Update the measure rects. |
---|
359 | var measure = g.selectAll("rect.measure") |
---|
360 | .data(measurez); |
---|
361 | |
---|
362 | measure.enter().append("svg:rect") |
---|
363 | .attr("class", function(d, i) { return "measure s" + i; }) |
---|
364 | .attr("width", w0) |
---|
365 | .attr("height", height / 3) |
---|
366 | .attr("x", reverse ? x0 : 0) |
---|
367 | .attr("y", height / 3) |
---|
368 | .transition() |
---|
369 | .duration(duration) |
---|
370 | .attr("width", w1) |
---|
371 | .attr("x", reverse ? x1 : 0); |
---|
372 | |
---|
373 | measure.transition() |
---|
374 | .duration(duration) |
---|
375 | .attr("width", w1) |
---|
376 | .attr("height", height / 3) |
---|
377 | .attr("x", reverse ? x1 : 0) |
---|
378 | .attr("y", height / 3); |
---|
379 | |
---|
380 | // Update the marker lines. |
---|
381 | var marker = g.selectAll("line.marker") |
---|
382 | .data(markerz); |
---|
383 | |
---|
384 | marker.enter().append("svg:line") |
---|
385 | .attr("class", "marker") |
---|
386 | .attr("x1", x0) |
---|
387 | .attr("x2", x0) |
---|
388 | .attr("y1", height / 6) |
---|
389 | .attr("y2", height * 5 / 6) |
---|
390 | .transition() |
---|
391 | .duration(duration) |
---|
392 | .attr("x1", x1) |
---|
393 | .attr("x2", x1); |
---|
394 | |
---|
395 | marker.transition() |
---|
396 | .duration(duration) |
---|
397 | .attr("x1", x1) |
---|
398 | .attr("x2", x1) |
---|
399 | .attr("y1", height / 6) |
---|
400 | .attr("y2", height * 5 / 6); |
---|
401 | |
---|
402 | // Compute the tick format. |
---|
403 | var format = tickFormat || x1.tickFormat(8); |
---|
404 | |
---|
405 | // Update the tick groups. |
---|
406 | var tick = g.selectAll("g.tick") |
---|
407 | .data(x1.ticks(8), function(d) { |
---|
408 | return this.textContent || format(d); |
---|
409 | }); |
---|
410 | |
---|
411 | // Initialize the ticks with the old scale, x0. |
---|
412 | var tickEnter = tick.enter().append("svg:g") |
---|
413 | .attr("class", "tick") |
---|
414 | .attr("transform", d3_chart_bulletTranslate(x0)) |
---|
415 | .style("opacity", 1e-6); |
---|
416 | |
---|
417 | tickEnter.append("svg:line") |
---|
418 | .attr("y1", height) |
---|
419 | .attr("y2", height * 7 / 6); |
---|
420 | |
---|
421 | tickEnter.append("svg:text") |
---|
422 | .attr("text-anchor", "middle") |
---|
423 | .attr("dy", "1em") |
---|
424 | .attr("y", height * 7 / 6) |
---|
425 | .text(format); |
---|
426 | |
---|
427 | // Transition the entering ticks to the new scale, x1. |
---|
428 | tickEnter.transition() |
---|
429 | .duration(duration) |
---|
430 | .attr("transform", d3_chart_bulletTranslate(x1)) |
---|
431 | .style("opacity", 1); |
---|
432 | |
---|
433 | // Transition the updating ticks to the new scale, x1. |
---|
434 | var tickUpdate = tick.transition() |
---|
435 | .duration(duration) |
---|
436 | .attr("transform", d3_chart_bulletTranslate(x1)) |
---|
437 | .style("opacity", 1); |
---|
438 | |
---|
439 | tickUpdate.select("line") |
---|
440 | .attr("y1", height) |
---|
441 | .attr("y2", height * 7 / 6); |
---|
442 | |
---|
443 | tickUpdate.select("text") |
---|
444 | .attr("y", height * 7 / 6); |
---|
445 | |
---|
446 | // Transition the exiting ticks to the new scale, x1. |
---|
447 | tick.exit().transition() |
---|
448 | .duration(duration) |
---|
449 | .attr("transform", d3_chart_bulletTranslate(x1)) |
---|
450 | .style("opacity", 1e-6) |
---|
451 | .remove(); |
---|
452 | }); |
---|
453 | d3.timer.flush(); |
---|
454 | } |
---|
455 | |
---|
456 | // left, right, top, bottom |
---|
457 | bullet.orient = function(x) { |
---|
458 | if (!arguments.length) return orient; |
---|
459 | orient = x; |
---|
460 | reverse = orient == "right" || orient == "bottom"; |
---|
461 | return bullet; |
---|
462 | }; |
---|
463 | |
---|
464 | // ranges (bad, satisfactory, good) |
---|
465 | bullet.ranges = function(x) { |
---|
466 | if (!arguments.length) return ranges; |
---|
467 | ranges = x; |
---|
468 | return bullet; |
---|
469 | }; |
---|
470 | |
---|
471 | // markers (previous, goal) |
---|
472 | bullet.markers = function(x) { |
---|
473 | if (!arguments.length) return markers; |
---|
474 | markers = x; |
---|
475 | return bullet; |
---|
476 | }; |
---|
477 | |
---|
478 | // measures (actual, forecast) |
---|
479 | bullet.measures = function(x) { |
---|
480 | if (!arguments.length) return measures; |
---|
481 | measures = x; |
---|
482 | return bullet; |
---|
483 | }; |
---|
484 | |
---|
485 | bullet.width = function(x) { |
---|
486 | if (!arguments.length) return width; |
---|
487 | width = x; |
---|
488 | return bullet; |
---|
489 | }; |
---|
490 | |
---|
491 | bullet.height = function(x) { |
---|
492 | if (!arguments.length) return height; |
---|
493 | height = x; |
---|
494 | return bullet; |
---|
495 | }; |
---|
496 | |
---|
497 | bullet.tickFormat = function(x) { |
---|
498 | if (!arguments.length) return tickFormat; |
---|
499 | tickFormat = x; |
---|
500 | return bullet; |
---|
501 | }; |
---|
502 | |
---|
503 | bullet.duration = function(x) { |
---|
504 | if (!arguments.length) return duration; |
---|
505 | duration = x; |
---|
506 | return bullet; |
---|
507 | }; |
---|
508 | |
---|
509 | return bullet; |
---|
510 | }; |
---|
511 | |
---|
512 | function d3_chart_bulletRanges(d) { |
---|
513 | return d.ranges; |
---|
514 | } |
---|
515 | |
---|
516 | function d3_chart_bulletMarkers(d) { |
---|
517 | return d.markers; |
---|
518 | } |
---|
519 | |
---|
520 | function d3_chart_bulletMeasures(d) { |
---|
521 | return d.measures; |
---|
522 | } |
---|
523 | |
---|
524 | function d3_chart_bulletTranslate(x) { |
---|
525 | return function(d) { |
---|
526 | return "translate(" + x(d) + ",0)"; |
---|
527 | }; |
---|
528 | } |
---|
529 | |
---|
530 | function d3_chart_bulletWidth(x) { |
---|
531 | var x0 = x(0); |
---|
532 | return function(d) { |
---|
533 | return Math.abs(x(d) - x0); |
---|
534 | }; |
---|
535 | } |
---|
536 | // Implements a horizon layout, which is a variation of a single-series |
---|
537 | // area chart where the area is folded into multiple bands. Color is used to |
---|
538 | // encode band, allowing the size of the chart to be reduced significantly |
---|
539 | // without impeding readability. This layout algorithm is based on the work of |
---|
540 | // J. Heer, N. Kong and M. Agrawala in "Sizing the Horizon: The Effects of Chart |
---|
541 | // Size and Layering on the Graphical Perception of Time Series Visualizations", |
---|
542 | // CHI 2009. http://hci.stanford.edu/publications/2009/heer-horizon-chi09.pdf |
---|
543 | d3.chart.horizon = function() { |
---|
544 | var bands = 1, // between 1 and 5, typically |
---|
545 | mode = "offset", // or mirror |
---|
546 | interpolate = "linear", // or basis, monotone, step-before, etc. |
---|
547 | x = d3_chart_horizonX, |
---|
548 | y = d3_chart_horizonY, |
---|
549 | w = 960, |
---|
550 | h = 40, |
---|
551 | duration = 0; |
---|
552 | |
---|
553 | var color = d3.scale.linear() |
---|
554 | .domain([-1, 0, 1]) |
---|
555 | .range(["#d62728", "#fff", "#1f77b4"]); |
---|
556 | |
---|
557 | // For each small multiple⊠|
---|
558 | function horizon(g) { |
---|
559 | g.each(function(d, i) { |
---|
560 | var g = d3.select(this), |
---|
561 | n = 2 * bands + 1, |
---|
562 | xMin = Infinity, |
---|
563 | xMax = -Infinity, |
---|
564 | yMax = -Infinity, |
---|
565 | x0, // old x-scale |
---|
566 | y0, // old y-scale |
---|
567 | id; // unique id for paths |
---|
568 | |
---|
569 | // Compute x- and y-values along with extents. |
---|
570 | var data = d.map(function(d, i) { |
---|
571 | var xv = x.call(this, d, i), |
---|
572 | yv = y.call(this, d, i); |
---|
573 | if (xv < xMin) xMin = xv; |
---|
574 | if (xv > xMax) xMax = xv; |
---|
575 | if (-yv > yMax) yMax = -yv; |
---|
576 | if (yv > yMax) yMax = yv; |
---|
577 | return [xv, yv]; |
---|
578 | }); |
---|
579 | |
---|
580 | // Compute the new x- and y-scales. |
---|
581 | var x1 = d3.scale.linear().domain([xMin, xMax]).range([0, w]), |
---|
582 | y1 = d3.scale.linear().domain([0, yMax]).range([0, h * bands]); |
---|
583 | |
---|
584 | // Retrieve the old scales, if this is an update. |
---|
585 | if (this.__chart__) { |
---|
586 | x0 = this.__chart__.x; |
---|
587 | y0 = this.__chart__.y; |
---|
588 | id = this.__chart__.id; |
---|
589 | } else { |
---|
590 | x0 = d3.scale.linear().domain([0, Infinity]).range(x1.range()); |
---|
591 | y0 = d3.scale.linear().domain([0, Infinity]).range(y1.range()); |
---|
592 | id = ++d3_chart_horizonId; |
---|
593 | } |
---|
594 | |
---|
595 | // We'll use a defs to store the area path and the clip path. |
---|
596 | var defs = g.selectAll("defs") |
---|
597 | .data([data]); |
---|
598 | |
---|
599 | var defsEnter = defs.enter().append("svg:defs"); |
---|
600 | |
---|
601 | // The clip path is a simple rect. |
---|
602 | defsEnter.append("svg:clipPath") |
---|
603 | .attr("id", "d3_chart_horizon_clip" + id) |
---|
604 | .append("svg:rect") |
---|
605 | .attr("width", w) |
---|
606 | .attr("height", h); |
---|
607 | |
---|
608 | defs.select("rect").transition() |
---|
609 | .duration(duration) |
---|
610 | .attr("width", w) |
---|
611 | .attr("height", h); |
---|
612 | |
---|
613 | // The area path is rendered with our resuable d3.svg.area. |
---|
614 | defsEnter.append("svg:path") |
---|
615 | .attr("id", "d3_chart_horizon_path" + id) |
---|
616 | .attr("d", d3_chart_horizonArea |
---|
617 | .interpolate(interpolate) |
---|
618 | .x(function(d) { return x0(d[0]); }) |
---|
619 | .y0(h * bands) |
---|
620 | .y1(function(d) { return h * bands - y0(d[1]); })) |
---|
621 | .transition() |
---|
622 | .duration(duration) |
---|
623 | .attr("d", d3_chart_horizonArea |
---|
624 | .x(function(d) { return x1(d[0]); }) |
---|
625 | .y1(function(d) { return h * bands - y1(d[1]); })); |
---|
626 | |
---|
627 | defs.select("path").transition() |
---|
628 | .duration(duration) |
---|
629 | .attr("d", d3_chart_horizonArea); |
---|
630 | |
---|
631 | // We'll use a container to clip all horizon layers at once. |
---|
632 | g.selectAll("g") |
---|
633 | .data([null]) |
---|
634 | .enter().append("svg:g") |
---|
635 | .attr("clip-path", "url(#d3_chart_horizon_clip" + id + ")"); |
---|
636 | |
---|
637 | // Define the transform function based on the mode. |
---|
638 | var transform = mode == "offset" |
---|
639 | ? function(d) { return "translate(0," + (d + (d < 0) - bands) * h + ")"; } |
---|
640 | : function(d) { return (d < 0 ? "scale(1,-1)" : "") + "translate(0," + (d - bands) * h + ")"; }; |
---|
641 | |
---|
642 | // Instantiate each copy of the path with different transforms. |
---|
643 | var u = g.select("g").selectAll("use") |
---|
644 | .data(d3.range(-1, -bands - 1, -1).concat(d3.range(1, bands + 1)), Number); |
---|
645 | |
---|
646 | // TODO don't fudge the enter transition |
---|
647 | u.enter().append("svg:use") |
---|
648 | .attr("xlink:href", "#d3_chart_horizon_path" + id) |
---|
649 | .attr("transform", function(d) { return transform(d + (d > 0 ? 1 : -1)); }) |
---|
650 | .style("fill", color) |
---|
651 | .transition() |
---|
652 | .duration(duration) |
---|
653 | .attr("transform", transform); |
---|
654 | |
---|
655 | u.transition() |
---|
656 | .duration(duration) |
---|
657 | .attr("transform", transform) |
---|
658 | .style("fill", color); |
---|
659 | |
---|
660 | u.exit().transition() |
---|
661 | .duration(duration) |
---|
662 | .attr("transform", transform) |
---|
663 | .remove(); |
---|
664 | |
---|
665 | // Stash the new scales. |
---|
666 | this.__chart__ = {x: x1, y: y1, id: id}; |
---|
667 | }); |
---|
668 | d3.timer.flush(); |
---|
669 | } |
---|
670 | |
---|
671 | horizon.duration = function(x) { |
---|
672 | if (!arguments.length) return duration; |
---|
673 | duration = +x; |
---|
674 | return horizon; |
---|
675 | }; |
---|
676 | |
---|
677 | horizon.bands = function(x) { |
---|
678 | if (!arguments.length) return bands; |
---|
679 | bands = +x; |
---|
680 | color.domain([-bands, 0, bands]); |
---|
681 | return horizon; |
---|
682 | }; |
---|
683 | |
---|
684 | horizon.mode = function(x) { |
---|
685 | if (!arguments.length) return mode; |
---|
686 | mode = x + ""; |
---|
687 | return horizon; |
---|
688 | }; |
---|
689 | |
---|
690 | horizon.colors = function(x) { |
---|
691 | if (!arguments.length) return color.range(); |
---|
692 | color.range(x); |
---|
693 | return horizon; |
---|
694 | }; |
---|
695 | |
---|
696 | horizon.interpolate = function(x) { |
---|
697 | if (!arguments.length) return interpolate; |
---|
698 | interpolate = x + ""; |
---|
699 | return horizon; |
---|
700 | }; |
---|
701 | |
---|
702 | horizon.x = function(z) { |
---|
703 | if (!arguments.length) return x; |
---|
704 | x = z; |
---|
705 | return horizon; |
---|
706 | }; |
---|
707 | |
---|
708 | horizon.y = function(z) { |
---|
709 | if (!arguments.length) return y; |
---|
710 | y = z; |
---|
711 | return horizon; |
---|
712 | }; |
---|
713 | |
---|
714 | horizon.width = function(x) { |
---|
715 | if (!arguments.length) return w; |
---|
716 | w = +x; |
---|
717 | return horizon; |
---|
718 | }; |
---|
719 | |
---|
720 | horizon.height = function(x) { |
---|
721 | if (!arguments.length) return h; |
---|
722 | h = +x; |
---|
723 | return horizon; |
---|
724 | }; |
---|
725 | |
---|
726 | return horizon; |
---|
727 | }; |
---|
728 | |
---|
729 | var d3_chart_horizonArea = d3.svg.area(), |
---|
730 | d3_chart_horizonId = 0; |
---|
731 | |
---|
732 | function d3_chart_horizonX(d) { |
---|
733 | return d[0]; |
---|
734 | } |
---|
735 | |
---|
736 | function d3_chart_horizonY(d) { |
---|
737 | return d[1]; |
---|
738 | } |
---|
739 | // Based on http://vis.stanford.edu/protovis/ex/qqplot.html |
---|
740 | d3.chart.qq = function() { |
---|
741 | var width = 1, |
---|
742 | height = 1, |
---|
743 | duration = 0, |
---|
744 | domain = null, |
---|
745 | tickFormat = null, |
---|
746 | n = 100, |
---|
747 | x = d3_chart_qqX, |
---|
748 | y = d3_chart_qqY; |
---|
749 | |
---|
750 | // For each small multiple⊠|
---|
751 | function qq(g) { |
---|
752 | g.each(function(d, i) { |
---|
753 | var g = d3.select(this), |
---|
754 | qx = d3_chart_qqQuantiles(n, x.call(this, d, i)), |
---|
755 | qy = d3_chart_qqQuantiles(n, y.call(this, d, i)), |
---|
756 | xd = domain && domain.call(this, d, i) || [d3.min(qx), d3.max(qx)], // new x-domain |
---|
757 | yd = domain && domain.call(this, d, i) || [d3.min(qy), d3.max(qy)], // new y-domain |
---|
758 | x0, // old x-scale |
---|
759 | y0; // old y-scale |
---|
760 | |
---|
761 | // Compute the new x-scale. |
---|
762 | var x1 = d3.scale.linear() |
---|
763 | .domain(xd) |
---|
764 | .range([0, width]); |
---|
765 | |
---|
766 | // Compute the new y-scale. |
---|
767 | var y1 = d3.scale.linear() |
---|
768 | .domain(yd) |
---|
769 | .range([height, 0]); |
---|
770 | |
---|
771 | // Retrieve the old scales, if this is an update. |
---|
772 | if (this.__chart__) { |
---|
773 | x0 = this.__chart__.x; |
---|
774 | y0 = this.__chart__.y; |
---|
775 | } else { |
---|
776 | x0 = d3.scale.linear().domain([0, Infinity]).range(x1.range()); |
---|
777 | y0 = d3.scale.linear().domain([0, Infinity]).range(y1.range()); |
---|
778 | } |
---|
779 | |
---|
780 | // Stash the new scales. |
---|
781 | this.__chart__ = {x: x1, y: y1}; |
---|
782 | |
---|
783 | // Update diagonal line. |
---|
784 | var diagonal = g.selectAll("line.diagonal") |
---|
785 | .data([null]); |
---|
786 | |
---|
787 | diagonal.enter().append("svg:line") |
---|
788 | .attr("class", "diagonal") |
---|
789 | .attr("x1", x1(yd[0])) |
---|
790 | .attr("y1", y1(xd[0])) |
---|
791 | .attr("x2", x1(yd[1])) |
---|
792 | .attr("y2", y1(xd[1])); |
---|
793 | |
---|
794 | diagonal.transition() |
---|
795 | .duration(duration) |
---|
796 | .attr("x1", x1(yd[0])) |
---|
797 | .attr("y1", y1(xd[0])) |
---|
798 | .attr("x2", x1(yd[1])) |
---|
799 | .attr("y2", y1(xd[1])); |
---|
800 | |
---|
801 | // Update quantile plots. |
---|
802 | var circle = g.selectAll("circle") |
---|
803 | .data(d3.range(n).map(function(i) { |
---|
804 | return {x: qx[i], y: qy[i]}; |
---|
805 | })); |
---|
806 | |
---|
807 | circle.enter().append("svg:circle") |
---|
808 | .attr("class", "quantile") |
---|
809 | .attr("r", 4.5) |
---|
810 | .attr("cx", function(d) { return x0(d.x); }) |
---|
811 | .attr("cy", function(d) { return y0(d.y); }) |
---|
812 | .style("opacity", 1e-6) |
---|
813 | .transition() |
---|
814 | .duration(duration) |
---|
815 | .attr("cx", function(d) { return x1(d.x); }) |
---|
816 | .attr("cy", function(d) { return y1(d.y); }) |
---|
817 | .style("opacity", 1); |
---|
818 | |
---|
819 | circle.transition() |
---|
820 | .duration(duration) |
---|
821 | .attr("cx", function(d) { return x1(d.x); }) |
---|
822 | .attr("cy", function(d) { return y1(d.y); }) |
---|
823 | .style("opacity", 1); |
---|
824 | |
---|
825 | circle.exit().transition() |
---|
826 | .duration(duration) |
---|
827 | .attr("cx", function(d) { return x1(d.x); }) |
---|
828 | .attr("cy", function(d) { return y1(d.y); }) |
---|
829 | .style("opacity", 1e-6) |
---|
830 | .remove(); |
---|
831 | |
---|
832 | var xformat = tickFormat || x1.tickFormat(4), |
---|
833 | yformat = tickFormat || y1.tickFormat(4), |
---|
834 | tx = function(d) { return "translate(" + x1(d) + "," + height + ")"; }, |
---|
835 | ty = function(d) { return "translate(0," + y1(d) + ")"; }; |
---|
836 | |
---|
837 | // Update x-ticks. |
---|
838 | var xtick = g.selectAll("g.x.tick") |
---|
839 | .data(x1.ticks(4), function(d) { |
---|
840 | return this.textContent || xformat(d); |
---|
841 | }); |
---|
842 | |
---|
843 | var xtickEnter = xtick.enter().append("svg:g") |
---|
844 | .attr("class", "x tick") |
---|
845 | .attr("transform", function(d) { return "translate(" + x0(d) + "," + height + ")"; }) |
---|
846 | .style("opacity", 1e-6); |
---|
847 | |
---|
848 | xtickEnter.append("svg:line") |
---|
849 | .attr("y1", 0) |
---|
850 | .attr("y2", -6); |
---|
851 | |
---|
852 | xtickEnter.append("svg:text") |
---|
853 | .attr("text-anchor", "middle") |
---|
854 | .attr("dy", "1em") |
---|
855 | .text(xformat); |
---|
856 | |
---|
857 | // Transition the entering ticks to the new scale, x1. |
---|
858 | xtickEnter.transition() |
---|
859 | .duration(duration) |
---|
860 | .attr("transform", tx) |
---|
861 | .style("opacity", 1); |
---|
862 | |
---|
863 | // Transition the updating ticks to the new scale, x1. |
---|
864 | xtick.transition() |
---|
865 | .duration(duration) |
---|
866 | .attr("transform", tx) |
---|
867 | .style("opacity", 1); |
---|
868 | |
---|
869 | // Transition the exiting ticks to the new scale, x1. |
---|
870 | xtick.exit().transition() |
---|
871 | .duration(duration) |
---|
872 | .attr("transform", tx) |
---|
873 | .style("opacity", 1e-6) |
---|
874 | .remove(); |
---|
875 | |
---|
876 | // Update ticks. |
---|
877 | var ytick = g.selectAll("g.y.tick") |
---|
878 | .data(y1.ticks(4), function(d) { |
---|
879 | return this.textContent || yformat(d); |
---|
880 | }); |
---|
881 | |
---|
882 | var ytickEnter = ytick.enter().append("svg:g") |
---|
883 | .attr("class", "y tick") |
---|
884 | .attr("transform", function(d) { return "translate(0," + y0(d) + ")"; }) |
---|
885 | .style("opacity", 1e-6); |
---|
886 | |
---|
887 | ytickEnter.append("svg:line") |
---|
888 | .attr("x1", 0) |
---|
889 | .attr("x2", 6); |
---|
890 | |
---|
891 | ytickEnter.append("svg:text") |
---|
892 | .attr("text-anchor", "end") |
---|
893 | .attr("dx", "-.5em") |
---|
894 | .attr("dy", ".3em") |
---|
895 | .text(yformat); |
---|
896 | |
---|
897 | // Transition the entering ticks to the new scale, y1. |
---|
898 | ytickEnter.transition() |
---|
899 | .duration(duration) |
---|
900 | .attr("transform", ty) |
---|
901 | .style("opacity", 1); |
---|
902 | |
---|
903 | // Transition the updating ticks to the new scale, y1. |
---|
904 | ytick.transition() |
---|
905 | .duration(duration) |
---|
906 | .attr("transform", ty) |
---|
907 | .style("opacity", 1); |
---|
908 | |
---|
909 | // Transition the exiting ticks to the new scale, y1. |
---|
910 | ytick.exit().transition() |
---|
911 | .duration(duration) |
---|
912 | .attr("transform", ty) |
---|
913 | .style("opacity", 1e-6) |
---|
914 | .remove(); |
---|
915 | }); |
---|
916 | } |
---|
917 | |
---|
918 | qq.width = function(x) { |
---|
919 | if (!arguments.length) return width; |
---|
920 | width = x; |
---|
921 | return qq; |
---|
922 | }; |
---|
923 | |
---|
924 | qq.height = function(x) { |
---|
925 | if (!arguments.length) return height; |
---|
926 | height = x; |
---|
927 | return qq; |
---|
928 | }; |
---|
929 | |
---|
930 | qq.duration = function(x) { |
---|
931 | if (!arguments.length) return duration; |
---|
932 | duration = x; |
---|
933 | return qq; |
---|
934 | }; |
---|
935 | |
---|
936 | qq.domain = function(x) { |
---|
937 | if (!arguments.length) return domain; |
---|
938 | domain = x == null ? x : d3.functor(x); |
---|
939 | return qq; |
---|
940 | }; |
---|
941 | |
---|
942 | qq.count = function(z) { |
---|
943 | if (!arguments.length) return n; |
---|
944 | n = z; |
---|
945 | return qq; |
---|
946 | }; |
---|
947 | |
---|
948 | qq.x = function(z) { |
---|
949 | if (!arguments.length) return x; |
---|
950 | x = z; |
---|
951 | return qq; |
---|
952 | }; |
---|
953 | |
---|
954 | qq.y = function(z) { |
---|
955 | if (!arguments.length) return y; |
---|
956 | y = z; |
---|
957 | return qq; |
---|
958 | }; |
---|
959 | |
---|
960 | qq.tickFormat = function(x) { |
---|
961 | if (!arguments.length) return tickFormat; |
---|
962 | tickFormat = x; |
---|
963 | return qq; |
---|
964 | }; |
---|
965 | |
---|
966 | return qq; |
---|
967 | }; |
---|
968 | |
---|
969 | function d3_chart_qqQuantiles(n, values) { |
---|
970 | var m = values.length - 1; |
---|
971 | values = values.slice().sort(d3.ascending); |
---|
972 | return d3.range(n).map(function(i) { |
---|
973 | return values[~~(i * m / n)]; |
---|
974 | }); |
---|
975 | } |
---|
976 | |
---|
977 | function d3_chart_qqX(d) { |
---|
978 | return d.x; |
---|
979 | } |
---|
980 | |
---|
981 | function d3_chart_qqY(d) { |
---|
982 | return d.y; |
---|
983 | } |
---|
984 | })(); |
---|