toolkit/exemples/couple/javascript/d3/src/chart/horizon.js
changeset 47 c0b4a8b5a012
equal deleted inserted replaced
46:efd9c589177a 47:c0b4a8b5a012
       
     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 }