Andrew Trice

Real-World Rich Internet Applications

20090212 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>

Posted by andrewtrice | Feb 12 2009, 02:04:01 PM EST
XML