Cooking an MVC framework for data visualization: Models

Last time we learned how to create a dynamic bar chart with data that we had to type in manually. That data entry part was tedious and unnecessary. Today we will solve that problem by creating a model that can load and parse data from an external file. Although in this example we only use the model to parse comma delimited file (CSV), we will build it in a way that allow us to parse tab delimited file as well. CSV is a very popular and compact format for storing data. It is also very easy to parse.

If you didn’t follow the previous tutorial, you can download the project from here.

Before we create our CSV model, we need to modify the base Model class slightly to make it ready for external data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Model.as
//-----------------------------------------------------------------
package models {
  import flash.events.EventDispatcher;   
  public class Model extends EventDispatcher {
//-----------------------------------------------------------------
// EVENT CONSTANTS
//-----------------------------------------------------------------
    public static const LOADING:String = "loading";
    public static const LOADED:String = "loaded";
    public static const LOAD_ERROR:String = "loadError";   
//-----------------------------------------------------------------
// CONSTRUCTOR
//-----------------------------------------------------------------
    public function Model() {
    }
//-----------------------------------------------------------------
// API
//-----------------------------------------------------------------
    public function loadData(url:String):void {
     
    }
    public function getData():Object {
      return null;
    }
//-----------------------------------------------------------------
  }
}

The Model class now inherits from EventDispatcher allowing all of its subclasses to dispatch events. We will use this capability to notify the rest of the application when the file is loaded. We also added three event strings (line 9, 10, 11) that our parser will dispatch at different stages of loading data. The last thing we added to the Model class is the loadData() method.

Now we are ready for the DelimitedTextParser class. It is relatively long, but most of the code has to do with setting up the events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// DelimitedTextParser.as
//-----------------------------------------------------------------
package models {
  import flash.events.*;
  import flash.net.*;
  import flash.errors.*;
  import models.Model;
   
  public class DelimitedTextParser extends Model {
//-----------------------------------------------------------------
// CONSTANTS
//-----------------------------------------------------------------  
    public static const TAB:String = "\t";
    public static const COMMA:String = ",";
//-----------------------------------------------------------------
// PROPERTIES
//-----------------------------------------------------------------
    public var delimiter:String;
    public var data:Array;
    private var loader:URLLoader = new URLLoader();  
//-----------------------------------------------------------------
// CONSTRUCTOR
//-----------------------------------------------------------------
    public function DelimitedTextParser(delimiter:String = COMMA) {
      this.delimiter = delimiter;    
      setEventListeners();
    }      
//-----------------------------------------------------------------
// API
//-----------------------------------------------------------------
    public override function loadData(url:String):void {
      if(!url) {
        throw new Error( "Error:DelimitedTextParser invalid url." );     
      }
      loader.load(new URLRequest(url));
     }
    public override function getData():Object {
      return data;
    }
//-----------------------------------------------------------------
// EVENT HANDLERS
//-----------------------------------------------------------------
    private function onLoading(e:Event):void {
      dispatchEvent(new Event(Model.LOADING));
    }  
    private function onLoaded(e:Event):void {
      parseData(String(loader.data));        
      dispatchEvent(new Event(Model.LOADED));    
    }
    private function onLoadError(e:Event):void {
      dispatchEvent(new Event(Model.LOAD_ERROR));          
    }
//-----------------------------------------------------------------
// PRIVATE METHODS
//-----------------------------------------------------------------
    private function parseData(rawData:String):void {    
      data = new Array();
      //  split data into rows using \n or \r character
      var rows:Array = rawData.split( "\n" );
      if(rows.length == 1) {
        rows = rawData.split( "\r" );
      }
      //  get column names
      var columnsName:Array = rows[0].split( delimiter );
     
      //  Loop that create an array of data objects
      for ( var i:Number = 1 ; i < rows.length; i++ ) {
        var RecordArray:Array = new Array();
        RecordArray = rows[i].split( delimiter );      
        var record:Object = new Object();
        for (var j:Number = 0; j<columnsName.length;j++) {
          record[columnsName[j]] = RecordArray[j]
        }
        data.push(record);
      }    
    }  
    private function setEventListeners():void {
      loader.addEventListener(ProgressEvent.PROGRESS, onLoading);
      loader.addEventListener(Event.COMPLETE, onLoaded);
      loader.addEventListener(IOErrorEvent.IO_ERROR, onLoadError);
    }
//-----------------------------------------------------------------
  }
}

You may notice that the constructor takes a parameter delimiter that has a default value of COMMA. We will use this delimiter string to parse the text we load from the external file. The constructor also sets up all the event handlers related to loading files from an url. We are using the URLLoader class (line 20) from Flash to load files from an external location, so the events we are handling are the events dispatched by that class.

