Creating a simple Organigram control in Flex AS3

There are many solutions for this out there but I haven’t found any of them for free or as Open Source, so here we are going to learn how to make one from scratch. This version is written in Action Script 3, and it is part of the FlexTutsLibrary. If you want to know what the final product of this tutorial looks like, see it here: view Demo.

To get the FlexTutsLibrary for your projects go to the FlexTuts Library section and download the latest release.

This may be a very basic tutorial for many of you; if so, you are free to skip it and find one that make you feel you are, in fact, learning something :)

Step 1: Hand draw version of the control and functionality definition

In order to create a visual control I think it is important to have an image where you can analyze the behavior, here is my first visualization for this control.

As you see it's not much impressive but good enough to analize behavior, from the image we can conclude and/or assume:

Conclusions

Assumptions

Now we need to think on what this control can do.

Behavior

The node will have the distribution shown in the image

A node is composed by the following visible elements:

You may add more elements according to your needs.

Step 2: Project Organization

In this step we will create and organize the library project and a flex application project to test the library, from now on I'll assume that you have installed the Flex Builder 3.

Creating the library project: FlexTutsLibrary
To create a library project in Flex Builder 3 go to File>New>Flex Library Project. A window will be shown. Then fill the Project name field with 'FlexTutsLibrary' or the name you want to give to your library and click Finish at the bottom.

Creating the flex application: Organigram
Go to File>New>Flex Project. In the window fill the Project name field with 'Organigram' or the name you want to give to your application project, then click Finish at the bottom.

Creating class files: Node and Organigram
To create the Node class go to File>New>ActionScript Class, in the window check if the Project field says 'FlexTutsLibrary'; if not, you can change it by using the Browse button at the right then fill the Package name field with 'ft.controls' or use your own name for the package, in the Name field write 'Node', I recomend you not to use other name but you can use another if you want, in the Superclass field write 'mx.containers.Canvas'. Click Finish.

To create the Organigram you have to do the same you did to create ne Node class but in the Name field write Organigram instead.

In the next image you can see how your project should looks, at this point your project does not have resources folder that appears in the FlexTutsLibrary project, we will talk about it later. Another difference you may find are the icons. I am using a CVS repository to develop the project and the icons changed when I began to use CVS. It's not a problem not to use a CVS repository but if you have a CVS server I recommend you to use it.

Step 3: Implementing the Node class

Before reading this step, you have to download the source code of this control from the FlexTuts Library section, that way it will be easier to understand what we are doing.

Calculating the size (width and height) of a node
The following image will help us calculate the total width and height

from the image we can say:

nodeWidth = border + picture.width + space + title.width + border
nodeHeight = border + max ( picture.height , title.height + description.height ) + border

Overriding functions in the Node class
When you make a visual control you must at least inherit from the UIComponent of the mx.core package, in this case we made it inherit from Canvas just to avoid some events implementation like the "click".

There are some function you must override for your control to work properly and they are:

createChildren : will be called by the system when the control is created
commitProperties : will be called by the system when invalidateProperties is called
measure : will be called by the system when needs to calculate you control's size
updateDisplayList : will be called by the system when its time to place all controls insite yours.

In the createChildren function we create the objects that will be used in our Node control.

private var thumbnail:Image;
private var title:Text;
private var description:Text;

// Implement createChildren function. 
override protected function createChildren():void {
	super.createChildren();
	if(!thumbnail) { // validate if the thumbnail object was created before
		thumbnail = new Image(); // creates an image object
		thumbnail.explicitHeight = 60; // asigning a default value
		thumbnail.explicitWidth = 54; // asigning a default value
		thumbnail.setStyle("borderStyle", "solid");
		addChild(thumbnail);
	}
	if(!title) { // validate if the title object was created before
		title = new Text(); // creates a text object
		title.explicitWidth = 100;  // asigning a default value
		title.explicitHeight = 20;  // asigning a default value
		title.selectable = false; 
		title.setStyle("color",0xFFFFFF);
		title.setStyle("fontWeight","bold");
		addChild(title);
	}
	if(!description){ // validate if the description object was created before
		description = new Text(); // creates a text object
		description.explicitWidth = 100; // asigning a default value
		description.explicitHeight = 40; // asigning a default value
		description.selectable = false;
		description.setStyle("fontWeight","bold");
		addChild(description);
	}
	this.addEventListener("click", handleEventClick);
}

