Flex Chart DataTip Renderer

Here is a quick post with some code to create custom DataTip renderers for Flex charts using Spark components and containers. Below is a screen shot of the custom DataTip and the source code to create it follows.

Main.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
					   xmlns:s="library://ns.adobe.com/flex/spark" 
					   xmlns:mx="library://ns.adobe.com/flex/mx" 
					   xmlns:charts="com.thanksmister.charts.*" 
					   width="480" height="340">
	
	<fx:Style source="assets/css/style.css"/>
	
	<fx:Script>
		<![CDATA[
			import mx.charts.CategoryAxis;
			
			private function categoryAxis_labelFunc(item:Object, prevValue:Object, axis:CategoryAxis, categoryItem:Object):String 
			{
				var datNum:Number = Date.parse(item);
				var tempDate:Date = new Date(datNum);
				
				return dateFormatter.format(tempDate).toUpperCase();
			}
		]]>
	</fx:Script>
	
	<fx:Declarations>
		<mx:DateFormatter id="dateFormatter" formatString="MMMM-DD-YYYY" />
	
		<s:XMLListCollection id="dp">
			<s:source>
				<fx:XMLList>
					<quote date="8/1/2007" open="40.29" close="39.58" />
					<quote date="8/2/2007" open="39.4" close="39.52" />
					<quote date="8/3/2007" open="39.47" close="38.75" />
					<quote date="8/6/2007" open="38.71" close="39.38" />
					<quote date="8/7/2007" open="39.08" close="39.42" />
					<quote date="8/8/2007" open="39.61" close="40.23" />
					<quote date="8/9/2007" open="39.9" close="40.75" />
					<quote date="8/10/2007" open="41.3" close="41.06" />
					<quote date="8/13/2007" open="41" close="40.83" />
					<quote date="8/14/2007" open="41.01" close="40.41" />
					<quote date="8/15/2007" open="40.22" close="40.18" />
					<quote date="8/16/2007" open="39.83" close="39.96" />
					<quote date="8/17/2007" open="40.18" close="40.32" />
					<quote date="8/20/2007" open="40.55" close="40.74" />
					<quote date="8/21/2007" open="40.41" close="40.13" />
					<quote date="8/22/2007" open="40.4" close="40.77" />
					<quote date="8/23/2007" open="40.82" close="40.6" />
					<quote date="8/24/2007" open="40.5" close="40.41" />
					<quote date="8/27/2007" open="40.38" close="40.81" />
				</fx:XMLList>
			</s:source>
		</s:XMLListCollection>

		<s:SolidColorStroke id="lineStroke" color="#CCCCCCC" alpha=".2" weight="1"/>
		
	</fx:Declarations>
	
	<s:VGroup width="100%" height="100%" paddingBottom="10" paddingTop="10" paddingLeft="10" paddingRight="10">
		
		<mx:LineChart id="lineChart"
					  showDataTips="true"
					  dataProvider="{dp}"
					  width="100%" gutterRight="10"
					  height="100%" 
					  dataTipRenderer="com.thanksmister.charts.DataTipSkin">
			
			<!-- vertical axis -->
			<mx:verticalAxis>
				<mx:LinearAxis baseAtZero="false" title="Price" />
			</mx:verticalAxis>
			
			<!-- horizontal axis -->
			<mx:horizontalAxis>
				<mx:CategoryAxis id="ca" categoryField="@date" title="Date" labelFunction="categoryAxis_labelFunc" />
			</mx:horizontalAxis>
			
			<!-- horizontal axis renderer -->
			<mx:horizontalAxisRenderers>
				<mx:AxisRenderer axis="{ca}" canDropLabels="true" />
			</mx:horizontalAxisRenderers>
			
			<!-- series -->
			<mx:series>
				<mx:LineSeries yField="@open" form="segment" displayName="Open" />
			</mx:series>
			
			<!-- series filters -->
			<mx:seriesFilters>
				<fx:Array/>
			</mx:seriesFilters>
			
			<!-- assign stroke to grid lines -->
			<mx:backgroundElements>
				<mx:GridLines gridDirection="both" horizontalChangeCount="2" verticalChangeCount="6">
					<mx:horizontalStroke>{lineStroke}</mx:horizontalStroke>
					<mx:verticalStroke>{lineStroke}</mx:verticalStroke>
				</mx:GridLines>
			</mx:backgroundElements>
			
			<mx:annotationElements>
				<fx:Array>
					<charts:RangeSelector id="selectedRange" />
				</fx:Array>
			</mx:annotationElements>
			
		</mx:LineChart>
		
	</s:VGroup>
	
