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

1005 lines
26 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.axes
{
import com.yahoo.astra.fl.charts.series.ISeries;
import com.yahoo.astra.fl.charts.CartesianChart;
import com.yahoo.astra.utils.DateUtil;
import com.yahoo.astra.utils.TimeUnit;
import fl.core.UIComponent;
import flash.text.TextFormat;
/**
* An axis type representing a date and time range from minimum to maximum
* with major and minor divisions.
*
* @author Josh Tynjala
*/
public class TimeAxis extends BaseAxis implements IAxis, IStackingAxis
{
//--------------------------------------
// Static Properties
//--------------------------------------
/**
* @private
*/
private static const TIME_UNITS:Array = [TimeUnit.MILLISECONDS, TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAY, TimeUnit.MONTH, TimeUnit.YEAR];
//--------------------------------------
// Constructor
//--------------------------------------
/**
* Constructor.
*/
public function TimeAxis()
{
super();
}
//--------------------------------------
// Properties
//--------------------------------------
protected var positionMultiplier:Number = 0;
/**
* @private
* Storage for the minimum value.
*/
private var _minimum:Date;
/**
* @private
* Indicates whether the minimum bound is user-defined or generated by the axis.
*/
private var _minimumSetByUser:Boolean = false;
/**
* The minimum value displayed on the axis.
*/
public function get minimum():Date
{
return this._minimum;
}
/**
* @private
*/
public function set minimum(value:Date):void
{
this._minimum = value;
this._minimumSetByUser = value != null;
}
/**
* @private
* Storage for the maximum value.
*/
private var _maximum:Date;
/**
* @private
* Indicates whether the maximum bound is user-defined or generated by the axis.
*/
private var _maximumSetByUser:Boolean = false;
/**
* The maximum value displayed on the axis.
*/
public function get maximum():Date
{
return this._maximum;
}
/**
* @private
*/
public function set maximum(value:Date):void
{
this._maximum = value;
this._maximumSetByUser = value != null;
}
//-- Units
/**
* @private
* Storage for the major unit.
*/
private var _majorUnit:int = 1;
/**
* @private
* Indicates whether the major unit is user-defined or generated by the axis.
*/
private var _majorUnitSetByUser:Boolean = false;
/**
* The major unit at which new lines are drawn.
*/
public function get majorUnit():Number
{
return this._majorUnit;
}
/**
* @private
*/
public function set majorUnit(value:Number):void
{
this._majorUnit = value;
this._majorUnitSetByUser = !isNaN(value);
}
/**
* @private
* Storage for the majorTimeUnit property.
*/
private var _majorTimeUnit:String = TimeUnit.MONTH;
/**
* @private
* Indicates whether the major time unit is user-defined or generated by the axis.
*/
private var _majorTimeUnitSetByUser:Boolean = false;
/**
* Combined with majorUnit, determines the amount of time between major ticks and labels.
*
* @see com.yahoo.astra.fl.charts.TimeUnit;
*/
public function get majorTimeUnit():String
{
return this._majorTimeUnit;
}
/**
* @private
*/
public function set majorTimeUnit(value:String):void
{
this._majorTimeUnit = value;
this._majorTimeUnitSetByUser = value != null;
}
/**
* @private
* Storage for the minor unit.
*/
private var _minorUnit:int = 1;
/**
* @private
* Indicates whether the minor unit is user-defined or generated by the axis.
*/
private var _minorUnitSetByUser:Boolean = false;
/**
* The minor unit at which new lines are drawn.
*/
public function get minorUnit():Number
{
return this._minorUnit;
}
/**
* @private
*/
public function set minorUnit(value:Number):void
{
this._minorUnit = value;
this._minorUnitSetByUser = !isNaN(value);
}
/**
* @private
* Storage for the minorTimeUnit property.
*/
private var _minorTimeUnit:String = TimeUnit.MONTH;
/**
* @private
* Indicates whether the minor time unit is user-defined or generated by the axis.
*/
private var _minorTimeUnitSetByUser:Boolean = false;
/**
* Combined with minorUnit, determines the amount of time between minor ticks.
*
* @see com.yahoo.astra.fl.charts.TimeUnit;
*/
public function get minorTimeUnit():String
{
return this._minorTimeUnit;
}
/**
* @private
*/
public function set minorTimeUnit(value:String):void
{
this._minorTimeUnit = value;
this._minorTimeUnitSetByUser = value != null;
}
/**
* @private
* Storage for the stackingEnabled property.
*/
private var _stackingEnabled:Boolean = false;
/**
* @inheritDoc
*/
public function get stackingEnabled():Boolean
{
return this._stackingEnabled;
}
/**
* @private
*/
public function set stackingEnabled(value:Boolean):void
{
this._stackingEnabled = value;
}
/**
* @private
* Storage for the snapToUnits property.
*/
private var _snapToUnits:Boolean = true;
/**
* If true, the labels, ticks, gridlines, and other objects will snap to
* the nearest major or minor unit. If false, their position will be based
* on the minimum value.
*/
public function get snapToUnits():Boolean
{
return this._snapToUnits;
}
/**
* @private
*/
public function set snapToUnits(value:Boolean):void
{
this._snapToUnits = value;
}
/**
* @private
*/
private var _dataMinimum:Date;
/**
* @private
*/
private var _dataMaximum:Date;
/**
* @private
*/
private var _numLabels:Number;
/**
* @private
*/
private var _numLabelsSetByUser:Boolean = false;
/**
* @inheritDoc
*/
public function get numLabels():Number
{
return _numLabels;
}
/**
* @private (setter)
*/
public function set numLabels(value:Number):void
{
if(_numLabelsSetByUser) return;
_numLabels = value;
_numLabelsSetByUser = true;
_majorUnitSetByUser = false;
_minorUnitSetByUser = false;
}
/**
* @private
* Holds value for idealPixels
*/
private var _idealPixels:Number = 60;
/**
* Desired distance between majorUnits. Used to calculate the major unit
* when unspecified and <code>calculateByLabelSize</code> is set to false.
*/
public function get idealPixels():Number
{
return _idealPixels;
}
/**
* @private (setter)
*/
public function set idealPixels(value:Number):void
{
_idealPixels = value;
}
/**
* @private
* Holds value for calculateByLabelSize
*/
private var _calculateByLabelSize:Boolean = false;
/**
* Indicates whether to use the maximum size of an axis label
* when calculating the majorUnit.
*/
public function get calculateByLabelSize():Boolean
{
return _calculateByLabelSize;
}
/**
* @private (setter)
*/
public function set calculateByLabelSize(value:Boolean):void
{
_calculateByLabelSize = value;
}
//--------------------------------------
// Public Methods
//--------------------------------------
/**
* @inheritDoc
*/
public function stack(top:Object, ...rest:Array):Object
{
var value:Number = this.valueToNumber(top);
var restCount:int = rest.length;
for(var i:int = 0; i < restCount; i++)
{
value += this.valueToNumber(rest[i]);
}
return value;
}
/**
* @inheritDoc
*/
public function updateScale():void
{
this.resetScale();
this.calculatePositionMultiplier();
(this.renderer as ICartesianAxisRenderer).majorUnitSetByUser = this._majorUnitSetByUser;
this.renderer.ticks = this.createAxisData(this.majorUnit, this.majorTimeUnit);
this.renderer.minorTicks = this.createAxisData(this.minorUnit, this.minorTimeUnit, false);
}
/**
* @inheritDoc
*/
public function valueToLocal(value:Object):Number
{
var numericValue:Number = this.valueToNumber(value);
var position:Number = (numericValue - this.minimum.valueOf()) * this.positionMultiplier;
if(this.reverse)
{
position = this.renderer.length - position;
}
//the vertical axis has its origin on the bottom
if(this.renderer is ICartesianAxisRenderer && ICartesianAxisRenderer(this.renderer).orientation == AxisOrientation.VERTICAL)
{
position = this.renderer.length - position;
}
return position;
}
/**
* @inheritDoc
*/
override public function valueToLabel(value:Object):String
{
var text:String = value.toString();
if(this.labelFunction != null)
{
var numericValue:Number = this.valueToNumber(value);
text = this.labelFunction(new Date(numericValue), this.majorTimeUnit);
}
if(text == null)
{
text = "";
}
return text;
}
//--------------------------------------
// Protected Methods
//--------------------------------------
/**
* @private
* Converts one of the accepted values to a Number that can be
* used for position calculation.
*/
protected function valueToNumber(value:Object):Number
{
var convertedValue:Number = 0;
if(value is Date)
{
convertedValue = (value as Date).valueOf();
}
else if(!(value is Number))
{
convertedValue = new Date(value.toString()).valueOf();
}
else
{
convertedValue = value as Number;
}
return convertedValue;
}
/**
* @private
* Calculates the best scale.
*/
protected function resetScale():void
{
if(!this._minimumSetByUser)
{
this._minimum = new Date(this._dataMinimum.valueOf());
}
if(!this._maximumSetByUser)
{
this._maximum = new Date(this._dataMaximum.valueOf());
}
this.checkMinLessThanMax();
this.calculateMajorUnit();
this.calculateMinorUnit();
}
/**
* @private
* Generates AxisData objects for use by the axis renderer.
*/
protected function createAxisData(unit:Number, timeUnit:String, isMajorUnit:Boolean = true):Array
{
if(unit <= 0)
{
return [];
}
var data:Array = [];
var displayedMaximum:Boolean = false;
var displayedMinimum:Boolean = false;
var date:Date = new Date(this.minimum.valueOf());
var itemCount:int = 0;
while(date.valueOf() <= this.maximum.valueOf())
{
date = new Date(this.minimum.valueOf());
if(itemCount > 0)
{
var unitValue:Number = itemCount * unit;
date = this.updateDate(date, timeUnit, unitValue, this.snapToUnits);
}
//stop at the maximum value.
if(date.valueOf() > this.maximum.valueOf())
{
if(!this._majorUnitSetByUser && this.calculateByLabelSize) break;
date = new Date(this.maximum.valueOf());
}
//because Flash UIComponents round the position to the nearest pixel, we need to do the same.
var position:Number = Math.round(this.valueToLocal(date));
var label:String = this.valueToLabel(date);
var axisData:AxisData = new AxisData(position, date, label);
data.push(axisData);
itemCount++;
if(date.valueOf() == this.maximum.valueOf())
{
break;
}
}
return data;
}
/**
* @inheritDoc
*/
public function getMaxLabel():String
{
var maxAbbrevDate:String = this.valueToLabel(new Date(2008, 4, 30));
var maxFullDate:String = this.valueToLabel(new Date(2009, 8, 30));
var maxDate:String = maxAbbrevDate.length > maxFullDate.length ? maxAbbrevDate : maxFullDate;
return maxDate;
}
//--------------------------------------
// Private Methods
//--------------------------------------
/**
* @private
*/
private function updateDate(date:Date, timeUnit:String, unitValue:Number, snapToUnits:Boolean):Date
{
switch(timeUnit)
{
case TimeUnit.YEAR:
date.fullYear += unitValue;
if(snapToUnits)
{
date.month = 0;
date.date = 1;
date.hours = 0;
date.minutes = 0;
date.seconds = 0;
date.milliseconds = 0;
}
break;
case TimeUnit.MONTH:
date.month += unitValue;
if(snapToUnits)
{
date.date = 1;
date.hours = 0;
date.minutes = 0;
date.seconds = 0;
date.milliseconds = 0;
}
break;
case TimeUnit.DAY:
date.date += unitValue;
if(snapToUnits)
{
date.hours = 0;
date.minutes = 0;
date.seconds = 0;
date.milliseconds = 0;
}
break;
case TimeUnit.HOURS:
date.hours += unitValue;
if(snapToUnits)
{
date.minutes = 0;
date.seconds = 0;
date.milliseconds = 0;
}
break;
case TimeUnit.MINUTES:
date.minutes += unitValue;
if(snapToUnits)
{
date.seconds = 0;
date.milliseconds = 0;
}
break;
case TimeUnit.SECONDS:
date.seconds += unitValue;
if(snapToUnits)
{
date.milliseconds = 0;
}
break;
case TimeUnit.MILLISECONDS:
date.milliseconds += unitValue;
break;
}
return date;
}
/**
* @private
* Swaps the minimum and maximum values, if needed.
*/
private function checkMinLessThanMax():void
{
if(this._minimum.valueOf() > this._maximum.valueOf())
{
var temp:Date = this._minimum;
this._minimum = this._maximum;
this._maximum = temp;
//be sure to swap these flags too!
var temp2:Boolean = this._minimumSetByUser;
this._minimumSetByUser = this._maximumSetByUser;
this._maximumSetByUser = temp2;
}
}
/**
* @private
* Determines the best major unit.
*/
private function calculateMajorUnit():void
{
if(!this._majorTimeUnitSetByUser)
{
//ballpark it
var dayCount:Number = DateUtil.countDays(this.minimum, this.maximum);
var yearCount:Number = DateUtil.getDateDifferenceByTimeUnit(this.minimum, this.maximum, TimeUnit.YEAR);
var monthCount:Number = DateUtil.getDateDifferenceByTimeUnit(this.minimum, this.maximum, TimeUnit.MONTH);
var hourCount:Number = dayCount * 24;
var minuteCount:Number = hourCount * 60;
var secondCount:Number = minuteCount * 60;
if(yearCount >= 1) this._majorTimeUnit = TimeUnit.YEAR;
else if(monthCount >= 1) this._majorTimeUnit = TimeUnit.MONTH;
else if(dayCount >= 1) this._majorTimeUnit = TimeUnit.DAY;
else if(hourCount >= 1) this._majorTimeUnit = TimeUnit.HOURS;
else if(minuteCount >= 1) this.majorTimeUnit = TimeUnit.MINUTES;
else if(secondCount >= 1) this.majorTimeUnit = TimeUnit.SECONDS;
else this.majorTimeUnit = TimeUnit.MILLISECONDS;
}
if(this._majorUnitSetByUser)
{
return;
}
this.calculateMaximumAndMinimum();
var chart:CartesianChart = this.chart as CartesianChart;
var labelSpacing:Number = 0;
var overflow:Number = 0;
var approxLabelDistance:Number = this.idealPixels;
if(this.calculateByLabelSize)
{
var rotation:Number;
//Check to see if this axis is horizontal. Since the width of labels will be variable, we will need to apply a different alogrithm to determine the majorUnit.
if(chart.horizontalAxis == this)
{
//extract the approximate width of the labels by getting the textWidth of the maximum date when rendered by the label function with the textFormat of the renderer.
approxLabelDistance = this.maxLabelWidth;
rotation = chart.getHorizontalAxisStyle("rotation") as Number;
if(rotation >= 0)
{
if(!isNaN(chart.horizontalAxisLabelData.rightLabelOffset)) overflow += chart.horizontalAxisLabelData.rightLabelOffset as Number;
}
if(rotation <= 0)
{
if(!isNaN(chart.horizontalAxisLabelData.leftLabelOffset)) overflow += chart.horizontalAxisLabelData.leftLabelOffset as Number;
}
}
else
{
approxLabelDistance = this.maxLabelHeight;
rotation = chart.getVerticalAxisStyle("rotation") as Number;
if(!isNaN(chart.verticalAxisLabelData.topLabelOffset)) overflow = chart.verticalAxisLabelData.topLabelOffset as Number;
}
labelSpacing = this.labelSpacing;
approxLabelDistance += (labelSpacing*2);
}
var dateDifference:Number = Math.round(DateUtil.getDateDifferenceByTimeUnit(this.minimum, this.maximum, this.majorTimeUnit));
var tempMajorUnit:Number = 0;
var maxLabels:Number = Math.floor(((this.renderer.length + overflow) - labelSpacing)/approxLabelDistance);
//If set by user, use specified number of labels unless its too many
if(this._numLabelsSetByUser)
{
maxLabels = Math.min(maxLabels, this.numLabels);
}
tempMajorUnit = dateDifference/maxLabels;
tempMajorUnit = Math.ceil(tempMajorUnit);
if(tempMajorUnit > Math.round(dateDifference/2)) tempMajorUnit = dateDifference;
this._majorUnit = tempMajorUnit;
if(dateDifference%tempMajorUnit != 0 && this.calculateByLabelSize)
{
var len:Number = Math.min(tempMajorUnit, ((dateDifference/2)-tempMajorUnit));
for(var i:int = 0;i < len; i++)
{
tempMajorUnit++;
if(dateDifference%tempMajorUnit == 0)
{
this._majorUnit = tempMajorUnit;
break;
}
}
}
}
/**
* @private
* Determines the best minor unit.
*/
private function calculateMinorUnit():void
{
if(!this._minorTimeUnitSetByUser)
{
//if the numeric part of the major unit is 1, we want to move
//the time part of the minor unit to a interval lower than the major.
//...unless the user has set the minor unit. this is a weird case
//that shouldn't happen, but it might.
//in that case, we go with the standard behavior where major unit
//and minor unit are the same.
if(!this._minorUnitSetByUser && this._majorUnit == 1)
{
var index:int = TIME_UNITS.indexOf(this._majorTimeUnit);
if(index > 0) this._minorTimeUnit = TIME_UNITS[index - 1];
}
else this._minorTimeUnit = this._majorTimeUnit;
}
if(this._minorUnitSetByUser)
{
return;
}
if(this.majorTimeUnit == this.minorTimeUnit && this._majorUnit != 1)
{
if(this._majorUnit % 2 == 0)
{
this._minorUnit = this._majorUnit / 2;
}
else if(this._majorUnit % 3 == 0)
{
this._minorUnit = this._majorUnit / 3;
}
else this._minorUnit = 0;
}
else
{
//in this case, we know that the time portion of the minor
//unit is a smaller interval than the major unit.
switch(this._minorTimeUnit)
{
case TimeUnit.MONTH:
this._minorUnit = 6;
break;
//no perfect half-way point for number of days in a month
//so use the default of zero
/*case TimeUnit.DAY:
break;*/
case TimeUnit.HOURS:
this._minorUnit = 12;
break;
case TimeUnit.MINUTES:
this._minorUnit = 30;
break;
case TimeUnit.SECONDS:
this._minorUnit = 30;
break;
default:
this._minorUnit = 0;
break;
}
}
}
/**
* @private
* Determines the best time unit.
*/
private function calculateTimeUnitSize(timeUnit:String):Number
{
switch(timeUnit)
{
case TimeUnit.YEAR:
var year:Date = new Date(1970, 11, 31, 16);
return year.valueOf();
case TimeUnit.MONTH:
var month:Date = new Date(1970, 0, 31, 16);
return month.valueOf();
case TimeUnit.DAY:
var day:Date = new Date(1970, 0, 1, 16);
return day.valueOf();
case TimeUnit.HOURS:
var hour:Date = new Date(1969, 11, 31, 17);
return hour.valueOf();
case TimeUnit.MINUTES:
var minute:Date = new Date(1969, 11, 31, 16, 1);
return minute.valueOf();
case TimeUnit.SECONDS:
var second:Date = new Date(1969, 11, 31, 16, 0, 1);
return second.valueOf();
default: //millisecond
return 1;
}
}
/**
* @private
* Using the major time unit, and the current minimum and maximum, generate
* the ideal minimum and maximum.
*/
private function calculateMaximumAndMinimum():void
{
switch(this.majorTimeUnit)
{
case TimeUnit.YEAR:
{
if(!this._minimumSetByUser)
{
this._minimum = new Date(this._minimum.fullYear, 0);
}
if(!this._maximumSetByUser)
{
var beginningOfYear:Date = new Date(this._maximum.fullYear, 0);
//don't change the maximum if it is the exact beginning of the year
if(beginningOfYear.valueOf() != this._maximum.valueOf())
this._maximum = new Date(this._maximum.fullYear + 1, 0);
}
break;
}
case TimeUnit.MONTH:
{
if(!this._minimumSetByUser)
this._minimum = new Date(this._minimum.fullYear, this._minimum.month);
if(!this._maximumSetByUser)
{
var beginningOfMonth:Date = new Date(this._maximum.fullYear, this._maximum.month);
//don't change the maximum if it is the exact beginning of the month
if(beginningOfMonth.valueOf() != this._maximum.valueOf())
this._maximum = new Date(this._maximum.fullYear, this._maximum.month + 1);
}
break;
}
case TimeUnit.DAY:
{
if(!this._minimumSetByUser)
this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date);
if(!this._maximumSetByUser)
{
var beginningOfDay:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date);
//don't change the maximum if it is the exact beginning of the day
if(beginningOfDay.valueOf() != this._maximum.valueOf())
this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date + 1);
}
break;
}
case TimeUnit.HOURS:
{
if(!this._minimumSetByUser)
this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date, this._minimum.hours);
if(!this._maximumSetByUser)
{
var beginningOfHour:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours);
//don't change the maximum if it is the exact beginning of the day
if(beginningOfHour.valueOf() != this._maximum.valueOf())
this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours + 1);
}
break;
}
case TimeUnit.MINUTES:
{
if(!this._minimumSetByUser)
this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date, this._minimum.hours, this._minimum.minutes);
if(!this._maximumSetByUser)
{
var beginningOfMinute:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes);
//don't change the maximum if it is the exact beginning of the day
if(beginningOfMinute.valueOf() != this._maximum.valueOf())
this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes + 1);
}
break;
}
case TimeUnit.SECONDS:
{
if(!this._minimumSetByUser)
this._minimum = new Date(this._minimum.fullYear, this._minimum.month, this._minimum.date, this._minimum.hours, this._minimum.minutes, this._minimum.seconds);
if(!this._maximumSetByUser)
{
var beginningOfSecond:Date = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes, this._maximum.seconds);
//don't change the maximum if it is the exact beginning of the day
if(beginningOfSecond.valueOf() != this._maximum.valueOf())
this._maximum = new Date(this._maximum.fullYear, this._maximum.month, this._maximum.date, this._maximum.hours, this._maximum.minutes, this._maximum.seconds + 1);
}
break;
}
}
}
/**
* @private
* Calculates the multiplier used to convert a data point to an actual position
* on the axis.
*/
private function calculatePositionMultiplier():void
{
var range:Number = this.maximum.valueOf() - this.minimum.valueOf();
if(range == 0)
{
this.positionMultiplier = 0;
return;
}
this.positionMultiplier = this.renderer.length / range;
}
/**
* @private
*/
override protected function parseDataProvider():void
{
var seriesCount:int = this.dataProvider.length;
var min:Number = NaN;
var max:Number = NaN;
for(var i:int = 0; i < seriesCount; i++)
{
var series:ISeries = ISeries(this.dataProvider[i]);
var seriesLength:int = series.length;
for(var j:int = 0; j < seriesLength; j++)
{
var item:Object = series.dataProvider[j];
var value:Object = this.chart.itemToAxisValue(series, j, this);
var numericValue:Number = this.valueToNumber(value);
if(isNaN(min))
{
min = numericValue;
}
else
{
min = Math.min(min, numericValue);
}
if(isNaN(max))
{
max = numericValue;
}
else
{
max = Math.max(max, numericValue);
}
}
}
//bad data. show yesterday through today.
if(isNaN(min) || isNaN(max))
{
var today:Date = new Date();
max = today.valueOf();
today.setDate(today.getDate() - 1);
min = today.valueOf();
}
this._dataMinimum = new Date(min);
this._dataMaximum = new Date(max);
}
}
}