InfoVis

InfoVis

A friend asked me to rewrite a piece of code he found and was working with to create a simple info visualizer for a site. The original code came from a book by Jamie MacDonald, and as it was in AS2 I found the text of limited use, though I borrowed the bones of the XML parsing function.

The concept was that a central cluster could be manipulated so that each leaf element of the cluster can be expanded to a secondary cluster and then each leaf of that node cluster can be selected. Though at first I protested, I kept to keeping it linear rather than expanding this to handle multiple nested nodes with the parents receding into 3D space. Thanks to Neil for keeping it simple.

The XML looks like this, but the recursive code should be able to handle more nesting.

<cat catname="Info-Vis">
    <color_variables cluster_linecolor="0xFF0000" node_linecolor="0xFFD700"/>
    <cat catname="Art">
            <cat catname="Nakamura" caturl="http://url1 />
            <cat catname="Weskamp" caturl="http://url2" />
            <cat catname="Web Wizards" caturl="http://url3" />
    </cat>
    <cat catname="Research">
            <cat catname="Eyetracker" caturl="http://url4"/>
            <cat catname="IEEE" caturl="http:///url5"/>
            <cat catname="Schneider" caturl="http:///url6"/>
            <cat catname="Fry" caturl="http:///url7"/>
    </cat>

     ...

</cat>

What follows comes from the Main class. After loading the XML parseNodes is called which builds the data into an Object. Then buildNodeset instantiates the first Cluster, which builds all of the data recursively, seen below in Cluster.as. Node_Config is a singleton that is keeping track of a couple of globals including the center screen location and a handle to the main class. These are incomplete code snippets and variables beginning with "_" are class properties.

from Main.as

       private function onLoadXML(e:Event):void{
            _appMetaData.removeEventListener("xmlLoaded", onLoadXML);
            parseNodes(_appMetaData.xmlData,_nodeObject);
            buildNodeset();
        }   

       private function parseNodes(xmNode:XML,currentObj:Object){
            currentObj.cat_name = xmNode.@catname;
            if(xmNode.cat.length()>0){
                currentObj.kidsArray = [];
                for (var i=0; i<xmNode.cat.length();i++){
                    currentObj.kidsArray[i]={};
                    parseNodes(xmNode.cat[i], currentObj.kidsArray[i]);
                }
            } else {
                // it has no child nodes, so it must be a URL node
                // grab the "url" attribute of the XML node, save it to a field of the current object
                currentObj.cat_URL = xmNode.@caturl;
            }
            
        }

        private function buildNodeset():void{
            Node_Config.CLUSTERLINECOLOR = uint(_appMetaData.xmlData.color_variables.@cluster_linecolor);
            Node_Config.NODELINECOLOR = uint(_appMetaData.xmlData.color_variables.@node_linecolor);
            _baseNode = new Cluster(this,_nodeObject.cat_name,_nodeObject.kidsArray);
            addChild(_baseNode);
            this.x = Node_Config.CENTERX;
            this.y = Node_Config.CENTERY;
            Node_Config.CURRENTCLUSTER = _baseNode;
            Node_Config.CLUSTERCOUNT = 1;
            _baseNode.showCluster();
        }

Cluster.as extends the class Node.as which  provides the updating for the rotation of the lines.

... 
        public function Node(myDaddy,type) {
            _myDaddy = myDaddy;
            _lineColor = Node_Config.NODELINECOLOR;
            _type = type;
            _lineSprite  = new Sprite();
            _gLine = _lineSprite.graphics;
            addChild(_lineSprite);       
            this.addEventListener(Event.ENTER_FRAME,update,false,0,true);
        }
        
        public function update(e:Event):void{
            this.x = _myRadius*Math.cos(_myAngle);
            this.y = _myRadius*Math.sin(_myAngle);
            _gLine.clear();
            _gLine.lineStyle(2, _lineColor, 1, true);
            _gLine.lineTo(_myDaddy._myDaddy.x-this.x, _myDaddy._myDaddy.y-this.y);
        }

 

