This article is based on Flex on Java , published on October 2010. It is being reproduced here by permission from Manning Publications. Manning publishes MEAP (Manning Early Access Program,) eBooks and pBooks. MEAPs are sold exclusively through Manning.com. All pBook purchases include free PDF, mobi and epub. When mobile formats become available all customers will be contacted and upgraded. Visit Manning.com for more information. [ Use promotional code ‘java40beat’ and get 40% discount on eBooks and pBooks ]
also read:
Creating a Custom Pie Chart Component with Degrafa
Adobe provides data visualization components, but only when you purchase a license for the professional version of the Flash Builder IDE. Because our goal is to do Flex development using only free and open source technologies, we’ve decided to create our own visualization components—besides, it’s more fun.
Drawing in Flex
Flex and Flash provide powerful drawing libraries that we could leverage to create our custom graph components, but we’re going to leverage an open source graphics library called Degrafa. Using Degrafa gives us the ability to declaratively build our graphing components rather than having to deal with the complex calculations involved in drawing pie chart slices as illustrated in listing 1, which shows an example ActionScript class specifically for drawing a pie chart slice found at http://www.adobe.com/devnet/flash/articles/adv_draw_methods.html. Notice how much trigonometry is involved in creating something as simple as a pie chart slice from scratch.
Listing 1 Example of drawing in ActionScript
/*------------------------------------------------------------- mc.drawWedge is a method for drawing pie shaped wedges. Very useful for creating charts. Special thanks to: Robert Penner, Eric Mueller and Michael Hurwicz for their contributions. -------------------------------------------------------------*/ MovieClip.prototype.drawWedge = function(x, y, startAngle, arc, radius, yRadius) { ============== // mc.drawWedge() - by Ric Ewing (ric@formequalsfunction.com) - version 1.3 - 6.12.2002 // // x, y = center point of the wedge. // startAngle = starting angle in degrees. // arc = sweep of the wedge. Negative values draw clockwise. // radius = radius of wedge. If [optional] yRadius is defined, then radius is the x radius. // yRadius = [optional] y radius for wedge. // ============== // Thanks to: Robert Penner, Eric Mueller and Michael Hurwicz for their contributions. // ============== if (arguments.length<5) { return; } // move to x,y position this.moveTo(x, y); // if yRadius is undefined, yRadius = radius if (yRadius == undefined) { yRadius = radius; } // Init vars var segAngle, theta, angle, angleMid, segs, ax, ay, bx, by, cx, cy; // limit sweep to reasonable numbers if (Math.abs(arc)>360) { arc = 360; } // Flash uses 8 segments per circle, to match that, draw in a maximum // of 45 degree segments. First calculate how many segments are needed // for our arc. segs = Math.ceil(Math.abs(arc)/45); // Now calculate the sweep of each segment. segAngle = arc/segs; // The math requires radians rather than degrees. To convert from degrees // use the formula (degrees/180)*Math.PI to get radians. theta = -(segAngle/180)*Math.PI; // convert angle startAngle to radians angle = -(startAngle/180)*Math.PI; // draw the curve in segments no larger than 45 degrees. if (segs>0) { // draw a line from the center to the start of the curve ax = x+Math.cos(startAngle/180*Math.PI)*radius; ay = y+Math.sin(-startAngle/180*Math.PI)*yRadius; this.lineTo(ax, ay); // Loop for drawing curve segments for (var i = 0; i<segs; i++) { angle += theta; angleMid = angle-(theta/2); bx = x+Math.cos(angle)*radius; by = y+Math.sin(angle)*yRadius; cx = x+Math.cos(angleMid)*(radius/Math.cos(theta/2)); cy = y+Math.sin(angleMid)*(yRadius/Math.cos(theta/2)); this.curveTo(cx, cy, bx, by); } // close the wedge by drawing a line to the center this.lineTo(x, y); } };
Adobe has released the specifications for its declarative graphics library, called FXG. It appears that the Degrafa team has collaborated with the Adobe team to create this specification, but the FXG functionality is only a subset of what is available from the Degrafa library. This may be a library to keep your eye on as it’s being developed.
Common Degrafa concepts
Before diving into developing the component, let’s familiarize ourselves with some of the terms and concepts that we’ll see as we work through this example.
- Surface—This is the base component for everything you’ll do in Degrafa. All other Degrafa components will be composed within a Surface.
- GeometryGroup—After the Surface, this is the next level of composition. The GeometryGroup tag allows you to group Degrafa components to compose an object.
- Stroke—Stroke is the object that is used to define the look of an object’s outline, in terms of color, thickness, and style. Degrafa provides different Stroke objects for your use depending on the style of stroke you want: SolidStroke, Linear-Gradient, and RadialGradient.
- Fill—Fill refers to the appearance of the bounded area of a graphical component. Degrafa provides the
following fills: SolidFill, LinearGradient, Radial-Gradient, BitmapFill, BlendFill, and ComplexFill. - Shapes—Degrafa supports drawing many different shapes out of the box, such as Circle, Ellipse, RegularRectangle, RoundedRectangle, Polygon, and more. For irregular shapes, Degrafa also has an extensive library of auto shapes and enables defining any shape you’d like by providing a Scalable Vector Graphics (SVG) path.
- Repeaters—This gives you the ability to repeat a shape any number of times on the surface.
Much like other Flex components, the Degrafa components are considered either container components, meaning they will contain other Degrafa components, or graphical elements. Figure 1 shows the relationship of the common components.
We’re only going to scratch the surface; to learn more about Degrafa, you can start with the Foundation section of the documentation at http://www.degrafa.org/samples/foundation.html.
Creating a pie chart for fun and profit
Now that we have some of the basic concepts, let’s get on with the task of creating a custom pie chart component. We were inspired by a blog posting by Derrick Grigg titled appropriately enough Degrafa Pie Chart, which can be found at http://www.dgrigg.com/post.cfm/04/15/2008/Degrafa-Pie-Chart. After we decomposed it and removed some of the extra visual effects such as tweening and gradients, it barely resembles what we started with. Figure 2 shows a mock-up of the chart we’ll be developing in this article.
For this example you’ll be developing only a single pie chart component, but some of the concepts illustrated here could potentially be applied to creating any number of charting components.
The component you’ll develop is a combination of a pie chart and a data grid, which will serve the purpose of a legend for the pie chart. Without this it may be difficult for someone looking at the chart to differentiate between data points on the graph. The pie chart will consist of the pie chart itself and another component for each of the slices that make up the chart. You’ll also develop a simple custom ItemRenderer for the chart legend to draw a simple box inside one of the cells in the data grid.
You’ll also be adding a label and a combo box to the GraphView to allow the user to change the data the chart shows. By changing the value of the combo box the user can show how many issues there are by project, type, status, or severity.
New custom event
We’re going to create a new custom event for our pie chart. The reason we’re creating a new one is that if we ever wanted to put more than one pie chart component into our application, we’d need to be able to distinguish which component fired the event.
Listing 2 PieChartEvent.as
package org.foj.event { import flash.events.Event; public class PieChartEvent extends Event{ public static const DATA_PROVIDER_UPDATED:String = "dataProviderUpdated"; public var data:*; public var id:*; #1 public function PieChartEvent(type : String, bubbles : Boolean = true, cancelable : Boolean = false) { super(type, bubbles, cancelable); } } } #1 id property
This event differs from the one created previously in the addition of an id property (#1). This is done so that the presenter can decide whether or not it needs to react to the event. With the new event created, you can move on to creating the component itself.
PieChart component
First, you’ll develop the view that contains the pie chart and legend. You’ll create these view components in a new package, so create a file named PieChart.mxml in the org.foj.components package of your project. The following listing shows the first part of the code for the PieChart view.
Listing 3 PieChart.mxml
<?xml version="1.0" encoding="utf-8"?> <s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/halo" xmlns:degrafa="http://www.degrafa.com/2007" creationComplete="init()"> #1 <s:layout> #2 <s:HorizontalLayout/> </s:layout> <fx:Script> <![CDATA[ import mx.collections.ICollectionView; import org.foj.event.EventDispatcherFactory; import org.foj.event.PieChartEvent; import org.foj.presenter.PieChartPresenter; private var _presenter:PieChartPresenter; #3 private var _dataProvider:ICollectionView; #4 private function init():void { _presenter = new PieChartPresente r(this, id); #5 } public function set dataProvider(dataProvider:ICollectionView):void { #6 var refreshEvent:PieChartEvent = new PieChartEvent(PieChartEvent.DATA_PROVIDER_UPDATED); refreshEvent.id = id; refreshEvent.data = dataProvider; EventDispatcherFactory.getEventDispatcher().dispatchEvent(refreshEvent); } ]]> </fx:Script> ... </s:Group> #1 CreationComplete handler #2 HorizontalLayout #3 Define presenter field #4 Define data provider field #5 Pass component's id to presenter #6 Set property for data provider
You set the creationComplete event (#1) to call the init method. Next, you set the layout of your component to use HorizontalLayout (#2). Then, you declare a couple of private member variables for the data provider and its presenter (#3, #4).
Inside the init method, you bootstrap your presenter (#5). Last, you create a set property (#6) for the data provider where you create an event to notify the presenter that the data provider was updated. Listing 4 shows the rest of your pie chart component.
Listing 4 PieChart.mxml (continued)
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/halo" xmlns:degrafa="http://www.degrafa.com/2007" creationComplete="init()"> ... </fx:Script> <mx:Spacer width="10"/> #1 <degrafa:Surface id="pieSurface" width="200" height="200"> #2 <degrafa:GeometryGroup id="pieGroup"> #3 <degrafa:filters> <mx:DropShadowFilter color="0x000000" alpha="0.5"/> </degrafa:filters> #4 </degrafa:GeometryGroup> </degrafa:Surface> <mx:DataGrid id="legendDataGrid"> #5 <mx:columns> <mx:DataGridColumn width="40" sortable="false" #6 itemRenderer="org.foj.components.PieLegendRenderer"/> <mx:DataGridColumn dataField="label" headerText="Label"/> <mx:DataGridColumn dataField="units" headerText="Units"/> </mx:columns> </mx:DataGrid> </s:Group> #1 Spacer to help lay out the component #2 Degrafa surface #3 Geometry group containing pie chart #4 Drop shadow for pie chart #5 Legend for pie chart #6 Custom ItemRenderer
First, you added a spacer (#1) to the component to put a bit of padding between your pie chart and its surrounding components. Next you added a Degrafa Surface component (#2) and a GeometryGroup (#3) to hold the rest of the Degrafa components necessary for the pie chart component. The GeometryGroup is the component to which you’ll add your pie chart slices when you create them. You’ve also added a Drop-ShadowFilter (#4) to the GeometryGroup to add a bit of visual flair to the pie chart. Last you defined the DataGrid component (#5) for your chart legend, with a custom Item-Renderer (#6) to display the color that corresponds to the data in the chart, which you’ll create in a bit.
PieChartSlice
Now that we’ve defined the pie chart component, let’s move on to defining the slices that will make up the pie chart. The pie chart slice is a rather simple component. We probably could have created the pie chart slice programmatically in ActionScript; however, this approach allows us to define sensible defaults declaratively in MXML, adding behavior as well.
Listing 5 PieChartSlice.mxml
<?xml version="1.0" encoding="utf-8"?> <degrafa:GeometryGroup xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:degrafa="http://www.degrafa.com/2007" height="400" width="400"> #1 <fx:Script> <![CDATA[ public function refresh():void #2 { this.graphics.clear(); this.arc.preDraw(); this.arc.draw(graphics, null); } ]]> </fx:Script> <degrafa:EllipticalArc #3 id="arc" width="200" height="200" closureType="pie"/> </degrafa:GeometryGroup> Extends GeometryGroup Your pie chart slice Adds EllipticalArc
The pie chart slice will extend from GeometryGroup (#1). Next, you define a refresh method (#2) to abstract behavior away from your presenter. Last you add an EllipticalArc component to the component (#3) and set default values such as its width, height, and most importantly closureType property, which you set to “pie”.
Custom ItemRenderer
The next component you’re going to create is the custom ItemRenderer for the pie chart legend. This simple component will draw a colored box in the data grid cell to correspond with the colors of the pie chart.
Listing 6 PieLegendRenderer.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/halo" xmlns:degrafa="http://www.degrafa.com/2007"> #1 <mx:Spacer width="2"/> #2 <degrafa:Surface> <degrafa:GeometryGroup> <degrafa:fill> #3 <degrafa:SolidFill id="fill"> <degrafa:color>{data.legend}"</degrafa:color> #4 </degrafa:SolidFill> </degrafa:fill> <degrafa:RegularRectangle #5 width="20" height="20" fill="{fill}"/> </degrafa:GeometryGroup> </degrafa:Surface> </mx:HBox> #1 Extends HBox Spacer to help align box #2 SolidFill for rectangle #3 Color of legend item #4 Rectangle
The ItemRenderer is extending HBox (#1) because all ItemRenderer objects for the DataGrid component must be halo components. Add a Spacer component (#2) to help align the rectangle the way you want it. The SolidFill component (#3) defines the fill color for the RegularRectangle (#5). Now that you’ve finished creating all the visual components for the pie chart, let’s move on to creating the Presenter. An implicitly defined variable is available to the ItemRenderer named data, which corresponds to the item in the dataProvider that you’re rendering. Use this implicit variable to set the color of the Fill object (#4), which is contained in the legend property of the object.
Presenter for the PieChart
Now that all the visual components are created for the pie chart, let’s create the Presenter. The Presenter for the pie chart becomes more involved than any of your previous ones, but it shouldn’t be hard to follow.
Listing 7 PieChartPresenter.as
package org.foj.presenter { import com.degrafa.paint.SolidFill; import org.foj.components.PieChart; import mx.collections.ArrayCollection; import org.foj.components.PieChartSlice; import org.foj.event.EventDispatcherFactory; import org.foj.event.PieChartEvent; import org.foj.model.PieChartModel; public class PieChartPresenter { private var _view:PieChart; #1 private var _model:PieChartModel; private var _id:String; private var _dataProvider:ArrayCollection; public function PieChartPresenter(view:PieChart, id:String) { #2 this._view = view; this._model = new PieChartModel(); this._id = id; EventDispatcherFactory.getEventDispatcher() .addEventListener(PieChartEvent.DATA_PROVIDER_UPDATED, refreshData); } public function set dataProvider(data:ArrayCollection):void { #3 _view.legendDataGrid.dataProvider = data; this._dataProvider = data; } public function get dataProvider():ArrayCollection { return this._dataProvider; } private function refreshData(event:PieChartEvent = null):void { #4 if (event.id == _id) { changeData(event.data); } } private function changeData(data:ArrayCollection):void { dataProvider = data; createSlices(); } private function createSlices():void { #5 while (dataProvider.length > _view.pieGroup.numChildren) { _view.pieGroup.addChild(new PieChartSlice()); } setLegendColors(); redrawSlices(); } private function setLegendColors():void { #6 for (var i:int = 0; i < dataProvider.length; i++) { dataProvider.getItemAt(i).legend = _model.getLegendColorForIndex(i); } } private function redrawSlices():void { #7 var currentAngle:Number = 0; var totalUnits:Number = _model.getTotalUnits(_dataProvider); for (var i:int = 0; i < _view.pieGroup.numChildren; i++) { var slice:PieChartSlice = _view.pieGroup.getChildAt(i) as PieChartSlice; var legendColor:Number = _model.getLegendColorForIndex(i); var arc:Number = i < dataProvider.length ? _model.getAngleForItem( dataProvider[i].units, totalUnits) : 0; // workaround for weird display if only one arc and it's 360 degrees arc = arc < 360 ? arc : 359.99; #8 redrawSlice(slice, currentAngle, arc, legendColor); currentAngle += arc; } _view.pieGroup.draw(null, null); } private function redrawSlice(slice:PieChartSlice, #9 startAngle:Number, arc:Number, color:Number):void { slice.arc.fill = new SolidFill(color, 1); slice.arc.startAngle = startAngle; slice.arc.arc = arc; slice.refresh(); } } } #1 Private member variables #2 Constructor #3 Set property for dataProvider #4 Event handler #5 Create slices method #6 Set legend colors on data #7 Redraw slices after update #8 Workaround #9 Redraw slice
You first define private member variables to hold onto references to the view and the model, the id of the component this Presenter belongs to, and the dataProvider for your pie chart (#1). The constructor (#2) for this Presenter not only takes in a reference to the view, but also is used to bootstrap the id for the view component because there may be multiple pie charts contained in the application. You also define an event listener for the dataProvider being updated in the view component. Next you define a pair of get and set properties (#3) for the dataProvider you leverage to update the dataProvider property of the legend data grid whenever the data-Provider for the pie chart is updated.
You then define the event handler method for the event that is fired whenever the dataProvider for the view is updated (#4). Inside this method you check to see if the id of the component firing the event is the same as the id that created this Presenter.
That way if there are multiple pie chart components, this method can determine whether or not it needs to react.
The createSlices method (#5) checks to see if the data provider has more elements contained in it than there are pie chart slices in your pie chart. If there are more elements in the data provider, it will create more pie chart slices. In the set-LegendColors method (#6) you iterate through the items in the dataProvider and set the legend property of the item to the corresponding color, which you’ll get from the pie chart model class.
After all of that, refresh your pie chart with a call to the redrawSlices method (#7).
This will iterate over the pie chart slices and update the data values, such as the start angle of the slice and its arc. You iterate over the pie chart slices instead of the data provider because there may be more slices than items in the dataProvider, and this will draw the extra slices with an arc of 0. There is also a little workaround (#8) for when there is only a single slice and its arc is 360, which sets its arc to 359.99 so that it would draw correctly.
After all of the data for the slice is updated, it is passed into the redrawSlice method (#9) to tell the slice to redraw itself.
Model for the PieChart
Now you only have one piece of the MVP triad to complete for your pie chart. Even though the pie chart doesn’t need to call out to any remote services, you’ve still refactored a couple of methods that could be considered business logic rather than presentation logic and have no need to maintain any kind of state. The following listing shows the code for the pie chart model.
Listing 8 PieChartModel.as
package org.foj.model { import mx.collections.ICollectionView; public class PieChartModel { private var colors:Array = [ #1 0x468966, 0xFFB03B, 0xFFF0A5, 0x999574, 0x007D9F, 0x8E2800, 0x8E28F4, 0x0528F4, 0xF42105, 0x0CF405 ]; public function getLegendColorForIndex(index:Number):Number { #2 return colors[index]; } public function getAngleForItem(units:Number, totalUnits:Number):Number { #3 return ((((units / totalUnits) * 100) * 360) / 100) ; } public function getTotalUnits(dataProvider:ICollectionView):Number { #4 var total:Number = 0; for each(var item:Object in dataProvider) { total += item.units; } return total; } } } #1 Array of colors for legend #2 Convenience method for getting color #3 Calculate angle for item #4 Calculate total number of items for pie chart
An array of 10 different hex values (#1) corresponds to the colors you want the pie chart to use for its data points. This number could easily be increased should the need arise for more data points in your graphs; for this example this number should suffice.
Next, you define a convenience method (#2) for getting the color value for a specific index. The method getAngleForItem (#3) takes care of the calculation for determining the size of the angle for an item based on the total number of items contained within the pie chart and the number of items passed in. The last method you define (#4) in your model iterates through the data set passed in and returns back the total number of items for the pie chart.
Summary
In this article, we’ve introduced the Degrafa framework and created a custom pie chart component. Using Degrafa gives us the ability to declaratively build our graphing components rather than having to deal with complex calculations.