</s:WindowedApplication>

DataTipSkin.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"  
		 implements="mx.core.IFlexDisplayObject, mx.core.IDataRenderer"
		 xmlns:mx="library://ns.adobe.com/flex/mx" width="120">
	
	<fx:Script>
		<![CDATA[
			import flashx.textLayout.conversion.TextConverter;
			import flashx.textLayout.elements.TextFlow;
			
			import mx.charts.HitData;
			import mx.charts.series.items.LineSeriesItem;
			
			private var _data:HitData;
			
			[Bindable]
			private var _xValue:String;
			
			[Bindable]
			private var _yValue:String;
			
			[Bindable]
			private var _displayText:TextFlow;
			
			public function get data():Object
			{
				// TODO Auto Generated method stub
				return null;
			}
			
			public function set data(value:Object):void
			{
				// HitData data from chart
				_data = value as HitData;
				
				// The display text used in datatip which comes in HTML format
				_displayText = TextConverter.importToFlow(_data.displayText, TextConverter.TEXT_FIELD_HTML_FORMAT);
				
				// HitData contains a reference to the ChartItem
				var item:LineSeriesItem = _data.chartItem as LineSeriesItem;
				
				// ChartItem xValue and yValue 
				_xValue = String(item.xValue);
				_yValue = String(item.yValue);
			}
		]]>
	</fx:Script>
	
	<fx:Declarations>
		
	</fx:Declarations>
	
	<s:Rect right="0" left="0" bottom="0" top="0">
		<s:filters>
			<s:DropShadowFilter blurX="20" blurY="20" alpha="0.22" distance="5" angle="90" knockout="false" />
		</s:filters>
		<s:fill>
			<s:SolidColor color="0x393939"/>
		</s:fill>    
		<s:stroke>
			<s:SolidColorStroke color="0x1a1a19"  weight="1" alpha=".2" />
		</s:stroke>
	</s:Rect>
	
	<s:VGroup width="100%" height="100%" paddingTop="10" paddingRight="10" paddingBottom="10" paddingLeft="10">
		
		<s:RichEditableText textFlow="{_displayText}" width="100%" textAlign="center" selectable="false" editable="false"/>
		
	</s:VGroup>

</s:Group>

-Mister

Determining local timezone in ActionScript (AIR, Flex, AS3)

I made a little utility class that determines the local timezone (PST, EST, MST, CST, etc.). The guts of the utility are three methods, one for determining if the local machine is currently observing daylight savings time, another determines the GMT time from the the local machines current Date, and one for looking up the timezone abbreviation by GMT time. This is sort of a conglomerate of code that I could find on different posts and leveraged them to achieve my own goals.

Determining if the daylight savings time is currently being observed.

**
* Determines if local computer is observing daylight savings time for US and London.
* */
public static function isObservingDTS():Boolean
{
     var winter:Date = new Date(2011, 01, 01); // after daylight savings time ends
     var summer:Date = new Date(2011, 07, 01); // during daylight savings time
     var now:Date = new Date();

     var winterOffset:Number = winter.getTimezoneOffset();
     var summerOffset:Number = summer.getTimezoneOffset();
     var nowOffset:Number = now.getTimezoneOffset();

     if((nowOffset == summerOffset) && (nowOffset != winterOffset)) {
          return true;
     } else {
          return false;
     }
}

Creating the GMT from a Date object and adjusting for daylight savings time.