In the commitProperties function we assign values from the temporary variables to the controls created before

// Implement commitProperties function.
override protected function commitProperties():void{
	super.commitProperties();
	if(_thumbnailChanged){
		_thumbnailChanged = false;
		thumbnail.source = _thumbSource;
	}
	if(_descriptionChanged){
		_descriptionChanged = false;
		description.text = _descText;
	}
	if(_titleChanged){
		_titleChanged = false;
		title.text = _titleText;
	}	
}

In the measure function we calculate the control size, here we will use the formula we got from the image a the top.

// Implement measure() function.
override protected function measure():void{
	super.measure();

	var titleWidth:Number;
	var descWidth:Number;
	var thumbWidth:Number = thumbnail.getExplicitOrMeasuredWidth();
	
	titleWidth = descWidth = Math.max( title.getExplicitOrMeasuredWidth(), description.getExplicitOrMeasuredWidth());
	title.measuredWidth = titleWidth;
	description.measuredWidth = descWidth;
	
	var titleHeight:Number = title.getExplicitOrMeasuredHeight();
	var descHeight:Number = description.getExplicitOrMeasuredHeight();
	var thumbHeight:Number = thumbnail.getExplicitOrMeasuredHeight();
	
	this.measuredWidth = this.measuredMinWidth = border + thumbWidth + space + titleWidth + border;
	this.measuredHeight = this.measuredMinHeight = Math.max(titleHeight + descHeight, thumbHeight) + 2 * border;
}

In the updateDisplayList function we place the controls in the position they should go.

override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
	super.updateDisplayList(unscaledWidth, unscaledHeight);
		
	var titleWidth:Number;
	var descWidth:Number;
	var thumbWidth:Number = thumbnail.getExplicitOrMeasuredWidth();
	
	titleWidth = descWidth = Math.max( title.getExplicitOrMeasuredWidth(), description.getExplicitOrMeasuredWidth());

	var titleHeight:Number = title.getExplicitOrMeasuredHeight();
	var descHeight:Number = description.getExplicitOrMeasuredHeight();
	var thumbHeight:Number = thumbnail.getExplicitOrMeasuredHeight();

	// Set actual size of each child component
	// Set thumbnail actual size
	thumbnail.setActualSize(thumbWidth, thumbHeight);
	// Set title actual size
	title.setActualSize(titleWidth, titleHeight);
	// Set description actual size
	description.setActualSize(descWidth, descHeight);
		
	// Placing thumbnail 
	thumbnail.move(border, border);
		
	// Placing title
	title.move(border + thumbWidth + space, border);
		
	//  Placing description
	description.move(border + thumbWidth + space, border + titleHeight);  
}

Step 4: Implementing additional functions for Node

In the previous step I left an important implementation, it is important because every class visual or not must have it, it is called the constructor and it has the same as the class that is being implemented, in our case we have the following code for the constructor.

// Implement class constructor
public function Node(): void {
	super(); // always call the superclass constructor in aour case Canvas
		
	setStyle("backgroundImage", normalSkin); // Setting up the background image
	setStyle("backgroundSize", "100%"); // adjusting background image to the size of our control
	setStyle("backgroundAlpha", 0.8); // setting transparency for background image

	var fadeIn:Fade = new Fade(); // Effect for for rollover
	fadeIn.alphaFrom = 1.0;
	fadeIn.alphaTo = 0.7;
	fadeIn.duration = 500;

	var fadeOut:Fade = new Fade(); / Effect for rolout
	fadeOut.alphaFrom = 0.7;
	fadeOut.alphaTo = 1.0;
	fadeOut.duration = 500;

	// Setting up effects
	setStyle("rollOverEffect",fadeIn);
	setStyle("rollOutEffect",fadeOut);
}

Now, from the step 1 we conclude that each node has one parent and it may have children, to implement this we will declare a parent variable of the type Node and an array for the children, also we need to implement an addChild method to let our control create a new node and add it as a child of the current node.

public var parentNode:Node;
public var childNodes:Array;

// Implementing parents, childs association
public function addChildNode(child:Node):void{
	if(!childNodes){
		childNodes = [child];
	}
	else {
		childNodes.push(child);
	}
	child.parentNode = this;
}

Additionally from behavior in step 1 we conclude that we need a status variable for the selection, we will declare selected as boolean to do this and we will change it when the control is clicked, a function to deselect the node is also needed.

public var selected:Boolean = false;
// Event handlers
private function handleEventClick(objEvent:MouseEvent):void{
	// find the root node, we will deselect all the nodes and then set this instance as selected.
	var rootNode:Node = this;
	while(rootNode.parentNode){
		rootNode = rootNode.parentNode;
	}
			
	deselectNodes(rootNode);
			
	this.selected = true;
	this.setStyle("backgroundImage", activeSkin);
	invalidateDisplayList();
	dispatchEvent(new Event("nodeSelected") );
}
		
// deselect the node and its children
private function deselectNodes(node:Node):void{
	node.selected = false;
	node.setStyle("backgroundImage", normalSkin);
			
	node.invalidateDisplayList();
	if(node.childNodes){
		for( var iNode:int = 0; iNode < node.childNodes.length; iNode++){
			deselectNodes(node.childNodes[iNode] as Node);
		}
	}
}

Step 5: Implementing the Organigram Class

The Node class was simple, the Organigram is not going to be different, as we did before we need to implement a constructor, override some functions and implement some additional functions.

Calculating the size (width and height) of the organigram
Let's check the picture

To calculate the width we have to find all the nodes without children in the picture those nodes are numbered from 1 to 9, in general we will use N as the number of nodes without children, so we have the next formula for the minimun width:

organigram.width = hSpace + N * node.width + ( N - 1 ) * hSpace + hSpace

Calculating the height is easier, in the image we see 5 levels, we will use L to denote the number of level:

organigram.height = vSpace + L * node.height + ( L - 1 ) * vSpace + vSpace

Implementing the contructor

// Implement the constructor
public function Organigram(){
	super();
}

Overriding functions in the Organigram class

In this case the createChildren will not do anything special, just call the commitProperties super class method. We will create the children controls later.

// Implement createChildren function
override protected function createChildren():void{
	super.createChildren();
}

The same occurs with the commitProperties function

// Implement commitProperties function.
override protected function commitProperties():void{
	super.commitProperties();
}

In the measure function we need to calculate the size of our control, for this we will create two additional functions that will give us the number of levels (countLevels) and the number of nodes without children (countNodesWithoutChilden).

// Implement measure function.
override protected function measure():void{
	super.measure();
	
	if(rootNode){
		// Use new functions to calculate with and height
		 var nLevels:uint = countLevels(rootNode);
		 var nFinalNodes:uint = countNodesWithoutChildren(rootNode);
				 
		 // Calculate with ( we assume that all nodes have the same with )
		 var nodeWidth:Number = rootNode.width;
		 var compWidth:Number = nFinalNodes * nodeWidth + (nFinalNodes + 1) * _hSpace ;
				 
		 // Calculate height
		 var nodeHeight:Number = rootNode.height;
		 var compHeight:Number = nLevels * nodeHeight + (nLevels + 1 ) * _vSpace ;
				 
		 this.measuredWidth = this.measuredMinWidth = compWidth;
		 this.measuredHeight = this.measuredMinHeight = compHeight;
	}
}

The countLevel function looks for the highest level of all the children for a given node.

// Calculates how many level have this tree
private function countLevels(node:Node, level:uint = 1):uint{
	if(node)
	{
		if(!node.childNodes){
			return level;
		}
		else{
			var maxLevel:uint = level;
			for( var iNode:Number = 0; iNode < node.childNodes.length ; iNode++) {
				var childNode:Node = node.childNodes[iNode] as Node;
				maxLevel = Math.max(maxLevel, countLevels(childNode, level + 1));
			}
			return maxLevel;
		}
	}
	else{
		return 0;
	}
}

