1 /** 2 * Constructs a new, empty stack 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 layout for stacked visualizations, ranging from simple 7 * stacked bar charts to more elaborate "streamgraphs" composed of stacked 8 * areas. Stack layouts uses length as a visual encoding, as opposed to 9 * position, as the layers do not share an aligned axis. 10 * 11 * <p>Marks can be stacked vertically or horizontally. For example, 12 * 13 * <pre>vis.add(pv.Layout.Stack) 14 * .layers([[1, 1.2, 1.7, 1.5, 1.7], 15 * [.5, 1, .8, 1.1, 1.3], 16 * [.2, .5, .8, .9, 1]]) 17 * .x(function() this.index * 35) 18 * .y(function(d) d * 40) 19 * .layer.add(pv.Area);</pre> 20 * 21 * specifies a vertically-stacked area chart, using the default "bottom-left" 22 * orientation with "zero" offset. This visualization can be easily changed into 23 * a streamgraph using the "wiggle" offset, which attempts to minimize change in 24 * slope weighted by layer thickness. See the {@link #offset} property for more 25 * supported streamgraph algorithms. 26 * 27 * <p>In the simplest case, the layer data can be specified as a two-dimensional 28 * array of numbers. The <tt>x</tt> and <tt>y</tt> psuedo-properties are used to 29 * define the thickness of each layer at the given position, respectively; in 30 * the above example of the "bottom-left" orientation, the <tt>x</tt> and 31 * <tt>y</tt> psuedo-properties are equivalent to the <tt>left</tt> and 32 * <tt>height</tt> properties that you might use if you implemented a stacked 33 * area by hand. 34 * 35 * <p>The advantage of using the stack layout is that the baseline, i.e., the 36 * <tt>bottom</tt> property is computed automatically using the specified offset 37 * algorithm. In addition, the order of layers can be computed using a built-in 38 * algorithm via the <tt>order</tt> property. 39 * 40 * <p>With the exception of the "expand" <tt>offset</tt>, the stack layout does 41 * not perform any automatic scaling of data; the values returned from 42 * <tt>x</tt> and <tt>y</tt> specify pixel sizes. To simplify scaling math, use 43 * this layout in conjunction with {@link pv.Scale.linear} or similar. 44 * 45 * <p>In other cases, the <tt>values</tt> psuedo-property can be used to define 46 * the data more flexibly. As with a typical panel & area, the 47 * <tt>layers</tt> property corresponds to the data in the enclosing panel, 48 * while the <tt>values</tt> psuedo-property corresponds to the data for the 49 * area within the panel. For example, given an array of data values: 50 * 51 * <pre>var crimea = [ 52 * { date: "4/1854", wounds: 0, other: 110, disease: 110 }, 53 * { date: "5/1854", wounds: 0, other: 95, disease: 105 }, 54 * { date: "6/1854", wounds: 0, other: 40, disease: 95 }, 55 * ...</pre> 56 * 57 * and a corresponding array of series names: 58 * 59 * <pre>var causes = ["wounds", "other", "disease"];</pre> 60 * 61 * Separate layers can be defined for each cause like so: 62 * 63 * <pre>vis.add(pv.Layout.Stack) 64 * .layers(causes) 65 * .values(crimea) 66 * .x(function(d) x(d.date)) 67 * .y(function(d, p) y(d[p])) 68 * .layer.add(pv.Area) 69 * ...</pre> 70 * 71 * As with the panel & area case, the datum that is passed to the 72 * psuedo-properties <tt>x</tt> and <tt>y</tt> are the values (an element in 73 * <tt>crimea</tt>); the second argument is the layer data (a string in 74 * <tt>causes</tt>). Additional arguments specify the data of enclosing panels, 75 * if any. 76 * 77 * @extends pv.Layout 78 */ 79 pv.Layout.Stack = function() { 80 pv.Layout.call(this); 81 var that = this, 82 /** @ignore */ none = function() { return null; }, 83 prop = {t: none, l: none, r: none, b: none, w: none, h: none}, 84 values, 85 buildImplied = that.buildImplied; 86 87 /** @private Proxy the given property on the layer. */ 88 function proxy(name) { 89 return function() { 90 return prop[name](this.parent.index, this.index); 91 }; 92 } 93 94 /** @private Compute the layout! */ 95 this.buildImplied = function(s) { 96 buildImplied.call(this, s); 97 98 var data = s.layers, 99 n = data.length, 100 m, 101 orient = s.orient, 102 horizontal = /^(top|bottom)\b/.test(orient), 103 h = this.parent[horizontal ? "height" : "width"](), 104 x = [], 105 y = [], 106 dy = []; 107 108 /* 109 * Iterate over the data, evaluating the values, x and y functions. The 110 * context in which the x and y psuedo-properties are evaluated is a 111 * pseudo-mark that is a grandchild of this layout. 112 */ 113 var stack = pv.Mark.stack, o = {parent: {parent: this}}; 114 stack.unshift(null); 115 values = []; 116 for (var i = 0; i < n; i++) { 117 dy[i] = []; 118 y[i] = []; 119 o.parent.index = i; 120 stack[0] = data[i]; 121 values[i] = this.$values.apply(o.parent, stack); 122 if (!i) m = values[i].length; 123 stack.unshift(null); 124 for (var j = 0; j < m; j++) { 125 stack[0] = values[i][j]; 126 o.index = j; 127 if (!i) x[j] = this.$x.apply(o, stack); 128 dy[i][j] = this.$y.apply(o, stack); 129 } 130 stack.shift(); 131 } 132 stack.shift(); 133 134 /* order */ 135 var index; 136 switch (s.order) { 137 case "inside-out": { 138 var max = dy.map(function(v) { return pv.max.index(v); }), 139 map = pv.range(n).sort(function(a, b) { return max[a] - max[b]; }), 140 sums = dy.map(function(v) { return pv.sum(v); }), 141 top = 0, 142 bottom = 0, 143 tops = [], 144 bottoms = []; 145 for (var i = 0; i < n; i++) { 146 var j = map[i]; 147 if (top < bottom) { 148 top += sums[j]; 149 tops.push(j); 150 } else { 151 bottom += sums[j]; 152 bottoms.push(j); 153 } 154 } 155 index = bottoms.reverse().concat(tops); 156 break; 157 } 158 case "reverse": index = pv.range(n - 1, -1, -1); break; 159 default: index = pv.range(n); break; 160 } 161 162 /* offset */ 163 switch (s.offset) { 164 case "silohouette": { 165 for (var j = 0; j < m; j++) { 166 var o = 0; 167 for (var i = 0; i < n; i++) o += dy[i][j]; 168 y[index[0]][j] = (h - o) / 2; 169 } 170 break; 171 } 172 case "wiggle": { 173 var o = 0; 174 for (var i = 0; i < n; i++) o += dy[i][0]; 175 y[index[0]][0] = o = (h - o) / 2; 176 for (var j = 1; j < m; j++) { 177 var s1 = 0, s2 = 0, dx = x[j] - x[j - 1]; 178 for (var i = 0; i < n; i++) s1 += dy[i][j]; 179 for (var i = 0; i < n; i++) { 180 var s3 = (dy[index[i]][j] - dy[index[i]][j - 1]) / (2 * dx); 181 for (var k = 0; k < i; k++) { 182 s3 += (dy[index[k]][j] - dy[index[k]][j - 1]) / dx; 183 } 184 s2 += s3 * dy[index[i]][j]; 185 } 186 y[index[0]][j] = o -= s1 ? s2 / s1 * dx : 0; 187 } 188 break; 189 } 190 case "expand": { 191 for (var j = 0; j < m; j++) { 192 y[index[0]][j] = 0; 193 var k = 0; 194 for (var i = 0; i < n; i++) k += dy[i][j]; 195 if (k) { 196 k = h / k; 197 for (var i = 0; i < n; i++) dy[i][j] *= k; 198 } else { 199 k = h / n; 200 for (var i = 0; i < n; i++) dy[i][j] = k; 201 } 202 } 203 break; 204 } 205 default: { 206 for (var j = 0; j < m; j++) y[index[0]][j] = 0; 207 break; 208 } 209 } 210 211 /* Propagate the offset to the other series. */ 212 for (var j = 0; j < m; j++) { 213 var o = y[index[0]][j]; 214 for (var i = 1; i < n; i++) { 215 o += dy[index[i - 1]][j]; 216 y[index[i]][j] = o; 217 } 218 } 219 220 /* Find the property definitions for dynamic substitution. */ 221 var i = orient.indexOf("-"), 222 pdy = horizontal ? "h" : "w", 223 px = i < 0 ? (horizontal ? "l" : "b") : orient.charAt(i + 1), 224 py = orient.charAt(0); 225 for (var p in prop) prop[p] = none; 226 prop[px] = function(i, j) { return x[j]; }; 227 prop[py] = function(i, j) { return y[i][j]; }; 228 prop[pdy] = function(i, j) { return dy[i][j]; }; 229 }; 230 231 /** 232 * The layer prototype. This prototype is intended to be used with an area, 233 * bar or panel mark (or subclass thereof). Other mark types may be possible, 234 * though note that the stack layout is not currently designed to support 235 * radial stacked visualizations using wedges. 236 * 237 * <p>The layer is not a direct child of the stack layout; a hidden panel is 238 * used to replicate layers. 239 * 240 * @type pv.Mark 241 * @name pv.Layout.Stack.prototype.layer 242 */ 243 this.layer = new pv.Mark() 244 .data(function() { return values[this.parent.index]; }) 245 .top(proxy("t")) 246 .left(proxy("l")) 247 .right(proxy("r")) 248 .bottom(proxy("b")) 249 .width(proxy("w")) 250 .height(proxy("h")); 251 252 this.layer.add = function(type) { 253 return that.add(pv.Panel) 254 .data(function() { return that.layers(); }) 255 .add(type) 256 .extend(this); 257 }; 258 }; 259 260 pv.Layout.Stack.prototype = pv.extend(pv.Layout) 261 .property("orient", String) 262 .property("offset", String) 263 .property("order", String) 264 .property("layers"); 265 266 /** 267 * Default properties for stack layouts. The default orientation is 268 * "bottom-left", the default offset is "zero", and the default layers is 269 * <tt>[[]]</tt>. 270 * 271 * @type pv.Layout.Stack 272 */ 273 pv.Layout.Stack.prototype.defaults = new pv.Layout.Stack() 274 .extend(pv.Layout.prototype.defaults) 275 .orient("bottom-left") 276 .offset("zero") 277 .layers([[]]); 278 279 /** @private */ 280 pv.Layout.Stack.prototype.$x 281 = /** @private */ pv.Layout.Stack.prototype.$y 282 = function() { return 0; }; 283 284 /** 285 * The x psuedo-property; determines the position of the value within the layer. 286 * This typically corresponds to the independent variable. For example, with the 287 * default "bottom-left" orientation, this function defines the "left" property. 288 * 289 * @param {function} f the x function. 290 * @returns {pv.Layout.Stack} this. 291 */ 292 pv.Layout.Stack.prototype.x = function(f) { 293 /** @private */ this.$x = pv.functor(f); 294 return this; 295 }; 296 297 /** 298 * The y psuedo-property; determines the thickness of the layer at the given 299 * value. This typically corresponds to the dependent variable. For example, 300 * with the default "bottom-left" orientation, this function defines the 301 * "height" property. 302 * 303 * @param {function} f the y function. 304 * @returns {pv.Layout.Stack} this. 305 */ 306 pv.Layout.Stack.prototype.y = function(f) { 307 /** @private */ this.$y = pv.functor(f); 308 return this; 309 }; 310 311 /** @private The default value function; identity. */ 312 pv.Layout.Stack.prototype.$values = pv.identity; 313 314 /** 315 * The values function; determines the values for a given layer. The default 316 * value is the identity function, which assumes that the layers property is 317 * specified as a two-dimensional (i.e., nested) array. 318 * 319 * @param {function} f the values function. 320 * @returns {pv.Layout.Stack} this. 321 */ 322 pv.Layout.Stack.prototype.values = function(f) { 323 this.$values = pv.functor(f); 324 return this; 325 }; 326 327 /** 328 * The layer data in row-major order. The value of this property is typically a 329 * two-dimensional (i.e., nested) array, but any array can be used, provided the 330 * values psuedo-property is defined accordingly. 331 * 332 * @type array[] 333 * @name pv.Layout.Stack.prototype.layers 334 */ 335 336 /** 337 * The layer orientation. The following values are supported:<ul> 338 * 339 * <li>bottom-left == bottom 340 * <li>bottom-right 341 * <li>top-left == top 342 * <li>top-right 343 * <li>left-top 344 * <li>left-bottom == left 345 * <li>right-top 346 * <li>right-bottom == right 347 * 348 * </ul>. The default value is "bottom-left", which means that the layers will 349 * be built from the bottom-up, and the values within layers will be laid out 350 * from left-to-right. 351 * 352 * <p>Note that with non-zero baselines, some orientations may give similar 353 * results. For example, offset("silohouette") centers the layers, resulting in 354 * a streamgraph. Thus, the orientations "bottom-left" and "top-left" will 355 * produce similar results, differing only in the layer order. 356 * 357 * @type string 358 * @name pv.Layout.Stack.prototype.orient 359 */ 360 361 /** 362 * The layer order. The following values are supported:<ul> 363 * 364 * <li><i>null</i> - use given layer order. 365 * <li>inside-out - sort by maximum value, with balanced order. 366 * <li>reverse - use reverse of given layer order. 367 * 368 * </ul>For details on the inside-out order algorithm, refer to "Stacked Graphs 369 * -- Geometry & Aesthetics" by L. Byron and M. Wattenberg, IEEE TVCG 370 * November/December 2008. 371 * 372 * @type string 373 * @name pv.Layout.Stack.prototype.order 374 */ 375 376 /** 377 * The layer offset; the y-position of the bottom of the lowest layer. The 378 * following values are supported:<ul> 379 * 380 * <li>zero - use a zero baseline, i.e., the y-axis. 381 * <li>silohouette - center the stream, i.e., ThemeRiver. 382 * <li>wiggle - minimize weighted change in slope. 383 * <li>expand - expand layers to fill the enclosing layout dimensions. 384 * 385 * </ul>For details on these offset algorithms, refer to "Stacked Graphs -- 386 * Geometry & Aesthetics" by L. Byron and M. Wattenberg, IEEE TVCG 387 * November/December 2008. 388 * 389 * @type string 390 * @name pv.Layout.Stack.prototype.offset 391 */ 392