/*
This file is part of Ext JS 4.2

Copyright (c) 2011-2013 Sencha Inc

Contact:  http://www.sencha.com/contact

GNU General Public License Usage
This file may be used under the terms of the GNU General Public License version 3.0 as
published by the Free Software Foundation and appearing in the file LICENSE included in the
packaging of this file.

Please review the following information to ensure the GNU General Public License version 3.0
requirements will be met: http://www.gnu.org/copyleft/gpl.html.

If you are unsure which license is appropriate for your use, please contact the sales department
at http://www.sencha.com/contact.

Build date: 2013-05-16 14:36:50 (f9be68accb407158ba2b1be2c226a6ce1f649314)
*/
/**
 * Creates a Bar Chart. A Bar Chart is a useful visualization technique to display quantitative information for
 * different categories that can show some progression (or regression) in the dataset. As with all other series, the Bar
 * Series must be appended in the *series* Chart array configuration. See the Chart documentation for more information.
 * A typical configuration object for the bar series could be:
 *
 *     @example
 *     var store = Ext.create('Ext.data.JsonStore', {
 *         fields: ['name', 'data'],
 *         data: [
 *             { 'name': 'metric one',   'data':10 },
 *             { 'name': 'metric two',   'data': 7 },
 *             { 'name': 'metric three', 'data': 5 },
 *             { 'name': 'metric four',  'data': 2 },
 *             { 'name': 'metric five',  'data':27 }
 *         ]
 *     });
 *
 *     Ext.create('Ext.chart.Chart', {
 *         renderTo: Ext.getBody(),
 *         width: 500,
 *         height: 300,
 *         animate: true,
 *         store: store,
 *         axes: [{
 *             type: 'Numeric',
 *             position: 'bottom',
 *             fields: ['data'],
 *             label: {
 *                 renderer: Ext.util.Format.numberRenderer('0,0')
 *             },
 *             title: 'Sample Values',
 *             grid: true,
 *             minimum: 0
 *         }, {
 *             type: 'Category',
 *             position: 'left',
 *             fields: ['name'],
 *             title: 'Sample Metrics'
 *         }],
 *         series: [{
 *             type: 'bar',
 *             axis: 'bottom',
 *             highlight: true,
 *             tips: {
 *               trackMouse: true,
 *               width: 140,
 *               height: 28,
 *               renderer: function(storeItem, item) {
 *                 this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data') + ' views');
 *               }
 *             },
 *             label: {
 *               display: 'insideEnd',
 *                 field: 'data',
 *                 renderer: Ext.util.Format.numberRenderer('0'),
 *                 orientation: 'horizontal',
 *                 color: '#333',
 *                 'text-anchor': 'middle'
 *             },
 *             xField: 'name',
 *             yField: 'data'
 *         }]
 *     });
 *
 * In this configuration we set `bar` as the series type, bind the values of the bar to the bottom axis and set the
 * xField or category field to the `name` parameter of the store. We also set `highlight` to true which enables smooth
 * animations when bars are hovered. We also set some configuration for the bar labels to be displayed inside the bar,
 * to display the information found in the `data1` property of each element store, to render a formated text with the
 * `Ext.util.Format` we pass in, to have an `horizontal` orientation (as opposed to a vertical one) and we also set
 * other styles like `color`, `text-anchor`, etc.
 */
