1 package flare.vis.operator.layout
3 import flare.animate.Transitioner;
4 import flare.util.Arrays;
5 import flare.vis.data.NodeSprite;
7 import flash.geom.Rectangle;
10 * Layout that places tree nodes in a radial layout, laying out subsequent
11 * depth levels of a tree on circles of progressively increasing radius.
12 * This layout can be used for both node-link diagrams, where nodes are
13 * connected by edges, and for radial space-filling ("starburst") diagrams.
14 * To generate space-filling layouts, nodes should have their shape
15 * property set to <code>Shapes.WEDGE</code> and the layout instance should
16 * have the <code>useNodeSize<code> property set to false.
18 * <p>The algorithm used is an adaptation of a technique by Ka-Ping Yee,
19 * Danyel Fisher, Rachna Dhamija, and Marti Hearst, published in the paper
20 * <a href="http://citeseer.ist.psu.edu/448292.html">Animated Exploration of
21 * Dynamic Graphs with Radial Layout</a>, InfoVis 2001. This algorithm computes
22 * a radial layout which factors in possible variation in sizes, and maintains
23 * both orientation and ordering constraints to facilitate smooth and
24 * understandable transitions between layout configurations.
27 public class RadialTreeLayout extends Layout
29 // -- Properties ------------------------------------------------------
31 /** Property name for storing parameters for this layout. */
32 public static const PARAMS:String = "radialTreeLayoutParams";
33 /** The default radius increment between depth levels. */
34 public static const DEFAULT_RADIUS:int = 50;
36 private var _maxDepth:int = 0;
37 private var _radiusInc:Number = DEFAULT_RADIUS;
38 private var _theta1:Number = Math.PI/2;
39 private var _theta2:Number = Math.PI/2 + 2*Math.PI;
40 private var _sortAngles:Boolean = true;
41 private var _setTheta:Boolean = false;
42 private var _autoScale:Boolean = true;
43 private var _useNodeSize:Boolean = true;
44 private var _prevRoot:NodeSprite = null;
46 private var _t:Transitioner;
48 /** The radius increment between depth levels. */
49 public function get radiusIncrement():Number { return _radiusInc; }
50 public function set radiusIncrement(r:Number):void { _radiusInc = r; }
52 /** Flag determining if nodes should be sorted by angles to help
53 * maintain ordering across different spanning-tree configurations.
54 * This sorting is important for understandable transitions when using
55 * a radial layout of a general graph. However, it is unnecessary for
56 * tree structures and increases the running time of layout. */
57 public function get sortAngles():Boolean { return _sortAngles; }
58 public function set sortAngles(b:Boolean):void { _sortAngles = b; }
60 /** Flag indicating if the layout should automatically be scaled to
61 * fit within the layout bounds. */
62 public function get autoScale():Boolean { return _autoScale; }
63 public function set autoScale(b:Boolean):void { _autoScale = b; }
65 /** The initial angle for the radial layout. */
66 public function get startAngle():Number { return _theta1; }
67 public function set startAngle(a:Number):void {
68 _theta2 += a - _theta1;
73 /** The total angular width the layout should use (2*pi by default).*/
74 public function get angularWidth():Number { return _theta2 - _theta1; }
75 public function set angularWidth(w:Number):void {
76 _theta2 = _theta1 + w;
80 /** Flag indicating if node's <code>size</code> property should be
81 * used to determine layout spacing. If a space-filling radial
82 * layout is desired, this value must be false for the layout
84 public function get useNodeSize():Boolean { return _useNodeSize; }
85 public function set useNodeSize(b:Boolean):void {
89 // -- Methods ---------------------------------------------------------
92 * Creates a new RadialTreeLayout.
93 * @param radius the radius increment between depth levels
94 * @param sortAngles flag indicating if nodes should be sorted by angle
95 * to maintain node ordering across spanning-tree configurations
96 * @param autoScale flag indicating if the layout should automatically
97 * be scaled to fit within the layout bounds
99 public function RadialTreeLayout(radius:Number=DEFAULT_RADIUS,
100 sortAngles:Boolean=true, autoScale:Boolean=true)
103 _sortAngles = sortAngles;
104 _autoScale = autoScale;
108 public override function operate(t:Transitioner=null):void
110 _t = (t!=null ? t : Transitioner.DEFAULT);
112 var n:NodeSprite = layoutRoot as NodeSprite;
113 if (n == null) { _t = null; return; }
114 var np:Params = params(n);
116 // calc relative widths and maximum tree depth
117 // performs one pass over the tree
119 calcAngularWidth(n, 0);
121 if (_autoScale) setScale(layoutBounds);
122 if (!_setTheta) calcAngularBounds(n);
124 // perform the layout
126 layout(n, _radiusInc, _theta1, _theta2);
127 } else if (n.childDegree > 0) {
128 n.visitTreeDepthFirst(function(n:NodeSprite):void {
131 _t.$(n).mouseEnabled = false;
132 if (n.parentEdge != null) {
133 _t.$(n.parentEdge).alpha = 0;
138 // update properties of the root node
139 np.angle = _theta2 - _theta1;
140 update(n, 0, 0, np.angle, true);
145 private function setScale(bounds:Rectangle):void
147 var r:Number = Math.min(bounds.width, bounds.height)/2.0;
148 if (_maxDepth > 0) _radiusInc = r / (_maxDepth+1);
152 * Calculates the angular bounds of the layout, attempting to
153 * preserve the angular orientation of the display across transitions.
155 private function calcAngularBounds(r:NodeSprite):void
157 if (_prevRoot == null || r == _prevRoot)
159 _prevRoot = r; return;
162 // try to find previous parent of root
163 var p:NodeSprite = _prevRoot, pp:NodeSprite;
168 } else if (pp == null) {
175 // compute offset due to children's angular width
178 for each (var n:NodeSprite in sortedChildren(r)) {
180 dt += params(n).width;
183 var rw:Number = params(r).width;
184 var pw:Number = params(p).width;
185 dt = -2*Math.PI * (dt+pw/2)/rw;
187 // set angular bounds
188 _theta1 = dt + Math.atan2(p.y-r.y, p.x-r.x);
189 _theta2 = _theta1 + 2*Math.PI;
194 * Computes relative measures of the angular widths of each
195 * expanded subtree. Node diameters are taken into account
196 * to improve space allocation for variable-sized nodes.
198 * This method also updates the base angle value for nodes
199 * to ensure proper ordering of nodes.
201 private function calcAngularWidth(n:NodeSprite, d:int):Number
203 if (d > _maxDepth) _maxDepth = d;
204 var aw:Number = 0, diameter:Number = 0;
205 if (_useNodeSize && d > 0) {
207 diameter = n.expanded && n.childDegree > 0 ? 0 : _t.$(n).size;
209 var w:Number = n.width, h:Number = n.height;
210 diameter = Math.sqrt(w*w+h*h)/d;
211 if (isNaN(diameter)) diameter = 0;
214 if (n.expanded && n.childDegree > 0) {
215 for (var c:NodeSprite=n.firstChildNode; c!=null; c=c.nextNode)
217 aw += calcAngularWidth(c, d+1);
219 aw = Math.max(diameter, aw);
223 params(n).width = aw;
227 private static function normalize(angle:Number):Number
229 while (angle > 2*Math.PI)
236 private static function minDist(a1:Number, a2:Number):Number
238 var d1:Number = a2 - a1;
239 var d2:Number = Math.abs(d1 - 2*Math.PI);
240 var d3:Number = Math.abs(d1 + 2*Math.PI);
241 var dd:Number = Math.min(d1, d2, d3);
245 } else if (dd == d2) {
246 return a2 - 2*Math.PI;
248 return a2 + 2*Math.PI;
252 private function sortedChildren(n:NodeSprite):Array
254 var cc:int = n.childDegree;
255 if (cc == 0) return Arrays.EMPTY;
256 var angles:Array = new Array(cc);
259 // update base angle for node ordering
260 var base:Number = -_theta1;
261 var p:NodeSprite = n.parentNode;
262 if (p != null) base = normalize(Math.atan2(p.y-n.y, n.x-p.x));
264 // collect the angles
265 var c:NodeSprite = n.firstChildNode;
266 for (var i:uint=0; i<cc; ++i, c=c.nextNode) {
267 angles[i] = normalize(-base + Math.atan2(c.y-n.y,n.x-c.x));
269 // get array of indices, sorted by angle
270 angles = angles.sort(Array.NUMERIC | Array.RETURNINDEXEDARRAY);
271 // switch in the actual nodes and return
272 for (i=0; i<cc; ++i) {
273 angles[i] = n.getChildNode(angles[i]);
276 for (i=0; i<cc; ++i) {
277 angles[i] = n.getChildNode(i);
285 * Compute the layout.
286 * @param n the root of the current subtree under consideration
287 * @param r the radius, current distance from the center
288 * @param theta1 the start (in radians) of this subtree's angular region
289 * @param theta2 the end (in radians) of this subtree's angular region
291 private function layout(n:NodeSprite, r:Number, theta1:Number, theta2:Number):void
293 var dtheta:Number = (theta2-theta1);
294 var dtheta2:Number = dtheta / 2.0;
295 var width:Number = params(n).width;
296 var cfrac:Number, nfrac:Number = 0;
298 for each (var c:NodeSprite in sortedChildren(n)) {
299 var cp:Params = params(c);
300 cfrac = cp.width / width;
301 if (c.expanded && c.childDegree > 0)
303 layout(c, r+_radiusInc, theta1 + nfrac*dtheta,
304 theta1 + (nfrac+cfrac)*dtheta);
306 else if (c.childDegree > 0)
308 var cr:Number = r + _radiusInc;
309 var ca:Number = theta1 + nfrac*dtheta + cfrac*dtheta2;
311 c.visitTreeDepthFirst(function(n:NodeSprite):void {
312 update(n, cr, minDist(n.angle, ca), 0, false);
316 var a:Number = minDist(c.angle, theta1 + nfrac*dtheta + cfrac*dtheta2);
317 cp.angle = cfrac * dtheta;
318 update(c, r, a, cp.angle, true);
323 private function update(n:NodeSprite, r:Number, a:Number,
324 aw:Number, v:Boolean) : void
326 var o:Object = _t.$(n), alpha:Number = v ? 1 : 0;
329 o.h = r + _radiusInc/2;
330 o.v = r - _radiusInc/2;
335 if (n.parentEdge != null)
336 _t.$(n.parentEdge).alpha = alpha;
339 private function params(n:NodeSprite):Params
341 var p:Params = n.props[PARAMS];
349 } // end of class RadialTreeLayout
353 public var width:Number = 0;
354 public var angle:Number = 0;