|
1 // Based on http://vis.stanford.edu/protovis/ex/qqplot.html |
|
2 d3.chart.qq = function() { |
|
3 var width = 1, |
|
4 height = 1, |
|
5 duration = 0, |
|
6 domain = null, |
|
7 tickFormat = null, |
|
8 n = 100, |
|
9 x = d3_chart_qqX, |
|
10 y = d3_chart_qqY; |
|
11 |
|
12 // For each small multiple… |
|
13 function qq(g) { |
|
14 g.each(function(d, i) { |
|
15 var g = d3.select(this), |
|
16 qx = d3_chart_qqQuantiles(n, x.call(this, d, i)), |
|
17 qy = d3_chart_qqQuantiles(n, y.call(this, d, i)), |
|
18 xd = domain && domain.call(this, d, i) || [d3.min(qx), d3.max(qx)], // new x-domain |
|
19 yd = domain && domain.call(this, d, i) || [d3.min(qy), d3.max(qy)], // new y-domain |
|
20 x0, // old x-scale |
|
21 y0; // old y-scale |
|
22 |
|
23 // Compute the new x-scale. |
|
24 var x1 = d3.scale.linear() |
|
25 .domain(xd) |
|
26 .range([0, width]); |
|
27 |
|
28 // Compute the new y-scale. |
|
29 var y1 = d3.scale.linear() |
|
30 .domain(yd) |
|
31 .range([height, 0]); |
|
32 |
|
33 // Retrieve the old scales, if this is an update. |
|
34 if (this.__chart__) { |
|
35 x0 = this.__chart__.x; |
|
36 y0 = this.__chart__.y; |
|
37 } else { |
|
38 x0 = d3.scale.linear().domain([0, Infinity]).range(x1.range()); |
|
39 y0 = d3.scale.linear().domain([0, Infinity]).range(y1.range()); |
|
40 } |
|
41 |
|
42 // Stash the new scales. |
|
43 this.__chart__ = {x: x1, y: y1}; |
|
44 |
|
45 // Update diagonal line. |
|
46 var diagonal = g.selectAll("line.diagonal") |
|
47 .data([null]); |
|
48 |
|
49 diagonal.enter().append("svg:line") |
|
50 .attr("class", "diagonal") |
|
51 .attr("x1", x1(yd[0])) |
|
52 .attr("y1", y1(xd[0])) |
|
53 .attr("x2", x1(yd[1])) |
|
54 .attr("y2", y1(xd[1])); |
|
55 |
|
56 diagonal.transition() |
|
57 .duration(duration) |
|
58 .attr("x1", x1(yd[0])) |
|
59 .attr("y1", y1(xd[0])) |
|
60 .attr("x2", x1(yd[1])) |
|
61 .attr("y2", y1(xd[1])); |
|
62 |
|
63 // Update quantile plots. |
|
64 var circle = g.selectAll("circle") |
|
65 .data(d3.range(n).map(function(i) { |
|
66 return {x: qx[i], y: qy[i]}; |
|
67 })); |
|
68 |
|
69 circle.enter().append("svg:circle") |
|
70 .attr("class", "quantile") |
|
71 .attr("r", 4.5) |
|
72 .attr("cx", function(d) { return x0(d.x); }) |
|
73 .attr("cy", function(d) { return y0(d.y); }) |
|
74 .style("opacity", 1e-6) |
|
75 .transition() |
|
76 .duration(duration) |
|
77 .attr("cx", function(d) { return x1(d.x); }) |
|
78 .attr("cy", function(d) { return y1(d.y); }) |
|
79 .style("opacity", 1); |
|
80 |
|
81 circle.transition() |
|
82 .duration(duration) |
|
83 .attr("cx", function(d) { return x1(d.x); }) |
|
84 .attr("cy", function(d) { return y1(d.y); }) |
|
85 .style("opacity", 1); |
|
86 |
|
87 circle.exit().transition() |
|
88 .duration(duration) |
|
89 .attr("cx", function(d) { return x1(d.x); }) |
|
90 .attr("cy", function(d) { return y1(d.y); }) |
|
91 .style("opacity", 1e-6) |
|
92 .remove(); |
|
93 |
|
94 var xformat = tickFormat || x1.tickFormat(4), |
|
95 yformat = tickFormat || y1.tickFormat(4), |
|
96 tx = function(d) { return "translate(" + x1(d) + "," + height + ")"; }, |
|
97 ty = function(d) { return "translate(0," + y1(d) + ")"; }; |
|
98 |
|
99 // Update x-ticks. |
|
100 var xtick = g.selectAll("g.x.tick") |
|
101 .data(x1.ticks(4), function(d) { |
|
102 return this.textContent || xformat(d); |
|
103 }); |
|
104 |
|
105 var xtickEnter = xtick.enter().append("svg:g") |
|
106 .attr("class", "x tick") |
|
107 .attr("transform", function(d) { return "translate(" + x0(d) + "," + height + ")"; }) |
|
108 .style("opacity", 1e-6); |
|
109 |
|
110 xtickEnter.append("svg:line") |
|
111 .attr("y1", 0) |
|
112 .attr("y2", -6); |
|
113 |
|
114 xtickEnter.append("svg:text") |
|
115 .attr("text-anchor", "middle") |
|
116 .attr("dy", "1em") |
|
117 .text(xformat); |
|
118 |
|
119 // Transition the entering ticks to the new scale, x1. |
|
120 xtickEnter.transition() |
|
121 .duration(duration) |
|
122 .attr("transform", tx) |
|
123 .style("opacity", 1); |
|
124 |
|
125 // Transition the updating ticks to the new scale, x1. |
|
126 xtick.transition() |
|
127 .duration(duration) |
|
128 .attr("transform", tx) |
|
129 .style("opacity", 1); |
|
130 |
|
131 // Transition the exiting ticks to the new scale, x1. |
|
132 xtick.exit().transition() |
|
133 .duration(duration) |
|
134 .attr("transform", tx) |
|
135 .style("opacity", 1e-6) |
|
136 .remove(); |
|
137 |
|
138 // Update ticks. |
|
139 var ytick = g.selectAll("g.y.tick") |
|
140 .data(y1.ticks(4), function(d) { |
|
141 return this.textContent || yformat(d); |
|
142 }); |
|
143 |
|
144 var ytickEnter = ytick.enter().append("svg:g") |
|
145 .attr("class", "y tick") |
|
146 .attr("transform", function(d) { return "translate(0," + y0(d) + ")"; }) |
|
147 .style("opacity", 1e-6); |
|
148 |
|
149 ytickEnter.append("svg:line") |
|
150 .attr("x1", 0) |
|
151 .attr("x2", 6); |
|
152 |
|
153 ytickEnter.append("svg:text") |
|
154 .attr("text-anchor", "end") |
|
155 .attr("dx", "-.5em") |
|
156 .attr("dy", ".3em") |
|
157 .text(yformat); |
|
158 |
|
159 // Transition the entering ticks to the new scale, y1. |
|
160 ytickEnter.transition() |
|
161 .duration(duration) |
|
162 .attr("transform", ty) |
|
163 .style("opacity", 1); |
|
164 |
|
165 // Transition the updating ticks to the new scale, y1. |
|
166 ytick.transition() |
|
167 .duration(duration) |
|
168 .attr("transform", ty) |
|
169 .style("opacity", 1); |
|
170 |
|
171 // Transition the exiting ticks to the new scale, y1. |
|
172 ytick.exit().transition() |
|
173 .duration(duration) |
|
174 .attr("transform", ty) |
|
175 .style("opacity", 1e-6) |
|
176 .remove(); |
|
177 }); |
|
178 } |
|
179 |
|
180 qq.width = function(x) { |
|
181 if (!arguments.length) return width; |
|
182 width = x; |
|
183 return qq; |
|
184 }; |
|
185 |
|
186 qq.height = function(x) { |
|
187 if (!arguments.length) return height; |
|
188 height = x; |
|
189 return qq; |
|
190 }; |
|
191 |
|
192 qq.duration = function(x) { |
|
193 if (!arguments.length) return duration; |
|
194 duration = x; |
|
195 return qq; |
|
196 }; |
|
197 |
|
198 qq.domain = function(x) { |
|
199 if (!arguments.length) return domain; |
|
200 domain = x == null ? x : d3.functor(x); |
|
201 return qq; |
|
202 }; |
|
203 |
|
204 qq.count = function(z) { |
|
205 if (!arguments.length) return n; |
|
206 n = z; |
|
207 return qq; |
|
208 }; |
|
209 |
|
210 qq.x = function(z) { |
|
211 if (!arguments.length) return x; |
|
212 x = z; |
|
213 return qq; |
|
214 }; |
|
215 |
|
216 qq.y = function(z) { |
|
217 if (!arguments.length) return y; |
|
218 y = z; |
|
219 return qq; |
|
220 }; |
|
221 |
|
222 qq.tickFormat = function(x) { |
|
223 if (!arguments.length) return tickFormat; |
|
224 tickFormat = x; |
|
225 return qq; |
|
226 }; |
|
227 |
|
228 return qq; |
|
229 }; |
|
230 |
|
231 function d3_chart_qqQuantiles(n, values) { |
|
232 var m = values.length - 1; |
|
233 values = values.slice().sort(d3.ascending); |
|
234 return d3.range(n).map(function(i) { |
|
235 return values[~~(i * m / n)]; |
|
236 }); |
|
237 } |
|
238 |
|
239 function d3_chart_qqX(d) { |
|
240 return d.x; |
|
241 } |
|
242 |
|
243 function d3_chart_qqY(d) { |
|
244 return d.y; |
|
245 } |