|
1 // Chart design based on the recommendations of Stephen Few. Implementation |
|
2 // based on the work of Clint Ivy, Jamie Love, and Jason Davies. |
|
3 // http://projects.instantcognition.com/protovis/bulletchart/ |
|
4 d3.chart.bullet = function() { |
|
5 var orient = "left", // TODO top & bottom |
|
6 reverse = false, |
|
7 duration = 0, |
|
8 ranges = d3_chart_bulletRanges, |
|
9 markers = d3_chart_bulletMarkers, |
|
10 measures = d3_chart_bulletMeasures, |
|
11 width = 380, |
|
12 height = 30, |
|
13 tickFormat = null; |
|
14 |
|
15 // For each small multiple… |
|
16 function bullet(g) { |
|
17 g.each(function(d, i) { |
|
18 var rangez = ranges.call(this, d, i).slice().sort(d3.descending), |
|
19 markerz = markers.call(this, d, i).slice().sort(d3.descending), |
|
20 measurez = measures.call(this, d, i).slice().sort(d3.descending), |
|
21 g = d3.select(this); |
|
22 |
|
23 // Compute the new x-scale. |
|
24 var x1 = d3.scale.linear() |
|
25 .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) |
|
26 .range(reverse ? [width, 0] : [0, width]); |
|
27 |
|
28 // Retrieve the old x-scale, if this is an update. |
|
29 var x0 = this.__chart__ || d3.scale.linear() |
|
30 .domain([0, Infinity]) |
|
31 .range(x1.range()); |
|
32 |
|
33 // Stash the new scale. |
|
34 this.__chart__ = x1; |
|
35 |
|
36 // Derive width-scales from the x-scales. |
|
37 var w0 = d3_chart_bulletWidth(x0), |
|
38 w1 = d3_chart_bulletWidth(x1); |
|
39 |
|
40 // Update the range rects. |
|
41 var range = g.selectAll("rect.range") |
|
42 .data(rangez); |
|
43 |
|
44 range.enter().append("svg:rect") |
|
45 .attr("class", function(d, i) { return "range s" + i; }) |
|
46 .attr("width", w0) |
|
47 .attr("height", height) |
|
48 .attr("x", reverse ? x0 : 0) |
|
49 .transition() |
|
50 .duration(duration) |
|
51 .attr("width", w1) |
|
52 .attr("x", reverse ? x1 : 0); |
|
53 |
|
54 range.transition() |
|
55 .duration(duration) |
|
56 .attr("x", reverse ? x1 : 0) |
|
57 .attr("width", w1) |
|
58 .attr("height", height); |
|
59 |
|
60 // Update the measure rects. |
|
61 var measure = g.selectAll("rect.measure") |
|
62 .data(measurez); |
|
63 |
|
64 measure.enter().append("svg:rect") |
|
65 .attr("class", function(d, i) { return "measure s" + i; }) |
|
66 .attr("width", w0) |
|
67 .attr("height", height / 3) |
|
68 .attr("x", reverse ? x0 : 0) |
|
69 .attr("y", height / 3) |
|
70 .transition() |
|
71 .duration(duration) |
|
72 .attr("width", w1) |
|
73 .attr("x", reverse ? x1 : 0); |
|
74 |
|
75 measure.transition() |
|
76 .duration(duration) |
|
77 .attr("width", w1) |
|
78 .attr("height", height / 3) |
|
79 .attr("x", reverse ? x1 : 0) |
|
80 .attr("y", height / 3); |
|
81 |
|
82 // Update the marker lines. |
|
83 var marker = g.selectAll("line.marker") |
|
84 .data(markerz); |
|
85 |
|
86 marker.enter().append("svg:line") |
|
87 .attr("class", "marker") |
|
88 .attr("x1", x0) |
|
89 .attr("x2", x0) |
|
90 .attr("y1", height / 6) |
|
91 .attr("y2", height * 5 / 6) |
|
92 .transition() |
|
93 .duration(duration) |
|
94 .attr("x1", x1) |
|
95 .attr("x2", x1); |
|
96 |
|
97 marker.transition() |
|
98 .duration(duration) |
|
99 .attr("x1", x1) |
|
100 .attr("x2", x1) |
|
101 .attr("y1", height / 6) |
|
102 .attr("y2", height * 5 / 6); |
|
103 |
|
104 // Compute the tick format. |
|
105 var format = tickFormat || x1.tickFormat(8); |
|
106 |
|
107 // Update the tick groups. |
|
108 var tick = g.selectAll("g.tick") |
|
109 .data(x1.ticks(8), function(d) { |
|
110 return this.textContent || format(d); |
|
111 }); |
|
112 |
|
113 // Initialize the ticks with the old scale, x0. |
|
114 var tickEnter = tick.enter().append("svg:g") |
|
115 .attr("class", "tick") |
|
116 .attr("transform", d3_chart_bulletTranslate(x0)) |
|
117 .style("opacity", 1e-6); |
|
118 |
|
119 tickEnter.append("svg:line") |
|
120 .attr("y1", height) |
|
121 .attr("y2", height * 7 / 6); |
|
122 |
|
123 tickEnter.append("svg:text") |
|
124 .attr("text-anchor", "middle") |
|
125 .attr("dy", "1em") |
|
126 .attr("y", height * 7 / 6) |
|
127 .text(format); |
|
128 |
|
129 // Transition the entering ticks to the new scale, x1. |
|
130 tickEnter.transition() |
|
131 .duration(duration) |
|
132 .attr("transform", d3_chart_bulletTranslate(x1)) |
|
133 .style("opacity", 1); |
|
134 |
|
135 // Transition the updating ticks to the new scale, x1. |
|
136 var tickUpdate = tick.transition() |
|
137 .duration(duration) |
|
138 .attr("transform", d3_chart_bulletTranslate(x1)) |
|
139 .style("opacity", 1); |
|
140 |
|
141 tickUpdate.select("line") |
|
142 .attr("y1", height) |
|
143 .attr("y2", height * 7 / 6); |
|
144 |
|
145 tickUpdate.select("text") |
|
146 .attr("y", height * 7 / 6); |
|
147 |
|
148 // Transition the exiting ticks to the new scale, x1. |
|
149 tick.exit().transition() |
|
150 .duration(duration) |
|
151 .attr("transform", d3_chart_bulletTranslate(x1)) |
|
152 .style("opacity", 1e-6) |
|
153 .remove(); |
|
154 }); |
|
155 d3.timer.flush(); |
|
156 } |
|
157 |
|
158 // left, right, top, bottom |
|
159 bullet.orient = function(x) { |
|
160 if (!arguments.length) return orient; |
|
161 orient = x; |
|
162 reverse = orient == "right" || orient == "bottom"; |
|
163 return bullet; |
|
164 }; |
|
165 |
|
166 // ranges (bad, satisfactory, good) |
|
167 bullet.ranges = function(x) { |
|
168 if (!arguments.length) return ranges; |
|
169 ranges = x; |
|
170 return bullet; |
|
171 }; |
|
172 |
|
173 // markers (previous, goal) |
|
174 bullet.markers = function(x) { |
|
175 if (!arguments.length) return markers; |
|
176 markers = x; |
|
177 return bullet; |
|
178 }; |
|
179 |
|
180 // measures (actual, forecast) |
|
181 bullet.measures = function(x) { |
|
182 if (!arguments.length) return measures; |
|
183 measures = x; |
|
184 return bullet; |
|
185 }; |
|
186 |
|
187 bullet.width = function(x) { |
|
188 if (!arguments.length) return width; |
|
189 width = x; |
|
190 return bullet; |
|
191 }; |
|
192 |
|
193 bullet.height = function(x) { |
|
194 if (!arguments.length) return height; |
|
195 height = x; |
|
196 return bullet; |
|
197 }; |
|
198 |
|
199 bullet.tickFormat = function(x) { |
|
200 if (!arguments.length) return tickFormat; |
|
201 tickFormat = x; |
|
202 return bullet; |
|
203 }; |
|
204 |
|
205 bullet.duration = function(x) { |
|
206 if (!arguments.length) return duration; |
|
207 duration = x; |
|
208 return bullet; |
|
209 }; |
|
210 |
|
211 return bullet; |
|
212 }; |
|
213 |
|
214 function d3_chart_bulletRanges(d) { |
|
215 return d.ranges; |
|
216 } |
|
217 |
|
218 function d3_chart_bulletMarkers(d) { |
|
219 return d.markers; |
|
220 } |
|
221 |
|
222 function d3_chart_bulletMeasures(d) { |
|
223 return d.measures; |
|
224 } |
|
225 |
|
226 function d3_chart_bulletTranslate(x) { |
|
227 return function(d) { |
|
228 return "translate(" + x(d) + ",0)"; |
|
229 }; |
|
230 } |
|
231 |
|
232 function d3_chart_bulletWidth(x) { |
|
233 var x0 = x(0); |
|
234 return function(d) { |
|
235 return Math.abs(x(d) - x0); |
|
236 }; |
|
237 } |