2020-10-20 00:58:15 +02:00

536 lines
14 KiB
ActionScript
Executable File

/*
Copyright (c) 2009 Yahoo! Inc. All rights reserved.
The copyrights embodied in the content of this file are licensed under the BSD (revised) open source license
*/
package com.yahoo.astra.fl.charts.series
{
import com.yahoo.astra.fl.charts.events.ChartEvent;
import fl.core.InvalidationType;
import fl.core.UIComponent;
import fl.transitions.easing.Strong;
import flash.display.DisplayObject;
import flash.display.InteractiveObject;
import flash.display.Shape;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;
//--------------------------------------
// Styles
//--------------------------------------
/**
* The easing function for animations that occur on data changes.
*/
[Style(name="animationEasingFunction", type="Function")]
/**
* The duration for animations that occur on data changes.
*/
[Style(name="animationDuration", type="int")]
/**
* If true, data changes will be displayed with animations. If false, changes will happen instantly.
*/
[Style(name="animationEnabled", type="Boolean")]
/**
* The base color used by objects displayed in this series.
*/
[Style(name="color", type="uint")]
/**
* The border color used by programatic skins in this series.
*/
[Style(name="borderColor", type="uint")]
/**
* The fill color used by programatic skins in this series.
*/
[Style(name="fillColor", type="uint")]
/**
* The Class used to instantiate each marker's skin.
*/
[Style(name="markerSkin", type="Class")]
/**
* The size, in pixels, of each marker.
*/
[Style(name="markerSize", type="Number")]
/**
* The alpha value from 0.0 to 1.0 to use for drawing the markers.
*/
[Style(name="markerAlpha", type="Number")]
/**
* The alpha value from 0.0 to 1.0 to use for drawing the fills of markers.
*/
[Style(name="fillAlpha", type="Number")]
/**
* The alpha value from 0.0 to 1.0 to use for drawing the border of markers.
*/
[Style(name="borderAlpha", type="Number")]
/**
* Functionality common to most series. Generally, a <code>Series</code> object
* shouldn't be instantiated directly. Instead, a subclass with a concrete
* implementation should be used.
*
* @author Josh Tynjala
*/
public class Series extends UIComponent implements ISeries
{
//--------------------------------------
// Class Variables
//--------------------------------------
/**
* @private
*/
private static var defaultStyles:Object =
{
markerSkin: Shape, //an empty display object
fillColor: null,
markerSize: 10,
markerAlpha: 1.0,
fillAlpha: 1.0,
borderAlpha: 1.0,
animationEnabled: true,
animationEasingFunction: fl.transitions.easing.Strong.easeOut,
animationDuration: 500,
borderColor: null,
color: 0x00b8bf
};
/**
* @private
*/
private static const RENDERER_STYLES:Object =
{
skin: "markerSkin",
fillColor: "fillColor",
borderColor: "borderColor",
color: "color",
fillAlpha: "fillAlpha",
borderAlpha: "borderAlpha"
};
//--------------------------------------
// Class Methods
//--------------------------------------
/**
* @copy fl.core.UIComponent#getStyleDefinition()
*/
public static function getStyleDefinition():Object
{
return mergeStyles(defaultStyles, UIComponent.getStyleDefinition());
}
//--------------------------------------
// Constructor
//--------------------------------------
/**
* Constructor.
*/
public function Series(dataProvider:Object = null)
{
super();
this._dataProvider = dataProvider;
}
//--------------------------------------
// Properties
//--------------------------------------
/**
* @private
*/
protected var markers:Array = [];
/**
* @private
* A set of flags to indicate if special considerations need to be taken for the markers.
*/
protected var markerInvalidHash:Dictionary = new Dictionary(true);
/**
* @private
* Storage for the chart property.
*/
private var _chart:Object;
/**
* @copy com.yahoo.astra.fl.charts.ISeries#chart
*/
public function get chart():Object
{
return this._chart;
}
/**
* @private
*/
public function set chart(value:Object):void
{
this._chart = value;
//this is a fun hack to ensure that series know if their parent charts are in live preview
if(this._chart == null || this._chart.parent == null)
{
this.isLivePreview = false;
}
var className:String;
try
{
className = getQualifiedClassName(this._chart.parent);
}
catch (e:Error) {}
this.isLivePreview = (className == "fl.livepreview::LivePreviewParent");
}
/**
* @private
* A lookup system to convert from an item to its item renderer.
*/
private var _itemToItemRendererHash:Dictionary = new Dictionary();
/**
* @private
* Storage for the itemRenderer property.
*/
private var _itemRenderer:Object = SeriesItemRenderer;
/**
* The class used to instantiate item renderers.
*/
public function get itemRenderer():Object
{
return this._itemRenderer;
}
/**
* @private
*/
public function set itemRenderer(value:Object):void
{
if(this._itemRenderer != value)
{
this._itemRenderer = value;
this.invalidate("itemRenderer");
}
}
/**
* @private
* Storage for the data property.
*/
private var _dataProvider:Object;
/**
* @copy com.yahoo.astra.fl.charts.ISeries#data
*/
public function get dataProvider():Object
{
return this._dataProvider;
}
/**
* @private
*/
public function set dataProvider(value:Object):void
{
if(this._dataProvider != value)
{
//if we get XML data and it isn't an XMLList,
//ignore the root tag
if(value is XML && !(value is XMLList))
{
value = value.elements();
}
if(value is XMLList)
{
value = XMLList(value).copy();
}
else if(value is Array)
{
value = (value as Array).concat();
}
this._dataProvider = value;
this.dispatchEvent(new Event("dataChange"));
this.invalidate(InvalidationType.DATA);
}
}
/**
* @private
* Storage for the displayName property.
*/
private var _displayName:String;
/**
* @copy com.yahoo.astra.fl.charts.ISeries#data
*/
public function get displayName():String
{
return this._displayName;
}
/**
* @private
*/
public function set displayName(value:String):void
{
this._displayName = value;
}
/**
* @copy com.yahoo.astra.fl.charts.ISeries#length
*/
public function get length():int
{
if(this._dataProvider is Array)
{
return (this._dataProvider as Array).length;
}
else if(this._dataProvider is XMLList)
{
return (this._dataProvider as XMLList).length();
}
return 0;
}
//--------------------------------------
// Public Methods
//--------------------------------------
/**
* @copy com.yahoo.astra.fl.charts.ISeries#clone()
*/
public function clone():ISeries
{
var series:Series = new Series();
series.dataProvider = this.dataProvider;
series.displayName = this.displayName;
return series;
}
/**
* @copy com.yahoo.astra.fl.charts.ISeries#itemRendererToIndex()
*/
public function itemRendererToIndex(renderer:ISeriesItemRenderer):int
{
return this.markers.indexOf(renderer);
}
public function itemToItemRenderer(item:Object):ISeriesItemRenderer
{
return this._itemToItemRendererHash[item] as ISeriesItemRenderer;
}
//--------------------------------------
// Protected Methods
//--------------------------------------
/**
* @private
*/
override protected function draw():void
{
//the class for the item renderers has changed. remove all markers
//so that they may be recreated.
if(this.isInvalid("itemRenderer"))
{
this.removeAllMarkers();
}
if(this.isInvalid("itemRenderer", InvalidationType.DATA, InvalidationType.STYLES))
{
this.refreshMarkers();
this._itemToItemRendererHash = new Dictionary(true);
var itemCount:int = this.markers.length;
for(var i:int = 0; i < itemCount; i++)
{
var marker:ISeriesItemRenderer = this.markers[i] as ISeriesItemRenderer;
if(this.isInvalid(InvalidationType.DATA)) //update data if needed
{
marker.data = this.dataProvider[i];
}
this._itemToItemRendererHash[marker.data] = marker;
var markerComponent:UIComponent = marker as UIComponent;
this.copyStylesToChild(markerComponent, RENDERER_STYLES);
markerComponent.drawNow();
}
}
super.draw();
}
/**
* @private
* All markers are removed from the display list.
*/
protected function removeAllMarkers():void
{
var markerCount:int = this.markers.length;
for(var i:int = 0; i < markerCount; i++)
{
var marker:ISeriesItemRenderer = this.markers.pop() as ISeriesItemRenderer;
marker.removeEventListener(MouseEvent.ROLL_OVER, markerRollOverHandler);
marker.removeEventListener(MouseEvent.ROLL_OUT, markerRollOutHandler);
marker.removeEventListener(MouseEvent.CLICK, markerClickHandler);
marker.removeEventListener(MouseEvent.DOUBLE_CLICK, markerDoubleClickHandler);
this.removeChild(DisplayObject(marker));
}
}
/**
* @private
* Add or remove markers as needed. current markers will be reused.
*/
protected function refreshMarkers():void
{
var itemCount:int = this.length;
var difference:int = itemCount - this.markers.length;
if(difference > 0)
{
for(var i:int = 0; i < difference; i++)
{
var marker:ISeriesItemRenderer = new this.itemRenderer();
marker.series = this;
InteractiveObject(marker).doubleClickEnabled = true;
marker.addEventListener(MouseEvent.ROLL_OVER, markerRollOverHandler, false, 0, true);
marker.addEventListener(MouseEvent.ROLL_OUT, markerRollOutHandler, false, 0, true);
marker.addEventListener(MouseEvent.CLICK, markerClickHandler, false, 0, true);
marker.addEventListener(MouseEvent.DOUBLE_CLICK, markerDoubleClickHandler, false, 0, true);
this.addChild(DisplayObject(marker));
this.markers.push(marker);
this.invalidateMarker(marker);
}
}
else if(difference < 0)
{
difference = Math.abs(difference);
for(i = 0; i < difference; i++)
{
marker = this.markers.pop() as ISeriesItemRenderer;
this.validateMarker(marker);
marker.removeEventListener(MouseEvent.ROLL_OVER, markerRollOverHandler);
marker.removeEventListener(MouseEvent.ROLL_OUT, markerRollOutHandler);
marker.removeEventListener(MouseEvent.CLICK, markerClickHandler);
marker.removeEventListener(MouseEvent.DOUBLE_CLICK, markerDoubleClickHandler);
this.removeChild(DisplayObject(marker));
}
}
var markerCount:int = this.markers.length;
for(i = 0; i < markerCount; i++)
{
marker = ISeriesItemRenderer(this.markers[i]);
marker.data = this.dataProvider[i];
DisplayObject(marker).alpha = this.getStyleValue("markerAlpha") as Number;
this.copyStylesToChild(UIComponent(marker), RENDERER_STYLES);
}
}
/**
* Indicates whether special considerations should be taken for a newly created marker.
*/
protected function isMarkerInvalid(marker:ISeriesItemRenderer):Boolean
{
return this.markerInvalidHash[marker];
}
/**
* Invalidates a marker (considered new).
*/
protected function invalidateMarker(marker:ISeriesItemRenderer):void
{
markerInvalidHash[marker] = true;
DisplayObject(marker).visible = false;
}
/**
* @private
* We never want the series to callLater after invalidating.
* The chart will ALWAYS handle drawing.
*/
override public function invalidate(property:String = InvalidationType.ALL, callLater:Boolean = true):void
{
//never call later!
super.invalidate(property, false);
}
/**
* Makes a marker valid. To be used by subclasses.
*/
protected function validateMarker(marker:ISeriesItemRenderer):void
{
DisplayObject(marker).visible = true;
delete markerInvalidHash[marker];
}
/**
* @private
* Notify the parent chart that the user's mouse is over this marker.
*/
protected function markerRollOverHandler(event:MouseEvent):void
{
var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
var index:int = this.itemRendererToIndex(itemRenderer);
var item:Object = this.dataProvider[index];
var rollOver:ChartEvent = new ChartEvent(ChartEvent.ITEM_ROLL_OVER, index, item, itemRenderer, this);
this.dispatchEvent(rollOver);
}
/**
* @private
* Notify the parent chart that the user's mouse has left this marker.
*/
protected function markerRollOutHandler(event:MouseEvent):void
{
var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
var index:int = this.itemRendererToIndex(itemRenderer);
var item:Object = this.dataProvider[index];
var rollOut:ChartEvent = new ChartEvent(ChartEvent.ITEM_ROLL_OUT, index, item, itemRenderer, this);
this.dispatchEvent(rollOut);
}
/**
* @private
* Notify the parent chart that the user clicked this marker.
*/
protected function markerClickHandler(event:MouseEvent):void
{
var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
var index:int = this.itemRendererToIndex(itemRenderer);
var item:Object = this.dataProvider[index];
var click:ChartEvent = new ChartEvent(ChartEvent.ITEM_CLICK, index, item, itemRenderer, this);
this.dispatchEvent(click);
}
/**
* @private
* Notify the parent chart that the user double-clicked this marker.
*/
protected function markerDoubleClickHandler(event:MouseEvent):void
{
var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
var index:int = this.itemRendererToIndex(itemRenderer);
var item:Object = this.dataProvider[index];
var doubleClick:ChartEvent = new ChartEvent(ChartEvent.ITEM_DOUBLE_CLICK, index, item, itemRenderer, this);
this.dispatchEvent(doubleClick);
}
}
}