    package flare.vis.operator.layout
    {
        import flare.scale.LinearScale;
        import flare.scale.OrdinalScale;
        import flare.scale.QuantitativeScale;
        import flare.scale.Scale;
        import flare.scale.TimeScale;
        import flare.util.Arrays;
        import flare.util.Maths;
        import flare.util.Orientation;
        import flare.util.Stats;
        import flare.vis.axis.CartesianAxes;
        import flare.vis.data.NodeSprite;

        import flash.geom.Rectangle;

        /**
         * Layout that consecutively places items on top of each other. The layout
         * currently assumes that each column value is available as separate
         * properties of individual DataSprites.
         */
        public class StackedAreaLayout extends Layout
        {
            // -- Properties ------------------------------------------------------

            private var _columns:Array;
            private var _peaks:Array;
            private var _poly:Array;

            private var _orient:String = Orientation.BOTTOM_TO_TOP;
            private var _horiz:Boolean = false;
            private var _top:Boolean = false;
            private var _initAxes:Boolean = true;

            private var _normalize:Boolean = false;
            private var _padding:Number = 0.05;
            private var _threshold:Number = 1.0;

            private var _scale:QuantitativeScale = new LinearScale(0,0,10,true);
            private var _colScale:Scale;

            /** Array containing the column names. */
            public function get columns():Array { return _columns; }
            public function set columns(cols:Array):void {
                _columns = Arrays.copy(cols);
                _peaks = new Array(cols.length);
                _poly = new Array(cols.length);
                _colScale = getScale(_columns);
            }

            /** Flag indicating if the visualization should be normalized. */
            public function get normalize():Boolean { return _normalize; }
            public function set normalize(b:Boolean):void { _normalize = b; }

            /** Value indicating the padding (as a percentage of the view)
             *  that should be reserved within the visualization. */
            public function get padding():Number { return _padding; }
            public function set padding(p:Number):void {
                if (p<0 || isNaN(p) || !isFinite(p)) return;
                _padding = p;
            }

            /** Threshold size value (in pixels) that at least one column width
             *  must surpass for a stack to remain visible. */
            public function get threshold():Number { return _threshold; }
            public function set threshold(t:Number):void { _threshold = t; }

            /** The orientation of the layout. */
            public function get orientation():String { return _orient; }
            public function set orientation(o:String):void {
                _orient = o;
                _horiz = Orientation.isHorizontal(_orient);
                _top   = (_orient == Orientation.TOP_TO_BOTTOM ||
                          _orient == Orientation.LEFT_TO_RIGHT);
                initializeAxes();
            }

            /** The scale used to layout the stacked values. */
            public function get scale():QuantitativeScale { return _scale; }
            public function set scale(s:QuantitativeScale):void {
                _scale = s; _scale.dataMin = 0;
            }

            // -- Methods ---------------------------------------------------------

            /**
             * Creates a new StackedAreaLayout.
             * @param cols an ordered array of properties for the column values
             * @param padding percentage of space to leave as a padding margin
             *  for the stacked chart
             */
            public function StackedAreaLayout(cols:Array=null, padding:Number=0.05)
            {
                layoutType = CARTESIAN;
                if (cols != null) this.columns = cols;
                this.padding = padding;
            }

            private static function getScale(cols:Array):Scale
            {
                var stats:Stats = new Stats(cols);
                switch (stats.dataType) {
                    case Stats.NUMBER:
                        return new LinearScale(stats.minimum, stats.maximum, 10, true);
                    case Stats.DATE:
                        return new TimeScale(stats.minDate, stats.maxDate, true);
                    case Stats.OBJECT:
                    default:
                        return new OrdinalScale(stats.distinctValues, true, false);
                }
            }

            /** @inheritDoc */
            public override function setup():void
            {
                if (!_initAxes || visualization==null) return;
                initializeAxes();
                (_horiz ? xyAxes.yAxis : xyAxes.xAxis).showLines = false;
            }

            /**
             * Initializes the axes prior to layout.
             */
            protected function initializeAxes():void
            {
                if (!_initAxes || visualization==null) return;
                var axes:CartesianAxes = xyAxes;
                if (_horiz) {
                    axes.xAxis.axisScale = _scale;
                    axes.yAxis.axisScale = _colScale;
                } else {
                    axes.xAxis.axisScale = _colScale;
                    axes.yAxis.axisScale = _scale;
                }
            }

            /** @inheritDoc */
            protected override function layout():void
            {
                // get the orientation specifics sorted out
                var bounds:Rectangle = layoutBounds;
                var hgt:Number, wth:Number;
                var xbias:int, ybias:int, sign:int, len:int;
                hgt = (_horiz ? bounds.width : bounds.height);
                wth = (_horiz ? -bounds.height : bounds.width);
                xbias = (_horiz ? 1 : 0);
                ybias = (_horiz ? 0 : 1);
                sign = _top ? 1 : -1;
                len = _columns.length;

                var minX:Number = _horiz ? bounds.bottom : bounds.left;
                var minY:Number = _horiz ? (_top ? bounds.left : bounds.right)
                                         : (_top ? bounds.top : bounds.bottom);

                // perform first walk to get the data distribution
                _scale.dataMax = peaks();
                initializeAxes();

                // initialize current polygon
                var axes:CartesianAxes = super.xyAxes;
                var scale:Scale = (_horiz ? axes.yAxis : axes.xAxis).axisScale;
                var xx:Number;
                for (var j:uint=0; j<len; ++j) {
                    xx = minX + wth * scale.interpolate(_columns[j]);
                    _poly[2*(len+j)+xbias] = xx;
                    _poly[2*(len+j)+ybias] = minY;
                    _poly[2*(len-1-j)+xbias] = xx;
                    _poly[2*(len-1-j)+ybias] = minY;
                }

                // perform second walk to compute polygon layout
                visualization.data.nodes.visit(function(d:NodeSprite):void
                {
                    var obj:Object = _t.$(d);
                    var height:Number = 0, i:uint;
                    var visible:Boolean = d.visible && d.alpha>0;
                    var filtered:Boolean = !obj.visible;

                    // set full polygon to current baseline
                    for (i=0; i<len; ++i) {
                        _poly[2*(len-1-i)+ybias] = _poly[2*(len+i)+ybias];
                    }

                    // if not visible, flatten on current baseline
                    if (!visible || filtered) {
                        if (!visible || _t.immediate) {
                            // if already hidden, skip transitioner
                            d.points = Arrays.copy(_poly, d.points);
                        } else {
                            // otherwise interpolate the change
                            obj.points = Arrays.copy(_poly, d.props.poly);
                        }
                        return;
                    }
                    if (d.points == null) d.points = Arrays.copy(_poly);

                    // if visible, compute the new heights
                    for (i=0; i<len; ++i) {
                        var base:int = 2*(len+i), h:Number;
                        var value:Number = d.data[_columns[i]];

                        if (_normalize) {
                            _poly[base+ybias] += sign * hgt * Maths.invLinearInterp(value,0,_peaks[i]);
                        } else {
                            _poly[base+ybias] += sign * hgt * _scale.interpolate(value);
                        }

                        h = Math.abs(_poly[2*(len-1-i)+ybias] - _poly[base+ybias]);
                        if (h > height) height = h;
                    }

                    // if size is beneath threshold, then hide
                    if (height < _threshold) {
                        d.visible = false;
                    }

                    // update data sprite layout
                    obj.x = 0; obj.y = 0;
                    if (_t.immediate) {
                        d.points = Arrays.copy(_poly, d.points);
                    } else {
                        obj.points = Arrays.copy(_poly, d.props.poly);
                    }
                }, null, true);
            }

            private function peaks():Number
            {
                var sum:Number = 0;

                // first, compute max value of the current data
                Arrays.fill(_peaks, 0);
                visualization.data.nodes.visit(function(d:NodeSprite):void {
                    if (!d.visible || d.alpha <= 0 || !_t.$(d).visible)
                        return;

                    for (var i:uint=0; i<_columns.length; ++i) {
                        var val:Number = d.data[_columns[i]];
                        _peaks[i] += val;
                        sum += val;
                    }
                });
                var max:Number = Arrays.max(_peaks);

                // update peaks array as needed
                // adjust peaks to include padding space
                if (!_normalize) {
                    Arrays.fill(_peaks, max);
                    for (var i:uint=0; i<_peaks.length; ++i) {
                        _peaks[i] += _padding * _peaks[i];
                    }
                    max += _padding*max;
                }

                // return max range value
                if (_normalize) max = 1.0;
                if (isNaN(max)) max = 0;
                return max;
            }

        } // end of class StackedAreaLayout
    }