package flare.vis.legend
{
import flare.animate.Transitioner;
import flare.display.RectSprite;
import flare.display.TextSprite;
import flare.scale.Scale;
import flare.scale.ScaleType;
import flare.util.Displays;
import flare.util.Orientation;
import flare.util.palette.ColorPalette;
import flare.util.palette.Palette;
import flare.util.palette.ShapePalette;
import flare.util.palette.SizePalette;
import flare.vis.data.ScaleBinding;
import flash.display.Sprite;
import flash.geom.Rectangle;
import flash.text.TextFormat;
/**
* A legend describing the visual encoding of a data property. Legends
* support both discrete legends that list individual items and
* range legends the convey a continuous range of values. Discrete
* legends consist of a collection of <code>LegendItem</code> instances
* stored in the <code>items</code> sprite. Range legends consist of
* a single <code>LegendRange</code> instance stored in the
* <code>items</code> sprite.
*
* <p>There are multiple ways to generate a legend. To build a legend
* based on an existing visual encoding, use the static
* <code>fromScale</code> constructor. This method takes a data scale
* and one more or more palettes (e.g., color, shape, or size palettes)
* and uses them to generate an appropriate legend. If the data scale
* is a quantitative scale and only a color palette is provided, a
* continuous range legend will be generated. Otherwise, a discrete
* legend will be created.</p>
*
* <p>Legends can also be created from a collection of independent
* values using the static <code>fromValues</code> constructor. This
* method takes an array of legend item descriptions and uses them to
* generate a legend. For example, consider this code:</p>
*
* <pre>
* var legend:Legend = Legend.fromValues("Legend Title", [
* {color: 0xff0000, shape:Shapes.X, label:"Red X"},
* {color: 0x00ff00, shape:Shapes.SQUARE, label:"Green Square"},
* {color: 0x0000ff, shape:Shapes.CIRCLE, label:"Blue Circle"}
* ]);
* </pre>
*
* <p>This example will create a legend with the described values.
* See the documentation for the <code>buildFromValues</code> method
* for more details.</p>
*/
public class Legend extends Sprite
{
/** @private The layout bounds for this legend instance. */
protected var _bounds:Rectangle = null;
/** @private Sprite defining the border of the legend. */
protected var _border:RectSprite;
/** @private Sprite containing the legend items. */
protected var _items:Sprite;
/** @private TextSprite containing the legend title.*/
protected var _title:TextSprite;
/** @private Scale instance used to define the legend mapping. */
protected var _scale:Scale;
/** @private Flag for if this legend is discrete or continuous. */
protected var _discrete:Boolean = true;
/** @private The default color to use for legend items. */
protected var _defaultColor:uint = 0xff888888;
/** @private The color palette used to encode values (may be null). */
protected var _colors:ColorPalette;
/** @private The shape palette used to encode values (may be null). */
protected var _shapes:ShapePalette;
/** @private The size palette used to encode values (may be null). */
protected var _sizes:SizePalette;
/** @private Flag indicating the desired orientation of this legend. */
protected var _orient:String = null;
/** @private Margins within legend items. */
protected var _margin:Number = 4;
/** @private Spacing between legend items. */
protected var _spacing:Number = 0;
/** @private Base icon size */
protected var _baseIconSize:Number = 12;
/** @private TextFormat (font, size, style) of legend item labels. */
protected var _labelTextFormat:TextFormat = new TextFormat("Arial",12,0);
/** @private Label text mode. */
protected var _labelTextMode:int = TextSprite.BITMAP;
/** @private The calculated internal width of the legend. */
protected var _iw:Number;
/** @private The calculated internal height of the legend. */
protected var _ih:Number;
/** The layout bounds for this legend instance. */
public function get bounds():Rectangle { return _bounds; }
public function set bounds(b:Rectangle):void { _bounds = b; }
/** Sprite defining the border of the legend. */
public function get border():RectSprite { return _border; }
/** Sprite containing the legend items. */
public function get items():Sprite { return _items; }
/** TextSprite containing the legend title.*/
public function get title():TextSprite { return _title; }
/** Flag indicating if this legend is discrete or continuous. */
public function get discrete():Boolean { return _discrete; }
/** Scale instance used to define the legend mapping. */
public function get scale():Scale { return _scale; }
public function set scale(s:Scale):void { _scale = s; }
/** The legend range, if this legend is continuous. This
* value is null if the legend is discrete. */
public function get range():LegendRange {
return _discrete ? null : LegendRange(_items.getChildAt(0));
}
/** The default color to use for legend items. */
public function get defaultColor():uint { return _defaultColor; }
public function set defaultColor(c:uint):void { _defaultColor = c; }
/** The color palette used to encode values (may be null). */
public function get colorPalette():ColorPalette { return _colors; }
public function set colorPalette(cp:ColorPalette):void { _colors = cp; }
/** The shape palette used to encode values (may be null). */
public function get shapePalette():ShapePalette { return _shapes; }
public function set shapePalette(sp:ShapePalette):void { _shapes = sp; }
/** The size palette used to encode values (may be null). */
public function get sizePalette():SizePalette { return _sizes; }
public function set sizePalette(sp:SizePalette):void { _sizes = sp; }
/** The desired orientation of this legend. */
public function get orientation():String { return _orient; }
public function set orientation(o:String):void { _orient = o; }
/** Margins within legend items. */
public function get margin():Number { return _margin; }
public function set margin(m:Number):void { _margin = m; }
/** Spacing between legend items. */
public function get spacing():Number { return _spacing; }
public function set spacing(s:Number):void { _spacing = s; }
/** Base icon size, corresponding to a size factor of 1. */
public function get baseIconSize():Number { return _baseIconSize; }
public function set baseIconSize(s:Number):void {
if (_baseIconSize != s && _discrete) {
for (var i:uint=0; i<_items.numChildren; ++i) {
var li:LegendItem = LegendItem(_items.getChildAt(i));
li.iconSize *= (s / _baseIconSize);
li.maxIconSize *= (s / _baseIconSize);
}
}
_baseIconSize = s;
}
/** TextFormat (font, size, style) of legend item labels. */
public function get labelTextFormat():TextFormat { return _labelTextFormat; }
public function set labelTextFormat(f:TextFormat):void {
_labelTextFormat = f; updateItems();
}
/** Label text mode. */
public function get labelTextMode():int { return _labelTextMode; }
public function set labelTextMode(mode:int):void {
_labelTextMode = mode; updateItems();
}
/**
* Creates a new Legend for the given data field.
* @param dataField the data field to describe with the legend
* @param vis the visualization corresponding to this legend
* @param scale the scale value used to map the data field to visual
* variables
*/
public function Legend(title:String, scale:Scale=null,
colors:ColorPalette=null, shapes:ShapePalette=null,
sizes:SizePalette=null)
{
this.scale = scale;
addChild(_border = new RectSprite(0,0,0,0,13,13));
addChild(_title = new TextSprite());
addChild(_items = new Sprite());
_border.lineColor = 0;
_border.fillColor = 0;
_colors = colors;
_shapes = shapes;
_sizes = sizes;
_title.textField.defaultTextFormat =
new TextFormat("Arial",12,null,true);
if (title != null)
_title.text = title;
else
_title.visible = false;
if (scale != null) {
buildFromScale();
update();
}
}
/**
* Update the legend, recomputing layout of items.
* @param t a transitioner for value updates
* @return the input transitioner
*/
public function update(t:Transitioner=null):Transitioner
{
if (_scale is ScaleBinding && ScaleBinding(_scale).updateBinding())
buildFromScale();
updateItems();
layout(t);
return t;
}
/**
* Builds the contents of this legend from the current scale values.
* This method will remove all items from the legend and rebuild the
* legend using the current scale and palette settings.
*/
public function buildFromScale():void
{
while (_items.numChildren > 0) {
_items.removeChildAt(_items.numChildren-1);
}
var type:String = _scale.scaleType;
if (ScaleType.isQuantitative(type) && !_sizes && !_shapes) {
_discrete = false;
if (!_orient) _orient = Orientation.LEFT_TO_RIGHT;
_items.addChild(new LegendRange(_title.text,
_scale, _colors, _orient));
} else {
_discrete = true;
if (!_orient) _orient = Orientation.TOP_TO_BOTTOM;
var numVals:int = ScaleType.isQuantitative(type) ? 5 : -1;
var maxSize:Number = Number.MIN_VALUE;
var vals:Array = _scale.values(numVals);
for (var i:uint=0; i<vals.length; ++i) {
var f:Number = _scale.interpolate(vals[i]);
var color:uint = _defaultColor;
if (_colors && ScaleType.isOrdinal(type)) {
color = _colors.getColorByIndex(i);
} else if (_colors) {
color = _colors.getColor(f);
}
var shape:String = _shapes ? _shapes.getShape(i) : null;
var size:Number = _baseIconSize*(_sizes?_sizes.getSize(f):1);
if (size > maxSize) maxSize = size;
var item:LegendItem = new LegendItem(
_scale.label(vals[i]), color, shape, size);
item.value = vals[i];
_items.addChild(item);
}
for (i=0; i<_items.numChildren; ++i)
LegendItem(_items.getChildAt(i)).maxIconSize = maxSize;
}
}
/**
* Populates the contents of this legend from a list of value objects.
* This method will create a legend with discrete entries determined by
* the contents of the input <code>values</code> array. This should be
* an array of objects containing the following properties:
* <ul>
* <li><code>value</code>: The data value the legend item represents.
* This value is not required.</li>
* <li><code>label</code>: The text label to place in the legend item.
* If this value is not provided, the method will attempt to
* generate a label string from the <code>value</code> property.</li>
* <li><code>color</code>: The color for the legend item. If missing,
* this legend's default color will be used.</li>
* <li><code>shape</code>: The shape for the legend item. If missing,
* a default circle shape will be used.</li>
* <li><code>size</code>: The size for the legend item. If missing,
* a size value of 1 will be used.</li>
* </ul>
* When this method is called, any previous values in the legend will
* be removed.
* @param values an array of value to include in the legend.
*/
public function buildFromValues(values:Array):void
{
while (_items.numChildren > 0) {
_items.removeChildAt(_items.numChildren-1);
}
_discrete = true;
if (!_orient) _orient = Orientation.TOP_TO_BOTTOM;
var maxSize:Number = Number.MIN_VALUE;
for each (var v:Object in values) {
var value:Object = v.value != undefined ? v.value : null;
var label:String = v.label ? v.label.toString() : value ? value.toString() : "???";
var color:uint = v.color != undefined ? uint(v.color) : _defaultColor;
var shape:String = v.shape ? v.shape as String : null;
var size:Number = _baseIconSize*(v.size is Number ? v.size : 1);
if (size > maxSize) maxSize = size;
var item:LegendItem = new LegendItem(label, color, shape, size);
item.value = value;
item.margin = margin;
_items.addChild(item);
}
for (var i:uint=0; i<_items.numChildren; ++i)
LegendItem(_items.getChildAt(i)).maxIconSize = maxSize;
}
/**
* Performs layout, setting the position for all items in the legend.
* @param t a transitioner for value updates
*/
public function layout(t:Transitioner=null):void
{
t = (t ? t : Transitioner.DEFAULT);
var vert:Boolean = Orientation.isVertical(_orient);
var o:Object;
var b:Rectangle = bounds;
var x:Number = b ? b.left : 0;
var y:Number = b ? b.top : 0;
var w:Number, h:Number, th:Number = 0;
o = t.$(_title);
if (_title.text != null && _title.text.length > 0) {
o.x = x + _margin;
o.alpha = 1;
_title.visible = true;
y += (th = _title.height + (vert?_spacing:0));
} else {
o.alpha = 0;
o.visible = false;
}
o = t.$(_items);
o.x = x;
o.y = y;
if (_discrete) {
layoutDiscrete(t);
} else {
layoutContinuous(t);
}
w = b ? (vert ? b.width : Math.min(_iw, b.width)) : _iw;
h = b ? (vert ? Math.min(_ih, b.height) : _ih) : _ih;
o = t.$(_border);
o.x = x;
o.y = b ? b.top : 0;
o.w = w;
o.h = h + th;
if (t.immediate) _border.render();
t.$(items).scrollRect = new Rectangle(0, 0, 1+w, 1+h);
}
/**
* @private
* Layout helper for positioning discrete legend items.
* @param t a transitioner for value updates
*/
protected function layoutDiscrete(t:Transitioner):void
{
var vert:Boolean = Orientation.isVertical(_orient);
var rev:Boolean = _orient == Orientation.RIGHT_TO_LEFT ||
_orient == Orientation.BOTTOM_TO_TOP;
var x:Number = 0, y:Number = 0, i:uint, j:uint;
var item:LegendItem, o:Object;
var bw:Number = vert && bounds ? bounds.width : NaN;
if (vert && isNaN(bw)) {
bw = Number.MIN_VALUE;
for (i=0; i<_items.numChildren; ++i) {
item = _items.getChildAt(i) as LegendItem;
bw = Math.max(bw, item.innerWidth);
}
}
_iw = _ih = 0;
for (i=0; i<_items.numChildren; ++i) {
j = rev ? _items.numChildren-i-1 : i;
item = _items.getChildAt(j) as LegendItem;
o = t.$(item);
o.x = x;
o.y = y;
o.w = isNaN(bw) ? item.innerWidth : bw;
if (vert) {
y += item.innerHeight + _spacing;
_iw = Math.max(_iw, item.innerWidth);
}
else {
x += item.innerWidth + _spacing;
_ih = Math.max(_ih, item.innerHeight);
}
}
_iw = vert ? _iw : x-_spacing;
_ih = vert ? y-_spacing : _ih;
}
/**
* @private
* Layout helper for positioning a continous legend range.
* @param trans a transitioner for value updates
*/
protected function layoutContinuous(t:Transitioner):void
{
var lr:LegendRange = _items.getChildAt(0) as LegendRange;
lr.orientation = _orient;
if (Orientation.isHorizontal(_orient)) {
_iw = lr.w = bounds ? bounds.width : 200;
lr.updateLabels();
_ih = lr.height + lr.margin;
} else {
_ih = lr.h = bounds ? bounds.height : 200;
lr.updateLabels();
_iw = lr.width;
}
}
/** @private */
protected function updateItems() : void
{
if (_items.numChildren == 0) {
return;
} else if (_discrete) {
for (var i:uint = 0; i<_items.numChildren; ++i)
updateItem(_items.getChildAt(i) as LegendItem);
} else {
updateRange(_items.getChildAt(0) as LegendRange);
}
}
/** @private */
protected function updateItem(item:LegendItem):void
{
item.label.textMode = _labelTextMode;
item.label.applyFormat(_labelTextFormat);
item.margin = _margin;
}
/** @private */
protected function updateRange(range:LegendRange):void
{
range.labelTextMode = _labelTextMode;
range.labelTextFormat = _labelTextFormat;
range.margin = _margin;
}
/**
* Sets property values on all legend items. The values
* within the <code>vals</code> argument can take a number of forms:
* <ul>
* <li>If a value is a <code>Function</code>, it will be evaluated
* for each element and the result will be used as the property
* value for that element.</li>
* <li>If a value is an <code>IEvaluable</code> instance, such as
* <code>flare.util.Property</code> or
* <code>flare.query.Expression</code>, it will be evaluated for
* each element and the result will be used as the property value
* for that element.</li>
* <li>In all other cases, a property value will be treated as a
* literal and assigned for all elements.</li>
* </ul>
* @param vals an object containing the properties and values to set.
* @param t a transitioner or time span for updating object values. If
* the input is a transitioner, it will be used to store the updated
* values. If the input is a number, a new Transitioner with duration
* set to the input value will be used. The input is null by default,
* in which case object values are updated immediately.
* @return the transitioner used to update the values
*/
public function setItemProperties(vals:Object, t:*=n