/* 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(); } } }