Ext.define('Ext.chart.series.Bar', {

    /* Begin Definitions */

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

    alternateClassName: ['Ext.chart.BarSeries', 'Ext.chart.BarChart', 'Ext.chart.StackedBarChart'],

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

    /* End Definitions */

    type: 'bar',

    alias: 'series.bar',
    /**
     * @cfg {Boolean} column Whether to set the visualization as column chart or horizontal bar chart.
     */
    column: false,

    /**
     * @cfg style Style properties that will override the theming series styles.
     */
    style: {},

    /**
     * @cfg {Number} gutter The gutter space between single bars, as a percentage of the bar width
     */
    gutter: 38.2,

    /**
     * @cfg {Number} groupGutter The gutter space between groups of bars, as a percentage of the bar width
     */
    groupGutter: 38.2,

    /**
     * @cfg {Number/Object} xPadding Padding between the left/right axes and the bars.
     * The possible values are a number (the number of pixels for both left and right padding)
     * or an object with `{ left, right }` properties.
     */
    xPadding: 0,

    /**
     * @cfg {Number/Object} yPadding Padding between the top/bottom axes and the bars.
     * The possible values are a number (the number of pixels for both top and bottom padding)
     * or an object with `{ top, bottom }` properties.
     */
    yPadding: 10,

    /**
     * @cfg {Boolean} stacked
     * If set to `true` then bars for multiple `yField` values will be rendered stacked on top of one another.
     * Otherwise, they will be rendered side-by-side. Defaults to `false`.
     */

    constructor: function(config) {
        this.callParent(arguments);
        var me = this,
            surface = me.chart.surface,
            shadow = me.chart.shadow,
            i, l;
        config.highlightCfg = Ext.Object.merge({
            lineWidth: 3,
            stroke: '#55c',
            opacity: 0.8,
            color: '#f00'
        }, config.highlightCfg);
        Ext.apply(me, config, {
            shadowAttributes: [{
                "stroke-width": 6,
                "stroke-opacity": 0.05,
                stroke: 'rgb(200, 200, 200)',
                translate: {
                    x: 1.2,
                    y: 1.2
                }
            }, {
                "stroke-width": 4,
                "stroke-opacity": 0.1,
                stroke: 'rgb(150, 150, 150)',
                translate: {
                    x: 0.9,
                    y: 0.9
                }
            }, {
                "stroke-width": 2,
                "stroke-opacity": 0.15,
                stroke: 'rgb(100, 100, 100)',
                translate: {
                    x: 0.6,
                    y: 0.6
                }
            }]
        });

        me.group = surface.getGroup(me.seriesId + '-bars');
        if (shadow) {
            for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
                me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
            }
        }
    },

    // @private returns the padding.
    getPadding: function() {
        var me = this,
            xPadding = me.xPadding,
            yPadding = me.yPadding,
            padding = { };

        if (Ext.isNumber(xPadding)) {
            padding.left = xPadding;
            padding.right = xPadding;
        } else if (Ext.isObject(xPadding)) {
            padding.left = xPadding.left;
            padding.right = xPadding.right;
        } else {
            padding.left = 0;
            padding.right = 0;
        }
        padding.width = padding.left + padding.right;

        if (Ext.isNumber(yPadding)) {
            padding.bottom = yPadding;
            padding.top = yPadding;
        } else if (Ext.isObject(yPadding)) {
            padding.bottom = yPadding.bottom;
            padding.top = yPadding.top;
        } else {
            padding.bottom = 0;
            padding.top = 0;
        }
        padding.height = padding.bottom + padding.top;

        return padding;
    },

    // @private returns the bar girth.
    getBarGirth: function() {
        var me = this,
            store = me.chart.getChartStore(),
            column = me.column,
            ln = store.getCount(),
            gutter = me.gutter / 100,
            padding,
            property;

        if (me.style && me.style.width) {
            return me.style.width;
        }
        padding = me.getPadding();
        property = (column ? 'width' : 'height');
        return (me.chart.chartBBox[property] - padding[property]) / (ln * (gutter + 1) - gutter);
    },

    // @private returns the gutters.
    getGutters: function() {
        var me = this,
            column = me.column,
            padding = me.getPadding(),
            halfBarGirth = me.getBarGirth() / 2,
            lowerGutter = Math.ceil((column ? padding.left : padding.bottom) + halfBarGirth),
            upperGutter = Math.ceil((column ? padding.right : padding.top) + halfBarGirth);

        return {
            lower: lowerGutter,
            upper: upperGutter,
            verticalAxis: !column
        };
    },

    // @private Get chart and data boundaries
    getBounds: function() {
        var me = this,
            chart = me.chart,
            store = chart.getChartStore(),
            data = store.data.items,
            i, ln, record,
            bars = [].concat(me.yField),
            barsLoc,
            barsLen = bars.length,
            groupBarsLen = barsLen,
            groupGutter = me.groupGutter / 100,
            column = me.column,
            padding = me.getPadding(),
            stacked = me.stacked,
            barWidth = me.getBarGirth(),
            barWidthProperty = column ? 'width' : 'height',
            math = Math,
            mmin = math.min,
            mmax = math.max,
            mabs = math.abs,
            boundAxes = me.getAxesForXAndYFields(),
            boundYAxis = boundAxes.yAxis,
            minX, maxX, colsScale, colsZero, gutters,
            ends, shrunkBarWidth, groupBarWidth, bbox, minY, maxY, axis, out,
            scale, zero, total, rec, j, plus, minus;

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

        //Skip excluded series
        if (me.__excludes) {
            for (j = 0, total = me.__excludes.length; j < total; j++) {
                if (me.__excludes[j]) {
                    groupBarsLen--;
                }
            }
        }
        axis = chart.axes.get(boundYAxis);
        if (axis) {
            ends = axis.applyData();
            minY = ends.from;
            maxY = ends.to;
        }

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

        if (!Ext.isNumber(minY)) {
            minY = 0;
        }
        if (!Ext.isNumber(maxY)) {
            maxY = 0;
        }
        scale = (column ? bbox.height - padding.height : bbox.width - padding.width) / (maxY - minY);
        shrunkBarWidth = barWidth;
        groupBarWidth = (barWidth / ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter));
        
        if (barWidthProperty in me.style) {
            groupBarWidth = mmin(groupBarWidth, me.style[barWidthProperty]);
            shrunkBarWidth = groupBarWidth * ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter);
        }
        zero = (column) ? bbox.y + bbox.height - padding.bottom : bbox.x + padding.left;

        if (stacked) {
            total = [[], []];
            for (i = 0, ln = data.length; i < ln; i++) {
                record = data[i];
                total[0][i] = total[0][i] || 0;
                total[1][i] = total[1][i] || 0;
                for (j = 0; j < barsLen; j++) {
                    if (me.__excludes && me.__excludes[j]) {
                        continue;
                    }
                    rec = record.get(bars[j]);
                    total[+(rec > 0)][i] += mabs(rec);
                }
            }
            total[+(maxY > 0)].push(mabs(maxY));
            total[+(minY > 0)].push(mabs(minY));
            minus = mmax.apply(math, total[0]);
            plus = mmax.apply(math, total[1]);
            scale = (column ? bbox.height - padding.height : bbox.width - padding.width) / (plus + minus);
            zero = zero + minus * scale * (column ? -1 : 1);
        }
        else if (minY / maxY < 0) {
            zero = zero - minY * scale * (column ? -1 : 1);
        }

        // If the columns are bound to the x-axis, calculate their positions
        if (me.boundColumn) {
            axis = chart.axes.get(boundAxes.xAxis);
            if (axis) {
                ends = axis.applyData();
                minX = ends.from;
                maxX = ends.to;
            }
            if (me.xField && !Ext.isNumber(minX)) {
                out = me.getMinMaxYValues();
                minX = out[0];
                maxX = out[1];
            }
            if (!Ext.isNumber(minX)) {
                minX = 0;
            }
            if (!Ext.isNumber(maxX)) {
                maxX = 0;
            }
            gutters = me.getGutters();
            colsScale = (bbox.width - (gutters.lower + gutters.upper)) / ((maxX - minX) || 1);

            colsZero = bbox.x + gutters.lower;
        
            barsLoc = [];
            for (i = 0, ln = data.length; i < ln; i++) {
                record = data[i];
                rec = record.get(me.xField);
                barsLoc[i] = colsZero + (rec - minX) * colsScale - (groupBarWidth / 2);
            }
        }

        return {
            bars: bars,
            barsLoc: barsLoc,
            bbox: bbox,
            shrunkBarWidth: shrunkBarWidth,
            barsLen: barsLen,
            groupBarsLen: groupBarsLen,
            barWidth: barWidth,
            groupBarWidth: groupBarWidth,
            scale: scale,
            zero: zero,
            padding: padding,
            signed: minY / maxY < 0,
            minY: minY,
            maxY: maxY
        };
    },

    // @private Build an array of paths for the chart
    getPaths: function() {
        var me = this,
            chart = me.chart,
            store = chart.getChartStore(),
            data = store.data.items,
            i, total, record,
            bounds = me.bounds = me.getBounds(),
            items = me.items = [],
            yFields = Ext.isArray(me.yField) ? me.yField : [me.yField],
            gutter = me.gutter / 100,
            groupGutter = me.groupGutter / 100,
            animate = chart.animate,
            column = me.column,
            group = me.group,
            enableShadows = chart.shadow,
            shadowGroups = me.shadowGroups,
            shadowAttributes = me.shadowAttributes,
            shadowGroupsLn = shadowGroups.length,
            bbox = bounds.bbox,
            barWidth = bounds.barWidth,
            shrunkBarWidth = bounds.shrunkBarWidth,
            padding = me.getPadding(),
            stacked = me.stacked,
            barsLen = bounds.barsLen,
            colors = me.colorArrayStyle,
            colorLength = colors && colors.length || 0,
            themeIndex = me.themeIdx,
            math = Math,
            mmax = math.max,
            mmin = math.min,
            mabs = math.abs,
            j, yValue, height, totalDim, totalNegDim, bottom, top, hasShadow, barAttr, attrs, counter,
            totalPositiveValues, totalNegativeValues,
            shadowIndex, shadow, sprite, offset, floorY, idx;

        for (i = 0, total = data.length; i < total; i++) {
            record = data[i];
            bottom = bounds.zero;
            top = bounds.zero;
            totalDim = 0;
            totalNegDim = 0;
            totalPositiveValues = totalNegativeValues = 0;
            hasShadow = false;
            for (j = 0, counter = 0; j < barsLen; j++) {
                // Excluded series
                if (me.__excludes && me.__excludes[j]) {
                    continue;
                }
                yValue = record.get(bounds.bars[j]);
                if (yValue >= 0) {
                    totalPositiveValues += yValue;
                }
                else {
                    totalNegativeValues += yValue;
                }
                height = Math.round((yValue - mmax(bounds.minY, 0)) * bounds.scale);
                idx = themeIndex + (barsLen > 1 ? j : 0);
                barAttr = {
                    fill: colors[idx % colorLength]
                };
                if (column) {
                    Ext.apply(barAttr, {
                        height: height,
                        width: mmax(bounds.groupBarWidth, 0),
                        x: (me.boundColumn ? bounds.barsLoc[i] 
                                           : (bbox.x + padding.left 
                                                + (barWidth - shrunkBarWidth) * 0.5
                                                + i * barWidth * (1 + gutter) 
                                                + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked)),
                        y: bottom - height
                    });
                }
                else {
                    // draw in reverse order
                    offset = (total - 1) - i;
                    Ext.apply(barAttr, {
                        height: mmax(bounds.groupBarWidth, 0),
                        width: height + (bottom == bounds.zero),
                        x: bottom + (bottom != bounds.zero),
                        y: (bbox.y + padding.top 
                            + (barWidth - shrunkBarWidth) * 0.5 
                            + offset * barWidth * (1 + gutter) 
                            + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked + 1)
                    });
                }
                if (height < 0) {
                    if (column) {
                        barAttr.y = top;
                        barAttr.height = mabs(height);
                    } else {
                        barAttr.x = top + height;
                        barAttr.width = mabs(height);
                    }
                }
                if (stacked) {
                    if (height < 0) {
                        top += height * (column ? -1 : 1);
                    } else {
                        bottom += height * (column ? -1 : 1);
                    }
                    totalDim += mabs(height);
                    if (height < 0) {
                        totalNegDim += mabs(height);
                    }
                }
                barAttr.x = Math.floor(barAttr.x) + 1;
                floorY = Math.floor(barAttr.y);
                if (Ext.isIE8m && barAttr.y > floorY) {
                    floorY--;
                }
                barAttr.y = floorY;
                barAttr.width = Math.floor(barAttr.width);
                barAttr.height = Math.floor(barAttr.height);
                items.push({
                    series: me,
                    yField: yFields[j],
                    storeItem: record,
                    value: [record.get(me.xField), yValue],
                    attr: barAttr,
                    point: column ? [barAttr.x + barAttr.width / 2, yValue >= 0 ? barAttr.y : barAttr.y + barAttr.height] :
                                    [yValue >= 0 ? barAttr.x + barAttr.width : barAttr.x, barAttr.y + barAttr.height / 2]
                });
                // When resizing, reset before animating
                if (animate && chart.resizing) {
                    attrs = column ? {
                        x: barAttr.x,
                        y: bounds.zero,
                        width: barAttr.width,
                        height: 0
                    } : {
                        x: bounds.zero,
                        y: barAttr.y,
                        width: 0,
                        height: barAttr.height
                    };
                    if (enableShadows && (stacked && !hasShadow || !stacked)) {
                        hasShadow = true;
                        //update shadows
                        for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
                            shadow = shadowGroups[shadowIndex].getAt(stacked ? i : (i * barsLen + j));
                            if (shadow) {
                                shadow.setAttributes(attrs, true);
                            }
                        }
                    }
                    //update sprite position and width/height
                    sprite = group.getAt(i * barsLen + j);
                    if (sprite) {
                        sprite.setAttributes(attrs, true);
                    }
                }
                counter++;
            }
            if (stacked && items.length) {
                items[i * counter].totalDim = totalDim;
                items[i * counter].totalNegDim = totalNegDim;
                items[i * counter].totalPositiveValues = totalPositiveValues;
                items[i * counter].totalNegativeValues = totalNegativeValues;
            }
        }
        if (stacked && counter == 0) {
            // Remove ghost shadow ref: EXTJSIV-5982
            for (i = 0, total = data.length; i < total; i++) {
                for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
                    shadow = shadowGroups[shadowIndex].getAt(i);
                    if (shadow) {
                        shadow.hide(true);
                    }
                }
            }
        }
    },

    // @private render/setAttributes on the shadows
    renderShadows: function(i, barAttr, baseAttrs, bounds) {
        var me = this,
            chart = me.chart,
            surface = chart.surface,
            animate = chart.animate,
            stacked = me.stacked,
            shadowGroups = me.shadowGroups,
            shadowAttributes = me.shadowAttributes,
            shadowGroupsLn = shadowGroups.length,
            store = chart.getChartStore(),
            column = me.column,
            items = me.items,
            shadows = [],
            zero = bounds.zero,
            shadowIndex, shadowBarAttr, shadow, totalDim, totalNegDim, j, rendererAttributes;

        if ((stacked && (i % bounds.groupBarsLen === 0)) || !stacked) {
            j = i / bounds.groupBarsLen;
            //create shadows
            for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
                shadowBarAttr = Ext.apply({}, shadowAttributes[shadowIndex]);
                shadow = shadowGroups[shadowIndex].getAt(stacked ? j : i);
                Ext.copyTo(shadowBarAttr, barAttr, 'x,y,width,height');
                if (!shadow) {
                    shadow = surface.add(Ext.apply({
                        type: 'rect',
                        group: shadowGroups[shadowIndex]
                    }, Ext.apply({}, baseAttrs, shadowBarAttr)));
                }
                if (stacked) {
                    totalDim = items[i].totalDim;
                    totalNegDim = items[i].totalNegDim;
                    if (column) {
                        shadowBarAttr.y = zero + totalNegDim - totalDim - 1;
                        shadowBarAttr.height = totalDim;
                    }
                    else {
                        shadowBarAttr.x = zero - totalNegDim;
                        shadowBarAttr.width = totalDim;
                    }
                }
                
                rendererAttributes = me.renderer(shadow, store.getAt(j), shadowBarAttr, i, store);
                rendererAttributes.hidden = !!barAttr.hidden;
                if (animate) {
                    me.onAnimate(shadow, { to: rendererAttributes });
                }
                else {
                    shadow.setAttributes(rendererAttributes, true);
                }
                shadows.push(shadow);
            }
        }
        return shadows;
    },

    /**
     * Draws the series for the current chart.
     */
    drawSeries: function() {
        var me = this,
            chart = me.chart,
            store = chart.getChartStore(),
            surface = chart.surface,
            animate = chart.animate,
            stacked = me.stacked,
            column = me.column,
            chartAxes = chart.axes,
            boundAxes = me.getAxesForXAndYFields(),
            boundXAxis = boundAxes.xAxis,
            boundYAxis = boundAxes.yAxis,
            enableShadows = chart.shadow,
            shadowGroups = me.shadowGroups,
            shadowGroupsLn = shadowGroups.length,
            group = me.group,
            seriesStyle = me.seriesStyle,
            items, ln, i, j, baseAttrs, sprite, rendererAttributes, shadowIndex, shadowGroup,
            bounds, endSeriesStyle, barAttr, attrs, anim;

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

        //fill colors are taken from the colors array.
        endSeriesStyle = Ext.apply({}, this.style, seriesStyle);
        delete endSeriesStyle.fill;
        delete endSeriesStyle.x;
        delete endSeriesStyle.y;
        delete endSeriesStyle.width;
        delete endSeriesStyle.height;
        
        me.unHighlightItem();
        me.cleanHighlights();
        
        me.boundColumn = (boundXAxis && Ext.Array.contains(me.axis,boundXAxis) 
                            && chartAxes.get(boundXAxis) 
                            && chartAxes.get(boundXAxis).isNumericAxis);

        me.getPaths();
        bounds = me.bounds;
        items = me.items;

        baseAttrs = column ? {
            y: bounds.zero,
            height: 0
        } : {
            x: bounds.zero,
            width: 0
        };
        ln = items.length;

        // Create new or reuse sprites and animate/display
        for (i = 0; i < ln; i++) {
            sprite = group.getAt(i);
            barAttr = items[i].attr;

            if (enableShadows) {
                items[i].shadows = me.renderShadows(i, barAttr, baseAttrs, bounds);
            }

            // Create a new sprite if needed (no height)
            if (!sprite) {
                attrs = Ext.apply({}, baseAttrs, barAttr);
                attrs = Ext.apply(attrs, endSeriesStyle || {});
                sprite = surface.add(Ext.apply({}, {
                    type: 'rect',
                    group: group
                }, attrs));
            }
            if (animate) {
                rendererAttributes = me.renderer(sprite, store.getAt(i), barAttr, i, store);
                sprite._to = rendererAttributes;
                anim = me.onAnimate(sprite, { to: Ext.apply(rendererAttributes, endSeriesStyle) });
                if (enableShadows && stacked && (i % bounds.barsLen === 0)) {
                    j = i / bounds.barsLen;
                    for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
                        anim.on('afteranimate', function() {
                            this.show(true);
                        }, shadowGroups[shadowIndex].getAt(j));
                    }
                }
            }
            else {
                rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(barAttr, { hidden: false }), i, store);
                sprite.setAttributes(Ext.apply(rendererAttributes, endSeriesStyle), true);
            }
            items[i].sprite = sprite;
        }

        // Hide unused sprites
        ln = group.getCount();
        for (j = i; j < ln; j++) {
            group.getAt(j).hide(true);
        }
        
        if (me.stacked) {
            // If stacked, we have only store.getCount() shadows.
            i = store.getCount();    
        }
        
        // Hide unused shadows
        if (enableShadows) {
            for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
                shadowGroup = shadowGroups[shadowIndex];
                ln = shadowGroup.getCount();
                for (j = i; j < ln; j++) {
                    shadowGroup.getAt(j).hide(true);
                }
            }
        }
        me.renderLabels();
    },

    // @private called when a label is to be created.
    onCreateLabel: function(storeItem, item, i, display) {
        var me = this,
            surface = me.chart.surface,
            group = me.labelsGroup,
            config = me.label,
            endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}),
            sprite;

        return surface.add(Ext.apply({
            type: 'text',
            group: group
        }, endLabelStyle || {}));
    },

    // @private called when a label is to be positioned.
    onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
        // Determine the label's final position. Starts with the configured preferred value but
        // may get flipped from inside to outside or vice-versa depending on space.
        var me = this,
            opt = me.bounds,
            groupBarWidth = opt.groupBarWidth,
            column = me.column,
            chart = me.chart,
            chartBBox = chart.chartBBox,
            resizing = chart.resizing,
            xValue = item.value[0],
            yValue = item.value[1],
            attr = item.attr,
            config = me.label,
            stacked = me.stacked,
            stackedDisplay = config.stackedDisplay,
            rotate = (config.orientation == 'vertical'),
            field = [].concat(config.field),
            format = config.renderer,
            text, size, width, height,
            zero = opt.zero,
            insideStart = 'insideStart',
            insideEnd = 'insideEnd',
            outside = 'outside',
            over = 'over',
            under = 'under',
            labelMarginX = 4,   // leave space around the labels (important when saving chart as image)
            labelMarginY = 2,
            signed = opt.signed,
            x, y, finalAttr;

        if (display == insideStart || display == insideEnd || display == outside) {
            if (stacked && (display == outside)) {
                // It doesn't make sense to use 'outside' on a stacked chart
                // unless we only want to display the 'stackedDisplay' labels.
                label.hide(true);
                return;
            }
            label.setAttributes({
                // Reset the style in case the label is being reused (for instance, if a series is excluded)
                // and do it before calling the renderer function.
                style: undefined
            });
            text = (Ext.isNumber(index) ? format(storeItem.get(field[index]), label, storeItem, item, i, display, animate, index) : '');
            label.setAttributes({
                // Set the text onto the label.
                text: text
            });
            size = me.getLabelSize(text, label.attr.style);
            width = size.width;
            height = size.height;
            if (column) {
                //-----------------------------------------
                // Position the label within a column chart
            
                // If there is no label to display, or if the corresponding box in a stacked column 
                // isn't tall enough to display the label, then leave.
                if (!width || !height || (stacked && (attr.height < height))) {
                    label.hide(true);
                    return;
                }
                
                // Align horizontally the label in the middle of the column
                x = attr.x + (rotate ? groupBarWidth/2 : (groupBarWidth - width)/2);
                
                // If the label is to be displayed outside, make sure there is room for it, otherwise display it inside.
                if (display == outside) {
                    var free = (yValue >= 0 ? (attr.y - chartBBox.y) : (chartBBox.y + chartBBox.height - attr.y - attr.height));
                    if (free < height + labelMarginY) {
                        display = insideEnd;
                    }
                }
    
                // If the label is to be displayed inside a non-stacked chart, make sure it is 
                // not taller than the box, otherwise move it outside.
                if (!stacked && (display != outside)) {
                    if (height + labelMarginY > attr.height) {
                        display = outside;
                    }
                }
    
                // Place the label vertically depending on its config and on whether the value
                // it represents is positive (above the X-axis) or negative (below the X-axis)
                if (!y) {
                    y = attr.y;
                    if (yValue >= 0) {
                        switch (display) {
                            case insideStart: y += attr.height + (rotate ? -labelMarginY : -height/2);  break;
                            case insideEnd:   y += (rotate ? height + labelMarginX : height/2);         break;
                            case outside:     y += (rotate ? -labelMarginY : -height/2);                break;
                        }
                    } else {
                        switch (display) {
                            case insideStart: y += (rotate ? height + labelMarginY : height/2);                             break;
                            case insideEnd:   y += (rotate ? attr.height - labelMarginY : attr.height - height/2);          break;
                            case outside:     y += (rotate ? attr.height + height + labelMarginY : attr.height + height/2); break;
                        }
                    }
                }
            }
            else {
                //-----------------------------------------
                // Position the label within a bar chart
    
                // If there is no label to display, or if the corresponding box has no width, then leave.
                if (!width || !height || (stacked && !attr.width)) {
                    label.hide(true);
                    return;
                }
    
                // Align vertically the label in the middle of the bar
                y = attr.y + (rotate ? (groupBarWidth + height)/2 : groupBarWidth/2);
    
                // If the label is to be displayed outside, make sure there is room for it otherwise display it inside.
                if (display == outside) {
                    var free = (yValue >= 0 ? (chartBBox.x + chartBBox.width - attr.x - attr.width) :  (attr.x - chartBBox.x));
                    if (free < width + labelMarginX) {
                        display = insideEnd;
                    }
                }
    
                // If the label is to be displayed inside (and it is not rotated yet), make sure it is
                // not wider than the box it represents otherwise (for a stacked chart) rotate it vertically
                // and center it, or (for a non-stacked chart) move it outside.
                if ((display != outside) && !rotate) {
                    if (width + labelMarginX > attr.width) {
                        if (stacked) {
                            if (height > attr.width) {
                                label.hide(true);
                                return; // Even rotated, there isn't enough room.
                            }
                            x = attr.x + attr.width/2;
                            y = attr.y + attr.height - (attr.height - width)/2;
                            rotate = true;
                        } else {
                            display = outside;
                        }
                    }
                }
    
                // Place the label horizontally depending on its config and on whether the value
                // it represents is positive (above the X-axis) or negative (below the X-axis)
                if (!x) {
                    x = attr.x;
                    if (yValue >= 0) {
                        switch (display) {
                            case insideStart: x += (rotate ? width/2 : labelMarginX);                           break;
                            case insideEnd:   x += attr.width + (rotate ? -width/2 : -width - labelMarginX);    break;
                            case outside:     x += attr.width + (rotate ? width/2 : labelMarginX);              break;
                        }
                    } else {
                        switch (display) {
                            case insideStart: x += attr.width + (rotate ? -width/2 : -width - labelMarginX);    break;
                            case insideEnd:   x += (rotate ? width/2 : labelMarginX);                           break;
                            case outside:     x += (rotate ? -width/2 : -width - labelMarginX);                 break;
                        }
                    }
                }
            }
        } else if (display == over || display == under) {
            if (stacked && stackedDisplay) {
                //-----------------------------------------
                // Position the label on top or at the bottom of a stacked bar/column
    
                text = label.attr.text;
                label.setAttributes({
                    // The text is already set onto the label: we just need to set the style
                    // (but don't overwrite any custom style that might have been set by an app override).
                    style: Ext.applyIf((label.attr && label.attr.style) || {},
                        {
                            'font-weight':'bold',
                            'font-size':'14px'
                        }
                    )
                });

                size = me.getLabelSize(text, label.attr.style);
                width = size.width;
                height = size.height;
    
                switch (display) {
                    case over:
                        if (column) {
                            x = attr.x + (rotate ? groupBarWidth/2 : (groupBarWidth - width)/2);
                            y = zero - (item.totalDim - item.totalNegDim) - height/2 - labelMarginY;
                        } else {
                            x = zero + (item.totalDim - item.totalNegDim) + labelMarginX;
                            y = attr.y + (rotate ? (groupBarWidth + height)/2 : groupBarWidth/2);
                        }
                        break;
                    case under:
                        if (column) {
                            x = attr.x + (rotate ? groupBarWidth/2 : (groupBarWidth - width)/2);
                            y = zero + item.totalNegDim + height/2;
                        } else {
                            x = zero - item.totalNegDim - width - labelMarginX;
                            y = attr.y + (rotate ? (groupBarWidth + height)/2 : groupBarWidth/2);
                        }
                        break;
                }
            }
        }
        
        if (x == undefined || y == undefined) {
            // bad configuration: x/y are not set
            label.hide(true);
            return;
        }

        label.isOutside = (display == outside);
        label.setAttributes({
            text: text
        });

        //set position
        finalAttr = {
            x: x,
            y: y
        };
        //rotate
        if (rotate) {
            finalAttr.rotate = {
                x: x,
                y: y,
                degrees: 270
            };
        }
        //check for resizing
        if (animate && resizing) {
            if (column) {
                x = attr.x + attr.width / 2;
                y = zero;
            } else {
                x = zero;
                y = attr.y + attr.height / 2;
            }
            label.setAttributes({
                x: x,
                y: y
            }, true);
            if (rotate) {
                label.setAttributes({
                    rotate: {
                        x: x,
                        y: y,
                        degrees: 270
                    }
                }, true);
            }
        }
        //handle animation
        if (animate) {
            me.onAnimate(label, { to: finalAttr });
        }
        else {
            label.setAttributes(Ext.apply(finalAttr, {
                hidden: false
            }), true);
        }
    },

    /* @private
     * Gets the dimensions of a given bar label. Uses a single hidden sprite to avoid
     * changing visible sprites.
     * @param value
     */
    getLabelSize: function(value, labelStyle) {
        var tester = this.testerLabel,
            config = this.label,
            endLabelStyle = Ext.apply({}, config, labelStyle, this.seriesLabelStyle || {}),
            rotated = config.orientation === 'vertical',
            bbox, w, h,
            undef;
        if (!tester) {
            tester = this.testerLabel = this.chart.surface.add(Ext.apply({
                type: 'text',
                opacity: 0
            }, endLabelStyle));
        }
        tester.setAttributes({
            style: labelStyle,
            text: value
        }, true);

        // Flip the width/height if rotated, as getBBox returns the pre-rotated dimensions
        bbox = tester.getBBox();
        w = bbox.width;
        h = bbox.height;
        return {
            width: rotated ? h : w,
            height: rotated ? w : h
        };
    },

    // @private used to animate label, markers and other sprites.
    onAnimate: function(sprite, attr) {
        sprite.show();
        return this.callParent(arguments);
    },

    isItemInPoint: function(x, y, item) {
        var bbox = item.sprite.getBBox();
        return bbox.x <= x && bbox.y <= y
            && (bbox.x + bbox.width) >= x
            && (bbox.y + bbox.height) >= y;
    },

    // @private hide all markers
    hideAll: function(index) {
        var axes      = this.chart.axes,
            axesItems = axes.items,
            ln        = axesItems.length,
            i         = 0;

        index = (isNaN(this._index) ? index : this._index) || 0;

        if (!this.__excludes) {
            this.__excludes = [];
        }

        this.__excludes[index] = true;
        this.drawSeries();

        for (i; i < ln; i++) {
            axesItems[i].drawAxis();
        }    
    },

    // @private show all markers
    showAll: function(index) {
        var axes = this.chart.axes,
            axesItems = axes.items,
            ln        = axesItems.length,
            i         = 0;

        index = (isNaN(this._index) ? index : this._index) || 0;

        if (!this.__excludes) {
            this.__excludes = [];
        }

        this.__excludes[index] = false;
        this.drawSeries();

        for (i; i < ln; i++) {
            axesItems[i].drawAxis();
        }    
    },

    /**
     * Returns a string with the color to be used for the series legend item.
     * @param index
     */
    getLegendColor: function(index) {
        var me = this,
            colorLength = me.colorArrayStyle.length;

        if (me.style && me.style.fill) {
            return me.style.fill;
        } else {
            return me.colorArrayStyle[index % colorLength];
        }
    },

    highlightItem: function(item) {
        this.callParent(arguments);
        this.renderLabels();
    },

    unHighlightItem: function() {
        this.callParent(arguments);
        this.renderLabels();
    },

    cleanHighlights: function() {
        this.callParent(arguments);
        this.renderLabels();
    }
});
