package flare.vis.operator.layout { import flare.physics.Particle; import flare.physics.Simulation; import flare.physics.Spring; import flare.vis.data.Data; import flare.vis.data.DataSprite; import flare.vis.data.EdgeSprite; import flare.vis.data.NodeSprite; /** * Layout that places nodes based on a physics simulation of * interacting forces. By default, nodes repel each other, edges act as * springs, and drag forces (similar to air resistance) are applied. This * algorithm can be run for multiple iterations for a run-once layout * computation or repeatedly run in an animated fashion for a dynamic and * interactive layout (set Visualization.continuousUpdates = true * ). * *

The running time of this layout algorithm is the greater of O(N log N) * and O(E), where N is the number of nodes and E the number of edges. * The addition of custom forces to the simulation may affect this.

* *

The force directed layout is implemented using the physics simulator * provided by the flare.physics package. The * Simulation used to drive this layout can be set explicitly, * allowing any number of custom force directed layouts to be created * through the selection of IForce modules. Each node in the * layout is mapped to a Particle instance and each edge * to a Spring in the simulation. Once the simulation has been * initialized, you can retrieve these instances through the * node.props.particle and edge.props.spring * properties, respectively.

* * @see flare.physics */ public class ForceDirectedLayout extends Layout { private var _sim:Simulation; private var _step:Number = 1; private var _iter:int = 1; private var _gen:uint = 0; private var _enforceBounds:Boolean = false; // simulation defaults private var _mass:Number = 1; private var _restLength:Number = 30; private var _tension:Number = 0.3; private var _damping:Number = 0.1; /** The default mass value for node/particles. */ public function get defaultParticleMass():Number { return _mass; } public function set defaultParticleMass(v:Number):void { _mass = v; } /** The default spring rest length for edge/springs. */ public function get defaultSpringLength():Number { return _restLength; } public function set defaultSpringLength(v:Number):void { _restLength = v; } /** The default spring tension for edge/springs. */ public function get defaultSpringTension():Number { return _tension; } public function set defaultSpringTension(v:Number):void { _tension = v; } /** The number of iterations to run the simulation per invocation * (default is 1, expecting continuous updates). */ public function get iterations():int { return _iter; } public function set iterations(iter:int):void { _iter = iter; } /** The number of time ticks to advance the simulation on each * iteration (default is 1). */ public function get ticksPerIteration():int { return _step; } public function set ticksPerIteration(ticks:int):void { _step = ticks; } /** The physics simulation driving this layout. */ public function get simulation():Simulation { return _sim; } /** Flag indicating if the layout bounds should be enforced. * If true, the layoutBounds will limit node placement. */ public function get enforceBounds():Boolean { return _enforceBounds; } public function set enforceBounds(b:Boolean):void { _enforceBounds = b; } // -------------------------------------------------------------------- /** * Creates a new ForceDirectedLayout. * @param iterations the number of iterations to run the simulation * per invocation * @param sim the physics simulation to use for the layout. If null * (the default), default simulation settings will be used */ public function ForceDirectedLayout(enforceBounds:Boolean=false, iterations:int=1, sim:Simulation=null) { _enforceBounds = enforceBounds; _iter = iterations; _sim = (sim==null ? new Simulation(0, 0, 0.1, -10) : sim); } /** @inheritDoc */ protected override function layout():void { ++_gen; // update generation counter init(); // populate simulation // run simulation _sim.bounds = _enforceBounds ? layoutBounds : null; for (var i:uint=0; i<_iter; ++i) { _sim.tick(_step); } visualization.data.nodes.visit(update); // update positions updateEdgePoints(_t); } // -- value transfer -------------------------------------------------- /** * Transfer the physics simulation results to an item's layout. * @param d a DataSprite * @return true, to signal a visitor to continue */ protected function update(d:DataSprite):void { var p:Particle = d.props.particle; if (!p.fixed) { var o:Object = _t.$(d); o.x = p.x; o.y = p.y; } } // -- simulation management ------------------------------------------- /** * Initializes the Simulation for this ForceDirectedLayout */ protected function init():void { var data:Data = visualization.data, o:Object; var p:Particle, s:Spring, n:NodeSprite, e:EdgeSprite; // initialize all simulation entries for each (n in data.nodes) { p = n.props.particle; o = _t.$(n); if (p == null) { n.props.particle = (p = _sim.addParticle(_mass, o.x, o.y)); p.fixed = o.fixed; } else { p.x = o.x; p.y = o.y; p.fixed = o.fixed; } p.tag = _gen; } for each (e in data.edges) { s = e.props.spring; if (s == null) { e.props.spring = (s = _sim.addSpring( e.source.props.particle, e.target.props.particle, _restLength, _tension, _damping)); } s.tag = _gen; } // set up simulation parameters // this needs to be kept separate from the above initialization // to ensure all simulation items are created first if (mass != null) { for each (n in data.nodes) { p = n.props.particle; p.mass = mass(n); } } for each (e in data.edges) { s = e.props.spring; if (restLength != null) s.restLength = restLength(e); if (tension != null) s.tension = tension(e); if (damping != null) s.damping = damping(e); } // clean-up unused items for each (p in _sim.particles) if (p.tag != _gen) p.kill(); for each (s in _sim.springs) if (s.tag != _gen) s.kill(); } /** * Function for assigning mass values to particles. By default, this * simply returns the default mass value. This function can be replaced * to perform custom mass assignment. */ public var mass:Function = function(d:DataSprite):Number { return _mass; } /** * Function for assigning rest length values to springs. By default, * this simply returns the default rest length value. This function can * be replaced to perform custom rest length assignment. */ public var restLength:Function = function(e:EdgeSprite):Number { return _restLength; } /** * Function for assigning tension values to springs. By default, this * method computes spring tension adaptively, based on the connectivity * of the attached particles, to create more stable layouts. More * specifically, the tension is computed as the default tension value * divided by the square root of the maximum degree of the attached * particles. This function can be replaced to perform custom tension * assignment. */ public var tension:Function = function(e:EdgeSprite):Number { var s:Spring = Spring(e.props.spring); var n:Number = Math.max(s.p1.degree, s.p2.degree); return _tension / Math.sqrt(n); } /** * Function for assigning damping constant values to springs. By * default, this simply uses the spring's computed tension value * divided by 10. This function can be replaced to perform custom * damping assignment. */ public var damping:Function = function(e:EdgeSprite):Number { return Spring(e.props.spring).tension / 10; } } // end of class ForceDirectedLayout }