Andrew Trice
Real-World Rich Internet Applications
Thursday February 12, 2009
Bitmap-Based Charting & Real Data
Recently, I've written a few posts explaining techniques for charting extremely large data sets using bitmap manipulation (here and here). Those posts show the techniques used to visualize large data sets, however all of the data is randomly generated on the client side. In this example, I wanted to find some data sets that represent real-world objects, so that I can demonstrate this technique with meaningful data to back it up.
I did a quick web search for "large statistical data set", and actually found a number of online data sets. In particular, I found one showing a distribution of height and weight for random set of 25,000 people. The data set is online here, via UCLA.
I modified the code from my previous example so that it will download and parse the data set. Take a look at the output below. Be patient while the data loads, it's about 6 MB. The download speed depends on your internet connection, but the data was parsed in about 1 second, and rendered in about 70 ms on my machine.
Launch example in a new window
Really, the only significant changes from my previous version is the infrastructure to load a remote data set for visualization. The data is loaded, parsed, and then rendered to the screen in exactly the same manner as before (although with a few tweaks geared directly towards this data set). The full source is located below. Be sure to check out comments to see what is going on.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
scriptTimeLimit="500"
creationComplete="onCreationComplete()"
backgroundColor="#999999">
<mx:Script>
<![CDATA[
import mx.controls.Text;
import mx.controls.Alert;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.formatters.NumberFormatter;
import mx.core.UITextField;
import flash.utils.getTimer;
private static const URL : String = "data/dataset.xhtml";
private static const MIN_HEIGHT : int = 60;
private static const MAX_HEIGHT : int = 76;
private static const MIN_WEIGHT : int = 74;
private static const MAX_WEIGHT : int = 172;
private var datum : Array = [];
private var textField : UITextField = new UITextField();
private var nf : NumberFormatter = new NumberFormatter();
private var request : URLRequest = new URLRequest(URL);
private var loader : URLLoader = new URLLoader();
private var parseTime : int = 0;
private var renderTime : int = 0;
private function onCreationComplete() : void
{
loadData();
}
private function loadData() : void
{
//load the data using a Loader object, so that we can monitor progress
loader.addEventListener( ProgressEvent.PROGRESS, onDownloadProgress );
loader.addEventListener( Event.COMPLETE, onComplete )
loader.addEventListener( Event.CLOSE, onFault );
loader.addEventListener( IOErrorEvent.IO_ERROR, onFault );
loader.load(request);
}
private function onDownloadProgress( event : ProgressEvent ) : void
{
progress.setProgress( event.bytesLoaded, event.bytesTotal );
}
private function onComplete( event : Event ) : void
{
parseTime = getTimer();
controlBar.visible = false;
controlBar.includeInLayout = false;
//convert the html output string to an xml object for parsing
var result : String = loader.data.toString();
var xml : XML = new XML( result );
var maxW : Number = 0;
var maxH : Number = 0;
//use this namespace to access the "x:" namespace inside of the xml structure
namespace ns = "http://wiki.stat.ucla.edu/socr/index.php/SOCR_Data_Dinov_020108_HeightsWeights";
use namespace ns;
//loop over the records and parse them
for each ( var tr : XML in xml..tr )
{
if ( XMLList( tr.td ).length() == 3 )
{
var dataPoint : Object = { height: parseFloat( tr.td[1].toString() ), weight: parseFloat( tr.td[2].toString() ) }
dataPoint.x = (dataPoint.weight - MIN_WEIGHT) / (MAX_WEIGHT - MIN_WEIGHT);
dataPoint.y = (dataPoint.height - MIN_HEIGHT) / (MAX_HEIGHT - MIN_HEIGHT);
datum.push( dataPoint );
}
}
parseTime = getTimer() - parseTime;
renderData();
}
private function onFault( event : Event ) : void
{
Alert.show( "ERROR LOADING DATA" );
}
private function renderData() : void
{
renderTime = getTimer();
nf.precision = 1;
var i : Number = 0;
var w : int = chart.width;
var h : int = chart.height;
var borderSize : int = 40;
var chartW : int = w - 2*borderSize;
var chartH : int = h - 2*borderSize;
var bd : BitmapData = new BitmapData( w, h, false, 0xFFFFFF );
var color : int = 0xFF0000;
//render each data point
for each (var o : Object in datum)
{
//set pixels in a cross-shape
bd.setPixel( borderSize+(o.x * chartW), borderSize+(chartH-(o.y * chartH)), color );
bd.setPixel( borderSize+(o.x * chartW)+1, borderSize+(chartH-(o.y * chartH)), color );
bd.setPixel( borderSize+(o.x * chartW)-1, borderSize+(chartH-(o.y * chartH)), color );
bd.setPixel( borderSize+(o.x * chartW), borderSize+(chartH-(o.y * chartH))+1, color );
bd.setPixel( borderSize+(o.x * chartW), borderSize+(chartH-(o.y * chartH))-1, color );
bd.setPixel( borderSize+(o.x * chartW)+2, borderSize+(chartH-(o.y * chartH)), color );
bd.setPixel( borderSize+(o.x * chartW)-2, borderSize+(chartH-(o.y * chartH)), color );
bd.setPixel( borderSize+(o.x * chartW), borderSize+(chartH-(o.y * chartH))+2, color );
bd.setPixel( borderSize+(o.x * chartW), borderSize+(chartH-(o.y * chartH))-2, color );
}
var segments : int = 10;
var interval : Number = chartW/segments;
var q:Number;
var labelCount : int = 0;
var labelIncrement : Number = .1;
var matrix : Matrix = new Matrix();
matrix.identity();
matrix.translate( borderSize-5, h - borderSize );
//render verical line overlays
for ( i = borderSize; i<=w-(borderSize-2); i += interval )
{
for ( var j : int = borderSize; j < h-borderSize; j ++ )
{
bd.setPixel( i, j, 0 );
}
//add horizontal labels
textField.text = nf.format(MIN_WEIGHT + (labelCount * labelIncrement * (MAX_WEIGHT-MIN_WEIGHT) ));
bd.draw( textField, matrix );
matrix.translate( interval, 0 );
labelCount++;
}
//render horizontal line overlays
interval = chartH/segments;
labelCount = 0;
matrix.identity();
matrix.translate( 11, h - (borderSize + 10) );
for ( i = borderSize; i<=h-(borderSize-2); i += interval )
{
for ( j = borderSize; j < w-borderSize; j ++ )
{
bd.setPixel( j, i, 0 );
}
//add verical labels
textField.text = nf.format(MIN_HEIGHT + (labelCount * labelIncrement * (MAX_HEIGHT-MIN_HEIGHT)));
bd.draw( textField, matrix );
matrix.translate( 0, -interval );
labelCount++;
}
//add header
nf.precision = 0;
textField.text = "Random Height & Weight Distribution: " + nf.format(datum.length) + " People";
textField.width = textField.textWidth + 10;
matrix.identity();
matrix.translate( ( w-textField.width)/2, 5 );
bd.draw( textField, matrix );
//add horizontal label
textField.text = "Weight (Pounds) ";
textField.width = textField.textWidth + 10;
matrix.identity();
matrix.translate( ( w-textField.width)/2, h - 22 );
bd.draw( textField, matrix );
//add vertical label
textField.text = "Height (Inches) ";
textField.width = textField.textWidth + 10;
matrix.identity();
matrix.translate( 10, 10 );
bd.draw( textField, matrix );
chart.source = new Bitmap( bd );
renderTime = getTimer() - renderTime;
//write the processing time output
timeOutput.text = "parsed in " + parseTime.toString() + "ms, rendered in " + renderTime.toString() + "ms";
}
]]>
</mx:Script>
<mx:ApplicationControlBar
id="controlBar"
dock="true">
<mx:ProgressBar
themeColor="#FF0000"
mode="manual"
id="progress"
width="100%"
height="100%"
labelPlacement="center" />
</mx:ApplicationControlBar>
<mx:Image
id="chart"
top="10" bottom="10" left="10" right="10"
cacheAsBitmap="true"
resize="renderData()" />
<mx:Text bottom="10" left="10" id="timeOutput" />
</mx:Application>






