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><mx:LinearAxis></code> tag inherits all the properties 60 * of its parent classes and adds the following properties:</p> 61 * 62 * <pre> 63 * <mx:LinearAxis 64 * <strong>Properties</strong> 65 * interval="null" 66 * maximum="null" 67 * maximumLabelPrecision="null" 68 * minimum="null" 69 * minorInterval="null" 70 * /> 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