Chapter 13. NETWORK GRAPH COMPONENTS

NETWORK GRAPH COMPONENTS

Updated 7/8/08

Don't let the title of this chapter keep you from digging in; "network" doesn't always mean a computer network. In fact, a network graph is any set of interconnected nodes. For example, Facebook (http://facebook.com) is one big network graph. If I know you, and you know me, and we know another person, then our graph can be represented as three nodes with six connections (or edges) between the three nodes.

In this chapter, I present several controls that not only visualize network graphs, but also allow you to interact with the graphs in a dynamic way.

Graphing Flickr with SpringGraph

The SpringGraph component (http://mark-shepherd.com/blog/springgraph-flex-component/) makes it easy to build a set of nodes, create links between those nodes, and display them in a dynamic network graph. You can customize the display of the nodes using an itemRenderer.

Flickr makes for an interesting data source for node graphs because you have images connected to tags, which in turn connect to other tags with more images. For example, the tag "dog" might lead to images of someone walking their dog in a park, which is also tagged with "park" and has its own set of pictures.

A spring graph, like the one in this application, allows you to associate text nodes with image nodes and move them around interactively. You can then double-click the node to dig down even further.

In the following application code, we use the SpringGraph component to visualize all of the results from Flickr (http://flickr.com) to a particular query:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  xmlns:fc="http://www.adobe.com/2006/fc"
  layout="absolute" creationComplete="onStartup()">
<mx:Script>
<![CDATA[
import com.adobe.flex.extras.controls.springgraph.Graph;
import com.adobe.webapis.flickr.methodgroups.Photos;
import com.adobe.webapis.flickr.events.FlickrResultEvent;
import com.adobe.webapis.flickr.*;

private var flickrSvc:FlickrService = new FlickrService(
  "Your Flickr Key" );

private var items:Graph = new Graph();
private var rootItem:FlickrItemData;
private var searchText:String = 'dog';

public static var instance:flickrspring = null;
public function openNode( item:FlickrItemData ) : void {
  if ( item.photo != null ) {
    flickrSvc.photos.getInfo( item.photo.id );
  }
  if ( item.text != null ) {
    searchText = item.text;
    onSearch();
  }
}

private function onStartup() : void {
  instance = this;

  flickrSvc.permission = AuthPerm.NONE;
  flickrSvc.addEventListener( FlickrResultEvent.PHOTOS_SEARCH,
  onPhotoSearchResult );
  flickrSvc.addEventListener( FlickrResultEvent.PHOTOS_GET_INFO,
  onPhotoInfoResult );

  rootItem = new FlickrItemData( searchText );
  rootItem.text = searchText;
  items.add( rootItem );
  s.dataProvider = items;

  onSearch();
}
private function onSearch() : void {
  flickrSvc.photos.search( "", searchText, "", "", null, null, null,
  null, License.ATTRIBUTION, "tags", 10 );
}
private function onPhotoInfoResult( event:FlickrResultEvent ) : void {
  for each( var t:PhotoTag in event.data.photo.tags ) {
    if ( items.find( t.tag ) == null ) {
    var item:FlickrItemData = new FlickrItemData( t.tag );
    item.text = t.tag;
    items.add( item );
    items.link( items.find( event.data.photo.id ), item );
    }
  }
}
private function onPhotoSearchResult( event:FlickrResultEvent ) : void{
  for each( var p:Photo in event.data.photos.photos ) {
    var item:FlickrItemData = new FlickrItemData( p.id );
    item.photo = p;
    items.add( item );
    items.link( items.find( searchText ), item );
  }
}
]]>
</mx:Script>
<fc:SpringGraph id="s" backgroundColor="#ffffff"
  lineColor="#333388CC" left="0" right="0" top="0" bottom="0"
  itemRenderer="FlickrItem" repulsionFactor="0.4" width="100%"
    height="100%">
</fc:SpringGraph>
</mx:Application>

The application uses the AS3 Flickr Library (http://as3flickrlib.googlecode.com) to run initial requests. The application also fields requests to open subnodes and make subqueries. In both cases, it creates new FlickrItemData nodes, which are defined by the AS3 class shown here:

package
{
  import com.adobe.flex.extras.controls.springgraph.Item;
  import com.adobe.webapis.flickr.*;

  public class FlickrItemData extends Item
  {
    public var photo:Photo = null;

    public var text:String = null;

    public function FlickrItemData(id:String=null)
    {
      super(id);
    }
  }
}

Associated with each of the nodes is a Flickr Photo object that comes from the AS3 Flickr Library. If there is no photo associated with the node, the text field must be nonnull.

The itemRenderer that displays these nodes is shown in the following code:

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="75"
  height="75" dataChange="onDataChange()"
  creationComplete="onDataChange()" horizontalScrollPolicy="off"
  verticalScrollPolicy="off"
  doubleClickEnabled="true" doubleClick="onDblClick()">
<mx:Script>
<![CDATA[
import com.adobe.webapis.flickr.Photo;
private function onDblClick() : void {
  flickrspring.instance.openNode( data as FlickrItemData );
}
private function onDataChange() : void {
  if ( data == null || !initialized ) return;
  if ( data.photo != null ) {
    var p:Photo = data.photo as Photo;
    img.source = "http://static.flickr.com/"+p.server+"/"+p.id+"_"+ 
Graphing Flickr with SpringGraph
p.secret+"_s.jpg"; textItem.visible = false; } if ( data.text != null ) { img.visible = false; var tl:TextLineMetrics = textItem.measureText( data.text ); textItem.text = data.text; textItem.width = tl.width + 10; textItem.height = tl.height + 10; width = tl.width + 10; height = tl.height + 10; } } ]]> </mx:Script> <mx:Image id="img" width="75" height="75" /> <mx:Label id="textItem" fontSize="20" /> </mx:Canvas>

This MXML component takes a FlickrItemData object as data. If the photo field of the object is defined, it uses the image tag to display the photo. If the text is set, the textItem label is set and the box is resized to show the whole text.

Getting set up with Flickr

To get this application to work, you need a Flickr API key (which is free). Once you have it, you can run the application, and you should see something like Figure 13-1.

The result of the "dog" query

Figure 13-1. The result of the "dog" query

From here you can double-click one of the dog images to bring up the tags associated with that image. This is shown in Figure 13-2.

More text and image nodes in the SpringGraph

Figure 13-2. More text and image nodes in the SpringGraph

As you can see from Figure 13-3, the number of nodes with text and graphics can get fairly large.

A SpringGraph with a bunch of open nodes

Figure 13-3. A SpringGraph with a bunch of open nodes

As the node tree grows, the SpringGraph automatically adjusts to show the new data. If you don't like where it's going, you can just grab a node and move it around for yourself.

As you can see for yourself, the SpringGraph is a robust and easy-to-use node graph viewer.

Flare for displaying tree structures

The Flare library (http://flare.prefuse.org/) is the Flash version of the Prefuse library (http://prefuse.org/) that's used to display data structures like node graphs. Actually, it can do a whole bunch of things, of which displaying tree structures is just a part.

The Flare project is more about computer science than it is about front-end user-facing features. Where the SpringGraph project is about showing things like pictures or people as nodes, the Flare project is more about showing tree graphs in a more mathematical context.

The Flex application code that hosts the Flare graph is shown here:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  layout="absolute"
  creationComplete="onStartup()">
<mx:Script>
<![CDATA[
import flex.FlexGraph;
import mx.core.UIComponent;
import flare.demos.*;

private var graph:FlexGraph = new FlexGraph();

private function onStartup() : void {
  var uic:UIComponent = new UIComponent();
  addChild( uic );
  uic.addChild( graph );
}
]]>
</mx:Script>
<mx:ApplicationControlBar dock="true">
  <mx:Button label="Starburst" click="graph.starbust();" />
  <mx:ButtonBar id="treeType"
    itemClick="graph.changeLayout(treeType.selectedIndex)"
    dataProvider="{['Force Directed','Node Link Tree',
     'Indented Tree','Radial Tree','Circle']}"
    selectedIndex="0" />
</mx:ApplicationControlBar>
</mx:Application>

The FlexGraph component is a wrapper for the Flare library's Visualization Flash object. Following is the code for this class:

package flex
{
  import flare.animate.Sequence;
  import flare.animate.Transition;
  import flare.animate.Transitioner;
  import flare.util.Button;
  import flare.util.GraphUtil;
  import flare.vis.Visualization;
  import flare.vis.controls.DragControl;
  import flare.vis.data.Data;
  import flare.vis.data.NodeSprite;
  import flare.vis.operator.OperatorSwitch;
  import flare.vis.operator.layout.CircleLayout;
  import flare.vis.operator.layout.ForceDirectedLayout;
  import flare.vis.operator.layout.IndentedTreeLayout;
  import flare.vis.operator.layout.NodeLinkTreeLayout;
import flare.vis.operator.layout.Orientation;
  import flare.vis.operator.layout.RadialTreeLayout;
  import flare.vis.util.graphics.Shapes;

  import flash.display.Sprite;
  import flash.events.MouseEvent;
  import flash.geom.Point;
  import flash.geom.Rectangle;

  public class FlexGraph extends Sprite
  {
    private var vis:Visualization;
    private var os:OperatorSwitch;
    private var anchors:Array;
    private var shape:int = 0;

    public var WIDTH:Number = 800;
    public var HEIGHT:Number = 550;

    public function FlexGraph() {
      name = "FlexGraph";
      var w:Number = WIDTH;
      var h:Number = HEIGHT;

      var data:Data = GraphUtil.diamondTree(5,3,5);
      vis = new Visualization(data);
      vis.bounds = new Rectangle(0,0,w,h);

      os = new OperatorSwitch(
        new ForceDirectedLayout(),
        new NodeLinkTreeLayout(Orientation.LEFT_TO_RIGHT, 20, 5, 10),
        new IndentedTreeLayout(20),
        new RadialTreeLayout(50, false),
        new CircleLayout()
      );
      anchors = [
        null,
        new Point(40, h/2),
        new Point(40, 40),
        new Point(w/2, h/2),
        new Point(0, 0)
      ];
      os.index = 1;
      vis.marks.x = anchors[1].x;
      vis.marks.y = anchors[1].y;

      vis.operators.add(os);
      vis.tree.nodes.visit(function(n:NodeSprite):void {
        n.size = 1.5;
n.fillColor = 0x666666; n.fillAlpha = 0.8;
        n.lineColor = 0x999999; n.lineAlpha = 0.9;
        n.lineWidth = 2;
        n.buttonMode = true;
      });
      vis.update();
      addChild(vis);

      vis.controls.add(new DragControl());
    }

    public function changeLayout( index:int ) : void {
      switchLayout( index ).play();
    }

    public function starbust() : void {
      toStarburst().play();
    }

    private function switchLayout(idx:int):Transition
    {
      vis.operators.clear();
      vis.operators.add(os);
      vis.continuousUpdates = false;
      vis.operators[0].index = idx;

      var seq:Sequence;
      if (shape != 0) {
        seq = new Sequence(
          vis.data.nodes.setProperties({scaleX:0, scaleY:0}, 0.5),
          vis.data.nodes.setProperties({shape:0,
              lineColor:0xffdddddd}, 0.5),
          vis.data.nodes.setProperties({scaleX:1, scaleY:1}, 0),
          vis.data.edges.setProperties({lineColor:0xffcccccc}, 0.5)
        );
      } else {
        seq = new Sequence();
      }

      shape = 0;
      if (idx > 0) {
        seq.onEnd = function():void {
          var t:Transitioner = new Transitioner(2);
          t.$(vis.marks).x = anchors[idx].x;
          t.$(vis.marks).y = anchors[idx].y;
          vis.update(t).play();
        };
      } else {
        seq.onEnd = function():void { vis.continuousUpdates = true; };
}
      return seq;
    }

    private function toStarburst():Transition
    {
      vis.operators.clear();
      vis.operators.add(new RadialTreeLayout(50,false));
      var t0:Transitioner = new Transitioner(2);

      t0.$(vis.marks).x = WIDTH/2;
      t0.$(vis.marks).y = HEIGHT/2;
      if (shape == Shapes.WEDGE) {
        return vis.update(t0);
      } else {
        shape = Shapes.WEDGE;
        return new Sequence(
          vis.update(t0),
          vis.data.edges.setProperties({lineColor:0}, 0.5),
          vis.data.nodes.setProperties({scaleX:0, scaleY:0}, 0.5),
          vis.data.nodes.setProperties({shape:Shapes.WEDGE,
             lineColor:0xffffffff}, 0),
          vis.data.nodes.setProperties({scaleX:1, scaleY:1}, 0.5)
        );
      }
    }

    public function play():void
    {
      var os:OperatorSwitch = vis.operators.getOperatorAt(0) as 
Flare for displaying tree structures
OperatorSwitch; if (os.index == 0) vis.continuousUpdates = true; } public function stop():void { vis.continuousUpdates = false; } } }

It looks like there is a lot to it, but it's actually fairly straightforward code. Most of the work is done in the constructor, where the data for the graph is set up along with additional controls. The switchLayout method changes the display mode of the graph and animates that transition to create some very cool effects. The toStarburst method changes from a traditional node display to a "starburst" display, which is shown a little later in Figure 13-6.

When the application first starts up, it looks like Figure 13-4.

The initial startup state of the application

Figure 13-4. The initial startup state of the application

You can click the buttons in the application control bar at the top of the window to change how the nodes are displayed. Figure 13-5 shows the result of clicking the Radial Tree button.

Clicking the Starburst button results in a very cool transition from whatever the current state of the graph is to the Radial Tree design first, and then on to the slicing design shown in Figure 13-6.

The Flare library is an amazingly cool library filled with unique visualizations, effects, and more. It's definitely worth your time to look into.

The Radial Tree design

Figure 13-5. The Radial Tree design

The Starburst pattern

Figure 13-6. The Starburst pattern

The RaVis node graph

The BirdEye library (http://birdeye.googlecode.com/) contains a growing set of visualizations, including a node graph. The following application code visualizes the band data from the music database as a set of nodes in a node graph:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  layout="vertical"
  xmlns:vg="org.un.cava.birdeye.ravis.graphLayout.visual.*"
  creationComplete="bands.send()">
<mx:HTTPService id="bands" url="http://localhost/music/music.xml"
  resultFormat="e4x" result="onBands(event);" />
<mx:Script>
<![CDATA[
import org.un.cava.birdeye.ravis.graphLayout.visual.edgeRenderers. 
The RaVis node graph
BaseEdgeRenderer; import org.un.cava.birdeye.ravis.graphLayout.layout.
The RaVis node graph
ConcentricRadialLayouter; import org.un.cava.birdeye.ravis.graphLayout.data.Graph; import org.un.cava.birdeye.ravis.graphLayout.visual.IVisualNode; import mx.rpc.events.ResultEvent; private function addMembers( parent:IVisualNode, band:XML ) : void { for each( var member:XML in band.member ) { var memberNode:IVisualNode = vgraph.createNode(member.toString()); memberNode.data = { name: member.toString(), color: 0x0000FF, image:null }; vgraph.linkNodes( parent, memberNode ); } } private function addBands( parent:IVisualNode, genre:XML ) : void { for each( var band:XML in genre.band ) { var bandNode:IVisualNode = vgraph.createNode(
The RaVis node graph
[email protected]()); bandNode.data = { name: [email protected](), color: 0x00FF00, image: 'http://localhost/music/'[email protected]() }; vgraph.linkNodes( parent, bandNode ); addMembers( bandNode, band ); } } private function onBands( event:ResultEvent ) : void { vgraph.graph = new org.un.cava.birdeye.ravis.graphLayout.data.Graph( "genres"); var rootNode:IVisualNode = vgraph.createNode("root"); rootNode.data = { name: 'Genres', color: 0x333333, image:null }; for each ( var genre:XML in event.result..genre ) { var genreNode:IVisualNode = vgraph.createNode(
[email protected]());
    genreNode.data = { name: [email protected](), color: 0xFF0000 };
    vgraph.linkNodes( rootNode, genreNode );
    addBands( genreNode, genre );
  }

  vgraph.layouter = new ConcentricRadialLayouter(vgraph);
  vgraph.layouter.autoFitEnabled = true;
  vgraph.edgeRenderer = new BaseEdgeRenderer(vgraph.edgeDrawGraphics);
  vgraph.maxVisibleDistance = 2;
  vgraph.displayEdgeLabels = false;
  vgraph.currentRootVNode = rootNode;
  vgraph.draw();
}
]]>
</mx:Script>
<mx:Panel width="100%" height="100%" title="Bands">
<vg:VisualGraph id="vgraph" width="100%" height="100%"
  itemRenderer="MyNodeRenderer" />
</mx:Panel>
</mx:Application>

The application first starts up by requesting the XML for the music library. The onBands method creates a graph, and then creates the root node for the data. From there, it iterates over each genre, creating a node for it, and then calling the addBands method to add the bands to the node. The addBands method adds a node for each band to the graph and calls the addMembers method to add nodes for all of the members.

Rendering the nodes

After creating the graph, the onBands method sets up the rest of the vgraph member variables and calls the draw method to render the graph. If you don't like how the graph is laid out, you can change the object that is set in the layouter member variable to a different layouter type.

The itemRenderer that renders each node is shown in the following code:

<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100"
  height="20" horizontalScrollPolicy="off"
  verticalScrollPolicy="off" creationComplete="onData()"
  dataChange="onData()" paddingBottom="3"
  paddingLeft="3" paddingRight="3" paddingTop="3" cornerRadius="5"
  borderColor="#999999" borderStyle="solid"
  backgroundColor="#cccccc" backgroundAlpha="0.7" borderThickness="1"
  horizontalAlign="center">
<mx:Script>
<![CDATA[
private function onData() : void {
if ( !initialized || !data ) return;
  var met:TextLineMetrics = txt.measureText( data.data.name );
  width = met.width + 15;
  height = met.height + 8;

  setStyle( 'backgroundColor', data.data.color );
  setStyle( 'borderColor', data.data.color );

  if ( data.data.image != null ) {
    img.source = data.data.image;
    img.width = 50;
    img.height = 50;
    img.visible = true;
    img.includeInLayout = true;
    height += 60;
  }
}
]]>
</mx:Script>
<mx:Image id="img" visible="false" height="0" width="0"
  includeInLayout="false" />
<mx:Label id="txt" text="{data.data.name}" fontSize="14"
  fontWeight="bold" color="white" />
</mx:VBox>

This is a pretty simple component. It just takes the name of the node and puts it into the label. It then uses the measureText method to figure out just how big the name is and adjusts the size of the container accordingly. If there is an image associated with the node, it sets the local image object and resizes the node to match.

It also sets the colors so that genres are in red, bands are in green, and band members are in blue.

When the application first starts up, it looks like Figure 13-7.

From here, you can double-click any of the nodes to navigate along the tree. In this case, I double-clicked The Clash, and you can see the result in Figure 13-8.

The startup state of the application after the music database has been downloaded

Figure 13-7. The startup state of the application after the music database has been downloaded

The members of The Clash

Figure 13-8. The members of The Clash

One very nice thing about the RaVis network graph component is that it automatically hides nodes that aren't in the area you are currently navigating. That way it kind of cleans up after you as you navigate around big node graphs.

yFiles FLEX

The yFiles FLEX framework (http://www.yworks.com/en/products_yfilesflex_about.html) is the pinnacle of node graph systems. It's an OEM product that has both a client and a server component to it. The client does the basic rendering of the graph and the editing. The server does the graph layout and maintains the persistent state of the network so that multiple users can edit a central graph simultaneously.

You can see the yFiles node graph in action in Figure 13-9.

A network map shown in the yFiles node graph

Figure 13-9. A network map shown in the yFiles node graph

The yFiles FLEX framework is a serious piece of work. It's well worth considering if you are looking to build an industrial-strength application that has serious multiuser diagramming requirements.

Where we will go from here

The few examples in this chapter barely scratch the surface of what you can do with some creativity and these components. Even if this type of dynamic graphing isn't your thing, you should try some of these examples out anyway as they demonstrate what can be done in terms of interactivity with Flex and Flash applications. This kind of thing would be insanely difficult to do with DHTML and Ajax.

In the next chapter, I'll show how to reskin your application to give the whole thing a completely different look and feel with very little effort.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.40.64