/**
* Method to build GMT from date and timezone offset and accounting for daylight savings.
*
* Originally code befor modifications:
* http://flexoop.com/2008/12/flex-date-utils-date-and-time-format-part-i/
* */
private static function buildTimeZoneDesignation( date:Date, dts:Boolean ):String
{
     if ( !date ) {
          return "";
     }

     var timeZoneAsString:String = "GMT";
     var timeZoneOffset:Number;

     // timezoneoffset is the number that needs to be added to the local time to get to GMT, so
     // a positive number would actually be GMT -X hours
     if ( date.getTimezoneOffset() / 60 > 0 && date.getTimezoneOffset() / 60 < 10 ) {
          timeZoneOffset = (dts)? ( date.getTimezoneOffset() / 60 ):( date.getTimezoneOffset() / 60 - 1 );
          timeZoneAsString += "-0" + timeZoneOffset.toString();
     } else if ( date.getTimezoneOffset() < 0 && date.timezoneOffset / 60 > -10 ) {
          timeZoneOffset = (dts)? ( date.getTimezoneOffset() / 60 ):( date.getTimezoneOffset() / 60 + 1 );
          timeZoneAsString += "+0" + ( -1 * timeZoneOffset ).toString();
     } else {
          timeZoneAsString += "+00";
     }

     // add zeros to match standard format
     timeZoneAsString += "00";
     return timeZoneAsString;
}

Finally, parsing the abbreviation from a simple lookup Array object.

/**
* List of timezone abbreviations and matching GMT times.
* Modified form original code at:
* http://blog.flexexamples.com/2009/07/27/parsing-dates-with-timezones-in-flex/
* */
private static var timeZoneAbbreviations:Array = [
     /* Hawaii-Aleutian Standard/Daylight Time */
     {abbr:"HAST", zone:"GMT-1000"},
     {abbr:"HADT", zone:"GMT-0900"},
     /* Alaska Standard/Daylight Time */
     {abbr:"AKST", zone:"GMT-0900"},
     {abbr:"ASDT", zone:"GMT-0800"},
     /* Pacific Standard/Daylight Time */
     {abbr:"PST", zone:"GMT-0800"},
     {abbr:"PDT", zone:"GMT-0700"},
     /* Mountain Standard/Daylight Time */
     {abbr:"MST", zone:"GMT-0700"},
     {abbr:"MDT", zone:"GMT-0600"},
     /* Central Standard/Daylight Time */
     {abbr:"CST", zone:"GMT-0600"},
     {abbr:"CDT", zone:"GMT-0500"},
     /* Eastern Standard/Daylight Time */
     {abbr:"EST", zone:"GMT-0500"},
     {abbr:"EDT", zone:"GMT-0400"},
     /* Atlantic Standard/Daylight Time */
     {abbr:"AST", zone:"GMT-0400"},
     {abbr:"ADT", zone:"GMT-0300"},
     /* Newfoundland Standard/Daylight Time */
     {abbr:"NST", zone:"GMT-0330"},
     {abbr:"NDT", zone:"GMT-0230"},
     /* London Standard/Daylight Time */
     {abbr:"BST", zone:"GMT+0100"},
     {abbr:"GMT", zone:"GMT+0000"}
];

/**
* Goes through the timze zone abbreviations looking for matching GMT time.
* */
private static function parseTimeZoneFromGMT(gmt:String):String
{
     for each (var obj:Object in timeZoneAbbreviations) {
          if(obj.zone == gmt){
               return obj.abbr;
          }
     }
     return gmt;
}

This utility is obviously not robust enough to do world timezones, but its enough of a framework to work with if you want to expand past just US timezones (and London). I am sure there are a lot of different ways this could be improved, so please share resources or ideas if you have them. You can get the code for the complete utility from GitHub:

TimzeZoneUtil.as

Thanks!

-Mister

Spark Button and ButtonBar with icons and rollover states

The Flash Builder Spark Button control doesn’t come with an icon property out of the box. So you have to extend the Button class and add your own. I created a Spark Skin to add two icons to the Button control, one for the up/disabled state and one for the over/down states.

The Spark ButtonBar control does accommodate an icon, but there is no way to change the icon when the selected index changes. So to change the icon of the selected item, I built a Spark Skin for the ButtonBarButton and the ButtonBar to accomplish the job. Here is the running example of the buttons in action:

IconButtons.swf