The following is from Cluster.as.

      public function Cluster(myDaddy:Object,clusterName:String, kidsList:Array) {
            super(myDaddy,"cluster");
            _clusterName = clusterName;
            _Config = Node_Config.getInstance();
            var numOfChildren = kidsList.length;
            _lineColor = Node_Config.CLUSTERLINECOLOR;
            for(var i:uint=0;i<numOfChildren;i++){
                if(kidsList[i].hasOwnProperty("cat_URL")){
                    //is an end node not a new cluster
                    _kidsNodeArray.push(new EndNode(this,kidsList[i].cat_name, kidsList[i].cat_URL));
                }else{
                    //is a new cluster
                    _kidsNodeArray.push(new Cluster(this,kidsList[i].cat_name, kidsList[i].kidsArray));
                }
                addChild(_kidsNodeArray[i]);
                _kidsNodeArray[i].alpha = 0;
                _kidsNodeArray[i].visible = false;
            }
            _centerNode = new EndNode(this,_clusterName);
            addChild(_centerNode);
           ...

As you can see Cluster's constructor will call itself to create new Clusters and EndNodes; instantiating _baseNode in buildNodeset builds out the entire dataset. EndNodes are nodes with URLs to link to. This ended up being pretty simple as the XML came with a single cluster with clusters at it's leaves, and the rest were end nodes. I use Greensocks' TweenMax and/or TweenLite extensively. Two of Node's class  properties, _myRadius and _myAngle, are manipulated within the base class Node.as for rotation and extension animations.

There is a NodeLabel class that takes care of creating the label. The EndNode class, which also extends Node, has the MouseEvent listener and opens the URL for the labeled end node, after rotation and translation. I apologize that this is not cleaned up and of course it is incomplete.

      public function EndNode(myDaddy,theText,theURL = "") {
            super(myDaddy,"endnode");
            _text = theText;
            _URL = theURL;
            _Config = Node_Config.getInstance();
            var thecolor:uint = _backgroundColor1;
            if (_URL != "") thecolor = _backgroundColor2;
            _cNameSprite = new NodeLabel(this);
            _cNameSprite.mShow(theText, _fontSize, false, true, thecolor);
            addChild(_cNameSprite);
            _cNameSprite.buttonMode = true;
            buttonMode = true;
            _cNameSprite.addEventListener(MouseEvent.CLICK,onMouseClick,false,0,true);
            if(Node_Config.MAIN == _myDaddy._myDaddy){
                this.updateOff();
            }
        }

        private function onMouseClick(e:MouseEvent):void{
            if(_URL ==""){
                //is the center of a cluster
                if (Node_Config.CURRENTURLNODE != "") Node_Config.CURRENTURLNODE.normalNode();
                Node_Config.CURRENTURLNODE = ""
                _myDaddy.showCluster(this);
            }else{
                // is an endNode
                showNode();
            }
        }
        
        private function normalNode():void{
            TweenMax.to(_cNameSprite,.7,{scaleX:1, scaleY:1, alpha:1.0});
            TweenMax.to(this,.7,{_myRadius:_myDaddy._radius});
        }
        
        public function showNode():void{
            playSound(_snd4);
            _myDaddy.normalNodes(this);
            var theNode = this;
            //Rotate all siblings so selected cluster is at 0 degrees
            var theCluster = theNode._myDaddy;
            for (var j:int=0;j<theCluster._kidsNodeArray.length;j++){
                var node = theCluster._kidsNodeArray[j];
                var myAngle2 = node._myAngle - theNode._myAngle;
                //NEW OPENING NODE
                if(theNode == node){
                    TweenMax.to(theNode,1.5,{_myAngle:0,_myRadius:_radius*2});
                    TweenMax.to(_cNameSprite,.7,{scaleX:1.0, scaleY:1.0, alpha:1.0});
                }else{
                    TweenMax.to(theCluster._kidsNodeArray[j],1.5,{_myAngle:myAngle2});
                }                        
            }
            if(Node_Config.CLUSTERCOUNT > 0){
                TweenMax.to(Node_Config.MAIN,1.5,{x:Node_Config.SCREENW/(Node_Config.CLUSTERCOUNT +2)-30, onComplete:showNodeDone});
            }
        }     

        private function showNodeDone():void{
            var targetURL:URLRequest = new URLRequest(_URL);
            navigateToURL(targetURL, "linkdisplay_iframe");
        }

"linkdisplay_iframe" is the name of the iframe that the link will open in. The swf is 900x400 scaled down here to 620x276 so it's hard to read. Use this link to see a full size implementation.