source: Dev/trunk/d3/src/chart/horizon.js @ 76

Last change on this file since 76 was 76, checked in by fpvanagthoven, 14 years ago

d3

File size: 5.9 KB
Line 
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
8d3.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
194var d3_chart_horizonArea = d3.svg.area(),
195    d3_chart_horizonId = 0;
196
197function d3_chart_horizonX(d) {
198  return d[0];
199}
200
201function d3_chart_horizonY(d) {
202  return d[1];
203}
Note: See TracBrowser for help on using the repository browser.