The code of the IconButton class and the IconButtonSkin mxml file that accompanies the class:

https://gist.github.com/946886

And the code of the IconButtonBarButton class and the IconButtonBarSkin and IconButtonBarButtonskin mxml

https://gist.github.com/947130

You can also download the IconButtons Flash Builder Project.

-Mr

Bubble ItemRenderer Events in Flex

Often times I find myself needing to bubble events from an itemRenderer to the parent control.   This is especially important when using a CairngormEventDispatcher.  Events should be used by the views, rather than having them nested within the itemRenderer.  This is commandment #10 on Jesse Warden’s post 10 Tips For Working With Cairngorm (or the way Jesse writes about it, 10 Thinks I Love to Hate About Cairngorm). 

I try, when time permits, to adhere to this practice as it can be a big pain in the ass to dig down deep within your structure hunting for events. This also makes the code more reusable as you are not having to customize itemRenderers when they need to pass different events, this can be handled in the view instead, nice an neat.

For my example I extended a List control to have a new event “menuClicked”.  

List:

package com.mister.controls
{
    import mx.controls.List;

    [Event(name="menuClick", type="mx.events.MenuEvent")]

    public class List extends mx.controls.List
    {
        public function List()
        {
            super();
        }
    }
}

I used an MXML itemRenderer instead of a class, just because I like to have the layout ease that comes with MXML.  

ItemRenderer:

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="24">
    <mx:Script>
        <![CDATA[
            import mx.events.MenuEvent;
            import mx.controls.Menu;

            private function createMenu(event:Event):void
            {

                  var menu:Menu = Menu.createMenu(menuButton, menuData, false);
                      menu.labelField="@label";
                      menu.addEventListener(MenuEvent.ITEM_CLICK, bubbleMenuEvent);

                   var point:Point = new Point();
                       point.x = menuButton.x;
                    point.y = menuButton.y;
                        point = this.localToGlobal(point);

                menu.show(point.x, point.y);
            }

            private function bubbleMenuEvent(event : MenuEvent):void
            {
                var e : MenuEvent = new MenuEvent("menuClick", true, true, null, null, event.item, this, data.label);
                dispatchEvent(e);
            }

        ]]>
    </mx:Script>

    <mx:XML id="menuData">
        <root>
            <menuitem label="Edit" data="edit"/>
            <menuitem label="Delete" data="delete"/>
            <menuitem label="Save" data="save"/>
           </root>
    </mx:XML>

    <mx:Label text="{data.label}" verticalCenter="0" x="10"/>
    <mx:Button id="menuButton" click="createMenu(event)" verticalCenter="0" right="10" width="54" label="menu"/>

</mx:Canvas>

I made an clickable event using a Menu control and bubbled the event to the view of my application.    

Application:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
    creationComplete="initApp()" viewSourceURL="srcview/index.html">

    <mx:Script>
        <![CDATA[
            import mx.controls.Alert;
            import mx.events.MenuEvent;
            import mx.collections.ArrayCollection;

             private var listArray:Array=[
                 {label: "Label One", data: 1},
                 {label: "Label Two", data: 2},
                 {label: "Label Three", data: 3},
                 {label: "Label Four", data: 4},
                 {label: "Label Five", data: 5}];

             [Bindable]
            public var dp:ArrayCollection;

            public function initApp():void
            {
                dp = new ArrayCollection(listArray);
            }

            private function handleMenuEvent(event : MenuEvent):void
            {
                Alert.show("Menu Item Clicked : " + event.item.@data + " and List Row Label : " + event.label);
            }
        ]]>
    </mx:Script>

    <controls:List dataProvider="{dp}" menuClick="handleMenuEvent(event)"
        itemRenderer="com.mister.custom.BubbleItemRenderer"
        width="450" height="240" horizontalCenter="0" top="50"
        xmlns:controls="com.mister.controls.*"/>

    <mx:Label y="24" text="Bubble Event from List ItemRenderer" horizontalCenter="0" fontWeight="bold"/>

</mx:Application>

This example is simple, but you can see how you can extend this to types of itemRenderers.

View Source (right-click) and Example

Download Example

-Mister