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