Introduction to Flare Visualization Library

From Data-gov Wiki

Jump to: navigation, search

Contents

Live Demo

What is Flare?

From Flare's own site, it can be described as "an ActionScript library for creating visualizations that run in the Adobe Flash Player. From basic charts and graphs to complex interactive graphics, the toolkit supports data management, visual encoding, animation, and interaction techniques. Even better, flare features a modular design that lets developers create customized visualization techniques without having to reinvent the wheel."

Additionally, as a side note, Flare itself is not strictly just "ActionScript" (although it is). It's really built off an extension of ActionScript called Flex, which is a language unto itself. The main difference is that while ActionScript tends to be directly used in a Flash application (you develop the code alongside some sort of visual component within Adobe Flash), Flex is creating the images through the code more directly, and you don't really ever touch a visual object. In other words, its a little more like actual programming.

Sounds like exactly what we need! Something that isn't as bare as, say, raw ActionScript programming or the Processing framework for java. At the same time, however, we have a much higher degree of control on what the graphics can look like, than, say, the Google Visualization API. In short, we are actually writing the code, but the development of the code is highly structured, and includes a robust level of detail built in for creating the basic parts of a graph.

This high degree of structure and robustness in utilities does mean that there's a pretty high learning curve, however, so an introduction to your first tutorial would serve you well. In this, we're going to use Dataset 401, the SPARQL Query that's included in this article, and the ActionScript/Flex/Flare (which for simplicity's sake we'll just fall "Flare code") code that follows.

Flare Graphs

Flare supports a wide array of graphs. The best way to see what's available is here: [1]

As an explanation, we can look at this chart to determine what type of Flare graph lends itself easier to a given data set we have on hand:

Flat Data (one set of objects) | Tree Data (objects with child/parent nodes)
----------------------------------------------------------------------------
Stacked Area Chart             | All "Layouts":
Donut/Pie                      | Sunburst, Circle Pack, Tree, Force
Scatter                        | Radial, Circle, Dendrogram, Icicle
Bars                           | Sunburst

Both:
--------------------------------
Treemap

Demo Tutorial

As always, the best way to learn something is by example. Let's run through this data set example, and highlight some of the important aspects of building something in "Flare code" using semantic technologies. As stated above, we're working with Dataset 401. This data set is a relatively comprehensive budget summary for a huge amount (456) of various departments and agencies in the Government. This budget collection is annual, and the range is 1976-2008.

Immediately, I'm personally drawn to the Stacked Area Layout. As you may notice, Alvaro Graves and Li Ding already used this data set in the Comparison between two budgets demo. I think we could benefit from a demo that lets us look at all of these departments together at once, however, as we can then see how the appropriations change and expand (or detract) over a long period of time, and possibly start to draw novel conclusions. So let's do that.

(Flex requires some infrastructure. If you're not familiar with it, see Getting setup to compile Flex).

SPARQL Query

First, we want to grab all of the data from Dataset 401. In the original SPARQL query for the Comparison between two budgets demo, there was a filter on the query itself, which is dynamically generated so that we can compare two agencies; apparently, the request would be set, then this PHP SPARQL query would be passed the parameters for the name of the agencies, and return the correct data. If we want all of the data, then we can just remove the filter and be done with SPARQL. Our query would look like this:

PREFIX dgp401:  <http://data-gov.tw.rpi.edu/vocab/p/401/>
SELECT  ?buname 

       (sum (?fy_1976  ) as  ?fiscal_year_1976 )
       (sum (?fy_1977  ) as  ?fiscal_year_1977 )
       (sum (?fy_1978  ) as  ?fiscal_year_1978 )
       (sum (?fy_1979  ) as  ?fiscal_year_1979 )
       (sum (?fy_1980  ) as  ?fiscal_year_1980 )
       (sum (?fy_1981  ) as  ?fiscal_year_1981 )
       (sum (?fy_1982  ) as  ?fiscal_year_1982 )
       (sum (?fy_1983  ) as  ?fiscal_year_1983 )
       (sum (?fy_1984  ) as  ?fiscal_year_1984 )
       (sum (?fy_1985  ) as  ?fiscal_year_1985 )
       (sum (?fy_1986  ) as  ?fiscal_year_1986 )
       (sum (?fy_1987  ) as  ?fiscal_year_1987 )
       (sum (?fy_1988  ) as  ?fiscal_year_1988 )
       (sum (?fy_1989  ) as  ?fiscal_year_1989 )
       (sum (?fy_1990  ) as  ?fiscal_year_1990 )
       (sum (?fy_1991  ) as  ?fiscal_year_1991 )
       (sum (?fy_1992  ) as  ?fiscal_year_1992 )
       (sum (?fy_1993  ) as  ?fiscal_year_1993 )
       (sum (?fy_1994  ) as  ?fiscal_year_1994 )
       (sum (?fy_1995  ) as  ?fiscal_year_1995 )
       (sum (?fy_1996  ) as  ?fiscal_year_1996 )
       (sum (?fy_1997  ) as  ?fiscal_year_1997 )
       (sum (?fy_1998  ) as  ?fiscal_year_1998 )
       (sum (?fy_1999  ) as  ?fiscal_year_1999 )
       (sum (?fy_2000  ) as  ?fiscal_year_2000 )
       (sum (?fy_2001  ) as  ?fiscal_year_2001 )
       (sum (?fy_2002  ) as  ?fiscal_year_2002 )
       (sum (?fy_2003  ) as  ?fiscal_year_2003 )
       (sum (?fy_2004  ) as  ?fiscal_year_2004 )
       (sum (?fy_2005  ) as  ?fiscal_year_2005 )
       (sum (?fy_2006  ) as  ?fiscal_year_2006 )
       (sum (?fy_2007  ) as  ?fiscal_year_2007 )
       (sum (?fy_2008  ) as  ?fiscal_year_2008 )
WHERE {  
  GRAPH <http://data-gov.tw.rpi.edu/vocab/Dataset_401>
    {	
         ?entry dgp401:bureau_name ?buname .

         ?entry dgp401:num2008 ?fy_2008 .
         ?entry dgp401:num2007 ?fy_2007 .
         ?entry dgp401:num2006 ?fy_2006 .
         ?entry dgp401:num2005 ?fy_2005 .
         ?entry dgp401:num2004 ?fy_2004 .
         ?entry dgp401:num2003 ?fy_2003 .
         ?entry dgp401:num2002 ?fy_2002 .
         ?entry dgp401:num2001 ?fy_2001 .
         ?entry dgp401:num2000 ?fy_2000 .
         ?entry dgp401:num1999 ?fy_1999 .
         ?entry dgp401:num1998 ?fy_1998 .
         ?entry dgp401:num1997 ?fy_1997 .
         ?entry dgp401:num1996 ?fy_1996 .
         ?entry dgp401:num1995 ?fy_1995 .
         ?entry dgp401:num1994 ?fy_1994 .
         ?entry dgp401:num1993 ?fy_1993 .
         ?entry dgp401:num1992 ?fy_1992 .
         ?entry dgp401:num1991 ?fy_1991 .
         ?entry dgp401:num1990 ?fy_1990 .
         ?entry dgp401:num1989 ?fy_1989 .
         ?entry dgp401:num1988 ?fy_1988 .
         ?entry dgp401:num1987 ?fy_1987 .
         ?entry dgp401:num1986 ?fy_1986 .
         ?entry dgp401:num1985 ?fy_1985 .
         ?entry dgp401:num1984 ?fy_1984 .
         ?entry dgp401:num1983 ?fy_1983 .
         ?entry dgp401:num1982 ?fy_1982 .
         ?entry dgp401:num1981 ?fy_1981 .
         ?entry dgp401:num1980 ?fy_1980 .
         ?entry dgp401:num1979 ?fy_1979 .
         ?entry dgp401:num1978 ?fy_1978 .
         ?entry dgp401:num1977 ?fy_1977 .
         ?entry dgp401:num1976 ?fy_1976 .

     }
}
group by ?buname

For this demo, you'll want to select an easy JSON format. Let's use Exhibit, since it's relatively bare in structure, and should be easy to use in Flare.

Using JSON in Flare

Before we even hit this snag, I'm going to point it out now. The first line of returned JSON should look something like this:

{"items":[
  {
    "buname":"Administration on Aging",
    "fiscal_year_1976":0,
    "fiscal_year_1977":0,
    "fiscal_year_1978":0,
    "fiscal_year_1979":0,
    "fiscal_year_1980":0,
    "fiscal_year_1981":0,
    "fiscal_year_1982":0,
    "fiscal_year_1983":0,
    ...
    "fiscal_year_2008":1411000
   },

We need to remove the "items" array format right off the bat, because in Flare, we are going to expect a "dataSet" object. One of the requirements for the data being returned is that it is an array type when its received, and this would actually be a Hash, or as they call it in ActionScript, an Associative Array. Why it won't just take what it's given is kind of an unclear question, but the end result is that, as the JSON stands, it wouldn't parse it correctly despite any attempts. So, for now, let's motor past that problem and just make the fix ourselves:

{
  {
    "buname":"Administration on Aging",
    "fiscal_year_1976":0,
    "fiscal_year_1977":0,
    "fiscal_year_1978":0,
    "fiscal_year_1979":0,
    "fiscal_year_1980":0,
    "fiscal_year_1981":0,
    "fiscal_year_1982":0,
    "fiscal_year_1983":0,
    ...
    "fiscal_year_2008":1411000
   },
  • (Note: eventually, we will most likely have another "Flare" format method option on the SPARQL proxy, but as of now (Jan 28, 2010), this has not been implemented.) (Mar 27, 2010: It requires a one-line change to sparqlxml2exhibitjson.xsl and could even be a parameter into it. -Tim Lebo)

Save this file somewhere online, or have it automatically cut the "items" part out somewhere via a script, then point the flare towards that. Either way, make this change, and make the resulting JSON available online. Or, if you're really strapped, I guess you could create the objects by placing the data in the actual Flare code, but that would be kind of silly and not make a ton of sense given what we want to do.

The code

For our demo, we are going to be altering some of the JobVoyager.as code. What is that exactly? It's one of the default flare demo applications. You can see a live demo of this particular utilization here: http://flare.prefuse.org/apps/job_voyager

Additionally, we will add in some parts from a tutorial explaining how to alter this code. This tutorial is located here: http://flowingdata.com/2009/12/09/how-to-make-an-interactive-area-graph/

Let's take a look at the JobVoyager code. Here's a listing of the various functions currently in place, along with what they do:

// Initialize. Load the data, parse it, call the visualization
protected override function init():void

// Visualize. Self explanatory. Most other functions (anything dependent on user input) 
// are conditionally called from within this section
private function visualize(data:Data):void

// Update the window that appears alongside the cursor with the relevant data.
private function updateTooltip(e:TooltipEvent):void

// Resize the chart if the window changes size
public override function resize(bounds:Rectangle):void

// Define the layout for the visualization (labels, guidelines, etc)
private function layout():void

// Application specific: filter data based on the gender of the data being viewed. 
private function filter(d:DataSprite):Boolean

// Callback for clearing changes made during a filter
private function onFilter(evt:Event=null):void

// Add all sorts of controls: Put the graph title up, create a gender filter set of 
// buttons, create the search box, change colors of sections of data based on where 
// the mouse is, and make the gender filter clickable, in that order.
private function addControls():void

// A terrible name for reshaping the data; it sounds like its a visual function 
// from its name, but it's actually talking about converting the data set into something
// that will be useful for the stackedAreaLayout to interpret.
public static function reshape(tuples:Array, cats:Array, dim:String, measure:String, cols:Array, normalize:Boolean=true):Array

FlowingData Tutorial

The Flowing Data Tutorial on StackAreaLayouts clearly explains how to take the sample JobVoyager.as code and manipulate it into something new. This is a perfect tutorial for learning what parts of the code you want to generally change, and which parts you want to leave alone, as it is generally going to stay the same (or almost exactly the same with only slight modifications) in the various iterations you develop in the beginning. That is to say, you don't have to know Flex to make a StackAreaLayout and use it in a compelling way; you can, however, further refine your knowledge of the language, and make extraordinary demos. That, however, begins to leave the purview of our research, and goes into just making awesome visualizations, which is great.

So, with all of that in mind, go ahead and run through that tutorial. In my own run through, I was particularly meticulous. If you want, you can also just cheat and grab the source for this demo and run ahead. Your call.

Altering the code once more

So, finally, we arrive at altering the code and making our own demo! Let's first manipulate the obvious parts. First, change the source of the data to whatever you're using. In my case, i'm changing it from:

		private var _url:String = "http://projects.flowingdata.com/america/spending/expenditures.txt";

To:

		private var _url:String = "http://devingaffney.com/example.json";


Additionally, the data set that the flowing data is annual for the years 1984-2008. That's just not going to work with our set, as we have 1976-2008:

private var _cols:Array = [1984,1985,1986,1987,1988,1989,1990,1991,
		           1992,1993,1994,1995,1996,1997,1998,1999,
			   2000,2001,2002,2003,2004,2005,2006,2007,2008];
private var _cols:Array = [1976,1977,1978,1979,1980,1981,1982,1983,
		           1984,1985,1986,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,
		           1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008];


Next, we want to change some more core parts. Let's look at init() and go from there.

In their code, init reads as such:


		protected override function init():void
		{
			addChild(_bar = new ProgressBar("Loading...", 150, 4, 0x666666));
			_bar.bar.filters = [new DropShadowFilter(1)];
			
			var ds:DataSource = new DataSource(_url, "tab");
			var ldr:URLLoader = ds.load();
			_bar.loadURL(ldr, function():void {
				// get loaded data, reshape for stacked columns
 				var ds:DataSet = ldr.data as DataSet;
           	                var dr:Array = reshape(ds.nodes.data, ["category"], "year", "expenditure", _cols);
           	                visualize(Data.fromArray(dr));
       		        _bar = null;
			});
  		}


In ours, we will want to make these changes:


		protected override function init():void
		{
			addChild(_bar = new ProgressBar("Loading...", 150, 4, 0x666666));
			_bar.bar.filters = [new DropShadowFilter(1)];
			
			var ds:DataSource = new DataSource(_dataSet, "json");
			var ldr:URLLoader = ds.load();
			_bar.loadURL(ldr, function():void {
  				var ds:DataSet = ldr.data as DataSet;
       	                var dr:Array = buildData(ds.nodes.data);
                               visualize(Data.fromArray(dr));
      		                _bar = null;
			});
  		}


Notice that only one line, var dr:Array = buildData(ds.nodes.data); is being replaced. This also inherently means some other things: we are no longer using the reshape() function, and instead are using an ad-hoc produced buildData() function. This is really the only work that you'll need to do directly in Flare, and it can usually be done relatively simply: given a set of data, how do we properly clean the data set so that when it is returned, we have a structure that can be used in the visualization?

In our case, since SPARQL's variables can't just be numbers (obviously) (I'm talking about ?fiscal_year_1976, all the way up to ?fiscal_year_2008), but we need numbers in the actual data objects to append it correctly to the StackAreaLayout, we have to do some work. In other words, initially, ds.nodes.data would read as an array of data objects, each one looking something similar to this:


Object.buname = "Department of Defense";
Object.fiscal_year_1976 = 20;
...
Object.fiscal_year_2008 = 2000000000;

When in reality, we want to conform our data set to the structure that has been laid out (this is sort of cargo cult programming, but then again, its easier to just transform our data than force the visualization to work with however we have our data as-is):


Object.buname = "Department of Defense";
Object[1976] = 20;
...
Object.[2008] = 2000000000;

So, we implement buildData(). Additionally, we could also add whatever we want. For instance, although the code above may give us accurate budgets, we'll actually want something a little more specific: percentages, or percent proportions of the total government budget for that fiscal year that was accounted for by that agency. For that, we implemented the system that first collects the sum budget of the year, then divides the particular agency's budget by that sum. So, buildData() reads as follows:


private function buildData(data:Array):Array{
	var totals:Object = new Object; //create a totals object with totals[1976] = the sum budget for the year, through 2008.
	for(var x:int = 1976; x<2009; x++) {
		var running:Number = 0;
		for(var y:int = 0; y < data.length; y++){
			running = running+Math.abs(data[y]['fiscal_year_'+x]); 
                                 // there are some negative numbers in the sets. No one is sure how 
                                 // to handle these, so my assumption right now is to just turn them 
                                 // into positives, assuming some typo. This is an ambiguity, and is a problem.
		}
		totals[x] = running; // sum the amount into running, save as totals[x], 
                                     // then clear running at the beginning of the next loop			
	}
	var cleanData:Array = new Array;
	for(var i:int = 0; i < data.length; i++){ // for each department
		var newObj:Object = new Object;
		newObj.buname = data[i].buname
		newObj.hsv = (1.0/data.length)*i;
		for(var j:int = 1976; j<2009; j++) { // for each year
                                                     // calculate percentage for the given year
			newObj[j] = Number(Math.abs(data[i]['fiscal_year_'+j])/totals[j]); 
		}
		cleanData[i] = newObj; // save the new object, cleaned up, into the array of good objects
	}
	return cleanData;              // and send that new one back to be visualized.
}

  • (Note: Remember, at this point, completely remove reshape() and any static variables it may have been using.)

Now, we need to address some final problems with layouts before we can call this good.

Cut out the functions

Next, let's get rid of things that flowing data didn't want to address; namely, all the vesitigal gender references and associated sections. Let's go through this surgically. The line numbers will change, as we're going through the code. If you follow the steps in order though, this should work in getting you there, and you'll be able to see the specific parts of the code that we touch, and the other stuff that we leave behind, and how we do it, which is particularly instructive:

1. remove line 45:(private var _gender:Legend;)
2. In layout, remove lines 209-212 (starts with "if (_gender) {")
3. Remove (.attach(_gender);) from line 282. This is attaching the gender Legend to the LegendItem HoverControl. 
    By removing it, we're merely just making it work for all of the items equally.
4. Remove 283-290:(starting with: "// filter by gender on legend click", obviously we don't need that functionality any more)
5. Remove the lines 61-63 (starts with: "private var _titleText:String =", this isn't our title.)
6. Line 167: change e.node.data.category to e.node.data.buname, since our "category" analog is a "buname"
7. Line 215: same deal, change "category" to "buname".
8. There's a whole bunch of crufty comments. get them out of here to clear up the code: Lines 243-248, lines 251-264
9. Remove that huge comment section prior to reshape(), and replace reshape with buildData().
10. Remove everything after the end of this package, namely from 288 on, the transitioner business.
11. SUPER IMPORTANT: change "tab" to "json" in the DataSource loading on line 66.

Now that that's finished, we can move on to the final set of pre-finished work, cleaning up the mess in the visualize() function. Before we continue, though, give it a run; it should work to a certain extent, but some fun parts, like clicking on the data and searching on a name, don't work. Lets make that happen now, and do it by changing two last commands in visualize() from "category" to "buname." You find them!

Finally, here is our working code:

package 
{
	import flare.animate.Transitioner;
	import flare.data.DataSet;
	import flare.data.DataSource;
	import flare.display.TextSprite;
	import flare.util.Shapes;
	import flare.util.Strings;
	import flare.util.palette.ColorPalette;
	import flare.vis.Visualization;
	import flare.vis.controls.ClickControl;
	import flare.vis.controls.HoverControl;
	import flare.vis.controls.TooltipControl;
	import flare.vis.data.Data;
	import flare.vis.data.DataSprite;
	import flare.vis.data.NodeSprite;
	import flare.vis.events.SelectionEvent;
	import flare.vis.events.TooltipEvent;
	import flare.vis.legend.Legend;
	import flare.vis.legend.LegendItem;
	import flare.vis.operator.encoder.ColorEncoder;
	import flare.vis.operator.filter.VisibilityFilter;
	import flare.vis.operator.label.StackedAreaLabeler;
	import flare.vis.operator.layout.StackedAreaLayout;
	import flare.widgets.ProgressBar;
	import flare.widgets.SearchBox;
	
	import flash.display.Shape;
	import flash.events.Event;
	import flash.filters.DropShadowFilter;
	import flash.geom.Rectangle;
	import flash.net.URLLoader;
	import flash.text.TextFormat;
	
	[SWF(backgroundColor="#ffffff", frameRate="30")]
	public class data_gov extends App
	{
		private var _bar:ProgressBar;
		private var _bounds:Rectangle;
		
		private var _vis:Visualization;
		private var _labelMask:Shape;
		private var _title:TextSprite;
		private var _search:SearchBox;
		
		private var _fmt:TextFormat = new TextFormat("Arial,Helvetica",12,0, false);
		private var _dur:Number = 1.25; // animation duration
		
		private var _reds:Array = [0xFFFEF0D9, 0xFFFDD49E, 0xFFFDBB84, 0xFFFC8D59, 0xFFE34A33, 0xFFB30000];
		private var _t:Transitioner;
		
		private var _query:Array;
		private var _filter:String = "All";
		private var _exact:Boolean = false;
		
		private var _url:String = "http://devingaffney.com/example.json";
		private var _cols:Array = [1976,1977,1978,1979,1980,1981,1982,1983,
		1984,1985,1986,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,
		1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008];
		
		protected override function init():void
		{
			addChild(_bar = new ProgressBar("Loading...", 150, 4, 0x666666));
			_bar.bar.filters = [new DropShadowFilter(1)];
			
			var ds:DataSource = new DataSource(_url, "json");
			var ldr:URLLoader = ds.load();
			_bar.loadURL(ldr, function():void {
				// get loaded data, reshape for stacked columns
  				var ds:DataSet = ldr.data as DataSet;
            	var dr:Array = buildData(ds.nodes.data);
            	visualize(Data.fromArray(dr));
        		_bar = null;
			});
  		}
  		
  		private function visualize(data:Data):void
		{
			// prepare data with default settings and sort
//			data.nodes.sortBy("data.occupation","data.sex");
			data.nodes.sortBy("data.buname");
			data.nodes.setProperties({
				shape: Shapes.POLYGON,
				lineColor: 0xFFFFFFFF
				 //fillColor: 0xFFEECE80
			});
			// expression sets male -> blue, female -> red
//			data.nodes.setProperty("fillHue", iff(eq("data.sex",1), 0.7, 0));
			
			// define the visualization
			_vis = new Visualization(data);
			// first, set the visibility according to the query
			_vis.operators.add(new VisibilityFilter(filter));
			_vis.operators[0].immediate = true; // filter immediately!
			// second, layout the stacked chart
			_vis.operators.add(new StackedAreaLayout(_cols, 0));
			_vis.operators[1].scale.labelFormat = "0.##%"; // show as percent
			// third, label the stacks
			_vis.operators.add(new StackedAreaLabeler("data.buname"));
			// fourth, set the color saturation for the current view
//			_vis.operators.add(new SaturationEncoder());

			var colorPalette:ColorPalette = new ColorPalette(_reds);
			_vis.operators.add(new ColorEncoder("data.max", "nodes", "fillColor", null, colorPalette));

			
			// initialize y-axis labels: align and add mask
			_labelMask = new Shape();
			_vis.xyAxes.addChild(_labelMask); // hides extreme labels
			_vis.xyAxes.yAxis.labels.mask = _labelMask;
			_vis.xyAxes.yAxis.verticalAnchor = TextSprite.TOP;
			_vis.xyAxes.yAxis.horizontalAnchor = TextSprite.RIGHT;
			_vis.xyAxes.yAxis.labelOffsetX = 50;  // offset labels to the right
			_vis.xyAxes.yAxis.lineCapX1 = 15; // extra line length to the left
			_vis.xyAxes.yAxis.lineCapX2 = 50; // extra line length to the right
			_vis.xyAxes.showBorder = false;
			
			// place and update
			_vis.update();
			addChild(_vis);
						
			// add mouse-over highlight
			_vis.controls.add(new HoverControl(NodeSprite,
			    // move highlighted node to be drawn on top
				HoverControl.MOVE_AND_RETURN,
				// highlight node to full saturation
				function(e:SelectionEvent):void {
					e.node.props.saturation = e.node.fillSaturation;
					e.node.fillSaturation = 1;
				},
				// return node to previous saturation
				function(e:SelectionEvent):void {
					e.node.fillSaturation = e.node.props.saturation;
				}
			));
				
			// add filter on click
			_vis.controls.add(new ClickControl(NodeSprite, 1,
				// set search query to the occupation name
				function(e:SelectionEvent):void {
					_exact = true; // force an exact search
					_search.query = e.node.data.buname;
				}
			));
			
			// add tooltips
			_vis.controls.add(new TooltipControl(NodeSprite, null,
			    // update on both roll-over and mouse-move
				updateTooltip, updateTooltip));
			
			// add title and search box
			addControls();
			layout();
		}
		
		private function updateTooltip(e:TooltipEvent):void
		{
			// get current year value from axes, and map to data
			var yr:Number = Number(
				_vis.xyAxes.xAxis.value(_vis.mouseX, _vis.mouseY));
			var year:String = Math.round(yr).toString();
//			var year:String = (10 * Math.round(yr/10)).toString();
			var def:Boolean = (e.node.data[year] != undefined);
			
			TextSprite(e.tooltip).htmlText = Strings.format(
				"<i>{0}</i><br/>{1}, "+(def?"{2:0.##%}":"{2}"),
				year, e.node.data.buname, (def ? e.node.data[year] : "Missing Data"));
		}
		
		public override function resize(bounds:Rectangle):void
		{
			if (_bar) {
				_bar.x = bounds.width/2 - _bar.width/2;
				_bar.y = bounds.height/2 - _bar.height/2;
			}
			bounds.width -= (35 + 50);
			bounds.height -= (45 + 35);
			bounds.x += 20;
			bounds.y += 45;
			_bounds = bounds;
			layout();
		}
		
		private function layout():void
		{
			if (_vis) {
				// compute the visualization bounds
				_vis.bounds = _bounds;
				// mask the y-axis labels to hide extreme animation
				_labelMask.graphics.clear();
				_labelMask.graphics.beginFill(0);
				_labelMask.graphics.drawRect(_vis.bounds.right,
					 _vis.bounds.top, 60, 1+_vis.bounds.height);
				// update
				_vis.update();
			}
			if (_title) {
				_title.x = -1;
				_title.y = _bounds.top - _title.height - 45;
			}
			if (_search) {
				_search.x = 0;
				_search.y = 0;
			}

			
		}
		
		/** Filter function for determining visibility. */
		private function filter(d:DataSprite):Boolean
		{
			if (!_query || _query.length==0) {
				return true;
			} else {
				var s:String = String(d.data["buname"]).toLowerCase();
				for each (var q:String in _query) {
					var len:int = q.length;
					if (len == 0) continue;
					if (!_exact && s.substr(0,len)==q) return true;
					if (_exact && q==s) return true;
				}
				return false;
			}
		}
		
		/** Callback for filter events. */
		private function onFilter(evt:Event=null):void
		{
			_query = _search.query.toLowerCase().split(/\|/);
			if (_query.length==1 && _query[0].length==0) _query.pop();
			
			if (_t && _t.running) _t.stop();
			_t = _vis.update(_dur);
			_t.play();
			
			_exact = false; // reset exact match after each search
		}
		
		// --------------------------------------------------------------------
		
		private function addControls():void
		{			
			// create search box
			_search = new SearchBox(_fmt, "Search: ", 250);
			_search.borderColor = 0xcccccc;
			_search.input.tabIndex = 0;
			_search.input.restrict = "a-zA-Z \\-";
			_search.addEventListener(SearchBox.SEARCH, onFilter);
			addChild(_search);
			
			// change alpha value on legend mouse-over
			new HoverControl(LegendItem, 0,
				function(e:SelectionEvent):void { e.object.alpha = 1; },
				function(e:SelectionEvent):void {
					var li:LegendItem = LegendItem(e.object);
					if (li.text != _filter) li.alpha = 0.3;
				}
			)

		}
		
		
		
		private function buildData(data:Array):Array{
				var totals:Object = new Object;
				for(var x:int = 1976; x<2009; x++) {
					var running:Number = 0;
					for(var y:int = 0; y < data.length; y++){
						running = running+Math.abs(data[y]['fiscal_year_'+x]);
					}
					totals[x] = running;				
				}
				var cleanData:Array = new Array;
				for(var i:int = 0; i < data.length; i++){
					var newObj:Object = new Object;
					newObj.buname = data[i].buname
					newObj.hsv = (1.0/data.length)*i;
					for(var j:int = 1976; j<2009; j++) { 
						newObj[j] = Number(Math.abs(data[i]['fiscal_year_'+j])/totals[j]);
					}
					cleanData[i] = newObj;
				}
				return cleanData;
			}
		
	} // end of class Stacks	
}


Now we understand a little more about the areas to play with, and the areas to treat like a black box. Let's look at a functional overview of this updated code:


Important Links

Personal tools
internal pages