|
1 // Implements a horizon layout, which is a variation of a single-series |
|
2 // area chart where the area is folded into multiple bands. Color is used to |
|
3 // encode band, allowing the size of the chart to be reduced significantly |
|
4 // without impeding readability. This layout algorithm is based on the work of |
|
5 // J. Heer, N. Kong and M. Agrawala in "Sizing the Horizon: The Effects of Chart |
|
6 // Size and Layering on the Graphical Perception of Time Series Visualizations", |
|
7 // CHI 2009. http://hci.stanford.edu/publications/2009/heer-horizon-chi09.pdf |
|
8 d3.chart.horizon = function() { |
|
9 var bands = 1, // between 1 and 5, typically |
|
10 mode = "offset", // or mirror |
|
11 interpolate = "linear", // or basis, monotone, step-before, etc. |
|
12 x = d3_chart_horizonX, |
|
13 y = d3_chart_horizonY, |
|
14 w = 960, |
|
15 h = 40, |
|
16 duration = 0; |
|
17 |
|
18 var color = d3.scale.linear() |
|
19 .domain([-1, 0, 1]) |
|
20 .range(["#d62728", "#fff", "#1f77b4"]); |
|
21 |
|
22 // For each small multiple… |
|
23 function horizon(g) { |
|
24 g.each(function(d, i) { |
|
25 var g = d3.select(this), |
|
26 n = 2 * bands + 1, |
|
27 xMin = Infinity, |
|
28 xMax = -Infinity, |
|
29 yMax = -Infinity, |
|
30 x0, // old x-scale |
|
31 y0, // old y-scale |
|
32 id; // unique id for paths |
|
33 |
|
34 // Compute x- and y-values along with extents. |
|
35 var data = d.map(function(d, i) { |
|
36 var xv = x.call(this, d, i), |
|
37 yv = y.call(this, d, i); |
|
38 if (xv < xMin) xMin = xv; |
|
39 if (xv > xMax) xMax = xv; |
|
40 if (-yv > yMax) yMax = -yv; |
|
41 if (yv > yMax) yMax = yv; |
|
42 return [xv, yv]; |
|
43 }); |
|
44 |
|
45 // Compute the new x- and y-scales. |
|
46 var x1 = d3.scale.linear().domain([xMin, xMax]).range([0, w]), |
|
47 y1 = d3.scale.linear().domain([0, yMax]).range([0, h * bands]); |
|
48 |
|
49 // Retrieve the old scales, if this is an update. |
|
50 if (this.__chart__) { |
|
51 x0 = this.__chart__.x; |
|
52 y0 = this.__chart__.y; |
|
53 id = this.__chart__.id; |
|
54 } else { |
|
55 x0 = d3.scale.linear().domain([0, Infinity]).range(x1.range()); |
|
56 y0 = d3.scale.linear().domain([0, Infinity]).range(y1.range()); |
|
57 id = ++d3_chart_horizonId; |
|
58 } |
|
59 |
|
60 // We'll use a defs to store the area path and the clip path. |
|
61 var defs = g.selectAll("defs") |
|
62 .data([data]); |
|
63 |
|
64 var defsEnter = defs.enter().append("svg:defs"); |
|
65 |
|
66 // The clip path is a simple rect. |
|
67 defsEnter.append("svg:clipPath") |
|
68 .attr("id", "d3_chart_horizon_clip" + id) |
|
69 .append("svg:rect") |
|
70 .attr("width", w) |
|
71 .attr("height", h); |
|
72 |
|
73 defs.select("rect").transition() |
|
74 .duration(duration) |
|
75 .attr("width", w) |
|
76 .attr("height", h); |
|
77 |
|
78 // The area path is rendered with our resuable d3.svg.area. |
|
79 defsEnter.append("svg:path") |
|
80 .attr("id", "d3_chart_horizon_path" + id) |
|
81 .attr("d", d3_chart_horizonArea |
|
82 .interpolate(interpolate) |
|
83 .x(function(d) { return x0(d[0]); }) |
|
84 .y0(h * bands) |
|
85 .y1(function(d) { return h * bands - y0(d[1]); })) |
|
86 .transition() |
|
87 .duration(duration) |
|
88 .attr("d", d3_chart_horizonArea |
|
89 .x(function(d) { return x1(d[0]); }) |
|
90 .y1(function(d) { return h * bands - y1(d[1]); })); |
|
91 |
|
92 defs.select("path").transition() |
|
93 .duration(duration) |
|
94 .attr("d", d3_chart_horizonArea); |
|
95 |
|
96 // We'll use a container to clip all horizon layers at once. |
|
97 g.selectAll("g") |
|
98 .data([null]) |
|
99 .enter().append("svg:g") |
|
100 .attr("clip-path", "url(#d3_chart_horizon_clip" + id + ")"); |
|
101 |
|
102 // Define the transform function based on the mode. |
|
103 var transform = mode == "offset" |
|
104 ? function(d) { return "translate(0," + (d + (d < 0) - bands) * h + ")"; } |
|
105 : function(d) { return (d < 0 ? "scale(1,-1)" : "") + "translate(0," + (d - bands) * h + ")"; }; |
|
106 |
|
107 // Instantiate each copy of the path with different transforms. |
|
108 var u = g.select("g").selectAll("use") |
|
109 .data(d3.range(-1, -bands - 1, -1).concat(d3.range(1, bands + 1)), Number); |
|
110 |
|
111 // TODO don't fudge the enter transition |
|
112 u.enter().append("svg:use") |
|
113 .attr("xlink:href", "#d3_chart_horizon_path" + id) |
|
114 .attr("transform", function(d) { return transform(d + (d > 0 ? 1 : -1)); }) |
|
115 .style("fill", color) |
|
116 .transition() |
|
117 .duration(duration) |
|
118 .attr("transform", transform); |
|
119 |
|
120 u.transition() |
|
121 .duration(duration) |
|
122 .attr("transform", transform) |
|
123 .style("fill", color); |
|
124 |
|
125 u.exit().transition() |
|
126 .duration(duration) |
|
127 .attr("transform", transform) |
|
128 .remove(); |
|
129 |
|
130 // Stash the new scales. |
|
131 this.__chart__ = {x: x1, y: y1, id: id}; |
|
132 }); |
|
133 d3.timer.flush(); |
|
134 } |
|
135 |
|
136 horizon.duration = function(x) { |
|
137 if (!arguments.length) return duration; |
|
138 duration = +x; |
|
139 return horizon; |
|
140 }; |
|
141 |
|
142 horizon.bands = function(x) { |
|
143 if (!arguments.length) return bands; |
|
144 bands = +x; |
|
145 color.domain([-bands, 0, bands]); |
|
146 return horizon; |
|
147 }; |
|
148 |
|
149 horizon.mode = function(x) { |
|
150 if (!arguments.length) return mode; |
|
151 mode = x + ""; |
|
152 return horizon; |
|
153 }; |
|
154 |
|
155 horizon.colors = function(x) { |
|
156 if (!arguments.length) return color.range(); |
|
157 color.range(x); |
|
158 return horizon; |
|
159 }; |
|
160 |
|
161 horizon.interpolate = function(x) { |
|
162 if (!arguments.length) return interpolate; |
|
163 interpolate = x + ""; |
|
164 return horizon; |
|
165 }; |
|
166 |
|
167 horizon.x = function(z) { |
|
168 if (!arguments.length) return x; |
|
169 x = z; |
|
170 return horizon; |
|
171 }; |
|
172 |
|
173 horizon.y = function(z) { |
|
174 if (!arguments.length) return y; |
|
175 y = z; |
|
176 return horizon; |
|
177 }; |
|
178 |
|
179 horizon.width = function(x) { |
|
180 if (!arguments.length) return w; |
|
181 w = +x; |
|
182 return horizon; |
|
183 }; |
|
184 |
|
185 horizon.height = function(x) { |
|
186 if (!arguments.length) return h; |
|
187 h = +x; |
|
188 return horizon; |
|
189 }; |
|
190 |
|
191 return horizon; |
|
192 }; |
|
193 |
|
194 var d3_chart_horizonArea = d3.svg.area(), |
|
195 d3_chart_horizonId = 0; |
|
196 |
|
197 function d3_chart_horizonX(d) { |
|
198 return d[0]; |
|
199 } |
|
200 |
|
201 function d3_chart_horizonY(d) { |
|
202 return d[1]; |
|
203 } |