PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/charts/as/com/yahoo/astra/fl/charts/series/PieSeries.as

https://github.com/sdesai/yui2
ActionScript | 767 lines | 467 code | 82 blank | 218 comment | 53 complexity | 8f5684f8ea90444529511cfc9bdef505 MD5 | raw file
  1. package com.yahoo.astra.fl.charts.series
  2. {
  3. import com.yahoo.astra.animation.Animation;
  4. import com.yahoo.astra.animation.AnimationEvent;
  5. import com.yahoo.astra.fl.charts.PieChart;
  6. import com.yahoo.astra.fl.charts.legend.LegendItemData;
  7. import com.yahoo.astra.fl.charts.skins.RectangleSkin;
  8. import com.yahoo.astra.utils.GeomUtil;
  9. import com.yahoo.astra.utils.GraphicsUtil;
  10. import com.yahoo.astra.utils.NumberUtil;
  11. import fl.core.InvalidationType;
  12. import fl.core.UIComponent;
  13. import flash.display.Shape;
  14. import flash.display.Sprite;
  15. import flash.events.Event;
  16. import flash.geom.Point;
  17. import flash.text.TextField;
  18. import flash.text.TextFieldAutoSize;
  19. import flash.text.TextFormat;
  20. import flash.text.TextFormatAlign;
  21. //--------------------------------------
  22. // Styles
  23. //--------------------------------------
  24. /**
  25. * The colors of the markers in this series.
  26. *
  27. * @default [0x729fcf, 0xfcaf3e, 0x73d216, 0xfce94f, 0xad7fa8, 0x3465a4]
  28. */
  29. [Style(name="fillColors", type="Array")]
  30. /**
  31. * If true, a label is displayed on each marker. The label text is created
  32. * with the labelFunction property of the series. The default label function
  33. * sets the label to the percentage value of the item.
  34. *
  35. * @default false
  36. * @see PieSeries#labelFunction
  37. */
  38. [Style(name="showLabels", type="Boolean")]
  39. /**
  40. * If true, marker labels that overlap previously-created labels will be
  41. * hidden to improve readability.
  42. *
  43. * @default true
  44. */
  45. [Style(name="hideOverlappingLabels", type="Boolean")]
  46. /**
  47. * Renders data points as a series of pie-like wedges.
  48. *
  49. * @author Josh Tynjala
  50. */
  51. public class PieSeries extends Series implements ICategorySeries
  52. {
  53. //--------------------------------------
  54. // Class Variables
  55. //--------------------------------------
  56. /**
  57. * @private
  58. */
  59. private static var defaultStyles:Object =
  60. {
  61. fillColors:
  62. [
  63. 0x00b8bf, 0x8dd5e7, 0xc0fff6, 0xffa928, 0xedff9f, 0xd00050,
  64. 0xc6c6c6, 0xc3eafb, 0xfcffad, 0xcfff83, 0x444444, 0x4d95dd,
  65. 0xb8ebff, 0x60558f, 0x737d7e, 0xa64d9a, 0x8e9a9b, 0x803e77
  66. ],
  67. borderColors:
  68. [
  69. 0x00b8bf, 0x8dd5e7, 0xc0fff6, 0xffa928, 0xedff9f, 0xd00050,
  70. 0xc6c6c6, 0xc3eafb, 0xfcffad, 0xcfff83, 0x444444, 0x4d95dd,
  71. 0xb8ebff, 0x60558f, 0x737d7e, 0xa64d9a, 0x8e9a9b, 0x803e77
  72. ],
  73. fillAlphas: [1.0],
  74. borderAlphas: [0.0],
  75. markerSkins: [RectangleSkin],
  76. showLabels: false,
  77. hideOverlappingLabels: true
  78. //see textFormat default style defined in constructor below
  79. //works around stylemanager global style bug!
  80. };
  81. /**
  82. * @private
  83. */
  84. private static const RENDERER_STYLES:Object =
  85. {
  86. fillColor: "fillColors",
  87. fillAlpha: "fillAlphas",
  88. borderColor: "borderColors",
  89. borderAlpha: "borderAlphas",
  90. skin: "markerSkins"
  91. };
  92. //--------------------------------------
  93. // Class Methods
  94. //--------------------------------------
  95. /**
  96. * @private
  97. * @copy fl.core.UIComponent#getStyleDefinition()
  98. */
  99. public static function getStyleDefinition():Object
  100. {
  101. return mergeStyles(defaultStyles, Series.getStyleDefinition());
  102. }
  103. //--------------------------------------
  104. // Constructor
  105. //--------------------------------------
  106. /**
  107. * Constructor.
  108. */
  109. public function PieSeries(data:Object = null)
  110. {
  111. super(data);
  112. //we have to set this as an instance style because textFormat is
  113. //defined as a global style in StyleManager, and that takes
  114. //precedence over shared/class styles
  115. this.setStyle("textFormat", new TextFormat("_sans", 11, 0x000000, true, false, false, "", "", TextFormatAlign.LEFT, 0, 0, 0, 0));
  116. }
  117. //--------------------------------------
  118. // Properties
  119. //--------------------------------------
  120. /**
  121. * @private
  122. * The text fields used to display labels over each marker.
  123. */
  124. protected var labels:Array = [];
  125. /**
  126. * @private
  127. * Holds the labels created by the previous redraw so that they can
  128. * be reused.
  129. */
  130. protected var labelsCache:Array;
  131. /**
  132. * @private
  133. * Storage for the masks that define the shapes of the markers.
  134. */
  135. protected var markerMasks:Array = [];
  136. /**
  137. * @private
  138. * The Animation instance that controls animation in this series.
  139. */
  140. private var _animation:Animation;
  141. /**
  142. * @private
  143. */
  144. private var _previousData:Array = [];
  145. /**
  146. * @private
  147. * Storage for the dataField property.
  148. */
  149. private var _dataField:String;
  150. /**
  151. * The field used to access data for this series.
  152. */
  153. public function get dataField():String
  154. {
  155. return this._dataField;
  156. }
  157. /**
  158. * @private
  159. */
  160. public function set dataField(value:String):void
  161. {
  162. if(this._dataField != value)
  163. {
  164. this._dataField = value;
  165. this.dispatchEvent(new Event("dataChange"));
  166. this.invalidate(InvalidationType.DATA);
  167. }
  168. }
  169. /**
  170. * @private
  171. * Storage for the categoryField property.
  172. */
  173. private var _categoryField:String;
  174. /**
  175. * @copy com.yahoo.astra.fl.charts.series.ICategorySeries#categoryField
  176. */
  177. public function get categoryField():String
  178. {
  179. return this._categoryField;
  180. }
  181. /**
  182. * @private
  183. */
  184. public function set categoryField(value:String):void
  185. {
  186. if(this._categoryField != value)
  187. {
  188. this._categoryField = value;
  189. this.dispatchEvent(new Event("dataChange"));
  190. this.invalidate(InvalidationType.DATA);
  191. }
  192. }
  193. /**
  194. * @private
  195. * Storage for the categoryNames property.
  196. */
  197. private var _categoryNames:Array;
  198. /**
  199. * @copy com.yahoo.astra.fl.charts.series.ICategorySeries#categoryNames
  200. */
  201. public function get categoryNames():Array
  202. {
  203. return this._categoryNames;
  204. }
  205. /**
  206. * @private
  207. */
  208. public function set categoryNames(value:Array):void
  209. {
  210. this._categoryNames = value;
  211. }
  212. /**
  213. * @private
  214. * Storage for the labelFunction property.
  215. */
  216. private var _labelFunction:Function = defaultLabelFunction;
  217. /**
  218. * A function may be set to determine the text value of the labels.
  219. *
  220. * <pre>function labelFunction(item:Object):String</pre>
  221. */
  222. public function get labelFunction():Function
  223. {
  224. return this._labelFunction;
  225. }
  226. /**
  227. * @private
  228. */
  229. public function set labelFunction(value:Function):void
  230. {
  231. this._labelFunction = value;
  232. this.invalidate();
  233. }
  234. //--------------------------------------
  235. // Public Methods
  236. //--------------------------------------
  237. /**
  238. * @inheritDoc
  239. */
  240. override public function clone():ISeries
  241. {
  242. var series:PieSeries = new PieSeries();
  243. if(this.dataProvider is Array)
  244. {
  245. //copy the array rather than pass it by reference
  246. series.dataProvider = (this.dataProvider as Array).concat();
  247. }
  248. else if(this.dataProvider is XMLList)
  249. {
  250. series.dataProvider = (this.dataProvider as XMLList).copy();
  251. }
  252. series.displayName = this.displayName;
  253. return series;
  254. }
  255. /**
  256. * Converts an item to it's value.
  257. */
  258. public function itemToData(item:Object):Number
  259. {
  260. var primaryDataField:String = PieChart(this.chart).seriesToDataField(this);
  261. if(primaryDataField)
  262. {
  263. return Number(item[primaryDataField]);
  264. }
  265. return Number(item);
  266. }
  267. /**
  268. * Converts an item to the category in which it is displayed.
  269. */
  270. public function itemToCategory(item:Object, index:int):String
  271. {
  272. var primaryCategoryField:String = PieChart(this.chart).seriesToCategoryField(this);
  273. if(primaryCategoryField)
  274. {
  275. return item[primaryCategoryField];
  276. }
  277. if(this._categoryNames && index >= 0 && index < this._categoryNames.length)
  278. {
  279. return this._categoryNames[index];
  280. }
  281. return index.toString();
  282. }
  283. /**
  284. * Converts an item's value to its percentage equivilent.
  285. */
  286. public function itemToPercentage(item:Object):Number
  287. {
  288. var totalValue:Number = this.calculateTotalValue();
  289. if(totalValue == 0)
  290. {
  291. return 0;
  292. }
  293. return 100 * (this.itemToData(item) / totalValue);
  294. }
  295. /**
  296. * @inheritDoc
  297. */
  298. public function createLegendItemData():Array
  299. {
  300. var items:Array = [];
  301. var markerSkins:Array = this.getStyleValue("markerSkins") as Array;
  302. var fillColors:Array = this.getStyleValue("fillColors") as Array;
  303. var legendItemCount:int = this.length;
  304. for(var i:int = 0; i < legendItemCount; i++)
  305. {
  306. var item:Object = this.dataProvider[i];
  307. var categoryName:String = this.itemToCategory(item, i);
  308. var markerSkin:Object = markerSkins[i % markerSkins.length]
  309. var fillColor:uint = fillColors[i % fillColors.length];
  310. var data:LegendItemData = new LegendItemData(categoryName, markerSkin, fillColor, 1, fillColor, 1);
  311. items.push(data);
  312. }
  313. return items;
  314. }
  315. //--------------------------------------
  316. // Protected Methods
  317. //--------------------------------------
  318. /**
  319. * @private
  320. */
  321. override protected function draw():void
  322. {
  323. var stylesInvalid:Boolean = this.isInvalid(InvalidationType.STYLES);
  324. var sizeInvalid:Boolean = this.isInvalid(InvalidationType.SIZE);
  325. var dataInvalid:Boolean = this.isInvalid(InvalidationType.DATA);
  326. super.draw();
  327. this.drawMarkers(stylesInvalid, sizeInvalid);
  328. var showLabels:Boolean = this.getStyleValue("showLabels") as Boolean;
  329. this.createLabelCache();
  330. if(showLabels)
  331. {
  332. this.drawLabels();
  333. }
  334. this.clearLabelCache();
  335. this.beginAnimation();
  336. }
  337. /**
  338. * @private
  339. * All markers are removed from the display list.
  340. */
  341. override protected function removeAllMarkers():void
  342. {
  343. super.removeAllMarkers();
  344. var markerCount:int = this.markerMasks.length;
  345. for(var i:int = 0; i < markerCount; i++)
  346. {
  347. var mask:Shape = this.markerMasks.pop() as Shape;
  348. this.removeChild(mask);
  349. }
  350. }
  351. /**
  352. * @private
  353. * Add or remove markers as needed. current markers will be reused.
  354. */
  355. override protected function refreshMarkers():void
  356. {
  357. super.refreshMarkers();
  358. var itemCount:int = this.length;
  359. var difference:int = itemCount - this.markerMasks.length;
  360. if(difference > 0)
  361. {
  362. for(var i:int = 0; i < difference; i++)
  363. {
  364. var mask:Shape = new Shape();
  365. this.addChild(mask);
  366. this.markerMasks.push(mask);
  367. var marker:Sprite = this.markers[i + (itemCount-difference)] as Sprite;
  368. marker.mask = mask;
  369. marker.width = this.width;
  370. marker.height = this.height;
  371. }
  372. }
  373. else if(difference < 0)
  374. {
  375. difference = Math.abs(difference);
  376. for(i = 0; i < difference; i++)
  377. {
  378. mask = this.markerMasks.pop() as Shape;
  379. this.removeChild(mask);
  380. }
  381. }
  382. }
  383. /**
  384. * @private
  385. * The default function called to initialize the text on the marker
  386. * labels.
  387. */
  388. protected function defaultLabelFunction(item:Object):String
  389. {
  390. var percentage:Number = this.itemToPercentage(item);
  391. return (percentage < 0.01 ? "< 0.01" : NumberUtil.roundToNearest(percentage, 0.01)) + "%";
  392. }
  393. /**
  394. * @private
  395. * Draws the markers in this series.
  396. */
  397. protected function drawMarkers(stylesInvalid:Boolean, sizeInvalid:Boolean):void
  398. {
  399. var markerCount:int = this.markers.length;
  400. for(var i:int = 0; i < markerCount; i++)
  401. {
  402. var marker:UIComponent = UIComponent(this.markers[i]);
  403. if(stylesInvalid)
  404. {
  405. this.copyStylesToRenderer(ISeriesItemRenderer(marker), RENDERER_STYLES);
  406. }
  407. if(sizeInvalid)
  408. {
  409. marker.width = this.width;
  410. marker.height = this.height;
  411. }
  412. //not really required, but we should validate anyway.
  413. this.validateMarker(ISeriesItemRenderer(marker));
  414. }
  415. }
  416. /**
  417. * @private
  418. * Either creates a new label TextField or retrieves one from the
  419. * cache.
  420. */
  421. protected function getLabel():TextField
  422. {
  423. var label:TextField;
  424. if(this.labelsCache.length > 0)
  425. {
  426. label = TextField(this.labelsCache.shift());
  427. }
  428. else
  429. {
  430. label = new TextField();
  431. label.autoSize = TextFieldAutoSize.LEFT;
  432. label.selectable = false;
  433. label.mouseEnabled = false;
  434. this.addChild(label);
  435. }
  436. label.visible = true;
  437. return label;
  438. }
  439. /**
  440. * @private
  441. * Updates the label text and positions the labels.
  442. */
  443. protected function drawLabels():void
  444. {
  445. var textFormat:TextFormat = this.getStyleValue("textFormat") as TextFormat;
  446. var embedFonts:Boolean = this.getStyleValue("embedFonts") as Boolean;
  447. var hideOverlappingLabels:Boolean = this.getStyleValue("hideOverlappingLabels") as Boolean;
  448. var angle:Number = 0;
  449. var valueSum:Number = 0;
  450. var totalValue:Number = this.calculateTotalValue();
  451. var markerCount:int = this.markers.length;
  452. for(var i:int = 0; i < markerCount; i++)
  453. {
  454. var label:TextField = this.getLabel();
  455. this.labels.push(label);
  456. label.defaultTextFormat = textFormat;
  457. label.embedFonts = embedFonts;
  458. label.text = this.labelFunction(this.dataProvider[i]);
  459. var value:Number = this.itemToData(this.dataProvider[i]);
  460. if(totalValue == 0)
  461. {
  462. angle = 360 / this.length;
  463. }
  464. else
  465. {
  466. angle = 360 * ((valueSum + value / 2) / totalValue);
  467. }
  468. valueSum += value;
  469. var halfWidth:Number = this.width / 2;
  470. var halfHeight:Number = this.height / 2;
  471. var radius:Number = Math.min(halfWidth, halfHeight);
  472. var position:Point = Point.polar(2 * radius / 3, -GeomUtil.degreesToRadians(angle));
  473. label.x = halfWidth + position.x - label.width / 2;
  474. label.y = halfHeight + position.y - label.height / 2;
  475. if(hideOverlappingLabels)
  476. {
  477. for(var j:int = 0; j < i; j++)
  478. {
  479. var previousLabel:TextField = TextField(this.labels[j]);
  480. if(previousLabel.hitTestObject(label))
  481. {
  482. label.visible = false;
  483. }
  484. }
  485. }
  486. }
  487. }
  488. /**
  489. * Copies a styles from the series to a child through a style map.
  490. *
  491. * @see copyStylesToChild()
  492. */
  493. protected function copyStylesToRenderer(child:ISeriesItemRenderer, styleMap:Object):void
  494. {
  495. var index:int = this.markers.indexOf(child);
  496. var childComponent:UIComponent = child as UIComponent;
  497. for(var n:String in styleMap)
  498. {
  499. var styleValues:Array = this.getStyleValue(styleMap[n]) as Array;
  500. //if it doesn't exist, ignore it and go with the defaults for this series
  501. if(!styleValues) continue;
  502. childComponent.setStyle(n, styleValues[index % styleValues.length])
  503. }
  504. }
  505. //--------------------------------------
  506. // Private Methods
  507. //--------------------------------------
  508. /**
  509. * @private
  510. * Sets up the animation for the markers.
  511. */
  512. private function beginAnimation():void
  513. {
  514. var itemCount:int = this.length;
  515. if(!this._previousData || this._previousData.length != this.length)
  516. {
  517. this._previousData = [];
  518. for(var i:int = 0; i < itemCount; i++)
  519. {
  520. this._previousData.push(0);
  521. }
  522. }
  523. //handle animating all the markers in one fell swoop.
  524. if(this._animation)
  525. {
  526. if(this._animation.active)
  527. {
  528. this._animation.pause();
  529. }
  530. this._animation.removeEventListener(AnimationEvent.UPDATE, tweenUpdateHandler);
  531. this._animation.removeEventListener(AnimationEvent.PAUSE, tweenPauseHandler);
  532. this._animation.removeEventListener(AnimationEvent.COMPLETE, tweenCompleteHandler);
  533. this._animation = null;
  534. }
  535. var data:Array = this.dataProviderToArrayOfNumbers();
  536. //don't animate on livepreview!
  537. if(this.isLivePreview || !this.getStyleValue("animationEnabled"))
  538. {
  539. this.renderMarkerMasks(data);
  540. }
  541. else
  542. {
  543. var animationDuration:int = this.getStyleValue("animationDuration") as int;
  544. var animationEasingFunction:Function = this.getStyleValue("animationEasingFunction") as Function;
  545. this._animation = new Animation(animationDuration, this._previousData, data);
  546. this._animation.addEventListener(AnimationEvent.UPDATE, tweenUpdateHandler);
  547. this._animation.addEventListener(AnimationEvent.PAUSE, tweenPauseHandler);
  548. this._animation.addEventListener(AnimationEvent.COMPLETE, tweenCompleteHandler);
  549. this._animation.easingFunction = animationEasingFunction;
  550. this.renderMarkerMasks(this._previousData);
  551. }
  552. }
  553. /**
  554. * @private
  555. * Determines the total sum of all values in the data provider.
  556. */
  557. private function calculateTotalValue():Number
  558. {
  559. var totalValue:Number = 0;
  560. var itemCount:int = this.length;
  561. for(var i:int = 0; i < itemCount; i++)
  562. {
  563. var currentItem:Object = this.dataProvider[i];
  564. var value:Number = this.itemToData(currentItem);
  565. if(!isNaN(value))
  566. {
  567. totalValue += value;
  568. }
  569. }
  570. return totalValue;
  571. }
  572. /**
  573. * @private
  574. * Retreives all the numeric values from the data provider
  575. * and places them into an Array so that they may be used
  576. * in an animation.
  577. */
  578. private function dataProviderToArrayOfNumbers():Array
  579. {
  580. var output:Array = [];
  581. var itemCount:int = this.length;
  582. for(var i:int = 0; i < itemCount; i++)
  583. {
  584. var item:Object = this.dataProvider[i];
  585. var value:Number = 0;
  586. if(item != null)
  587. {
  588. value = this.itemToData(item);
  589. }
  590. output.push(value);
  591. }
  592. return output;
  593. }
  594. /**
  595. * @private
  596. */
  597. private function tweenUpdateHandler(event:AnimationEvent):void
  598. {
  599. this.renderMarkerMasks(event.parameters as Array);
  600. }
  601. /**
  602. * @private
  603. */
  604. private function tweenCompleteHandler(event:AnimationEvent):void
  605. {
  606. this.tweenUpdateHandler(event);
  607. this.tweenPauseHandler(event);
  608. }
  609. /**
  610. * @private
  611. */
  612. private function tweenPauseHandler(event:AnimationEvent):void
  613. {
  614. this._previousData = (event.parameters as Array).concat();
  615. }
  616. /**
  617. * @private
  618. */
  619. private function renderMarkerMasks(data:Array):void
  620. {
  621. var values:Array = [];
  622. var totalValue:Number = 0;
  623. var itemCount:int = data.length;
  624. for(var i:int = 0; i < itemCount; i++)
  625. {
  626. var value:Number = Number(data[i]);
  627. values.push(value);
  628. if(!isNaN(value))
  629. {
  630. totalValue += value;
  631. }
  632. }
  633. var totalAngle:Number = 0;
  634. var halfWidth:Number = this.width / 2;
  635. var halfHeight:Number = this.height / 2;
  636. var radius:Number = Math.min(halfWidth, halfHeight);
  637. var fillColors:Array = this.getStyleValue("fillColors") as Array;
  638. var angle:Number = 0;
  639. for(i = 0; i < itemCount; i++)
  640. {
  641. value = Number(data[i]);
  642. if(totalValue == 0)
  643. {
  644. angle = 360 / data.length;
  645. }
  646. else
  647. {
  648. angle = 360 * (value / totalValue);
  649. }
  650. var mask:Shape = this.markerMasks[i] as Shape;
  651. mask.graphics.clear();
  652. mask.graphics.beginFill(0xff0000);
  653. GraphicsUtil.drawWedge(mask.graphics, halfWidth, halfHeight, totalAngle, angle, radius);
  654. mask.graphics.endFill();
  655. totalAngle += angle;
  656. var marker:UIComponent = UIComponent(this.markers[i]);
  657. marker.drawNow();
  658. }
  659. }
  660. /**
  661. * @private
  662. * Places all the existing labels in a cache so that they may be reused
  663. * when we redraw the series.
  664. */
  665. private function createLabelCache():void
  666. {
  667. this.labelsCache = this.labels.concat();
  668. this.labels = [];
  669. }
  670. /**
  671. * @private
  672. * If any labels are left in the cache after we've redrawn, they can be
  673. * removed from the display list.
  674. */
  675. private function clearLabelCache():void
  676. {
  677. var cacheLength:int = this.labelsCache.length;
  678. for(var i:int = 0; i < cacheLength; i++)
  679. {
  680. var label:TextField = TextField(this.labelsCache.shift());
  681. this.removeChild(label);
  682. }
  683. }
  684. }
  685. }