1////////////////////////////////////////////////////////////////////////////////
2//
3//  ADOBE SYSTEMS INCORPORATED
4//  Copyright 2009 Adobe Systems Incorporated
5//  All Rights Reserved.
6//
7//  NOTICE: Adobe permits you to use, modify, and distribute this file
8//  in accordance with the terms of the license agreement accompanying it.
9//
10////////////////////////////////////////////////////////////////////////////////
11
12package mx.charts
13{
14
15import flash.events.Event;
16import mx.charts.chartClasses.AxisLabelSet;
17import mx.charts.chartClasses.NumericAxis;
18import mx.core.mx_internal;
19
20use namespace mx_internal;
21
22/**
23 *  The LinearAxis class maps numeric values evenly
24 *  between a minimum and maximum value along a chart axis.
25 *  By default, it determines <code>minimum</code>, <code>maximum</code>,
26 *  and <code>interval</code> values from the charting data
27 *  to fit all of the chart elements on the screen.
28 *  You can also explicitly set specific values for these properties.
29 *
30 *  <p>The auto-determination of range values works as follows:
31 *
32 *  <ol>
33 *    <li> Flex determines a minimum and maximum value
34 *    that accomodates all the data being displayed in the chart.</li>
35 *    <li> If the <code>autoAdjust</code> and <code>baseAtZero</code> properties
36 *    are set to <code>true</code>, Flex makes the following adjustments:
37 *      <ul>
38 *        <li>If all values are positive,
39 *        Flex sets the <code>minimum</code> property to zero.</li>
40 *  	  <li>If all values are negative,
41 *        Flex sets the <code>maximum</code> property to zero.</li>
42 *  	</ul>
43 *    </li>
44 *    <li> If the <code>autoAdjust</code> property is set to <code>true</code>,
45 *    Flex adjusts values of the <code>minimum</code> and <code>maximum</code>
46 *    properties by rounding them up or down.</li>
47 *    <li> Flex checks if any of the elements displayed in the chart
48 *    require extra padding to display properly (for example, for labels).
49 *    It adjusts the values of the <code>minimum</code> and
50 *    <code>maximum</code> properties accordingly.</li>
51 *    <li> Flex determines if you have explicitly specified any padding
52 *    around the <code>minimum</code> and <code>maximum</code> values,
53 *    and adjusts their values accordingly.</li>
54 *  </ol>
55 *  </p>
56 *
57 *  @mxml
58 *
59 *  <p>The <code>&lt;mx:LinearAxis&gt;</code> tag inherits all the properties
60 *  of its parent classes and adds the following properties:</p>
61 *
62 *  <pre>
63 *  &lt;mx:LinearAxis
64 *    <strong>Properties</strong>
65 *    interval="null"
66 *    maximum="null"
67 *    maximumLabelPrecision="null"
68 *    minimum="null"
69 *    minorInterval="null"
70 *  /&gt;
71 *  </pre>
72 *
73 *  @see mx.charts.chartClasses.IAxis
74 *
75 *  @includeExample examples/HLOCChartExample.mxml
76 *
77 *  @langversion 3.0
78 *  @playerversion Flash 9
79 *  @playerversion AIR 1.1
80 *  @productversion Flex 3
81 */
82public class LinearAxis extends NumericAxis
83{
84    include "../core/Version.as";
85
86	//--------------------------------------------------------------------------
87	//
88	//  Constructor
89	//
90	//--------------------------------------------------------------------------
91
92	/**
93	 *	Constructor.
94	 *
95	 *  @langversion 3.0
96	 *  @playerversion Flash 9
97	 *  @playerversion AIR 1.1
98	 *  @productversion Flex 3
99	 */
100	public function LinearAxis()
101	{
102		super();
103	}
104
105	//-------------------------------------------------------------------------
106	//
107	//   Variables
108	//
109	//-------------------------------------------------------------------------
110
111    private var _actualAssignedMaximum:Number;
112
113    private var _actualAssignedMinimum:Number;
114
115
116	//-----------------------------------------------------------------------
117	//
118	// Overridden properties
119	//
120	//-----------------------------------------------------------------------
121
122	//------------------------------------------
123	// direction
124	//------------------------------------------
125	[Inspectable(category="General", enumeration="normal,inverted", defaultValue="normal")]
126	/**
127	 *  @private
128	 */
129	override public function set direction(value:String):void
130	{
131		if(value == "inverted")
132		{
133			if(!(isNaN(_actualAssignedMaximum)))
134			{
135				computedMinimum = -_actualAssignedMaximum;
136				assignedMinimum = -_actualAssignedMaximum;
137			}
138			if(!(isNaN(_actualAssignedMinimum)))
139			{
140				computedMaximum = -_actualAssignedMinimum;
141				assignedMaximum = -_actualAssignedMinimum;
142			}
143		}
144		else
145		{
146			if(!(isNaN(_actualAssignedMaximum)))
147			{
148				computedMaximum = _actualAssignedMaximum;
149				assignedMaximum = _actualAssignedMaximum;
150			}
151			if(!(isNaN(_actualAssignedMinimum)))
152			{
153				computedMinimum = _actualAssignedMinimum;
154				assignedMinimum = _actualAssignedMinimum;
155			}
156		}
157		super.direction = value;
158	}
159
160	//--------------------------------------------------------------------------
161	//
162	//  Properties
163	//
164	//--------------------------------------------------------------------------
165
166	//--------------------------------------------
167	// alignLabelsToInterval
168	//--------------------------------------------
169
170	/**
171	 *  @private
172	 *  Storage for alignLabelsToInterval property
173	 */
174	private var _alignLabelsToInterval:Boolean = true;
175
176
177    /**
178	 * @private
179	 */
180	public function get alignLabelsToInterval():Boolean
181	{
182		return _alignLabelsToInterval;
183	}
184	/**
185	* @private
186	*/
187	public function set alignLabelsToInterval(value:Boolean):void
188	{
189		if (value != _alignLabelsToInterval)
190		{
191			_alignLabelsToInterval = value;
192			invalidateCache();
193			dispatchEvent(new Event("mappingChange"));
194			dispatchEvent(new Event("axisChange"));
195		}
196	}
197
198
199	//----------------------------------
200	//  interval
201	//----------------------------------
202
203	/**
204	 *  @private
205	 */
206	private var _userInterval:Number;
207
208	[Inspectable(category="General")]
209
210	/**
211	 *  Specifies the numeric difference between label values along the axis.
212	 *  Flex calculates the interval if this property
213	 *  is set to <code>NaN</code>.
214	 *  The default value is <code>NaN</code>.
215	 *
216	 *  @langversion 3.0
217	 *  @playerversion Flash 9
218	 *  @playerversion AIR 1.1
219	 *  @productversion Flex 3
220	 */
221	public function get interval():Number
222	{
223		return computedInterval;
224	}
225
226	/**
227	 *  @private
228	 */
229	public function set interval(value:Number):void
230	{
231		if (value <= 0)
232			value = NaN;
233
234		_userInterval = value;
235
236		computedInterval = value;
237		invalidateCache();
238
239		dispatchEvent(new Event("axisChange"));
240	}
241
242	//----------------------------------
243	//  maximum
244	//----------------------------------
245
246    [Inspectable(category="General")]
247
248	/**
249	 *  Specifies the maximum value for an axis label.
250	 *  If you set the <code>autoAdjust</code> property to <code>true</code>,
251	 *  Flex calculates this value.
252	 *  If <code>NaN</code>, Flex determines the maximum value
253	 *  from the data in the chart.
254	 *  The default value is <code>NaN</code>.
255	 *
256	 *  @langversion 3.0
257	 *  @playerversion Flash 9
258	 *  @playerversion AIR 1.1
259	 *  @productversion Flex 3
260	 */
261	public function get maximum():Number
262	{
263		if(direction == "inverted")
264			return -computedMinimum;
265		else
266			return computedMaximum;
267	}
268
269	/**
270	 *  @private
271	 */
272	public function set maximum(value:Number):void
273	{
274		if(direction == "inverted")
275		{
276			assignedMinimum = -value;
277			computedMinimum = -value;
278		}
279		else
280		{
281			assignedMaximum = value;
282			computedMaximum = value;
283		}
284		_actualAssignedMaximum = value;
285		invalidateCache();
286
287		dispatchEvent(new Event("mappingChange"));
288		dispatchEvent(new Event("axisChange"));
289	}
290
291	//----------------------------------
292	//  maximumLabelPrecision
293	//----------------------------------
294
295	/**
296	 *  @private
297	 *  Storage for maximumLabelPrecision property
298	 */
299	private var _maximumLabelPrecision:Number;
300
301	/**
302	 *  @private
303	 */
304	public function get maximumLabelPrecision():Number
305	{
306		return _maximumLabelPrecision;
307	}
308
309	/**
310	 *  Specifies the maximum number of decimal places for representing fractional values on the labels
311	 *  generated by this axis. By default, the axis autogenerates this value from the labels themselves.
312	 *  A value of 0 rounds to the nearest integer value, while a value of 2 rounds to the nearest 1/100th
313	 *  of a value.
314	 *
315	 *  @langversion 3.0
316	 *  @playerversion Flash 9
317	 *  @playerversion AIR 1.1
318	 *  @productversion Flex 3
319	 */
320	public function set maximumLabelPrecision(value:Number):void
321	{
322		_maximumLabelPrecision = value;
323
324		invalidateCache();
325	}
326
327	//----------------------------------
328	//  minimum
329	//----------------------------------
330
331    [Inspectable(category="General")]
332
333	/**
334	 *  Specifies the minimum value for an axis label.
335	 *  If <code>NaN</code>, Flex determines the minimum value
336	 *  from the data in the chart.
337	 *  The default value is <code>NaN</code>.
338	 *
339	 *  @langversion 3.0
340	 *  @playerversion Flash 9
341	 *  @playerversion AIR 1.1
342	 *  @productversion Flex 3
343	 */
344	public function get minimum():Number
345	{
346		if(direction == "inverted")
347			return -computedMaximum;
348		else
349			return computedMinimum;
350	}
351
352	/**
353	 *  @private
354	 */
355	public function set minimum(value:Number):void
356	{
357		if(direction == "inverted")
358		{
359			assignedMaximum = -value;
360			computedMaximum = -value;
361		}
362		else
363		{
364			assignedMinimum = value;
365			computedMinimum = value;
366		}
367		_actualAssignedMinimum = value;
368		invalidateCache();
369
370		dispatchEvent(new Event("mappingChange"));
371		dispatchEvent(new Event("axisChange"));
372
373	}
374
375	//----------------------------------
376	//  minorInterval
377	//----------------------------------
378
379	/**
380	 *  @private
381	 *  Storage for the minorInterval property.
382	 */
383	private var _minorInterval:Number;
384
385	/**
386	 *  @private
387	 */
388	private var _userMinorInterval:Number;
389
390    [Inspectable(category="General")]
391
392	/**
393	 *  Specifies the numeric difference between minor tick marks along the axis.
394	 *  Flex calculates the difference if this property
395	 *  is set to <code>NaN</code>.
396	 *
397	 *  @langversion 3.0
398	 *  @playerversion Flash 9
399	 *  @playerversion AIR 1.1
400	 *  @productversion Flex 3
401	 */
402	public function get minorInterval():Number
403	{
404		return _minorInterval;
405	}
406
407	/**
408	 *  @private
409	 */
410	public function set minorInterval(value:Number):void
411	{
412		if (value <= 0)
413			value = NaN;
414
415		_userMinorInterval = value;
416		_minorInterval = value;
417
418		invalidateCache();
419
420		dispatchEvent(new Event("axisChange"));
421	}
422
423	//--------------------------------------------------------------------------
424	//
425	//  Overridden methods: NumericAxis
426	//
427	//--------------------------------------------------------------------------
428
429	/**
430	 *  @private
431	 */
432	override protected function buildLabelCache():Boolean
433	{
434		if (labelCache)
435			return false;
436
437		labelCache = [];
438
439		var r:Number = computedMaximum - computedMinimum;
440
441		var labelBase:Number = labelMinimum -
442			Math.floor((labelMinimum - computedMinimum) / computedInterval) *
443			computedInterval;
444
445		if (_alignLabelsToInterval)
446			labelBase = Math.ceil(labelBase / computedInterval) * computedInterval;
447
448		var labelTop:Number = computedMaximum;
449
450		var i:Number;
451
452		var precision:Number = _maximumLabelPrecision;
453		if (isNaN(precision))
454		{
455			var decimal:Number = Math.abs(computedInterval) -
456								 Math.floor(Math.abs(computedInterval));
457
458			precision =
459				decimal == 0 ? 1 : -Math.floor(Math.log(decimal) / Math.LN10);
460
461			decimal = Math.abs(computedMinimum) -
462					  Math.floor(Math.abs(computedMinimum));
463
464			precision = Math.max(precision,
465				decimal == 0 ? 1: -Math.floor(Math.log(decimal) / Math.LN10));
466		}
467		var roundBase:Number = Math.pow(10, precision);
468
469		var labelFunction:Function = this.labelFunction;
470
471		var roundedValue:Number;
472
473		if (labelFunction != null)
474		{
475			var previousValue:Number = NaN;
476			for (i = labelBase; i <= labelTop; i += computedInterval)
477			{
478				roundedValue = Math.round(i * roundBase) / roundBase;
479				if(direction == "inverted")
480					roundedValue = -roundedValue;
481
482				labelCache.push(new AxisLabel(
483					(i - computedMinimum) / r, i,
484					labelFunction(roundedValue, previousValue, this)));
485
486				previousValue = roundedValue;
487			}
488		}
489		else
490		{
491			for (i = labelBase; i <= labelTop; i += computedInterval)
492			{
493				roundedValue = Math.round(i * roundBase) / roundBase;
494				if(direction == "inverted")
495					roundedValue = -roundedValue;
496				labelCache.push(new AxisLabel(
497					(i - computedMinimum) / r, i, roundedValue.toString()));
498			}
499		}
500
501		return true;
502	}
503
504	/**
505	 *  @private
506	 */
507	override public function reduceLabels(intervalStart:AxisLabel,
508										  intervalEnd:AxisLabel):AxisLabelSet
509	{
510		// What's this calculation?
511		// We're trying to figure out how many labels we need to skip.
512		// If we assume that every adjacent label is 1 computedInterval apart,
513		// then we can guess the ordinal distance between any two labels by
514		// dividing the difference in their values by computedInterval.
515		// So, what's with the round? Well, in theory, the distance
516		// between any two labels is an integral number of _intervals.
517		// but floating-point rounding errors, especially on small intervals,
518		// can throw us off by a little bit. So we add in a round()
519		// to get us back to a nice whole integer.
520		var intervalMultiplier:Number =
521			Math.round((Number(intervalEnd.value) -
522			Number(intervalStart.value)) / computedInterval) + 1;
523
524		var newMinorInterval:Number = intervalMultiplier * _minorInterval;
525
526		var labels:Array /* of AxisLabel */ = [];
527		var newMinorTicks:Array /* of Number */ = [];
528		var newTicks:Array /* of Number */ = [];
529
530		var n:int = labelCache.length;
531		for (var i:int = 0; i < n; i += intervalMultiplier)
532		{
533			labels.push(labelCache[i]);
534			newTicks.push(labelCache[i].position);
535		}
536
537		var r:Number = computedMaximum - computedMinimum;
538
539		var labelBase:Number = labelMinimum -
540			Math.floor((labelMinimum - computedMinimum)/newMinorInterval) *
541			newMinorInterval;
542
543		if (_alignLabelsToInterval)
544			labelBase = Math.ceil(labelBase / newMinorInterval) * newMinorInterval;
545
546		var labelTop:Number = computedMaximum + 0.000001
547
548		for (var j:Number = labelBase; j <= labelTop; j += newMinorInterval)
549		{
550			newMinorTicks.push((j - computedMinimum) / r);
551		}
552
553		var labelSet:AxisLabelSet = new AxisLabelSet();
554		labelSet.labels = labels;
555		labelSet.minorTicks = newMinorTicks;
556		labelSet.ticks = newTicks;
557		labelSet.accurate = true;
558		return labelSet;
559	}
560
561	/**
562	 *  @private
563	 */
564	override protected function buildMinorTickCache():Array /* of Number */
565	{
566		var cache:Array /* of Number */ = [];
567
568		var r:Number = computedMaximum - computedMinimum;
569
570		var labelBase:Number = labelMinimum -
571			Math.floor((labelMinimum - computedMinimum) / _minorInterval) *
572			_minorInterval;
573
574		if (_alignLabelsToInterval)
575			labelBase = Math.ceil(labelBase / _minorInterval) * _minorInterval;
576
577		var labelTop:Number = computedMaximum;
578
579		for (var i:Number = labelBase; i <= labelTop; i += _minorInterval)
580		{
581			cache.push((i - computedMinimum) / r);
582		}
583
584		return cache;
585	}
586
587	/**
588	 *  @private
589	 */
590	override protected function adjustMinMax(minValue:Number,
591											 maxValue:Number):void
592	{
593		var interval:Number = _userInterval;
594
595		if (autoAdjust == false &&
596			!isNaN(_userInterval) &&
597			!isNaN(_userMinorInterval))
598		{
599			return;
600		}
601
602		// New calculations to accomodate negative values.
603		// Find the nearest power of ten for y_userInterval
604		// for line-grid and labelling positions.
605		if (maxValue == 0 && minValue == 0)
606			maxValue = 100;
607		var maxPowerOfTen:Number =
608			Math.floor(Math.log(Math.abs(maxValue)) / Math.LN10);
609		var minPowerOfTen:Number =
610			Math.floor(Math.log(Math.abs(minValue)) / Math.LN10);
611		var powerOfTen:Number =
612			Math.floor(Math.log(Math.abs(maxValue - minValue)) / Math.LN10)
613
614		var y_userInterval:Number;
615
616		if (isNaN(_userInterval))
617		{
618			y_userInterval = Math.pow(10, powerOfTen);
619
620			if (Math.abs(maxValue - minValue) / y_userInterval < 4)
621			{
622				powerOfTen--;
623				y_userInterval = y_userInterval * 2 / 10;
624			}
625		}
626		else
627		{
628			y_userInterval = _userInterval;
629		}
630
631		// Bug 148745:
632		// Using % to decide if y_userInterval divides maxValue evenly
633		// is running into floating point errors.
634		// For example, 3 % .2 == .2.
635		// Multiplication and division don't seem to have the same problems,
636		// so instead we divide, round and multiply.
637		// If we get back to the same value, it means that either it fit evenly,
638		// or the difference was trivial enough to get rounded out
639		// by imprecision.
640
641		var y_topBound:Number =
642			Math.round(maxValue / y_userInterval) * y_userInterval == maxValue ?
643			maxValue :
644			(Math.floor(maxValue / y_userInterval) + 1) * y_userInterval;
645
646		var y_lowerBound:Number;
647
648		if (isFinite(minValue))
649			y_lowerBound = 0;
650
651		if (minValue < 0 || baseAtZero == false)
652		{
653			y_lowerBound =
654				Math.floor(minValue / y_userInterval) * y_userInterval;
655
656			if (maxValue < 0 && baseAtZero)
657				y_topBound = 0;
658		}
659		else
660		{
661			y_lowerBound = 0;
662		}
663
664		// OK, we've figured out our interval.
665		// If the caller wants us to lower it based on layout rules,
666		// we have more to do. Otherwise, return here.
667		// If the user didn't provide us with an interval,
668		// we'll use the one we just generated
669
670		if (isNaN(_userInterval))
671			computedInterval = y_userInterval;
672
673		if (isNaN(_userMinorInterval))
674			_minorInterval = computedInterval / 2;
675
676		// If the user wanted to us to autoadjust the min/max
677		// to nice clean values, record the ones we just caluclated.
678		// If the user has provided us with specific min/max values,
679		// we won't blow that away here.
680		if (autoAdjust)
681		{
682			if (isNaN(assignedMinimum))
683				computedMinimum = y_lowerBound;
684
685			if (isNaN(assignedMaximum))
686				computedMaximum = y_topBound;
687		}
688	}
689}
690
691}
692