    /**
     * @class Ext.chart.series.Line
     * @extends Ext.chart.series.Cartesian
     *
     * Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different
     * categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset.
     * As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart
     * documentation for more information. A typical configuration object for the line 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': 4,  'data2': 4,  'data3': 36, 'data4': 13, 'data5': 33 }
     *         ]
     *     });
     *
     *     Ext.create('Ext.chart.Chart', {
     *         renderTo: Ext.getBody(),
     *         width: 500,
     *         height: 300,
     *         animate: true,
     *         store: store,
     *         axes: [
     *             {
     *                 type: 'Numeric',
     *                 position: 'left',
     *                 fields: ['data1', 'data2'],
     *                 label: {
     *                     renderer: Ext.util.Format.numberRenderer('0,0')
     *                 },
     *                 title: 'Sample Values',
     *                 grid: true,
     *                 minimum: 0
     *             },
     *             {
     *                 type: 'Category',
     *                 position: 'bottom',
     *                 fields: ['name'],
     *                 title: 'Sample Metrics'
     *             }
     *         ],
     *         series: [
     *             {
     *                 type: 'line',
     *                 highlight: {
     *                     size: 7,
     *                     radius: 7
     *                 },
     *                 axis: 'left',
     *                 xField: 'name',
     *                 yField: 'data1',
     *                 markerConfig: {
     *                     type: 'cross',
     *                     size: 4,
     *                     radius: 4,
     *                     'stroke-width': 0
     *                 }
     *             },
     *             {
     *                 type: 'line',
     *                 highlight: {
     *                     size: 7,
     *                     radius: 7
     *                 },
     *                 axis: 'left',
     *                 fill: true,
     *                 xField: 'name',
     *                 yField: 'data2',
     *                 markerConfig: {
     *                     type: 'circle',
     *                     size: 4,
     *                     radius: 4,
     *                     'stroke-width': 0
     *                 }
     *             }
     *         ]
     *     });
     *
     * In this configuration we're adding two series (or lines), one bound to the `data1`
     * property of the store and the other to `data3`. The type for both configurations is
     * `line`. The `xField` for both series is the same, the name propert of the store.
     * Both line series share the same axis, the left axis. You can set particular marker
     * configuration by adding properties onto the markerConfig object. Both series have
     * an object as highlight so that markers animate smoothly to the properties in highlight
     * when hovered. The second series has `fill=true` which means that the line will also
     * have an area below it of the same color.
     *
     * **Note:** In the series definition remember to explicitly set the axis to bind the
     * values of the line series to. This can be done by using the `axis` configuration property.
     */
    Ext.define('Ext.chart.series.Line', {

        /* Begin Definitions */

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

        alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'],

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

        /* End Definitions */

        type: 'line',

        alias: 'series.line',

        /**
         * @cfg {String} axis
         * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
         * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
         * relative scale will be used.
         */

        /**
         * @cfg {Number} selectionTolerance
         * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
         */
        selectionTolerance: 20,

        /**
         * @cfg {Boolean} showMarkers
         * Whether markers should be displayed at the data points along the line. If true,
         * then the {@link #markerConfig} config item will determine the markers' styling.
         */
        showMarkers: true,

        /**
         * @cfg {Object} markerConfig
         * The display style for the markers. Only used if {@link #showMarkers} is true.
         * The markerConfig is a configuration object containing the same set of properties defined in
         * the Sprite class. For example, if we were to set red circles as markers to the line series we could
         * pass the object:
         *
         <pre><code>
            markerConfig: {
                type: 'circle',
                radius: 4,
                'fill': '#f00'
            }
         </code></pre>

         */
        markerConfig: {},

        /**
         * @cfg {Object} style
         * An object containing style properties for the visualization lines and fill.
         * These styles will override the theme styles.  The following are valid style properties:
         *
         * - `stroke` - an rgb or hex color string for the background color of the line
         * - `stroke-width` - the width of the stroke (integer)
         * - `fill` - the background fill color string (hex or rgb), only works if {@link #fill} is `true`
         * - `opacity` - the opacity of the line and the fill color (decimal)
         *
         * Example usage:
         *
         *     style: {
         *         stroke: '#00ff00',
         *         'stroke-width': 10,
         *         fill: '#80A080',
         *         opacity: 0.2
         *     }
         */
        style: {},

        /**
         * @cfg {Boolean/Number} smooth
         * If set to `true` or a non-zero number, the line will be smoothed/rounded around its points; otherwise
         * straight line segments will be drawn.
         *
         * A numeric value is interpreted as a divisor of the horizontal distance between consecutive points in
         * the line; larger numbers result in sharper curves while smaller numbers result in smoother curves.
         *
         * If set to `true` then a default numeric value of 3 will be used. Defaults to `false`.
         */
        smooth: false,

        /**
         * @private Default numeric smoothing value to be used when {@link #smooth} = true.
         */
        defaultSmoothness: 3,

        /**
         * @cfg {Boolean} fill
         * If true, the area below the line will be filled in using the {@link #style eefill} and
         * {@link #style opacity} config properties. Defaults to false.
         */
        fill: 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({ 'stroke-width': 3 }, config.highlightCfg);
            Ext.apply(me, config, {
                shadowAttributes: [{
                    "stroke-width": 6,
                    "stroke-opacity": 0.05,
                    stroke: 'rgb(0, 0, 0)',
                    translate: {
                        x: 1,
                        y: 1
                    }
                }, {
                    "stroke-width": 4,
                    "stroke-opacity": 0.1,
                    stroke: 'rgb(0, 0, 0)',
                    translate: {
                        x: 1,
                        y: 1
                    }
                }, {
                    "stroke-width": 2,
                    "stroke-opacity": 0.15,
                    stroke: 'rgb(0, 0, 0)',
                    translate: {
                        x: 1,
                        y: 1
                    }
                }]
            });
            me.group = surface.getGroup(me.seriesId);
            if (me.showMarkers) {
                me.markerGroup = surface.getGroup(me.seriesId + '-markers');
            }
            if (shadow) {
                for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
                    me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
                }
            }
        },

        // @private makes an average of points when there are more data points than pixels to be rendered.
        shrink: function(xValues, yValues, size) {
            // Start at the 2nd point...
            var len = xValues.length,
                ratio = Math.floor(len / size),
                i = 1,
                xSum = 0,
                ySum = 0,
                xRes = [+xValues[0]],
                yRes = [+yValues[0]];

            for (; i < len; ++i) {
                xSum += +xValues[i] || 0;
                ySum += +yValues[i] || 0;
                if (i % ratio == 0) {
                    xRes.push(xSum/ratio);
                    yRes.push(ySum/ratio);
                    xSum = 0;
                    ySum = 0;
                }
            }
            return {
                x: xRes,
                y: yRes
            };
        },

        /**
         * Draws the series for the current chart.
         */
        drawSeries: function() {
            var me = this,
                chart = me.chart,
                chartAxes = chart.axes,
                store = chart.getChartStore(),
                data = store.data.items,
                record,
                storeCount = store.getCount(),
                surface = me.chart.surface,
                bbox = {},
                group = me.group,
                showMarkers = me.showMarkers,
                markerGroup = me.markerGroup,
                enableShadows = chart.shadow,
                shadowGroups = me.shadowGroups,
                shadowAttributes = me.shadowAttributes,
                smooth = me.smooth,
                lnsh = shadowGroups.length,
                dummyPath = ["M"],
                path = ["M"],
                renderPath = ["M"],
                smoothPath = ["M"],
                markerIndex = chart.markerIndex,
                axes = [].concat(me.axis),
                shadowBarAttr,
                xValues = [],
                xValueMap = {},
                yValues = [],
                yValueMap = {},
                onbreak = false,
                storeIndices = [],
                markerStyle = me.markerStyle,
                seriesStyle = me.seriesStyle,
                colorArrayStyle = me.colorArrayStyle,
                colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
                isNumber = Ext.isNumber,
                seriesIdx = me.seriesIdx,
                boundAxes = me.getAxesForXAndYFields(),
                boundXAxis = boundAxes.xAxis,
                boundYAxis = boundAxes.yAxis,
                shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
                x, y, prevX, prevY, firstX, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
                yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle,
                endLineStyle, type, count, opacity, lineOpacity, fillOpacity, fillDefaultValue;

            if (me.fireEvent('beforedraw', me) === false) {
                return;
            }

            //if store is empty or the series is excluded in the legend then there's nothing to draw.
            if (!storeCount || me.seriesIsHidden) {
                me.hide();
                me.items = [];
                if (me.line) {
                    me.line.hide(true);
                    if (me.line.shadows) {
                        shadows = me.line.shadows;
                        for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
                            shadow = shadows[j];
                            shadow.hide(true);
                        }
                    }
                    if (me.fillPath) {
                        me.fillPath.hide(true);
                    }
                }
                me.line = null;
                me.fillPath = null;
                return;
            }

            //prepare style objects for line and markers
            endMarkerStyle = Ext.apply(markerStyle || {}, me.markerConfig, {
                fill: me.seriesStyle.fill || colorArrayStyle[seriesIdx % colorArrayStyle.length]
            });
            type = endMarkerStyle.type;
            delete endMarkerStyle.type;
            endLineStyle = seriesStyle;
            //if no stroke with is specified force it to 0.5 because this is
            //about making *lines*
            if (!endLineStyle['stroke-width']) {
                endLineStyle['stroke-width'] = 0.5;
            }

            //set opacity values
            opacity = 'opacity' in endLineStyle ? endLineStyle.opacity : 1;
            fillDefaultValue = 'opacity' in endLineStyle ? endLineStyle.opacity : 0.3;
            lineOpacity = 'lineOpacity' in endLineStyle ? endLineStyle.lineOpacity : opacity;
            fillOpacity = 'fillOpacity' in endLineStyle ? endLineStyle.fillOpacity : fillDefaultValue;

            //If we're using a time axis and we need to translate the points,
            //then reuse the first markers as the last markers.
            if (markerIndex && markerGroup && markerGroup.getCount()) {
                for (i = 0; i < markerIndex; i++) {
                    marker = markerGroup.getAt(i);
                    markerGroup.remove(marker);
                    markerGroup.add(marker);
                    markerAux = markerGroup.getAt(markerGroup.getCount() - 2);
                    marker.setAttributes({
                        x: 0,
                        y: 0,
                        translate: {
                            x: markerAux.attr.translation.x,
                            y: markerAux.attr.translation.y
                        }
                    }, true);
                }
            }

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

            me.setBBox();
            bbox = me.bbox;
            me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];

            if (axis = chartAxes.get(boundXAxis)) {
                ends = axis.applyData();
                minX = ends.from;
                maxX = ends.to;
            }

            if (axis = chartAxes.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();
                minX = axis[0];
                maxX = axis[1];
            }

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

            if (isNaN(minX)) {
                minX = 0;
                xScale = bbox.width / ((storeCount - 1) || 1);
            }
            else {
                xScale = bbox.width / ((maxX - minX) || (storeCount -1) || 1);
            }

            if (isNaN(minY)) {
                minY = 0;
                yScale = bbox.height / ((storeCount - 1) || 1);
            }
            else {
                yScale = bbox.height / ((maxY - minY) || (storeCount - 1) || 1);
            }
            // Extract all x and y values from the store
            for (i = 0, ln = data.length; i < ln; i++) {
                record = data[i];
                xValue = record.get(me.xField);

                // Ensure a value
                if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)
                    //set as uniform distribution if the axis is a category axis.
                    || boundXAxis && chartAxes.get(boundXAxis) && chartAxes.get(boundXAxis).type == 'Category') {
                        if (xValue in xValueMap) {
                            xValue = xValueMap[xValue];
                        } else {
                            xValue = xValueMap[xValue] = i;
                        }
                }

                // Filter out values that don't fit within the pan/zoom buffer area
                yValue = record.get(me.yField);
                //skip undefined values
                if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
                    //<debug warn>
                    if (Ext.isDefined(Ext.global.console)) {
                        Ext.global.console.warn("[Ext.chart.series.Line]  Skipping a store element with an undefined value at ", record, xValue, yValue);
                    }
                    //</debug>
                    continue;
                }
                // Ensure a value
                if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)
                    //set as uniform distribution if the axis is a category axis.
                    || boundYAxis && chartAxes.get(boundYAxis) && chartAxes.get(boundYAxis).type == 'Category') {
                    yValue = i;
                }
                storeIndices.push(i);
                xValues.push(xValue);
                yValues.push(yValue);
            }

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

            me.items = [];

            count = 0;
            ln = xValues.length;
            for (i = 0; i < ln; i++) {
                xValue = xValues[i];
                yValue = yValues[i];
                if (yValue === false) {
                    if (path.length == 1) {
                        path = [];
                    }
                    onbreak = true;
                    me.items.push(false);
                    continue;
                } else {
                    x = (bbox.x + (xValue - minX) * xScale).toFixed(2);
                    y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2);
                    if (onbreak) {
                        onbreak = false;
                        path.push('M');
                    }
                    path = path.concat([x, y]);
                }
                if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) {
                    firstY = y;
                    firstX = x;
                }
                // If this is the first line, create a dummypath to animate in from.
                if (!me.line || chart.resizing) {
                    dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]);
                }

                // When resizing, reset before animating
                if (chart.animate && chart.resizing && me.line) {
                    me.line.setAttributes({
                        path: dummyPath,
                        opacity: lineOpacity
                    }, true);
                    if (me.fillPath) {
                        me.fillPath.setAttributes({
                            path: dummyPath,
                            opacity: fillOpacity
                        }, true);
                    }
                    if (me.line.shadows) {
                        shadows = me.line.shadows;
                        for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
                            shadow = shadows[j];
                            shadow.setAttributes({
                                path: dummyPath
                            }, true);
                        }
                    }
                }
                if (showMarkers) {
                    marker = markerGroup.getAt(count++);
                    if (!marker) {
                        marker = Ext.chart.Shape[type](surface, Ext.apply({
                            group: [group, markerGroup],
                            x: 0, y: 0,
                            translate: {
                                x: +(prevX || x),
                                y: prevY || (bbox.y + bbox.height / 2)
                            },
                            value: '"' + xValue + ', ' + yValue + '"',
                            zIndex: 4000
                        }, endMarkerStyle));
                        marker._to = {
                            translate: {
                                x: +x,
                                y: +y
                            }
                        };
                    } else {
                        marker.setAttributes({
                            value: '"' + xValue + ', ' + yValue + '"',
                            x: 0, y: 0,
                            hidden: false
                        }, true);
                        marker._to = {
                            translate: {
                                x: +x,
                                y: +y
                            }
                        };
                    }
                }
                me.items.push({
                    series: me,
                    value: [xValue, yValue],
                    point: [x, y],
                    sprite: marker,
                    storeItem: store.getAt(storeIndices[i])
                });
                prevX = x;
                prevY = y;
            }

            if (path.length <= 1) {
                //nothing to be rendered
                return;
            }

            if (me.smooth) {
                smoothPath = Ext.draw.Draw.smooth(path, isNumber(smooth) ? smooth : me.defaultSmoothness);
            }

            renderPath = smooth ? smoothPath : path;

            //Correct path if we're animating timeAxis intervals
            if (chart.markerIndex && me.previousPath) {
                fromPath = me.previousPath;
                if (!smooth) {
                    Ext.Array.erase(fromPath, 1, 2);
                }
            } else {
                fromPath = path;
            }

            // Only create a line if one doesn't exist.
            if (!me.line) {
                me.line = surface.add(Ext.apply({
                    type: 'path',
                    group: group,
                    path: dummyPath,
                    stroke: endLineStyle.stroke || endLineStyle.fill
                }, endLineStyle || {}));

                //set configuration opacity
                me.line.setAttributes({
                    opacity: lineOpacity
                }, true);

                if (enableShadows) {
                    me.line.setAttributes(Ext.apply({}, me.shadowOptions), true);
                }

                //unset fill here (there's always a default fill withing the themes).
                me.line.setAttributes({
                    fill: 'none',
                    zIndex: 3000
                });
                if (!endLineStyle.stroke && colorArrayLength) {
                    me.line.setAttributes({
                        stroke: colorArrayStyle[seriesIdx % colorArrayLength]
                    }, true);
                }
                if (enableShadows) {
                    //create shadows
                    shadows = me.line.shadows = [];
                    for (shindex = 0; shindex < lnsh; shindex++) {
                        shadowBarAttr = shadowAttributes[shindex];
                        shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath });
                        shadow = surface.add(Ext.apply({}, {
                            type: 'path',
                            group: shadowGroups[shindex]
                        }, shadowBarAttr));
                        shadows.push(shadow);
                    }
                }
            }
            if (me.fill) {
                fillPath = renderPath.concat([
                    ["L", x, bbox.y + bbox.height],
                    ["L", firstX, bbox.y + bbox.height],
                    ["L", firstX, firstY]
                ]);
                if (!me.fillPath) {
                    me.fillPath = surface.add({
                        group: group,
                        type: 'path',
                        fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
                        path: dummyPath
                    });
                }
            }
            markerCount = showMarkers && markerGroup.getCount();
            if (chart.animate) {
                fill = me.fill;
                line = me.line;
                //Add renderer to line. There is not unique record associated with this.
                rendererAttributes = me.renderer(line, false, { path: renderPath }, i, store);
                Ext.apply(rendererAttributes, endLineStyle || {}, {
                    stroke: endLineStyle.stroke || endLineStyle.fill
                });
                //fill should not be used here but when drawing the special fill path object
                delete rendererAttributes.fill;
                line.show(true);
                if (chart.markerIndex && me.previousPath) {
                    me.animation = animation = me.onAnimate(line, {
                        to: rendererAttributes,
                        from: {
                            path: fromPath
                        }
                    });
                } else {
                    me.animation = animation = me.onAnimate(line, {
                        to: rendererAttributes
                    });
                }
                //animate shadows
                if (enableShadows) {
                    shadows = line.shadows;
                    for(j = 0; j < lnsh; j++) {
                        shadows[j].show(true);
                        if (chart.markerIndex && me.previousPath) {
                            me.onAnimate(shadows[j], {
                                to: { path: renderPath },
                                from: { path: fromPath }
                            });
                        } else {
                            me.onAnimate(shadows[j], {
                                to: { path: renderPath }
                            });
                        }
                    }
                }
                //animate fill path
                if (fill) {
                    me.fillPath.show(true);
                    me.onAnimate(me.fillPath, {
                        to: Ext.apply({}, {
                            path: fillPath,
                            fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
                            'stroke-width': 0,
                            opacity: fillOpacity
                        }, endLineStyle || {})
                    });
                }
                //animate markers
                if (showMarkers) {
                    count = 0;
                    for(i = 0; i < ln; i++) {
                        if (me.items[i]) {
                            item = markerGroup.getAt(count++);
                            if (item) {
                                rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
                                me.onAnimate(item, {
                                    to: Ext.apply(rendererAttributes, endMarkerStyle || {})
                                });
                                item.show(true);
                            }
                        }
                    }
                    for(; count < markerCount; count++) {
                        item = markerGroup.getAt(count);
                        item.hide(true);
                    }
    //                for(i = 0; i < (chart.markerIndex || 0)-1; i++) {
    //                    item = markerGroup.getAt(i);
    //                    item.hide(true);
    //                }
                }
            } else {
                rendererAttributes = me.renderer(me.line, false, { path: renderPath, hidden: false }, i, store);
                Ext.apply(rendererAttributes, endLineStyle || {}, {
                    stroke: endLineStyle.stroke || endLineStyle.fill
                });
                //fill should not be used here but when drawing the special fill path object
                delete rendererAttributes.fill;
                me.line.setAttributes(rendererAttributes, true);
                me.line.setAttributes({
                    opacity: lineOpacity
                }, true);
                //set path for shadows
                if (enableShadows) {
                    shadows = me.line.shadows;
                    for(j = 0; j < lnsh; j++) {
                        shadows[j].setAttributes({
                            path: renderPath,
                            hidden: false
                        }, true);
                    }
                }
                if (me.fill) {
                    me.fillPath.setAttributes({
                        path: fillPath,
                        hidden: false,
                        opacity: fillOpacity
                    }, true);
                }
                if (showMarkers) {
                    count = 0;
                    for(i = 0; i < ln; i++) {
                        if (me.items[i]) {
                            item = markerGroup.getAt(count++);
                            if (item) {
                                rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
                                item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true);
                                if (!item.attr.hidden) {
                                    item.show(true);
                                }
                            }
                        }
                    }
                    for(; count < markerCount; count++) {
                        item = markerGroup.getAt(count);
                        item.hide(true);
                    }
                }
            }

            if (chart.markerIndex) {
                if (me.smooth) {
                    Ext.Array.erase(path, 1, 2);
                } else {
                    Ext.Array.splice(path, 1, 0, path[1], path[2]);
                }
                me.previousPath = path;
            }
            me.renderLabels();
            me.renderCallouts();

            me.fireEvent('draw', me);
        },

        // @private called when a label is to be created.
        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 called when a label is to be created.
        onPlaceLabel: function(label, storeItem, item, i, display, animate) {
            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],
                radius = item.sprite.attr.radius,
                bb, width, height;

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

            if (display == 'rotate') {
                label.setAttributes({
                    'text-anchor': 'start',
                    'rotation': {
                        x: x,
                        y: y,
                        degrees: -45
                    }
                }, true);
                //correct label position to fit into the box
                bb = label.getBBox();
                width = bb.width;
                height = bb.height;
                x = x < bbox.x? bbox.x : 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;

            } else if (display == 'under' || display == 'over') {
                //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
                bb = item.sprite.getBBox();
                bb.width = bb.width || (radius * 2);
                bb.height = bb.height || (radius * 2);
                y = y + (display == 'over'? -bb.height : bb.height);
                //correct label position to fit into the box
                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) {
                    me.animation.on('afteranimate', function() {
                        label.show(true);
                    });
                } else {
                    label.show(true);
                }
            }
        },

        // @private Overriding highlights.js highlightItem method.
        highlightItem: function() {
            var me = this;
            me.callParent(arguments);
            if (me.line && !me.highlighted) {
                if (!('__strokeWidth' in me.line)) {
                    me.line.__strokeWidth = parseFloat(me.line.attr['stroke-width']) || 0;
                }
                if (me.line.__anim) {
                    me.line.__anim.paused = true;
                }
                me.line.__anim = Ext.create('Ext.fx.Anim', {
                    target: me.line,
                    to: {
                        'stroke-width': me.line.__strokeWidth + 3
                    }
                });
                me.highlighted = true;
            }
        },

        // @private Overriding highlights.js unHighlightItem method.
        unHighlightItem: function() {
            var me = this;
            me.callParent(arguments);
            if (me.line && me.highlighted) {
                me.line.__anim = Ext.create('Ext.fx.Anim', {
                    target: me.line,
                    to: {
                        'stroke-width': me.line.__strokeWidth
                    }
                });
                me.highlighted = false;
            }
        },

        // @private called when a callout needs to be placed.
        onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
            if (!display) {
                return;
            }

            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[0], +item.point[1]],
                dir, norm, normal, a, aprev, anext,
                offsetFromViz = config.offsetFromViz || 30,
                offsetToSide = config.offsetToSide || 10,
                offsetBox = config.offsetBox || 3,
                boxx, boxy, boxw, boxh,
                p, clipRect = me.clipRect,
                bbox = {
                    width: config.styles.width || 10,
                    height: config.styles.height || 10
                },
                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;

            if (chart.animate) {
                //set the line from the middle of the pie to the box.
                me.onAnimate(callout.lines, {
                    to: {
                        path: ["M", cur[0], cur[1], "L", x, y, "Z"]
                    }
                });
                //set component position
                if (callout.panel) {
                    callout.panel.setPosition(boxx, boxy, true);
                }
            }
            else {
                //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 component position
                if (callout.panel) {
                    callout.panel.setPosition(boxx, boxy);
                }
            }
            for (p in callout) {
                callout[p].show(true);
            }
        },

        isItemInPoint: function(x, y, item, i) {
            var me = this,
                items = me.items,
                tolerance = me.selectionTolerance,
                result = null,
                prevItem,
                nextItem,
                prevPoint,
                nextPoint,
                ln,
                x1,
                y1,
                x2,
                y2,
                xIntersect,
                yIntersect,
                dist1, dist2, dist, midx, midy,
                sqrt = Math.sqrt, abs = Math.abs;

            nextItem = items[i];
            prevItem = i && items[i - 1];

            if (i >= ln) {
                prevItem = items[ln - 1];
            }
            prevPoint = prevItem && prevItem.point;
            nextPoint = nextItem && nextItem.point;
            x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance;
            y1 = prevItem ? prevPoint[1] : nextPoint[1];
            x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance;
            y2 = nextItem ? nextPoint[1] : prevPoint[1];
            dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
            dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
            dist = Math.min(dist1, dist2);

            if (dist <= tolerance) {
                return dist == dist1? prevItem : nextItem;
            }
            return false;
        },

        // @private toggle visibility of all series elements (markers, sprites).
        toggleAll: function(show) {
            var me = this,
                i, ln, shadow, shadows;
            if (!show) {
                Ext.chart.series.Cartesian.prototype.hideAll.call(me);
            }
            else {
                Ext.chart.series.Cartesian.prototype.showAll.call(me);
            }
            if (me.line) {
                me.line.setAttributes({
                    hidden: !show
                }, true);
                //hide shadows too
                if (me.line.shadows) {
                    for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) {
                        shadow = shadows[i];
                        shadow.setAttributes({
                            hidden: !show
                        }, true);
                    }
                }
            }
            if (me.fillPath) {
                me.fillPath.setAttributes({
                    hidden: !show
                }, true);
            }
        },

        // @private hide all series elements (markers, sprites).
        hideAll: function() {
            this.toggleAll(false);
        },

        // @private hide all series elements (markers, sprites).
        showAll: function() {
            this.toggleAll(true);
        }
    });