    /**
     * @class Ext.chart.series.Area
     * @extends Ext.chart.series.Cartesian
     *
     * Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information.
     * As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart
     * documentation for more information. A typical configuration object for the area series could be:
     *
     *     @example
     *     var store = Ext.create('Ext.data.JsonStore', {
     *         fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
     *         data: [
     *             { 'name': 'metric one',   'data1':10, 'data2':12, 'data3':14, 'data4':8,  'data5':13 },
     *             { 'name': 'metric two',   'data1':7,  'data2':8,  'data3':16, 'data4':10, 'data5':3  },
     *             { 'name': 'metric three', 'data1':5,  'data2':2,  'data3':14, 'data4':12, 'data5':7  },
     *             { 'name': 'metric four',  'data1':2,  'data2':14, 'data3':6,  'data4':1,  'data5':23 },
     *             { 'name': 'metric five',  'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33 }
     *         ]
     *     });
     *
     *     Ext.create('Ext.chart.Chart', {
     *         renderTo: Ext.getBody(),
     *         width: 500,
     *         height: 300,
     *         store: store,
     *         axes: [
     *             {
     *                 type: 'Numeric',
     *                 grid: true,
     *                 position: 'left',
     *                 fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
     *                 title: 'Sample Values',
     *                 grid: {
     *                     odd: {
     *                         opacity: 1,
     *                         fill: '#ddd',
     *                         stroke: '#bbb',
     *                         'stroke-width': 1
     *                     }
     *                 },
     *                 minimum: 0,
     *                 adjustMinimumByMajorUnit: 0
     *             },
     *             {
     *                 type: 'Category',
     *                 position: 'bottom',
     *                 fields: ['name'],
     *                 title: 'Sample Metrics',
     *                 grid: true,
     *                 label: {
     *                     rotate: {
     *                         degrees: 315
     *                     }
     *                 }
     *             }
     *         ],
     *         series: [{
     *             type: 'area',
     *             highlight: false,
     *             axis: 'left',
     *             xField: 'name',
     *             yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
     *             style: {
     *                 opacity: 0.93
     *             }
     *         }]
     *     });
     *
     * In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover,
     * take the left axis to measure the data in the area series, set as xField (x values) the name field of each element in the store,
     * and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity
     * to the style object.
     *
     * @xtype area
     */
    Ext.define('Ext.chart.series.Area', {

        /* Begin Definitions */

        extend: 'Ext.chart.series.Cartesian',

        alias: 'series.area',

        requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],

        /* End Definitions */

        type: 'area',

        // @private Area charts are alyways stacked
        stacked: true,

        /**
         * @cfg {Object} style
         * Append styling properties to this object for it to override theme properties.
         */
        style: {},

        constructor: function(config) {
            this.callParent(arguments);
            var me = this,
                surface = me.chart.surface,
                i, l;
            config.highlightCfg = Ext.Object.merge({}, {
                lineWidth: 3,
                stroke: '#55c',
                opacity: 0.8,
                color: '#f00'
            }, config.highlightCfg);

            Ext.apply(me, config, {
                __excludes: []
            });
            if (me.highlight) {
                me.highlightSprite = surface.add({
                    type: 'path',
                    path: ['M', 0, 0],
                    zIndex: 1000,
                    opacity: 0.3,
                    lineWidth: 5,
                    hidden: true,
                    stroke: '#444'
                });
            }
            me.group = surface.getGroup(me.seriesId);
        },

        // @private Shrinks dataSets down to a smaller size
        shrink: function(xValues, yValues, size) {
            var len = xValues.length,
                ratio = Math.floor(len / size),
                i, j,
                xSum = 0,
                yCompLen = this.areas.length,
                ySum = [],
                xRes = [],
                yRes = [];
            //initialize array
            for (j = 0; j < yCompLen; ++j) {
                ySum[j] = 0;
            }
            for (i = 0; i < len; ++i) {
                xSum += +xValues[i];
                for (j = 0; j < yCompLen; ++j) {
                    ySum[j] += +yValues[i][j];
                }
                if (i % ratio == 0) {
                    //push averages
                    xRes.push(xSum/ratio);
                    for (j = 0; j < yCompLen; ++j) {
                        ySum[j] /= ratio;
                    }
                    yRes.push(ySum);
                    //reset sum accumulators
                    xSum = 0;
                    for (j = 0, ySum = []; j < yCompLen; ++j) {
                        ySum[j] = 0;
                    }
                }
            }
            return {
                x: xRes,
                y: yRes
            };
        },

        // @private Get chart and data boundaries
        getBounds: function() {
            var me = this,
                chart = me.chart,
                store = chart.getChartStore(),
                data = store.data.items,
                i, l, record,
                areas = [].concat(me.yField),
                areasLen = areas.length,
                xValues = [],
                yValues = [],
                infinity = Infinity,
                minX = infinity,
                minY = infinity,
                maxX = -infinity,
                maxY = -infinity,
                math = Math,
                mmin = math.min,
                mmax = math.max,
                boundAxis = me.getAxesForXAndYFields(),
                boundXAxis = boundAxis.xAxis,
                boundYAxis = boundAxis.yAxis,
                ends, allowDate,
                bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem, axis, out;

            me.setBBox();
            bbox = me.bbox;

            if (axis = chart.axes.get(boundXAxis)) {
                if (axis.type === 'Time') {
                    allowDate = true;
                }
                ends = axis.applyData();
                minX = ends.from;
                maxX = ends.to;
            }

            if (axis = chart.axes.get(boundYAxis)) {
                ends = axis.applyData();
                minY = ends.from;
                maxY = ends.to;
            }

            // If a field was specified without a corresponding axis, create one to get bounds
            if (me.xField && !Ext.isNumber(minX)) {
                axis = me.getMinMaxXValues();
                allowDate = true;
                minX = axis[0];
                maxX = axis[1];
            }

            if (me.yField && !Ext.isNumber(minY)) {
                axis = me.getMinMaxYValues();
                minY = axis[0];
                maxY = axis[1];
            }

            if (!Ext.isNumber(minY)) {
                minY = 0;
            }
            if (!Ext.isNumber(maxY)) {
                maxY = 0;
            }

            for (i = 0, l = data.length; i < l; i++) {
                record = data[i];
                xValue = record.get(me.xField);
                yValue = [];
                if (typeof xValue != 'number') {
                    if (allowDate) {
                        xValue = +xValue;
                    } else {
                        xValue = i;
                    }
                }
                xValues.push(xValue);
                acumY = 0;
                for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
                    // Excluded series
                    if (me.__excludes[areaIndex]) {
                        continue;
                    }
                    areaElem = record.get(areas[areaIndex]);
                    if (typeof areaElem == 'number') {
                        yValue.push(areaElem);
                    }
                }
                yValues.push(yValue);
            }

            xScale = bbox.width / ((maxX - minX) || 1);
            yScale = bbox.height / ((maxY - minY) || 1);

            ln = xValues.length;
            if ((ln > bbox.width) && me.areas) {
                sumValues = me.shrink(xValues, yValues, bbox.width);
                xValues = sumValues.x;
                yValues = sumValues.y;
            }

            return {
                bbox: bbox,
                minX: minX,
                minY: minY,
                xValues: xValues,
                yValues: yValues,
                xScale: xScale,
                yScale: yScale,
                areasLen: areasLen
            };
        },

        // @private Build an array of paths for the chart
        getPaths: function() {
            var me = this,
                chart = me.chart,
                store = chart.getChartStore(),
                first = true,
                bounds = me.getBounds(),
                bbox = bounds.bbox,
                items = me.items = [],
                componentPaths = [],
                componentPath,
                count = 0,
                paths = [],
                i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path;

            ln = bounds.xValues.length;
            // Start the path
            for (i = 0; i < ln; i++) {
                xValue = bounds.xValues[i];
                yValue = bounds.yValues[i];
                x = bbox.x + (xValue - bounds.minX) * bounds.xScale;
                acumY = 0;
                count = 0;
                for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
                    // Excluded series
                    if (me.__excludes[areaIndex]) {
                        continue;
                    }
                    if (!componentPaths[areaIndex]) {
                        componentPaths[areaIndex] = [];
                    }
                    areaElem = yValue[count];
                    acumY += areaElem;
                    y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale;
                    if (!paths[areaIndex]) {
                        paths[areaIndex] = ['M', x, y];
                        componentPaths[areaIndex].push(['L', x, y]);
                    } else {
                        paths[areaIndex].push('L', x, y);
                        componentPaths[areaIndex].push(['L', x, y]);
                    }
                    if (!items[areaIndex]) {
                        items[areaIndex] = {
                            pointsUp: [],
                            pointsDown: [],
                            series: me
                        };
                    }
                    items[areaIndex].pointsUp.push([x, y]);
                    count++;
                }
            }

            // Close the paths
            for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
                // Excluded series
                if (me.__excludes[areaIndex]) {
                    continue;
                }
                path = paths[areaIndex];
                // Close bottom path to the axis
                if (areaIndex == 0 || first) {
                    first = false;
                    path.push('L', x, bbox.y + bbox.height,
                              'L', bbox.x, bbox.y + bbox.height,
                              'Z');
                }
                // Close other paths to the one before them
                else {
                    componentPath = componentPaths[prevAreaIndex];
                    componentPath.reverse();
                    path.push('L', x, componentPath[0][2]);
                    for (i = 0; i < ln; i++) {
                        path.push(componentPath[i][0],
                                  componentPath[i][1],
                                  componentPath[i][2]);
                        items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]];
                    }
                    path.push('L', bbox.x, path[2], 'Z');
                }
                prevAreaIndex = areaIndex;
            }
            return {
                paths: paths,
                areasLen: bounds.areasLen
            };
        },

        /**
         * Draws the series for the current chart.
         */
        drawSeries: function() {
            var me = this,
                chart = me.chart,
                store = chart.getChartStore(),
                surface = chart.surface,
                animate = chart.animate,
                group = me.group,
                endLineStyle = Ext.apply(me.seriesStyle, me.style),
                colorArrayStyle = me.colorArrayStyle,
                colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
                areaIndex, areaElem, paths, path, rendererAttributes;

            me.unHighlightItem();
            me.cleanHighlights();

            if (!store || !store.getCount() || me.seriesIsHidden) {
                me.hide();
                me.items = [];
                return;
            }

            paths = me.getPaths();

            if (!me.areas) {
                me.areas = [];
            }

            for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) {
                // Excluded series
                if (me.__excludes[areaIndex]) {
                    continue;
                }
                if (!me.areas[areaIndex]) {
                    me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, {
                        type: 'path',
                        group: group,
                        // 'clip-rect': me.clipBox,
                        path: paths.paths[areaIndex],
                        stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength],
                        fill: colorArrayStyle[areaIndex % colorArrayLength]
                    }, endLineStyle || {}));
                }
                areaElem = me.areas[areaIndex];
                path = paths.paths[areaIndex];
                if (animate) {
                    //Add renderer to line. There is not a unique record associated with this.
                    rendererAttributes = me.renderer(areaElem, false, {
                        path: path,
                        // 'clip-rect': me.clipBox,
                        fill: colorArrayStyle[areaIndex % colorArrayLength],
                        stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
                    }, areaIndex, store);
                    //fill should not be used here but when drawing the special fill path object
                    me.animation = me.onAnimate(areaElem, {
                        to: rendererAttributes
                    });
                } else {
                    rendererAttributes = me.renderer(areaElem, false, {
                        path: path,
                        // 'clip-rect': me.clipBox,
                        hidden: false,
                        fill: colorArrayStyle[areaIndex % colorArrayLength],
                        stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
                    }, areaIndex, store);
                    me.areas[areaIndex].setAttributes(rendererAttributes, true);
                }
            }
            me.renderLabels();
            me.renderCallouts();
        },

        // @private
        onAnimate: function(sprite, attr) {
            sprite.show();
            return this.callParent(arguments);
        },

        // @private
        onCreateLabel: function(storeItem, item, i, display) {
            var me = this,
                group = me.labelsGroup,
                config = me.label,
                bbox = me.bbox,
                endLabelStyle = Ext.apply(config, me.seriesLabelStyle);

            return me.chart.surface.add(Ext.apply({
                'type': 'text',
                'text-anchor': 'middle',
                'group': group,
                'x': item.point[0],
                'y': bbox.y + bbox.height / 2
            }, endLabelStyle || {}));
        },

        // @private
        onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
            var me = this,
                chart = me.chart,
                resizing = chart.resizing,
                config = me.label,
                format = config.renderer,
                field = config.field,
                bbox = me.bbox,
                x = item.point[0],
                y = item.point[1],
                bb, width, height;

            label.setAttributes({
                text: format(storeItem.get(field[index])),
                hidden: true
            }, true);

            bb = label.getBBox();
            width = bb.width / 2;
            height = bb.height / 2;

            x = x - width < bbox.x? bbox.x + width : x;
            x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
            y = y - height < bbox.y? bbox.y + height : y;
            y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;

            if (me.chart.animate && !me.chart.resizing) {
                label.show(true);
                me.onAnimate(label, {
                    to: {
                        x: x,
                        y: y
                    }
                });
            } else {
                label.setAttributes({
                    x: x,
                    y: y
                }, true);
                if (resizing) {
                    me.animation.on('afteranimate', function() {
                        label.show(true);
                    });
                } else {
                    label.show(true);
                }
            }
        },

        // @private
        onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
            var me = this,
                chart = me.chart,
                surface = chart.surface,
                resizing = chart.resizing,
                config = me.callouts,
                items = me.items,
                prev = (i == 0) ? false : items[i -1].point,
                next = (i == items.length -1) ? false : items[i +1].point,
                cur = item.point,
                dir, norm, normal, a, aprev, anext,
                bbox = callout.label.getBBox(),
                offsetFromViz = 30,
                offsetToSide = 10,
                offsetBox = 3,
                boxx, boxy, boxw, boxh,
                p, clipRect = me.clipRect,
                x, y;

            //get the right two points
            if (!prev) {
                prev = cur;
            }
            if (!next) {
                next = cur;
            }
            a = (next[1] - prev[1]) / (next[0] - prev[0]);
            aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
            anext = (next[1] - cur[1]) / (next[0] - cur[0]);

            norm = Math.sqrt(1 + a * a);
            dir = [1 / norm, a / norm];
            normal = [-dir[1], dir[0]];

            //keep the label always on the outer part of the "elbow"
            if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) {
                normal[0] *= -1;
                normal[1] *= -1;
            } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
                normal[0] *= -1;
                normal[1] *= -1;
            }

            //position
            x = cur[0] + normal[0] * offsetFromViz;
            y = cur[1] + normal[1] * offsetFromViz;

            //box position and dimensions
            boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
            boxy = y - bbox.height /2 - offsetBox;
            boxw = bbox.width + 2 * offsetBox;
            boxh = bbox.height + 2 * offsetBox;

            //now check if we're out of bounds and invert the normal vector correspondingly
            //this may add new overlaps between labels (but labels won't be out of bounds).
            if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
                normal[0] *= -1;
            }
            if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
                normal[1] *= -1;
            }

            //update positions
            x = cur[0] + normal[0] * offsetFromViz;
            y = cur[1] + normal[1] * offsetFromViz;

            //update box position and dimensions
            boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
            boxy = y - bbox.height /2 - offsetBox;
            boxw = bbox.width + 2 * offsetBox;
            boxh = bbox.height + 2 * offsetBox;

            //set the line from the middle of the pie to the box.
            callout.lines.setAttributes({
                path: ["M", cur[0], cur[1], "L", x, y, "Z"]
            }, true);
            //set box position
            callout.box.setAttributes({
                x: boxx,
                y: boxy,
                width: boxw,
                height: boxh
            }, true);
            //set text position
            callout.label.setAttributes({
                x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
                y: y
            }, true);
            for (p in callout) {
                callout[p].show(true);
            }
        },

        isItemInPoint: function(x, y, item, i) {
            var me = this,
                pointsUp = item.pointsUp,
                pointsDown = item.pointsDown,
                abs = Math.abs,
                distChanged = false,
                last = false,
                dist = Infinity, p, pln, point;

            for (p = 0, pln = pointsUp.length; p < pln; p++) {
                point = [pointsUp[p][0], pointsUp[p][1]];

                distChanged = false;
                last = p == pln -1;

                if (dist > abs(x - point[0])) {
                    dist = abs(x - point[0]);
                    distChanged = true;
                    if (last) {
                        ++p;
                    }
                }

                if (!distChanged || (distChanged && last)) {
                    point = pointsUp[p -1];
                    if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) {
                        item.storeIndex = p -1;
                        item.storeField = me.yField[i];
                        item.storeItem = me.chart.store.getAt(p -1);
                        item._points = pointsDown.length? [point, pointsDown[p -1]] : [point];
                        return true;
                    } else {
                        break;
                    }
                }
            }
            return false;
        },

        /**
         * Highlight this entire series.
         * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
         */
        highlightSeries: function() {
            var area, to, fillColor;
            if (this._index !== undefined) {
                area = this.areas[this._index];
                if (area.__highlightAnim) {
                    area.__highlightAnim.paused = true;
                }
                area.__highlighted = true;
                area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1;
                area.__prevFill = area.__prevFill || area.attr.fill;
                area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth;
                fillColor = Ext.draw.Color.fromString(area.__prevFill);
                to = {
                    lineWidth: (area.__prevLineWidth || 0) + 2
                };
                if (fillColor) {
                    to.fill = fillColor.getLighter(0.2).toString();
                }
                else {
                    to.opacity = Math.max(area.__prevOpacity - 0.3, 0);
                }
                if (this.chart.animate) {
                    area.__highlightAnim = new Ext.fx.Anim(Ext.apply({
                        target: area,
                        to: to
                    }, this.chart.animate));
                }
                else {
                    area.setAttributes(to, true);
                }
            }
        },

        /**
         * UnHighlight this entire series.
         * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
         */
        unHighlightSeries: function() {
            var area;
            if (this._index !== undefined) {
                area = this.areas[this._index];
                if (area.__highlightAnim) {
                    area.__highlightAnim.paused = true;
                }
                if (area.__highlighted) {
                    area.__highlighted = false;
                    area.__highlightAnim = new Ext.fx.Anim({
                        target: area,
                        to: {
                            fill: area.__prevFill,
                            opacity: area.__prevOpacity,
                            lineWidth: area.__prevLineWidth
                        }
                    });
                }
            }
        },

        /**
         * Highlight the specified item. If no item is provided the whole series will be highlighted.
         * @param item {Object} Info about the item; same format as returned by #getItemForPoint
         */
        highlightItem: function(item) {
            var me = this,
                points, path;
            if (!item) {
                this.highlightSeries();
                return;
            }
            points = item._points;
            path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]]
                    : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height];
            me.highlightSprite.setAttributes({
                path: path,
                hidden: false
            }, true);
        },

        /**
         * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
         * @param {Object} item Info about the item; same format as returned by #getItemForPoint
         */
        unHighlightItem: function(item) {
            if (!item) {
                this.unHighlightSeries();
            }

            if (this.highlightSprite) {
                this.highlightSprite.hide(true);
            }
        },

        // @private
        hideAll: function(index) {
            var me = this;
            index = (isNaN(me._index) ? index : me._index) || 0;
            me.__excludes[index] = true;
            me.areas[index].hide(true);
            me.redraw();
        },

        // @private
        showAll: function(index) {
            var me = this;
            index = (isNaN(me._index) ? index : me._index) || 0;
            me.__excludes[index] = false;
            me.areas[index].show(true);
            me.redraw();
        },

        redraw: function() {
            //store previous configuration for the legend
            //and set it to false so we don't
            //re-build label elements if not necessary.
            var me = this,
                prevLegendConfig;
            prevLegendConfig = me.chart.legend.rebuild;
            me.chart.legend.rebuild = false;
            me.chart.redraw();
            me.chart.legend.rebuild = prevLegendConfig;
        },

        hide: function() {
            if (this.areas) {
                var me = this,
                    areas = me.areas,
                    i, j, l, ln, shadows;

                if (areas && areas.length) {
                    for (i = 0, ln = areas.length; i < ln; ++i) {
                        if (areas[i]) {
                            areas[i].hide(true);
                        }
                    }
                    me.hideLabels();
                }
            }
        },

        /**
         * Returns the color of the series (to be displayed as color for the series legend item).
         * @param {Object} item Info about the item; same format as returned by #getItemForPoint
         */
        getLegendColor: function(index) {
            var me = this;
            return me.colorArrayStyle[index % me.colorArrayStyle.length];
        }
    });