Once the file is loaded, the onLoaded() (line 46) method is called. The model then parses the data and dispatches a Model.LOADED event. The parseData() (line 56) method is what does the actual job. It splits the text by lines and then by columns (line 64) based on the delimiter. It then creates an array of objects with each object representing the data on each line. It will convert raw text that looks like this:

1
2
3
4
5
// data.csv
//-----------------------------------------------------------------
year,period,value
2000,M01,4.0
2000,M02,4.1

To an array of objects that looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Array of objects
//-----------------------------------------------------------------
[
  {
    "year":2000,
    "period":M01,
    "value":4.0
  },
  {
    "year":2000,
    "period":M02,
    "value":4.1
  }
]

Now let’s put all the pieces together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Main.as
//-----------------------------------------------------------------
package {
  import flash.display.*;
  import flash.events.*;
  import flash.text.*;
  import models.*;
  import views.*;
  import views.barchart.*;

  public class Main extends MovieClip {
//-----------------------------------------------------------------
//  CONSTRUCTOR
//-----------------------------------------------------------------
    public function Main() {
      createSimpleBarChart();
    }
//-----------------------------------------------------------------
// PRIVATE METHODS
//-----------------------------------------------------------------
    private function createSimpleBarChart():void {
      // Create a parser model
      var myModel:Model = new DelimitedTextParser();     
      // Create a chart view
      var myChart:SimpleBarChart = new SimpleBarChart();
      // Add to stage
      addChild(myChart);             
      // Send data to the chart once it is loaded      
      myModel.addEventListener(Model.LOADED, function(e:Event) {       
        // Send data to chart
        myChart.render( myModel.getData() );
      });    
      // Load the data from a csv file
      myModel.loadData("unemployment.csv");
     
    }
//-----------------------------------------------------------------
  }
}

The Main class looks almost the same. We now wait for the data to be loaded and parsed (line 29) before sending it to the view (line 31). We will also need to make a small change to the SimpleBarChart view. It used to take an array of numbers to draw the bars, now it will take an array of objects (line 43 below). Pay attention to this part content[i]["value"]. The chart view looks for the “value” property in the data object, so we need to have a column called “value” in the original csv file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// SimpleBarChart.as
//----------------------------------------------------------------
package views.barchart {
  import flash.display.*;
  import flash.events.*;
  import flash.text.*;
  import views.View;
  import views.barchart.*;

  public class SimpleBarChart extends View {
//-----------------------------------------------------------------
// CONSTANTS
//-----------------------------------------------------------------
    // Padding between bars
    private static const PADDING:int = 1;
    // Default sizes for the chart
    private static const DEFAULT_WIDTH:int = 430;
    private static const DEFAULT_HEIGHT:int = 200;
//-----------------------------------------------------------------
// GLOBAL VARIABLES
//-----------------------------------------------------------------
    // store references to all bars on stage
    private var bars:Array = [];
//-----------------------------------------------------------------
// CONSTRUCTOR
//-----------------------------------------------------------------
    public function SimpleBarChart() {
    }
//-----------------------------------------------------------------
// API
//-----------------------------------------------------------------  
    // Render the view
    public override function render(content:Object):void {     
     
      super.render(content);
      // create the bars and add them to the stage
      var maxValue = getMaxValue();
     
      var maxBarWidth:Number = DEFAULT_WIDTH/content.length - PADDING;
      for (var i:int = 0; i < content.length; i++) {               
        var data:Object = new Object();
        // data that each bar will need to render itself
        data.height = content[i]["value"]/maxValue*DEFAULT_HEIGHT;
        data.width = maxBarWidth;
        // create and render a bar
        bars[i] = new Bar();
        bars[i].render(data);
        // position the bar
        bars[i].x = (maxBarWidth + PADDING)*i;
        bars[i].y = DEFAULT_HEIGHT - bars[i].height;
        addChild(bars[i]);       
      }
    }  
//-----------------------------------------------------------------
// PRIVATE METHODS
//-----------------------------------------------------------------
    private function getMaxValue():Number {
      var maxValue:Number = 0;
      for (var i:int = 0; i < content.length; i++) {
        if(maxValue < content[i]["value"]) maxValue = content[i]["value"];
      }
      return maxValue;
    }  
//-----------------------------------------------------------------
  }
}

That’s it, we are done. If you publish your application now, you should see something like this:

The chart shows monthly unemployment rate since 2000 from the U.S. Bureau of Labor Statistics.

We covered a very important concept in this tutorial. Following this pattern you should be able to build models that load data from a server and use the data to power your views. Next time we will go over how to add interactions to your view. We will also cover a very important design pattern called Singleton.

Follow any comments here with the RSS feed for this post. Both comments and trackbacks are currently closed.