PageRenderTime 2138ms CodeModel.GetById 55ms RepoModel.GetById 1ms app.codeStats 0ms

/flare/src/flare/vis/legend/Legend.as

http://github.com/prefuse/Flare
ActionScript | 571 lines | 319 code | 62 blank | 190 comment | 71 complexity | ffa959751d11cf1d2e810065d405883b MD5 | raw file
  1. package flare.vis.legend
  2. {
  3. import flare.animate.Transitioner;
  4. import flare.display.RectSprite;
  5. import flare.display.TextSprite;
  6. import flare.scale.Scale;
  7. import flare.scale.ScaleType;
  8. import flare.util.Displays;
  9. import flare.util.Orientation;
  10. import flare.util.palette.ColorPalette;
  11. import flare.util.palette.Palette;
  12. import flare.util.palette.ShapePalette;
  13. import flare.util.palette.SizePalette;
  14. import flare.vis.data.ScaleBinding;
  15. import flash.display.Sprite;
  16. import flash.geom.Rectangle;
  17. import flash.text.TextFormat;
  18. /**
  19. * A legend describing the visual encoding of a data property. Legends
  20. * support both discrete legends that list individual items and
  21. * range legends the convey a continuous range of values. Discrete
  22. * legends consist of a collection of <code>LegendItem</code> instances
  23. * stored in the <code>items</code> sprite. Range legends consist of
  24. * a single <code>LegendRange</code> instance stored in the
  25. * <code>items</code> sprite.
  26. *
  27. * <p>There are multiple ways to generate a legend. To build a legend
  28. * based on an existing visual encoding, use the static
  29. * <code>fromScale</code> constructor. This method takes a data scale
  30. * and one more or more palettes (e.g., color, shape, or size palettes)
  31. * and uses them to generate an appropriate legend. If the data scale
  32. * is a quantitative scale and only a color palette is provided, a
  33. * continuous range legend will be generated. Otherwise, a discrete
  34. * legend will be created.</p>
  35. *
  36. * <p>Legends can also be created from a collection of independent
  37. * values using the static <code>fromValues</code> constructor. This
  38. * method takes an array of legend item descriptions and uses them to
  39. * generate a legend. For example, consider this code:</p>
  40. *
  41. * <pre>
  42. * var legend:Legend = Legend.fromValues("Legend Title", [
  43. * {color: 0xff0000, shape:Shapes.X, label:"Red X"},
  44. * {color: 0x00ff00, shape:Shapes.SQUARE, label:"Green Square"},
  45. * {color: 0x0000ff, shape:Shapes.CIRCLE, label:"Blue Circle"}
  46. * ]);
  47. * </pre>
  48. *
  49. * <p>This example will create a legend with the described values.
  50. * See the documentation for the <code>buildFromValues</code> method
  51. * for more details.</p>
  52. */
  53. public class Legend extends Sprite
  54. {
  55. /** @private The layout bounds for this legend instance. */
  56. protected var _bounds:Rectangle = null;
  57. /** @private Sprite defining the border of the legend. */
  58. protected var _border:RectSprite;
  59. /** @private Sprite containing the legend items. */
  60. protected var _items:Sprite;
  61. /** @private TextSprite containing the legend title.*/
  62. protected var _title:TextSprite;
  63. /** @private Scale instance used to define the legend mapping. */
  64. protected var _scale:Scale;
  65. /** @private Flag for if this legend is discrete or continuous. */
  66. protected var _discrete:Boolean = true;
  67. /** @private The default color to use for legend items. */
  68. protected var _defaultColor:uint = 0xff888888;
  69. /** @private The color palette used to encode values (may be null). */
  70. protected var _colors:ColorPalette;
  71. /** @private The shape palette used to encode values (may be null). */
  72. protected var _shapes:ShapePalette;
  73. /** @private The size palette used to encode values (may be null). */
  74. protected var _sizes:SizePalette;
  75. /** @private Flag indicating the desired orientation of this legend. */
  76. protected var _orient:String = null;
  77. /** @private Margins within legend items. */
  78. protected var _margin:Number = 4;
  79. /** @private Spacing between legend items. */
  80. protected var _spacing:Number = 0;
  81. /** @private Base icon size */
  82. protected var _baseIconSize:Number = 12;
  83. /** @private TextFormat (font, size, style) of legend item labels. */
  84. protected var _labelTextFormat:TextFormat = new TextFormat("Arial",12,0);
  85. /** @private Label text mode. */
  86. protected var _labelTextMode:int = TextSprite.BITMAP;
  87. /** @private The calculated internal width of the legend. */
  88. protected var _iw:Number;
  89. /** @private The calculated internal height of the legend. */
  90. protected var _ih:Number;
  91. // -- Properties ------------------------------------------------------
  92. /** The layout bounds for this legend instance. */
  93. public function get bounds():Rectangle { return _bounds; }
  94. public function set bounds(b:Rectangle):void { _bounds = b; }
  95. /** Sprite defining the border of the legend. */
  96. public function get border():RectSprite { return _border; }
  97. /** Sprite containing the legend items. */
  98. public function get items():Sprite { return _items; }
  99. /** TextSprite containing the legend title.*/
  100. public function get title():TextSprite { return _title; }
  101. /** Flag indicating if this legend is discrete or continuous. */
  102. public function get discrete():Boolean { return _discrete; }
  103. /** Scale instance used to define the legend mapping. */
  104. public function get scale():Scale { return _scale; }
  105. public function set scale(s:Scale):void { _scale = s; }
  106. /** The legend range, if this legend is continuous. This
  107. * value is null if the legend is discrete. */
  108. public function get range():LegendRange {
  109. return _discrete ? null : LegendRange(_items.getChildAt(0));
  110. }
  111. /** The default color to use for legend items. */
  112. public function get defaultColor():uint { return _defaultColor; }
  113. public function set defaultColor(c:uint):void { _defaultColor = c; }
  114. /** The color palette used to encode values (may be null). */
  115. public function get colorPalette():ColorPalette { return _colors; }
  116. public function set colorPalette(cp:ColorPalette):void { _colors = cp; }
  117. /** The shape palette used to encode values (may be null). */
  118. public function get shapePalette():ShapePalette { return _shapes; }
  119. public function set shapePalette(sp:ShapePalette):void { _shapes = sp; }
  120. /** The size palette used to encode values (may be null). */
  121. public function get sizePalette():SizePalette { return _sizes; }
  122. public function set sizePalette(sp:SizePalette):void { _sizes = sp; }
  123. /** The desired orientation of this legend. */
  124. public function get orientation():String { return _orient; }
  125. public function set orientation(o:String):void { _orient = o; }
  126. /** Margins within legend items. */
  127. public function get margin():Number { return _margin; }
  128. public function set margin(m:Number):void { _margin = m; }
  129. /** Spacing between legend items. */
  130. public function get spacing():Number { return _spacing; }
  131. public function set spacing(s:Number):void { _spacing = s; }
  132. /** Base icon size, corresponding to a size factor of 1. */
  133. public function get baseIconSize():Number { return _baseIconSize; }
  134. public function set baseIconSize(s:Number):void {
  135. if (_baseIconSize != s && _discrete) {
  136. for (var i:uint=0; i<_items.numChildren; ++i) {
  137. var li:LegendItem = LegendItem(_items.getChildAt(i));
  138. li.iconSize *= (s / _baseIconSize);
  139. li.maxIconSize *= (s / _baseIconSize);
  140. }
  141. }
  142. _baseIconSize = s;
  143. }
  144. /** TextFormat (font, size, style) of legend item labels. */
  145. public function get labelTextFormat():TextFormat { return _labelTextFormat; }
  146. public function set labelTextFormat(f:TextFormat):void {
  147. _labelTextFormat = f; updateItems();
  148. }
  149. /** Label text mode. */
  150. public function get labelTextMode():int { return _labelTextMode; }
  151. public function set labelTextMode(mode:int):void {
  152. _labelTextMode = mode; updateItems();
  153. }
  154. // -- Initialization --------------------------------------------------
  155. /**
  156. * Creates a new Legend for the given data field.
  157. * @param dataField the data field to describe with the legend
  158. * @param vis the visualization corresponding to this legend
  159. * @param scale the scale value used to map the data field to visual
  160. * variables
  161. */
  162. public function Legend(title:String, scale:Scale=null,
  163. colors:ColorPalette=null, shapes:ShapePalette=null,
  164. sizes:SizePalette=null)
  165. {
  166. this.scale = scale;
  167. addChild(_border = new RectSprite(0,0,0,0,13,13));
  168. addChild(_title = new TextSprite());
  169. addChild(_items = new Sprite());
  170. _border.lineColor = 0;
  171. _border.fillColor = 0;
  172. _colors = colors;
  173. _shapes = shapes;
  174. _sizes = sizes;
  175. _title.textField.defaultTextFormat =
  176. new TextFormat("Arial",12,null,true);
  177. if (title != null)
  178. _title.text = title;
  179. else
  180. _title.visible = false;
  181. if (scale != null) {
  182. buildFromScale();
  183. update();
  184. }
  185. }
  186. /**
  187. * Update the legend, recomputing layout of items.
  188. * @param t a transitioner for value updates
  189. * @return the input transitioner
  190. */
  191. public function update(t:Transitioner=null):Transitioner
  192. {
  193. if (_scale is ScaleBinding && ScaleBinding(_scale).updateBinding())
  194. buildFromScale();
  195. updateItems();
  196. layout(t);
  197. return t;
  198. }
  199. /**
  200. * Builds the contents of this legend from the current scale values.
  201. * This method will remove all items from the legend and rebuild the
  202. * legend using the current scale and palette settings.
  203. */
  204. public function buildFromScale():void
  205. {
  206. // first, remove all items
  207. while (_items.numChildren > 0) {
  208. _items.removeChildAt(_items.numChildren-1);
  209. }
  210. // determine legend type
  211. var type:String = _scale.scaleType;
  212. if (ScaleType.isQuantitative(type) && !_sizes && !_shapes) {
  213. // build continuous legend
  214. _discrete = false;
  215. if (!_orient) _orient = Orientation.LEFT_TO_RIGHT;
  216. _items.addChild(new LegendRange(_title.text,
  217. _scale, _colors, _orient));
  218. } else {
  219. // build discrete legend
  220. _discrete = true;
  221. if (!_orient) _orient = Orientation.TOP_TO_BOTTOM;
  222. var numVals:int = ScaleType.isQuantitative(type) ? 5 : -1;
  223. var maxSize:Number = Number.MIN_VALUE;
  224. var vals:Array = _scale.values(numVals);
  225. for (var i:uint=0; i<vals.length; ++i) {
  226. // determine legend item properties
  227. var f:Number = _scale.interpolate(vals[i]);
  228. var color:uint = _defaultColor;
  229. if (_colors && ScaleType.isOrdinal(type)) {
  230. color = _colors.getColorByIndex(i);
  231. } else if (_colors) {
  232. color = _colors.getColor(f);
  233. }
  234. var shape:String = _shapes ? _shapes.getShape(i) : null;
  235. var size:Number = _baseIconSize*(_sizes?_sizes.getSize(f):1);
  236. if (size > maxSize) maxSize = size;
  237. var item:LegendItem = new LegendItem(
  238. _scale.label(vals[i]), color, shape, size);
  239. item.value = vals[i];
  240. _items.addChild(item);
  241. }
  242. for (i=0; i<_items.numChildren; ++i)
  243. LegendItem(_items.getChildAt(i)).maxIconSize = maxSize;
  244. }
  245. }
  246. /**
  247. * Populates the contents of this legend from a list of value objects.
  248. * This method will create a legend with discrete entries determined by
  249. * the contents of the input <code>values</code> array. This should be
  250. * an array of objects containing the following properties:
  251. * <ul>
  252. * <li><code>value</code>: The data value the legend item represents.
  253. * This value is not required.</li>
  254. * <li><code>label</code>: The text label to place in the legend item.
  255. * If this value is not provided, the method will attempt to
  256. * generate a label string from the <code>value</code> property.</li>
  257. * <li><code>color</code>: The color for the legend item. If missing,
  258. * this legend's default color will be used.</li>
  259. * <li><code>shape</code>: The shape for the legend item. If missing,
  260. * a default circle shape will be used.</li>
  261. * <li><code>size</code>: The size for the legend item. If missing,
  262. * a size value of 1 will be used.</li>
  263. * </ul>
  264. * When this method is called, any previous values in the legend will
  265. * be removed.
  266. * @param values an array of value to include in the legend.
  267. */
  268. public function buildFromValues(values:Array):void
  269. {
  270. // first, remove all items
  271. while (_items.numChildren > 0) {
  272. _items.removeChildAt(_items.numChildren-1);
  273. }
  274. _discrete = true;
  275. if (!_orient) _orient = Orientation.TOP_TO_BOTTOM;
  276. var maxSize:Number = Number.MIN_VALUE;
  277. for each (var v:Object in values) {
  278. var value:Object = v.value != undefined ? v.value : null;
  279. var label:String = v.label ? v.label.toString() : value ? value.toString() : "???";
  280. var color:uint = v.color != undefined ? uint(v.color) : _defaultColor;
  281. var shape:String = v.shape ? v.shape as String : null;
  282. var size:Number = _baseIconSize*(v.size is Number ? v.size : 1);
  283. if (size > maxSize) maxSize = size;
  284. var item:LegendItem = new LegendItem(label, color, shape, size);
  285. item.value = value;
  286. item.margin = margin;
  287. _items.addChild(item);
  288. }
  289. for (var i:uint=0; i<_items.numChildren; ++i)
  290. LegendItem(_items.getChildAt(i)).maxIconSize = maxSize;
  291. }
  292. // -- Layout ----------------------------------------------------------
  293. /**
  294. * Performs layout, setting the position for all items in the legend.
  295. * @param t a transitioner for value updates
  296. */
  297. public function layout(t:Transitioner=null):void
  298. {
  299. t = (t ? t : Transitioner.DEFAULT);
  300. var vert:Boolean = Orientation.isVertical(_orient);
  301. var o:Object;
  302. var b:Rectangle = bounds;
  303. var x:Number = b ? b.left : 0;
  304. var y:Number = b ? b.top : 0;
  305. var w:Number, h:Number, th:Number = 0;
  306. // layout the title
  307. o = t.$(_title);
  308. if (_title.text != null && _title.text.length > 0) {
  309. o.x = x + _margin;
  310. o.alpha = 1;
  311. _title.visible = true;
  312. y += (th = _title.height + (vert?_spacing:0));
  313. } else {
  314. o.alpha = 0;
  315. o.visible = false;
  316. }
  317. // layout item container
  318. o = t.$(_items);
  319. o.x = x;
  320. o.y = y;
  321. // layout items
  322. if (_discrete) {
  323. layoutDiscrete(t);
  324. } else {
  325. layoutContinuous(t);
  326. }
  327. w = b ? (vert ? b.width : Math.min(_iw, b.width)) : _iw;
  328. h = b ? (vert ? Math.min(_ih, b.height) : _ih) : _ih;
  329. // size the border
  330. o = t.$(_border);
  331. o.x = x;
  332. o.y = b ? b.top : 0;
  333. o.w = w;
  334. o.h = h + th;
  335. if (t.immediate) _border.render();
  336. // create clipping panel
  337. t.$(items).scrollRect = new Rectangle(0, 0, 1+w, 1+h);
  338. }
  339. /**
  340. * @private
  341. * Layout helper for positioning discrete legend items.
  342. * @param t a transitioner for value updates
  343. */
  344. protected function layoutDiscrete(t:Transitioner):void
  345. {
  346. var vert:Boolean = Orientation.isVertical(_orient);
  347. var rev:Boolean = _orient == Orientation.RIGHT_TO_LEFT ||
  348. _orient == Orientation.BOTTOM_TO_TOP;
  349. var x:Number = 0, y:Number = 0, i:uint, j:uint;
  350. var item:LegendItem, o:Object;
  351. var bw:Number = vert && bounds ? bounds.width : NaN;
  352. // if needed, compute shared width for legend items
  353. if (vert && isNaN(bw)) {
  354. bw = Number.MIN_VALUE;
  355. for (i=0; i<_items.numChildren; ++i) {
  356. item = _items.getChildAt(i) as LegendItem;
  357. bw = Math.max(bw, item.innerWidth);
  358. }
  359. }
  360. _iw = _ih = 0;
  361. for (i=0; i<_items.numChildren; ++i) {
  362. j = rev ? _items.numChildren-i-1 : i;
  363. // layout the item
  364. item = _items.getChildAt(j) as LegendItem;
  365. o = t.$(item);
  366. o.x = x;
  367. o.y = y;
  368. o.w = isNaN(bw) ? item.innerWidth : bw;
  369. // increment spacing
  370. if (vert) {
  371. y += item.innerHeight + _spacing;
  372. _iw = Math.max(_iw, item.innerWidth);
  373. }
  374. else {
  375. x += item.innerWidth + _spacing;
  376. _ih = Math.max(_ih, item.innerHeight);
  377. }
  378. }
  379. _iw = vert ? _iw : x-_spacing;
  380. _ih = vert ? y-_spacing : _ih;
  381. }
  382. /**
  383. * @private
  384. * Layout helper for positioning a continous legend range.
  385. * @param trans a transitioner for value updates
  386. */
  387. protected function layoutContinuous(t:Transitioner):void
  388. {
  389. var lr:LegendRange = _items.getChildAt(0) as LegendRange;
  390. lr.orientation = _orient;
  391. if (Orientation.isHorizontal(_orient)) {
  392. _iw = lr.w = bounds ? bounds.width : 200;
  393. lr.updateLabels();
  394. _ih = lr.height + lr.margin;
  395. } else {
  396. _ih = lr.h = bounds ? bounds.height : 200;
  397. lr.updateLabels();
  398. _iw = lr.width;
  399. }
  400. }
  401. // -- Legend Items ----------------------------------------------------
  402. /** @private */
  403. protected function updateItems() : void
  404. {
  405. if (_items.numChildren == 0) {
  406. return;
  407. } else if (_discrete) {
  408. for (var i:uint = 0; i<_items.numChildren; ++i)
  409. updateItem(_items.getChildAt(i) as LegendItem);
  410. } else {
  411. updateRange(_items.getChildAt(0) as LegendRange);
  412. }
  413. }
  414. /** @private */
  415. protected function updateItem(item:LegendItem):void
  416. {
  417. item.label.textMode = _labelTextMode;
  418. item.label.applyFormat(_labelTextFormat);
  419. item.margin = _margin;
  420. }
  421. /** @private */
  422. protected function updateRange(range:LegendRange):void
  423. {
  424. range.labelTextMode = _labelTextMode;
  425. range.labelTextFormat = _labelTextFormat;
  426. range.margin = _margin;
  427. }
  428. /**
  429. * Sets property values on all legend items. The values
  430. * within the <code>vals</code> argument can take a number of forms:
  431. * <ul>
  432. * <li>If a value is a <code>Function</code>, it will be evaluated
  433. * for each element and the result will be used as the property
  434. * value for that element.</li>
  435. * <li>If a value is an <code>IEvaluable</code> instance, such as
  436. * <code>flare.util.Property</code> or
  437. * <code>flare.query.Expression</code>, it will be evaluated for
  438. * each element and the result will be used as the property value
  439. * for that element.</li>
  440. * <li>In all other cases, a property value will be treated as a
  441. * literal and assigned for all elements.</li>
  442. * </ul>
  443. * @param vals an object containing the properties and values to set.
  444. * @param t a transitioner or time span for updating object values. If
  445. * the input is a transitioner, it will be used to store the updated
  446. * values. If the input is a number, a new Transitioner with duration
  447. * set to the input value will be used. The input is null by default,
  448. * in which case object values are updated immediately.
  449. * @return the transitioner used to update the values
  450. */
  451. public function setItemProperties(vals:Object, t:*=null):Transitioner
  452. {
  453. var trans:Transitioner = Transitioner.instance(t);
  454. Displays.setChildrenProperties(items, vals, trans);
  455. return trans;
  456. }
  457. // -- Static Constructors ---------------------------------------------
  458. /**
  459. * Generates a legend from a given scale and one or more palettes. If
  460. * multiple palettes of the same type are provided, only the first of
  461. * each type will be used for the legend, the others will be ignored.
  462. * @param title the title text for the legend
  463. * @param scale the scale instance determining the legend values
  464. * @param palette a color, shape, or size palette for legend items
  465. * @param args one or more additional palettes
  466. * @return the generated Legend, or null if a legend could not be built
  467. */
  468. public static function fromScale(title:String, scale:Scale,
  469. palette:Palette, ...args):Legend
  470. {
  471. if (scale == null || palette == null)
  472. return null;
  473. var colors:ColorPalette;
  474. var shapes:ShapePalette;
  475. var sizes:SizePalette;
  476. // construct palette collection
  477. var palettes:Array = args as Array;
  478. palettes.unshift(palette);
  479. for each (var p:Palette in palettes) {
  480. if (p is ColorPalette && !colors)
  481. colors = ColorPalette(p);
  482. else if (p is ShapePalette && !shapes)
  483. shapes = ShapePalette(p);
  484. else if (p is SizePalette && !sizes)
  485. sizes = SizePalette(p);
  486. }
  487. if (!colors && !shapes && !sizes)
  488. return null; // no palette to use
  489. return new Legend(title, scale, colors, shapes, sizes);
  490. }
  491. /**
  492. * Generates a legend from an array of legend item values. This method
  493. * simply instantiates a new <code>Legend</code> instance with the
  494. * given title and then invokes the <code>buildFromValues</code> method
  495. * with the given value array.
  496. * @param title the legend title text, or null for no title.
  497. * @param values an array of values for the legend items. See the
  498. * documentation for the <code>buildFromValues</code> method for more.
  499. * @return the generated legend
  500. */
  501. public static function fromValues(title:String, values:Array):Legend
  502. {
  503. var legend:Legend = new Legend(title);
  504. legend.buildFromValues(values);
  505. legend.update();
  506. return legend;
  507. }
  508. } // end of class Legend
  509. }