1 /** 2 * Constructs a new, empty hierarchy layout. Layouts are not typically 3 * constructed directly; instead, they are added to an existing panel via 4 * {@link pv.Mark#add}. 5 * 6 * @class Represents an abstract layout for hierarchy diagrams. This class is a 7 * specialization of {@link pv.Layout.Network}, providing the basic structure 8 * for both hierarchical node-link diagrams (such as Reingold-Tilford trees) and 9 * space-filling hierarchy diagrams (such as sunbursts and treemaps). 10 * 11 * <p>Unlike general network layouts, the <tt>links</tt> property need not be 12 * defined explicitly. Instead, the links are computed implicitly from the 13 * <tt>parentNode</tt> attribute of the node objects, as defined by the 14 * <tt>nodes</tt> property. This implementation is also available as 15 * {@link #links}, for reuse with non-hierarchical layouts; for example, to 16 * render a tree using force-directed layout. 17 * 18 * <p>Correspondingly, the <tt>nodes</tt> property is represented as a union of 19 * {@link pv.Layout.Network.Node} and {@link pv.Dom.Node}. To construct a node 20 * hierarchy from a simple JSON map, use the {@link pv.Dom} operator; this 21 * operator also provides an easy way to sort nodes before passing them to the 22 * layout. 23 * 24 * <p>For more details on how to use this layout, see 25 * {@link pv.Layout.Network}. 26 * 27 * @see pv.Layout.Cluster 28 * @see pv.Layout.Partition 29 * @see pv.Layout.Tree 30 * @see pv.Layout.Treemap 31 * @see pv.Layout.Indent 32 * @see pv.Layout.Pack 33 * @extends pv.Layout.Network 34 */ 35 pv.Layout.Hierarchy = function() { 36 pv.Layout.Network.call(this); 37 this.link.strokeStyle("#ccc"); 38 }; 39 40 pv.Layout.Hierarchy.prototype = pv.extend(pv.Layout.Network); 41 42 /** @private Compute the implied links. (Links are null by default.) */ 43 pv.Layout.Hierarchy.prototype.buildImplied = function(s) { 44 if (!s.links) s.links = pv.Layout.Hierarchy.links.call(this); 45 pv.Layout.Network.prototype.buildImplied.call(this, s); 46 }; 47 48 /** The implied links; computes links using the <tt>parentNode</tt> attribute. */ 49 pv.Layout.Hierarchy.links = function() { 50 return this.nodes() 51 .filter(function(n) { return n.parentNode; }) 52 .map(function(n) { 53 return { 54 sourceNode: n, 55 targetNode: n.parentNode, 56 linkValue: 1 57 }; 58 }); 59 }; 60 61 /** @private Provides standard node-link layout based on breadth & depth. */ 62 pv.Layout.Hierarchy.NodeLink = { 63 64 /** @private */ 65 buildImplied: function(s) { 66 var nodes = s.nodes, 67 orient = s.orient, 68 horizontal = /^(top|bottom)$/.test(orient), 69 w = s.width, 70 h = s.height; 71 72 /* Compute default inner and outer radius. */ 73 if (orient == "radial") { 74 var ir = s.innerRadius, or = s.outerRadius; 75 if (ir == null) ir = 0; 76 if (or == null) or = Math.min(w, h) / 2; 77 } 78 79 /** @private Returns the radius of the given node. */ 80 function radius(n) { 81 return n.parentNode ? (n.depth * (or - ir) + ir) : 0; 82 } 83 84 /** @private Returns the angle of the given node. */ 85 function midAngle(n) { 86 return (n.parentNode ? (n.breadth - .25) * 2 * Math.PI : 0); 87 } 88 89 /** @private */ 90 function x(n) { 91 switch (orient) { 92 case "left": return n.depth * w; 93 case "right": return w - n.depth * w; 94 case "top": return n.breadth * w; 95 case "bottom": return w - n.breadth * w; 96 case "radial": return w / 2 + radius(n) * Math.cos(n.midAngle); 97 } 98 } 99 100 /** @private */ 101 function y(n) { 102 switch (orient) { 103 case "left": return n.breadth * h; 104 case "right": return h - n.breadth * h; 105 case "top": return n.depth * h; 106 case "bottom": return h - n.depth * h; 107 case "radial": return h / 2 + radius(n) * Math.sin(n.midAngle); 108 } 109 } 110 111 for (var i = 0; i < nodes.length; i++) { 112 var n = nodes[i]; 113 n.midAngle = orient == "radial" ? midAngle(n) 114 : horizontal ? Math.PI / 2 : 0; 115 n.x = x(n); 116 n.y = y(n); 117 if (n.firstChild) n.midAngle += Math.PI; 118 } 119 } 120 }; 121 122 /** @private Provides standard space-filling layout based on breadth & depth. */ 123 pv.Layout.Hierarchy.Fill = { 124 125 /** @private */ 126 constructor: function() { 127 this.node 128 .strokeStyle("#fff") 129 .fillStyle("#ccc") 130 .width(function(n) { return n.dx; }) 131 .height(function(n) { return n.dy; }) 132 .innerRadius(function(n) { return n.innerRadius; }) 133 .outerRadius(function(n) { return n.outerRadius; }) 134 .startAngle(function(n) { return n.startAngle; }) 135 .angle(function(n) { return n.angle; }); 136 137 this.label 138 .textAlign("center") 139 .left(function(n) { return n.x + (n.dx / 2); }) 140 .top(function(n) { return n.y + (n.dy / 2); }); 141 142 /* Hide unsupported link. */ 143 delete this.link; 144 }, 145 146 /** @private */ 147 buildImplied: function(s) { 148 var nodes = s.nodes, 149 orient = s.orient, 150 horizontal = /^(top|bottom)$/.test(orient), 151 w = s.width, 152 h = s.height, 153 depth = -nodes[0].minDepth; 154 155 /* Compute default inner and outer radius. */ 156 if (orient == "radial") { 157 var ir = s.innerRadius, or = s.outerRadius; 158 if (ir == null) ir = 0; 159 if (ir) depth *= 2; // use full depth step for root 160 if (or == null) or = Math.min(w, h) / 2; 161 } 162 163 /** @private Scales the specified depth for a space-filling layout. */ 164 function scale(d, depth) { 165 return (d + depth) / (1 + depth); 166 } 167 168 /** @private */ 169 function x(n) { 170 switch (orient) { 171 case "left": return scale(n.minDepth, depth) * w; 172 case "right": return (1 - scale(n.maxDepth, depth)) * w; 173 case "top": return n.minBreadth * w; 174 case "bottom": return (1 - n.maxBreadth) * w; 175 case "radial": return w / 2; 176 } 177 } 178 179 /** @private */ 180 function y(n) { 181 switch (orient) { 182 case "left": return n.minBreadth * h; 183 case "right": return (1 - n.maxBreadth) * h; 184 case "top": return scale(n.minDepth, depth) * h; 185 case "bottom": return (1 - scale(n.maxDepth, depth)) * h; 186 case "radial": return h / 2; 187 } 188 } 189 190 /** @private */ 191 function dx(n) { 192 switch (orient) { 193 case "left": 194 case "right": return (n.maxDepth - n.minDepth) / (1 + depth) * w; 195 case "top": 196 case "bottom": return (n.maxBreadth - n.minBreadth) * w; 197 case "radial": return n.parentNode ? (n.innerRadius + n.outerRadius) * Math.cos(n.midAngle) : 0; 198 } 199 } 200 201 /** @private */ 202 function dy(n) { 203 switch (orient) { 204 case "left": 205 case "right": return (n.maxBreadth - n.minBreadth) * h; 206 case "top": 207 case "bottom": return (n.maxDepth - n.minDepth) / (1 + depth) * h; 208 case "radial": return n.parentNode ? (n.innerRadius + n.outerRadius) * Math.sin(n.midAngle) : 0; 209 } 210 } 211 212 /** @private */ 213 function innerRadius(n) { 214 return Math.max(0, scale(n.minDepth, depth / 2)) * (or - ir) + ir; 215 } 216 217 /** @private */ 218 function outerRadius(n) { 219 return scale(n.maxDepth, depth / 2) * (or - ir) + ir; 220 } 221 222 /** @private */ 223 function startAngle(n) { 224 return (n.parentNode ? n.minBreadth - .25 : 0) * 2 * Math.PI; 225 } 226 227 /** @private */ 228 function angle(n) { 229 return (n.parentNode ? n.maxBreadth - n.minBreadth : 1) * 2 * Math.PI; 230 } 231 232 for (var i = 0; i < nodes.length; i++) { 233 var n = nodes[i]; 234 n.x = x(n); 235 n.y = y(n); 236 if (orient == "radial") { 237 n.innerRadius = innerRadius(n); 238 n.outerRadius = outerRadius(n); 239 n.startAngle = startAngle(n); 240 n.angle = angle(n); 241 n.midAngle = n.startAngle + n.angle / 2; 242 } else { 243 n.midAngle = horizontal ? -Math.PI / 2 : 0; 244 } 245 n.dx = dx(n); 246 n.dy = dy(n); 247 } 248 } 249 }; 250