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