The countNodesWithoutChildren is also recursive and returns 1 for any node withoutchildren, if the node has children it evaluates every children and accumulates the returned values.

// Calculates how many nodes without children have this tree  
private function countNodesWithoutChildren(node:Node):uint{
	if(node){
		if(!node.childNodes){
			return 1;
		}
		else{
			var count:uint = 0;
			for( var iNode:Number = 0; iNode < node.childNodes.length ; iNode++){
				var childNode:Node = node.childNodes[iNode] as Node;
				count += countNodesWithoutChildren(childNode);
			}
			return count;
		}
	}
	else{
		return 0;
	}
}

In the next step we will see how to place every node in the correct position.

Step 6: Placing nodes in the organigram

In this final step we will place every node in it's correct position, from the previous step we know that the width of the entire control depends on the number of nodes without children and the number of level, so, we can easily place the nodes without children, its position is given by the up left corner position (x, y).

For every node without children we have:

x = (n - 1) * nodeWidth + (n - 1) * hSpace + hSpace; // n = number of node without children
y = (l - 1) * nodeHeight + (l - 1) * vSpace + vSpace; // l = number of level

the previous formulas can be writen like this:

x = (n - 1) * (nodeWidth + hSpace) + hSpace; // n = number of node without children
y = (l - 1) * (nodeHeight + vSpace) + vSpace; // l = number of level

After placing nodes without children it is very easy to place parent nodes, we just have to recognize that a parent node its at a lower level and in the middle of its first and last children.

To analyze nodes of the entire tree, we will call a recursive function and pass an array reference to let u count how many nodes without children we find.

We have to do this on the updateDisplayList function, let's see how to implemented it.

// Implement updateDisplayList function
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
	super.updateDisplayList(unscaledWidth,unscaledHeight);
	if(rootNode){
		// We will calculate the order in wich the nodes should be placed.
		var nodesWOC:Array = [0];
		var posRoot:Array = placeNode(rootNode,1, nodesWOC);
		drawPathLines(rootNode); 
	}
	else{
		graphics.clear();
	}
}

The placeNode function will place all the modes begining with the nodes without children.

private function placeNode(node:Node, level:uint, nodesWOC:Array):Array{
	var nodeHeight:Number = node.height;
	var nodeWidth:Number = node.width;
	
	if(!node.childNodes){
		// Placing a node without children
		nodesWOC[0] = (nodesWOC[0] as Number ) + 1; 
		node.move( ( nodeWidth + _hSpace )* ( (nodesWOC[0] as Number) - 1) + _hSpace, (nodeHeight + _vSpace) * (level - 1) + _vSpace );
		return [ (nodeWidth + _hSpace )* ( (nodesWOC[0] as Number) - 1) + _hSpace, (nodeHeight + _vSpace) * (level - 1) + _vSpace];
	}
	else{
		// Placing a parent node
		var positionX:Number;
		var positionY:Number;
		var posFirst:Array;
		var posLast:Array;
		var posTemp:Array;
		// Placing all children first
		for( var iNode:Number = 0; iNode < node.childNodes.length; iNode++ ){
			var childNode:Node = node.childNodes[iNode];
			posTemp = placeNode(childNode, level + 1, nodesWOC);
			if( iNode == 0){
				posFirst = posTemp;
			}
			if( iNode == node.childNodes.length - 1){
				posLast = posTemp;
			}
		}
		// Calculating position for parent node based on first and last children
		positionX = ( (posFirst[0] as Number) + (posLast[0] as Number) )/2;
		positionY = (nodeHeight + _vSpace) * (level - 1) + _vSpace;
		node.move(positionX, positionY);
		return [positionX, positionY];
	}
}

Well, that's all the "difficult" part, I'm sure you can handle what is not explained here.
I hope you liked it.