421 lines
13 KiB
ActionScript
Executable File
421 lines
13 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.animation.Animation;
|
|
import com.yahoo.astra.animation.AnimationEvent;
|
|
import com.yahoo.astra.fl.charts.*;
|
|
import com.yahoo.astra.fl.charts.axes.NumericAxis;
|
|
import com.yahoo.astra.fl.charts.skins.CircleSkin;
|
|
import com.yahoo.astra.utils.GraphicsUtil;
|
|
|
|
import fl.core.UIComponent;
|
|
|
|
import flash.display.DisplayObject;
|
|
import flash.geom.Point;
|
|
import flash.geom.Rectangle;
|
|
|
|
/**
|
|
* The weight, in pixels, of the line drawn between points in this series.
|
|
*
|
|
* @default 3
|
|
*/
|
|
[Style(name="lineWeight", type="Number")]
|
|
|
|
/**
|
|
* If true, lines are drawn between the markers. If false, only the markers are drawn.
|
|
*
|
|
* @default true
|
|
*/
|
|
[Style(name="connectPoints", type="Boolean")]
|
|
|
|
/**
|
|
* If true, draws a dashed line between discontinuous points.
|
|
*
|
|
* @default false
|
|
*/
|
|
[Style(name="connectDiscontinuousPoints", type="Boolean")]
|
|
|
|
/**
|
|
* The length of dashes in a discontinuous line.
|
|
*
|
|
* @default 10
|
|
*/
|
|
[Style(name="discontinuousDashLength", type="Number")]
|
|
|
|
/**
|
|
* If true, the series will include a fill under the line, extending to the axis.
|
|
*
|
|
* @default false
|
|
*/
|
|
[Style(name="showAreaFill", type="Boolean")]
|
|
|
|
/**
|
|
* The alpha value of the area fill.
|
|
*
|
|
* @default 0.6
|
|
*/
|
|
[Style(name="areaFillAlpha", type="Number")]
|
|
|
|
/**
|
|
* Renders data points as a series of connected line segments.
|
|
*
|
|
* @author Josh Tynjala
|
|
*/
|
|
public class LineSeries extends CartesianSeries
|
|
{
|
|
|
|
//--------------------------------------
|
|
// Class Variables
|
|
//--------------------------------------
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
private static var defaultStyles:Object =
|
|
{
|
|
markerSkin: CircleSkin,
|
|
lineWeight: 3,
|
|
connectPoints: true,
|
|
connectDiscontinuousPoints: false,
|
|
discontinuousDashLength: 10,
|
|
showAreaFill: false,
|
|
areaFillAlpha: 0.6
|
|
};
|
|
|
|
//--------------------------------------
|
|
// Class Methods
|
|
//--------------------------------------
|
|
|
|
/**
|
|
* @copy fl.core.UIComponent#getStyleDefinition()
|
|
*/
|
|
public static function getStyleDefinition():Object
|
|
{
|
|
return mergeStyles(defaultStyles, Series.getStyleDefinition());
|
|
}
|
|
|
|
//--------------------------------------
|
|
// Constructor
|
|
//--------------------------------------
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function LineSeries(data:Object = null)
|
|
{
|
|
super(data);
|
|
}
|
|
|
|
//--------------------------------------
|
|
// Properties
|
|
//--------------------------------------
|
|
|
|
/**
|
|
* @private
|
|
* The Animation instance that controls animation in this series.
|
|
*/
|
|
private var _animation:Animation;
|
|
|
|
//--------------------------------------
|
|
// Public Methods
|
|
//--------------------------------------
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
override public function clone():ISeries
|
|
{
|
|
var series:LineSeries = new LineSeries();
|
|
if(this.dataProvider is Array)
|
|
{
|
|
//copy the array rather than pass it by reference
|
|
series.dataProvider = (this.dataProvider as Array).concat();
|
|
}
|
|
else if(this.dataProvider is XMLList)
|
|
{
|
|
series.dataProvider = (this.dataProvider as XMLList).copy();
|
|
}
|
|
series.displayName = this.displayName;
|
|
series.horizontalField = this.horizontalField;
|
|
series.verticalField = this.verticalField;
|
|
|
|
return series;
|
|
}
|
|
|
|
//--------------------------------------
|
|
// Protected Methods
|
|
//--------------------------------------
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
override protected function draw():void
|
|
{
|
|
super.draw();
|
|
|
|
this.graphics.clear();
|
|
|
|
if(!this.dataProvider)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var markerSize:Number = this.getStyleValue("markerSize") as Number;
|
|
|
|
var startValues:Array = [];
|
|
var endValues:Array = [];
|
|
var itemCount:int = this.length;
|
|
for(var i:int = 0; i < itemCount; i++)
|
|
{
|
|
var position:Point = CartesianChart(this.chart).itemToPosition(this, i);
|
|
|
|
var marker:DisplayObject = this.markers[i] as DisplayObject;
|
|
var ratio:Number = marker.width / marker.height;
|
|
if(isNaN(ratio)) ratio = 1;
|
|
marker.height = markerSize;
|
|
marker.width = marker.height * ratio;
|
|
|
|
if(marker is UIComponent)
|
|
{
|
|
(marker as UIComponent).drawNow();
|
|
}
|
|
|
|
//if we have a bad position, don't display the marker
|
|
if(isNaN(position.x) || isNaN(position.y))
|
|
{
|
|
this.invalidateMarker(ISeriesItemRenderer(marker));
|
|
}
|
|
else if(this.isMarkerInvalid(ISeriesItemRenderer(marker)))
|
|
{
|
|
marker.x = position.x - marker.width / 2;
|
|
marker.y = position.y - marker.height / 2;
|
|
this.validateMarker(ISeriesItemRenderer(marker));
|
|
}
|
|
|
|
//correct start value for marker size
|
|
startValues.push(marker.x + marker.width / 2);
|
|
startValues.push(marker.y + marker.height / 2);
|
|
|
|
endValues.push(position.x);
|
|
endValues.push(position.y);
|
|
}
|
|
|
|
//handle animating all the markers in one fell swoop.
|
|
if(this._animation)
|
|
{
|
|
this._animation.removeEventListener(AnimationEvent.UPDATE, tweenUpdateHandler);
|
|
this._animation.removeEventListener(AnimationEvent.COMPLETE, tweenUpdateHandler);
|
|
this._animation = null;
|
|
}
|
|
|
|
//don't animate on livepreview!
|
|
if(this.isLivePreview || !this.getStyleValue("animationEnabled"))
|
|
{
|
|
this.drawMarkers(endValues);
|
|
}
|
|
else
|
|
{
|
|
var animationDuration:int = this.getStyleValue("animationDuration") as int;
|
|
var animationEasingFunction:Function = this.getStyleValue("animationEasingFunction") as Function;
|
|
|
|
this._animation = new Animation(animationDuration, startValues, endValues);
|
|
this._animation.addEventListener(AnimationEvent.UPDATE, tweenUpdateHandler);
|
|
this._animation.addEventListener(AnimationEvent.COMPLETE, tweenUpdateHandler);
|
|
this._animation.easingFunction = animationEasingFunction;
|
|
this.drawMarkers(startValues);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
private function tweenUpdateHandler(event:AnimationEvent):void
|
|
{
|
|
this.drawMarkers(event.parameters as Array);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
private function drawMarkers(data:Array):void
|
|
{
|
|
var primaryIsVertical:Boolean = true;
|
|
var primaryAxis:NumericAxis = CartesianChart(this.chart).verticalAxis as NumericAxis;
|
|
if(!primaryAxis)
|
|
{
|
|
primaryIsVertical = false;
|
|
primaryAxis = CartesianChart(this.chart).horizontalAxis as NumericAxis;
|
|
}
|
|
|
|
var originPosition:Number = primaryAxis.valueToLocal(primaryAxis.origin);
|
|
|
|
|
|
|
|
var lineColor:uint = this.getStyleValue("lineColor") != null ? this.getStyleValue("lineColor") as uint : this.getStyleValue("color") as uint;
|
|
var connectDiscontinuousPoints:Boolean = this.getStyleValue("connectDiscontinuousPoints") as Boolean;
|
|
var discontinuousDashLength:Number = this.getStyleValue("discontinuousDashLength") as Number;
|
|
var showAreaFill:Boolean = this.getStyleValue("showAreaFill") as Boolean;
|
|
|
|
this.graphics.clear();
|
|
this.setPrimaryLineStyle();
|
|
this.beginAreaFill(showAreaFill, lineColor);
|
|
|
|
var firstValidPosition:Point;
|
|
var lastValidPosition:Point;
|
|
|
|
//used to determine if the data must be drawn
|
|
var seriesBounds:Rectangle = new Rectangle(0, 0, this.width, this.height);
|
|
var lastMarkerValid:Boolean = false;
|
|
var itemCount:int = this.length;
|
|
for(var i:int = 0; i < itemCount; i++)
|
|
{
|
|
var marker:DisplayObject = DisplayObject(this.markers[i]);
|
|
var xPosition:Number = data[i * 2] as Number;
|
|
var yPosition:Number = data[i * 2 + 1] as Number;
|
|
var markerValid:Boolean = !this.isMarkerInvalid(ISeriesItemRenderer(marker));
|
|
|
|
//if the position is valid, move or draw as needed
|
|
if(markerValid)
|
|
{
|
|
marker.x = xPosition - marker.width / 2;
|
|
marker.y = yPosition - marker.height / 2;
|
|
|
|
if(lastValidPosition && !lastMarkerValid && connectDiscontinuousPoints)
|
|
{
|
|
this.setPrimaryLineStyle(connectDiscontinuousPoints);
|
|
|
|
//draw a discontinuous line from the last valid position and the new valid position
|
|
GraphicsUtil.drawDashedLine(this.graphics, lastValidPosition.x, lastValidPosition.y, xPosition, yPosition, discontinuousDashLength, discontinuousDashLength);
|
|
|
|
this.setPrimaryLineStyle();
|
|
}
|
|
else if(!lastValidPosition || (!lastMarkerValid && !connectDiscontinuousPoints))
|
|
{
|
|
//if the last position is not valid, simply move to the new position
|
|
this.graphics.moveTo(xPosition, yPosition);
|
|
}
|
|
else //current and last position are both valid
|
|
{
|
|
var minX:Number = Math.min(lastValidPosition.x, xPosition);
|
|
var maxX:Number = Math.max(lastValidPosition.x, xPosition);
|
|
var minY:Number = Math.min(lastValidPosition.y, yPosition);
|
|
var maxY:Number = Math.max(lastValidPosition.y, yPosition);
|
|
var lineBounds:Rectangle = new Rectangle(minX, minY, maxX - minX, maxY - minY);
|
|
|
|
//if x or y position is equal between points, the rectangle will have
|
|
//a width or height of zero (so no line will be drawn where one should!)
|
|
if(lineBounds.width == 0)
|
|
{
|
|
lineBounds.width = 1;
|
|
}
|
|
|
|
if(lineBounds.height == 0)
|
|
{
|
|
lineBounds.height = 1;
|
|
}
|
|
|
|
//if line between the last point and this point is within
|
|
//the series bounds, draw it, otherwise, only move to the new point.
|
|
if(lineBounds.intersects(seriesBounds) ||
|
|
yPosition == seriesBounds.y ||
|
|
yPosition == seriesBounds.y + seriesBounds.height ||
|
|
xPosition == seriesBounds.x ||
|
|
xPosition == seriesBounds.x + seriesBounds.width)
|
|
{
|
|
this.graphics.lineTo(xPosition, yPosition);
|
|
}
|
|
else
|
|
{
|
|
this.graphics.moveTo(xPosition, yPosition);
|
|
}
|
|
}
|
|
lastMarkerValid = true;
|
|
lastValidPosition = new Point(xPosition, yPosition);
|
|
if(!firstValidPosition)
|
|
{
|
|
firstValidPosition = lastValidPosition.clone();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(showAreaFill) this.closeAreaFill(primaryIsVertical, originPosition, firstValidPosition, lastValidPosition);
|
|
this.setPrimaryLineStyle();
|
|
this.beginAreaFill(showAreaFill, lineColor);
|
|
lastMarkerValid = false;
|
|
firstValidPosition = null;
|
|
}
|
|
}
|
|
|
|
if(showAreaFill)
|
|
{
|
|
this.closeAreaFill(primaryIsVertical, originPosition, firstValidPosition, lastValidPosition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* Begins drawing an area fill.
|
|
*/
|
|
private function beginAreaFill(showAreaFill:Boolean, color:uint):void
|
|
{
|
|
if(!showAreaFill)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var areaFillAlpha:Number = this.getStyleValue("areaFillAlpha") as Number;
|
|
this.graphics.beginFill(color, areaFillAlpha);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* Sets the line style when connecting points. The forceColor flag
|
|
* will use the color even when the connectPoints style is set to false.
|
|
* This is used primarily to allow connectDiscontinousPoints to work
|
|
* when connectPoints is false.
|
|
*/
|
|
private function setPrimaryLineStyle(forceColor:Boolean = false):void
|
|
{
|
|
var connectPoints:Boolean = this.getStyleValue("connectPoints") as Boolean;
|
|
if(!connectPoints && !forceColor)
|
|
{
|
|
this.graphics.lineStyle(0, 0, 0);
|
|
return;
|
|
}
|
|
|
|
var lineWeight:int = this.getStyleValue("lineWeight") as int;
|
|
var lineColor:uint = this.getStyleValue("lineColor") != null ? this.getStyleValue("lineColor") as uint : this.getStyleValue("color") as uint;
|
|
var lineAlpha:Number = this.getStyleValue("lineAlpha") as Number;
|
|
this.graphics.lineStyle(lineWeight, lineColor, lineAlpha);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* Closes an area fill. Called after the full line is drawn. May also be
|
|
* called when bad data is encountered.
|
|
*/
|
|
private function closeAreaFill(vertical:Boolean, origin:Number, firstValidPosition:Point, lastValidPosition:Point):void
|
|
{
|
|
if(isNaN(origin) || firstValidPosition == null || lastValidPosition == null) return;
|
|
this.graphics.lineStyle(0, 0, 0);
|
|
if(vertical)
|
|
{
|
|
this.graphics.lineTo(lastValidPosition.x, origin);
|
|
this.graphics.lineTo(firstValidPosition.x, origin);
|
|
this.graphics.lineTo(firstValidPosition.x, firstValidPosition.y);
|
|
}
|
|
else
|
|
{
|
|
this.graphics.lineTo(origin, lastValidPosition.y);
|
|
this.graphics.lineTo(origin, firstValidPosition.y);
|
|
this.graphics.lineTo(firstValidPosition.x, firstValidPosition.y);
|
|
}
|
|
this.graphics.endFill();
|
|
}
|
|
|
|
}
|
|
}
|