Cooking an MVC framework for data visualization: Views part 1

In this second tutorial on MVC we will learn how to create a simple bar chart view. We will also cover a new concept called composite view. It is an important concept in the MVC world that allows a view to contain other views. It will let you build complex views out of small interchangeable pieces. When your application grows, it is crucial to write code that is reusable, so your application remains maintainable. Breaking views into small reusable chunks of code is one way to do that. Below is the result of what we are going to build in this tutorial.

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

Preparation

mvc views project overview

Here is an overview of what we are going to build in this tutorial. If you followed the last tutorial, you should have the Model and the View class ready. If not, you can download the whole project from here. We will write the SimpleModel class that is used to feed data to the chart. The SimpleBarChart class and the Bar class are parts of the bar chart view. We will talk about the relationship between them in the next section.

 

Building a composite view

To make the composite view work, we have to make sure that all of its pieces work together. That means they have to speak a common language. We have been doing that all along with our base View class. Essentially every view that inherits from the View class will have a render(content:Object) method. We don’t care about the specific implementation of that method, all we care about is that we can call it. Below is a diagram that explains this concept.

views project overview

The application contains myView, an instance of the ParentView class, and communicates with it through the render() method. The parent view then calls render() on its child view. The application doesn’t know or care about the child view nor the other way around. This makes it easy to swap the child view when needed. Note that in this system, the parent view also doesn’t know how the child view operates, it only knows that the child view expects a data object. Below is the code that will put this concept to practice.

 

 

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
// Bar.as
//-----------------------------------------------------------------
package views.barchart {
  import flash.display.*;
  import flash.events.*;
  import flash.text.*;
  import views.View;

  public class Bar extends View {
//-----------------------------------------------------------------
// CONSTANTS
//-----------------------------------------------------------------
    private static const DEFAULT_COLOR:int = 0x000000;
    private static const DEFAULT_ALPHA:Number = 0.2;
//-----------------------------------------------------------------
// CONSTRUCTOR
//-----------------------------------------------------------------
    public function Bar() {
    }
//-----------------------------------------------------------------
// API
//-----------------------------------------------------------------
    //  Render the view
    public override function render(content:Object):void {
      // A call to the base View's render() method
      // to store the content
      super.render(content);
      // draw the bar
      this.graphics.beginFill(DEFAULT_COLOR,DEFAULT_ALPHA);
      this.graphics.drawRect(
        0,
        0,
        this.content.width,
        this.content.height
      );
    }
//-----------------------------------------------------------------
  }
}

The render() method of the Bar class doesn’t do anything complex. It takes an object that has width and height properties and draws itself on the stage.

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
// 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 = 10;
    // 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]/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]) maxValue = content[i];
      }
      return maxValue;
    }  
//-----------------------------------------------------------------
  }
}

The SimpleBarChart class contains the most code we have written so far, but most of it is pretty straight forward. The render() method takes an array of numbers, loops through it to find the max value, calculates the bar width and creates instances of the Bar class. Line 37 to 40 is where the magic happens. We create the content object for the Bar class there.

Now that we have the bar chart view ready, let’s feed it data to see how it works.

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
// SimpleModel.as
//-----------------------------------------------------------------  
package models {
  import flash.display.*;
  import flash.events.*;
  import flash.text.*;
  import models.Model;
 
  public class SimpleModel extends Model {
//-----------------------------------------------------------------
// CONSTRUCTOR
//-----------------------------------------------------------------
    public function SimpleModel() {
    }
//-----------------------------------------------------------------
// API
//-----------------------------------------------------------------
    public override function getData():Object {
      return [
        5.3, 5.4, 5.2, 5.2, 5.1, 5.0, 5.0, 4.9, 5.0, 5.0,
        5.0, 4.9, 4.7, 4.8, 4.7, 4.7, 4.6, 4.6, 4.7, 4.7,
        4.5, 4.4, 4.5, 4.4, 4.6, 4.5, 4.4, 4.5, 4.4, 4.6,
        4.6, 4.6, 4.7, 4.7, 4.7, 5.0, 5.0, 4.8, 5.1, 5.0,
        5.4, 5.5, 5.8, 6.1, 6.2, 6.6, 6.9, 7.4, 7.7, 8.2,
        8.6, 8.9, 9.4, 9.5, 9.4, 9.7, 9.8, 10.1, 10.0,
        10.0, 9.7, 9.7, 9.7      
      ];     
    }
//-----------------------------------------------------------------
  }
}

The SimpleModel class just returns an array of numbers. All we have left is the Main class that puts everything 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
// 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 {
      // Get data from simple model
      var myModel:Model = new SimpleModel();     
      // Create a chart view
      var myChart:SimpleBarChart = new SimpleBarChart();
      // Send data to chart
      myChart.render( myModel.getData() );
      // Add to stage
      addChild(myChart);
    }
//-----------------------------------------------------------------
  }
}

Voilà! If you have typed everything correctly, you should see something like this.

We have gone over a very important concept today. Please spend some time playing around with this and let me know if you have any questions. Next time we will learn how to create a model class that can parse data from a CSV file, so we don’t have to type all the data manually.

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