📄 radialtreelayout.as
字号:
package flare.vis.operator.layout
{
import flare.util.Arrays;
import flare.vis.data.NodeSprite;
import flash.geom.Rectangle;
/**
* Layout that places tree nodes in a radial layout, laying out depths of a tree
* along circles of increasing radius.
* This layout can be used for both node-link diagrams, where nodes are
* connected by edges, and for radial space-filling ("sunburst") diagrams.
* To generate space-filling layouts, nodes should have their shape
* property set to <code>Shapes.WEDGE</code> and the layout instance should
* have the <code>useNodeSize<code> property set to false.
*
* <p>The algorithm used is an adaptation of a technique by Ka-Ping Yee,
* Danyel Fisher, Rachna Dhamija, and Marti Hearst, published in the paper
* <a href="http://citeseer.ist.psu.edu/448292.html">Animated Exploration of
* Dynamic Graphs with Radial Layout</a>, InfoVis 2001. This algorithm computes
* a radial layout which factors in possible variation in sizes, and maintains
* both orientation and ordering constraints to facilitate smooth and
* understandable transitions between layout configurations.
* </p>
*/
public class RadialTreeLayout extends Layout
{
// -- Properties ------------------------------------------------------
/** Property name for storing parameters for this layout. */
public static const PARAMS:String = "radialTreeLayoutParams";
/** The default radius increment between depth levels. */
public static const DEFAULT_RADIUS:int = 50;
private var _maxDepth:int = 0;
private var _radiusInc:Number = DEFAULT_RADIUS;
private var _theta1:Number = Math.PI/2;
private var _theta2:Number = Math.PI/2 - 2*Math.PI;
private var _sortAngles:Boolean = true;
private var _setTheta:Boolean = false;
private var _autoScale:Boolean = true;
private var _useNodeSize:Boolean = true;
private var _prevRoot:NodeSprite = null;
/** The radius increment between depth levels. */
public function get radiusIncrement():Number { return _radiusInc; }
public function set radiusIncrement(r:Number):void { _radiusInc = r; }
/** Flag determining if nodes should be sorted by angles to help
* maintain ordering across different spanning-tree configurations.
* This sorting is important for understandable transitions when using
* a radial layout of a general graph. However, it is unnecessary for
* tree structures and increases the running time of layout. */
public function get sortAngles():Boolean { return _sortAngles; }
public function set sortAngles(b:Boolean):void { _sortAngles = b; }
/** Flag indicating if the layout should automatically be scaled to
* fit within the layout bounds. */
public function get autoScale():Boolean { return _autoScale; }
public function set autoScale(b:Boolean):void { _autoScale = b; }
/** The initial angle for the radial layout (in radians). */
public function get startAngle():Number { return _theta1; }
public function set startAngle(a:Number):void {
_theta2 += (a - _theta1);
_theta1 = a;
_setTheta = true;
}
/** The angular width of the layout (in radians, default is 2 pi). */
public function get angleWidth():Number { return _theta1 - _theta2; }
public function set angleWidth(w:Number):void {
_theta2 = _theta1 - w;
_setTheta = true;
}
/** Flag indicating if node's <code>size</code> property should be
* used to determine layout spacing. If a space-filling radial
* layout is desired, this value must be false for the layout
* to be accurate. */
public function get useNodeSize():Boolean { return _useNodeSize; }
public function set useNodeSize(b:Boolean):void {
_useNodeSize = b;
}
// -- Methods ---------------------------------------------------------
/**
* Creates a new RadialTreeLayout.
* @param radius the radius increment between depth levels
* @param sortAngles flag indicating if nodes should be sorted by angle
* to maintain node ordering across spanning-tree configurations
* @param autoScale flag indicating if the layout should automatically
* be scaled to fit within the layout bounds
*/
public function RadialTreeLayout(radius:Number=DEFAULT_RADIUS,
sortAngles:Boolean=true, autoScale:Boolean=true)
{
layoutType = POLAR;
_radiusInc = radius;
_sortAngles = sortAngles;
_autoScale = autoScale;
}
/** @inheritDoc */
protected override function layout():void
{
var n:NodeSprite = layoutRoot as NodeSprite;
if (n == null) { _t = null; return; }
var np:Params = params(n);
// calc relative widths and maximum tree depth
// performs one pass over the tree
_maxDepth = 0;
calcAngularWidth(n, 0);
if (_autoScale) setScale(layoutBounds);
if (!_setTheta) calcAngularBounds(n);
_anchor = layoutAnchor;
// perform the layout
if (_maxDepth > 0) {
doLayout(n, _radiusInc, _theta1, _theta2);
} else if (n.childDegree > 0) {
n.visitTreeDepthFirst(function(n:NodeSprite):void {
n.origin = _anchor;
var o:Object = _t.$(n);
// collapse to inner radius
o.radius = o.h = o.v = _radiusInc / 2;
o.alpha = 0;
o.mouseEnabled = false;
if (n.parentEdge != null)
_t.$(n.parentEdge).alpha = false;
});
}
// update properties of the root node
np.angle = _theta2 - _theta1;
n.origin = _anchor;
update(n, 0, _theta1+np.angle/2, np.angle, true);
if (!_t.immediate) {
delete _t._(n).values.radius;
delete _t._(n).values.angle;
}
_t.$(n).x = _anchor.x;
_t.$(n).y = _anchor.y;
updateEdgePoints(_t);
}
private function setScale(bounds:Rectangle):void
{
var r:Number = Math.min(bounds.width, bounds.height)/2.0;
if (_maxDepth > 0) _radiusInc = r / _maxDepth;
}
/**
* Calculates the angular bounds of the layout, attempting to
* preserve the angular orientation of the display across transitions.
*/
private function calcAngularBounds(r:NodeSprite):void
{
if (_prevRoot == null || r == _prevRoot)
{
_prevRoot = r; return;
}
// try to find previous parent of root
var p:NodeSprite = _prevRoot, pp:NodeSprite;
while (true) {
pp = p.parentNode;
if (pp == r) {
break;
} else if (pp == null) {
_prevRoot = r;
return;
}
p = pp;
}
// compute offset due to children's angular width
var dt:Number = 0;
for each (var n:NodeSprite in sortedChildren(r)) {
if (n == p) break;
dt += params(n).width;
}
var rw:Number = params(r).width;
var pw:Number = params(p).width;
dt = -2*Math.PI * (dt+pw/2)/rw;
// set angular bounds
_theta1 = dt + Math.atan2(p.y-r.y, p.x-r.x);
_theta2 = _theta1 + 2*Math.PI;
_prevRoot = r;
}
/**
* Computes relative measures of the angular widths of each
* expanded subtree. Node diameters are taken into account
* to improve space allocation for variable-sized nodes.
*
* This method also updates the base angle value for nodes
* to ensure proper ordering of nodes.
*/
private function calcAngularWidth(n:NodeSprite, d:int):Number
{
if (d > _maxDepth) _maxDepth = d;
var aw:Number = 0, diameter:Number = 0;
if (_useNodeSize && d > 0) {
//diameter = 1;
diameter = n.expanded && n.childDegree > 0 ? 0 : _t.$(n).size;
} else if (d > 0) {
var w:Number = n.width, h:Number = n.height;
diameter = Math.sqrt(w*w+h*h)/d;
if (isNaN(diameter)) diameter = 0;
}
if (n.expanded && n.childDegree > 0) {
for (var c:NodeSprite=n.firstChildNode; c!=null; c=c.nextNode)
{
aw += calcAngularWidth(c, d+1);
}
aw = Math.max(diameter, aw);
} else {
aw = diameter;
}
params(n).width = aw;
return aw;
}
private static function normalize(angle:Number):Number
{
while (angle > 2*Math.PI)
angle -= 2*Math.PI;
while (angle < 0)
angle += 2*Math.PI;
return angle;
}
private function sortedChildren(n:NodeSprite):Array
{
var cc:int = n.childDegree;
if (cc == 0) return Arrays.EMPTY;
var angles:Array = new Array(cc);
if (_sortAngles) {
// update base angle for node ordering
var base:Number = -_theta1;
var p:NodeSprite = n.parentNode;
if (p != null) base = normalize(Math.atan2(p.y-n.y, n.x-p.x));
// collect the angles
var c:NodeSprite = n.firstChildNode;
for (var i:uint=0; i<cc; ++i, c=c.nextNode) {
angles[i] = normalize(-base + Math.atan2(c.y-n.y,n.x-c.x));
}
// get array of indices, sorted by angle
angles = angles.sort(Array.NUMERIC | Array.RETURNINDEXEDARRAY);
// switch in the actual nodes and return
for (i=0; i<cc; ++i) {
angles[i] = n.getChildNode(angles[i]);
}
} else {
for (i=0; i<cc; ++i) {
angles[i] = n.getChildNode(i);
}
}
return angles;
}
/**
* Compute the layout.
* @param n the root of the current subtree under consideration
* @param r the radius, current distance from the center
* @param theta1 the start (in radians) of this subtree's angular region
* @param theta2 the end (in radians) of this subtree's angular region
*/
private function doLayout(n:NodeSprite, r:Number,
theta1:Number, theta2:Number):void
{
var dtheta:Number = theta2 - theta1;
var dtheta2:Number = dtheta / 2.0;
var width:Number = params(n).width;
var cfrac:Number, nfrac:Number = 0;
for each (var c:NodeSprite in sortedChildren(n)) {
var cp:Params = params(c);
cfrac = cp.width / width;
if (c.expanded && c.childDegree > 0)
{
doLayout(c, r+_radiusInc, theta1 + nfrac*dtheta,
theta1 + (nfrac+cfrac)*dtheta);
}
else if (c.childDegree > 0)
{
var cr:Number = r + _radiusInc;
var ca:Number = theta1 + nfrac*dtheta + cfrac*dtheta2;
c.visitTreeDepthFirst(function(n:NodeSprite):void {
n.origin = _anchor;
update(n, cr, minAngle(n.angle, ca), 0, false);
});
}
c.origin = _anchor;
var a:Number = minAngle(c.angle, theta1 + nfrac*dtheta + cfrac*dtheta2);
cp.angle = cfrac * dtheta;
update(c, r, a, cp.angle, true);
nfrac += cfrac;
}
}
private function update(n:NodeSprite, r:Number, a:Number,
aw:Number, v:Boolean) : void
{
var o:Object = _t.$(n), alpha:Number = v ? 1 : 0;
o.radius = r;
o.angle = a;
if (aw == 0) {
o.h = o.v = r - _radiusInc/2;
} else {
o.h = r + _radiusInc/2;
o.v = r - _radiusInc/2;
}
o.w = aw;
o.u = a - aw/2;
o.alpha = alpha;
o.mouseEnabled = v;
if (n.parentEdge != null)
_t.$(n.parentEdge).alpha = alpha;
}
private function params(n:NodeSprite):Params
{
var p:Params = n.props[PARAMS];
if (p == null) {
p = new Params();
n.props[PARAMS] = p;
}
return p;
}
} // end of class RadialTreeLayout
}
class Params {
public var width:Number = 0;
public var angle:Number = 0;
}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -