Collections of web application techniques

Saturday, September 4, 2010

How to Create Gantt Chart in Flex.

Gantt chart as below can be created using Flex components.  The chart below is created using mx:Barchart.


There are a few steps to create such a chart:
  1. Data model
  2. Configure mx:Barchart
  3. Tooltip
  4. Bar drawing

To encapsulate all the data to be sent to the chart, I define a model.  Below is the code to create the model which contains the data to be display on the chart.   The model contains an array collection of projects to be displayed on the chart.

Data Model

package model
{    
    import mx.collections.ArrayCollection;

    [Bindable]
    public class Model
    {
        private static var modelLocator:Model;

        public var projectsToDisplay:ArrayCollection = new ArrayCollection();
        
    }    
}

The projectsToDisplay collection contains a collection of Project objects.  The Project class is defined below.  To make things more interesting on my chart, I added phases and milestones which are also defined as ArrayCollection in a project.

Here are the attributes for my project:

public class Project
    {
        public var id:Number;
        public var status:StatusVO;
        public var title:String;
        public var startDate:Date;
        public var endDate:Date;
        public var notes:String;
        public var percentComplete:Number;
        public var currentStatus:String;
        public var persons:ArrayCollection;
        public var milestones:ArrayCollection;
        public var phases:ArrayCollection;
    }

BarChart

Here is how the chart is created using mx:BarChart.  Attributes to pay attention to are:
  • itemClick: this defines an event handler if you want anything to occur when one clicks on a bar on the chart.
  • dataProvider: notice how the data is sent to the chart.
  • dataTipProvider: this allows you to customize how the tool tip will be display when the cursor is on the bar.
  • Note how the vertical and horizontal axes are defined as this may take time to figure out.  You can pick what field of you class to be displayed on the vertical axis.  In my case I pick the title field.  Since I have many projects to display, I wanted some control over the date range so I specify them via the minimum and maximum attributes on the horizontal axis. This is the range for the horizontal scale. Figure out the attributes for the mx:BarSeries also took me a bit of time.  Notice that the startDate is specify in the minField attribute and the endDate is in the xField attribute.  itemRenderer is where you can customize how your bars are drawn.


<mx:BarChart id="bar1" height="50%" width="100%" paddingLeft="5" paddingRight="5" showDataTips="true" itemClick="setProject(event)" dataProvider="{myModel.projectsToDisplayPhase}" themeColor="#174F73" dataTipRenderer="project.renderers.ProjectToolTip">
    <mx:verticalAxis>
        <mx:CategoryAxis categoryField="title" labelFunction="chartLabel" />
    </mx:verticalAxis>
    <mx:horizontalAxis>
        <mx:DateTimeAxis dataUnits="days" minimum="{minDate}" maximum="{maxDate}" dataInterval="1"
                         minorTickInterval="3" minorTickUnits="months" labelUnits="months" />
    </mx:horizontalAxis>
    <mx:series>
        <mx:BarSeries yField="title" xField="endDate" minField="startDate" itemRenderer="project.renderers.ProjectRenderer3" />
    </mx:series>
</mx:BarChart>

Tooltip

To create the tooltip, you have to specify in the dataTipRenderer attribute of the BarChart component.  Here is the code for my tooltip:

<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" backgroundColor="0xFFBBBB" backgroundAlpha=".9"
         borderColor="0xFF0000" borderStyle="solid" paddingTop="5" paddingBottom="5"
         paddingRight="5" paddingLeft="5" verticalGap="-4" color="0x222222" minHeight="100"
         minWidth="350" maxWidth="500" xmlns:renderers="renderers.*"
         xmlns:local="project.renderers.*" creationComplete="init()">
    <mx:HBox>
        <mx:Label id="projectLabel" textAlign="center" fontWeight="bold" text="Project:" />
        <mx:Text text="{project.title}" />
    </mx:HBox>
    <mx:HBox>
        <mx:Label fontWeight="bold" text="Phases: {project.phases.length}" />
        <mx:Label fontWeight="bold" text="Milestones: {project.milestones.length}" />
        <mx:Label fontWeight="bold" text="Team: {project.persons.length}" />
    </mx:HBox>

    <mx:HBox horizontalGap="0">
        <mx:Label fontWeight="bold" text="Complete:" />
        <mx:Text text="{project.percentComplete}%" />
        <mx:Spacer width="20" />
        <mx:Label fontWeight="bold" text="Status:" />
        <mx:Text text="{project.currentStatus}" fontWeight="bold" color="green" />
        <mx:Spacer width="20" />
        <mx:Label fontWeight="bold" text="Date:" />
        <mx:Text id="dateRange" />
    </mx:HBox>

    <mx:HBox width="100%">
        <mx:Label fontWeight="bold" text="Notes:" width="45" />
        <mx:TextArea width="100%" text="{project.notes.length > 0 ? project.notes : '(No notes)'}"
                     backgroundColor="0xFFDDDD" alpha=".9" borderColor="0xFFAAAA" />
    </mx:HBox>

    <mx:DateFormatter id="dateFormatter" formatString="MM/DD/YYYY" />

    <mx:Script>
        <![CDATA[
            import vo.ProjectVO;
            import mx.charts.series.items.BarSeriesItem;
            import mx.charts.HitData;

            [Bindable]
            public var project:ProjectVO;

            [Bindable]
            private var projectView:String = "None";

            private function init():void
            {
                dateRange.text = dateFormatter.format(project.startDate) + " - " + dateFormatter.format(project.endDate);
            }

            override public function set data(value:Object):void
            {
                var hd:HitData = value as HitData;
                project = hd.item as ProjectVO;
                init();
            }
        ]]>
    </mx:Script>
