1package com.yahoo.astra.fl.charts
2{
3    import com.yahoo.astra.fl.charts.events.ChartEvent;
4    import com.yahoo.astra.fl.charts.legend.ILegend;
5    import com.yahoo.astra.fl.charts.legend.LegendItemData;
6    import com.yahoo.astra.fl.charts.series.ICategorySeries;
7    import com.yahoo.astra.fl.charts.series.ILegendItemSeries;
8    import com.yahoo.astra.fl.charts.series.ISeries;
9    import com.yahoo.astra.fl.charts.series.ISeriesItemRenderer;
10    import com.yahoo.astra.fl.utils.UIComponentUtil;
11
12    import fl.core.InvalidationType;
13    import fl.core.UIComponent;
14
15    import flash.accessibility.AccessibilityProperties;
16    import flash.display.DisplayObject;
17    import flash.display.Sprite;
18    import flash.events.Event;
19    import flash.events.MouseEvent;
20    import flash.geom.Point;
21    import flash.text.TextFormat;
22    import flash.text.TextFormatAlign;
23    import flash.utils.getDefinitionByName;
24    import flash.events.ErrorEvent;
25
26    //--------------------------------------
27    //  Styles
28    //--------------------------------------
29
30    /**
31     * The padding that separates the border of the component from its contents,
32     * in pixels.
33     *
34     * @default 10
35     */
36    [Style(name="contentPadding", type="Number")]
37
38    /**
39     * Name of the class to use as the skin for the background and border of the
40     * component.
41     *
42     * @default ChartBackgroundSkin
43     */
44    [Style(name="backgroundSkin", type="Class")]
45
46    /**
47     * The default colors for each series. These colors are used for markers,
48     * in most cases, but they may apply to lines, fills, or other graphical
49     * items.
50     *
51     * <p>An Array of values that correspond to series indices in the data
52     * provider. If the number of values in the Array is less than the number
53     * of series, then the next series will restart at index zero in the style
54     * Array. If the value of this style is an empty Array, then each individual series
55     * will use the default or modified value set on the series itself.</p>
56     *
57     * <p>Example: If the seriesColors style is equal to [0xffffff, 0x000000] and there
58     * are three series in the chart's data provider, then the series at index 0
59     * will have a color of 0xffffff, index 1 will have a color of 0x000000, and
60     * index 2 will have a color of 0xffffff (starting over from the beginning).</p>
61     *
62     * @default [0x00b8bf, 0x8dd5e7, 0xedff9f, 0xffa928, 0xc0fff6, 0xd00050, 0xc6c6c6, 0xc3eafb, 0xfcffad, 0xcfff83, 0x444444, 0x4d95dd, 0xb8ebff, 0x60558f, 0x737d7e, 0xa64d9a, 0x8e9a9b, 0x803e77]
63     */
64    [Style(name="seriesColors", type="Array")]
65
66    /**
67     * The default size of the markers in pixels. The actual drawn size of the
68     * markers could end up being different in some cases. For example, bar charts
69     * and column charts display markers side-by-side, and a chart may need to make
70     * the bars or columns smaller to fit within the required region.
71     *
72     * <p>An Array of values that correspond to series indices in the data
73     * provider. If the number of values in the Array is less than the number
74     * of series, then the next series will restart at index zero in the style
75     * Array. If the value of this style is an empty Array, then each individual series
76     * will use the default or modified value set on the series itself.</p>
77     *
78     * <p>Example: If the seriesMarkerSizes style is equal to [10, 15] and there
79     * are three series in the chart's data provider, then the series at index 0
80     * will have a marker size of 10, index 1 will have a marker size of 15, and
81     * index 2 will have a marker size of 10 (starting over from the beginning).</p>
82     *
83     * @default []
84     */
85    [Style(name="seriesMarkerSizes", type="Array")]
86
87    /**
88     * An Array containing the default skin classes for each series. These classes
89     * are used to instantiate the marker skins. The values may be fully-qualified
90     * package and class strings or a reference to the classes themselves.
91     *
92     * <p>An Array of values that correspond to series indices in the data
93     * provider. If the number of values in the Array is less than the number
94     * of series, then the next series will restart at index zero in the style
95     * Array. If the value of this style is an empty Array, then each individual series
96     * will use the default or modified value set on the series itself.</p>
97     *
98     * <p>Example: If the seriesMarkerSkins style is equal to [CircleSkin, DiamondSkin] and there
99     * are three series in the chart's data provider, then the series at index 0
100     * will have a marker skin of CircleSkin, index 1 will have a marker skin of DiamondSkin, and
101     * index 2 will have a marker skin of CircleSkin (starting over from the beginning).</p>
102     *
103     * @default []
104     */
105    [Style(name="seriesMarkerSkins", type="Array")]
106
107    /**
108     * The TextFormat object to use to render data tips.
109     *
110     * @default TextFormat("_sans", 11, 0x000000, false, false, false, '', '', TextFormatAlign.LEFT, 0, 0, 0, 0)
111     */
112    [Style(name="dataTipTextFormat", type="TextFormat")]
113
114    /**
115     * Name of the class to use as the skin for the background and border of the
116     * chart's data tip.
117     *
118     * @default ChartDataTipBackground
119     */
120    [Style(name="dataTipBackgroundSkin", type="Class")]
121
122    /**
123     * If the datatip's content padding is customizable, it will use this value.
124     * The padding that separates the border of the component from its contents,
125     * in pixels.
126     *
127     * @default 6
128     */
129    [Style(name="dataTipContentPadding", type="Number")]
130
131    /**
132     * Determines if data changes should be displayed with animation.
133     *
134     * @default true
135     */
136    [Style(name="animationEnabled", type="Boolean")]
137
138    /**
139     * Indicates whether embedded font outlines are used to render the text
140     * field. If this value is true, Flash Player renders the text field by
141     * using embedded font outlines. If this value is false, Flash Player
142     * renders the text field by using device fonts.
143     *
144     * If you set the embedFonts property to true for a text field, you must
145     * specify a font for that text by using the font property of a TextFormat
146     * object that is applied to the text field. If the specified font is not
147     * embedded in the SWF file, the text is not displayed.
148     *
149     * @default false
150     */
151    [Style(name="embedFonts", type="Boolean")]
152
153    /**
154     * Functionality common to most charts. Generally, a <code>Chart</code> object
155     * shouldn't be instantiated directly. Instead, a subclass with a concrete
156     * implementation should be used. That subclass generally should implement the
157     * <code>IPlotArea</code> interface.
158     *
159     * @author Josh Tynjala
160     */
161    public class Chart extends UIComponent
162    {
163
164    //--------------------------------------
165    //  Class Variables
166    //--------------------------------------
167
168        /**
169         * @private
170         */
171        private static var defaultStyles:Object =
172        {
173            seriesMarkerSizes: null,
174            seriesMarkerSkins: null,
175            seriesColors:
176            [
177                0x00b8bf, 0x8dd5e7, 0xedff9f, 0xffa928, 0xc0fff6, 0xd00050,
178                0xc6c6c6, 0xc3eafb, 0xfcffad, 0xcfff83, 0x444444, 0x4d95dd,
179                0xb8ebff, 0x60558f, 0x737d7e, 0xa64d9a, 0x8e9a9b, 0x803e77
180            ],
181            seriesBorderColors:[],
182            seriesFillColors:[],
183            seriesLineColors:[],
184            seriesBorderAlphas:[1],
185            seriesFillAlphas:[1],
186            seriesLineAlphas:[1],
187            contentPadding: 10,
188            backgroundSkin: "ChartBackground",
189            backgroundColor: 0xffffff,
190            dataTipBackgroundSkin: "ChartDataTipBackground",
191            dataTipContentPadding: 6,
192            dataTipTextFormat: new TextFormat("_sans", 11, 0x000000, false, false, false, '', '', TextFormatAlign.LEFT, 0, 0, 0, 0),
193            animationEnabled: true,
194            embedFonts: false
195        };
196
197        /**
198         * @private
199         */
200        private static const ALL_SERIES_STYLES:Object =
201        {
202            color: "seriesColors",
203            markerSize: "seriesMarkerSizes",
204            markerSkin: "seriesMarkerSkins",
205            borderColor: "seriesBorderColors",
206            fillColor: "seriesFillColors",
207            lineColor: "seriesLineColors",
208            borderAlpha: "seriesBorderAlphas",
209            fillAlpha: "seriesFillAlphas",
210            lineAlpha: "seriesLineAlphas"
211        };
212
213        /**
214         * @private
215         */
216        private static const SHARED_SERIES_STYLES:Object =
217        {
218            animationEnabled: "animationEnabled"
219        };
220
221        private static const DATA_TIP_STYLES:Object =
222        {
223            backgroundSkin: "dataTipBackgroundSkin",
224            contentPadding: "dataTipContentPadding",
225            textFormat: "dataTipTextFormat",
226            embedFonts: "embedFonts"
227        };
228
229    //--------------------------------------
230    //  Class Methods
231    //--------------------------------------
232
233        /**
234         * @private
235         * @copy fl.core.UIComponent#getStyleDefinition()
236         */
237        public static function getStyleDefinition():Object
238        {
239            return mergeStyles(defaultStyles, UIComponent.getStyleDefinition());
240        }
241
242    //--------------------------------------
243    //  Constructor
244    //--------------------------------------
245
246        /**
247         * Constructor.
248         */
249        public function Chart()
250        {
251            super();
252            this.accessibilityProperties = new AccessibilityProperties();
253            this.accessibilityProperties.forceSimple = true;
254            this.accessibilityProperties.description = "Chart";
255        }
256
257    //--------------------------------------
258    //  Variables and Properties
259    //--------------------------------------
260
261        /**
262         * @private
263         * The display object representing the chart background.
264         */
265        protected var background:DisplayObject;
266
267        /**
268         * @private
269         * The area where series are drawn.
270         */
271        protected var content:Sprite;
272
273        /**
274         * @private
275         * The mouse over data tip that displays information about an item on the chart.
276         */
277        protected var dataTip:DisplayObject;
278
279        /**
280         * @private
281         * Storage for the data property. Saves a copy of the unmodified data.
282         */
283        private var _dataProvider:Object;
284
285        /**
286         * @private
287         * Modified version of the stored data.
288         */
289        protected var series:Array = [];
290
291        [Inspectable(type=Array)]
292        /**
293         * @copy com.yahoo.astra.fl.charts.IChart#dataProvider
294         */
295        public function get dataProvider():Object
296        {
297            return this.series;
298        }
299
300        /**
301         * @private
302         */
303        public function set dataProvider(value:Object):void
304        {
305            if(this._dataProvider != value)
306            {
307                this._dataProvider = value;
308                this.invalidate(InvalidationType.DATA);
309            }
310        }
311
312        /**
313         * @private
314         * Storage for the defaultSeriesType property.
315         */
316        private var _defaultSeriesType:Class;
317
318        /**
319         * When raw data (like an Array of Numbers) is encountered where an
320         * ISeries instance is expected, it will be converted to this default
321         * type. Accepts either a Class instance or a String referencing a
322         * fully-qualified class name.
323         */
324        public function get defaultSeriesType():Object
325        {
326            return this._defaultSeriesType;
327        }
328
329        /**
330         * @private
331         */
332        public function set defaultSeriesType(value:Object):void
333        {
334            if(!value) return;
335            var classDefinition:Class = null;
336            if(value is Class)
337            {
338                classDefinition = value as Class;
339            }
340            else
341            {
342                // borrowed from fl.core.UIComponent#getDisplayObjectInstance()
343                try
344                {
345                    classDefinition = getDefinitionByName(value.toString()) as Class;
346                }
347                catch(e:Error)
348                {
349                    try
350                    {
351                        classDefinition = this.loaderInfo.applicationDomain.getDefinition(value.toString()) as Class;
352                    }
353                    catch (e:Error)
354                    {
355                        // Nothing
356                    }
357                }
358            }
359
360            this._defaultSeriesType = classDefinition;
361            //no need to redraw.
362            //if the series have already been created, the user probably wanted it that way.
363            //we have no way to tell if the user chose a particular series' type or not anyway.
364        }
365
366        private var _lastDataTipRenderer:ISeriesItemRenderer;
367
368        /**
369         * @private
370         * Storage for the dataTipFunction property.
371         */
372        private var _dataTipFunction:Function = defaultDataTipFunction;
373
374        /**
375         * If defined, the chart will call the input function to determine the
376         * text displayed in the chart's data tip. The function uses the following
377         * signature:
378         *
379         * <p><code>function dataTipFunction(item:Object, index:int, series:ISeries):String</code></p>
380         */
381        public function get dataTipFunction():Function
382        {
383            return this._dataTipFunction;
384        }
385
386        /**
387         * @private
388         */
389        public function set dataTipFunction(value:Function):void
390        {
391            this._dataTipFunction = value;
392        }
393
394        /**
395         * @private
396         * Storage for the legend property.
397         */
398        private var _legend:ILegend;
399
400        /**
401         * The component that will display a human-readable legend for the chart.
402         */
403        public function get legend():ILegend
404        {
405            return this._legend;
406        }
407
408        /**
409         * @private
410         */
411        public function set legend(value:ILegend):void
412        {
413            this._legend = value;
414            this.invalidate();
415        }
416
417        /**
418         * @private
419         * Storage for legendLabelFunction
420         */
421        private var _legendLabelFunction:Function;
422
423        /**
424         * If defined, the chart will call the input function to determine the text displayed in
425         * in the chart's legend.
426         */
427        public function get legendLabelFunction():Function
428        {
429            return this._legendLabelFunction;
430        }
431
432        /**
433         * @private
434         */
435        public function set legendLabelFunction(value:Function):void
436        {
437            this._legendLabelFunction = value;
438        }
439
440    //--------------------------------------
441    //  Public Methods
442    //--------------------------------------
443
444        /**
445         * Returns the index within this plot area of the input ISeries object.
446         *
447         * @param series	a series that is displayed in this plot area.
448         * @return			the index of the input series
449         */
450        public function seriesToIndex(series:ISeries):int
451        {
452            return this.series.indexOf(series);
453        }
454
455        /**
456         * Returns the ISeries object at the specified index.
457         *
458         * @param index		the index of the series to return
459         * @return			the series that appears at the input index or null if out of bounds
460         */
461        public function indexToSeries(index:int):ISeries
462        {
463            if(index < 0 || index >= this.series.length) return null;
464            return this.series[index];
465        }
466
467    //--------------------------------------
468    //  Protected Methods
469    //--------------------------------------
470
471        /**
472         * @private
473         */
474        override protected function configUI():void
475        {
476            super.width = 400;
477            super.height = 300;
478
479            super.configUI();
480
481            this.content = new Sprite();
482            this.addChild(this.content);
483
484            this.dataTip = new DataTipRenderer();
485            this.dataTip.visible = false;
486            this.addChild(this.dataTip);
487        }
488
489        /**
490         * @private
491         */
492        override protected function draw():void
493        {
494            var dataInvalid:Boolean = this.isInvalid(InvalidationType.DATA);
495            var stylesInvalid:Boolean = this.isInvalid(InvalidationType.STYLES);
496            var sizeInvalid:Boolean = this.isInvalid(InvalidationType.SIZE);
497
498            if(stylesInvalid || dataInvalid)
499            {
500                this.refreshSeries();
501            }
502
503            //update the background if needed
504            if(stylesInvalid)
505            {
506                if(this.background)
507                {
508                    this.removeChild(this.background);
509                }
510                var skinClass:Object = this.getStyleValue("backgroundSkin");
511                this.background = UIComponentUtil.getDisplayObjectInstance(this, skinClass);
512                this.addChildAt(this.background, 0);
513            }
514
515            if(this.background && (stylesInvalid || sizeInvalid))
516            {
517                this.background.width = this.width;
518                this.background.height = this.height;
519
520                //force the background to redraw if it is a UIComponent
521                if(this.background is UIComponent)
522                {
523                    (this.background as UIComponent).drawNow();
524                }
525            }
526
527            if(this.dataTip is UIComponent)
528            {
529                var dataTip:UIComponent = UIComponent(this.dataTip);
530                this.copyStylesToChild(dataTip, DATA_TIP_STYLES);
531                dataTip.drawNow();
532            }
533
534            super.draw();
535        }
536
537        /**
538         * Analyzes the input data and smartly converts it to the correct ISeries type
539         * required for drawing. Adds new ISeries objects to the display list and removes
540         * unused series objects that no longer need to be drawn.
541         */
542        protected function refreshSeries():void
543        {
544            var modifiedData:Object = this._dataProvider;
545
546            //loop through each series and convert it to the correct data type
547            if(modifiedData is Array)
548            {
549                var arrayData:Array = (modifiedData as Array).concat();
550                var seriesCount:int = arrayData.length;
551                var foundIncompatibleData:Boolean = false;
552                for(var i:int = 0; i < seriesCount; i++)
553                {
554                    var currentItem:Object = arrayData[i];
555                    if(currentItem is Array || currentItem is XMLList)
556                    {
557                        var itemSeries:ISeries = new this.defaultSeriesType();
558                        if(currentItem is Array)
559                        {
560                            itemSeries.dataProvider = (currentItem as Array).concat();
561                        }
562                        else if(currentItem is XMLList)
563                        {
564                            itemSeries.dataProvider = (currentItem as XMLList).copy();
565                        }
566                        arrayData[i] = itemSeries;
567                    }
568                    else if(!(currentItem is ISeries))
569                    {
570                        //we only support Array, XMLList, and ISeries
571                        //anything else means that we should restore the original data
572                        var originalData:Array = (modifiedData as Array).concat();
573                        modifiedData = new this.defaultSeriesType(originalData);
574                        foundIncompatibleData = true;
575                        break;
576                    }
577                }
578                if(!foundIncompatibleData)
579                {
580                    modifiedData = arrayData;
581                }
582            }
583
584            //attempt to turn a string into XML
585            if(modifiedData is String)
586            {
587                try
588                {
589                    modifiedData = new XML(modifiedData);
590                }
591                catch(error:Error)
592                {
593                    //this isn't a valid xml string, so ignore it
594                    return;
595                }
596            }
597
598            //we need an XMLList, so get the elements
599            if(modifiedData is XML)
600            {
601                modifiedData = (modifiedData as XML).elements();
602            }
603
604            //convert the XMLList to a series
605            if(modifiedData is XMLList)
606            {
607                modifiedData = new this.defaultSeriesType(modifiedData);
608            }
609
610            //we should have an ISeries object by now, so put it in an Array
611            if(modifiedData is ISeries)
612            {
613                //if the main data is a series, put it in an array
614                modifiedData = [modifiedData];
615            }
616
617            //if it's not an array, we have bad data, so ignore it
618            if(!(modifiedData is Array))
619            {
620                return;
621            }
622
623            arrayData = modifiedData as Array;
624
625            seriesCount = this.series.length;
626            for(i = 0; i < seriesCount; i++)
627            {
628                var currentSeries:ISeries = this.series[i] as ISeries;
629                if(arrayData.indexOf(currentSeries) < 0)
630                {
631                    //if the series no longer exists, remove it from the display list and stop listening to it
632                    this.content.removeChild(DisplayObject(currentSeries));
633                    currentSeries.removeEventListener("dataChange", seriesDataChangeHandler);
634                    currentSeries.removeEventListener(ChartEvent.ITEM_ROLL_OVER, chartItemRollOver);
635                    currentSeries.removeEventListener(ChartEvent.ITEM_ROLL_OUT, chartItemRollOut);
636                    currentSeries.chart = null;
637                }
638            }
639
640            //rebuild the series Array
641            this.series = [];
642            seriesCount = arrayData.length;
643            for(i = 0; i < seriesCount; i++)
644            {
645                currentSeries = arrayData[i] as ISeries;
646                this.series.push(currentSeries);
647                if(!this.contains(DisplayObject(currentSeries)))
648                {
649                    //if this is a new series, add it to the display list and listen for events
650                    currentSeries.addEventListener("dataChange", seriesDataChangeHandler, false, 0, true);
651                    currentSeries.addEventListener(ChartEvent.ITEM_ROLL_OVER, chartItemRollOver, false, 0, true);
652                    currentSeries.addEventListener(ChartEvent.ITEM_ROLL_OUT, chartItemRollOut, false, 0, true);
653                    currentSeries.chart = this;
654                    this.content.addChild(DisplayObject(currentSeries));
655                }
656
657                DisplayObject(currentSeries).x = 0;
658                DisplayObject(currentSeries).y = 0;
659
660                //make sure the series are displayed in the correct order
661                this.content.setChildIndex(DisplayObject(currentSeries), this.content.numChildren - 1);
662
663                //update the series styles
664                this.copyStylesToSeries(currentSeries, ALL_SERIES_STYLES);
665                if(currentSeries is UIComponent)
666                {
667                    this.copyStylesToChild(UIComponent(currentSeries), SHARED_SERIES_STYLES);
668                }
669            }
670        }
671
672        /**
673         * @private
674         * Refreshes the legend's data provider.
675         */
676        protected function updateLegend():void
677        {
678            if(!this.legend) return;
679
680            var legendData:Array = [],
681                series:ISeries,
682                seriesCount:int = this.series.length,
683                i:int = 0,
684                n:int = 0,
685                itemLen:int,
686                itemData:LegendItemData,
687                message:String;
688            for(; i < seriesCount; i++)
689            {
690                series = ISeries(this.series[i]);
691                if(series is ILegendItemSeries)
692                {
693                    if(!(series as ILegendItemSeries).showInLegend) continue;
694                    itemData = ILegendItemSeries(series).createLegendItemData();
695                    itemData.label = itemData.label ? itemData.label : i.toString();
696                    if(series.legendLabelFunction != null && series.legendLabelFunction is Function)
697                    {
698                        try
699                        {
700                            itemData.label = series.legendLabelFunction(itemData.label);
701                        }
702                        catch(e:Error)
703                        {
704                            message = "There is an error in the series level legendLabelFunction.";
705                            this.dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, message));
706                        }
707                    }
708                    else if(this.legendLabelFunction != null && this.legendLabelFunction is Function)
709                    {
710                        try
711                        {
712                            message = "There is an error in the legendLabelFunction.";
713                            itemData.label = this.legendLabelFunction(itemData.label);
714                        }
715                        catch(e:Error)
716                        {
717                            this.dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, message));
718                        }
719                    }
720                    legendData.push(itemData);
721                }
722                else if(series is ICategorySeries)
723                {
724                    legendData = legendData.concat(ICategorySeries(series).createLegendItemData());
725                    if(this.legendLabelFunction != null && this.legendLabelFunction is Function)
726                    {
727                        if(legendData && legendData.length)
728                        {
729                            itemLen = legendData.length;
730                        }
731                        for(; n < itemLen; ++n)
732                        {
733                            itemData = legendData[n];
734                            itemData.label = this.legendLabelFunction(itemData.label);
735                        }
736                    }
737                }
738            }
739
740            this.legend.dataProvider = legendData;
741
742            if(UIComponent.inCallLaterPhase)
743            {
744                UIComponent(this.legend).drawNow();
745            }
746        }
747
748        /**
749         * @private
750         * Tranfers the chart's styles to the ISeries components it contains. These styles
751         * must be of the type Array, and the series index determines the index of the value
752         * to use from that Array. If the chart contains more ISeries components than there
753         * are values in the Array, the indices are reused starting from zero.
754         */
755        protected function copyStylesToSeries(child:ISeries, styleMap:Object):void
756        {
757            var index:int = this.series.indexOf(child);
758            var childComponent:UIComponent = child as UIComponent;
759            for(var n:String in styleMap)
760            {
761                var styleValues:Array = this.getStyleValue(styleMap[n]) as Array;
762
763                //if it doesn't exist, ignore it and go with the defaults for this series
764                if(styleValues == null || styleValues.length == 0) continue;
765                childComponent.setStyle(n, styleValues[index % styleValues.length])
766            }
767        }
768
769        /**
770         * @private
771         */
772        protected function defaultDataTipFunction(item:Object, index:int, series:ISeries):String
773        {
774            if(series.displayName)
775            {
776                return series.displayName;
777            }
778            return "";
779        }
780
781        /**
782         * @private
783         * Passes data to the data tip.
784         */
785        protected function refreshDataTip():void
786        {
787            var item:Object = this._lastDataTipRenderer.data;
788            var series:ISeries = this._lastDataTipRenderer.series;
789            var index:int = series.itemRendererToIndex(this._lastDataTipRenderer);
790
791            var dataTipText:String = "";
792            if(series.dataTipFunction != null)
793            {
794                try
795                {
796                    dataTipText = series.dataTipFunction(item, index, series);
797                }
798                catch(e:Error)
799                {
800                    var message:String = "There is an error in your series level dataTipFunction";
801                    this.dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, message));
802                }
803            }
804            else if(this.dataTipFunction != null)
805            {
806                try
807                {
808                    dataTipText = this.dataTipFunction(item, index, series);
809                }
810                catch(e:Error)
811                {
812                    message = "There is an error in your dataTipFunction";
813                    this.dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, message));
814                }
815            }
816
817            var dataTipRenderer:IDataTipRenderer = this.dataTip as IDataTipRenderer;
818            dataTipRenderer.text = dataTipText;
819            dataTipRenderer.data = item;
820
821            this.setChildIndex(this.dataTip, this.numChildren - 1);
822            if(this.dataTip is UIComponent)
823            {
824                UIComponent(this.dataTip).drawNow();
825            }
826        }
827
828    //--------------------------------------
829    //  Protected Event Handlers
830    //--------------------------------------
831
832        /**
833         * @private
834         * Display the data tip when the user moves the mouse over a chart marker.
835         */
836        protected function chartItemRollOver(event:ChartEvent):void
837        {
838            this._lastDataTipRenderer = event.itemRenderer;
839            this.refreshDataTip();
840
841            var position:Point = this.mousePositionToDataTipPosition();
842            this.dataTip.x = position.x;
843            this.dataTip.y = position.y;
844            this.dataTip.visible = true;
845
846            this.stage.addEventListener(MouseEvent.MOUSE_MOVE, stageMouseMoveHandler, false, 0 ,true);
847        }
848
849        /**
850         * @private
851         * Hide the data tip when the user moves the mouse off a chart marker.
852         */
853        protected function chartItemRollOut(event:ChartEvent):void
854        {
855            this.stage.removeEventListener(MouseEvent.MOUSE_MOVE, stageMouseMoveHandler);
856            this.dataTip.visible = false;
857        }
858
859    //--------------------------------------
860    //  Private Methods
861    //--------------------------------------
862
863        /**
864         * @private
865         * Determines the position for the data tip based on the mouse position
866         * and the bounds of the chart. Attempts to keep the data tip within the
867         * chart bounds so that it isn't hidden by any other display objects.
868         */
869        private function mousePositionToDataTipPosition():Point
870        {
871            var position:Point = new Point();
872            position.x = this.mouseX + 2;
873            position.x = Math.min(this.width - this.dataTip.width, position.x);
874            position.y = this.mouseY - this.dataTip.height - 2;
875            position.y = Math.max(0, position.y);
876            return position;
877        }
878
879    //--------------------------------------
880    //  Private Event Handlers
881    //--------------------------------------
882
883        /**
884         * @private
885         * The plot area needs to redraw the axes if series data changes.
886         */
887        private function seriesDataChangeHandler(event:Event):void
888        {
889            this.invalidate(InvalidationType.DATA);
890            if(this.dataTip.visible)
891            {
892                this.refreshDataTip();
893            }
894        }
895
896        /**
897         * @private
898         * Make the data tip follow the mouse.
899         */
900        private function stageMouseMoveHandler(event:MouseEvent):void
901        {
902            var position:Point = this.mousePositionToDataTipPosition();
903            this.dataTip.x = position.x;
904            this.dataTip.y = position.y;
905        }
906    }
907}
908