    /**
     * @class Ext.chart.series.Area
     * @extends Ext.chart.series.Cartesian
     *
     <p>
        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:
     </p>
    {@img Ext.chart.series.Area/Ext.chart.series.Area.png Ext.chart.series.Area chart series}
      <pre><code>
       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
                }
            }]
        });
       </code></pre>

     <p>
      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.
     </p>

     * @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;
            Ext.apply(me, config, {
                __excludes: [],
                highlightCfg: {
                    lineWidth: 3,
                    stroke: '#55c',
                    opacity: 0.8,
                    color: '#f00'
                }
            });
            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.substore || chart.store,
                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,
                bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem;

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

            // Run through the axis
            if (me.axis) {
                axis = chart.axes.get(me.axis);
                if (axis) {
                    out = axis.calcEnds();
                    minY = out.from || axis.prevMin;
                    maxY = mmax(out.to || axis.prevMax, 0);
                }
            }

            if (me.yField && !Ext.isNumber(minY)) {
                axis = Ext.create('Ext.chart.axis.Axis', {
                    chart: chart,
                    fields: [].concat(me.yField)
                });
                out = axis.calcEnds();
                minY = out.from || axis.prevMin;
                maxY = mmax(out.to || axis.prevMax, 0);
            }

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

            store.each(function(record, i) {
                xValue = record.get(me.xField);
                yValue = [];
                if (typeof xValue != 'number') {
                    xValue = i;
                }
                xValues.push(xValue);
                acumY = 0;
                for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
                    areaElem = record.get(areas[areaIndex]);
                    if (typeof areaElem == 'number') {
                        minY = mmin(minY, areaElem);
                        yValue.push(areaElem);
                        acumY += areaElem;
                    }
                }
                minX = mmin(minX, xValue);
                maxX = mmax(maxX, xValue);
                maxY = mmax(maxY, acumY);
                yValues.push(yValue);
            }, me);

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

            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.substore || chart.store,
                first = true,
                bounds = me.getBounds(),
                bbox = bounds.bbox,
                items = me.items = [],
                componentPaths = [],
                componentPath,
                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;
                for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
                    // Excluded series
                    if (me.__excludes[areaIndex]) {
                        continue;
                    }
                    if (!componentPaths[areaIndex]) {
                        componentPaths[areaIndex] = [];
                    }
                    areaElem = yValue[areaIndex];
                    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]);
                }
            }

            // 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.substore || chart.store,
                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()) {
                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,
                dist = Infinity, p, pln, point;

            for (p = 0, pln = pointsUp.length; p < pln; p++) {
                point = [pointsUp[p][0], pointsUp[p][1]];
                if (dist > abs(x - point[0])) {
                    dist = abs(x - point[0]);
                } else {
                    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 = Ext.create('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 = Ext.create('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 item {Object} 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() {
            if (!isNaN(this._index)) {
                this.__excludes[this._index] = true;
                this.areas[this._index].hide(true);
                this.drawSeries();
            }
        },

        // @private
        showAll: function() {
            if (!isNaN(this._index)) {
                this.__excludes[this._index] = false;
                this.areas[this._index].show(true);
                this.drawSeries();
            }
        },

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