Advanced time series graph with Prefuse Flare

In the prior time series tutorial I stated that I was going to use Flare in favor of stuff like jFlot and jqPlot.


Where we are now, that’s not going to happen and you will see why when we discuss 3 different demos. First the basic graph, the fish eye graph and the graph we can zoom in.

All three graphs show a dataset with word counts (programming languages) from various blogs, split by week.

As you can see at the right end of the basic demo we’re having serious difficulties making out the word frequencies. The fish eye operator alleviates the problem somewhat, but is eating a lot of CPU, and the third zooming example (you zoom by scrolling and drag by left clicking and moving the mouse) is not a help at all.

Update: Setting the last scale size value to 0 in the constructor call helps, but the situation is still not acceptable: var distort = new FisheyeDistortion(4,4,0);. I also tried bifocal distortion with distort.distortSize = false;, also pretty good but the disentangling effect is still not powerful enough.

What we need to be able to do is zoom in a similar fashion done in these jqPlot examples. I will try and figure out how to do it in Flare, and if I can’t manage (or if there’s no operator for it) I will have to switch back to jFlot or maybe try jqPlot for the first time.

Let’s walk through the changes that have been introduced since last time anyway:

package {
	import com.adobe.serialization.json.JSON;
	
    import flash.events.MouseEvent;
    import flare.animate.Transitioner;
    import flare.display.TextSprite;
    import flare.util.palette.ColorPalette;
    
. . .
    import flare.vis.legend.Legend;
    import flare.vis.events.SelectionEvent;
    import flare.vis.controls.ClickControl;
    import flare.vis.controls.HoverControl;
    import flare.vis.operator.distortion.FisheyeDistortion;
    import flare.vis.data.NodeSprite;
    import flare.vis.controls.AnchorControl;
    import flare.vis.operator.layout.Layout;
	
    import flare.vis.controls.PanZoomControl;
 
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.utils.Dictionary;
    import flash.external.*;
    import flare.util.*;
    
. . .

    [SWF(width="1100", height="450", backgroundColor="#ffffff", frameRate="30")]
    public class WordTrends extends Sprite{
    	[Embed(source="verdana.TTF", fontName="Verdana")]
    	private static var _font:Class;
    	
        private var vis:Visualization;
        private var words:Array = new Array();
        private var _detail:TextSprite;
 
        public function WordTrends(){   	
            var data = getTimeline();
        	visualize(data); 
        }
 
        private function visualize(data:Data):void{
            vis = new Visualization(data);
            vis.bounds = new Rectangle(0, 0, 850, 300);
            var offsetX = 150;
            var offsetY = 10;
            vis.x = offsetX;
            vis.y = offsetY;
            addChild(vis);
            
            var fmt:TextFormat = new TextFormat("Verdana", 12, 0x000000);
            
            vis.operators.add(new AxisLayout("data.date", "data.count"));
            
            var colPal = ColorPalette.category();
            
            vis.operators.add(new PropertyEncoder({scaleX:1, scaleY:1}));
            vis.operators.add(new ColorEncoder("data.series", Data.NODES, "fillColor", ScaleType.CATEGORIES, colPal));
            vis.operators.add(new ColorEncoder("data.series", Data.EDGES, "lineColor", ScaleType.CATEGORIES, colPal));
            vis.operators.add(new PropertyEncoder({lineWidth:2}, Data.EDGES));
            vis.operators.add(new PropertyEncoder({lineColor: 0xffffffff, lineWidth:1, size:0.5}, Data.NODES));
            
            var distort = new FisheyeDistortion(2,2,2);
            vis.operators.add(distort);
            vis.controls.add(new AnchorControl(distort as Layout));
           
            with (vis.xyAxes.xAxis) {
                horizontalAnchor = TextSprite.LEFT;
                verticalAnchor = TextSprite.MIDDLE;
                labelAngle = Math.PI / 2;
                labelTextFormat = fmt;
            }

            var legendVals = new Array();
            var i = 0;
            for each (var name:String in words){
            	legendVals.push({color:colPal.getColorByIndex(i), size: 0.75, label: name});
            	i++;
            }
            var _legend = Legend.fromValues(null, legendVals);
            _legend.labelTextFormat = fmt;
            _legend.labelTextMode = TextSprite.EMBED;
            _legend.update();
            addChild(_legend);
            
            _detail = new TextSprite("", fmt, TextSprite.EMBED);
            _detail.textField.multiline = false;
            _detail.htmlText = "";
            _detail.x = -100;
            _detail.y = -100;
            addChild(_detail);
            
            vis.controls.add(new HoverControl(NodeSprite,
                HoverControl.DONT_MOVE,
                function(evt:SelectionEvent):void {
                    _detail.text = evt.node.data.count;
                    _detail.x = evt.node.x + offsetX;
                    _detail.y = evt.node.y + offsetY - 20;
                    evt.node.size = 1;
                },
                function(evt:SelectionEvent):void {
                    _detail.text = "";
                    _detail.x = -100;
                    _detail.y = -100;
                    evt.node.size = 0.5;
                }
            ));
            
	    new PanZoomControl().attach(this);
			
            vis.update();
        }
        
        public function setSeriesColor(color, series){
        	vis.operators.add(new PropertyEncoder({lineWidth:2, lineColor: color}, Data.EDGES, function(node){
                return node.data.series == series;
            }));
            
            vis.operators.add(new PropertyEncoder({fillColor:color, lineColor: 0xffffffff, lineWidth:1, size:0.5}, Data.NODES, function(node){
                return node.data.series == series;
            }));
        }
        
        public function getTimeline():Data{

. . .

           var json:Array = JSON.decode(jsonString as String) as Array;
            var jsonData:Array = new Array();
            
            for each (var item:Array in json){
            	var tmpObj:Object = new Object(); 
            	var tmp:Array = item[1];
            	
            	tmpObj.name = item[0];
            	
            	tmp = tmp.map(function(element:*, index:int, arr:Array){
	                element[0] = Dates.roundTime(new Date(element[0]), Dates.WEEKS);
	                return element;
	            });
	            
	            var summed:Array = new Array();
	            for each (var el:Array in tmp){
	                var x = el[0].time;
	                if(summed[x] == null)
	                    summed[x] = el;
	                else
	                    summed[x][1] += el[1];
	            }
	            tmpObj.data = summed;
	            jsonData.push(tmpObj);
            }
            
            var data:Data = new Data();

            var j:uint = 0;   
            
            for each (var dat:Object in jsonData){
            	words.push(dat.name);
            	for each (var itm:Array in dat.data)
	                data.addNode({series: int(j), date: itm[0], count: int(itm[1])});
                j++;
            }
            
            data.createEdges("data.date", "data.series");

            return data;
        }
    }
}

Let’s go with the data flow. The data will start as a JSON string imported by way of the external interface, just like last time, this time however it will be in this form: [ [“google”: [“09/10/2009”, 66, 209] [“09/08/2009”, 66, 209]], [“yahoo”: [“09/10/2009”, 66, 105] [“09/08/2009”, 66, 209]]… ]. In this case “google” and “yahoo” will become legend labels, we keep track of them in the class variable array words.

Let’s move over to the visualize function. We need to create our own custom color palette in order to keep track of which color each graph has and map that color to the corresponding legend. We loop through the words and assign a color – through the use of getColorByIndex – to each legend.

Next we assign a new texField to the _detail class variable, this little text field will start its life outside of the visible area and later be transformed to where the mouse is hovering if we hover over a node. If that happens the node will grow bigger and the text field displaying the word count for that week will show. On mouse out we reset everything.

The single line new PanZoomControl().attach(this); will create the zooming ability and the following three lines are responsible for the fish eye effect:

var distort = new FisheyeDistortion(2,2,2);
vis.operators.add(distort);
vis.controls.add(new AnchorControl(distort as Layout));



Related Posts

Tags: , , , , , ,