package flare.vis.operator.layout { import flare.util.Orientation; import flare.util.Vectors; import flare.vis.data.NodeSprite; import flash.geom.Point; import flash.geom.Rectangle; /** * Layout that places nodes using a tidy layout of a node-link tree * diagram. This algorithm lays out a rooted tree such that each * depth level of the tree is on a shared line. The orientation of the * tree can be set such that the tree goes left-to-right (default), * right-to-left, top-to-bottom, or bottom-to-top. * *

The algorithm used is that of Christoph Buchheim, Michael Jünger, * and Sebastian Leipert from their research paper * * Improving Walker's Algorithm to Run in Linear Time, Graph Drawing 2002. * This algorithm corrects performance issues in Walker's algorithm, which * generalizes Reingold and Tilford's method for tidy drawings of trees to * support trees with an arbitrary number of children at any given node.

*/ public class NodeLinkTreeLayout extends Layout { // -- Properties ------------------------------------------------------ /** Property name for storing parameters for this layout. */ public static const PARAMS:String = "nodeLinkTreeLayoutParams"; private var _orient:String = Orientation.LEFT_TO_RIGHT; // orientation private var _bspace:Number = 5; // the spacing between sibling nodes private var _tspace:Number = 25; // the spacing between subtrees private var _dspace:Number = 50; // the spacing between depth levels private var _depths:Vector. = new Vector.(20); // stores depth co-ords private var _maxDepth:int = 0; private var _ax:Number, _ay:Number; // for holding anchor co-ordinates /** The orientation of the layout. */ public function get orientation():String { return _orient; } public function set orientation(o:String):void { _orient = o; } /** The space between successive depth levels of the tree. */ public function get depthSpacing():Number { return _dspace; } public function set depthSpacing(s:Number):void { _dspace = s; } /** The space between siblings in the tree. */ public function get breadthSpacing():Number { return _bspace; } public function set breadthSpacing(s:Number):void { _bspace = s; } /** The space between different sub-trees. */ public function get subtreeSpacing():Number { return _tspace; } public function set subtreeSpacing(s:Number):void { _tspace = s; } // -- Methods --------------------------------------------------------- /** * Creates a new NodeLinkTreeLayout. * @param orientation the orientation of the layout * @param depthSpace the space between depth levels in the tree * @param breadthSpace the space between siblings in the tree * @param subtreeSpace the space between different sub-trees */ public function NodeLinkTreeLayout( orientation:String=Orientation.LEFT_TO_RIGHT, depthSpace:Number=50, breadthSpace:Number=5, subtreeSpace:Number=25) { _orient = orientation; _dspace = depthSpace; _bspace = breadthSpace; _tspace = subtreeSpace; } /** @inheritDoc */ protected override function layout():void { Vectors.fill(_depths, 0); _maxDepth = 0; var root:NodeSprite = layoutRoot as NodeSprite; if (root == null) { _t = null; return; } var rp:Params = params(root); firstWalk(root, 0, 1); // breadth/depth stats var a:Point = layoutAnchor; _ax = a.x; _ay = a.y; // determine anchor determineDepths(); // sum depth info secondWalk(root, null, -rp.prelim, 0, true); // assign positions updateEdgePoints(_t); // update edges } protected override function autoAnchor():void { // otherwise generate anchor based on the bounds var b:Rectangle = layoutBounds; var r:NodeSprite = layoutRoot as NodeSprite; switch (_orient) { case Orientation.LEFT_TO_RIGHT: _ax = b.x + _dspace + r.w; _ay = b.y + b.height / 2; break; case Orientation.RIGHT_TO_LEFT: _ax = b.width - (_dspace + r.w); _ay = b.y + b.height / 2; break; case Orientation.TOP_TO_BOTTOM: _ax = b.x + b.width / 2; _ay = b.y + _dspace + r.h; break; case Orientation.BOTTOM_TO_TOP: _ax = b.x + b.width / 2; _ay = b.height - (_dspace + r.h); break; default: throw new Error("Unrecognized orientation value"); } _anchor.x = _ax; _anchor.y = _ay; } private function firstWalk(n:NodeSprite, num:int, depth:uint):void { setSizes(n); updateDepths(depth, n); var np:Params = params(n); np.number = num; var expanded:Boolean = n.expanded; if (n.childDegree == 0 || !expanded) // is leaf { var l:NodeSprite = n.prevNode; np.prelim = l==null ? 0 : params(l).prelim + spacing(l,n,true); } else if (expanded) // has children, is expanded { var midpoint:Number, i:uint; var lefty:NodeSprite = n.firstChildNode; var right:NodeSprite = n.lastChildNode; var ancestor:NodeSprite = lefty; var c:NodeSprite = lefty; for (i=0; c != null; ++i, c = c.nextNode) { firstWalk(c, i, depth+1); ancestor = apportion(c, ancestor); } executeShifts(n); midpoint = 0.5 * (params(lefty).prelim + params(right).prelim); l = n.prevNode; if (l != null) { np.prelim = params(l).prelim + spacing(l,n,true); np.mod = np.prelim - midpoint; } else { np.prelim = midpoint; } } } private function apportion(v:NodeSprite, a:NodeSprite):NodeSprite { var w:NodeSprite = v.prevNode; if (w != null) { var vip:NodeSprite, vim:NodeSprite, vop:NodeSprite, vom:NodeSprite; var sip:Number, sim:Number, sop:Number, som:Number; vip = vop = v; vim = w; vom = vip.parentNode.firstChildNode; sip = params(vip).mod; sop = params(vop).mod; sim = params(vim).mod; som = params(vom).mod; var shift:Number; var nr:NodeSprite = nextRight(vim); var nl:NodeSprite = nextLeft(vip); while (nr != null && nl != null) { vim = nr; vip = nl; vom = nextLeft(vom); vop = nextRight(vop); params(vop).ancestor = v; shift = (params(vim).prelim + sim) - (params(vip).prelim + sip) + spacing(vim,vip,false); if (shift > 0) { moveSubtree(ancestor(vim,v,a), v, shift); sip += shift; sop += shift; } sim += params(vim).mod; sip += params(vip).mod; som += params(vom).mod; sop += params(vop).mod; nr = nextRight(vim); nl = nextLeft(vip); } if (nr != null && nextRight(vop) == null) { var vopp:Params = params(vop); vopp.thread = nr; vopp.mod += sim - sop; } if (nl != null && nextLeft(vom) == null) { var vomp:Params = params(vom); vomp.thread = nl; vomp.mod += sip - som; a = v; } } return a; } private function nextLeft(n:NodeSprite):NodeSprite { var c:NodeSprite = null; if (n.expanded) c = n.firstChildNode; return (c != null ? c : params(n).thread); } private function nextRight(n:NodeSprite):NodeSprite { var c:NodeSprite = null; if (n.expanded) c = n.lastChildNode; return (c != null ? c : params(n).thread); } private function moveSubtree(wm:NodeSprite, wp:NodeSprite, shift:Number):void { var wmp:Params = params(wm); var wpp:Params = params(wp); var subtrees:Number = wpp.number - wmp.number; wpp.change -= shift/subtrees; wpp.shift += shift; wmp.change += shift/subtrees; wpp.prelim += shift; wpp.mod += shift; } private function executeShifts(n:NodeSprite):void { var shift:Number = 0, change:Number = 0; for (var c:NodeSprite = n.lastChildNode; c != null; c = c.prevNode) { var cp:Params = params(c); cp.prelim += shift; cp.mod += shift; change += cp.change; shift += cp.shift + change; } } private function ancestor(vim:NodeSprite, v:NodeSprite, a:NodeSprite):NodeSprite { var vimp:Params = params(vim); var p:NodeSprite = v.parentNode; return (vimp.ancestor.parentNode == p ? vimp.ancestor : a); } private function secondWalk(n:NodeSprite, p:NodeSprite, m:Number, depth:uint, visible:Boolean):void { // set position var np:Params = params(n); var o:Object = _t.$(n); setBreadth(o, p, (visible ? np.prelim : 0) + m); setDepth(o, p, _depths[depth] as Number); setVisibility(n, o, visible); // recurse var v:Boolean = n.expanded ? visible : false; var b:Number = m + (n.expanded ? np.mod : np.prelim) if (v) depth += 1; for (var c:NodeSprite = n.firstChildNode; c!=null; c=c.nextNode) { secondWalk(c, n, b, depth, v); } np.clear(); } private function setBreadth(n:Object, p:NodeSprite, b:Number):void { switch (_orient) { case Orientation.LEFT_TO_RIGHT: case Orientation.RIGHT_TO_LEFT: n.y = _ay + b; break; case Orientation.TOP_TO_BOTTOM: case Orientation.BOTTOM_TO_TOP: n.x = _ax + b; break; default: throw new Error("Unrecognized orientation value"); } } private function setDepth(n:Object, p:NodeSprite, d:Number):void { switch (_orient) { case Orientation.LEFT_TO_RIGHT: n.x = _ax + d; break; case Orientation.RIGHT_TO_LEFT: n.x = _ax - d; break; case Orientation.TOP_TO_BOTTOM: n.y = _ay + d; break; case Orientation.BOTTOM_TO_TOP: n.y = _ax - d; break; default: throw new Error("Unrecognized orientation value"); } } private function setVisibility(n:NodeSprite, o:Object, visible:Boolean):void { o.alpha = visible ? 1.0 : 0.0; o.mouseEnabled = visible; if (n.parentEdge != null) { o = _t.$(n.parentEdge); o.alpha = visible ? 1.0 : 0.0; o.mouseEnabled = visible; } } private function setSizes(n:NodeSprite):void { _t.endSize(n, _rect); n.w = _rect.width; n.h = _rect.height; } private function spacing(l:NodeSprite, r:NodeSprite, siblings:Boolean):Number { var w:Boolean = Orientation.isVertical(_orient); return (siblings ? _bspace : _tspace) + 0.5 * (w ? l.w + r.w : l.h + r.h) } private function updateDepths(depth:uint, item:NodeSprite):void { var v:Boolean = Orientation.isVertical(_orient); var d:Number = v ? item.h : item.w; // resize if needed if (depth >= _depths.length) { _depths = Vectors.copy(_depths, new Vector.(int(1.5*depth))); for (var i:int=depth; i<_depths.length; ++i) _depths[i] = 0; } _depths[depth] = Math.max(_depths[depth] as Number, d); _maxDepth = Math.max(_maxDepth, depth); } private function determineDepths():void { for (var i:uint=1; i<_maxDepth; ++i) _depths[i] += (_depths[i-1] as Number) + _dspace; } // -- Parameter Access ------------------------------------------------ private function params(n:NodeSprite):Params { var p:Params = n.props[PARAMS] as Params; if (p == null) { p = new Params(); n.props[PARAMS] = p; } if (p.number == -2) { p.init(n); } return p; } } // end of class NodeLinkTreeLayout } import flare.vis.data.NodeSprite; class Params { public var prelim:Number = 0; public var mod:Number = 0; public var shift:Number = 0; public var change:Number = 0; public var number:int = -2; public var ancestor:NodeSprite = null; public var thread:NodeSprite = null; public function init(item:NodeSprite):void { ancestor = item; number = -1; } public function clear():void { number = -2; prelim = mod = shift = change = 0; ancestor = thread = null; } } // end of class Params