1 /**
  2  * Constructs a new, empty horizon layout. Layouts are not typically constructed
  3  * directly; instead, they are added to an existing panel via
  4  * {@link pv.Mark#add}.
  5  *
  6  * @class Implements a horizon layout, which is a variation of a single-series
  7  * area chart where the area is folded into multiple bands. Color is used to
  8  * encode band, allowing the size of the chart to be reduced significantly
  9  * without impeding readability. This layout algorithm is based on the work of
 10  * J. Heer, N. Kong and M. Agrawala in <a
 11  * href="http://hci.stanford.edu/publications/2009/heer-horizon-chi09.pdf">"Sizing
 12  * the Horizon: The Effects of Chart Size and Layering on the Graphical
 13  * Perception of Time Series Visualizations"</a>, CHI 2009.
 14  *
 15  * <p>This layout exports a single <tt>band</tt> mark prototype, which is
 16  * intended to be used with an area mark. The band mark is contained in a panel
 17  * which is replicated per band (and for negative/positive bands). For example,
 18  * to create a simple horizon graph given an array of numbers:
 19  *
 20  * <pre>vis.add(pv.Layout.Horizon)
 21  *     .bands(n)
 22  *   .band.add(pv.Area)
 23  *     .data(data)
 24  *     .left(function() this.index * 35)
 25  *     .height(function(d) d * 40);</pre>
 26  *
 27  * The layout can be further customized by changing the number of bands, and
 28  * toggling whether the negative bands are mirrored or offset. (See the
 29  * above-referenced paper for guidance.)
 30  *
 31  * <p>The <tt>fillStyle</tt> of the area can be overridden, though typically it
 32  * is easier to customize the layout's behavior through the custom
 33  * <tt>backgroundStyle</tt>, <tt>positiveStyle</tt> and <tt>negativeStyle</tt>
 34  * properties. By default, the background is white, positive bands are blue, and
 35  * negative bands are red. For the most accurate presentation, use fully-opaque
 36  * colors of equal intensity for the negative and positive bands.
 37  *
 38  * @extends pv.Layout
 39  */
 40 pv.Layout.Horizon = function() {
 41   pv.Layout.call(this);
 42   var that = this,
 43       bands, // cached bands
 44       mode, // cached mode
 45       size, // cached height
 46       fill, // cached background style
 47       red, // cached negative color (ramp)
 48       blue, // cached positive color (ramp)
 49       buildImplied = this.buildImplied;
 50 
 51   /** @private Cache the layout state to optimize properties. */
 52   this.buildImplied = function(s) {
 53     buildImplied.call(this, s);
 54     bands = s.bands;
 55     mode = s.mode;
 56     size = Math.round((mode == "color" ? .5 : 1) * s.height);
 57     fill = s.backgroundStyle;
 58     red = pv.ramp(fill, s.negativeStyle).domain(0, bands);
 59     blue = pv.ramp(fill, s.positiveStyle).domain(0, bands);
 60   };
 61 
 62   var bands = new pv.Panel()
 63       .data(function() { return pv.range(bands * 2); })
 64       .overflow("hidden")
 65       .height(function() { return size; })
 66       .top(function(i) { return mode == "color" ? (i & 1) * size : 0; })
 67       .fillStyle(function(i) { return i ? null : fill; });
 68 
 69   /**
 70    * The band prototype. This prototype is intended to be used with an Area
 71    * mark to render the horizon bands.
 72    *
 73    * @type pv.Mark
 74    * @name pv.Layout.Horizon.prototype.band
 75    */
 76   this.band = new pv.Mark()
 77       .top(function(d, i) {
 78           return mode == "mirror" && i & 1
 79               ? (i + 1 >> 1) * size
 80               : null;
 81         })
 82       .bottom(function(d, i) {
 83           return mode == "mirror"
 84               ? (i & 1 ? null : (i + 1 >> 1) * -size)
 85               : ((i & 1 || -1) * (i + 1 >> 1) * size);
 86         })
 87       .fillStyle(function(d, i) {
 88           return (i & 1 ? red : blue)((i >> 1) + 1);
 89         });
 90 
 91   this.band.add = function(type) {
 92     return that.add(pv.Panel).extend(bands).add(type).extend(this);
 93   };
 94 };
 95 
 96 pv.Layout.Horizon.prototype = pv.extend(pv.Layout)
 97     .property("bands", Number)
 98     .property("mode", String)
 99     .property("backgroundStyle", pv.color)
100     .property("positiveStyle", pv.color)
101     .property("negativeStyle", pv.color);
102 
103 /**
104  * Default properties for horizon layouts. By default, there are two bands, the
105  * mode is "offset", the background style is "white", the positive style is
106  * blue, negative style is red.
107  *
108  * @type pv.Layout.Horizon
109  */
110 pv.Layout.Horizon.prototype.defaults = new pv.Layout.Horizon()
111     .extend(pv.Layout.prototype.defaults)
112     .bands(2)
113     .mode("offset")
114     .backgroundStyle("white")
115     .positiveStyle("#1f77b4")
116     .negativeStyle("#d62728");
117 
118 /**
119  * The horizon mode: offset, mirror, or color. The default is "offset".
120  *
121  * @type string
122  * @name pv.Layout.Horizon.prototype.mode
123  */
124 
125 /**
126  * The number of bands. Must be at least one. The default value is two.
127  *
128  * @type number
129  * @name pv.Layout.Horizon.prototype.bands
130  */
131 
132 /**
133  * The positive band color; if non-null, the interior of positive bands are
134  * filled with the specified color. The default value of this property is blue.
135  * For accurate blending, this color should be fully opaque.
136  *
137  * @type pv.Color
138  * @name pv.Layout.Horizon.prototype.positiveStyle
139  */
140 
141 /**
142  * The negative band color; if non-null, the interior of negative bands are
143  * filled with the specified color. The default value of this property is red.
144  * For accurate blending, this color should be fully opaque.
145  *
146  * @type pv.Color
147  * @name pv.Layout.Horizon.prototype.negativeStyle
148  */
149 
150 /**
151  * The background color. The panel background is filled with the specified
152  * color, and the negative and positive bands are filled with an interpolated
153  * color between this color and the respective band color. The default value of
154  * this property is white. For accurate blending, this color should be fully
155  * opaque.
156  *
157  * @type pv.Color
158  * @name pv.Layout.Horizon.prototype.backgroundStyle
159  */
160