</mx:VBox>

You need to override the set data method to get a handle to the data object to be displayed on the tooltip.  Once you figure that out, the rest is just pure fun on however you want to display the data.

Bar Drawing

Below is the code to draw the bars are drawn on the chart.


<?xml version="1.0"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" horizontalScrollPolicy="off" xmlns:com="com.*"
           verticalScrollPolicy="off" implements="mx.core.IDataRenderer"
           xmlns:renderers="project.renderers.*" creationComplete="init()">
    <mx:Script>
        <![CDATA[
            import mx.controls.Button;
            import mx.controls.Label;
            import mx.core.UIComponent;
            import mx.events.ToolTipEvent;
            import vo.*;

            import flash.display.Graphics;
            import mx.controls.Alert;

            import mx.charts.series.items.BarSeriesItem;
            import mx.collections.ArrayCollection;
            import mx.core.IDataRenderer;
            import mx.skins.ProgrammaticSkin;
            import flash.geom.Matrix;
            import flash.display.GradientType;
            import mx.charts.series.items.BarSeriesItem;

            public static var projectColors:Array = [0xFF3333, 0x00DDDD, 0x0066DD, 0x00DD99, 0x00DD22, 0xDDDD00, 0xAAAA00];

            private var _chartItem:BarSeriesItem;

            private static var MAX_BAR_HEIGHT:Number = 20;

            private static var HALF_MAX_BAR_HEIGHT:Number = 10;

            private var milestoneRenderers:ArrayCollection = new ArrayCollection();

            [Bindable]
            private var yPosition:Number;

            private function init():void
            {
            }

            private function handleEdit():void
            {
                var url:String = "<a href="http://inventasoft.blogspot.com">http://inventasoft.blogspot.com</a>"
                var request:URLRequest = new URLRequest(url);
                navigateToURL(request, "_blank");
            }

            private function handleClick():void
            {
                Alert.show("Test: ");
            }

            override public function get data():Object
            {
                return _chartItem;
            }

            override public function set data(value:Object):void
            {
                _chartItem = value as BarSeriesItem;
                invalidateDisplayList();
            }

            override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
            {
                super.updateDisplayList(unscaledWidth, unscaledHeight);

                var status:Number = _chartItem.item.status != null ? _chartItem.item.status.id : 0;

                var yStart:Number;
                var yMid:Number;
                var yEnd:Number;
                var ySize:Number;

                var xStart:Number;
                var xMid:Number;
                var xEnd:Number;

                var project:ProjectVO = _chartItem.item as ProjectVO;

                var startDate:Date = project.startDate;
                var endDate:Date = project.endDate;
                var del:Number = endDate.getTime() - startDate.getTime();

                yStart = 0;
                ySize = unscaledHeight;

                // limit height
                if (unscaledHeight > MAX_BAR_HEIGHT)
                {
                    yStart = unscaledHeight / 2 - HALF_MAX_BAR_HEIGHT;
                    ySize = MAX_BAR_HEIGHT;
                }

                graphics.clear();

                var phases:ArrayCollection = project.phases;

                var m:Matrix = new Matrix();
                m.createGradientBox(unscaledWidth, ySize, 0, 0, 0);
                var color:uint = projectColors[(status >= 1 && status <= 6) ? status : 0];
                graphics.beginGradientFill(GradientType.LINEAR, [color, 0xDDDDDD], [.9, .9], [0, 255], m, null, null, 0);
                graphics.drawRoundRect(0, yStart, unscaledWidth, ySize, ySize);
                graphics.endFill();

                // links position
                yPosition = yStart;
            }
        ]]>
    </mx:Script>
</mx:Canvas>

The important things you need are the set and get data so you can get a handle to your object containing data to be drawn and the updateDisplayList where you actually draw the bar.  On my snapshot, I also show the milestones displayed as diamonds and the phases which are displayed in different color codes on the same horizontal bar.  They are just extension of the same concept.  If you have enough patient to figure out so far, chances are very good that you can customize your chart to however your heart’s content.

1 comment: