/**
 * @class Ext.draw.ContainerBase
 * @private
 */
Ext.define('Ext.draw.ContainerBase', {
    extend: 'Ext.panel.Panel',
    requires: [
        'Ext.window.Window'
    ],
    /**
     * @cfg {String} previewTitleText The text to place in Preview Chart window title.
     */
    previewTitleText: 'Chart Preview',
    /**
     * @cfg {String} previewAltText The text to place in the Preview image alt attribute.
     */
    previewAltText: 'Chart preview',
    layout: 'container',
    // Adds a listener to this draw container's element. If the element does not yet exist
    // addition of the listener will be deferred until onRender.  Useful when listeners
    // need to be attached during initConfig.
    addElementListener: function() {
        var me = this,
            args = arguments;
        if (me.rendered) {
            me.el.on.apply(me.el, args);
        } else {
            me.on('render', function() {
                me.el.on.apply(me.el, args);
            });
        }
    },
    removeElementListener: function() {
        var me = this;
        if (me.rendered) {
            me.el.un.apply(me.el, arguments);
        }
    },
    afterRender: function() {
        this.callParent(arguments);
        this.initAnimator();
    },
    getItems: function() {
        var me = this,
            items = me.items;
        if (!items || !items.isMixedCollection) {
            // getItems may be called before initItems has run and created the items
            // collection, so we have to create it here just in case (this can happen
            // if getItems is called during initConfig)
            me.initItems();
        }
        return me.items;
    },
    onRender: function() {
        this.callParent(arguments);
        this.element = this.el;
        this.bodyElement = this.body;
    },
    setItems: function(items) {
        this.items = items;
        return items;
    },
    setSurfaceSize: function(width, height) {
        this.resizeHandler({
            width: width,
            height: height
        });
        this.renderFrame();
    },
    onResize: function(width, height, oldWidth, oldHeight) {
        this.handleResize({
            width: width,
            height: height
        }, !this.size);
    },
    // First resize should be performed without any delay.
    preview: function(image) {
        var items;
        if (Ext.isIE8) {
            return false;
        }
        image = image || this.getImage();
        if (image.type === 'svg-markup') {
            items = {
                xtype: 'container',
                html: image.data
            };
        } else {
            items = {
                xtype: 'image',
                mode: 'img',
                cls: Ext.baseCSSPrefix + 'chart-image',
                alt: this.previewAltText,
                src: image.data,
                listeners: {
                    afterrender: function() {
                        var me = this,
                            img = me.imgEl.dom,
                            // eslint-disable-next-line dot-notation
                            ratio = image.type === 'svg' ? 1 : (window['devicePixelRatio'] || 1),
                            size;
                        if (!img.naturalWidth || !img.naturalHeight) {
                            img.onload = function() {
                                var width = img.naturalWidth,
                                    height = img.naturalHeight;
                                me.setWidth(Math.floor(width / ratio));
                                me.setHeight(Math.floor(height / ratio));
                            };
                        } else {
                            size = me.getSize();
                            me.setWidth(Math.floor(size.width / ratio));
                            me.setHeight(Math.floor(size.height / ratio));
                        }
                    }
                }
            };
        }
        new Ext.window.Window({
            title: this.previewTitleText,
            closable: true,
            renderTo: Ext.getBody(),
            autoShow: true,
            maximizeable: true,
            maximized: true,
            border: true,
            layout: {
                type: 'hbox',
                pack: 'center',
                align: 'middle'
            },
            items: {
                xtype: 'container',
                items: items
            }
        });
    },
    privates: {
        getTargetEl: function() {
            return this.bodyElement;
        },
        reattachToBody: function() {
            // This is to ensure charts work properly as grid column widgets.
            var me = this;
            if (me.pendingDetachSize) {
                me.handleResize();
            }
            me.pendingDetachSize = false;
            me.callParent();
        }
    }
});

/**
 * @private
 * @class Ext.draw.SurfaceBase (Classic)
 */
Ext.define('Ext.draw.SurfaceBase', {
    extend: 'Ext.Widget',
    getOwnerBody: function() {
        return this.ownerCt.body;
    }
});

/**
 * @private
 * @class Ext.draw.sprite.AnimationParser
 *
 * Computes an intermidiate value between two values of the same type for use in animations.
 * Can have pre- and post- processor functions if the values need to be processed
 * before an intermidiate value can be computed (parseInitial), or the computed value
 * needs to be processed before it can be used as a valid attribute value (serve).
 */
Ext.define('Ext.draw.sprite.AnimationParser', function() {
    function compute(from, to, delta) {
        return from + (to - from) * delta;
    }
    return {
        singleton: true,
        attributeRe: /^url\(#([a-zA-Z-]+)\)$/,
        requires: [
            'Ext.draw.Color'
        ],
        color: {
            parseInitial: function(color1, color2) {
                if (Ext.isString(color1)) {
                    color1 = Ext.util.Color.create(color1);
                }
                if (Ext.isString(color2)) {
                    color2 = Ext.util.Color.create(color2);
                }
                if ((color1 && color1.isColor) && (color2 && color2.isColor)) {
                    return [
                        [
                            color1.r,
                            color1.g,
                            color1.b,
                            color1.a
                        ],
                        [
                            color2.r,
                            color2.g,
                            color2.b,
                            color2.a
                        ]
                    ];
                } else {
                    return [
                        color1 || color2,
                        color2 || color1
                    ];
                }
            },
            compute: function(from, to, delta) {
                if (!Ext.isArray(from) || !Ext.isArray(to)) {
                    return to || from;
                } else {
                    return [
                        compute(from[0], to[0], delta),
                        compute(from[1], to[1], delta),
                        compute(from[2], to[2], delta),
                        compute(from[3], to[3], delta)
                    ];
                }
            },
            serve: function(array) {
                var color = Ext.util.Color.fly(array[0], array[1], array[2], array[3]);
                return color.toString();
            }
        },
        number: {
            parse: function(n) {
                return n === null ? null : +n;
            },
            compute: function(from, to, delta) {
                if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
                    return to || from;
                } else {
                    return compute(from, to, delta);
                }
            }
        },
        angle: {
            parseInitial: function(from, to) {
                if (to - from > Math.PI) {
                    to -= Math.PI * 2;
                } else if (to - from < -Math.PI) {
                    to += Math.PI * 2;
                }
                return [
                    from,
                    to
                ];
            },
            compute: function(from, to, delta) {
                if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
                    return to || from;
                } else {
                    return compute(from, to, delta);
                }
            }
        },
        path: {
            parseInitial: function(from, to) {
                var fromStripes = from.toStripes(),
                    toStripes = to.toStripes(),
                    i, j,
                    fromLength = fromStripes.length,
                    toLength = toStripes.length,
                    fromStripe, toStripe, length,
                    lastStripe = toStripes[toLength - 1],
                    endPoint = [
                        lastStripe[lastStripe.length - 2],
                        lastStripe[lastStripe.length - 1]
                    ];
                for (i = fromLength; i < toLength; i++) {
                    fromStripes.push(fromStripes[fromLength - 1].slice(0));
                }
                for (i = toLength; i < fromLength; i++) {
                    toStripes.push(endPoint.slice(0));
                }
                length = fromStripes.length;
                toStripes.path = to;
                toStripes.temp = new Ext.draw.Path();
                for (i = 0; i < length; i++) {
                    fromStripe = fromStripes[i];
                    toStripe = toStripes[i];
                    fromLength = fromStripe.length;
                    toLength = toStripe.length;
                    toStripes.temp.commands.push('M');
                    for (j = toLength; j < fromLength; j += 6) {
                        toStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
                    }
                    lastStripe = toStripes[toStripes.length - 1];
                    endPoint = [
                        lastStripe[lastStripe.length - 2],
                        lastStripe[lastStripe.length - 1]
                    ];
                    for (j = fromLength; j < toLength; j += 6) {
                        fromStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
                    }
                    for (i = 0; i < toStripe.length; i++) {
                        toStripe[i] -= fromStripe[i];
                    }
                    for (i = 2; i < toStripe.length; i += 6) {
                        toStripes.temp.commands.push('C');
                    }
                }
                return [
                    fromStripes,
                    toStripes
                ];
            },
            compute: function(fromStripes, toStripes, delta) {
                if (delta >= 1) {
                    return toStripes.path;
                }
                // eslint-disable-next-line vars-on-top
                var i = 0,
                    ln = fromStripes.length,
                    j = 0,
                    ln2, from, to,
                    temp = toStripes.temp.params,
                    pos = 0;
                for (; i < ln; i++) {
                    from = fromStripes[i];
                    to = toStripes[i];
                    ln2 = from.length;
                    for (j = 0; j < ln2; j++) {
                        temp[pos++] = to[j] * delta + from[j];
                    }
                }
                return toStripes.temp;
            }
        },
        data: {
            compute: function(from, to, delta, target) {
                var iMaxFrom = from.length - 1,
                    iMaxTo = to.length - 1,
                    iMax = Math.max(iMaxFrom, iMaxTo),
                    i, start, end;
                if (!target || target === from) {
                    target = [];
                }
                target.length = iMax + 1;
                for (i = 0; i <= iMax; i++) {
                    start = from[Math.min(i, iMaxFrom)];
                    end = to[Math.min(i, iMaxTo)];
                    if (Ext.isNumber(start)) {
                        if (!Ext.isNumber(end)) {
                            // This may not give the desired visual result during
                            // animation (after all, we don't know what the target
                            // value should be, if it wasn't given to us), but it's
                            // better than spitting out a bunch of NaNs in the target
                            // array, when transitioning from a non-empty to an empty
                            // array.
                            end = 0;
                        }
                        target[i] = start + (end - start) * delta;
                    } else {
                        target[i] = end;
                    }
                }
                return target;
            }
        },
        text: {
            compute: function(from, to, delta) {
                return from.substr(0, Math.round(from.length * (1 - delta))) + to.substr(Math.round(to.length * (1 - delta)));
            }
        },
        limited: 'number',
        limited01: 'number'
    };
});

/* global Float32Array */
/* eslint-disable indent */
(function() {
    if (!Ext.global.Float32Array) {
        // Typed Array polyfill
        // eslint-disable-next-line vars-on-top
        var Float32Array = function(array) {
                var i, len;
                if (typeof array === 'number') {
                    this.length = array;
                } else if ('length' in array) {
                    this.length = array.length;
                    for (i = 0 , len = array.length; i < len; i++) {
                        this[i] = +array[i];
                    }
                }
            };
        Float32Array.prototype = [];
        Ext.global.Float32Array = Float32Array;
    }
})();
/* eslint-enable indent */
/**
 * Utility class providing mathematics functionalities through all the draw package.
 */
Ext.define('Ext.draw.Draw', {
    singleton: true,
    radian: Math.PI / 180,
    pi2: Math.PI * 2,
    /**
     * @deprecated 6.5.0 Please use the {@link Ext#identityFn} instead.
     * Function that returns its first element.
     * @param {Mixed} a
     * @return {Mixed}
     */
    reflectFn: function(a) {
        return a;
    },
    /**
     * Converting degrees to radians.
     * @param {Number} degrees
     * @return {Number}
     */
    rad: function(degrees) {
        return (degrees % 360) * this.radian;
    },
    /**
     * Converting radians to degrees.
     * @param {Number} radian
     * @return {Number}
     */
    degrees: function(radian) {
        return (radian / this.radian) % 360;
    },
    /**
     *
     * @param {Object} bbox1
     * @param {Object} bbox2
     * @param {Number} [padding]
     * @return {Boolean}
     */
    isBBoxIntersect: function(bbox1, bbox2, padding) {
        padding = padding || 0;
        return (Math.max(bbox1.x, bbox2.x) - padding > Math.min(bbox1.x + bbox1.width, bbox2.x + bbox2.width)) || (Math.max(bbox1.y, bbox2.y) - padding > Math.min(bbox1.y + bbox1.height, bbox2.y + bbox2.height));
    },
    /**
     * Checks if a point is within a bounding box.
     * @param x
     * @param y
     * @param bbox
     * @return {Boolean}
     */
    isPointInBBox: function(x, y, bbox) {
        return !!bbox && x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height);
    },
    /**
     * Natural cubic spline interpolation.
     * This algorithm runs in linear time.
     *
     * @param {Array} points Array of numbers.
     */
    naturalSpline: function(points) {
        var i, j,
            ln = points.length,
            nd, d, y, ny,
            r = 0,
            zs = new Float32Array(points.length),
            result = new Float32Array(points.length * 3 - 2);
        zs[0] = 0;
        zs[ln - 1] = 0;
        for (i = 1; i < ln - 1; i++) {
            zs[i] = (points[i + 1] + points[i - 1] - 2 * points[i]) - zs[i - 1];
            r = 1 / (4 - r);
            zs[i] *= r;
        }
        for (i = ln - 2; i > 0; i--) {
            r = 3.732050807568877 + 48.248711305964385 / (-13.928203230275537 + Math.pow(0.07179676972449123, i));
            zs[i] -= zs[i + 1] * r;
        }
        ny = points[0];
        nd = ny - zs[0];
        for (i = 0 , j = 0; i < ln - 1; j += 3) {
            y = ny;
            d = nd;
            i++;
            ny = points[i];
            nd = ny - zs[i];
            result[j] = y;
            result[j + 1] = (nd + 2 * d) / 3;
            result[j + 2] = (nd * 2 + d) / 3;
        }
        result[j] = ny;
        return result;
    },
    /**
     * Shorthand for {@link #naturalSpline}
     */
    spline: function(points) {
        return this.naturalSpline(points);
    },
    /**
     * @private
     * Cardinal spline interpolation.
     * Goes from cardinal control points to cubic Bezier control points.
     */
    cardinalToBezier: function(P1, P2, P3, P4, tension) {
        return [
            P2,
            P2 + (P3 - P1) / 6 * tension,
            P3 - (P4 - P2) / 6 * tension,
            P3
        ];
    },
    /**
     * @private
     * @param {Number[]} P An array of n x- or y-coordinates.
     * @param {Number} tension
     * @return {Float32Array} An array of 3n - 2 Bezier control points.
     */
    cardinalSpline: function(P, tension) {
        var n = P.length,
            result = new Float32Array(n * 3 - 2),
            i, bezier;
        if (tension === undefined) {
            tension = 0.5;
        }
        bezier = this.cardinalToBezier(P[0], P[0], P[1], P[2], tension);
        result[0] = bezier[0];
        result[1] = bezier[1];
        result[2] = bezier[2];
        result[3] = bezier[3];
        for (i = 0; i < n - 3; i++) {
            bezier = this.cardinalToBezier(P[i], P[i + 1], P[i + 2], P[i + 3], tension);
            result[4 + i * 3] = bezier[1];
            result[4 + i * 3 + 1] = bezier[2];
            result[4 + i * 3 + 2] = bezier[3];
        }
        bezier = this.cardinalToBezier(P[n - 3], P[n - 2], P[n - 1], P[n - 1], tension);
        result[4 + i * 3] = bezier[1];
        result[4 + i * 3 + 1] = bezier[2];
        result[4 + i * 3 + 2] = bezier[3];
        return result;
    },
    /**
     * @private
     *
     * Calculates bezier curve control anchor points for a particular point in a path, with a
     * smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter.
     * Note that this algorithm assumes that the line being smoothed is normalized going from left
     * to right; it makes special adjustments assuming this orientation.
     *
     * @param {Number} prevX X coordinate of the previous point in the path
     * @param {Number} prevY Y coordinate of the previous point in the path
     * @param {Number} curX X coordinate of the current point in the path
     * @param {Number} curY Y coordinate of the current point in the path
     * @param {Number} nextX X coordinate of the next point in the path
     * @param {Number} nextY Y coordinate of the next point in the path
     * @param {Number} value A value to control the smoothness of the curve; this is used to
     *                 divide the distance between points, so a value of 2 corresponds to
     *                 half the distance between points (a very smooth line) while higher values
     *                 result in less smooth curves. Defaults to 4.
     * @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1
     *                  are the control point for the curve toward the previous path point, and
     *                  x2 and y2 are the control point for the curve toward the next path point.
     */
    getAnchors: function(prevX, prevY, curX, curY, nextX, nextY, value) {
        var PI = Math.PI,
            halfPI = PI / 2,
            abs = Math.abs,
            sin = Math.sin,
            cos = Math.cos,
            atan = Math.atan,
            control1Length, control2Length, control1Angle, control2Angle, control1X, control1Y, control2X, control2Y, alpha;
        value = value || 4;
        // Find the length of each control anchor line, by dividing the horizontal distance
        // between points by the value parameter.
        control1Length = (curX - prevX) / value;
        control2Length = (nextX - curX) / value;
        // Determine the angle of each control anchor line. If the middle point is a vertical
        // turnaround then we force it to a flat horizontal angle to prevent the curve from
        // dipping above or below the middle point. Otherwise we use an angle that points
        // toward the previous/next target point.
        if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) {
            control1Angle = control2Angle = halfPI;
        } else {
            control1Angle = atan((curX - prevX) / abs(curY - prevY));
            if (prevY < curY) {
                control1Angle = PI - control1Angle;
            }
            control2Angle = atan((nextX - curX) / abs(curY - nextY));
            if (nextY < curY) {
                control2Angle = PI - control2Angle;
            }
        }
        // Adjust the calculated angles so they point away from each other on the same line
        alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2;
        if (alpha > halfPI) {
            alpha -= PI;
        }
        control1Angle += alpha;
        control2Angle += alpha;
        // Find the control anchor points from the angles and length
        control1X = curX - control1Length * sin(control1Angle);
        control1Y = curY + control1Length * cos(control1Angle);
        control2X = curX + control2Length * sin(control2Angle);
        control2Y = curY + control2Length * cos(control2Angle);
        // One last adjustment, make sure that no control anchor point extends vertically past
        // its target prev/next point, as that results in curves dipping above or below and
        // bending back strangely. If we find this happening we keep the control angle but
        // reduce the length of the control line so it stays within bounds.
        if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) {
            control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY);
            control1Y = prevY;
        }
        if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) {
            control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY);
            control2Y = nextY;
        }
        return {
            x1: control1X,
            y1: control1Y,
            x2: control2X,
            y2: control2Y
        };
    },
    /**
     * Given coordinates of the points, calculates coordinates of a Bezier curve
     * that goes through them.
     * @param dataX x-coordinates of the points.
     * @param dataY y-coordinates of the points.
     * @param value A value to control the smoothness of the curve.
     * @return {Object} Object holding two arrays, for x and y coordinates of the curve.
     */
    smooth: function(dataX, dataY, value) {
        var ln = dataX.length,
            prevX, prevY, curX, curY, nextX, nextY, x, y,
            smoothX = [],
            smoothY = [],
            i, anchors;
        for (i = 0; i < ln - 1; i++) {
            prevX = dataX[i];
            prevY = dataY[i];
            if (i === 0) {
                x = prevX;
                y = prevY;
                smoothX.push(x);
                smoothY.push(y);
                if (ln === 1) {
                    break;
                }
            }
            curX = dataX[i + 1];
            curY = dataY[i + 1];
            nextX = dataX[i + 2];
            nextY = dataY[i + 2];
            if (!(Ext.isNumber(nextX) && Ext.isNumber(nextY))) {
                smoothX.push(x, curX, curX);
                smoothY.push(y, curY, curY);
                break;
            }
            anchors = this.getAnchors(prevX, prevY, curX, curY, nextX, nextY, value);
            smoothX.push(x, anchors.x1, curX);
            smoothY.push(y, anchors.y1, curY);
            x = anchors.x2;
            y = anchors.y2;
        }
        return {
            smoothX: smoothX,
            smoothY: smoothY
        };
    },
    /**
     * @method
     * @private
     * Work around for iOS.
     * Nested 3d-transforms seems to prevent the redraw inside it until some event is fired.
     */
    beginUpdateIOS: Ext.os.is.iOS ? function() {
        this.iosUpdateEl = Ext.getBody().createChild({
            //<debug>
            'data-sticky': true,
            //</debug>
            style: 'position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px; ' + 'background: rgba(0,0,0,0.001); z-index: 100000'
        });
    } : Ext.emptyFn,
    endUpdateIOS: function() {
        this.iosUpdateEl = Ext.destroy(this.iosUpdateEl);
    }
});

/**
 * @class Ext.draw.gradient.Gradient
 *
 * Creates a gradient.
 */
Ext.define('Ext.draw.gradient.Gradient', {
    requires: [
        'Ext.draw.Color'
    ],
    isGradient: true,
    config: {
        /**
         * @cfg {Object[]} stops
         * Defines the stops of the gradient.
         */
        stops: []
    },
    applyStops: function(newStops) {
        var stops = [],
            ln = newStops.length,
            i, stop, color;
        for (i = 0; i < ln; i++) {
            stop = newStops[i];
            color = stop.color;
            if (!(color && color.isColor)) {
                color = Ext.util.Color.fly(color || Ext.util.Color.NONE);
            }
            stops.push({
                // eslint-disable-next-line max-len
                offset: Math.min(1, Math.max(0, 'offset' in stop ? stop.offset : stop.position || 0)),
                color: color.toString()
            });
        }
        stops.sort(function(a, b) {
            return a.offset - b.offset;
        });
        return stops;
    },
    onClassExtended: function(subClass, member) {
        if (!member.alias && member.type) {
            member.alias = 'gradient.' + member.type;
        }
    },
    constructor: function(config) {
        this.initConfig(config);
    },
    /**
     * @method
     * @protected
     * Generates the gradient for the given context.
     * @param {Ext.draw.engine.SvgContext} ctx The context.
     * @param {Object} bbox
     * @return {CanvasGradient/Ext.draw.engine.SvgContext.Gradient/Ext.util.Color.NONE}
     */
    generateGradient: Ext.emptyFn
});

/**
 * @class Ext.draw.gradient.GradientDefinition
 *
 * A global map of all gradient configs.
 */
Ext.define('Ext.draw.gradient.GradientDefinition', {
    singleton: true,
    urlStringRe: /^url\(#([\w-]+)\)$/,
    gradients: {},
    add: function(gradients) {
        var store = this.gradients,
            i, n, gradient;
        for (i = 0 , n = gradients.length; i < n; i++) {
            gradient = gradients[i];
            if (Ext.isString(gradient.id)) {
                store[gradient.id] = gradient;
            }
        }
    },
    get: function(str) {
        var store = this.gradients,
            match = str.match(this.urlStringRe),
            gradient;
        if (match && match[1] && (gradient = store[match[1]])) {
            return gradient || str;
        }
        return str;
    }
});

/* global Float32Array */
/**
 * @private
 * @class Ext.draw.sprite.AttributeParser
 *
 * Parsers used for sprite attributes if they are
 * {@link Ext.draw.sprite.AttributeDefinition#normalize normalized} (default) when being
 * {@link Ext.draw.sprite.Sprite#setAttributes set}.
 *
 * Methods of the singleton correpond either to the processor functions themselves or processor
 * factories.
 */
Ext.define('Ext.draw.sprite.AttributeParser', {
    singleton: true,
    attributeRe: /^url\(#([a-zA-Z-]+)\)$/,
    requires: [
        'Ext.draw.Color',
        'Ext.draw.gradient.GradientDefinition'
    ],
    'default': Ext.identityFn,
    string: function(n) {
        return String(n);
    },
    number: function(n) {
        // Numbers as strings will be converted to numbers,
        // null will be converted to 0.
        if (Ext.isNumber(+n)) {
            return n;
        }
    },
    /**
     * Normalize angle to the [-180,180) interval.
     * @param n Angle in radians.
     * @return {Number/undefined} Normalized angle or undefined.
     */
    angle: function(n) {
        if (Ext.isNumber(n)) {
            n %= Math.PI * 2;
            if (n < -Math.PI) {
                n += Math.PI * 2;
            } else if (n >= Math.PI) {
                n -= Math.PI * 2;
            }
            return n;
        }
    },
    data: function(n) {
        if (Ext.isArray(n)) {
            return n.slice();
        } else if (n instanceof Float32Array) {
            return new Float32Array(n);
        }
    },
    bool: function(n) {
        return !!n;
    },
    color: function(n) {
        if (n && n.isColor) {
            return n.toString();
        } else if (n && n.isGradient) {
            return n;
        } else if (!n) {
            return Ext.util.Color.NONE;
        } else if (Ext.isString(n)) {
            if (n.substr(0, 3) === 'url') {
                n = Ext.draw.gradient.GradientDefinition.get(n);
                if (Ext.isString(n)) {
                    return n;
                }
            } else {
                return Ext.util.Color.fly(n).toString();
            }
        }
        if (n.type === 'linear') {
            return Ext.create('Ext.draw.gradient.Linear', n);
        } else if (n.type === 'radial') {
            return Ext.create('Ext.draw.gradient.Radial', n);
        } else if (n.type === 'pattern') {
            return Ext.create('Ext.draw.gradient.Pattern', n);
        } else {
            return Ext.util.Color.NONE;
        }
    },
    limited: function(low, hi) {
        return function(n) {
            n = +n;
            return Ext.isNumber(n) ? Math.min(Math.max(n, low), hi) : undefined;
        };
    },
    limited01: function(n) {
        n = +n;
        return Ext.isNumber(n) ? Math.min(Math.max(n, 0), 1) : undefined;
    },
    /**
     * Generates a function that checks if a value matches
     * one of the given attributes.
     * @return {Function}
     */
    enums: function() {
        var enums = {},
            args = Array.prototype.slice.call(arguments, 0),
            i, ln;
        for (i = 0 , ln = args.length; i < ln; i++) {
            enums[args[i]] = true;
        }
        return function(n) {
            return n in enums ? n : undefined;
        };
    }
});

/**
 * @private
 * Flyweight object to process the attributes of a sprite.
 * A single instance of the AttributeDefinition is created per sprite class.
 * See `onClassCreated` and `onClassExtended` callbacks
 * of the {@link Ext.draw.sprite.Sprite} for more info.
 */
Ext.define('Ext.draw.sprite.AttributeDefinition', {
    requires: [
        'Ext.draw.sprite.AttributeParser',
        'Ext.draw.sprite.AnimationParser'
    ],
    config: {
        /**
         * @cfg {Object} defaults Defines the default values of attributes.
         */
        defaults: {
            $value: {},
            lazy: true
        },
        /**
         * @cfg {Object} aliases Defines the alternative names for attributes.
         */
        aliases: {},
        /**
         * @cfg {Object} animationProcessors Defines the process used to animate between attributes.
         * One doesn't have to define animation processors for sprite attributes that use
         * predefined {@link #processors} from the {@link Ext.draw.sprite.AttributeParser}
         * singleton.
         * For such attributes matching animation processors from the
         * {@link Ext.draw.sprite.AnimationParser} singleton will be used automatically.
         * However, if you have a custom processor for an attribute that should support
         * animation, you must provide a corresponding animation processor for it here.
         * For more information on animation processors please see
         * {@link Ext.draw.sprite.AnimationParser} documentation.
         */
        animationProcessors: {},
        /**
         * @cfg {Object} processors Defines the preprocessing used on the attributes.
         * One can define a custom processor function here or use the name of a predefined
         * processor from the {@link Ext.draw.sprite.AttributeParser} singleton.
         */
        processors: {
            // A plus side of lazy initialization is that the 'processors' and 'defaults' will
            // only be applied for those sprite classes that are actually instantiated.
            $value: {},
            lazy: true
        },
        /**
         * @cfg {Object} dirtyTriggers
         * @deprecated 6.5.0 Use the {@link #triggers} config instead.
         */
        dirtyTriggers: {},
        /* eslint-disable max-len */
        /**
         * @cfg {Object} triggers Defines which updaters have to be called when an attribute is changed.
         * For example, the config below indicates that the 'size' updater
         * of a {@link Ext.draw.sprite.Square square} sprite has to be called
         * when the 'size' attribute changes.
         *
         *     triggers: {
         *         size: 'size'   // Use comma-separated values here if multiple updaters have to be called.
         *     }                  // Note that the order is _not_ guaranteed.
         *
         * If any of the updaters to be called (triggered by the {@link Ext.draw.sprite.Sprite#setAttributes} call)
         * set attributes themselves and those attributes have triggers defined for them,
         * then their updaters will be called after all current updaters finish execution.
         *
         * The updater functions themselves are defined in the {@link #updaters} config,
         * aside from the 'canvas' updater, which doesn't have to be defined and acts as a flag,
         * indicating that this attribute should be applied to a Canvas context (or whatever emulates it).
         * @since 5.1.0
         */
        triggers: {},
        /**
         * @cfg {Object} updaters Defines the postprocessing used by the attribute.
         * Inside the updater function 'this' refers to the sprite that the attributes belong to.
         * In case of an instancing sprite 'this' will refer to the instancing template.
         * The two parameters passed to the updater function are the attributes object
         * of the sprite or instance, and the names of attributes that triggered this updater call.
         *
         * The example below shows how the 'size' updater changes other attributes
         * of a {@link Ext.draw.sprite.Square square} sprite sprite when its 'size' attribute changes.
         *
         *     updaters: {
         *         size: function (attr) {
         *             var size = attr.size;
         *             this.setAttributes({   // Changes to these attributes will trigger the 'path' updater.
         *                 x: attr.x - size,
         *                 y: attr.y - size,
         *                 height: 2 * size,
         *                 width: 2 * size
         *             });
         *         }
         *     }
         */
        updaters: {}
    },
    /* eslint-enable max-len */
    inheritableStatics: {
        /**
         * @private
         * Processor declaration in the form of 'processorFactory(argument1,argument2,...)'.
         * E.g.: {@link Ext.draw.sprite.AttributeParser#enums enums},
         * {@link Ext.draw.sprite.AttributeParser#limited limited}.
         */
        processorFactoryRe: /^(\w+)\(([\w\-,]*)\)$/
    },
    // The sprite class for which AttributeDefinition instance is created.
    spriteClass: null,
    constructor: function(config) {
        var me = this;
        me.initConfig(config);
    },
    applyDefaults: function(defaults, oldDefaults) {
        oldDefaults = Ext.apply(oldDefaults || {}, this.normalize(defaults));
        return oldDefaults;
    },
    applyAliases: function(aliases, oldAliases) {
        return Ext.apply(oldAliases || {}, aliases);
    },
    applyProcessors: function(processors, oldProcessors) {
        this.getAnimationProcessors();
        // Apply custom animation processors first.
        // eslint-disable-next-line vars-on-top
        var result = oldProcessors || {},
            defaultProcessor = Ext.draw.sprite.AttributeParser,
            processorFactoryRe = this.self.processorFactoryRe,
            animationProcessors = {},
            anyAnimationProcessors, name, match, fn;
        for (name in processors) {
            fn = processors[name];
            if (typeof fn === 'string') {
                match = fn.match(processorFactoryRe);
                if (match) {
                    // enums(... , limited(... or something of that nature.
                    fn = defaultProcessor[match[1]].apply(defaultProcessor, match[2].split(','));
                } else if (defaultProcessor[fn]) {
                    // Names of animation parsers match the names of attribute parsers.
                    animationProcessors[name] = fn;
                    anyAnimationProcessors = true;
                    fn = defaultProcessor[fn];
                }
            }
            //<debug>
            if (!Ext.isFunction(fn)) {
                Ext.raise(this.spriteClass.$className + ": processor '" + name + "' has not been found.");
            }
            //</debug>
            result[name] = fn;
        }
        if (anyAnimationProcessors) {
            this.setAnimationProcessors(animationProcessors);
        }
        return result;
    },
    applyAnimationProcessors: function(animationProcessors, oldAnimationProcessors) {
        var parser = Ext.draw.sprite.AnimationParser,
            name, item;
        if (!oldAnimationProcessors) {
            oldAnimationProcessors = {};
        }
        for (name in animationProcessors) {
            item = animationProcessors[name];
            if (item === 'none') {
                oldAnimationProcessors[name] = null;
            } else if (Ext.isString(item) && !(name in oldAnimationProcessors)) {
                if (item in parser) {
                    // The while loop is used to resolve aliases, e.g. `num: 'number'`,
                    // where `number` maps to a parser object or is an alias too.
                    while (Ext.isString(parser[item])) {
                        item = parser[item];
                    }
                    oldAnimationProcessors[name] = parser[item];
                }
            } else if (Ext.isObject(item)) {
                oldAnimationProcessors[name] = item;
            }
        }
        return oldAnimationProcessors;
    },
    updateDirtyTriggers: function(dirtyTriggers) {
        this.setTriggers(dirtyTriggers);
    },
    applyTriggers: function(triggers, oldTriggers) {
        var name;
        if (!oldTriggers) {
            oldTriggers = {};
        }
        for (name in triggers) {
            oldTriggers[name] = triggers[name].split(',');
        }
        return oldTriggers;
    },
    applyUpdaters: function(updaters, oldUpdaters) {
        return Ext.apply(oldUpdaters || {}, updaters);
    },
    batchedNormalize: function(batchedChanges, keepUnrecognized) {
        if (!batchedChanges) {
            return {};
        }
        // eslint-disable-next-line vars-on-top
        var processors = this.getProcessors(),
            aliases = this.getAliases(),
            translation = batchedChanges.translation || batchedChanges.translate,
            normalized = {},
            i, ln, name, val, rotation, scaling, matrix, subVal, split;
        if ('rotation' in batchedChanges) {
            rotation = batchedChanges.rotation;
        } else {
            rotation = ('rotate' in batchedChanges) ? batchedChanges.rotate : undefined;
        }
        if ('scaling' in batchedChanges) {
            scaling = batchedChanges.scaling;
        } else {
            scaling = ('scale' in batchedChanges) ? batchedChanges.scale : undefined;
        }
        if (typeof scaling !== 'undefined') {
            if (Ext.isNumber(scaling)) {
                normalized.scalingX = scaling;
                normalized.scalingY = scaling;
            } else {
                if ('x' in scaling) {
                    normalized.scalingX = scaling.x;
                }
                if ('y' in scaling) {
                    normalized.scalingY = scaling.y;
                }
                if ('centerX' in scaling) {
                    normalized.scalingCenterX = scaling.centerX;
                }
                if ('centerY' in scaling) {
                    normalized.scalingCenterY = scaling.centerY;
                }
            }
        }
        if (typeof rotation !== 'undefined') {
            if (Ext.isNumber(rotation)) {
                rotation = Ext.draw.Draw.rad(rotation);
                normalized.rotationRads = rotation;
            } else {
                if ('rads' in rotation) {
                    normalized.rotationRads = rotation.rads;
                } else if ('degrees' in rotation) {
                    if (Ext.isArray(rotation.degrees)) {
                        normalized.rotationRads = Ext.Array.map(rotation.degrees, function(deg) {
                            return Ext.draw.Draw.rad(deg);
                        });
                    } else {
                        normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
                    }
                }
                if ('centerX' in rotation) {
                    normalized.rotationCenterX = rotation.centerX;
                }
                if ('centerY' in rotation) {
                    normalized.rotationCenterY = rotation.centerY;
                }
            }
        }
        if (typeof translation !== 'undefined') {
            if ('x' in translation) {
                normalized.translationX = translation.x;
            }
            if ('y' in translation) {
                normalized.translationY = translation.y;
            }
        }
        if ('matrix' in batchedChanges) {
            matrix = Ext.draw.Matrix.create(batchedChanges.matrix);
            split = matrix.split();
            normalized.matrix = matrix;
            normalized.rotationRads = split.rotation;
            normalized.rotationCenterX = 0;
            normalized.rotationCenterY = 0;
            normalized.scalingX = split.scaleX;
            normalized.scalingY = split.scaleY;
            normalized.scalingCenterX = 0;
            normalized.scalingCenterY = 0;
            normalized.translationX = split.translateX;
            normalized.translationY = split.translateY;
        }
        for (name in batchedChanges) {
            val = batchedChanges[name];
            if (typeof val === 'undefined') {
                
                continue;
            } else if (Ext.isArray(val)) {
                if (name in aliases) {
                    name = aliases[name];
                }
                if (name in processors) {
                    normalized[name] = [];
                    for (i = 0 , ln = val.length; i < ln; i++) {
                        subVal = processors[name].call(this, val[i]);
                        if (typeof subVal !== 'undefined') {
                            normalized[name][i] = subVal;
                        }
                    }
                } else if (keepUnrecognized) {
                    normalized[name] = val;
                }
            } else {
                if (name in aliases) {
                    name = aliases[name];
                }
                if (name in processors) {
                    val = processors[name].call(this, val);
                    if (typeof val !== 'undefined') {
                        normalized[name] = val;
                    }
                } else if (keepUnrecognized) {
                    normalized[name] = val;
                }
            }
        }
        return normalized;
    },
    /**
     * Normalizes the changes given via their processors before they are applied as attributes.
     *
     * @param {Object} changes The changes given.
     * @param {Boolean} keepUnrecognized If 'true', unknown attributes will be passed through
     * as normalized values.
     * @return {Object} The normalized values.
     */
    normalize: function(changes, keepUnrecognized) {
        if (!changes) {
            return {};
        }
        // eslint-disable-next-line vars-on-top
        var processors = this.getProcessors(),
            aliases = this.getAliases(),
            translation = changes.translation || changes.translate,
            normalized = {},
            name, val, rotation, scaling, matrix, split;
        if ('rotation' in changes) {
            rotation = changes.rotation;
        } else {
            rotation = ('rotate' in changes) ? changes.rotate : undefined;
        }
        if ('scaling' in changes) {
            scaling = changes.scaling;
        } else {
            scaling = ('scale' in changes) ? changes.scale : undefined;
        }
        if (translation) {
            if ('x' in translation) {
                normalized.translationX = translation.x;
            }
            if ('y' in translation) {
                normalized.translationY = translation.y;
            }
        }
        if (typeof scaling !== 'undefined') {
            if (Ext.isNumber(scaling)) {
                normalized.scalingX = scaling;
                normalized.scalingY = scaling;
            } else {
                if ('x' in scaling) {
                    normalized.scalingX = scaling.x;
                }
                if ('y' in scaling) {
                    normalized.scalingY = scaling.y;
                }
                if ('centerX' in scaling) {
                    normalized.scalingCenterX = scaling.centerX;
                }
                if ('centerY' in scaling) {
                    normalized.scalingCenterY = scaling.centerY;
                }
            }
        }
        if (typeof rotation !== 'undefined') {
            if (Ext.isNumber(rotation)) {
                rotation = Ext.draw.Draw.rad(rotation);
                normalized.rotationRads = rotation;
            } else {
                if ('rads' in rotation) {
                    normalized.rotationRads = rotation.rads;
                } else if ('degrees' in rotation) {
                    normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
                }
                if ('centerX' in rotation) {
                    normalized.rotationCenterX = rotation.centerX;
                }
                if ('centerY' in rotation) {
                    normalized.rotationCenterY = rotation.centerY;
                }
            }
        }
        if ('matrix' in changes) {
            matrix = Ext.draw.Matrix.create(changes.matrix);
            split = matrix.split();
            // This will NOT update the transformation matrix of a sprite
            // with the given elements. It will attempt to extract the
            // individual transformation attributes from the transformation matrix
            // elements provided. Then the extracted attributes will be used by
            // the sprite's 'applyTransformations' method to calculate
            // the transformation matrix of the sprite.
            // It's not possible to recover all the information from the given
            // transformation matrix elements. Shearing and centers of rotation
            // and scaling are not recovered.
            // Ideally, this should work like sprite.transform([elements], true),
            // i.e. update the transformation matrix of a sprite directly,
            // without attempting to update sprite's transformation attributes.
            // But we are not changing the behavior (just yet) for compatibility
            // reasons.
            normalized.matrix = matrix;
            normalized.rotationRads = split.rotation;
            normalized.rotationCenterX = 0;
            normalized.rotationCenterY = 0;
            normalized.scalingX = split.scaleX;
            normalized.scalingY = split.scaleY;
            normalized.scalingCenterX = 0;
            normalized.scalingCenterY = 0;
            normalized.translationX = split.translateX;
            normalized.translationY = split.translateY;
        }
        for (name in changes) {
            val = changes[name];
            if (typeof val === 'undefined') {
                
                continue;
            }
            if (name in aliases) {
                name = aliases[name];
            }
            if (name in processors) {
                val = processors[name].call(this, val);
                if (typeof val !== 'undefined') {
                    normalized[name] = val;
                }
            } else if (keepUnrecognized) {
                normalized[name] = val;
            }
        }
        return normalized;
    },
    setBypassingNormalization: function(attr, modifierStack, changes) {
        return modifierStack.pushDown(attr, changes);
    },
    set: function(attr, modifierStack, changes) {
        changes = this.normalize(changes);
        return this.setBypassingNormalization(attr, modifierStack, changes);
    }
});

/**
 * Ext.draw.Matix is a utility class used to calculate 
 * [affine transformation](http://en.wikipedia.org/wiki/Affine_transformation) matrix.  
 * The matrix class is used to apply transformations to existing 
 * {@link Ext.draw.sprite.Sprite sprites} using a number of convenience transform 
 * methods.
 * 
 * Transformations configured directly on a sprite are processed in the following order: 
 * scaling, rotation, and translation.  The matrix class offers additional flexibility.  
 * Once a sprite is created, you can use the matrix class's transform methods as many 
 * times as needed and in any order you choose. 
 *
 * To demonstrate, we'll start with a simple {@link Ext.draw.sprite.Rect rect} sprite 
 * with the intent of rotating it 180 degrees with the bottom right corner being the 
 * center of rotation.  To begin, let's look at the initial, untransformed sprite:
 * 
 *     @example
 *     var drawContainer = new Ext.draw.Container({
 *         renderTo: Ext.getBody(),
 *         width: 380,
 *         height: 380,
 *         sprites: [{
 *             type: 'rect',
 *             width: 100,
 *             height: 100,
 *             fillStyle: 'red'
 *         }]
 *     });
 * 
 * Next, we'll use the {@link #rotate} and {@link #translate} methods from our matrix 
 * class to position the rect sprite.
 * 
 *     @example
 *     var drawContainer = new Ext.draw.Container({
 *         renderTo: Ext.getBody(),
 *         width: 380,
 *         height: 380,
 *         sprites: [{
 *             type: 'rect',
 *             width: 100,
 *             height: 100,
 *             fillStyle: 'red'
 *         }]
 *     });
 *     
 *     var main = drawContainer.getSurface();
 *     var rect = main.getItems()[0];
 *     
 *     var m = new Ext.draw.Matrix().translate(100, 100).
 *     rotate(Math.PI).
 *     translate(-100, - 100);
 *     
 *     rect.setTransform(m);
 *     main.renderFrame();
 * 
 * In the previous example we perform the following steps in order to achieve our 
 * desired rotated output:
 * 
 *  - translate the rect to the right and down by 100
 *  - rotate by 180 degrees
 *  - translate the rect to the right and down by 100
 * 
 * **Note:** A couple of things to note at this stage; 1) the rotation center point is 
 * the upper left corner of the sprite by default and 2) with transformations, the 
 * sprite itself isn't transformed, but rather the entire coordinate plane of the sprite 
 * is transformed.  The coordinate plane itself is translated by 100 and then rotated 
 * 180 degrees.  And that is why in the third step we translate the sprite using 
 * negative values.  Translating by -100 in the third step results in the sprite 
 * visually moving to the right and down within the draw container.
 * 
 * Fortunately there is a shortcut we can apply using two optional params of the rotate 
 * method allowing us to specify the center point of rotation:
 * 
 *     @example
 *     var drawContainer = new Ext.draw.Container({
 *         renderTo: Ext.getBody(),
 *         width: 380,
 *         height: 380,
 *         sprites: [{
 *             type: 'rect',
 *             width: 100,
 *             height: 100,
 *             fillStyle: 'red'
 *         }]
 *     });
 *     
 *     var main = drawContainer.getSurface();
 *     var rect = main.getItems()[0];
 *     
 *     var m = new Ext.draw.Matrix().rotate(Math.PI, 100, 100);
 *     
 *     rect.setTransform(m);
 *     main.renderFrame();
 * 
 * 
 * This class is compatible with 
 * [SVGMatrix](http://www.w3.org/TR/SVG11/coords.html#InterfaceSVGMatrix) except:
 *
 *   1. Ext.draw.Matrix is not read only
 *   2. Using Number as its values rather than floats
 * 
 * Using this class helps to reduce the severe numeric 
 * [problem with HTML Canvas and SVG transformation](http://stackoverflow.com/questions/8784405/large-numbers-in-html-canvas-translate-result-in-strange-behavior)
 * 
 * Additionally, there's no way to get the current transformation matrix 
 * [in Canvas](http://stackoverflow.com/questions/7395813/html5-canvas-get-transform-matrix).
 */
Ext.define('Ext.draw.Matrix', {
    isMatrix: true,
    statics: {
        /**
         * @static
         * Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p)
         * and (x1p, y1p)
         * @param {Number} x0
         * @param {Number} y0
         * @param {Number} x1
         * @param {Number} y1
         * @param {Number} x0p
         * @param {Number} y0p
         * @param {Number} x1p
         * @param {Number} y1p
         */
        createAffineMatrixFromTwoPair: function(x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
            var dx = x1 - x0,
                dy = y1 - y0,
                dxp = x1p - x0p,
                dyp = y1p - y0p,
                r = 1 / (dx * dx + dy * dy),
                a = dx * dxp + dy * dyp,
                b = dxp * dy - dx * dyp,
                c = -a * x0 - b * y0,
                f = b * x0 - a * y0;
            return new this(a * r, -b * r, b * r, a * r, c * r + x0p, f * r + y0p);
        },
        /**
         * @static
         * Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p)
         * and (x1p, y1p)
         * @param {Number} x0
         * @param {Number} y0
         * @param {Number} x1
         * @param {Number} y1
         * @param {Number} x0p
         * @param {Number} y0p
         * @param {Number} x1p
         * @param {Number} y1p
         */
        createPanZoomFromTwoPair: function(x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
            if (arguments.length === 2) {
                return this.createPanZoomFromTwoPair.apply(this, x0.concat(y0));
            }
            // eslint-disable-next-line vars-on-top
            var dx = x1 - x0,
                dy = y1 - y0,
                cx = (x0 + x1) * 0.5,
                cy = (y0 + y1) * 0.5,
                dxp = x1p - x0p,
                dyp = y1p - y0p,
                cxp = (x0p + x1p) * 0.5,
                cyp = (y0p + y1p) * 0.5,
                r = dx * dx + dy * dy,
                rp = dxp * dxp + dyp * dyp,
                scale = Math.sqrt(rp / r);
            return new this(scale, 0, 0, scale, cxp - scale * cx, cyp - scale * cy);
        },
        /**
         * @method
         * @static
         * Create a flyweight to wrap the given array.
         * The flyweight will directly refer the object and the elements can be changed
         * by other methods.
         *
         * Do not hold the instance of flyweight matrix.
         *
         * @param {Array} elements
         * @return {Ext.draw.Matrix}
         */
        fly: (function() {
            var flyMatrix = null,
                simplefly = function(elements) {
                    flyMatrix.elements = elements;
                    return flyMatrix;
                };
            return function(elements) {
                if (!flyMatrix) {
                    flyMatrix = new Ext.draw.Matrix();
                }
                flyMatrix.elements = elements;
                Ext.draw.Matrix.fly = simplefly;
                return flyMatrix;
            };
        })(),
        /**
         * @static
         * Create a matrix from `mat`. If `mat` is already a matrix, returns it.
         * @param {Mixed} mat
         * @return {Ext.draw.Matrix}
         */
        create: function(mat) {
            if (mat instanceof this) {
                return mat;
            }
            return new this(mat);
        }
    },
    /**
     * Create an affine transform matrix.
     *
     * @param {Number} xx Coefficient from x to x
     * @param {Number} xy Coefficient from x to y
     * @param {Number} yx Coefficient from y to x
     * @param {Number} yy Coefficient from y to y
     * @param {Number} dx Offset of x
     * @param {Number} dy Offset of y
     */
    constructor: function(xx, xy, yx, yy, dx, dy) {
        if (xx && xx.length === 6) {
            this.elements = xx.slice();
        } else if (xx !== undefined) {
            this.elements = [
                xx,
                xy,
                yx,
                yy,
                dx,
                dy
            ];
        } else {
            this.elements = [
                1,
                0,
                0,
                1,
                0,
                0
            ];
        }
    },
    /**
     * Prepend a matrix onto the current.
     *
     * __Note:__ The given transform will come after the current one.
     *
     * @param {Number} xx Coefficient from x to x.
     * @param {Number} xy Coefficient from x to y.
     * @param {Number} yx Coefficient from y to x.
     * @param {Number} yy Coefficient from y to y.
     * @param {Number} dx Offset of x.
     * @param {Number} dy Offset of y.
     * @return {Ext.draw.Matrix} this
     */
    prepend: function(xx, xy, yx, yy, dx, dy) {
        var elements = this.elements,
            xx0 = elements[0],
            xy0 = elements[1],
            yx0 = elements[2],
            yy0 = elements[3],
            dx0 = elements[4],
            dy0 = elements[5];
        elements[0] = xx * xx0 + yx * xy0;
        elements[1] = xy * xx0 + yy * xy0;
        elements[2] = xx * yx0 + yx * yy0;
        elements[3] = xy * yx0 + yy * yy0;
        elements[4] = xx * dx0 + yx * dy0 + dx;
        elements[5] = xy * dx0 + yy * dy0 + dy;
        return this;
    },
    /**
     * Prepend a matrix onto the current.
     *
     * __Note:__ The given transform will come after the current one.
     * @param {Ext.draw.Matrix} matrix
     * @return {Ext.draw.Matrix} this
     */
    prependMatrix: function(matrix) {
        return this.prepend.apply(this, matrix.elements);
    },
    /**
     * Postpend a matrix onto the current.
     *
     * __Note:__ The given transform will come before the current one.
     *
     * @param {Number} xx Coefficient from x to x.
     * @param {Number} xy Coefficient from x to y.
     * @param {Number} yx Coefficient from y to x.
     * @param {Number} yy Coefficient from y to y.
     * @param {Number} dx Offset of x.
     * @param {Number} dy Offset of y.
     * @return {Ext.draw.Matrix} this
     */
    append: function(xx, xy, yx, yy, dx, dy) {
        var elements = this.elements,
            xx0 = elements[0],
            xy0 = elements[1],
            yx0 = elements[2],
            yy0 = elements[3],
            dx0 = elements[4],
            dy0 = elements[5];
        elements[0] = xx * xx0 + xy * yx0;
        elements[1] = xx * xy0 + xy * yy0;
        elements[2] = yx * xx0 + yy * yx0;
        elements[3] = yx * xy0 + yy * yy0;
        elements[4] = dx * xx0 + dy * yx0 + dx0;
        elements[5] = dx * xy0 + dy * yy0 + dy0;
        return this;
    },
    /**
     * Postpend a matrix onto the current.
     *
     * __Note:__ The given transform will come before the current one.
     *
     * @param {Ext.draw.Matrix} matrix
     * @return {Ext.draw.Matrix} this
     */
    appendMatrix: function(matrix) {
        return this.append.apply(this, matrix.elements);
    },
    /**
     * Set the elements of a Matrix
     * @param {Number} xx
     * @param {Number} xy
     * @param {Number} yx
     * @param {Number} yy
     * @param {Number} dx
     * @param {Number} dy
     * @return {Ext.draw.Matrix} this
     */
    set: function(xx, xy, yx, yy, dx, dy) {
        var elements = this.elements;
        elements[0] = xx;
        elements[1] = xy;
        elements[2] = yx;
        elements[3] = yy;
        elements[4] = dx;
        elements[5] = dy;
        return this;
    },
    /**
     * Return a new matrix represents the opposite transformation of the current one.
     *
     * @param {Ext.draw.Matrix} [target] A target matrix. If present, it will receive
     * the result of inversion to avoid creating a new object.
     *
     * @return {Ext.draw.Matrix}
     */
    inverse: function(target) {
        var elements = this.elements,
            a = elements[0],
            b = elements[1],
            c = elements[2],
            d = elements[3],
            e = elements[4],
            f = elements[5],
            rDim = 1 / (a * d - b * c);
        a *= rDim;
        b *= rDim;
        c *= rDim;
        d *= rDim;
        if (target) {
            target.set(d, -b, -c, a, c * f - d * e, b * e - a * f);
            return target;
        } else {
            return new Ext.draw.Matrix(d, -b, -c, a, c * f - d * e, b * e - a * f);
        }
    },
    /**
     * Translate the matrix.
     *
     * @param {Number} x
     * @param {Number} y
     * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
     * @return {Ext.draw.Matrix} this
     */
    translate: function(x, y, prepend) {
        if (prepend) {
            return this.prepend(1, 0, 0, 1, x, y);
        } else {
            return this.append(1, 0, 0, 1, x, y);
        }
    },
    /**
     * Scale the matrix.
     *
     * @param {Number} sx
     * @param {Number} sy
     * @param {Number} scx
     * @param {Number} scy
     * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
     * @return {Ext.draw.Matrix} this
     */
    scale: function(sx, sy, scx, scy, prepend) {
        var me = this;
        // null or undefined
        if (sy == null) {
            sy = sx;
        }
        if (scx === undefined) {
            scx = 0;
        }
        if (scy === undefined) {
            scy = 0;
        }
        if (prepend) {
            return me.prepend(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
        } else {
            return me.append(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
        }
    },
    /**
     * Rotate the matrix.
     *
     * @param {Number} angle Radians to rotate
     * @param {Number|null} rcx Center of rotation.
     * @param {Number|null} rcy Center of rotation.
     * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
     * @return {Ext.draw.Matrix} this
     */
    rotate: function(angle, rcx, rcy, prepend) {
        var me = this,
            cos = Math.cos(angle),
            sin = Math.sin(angle);
        rcx = rcx || 0;
        rcy = rcy || 0;
        if (prepend) {
            return me.prepend(cos, sin, -sin, cos, rcx - cos * rcx + rcy * sin, rcy - cos * rcy - rcx * sin);
        } else {
            return me.append(cos, sin, -sin, cos, rcx - cos * rcx + rcy * sin, rcy - cos * rcy - rcx * sin);
        }
    },
    /**
     * Rotate the matrix by the angle of a vector.
     *
     * @param {Number} x
     * @param {Number} y
     * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
     * @return {Ext.draw.Matrix} this
     */
    rotateFromVector: function(x, y, prepend) {
        var me = this,
            d = Math.sqrt(x * x + y * y),
            cos = x / d,
            sin = y / d;
        if (prepend) {
            return me.prepend(cos, sin, -sin, cos, 0, 0);
        } else {
            return me.append(cos, sin, -sin, cos, 0, 0);
        }
    },
    /**
     * Clone this matrix.
     * @return {Ext.draw.Matrix}
     */
    clone: function() {
        return new Ext.draw.Matrix(this.elements);
    },
    /**
     * Horizontally flip the matrix
     * @return {Ext.draw.Matrix} this
     */
    flipX: function() {
        return this.append(-1, 0, 0, 1, 0, 0);
    },
    /**
     * Vertically flip the matrix
     * @return {Ext.draw.Matrix} this
     */
    flipY: function() {
        return this.append(1, 0, 0, -1, 0, 0);
    },
    /**
     * Skew the matrix
     * @param {Number} angle
     * @return {Ext.draw.Matrix} this
     */
    skewX: function(angle) {
        return this.append(1, 0, Math.tan(angle), 1, 0, 0);
    },
    /**
     * Skew the matrix
     * @param {Number} angle
     * @return {Ext.draw.Matrix} this
     */
    skewY: function(angle) {
        return this.append(1, Math.tan(angle), 0, 1, 0, 0);
    },
    /**
     * Shear the matrix along the x-axis.
     * @param factor The horizontal shear factor.
     * @return {Ext.draw.Matrix} this
     */
    shearX: function(factor) {
        return this.append(1, 0, factor, 1, 0, 0);
    },
    /**
     * Shear the matrix along the y-axis.
     * @param factor The vertical shear factor.
     * @return {Ext.draw.Matrix} this
     */
    shearY: function(factor) {
        return this.append(1, factor, 0, 1, 0, 0);
    },
    /**
     * Reset the matrix to identical.
     * @return {Ext.draw.Matrix} this
     */
    reset: function() {
        return this.set(1, 0, 0, 1, 0, 0);
    },
    /* eslint-disable max-len */
    /**
     * @private
     * Split Matrix to `{{devicePixelRatio,c,0},{b,devicePixelRatio,0},{0,0,1}}.{{xx,0,dx},{0,yy,dy},{0,0,1}}`
     * @return {Object} Object with b,c,d=devicePixelRatio,xx,yy,dx,dy
     */
    precisionCompensate: function(devicePixelRatio, comp) {
        /* eslint-enable max-len */
        var elements = this.elements,
            x2x = elements[0],
            x2y = elements[1],
            y2x = elements[2],
            y2y = elements[3],
            newDx = elements[4],
            newDy = elements[5],
            r = x2y * y2x - x2x * y2y;
        comp.b = devicePixelRatio * x2y / x2x;
        comp.c = devicePixelRatio * y2x / y2y;
        comp.d = devicePixelRatio;
        comp.xx = x2x / devicePixelRatio;
        comp.yy = y2y / devicePixelRatio;
        comp.dx = (newDy * x2x * y2x - newDx * x2x * y2y) / r / devicePixelRatio;
        comp.dy = (newDx * x2y * y2y - newDy * x2x * y2y) / r / devicePixelRatio;
    },
    /**
     * @private
     * Split Matrix to `{{1,c,0},{b,d,0},{0,0,1}}.{{xx,0,dx},{0,xx,dy},{0,0,1}}`
     * @return {Object} Object with b,c,d,xx,yy=xx,dx,dy
     */
    precisionCompensateRect: function(devicePixelRatio, comp) {
        var elements = this.elements,
            x2x = elements[0],
            x2y = elements[1],
            y2x = elements[2],
            y2y = elements[3],
            newDx = elements[4],
            newDy = elements[5],
            yxOnXx = y2x / x2x;
        comp.b = devicePixelRatio * x2y / x2x;
        comp.c = devicePixelRatio * yxOnXx;
        comp.d = devicePixelRatio * y2y / x2x;
        comp.xx = x2x / devicePixelRatio;
        comp.yy = x2x / devicePixelRatio;
        comp.dx = (newDy * y2x - newDx * y2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
        comp.dy = -(newDy * x2x - newDx * x2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
    },
    /**
     * Transform point returning the x component of the result.
     * @param {Number} x
     * @param {Number} y
     * @return {Number} x component of the result.
     */
    x: function(x, y) {
        var elements = this.elements;
        return x * elements[0] + y * elements[2] + elements[4];
    },
    /**
     * Transform point returning the y component of the result.
     * @param {Number} x
     * @param {Number} y
     * @return {Number} y component of the result.
     */
    y: function(x, y) {
        var elements = this.elements;
        return x * elements[1] + y * elements[3] + elements[5];
    },
    /**
     * @private
     * @param {Number} i
     * @param {Number} j
     * @return {String}
     */
    get: function(i, j) {
        return +this.elements[i + j * 2].toFixed(4);
    },
    /**
     * Transform a point to a new array.
     * @param {Array} point
     * @return {Array}
     */
    transformPoint: function(point) {
        var elements = this.elements,
            x, y;
        if (point.isPoint) {
            x = point.x;
            y = point.y;
        } else {
            x = point[0];
            y = point[1];
        }
        return [
            x * elements[0] + y * elements[2] + elements[4],
            x * elements[1] + y * elements[3] + elements[5]
        ];
    },
    /**
     * @param {Object} bbox Given as `{x: Number, y: Number, width: Number, height: Number}`.
     * @param {Number} [radius]
     * @param {Object} [target] Optional target object to recieve the result.
     * Recommended to use it for better gc.
     *
     * @return {Object} Object with x, y, width and height.
     */
    transformBBox: function(bbox, radius, target) {
        var elements = this.elements,
            l = bbox.x,
            t = bbox.y,
            w0 = bbox.width * 0.5,
            h0 = bbox.height * 0.5,
            xx = elements[0],
            xy = elements[1],
            yx = elements[2],
            yy = elements[3],
            cx = l + w0,
            cy = t + h0,
            w, h, scales;
        if (radius) {
            w0 -= radius;
            h0 -= radius;
            scales = [
                Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]),
                Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3])
            ];
            w = Math.abs(w0 * xx) + Math.abs(h0 * yx) + Math.abs(scales[0] * radius);
            h = Math.abs(w0 * xy) + Math.abs(h0 * yy) + Math.abs(scales[1] * radius);
        } else {
            w = Math.abs(w0 * xx) + Math.abs(h0 * yx);
            h = Math.abs(w0 * xy) + Math.abs(h0 * yy);
        }
        if (!target) {
            target = {};
        }
        target.x = cx * xx + cy * yx + elements[4] - w;
        target.y = cx * xy + cy * yy + elements[5] - h;
        target.width = w + w;
        target.height = h + h;
        return target;
    },
    /**
     * Transform a list for points.
     *
     * __Note:__ will change the original list but not points inside it.
     * @param {Array} list
     * @return {Array} list
     */
    transformList: function(list) {
        var elements = this.elements,
            xx = elements[0],
            yx = elements[2],
            dx = elements[4],
            xy = elements[1],
            yy = elements[3],
            dy = elements[5],
            ln = list.length,
            p, i;
        for (i = 0; i < ln; i++) {
            p = list[i];
            list[i] = [
                p[0] * xx + p[1] * yx + dx,
                p[0] * xy + p[1] * yy + dy
            ];
        }
        return list;
    },
    /**
     * Determines whether this matrix is an identity matrix (no transform).
     * @return {Boolean}
     */
    isIdentity: function() {
        var elements = this.elements;
        return elements[0] === 1 && elements[1] === 0 && elements[2] === 0 && elements[3] === 1 && elements[4] === 0 && elements[5] === 0;
    },
    /**
     * Determines if this matrix has the same values as another matrix.
     * @param {Ext.draw.Matrix} matrix A maxtrix or array of its elements.
     * @return {Boolean}
     */
    isEqual: function(matrix) {
        var elements = matrix && matrix.isMatrix ? matrix.elements : matrix,
            myElements = this.elements;
        return myElements[0] === elements[0] && myElements[1] === elements[1] && myElements[2] === elements[2] && myElements[3] === elements[3] && myElements[4] === elements[4] && myElements[5] === elements[5];
    },
    /**
     * @deprecated 6.0.1 This method is deprecated.
     * Determines if this matrix has the same values as another matrix.
     * @param {Ext.draw.Matrix} matrix
     * @return {Boolean}
     */
    equals: function(matrix) {
        return this.isEqual(matrix);
    },
    /**
     * Create an array of elements by horizontal order (xx,yx,dx,yx,yy,dy).
     * @return {Array}
     */
    toArray: function() {
        var elements = this.elements;
        return [
            elements[0],
            elements[2],
            elements[4],
            elements[1],
            elements[3],
            elements[5]
        ];
    },
    /**
     * Create an array of elements by vertical order (xx,xy,yx,yy,dx,dy).
     * @return {Array|String}
     */
    toVerticalArray: function() {
        return this.elements.slice();
    },
    /**
     * Get an array of elements.
     * The numbers are rounded to keep only 4 decimals.
     * @return {Array}
     */
    toString: function() {
        var me = this;
        return [
            me.get(0, 0),
            me.get(0, 1),
            me.get(1, 0),
            me.get(1, 1),
            me.get(2, 0),
            me.get(2, 1)
        ].join(',');
    },
    /**
     * Apply the matrix to a drawing context.
     * @param {Object} ctx
     * @return {Ext.draw.Matrix} this
     */
    toContext: function(ctx) {
        ctx.transform.apply(ctx, this.elements);
        return this;
    },
    /**
     * Return a string that can be used as transform attribute in SVG.
     * @return {String}
     */
    toSvg: function() {
        var elements = this.elements;
        // The reason why we cannot use `.join` is the `1e5` form is not accepted in svg.
        return "matrix(" + elements[0].toFixed(9) + ',' + elements[1].toFixed(9) + ',' + elements[2].toFixed(9) + ',' + elements[3].toFixed(9) + ',' + elements[4].toFixed(9) + ',' + elements[5].toFixed(9) + ")";
    },
    /**
     * Get the x scale of the matrix.
     * @return {Number}
     */
    getScaleX: function() {
        var elements = this.elements;
        return Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]);
    },
    /**
     * Get the y scale of the matrix.
     * @return {Number}
     */
    getScaleY: function() {
        var elements = this.elements;
        return Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3]);
    },
    /**
     * Get x-to-x component of the matrix
     * @return {Number}
     */
    getXX: function() {
        return this.elements[0];
    },
    /**
     * Get x-to-y component of the matrix.
     * @return {Number}
     */
    getXY: function() {
        return this.elements[1];
    },
    /**
     * Get y-to-x component of the matrix.
     * @return {Number}
     */
    getYX: function() {
        return this.elements[2];
    },
    /**
     * Get y-to-y component of the matrix.
     * @return {Number}
     */
    getYY: function() {
        return this.elements[3];
    },
    /**
     * Get offset x component of the matrix.
     * @return {Number}
     */
    getDX: function() {
        return this.elements[4];
    },
    /**
     * Get offset y component of the matrix.
     * @return {Number}
     */
    getDY: function() {
        return this.elements[5];
    },
    /**
     * Splits this transformation matrix into Scale, Rotate, Translate components,
     * assuming it was produced by applying transformations in that order.
     * @return {Object}
     */
    split: function() {
        var el = this.elements,
            xx = el[0],
            xy = el[1],
            yy = el[3],
            out = {
                translateX: el[4],
                translateY: el[5]
            };
        out.rotate = out.rotation = Math.atan2(xy, xx);
        out.scaleX = xx / Math.cos(out.rotate);
        out.scaleY = yy / xx * out.scaleX;
        return out;
    }
}, function() {
    function registerName(properties, name, i) {
        properties[name] = {
            get: function() {
                return this.elements[i];
            },
            set: function(val) {
                this.elements[i] = val;
            }
        };
    }
    // Compatibility with SVGMatrix.
    if (Object.defineProperties) {
        var properties = {};
        // eslint-disable-line vars-on-top
        /**
         * @property {Number} a Get x-to-x component of the matrix. Avoid using it for performance
         * consideration.
         * Use {@link #getXX} instead.
         */
        registerName(properties, 'a', 0);
        registerName(properties, 'b', 1);
        registerName(properties, 'c', 2);
        registerName(properties, 'd', 3);
        registerName(properties, 'e', 4);
        registerName(properties, 'f', 5);
        Object.defineProperties(this.prototype, properties);
    }
    /**
     * Performs matrix multiplication. This matrix is post-multiplied by another matrix.
     *
     * __Note:__ The given transform will come before the current one.
     *
     * @method
     * @param {Ext.draw.Matrix} matrix
     * @return {Ext.draw.Matrix} this
     */
    this.prototype.multiply = this.prototype.appendMatrix;
});

/**
 * @class Ext.draw.modifier.Modifier
 *
 * Each sprite has a stack of modifiers. The resulting attributes of sprite is
 * the content of the stack top. When setting attributes to a sprite,
 * changes will be pushed-down though the stack of modifiers and pop-back the
 * additive changes; When modifier is triggered to change the attribute of a
 * sprite, it will pop-up the changes to the top.
 */
Ext.define('Ext.draw.modifier.Modifier', {
    isModifier: true,
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    config: {
        /**
         * @private
         * @cfg {Ext.draw.modifier.Modifier} lower Modifier that receives the push-down changes.
         */
        lower: null,
        /**
         * @private
         * @cfg {Ext.draw.modifier.Modifier} upper Modifier that receives the pop-up changes.
         */
        upper: null,
        /**
         * @cfg {Ext.draw.sprite.Sprite} sprite The sprite to which the modifier belongs.
         */
        sprite: null
    },
    constructor: function(config) {
        this.mixins.observable.constructor.call(this, config);
    },
    updateUpper: function(upper) {
        if (upper) {
            upper.setLower(this);
        }
    },
    updateLower: function(lower) {
        if (lower) {
            lower.setUpper(this);
        }
    },
    /**
     * @private
     * Validate attribute set before use.
     *
     * @param {Object} attr The attribute to be validated. Note that it may be already initialized,
     * so do not override properties that have already been used.
     */
    prepareAttributes: function(attr) {
        if (this._lower) {
            this._lower.prepareAttributes(attr);
        }
    },
    /**
     * @private
     * Invoked when changes need to be popped up to the top.
     * @param {Object} attr The source attributes.
     * @param {Object} changes The changes to be popped up.
     */
    popUp: function(attr, changes) {
        if (this._upper) {
            this._upper.popUp(attr, changes);
        } else {
            Ext.apply(attr, changes);
        }
    },
    /**
     * @private
     *
     * This method will filter out the properties from the `changes` object, if they
     * have the same values as in the `attr` object (sprite's attributes).
     *
     * If the `receiver` object is provided, the attributes with the new values will be
     * copied from the `changes` object to the `receiver` object, and the `changes`
     * object will be left unchanged.
     *
     * The method returns the `receiver` object, if it was provided, or the `changes`
     * object otherwise.
     *
     * The method also handles a special case when a sprite attribute that is meant to be
     * animated was set to a certain value (e.g. 5), that is different from the original
     * value (e.g. 3) of the attribute, and immediately set to another value again, that
     * is the same as the original value (3). In this case, the attribute's current
     * value is still the original value, because the attribute hasn't started animating
     * yet, so a comparison against the current value is not appropriate, and the target
     * value (value at the end of animation, 5) should be used for comparison instead, so
     * that 3 won't be filtered out.
     */
    filterChanges: function(attr, changes, receiver) {
        var targets = attr.targets,
            name, value;
        if (receiver) {
            for (name in changes) {
                value = changes[name];
                if (value !== attr[name] || (targets && value !== targets[name])) {
                    receiver[name] = value;
                }
            }
        } else {
            for (name in changes) {
                value = changes[name];
                if (value === attr[name] && (!targets || value === targets[name])) {
                    delete changes[name];
                }
            }
        }
        return receiver || changes;
    },
    /**
     * @private
     * Invoked when changes need to be pushed down to the sprite.
     * @param {Object} attr The source attributes.
     * @param {Object} changes The changes to make. This object might be changed unexpectedly
     * inside the method.
     * @return {Mixed}
     */
    pushDown: function(attr, changes) {
        return this._lower ? this._lower.pushDown(attr, changes) : this.filterChanges(attr, changes);
    }
});

/**
 * @class Ext.draw.modifier.Target
 * @extends Ext.draw.modifier.Modifier
 *
 * This is the destination (top) modifier that has to be put at
 * the top of the modifier stack.
 *
 * The Target modifier figures out which updaters have to be called
 * for the changed set of attributes and makes the sprite and its instances (if any)
 * call them.
 */
Ext.define('Ext.draw.modifier.Target', {
    requires: [
        'Ext.draw.Matrix'
    ],
    extend: 'Ext.draw.modifier.Modifier',
    alias: 'modifier.target',
    statics: {
        /**
         * @private
         */
        uniqueId: 0
    },
    prepareAttributes: function(attr) {
        if (this._lower) {
            this._lower.prepareAttributes(attr);
        }
        attr.attributeId = 'attribute-' + Ext.draw.modifier.Target.uniqueId++;
        if (!attr.hasOwnProperty('canvasAttributes')) {
            attr.bbox = {
                plain: {
                    dirty: true
                },
                transform: {
                    dirty: true
                }
            };
            attr.dirty = true;
            /*
            Maps updaters that have to be called to the attributes that triggered the update.
            It is basically a reversed `triggers` map (see Ext.draw.sprite.AttributeDefinition),
            but only for those attributes that have changed.
            Pending updaters are called by the Ext.draw.sprite.Sprite.callUpdaters method.

            The 'canvas' updater is a special kind of updater that is not actually a function
            but a flag indicating that the attribute should be applied directly to a canvas
            context.
            */
            attr.pendingUpdaters = {};
            /*
            Holds the attributes that triggered the canvas update (attr.pendingUpdaters.canvas).
            Canvas attributes are applied directly to a canvas context
            by the sprite.useAttributes method.
            */
            attr.canvasAttributes = {};
            attr.matrix = new Ext.draw.Matrix();
            attr.inverseMatrix = new Ext.draw.Matrix();
        }
    },
    /**
     * @private
     * Applies changes to sprite/instance attributes and determines which updaters
     * have to be called as a result of attributes change.
     * @param {Object} attr The source attributes.
     * @param {Object} changes The modifier changes.
     */
    applyChanges: function(attr, changes) {
        Ext.apply(attr, changes);
        // eslint-disable-next-line vars-on-top
        var sprite = this.getSprite(),
            pendingUpdaters = attr.pendingUpdaters,
            triggers = sprite.self.def.getTriggers(),
            updaters, instances, instance, name, hasChanges, canvasAttributes, i, j, ln;
        for (name in changes) {
            hasChanges = true;
            if ((updaters = triggers[name])) {
                sprite.scheduleUpdaters(attr, updaters, [
                    name
                ]);
            }
            if (attr.template && changes.removeFromInstance && changes.removeFromInstance[name]) {
                delete attr[name];
            }
        }
        if (!hasChanges) {
            return;
        }
        // This can prevent sub objects to set duplicated attributes to context.
        if (pendingUpdaters.canvas) {
            canvasAttributes = pendingUpdaters.canvas;
            delete pendingUpdaters.canvas;
            for (i = 0 , ln = canvasAttributes.length; i < ln; i++) {
                name = canvasAttributes[i];
                attr.canvasAttributes[name] = attr[name];
            }
        }
        // If the attributes of an instancing sprite template are being modified here,
        // then spread the pending updaters to the instances (template's children).
        if (attr.hasOwnProperty('children')) {
            instances = attr.children;
            for (i = 0 , ln = instances.length; i < ln; i++) {
                instance = instances[i];
                Ext.apply(instance.pendingUpdaters, pendingUpdaters);
                if (canvasAttributes) {
                    for (j = 0; j < canvasAttributes.length; j++) {
                        name = canvasAttributes[j];
                        instance.canvasAttributes[name] = instance[name];
                    }
                }
                sprite.callUpdaters(instance);
            }
        }
        sprite.setDirty(true);
        sprite.callUpdaters(attr);
    },
    popUp: function(attr, changes) {
        this.applyChanges(attr, changes);
    },
    pushDown: function(attr, changes) {
        // Modifier chain looks like this:
        // sprite.modifiers.target <---> postFx <---> sprite.modifiers.animation <---> preFx
        // There can be any number of postFx and preFx modifiers, the difference between them
        // is that:
        // `preFx` modifier changes are animated.
        // `postFx` modifier changes are not.
        // preFx modifiers include Highlight (Draw) and Callout (Charts).
        // There are no postFx modifiers at the moment.
        if (this._lower) {
            // Without any postFx modifiers, `lower` is going to be Animation.
            changes = this._lower.pushDown(attr, changes);
        }
        this.applyChanges(attr, changes);
        return changes;
    }
});

/**
 * @class Ext.draw.TimingFunctions
 * @singleton
 * 
 * Singleton that provides easing functions for use in sprite animations.
 */
Ext.define('Ext.draw.TimingFunctions', function() {
    var pow = Math.pow,
        sin = Math.sin,
        cos = Math.cos,
        sqrt = Math.sqrt,
        pi = Math.PI,
        poly = [
            'quad',
            'cube',
            'quart',
            'quint'
        ],
        easings = {
            pow: function(p, x) {
                return pow(p, x || 6);
            },
            expo: function(p) {
                return pow(2, 8 * (p - 1));
            },
            circ: function(p) {
                return 1 - sqrt(1 - p * p);
            },
            sine: function(p) {
                return 1 - sin((1 - p) * pi / 2);
            },
            back: function(p, n) {
                n = n || 1.616;
                return p * p * ((n + 1) * p - n);
            },
            bounce: function(p) {
                var a, b;
                // eslint-disable-next-line no-constant-condition
                for (a = 0 , b = 1; 1; a += b , b /= 2) {
                    if (p >= (7 - 4 * a) / 11) {
                        return b * b - pow((11 - 6 * a - 11 * p) / 4, 2);
                    }
                }
            },
            elastic: function(p, x) {
                return pow(2, 10 * --p) * cos(20 * p * pi * (x || 1) / 3);
            }
        },
        easingsMap = {},
        name, len, i;
    // Create polynomial easing equations.
    function createPoly(times) {
        return function(p) {
            return pow(p, times);
        };
    }
    function addEasing(name, easing) {
        easingsMap[name + 'In'] = function(pos) {
            return easing(pos);
        };
        easingsMap[name + 'Out'] = function(pos) {
            return 1 - easing(1 - pos);
        };
        easingsMap[name + 'InOut'] = function(pos) {
            return (pos <= 0.5) ? easing(2 * pos) / 2 : (2 - easing(2 * (1 - pos))) / 2;
        };
    }
    for (i = 0 , len = poly.length; i < len; ++i) {
        easings[poly[i]] = createPoly(i + 2);
    }
    for (name in easings) {
        addEasing(name, easings[name]);
    }
    // Add linear interpolator.
    easingsMap.linear = Ext.identityFn;
    // Add aliases for quad easings.
    easingsMap.easeIn = easingsMap.quadIn;
    easingsMap.easeOut = easingsMap.quadOut;
    easingsMap.easeInOut = easingsMap.quadInOut;
    return {
        singleton: true,
        easingMap: easingsMap
    };
}, function(Cls) {
    Ext.apply(Cls, Cls.easingMap);
});

/**
 * @class Ext.draw.Animator
 *
 * Singleton class that manages the animation pool.
 */
Ext.define('Ext.draw.Animator', {
    uses: [
        'Ext.draw.Draw'
    ],
    singleton: true,
    frameCallbacks: {},
    frameCallbackId: 0,
    scheduled: 0,
    frameStartTimeOffset: Ext.now(),
    animations: [],
    running: false,
    /**
     *  Cross platform `animationTime` implementation.
     *  @return {Number}
     */
    animationTime: function() {
        return Ext.AnimationQueue.frameStartTime - this.frameStartTimeOffset;
    },
    /**
     * Adds an animated object to the animation pool.
     *
     * @param {Object} animation The animation descriptor to add to the pool.
     */
    add: function(animation) {
        var me = this;
        if (!me.contains(animation)) {
            me.animations.push(animation);
            me.ignite();
            if ('fireEvent' in animation) {
                animation.fireEvent('animationstart', animation);
            }
        }
    },
    /**
     * Removes an animation from the pool.
     * TODO: This is broken when called within `step` method.
     * @param {Object} animation The animation to remove from the pool.
     */
    remove: function(animation) {
        var me = this,
            animations = me.animations,
            i = 0,
            l = animations.length;
        for (; i < l; ++i) {
            if (animations[i] === animation) {
                animations.splice(i, 1);
                if ('fireEvent' in animation) {
                    animation.fireEvent('animationend', animation);
                }
                return;
            }
        }
    },
    /**
     * Returns `true` or `false` whether it contains the given animation or not.
     *
     * @param {Object} animation The animation to check for.
     * @return {Boolean}
     */
    contains: function(animation) {
        return Ext.Array.indexOf(this.animations, animation) > -1;
    },
    /**
     * Returns `true` or `false` whether the pool is empty or not.
     * @return {Boolean}
     */
    empty: function() {
        return this.animations.length === 0;
    },
    idle: function() {
        return this.scheduled === 0 && this.animations.length === 0;
    },
    /**
     * Given a frame time it will filter out finished animations from the pool.
     *
     * @param {Number} frameTime The frame's start time, in milliseconds.
     */
    step: function(frameTime) {
        var me = this,
            animations = me.animations,
            animation,
            i = 0,
            ln = animations.length;
        for (; i < ln; i++) {
            animation = animations[i];
            animation.step(frameTime);
            if (!animation.animating) {
                animations.splice(i, 1);
                i--;
                ln--;
                if (animation.fireEvent) {
                    animation.fireEvent('animationend', animation);
                }
            }
        }
    },
    /**
     * Register a one-time callback that will be called at the next frame.
     * @param {Function/String} callback
     * @param {Object} scope
     * @return {String} The ID of the scheduled callback.
     */
    schedule: function(callback, scope) {
        var id = 'frameCallback' + (this.frameCallbackId++);
        scope = scope || this;
        if (Ext.isString(callback)) {
            callback = scope[callback];
        }
        Ext.draw.Animator.frameCallbacks[id] = {
            fn: callback,
            scope: scope,
            once: true
        };
        this.scheduled++;
        Ext.draw.Animator.ignite();
        return id;
    },
    /**
     * Register a one-time callback that will be called at the next frame,
     * if that callback (with a matching function and scope) isn't already scheduled.
     * @param {Function/String} callback
     * @param {Object} scope
     * @return {String/null} The ID of the scheduled callback or null, if that callback
     * has already been scheduled.
     */
    scheduleIf: function(callback, scope) {
        var frameCallbacks = Ext.draw.Animator.frameCallbacks,
            cb, id;
        scope = scope || this;
        if (Ext.isString(callback)) {
            callback = scope[callback];
        }
        for (id in frameCallbacks) {
            cb = frameCallbacks[id];
            if (cb.once && cb.fn === callback && cb.scope === scope) {
                return null;
            }
        }
        return this.schedule(callback, scope);
    },
    /**
     * Cancel a registered one-time callback
     * @param {String} id
     */
    cancel: function(id) {
        if (Ext.draw.Animator.frameCallbacks[id] && Ext.draw.Animator.frameCallbacks[id].once) {
            this.scheduled = Math.max(--this.scheduled, 0);
            delete Ext.draw.Animator.frameCallbacks[id];
            Ext.draw.Draw.endUpdateIOS();
        }
        if (this.idle()) {
            this.extinguish();
        }
    },
    clear: function() {
        this.animations.length = 0;
        Ext.draw.Animator.frameCallbacks = {};
        this.extinguish();
    },
    /**
     * Register a recursive callback that will be called at every frame.
     *
     * @param {Function} callback
     * @param {Object} scope
     * @return {String}
     */
    addFrameCallback: function(callback, scope) {
        var id = 'frameCallback' + (this.frameCallbackId++);
        scope = scope || this;
        if (Ext.isString(callback)) {
            callback = scope[callback];
        }
        Ext.draw.Animator.frameCallbacks[id] = {
            fn: callback,
            scope: scope
        };
        return id;
    },
    /**
     * Unregister a recursive callback.
     * @param {String} id
     */
    removeFrameCallback: function(id) {
        delete Ext.draw.Animator.frameCallbacks[id];
        if (this.idle()) {
            this.extinguish();
        }
    },
    /**
     * @private
     */
    fireFrameCallbacks: function() {
        var callbacks = this.frameCallbacks,
            id, fn, cb;
        for (id in callbacks) {
            cb = callbacks[id];
            fn = cb.fn;
            if (Ext.isString(fn)) {
                fn = cb.scope[fn];
            }
            fn.call(cb.scope);
            if (callbacks[id] && cb.once) {
                this.scheduled = Math.max(--this.scheduled, 0);
                delete callbacks[id];
            }
        }
    },
    handleFrame: function() {
        var me = this;
        me.step(me.animationTime());
        me.fireFrameCallbacks();
        if (me.idle()) {
            me.extinguish();
        }
    },
    ignite: function() {
        if (!this.running) {
            this.running = true;
            Ext.AnimationQueue.start(this.handleFrame, this);
            Ext.draw.Draw.beginUpdateIOS();
        }
    },
    extinguish: function() {
        this.running = false;
        Ext.AnimationQueue.stop(this.handleFrame, this);
        Ext.draw.Draw.endUpdateIOS();
    }
});

/**
 * The Animation modifier.
 *
 * Sencha Charts allow users to use transitional animation on sprites. Simply set the duration
 * and easing in the animation modifier, then all the changes to the sprites will be animated.
 * 
 *     @example
 *     var drawCt = Ext.create({
 *         xtype: 'draw',
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400,
 *         sprites: [{
 *             type: 'rect',
 *             x: 50,
 *             y: 50,
 *             width: 100,
 *             height: 100,
 *             fillStyle: '#1F6D91'
 *         }]
 *     });
 *     
 *     var rect = drawCt.getSurface().getItems()[0];
 *     
 *     rect.setAnimation({
 *         duration: 1000,
 *         easing: 'elasticOut'
 *     });
 *     
 *     Ext.defer(function () {
 *         rect.setAttributes({
 *             width: 250
 *         });
 *     }, 500);
 *
 * Also, you can use different durations and easing functions on different attributes by using
 * {@link #customDurations} and {@link #customEasings}.
 *
 * By default, an animation modifier will be created during the initialization of a sprite.
 * You can get the animation modifier of a sprite via its 
 * {@link Ext.draw.sprite.Sprite#method-getAnimation getAnimation} method.
 */
Ext.define('Ext.draw.modifier.Animation', {
    extend: 'Ext.draw.modifier.Modifier',
    alias: 'modifier.animation',
    requires: [
        'Ext.draw.TimingFunctions',
        'Ext.draw.Animator'
    ],
    config: {
        /**
         * @cfg {Function} easing
         * Default easing function.
         */
        easing: Ext.identityFn,
        /**
         * @cfg {Number} duration
         * Default duration time (ms).
         */
        duration: 0,
        /**
         * @cfg {Object} customEasings Overrides the default easing function for defined attributes.
         * E.g.:
         *
         *     // Assuming the sprite the modifier is applied to is a 'circle'.
         *     customEasings: {
         *         r: 'easeOut',
         *         'fillStyle,strokeStyle': 'linear',
         *         'cx,cy': function (p, n) {
         *             p = 1 - p;
         *             n = n || 1.616;
         *             return 1 - p * p * ((n + 1) * p - n);
         *         }
         *     }
         */
        customEasings: {},
        /**
         * @cfg {Object} customDurations Overrides the default duration for defined attributes.
         * E.g.:
         *
         *     // Assuming the sprite the modifier is applied to is a 'circle'.
         *     customDurations: {
         *         r: 1000,
         *         'fillStyle,strokeStyle': 2000,
         *         'cx,cy': 1000
         *     }
         */
        customDurations: {}
    },
    constructor: function(config) {
        var me = this;
        me.anyAnimation = me.anySpecialAnimations = false;
        me.animating = 0;
        me.animatingPool = [];
        me.callParent([
            config
        ]);
    },
    prepareAttributes: function(attr) {
        if (!attr.hasOwnProperty('timers')) {
            attr.animating = false;
            attr.timers = {};
            // The 'targets' object is used to hold the target values for the
            // attributes while they are being animated from source to target values.
            // The 'targets' is pushed down to the lower level modifiers,
            // instead of the actual attr object, to hide the fact that the
            // attributes are being animated.
            attr.targets = Ext.Object.chain(attr);
            attr.targets.prototype = attr;
        }
        if (this._lower) {
            this._lower.prepareAttributes(attr.targets);
        }
    },
    updateSprite: function(sprite) {
        this.setConfig(sprite.config.animation);
    },
    updateDuration: function(duration) {
        this.anyAnimation = duration > 0;
    },
    applyEasing: function(easing) {
        if (typeof easing === 'string') {
            easing = Ext.draw.TimingFunctions.easingMap[easing];
        }
        return easing;
    },
    applyCustomEasings: function(newEasings, oldEasings) {
        var any, key, attrs, easing, i, ln;
        oldEasings = oldEasings || {};
        for (key in newEasings) {
            any = true;
            easing = newEasings[key];
            attrs = key.split(',');
            if (typeof easing === 'string') {
                easing = Ext.draw.TimingFunctions.easingMap[easing];
            }
            for (i = 0 , ln = attrs.length; i < ln; i++) {
                oldEasings[attrs[i]] = easing;
            }
        }
        if (any) {
            this.anySpecialAnimations = any;
        }
        return oldEasings;
    },
    /**
     * Set special easings on the given attributes. E.g.:
     *
     *     circleSprite.getAnimation().setEasingOn('r', 'elasticIn');
     *
     * @param {String/Array} attrs The source attribute(s).
     * @param {String} easing The special easings.
     */
    setEasingOn: function(attrs, easing) {
        var customEasings = {},
            i, ln;
        attrs = Ext.Array.from(attrs).slice();
        for (i = 0 , ln = attrs.length; i < ln; i++) {
            customEasings[attrs[i]] = easing;
        }
        this.setCustomEasings(customEasings);
    },
    /**
     * Remove special easings on the given attributes.
     * @param {String/Array} attrs The source attribute(s).
     */
    clearEasingOn: function(attrs) {
        var i, ln;
        attrs = Ext.Array.from(attrs, true);
        for (i = 0 , ln = attrs.length; i < ln; i++) {
            delete this._customEasings[attrs[i]];
        }
    },
    applyCustomDurations: function(newDurations, oldDurations) {
        var any, key, duration, attrs, i, ln;
        oldDurations = oldDurations || {};
        for (key in newDurations) {
            any = true;
            duration = newDurations[key];
            attrs = key.split(',');
            for (i = 0 , ln = attrs.length; i < ln; i++) {
                oldDurations[attrs[i]] = duration;
            }
        }
        if (any) {
            this.anySpecialAnimations = any;
        }
        return oldDurations;
    },
    /**
     * Set special duration on the given attributes. E.g.:
     *
     *     rectSprite.getAnimation().setDurationOn('height', 2000);
     *
     * @param {String/Array} attrs The source attributes.
     * @param {Number} duration The special duration.
     */
    setDurationOn: function(attrs, duration) {
        var customDurations = {},
            i, ln;
        attrs = Ext.Array.from(attrs).slice();
        for (i = 0 , ln = attrs.length; i < ln; i++) {
            customDurations[attrs[i]] = duration;
        }
        this.setCustomDurations(customDurations);
    },
    /**
     * Remove special easings on the given attributes.
     * @param {Object} attrs The source attributes.
     */
    clearDurationOn: function(attrs) {
        var i, ln;
        attrs = Ext.Array.from(attrs, true);
        for (i = 0 , ln = attrs.length; i < ln; i++) {
            delete this._customDurations[attrs[i]];
        }
    },
    /**
     * @private
     * Initializes Animator for the animation.
     * @param {Object} attr The source attributes.
     * @param {Boolean} animating The animating flag.
     */
    setAnimating: function(attr, animating) {
        var me = this,
            pool = me.animatingPool,
            i;
        if (attr.animating !== animating) {
            attr.animating = animating;
            if (animating) {
                pool.push(attr);
                if (me.animating === 0) {
                    Ext.draw.Animator.add(me);
                }
                me.animating++;
            } else {
                for (i = pool.length; i--; ) {
                    if (pool[i] === attr) {
                        pool.splice(i, 1);
                    }
                }
                me.animating = pool.length;
            }
        }
    },
    /**
     * @private
     * Set the attr with given easing and duration.
     * @param {Object} attr The attributes collection.
     * @param {Object} changes The changes that popped up from lower modifier.
     * @return {Object} The changes to pop up.
     */
    setAttrs: function(attr, changes) {
        var me = this,
            timers = attr.timers,
            parsers = me._sprite.self.def._animationProcessors,
            defaultEasing = me._easing,
            defaultDuration = me._duration,
            customDurations = me._customDurations,
            customEasings = me._customEasings,
            anySpecial = me.anySpecialAnimations,
            any = me.anyAnimation || anySpecial,
            targets = attr.targets,
            ignite = false,
            timer, name, newValue, startValue, parser, easing, duration, initial;
        if (!any) {
            // If there is no animation enabled.
            // When applying changes to attributes, simply stop current animation
            // and set the value.
            for (name in changes) {
                if (attr[name] === changes[name]) {
                    delete changes[name];
                } else {
                    attr[name] = changes[name];
                }
                delete targets[name];
                delete timers[name];
            }
            return changes;
        } else {
            // If any animation.
            for (name in changes) {
                newValue = changes[name];
                startValue = attr[name];
                if (newValue !== startValue && startValue !== undefined && startValue !== null && (parser = parsers[name])) {
                    // If this property is animating.
                    // Figure out the desired duration and easing.
                    easing = defaultEasing;
                    duration = defaultDuration;
                    if (anySpecial) {
                        // Deducing the easing function and duration
                        if (name in customEasings) {
                            easing = customEasings[name];
                        }
                        if (name in customDurations) {
                            duration = customDurations[name];
                        }
                    }
                    // Transitions betweens color and gradient or between gradients
                    // are not supported.
                    if (startValue && startValue.isGradient || newValue && newValue.isGradient) {
                        duration = 0;
                    }
                    // If the property is animating
                    if (duration) {
                        if (!timers[name]) {
                            timers[name] = {};
                        }
                        timer = timers[name];
                        timer.start = 0;
                        timer.easing = easing;
                        timer.duration = duration;
                        timer.compute = parser.compute;
                        timer.serve = parser.serve || Ext.identityFn;
                        timer.remove = changes.removeFromInstance && changes.removeFromInstance[name];
                        if (parser.parseInitial) {
                            initial = parser.parseInitial(startValue, newValue);
                            timer.source = initial[0];
                            timer.target = initial[1];
                        } else if (parser.parse) {
                            timer.source = parser.parse(startValue);
                            timer.target = parser.parse(newValue);
                        } else {
                            timer.source = startValue;
                            timer.target = newValue;
                        }
                        // The animation started. Change to originalVal.
                        targets[name] = newValue;
                        delete changes[name];
                        ignite = true;
                        
                        continue;
                    } else {
                        delete targets[name];
                    }
                } else {
                    delete targets[name];
                }
                // If the property is not animating.
                delete timers[name];
            }
        }
        if (ignite && !attr.animating) {
            me.setAnimating(attr, true);
        }
        return changes;
    },
    /**
     * @private
     *
     * Update attributes to current value according to current animation time.
     * This method will not affect the values of lower layers, but may delete a
     * value from it.
     * @param {Object} attr The source attributes.
     * @return {Object} The changes to pop up or null.
     */
    updateAttributes: function(attr) {
        if (!attr.animating) {
            return {};
        }
        // eslint-disable-next-line vars-on-top
        var changes = {},
            any = false,
            timers = attr.timers,
            targets = attr.targets,
            now = Ext.draw.Animator.animationTime(),
            name, timer, delta;
        // If updated in the same frame, return.
        if (attr.lastUpdate === now) {
            return null;
        }
        for (name in timers) {
            timer = timers[name];
            if (!timer.start) {
                timer.start = now;
                delta = 0;
            } else {
                delta = (now - timer.start) / timer.duration;
            }
            if (delta >= 1) {
                changes[name] = targets[name];
                delete targets[name];
                if (timers[name].remove) {
                    changes.removeFromInstance = changes.removeFromInstance || {};
                    changes.removeFromInstance[name] = true;
                }
                delete timers[name];
            } else {
                changes[name] = timer.serve(timer.compute(timer.source, timer.target, timer.easing(delta), attr[name]));
                any = true;
            }
        }
        attr.lastUpdate = now;
        this.setAnimating(attr, any);
        return changes;
    },
    pushDown: function(attr, changes) {
        changes = this.callParent([
            attr.targets,
            changes
        ]);
        return this.setAttrs(attr, changes);
    },
    popUp: function(attr, changes) {
        attr = attr.prototype;
        changes = this.setAttrs(attr, changes);
        if (this._upper) {
            return this._upper.popUp(attr, changes);
        } else {
            return Ext.apply(attr, changes);
        }
    },
    /**
     * @private
     * This is called as an animated object in `Ext.draw.Animator`.
     */
    step: function(frameTime) {
        var me = this,
            pool = me.animatingPool.slice(),
            ln = pool.length,
            i = 0,
            attr, changes;
        for (; i < ln; i++) {
            attr = pool[i];
            changes = me.updateAttributes(attr);
            if (changes && me._upper) {
                me._upper.popUp(attr, changes);
            }
        }
    },
    /**
     * Stop all animations affected by this modifier.
     */
    stop: function() {
        var me = this,
            pool = me.animatingPool,
            i, ln;
        this.step();
        for (i = 0 , ln = pool.length; i < ln; i++) {
            pool[i].animating = false;
        }
        me.animatingPool.length = 0;
        me.animating = 0;
        Ext.draw.Animator.remove(me);
    },
    destroy: function() {
        Ext.draw.Animator.remove(this);
        this.callParent();
    }
});

/**
 * @class Ext.draw.modifier.Highlight
 * @extends Ext.draw.modifier.Modifier
 *
 * Highlight is a modifier that will override sprite attributes
 * with {@link Ext.draw.modifier.Highlight#style style} attributes
 * when sprite's `highlighted` attribute is true.
 */
Ext.define('Ext.draw.modifier.Highlight', {
    extend: 'Ext.draw.modifier.Modifier',
    alias: 'modifier.highlight',
    config: {
        /**
         * @cfg {Boolean} enabled 'true' if the highlight is applied.
         */
        enabled: false,
        /**
         * @cfg {Object} style The style attributes of the highlight modifier.
         */
        style: null
    },
    preFx: true,
    applyStyle: function(style, oldStyle) {
        oldStyle = oldStyle || {};
        if (this.getSprite()) {
            Ext.apply(oldStyle, this.getSprite().self.def.normalize(style));
        } else {
            Ext.apply(oldStyle, style);
        }
        return oldStyle;
    },
    prepareAttributes: function(attr) {
        if (!attr.hasOwnProperty('highlightOriginal')) {
            attr.highlighted = false;
            attr.highlightOriginal = Ext.Object.chain(attr);
            attr.highlightOriginal.prototype = attr;
            // A list of attributes that should be removed from a sprite instance
            // when it is unhighlighted.
            attr.highlightOriginal.removeFromInstance = {};
        }
        if (this._lower) {
            this._lower.prepareAttributes(attr.highlightOriginal);
        }
    },
    updateSprite: function(sprite, oldSprite) {
        var me = this,
            style = me.getStyle(),
            attributeDefinitions;
        if (sprite) {
            attributeDefinitions = sprite.self.def;
            if (style) {
                me._style = attributeDefinitions.normalize(style);
            }
            me.setStyle(sprite.config.highlight);
            // Add highlight related attributes to sprite's attribute definition.
            // This will affect all sprites of the same type, even those without
            // the highlight modifier.
            attributeDefinitions.setConfig({
                defaults: {
                    highlighted: false
                },
                processors: {
                    highlighted: 'bool'
                }
            });
        }
        this.setSprite(sprite);
    },
    /**
     * @private
     * Filter out modifier changes that override highlight style or source attributes.
     * @param {Object} attr The source attributes.
     * @param {Object} changes The modifier changes.
     * @return {*} The filtered changes.
     */
    filterChanges: function(attr, changes) {
        var me = this,
            highlightOriginal = attr.highlightOriginal,
            style = me.getStyle(),
            name;
        if (attr.highlighted) {
            for (name in changes) {
                if (style.hasOwnProperty(name)) {
                    // If sprite is highlighted, then stash the changes
                    // to the `style` attributes made by lower level modifiers
                    // to apply them later when sprite is unhighlighted.
                    highlightOriginal[name] = changes[name];
                    delete changes[name];
                }
            }
        }
        return changes;
    },
    pushDown: function(attr, changes) {
        var style = this.getStyle(),
            highlightOriginal = attr.highlightOriginal,
            removeFromInstance = highlightOriginal.removeFromInstance,
            highlighted, name, tplAttr, timer;
        if (changes.hasOwnProperty('highlighted')) {
            highlighted = changes.highlighted;
            // Hide `highlighted` and `style` from underlying modifiers.
            delete changes.highlighted;
            if (this._lower) {
                changes = this._lower.pushDown(highlightOriginal, changes);
            }
            changes = this.filterChanges(attr, changes);
            if (highlighted !== attr.highlighted) {
                if (highlighted) {
                    // Switching ON.
                    // At this time, original should be empty.
                    for (name in style) {
                        // Remember the values of attributes to revert back to them on unhighlight.
                        if (name in changes) {
                            // Remember value set by lower level modifiers.
                            highlightOriginal[name] = changes[name];
                        } else {
                            // Remember the original value.
                            // If this is a sprite instance and it doesn't have its own
                            // 'name' attribute, (i.e. inherits template's attribute value)
                            // than we have to get the value for the 'name' attribute from
                            // the template's 'targets' object instead of its
                            // 'attr' object (which is the prototype of the instance),
                            // because the 'name' attribute of the template may be animating.
                            // Check out the prepareAttributes method of the Animation
                            // modifier for more details on the 'targets' object.
                            tplAttr = attr.template && attr.template.ownAttr;
                            if (tplAttr && !attr.prototype.hasOwnProperty(name)) {
                                removeFromInstance[name] = true;
                                highlightOriginal[name] = tplAttr.targets[name];
                            } else {
                                // Even if a sprite instance has its own property, it may
                                // still have to be removed from the instance after
                                // unhighlighting is done.
                                // Consider a situation where an instance doesn't originally
                                // have its own attribute (that is used for highlighting and
                                // unhighlighting). It will however have that attribute as
                                // its own when the highlight/unhighlight animation is in
                                // progress, until the attribute is removed from the instance
                                // when the unhighlighting is done.
                                // So in a scenario where the instance is highlighted, then
                                // unhighlighted (i.e. starts animating back to its original
                                // value) and then highlighted again before the unhighlight
                                // animation is done, we should still mark the attribute
                                // for removal from the instance, if it was our original
                                // intention. To tell if it was, we can check the timer
                                // for the attribute and see if the 'remove' flag is set.
                                timer = highlightOriginal.timers[name];
                                if (timer && timer.remove) {
                                    removeFromInstance[name] = true;
                                }
                                highlightOriginal[name] = attr[name];
                            }
                        }
                        if (highlightOriginal[name] !== style[name]) {
                            changes[name] = style[name];
                        }
                    }
                } else {
                    // Switching OFF.
                    for (name in style) {
                        if (!(name in changes)) {
                            changes[name] = highlightOriginal[name];
                        }
                        delete highlightOriginal[name];
                    }
                    changes.removeFromInstance = changes.removeFromInstance || {};
                    // Let the higher lever animation modifier know which attributes
                    // should be removed from instance when the animation is done.
                    Ext.apply(changes.removeFromInstance, removeFromInstance);
                    highlightOriginal.removeFromInstance = {};
                }
                changes.highlighted = highlighted;
            }
        } else {
            if (this._lower) {
                changes = this._lower.pushDown(highlightOriginal, changes);
            }
            changes = this.filterChanges(attr, changes);
        }
        return changes;
    },
    popUp: function(attr, changes) {
        changes = this.filterChanges(attr, changes);
        this.callParent([
            attr,
            changes
        ]);
    }
});

/**
 * A sprite is a basic primitive from the charts package which represents a graphical 
 * object that can be drawn. Sprites are used extensively in the charts package to 
 * create the visual elements of each chart.  You can also create a desired image by 
 * adding one or more sprites to a {@link Ext.draw.Container draw container}.
 * 
 * The Sprite class itself is an abstract class and is not meant to be used directly.  
 * There are many different kinds of sprites available in the charts package that extend 
 * Ext.draw.sprite.Sprite. Each sprite type has various attributes that define how that 
 * sprite should look. For example, this is a {@link Ext.draw.sprite.Rect rect} sprite:
 * 
 *     @example
 *     Ext.create({
 *         xtype: 'draw', 
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400,
 *         sprites: [{
 *             type: 'rect',
 *             x: 50,
 *             y: 50,
 *             width: 100,
 *             height: 100,
 *             fillStyle: '#1F6D91'
 *         }]
 *     });
 * 
 * By default, sprites are added to the default 'main' {@link Ext.draw.Surface surface} 
 * of the draw container.  However, sprites may also be configured with a reference to a 
 * specific Ext.draw.Surface when set in the draw container's 
 * {@link Ext.draw.Container#cfg-sprites sprites} config.  Specifying a surface 
 * other than 'main' will create a surface by that name if it does not already exist.
 * 
 *     @example
 *     Ext.create({
 *         xtype: 'draw', 
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400,
 *         sprites: [{
 *             type: 'rect',
 *             surface: 'anim',  // a surface with id "anim" will be created automatically
 *             x: 50,
 *             y: 50,
 *             width: 100,
 *             height: 100,
 *             fillStyle: '#1F6D91'
 *         }]
 *     });
 * 
 * The ability to have multiple surfaces is useful for performance (and battery life) 
 * reasons. Because changes to sprite attributes cause the whole surface (and all 
 * sprites in it) to re-render, it makes sense to group sprites by surface, so changes 
 * to one group of sprites will only trigger the surface they are in to re-render.
 * 
 * You can add a sprite to an existing drawing by adding the sprite to a draw surface.  
 * 
 *     @example
 *     var drawCt = Ext.create({
 *         xtype: 'draw',
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400
 *     });
 *     
 *     // If the surface name is not specified then 'main' will be used
 *     var surface = drawCt.getSurface();
 *     
 *     surface.add({
 *         type: 'rect',
 *         x: 50,
 *         y: 50,
 *         width: 100,
 *         height: 100,
 *         fillStyle: '#1F6D91'
 *     });
 *     
 *     surface.renderFrame();
 * 
 * **Note:** Changes to the sprites on a surface will be not be reflected in the DOM 
 * until you call the surface's {@link Ext.draw.Surface#method-renderFrame renderFrame} 
 * method.  This must be done after adding, removing, or modifying sprites in order to 
 * see the changes on-screen.
 * 
 * For information on configuring a sprite with an initial transformation see 
 * {@link #scaling}, {@link #rotation}, and {@link #translation}.
 * 
 * For information on applying a transformation to an existing sprite see the 
 * Ext.draw.Matrix class.
 */
Ext.define('Ext.draw.sprite.Sprite', {
    alias: 'sprite.sprite',
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    requires: [
        'Ext.draw.Draw',
        'Ext.draw.gradient.Gradient',
        'Ext.draw.sprite.AttributeDefinition',
        'Ext.draw.modifier.Target',
        'Ext.draw.modifier.Animation',
        'Ext.draw.modifier.Highlight'
    ],
    isSprite: true,
    $configStrict: false,
    statics: {
        //<debug>
        /* eslint-disable max-len */
        /**
         * Debug rendering options:
         *
         * debug: {
         *     bbox: true, // renders the bounding box of the sprite
         *     xray: true  // renders control points of the path (for Ext.draw.sprite.Path and descendants only)
         * }
         *
         */
        debug: false,
        /* eslint-enable max-len */
        //</debug>
        defaultHitTestOptions: {
            fill: true,
            stroke: true
        }
    },
    inheritableStatics: {
        def: {
            processors: {
                //<debug>
                debug: 'default',
                //</debug>
                /**
                 * @cfg {String} [strokeStyle="none"] The color of the stroke (a CSS color value).
                 */
                strokeStyle: "color",
                /**
                 * @cfg {String} [fillStyle="none"] The color of the shape (a CSS color value).
                 */
                fillStyle: "color",
                /**
                 * @cfg {Number} [strokeOpacity=1] The opacity of the stroke. Limited from 0 to 1.
                 */
                strokeOpacity: "limited01",
                /**
                 * @cfg {Number} [fillOpacity=1] The opacity of the fill. Limited from 0 to 1.
                 */
                fillOpacity: "limited01",
                /**
                 * @cfg {Number} [lineWidth=1] The width of the line stroke.
                 */
                lineWidth: "number",
                /**
                 * @cfg {String} [lineCap="butt"] The style of the line caps.
                 */
                lineCap: "enums(butt,round,square)",
                /**
                 * @cfg {String} [lineJoin="miter"] The style of the line join.
                 */
                lineJoin: "enums(round,bevel,miter)",
                /**
                 * @cfg {Array} [lineDash=[]]
                 * An even number of non-negative numbers specifying a dash/space sequence.
                 * Note that while this is supported in IE8 (VML engine), the behavior is
                 * different from Canvas and SVG. Please refer to this document for details:
                 * http://msdn.microsoft.com/en-us/library/bb264085(v=vs.85).aspx
                 * Although IE9 and IE10 have Canvas support, the 'lineDash'
                 * attribute is not supported in those browsers.
                 */
                lineDash: "data",
                /**
                 * @cfg {Number} [lineDashOffset=0]
                 * A number specifying how far into the line dash sequence drawing commences.
                 */
                lineDashOffset: "number",
                /**
                 * @cfg {Number} [miterLimit=10]
                 * Sets the distance between the inner corner and the outer corner
                 * where two lines meet.
                 */
                miterLimit: "number",
                /**
                 * @cfg {String} [shadowColor="none"] The color of the shadow (a CSS color value).
                 */
                shadowColor: "color",
                /**
                 * @cfg {Number} [shadowOffsetX=0] The offset of the sprite's shadow on the x-axis.
                 */
                shadowOffsetX: "number",
                /**
                 * @cfg {Number} [shadowOffsetY=0] The offset of the sprite's shadow on the y-axis.
                 */
                shadowOffsetY: "number",
                /**
                 * @cfg {Number} [shadowBlur=0] The amount blur used on the shadow.
                 */
                shadowBlur: "number",
                /**
                 * @cfg {Number} [globalAlpha=1] The opacity of the sprite. Limited from 0 to 1.
                 */
                globalAlpha: "limited01",
                /**
                 * @cfg {String} [globalCompositeOperation=source-over]
                 * Indicates how source images are drawn onto a destination image.
                 * globalCompositeOperation attribute is not supported by the SVG and VML
                 * (excanvas) engines.
                 */
                // eslint-disable-next-line max-len
                globalCompositeOperation: "enums(source-over,destination-over,source-in,destination-in,source-out,destination-out,source-atop,destination-atop,lighter,xor,copy)",
                /**
                 * @cfg {Boolean} [hidden=false] Determines whether or not the sprite is hidden.
                 */
                hidden: "bool",
                /**
                 * @cfg {Boolean} [transformFillStroke=false]
                 * Determines whether the fill and stroke are affected by sprite transformations.
                 */
                transformFillStroke: "bool",
                /**
                 * @cfg {Number} [zIndex=0]
                 * The stacking order of the sprite.
                 */
                zIndex: "number",
                /**
                 * @cfg {Number} [translationX=0]
                 * The translation, position offset, of the sprite on the x-axis.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #translation} and {@link #translationY}
                 */
                translationX: "number",
                /**
                 * @cfg {Number} [translationY=0]
                 * The translation, position offset, of the sprite on the y-axis.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #translation} and {@link #translationX}
                 */
                translationY: "number",
                /**
                 * @cfg {Number} [rotationRads=0]
                 * The angle of rotation of the sprite in radians.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #rotation}, {@link #rotationCenterX}, and 
                 * {@link #rotationCenterY}
                 */
                rotationRads: "number",
                /**
                 * @cfg {Number} [rotationCenterX=null]
                 * The central coordinate of the sprite's scale operation on the x-axis.  
                 * Unless explicitly set, will default to the calculated center of the 
                 * sprite along the x-axis.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #rotation}, {@link #rotationRads}, and 
                 * {@link #rotationCenterY}
                 */
                rotationCenterX: "number",
                /**
                 * @cfg {Number} [rotationCenterY=null]
                 * The central coordinate of the sprite's rotate operation on the y-axis.
                 * Unless explicitly set, will default to the calculated center of the 
                 * sprite along the y-axis.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #rotation}, {@link #rotationRads}, and 
                 * {@link #rotationCenterX}
                 */
                rotationCenterY: "number",
                /**
                 * @cfg {Number} [scalingX=1] The scaling of the sprite on the x-axis.
                 * The number value represents a percentage by which to scale the 
                 * sprite.  **1** is equal to 100%, **2** would be 200%, etc.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #scaling}, {@link #scalingY}, 
                 * {@link #scalingCenterX}, and {@link #scalingCenterY}
                 */
                scalingX: "number",
                /**
                 * @cfg {Number} [scalingY=1] The scaling of the sprite on the y-axis.  
                 * The number value represents a percentage by which to scale the 
                 * sprite.  **1** is equal to 100%, **2** would be 200%, etc.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #scaling}, {@link #scalingX}, 
                 * {@link #scalingCenterX}, and {@link #scalingCenterY}
                 */
                scalingY: "number",
                /**
                 * @cfg {Number} [scalingCenterX=null]
                 * The central coordinate of the sprite's scale operation on the x-axis.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #scaling}, {@link #scalingX}, 
                 * {@link #scalingY}, and {@link #scalingCenterY}
                 */
                scalingCenterX: "number",
                /**
                 * @cfg {Number} [scalingCenterY=null]
                 * The central coordinate of the sprite's scale operation on the y-axis.
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * See also: {@link #scaling}, {@link #scalingX}, 
                 * {@link #scalingY}, and {@link #scalingCenterX}
                 */
                scalingCenterY: "number",
                constrainGradients: "bool"
            },
            /**
                 * @cfg {Number/Object} rotation
                 * Applies an initial angle of rotation to the sprite.  May be a number 
                 * specifying the rotation in degrees.  Or may be a config object using 
                 * the below config options.
                 * 
                 * **Note:** Rotation config options will be overridden by values set on 
                 * the {@link #rotationRads}, {@link #rotationCenterX}, and 
                 * {@link #rotationCenterY} configs.  
                 * 
                 *     Ext.create({
                 *         xtype: 'draw',
                 *         renderTo: Ext.getBody(),
                 *         width: 600,
                 *         height: 400,
                 *         sprites: [{
                 *             type: 'rect',
                 *             x: 50,
                 *             y: 50,
                 *             width: 100,
                 *             height: 100,
                 *             fillStyle: '#1F6D91',
                 *             //rotation: 45
                 *             rotation: {
                 *                 degrees: 45,
                 *                 //rads: Math.PI / 4,
                 *                 //centerX: 50,
                 *                 //centerY: 50
                 *             }
                 *         }]
                 *     });
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * @cfg {Number} rotation.rads
                 * The angle in radians to rotate the sprite
                 * 
                 * @cfg {Number} rotation.degrees
                 * The angle in degrees to rotate the sprite (is ignored if rads or 
                 * {@link #rotationRads} is set
                 * 
                 * @cfg {Number} rotation.centerX
                 * The central coordinate of the sprite's rotation on the x-axis.  
                 * Unless explicitly set, will default to the calculated center of the 
                 * sprite along the x-axis.
                 * 
                 * @cfg {Number} rotation.centerY
                 * The central coordinate of the sprite's rotation on the y-axis.  
                 * Unless explicitly set, will default to the calculated center of the 
                 * sprite along the y-axis.
                 */
            /**
                 * @cfg {Number/Object} scaling
                 * Applies initial scaling to the sprite.  May be a number specifying 
                 * the amount to scale both the x and y-axis.  The number value 
                 * represents a percentage by which to scale the sprite.  **1** is equal 
                 * to 100%, **2** would be 200%, etc.  Or may be a config object using 
                 * the below config options.
                 * 
                 * **Note:** Scaling config options will be overridden by values set on 
                 * the {@link #scalingX}, {@link #scalingY}, {@link #scalingCenterX}, 
                 * and {@link #scalingCenterY} configs.
                 * 
                 *     Ext.create({
                 *         xtype: 'draw',
                 *         renderTo: Ext.getBody(),
                 *         width: 600,
                 *         height: 400,
                 *         sprites: [{
                 *             type: 'rect',
                 *             x: 50,
                 *             y: 50,
                 *             width: 100,
                 *             height: 100,
                 *             fillStyle: '#1F6D91',
                 *             //scaling: 2,
                 *             scaling: {
                 *                 x: 2,
	             *                 y: 2
                 *                 //centerX: 100,
                 *                 //centerY: 100
                 *             }
                 *         }]
                 *     });
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * @cfg {Number} scaling.x
                 * The amount by which to scale the sprite along the x-axis.  The number 
                 * value represents a percentage by which to scale the sprite.  **1** is 
                 * equal to 100%, **2** would be 200%, etc.
                 * 
                 * @cfg {Number} scaling.y
                 * The amount by which to scale the sprite along the y-axis.  The number 
                 * value represents a percentage by which to scale the sprite.  **1** is 
                 * equal to 100%, **2** would be 200%, etc.
                 * 
                 * @cfg scaling.centerX
                 * The central coordinate of the sprite's scaling on the x-axis.  Unless 
                 * explicitly set, will default to the calculated center of the sprite 
                 * along the x-axis.
                 * 
                 * @cfg {Number} scaling.centerY
                 * The central coordinate of the sprite's scaling on the y-axis.  Unless 
                 * explicitly set, will default to the calculated center of the sprite 
                 * along the y-axis.
                 */
            /**
                 * @cfg {Object} translation
                 * Applies an initial translation, adjustment in x/y positioning, to the 
                 * sprite.
                 * 
                 * **Note:** Translation config options will be overridden by values set 
                 * on the {@link #translationX} and {@link #translationY} configs.
                 * 
                 *     Ext.create({
                 *         xtype: 'draw',
                 *         renderTo: Ext.getBody(),
                 *         width: 600,
                 *         height: 400,
                 *             sprites: [{
                 *             type: 'rect',
                 *             x: 50,
                 *             y: 50,
                 *             width: 100,
                 *             height: 100,
                 *             fillStyle: '#1F6D91',
                 *             translation: {
                 *                 x: 50,
                 *                 y: 50
                 *             }
                 *         }]
                 *     });
                 * 
                 * **Note:** Transform configs are *always* performed in the following 
                 * order:
                 * 
                 *  1. Scaling
                 *  2. Rotation
                 *  3. Translation
                 * 
                 * @cfg {Number} translation.x
                 * The amount to translate the sprite along the x-axis.
                 * 
                 * @cfg {Number} translation.y
                 * The amount to translate the sprite along the y-axis.
                 */
            aliases: {
                "stroke": "strokeStyle",
                "fill": "fillStyle",
                "color": "fillStyle",
                "stroke-width": "lineWidth",
                "stroke-linecap": "lineCap",
                "stroke-linejoin": "lineJoin",
                "stroke-miterlimit": "miterLimit",
                "text-anchor": "textAlign",
                "opacity": "globalAlpha",
                translateX: "translationX",
                translateY: "translationY",
                rotateRads: "rotationRads",
                rotateCenterX: "rotationCenterX",
                rotateCenterY: "rotationCenterY",
                scaleX: "scalingX",
                scaleY: "scalingY",
                scaleCenterX: "scalingCenterX",
                scaleCenterY: "scalingCenterY"
            },
            defaults: {
                hidden: false,
                zIndex: 0,
                strokeStyle: "none",
                fillStyle: "none",
                lineWidth: 1,
                lineDash: [],
                lineDashOffset: 0,
                lineCap: "butt",
                lineJoin: "miter",
                miterLimit: 10,
                shadowColor: "none",
                shadowOffsetX: 0,
                shadowOffsetY: 0,
                shadowBlur: 0,
                globalAlpha: 1,
                strokeOpacity: 1,
                fillOpacity: 1,
                transformFillStroke: false,
                translationX: 0,
                translationY: 0,
                rotationRads: 0,
                rotationCenterX: null,
                rotationCenterY: null,
                scalingX: 1,
                scalingY: 1,
                scalingCenterX: null,
                scalingCenterY: null,
                constrainGradients: false
            },
            triggers: {
                zIndex: "zIndex",
                globalAlpha: "canvas",
                globalCompositeOperation: "canvas",
                transformFillStroke: "canvas",
                strokeStyle: "canvas",
                fillStyle: "canvas",
                strokeOpacity: "canvas",
                fillOpacity: "canvas",
                lineWidth: "canvas",
                lineCap: "canvas",
                lineJoin: "canvas",
                lineDash: "canvas",
                lineDashOffset: "canvas",
                miterLimit: "canvas",
                shadowColor: "canvas",
                shadowOffsetX: "canvas",
                shadowOffsetY: "canvas",
                shadowBlur: "canvas",
                translationX: "transform",
                translationY: "transform",
                rotationRads: "transform",
                rotationCenterX: "transform",
                rotationCenterY: "transform",
                scalingX: "transform",
                scalingY: "transform",
                scalingCenterX: "transform",
                scalingCenterY: "transform",
                constrainGradients: "canvas"
            },
            updaters: {
                // 'bbox' updater is meant to be called by subclasses when changes
                // to attributes are expected to result in a change in sprite's dimensions.
                bbox: 'bboxUpdater',
                zIndex: function(attr) {
                    attr.dirtyZIndex = true;
                },
                transform: function(attr) {
                    attr.dirtyTransform = true;
                    attr.bbox.transform.dirty = true;
                }
            }
        }
    },
    /**
     * @property {Object} attr
     * The visual attributes of the sprite, e.g. strokeStyle, fillStyle, lineWidth...
     */
    /**
     * @cfg {Ext.draw.modifier.Animation} animation
     * @accessor
     */
    config: {
        /**
         * @private
         * @cfg {Ext.draw.Surface/Ext.draw.sprite.Instancing/Ext.draw.sprite.Composite} parent
         * The immediate parent of the sprite. Not necessarily a surface.
         */
        parent: null,
        /**
         * @private
         * @cfg {Ext.draw.Surface} surface
         * The surface that this sprite is rendered into.
         * This config is not meant to be used directly.
         * Please use the {@link Ext.draw.Surface#add} method instead.
         */
        surface: null
    },
    onClassExtended: function(subClass, data) {
        // The `def` here is no longer a config, but an instance
        // of the AttributeDefinition class created with that config,
        // which can now be retrieved from `initialConfig`.
        var superclassCfg = subClass.superclass.self.def.initialConfig,
            ownCfg = data.inheritableStatics && data.inheritableStatics.def,
            cfg;
        // If sprite defines attributes of its own, merge that with those of its parent.
        if (ownCfg) {
            cfg = Ext.Object.merge({}, superclassCfg, ownCfg);
            subClass.def = new Ext.draw.sprite.AttributeDefinition(cfg);
            delete data.inheritableStatics.def;
        } else {
            subClass.def = new Ext.draw.sprite.AttributeDefinition(superclassCfg);
        }
        subClass.def.spriteClass = subClass;
    },
    constructor: function(config) {
        //<debug>
        if (Ext.getClassName(this) === 'Ext.draw.sprite.Sprite') {
            throw 'Ext.draw.sprite.Sprite is an abstract class';
        }
        //</debug>
        // eslint-disable-next-line vars-on-top
        var me = this,
            attributeDefinition = me.self.def,
            // It is important to get defaults (make sure
            // 'defaults' config applier of the AttributeDefinition is called,
            // since it is initialized lazily) before the attributes
            // are initialized ('initializeAttributes' call).
            defaults = attributeDefinition.getDefaults(),
            processors = attributeDefinition.getProcessors(),
            modifiers, name;
        config = Ext.isObject(config) ? config : {};
        me.id = config.id || Ext.id(null, 'ext-sprite-');
        me.attr = {};
        // Observable's constructor also calls the initConfig for us.
        me.mixins.observable.constructor.apply(me, arguments);
        modifiers = Ext.Array.from(config.modifiers, true);
        me.createModifiers(modifiers);
        me.initializeAttributes();
        me.setAttributes(defaults, true);
        //<debug>
        for (name in config) {
            if (name in processors && me['get' + name.charAt(0).toUpperCase() + name.substr(1)]) {
                Ext.raise('The ' + me.$className + ' sprite has both a config and an attribute with the same name: ' + name + '.');
            }
        }
        //</debug>
        me.setAttributes(config);
    },
    updateSurface: function(surface, oldSurface) {
        if (oldSurface) {
            oldSurface.remove(this);
        }
    },
    /**
     * @private
     * Current state of the sprite.
     * Set to `true` if the sprite needs to be repainted.
     * @cfg {Boolean} dirty
     * @accessor
     */
    getDirty: function() {
        return this.attr.dirty;
    },
    setDirty: function(dirty) {
        var parent;
        // This could have been a regular attribute.
        // Instead, it's a hidden one, which is initialized inside in the
        // Target's modifier `prepareAttributes` method and is exposed
        // as a config. The idea is to skip the modifier chain when
        // we simply need to change the sprite's state and notify
        // the sprite's parent.
        this.attr.dirty = dirty;
        if (dirty) {
            parent = this.getParent();
            if (parent) {
                parent.setDirty(true);
            }
        }
    },
    addModifier: function(modifier, reinitializeAttributes) {
        var me = this,
            mods = me.modifiers,
            animation = mods.animation,
            target = mods.target,
            type;
        if (!(modifier instanceof Ext.draw.modifier.Modifier)) {
            type = typeof modifier === 'string' ? modifier : modifier.type;
            if (type && !mods[type]) {
                mods[type] = modifier = Ext.factory(modifier, null, null, 'modifier');
            }
        }
        modifier.setSprite(me);
        if (modifier.preFx || modifier.config && modifier.config.preFx) {
            if (animation._lower) {
                animation._lower.setUpper(modifier);
            }
            modifier.setUpper(animation);
        } else {
            target._lower.setUpper(modifier);
            modifier.setUpper(target);
        }
        if (reinitializeAttributes) {
            me.initializeAttributes();
        }
        return modifier;
    },
    createModifiers: function(modifiers) {
        var me = this,
            Modifier = Ext.draw.modifier,
            animation = me.getInitialConfig().animation,
            mods, i, ln;
        // Create default modifiers.
        me.modifiers = mods = {
            target: new Modifier.Target({
                sprite: me
            }),
            animation: new Modifier.Animation(Ext.apply({
                sprite: me
            }, animation))
        };
        // Link modifiers.
        mods.animation.setUpper(mods.target);
        for (i = 0 , ln = modifiers.length; i < ln; i++) {
            me.addModifier(modifiers[i], false);
        }
        return mods;
    },
    /**
     * Returns the current animation instance.
     * return {Ext.draw.modifier.Animation} The animation modifier used to animate the 
     * sprite
     */
    getAnimation: function() {
        return this.modifiers.animation;
    },
    /**
     * Sets the animation config used by the sprite when animating the sprite's 
     * attributes and transformation properties.
     * 
     *     var drawCt = Ext.create({
     *         xtype: 'draw',
     *         renderTo: document.body,
     *         width: 400,
     *         height: 400,
     *         sprites: [{
     *             type: 'rect',
     *             x: 50,
     *             y: 50,
     *             width: 100,
     *             height: 100,
     *             fillStyle: '#1F6D91'
     *         }]
     *     });
     *     
     *     var rect = drawCt.getSurface().getItems()[0];
     *     
     *     rect.setAnimation({
     *         duration: 1000,
     *         easing: 'elasticOut'
     *     });
     *     
     *     Ext.defer(function () {
     *         rect.setAttributes({
     *             width: 250
     *         });
     *     }, 500);
     * 
     * @param {Object} config The Ext.draw.modifier.Animation config for this sprite's 
     * animations.
     */
    setAnimation: function(config) {
        if (!this.isConfiguring) {
            this.modifiers.animation.setConfig(config || {
                duration: 0
            });
        }
    },
    initializeAttributes: function() {
        this.modifiers.target.prepareAttributes(this.attr);
    },
    /**
     * @private
     * Calls updaters triggered by changes to sprite attributes.
     * @param attr The attributes of a sprite or its instance.
     */
    callUpdaters: function(attr) {
        var me = this,
            updaters = me.self.def.getUpdaters(),
            any = false,
            dirty = false,
            pendingUpdaters, flags, updater, fn;
        attr = attr || this.attr;
        pendingUpdaters = attr.pendingUpdaters;
        // If updaters set sprite attributes that trigger other updaters,
        // those updaters are not called right away, but wait until all current
        // updaters are called (till the next do/while loop iteration).
        me.callUpdaters = Ext.emptyFn;
        // Hide class method from the instance.
        do {
            any = false;
            for (updater in pendingUpdaters) {
                any = true;
                flags = pendingUpdaters[updater];
                delete pendingUpdaters[updater];
                fn = updaters[updater];
                if (typeof fn === 'string') {
                    fn = me[fn];
                }
                if (fn) {
                    fn.call(me, attr, flags);
                }
            }
            dirty = dirty || any;
        } while (any);
        delete me.callUpdaters;
        // Restore class method.
        if (dirty) {
            me.setDirty(true);
        }
    },
    /**
     * @private
     */
    callUpdater: function(attr, updater, triggers) {
        this.scheduleUpdater(attr, updater, triggers);
        this.callUpdaters(attr);
    },
    /**
     * @private
     * Schedules specified updaters to be called.
     * Updaters are called implicitly as a result of a change to sprite attributes.
     * But sometimes it may be required to call an updater without setting an attribute,
     * and without messing up the updater call order (by calling the updater immediately).
     * For example:
     *
     *     updaters: {
     *          onDataX: function (attr) {
     *              this.processDataX();
     *              // Process data Y every time data X is processed.
     *              // Call the onDataY updater as if changes to dataY attribute itself
     *              // triggered the update.
     *              this.scheduleUpdaters(attr, {onDataY: ['dataY']});
     *              // Alternatively:
     *              // this.scheduleUpdaters(attr, ['onDataY'], ['dataY']);
     *          }
     *     }
     *
     * @param {Object} attr The attributes object (not necesseraly of a sprite,
     * but of its instance).
     * @param {Object/String[]} updaters A map of updaters to be called to attributes
     * that triggered the update.
     * @param {String[]} [triggers] Attributes that triggered the update. An optional parameter.
     * If used, the `updaters` parameter will be treated as an array of updaters to be called.
     */
    scheduleUpdaters: function(attr, updaters, triggers) {
        var updater, i, ln;
        attr = attr || this.attr;
        if (triggers) {
            for (i = 0 , ln = updaters.length; i < ln; i++) {
                updater = updaters[i];
                this.scheduleUpdater(attr, updater, triggers);
            }
        } else {
            for (updater in updaters) {
                triggers = updaters[updater];
                this.scheduleUpdater(attr, updater, triggers);
            }
        }
    },
    /**
     * @private
     * @param attr {Object} The attributes object (not necesseraly of a sprite,
     * but of its instance).
     * @param updater {String} Updater to be called.
     * @param {String[]} [triggers] Attributes that triggered the update.
     */
    scheduleUpdater: function(attr, updater, triggers) {
        var pendingUpdaters;
        triggers = triggers || [];
        attr = attr || this.attr;
        pendingUpdaters = attr.pendingUpdaters;
        if (updater in pendingUpdaters) {
            if (triggers.length) {
                pendingUpdaters[updater] = Ext.Array.merge(pendingUpdaters[updater], triggers);
            }
        } else {
            pendingUpdaters[updater] = triggers;
        }
    },
    /**
     * Set attributes of the sprite.
     * By default only the attributes that have processors will be set
     * and all other attributes will be filtered out as a result of the
     * normalization process.
     * The normalization process can be skipped. In that case all the given
     * attributes will be set unprocessed. This will result in better
     * performance, but might also pollute the sprite's attributes with
     * unwanted attributes or attributes with invalid values, if one is not
     * careful. See also {@link #setAttributesBypassingNormalization}.
     * If normalization is skipped, one may also chose to avoid copying
     * the given object. This may result in even better performance, but
     * only in cases where most of the attributes have values that are
     * different from the old values, because copying additionally checks
     * if the value has changed.
     *
     * @param {Object} changes The content of the change.
     * @param {Boolean} [bypassNormalization] `true` to avoid normalization of the given changes.
     * @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
     * `bypassNormalization` should also be `true`. The content of object may be destroyed.
     */
    setAttributes: function(changes, bypassNormalization, avoidCopy) {
        var me = this,
            changesToPush;
        //<debug>
        if (me.destroyed) {
            Ext.Error.raise("Setting attributes of a destroyed sprite.");
        }
        //</debug>
        if (bypassNormalization) {
            if (avoidCopy) {
                changesToPush = changes;
            } else {
                changesToPush = Ext.apply({}, changes);
            }
        } else {
            changesToPush = me.self.def.normalize(changes);
        }
        me.modifiers.target.pushDown(me.attr, changesToPush);
    },
    /**
     * Set attributes of the sprite, assuming the names and values have already been
     * normalized.
     *
     * @deprecated 6.5.0 Use setAttributes directly with bypassNormalization argument being `true`.
     * @param {Object} changes The content of the change.
     * @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
     * The content of object may be destroyed.
     */
    setAttributesBypassingNormalization: function(changes, avoidCopy) {
        return this.setAttributes(changes, true, avoidCopy);
    },
    /**
     * @private
     */
    bboxUpdater: function(attr) {
        var hasRotation = attr.rotationRads !== 0,
            hasScaling = attr.scalingX !== 1 || attr.scalingY !== 1,
            noRotationCenter = attr.rotationCenterX === null || attr.rotationCenterY === null,
            noScalingCenter = attr.scalingCenterX === null || attr.scalingCenterY === null;
        // 'bbox' is not a standard attribute (in the sense that it doesn't have
        // a processor = not explicitly declared and cannot be set by a user)
        // and is calculated automatically by the 'getBBox' method.
        // The 'bbox' attribute is created by the 'prepareAttributes' method
        // of the Target modifier at construction time.
        // Both plain and tranformed bounding boxes need to be updated.
        // Mark them as such below.
        attr.bbox.plain.dirty = true;
        // updated by the 'updatePlainBBox' method
        // Before transformed bounding box can be updated,
        // we must ensure that we have correct forward and inverse
        // transformation matrices (which are also created by the Target modifier),
        // so that they reflect the current state of the scaling, rotation
        // and other transformation attributes.
        // The 'applyTransformations' method does just that.
        // The 'dirtyTransform' flag (another implicit attribute)
        // is set to true when any of the transformation attributes change,
        // to let us know that transformation matrices need to be updated.
        attr.bbox.transform.dirty = true;
        // updated by the 'updateTransformedBBox' method
        if (hasRotation && noRotationCenter || hasScaling && noScalingCenter) {
            this.scheduleUpdater(attr, 'transform');
        }
    },
    /**
     * Returns the bounding box for the given Sprite as calculated with the Canvas engine.
     *
     * @param {Boolean} [isWithoutTransform] Whether to calculate the bounding box
     * with the current transforms or not.
     */
    getBBox: function(isWithoutTransform) {
        var me = this,
            attr = me.attr,
            bbox = attr.bbox,
            plain = bbox.plain,
            transform = bbox.transform;
        if (plain.dirty) {
            me.updatePlainBBox(plain);
            plain.dirty = false;
        }
        if (!isWithoutTransform) {
            // If tranformations are to be applied ('dirtyTransform' is true),
            // then this will itself call the 'getBBox' method
            // to get the plain untransformed bbox and calculate its center.
            me.applyTransformations();
            if (transform.dirty) {
                me.updateTransformedBBox(transform, plain);
                transform.dirty = false;
            }
            return transform;
        }
        return plain;
    },
    /**
     * @method
     * @protected
     * Subclass will fill the plain object with `x`, `y`, `width`, `height` information
     * of the plain bounding box of this sprite.
     *
     * @param {Object} plain Target object.
     */
    updatePlainBBox: Ext.emptyFn,
    /**
     * @protected
     * Subclass will fill the plain object with `x`, `y`, `width`, `height` information
     * of the transformed bounding box of this sprite.
     *
     * @param {Object} transform Target object (transformed bounding box) to populate.
     * @param {Object} plain Untransformed bounding box.
     */
    updateTransformedBBox: function(transform, plain) {
        this.attr.matrix.transformBBox(plain, 0, transform);
    },
    /**
     * Subclass can rewrite this function to gain better performance.
     * @param {Boolean} isWithoutTransform
     * @return {Array}
     */
    getBBoxCenter: function(isWithoutTransform) {
        var bbox = this.getBBox(isWithoutTransform);
        if (bbox) {
            return [
                bbox.x + bbox.width * 0.5,
                bbox.y + bbox.height * 0.5
            ];
        } else {
            return [
                0,
                0
            ];
        }
    },
    /**
     * Hide the sprite.
     * @return {Ext.draw.sprite.Sprite} this
     * @chainable
     */
    hide: function() {
        this.attr.hidden = true;
        this.setDirty(true);
        return this;
    },
    /**
     * Show the sprite.
     * @return {Ext.draw.sprite.Sprite} this
     * @chainable
     */
    show: function() {
        this.attr.hidden = false;
        this.setDirty(true);
        return this;
    },
    /**
     * Applies sprite's attributes to the given context.
     * @param {Object} ctx Context to apply sprite's attributes to.
     * @param {Array} rect The rect of the context to be affected by gradients.
     */
    useAttributes: function(ctx, rect) {
        // Always (force) apply transformation to sprite instances,
        // even if their 'dirtyTransform' flag is false.
        // The 'dirtyTransform' flag of an instance may never be set to 'true', as the
        // 'transform' updater won't ever be called for sprite instances that have
        // the same transform attributes as their template, because there's nothing to update
        // (an instance is simply a prototype chained template's 'attr' object, that only
        // has own properties for attributes whose values are different).
        // Making the modifier recognize transform attributes set on sprite instances
        // (see Ext.draw.modifier.Modifier's 'pushDown' method, where attributes with
        // same values are removed from the 'changes' object) and making sure their 'dirtyTransform'
        // flag is set to 'true' is not a correct solution here, because of the way instances
        // are rendered (see Ext.draw.sprite.Instancing's 'render' method) - there is no way
        // an instance wounldn't want its 'applyTransformations' method called.
        this.applyTransformations(this.isSpriteInstance);
        // eslint-disable-next-line vars-on-top
        var attr = this.attr,
            canvasAttributes = attr.canvasAttributes,
            strokeStyle = canvasAttributes.strokeStyle,
            fillStyle = canvasAttributes.fillStyle,
            lineDash = canvasAttributes.lineDash,
            lineDashOffset = canvasAttributes.lineDashOffset,
            id;
        if (strokeStyle) {
            if (strokeStyle.isGradient) {
                ctx.strokeStyle = 'black';
                ctx.strokeGradient = strokeStyle;
            } else {
                ctx.strokeGradient = false;
            }
        }
        if (fillStyle) {
            if (fillStyle.isGradient) {
                ctx.fillStyle = 'black';
                ctx.fillGradient = fillStyle;
            } else {
                ctx.fillGradient = false;
            }
        }
        if (lineDash) {
            ctx.setLineDash(lineDash);
        }
        // Only set lineDashOffset to contexts that support the property (excludes VML).
        if (Ext.isNumber(lineDashOffset) && Ext.isNumber(ctx.lineDashOffset)) {
            ctx.lineDashOffset = lineDashOffset;
        }
        for (id in canvasAttributes) {
            if (canvasAttributes[id] !== undefined && canvasAttributes[id] !== ctx[id]) {
                ctx[id] = canvasAttributes[id];
            }
        }
        this.setGradientBBox(ctx, rect);
    },
    setGradientBBox: function(ctx, rect) {
        var attr = this.attr;
        if (attr.constrainGradients) {
            ctx.setGradientBBox({
                x: rect[0],
                y: rect[1],
                width: rect[2],
                height: rect[3]
            });
        } else {
            ctx.setGradientBBox(this.getBBox(attr.transformFillStroke));
        }
    },
    /**
     * @private
     *
     * Calculates forward and inverse transform matrices from sprite's attributes.
     * Transformations are applied in the following order: Scaling, Rotation, Translation.
     * @param {Boolean} [force=false] Forces recalculation of transform matrices even when
     * sprite's transform attributes supposedly haven't changed.
     */
    applyTransformations: function(force) {
        if (!force && !this.attr.dirtyTransform) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            attr = me.attr,
            center = me.getBBoxCenter(true),
            centerX = center[0],
            centerY = center[1],
            tx = attr.translationX,
            ty = attr.translationY,
            sx = attr.scalingX,
            sy = attr.scalingY === null ? attr.scalingX : attr.scalingY,
            scx = attr.scalingCenterX === null ? centerX : attr.scalingCenterX,
            scy = attr.scalingCenterY === null ? centerY : attr.scalingCenterY,
            rad = attr.rotationRads,
            rcx = attr.rotationCenterX === null ? centerX : attr.rotationCenterX,
            rcy = attr.rotationCenterY === null ? centerY : attr.rotationCenterY,
            cos = Math.cos(rad),
            sin = Math.sin(rad),
            tx_4, ty_4;
        if (sx === 1 && sy === 1) {
            scx = 0;
            scy = 0;
        }
        if (rad === 0) {
            rcx = 0;
            rcy = 0;
        }
        // Translation component after steps 1-4 (see below).
        // Saving it here to prevent double calculation.
        tx_4 = scx * (1 - sx) - rcx;
        ty_4 = scy * (1 - sy) - rcy;
        /* eslint-disable max-len */
        // The matrix below is a result of:
        //     (7)          (6)             (5)             (4)           (3)           (2)           (1)
        // | 1 0 tx |   | 1 0 rcx |   | cos -sin 0 |   | 1 0 -rcx |   | 1 0 scx |   | sx 0 0 |   | 1 0 -scx |
        // | 0 1 ty | * | 0 1 rcy | * | sin  cos 0 | * | 0 1 -rcy | * | 0 1 scy | * | 0 sy 0 | * | 0 1 -scy |
        // | 0 0  1 |   | 0 0  1  |   |  0    0  1 |   | 0 0  1   |   | 0 0  1  |   | 0  0 0 |   | 0 0  1   |
        /* eslint-enable max-len */
        attr.matrix.elements = [
            cos * sx,
            sin * sx,
            -sin * sy,
            cos * sy,
            cos * tx_4 - sin * ty_4 + rcx + tx,
            sin * tx_4 + cos * ty_4 + rcy + ty
        ];
        attr.matrix.inverse(attr.inverseMatrix);
        attr.dirtyTransform = false;
        attr.bbox.transform.dirty = true;
    },
    /**
     * Pre-multiplies the current transformation matrix of a sprite with the given matrix.
     * If `isSplit` parameter is `true`, the resulting matrix is also split into
     * individual components (scaling, rotation, translation) and corresponding sprite
     * attributes are updated. The shearing component is not extracted.
     * Note, that transformation attributes work as if transformations are applied to the
     * local coordinate system of a sprite, while matrix transformations transform
     * the global coordinate space or the surface grid.
     * Since the `transform` method returns the sprite itself, calls to the method
     * can be chained. And if updating sprite transformation attributes is desired,
     * it can be achieved by setting the `isSplit` parameter of the last call to `true`.
     * For example:
     *
     *     sprite.transform(matrixA).transform(matrixB).transform(matrixC, true);
     * 
     * See also: {@link #setTransform}
     *
     * @param {Ext.draw.Matrix/Number[]} matrix A transformation matrix or array of its elements.
     * @param {Boolean} [isSplit=false] If 'true', transformation attributes are updated.
     * @return {Ext.draw.sprite.Sprite} This sprite.
     */
    transform: function(matrix, isSplit) {
        var attr = this.attr,
            spriteMatrix = attr.matrix,
            elements;
        if (matrix && matrix.isMatrix) {
            elements = matrix.elements;
        } else {
            elements = matrix;
        }
        //<debug>
        if (!(Ext.isArray(elements) && elements.length === 6)) {
            Ext.raise("An instance of Ext.draw.Matrix or an array of 6 numbers is expected.");
        }
        //</debug>
        spriteMatrix.prepend.apply(spriteMatrix, elements.slice());
        spriteMatrix.inverse(attr.inverseMatrix);
        if (isSplit) {
            this.updateTransformAttributes();
        }
        attr.dirtyTransform = false;
        attr.bbox.transform.dirty = true;
        this.setDirty(true);
        return this;
    },
    /**
     * @private
     */
    updateTransformAttributes: function() {
        var attr = this.attr,
            split = attr.matrix.split();
        attr.rotationRads = split.rotate;
        attr.rotationCenterX = 0;
        attr.rotationCenterY = 0;
        attr.scalingX = split.scaleX;
        attr.scalingY = split.scaleY;
        attr.scalingCenterX = 0;
        attr.scalingCenterY = 0;
        attr.translationX = split.translateX;
        attr.translationY = split.translateY;
    },
    /**
     * Resets current transformation matrix of a sprite to the identify matrix.
     * @param {Boolean} [isSplit=false] If 'true', transformation attributes are updated.
     * @return {Ext.draw.sprite.Sprite} This sprite.
     */
    resetTransform: function(isSplit) {
        var attr = this.attr;
        attr.matrix.reset();
        attr.inverseMatrix.reset();
        if (!isSplit) {
            this.updateTransformAttributes();
        }
        attr.dirtyTransform = false;
        attr.bbox.transform.dirty = true;
        this.setDirty(true);
        return this;
    },
    /**
     * Resets current transformation matrix of a sprite to the identify matrix
     * and pre-multiplies it with the given matrix.
     * This is effectively the same as calling {@link #resetTransform},
     * followed by {@link #transform} with the same arguments.
     * 
     * See also: {@link #transform}
     * 
     *     var drawContainer = new Ext.draw.Container({
     *         renderTo: Ext.getBody(),
     *         width: 380,
     *         height: 380,
     *         sprites: [{
     *             type: 'rect',
     *             width: 100,
     *             height: 100,
     *             fillStyle: 'red'
     *         }]
     *     });
     *     
     *     var main = drawContainer.getSurface();
     *     var rect = main.getItems()[0];
     *     
     *     var m = new Ext.draw.Matrix().rotate(Math.PI, 100, 100);
     *     
     *     rect.setTransform(m);
     *     main.renderFrame();
     * 
     * There may be times where the transformation you need to apply cannot easily be 
     * accomplished using the sprite’s convenience transform methods.  Or, you may want 
     * to pass a matrix directly to the sprite in order to set a transformation.  The 
     * `setTransform` method allows for this sort of advanced usage as well.  The 
     * following tables show each transformation matrix used when applying 
     * transformations to a sprite.
     * 
     * ### Translate
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">1</td>
     *         <td style="font-weight: normal;">0</td>
     *         <td style="font-weight: normal;">tx</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>1</td>
     *         <td>ty</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * ### Rotate (θ is the angle of rotation)
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">cos(θ)</td>
     *         <td style="font-weight: normal;">-sin(θ)</td>
     *         <td style="font-weight: normal;">0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>cos(θ)</td>
     *         <td>0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * ### Scale
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">sx</td>
     *         <td style="font-weight: normal;">0</td>
     *         <td style="font-weight: normal;">0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>cos(θ)</td>
     *         <td>0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * ### Shear X _(λ is the distance on the x axis to shear by)_
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">1</td>
     *         <td style="font-weight: normal;">λx</td>
     *         <td style="font-weight: normal;">0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>1</td>
     *         <td>0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * ### Shear Y (λ is the distance on the y axis to shear by)
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">1</td>
     *         <td style="font-weight: normal;">0</td>
     *         <td style="font-weight: normal;">0</td>
     *     </tr>
     *     <tr>
     *         <td>λy</td>
     *         <td>1</td>
     *         <td>0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * ### Skew X (θ is the angle to skew by)
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">1</td>
     *         <td style="font-weight: normal;">tan(θ)</td>
     *         <td style="font-weight: normal;">0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>1</td>
     *         <td>0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * ### Skew Y (θ is the angle to skew by)
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">1</td>
     *         <td style="font-weight: normal;">0</td>
     *         <td style="font-weight: normal;">0</td>
     *     </tr>
     *     <tr>
     *         <td>tan(θ)</td>
     *         <td>1</td>
     *         <td>0</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * Multiplying matrices for translation, rotation, scaling, and shearing / skewing 
     * any number of times in the desired order produces a single matrix for a composite 
     * transformation.  You can use the product as a value for the `setTransform`method 
     * of a sprite:
     * 
     *     mySprite.setTransform([a, b, c, d, e, f]);
     * 
     * Where `a`, `b`, `c`, `d`, `e`, `f` are numeric values that correspond to the 
     * following transformation matrix components:
     * 
     * <table style="text-align: center;">
     *     <tr>
     *         <td style="font-weight: normal;">a</td>
     *         <td style="font-weight: normal;">c</td>
     *         <td style="font-weight: normal;">e</td>
     *     </tr>
     *     <tr>
     *         <td>b</td>
     *         <td>d</td>
     *         <td>f</td>
     *     </tr>
     *     <tr>
     *         <td>0</td>
     *         <td>0</td>
     *         <td>1</td>
     *     </tr>
     * </table>
     * 
     * @param {Ext.draw.Matrix/Number[]} matrix The transformation matrix to apply or its 
     * raw elements as an array.
     * @param {Boolean} [isSplit=false] If `true`, transformation attributes are updated.
     * @return {Ext.draw.sprite.Sprite} This sprite.
     */
    setTransform: function(matrix, isSplit) {
        this.resetTransform(true);
        this.transform.call(this, matrix, isSplit);
        return this;
    },
    /**
     * @method
     * Called before rendering.
     */
    preRender: Ext.emptyFn,
    /**
     * @method
     * This is where the actual sprite rendering happens by calling `ctx` methods.
     * @param {Ext.draw.Surface} surface A draw container surface.
     * @param {CanvasRenderingContext2D} ctx A context object that is API compatible with the native
     * [CanvasRenderingContext2D](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D).
     * @param {Number[]} surfaceClipRect The clip rect: [left, top, width, height].
     * Not to be confused with the `surface.getRect()`, which represents the location
     * and size of the surface in a draw container, in draw container coordinates.
     * The clip rect on the other hand represents the portion of the surface that is being
     * rendered, in surface coordinates.
     *
     * @return {*} returns `false` to stop rendering in this frame.
     * All the sprites that haven't been rendered will have their dirty flag untouched.
     */
    render: Ext.emptyFn,
    //<debug>
    /**
     * @private
     * Renders the bounding box of transformed sprite.
     */
    renderBBox: function(surface, ctx) {
        var bbox = this.getBBox();
        ctx.beginPath();
        ctx.moveTo(bbox.x, bbox.y);
        ctx.lineTo(bbox.x + bbox.width, bbox.y);
        ctx.lineTo(bbox.x + bbox.width, bbox.y + bbox.height);
        ctx.lineTo(bbox.x, bbox.y + bbox.height);
        ctx.closePath();
        ctx.strokeStyle = 'red';
        ctx.strokeOpacity = 1;
        ctx.lineWidth = 0.5;
        ctx.stroke();
    },
    //</debug>
    /**
     * Performs a hit test on the sprite.
     * @param {Array} point A two-item array containing x and y coordinates of the point.
     * @param {Object} options Hit testing options.
     * @return {Object} A hit result object that contains more information about what
     * exactly was hit or null if nothing was hit.
     */
    hitTest: function(point, options) {
        var x, y, bbox, isBBoxHit;
        // Meant to be overridden in subclasses for more precise hit testing.
        // This version doesn't take any options and simply hit tests sprite's
        // bounding box, if the sprite is visible.
        if (this.isVisible()) {
            x = point[0];
            y = point[1];
            bbox = this.getBBox();
            isBBoxHit = bbox && x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height);
            if (isBBoxHit) {
                return {
                    sprite: this
                };
            }
        }
        return null;
    },
    /**
     * @private
     * Checks if the sprite can be seen.
     * This includes the `hidden` attribute check, alpha/opacity checks,
     * fill/stroke color checks and surface/parent checks.
     * The method doesn't check if the sprite is off-screen.
     * @return {Boolean} Returns `true`, if the sprite can be seen.
     */
    isVisible: function() {
        var attr = this.attr,
            parent = this.getParent(),
            hasParent = parent && (parent.isSurface || parent.isVisible()),
            isSeen = hasParent && !attr.hidden && attr.globalAlpha,
            none1 = Ext.util.Color.NONE,
            none2 = Ext.util.Color.RGBA_NONE,
            hasFill = attr.fillOpacity && attr.fillStyle !== none1 && attr.fillStyle !== none2,
            hasStroke = attr.strokeOpacity && attr.strokeStyle !== none1 && attr.strokeStyle !== none2,
            result = isSeen && (hasFill || hasStroke);
        return !!result;
    },
    repaint: function() {
        var surface = this.getSurface();
        if (surface) {
            surface.renderFrame();
        }
    },
    /**
     * Removes this sprite from its surface.
     * The sprite itself is not destroyed.
     * @return {Ext.draw.sprite.Sprite} Returns the removed sprite or `null` otherwise.
     */
    remove: function() {
        var surface = this.getSurface();
        if (surface && surface.isSurface) {
            return surface.remove(this);
        }
        return null;
    },
    /**
     * Removes the sprite and clears all listeners.
     */
    destroy: function() {
        var me = this,
            modifier = me.modifiers.target,
            currentModifier;
        while (modifier) {
            currentModifier = modifier;
            modifier = modifier._lower;
            currentModifier.destroy();
        }
        delete me.attr;
        me.remove();
        if (me.fireEvent('beforedestroy', me) !== false) {
            me.fireEvent('destroy', me);
        }
        me.callParent();
    }
}, function() {
    // onClassCreated
    // Create one AttributeDefinition instance per sprite class when a class is created
    // and replace the `def` config with the instance that was created using that config.
    // Here we only create an AttributeDefinition instance for the base Sprite class,
    // attribute definitions for subclasses are created inside onClassExtended method.
    this.def = new Ext.draw.sprite.AttributeDefinition(this.def);
    this.def.spriteClass = this;
});

/**
 * Class representing a path.
 * Designed to be compatible with [CanvasPathMethods](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#canvaspathmethods)
 * and will hopefully be replaced by the browsers' implementation of the Path object.
 */
Ext.define('Ext.draw.Path', {
    requires: [
        'Ext.draw.Draw'
    ],
    statics: {
        pathRe: /,?([achlmqrstvxz]),?/gi,
        pathRe2: /-/gi,
        pathSplitRe: /\s|,/g
    },
    svgString: '',
    /**
     * Create a path from pathString.
     * @constructor
     * @param {String} pathString
     */
    constructor: function(pathString) {
        var me = this;
        me.commands = [];
        // Stores command letters from the SVG path data ('d' attribute).
        me.params = [];
        // Stores command parameters from the SVG path data.
        // All command parameters are actually point coordinates as the only commands used
        // are the M, L, C, Z. This makes path transformations and hit testing easier.
        // Arcs are approximated using cubic Bezier curves, H and S commands are translated
        // to L commands and relative commands are translated to their absolute versions.
        me.cursor = null;
        me.startX = 0;
        me.startY = 0;
        if (pathString) {
            me.fromSvgString(pathString);
        }
    },
    /**
     * Clear the path.
     */
    clear: function() {
        var me = this;
        me.params.length = 0;
        me.commands.length = 0;
        me.cursor = null;
        me.startX = 0;
        me.startY = 0;
        me.dirt();
    },
    /**
     * @private
     */
    dirt: function() {
        this.svgString = '';
    },
    /**
     * Move to a position.
     * @param {Number} x
     * @param {Number} y
     */
    moveTo: function(x, y) {
        var me = this;
        if (!me.cursor) {
            me.cursor = [
                x,
                y
            ];
        }
        me.params.push(x, y);
        me.commands.push('M');
        me.startX = x;
        me.startY = y;
        me.cursor[0] = x;
        me.cursor[1] = y;
        me.dirt();
    },
    /**
     * A straight line to a position.
     * @param {Number} x
     * @param {Number} y
     */
    lineTo: function(x, y) {
        var me = this;
        if (!me.cursor) {
            me.cursor = [
                x,
                y
            ];
            me.params.push(x, y);
            me.commands.push('M');
        } else {
            me.params.push(x, y);
            me.commands.push('L');
        }
        me.cursor[0] = x;
        me.cursor[1] = y;
        me.dirt();
    },
    /**
     * A cubic bezier curve to a position.
     * @param {Number} cx1
     * @param {Number} cy1
     * @param {Number} cx2
     * @param {Number} cy2
     * @param {Number} x
     * @param {Number} y
     */
    bezierCurveTo: function(cx1, cy1, cx2, cy2, x, y) {
        var me = this;
        if (!me.cursor) {
            me.moveTo(cx1, cy1);
        }
        me.params.push(cx1, cy1, cx2, cy2, x, y);
        me.commands.push('C');
        me.cursor[0] = x;
        me.cursor[1] = y;
        me.dirt();
    },
    /**
     * A quadratic bezier curve to a position.
     * @param {Number} cx
     * @param {Number} cy
     * @param {Number} x
     * @param {Number} y
     */
    quadraticCurveTo: function(cx, cy, x, y) {
        var me = this;
        if (!me.cursor) {
            me.moveTo(cx, cy);
        }
        me.bezierCurveTo((2 * cx + me.cursor[0]) / 3, (2 * cy + me.cursor[1]) / 3, (2 * cx + x) / 3, (2 * cy + y) / 3, x, y);
    },
    /**
     * Close this path with a straight line.
     */
    closePath: function() {
        var me = this;
        if (me.cursor) {
            me.cursor = null;
            me.commands.push('Z');
            me.dirt();
        }
    },
    /**
     * Create a elliptic arc curve compatible with SVG's arc to instruction.
     *
     * The curve start from (`x1`, `y1`) and ends at (`x2`, `y2`). The ellipse
     * has radius `rx` and `ry` and a rotation of `rotation`.
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} x2
     * @param {Number} y2
     * @param {Number} [rx]
     * @param {Number} [ry]
     * @param {Number} [rotation]
     */
    arcTo: function(x1, y1, x2, y2, rx, ry, rotation) {
        var me = this;
        if (ry === undefined) {
            ry = rx;
        }
        if (rotation === undefined) {
            rotation = 0;
        }
        if (!me.cursor) {
            me.moveTo(x1, y1);
            return;
        }
        if (rx === 0 || ry === 0) {
            me.lineTo(x1, y1);
            return;
        }
        x2 -= x1;
        y2 -= y1;
        // eslint-disable-next-line vars-on-top, one-var
        var x0 = me.cursor[0] - x1,
            y0 = me.cursor[1] - y1,
            area = x2 * y0 - y2 * x0,
            cos, sin, xx, yx, xy, yy,
            l0 = Math.sqrt(x0 * x0 + y0 * y0),
            l2 = Math.sqrt(x2 * x2 + y2 * y2),
            dist, cx, cy, temp;
        // cos rx, -sin ry , x1 - cos rx x1 + ry sin y1
        // sin rx, cos ry, -rx sin x1 + y1 - cos ry y1
        if (area === 0) {
            me.lineTo(x1, y1);
            return;
        }
        if (ry !== rx) {
            cos = Math.cos(rotation);
            sin = Math.sin(rotation);
            xx = cos / rx;
            yx = sin / ry;
            xy = -sin / rx;
            yy = cos / ry;
            temp = xx * x0 + yx * y0;
            y0 = xy * x0 + yy * y0;
            x0 = temp;
            temp = xx * x2 + yx * y2;
            y2 = xy * x2 + yy * y2;
            x2 = temp;
        } else {
            x0 /= rx;
            y0 /= ry;
            x2 /= rx;
            y2 /= ry;
        }
        cx = x0 * l2 + x2 * l0;
        cy = y0 * l2 + y2 * l0;
        dist = 1 / (Math.sin(Math.asin(Math.abs(area) / (l0 * l2)) * 0.5) * Math.sqrt(cx * cx + cy * cy));
        cx *= dist;
        cy *= dist;
        // eslint-disable-next-line vars-on-top, one-var
        var k0 = (cx * x0 + cy * y0) / (x0 * x0 + y0 * y0),
            k2 = (cx * x2 + cy * y2) / (x2 * x2 + y2 * y2),
            cosStart = x0 * k0 - cx,
            sinStart = y0 * k0 - cy,
            cosEnd = x2 * k2 - cx,
            sinEnd = y2 * k2 - cy,
            startAngle = Math.atan2(sinStart, cosStart),
            endAngle = Math.atan2(sinEnd, cosEnd);
        if (area > 0) {
            if (endAngle < startAngle) {
                endAngle += Math.PI * 2;
            }
        } else {
            if (startAngle < endAngle) {
                startAngle += Math.PI * 2;
            }
        }
        if (ry !== rx) {
            cx = cos * cx * rx - sin * cy * ry + x1;
            cy = sin * cy * ry + cos * cy * ry + y1;
            me.lineTo(cos * rx * cosStart - sin * ry * sinStart + cx, sin * rx * cosStart + cos * ry * sinStart + cy);
            me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
        } else {
            cx = cx * rx + x1;
            cy = cy * ry + y1;
            me.lineTo(rx * cosStart + cx, ry * sinStart + cy);
            me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
        }
    },
    /**
     * Create an elliptic arc.
     *
     * See [the whatwg reference of ellipse](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-ellipse).
     *
     * @param {Number} cx
     * @param {Number} cy
     * @param {Number} radiusX
     * @param {Number} radiusY
     * @param {Number} rotation
     * @param {Number} startAngle
     * @param {Number} endAngle
     * @param {Number} anticlockwise
     */
    ellipse: function(cx, cy, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
        var me = this,
            params = me.params,
            start = params.length,
            count, temp, i, j;
        if (endAngle - startAngle >= Math.PI * 2) {
            me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle, startAngle + Math.PI, anticlockwise);
            me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle + Math.PI, endAngle, anticlockwise);
            return;
        }
        if (!anticlockwise) {
            if (endAngle < startAngle) {
                endAngle += Math.PI * 2;
            }
            count = me.approximateArc(params, cx, cy, radiusX, radiusY, rotation, startAngle, endAngle);
        } else {
            if (startAngle < endAngle) {
                startAngle += Math.PI * 2;
            }
            count = me.approximateArc(params, cx, cy, radiusX, radiusY, rotation, endAngle, startAngle);
            for (i = start , j = params.length - 2; i < j; i += 2 , j -= 2) {
                temp = params[i];
                params[i] = params[j];
                params[j] = temp;
                temp = params[i + 1];
                params[i + 1] = params[j + 1];
                params[j + 1] = temp;
            }
        }
        if (!me.cursor) {
            me.cursor = [
                params[params.length - 2],
                params[params.length - 1]
            ];
            me.commands.push('M');
        } else {
            me.cursor[0] = params[params.length - 2];
            me.cursor[1] = params[params.length - 1];
            me.commands.push('L');
        }
        for (i = 2; i < count; i += 6) {
            me.commands.push('C');
        }
        me.dirt();
    },
    /**
     * Create an circular arc.
     *
     * @param {Number} x
     * @param {Number} y
     * @param {Number} radius
     * @param {Number} startAngle
     * @param {Number} endAngle
     * @param {Number} anticlockwise
     */
    arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
        this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
    },
    /**
     * Draw a rectangle and close it.
     *
     * @param {Number} x
     * @param {Number} y
     * @param {Number} width
     * @param {Number} height
     */
    rect: function(x, y, width, height) {
        var me = this;
        if (width === 0 || height === 0) {
            return;
        }
        me.moveTo(x, y);
        me.lineTo(x + width, y);
        me.lineTo(x + width, y + height);
        me.lineTo(x, y + height);
        me.closePath();
    },
    /**
     * @private
     * @param {Array} result
     * @param {Number} cx
     * @param {Number} cy
     * @param {Number} rx
     * @param {Number} ry
     * @param {Number} phi
     * @param {Number} theta1
     * @param {Number} theta2
     * @return {Number}
     */
    approximateArc: function(result, cx, cy, rx, ry, phi, theta1, theta2) {
        var cosPhi = Math.cos(phi),
            sinPhi = Math.sin(phi),
            cosTheta1 = Math.cos(theta1),
            sinTheta1 = Math.sin(theta1),
            xx = cosPhi * cosTheta1 * rx - sinPhi * sinTheta1 * ry,
            yx = -cosPhi * sinTheta1 * rx - sinPhi * cosTheta1 * ry,
            xy = sinPhi * cosTheta1 * rx + cosPhi * sinTheta1 * ry,
            yy = -sinPhi * sinTheta1 * rx + cosPhi * cosTheta1 * ry,
            rightAngle = Math.PI / 2,
            count = 2,
            exx = xx,
            eyx = yx,
            exy = xy,
            eyy = yy,
            rho = 0.547443256150549,
            temp, y1, x3, y3, x2, y2;
        theta2 -= theta1;
        if (theta2 < 0) {
            theta2 += Math.PI * 2;
        }
        result.push(xx + cx, xy + cy);
        while (theta2 >= rightAngle) {
            result.push(exx + eyx * rho + cx, exy + eyy * rho + cy, exx * rho + eyx + cx, exy * rho + eyy + cy, eyx + cx, eyy + cy);
            count += 6;
            theta2 -= rightAngle;
            temp = exx;
            exx = eyx;
            eyx = -temp;
            temp = exy;
            exy = eyy;
            eyy = -temp;
        }
        if (theta2) {
            y1 = (0.3294738052815987 + 0.012120855841304373 * theta2) * theta2;
            x3 = Math.cos(theta2);
            y3 = Math.sin(theta2);
            x2 = x3 + y1 * y3;
            y2 = y3 - y1 * x3;
            result.push(exx + eyx * y1 + cx, exy + eyy * y1 + cy, exx * x2 + eyx * y2 + cx, exy * x2 + eyy * y2 + cy, exx * x3 + eyx * y3 + cx, exy * x3 + eyy * y3 + cy);
            count += 6;
        }
        return count;
    },
    /**
     * [http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes](http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes)
     * @param {Number} rx
     * @param {Number} ry
     * @param {Number} rotation Differ from svg spec, this is radian.
     * @param {Number} fA
     * @param {Number} fS
     * @param {Number} x2
     * @param {Number} y2
     */
    arcSvg: function(rx, ry, rotation, fA, fS, x2, y2) {
        if (rx < 0) {
            rx = -rx;
        }
        if (ry < 0) {
            ry = -ry;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            x1 = me.cursor[0],
            y1 = me.cursor[1],
            hdx = (x1 - x2) / 2,
            hdy = (y1 - y2) / 2,
            cosPhi = Math.cos(rotation),
            sinPhi = Math.sin(rotation),
            xp = hdx * cosPhi + hdy * sinPhi,
            yp = -hdx * sinPhi + hdy * cosPhi,
            ratX = xp / rx,
            ratY = yp / ry,
            lambda = ratX * ratX + ratY * ratY,
            cx = (x1 + x2) * 0.5,
            cy = (y1 + y2) * 0.5,
            cpx = 0,
            cpy = 0,
            theta1, deltaTheta;
        if (lambda >= 1) {
            lambda = Math.sqrt(lambda);
            rx *= lambda;
            ry *= lambda;
        } else // me gives lambda == cpx == cpy == 0;
        {
            lambda = Math.sqrt(1 / lambda - 1);
            if (fA === fS) {
                lambda = -lambda;
            }
            cpx = lambda * rx * ratY;
            cpy = -lambda * ry * ratX;
            cx += cosPhi * cpx - sinPhi * cpy;
            cy += sinPhi * cpx + cosPhi * cpy;
        }
        theta1 = Math.atan2((yp - cpy) / ry, (xp - cpx) / rx);
        deltaTheta = Math.atan2((-yp - cpy) / ry, (-xp - cpx) / rx) - theta1;
        if (fS) {
            if (deltaTheta <= 0) {
                deltaTheta += Math.PI * 2;
            }
        } else {
            if (deltaTheta >= 0) {
                deltaTheta -= Math.PI * 2;
            }
        }
        me.ellipse(cx, cy, rx, ry, rotation, theta1, theta1 + deltaTheta, 1 - fS);
    },
    /**
     * Feed the path from svg path string.
     * @param {String} pathString
     */
    fromSvgString: function(pathString) {
        if (!pathString) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            parts,
            paramCounts = {
                a: 7,
                c: 6,
                h: 1,
                l: 2,
                m: 2,
                q: 4,
                s: 4,
                t: 2,
                v: 1,
                z: 0,
                A: 7,
                C: 6,
                H: 1,
                L: 2,
                M: 2,
                Q: 4,
                S: 4,
                T: 2,
                V: 1,
                Z: 0
            },
            lastCommand = '',
            lastControlX, lastControlY,
            lastX = 0,
            lastY = 0,
            part = false,
            i, partLength;
        // Split the string to items.
        if (Ext.isString(pathString)) {
            parts = pathString.replace(Ext.draw.Path.pathRe, " $1 ").replace(Ext.draw.Path.pathRe2, " -").split(Ext.draw.Path.pathSplitRe);
        } else if (Ext.isArray(pathString)) {
            parts = pathString.join(',').split(Ext.draw.Path.pathSplitRe);
        }
        // Remove empty entries
        for (i = 0 , partLength = 0; i < parts.length; i++) {
            if (parts[i] !== '') {
                parts[partLength++] = parts[i];
            }
        }
        parts.length = partLength;
        me.clear();
        for (i = 0; i < parts.length; ) {
            lastCommand = part;
            part = parts[i];
            i++;
            switch (part) {
                case 'M':
                    me.moveTo(lastX = +parts[i], lastY = +parts[i + 1]);
                    i += 2;
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
                        i += 2;
                    };
                    break;
                case 'L':
                    me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
                    i += 2;
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
                        i += 2;
                    };
                    break;
                case 'A':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.arcSvg(+parts[i], +parts[i + 1], +parts[i + 2] * Math.PI / 180, +parts[i + 3], +parts[i + 4], lastX = +parts[i + 5], lastY = +parts[i + 6]);
                        i += 7;
                    };
                    break;
                case 'C':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.bezierCurveTo(+parts[i], +parts[i + 1], lastControlX = +parts[i + 2], lastControlY = +parts[i + 3], lastX = +parts[i + 4], lastY = +parts[i + 5]);
                        i += 6;
                    };
                    break;
                case 'Z':
                    me.closePath();
                    break;
                case 'm':
                    me.moveTo(lastX += +parts[i], lastY += +parts[i + 1]);
                    i += 2;
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
                        i += 2;
                    };
                    break;
                case 'l':
                    me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
                    i += 2;
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
                        i += 2;
                    };
                    break;
                case 'a':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.arcSvg(+parts[i], +parts[i + 1], +parts[i + 2] * Math.PI / 180, +parts[i + 3], +parts[i + 4], lastX += +parts[i + 5], lastY += +parts[i + 6]);
                        i += 7;
                    };
                    break;
                case 'c':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.bezierCurveTo(lastX + (+parts[i]), lastY + (+parts[i + 1]), lastControlX = lastX + (+parts[i + 2]), lastControlY = lastY + (+parts[i + 3]), lastX += +parts[i + 4], lastY += +parts[i + 5]);
                        i += 6;
                    };
                    break;
                case 'z':
                    me.closePath();
                    break;
                case 's':
                    if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
                        lastControlX = lastX;
                        lastControlY = lastY;
                    };
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.bezierCurveTo(lastX + lastX - lastControlX, lastY + lastY - lastControlY, lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]), lastX += +parts[i + 2], lastY += +parts[i + 3]);
                        i += 4;
                    };
                    break;
                case 'S':
                    if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
                        lastControlX = lastX;
                        lastControlY = lastY;
                    };
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.bezierCurveTo(lastX + lastX - lastControlX, lastY + lastY - lastControlY, lastControlX = +parts[i], lastControlY = +parts[i + 1], lastX = (+parts[i + 2]), lastY = (+parts[i + 3]));
                        i += 4;
                    };
                    break;
                case 'q':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.quadraticCurveTo(lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]), lastX += +parts[i + 2], lastY += +parts[i + 3]);
                        i += 4;
                    };
                    break;
                case 'Q':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.quadraticCurveTo(lastControlX = +parts[i], lastControlY = +parts[i + 1], lastX = +parts[i + 2], lastY = +parts[i + 3]);
                        i += 4;
                    };
                    break;
                case 't':
                    if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
                        lastControlX = lastX;
                        lastControlY = lastY;
                    };
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.quadraticCurveTo(lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY, lastX += +parts[i + 1], lastY += +parts[i + 2]);
                        i += 2;
                    };
                    break;
                case 'T':
                    if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
                        lastControlX = lastX;
                        lastControlY = lastY;
                    };
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.quadraticCurveTo(lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY, lastX = (+parts[i + 1]), lastY = (+parts[i + 2]));
                        i += 2;
                    };
                    break;
                case 'h':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX += +parts[i], lastY);
                        i++;
                    };
                    break;
                case 'H':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX = +parts[i], lastY);
                        i++;
                    };
                    break;
                case 'v':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX, lastY += +parts[i]);
                        i++;
                    };
                    break;
                case 'V':
                    while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
                        me.lineTo(lastX, lastY = +parts[i]);
                        i++;
                    };
                    break;
            }
        }
    },
    /**
     * Clone this path.
     * @return {Ext.draw.Path}
     */
    clone: function() {
        var me = this,
            path = new Ext.draw.Path();
        path.params = me.params.slice(0);
        path.commands = me.commands.slice(0);
        path.cursor = me.cursor ? me.cursor.slice(0) : null;
        path.startX = me.startX;
        path.startY = me.startY;
        path.svgString = me.svgString;
        return path;
    },
    /**
     * Transform the current path by a matrix.
     * @param {Ext.draw.Matrix} matrix
     */
    transform: function(matrix) {
        if (matrix.isIdentity()) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var xx = matrix.getXX(),
            yx = matrix.getYX(),
            dx = matrix.getDX(),
            xy = matrix.getXY(),
            yy = matrix.getYY(),
            dy = matrix.getDY(),
            params = this.params,
            i = 0,
            ln = params.length,
            x, y;
        for (; i < ln; i += 2) {
            x = params[i];
            y = params[i + 1];
            params[i] = x * xx + y * yx + dx;
            params[i + 1] = x * xy + y * yy + dy;
        }
        this.dirt();
    },
    /**
     * Get the bounding box of this matrix.
     * @param {Object} [target] Optional object to receive the result.
     *
     * @return {Object} Object with x, y, width and height
     */
    getDimension: function(target) {
        if (!target) {
            target = {};
        }
        if (!this.commands || !this.commands.length) {
            target.x = 0;
            target.y = 0;
            target.width = 0;
            target.height = 0;
            return target;
        }
        target.left = Infinity;
        target.top = Infinity;
        target.right = -Infinity;
        target.bottom = -Infinity;
        // eslint-disable-next-line vars-on-top
        var i = 0,
            j = 0,
            commands = this.commands,
            params = this.params,
            ln = commands.length,
            x, y;
        for (; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                case 'L':
                    x = params[j];
                    y = params[j + 1];
                    target.left = Math.min(x, target.left);
                    target.top = Math.min(y, target.top);
                    target.right = Math.max(x, target.right);
                    target.bottom = Math.max(y, target.bottom);
                    j += 2;
                    break;
                case 'C':
                    this.expandDimension(target, x, y, params[j], params[j + 1], params[j + 2], params[j + 3], x = params[j + 4], y = params[j + 5]);
                    j += 6;
                    break;
            }
        }
        target.x = target.left;
        target.y = target.top;
        target.width = target.right - target.left;
        target.height = target.bottom - target.top;
        return target;
    },
    /**
     * Get the bounding box as if the path is transformed by a matrix.
     *
     * @param {Ext.draw.Matrix} matrix
     * @param {Object} [target] Optional object to receive the result.
     *
     * @return {Object} An object with x, y, width and height.
     */
    getDimensionWithTransform: function(matrix, target) {
        if (!this.commands || !this.commands.length) {
            if (!target) {
                target = {};
            }
            target.x = 0;
            target.y = 0;
            target.width = 0;
            target.height = 0;
            return target;
        }
        target.left = Infinity;
        target.top = Infinity;
        target.right = -Infinity;
        target.bottom = -Infinity;
        // eslint-disable-next-line vars-on-top
        var xx = matrix.getXX(),
            yx = matrix.getYX(),
            dx = matrix.getDX(),
            xy = matrix.getXY(),
            yy = matrix.getYY(),
            dy = matrix.getDY(),
            i = 0,
            j = 0,
            commands = this.commands,
            params = this.params,
            ln = commands.length,
            x, y;
        for (; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                case 'L':
                    x = params[j] * xx + params[j + 1] * yx + dx;
                    y = params[j] * xy + params[j + 1] * yy + dy;
                    target.left = Math.min(x, target.left);
                    target.top = Math.min(y, target.top);
                    target.right = Math.max(x, target.right);
                    target.bottom = Math.max(y, target.bottom);
                    j += 2;
                    break;
                case 'C':
                    this.expandDimension(target, x, y, params[j] * xx + params[j + 1] * yx + dx, params[j] * xy + params[j + 1] * yy + dy, params[j + 2] * xx + params[j + 3] * yx + dx, params[j + 2] * xy + params[j + 3] * yy + dy, x = params[j + 4] * xx + params[j + 5] * yx + dx, y = params[j + 4] * xy + params[j + 5] * yy + dy);
                    j += 6;
                    break;
            }
        }
        if (!target) {
            target = {};
        }
        target.x = target.left;
        target.y = target.top;
        target.width = target.right - target.left;
        target.height = target.bottom - target.top;
        return target;
    },
    /**
     * @private
     * Expand the rect by the bbox of a bezier curve.
     *
     * @param {Object} target
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} cx1
     * @param {Number} cy1
     * @param {Number} cx2
     * @param {Number} cy2
     * @param {Number} x2
     * @param {Number} y2
     */
    expandDimension: function(target, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
        var me = this,
            l = target.left,
            r = target.right,
            t = target.top,
            b = target.bottom,
            dim = me.dim || (me.dim = []);
        me.curveDimension(x1, cx1, cx2, x2, dim);
        l = Math.min(l, dim[0]);
        r = Math.max(r, dim[1]);
        me.curveDimension(y1, cy1, cy2, y2, dim);
        t = Math.min(t, dim[0]);
        b = Math.max(b, dim[1]);
        target.left = l;
        target.right = r;
        target.top = t;
        target.bottom = b;
    },
    /**
     * @private
     * Determine the curve
     * @param {Number} a
     * @param {Number} b
     * @param {Number} c
     * @param {Number} d
     * @param {Number} dim
     */
    curveDimension: function(a, b, c, d, dim) {
        var qa = 3 * (-a + 3 * (b - c) + d),
            qb = 6 * (a - 2 * b + c),
            qc = -3 * (a - b),
            x, y,
            min = Math.min(a, d),
            max = Math.max(a, d),
            delta;
        if (qa === 0) {
            if (qb === 0) {
                dim[0] = min;
                dim[1] = max;
                return;
            } else {
                x = -qc / qb;
                if (0 < x && x < 1) {
                    y = this.interpolate(a, b, c, d, x);
                    min = Math.min(min, y);
                    max = Math.max(max, y);
                }
            }
        } else {
            delta = qb * qb - 4 * qa * qc;
            if (delta >= 0) {
                delta = Math.sqrt(delta);
                x = (delta - qb) / 2 / qa;
                if (0 < x && x < 1) {
                    y = this.interpolate(a, b, c, d, x);
                    min = Math.min(min, y);
                    max = Math.max(max, y);
                }
                if (delta > 0) {
                    x -= delta / qa;
                    if (0 < x && x < 1) {
                        y = this.interpolate(a, b, c, d, x);
                        min = Math.min(min, y);
                        max = Math.max(max, y);
                    }
                }
            }
        }
        dim[0] = min;
        dim[1] = max;
    },
    /**
     * @private
     *
     * Returns `a * (1 - t) ^ 3 + 3 * b (1 - t) ^ 2 * t + 3 * c (1 - t) * t ^ 3 + d * t ^ 3`.
     *
     * @param {Number} a
     * @param {Number} b
     * @param {Number} c
     * @param {Number} d
     * @param {Number} t
     * @return {Number}
     */
    interpolate: function(a, b, c, d, t) {
        var rate;
        if (t === 0) {
            return a;
        }
        if (t === 1) {
            return d;
        }
        rate = (1 - t) / t;
        return t * t * t * (d + rate * (3 * c + rate * (3 * b + rate * a)));
    },
    /**
     * Reconstruct path from cubic bezier curve stripes.
     * @param {Array} stripes
     */
    fromStripes: function(stripes) {
        var me = this,
            i = 0,
            ln = stripes.length,
            j, ln2, stripe;
        me.clear();
        for (; i < ln; i++) {
            stripe = stripes[i];
            me.params.push.apply(me.params, stripe);
            me.commands.push('M');
            for (j = 2 , ln2 = stripe.length; j < ln2; j += 6) {
                me.commands.push('C');
            }
        }
        if (!me.cursor) {
            me.cursor = [];
        }
        me.cursor[0] = me.params[me.params.length - 2];
        me.cursor[1] = me.params[me.params.length - 1];
        me.dirt();
    },
    /**
     * Convert path to bezier curve stripes.
     * @param {Array} [target] The optional array to receive the result.
     * @return {Array}
     */
    toStripes: function(target) {
        var stripes = target || [],
            curr, x, y, lastX, lastY, startX, startY, i, j,
            commands = this.commands,
            params = this.params,
            ln = commands.length;
        for (i = 0 , j = 0; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    curr = [
                        startX = lastX = params[j++],
                        startY = lastY = params[j++]
                    ];
                    stripes.push(curr);
                    break;
                case 'L':
                    x = params[j++];
                    y = params[j++];
                    curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
                    break;
                case 'C':
                    curr.push(params[j++], params[j++], params[j++], params[j++], lastX = params[j++], lastY = params[j++]);
                    break;
                case 'Z':
                    x = startX;
                    y = startY;
                    curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
                    break;
            }
        }
        return stripes;
    },
    /**
     * @private
     * Update cache for svg string of this path.
     */
    updateSvgString: function() {
        var result = [],
            commands = this.commands,
            params = this.params,
            ln = commands.length,
            i = 0,
            j = 0;
        for (; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    result.push('M' + params[j] + ',' + params[j + 1]);
                    j += 2;
                    break;
                case 'L':
                    result.push('L' + params[j] + ',' + params[j + 1]);
                    j += 2;
                    break;
                case 'C':
                    result.push('C' + params[j] + ',' + params[j + 1] + ' ' + params[j + 2] + ',' + params[j + 3] + ' ' + params[j + 4] + ',' + params[j + 5]);
                    j += 6;
                    break;
                case 'Z':
                    result.push('Z');
                    break;
            }
        }
        this.svgString = result.join('');
    },
    /**
     * Return an svg path string for this path.
     * @return {String}
     */
    toString: function() {
        if (!this.svgString) {
            this.updateSvgString();
        }
        return this.svgString;
    }
});

/**
 * @private
 * Adds hit testing and path intersection points methods to the Ext.draw.Path.
 * Included by the Ext.draw.PathUtil.
 */
Ext.define('Ext.draw.overrides.hittest.Path', {
    override: 'Ext.draw.Path',
    // An arbitrary point outside the path used for hit testing with ray casting method.
    rayOrigin: {
        x: -10000,
        y: -10000
    },
    /**
     * Tests whether the given point is inside the path.
     * @param {Number} x
     * @param {Number} y
     * @return {Boolean}
     * @member Ext.draw.Path
     */
    isPointInPath: function(x, y) {
        var me = this,
            commands = me.commands,
            solver = Ext.draw.PathUtil,
            origin = me.rayOrigin,
            params = me.params,
            ln = commands.length,
            firstX = null,
            firstY = null,
            lastX = 0,
            lastY = 0,
            count = 0,
            i, j;
        for (i = 0 , j = 0; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    if (firstX !== null) {
                        // eslint-disable-next-line max-len
                        if (solver.linesIntersection(firstX, firstY, lastX, lastY, origin.x, origin.y, x, y)) {
                            count += 1;
                        }
                    };
                    firstX = lastX = params[j];
                    firstY = lastY = params[j + 1];
                    j += 2;
                    break;
                case 'L':
                    // eslint-disable-next-line max-len
                    if (solver.linesIntersection(lastX, lastY, params[j], params[j + 1], origin.x, origin.y, x, y)) {
                        count += 1;
                    };
                    lastX = params[j];
                    lastY = params[j + 1];
                    j += 2;
                    break;
                case 'C':
                    count += solver.cubicLineIntersections(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], origin.x, origin.y, x, y).length;
                    lastX = params[j + 4];
                    lastY = params[j + 5];
                    j += 6;
                    break;
                case 'Z':
                    if (firstX !== null) {
                        // eslint-disable-next-line max-len
                        if (solver.linesIntersection(firstX, firstY, lastX, lastY, origin.x, origin.y, x, y)) {
                            count += 1;
                        }
                    };
                    break;
            }
        }
        return count % 2 === 1;
    },
    /**
     * Tests whether the given point is on the path.
     * @param {Number} x
     * @param {Number} y
     * @return {Boolean}
     * @member Ext.draw.Path
     */
    isPointOnPath: function(x, y) {
        var me = this,
            commands = me.commands,
            solver = Ext.draw.PathUtil,
            params = me.params,
            ln = commands.length,
            firstX = null,
            firstY = null,
            lastX = 0,
            lastY = 0,
            i, j;
        for (i = 0 , j = 0; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    if (firstX !== null) {
                        if (solver.pointOnLine(firstX, firstY, lastX, lastY, x, y)) {
                            return true;
                        }
                    };
                    firstX = lastX = params[j];
                    firstY = lastY = params[j + 1];
                    j += 2;
                    break;
                case 'L':
                    if (solver.pointOnLine(lastX, lastY, params[j], params[j + 1], x, y)) {
                        return true;
                    };
                    lastX = params[j];
                    lastY = params[j + 1];
                    j += 2;
                    break;
                case 'C':
                    if (solver.pointOnCubic(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], x, y)) {
                        return true;
                    };
                    lastX = params[j + 4];
                    lastY = params[j + 5];
                    j += 6;
                    break;
                case 'Z':
                    if (firstX !== null) {
                        if (solver.pointOnLine(firstX, firstY, lastX, lastY, x, y)) {
                            return true;
                        }
                    };
                    break;
            }
        }
        return false;
    },
    /**
     * Calculates the points where the given segment intersects the path.
     * If four parameters are given then the segment is considered to be a line segment,
     * where given parameters are the coordinates of the start and end points.
     * If eight parameters are given then the segment is considered to be
     * a cubic Bezier curve segment, where given parameters are the
     * coordinates of its edge points and control points.
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @param x3
     * @param y3
     * @param x4
     * @param y4
     * @return {Array}
     * @member Ext.draw.Path
     */
    getSegmentIntersections: function(x1, y1, x2, y2, x3, y3, x4, y4) {
        var me = this,
            count = arguments.length,
            solver = Ext.draw.PathUtil,
            commands = me.commands,
            params = me.params,
            ln = commands.length,
            firstX = null,
            firstY = null,
            lastX = 0,
            lastY = 0,
            intersections = [],
            i, j, points;
        for (i = 0 , j = 0; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    if (firstX !== null) {
                        switch (count) {
                            case 4:
                                points = solver.linesIntersection(firstX, firstY, lastX, lastY, x1, y1, x2, y2);
                                if (points) {
                                    intersections.push(points);
                                };
                                break;
                            case 8:
                                points = solver.cubicLineIntersections(x1, x2, x3, x4, y1, y2, y3, y4, firstX, firstY, lastX, lastY);
                                intersections.push.apply(intersections, points);
                                break;
                        }
                    };
                    firstX = lastX = params[j];
                    firstY = lastY = params[j + 1];
                    j += 2;
                    break;
                case 'L':
                    switch (count) {
                        case 4:
                            points = solver.linesIntersection(lastX, lastY, params[j], params[j + 1], x1, y1, x2, y2);
                            if (points) {
                                intersections.push(points);
                            };
                            break;
                        case 8:
                            points = solver.cubicLineIntersections(x1, x2, x3, x4, y1, y2, y3, y4, lastX, lastY, params[j], params[j + 1]);
                            intersections.push.apply(intersections, points);
                            break;
                    };
                    lastX = params[j];
                    lastY = params[j + 1];
                    j += 2;
                    break;
                case 'C':
                    switch (count) {
                        case 4:
                            points = solver.cubicLineIntersections(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], x1, y1, x2, y2);
                            intersections.push.apply(intersections, points);
                            break;
                        case 8:
                            points = solver.cubicsIntersections(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], x1, x2, x3, x4, y1, y2, y3, y4);
                            intersections.push.apply(intersections, points);
                            break;
                    };
                    lastX = params[j + 4];
                    lastY = params[j + 5];
                    j += 6;
                    break;
                case 'Z':
                    if (firstX !== null) {
                        switch (count) {
                            case 4:
                                points = solver.linesIntersection(firstX, firstY, lastX, lastY, x1, y1, x2, y2);
                                if (points) {
                                    intersections.push(points);
                                };
                                break;
                            case 8:
                                points = solver.cubicLineIntersections(x1, x2, x3, x4, y1, y2, y3, y4, firstX, firstY, lastX, lastY);
                                intersections.push.apply(intersections, points);
                                break;
                        }
                    };
                    break;
            }
        }
        return intersections;
    },
    getIntersections: function(path) {
        var me = this,
            commands = me.commands,
            params = me.params,
            ln = commands.length,
            firstX = null,
            firstY = null,
            lastX = 0,
            lastY = 0,
            intersections = [],
            i, j, points;
        for (i = 0 , j = 0; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    if (firstX !== null) {
                        points = path.getSegmentIntersections.call(path, firstX, firstY, lastX, lastY);
                        intersections.push.apply(intersections, points);
                    };
                    firstX = lastX = params[j];
                    firstY = lastY = params[j + 1];
                    j += 2;
                    break;
                case 'L':
                    points = path.getSegmentIntersections.call(path, lastX, lastY, params[j], params[j + 1]);
                    intersections.push.apply(intersections, points);
                    lastX = params[j];
                    lastY = params[j + 1];
                    j += 2;
                    break;
                case 'C':
                    points = path.getSegmentIntersections.call(path, lastX, lastY, params[j], params[j + 1], params[j + 2], params[j + 3], params[j + 4], params[j + 5]);
                    intersections.push.apply(intersections, points);
                    lastX = params[j + 4];
                    lastY = params[j + 5];
                    j += 6;
                    break;
                case 'Z':
                    if (firstX !== null) {
                        points = path.getSegmentIntersections.call(path, firstX, firstY, lastX, lastY);
                        intersections.push.apply(intersections, points);
                    };
                    break;
            }
        }
        return intersections;
    }
});

/**
 * @class Ext.draw.sprite.Path
 * @extends Ext.draw.sprite.Sprite
 *
 * A sprite that represents a path.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'path',
 *            path: 'M20,30 c0,-50 75,50 75,0 c0,-50 -75,50 -75,0',
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 * 
 * ### Drawing with SVG Paths
 * You may use special SVG Path syntax to "describe" the drawing path.
 * Here are the SVG path commands:
 * 
 * + M = moveto
 * + L = lineto
 * + H = horizontal lineto
 * + V = vertical lineto
 * + C = curveto
 * + S = smooth curveto
 * + Q = quadratic Bézier curve
 * + T = smooth quadratic Bézier curveto
 * + A = elliptical Arc
 * + Z = closepath
 * 
 * **Note:** Capital letters indicate that the item should be absolutely positioned. 
 * Use lower case letters for relative positioning.
 */
Ext.define('Ext.draw.sprite.Path', {
    extend: 'Ext.draw.sprite.Sprite',
    requires: [
        'Ext.draw.Draw',
        'Ext.draw.Path'
    ],
    alias: [
        'sprite.path',
        'Ext.draw.Sprite'
    ],
    type: 'path',
    isPath: true,
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {String} path The SVG based path string used by the sprite.
                 */
                path: function(n, o) {
                    if (!(n instanceof Ext.draw.Path)) {
                        n = new Ext.draw.Path(n);
                    }
                    return n;
                }
            },
            aliases: {
                d: 'path'
            },
            triggers: {
                path: 'bbox'
            },
            updaters: {
                path: function(attr) {
                    var path = attr.path;
                    if (!path || path.bindAttr !== attr) {
                        path = new Ext.draw.Path();
                        path.bindAttr = attr;
                        attr.path = path;
                    }
                    path.clear();
                    this.updatePath(path, attr);
                    this.scheduleUpdater(attr, 'bbox', [
                        'path'
                    ]);
                }
            }
        }
    },
    updatePlainBBox: function(plain) {
        if (this.attr.path) {
            this.attr.path.getDimension(plain);
        }
    },
    updateTransformedBBox: function(transform) {
        if (this.attr.path) {
            this.attr.path.getDimensionWithTransform(this.attr.matrix, transform);
        }
    },
    render: function(surface, ctx) {
        var mat = this.attr.matrix,
            attr = this.attr;
        if (!attr.path || attr.path.params.length === 0) {
            return;
        }
        mat.toContext(ctx);
        ctx.appendPath(attr.path);
        ctx.fillStroke(attr);
        //<debug>
        // eslint-disable-next-line vars-on-top
        var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug;
        if (debug) {
            if (debug.bbox) {
                this.renderBBox(surface, ctx);
            }
            if (debug.xray) {
                this.renderXRay(surface, ctx);
            }
        }
    },
    //</debug>
    //<debug>
    renderXRay: function(surface, ctx) {
        var attr = this.attr,
            mat = attr.matrix,
            imat = attr.inverseMatrix,
            path = attr.path,
            commands = path.commands,
            params = path.params,
            ln = commands.length,
            twoPi = Math.PI * 2,
            size = 2,
            i, j;
        mat.toContext(ctx);
        ctx.beginPath();
        for (i = 0 , j = 0; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    ctx.moveTo(params[j] - size, params[j + 1] - size);
                    ctx.rect(params[j] - size, params[j + 1] - size, size * 2, size * 2);
                    j += 2;
                    break;
                case 'L':
                    ctx.moveTo(params[j] - size, params[j + 1] - size);
                    ctx.rect(params[j] - size, params[j + 1] - size, size * 2, size * 2);
                    j += 2;
                    break;
                case 'C':
                    ctx.moveTo(params[j] + size, params[j + 1]);
                    ctx.arc(params[j], params[j + 1], size, 0, twoPi, true);
                    j += 2;
                    ctx.moveTo(params[j] + size, params[j + 1]);
                    ctx.arc(params[j], params[j + 1], size, 0, twoPi, true);
                    j += 2;
                    ctx.moveTo(params[j] + size * 2, params[j + 1]);
                    ctx.rect(params[j] - size, params[j + 1] - size, size * 2, size * 2);
                    j += 2;
                    break;
                default:
            }
        }
        imat.toContext(ctx);
        ctx.strokeStyle = 'black';
        ctx.strokeOpacity = 1;
        ctx.lineWidth = 1;
        ctx.stroke();
        mat.toContext(ctx);
        ctx.beginPath();
        for (i = 0 , j = 0; i < ln; i++) {
            switch (commands[i]) {
                case 'M':
                    ctx.moveTo(params[j], params[j + 1]);
                    j += 2;
                    break;
                case 'L':
                    ctx.moveTo(params[j], params[j + 1]);
                    j += 2;
                    break;
                case 'C':
                    ctx.lineTo(params[j], params[j + 1]);
                    j += 2;
                    ctx.moveTo(params[j], params[j + 1]);
                    j += 2;
                    ctx.lineTo(params[j], params[j + 1]);
                    j += 2;
                    break;
                default:
            }
        }
        imat.toContext(ctx);
        ctx.lineWidth = 0.5;
        ctx.stroke();
    },
    //</debug>
    /**
     * Update the path.
     * @param {Ext.draw.Path} path An empty path to draw on using path API.
     * @param {Object} attr The attribute object. Note: DO NOT use the `sprite.attr` instead of this
     * if you want to work with instancing.
     */
    updatePath: function(path, attr) {}
});

/**
 * @private
 * Adds hit testing methods to the Ext.draw.sprite.Path sprite.
 * Included by the Ext.draw.PathUtil.
 */
Ext.define('Ext.draw.overrides.hittest.sprite.Path', {
    override: 'Ext.draw.sprite.Path',
    requires: [
        'Ext.draw.Color'
    ],
    /**
     * Tests whether the given point is inside the path.
     * @param x
     * @param y
     * @return {Boolean}
     * @member Ext.draw.sprite.Path
     */
    isPointInPath: function(x, y) {
        var attr = this.attr,
            path, matrix, params, result;
        if (attr.fillStyle === Ext.util.Color.RGBA_NONE) {
            return this.isPointOnPath(x, y);
        }
        path = attr.path;
        matrix = attr.matrix;
        if (!matrix.isIdentity()) {
            params = path.params.slice(0);
            path.transform(attr.matrix);
        }
        result = path.isPointInPath(x, y);
        if (params) {
            path.params = params;
        }
        return result;
    },
    /**
     * Tests whether the given point is on the path.
     * @param x
     * @param y
     * @return {Boolean}
     * @member Ext.draw.sprite.Path
     */
    isPointOnPath: function(x, y) {
        var attr = this.attr,
            path = attr.path,
            matrix = attr.matrix,
            params, result;
        if (!matrix.isIdentity()) {
            params = path.params.slice(0);
            path.transform(attr.matrix);
        }
        result = path.isPointOnPath(x, y);
        if (params) {
            path.params = params;
        }
        return result;
    },
    /**
     * @method hitTest
     * @inheritdoc Ext.draw.Surface#method-hitTest
     */
    hitTest: function(point, options) {
        var me = this,
            attr = me.attr,
            path = attr.path,
            matrix = attr.matrix,
            x = point[0],
            y = point[1],
            parentResult = me.callParent([
                point,
                options
            ]),
            result = null,
            params, isFilled;
        if (!parentResult) {
            // The sprite is not visible or bounding box wasn't hit.
            return result;
        }
        options = options || Ext.draw.sprite.Sprite.defaultHitTestOptions;
        if (!matrix.isIdentity()) {
            params = path.params.slice(0);
            path.transform(attr.matrix);
        }
        if (options.fill && options.stroke) {
            isFilled = attr.fillStyle !== Ext.util.Color.NONE && attr.fillStyle !== Ext.util.Color.RGBA_NONE;
            if (isFilled) {
                if (path.isPointInPath(x, y)) {
                    result = {
                        sprite: me
                    };
                }
            } else {
                if (path.isPointInPath(x, y) || path.isPointOnPath(x, y)) {
                    result = {
                        sprite: me
                    };
                }
            }
        } else if (options.stroke && !options.fill) {
            if (path.isPointOnPath(x, y)) {
                result = {
                    sprite: me
                };
            }
        } else if (options.fill && !options.stroke) {
            if (path.isPointInPath(x, y)) {
                result = {
                    sprite: me
                };
            }
        }
        if (params) {
            path.params = params;
        }
        return result;
    },
    /**
     * Returns all points where this sprite intersects the given sprite.
     * The given sprite must be an instance of the {@link Ext.draw.sprite.Path} class
     * or its subclass.
     * @param path
     * @return {Array}
     * @member Ext.draw.sprite.Path
     */
    getIntersections: function(path) {
        if (!(path.isSprite && path.isPath)) {
            return [];
        }
        // eslint-disable-next-line vars-on-top
        var aAttr = this.attr,
            bAttr = path.attr,
            aPath = aAttr.path,
            bPath = bAttr.path,
            aMatrix = aAttr.matrix,
            bMatrix = bAttr.matrix,
            aParams, bParams, intersections;
        if (!aMatrix.isIdentity()) {
            aParams = aPath.params.slice(0);
            aPath.transform(aAttr.matrix);
        }
        if (!bMatrix.isIdentity()) {
            bParams = bPath.params.slice(0);
            bPath.transform(bAttr.matrix);
        }
        intersections = aPath.getIntersections(bPath);
        if (aParams) {
            aPath.params = aParams;
        }
        if (bParams) {
            bPath.params = bParams;
        }
        return intersections;
    }
});

/**
 * @class Ext.draw.sprite.Circle
 * @extends Ext.draw.sprite.Path
 *
 * A sprite that represents a circle.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'circle',
 *            cx: 100,
 *            cy: 100,
 *            r: 50,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Circle', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.circle',
    type: 'circle',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
                 */
                cx: 'number',
                /**
                 * @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
                 */
                cy: 'number',
                /**
                 * @cfg {Number} [r=0] The radius of the sprite.
                 */
                r: 'number'
            },
            aliases: {
                radius: 'r',
                x: 'cx',
                y: 'cy',
                centerX: 'cx',
                centerY: 'cy'
            },
            defaults: {
                cx: 0,
                cy: 0,
                r: 4
            },
            triggers: {
                cx: 'path',
                cy: 'path',
                r: 'path'
            }
        }
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr,
            cx = attr.cx,
            cy = attr.cy,
            r = attr.r;
        plain.x = cx - r;
        plain.y = cy - r;
        plain.width = r + r;
        plain.height = r + r;
    },
    updateTransformedBBox: function(transform) {
        var attr = this.attr,
            cx = attr.cx,
            cy = attr.cy,
            r = attr.r,
            matrix = attr.matrix,
            scaleX = matrix.getScaleX(),
            scaleY = matrix.getScaleY(),
            rx, ry;
        rx = scaleX * r;
        ry = scaleY * r;
        transform.x = matrix.x(cx, cy) - rx;
        transform.y = matrix.y(cx, cy) - ry;
        transform.width = rx + rx;
        transform.height = ry + ry;
    },
    updatePath: function(path, attr) {
        path.arc(attr.cx, attr.cy, attr.r, 0, Math.PI * 2, false);
    }
});

/**
 * @class Ext.draw.sprite.Arc
 * @extend Ext.draw.sprite.Circle
 * 
 *  A sprite that represents a circular arc.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'arc',
 *            cx: 100,
 *            cy: 100,
 *            r: 80,
 *            fillStyle: '#1F6D91',
 *            startAngle: 0,
 *            endAngle: Math.PI,
 *            anticlockwise: true
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Arc', {
    extend: 'Ext.draw.sprite.Circle',
    alias: 'sprite.arc',
    type: 'arc',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [startAngle=0] The beginning angle of the arc.
                 */
                startAngle: 'number',
                /**
                 * @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
                 */
                endAngle: 'number',
                /**
                 * @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc is drawn
                 * clockwise.
                 */
                anticlockwise: 'bool'
            },
            aliases: {
                from: 'startAngle',
                to: 'endAngle',
                start: 'startAngle',
                end: 'endAngle'
            },
            defaults: {
                startAngle: 0,
                endAngle: Math.PI * 2,
                anticlockwise: false
            },
            triggers: {
                startAngle: 'path',
                endAngle: 'path',
                anticlockwise: 'path'
            }
        }
    },
    updatePath: function(path, attr) {
        path.arc(attr.cx, attr.cy, attr.r, attr.startAngle, attr.endAngle, attr.anticlockwise);
    }
});

/**
 * A sprite that represents an arrow.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'arrow',
 *            translationX: 100,
 *            translationY: 100,
 *            size: 40,
 *            fillStyle: '#30BDA7'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Arrow', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.arrow',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                /**
                 * @cfg {Number} [size=4] The size of the sprite.
                 * Meant to be comparable to the size of a circle sprite with the same radius.
                 */
                size: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                size: 4
            },
            triggers: {
                x: 'path',
                y: 'path',
                size: 'path'
            }
        }
    },
    updatePath: function(path, attr) {
        var s = attr.size * 1.5,
            x = attr.x - attr.lineWidth / 2,
            y = attr.y;
        path.fromSvgString('M'.concat(x - s * 0.7, ',', y - s * 0.4, 'l', [
            s * 0.6,
            0,
            0,
            -s * 0.4,
            s,
            s * 0.8,
            -s,
            s * 0.8,
            0,
            -s * 0.4,
            -s * 0.6,
            0
        ], 'z'));
    }
});

/**
 * @class Ext.draw.sprite.Composite
 * 
 * Represents a group of sprites.
 * Composite's sprites are rendered in the order they've been added to the Composite.
 * The rendering order of composite sprites themselves is determined by the value of
 * their zIndex attribute, just like with any other sprite.
 * Every sprite that is added to the Composite is removed from whatever Surface/Composite
 * it belongs to.
 */
Ext.define('Ext.draw.sprite.Composite', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.composite',
    type: 'composite',
    isComposite: true,
    config: {
        sprites: []
    },
    constructor: function(config) {
        this.sprites = [];
        this.map = {};
        this.callParent([
            config
        ]);
    },
    /**
     * Adds sprite(s) to the composite.
     * @param {Ext.draw.sprite.Sprite/Ext.draw.sprite.Sprite[]/Object/Object[]} sprite
     * @return {Ext.draw.sprite.Sprite/Ext.draw.sprite.Sprite[]}
     */
    addSprite: function(sprite) {
        var i = 0,
            attr, results, oldTransformations;
        if (Ext.isArray(sprite)) {
            results = [];
            while (i < sprite.length) {
                results.push(this.addSprite(sprite[i++]));
            }
            return results;
        }
        if (sprite && sprite.type && !sprite.isSprite) {
            sprite = Ext.create('sprite.' + sprite.type, sprite);
        }
        if (!sprite || !sprite.isSprite || sprite.isComposite) {
            return null;
        }
        sprite.setSurface(null);
        sprite.setParent(this);
        attr = this.attr;
        oldTransformations = sprite.applyTransformations;
        sprite.applyTransformations = function(force) {
            if (sprite.attr.dirtyTransform) {
                attr.dirtyTransform = true;
                attr.bbox.plain.dirty = true;
                attr.bbox.transform.dirty = true;
            }
            oldTransformations.call(sprite, force);
        };
        this.sprites.push(sprite);
        this.map[sprite.id] = sprite.getId();
        attr.bbox.plain.dirty = true;
        attr.bbox.transform.dirty = true;
        return sprite;
    },
    /**
     * @deprecated 6.2.1 Use {@link #addSprite} instead.
     */
    add: function(sprite) {
        return this.addSprite(sprite);
    },
    removeSprite: function(sprite, isDestroy) {
        var me = this,
            id, isOwnSprite;
        if (sprite) {
            if (sprite.charAt) {
                // is String
                sprite = me.map[sprite];
            }
            if (!sprite || !sprite.isSprite) {
                return null;
            }
            if (sprite.destroyed || sprite.destroying) {
                return sprite;
            }
            id = sprite.getId();
            isOwnSprite = me.map[id];
            delete me.map[id];
            if (isDestroy) {
                sprite.destroy();
            }
            if (!isOwnSprite) {
                return sprite;
            }
            sprite.setParent(null);
            // sprite.setSurface(null);
            Ext.Array.remove(me.sprites, sprite);
            me.dirtyZIndex = true;
            me.setDirty(true);
        }
        return sprite || null;
    },
    /**
     * @deprecated 6.2.1 Use {@link #addSprite} instead.
     * Adds a list of sprites to the composite.
     * @param {Ext.draw.sprite.Sprite[]|Object[]|Ext.draw.sprite.Sprite|Object} sprites
     */
    addAll: function(sprites) {
        var i = 0;
        if (sprites.isSprite || sprites.type) {
            this.add(sprites);
        } else if (Ext.isArray(sprites)) {
            while (i < sprites.length) {
                this.add(sprites[i++]);
            }
        }
    },
    /**
     * Updates the bounding box of the composite, which contains the bounding box of all sprites
     * in the composite.
     */
    updatePlainBBox: function(plain) {
        var me = this,
            left = Infinity,
            right = -Infinity,
            top = Infinity,
            bottom = -Infinity,
            sprite, bbox, i, ln;
        for (i = 0 , ln = me.sprites.length; i < ln; i++) {
            sprite = me.sprites[i];
            sprite.applyTransformations();
            bbox = sprite.getBBox();
            if (left > bbox.x) {
                left = bbox.x;
            }
            if (right < bbox.x + bbox.width) {
                right = bbox.x + bbox.width;
            }
            if (top > bbox.y) {
                top = bbox.y;
            }
            if (bottom < bbox.y + bbox.height) {
                bottom = bbox.y + bbox.height;
            }
        }
        plain.x = left;
        plain.y = top;
        plain.width = right - left;
        plain.height = bottom - top;
    },
    isVisible: function() {
        // Override the abstract Sprite's method.
        // Composite uses a simpler check, because it has no fill or stroke
        // style of its own, it just houses other sprites.
        var attr = this.attr,
            parent = this.getParent(),
            hasParent = parent && (parent.isSurface || parent.isVisible()),
            isSeen = hasParent && !attr.hidden && attr.globalAlpha;
        return !!isSeen;
    },
    /**
     * Renders all sprites contained in the composite to the surface.
     */
    render: function(surface, ctx, rect) {
        var me = this,
            attr = me.attr,
            mat = me.attr.matrix,
            sprites = me.sprites,
            ln = sprites.length,
            i = 0;
        mat.toContext(ctx);
        for (; i < ln; i++) {
            surface.renderSprite(sprites[i], rect);
        }
        //<debug>
        // eslint-disable-next-line vars-on-top
        var debug = attr.debug || me.statics().debug || Ext.draw.sprite.Sprite.debug;
        if (debug) {
            attr.inverseMatrix.toContext(ctx);
            if (debug.bbox) {
                me.renderBBox(surface, ctx);
            }
        }
    },
    //</debug>
    destroy: function() {
        var me = this,
            sprites = me.sprites,
            ln = sprites.length,
            i;
        for (i = 0; i < ln; i++) {
            sprites[i].destroy();
        }
        sprites.length = 0;
        me.callParent();
    }
});

/**
 * A sprite that represents a cross.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'cross',
 *            translationX: 100,
 *            translationY: 100,
 *            size: 40,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Cross', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.cross',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                /**
                 * @cfg {Number} [size=4] The size of the sprite.
                 * Meant to be comparable to the size of a circle sprite with the same radius.
                 */
                size: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                size: 4
            },
            triggers: {
                x: 'path',
                y: 'path',
                size: 'path'
            }
        }
    },
    updatePath: function(path, attr) {
        var s = attr.size / 1.7,
            x = attr.x - attr.lineWidth / 2,
            y = attr.y;
        path.fromSvgString('M'.concat(x - s, ',', y, 'l', [
            -s,
            -s,
            s,
            -s,
            s,
            s,
            s,
            -s,
            s,
            s,
            -s,
            s,
            s,
            s,
            -s,
            s,
            -s,
            -s,
            -s,
            s,
            -s,
            -s,
            'z'
        ]));
    }
});

/**
 * A sprite that represents a diamond.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'diamond',
 *            translationX: 100,
 *            translationY: 100,
 *            size: 40,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Diamond', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.diamond',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                /**
                 * @cfg {Number} [size=4] The size of the sprite.
                 * Meant to be comparable to the size of a circle sprite with the same radius.
                 */
                size: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                size: 4
            },
            triggers: {
                x: 'path',
                y: 'path',
                size: 'path'
            }
        }
    },
    updatePath: function(path, attr) {
        var s = attr.size * 1.25,
            x = attr.x - attr.lineWidth / 2,
            y = attr.y;
        path.fromSvgString([
            'M',
            x,
            y - s,
            'l',
            s,
            s,
            -s,
            s,
            -s,
            -s,
            s,
            -s,
            'z'
        ]);
    }
});

/**
 * @class Ext.draw.sprite.Ellipse
 * @extends Ext.draw.sprite.Path
 * 
 * A sprite that represents an ellipse.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'ellipse',
 *            cx: 100,
 *            cy: 100,
 *            rx: 80,
 *            ry: 50,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define("Ext.draw.sprite.Ellipse", {
    extend: "Ext.draw.sprite.Path",
    alias: 'sprite.ellipse',
    type: 'ellipse',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
                 */
                cx: "number",
                /**
                 * @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
                 */
                cy: "number",
                /**
                 * @cfg {Number} [rx=1] The radius of the sprite on the x-axis.
                 */
                rx: "number",
                /**
                 * @cfg {Number} [ry=1] The radius of the sprite on the y-axis.
                 */
                ry: "number",
                /**
                 * @cfg {Number} [axisRotation=0] The rotation of the sprite about its axis.
                 */
                axisRotation: "number"
            },
            aliases: {
                radius: "r",
                x: "cx",
                y: "cy",
                centerX: "cx",
                centerY: "cy",
                radiusX: "rx",
                radiusY: "ry"
            },
            defaults: {
                cx: 0,
                cy: 0,
                rx: 1,
                ry: 1,
                axisRotation: 0
            },
            triggers: {
                cx: 'path',
                cy: 'path',
                rx: 'path',
                ry: 'path',
                axisRotation: 'path'
            }
        }
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr,
            cx = attr.cx,
            cy = attr.cy,
            rx = attr.rx,
            ry = attr.ry;
        plain.x = cx - rx;
        plain.y = cy - ry;
        plain.width = rx + rx;
        plain.height = ry + ry;
    },
    updateTransformedBBox: function(transform) {
        var attr = this.attr,
            cx = attr.cx,
            cy = attr.cy,
            rx = attr.rx,
            ry = attr.ry,
            rxy = ry / rx,
            matrix = attr.matrix.clone(),
            xx, xy, yx, yy, dx, dy, w, h;
        matrix.append(1, 0, 0, rxy, 0, cy * (1 - rxy));
        xx = matrix.getXX();
        yx = matrix.getYX();
        dx = matrix.getDX();
        xy = matrix.getXY();
        yy = matrix.getYY();
        dy = matrix.getDY();
        w = Math.sqrt(xx * xx + yx * yx) * rx;
        h = Math.sqrt(xy * xy + yy * yy) * rx;
        transform.x = cx * xx + cy * yx + dx - w;
        transform.y = cx * xy + cy * yy + dy - h;
        transform.width = w + w;
        transform.height = h + h;
    },
    updatePath: function(path, attr) {
        path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, 0, Math.PI * 2, false);
    }
});

/**
 * @class Ext.draw.sprite.EllipticalArc
 * @extends Ext.draw.sprite.Ellipse
 *
 * A sprite that represents an elliptical arc.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'ellipticalArc',
 *            cx: 100,
 *            cy: 100,
 *            rx: 80,
 *            ry: 50,
 *            fillStyle: '#1F6D91',
 *            startAngle: 0,
 *            endAngle: Math.PI,
 *            anticlockwise: true
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.EllipticalArc', {
    extend: 'Ext.draw.sprite.Ellipse',
    alias: 'sprite.ellipticalArc',
    type: 'ellipticalArc',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [startAngle=0] The beginning angle of the arc.
                 */
                startAngle: 'number',
                /**
                 * @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
                 */
                endAngle: 'number',
                /**
                 * @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc
                 * is drawn clockwise.
                 */
                anticlockwise: 'bool'
            },
            aliases: {
                from: 'startAngle',
                to: 'endAngle',
                start: 'startAngle',
                end: 'endAngle'
            },
            defaults: {
                startAngle: 0,
                endAngle: Math.PI * 2,
                anticlockwise: false
            },
            triggers: {
                startAngle: 'path',
                endAngle: 'path',
                anticlockwise: 'path'
            }
        }
    },
    updatePath: function(path, attr) {
        path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, attr.startAngle, attr.endAngle, attr.anticlockwise);
    }
});

/**
 * @class Ext.draw.sprite.Rect
 * @extends Ext.draw.sprite.Path
 *
 * A sprite that represents a rectangle.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'rect',
 *            x: 50,
 *            y: 50,
 *            width: 100,
 *            height: 100,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Rect', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.rect',
    type: 'rect',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [x=0] The position of the sprite on the x-axis.
                 */
                x: 'number',
                /**
                 * @cfg {Number} [y=0] The position of the sprite on the y-axis.
                 */
                y: 'number',
                /**
                 * @cfg {Number} [width=8] The width of the sprite.
                 */
                width: 'number',
                /**
                 * @cfg {Number} [height=8] The height of the sprite.
                 */
                height: 'number',
                /**
                 * @cfg {Number} [radius=0] The radius of the rounded corners.
                 */
                radius: 'number'
            },
            aliases: {},
            triggers: {
                x: 'path',
                y: 'path',
                width: 'path',
                height: 'path',
                radius: 'path'
            },
            defaults: {
                x: 0,
                y: 0,
                width: 8,
                height: 8,
                radius: 0
            }
        }
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr;
        plain.x = attr.x;
        plain.y = attr.y;
        plain.width = attr.width;
        plain.height = attr.height;
    },
    updateTransformedBBox: function(transform, plain) {
        this.attr.matrix.transformBBox(plain, this.attr.radius, transform);
    },
    updatePath: function(path, attr) {
        var x = attr.x,
            y = attr.y,
            width = attr.width,
            height = attr.height,
            radius = Math.min(attr.radius, Math.abs(height) * 0.5, Math.abs(width) * 0.5);
        if (radius === 0) {
            path.rect(x, y, width, height);
        } else {
            path.moveTo(x + radius, y);
            path.arcTo(x + width, y, x + width, y + height, radius);
            path.arcTo(x + width, y + height, x, y + height, radius);
            path.arcTo(x, y + height, x, y, radius);
            path.arcTo(x, y, x + radius, y, radius);
            path.closePath();
        }
    }
});

/**
 * @class Ext.draw.sprite.Image
 * @extends Ext.draw.sprite.Rect
 *
 * A sprite that represents an image.
 */
Ext.define('Ext.draw.sprite.Image', {
    extend: 'Ext.draw.sprite.Rect',
    alias: 'sprite.image',
    type: 'image',
    statics: {
        imageLoaders: {}
    },
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {String} [src=''] The image source of the sprite.
                 */
                src: 'string'
            },
            /**
                 * @private
                 * @cfg {Number} radius
                 */
            triggers: {
                src: 'src'
            },
            updaters: {
                src: 'updateSource'
            },
            defaults: {
                src: '',
                /**
                 * @cfg {Number} [width=null] The width of the image.
                 * For consistent image size on all devices the width must be explicitly set.
                 * Otherwise the natural image width devided by the device pixel ratio
                 * (for a crisp looking image) will be used as the width of the sprite.
                 */
                width: null,
                /**
                 * @cfg {Number} [height=null] The height of the image.
                 * For consistent image size on all devices the height must be explicitly set.
                 * Otherwise the natural image height devided by the device pixel ratio
                 * (for a crisp looking image) will be used as the height of the sprite.
                 */
                height: null
            }
        }
    },
    updateSurface: function(surface) {
        if (surface) {
            this.updateSource(this.attr);
        }
    },
    updateSource: function(attr) {
        var me = this,
            src = attr.src,
            surface = me.getSurface(),
            loadingStub = Ext.draw.sprite.Image.imageLoaders[src],
            width = attr.width,
            height = attr.height,
            imageLoader, i;
        if (!surface) {
            // First time this is called the sprite won't have a surface yet.
            return;
        }
        if (!loadingStub) {
            imageLoader = new Image();
            loadingStub = Ext.draw.sprite.Image.imageLoaders[src] = {
                image: imageLoader,
                done: false,
                pendingSprites: [
                    me
                ],
                pendingSurfaces: [
                    surface
                ]
            };
            imageLoader.width = width;
            imageLoader.height = height;
            imageLoader.onload = function() {
                var item;
                if (!loadingStub.done) {
                    loadingStub.done = true;
                    for (i = 0; i < loadingStub.pendingSprites.length; i++) {
                        item = loadingStub.pendingSprites[i];
                        if (!item.destroyed) {
                            item.setDirty(true);
                        }
                    }
                    for (i = 0; i < loadingStub.pendingSurfaces.length; i++) {
                        item = loadingStub.pendingSurfaces[i];
                        if (!item.destroyed) {
                            item.renderFrame();
                        }
                    }
                }
            };
            imageLoader.src = src;
        } else {
            Ext.Array.include(loadingStub.pendingSprites, me);
            Ext.Array.include(loadingStub.pendingSurfaces, surface);
        }
    },
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            mat = attr.matrix,
            src = attr.src,
            x = attr.x,
            y = attr.y,
            width = attr.width,
            height = attr.height,
            loadingStub = Ext.draw.sprite.Image.imageLoaders[src],
            image;
        if (loadingStub && loadingStub.done) {
            mat.toContext(ctx);
            image = loadingStub.image;
            ctx.drawImage(image, x, y, width || (image.naturalWidth || image.width) / surface.devicePixelRatio, height || (image.naturalHeight || image.height) / surface.devicePixelRatio);
        }
        //<debug>
        // eslint-disable-next-line vars-on-top
        var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug;
        if (debug && debug.bbox) {
            this.renderBBox(surface, ctx);
        }
    },
    //</debug>
    /**
     * @private
     */
    isVisible: function() {
        var attr = this.attr,
            parent = this.getParent(),
            hasParent = parent && (parent.isSurface || parent.isVisible()),
            isSeen = hasParent && !attr.hidden && attr.globalAlpha;
        return !!isSeen;
    }
});

/**
 * @class Ext.draw.sprite.Instancing
 * @extends Ext.draw.sprite.Sprite
 *
 * Sprite that represents multiple instances based on the given template.
 */
Ext.define('Ext.draw.sprite.Instancing', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.instancing',
    type: 'instancing',
    isInstancing: true,
    config: {
        /**
         * @cfg {Object} [template] The sprite template used by all instances.
         */
        template: null,
        /**
         * @cfg {Array} [instances]
         * The instances of the {@link #template} sprite as configs of attributes.
         */
        instances: null
    },
    instances: null,
    applyTemplate: function(template) {
        var surface;
        //<debug>
        if (!Ext.isObject(template)) {
            Ext.raise("A template of an instancing sprite must either be " + "a sprite instance or a valid config object from which a template " + "sprite will be created.");
        } else if (template.isInstancing || template.isComposite) {
            Ext.raise("Can't use an instancing or composite sprite " + "as a template for an instancing sprite.");
        }
        //</debug>
        if (!template.isSprite) {
            if (!template.xclass && !template.type) {
                // For compatibility with legacy charts.
                template.type = 'circle';
            }
            template = Ext.create(template.xclass || 'sprite.' + template.type, template);
        }
        surface = template.getSurface();
        if (surface) {
            surface.remove(template);
        }
        template.setParent(this);
        return template;
    },
    updateTemplate: function(template, oldTemplate) {
        if (oldTemplate) {
            delete oldTemplate.ownAttr;
        }
        template.setSurface(this.getSurface());
        // ownAttr is used to get a reference to the template's attributes
        // when one of the instances is rendering, as at that moment the template's
        // attributes (template.attr) are the instance's attributes.
        template.ownAttr = template.attr;
        this.clearAll();
        this.setDirty(true);
    },
    updateInstances: function(instances) {
        var i, ln;
        this.clearAll();
        if (Ext.isArray(instances)) {
            for (i = 0 , ln = instances.length; i < ln; i++) {
                this.add(instances[i]);
            }
        }
    },
    updateSurface: function(surface) {
        var template = this.getTemplate();
        if (template && !template.destroyed) {
            template.setSurface(surface);
        }
    },
    get: function(index) {
        return this.instances[index];
    },
    getCount: function() {
        return this.instances.length;
    },
    clearAll: function() {
        var template = this.getTemplate();
        template.attr.children = this.instances = [];
        this.position = 0;
    },
    /**
     * @deprecated 6.2.0
     * Deprecated, use the {@link #add} method instead.
     */
    createInstance: function(config, bypassNormalization, avoidCopy) {
        return this.add(config, bypassNormalization, avoidCopy);
    },
    /**
     * Creates a new sprite instance.
     *
     * @param {Object} config The configuration of the instance.
     * @param {Boolean} [bypassNormalization] 'true' to bypass attribute normalization.
     * @param {Boolean} [avoidCopy] 'true' to avoid copying the `config` object.
     * @return {Object} The attributes of the instance.
     */
    add: function(config, bypassNormalization, avoidCopy) {
        var me = this,
            template = me.getTemplate(),
            originalAttr = template.attr,
            attr = Ext.Object.chain(originalAttr);
        template.modifiers.target.prepareAttributes(attr);
        template.attr = attr;
        template.setAttributes(config, bypassNormalization, avoidCopy);
        attr.template = template;
        me.instances.push(attr);
        template.attr = originalAttr;
        me.position++;
        return attr;
    },
    /**
     * Not supported.
     * 
     * @return {null}
     */
    getBBox: function() {
        return null;
    },
    /**
     * Returns the bounding box for the instance at the given index.
     *
     * @param {Number} index The index of the instance.
     * @param {Boolean} [isWithoutTransform] 'true' to not apply sprite transforms
     * to the bounding box.
     * @return {Object} The bounding box for the instance.
     */
    getBBoxFor: function(index, isWithoutTransform) {
        var template = this.getTemplate(),
            originalAttr = template.attr,
            bbox;
        template.attr = this.instances[index];
        bbox = template.getBBox(isWithoutTransform);
        template.attr = originalAttr;
        return bbox;
    },
    /**
     * @private
     * Checks if the instancing sprite can be seen.
     * @return {Boolean}
     */
    isVisible: function() {
        var attr = this.attr,
            parent = this.getParent(),
            result;
        result = parent && parent.isSurface && !attr.hidden && attr.globalAlpha;
        return !!result;
    },
    /**
     * @private
     * Checks if the instance of an instancing sprite can be seen.
     * @param {Number} index The index of the instance.
     */
    isInstanceVisible: function(index) {
        var me = this,
            template = me.getTemplate(),
            originalAttr = template.attr,
            instances = me.instances,
            result = false;
        if (!Ext.isNumber(index) || index < 0 || index >= instances.length || !me.isVisible()) {
            return result;
        }
        template.attr = instances[index];
        // TODO This is clearly a bug, fix it
        // eslint-disable-next-line no-undef
        result = template.isVisible(point, options);
        template.attr = originalAttr;
        return result;
    },
    render: function(surface, ctx, rect) {
        //<debug>
        if (!this.getTemplate()) {
            Ext.raise('An instancing sprite must have a template.');
        }
        //</debug>
        // eslint-disable-next-line vars-on-top
        var me = this,
            template = me.getTemplate(),
            surfaceRect = surface.getRect(),
            mat = me.attr.matrix,
            originalAttr = template.attr,
            instances = me.instances,
            ln = me.position,
            i;
        mat.toContext(ctx);
        template.preRender(surface, ctx, rect);
        template.useAttributes(ctx, surfaceRect);
        template.isSpriteInstance = true;
        for (i = 0; i < ln; i++) {
            if (instances[i].hidden) {
                
                continue;
            }
            ctx.save();
            template.attr = instances[i];
            template.useAttributes(ctx, surfaceRect);
            template.render(surface, ctx, rect);
            ctx.restore();
        }
        template.isSpriteInstance = false;
        template.attr = originalAttr;
    },
    /**
     * Sets the attributes for the instance at the given index.
     * 
     * @param {Number} index the index of the instance
     * @param {Object} changes the attributes to change
     * @param {Boolean} [bypassNormalization] 'true' to avoid attribute normalization
     */
    setAttributesFor: function(index, changes, bypassNormalization) {
        var template = this.getTemplate(),
            originalAttr = template.attr,
            attr = this.instances[index];
        if (!attr) {
            return;
        }
        template.attr = attr;
        if (bypassNormalization) {
            changes = Ext.apply({}, changes);
        } else {
            changes = template.self.def.normalize(changes);
        }
        template.modifiers.target.pushDown(attr, changes);
        template.attr = originalAttr;
    },
    destroy: function() {
        var me = this,
            template = me.getTemplate();
        me.instances = null;
        if (template) {
            template.destroy();
        }
        me.callParent();
    }
});

/**
 * @private
 * Adds hit testing methods to the Ext.draw.sprite.Instancing.
 * Included by the Ext.draw.plugin.SpriteEvents.
 */
Ext.define('Ext.draw.overrides.hittest.sprite.Instancing', {
    override: 'Ext.draw.sprite.Instancing',
    /**
     * Performs a hit test on the instances of an instancing sprite.
     * @param point A two-item array containing x and y coordinates of the point.
     * @param options Hit testing options.
     * @return {Object} A hit result object that contains more information about what
     * exactly was hit or null if nothing was hit.
     * @return {Boolean} return.isInstance `true` if an instance was hit.
     * @return {Ext.draw.sprite.Instancing} return.sprite The instancing sprite.
     * @return {Ext.draw.sprite.Sprite} return.template The template of the instancing sprite.
     * @return {Object} return.instance The attributes of the instance.
     * @return {Number} return.index The index of the instance.
     */
    hitTest: function(point, options) {
        var me = this,
            template = me.getTemplate(),
            originalAttr = template.attr,
            instances = me.instances,
            ln = instances.length,
            i = 0,
            result = null;
        if (!me.isVisible()) {
            return result;
        }
        for (; i < ln; i++) {
            template.attr = instances[i];
            result = template.hitTest(point, options);
            if (result) {
                result.isInstance = true;
                result.template = result.sprite;
                result.sprite = this;
                result.instance = instances[i];
                result.index = i;
                return result;
            }
        }
        template.attr = originalAttr;
        return result;
    }
});

/**
 * A sprite that represents a line.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw',
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'line',
 *            fromX: 20,
 *            fromY: 20,
 *            toX: 120,
 *            toY: 120,
 *            strokeStyle: '#1F6D91',
 *            lineWidth: 3
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Line', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.line',
    type: 'line',
    inheritableStatics: {
        def: {
            processors: {
                fromX: 'number',
                fromY: 'number',
                toX: 'number',
                toY: 'number',
                crisp: 'bool'
            },
            defaults: {
                fromX: 0,
                fromY: 0,
                toX: 1,
                toY: 1,
                crisp: false,
                strokeStyle: 'black'
            },
            aliases: {
                x1: 'fromX',
                y1: 'fromY',
                x2: 'toX',
                y2: 'toY'
            },
            triggers: {
                crisp: 'bbox'
            }
        }
    },
    updateLineBBox: function(bbox, isTransform, x1, y1, x2, y2) {
        var attr = this.attr,
            matrix = attr.matrix,
            halfLineWidth = attr.lineWidth / 2,
            fromX, fromY, toX, toY, p, angle, sin, cos, dx, dy;
        if (attr.crisp) {
            x1 = this.align(x1);
            x2 = this.align(x2);
            y1 = this.align(y1);
            y2 = this.align(y2);
        }
        if (isTransform) {
            p = matrix.transformPoint([
                x1,
                y1
            ]);
            x1 = p[0];
            y1 = p[1];
            p = matrix.transformPoint([
                x2,
                y2
            ]);
            x2 = p[0];
            y2 = p[1];
        }
        fromX = Math.min(x1, x2);
        toX = Math.max(x1, x2);
        fromY = Math.min(y1, y2);
        toY = Math.max(y1, y2);
        angle = Math.atan2(toX - fromX, toY - fromY);
        sin = Math.sin(angle);
        cos = Math.cos(angle);
        dx = halfLineWidth * cos;
        dy = halfLineWidth * sin;
        // Offset start and end points of the line by half its thickness,
        // while accounting for line's angle.
        fromX -= dx;
        fromY -= dy;
        toX += dx;
        toY += dy;
        bbox.x = fromX;
        bbox.y = fromY;
        bbox.width = toX - fromX;
        bbox.height = toY - fromY;
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr;
        this.updateLineBBox(plain, false, attr.fromX, attr.fromY, attr.toX, attr.toY);
    },
    updateTransformedBBox: function(transform, plain) {
        var attr = this.attr;
        this.updateLineBBox(transform, true, attr.fromX, attr.fromY, attr.toX, attr.toY);
    },
    align: function(x) {
        return Math.round(x) - 0.5;
    },
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            matrix = attr.matrix;
        matrix.toContext(ctx);
        ctx.beginPath();
        if (attr.crisp) {
            ctx.moveTo(me.align(attr.fromX), me.align(attr.fromY));
            ctx.lineTo(me.align(attr.toX), me.align(attr.toY));
        } else {
            ctx.moveTo(attr.fromX, attr.fromY);
            ctx.lineTo(attr.toX, attr.toY);
        }
        ctx.stroke();
        //<debug>
        // eslint-disable-next-line vars-on-top
        var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug;
        if (debug) {
            // This assumes no part of the sprite is rendered after this call.
            // If it is, we need to re-apply transformations.
            // But the bounding box should always be rendered as is, untransformed.
            this.attr.inverseMatrix.toContext(ctx);
            if (debug.bbox) {
                this.renderBBox(surface, ctx);
            }
        }
    }
});
//</debug>

/**
 * A sprite that represents a plus.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'plus',
 *            translationX: 100,
 *            translationY: 100,
 *            size: 40,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Plus', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.plus',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                /**
                 * @cfg {Number} [size=4] The size of the sprite.
                 * Meant to be comparable to the size of a circle sprite with the same radius.
                 */
                size: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                size: 4
            },
            triggers: {
                x: 'path',
                y: 'path',
                size: 'path'
            }
        }
    },
    updatePath: function(path, attr) {
        var s = attr.size / 1.3,
            x = attr.x - attr.lineWidth / 2,
            y = attr.y;
        path.fromSvgString('M'.concat(x - s / 2, ',', y - s / 2, 'l', [
            0,
            -s,
            s,
            0,
            0,
            s,
            s,
            0,
            0,
            s,
            -s,
            0,
            0,
            s,
            -s,
            0,
            0,
            -s,
            -s,
            0,
            0,
            -s,
            'z'
        ]));
    }
});

/**
 * @class Ext.draw.sprite.Sector
 * @extends Ext.draw.sprite.Path
 * 
 * A sprite representing a pie slice.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'sector',
 *            centerX: 100,
 *            centerY: 100,
 *            startAngle: -2.355,
 *            endAngle: -.785,
 *            endRho: 50,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Sector', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.sector',
    type: 'sector',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [centerX=0] The center coordinate of the sprite on the x-axis.
                 */
                centerX: 'number',
                /**
                 * @cfg {Number} [centerY=0] The center coordinate of the sprite on the y-axis.
                 */
                centerY: 'number',
                /**
                 * @cfg {Number} [startAngle=0] The starting angle of the sprite.
                 */
                startAngle: 'number',
                /**
                 * @cfg {Number} [endAngle=0] The ending angle of the sprite.
                 */
                endAngle: 'number',
                /**
                 * @cfg {Number} [startRho=0] The starting point of the radius of the sprite.
                 */
                startRho: 'number',
                /**
                 * @cfg {Number} [endRho=150] The ending point of the radius of the sprite.
                 */
                endRho: 'number',
                /**
                 * @cfg {Number} [margin=0] The margin of the sprite from the center of pie.
                 */
                margin: 'number'
            },
            aliases: {
                rho: 'endRho'
            },
            triggers: {
                centerX: 'path,bbox',
                centerY: 'path,bbox',
                startAngle: 'path,bbox',
                endAngle: 'path,bbox',
                startRho: 'path,bbox',
                endRho: 'path,bbox',
                margin: 'path,bbox'
            },
            defaults: {
                centerX: 0,
                centerY: 0,
                startAngle: 0,
                endAngle: 0,
                startRho: 0,
                endRho: 150,
                margin: 0,
                path: 'M 0,0'
            }
        }
    },
    getMidAngle: function() {
        return this.midAngle || 0;
    },
    updatePath: function(path, attr) {
        var startAngle = Math.min(attr.startAngle, attr.endAngle),
            endAngle = Math.max(attr.startAngle, attr.endAngle),
            midAngle = this.midAngle = (startAngle + endAngle) * 0.5,
            fullPie = Ext.Number.isEqual(Math.abs(endAngle - startAngle), Ext.draw.Draw.pi2, 1.0E-10),
            margin = attr.margin,
            centerX = attr.centerX,
            centerY = attr.centerY,
            startRho = Math.min(attr.startRho, attr.endRho),
            endRho = Math.max(attr.startRho, attr.endRho);
        if (margin) {
            centerX += margin * Math.cos(midAngle);
            centerY += margin * Math.sin(midAngle);
        }
        if (!fullPie) {
            path.moveTo(centerX + startRho * Math.cos(startAngle), centerY + startRho * Math.sin(startAngle));
            path.lineTo(centerX + endRho * Math.cos(startAngle), centerY + endRho * Math.sin(startAngle));
        }
        path.arc(centerX, centerY, endRho, startAngle, endAngle, false);
        path[fullPie ? 'moveTo' : 'lineTo'](centerX + startRho * Math.cos(endAngle), centerY + startRho * Math.sin(endAngle));
        path.arc(centerX, centerY, startRho, endAngle, startAngle, true);
    }
});

/**
 * A sprite that represents a square.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'square',
 *            x: 100,
 *            y: 100,
 *            size: 50,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Square', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.square',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                /**
                 * @cfg {Number} [size=4] The size of the sprite.
                 * Meant to be comparable to the size of a circle sprite with the same radius.
                 */
                size: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                size: 4
            },
            triggers: {
                x: 'path',
                y: 'path',
                size: 'size'
            }
        }
    },
    updatePath: function(path, attr) {
        var size = attr.size * 1.2,
            s = size * 2,
            x = attr.x - attr.lineWidth / 2,
            y = attr.y;
        path.fromSvgString('M'.concat(x - size, ',', y - size, 'l', [
            s,
            0,
            0,
            s,
            -s,
            0,
            0,
            -s,
            'z'
        ]));
    }
});

/**
 * Utility class to provide a way to *approximately* measure the dimension of text
 * without a drawing context.
 */
Ext.define('Ext.draw.TextMeasurer', {
    singleton: true,
    requires: [
        'Ext.util.TextMetrics'
    ],
    measureDiv: null,
    measureCache: {},
    /**
     * @cfg {Boolean} [precise=false]
     * This singleton tries not to make use of the Ext.util.TextMetrics because it is
     * several times slower than TextMeasurer's own solution. TextMetrics is more precise
     * though, so if you have a case where the error is too big, you may want to set
     * this config to `true` to get perfect results at the expense of performance.
     * Note: defaults to `true` in IE8.
     */
    precise: Ext.isIE8,
    measureDivTpl: {
        id: 'ext-draw-text-measurer',
        tag: 'div',
        style: {
            overflow: 'hidden',
            position: 'relative',
            // 'float' is a reserved word. Don't unquote, or it will break the CMD build.
            'float': 'left',
            width: 0,
            height: 0
        },
        //<debug>
        // Tell the spec runner to ignore this element when checking if the dom is clean.
        'data-sticky': true,
        //</debug>
        children: {
            tag: 'div',
            style: {
                display: 'block',
                position: 'absolute',
                x: -100000,
                y: -100000,
                padding: 0,
                margin: 0,
                'z-index': -100000,
                'white-space': 'nowrap'
            }
        }
    },
    /**
     * @private
     * Measure the size of a text with specific font by using DOM to measure it.
     * Could be very expensive therefore should be used lazily.
     * @param {String} text
     * @param {String} font
     * @return {Object} An object with `width` and `height` properties.
     * @return {Number} return.width
     * @return {Number} return.height
     */
    actualMeasureText: function(text, font) {
        var me = Ext.draw.TextMeasurer,
            measureDiv = me.measureDiv,
            FARAWAY = 100000,
            size, parent;
        if (!measureDiv) {
            parent = Ext.Element.create({
                //<debug>
                // Tell the spec runner to ignore this element when checking if the dom is clean.
                'data-sticky': true,
                //</debug>
                style: {
                    "overflow": "hidden",
                    "position": "relative",
                    "float": "left",
                    // DO NOT REMOVE THE QUOTE OR IT WILL BREAK COMPRESSOR
                    "width": 0,
                    "height": 0
                }
            });
            me.measureDiv = measureDiv = Ext.Element.create({
                style: {
                    "position": 'absolute',
                    "x": FARAWAY,
                    "y": FARAWAY,
                    "z-index": -FARAWAY,
                    "white-space": "nowrap",
                    "display": 'block',
                    "padding": 0,
                    "margin": 0
                }
            });
            Ext.getBody().appendChild(parent);
            parent.appendChild(measureDiv);
        }
        if (font) {
            measureDiv.setStyle({
                font: font,
                lineHeight: 'normal'
            });
        }
        measureDiv.setText('(' + text + ')');
        size = measureDiv.getSize();
        measureDiv.setText('()');
        size.width -= measureDiv.getSize().width;
        return size;
    },
    /**
     * Measure a single-line text with specific font.
     * This will split the text into characters and add up their size.
     * That may *not* be the exact size of the text as it is displayed.
     * @param {String} text
     * @param {String} font
     * @return {Object} An object with `width` and `height` properties.
     * @return {Number} return.width
     * @return {Number} return.height
     */
    measureTextSingleLine: function(text, font) {
        var width = 0,
            height = 0,
            cache, cachedItem, chars, charactor, i, ln, size;
        if (this.precise) {
            return this.preciseMeasureTextSingleLine(text, font);
        }
        text = text.toString();
        cache = this.measureCache;
        chars = text.split('');
        if (!cache[font]) {
            cache[font] = {};
        }
        cache = cache[font];
        if (cache[text]) {
            return cache[text];
        }
        for (i = 0 , ln = chars.length; i < ln; i++) {
            charactor = chars[i];
            if (!(cachedItem = cache[charactor])) {
                size = this.actualMeasureText(charactor, font);
                cachedItem = cache[charactor] = size;
            }
            width += cachedItem.width;
            height = Math.max(height, cachedItem.height);
        }
        return cache[text] = {
            width: width,
            height: height
        };
    },
    // A more precise but slower version of the measureTextSingleLine method.
    preciseMeasureTextSingleLine: function(text, font) {
        var measureDiv;
        text = text.toString();
        measureDiv = this.measureDiv || (this.measureDiv = Ext.getBody().createChild(this.measureDivTpl).down('div'));
        measureDiv.setStyle({
            font: font || ''
        });
        return Ext.util.TextMetrics.measure(measureDiv, text);
    },
    /**
     * Measure a text with specific font.
     * This will split the text to lines and add up their size.
     * That may *not* be the exact size of the text as it is displayed.
     * @param {String} text
     * @param {String} font
     * @return {Object} An object with `width`, `height` and `sizes` properties.
     * @return {Number} return.width
     * @return {Number} return.height
     * @return {Object} return.sizes Results of individual line measurements, in case of multiline
     * text.
     */
    measureText: function(text, font) {
        var lines = text.split('\n'),
            ln = lines.length,
            height = 0,
            width = 0,
            line, i, sizes;
        if (ln === 1) {
            return this.measureTextSingleLine(text, font);
        }
        sizes = [];
        for (i = 0; i < ln; i++) {
            line = this.measureTextSingleLine(lines[i], font);
            sizes.push(line);
            height += line.height;
            width = Math.max(width, line.width);
        }
        return {
            width: width,
            height: height,
            sizes: sizes
        };
    }
});

/**
 * @class Ext.draw.sprite.Text
 * @extends Ext.draw.sprite.Sprite
 *
 * A sprite that represents text.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw',
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'text',
 *            x: 50,
 *            y: 50,
 *            text: 'Sencha',
 *            fontSize: 30,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 */
/* eslint-disable indent */
Ext.define('Ext.draw.sprite.Text', function() {
    // Absolute font sizes.
    var fontSizes = {
            'xx-small': true,
            'x-small': true,
            'small': true,
            'medium': true,
            'large': true,
            'x-large': true,
            'xx-large': true
        },
        fontWeights = {
            normal: true,
            bold: true,
            bolder: true,
            lighter: true,
            100: true,
            200: true,
            300: true,
            400: true,
            500: true,
            600: true,
            700: true,
            800: true,
            900: true
        },
        textAlignments = {
            start: 'start',
            left: 'start',
            center: 'center',
            middle: 'center',
            end: 'end',
            right: 'end'
        },
        textBaselines = {
            top: 'top',
            hanging: 'hanging',
            middle: 'middle',
            center: 'middle',
            alphabetic: 'alphabetic',
            ideographic: 'ideographic',
            bottom: 'bottom'
        };
    return {
        extend: 'Ext.draw.sprite.Sprite',
        requires: [
            'Ext.draw.TextMeasurer',
            'Ext.draw.Color'
        ],
        alias: 'sprite.text',
        type: 'text',
        lineBreakRe: /\r?\n/g,
        //<debug>
        statics: {
            /**
         * Debug rendering options:
         *
         * debug: {
         *     bbox: true // renders the bounding box of the text sprite
         * }
         *
         */
            debug: false,
            fontSizes: fontSizes,
            fontWeights: fontWeights,
            textAlignments: textAlignments,
            textBaselines: textBaselines
        },
        //</debug>
        inheritableStatics: {
            def: {
                animationProcessors: {
                    text: 'text'
                },
                processors: {
                    /**
                 * @cfg {Number} [x=0]
                 * The position of the sprite on the x-axis.
                 */
                    x: 'number',
                    /**
                 * @cfg {Number} [y=0]
                 * The position of the sprite on the y-axis.
                 */
                    y: 'number',
                    /**
                 * @cfg {String} [text='']
                 * The text represented in the sprite.
                 */
                    text: 'string',
                    /**
                 * @cfg {String/Number} [fontSize='10px']
                 * The size of the font displayed.
                 */
                    fontSize: function(n) {
                        // Numbers as strings will be converted to numbers,
                        // null will be converted to 0.
                        if (Ext.isNumber(+n)) {
                            return n + 'px';
                        } else if (n.match(Ext.dom.Element.unitRe)) {
                            return n;
                        } else if (n in fontSizes) {
                            return n;
                        }
                    },
                    /**
                 * @cfg {String} [fontStyle='']
                 * The style of the font displayed. {normal, italic, oblique}
                 */
                    fontStyle: 'enums(,italic,oblique)',
                    /**
                 * @cfg {String} [fontVariant='']
                 * The variant of the font displayed. {normal, small-caps}
                 */
                    fontVariant: 'enums(,small-caps)',
                    /**
                 * @cfg {String} [fontWeight='']
                 * The weight of the font displayed. {normal, bold, bolder, lighter}
                 */
                    fontWeight: function(n) {
                        if (n in fontWeights) {
                            return String(n);
                        } else {
                            return '';
                        }
                    },
                    /**
                 * @cfg {String} [fontFamily='sans-serif']
                 * The family of the font displayed.
                 */
                    fontFamily: 'string',
                    /**
                 * @cfg {"left"/"right"/"center"/"start"/"end"} [textAlign='start']
                 * The alignment of the text displayed.
                 */
                    textAlign: function(n) {
                        return textAlignments[n] || 'center';
                    },
                    /**
                 * @cfg {String} [textBaseline="alphabetic"]
                 * The baseline of the text displayed.
                 * {top, hanging, middle, alphabetic, ideographic, bottom}
                 */
                    textBaseline: function(n) {
                        return textBaselines[n] || 'alphabetic';
                    },
                    //<debug>
                    debug: 'default',
                    //</debug>
                    /**
                 * @cfg {String} [font='10px sans-serif']
                 * The font displayed.
                 */
                    font: 'string'
                },
                aliases: {
                    'font-size': 'fontSize',
                    'font-family': 'fontFamily',
                    'font-weight': 'fontWeight',
                    'font-variant': 'fontVariant',
                    'text-anchor': 'textAlign',
                    'dominant-baseline': 'textBaseline'
                },
                defaults: {
                    fontStyle: '',
                    fontVariant: '',
                    fontWeight: '',
                    fontSize: '10px',
                    fontFamily: 'sans-serif',
                    font: '10px sans-serif',
                    textBaseline: 'alphabetic',
                    textAlign: 'start',
                    strokeStyle: 'rgba(0, 0, 0, 0)',
                    fillStyle: '#000',
                    x: 0,
                    y: 0,
                    text: ''
                },
                triggers: {
                    fontStyle: 'fontX,bbox',
                    fontVariant: 'fontX,bbox',
                    fontWeight: 'fontX,bbox',
                    fontSize: 'fontX,bbox',
                    fontFamily: 'fontX,bbox',
                    font: 'font,bbox,canvas',
                    textBaseline: 'bbox',
                    textAlign: 'bbox',
                    x: 'bbox',
                    y: 'bbox',
                    text: 'bbox'
                },
                updaters: {
                    fontX: 'makeFontShorthand',
                    font: 'parseFontShorthand'
                }
            }
        },
        config: {
            /**
         * @private
         * If the value is boolean, it overrides the TextMeasurer's 'precise' config
         * (for the given sprite only).
         */
            preciseMeasurement: undefined
        },
        constructor: function(config) {
            var key;
            if (config && config.font) {
                config = Ext.clone(config);
                for (key in config) {
                    if (key !== 'font' && key.indexOf('font') === 0) {
                        delete config[key];
                    }
                }
            }
            Ext.draw.sprite.Sprite.prototype.constructor.call(this, config);
        },
        // Maps values to font properties they belong to.
        fontValuesMap: {
            // Skip 'normal' and 'inherit' values, as the first one
            // is the default and the second one has no meaning in Canvas.
            'italic': 'fontStyle',
            'oblique': 'fontStyle',
            'small-caps': 'fontVariant',
            'bold': 'fontWeight',
            'bolder': 'fontWeight',
            'lighter': 'fontWeight',
            '100': 'fontWeight',
            '200': 'fontWeight',
            '300': 'fontWeight',
            '400': 'fontWeight',
            '500': 'fontWeight',
            '600': 'fontWeight',
            '700': 'fontWeight',
            '800': 'fontWeight',
            '900': 'fontWeight',
            // Absolute font sizes.
            'xx-small': 'fontSize',
            'x-small': 'fontSize',
            'small': 'fontSize',
            'medium': 'fontSize',
            'large': 'fontSize',
            'x-large': 'fontSize',
            'xx-large': 'fontSize'
        },
        // Relative font sizes like 'smaller' and 'larger'
        // have no meaning, and are not included.
        makeFontShorthand: function(attr) {
            var parts = [];
            if (attr.fontStyle) {
                parts.push(attr.fontStyle);
            }
            if (attr.fontVariant) {
                parts.push(attr.fontVariant);
            }
            if (attr.fontWeight) {
                parts.push(attr.fontWeight);
            }
            if (attr.fontSize) {
                parts.push(attr.fontSize);
            }
            if (attr.fontFamily) {
                parts.push(attr.fontFamily);
            }
            this.setAttributes({
                font: parts.join(' ')
            }, true);
        },
        // For more info see:
        // http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
        parseFontShorthand: function(attr) {
            var value = attr.font,
                ln = value.length,
                changes = {},
                dispatcher = this.fontValuesMap,
                start = 0,
                end, slashIndex, part, fontProperty;
            while (start < ln && end !== -1) {
                end = value.indexOf(' ', start);
                if (end < 0) {
                    part = value.substr(start);
                } else if (end > start) {
                    part = value.substr(start, end - start);
                } else {
                    
                    continue;
                }
                // Since Canvas fillText doesn't support multi-line text,
                // it is assumed that line height is never specified, i.e.
                // in entries like these the part after slash is omitted:
                // 12px/14px sans-serif
                // x-large/110% "New Century Schoolbook", serif
                slashIndex = part.indexOf('/');
                if (slashIndex > 0) {
                    part = part.substr(0, slashIndex);
                } else if (slashIndex === 0) {
                    
                    continue;
                }
                // All optional font properties (fontStyle, fontVariant or fontWeight) can be 'normal'.
                // They can go in any order. Which ones are 'normal' is determined by elimination.
                // E.g. if only fontVariant is specified, then 'normal' applies to fontStyle
                // and fontWeight.
                // If none are explicitly mentioned, then all are 'normal'.
                if (part !== 'normal' && part !== 'inherit') {
                    fontProperty = dispatcher[part];
                    if (fontProperty) {
                        changes[fontProperty] = part;
                    } else if (part.match(Ext.dom.Element.unitRe)) {
                        changes.fontSize = part;
                    } else {
                        // Assuming that font family always goes last in the font shorthand.
                        changes.fontFamily = value.substr(start);
                        break;
                    }
                }
                start = end + 1;
            }
            if (!changes.fontStyle) {
                changes.fontStyle = '';
            }
            // same as 'normal'
            if (!changes.fontVariant) {
                changes.fontVariant = '';
            }
            // same as 'normal'
            if (!changes.fontWeight) {
                changes.fontWeight = '';
            }
            // same as 'normal'
            this.setAttributes(changes, true);
        },
        fontProperties: {
            fontStyle: true,
            fontVariant: true,
            fontWeight: true,
            fontSize: true,
            fontFamily: true
        },
        setAttributes: function(changes, bypassNormalization, avoidCopy) {
            var key, obj;
            // Discard individual font properties if 'font' shorthand was also provided.
            // Example: a user provides a config for chart series labels, using the font
            // shorthand, which is parsed into individual font properties and corresponding
            // sprite attributes are set. Then a theme is applied to the chart, and
            // individual font properties from the theme make up the new font shorthand
            // that overrides the previous one. In other words, no matter what font
            // the user has specified, theme font will be used.
            // This workaround relies on the fact that the theme merges its own config with
            // the user config (where user config values take over the same theme config
            // values). So both user font shorthand and individual font properties from
            // the theme are present in the resulting config (since there are no collisions),
            // which ends up here as the 'changes' parameter.
            // If the user wants their font config to merged with the the theme's font config,
            // instead of taking over it, individual font properties should be used
            // by the user as well.
            if (changes && changes.font) {
                obj = {};
                for (key in changes) {
                    if (!(key in this.fontProperties)) {
                        obj[key] = changes[key];
                    }
                }
                changes = obj;
            }
            this.callParent([
                changes,
                bypassNormalization,
                avoidCopy
            ]);
        },
        // Overriding the getBBox method of the abstract sprite here to always
        // recalculate the bounding box of the text in flipped RTL mode
        // because in that case the position of the sprite depends not just on
        // the value of its 'x' attribute, but also on the width of the surface
        // the sprite belongs to.
        getBBox: function(isWithoutTransform) {
            var me = this,
                plain = me.attr.bbox.plain,
                surface = me.getSurface();
            //<debug>
            // The sprite's bounding box won't account for RTL if it doesn't
            // belong to a surface.
            // if (!surface) {
            //    Ext.raise("The sprite does not belong to a surface.");
            // }
            //</debug>
            if (plain.dirty) {
                me.updatePlainBBox(plain);
                plain.dirty = false;
            }
            if (surface && surface.getInherited().rtl && surface.getFlipRtlText()) {
                // Since sprite's attributes haven't actually changed at this point,
                // and we just want to update the position of its bbox
                // based on surface's width, there's no reason to perform
                // expensive text measurement operation here,
                // so we can use the result of the last measurement instead.
                me.updatePlainBBox(plain, true);
            }
            return me.callParent([
                isWithoutTransform
            ]);
        },
        rtlAlignments: {
            start: 'end',
            center: 'center',
            end: 'start'
        },
        updatePlainBBox: function(plain, useOldSize) {
            var me = this,
                attr = me.attr,
                x = attr.x,
                y = attr.y,
                dx = [],
                font = attr.font,
                text = attr.text,
                baseline = attr.textBaseline,
                alignment = attr.textAlign,
                precise = me.getPreciseMeasurement(),
                size, textMeasurerPrecision;
            if (useOldSize && me.oldSize) {
                size = me.oldSize;
            } else {
                textMeasurerPrecision = Ext.draw.TextMeasurer.precise;
                if (Ext.isBoolean(precise)) {
                    Ext.draw.TextMeasurer.precise = precise;
                }
                size = me.oldSize = Ext.draw.TextMeasurer.measureText(text, font);
                Ext.draw.TextMeasurer.precise = textMeasurerPrecision;
            }
            // eslint-disable-next-line vars-on-top, one-var
            var surface = me.getSurface(),
                isRtl = (surface && surface.getInherited().rtl) || false,
                flipRtlText = isRtl && surface.getFlipRtlText(),
                sizes = size.sizes,
                blockHeight = size.height,
                blockWidth = size.width,
                ln = sizes ? sizes.length : 0,
                lineWidth, rect,
                i = 0;
            // To get consistent results in all browsers we don't apply textAlign
            // and textBaseline attributes of the sprite to context, so text is always
            // left aligned and has an alphabetic baseline.
            //
            // Instead we have to calculate the horizontal offset of each line
            // based on sprite's textAlign, and the vertical offset of the bounding box
            // based on sprite's textBaseline.
            //
            // These offsets are then used by the sprite's 'render' method
            // to position text properly.
            switch (baseline) {
                case 'hanging':
                case 'top':
                    break;
                case 'ideographic':
                case 'bottom':
                    y -= blockHeight;
                    break;
                case 'alphabetic':
                    y -= blockHeight * 0.8;
                    break;
                case 'middle':
                    y -= blockHeight * 0.5;
                    break;
            }
            if (flipRtlText) {
                rect = surface.getRect();
                x = rect[2] - rect[0] - x;
                alignment = me.rtlAlignments[alignment];
            }
            switch (alignment) {
                case 'start':
                    if (isRtl) {
                        for (; i < ln; i++) {
                            lineWidth = sizes[i].width;
                            dx.push(-(blockWidth - lineWidth));
                        }
                    };
                    break;
                case 'end':
                    x -= blockWidth;
                    if (isRtl) {
                        break;
                    };
                    for (; i < ln; i++) {
                        lineWidth = sizes[i].width;
                        dx.push(blockWidth - lineWidth);
                    };
                    break;
                case 'center':
                    x -= blockWidth * 0.5;
                    for (; i < ln; i++) {
                        lineWidth = sizes[i].width;
                        dx.push((isRtl ? -1 : 1) * (blockWidth - lineWidth) * 0.5);
                    };
                    break;
            }
            attr.textAlignOffsets = dx;
            plain.x = x;
            plain.y = y;
            plain.width = blockWidth;
            plain.height = blockHeight;
        },
        setText: function(text) {
            this.setAttributes({
                text: text
            }, true);
        },
        render: function(surface, ctx, rect) {
            var me = this,
                attr = me.attr,
                mat = Ext.draw.Matrix.fly(attr.matrix.elements.slice(0)),
                bbox = me.getBBox(true),
                dx = attr.textAlignOffsets,
                none = Ext.util.Color.RGBA_NONE,
                x, y, i, lines, lineHeight;
            if (attr.text.length === 0) {
                return;
            }
            lines = attr.text.split(me.lineBreakRe);
            lineHeight = bbox.height / lines.length;
            // Simulate textBaseline and textAlign.
            x = attr.bbox.plain.x;
            // lineHeight * 0.78 is the approximate distance between the top
            // and the alphabetic baselines
            y = attr.bbox.plain.y + lineHeight * 0.78;
            mat.toContext(ctx);
            if (surface.getInherited().rtl) {
                // Canvas element in RTL mode automatically flips text alignment.
                // Here we compensate for that change.
                // So text is still positioned and aligned as in the LTR mode,
                // but the direction of the text is RTL.
                x += attr.bbox.plain.width;
            }
            for (i = 0; i < lines.length; i++) {
                if (ctx.fillStyle !== none) {
                    ctx.fillText(lines[i], x + (dx[i] || 0), y + lineHeight * i);
                }
                if (ctx.strokeStyle !== none) {
                    ctx.strokeText(lines[i], x + (dx[i] || 0), y + lineHeight * i);
                }
            }
            //<debug>
            // eslint-disable-next-line vars-on-top
            var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug;
            if (debug) {
                // This assumes no part of the sprite is rendered after this call.
                // If it is, we need to re-apply transformations.
                // But the bounding box is already transformed, so we remove the transformation.
                this.attr.inverseMatrix.toContext(ctx);
                if (debug.bbox) {
                    me.renderBBox(surface, ctx);
                }
            }
        }
    };
});
//</debug>

/**
 * A veritical line sprite. The x and y configs set the center of the line with the size 
 * value determining the height of the line (the line will be twice the height of 'size' 
 * since 'size' is added to above and below 'y' to set the line endpoints).
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'tick',
 *            x: 20,
 *            y: 40,
 *            size: 10,
 *            strokeStyle: '#388FAD',
 *            lineWidth: 2
 *        }]
 *     });
 */
Ext.define('Ext.draw.sprite.Tick', {
    extend: 'Ext.draw.sprite.Line',
    alias: 'sprite.tick',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Object} x The position of the center of the sprite on the x-axis.
                 */
                x: 'number',
                /**
                 * @cfg {Object} y The position of the center of the sprite on the y-axis.
                 */
                y: 'number',
                /**
                 * @cfg {Number} [size=4] The size of the sprite.
                 * Meant to be comparable to the size of a circle sprite with the same radius.
                 */
                size: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                size: 4
            },
            triggers: {
                x: 'tick',
                y: 'tick',
                size: 'tick'
            },
            updaters: {
                tick: function(attr) {
                    var size = attr.size * 1.5,
                        halfLineWidth = attr.lineWidth / 2,
                        x = attr.x,
                        y = attr.y;
                    this.setAttributes({
                        fromX: x - halfLineWidth,
                        fromY: y - size,
                        toX: x - halfLineWidth,
                        toY: y + size
                    });
                }
            }
        }
    }
});

/**
 * A sprite that represents a triangle.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'triangle',
 *            size: 50,
 *            translationX: 100,
 *            translationY: 100,
 *            fillStyle: '#1F6D91'
 *        }]
 *     });
 * 
 */
Ext.define('Ext.draw.sprite.Triangle', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'sprite.triangle',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                /**
                 * @cfg {Number} [size=4] The size of the sprite.
                 * Meant to be comparable to the size of a circle sprite with the same radius.
                 */
                size: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                size: 4
            },
            triggers: {
                x: 'path',
                y: 'path',
                size: 'path'
            }
        }
    },
    updatePath: function(path, attr) {
        var s = attr.size * 2.2,
            x = attr.x,
            y = attr.y;
        path.fromSvgString('M'.concat(x, ',', y, 'm0-', s * 0.48, 'l', s * 0.5, ',', s * 0.87, '-', s, ',0z'));
    }
});

/**
 * Linear gradient.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'circle',
 *            cx: 100,
 *            cy: 100,
 *            r: 100,
 *            fillStyle: {
 *                type: 'linear',
 *                degrees: 180,
 *                stops: [{
 *                    offset: 0,
 *                    color: '#1F6D91'
 *                }, {
 *                    offset: 1,
 *                    color: '#90BCC9'
 *                }]
 *            }
 *        }]
 *     });
 */
Ext.define('Ext.draw.gradient.Linear', {
    extend: 'Ext.draw.gradient.Gradient',
    requires: [
        'Ext.draw.Color'
    ],
    type: 'linear',
    config: {
        /**
         * @cfg {Number} degrees
         * The angle of rotation of the gradient in degrees.
         */
        degrees: 0,
        /**
         * @cfg {Number} radians
         * The angle of rotation of the gradient in radians.
         */
        radians: 0
    },
    applyRadians: function(radians, oldRadians) {
        if (Ext.isNumber(radians)) {
            return radians;
        }
        return oldRadians;
    },
    applyDegrees: function(degrees, oldDegrees) {
        if (Ext.isNumber(degrees)) {
            return degrees;
        }
        return oldDegrees;
    },
    updateRadians: function(radians) {
        this.setDegrees(Ext.draw.Draw.degrees(radians));
    },
    updateDegrees: function(degrees) {
        this.setRadians(Ext.draw.Draw.rad(degrees));
    },
    /**
     * @method generateGradient
     * @inheritdoc
     */
    generateGradient: function(ctx, bbox) {
        var angle = this.getRadians(),
            cos = Math.cos(angle),
            sin = Math.sin(angle),
            w = bbox.width,
            h = bbox.height,
            cx = bbox.x + w * 0.5,
            cy = bbox.y + h * 0.5,
            stops = this.getStops(),
            ln = stops.length,
            gradient, l, i;
        if (Ext.isNumber(cx) && Ext.isNumber(cy) && h > 0 && w > 0) {
            l = (Math.sqrt(h * h + w * w) * Math.abs(Math.cos(angle - Math.atan(h / w)))) / 2;
            gradient = ctx.createLinearGradient(cx + cos * l, cy + sin * l, cx - cos * l, cy - sin * l);
            for (i = 0; i < ln; i++) {
                gradient.addColorStop(stops[i].offset, stops[i].color);
            }
            return gradient;
        }
        return Ext.util.Color.NONE;
    }
});

/**
 * Radial gradient.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'draw', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        sprites: [{
 *            type: 'circle',
 *            cx: 100,
 *            cy: 100,
 *            r: 100,
 *            fillStyle: {
 *                type: 'radial',
 *                start: {
 *                    x: 0,
 *                    y: 0,
 *                    r: 0
 *                },
 *                end: {
 *                    x: 0,
 *                    y: 0,
 *                    r: 1
 *                },
 *                stops: [{
 *                    offset: 0,
 *                    color: '#90BCC9'
 *                }, {
 *                    offset: 1,
 *                    color: '#1F6D91'
 *                }]
 *            }
 *        }]
 *     });
 */
Ext.define('Ext.draw.gradient.Radial', {
    extend: 'Ext.draw.gradient.Gradient',
    type: 'radial',
    config: {
        /**
         * @cfg {Object} start
         * The starting circle of the gradient.
         */
        start: {
            x: 0,
            y: 0,
            r: 0
        },
        /**
         * @cfg {Object} end
         * The ending circle of the gradient.
         */
        end: {
            x: 0,
            y: 0,
            r: 1
        }
    },
    applyStart: function(newStart, oldStart) {
        var circle;
        if (!oldStart) {
            return newStart;
        }
        circle = {
            x: oldStart.x,
            y: oldStart.y,
            r: oldStart.r
        };
        if ('x' in newStart) {
            circle.x = newStart.x;
        } else if ('centerX' in newStart) {
            circle.x = newStart.centerX;
        }
        if ('y' in newStart) {
            circle.y = newStart.y;
        } else if ('centerY' in newStart) {
            circle.y = newStart.centerY;
        }
        if ('r' in newStart) {
            circle.r = newStart.r;
        } else if ('radius' in newStart) {
            circle.r = newStart.radius;
        }
        return circle;
    },
    applyEnd: function(newEnd, oldEnd) {
        var circle;
        if (!oldEnd) {
            return newEnd;
        }
        circle = {
            x: oldEnd.x,
            y: oldEnd.y,
            r: oldEnd.r
        };
        if ('x' in newEnd) {
            circle.x = newEnd.x;
        } else if ('centerX' in newEnd) {
            circle.x = newEnd.centerX;
        }
        if ('y' in newEnd) {
            circle.y = newEnd.y;
        } else if ('centerY' in newEnd) {
            circle.y = newEnd.centerY;
        }
        if ('r' in newEnd) {
            circle.r = newEnd.r;
        } else if ('radius' in newEnd) {
            circle.r = newEnd.radius;
        }
        return circle;
    },
    /**
     * @method generateGradient
     * @inheritdoc
     */
    generateGradient: function(ctx, bbox) {
        var start = this.getStart(),
            end = this.getEnd(),
            w = bbox.width * 0.5,
            h = bbox.height * 0.5,
            x = bbox.x + w,
            y = bbox.y + h,
            gradient = ctx.createRadialGradient(x + start.x * w, y + start.y * h, start.r * Math.max(w, h), x + end.x * w, y + end.y * h, end.r * Math.max(w, h)),
            stops = this.getStops(),
            ln = stops.length,
            i;
        for (i = 0; i < ln; i++) {
            gradient.addColorStop(stops[i].offset, stops[i].color);
        }
        return gradient;
    }
});

/**
 * A surface is an interface to render {@link Ext.draw.sprite.Sprite sprites} inside a
 * {@link Ext.draw.Container draw container}.  The surface API has methods to render
 * sprites, get sprite bounding boxes (dimensions), add sprites to the underlying DOM,
 * and more.
 *
 * A surface is automatically created when a draw container is created.  By default,
 * this will be a surface with an `id` of "main" and will manage all sprites in the draw
 * container (unless the sprite configs specify a unique surface "id").
 *
 *     @example
 *     Ext.create({
 *         xtype: 'draw',
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400,
 *         sprites: [{
 *             type: 'rect',
 *             surface: 'anim',  // a surface with id "anim" will be created automatically
 *             x: 50,
 *             y: 50,
 *             width: 100,
 *             height: 100,
 *             fillStyle: '#1F6D91'
 *         }]
 *     });
 *
 * The ability to have multiple surfaces is useful for performance (and battery life)
 * reasons. Because changes to sprite attributes cause the whole surface (and all
 * sprites in it) to re-render, it makes sense to group sprites by surface, so changes
 * to one group of sprites will only trigger the surface they are in to re-render.
 *
 * One of the more useful methods is the {@link #add} method used to add sprites to the
 * surface:
 *
 *     @example
 *     var drawCt = Ext.create({
 *         xtype: 'draw',
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400
 *     });
 *
 *     // If the surface name is not specified then 'main' will be used
 *     var surface = drawCt.getSurface();
 *
 *     surface.add({
 *         type: 'rect',
 *         x: 50,
 *         y: 50,
 *         width: 100,
 *         height: 100,
 *         fillStyle: '#1F6D91'
 *     });
 *
 *     surface.renderFrame();
 *
 * **Note:** Changes to the sprites on a surface will be not be reflected in the DOM
 * until you call the surface's {@link Ext.draw.Surface#method-renderFrame renderFrame}
 * method.  This must be done after adding, removing, or modifying sprites in order to
 * see the changes on-screen.
 */
Ext.define('Ext.draw.Surface', {
    extend: 'Ext.draw.SurfaceBase',
    xtype: 'surface',
    requires: [
        'Ext.draw.sprite.*',
        'Ext.draw.gradient.*',
        'Ext.draw.sprite.AttributeDefinition',
        'Ext.draw.Matrix',
        'Ext.draw.Draw'
    ],
    uses: [
        'Ext.draw.engine.Canvas'
    ],
    /**
     * The reported device pixel density.
     * devicePixelRatio is only supported from IE11,
     * so we use deviceXDPI and logicalXDPI that are supported from IE6.
     */
    devicePixelRatio: window.devicePixelRatio || window.screen.deviceXDPI / window.screen.logicalXDPI,
    deprecated: {
        '5.1.0': {
            statics: {
                methods: {
                    /**
                     * @deprecated 5.1.0
                     * Stably sort the list of sprites by their zIndex.
                     * Deprecated, use the {@link Ext.Array#sort} method instead.
                     * @param {Array} list
                     * @return {Array} Sorted array.
                     */
                    stableSort: function(list) {
                        return Ext.Array.sort(list, function(a, b) {
                            return a.attr.zIndex - b.attr.zIndex;
                        });
                    }
                }
            }
        }
    },
    cls: Ext.baseCSSPrefix + 'surface',
    config: {
        /**
         * @cfg {Array}
         * The [x, y, width, height] rect of the surface related to its container.
         */
        rect: null,
        /**
         * @cfg {Object}
         * Background sprite config of the surface.
         */
        background: null,
        /**
         * @cfg {Array}
         * Array of sprite instances.
         */
        items: [],
        /**
         * @cfg {Boolean}
         * Indicates whether the surface needs to redraw.
         */
        dirty: false,
        /**
         * @cfg {Boolean} flipRtlText
         * If the surface is in the RTL mode, text will render with the RTL direction,
         * but the alignment and position of the text won't change by default.
         * Setting this config to 'true' will get text alignment and its position
         * within a surface mirrored.
         */
        flipRtlText: false
    },
    isSurface: true,
    /**
     * @private
     * This flag is used to indicate that `predecessors` surfaces that should render
     * before this surface renders are dirty, and to call `renderFrame`
     * when all `predecessors` have their `renderFrame` called (i.e. not dirty anymore).
     * This flag indicates that current surface has surfaces that are yet to render
     * before current surface can render. When all the `predecessors` surfaces
     * have rendered, i.e. when `dirtyPredecessorCount` reaches zero,
     */
    isPendingRenderFrame: false,
    dirtyPredecessorCount: 0,
    emptyRect: [
        0,
        0,
        0,
        0
    ],
    constructor: function(config) {
        var me = this;
        me.predecessors = [];
        me.successors = [];
        me.map = {};
        me.callParent([
            config
        ]);
        me.matrix = new Ext.draw.Matrix();
        me.inverseMatrix = me.matrix.inverse();
    },
    /**
     * Round the number to align to the pixels on device.
     * @param {Number} num The number to align.
     * @return {Number} The resultant alignment.
     */
    roundPixel: function(num) {
        return Math.round(this.devicePixelRatio * num) / this.devicePixelRatio;
    },
    /**
     * Mark the surface to render after another surface is updated.
     * @param {Ext.draw.Surface} surface The surface to wait for.
     */
    waitFor: function(surface) {
        var me = this,
            predecessors = me.predecessors;
        if (!Ext.Array.contains(predecessors, surface)) {
            predecessors.push(surface);
            surface.successors.push(me);
            if (surface.getDirty()) {
                me.dirtyPredecessorCount++;
            }
        }
    },
    updateDirty: function(dirty) {
        var successors = this.successors,
            ln = successors.length,
            i = 0,
            successor;
        for (; i < ln; i++) {
            successor = successors[i];
            if (dirty) {
                successor.dirtyPredecessorCount++;
                successor.setDirty(true);
            } else {
                successor.dirtyPredecessorCount--;
                // Don't need to call `setDirty(false)` on a successor here,
                // as this will be done by `renderFrame`.
                if (successor.dirtyPredecessorCount === 0 && successor.isPendingRenderFrame) {
                    successor.renderFrame();
                }
            }
        }
    },
    applyBackground: function(background, oldBackground) {
        this.setDirty(true);
        if (Ext.isString(background)) {
            background = {
                fillStyle: background
            };
        }
        return Ext.factory(background, Ext.draw.sprite.Rect, oldBackground);
    },
    applyRect: function(rect, oldRect) {
        if (oldRect && rect[0] === oldRect[0] && rect[1] === oldRect[1] && rect[2] === oldRect[2] && rect[3] === oldRect[3]) {
            return oldRect;
        }
        if (Ext.isArray(rect)) {
            return [
                rect[0],
                rect[1],
                rect[2],
                rect[3]
            ];
        } else if (Ext.isObject(rect)) {
            return [
                rect.x || rect.left,
                rect.y || rect.top,
                rect.width || (rect.right - rect.left),
                rect.height || (rect.bottom - rect.top)
            ];
        }
    },
    updateRect: function(rect) {
        var me = this,
            l = rect[0],
            t = rect[1],
            r = l + rect[2],
            b = t + rect[3],
            background = me.getBackground(),
            element = me.element;
        element.setLocalXY(Math.floor(l), Math.floor(t));
        element.setSize(Math.ceil(r - Math.floor(l)), Math.ceil(b - Math.floor(t)));
        if (background) {
            background.setAttributes({
                x: 0,
                y: 0,
                width: Math.ceil(r - Math.floor(l)),
                height: Math.ceil(b - Math.floor(t))
            });
        }
        me.setDirty(true);
    },
    /**
     * Reset the matrix of the surface.
     */
    resetTransform: function() {
        this.matrix.set(1, 0, 0, 1, 0, 0);
        this.inverseMatrix.set(1, 0, 0, 1, 0, 0);
        this.setDirty(true);
    },
    /**
     * Get the sprite by id or index.
     * It will first try to find a sprite with the given id, otherwise will try to use the id
     * as an index.
     * @param {String|Number} id
     * @return {Ext.draw.sprite.Sprite}
     */
    get: function(id) {
        return this.map[id] || this.getItems()[id];
    },
    /**
     * @method
     * Add a Sprite to the surface.
     * You can put any number of objects as the parameter.
     * See {@link Ext.draw.sprite.Sprite} for the configuration object to be passed
     * into this method.
     *
     * For example:
     *
     *     drawContainer.getSurface().add({
     *         type: 'circle',
     *         fill: '#ffc',
     *         radius: 100,
     *         x: 100,
     *         y: 100
     *     });
     *     drawContainer.renderFrame();
     *
     * @param {Object/Object[]} sprite
     * @return {Ext.draw.sprite.Sprite/Ext.draw.sprite.Sprite[]}
     *
     */
    add: function() {
        var me = this,
            args = Array.prototype.slice.call(arguments),
            argIsArray = Ext.isArray(args[0]),
            map = me.map,
            results = [],
            items, item, sprite, oldSurface, i, ln;
        items = Ext.Array.clean(argIsArray ? args[0] : args);
        if (!items.length) {
            return results;
        }
        for (i = 0 , ln = items.length; i < ln; i++) {
            item = items[i];
            if (!item || item.destroyed) {
                
                continue;
            }
            sprite = null;
            if (item.isSprite && !map[item.getId()]) {
                sprite = item;
            } else if (!map[item.id]) {
                sprite = this.createItem(item);
            }
            if (sprite) {
                map[sprite.getId()] = sprite;
                results.push(sprite);
                oldSurface = sprite.getSurface();
                if (oldSurface && oldSurface.isSurface) {
                    oldSurface.remove(sprite);
                }
                sprite.setParent(me);
                sprite.setSurface(me);
                me.onAdd(sprite);
            }
        }
        items = me.getItems();
        if (items) {
            items.push.apply(items, results);
        }
        me.dirtyZIndex = true;
        me.setDirty(true);
        if (!argIsArray && results.length === 1) {
            return results[0];
        } else {
            return results;
        }
    },
    /**
     * @method
     * @protected
     * Invoked when a sprite is added to the surface.
     * @param {Ext.draw.sprite.Sprite} sprite The sprite to be added.
     */
    onAdd: Ext.emptyFn,
    /**
     * Remove a given sprite from the surface,
     * optionally destroying the sprite in the process.
     * You can also call the sprite's own `remove` method.
     *
     * For example:
     *
     *      drawContainer.surface.remove(sprite);
     *      // or...
     *      sprite.remove();
     *
     * @param {Ext.draw.sprite.Sprite/String} sprite A sprite instance or its ID.
     * @param {Boolean} [isDestroy=false] If `true`, the sprite will be destroyed.
     * @return {Ext.draw.sprite.Sprite} Returns the removed/destroyed sprite or `null` otherwise.
     */
    remove: function(sprite, isDestroy) {
        var me = this,
            destroying = me.clearing,
            id, isOwnSprite;
        if (sprite) {
            if (sprite.charAt) {
                // is String
                sprite = me.map[sprite];
            }
            if (!sprite || !sprite.isSprite) {
                return null;
            }
            id = sprite.id;
            isOwnSprite = me.map[id];
            delete me.map[id];
            if (sprite.destroyed || sprite.destroying) {
                if (isOwnSprite && !destroying) {
                    // Somehow this sprite was destroyed,
                    // but still belongs to the surface.
                    Ext.Array.remove(me.getItems(), sprite);
                }
                return sprite;
            }
            if (!isOwnSprite) {
                if (isDestroy) {
                    sprite.destroy();
                }
                return sprite;
            }
            sprite.setParent(null);
            sprite.setSurface(null);
            if (isDestroy) {
                sprite.destroy();
            }
            if (!destroying) {
                Ext.Array.remove(me.getItems(), sprite);
                me.dirtyZIndex = true;
                me.setDirty(true);
            }
        }
        return sprite || null;
    },
    /**
     * Remove all sprites from the surface, optionally destroying the sprites in the process.
     *
     * For example:
     *
     *     drawContainer.getSurface('main').removeAll();
     *
     * @param {Boolean} [isDestroy=false]
     */
    removeAll: function(isDestroy) {
        var me = this,
            items = me.getItems(),
            item, i;
        me.clearing = !!isDestroy;
        for (i = items.length - 1; i >= 0; i--) {
            item = items[i];
            if (isDestroy) {
                // Some sprites may destroy other sprites, however if we're destroying then
                // we don't remove anything from the items array since we'll just clear it later.
                // If a sprite is destroyed, the remove method will just drop out with no harm done.
                item.destroy();
            } else {
                item.setParent(null);
                item.setSurface(null);
            }
        }
        me.clearing = false;
        items.length = 0;
        me.map = {};
        me.dirtyZIndex = true;
        if (!me.destroying) {
            me.setDirty(true);
        }
    },
    /**
     * @private
     */
    applyItems: function(items) {
        if (this.getItems()) {
            this.removeAll(true);
        }
        return Ext.Array.from(this.add(items));
    },
    /**
     * @private
     * Creates an item and appends it to the surface. Called
     * as an internal method when calling `add`.
     */
    createItem: function(config) {
        return Ext.create(config.xclass || 'sprite.' + config.type, config);
    },
    /**
     * Return the minimal bounding box that contains all the sprites bounding boxes
     * in the given list of sprites.
     * @param {Ext.draw.sprite.Sprite[]|Ext.draw.sprite.Sprite} sprites
     * @param {Boolean} [isWithoutTransform=false]
     * @return {{x: Number, y: Number, width: number, height: number}}
     */
    getBBox: function(sprites, isWithoutTransform) {
        var left = Infinity,
            right = -Infinity,
            top = Infinity,
            bottom = -Infinity,
            sprite, bbox, i, ln;
        sprites = Ext.Array.from(sprites);
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            bbox = sprite.getBBox(isWithoutTransform);
            if (left > bbox.x) {
                left = bbox.x;
            }
            if (right < bbox.x + bbox.width) {
                right = bbox.x + bbox.width;
            }
            if (top > bbox.y) {
                top = bbox.y;
            }
            if (bottom < bbox.y + bbox.height) {
                bottom = bbox.y + bbox.height;
            }
        }
        return {
            x: left,
            y: top,
            width: right - left,
            height: bottom - top
        };
    },
    /**
     * @private
     * @method getOwnerBody
     * The body element of the chart or the draw container
     * (doesn't include docked items like a legend).
     * Draw Container is a Panel in Classic (to allow for docked items)
     * and a Container in Modern, so the body is retrieved differently.
     * @return {Ext.dom.Element}
     */
    /**
     * @private
     * Converts event's page coordinates into surface coordinates.
     * Note: surface's x-coordinates always go LTR, regardless of RTL mode.
     */
    getEventXY: function(e) {
        var me = this,
            isRtl = me.getInherited().rtl,
            pageXY = e.getXY(),
            // Event position in page coordinates.
            // The body of the chart (doesn't include docked items like legend).
            container = me.getOwnerBody(),
            xy = container.getXY(),
            // Surface container position in page coordinates.
            // Surface position in surface container coordinates (LTR).
            rect = me.getRect() || me.emptyRect,
            result = [],
            width;
        if (isRtl) {
            width = container.getWidth();
            // The line below is actually a simplified form of
            // rect[2] - (pageXY[0] - xy[0] - (width - (rect[0] + rect[2]))).
            result[0] = xy[0] - pageXY[0] - rect[0] + width;
        } else {
            result[0] = pageXY[0] - xy[0] - rect[0];
        }
        result[1] = pageXY[1] - xy[1] - rect[1];
        return result;
    },
    /**
     * @method
     * Empty the surface content (without touching the sprites.)
     */
    clear: Ext.emptyFn,
    /**
     * @private
     * Order the items by their z-index if any of that has been changed since last sort.
     */
    orderByZIndex: function() {
        var me = this,
            items = me.getItems(),
            dirtyZIndex = false,
            i, ln;
        if (me.getDirty()) {
            for (i = 0 , ln = items.length; i < ln; i++) {
                if (items[i].attr.dirtyZIndex) {
                    dirtyZIndex = true;
                    break;
                }
            }
            if (dirtyZIndex) {
                // sort by zIndex
                Ext.Array.sort(items, function(a, b) {
                    return a.attr.zIndex - b.attr.zIndex;
                });
                this.setDirty(true);
            }
            for (i = 0 , ln = items.length; i < ln; i++) {
                items[i].attr.dirtyZIndex = false;
            }
        }
    },
    /**
     * Force the element to redraw.
     */
    repaint: function() {
        var me = this;
        me.repaint = Ext.emptyFn;
        Ext.defer(function() {
            delete me.repaint;
            me.element.repaint();
        }, 1);
    },
    /**
     * Triggers the re-rendering of the canvas.
     */
    renderFrame: function() {
        var me = this,
            background, items, item, i, ln;
        if (!(me.element && me.getDirty() && me.getRect())) {
            return;
        }
        if (me.dirtyPredecessorCount > 0) {
            me.isPendingRenderFrame = true;
            return;
        }
        background = me.getBackground();
        items = me.getItems();
        // This will also check the dirty flags of the sprites.
        me.orderByZIndex();
        if (me.getDirty()) {
            me.clear();
            me.clearTransform();
            if (background) {
                me.renderSprite(background);
            }
            for (i = 0 , ln = items.length; i < ln; i++) {
                item = items[i];
                if (me.renderSprite(item) === false) {
                    return;
                }
                item.attr.textPositionCount = me.textPosition;
            }
            me.setDirty(false);
        }
    },
    /**
     * @method
     * @private
     * Renders a single sprite into the surface.
     * Do not call it from outside `renderFrame` method.
     *
     * @param {Ext.draw.sprite.Sprite} sprite The Sprite to be rendered.
     * @return {Boolean} returns `false` to stop the rendering to continue.
     */
    renderSprite: Ext.emptyFn,
    /**
     * @method flatten
     * Flattens the given drawing surfaces into a single image
     * and returns an object containing the data (in the DataURL format)
     * and the type (e.g. 'png' or 'svg') of that image.
     * @param {Object} size The size of the final image.
     * @param {Number} size.width
     * @param {Number} size.height
     * @param {Ext.draw.Surface[]} surfaces The surfaces to flatten.
     * @return {Object}
     * @return {String} return.data The DataURL of the flattened image.
     * @return {String} return.type The type of the image.
     *
     */
    /**
     * @method
     * @private
     * Clears the current transformation state on the surface.
     */
    clearTransform: Ext.emptyFn,
    /**
     * Destroys the surface. This is done by removing all components from it and
     * also removing its reference to a DOM element.
     *
     * For example:
     *
     *      drawContainer.surface.destroy();
     */
    destroy: function() {
        var me = this;
        me.destroying = true;
        me.removeAll(true);
        me.destroying = false;
        me.predecessors = me.successors = null;
        if (me.hasListeners.destroy) {
            me.fireEvent('destroy', me);
        }
        me.callParent();
    }
});

/**
 * @private
 * Adds hit testing methods to the Ext.draw.Surface.
 * Included by the Ext.draw.plugin.SpriteEvents.
 */
Ext.define('Ext.draw.overrides.hittest.Surface', {
    override: 'Ext.draw.Surface',
    /**
     * Performs a hit test on all sprites in the surface, returning the first matching one.
     * @param {Array} point A two-item array containing x and y coordinates of the point
     * in surface coordinate system.
     * @param {Object} options Hit testing options.
     * @return {Object} A hit result object that contains more information about what
     * exactly was hit or null if nothing was hit.
     * @member Ext.draw.Surface
     */
    hitTest: function(point, options) {
        var me = this,
            sprites = me.getItems(),
            i, sprite, result;
        options = options || Ext.draw.sprite.Sprite.defaultHitTestOptions;
        for (i = sprites.length - 1; i >= 0; i--) {
            sprite = sprites[i];
            if (sprite.hitTest) {
                result = sprite.hitTest(point, options);
                if (result) {
                    return result;
                }
            }
        }
        return null;
    },
    /**
     * Performs a hit test on all sprites in the surface, returning the first matching one.
     * Since hit testing is typically performed on mouse events, this convenience method
     * converts event's page coordinates to surface coordinates before calling {@link #hitTest}.
     * @param {Object} event An event object.
     * @param {Object} options Hit testing options.
     * @return {Object} A hit result object that contains more information about what
     * exactly was hit or null if nothing was hit.
     * @member Ext.draw.Surface
     */
    hitTestEvent: function(event, options) {
        var xy = this.getEventXY(event);
        return this.hitTest(xy, options);
    }
});

/**
 * @class Ext.draw.engine.SvgContext
 *
 * A class that imitates a canvas context but generates svg elements instead.
 */
Ext.define('Ext.draw.engine.SvgContext', {
    requires: [
        'Ext.draw.Color'
    ],
    /**
     * @private
     * Properties to be saved/restored in the `save` and `restore` methods.
     */
    toSave: [
        'strokeOpacity',
        'strokeStyle',
        'fillOpacity',
        'fillStyle',
        'globalAlpha',
        'lineWidth',
        'lineCap',
        'lineJoin',
        'lineDash',
        'lineDashOffset',
        'miterLimit',
        'shadowOffsetX',
        'shadowOffsetY',
        'shadowBlur',
        'shadowColor',
        'globalCompositeOperation',
        'position',
        'fillGradient',
        'strokeGradient'
    ],
    strokeOpacity: 1,
    strokeStyle: 'none',
    fillOpacity: 1,
    fillStyle: 'none',
    lineDas: [],
    lineDashOffset: 0,
    globalAlpha: 1,
    lineWidth: 1,
    lineCap: 'butt',
    lineJoin: 'miter',
    miterLimit: 10,
    shadowOffsetX: 0,
    shadowOffsetY: 0,
    shadowBlur: 0,
    shadowColor: 'none',
    globalCompositeOperation: 'src',
    urlStringRe: /^url\(#([\w-]+)\)$/,
    constructor: function(SvgSurface) {
        var me = this;
        me.surface = SvgSurface;
        // Stack of contexts.
        me.state = [];
        me.matrix = new Ext.draw.Matrix();
        // Currently manipulated path.
        me.path = null;
        me.clear();
    },
    /**
     * Clears the context.
     */
    clear: function() {
        // Current group to put paths into.
        this.group = this.surface.mainGroup;
        // Position within the current group.
        this.position = 0;
        this.path = null;
    },
    /**
     * @private
     * @param {String} tag
     * @return {*}
     */
    getElement: function(tag) {
        return this.surface.getSvgElement(this.group, tag, this.position++);
    },
    /**
     * Pushes the context state to the state stack.
     */
    save: function() {
        var toSave = this.toSave,
            obj = {},
            group = this.getElement('g'),
            key, i;
        for (i = 0; i < toSave.length; i++) {
            key = toSave[i];
            if (key in this) {
                obj[key] = this[key];
            }
        }
        this.position = 0;
        obj.matrix = this.matrix.clone();
        this.state.push(obj);
        this.group = group;
        return group;
    },
    /**
     * Pops the state stack and restores the state.
     */
    restore: function() {
        var toSave = this.toSave,
            obj = this.state.pop(),
            group = this.group,
            children = group.dom.childNodes,
            key, i;
        // Removing extra DOM elements that were not reused.
        while (children.length > this.position) {
            group.last().destroy();
        }
        for (i = 0; i < toSave.length; i++) {
            key = toSave[i];
            if (key in obj) {
                this[key] = obj[key];
            } else {
                delete this[key];
            }
        }
        this.setTransform.apply(this, obj.matrix.elements);
        this.group = group.getParent();
    },
    /**
     * Changes the transformation matrix to apply the matrix given by the arguments
     * as described below.
     * @param {Number} xx
     * @param {Number} yx
     * @param {Number} xy
     * @param {Number} yy
     * @param {Number} dx
     * @param {Number} dy
     */
    transform: function(xx, yx, xy, yy, dx, dy) {
        var inv;
        if (this.path) {
            inv = Ext.draw.Matrix.fly([
                xx,
                yx,
                xy,
                yy,
                dx,
                dy
            ]).inverse();
            this.path.transform(inv);
        }
        this.matrix.append(xx, yx, xy, yy, dx, dy);
    },
    /**
     * Changes the transformation matrix to the matrix given by the arguments as described below.
     * @param {Number} xx
     * @param {Number} yx
     * @param {Number} xy
     * @param {Number} yy
     * @param {Number} dx
     * @param {Number} dy
     */
    setTransform: function(xx, yx, xy, yy, dx, dy) {
        if (this.path) {
            this.path.transform(this.matrix);
        }
        this.matrix.reset();
        this.transform(xx, yx, xy, yy, dx, dy);
    },
    /**
     * Scales the current context by the specified horizontal (x) and vertical (y) factors.
     * @param {Number} x The horizontal scaling factor, where 1 equals unity or 100% scale.
     * @param {Number} y The vertical scaling factor.
     */
    scale: function(x, y) {
        this.transform(x, 0, 0, y, 0, 0);
    },
    /**
     * Rotates the current context coordinates (that is, a transformation matrix).
     * @param {Number} angle The rotation angle, in radians.
     */
    rotate: function(angle) {
        var xx = Math.cos(angle),
            yx = Math.sin(angle),
            xy = -Math.sin(angle),
            yy = Math.cos(angle);
        this.transform(xx, yx, xy, yy, 0, 0);
    },
    /**
     * Specifies values to move the origin point in a canvas.
     * @param {Number} x The value to add to horizontal (or x) coordinates.
     * @param {Number} y The value to add to vertical (or y) coordinates.
     */
    translate: function(x, y) {
        this.transform(1, 0, 0, 1, x, y);
    },
    setGradientBBox: function(bbox) {
        this.bbox = bbox;
    },
    /**
     * Resets the current default path.
     */
    beginPath: function() {
        this.path = new Ext.draw.Path();
    },
    /**
     * Creates a new subpath with the given point.
     * @param {Number} x
     * @param {Number} y
     */
    moveTo: function(x, y) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.moveTo(x, y);
        this.path.element = null;
    },
    /**
     * Adds the given point to the current subpath, connected to the previous one by a straight
     * line.
     * @param {Number} x
     * @param {Number} y
     */
    lineTo: function(x, y) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.lineTo(x, y);
        this.path.element = null;
    },
    /**
     * Adds a new closed subpath to the path, representing the given rectangle.
     * @param {Number} x
     * @param {Number} y
     * @param {Number} width
     * @param {Number} height
     */
    rect: function(x, y, width, height) {
        this.moveTo(x, y);
        this.lineTo(x + width, y);
        this.lineTo(x + width, y + height);
        this.lineTo(x, y + height);
        this.closePath();
    },
    /**
     * Paints the box that outlines the given rectangle onto the canvas, using the current
     * stroke style.
     * @param {Number} x
     * @param {Number} y
     * @param {Number} width
     * @param {Number} height
     */
    strokeRect: function(x, y, width, height) {
        this.beginPath();
        this.rect(x, y, width, height);
        this.stroke();
    },
    /**
     * Paints the given rectangle onto the canvas, using the current fill style.
     * @param {Number} x
     * @param {Number} y
     * @param {Number} width
     * @param {Number} height
     */
    fillRect: function(x, y, width, height) {
        this.beginPath();
        this.rect(x, y, width, height);
        this.fill();
    },
    /**
     * Marks the current subpath as closed, and starts a new subpath with a point the same
     * as the start and end of the newly closed subpath.
     */
    closePath: function() {
        if (!this.path) {
            this.beginPath();
        }
        this.path.closePath();
        this.path.element = null;
    },
    /**
     * Arc command using svg parameters.
     * @param {Number} r1
     * @param {Number} r2
     * @param {Number} rotation
     * @param {Number} large
     * @param {Number} swipe
     * @param {Number} x2
     * @param {Number} y2
     */
    arcSvg: function(r1, r2, rotation, large, swipe, x2, y2) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.arcSvg(r1, r2, rotation, large, swipe, x2, y2);
        this.path.element = null;
    },
    /**
     * Adds points to the subpath such that the arc described by the circumference of the circle
     * described by the arguments, starting at the given start angle and ending at the given
     * end angle, going in the given direction (defaulting to clockwise), is added to the path,
     * connected to the previous point by a straight line.
     * @param {Number} x
     * @param {Number} y
     * @param {Number} radius
     * @param {Number} startAngle
     * @param {Number} endAngle
     * @param {Number} anticlockwise
     */
    arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.arc(x, y, radius, startAngle, endAngle, anticlockwise);
        this.path.element = null;
    },
    /**
     * Adds points to the subpath such that the arc described by the circumference of the ellipse
     * described by the arguments, starting at the given start angle and ending at the given
     * end angle, going in the given direction (defaulting to clockwise), is added to the path,
     * connected to the previous point by a straight line.
     * @param {Number} x
     * @param {Number} y
     * @param {Number} radiusX
     * @param {Number} radiusY
     * @param {Number} rotation
     * @param {Number} startAngle
     * @param {Number} endAngle
     * @param {Number} anticlockwise
     */
    ellipse: function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
        this.path.element = null;
    },
    /**
     * Adds an arc with the given control points and radius to the current subpath, connected
     * to the previous point by a straight line. If two radii are provided, the first controls
     * the width of the arc's ellipse, and the second controls the height. If only one is provided,
     * or if they are the same, the arc is from a circle. In the case of an ellipse, the rotation
     * argument controls the clockwise inclination of the ellipse relative to the x-axis.
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} x2
     * @param {Number} y2
     * @param {Number} radiusX
     * @param {Number} radiusY
     * @param {Number} rotation
     */
    arcTo: function(x1, y1, x2, y2, radiusX, radiusY, rotation) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.arcTo(x1, y1, x2, y2, radiusX, radiusY, rotation);
        this.path.element = null;
    },
    /**
     * Adds the given point to the current subpath, connected to the previous one by a cubic Bézier
     * curve with the given control points.
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} x2
     * @param {Number} y2
     * @param {Number} x3
     * @param {Number} y3
     */
    bezierCurveTo: function(x1, y1, x2, y2, x3, y3) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.bezierCurveTo(x1, y1, x2, y2, x3, y3);
        this.path.element = null;
    },
    /**
     * Strokes the given text at the given position. If a maximum width is provided, the text
     * will be scaled to fit that width if necessary.
     * @param {String} text
     * @param {Number} x
     * @param {Number} y
     */
    strokeText: function(text, x, y) {
        var element, tspan;
        text = String(text);
        if (this.strokeStyle) {
            element = this.getElement('text');
            tspan = this.surface.getSvgElement(element, 'tspan', 0);
            this.surface.setElementAttributes(element, {
                "x": x,
                "y": y,
                "transform": this.matrix.toSvg(),
                "stroke": this.strokeStyle,
                "fill": "none",
                "opacity": this.globalAlpha,
                "stroke-opacity": this.strokeOpacity,
                "style": "font: " + this.font,
                "stroke-dasharray": this.lineDash.join(','),
                "stroke-dashoffset": this.lineDashOffset
            });
            if (this.lineDash.length) {
                this.surface.setElementAttributes(element, {
                    "stroke-dasharray": this.lineDash.join(','),
                    "stroke-dashoffset": this.lineDashOffset
                });
            }
            if (tspan.dom.firstChild) {
                tspan.dom.removeChild(tspan.dom.firstChild);
            }
            this.surface.setElementAttributes(tspan, {
                "alignment-baseline": "alphabetic"
            });
            tspan.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
        }
    },
    /**
     * Fills the given text at the given position. If a maximum width is provided, the text
     * will be scaled to fit that width if necessary.
     * @param {String} text
     * @param {Number} x
     * @param {Number} y
     */
    fillText: function(text, x, y) {
        var element, tspan;
        text = String(text);
        if (this.fillStyle) {
            element = this.getElement('text');
            tspan = this.surface.getSvgElement(element, 'tspan', 0);
            this.surface.setElementAttributes(element, {
                "x": x,
                "y": y,
                "transform": this.matrix.toSvg(),
                "fill": this.fillStyle,
                "opacity": this.globalAlpha,
                "fill-opacity": this.fillOpacity,
                "style": "font: " + this.font
            });
            if (tspan.dom.firstChild) {
                tspan.dom.removeChild(tspan.dom.firstChild);
            }
            this.surface.setElementAttributes(tspan, {
                "alignment-baseline": "alphabetic"
            });
            tspan.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
        }
    },
    /**
     * Draws the given image onto the canvas.
     * If the first argument isn't an img, canvas, or video element, throws a TypeMismatchError
     * exception. If the image has no image data, throws an InvalidStateError exception.
     * If the one of the source rectangle dimensions is zero, throws an IndexSizeError exception.
     * If the image isn't yet fully decoded, then nothing is drawn.
     * @param {HTMLElement} image
     * @param {Number} sx
     * @param {Number} sy
     * @param {Number} sw
     * @param {Number} sh
     * @param {Number} dx
     * @param {Number} dy
     * @param {Number} dw
     * @param {Number} dh
     */
    drawImage: function(image, sx, sy, sw, sh, dx, dy, dw, dh) {
        var me = this,
            element = me.getElement('image'),
            x = sx,
            y = sy,
            width = typeof sw === 'undefined' ? image.width : sw,
            height = typeof sh === 'undefined' ? image.height : sh,
            viewBox = null;
        if (typeof dh !== 'undefined') {
            viewBox = sx + " " + sy + " " + sw + " " + sh;
            x = dx;
            y = dy;
            width = dw;
            height = dh;
        }
        element.dom.setAttributeNS("http:/" + "/www.w3.org/1999/xlink", "href", image.src);
        me.surface.setElementAttributes(element, {
            viewBox: viewBox,
            x: x,
            y: y,
            width: width,
            height: height,
            opacity: me.globalAlpha,
            transform: me.matrix.toSvg()
        });
    },
    /**
     * Fills the subpaths of the current default path or the given path with the current fill style.
     */
    fill: function() {
        var me = this,
            path, fillGradient, element, bbox, fill;
        if (!me.path) {
            return;
        }
        if (me.fillStyle) {
            fillGradient = me.fillGradient;
            element = me.path.element;
            bbox = me.bbox;
            if (!element) {
                path = me.path.toString();
                element = me.path.element = me.getElement('path');
                me.surface.setElementAttributes(element, {
                    "d": path,
                    "transform": me.matrix.toSvg()
                });
            }
            if (fillGradient && bbox) {
                // This indirectly calls ctx.createLinearGradient or ctx.createRadialGradient,
                // depending on the type of gradient, and returns an instance of
                // Ext.draw.engine.SvgContext.Gradient.
                fill = fillGradient.generateGradient(me, bbox);
            } else {
                fill = me.fillStyle;
            }
            me.surface.setElementAttributes(element, {
                "fill": fill,
                "fill-opacity": me.fillOpacity * me.globalAlpha
            });
        }
    },
    /**
     * Strokes the subpaths of the current default path or the given path with the current
     * stroke style.
     */
    stroke: function() {
        var me = this,
            path, strokeGradient, element, bbox, stroke;
        if (!me.path) {
            return;
        }
        if (me.strokeStyle) {
            strokeGradient = me.strokeGradient;
            element = me.path.element;
            bbox = me.bbox;
            if (!element || !me.path.svgString) {
                path = me.path.toString();
                if (!path) {
                    return;
                }
                element = me.path.element = me.getElement('path');
                me.surface.setElementAttributes(element, {
                    "fill": "none",
                    "d": path,
                    "transform": me.matrix.toSvg()
                });
            }
            if (strokeGradient && bbox) {
                // This indirectly calls ctx.createLinearGradient or ctx.createRadialGradient,
                // depending on the type of gradient, and returns an instance of
                // Ext.draw.engine.SvgContext.Gradient.
                stroke = strokeGradient.generateGradient(me, bbox);
            } else {
                stroke = me.strokeStyle;
            }
            me.surface.setElementAttributes(element, {
                "stroke": stroke,
                "stroke-linecap": me.lineCap,
                "stroke-linejoin": me.lineJoin,
                "stroke-width": me.lineWidth,
                "stroke-opacity": me.strokeOpacity * me.globalAlpha,
                "stroke-dasharray": me.lineDash.join(','),
                "stroke-dashoffset": me.lineDashOffset
            });
            if (me.lineDash.length) {
                me.surface.setElementAttributes(element, {
                    "stroke-dasharray": me.lineDash.join(','),
                    "stroke-dashoffset": me.lineDashOffset
                });
            }
        }
    },
    /**
     * @protected
     *
     * Note: After the method guarantees the transform matrix will be inverted.
     * @param {Object} attr The attribute object
     * @param {Boolean} [transformFillStroke] Indicate whether to transform fill and stroke.
     * If this is not given, then uses `attr.transformFillStroke` instead.
     */
    fillStroke: function(attr, transformFillStroke) {
        var ctx = this,
            fillStyle = ctx.fillStyle,
            strokeStyle = ctx.strokeStyle,
            fillOpacity = ctx.fillOpacity,
            strokeOpacity = ctx.strokeOpacity;
        if (transformFillStroke === undefined) {
            transformFillStroke = attr.transformFillStroke;
        }
        if (!transformFillStroke) {
            attr.inverseMatrix.toContext(ctx);
        }
        if (fillStyle && fillOpacity !== 0) {
            ctx.fill();
        }
        if (strokeStyle && strokeOpacity !== 0) {
            ctx.stroke();
        }
    },
    appendPath: function(path) {
        this.path = path.clone();
    },
    setLineDash: function(lineDash) {
        this.lineDash = lineDash;
    },
    getLineDash: function() {
        return this.lineDash;
    },
    /**
     * Returns an object that represents a linear gradient that paints along the line
     * given by the coordinates represented by the arguments.
     * @param {Number} x0
     * @param {Number} y0
     * @param {Number} x1
     * @param {Number} y1
     * @return {Ext.draw.engine.SvgContext.Gradient}
     */
    createLinearGradient: function(x0, y0, x1, y1) {
        var me = this,
            element = me.surface.getNextDef('linearGradient'),
            gradient;
        me.surface.setElementAttributes(element, {
            "x1": x0,
            "y1": y0,
            "x2": x1,
            "y2": y1,
            "gradientUnits": "userSpaceOnUse"
        });
        gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element);
        return gradient;
    },
    /**
     * Returns a CanvasGradient object that represents a radial gradient that paints
     * along the cone given by the circles represented by the arguments.
     * If either of the radii are negative, throws an IndexSizeError exception.
     * @param {Number} x0
     * @param {Number} y0
     * @param {Number} r0
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} r1
     * @return {Ext.draw.engine.SvgContext.Gradient}
     */
    createRadialGradient: function(x0, y0, r0, x1, y1, r1) {
        var me = this,
            element = me.surface.getNextDef('radialGradient'),
            gradient;
        me.surface.setElementAttributes(element, {
            fx: x0,
            fy: y0,
            cx: x1,
            cy: y1,
            r: r1,
            gradientUnits: 'userSpaceOnUse'
        });
        gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element, r0 / r1);
        return gradient;
    }
});
/**
 * @class Ext.draw.engine.SvgContext.Gradient
 *
 * A class that implements native CanvasGradient interface
 * (https://developer.mozilla.org/en/docs/Web/API/CanvasGradient)
 * and a `toString` method that returns the ID of the gradient.
 */
Ext.define('Ext.draw.engine.SvgContext.Gradient', {
    // Gradients workflow in SVG engine:
    //
    // Inside the 'fill' & 'stroke' methods of the SVG Context
    // we check if the 'ctx.fillGradient' or 'ctx.strokeGradient'
    // objects exist.
    // These objects are instances of Ext.draw.gradient.Gradient
    // and are assigned to the ctx by the sprite's 'useAttributes' method,
    // if the sprite has any gradients.
    //
    // Additionally, we check if the 'ctx.bbox' object exists - the bounding box
    // for the gradients, set by the sprite's 'setGradientBBox' method.
    //
    // If we have both bbox and a valid instance of Ext.draw.gradient.Gradient,
    // the 'generateGradient' method of the instance is called,
    // which in turn calls 'ctx.createLinearGradient' or 'ctx.createRadialGradient'
    // depending on the type of the gradient represented by the instance.
    // These methods create a 'linearGradient' or 'radialGradient' SVG
    // node and wrap it into a Ext.draw.engine.SvgContext.Gradient instance.
    //
    // The Ext.draw.engine.SvgContext.Gradient instance is then used internally
    // by the Ext.draw.gradient.Gradient to add color 'stop' nodes
    // to the gradient node, and by the SVG context when the 'fill' or
    // 'stroke' attribute of a 'path' node is set to the Ext.draw.engine.SvgContext.Gradient
    // instance, which is implicitly converted to a string - a 'url(#id)' reference
    // to the gradient element wrapped by the instance.
    isGradient: true,
    constructor: function(ctx, surface, element, compression) {
        var me = this;
        me.ctx = ctx;
        me.surface = surface;
        me.element = element;
        me.position = 0;
        me.compression = compression || 0;
    },
    /**
     * Adds a color stop with the given color to the gradient at the given offset. 0.0 is the offset
     * at one end of the gradient, 1.0 is the offset at the other end.
     * @param {Number} offset
     * @param {String} color
     */
    addColorStop: function(offset, color) {
        var me = this,
            stop = me.surface.getSvgElement(me.element, 'stop', me.position++),
            compression = me.compression;
        me.surface.setElementAttributes(stop, {
            "offset": (((1 - compression) * offset + compression) * 100).toFixed(2) + '%',
            "stop-color": color,
            "stop-opacity": Ext.util.Color.fly(color).a.toFixed(15)
        });
    },
    toString: function() {
        var children = this.element.dom.childNodes;
        // Removing surplus stops in case existing gradient element with more stops was reused.
        while (children.length > this.position) {
            Ext.fly(children[children.length - 1]).destroy();
        }
        return 'url(#' + this.element.getId() + ')';
    }
});

/**
 * @class Ext.draw.engine.Svg
 * @extends Ext.draw.Surface
 *
 * SVG engine.
 */
Ext.define('Ext.draw.engine.Svg', {
    extend: 'Ext.draw.Surface',
    requires: [
        'Ext.draw.engine.SvgContext'
    ],
    isSVG: true,
    config: {
        /**
         * @cfg {Boolean} highPrecision
         * Nothing needs to be done in high precision mode.
         */
        highPrecision: false
    },
    getElementConfig: function() {
        return {
            reference: 'element',
            style: {
                position: 'absolute'
            },
            children: [
                {
                    reference: 'bodyElement',
                    style: {
                        width: '100%',
                        height: '100%',
                        position: 'relative'
                    },
                    children: [
                        {
                            tag: 'svg',
                            reference: 'svgElement',
                            namespace: "http://www.w3.org/2000/svg",
                            width: '100%',
                            height: '100%',
                            version: 1.1
                        }
                    ]
                }
            ]
        };
    },
    constructor: function(config) {
        var me = this;
        me.callParent([
            config
        ]);
        me.mainGroup = me.createSvgNode("g");
        me.defsElement = me.createSvgNode("defs");
        // me.svgElement is assigned in element creation of Ext.Component.
        me.svgElement.appendChild(me.mainGroup);
        me.svgElement.appendChild(me.defsElement);
        me.ctx = new Ext.draw.engine.SvgContext(me);
    },
    /**
     * Creates a DOM element under the SVG namespace of the given type.
     * @param {String} type The type of the SVG DOM element.
     * @return {*} The created element.
     */
    createSvgNode: function(type) {
        var node = document.createElementNS("http://www.w3.org/2000/svg", type);
        return Ext.get(node);
    },
    /**
     * @private
     * Returns the SVG DOM element at the given position.
     * If it does not already exist or is a different element tag,
     * it will be created and inserted into the DOM.
     * @param {Ext.dom.Element} group The parent DOM element.
     * @param {String} tag The SVG element tag.
     * @param {Number} position The position of the element in the DOM.
     * @return {Ext.dom.Element} The SVG element.
     */
    getSvgElement: function(group, tag, position) {
        var childNodes = group.dom.childNodes,
            length = childNodes.length,
            element;
        if (position < length) {
            element = childNodes[position];
            if (element.tagName === tag) {
                return Ext.get(element);
            } else {
                Ext.destroy(element);
            }
        } else if (position > length) {
            Ext.raise("Invalid position.");
        }
        element = Ext.get(this.createSvgNode(tag));
        if (position === 0) {
            group.insertFirst(element);
        } else {
            element.insertAfter(Ext.fly(childNodes[position - 1]));
        }
        element.cache = {};
        return element;
    },
    /**
     * @private
     * Applies attributes to the given element.
     * @param {Ext.dom.Element} element The DOM element to be applied.
     * @param {Object} attributes The attributes to apply to the element.
     */
    setElementAttributes: function(element, attributes) {
        var dom = element.dom,
            cache = element.cache,
            name, value;
        for (name in attributes) {
            value = attributes[name];
            if (cache[name] !== value) {
                cache[name] = value;
                dom.setAttribute(name, value);
            }
        }
    },
    /**
     * @private
     * Gets the next reference element under the SVG 'defs' tag.
     * @param {String} tagName The type of reference element.
     * @return {Ext.dom.Element} The reference element.
     */
    getNextDef: function(tagName) {
        return this.getSvgElement(this.defsElement, tagName, this.defsPosition++);
    },
    /**
     * @method clearTransform
     * @inheritdoc
     */
    clearTransform: function() {
        var me = this;
        me.mainGroup.set({
            transform: me.matrix.toSvg()
        });
    },
    /**
     * @method clear
     * @inheritdoc
     */
    clear: function() {
        this.ctx.clear();
        this.removeSurplusDefs();
        this.defsPosition = 0;
    },
    removeSurplusDefs: function() {
        var defsElement = this.defsElement,
            defs = defsElement.dom.childNodes,
            ln = defs.length,
            i;
        for (i = ln - 1; i > this.defsPosition; i--) {
            defsElement.removeChild(defs[i]);
        }
    },
    /**
     * @method renderSprite
     * @inheritdoc
     */
    renderSprite: function(sprite) {
        var me = this,
            rect = me.getRect(),
            ctx = me.ctx;
        // This check is simplistic, but should result in a better performance
        // compared to !sprite.isVisible() when most surface sprites are visible.
        if (sprite.attr.hidden || sprite.attr.globalAlpha === 0) {
            // Create an empty group for each hidden sprite,
            // so that when these sprites do become visible,
            // they don't need groups to be created and don't
            // mess up the previous order of elements in the
            // document, i.e. sprites rendered in the next
            // frame reuse the same elements they used in the
            // previous frame.
            ctx.save();
            ctx.restore();
            return;
        }
        // Each sprite is rendered in its own group ('g' element),
        // returned by the `ctx.save` method.
        // Essentially, the group _is_ the sprite.
        sprite.element = ctx.save();
        sprite.preRender(this);
        sprite.useAttributes(ctx, rect);
        if (false === sprite.render(this, ctx, [
            0,
            0,
            rect[2],
            rect[3]
        ])) {
            return false;
        }
        sprite.setDirty(false);
        ctx.restore();
    },
    /**
     * @private
     */
    toSVG: function(size, surfaces) {
        var className = Ext.getClassName(this),
            svg, surface, rect, i;
        svg = '<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"' + ' width="' + size.width + '"' + ' height="' + size.height + '">';
        for (i = 0; i < surfaces.length; i++) {
            surface = surfaces[i];
            if (Ext.getClassName(surface) !== className) {
                
                continue;
            }
            rect = surface.getRect();
            svg += '<g transform="translate(' + rect[0] + ',' + rect[1] + ')">';
            svg += this.serializeNode(surface.svgElement.dom);
            svg += '</g>';
        }
        svg += '</svg>';
        return svg;
    },
    b64EncodeUnicode: function(str) {
        // Since DOMStrings are 16-bit-encoded strings, in most browsers calling window.btoa
        // on a Unicode string will cause a Character Out Of Range exception if a character
        // exceeds the range of a 8-bit ASCII-encoded character. More information:
        // https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
        return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
            return String.fromCharCode('0x' + p1);
        }));
    },
    flatten: function(size, surfaces) {
        var svg = '<?xml version="1.0" standalone="yes"?>';
        svg += this.toSVG(size, surfaces);
        return {
            data: 'data:image/svg+xml;base64,' + this.b64EncodeUnicode(svg),
            type: 'svg'
        };
    },
    /**
     * @private
     * Serializes an SVG DOM element and its children recursively into a string.
     * @param {Object} node DOM element to serialize.
     * @return {String}
     */
    serializeNode: function(node) {
        var result = '',
            i, n, attr, child;
        if (node.nodeType === document.TEXT_NODE) {
            return node.nodeValue;
        }
        result += '<' + node.nodeName;
        if (node.attributes.length) {
            for (i = 0 , n = node.attributes.length; i < n; i++) {
                attr = node.attributes[i];
                result += ' ' + attr.name + '="' + Ext.String.htmlEncode(attr.value) + '"';
            }
        }
        result += '>';
        if (node.childNodes && node.childNodes.length) {
            for (i = 0 , n = node.childNodes.length; i < n; i++) {
                child = node.childNodes[i];
                result += this.serializeNode(child);
            }
        }
        result += '</' + node.nodeName + '>';
        return result;
    },
    /**
     * Destroys the Canvas element and prepares it for Garbage Collection.
     */
    destroy: function() {
        var me = this;
        me.ctx.destroy();
        me.mainGroup.destroy();
        me.defsElement.destroy();
        delete me.mainGroup;
        delete me.defsElement;
        delete me.ctx;
        me.callParent();
    },
    remove: function(sprite, destroySprite) {
        if (sprite && sprite.element) {
            // If sprite has an associated SVG element, remove it from the surface.
            sprite.element.destroy();
            sprite.element = null;
        }
        this.callParent(arguments);
    }
});

// @define Ext.draw.engine.excanvas
/**
 * @private
 */
if (!Ext.draw) {
    Ext.draw = {};
}
if (!Ext.draw.engine) {
    Ext.draw.engine = {};
}
Ext.draw.engine.excanvas = true;
// Copyright 2006 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Known Issues:
//
// * Patterns only support repeat.
// * Radial gradient are not implemented. The VML version of these look very
//   different from the canvas one.
// * Clipping paths are not implemented.
// * Coordsize. The width and height attribute have higher priority than the
//   width and height style values which isn't correct.
// * Painting mode isn't implemented.
// * Canvas width/height should is using content-box by default. IE in
//   Quirks mode will draw the canvas using border-box. Either change your
//   doctype to HTML5
//   (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
//   or use Box Sizing Behavior from WebFX
//   (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
// * Non uniform scaling does not correctly scale strokes.
// * Optimize. There is always room for speed improvements.
/* eslint-disable */
// Only add this code if we do not already have a canvas implementation
if (!document.createElement('canvas').getContext) {
    (function() {
        // alias some functions to make (compiled) code shorter
        var m = Math;
        var mr = m.round;
        var ms = m.sin;
        var mc = m.cos;
        var abs = m.abs;
        var sqrt = m.sqrt;
        // this is used for sub pixel precision
        var Z = 10;
        var Z2 = Z / 2;
        var IE_VERSION = +navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];
        /*
   * @method getContext
   * This function is assigned to the <canvas></canvas> elements as element.getContext().
   * @return {CanvasRenderingContext2D_}
   */
        function getContext() {
            return this.context_ || (this.context_ = new CanvasRenderingContext2D_(this));
        }
        var slice = Array.prototype.slice;
        /*
   * @method bind
   * Binds a function to an object. The returned function will always use the
   * passed in {@code obj} as {@code this}.
   *
   * Example:
   *
   *   g = bind(f, obj, a, b)
   *   g(c, d) // will do f.call(obj, a, b, c, d)
   *
   * @param {Function} f The function to bind the object to
   * @param {Object} obj The object that should act as this when the function
   *     is called
   * @param {*} var_args Rest arguments that will be used as the initial
   *     arguments when the function is called
   * @return {Function} A new function that has bound this
   */
        function bind(f, obj, var_args) {
            var a = slice.call(arguments, 2);
            return function() {
                return f.apply(obj, a.concat(slice.call(arguments)));
            };
        }
        function encodeHtmlAttribute(s) {
            return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
        }
        function addNamespace(doc, prefix, urn) {
            Ext.onReady(function() {
                if (!doc.namespaces[prefix]) {
                    doc.namespaces.add(prefix, urn, '#default#VML');
                }
            });
        }
        function addNamespacesAndStylesheet(doc) {
            addNamespace(doc, 'g_vml_', 'urn:schemas-microsoft-com:vml');
            addNamespace(doc, 'g_o_', 'urn:schemas-microsoft-com:office:office');
            // Setup default CSS.  Only add one style sheet per document
            if (!doc.styleSheets['ex_canvas_']) {
                var ss = doc.createStyleSheet();
                ss.owningElement.id = 'ex_canvas_';
                ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + // default size is 300x150 in Gecko and Opera
                'text-align:left;width:300px;height:150px}';
            }
        }
        // Add namespaces and stylesheet at startup.
        addNamespacesAndStylesheet(document);
        var G_vmlCanvasManager_ = {
                init: function(opt_doc) {
                    var doc = opt_doc || document;
                    // Create a dummy element so that IE will allow canvas elements to be
                    // recognized.
                    doc.createElement('canvas');
                    doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
                },
                init_: function(doc) {
                    // find all canvas elements
                    var els = doc.getElementsByTagName('canvas');
                    for (var i = 0; i < els.length; i++) {
                        this.initElement(els[i]);
                    }
                },
                /*
     * Public initializes a canvas element so that it can be used as canvas
     * element from now on. This is called automatically before the page is
     * loaded but if you are creating elements using createElement you need to
     * make sure this is called on the element.
     * @param {HTMLElement} el The canvas element to initialize.
     * @return {HTMLElement} the element that was created.
     */
                initElement: function(el) {
                    if (!el.getContext) {
                        el.getContext = getContext;
                        // Add namespaces and stylesheet to document of the element.
                        addNamespacesAndStylesheet(el.ownerDocument);
                        // Remove fallback content. There is no way to hide text nodes so we
                        // just remove all childNodes. We could hide all elements and remove
                        // text nodes but who really cares about the fallback content.
                        el.innerHTML = '';
                        // do not use inline function because that will leak memory
                        el.attachEvent('onpropertychange', onPropertyChange);
                        el.attachEvent('onresize', onResize);
                        var attrs = el.attributes;
                        if (attrs.width && attrs.width.specified) {
                            // TODO: use runtimeStyle and coordsize
                            // el.getContext().setWidth_(attrs.width.nodeValue);
                            el.style.width = attrs.width.nodeValue + 'px';
                        } else {
                            el.width = el.clientWidth;
                        }
                        if (attrs.height && attrs.height.specified) {
                            // TODO: use runtimeStyle and coordsize
                            // el.getContext().setHeight_(attrs.height.nodeValue);
                            el.style.height = attrs.height.nodeValue + 'px';
                        } else {
                            el.height = el.clientHeight;
                        }
                    }
                    //el.getContext().setCoordsize_()
                    return el;
                }
            };
        function onPropertyChange(e) {
            var el = e.srcElement;
            switch (e.propertyName) {
                case 'width':
                    el.getContext().clearRect();
                    el.style.width = el.attributes.width.nodeValue + 'px';
                    // In IE8 this does not trigger onresize.
                    el.firstChild.style.width = el.clientWidth + 'px';
                    break;
                case 'height':
                    el.getContext().clearRect();
                    el.style.height = el.attributes.height.nodeValue + 'px';
                    el.firstChild.style.height = el.clientHeight + 'px';
                    break;
            }
        }
        function onResize(e) {
            var el = e.srcElement;
            if (el.firstChild) {
                el.firstChild.style.width = el.clientWidth + 'px';
                el.firstChild.style.height = el.clientHeight + 'px';
            }
        }
        G_vmlCanvasManager_.init();
        // precompute "00" to "FF"
        var decToHex = [];
        for (var i = 0; i < 16; i++) {
            for (var j = 0; j < 16; j++) {
                decToHex[i * 16 + j] = i.toString(16) + j.toString(16);
            }
        }
        function createMatrixIdentity() {
            return [
                [
                    1,
                    0,
                    0
                ],
                [
                    0,
                    1,
                    0
                ],
                [
                    0,
                    0,
                    1
                ]
            ];
        }
        function matrixMultiply(m1, m2) {
            var result = createMatrixIdentity();
            for (var x = 0; x < 3; x++) {
                for (var y = 0; y < 3; y++) {
                    var sum = 0;
                    for (var z = 0; z < 3; z++) {
                        sum += m1[x][z] * m2[z][y];
                    }
                    result[x][y] = sum;
                }
            }
            return result;
        }
        function copyState(o1, o2) {
            o2.fillStyle = o1.fillStyle;
            o2.lineCap = o1.lineCap;
            o2.lineJoin = o1.lineJoin;
            o2.lineDash = o1.lineDash;
            o2.lineWidth = o1.lineWidth;
            o2.miterLimit = o1.miterLimit;
            o2.shadowBlur = o1.shadowBlur;
            o2.shadowColor = o1.shadowColor;
            o2.shadowOffsetX = o1.shadowOffsetX;
            o2.shadowOffsetY = o1.shadowOffsetY;
            o2.strokeStyle = o1.strokeStyle;
            o2.globalAlpha = o1.globalAlpha;
            o2.font = o1.font;
            o2.textAlign = o1.textAlign;
            o2.textBaseline = o1.textBaseline;
            o2.arcScaleX_ = o1.arcScaleX_;
            o2.arcScaleY_ = o1.arcScaleY_;
            o2.lineScale_ = o1.lineScale_;
        }
        var colorData = {
                aliceblue: '#F0F8FF',
                antiquewhite: '#FAEBD7',
                aquamarine: '#7FFFD4',
                azure: '#F0FFFF',
                beige: '#F5F5DC',
                bisque: '#FFE4C4',
                black: '#000000',
                blanchedalmond: '#FFEBCD',
                blueviolet: '#8A2BE2',
                brown: '#A52A2A',
                burlywood: '#DEB887',
                cadetblue: '#5F9EA0',
                chartreuse: '#7FFF00',
                chocolate: '#D2691E',
                coral: '#FF7F50',
                cornflowerblue: '#6495ED',
                cornsilk: '#FFF8DC',
                crimson: '#DC143C',
                cyan: '#00FFFF',
                darkblue: '#00008B',
                darkcyan: '#008B8B',
                darkgoldenrod: '#B8860B',
                darkgray: '#A9A9A9',
                darkgreen: '#006400',
                darkgrey: '#A9A9A9',
                darkkhaki: '#BDB76B',
                darkmagenta: '#8B008B',
                darkolivegreen: '#556B2F',
                darkorange: '#FF8C00',
                darkorchid: '#9932CC',
                darkred: '#8B0000',
                darksalmon: '#E9967A',
                darkseagreen: '#8FBC8F',
                darkslateblue: '#483D8B',
                darkslategray: '#2F4F4F',
                darkslategrey: '#2F4F4F',
                darkturquoise: '#00CED1',
                darkviolet: '#9400D3',
                deeppink: '#FF1493',
                deepskyblue: '#00BFFF',
                dimgray: '#696969',
                dimgrey: '#696969',
                dodgerblue: '#1E90FF',
                firebrick: '#B22222',
                floralwhite: '#FFFAF0',
                forestgreen: '#228B22',
                gainsboro: '#DCDCDC',
                ghostwhite: '#F8F8FF',
                gold: '#FFD700',
                goldenrod: '#DAA520',
                grey: '#808080',
                greenyellow: '#ADFF2F',
                honeydew: '#F0FFF0',
                hotpink: '#FF69B4',
                indianred: '#CD5C5C',
                indigo: '#4B0082',
                ivory: '#FFFFF0',
                khaki: '#F0E68C',
                lavender: '#E6E6FA',
                lavenderblush: '#FFF0F5',
                lawngreen: '#7CFC00',
                lemonchiffon: '#FFFACD',
                lightblue: '#ADD8E6',
                lightcoral: '#F08080',
                lightcyan: '#E0FFFF',
                lightgoldenrodyellow: '#FAFAD2',
                lightgreen: '#90EE90',
                lightgrey: '#D3D3D3',
                lightpink: '#FFB6C1',
                lightsalmon: '#FFA07A',
                lightseagreen: '#20B2AA',
                lightskyblue: '#87CEFA',
                lightslategray: '#778899',
                lightslategrey: '#778899',
                lightsteelblue: '#B0C4DE',
                lightyellow: '#FFFFE0',
                limegreen: '#32CD32',
                linen: '#FAF0E6',
                magenta: '#FF00FF',
                mediumaquamarine: '#66CDAA',
                mediumblue: '#0000CD',
                mediumorchid: '#BA55D3',
                mediumpurple: '#9370DB',
                mediumseagreen: '#3CB371',
                mediumslateblue: '#7B68EE',
                mediumspringgreen: '#00FA9A',
                mediumturquoise: '#48D1CC',
                mediumvioletred: '#C71585',
                midnightblue: '#191970',
                mintcream: '#F5FFFA',
                mistyrose: '#FFE4E1',
                moccasin: '#FFE4B5',
                navajowhite: '#FFDEAD',
                oldlace: '#FDF5E6',
                olivedrab: '#6B8E23',
                orange: '#FFA500',
                orangered: '#FF4500',
                orchid: '#DA70D6',
                palegoldenrod: '#EEE8AA',
                palegreen: '#98FB98',
                paleturquoise: '#AFEEEE',
                palevioletred: '#DB7093',
                papayawhip: '#FFEFD5',
                peachpuff: '#FFDAB9',
                peru: '#CD853F',
                pink: '#FFC0CB',
                plum: '#DDA0DD',
                powderblue: '#B0E0E6',
                rosybrown: '#BC8F8F',
                royalblue: '#4169E1',
                saddlebrown: '#8B4513',
                salmon: '#FA8072',
                sandybrown: '#F4A460',
                seagreen: '#2E8B57',
                seashell: '#FFF5EE',
                sienna: '#A0522D',
                skyblue: '#87CEEB',
                slateblue: '#6A5ACD',
                slategray: '#708090',
                slategrey: '#708090',
                snow: '#FFFAFA',
                springgreen: '#00FF7F',
                steelblue: '#4682B4',
                tan: '#D2B48C',
                thistle: '#D8BFD8',
                tomato: '#FF6347',
                turquoise: '#40E0D0',
                violet: '#EE82EE',
                wheat: '#F5DEB3',
                whitesmoke: '#F5F5F5',
                yellowgreen: '#9ACD32'
            };
        function getRgbHslContent(styleString) {
            var start = styleString.indexOf('(', 3);
            var end = styleString.indexOf(')', start + 1);
            var parts = styleString.substring(start + 1, end).split(',');
            // add alpha if needed
            if (parts.length != 4 || styleString.charAt(3) != 'a') {
                parts[3] = 1;
            }
            return parts;
        }
        function percent(s) {
            return parseFloat(s) / 100;
        }
        function clamp(v, min, max) {
            return Math.min(max, Math.max(min, v));
        }
        function hslToRgb(parts) {
            var r, g, b, h, s, l;
            h = parseFloat(parts[0]) / 360 % 360;
            if (h < 0)  {
                h++;
            }
            
            s = clamp(percent(parts[1]), 0, 1);
            l = clamp(percent(parts[2]), 0, 1);
            if (s == 0) {
                r = g = b = l;
            } else // achromatic
            {
                var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
                var p = 2 * l - q;
                r = hueToRgb(p, q, h + 1 / 3);
                g = hueToRgb(p, q, h);
                b = hueToRgb(p, q, h - 1 / 3);
            }
            return '#' + decToHex[Math.floor(r * 255)] + decToHex[Math.floor(g * 255)] + decToHex[Math.floor(b * 255)];
        }
        function hueToRgb(m1, m2, h) {
            if (h < 0)  {
                h++;
            }
            
            if (h > 1)  {
                h--;
            }
            
            if (6 * h < 1)  {
                return m1 + (m2 - m1) * 6 * h;
            }
            else if (2 * h < 1)  {
                return m2;
            }
            else if (3 * h < 2)  {
                return m1 + (m2 - m1) * (2 / 3 - h) * 6;
            }
            else  {
                return m1;
            }
            
        }
        var processStyleCache = {};
        function processStyle(styleString) {
            if (styleString in processStyleCache) {
                return processStyleCache[styleString];
            }
            var str,
                alpha = 1;
            styleString = String(styleString);
            if (styleString.charAt(0) == '#') {
                str = styleString;
            } else if (/^rgb/.test(styleString)) {
                var parts = getRgbHslContent(styleString);
                var str = '#',
                    n;
                for (var i = 0; i < 3; i++) {
                    if (parts[i].indexOf('%') != -1) {
                        n = Math.floor(percent(parts[i]) * 255);
                    } else {
                        n = +parts[i];
                    }
                    str += decToHex[clamp(n, 0, 255)];
                }
                alpha = +parts[3];
            } else if (/^hsl/.test(styleString)) {
                var parts = getRgbHslContent(styleString);
                str = hslToRgb(parts);
                alpha = parts[3];
            } else {
                str = colorData[styleString] || styleString;
            }
            return processStyleCache[styleString] = {
                color: str,
                alpha: alpha
            };
        }
        var DEFAULT_STYLE = {
                style: 'normal',
                variant: 'normal',
                weight: 'normal',
                size: 10,
                family: 'sans-serif'
            };
        // Internal text style cache
        var fontStyleCache = {};
        function processFontStyle(styleString) {
            if (fontStyleCache[styleString]) {
                return fontStyleCache[styleString];
            }
            var el = document.createElement('div');
            var style = el.style;
            try {
                style.font = styleString;
            } catch (ex) {}
            // Ignore failures to set to invalid font.
            return fontStyleCache[styleString] = {
                style: style.fontStyle || DEFAULT_STYLE.style,
                variant: style.fontVariant || DEFAULT_STYLE.variant,
                weight: style.fontWeight || DEFAULT_STYLE.weight,
                size: style.fontSize || DEFAULT_STYLE.size,
                family: style.fontFamily || DEFAULT_STYLE.family
            };
        }
        function getComputedStyle(style, element) {
            var computedStyle = {};
            for (var p in style) {
                computedStyle[p] = style[p];
            }
            // Compute the size
            var canvasFontSize = parseFloat(element.currentStyle.fontSize),
                fontSize = parseFloat(style.size);
            if (typeof style.size == 'number') {
                computedStyle.size = style.size;
            } else if (style.size.indexOf('px') != -1) {
                computedStyle.size = fontSize;
            } else if (style.size.indexOf('em') != -1) {
                computedStyle.size = canvasFontSize * fontSize;
            } else if (style.size.indexOf('%') != -1) {
                computedStyle.size = (canvasFontSize / 100) * fontSize;
            } else if (style.size.indexOf('pt') != -1) {
                computedStyle.size = fontSize / 0.75;
            } else {
                computedStyle.size = canvasFontSize;
            }
            // Different scaling between normal text and VML text. This was found using
            // trial and error to get the same size as non VML text.
            computedStyle.size *= 0.981;
            return computedStyle;
        }
        function buildStyle(style) {
            return style.style + ' ' + style.variant + ' ' + style.weight + ' ' + style.size + 'px ' + style.family;
        }
        var lineCapMap = {
                'butt': 'flat',
                'round': 'round'
            };
        function processLineCap(lineCap) {
            return lineCapMap[lineCap] || 'square';
        }
        /*
   * This class implements CanvasRenderingContext2D interface as described by
   * the WHATWG.
   * @param {HTMLElement} canvasElement The element that the 2D context should
   * be associated with
   * @private
   */
        function CanvasRenderingContext2D_(canvasElement) {
            this.m_ = createMatrixIdentity();
            this.mStack_ = [];
            this.aStack_ = [];
            this.currentPath_ = [];
            // Canvas context properties
            this.strokeStyle = '#000';
            this.fillStyle = '#000';
            this.lineWidth = 1;
            this.lineJoin = 'miter';
            this.lineDash = [];
            this.lineCap = 'butt';
            this.miterLimit = Z * 1;
            this.globalAlpha = 1;
            this.font = '10px sans-serif';
            this.textAlign = 'left';
            this.textBaseline = 'alphabetic';
            this.canvas = canvasElement;
            var cssText = 'width:' + canvasElement.clientWidth + 'px;height:' + canvasElement.clientHeight + 'px;overflow:hidden;position:absolute';
            var el = canvasElement.ownerDocument.createElement('div');
            el.style.cssText = cssText;
            canvasElement.appendChild(el);
            var overlayEl = el.cloneNode(false);
            // Use a non transparent background.
            overlayEl.style.backgroundColor = 'red';
            overlayEl.style.filter = 'alpha(opacity=0)';
            canvasElement.appendChild(overlayEl);
            this.element_ = el;
            this.arcScaleX_ = 1;
            this.arcScaleY_ = 1;
            this.lineScale_ = 1;
        }
        var contextPrototype = CanvasRenderingContext2D_.prototype;
        contextPrototype.clearRect = function() {
            if (this.textMeasureEl_) {
                this.textMeasureEl_.removeNode(true);
                this.textMeasureEl_ = null;
            }
            this.element_.innerHTML = '';
        };
        contextPrototype.beginPath = function() {
            // TODO: Branch current matrix so that save/restore has no effect
            //       as per safari docs.
            this.currentPath_ = [];
        };
        contextPrototype.moveTo = function(aX, aY) {
            var p = getCoords(this, aX, aY);
            this.currentPath_.push({
                type: 'moveTo',
                x: p.x,
                y: p.y
            });
            this.currentX_ = p.x;
            this.currentY_ = p.y;
        };
        contextPrototype.lineTo = function(aX, aY) {
            var p = getCoords(this, aX, aY);
            this.currentPath_.push({
                type: 'lineTo',
                x: p.x,
                y: p.y
            });
            this.currentX_ = p.x;
            this.currentY_ = p.y;
        };
        contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, aCP2x, aCP2y, aX, aY) {
            var p = getCoords(this, aX, aY);
            var cp1 = getCoords(this, aCP1x, aCP1y);
            var cp2 = getCoords(this, aCP2x, aCP2y);
            bezierCurveTo(this, cp1, cp2, p);
        };
        // Helper function that takes the already fixed cordinates.
        function bezierCurveTo(self, cp1, cp2, p) {
            self.currentPath_.push({
                type: 'bezierCurveTo',
                cp1x: cp1.x,
                cp1y: cp1.y,
                cp2x: cp2.x,
                cp2y: cp2.y,
                x: p.x,
                y: p.y
            });
            self.currentX_ = p.x;
            self.currentY_ = p.y;
        }
        contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
            // the following is lifted almost directly from
            // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
            var cp = getCoords(this, aCPx, aCPy);
            var p = getCoords(this, aX, aY);
            var cp1 = {
                    x: this.currentX_ + 2 / 3 * (cp.x - this.currentX_),
                    y: this.currentY_ + 2 / 3 * (cp.y - this.currentY_)
                };
            var cp2 = {
                    x: cp1.x + (p.x - this.currentX_) / 3,
                    y: cp1.y + (p.y - this.currentY_) / 3
                };
            bezierCurveTo(this, cp1, cp2, p);
        };
        contextPrototype.arc = function(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise) {
            aRadius *= Z;
            var arcType = aClockwise ? 'at' : 'wa';
            var xStart = aX + mc(aStartAngle) * aRadius - Z2;
            var yStart = aY + ms(aStartAngle) * aRadius - Z2;
            var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
            var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
            // IE won't render arches drawn counter clockwise if xStart == xEnd.
            if (xStart == xEnd && !aClockwise) {
                xStart += 0.125;
            }
            // Offset xStart by 1/80 of a pixel. Use something
            // that can be represented in binary
            var p = getCoords(this, aX, aY);
            var pStart = getCoords(this, xStart, yStart);
            var pEnd = getCoords(this, xEnd, yEnd);
            this.currentPath_.push({
                type: arcType,
                x: p.x,
                y: p.y,
                radius: aRadius,
                xStart: pStart.x,
                yStart: pStart.y,
                xEnd: pEnd.x,
                yEnd: pEnd.y
            });
        };
        contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
            this.moveTo(aX, aY);
            this.lineTo(aX + aWidth, aY);
            this.lineTo(aX + aWidth, aY + aHeight);
            this.lineTo(aX, aY + aHeight);
            this.closePath();
        };
        contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
            var oldPath = this.currentPath_;
            this.beginPath();
            this.moveTo(aX, aY);
            this.lineTo(aX + aWidth, aY);
            this.lineTo(aX + aWidth, aY + aHeight);
            this.lineTo(aX, aY + aHeight);
            this.closePath();
            this.stroke();
            this.currentPath_ = oldPath;
        };
        contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
            var oldPath = this.currentPath_;
            this.beginPath();
            this.moveTo(aX, aY);
            this.lineTo(aX + aWidth, aY);
            this.lineTo(aX + aWidth, aY + aHeight);
            this.lineTo(aX, aY + aHeight);
            this.closePath();
            this.fill();
            this.currentPath_ = oldPath;
        };
        contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
            var gradient = new CanvasGradient_('gradient');
            gradient.x0_ = aX0;
            gradient.y0_ = aY0;
            gradient.x1_ = aX1;
            gradient.y1_ = aY1;
            return gradient;
        };
        contextPrototype.createRadialGradient = function(aX0, aY0, aR0, aX1, aY1, aR1) {
            var gradient = new CanvasGradient_('gradientradial');
            gradient.x0_ = aX0;
            gradient.y0_ = aY0;
            gradient.r0_ = aR0;
            gradient.x1_ = aX1;
            gradient.y1_ = aY1;
            gradient.r1_ = aR1;
            return gradient;
        };
        contextPrototype.drawImage = function(image, var_args) {
            var dx, dy, dw, dh, sx, sy, sw, sh;
            // to find the original width we overide the width and height
            var oldRuntimeWidth = image.runtimeStyle.width;
            var oldRuntimeHeight = image.runtimeStyle.height;
            image.runtimeStyle.width = 'auto';
            image.runtimeStyle.height = 'auto';
            // get the original size
            var w = image.width;
            var h = image.height;
            // and remove overides
            image.runtimeStyle.width = oldRuntimeWidth;
            image.runtimeStyle.height = oldRuntimeHeight;
            if (arguments.length == 3) {
                dx = arguments[1];
                dy = arguments[2];
                sx = sy = 0;
                sw = dw = w;
                sh = dh = h;
            } else if (arguments.length == 5) {
                dx = arguments[1];
                dy = arguments[2];
                dw = arguments[3];
                dh = arguments[4];
                sx = sy = 0;
                sw = w;
                sh = h;
            } else if (arguments.length == 9) {
                sx = arguments[1];
                sy = arguments[2];
                sw = arguments[3];
                sh = arguments[4];
                dx = arguments[5];
                dy = arguments[6];
                dw = arguments[7];
                dh = arguments[8];
            } else {
                throw Error('Invalid number of arguments');
            }
            var d = getCoords(this, dx, dy);
            var vmlStr = [];
            var W = 10;
            var H = 10;
            var m = this.m_;
            vmlStr.push(' <g_vml_:group', ' coordsize="', Z * W, ',', Z * H, '"', ' coordorigin="0,0"', ' style="width:', mr(W * m[0][0]), 'px;height:', mr(H * m[1][1]), 'px;position:absolute;', 'top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px; rotation:', mr(Math.atan(m[0][1] / m[1][1]) * 180 / Math.PI), ';');
            vmlStr.push('" >', '<g_vml_:image src="', image.src, '"', ' style="width:', Z * dw, 'px;', ' height:', Z * dh, 'px"', ' cropleft="', sx / w, '"', ' croptop="', sy / h, '"', ' cropright="', (w - sx - sw) / w, '"', ' cropbottom="', (h - sy - sh) / h, '"', ' />', '</g_vml_:group>');
            this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join(''));
        };
        contextPrototype.setLineDash = function(lineDash) {
            if (lineDash.length === 1) {
                lineDash = lineDash.slice();
                lineDash[1] = lineDash[0];
            }
            this.lineDash = lineDash;
        };
        contextPrototype.getLineDash = function() {
            return this.lineDash;
        };
        contextPrototype.stroke = function(aFill) {
            var lineStr = [];
            var W = 10;
            var H = 10;
            lineStr.push('<g_vml_:shape', ' filled="', !!aFill, '"', ' style="position:absolute;width:', W, 'px;height:', H, 'px;left:0px;top:0px;"', ' coordorigin="0,0"', ' coordsize="', Z * W, ',', Z * H, '"', ' stroked="', !aFill, '"', ' path="');
            var min = {
                    x: null,
                    y: null
                };
            var max = {
                    x: null,
                    y: null
                };
            for (var i = 0; i < this.currentPath_.length; i++) {
                var p = this.currentPath_[i];
                var c;
                switch (p.type) {
                    case 'moveTo':
                        c = p;
                        lineStr.push(' m ', mr(p.x), ',', mr(p.y));
                        break;
                    case 'lineTo':
                        lineStr.push(' l ', mr(p.x), ',', mr(p.y));
                        break;
                    case 'close':
                        lineStr.push(' x ');
                        p = null;
                        break;
                    case 'bezierCurveTo':
                        lineStr.push(' c ', mr(p.cp1x), ',', mr(p.cp1y), ',', mr(p.cp2x), ',', mr(p.cp2y), ',', mr(p.x), ',', mr(p.y));
                        break;
                    case 'at':
                    case 'wa':
                        lineStr.push(' ', p.type, ' ', mr(p.x - this.arcScaleX_ * p.radius), ',', mr(p.y - this.arcScaleY_ * p.radius), ' ', mr(p.x + this.arcScaleX_ * p.radius), ',', mr(p.y + this.arcScaleY_ * p.radius), ' ', mr(p.xStart), ',', mr(p.yStart), ' ', mr(p.xEnd), ',', mr(p.yEnd));
                        break;
                }
                // TODO: Following is broken for curves due to
                //       move to proper paths.
                // Figure out dimensions so we can do gradient fills
                // properly
                if (p) {
                    if (min.x == null || p.x < min.x) {
                        min.x = p.x;
                    }
                    if (max.x == null || p.x > max.x) {
                        max.x = p.x;
                    }
                    if (min.y == null || p.y < min.y) {
                        min.y = p.y;
                    }
                    if (max.y == null || p.y > max.y) {
                        max.y = p.y;
                    }
                }
            }
            lineStr.push(' ">');
            if (!aFill) {
                appendStroke(this, lineStr);
            } else {
                appendFill(this, lineStr, min, max);
            }
            lineStr.push('</g_vml_:shape>');
            this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
        };
        function appendStroke(ctx, lineStr) {
            var a = processStyle(ctx.strokeStyle);
            var color = a.color;
            var opacity = a.alpha * ctx.globalAlpha;
            var lineWidth = ctx.lineScale_ * ctx.lineWidth;
            // VML cannot correctly render a line if the width is less than 1px.
            // In that case, we dilute the color to make the line look thinner.
            if (lineWidth < 1) {
                opacity *= lineWidth;
            }
            lineStr.push('<g_vml_:stroke', ' opacity="', opacity, '"', ' joinstyle="', ctx.lineJoin, '"', ' dashstyle="', ctx.lineDash.join(' '), '"', ' miterlimit="', ctx.miterLimit, '"', ' endcap="', processLineCap(ctx.lineCap), '"', ' weight="', lineWidth, 'px"', ' color="', color, '" />');
        }
        function appendFill(ctx, lineStr, min, max) {
            var fillStyle = ctx.fillStyle;
            var arcScaleX = ctx.arcScaleX_;
            var arcScaleY = ctx.arcScaleY_;
            var width = max.x - min.x;
            var height = max.y - min.y;
            if (fillStyle instanceof CanvasGradient_) {
                // TODO: Gradients transformed with the transformation matrix.
                var angle = 0;
                var focus = {
                        x: 0,
                        y: 0
                    };
                // additional offset
                var shift = 0;
                // scale factor for offset
                var expansion = 1;
                if (fillStyle.type_ == 'gradient') {
                    var x0 = fillStyle.x0_ / arcScaleX;
                    var y0 = fillStyle.y0_ / arcScaleY;
                    var x1 = fillStyle.x1_ / arcScaleX;
                    var y1 = fillStyle.y1_ / arcScaleY;
                    var p0 = getCoords(ctx, x0, y0);
                    var p1 = getCoords(ctx, x1, y1);
                    var dx = p1.x - p0.x;
                    var dy = p1.y - p0.y;
                    angle = Math.atan2(dx, dy) * 180 / Math.PI;
                    // The angle should be a non-negative number.
                    if (angle < 0) {
                        angle += 360;
                    }
                    // Very small angles produce an unexpected result because they are
                    // converted to a scientific notation string.
                    if (angle < 1.0E-6) {
                        angle = 0;
                    }
                } else {
                    var p0 = getCoords(ctx, fillStyle.x0_, fillStyle.y0_);
                    focus = {
                        x: (p0.x - min.x) / width,
                        y: (p0.y - min.y) / height
                    };
                    width /= arcScaleX * Z;
                    height /= arcScaleY * Z;
                    var dimension = m.max(width, height);
                    shift = 2 * fillStyle.r0_ / dimension;
                    expansion = 2 * fillStyle.r1_ / dimension - shift;
                }
                // We need to sort the color stops in ascending order by offset,
                // otherwise IE won't interpret it correctly.
                var stops = fillStyle.colors_;
                stops.sort(function(cs1, cs2) {
                    return cs1.offset - cs2.offset;
                });
                var length = stops.length;
                var color1 = stops[0].color;
                var color2 = stops[length - 1].color;
                var opacity1 = stops[0].alpha * ctx.globalAlpha;
                var opacity2 = stops[length - 1].alpha * ctx.globalAlpha;
                var colors = [];
                for (var i = 0; i < length; i++) {
                    var stop = stops[i];
                    colors.push(stop.offset * expansion + shift + ' ' + stop.color);
                }
                // When colors attribute is used, the meanings of opacity and o:opacity2
                // are reversed.
                lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"', ' method="none" focus="100%"', ' color="', color1, '"', ' color2="', color2, '"', ' colors="', colors.join(','), '"', ' opacity="', opacity2, '"', ' g_o_:opacity2="', opacity1, '"', ' angle="', angle, '"', ' focusposition="', focus.x, ',', focus.y, '" />');
            } else if (fillStyle instanceof CanvasPattern_) {
                if (width && height) {
                    var deltaLeft = -min.x;
                    var deltaTop = -min.y;
                    lineStr.push('<g_vml_:fill', ' position="', deltaLeft / width * arcScaleX * arcScaleX, ',', deltaTop / height * arcScaleY * arcScaleY, '"', ' type="tile"', // TODO: Figure out the correct size to fit the scale.
                    //' size="', w, 'px ', h, 'px"',
                    ' src="', fillStyle.src_, '" />');
                }
            } else {
                var a = processStyle(ctx.fillStyle);
                var color = a.color;
                var opacity = a.alpha * ctx.globalAlpha;
                lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity, '" />');
            }
        }
        contextPrototype.fill = function() {
            // Calling `$stroke` here because otherwise we'd call not the native ctx.stroke,
            // but our override from Ext.draw.engine.Canvas.statics.contextOverrides.
            this.$stroke(true);
        };
        contextPrototype.closePath = function() {
            this.currentPath_.push({
                type: 'close'
            });
        };
        function getCoords(ctx, aX, aY) {
            var m = ctx.m_;
            return {
                x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
                y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
            };
        }
        contextPrototype.save = function() {
            var o = {};
            copyState(this, o);
            this.aStack_.push(o);
            this.mStack_.push(this.m_);
        };
        contextPrototype.restore = function() {
            if (this.aStack_.length) {
                copyState(this.aStack_.pop(), this);
                this.m_ = this.mStack_.pop();
            }
        };
        function matrixIsFinite(m) {
            return isFinite(m[0][0]) && isFinite(m[0][1]) && isFinite(m[1][0]) && isFinite(m[1][1]) && isFinite(m[2][0]) && isFinite(m[2][1]);
        }
        function setM(ctx, m, updateLineScale) {
            if (!matrixIsFinite(m)) {
                return;
            }
            ctx.m_ = m;
            if (updateLineScale) {
                // Get the line scale.
                // Determinant of this.m_ means how much the area is enlarged by the
                // transformation. So its square root can be used as a scale factor
                // for width.
                var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
                ctx.lineScale_ = sqrt(abs(det));
            }
        }
        contextPrototype.translate = function(aX, aY) {
            var m1 = [
                    [
                        1,
                        0,
                        0
                    ],
                    [
                        0,
                        1,
                        0
                    ],
                    [
                        aX,
                        aY,
                        1
                    ]
                ];
            setM(this, matrixMultiply(m1, this.m_), false);
        };
        contextPrototype.rotate = function(aRot) {
            var c = mc(aRot);
            var s = ms(aRot);
            var m1 = [
                    [
                        c,
                        s,
                        0
                    ],
                    [
                        -s,
                        c,
                        0
                    ],
                    [
                        0,
                        0,
                        1
                    ]
                ];
            setM(this, matrixMultiply(m1, this.m_), false);
        };
        contextPrototype.scale = function(aX, aY) {
            this.arcScaleX_ *= aX;
            this.arcScaleY_ *= aY;
            var m1 = [
                    [
                        aX,
                        0,
                        0
                    ],
                    [
                        0,
                        aY,
                        0
                    ],
                    [
                        0,
                        0,
                        1
                    ]
                ];
            setM(this, matrixMultiply(m1, this.m_), true);
        };
        contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) {
            var m1 = [
                    [
                        m11,
                        m12,
                        0
                    ],
                    [
                        m21,
                        m22,
                        0
                    ],
                    [
                        dx,
                        dy,
                        1
                    ]
                ];
            setM(this, matrixMultiply(m1, this.m_), true);
        };
        contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) {
            var m = [
                    [
                        m11,
                        m12,
                        0
                    ],
                    [
                        m21,
                        m22,
                        0
                    ],
                    [
                        dx,
                        dy,
                        1
                    ]
                ];
            setM(this, m, true);
        };
        /*
   * The text drawing function.
   * The maxWidth argument isn't taken in account, since no browser supports
   * it yet.
   */
        contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) {
            var m = this.m_,
                delta = 1000,
                left = 0,
                right = delta,
                offset = {
                    x: 0,
                    y: 0
                },
                lineStr = [];
            var fontStyle = getComputedStyle(processFontStyle(this.font), this.element_);
            var fontStyleString = buildStyle(fontStyle);
            var elementStyle = this.element_.currentStyle;
            var textAlign = this.textAlign.toLowerCase();
            switch (textAlign) {
                case 'left':
                case 'center':
                case 'right':
                    break;
                case 'end':
                    textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left';
                    break;
                case 'start':
                    textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left';
                    break;
                default:
                    textAlign = 'left';
            }
            // 1.75 is an arbitrary number, as there is no info about the text baseline
            switch (this.textBaseline) {
                case 'hanging':
                case 'top':
                    offset.y = fontStyle.size / 1.75;
                    break;
                case 'middle':
                    break;
                default:
                case null:
                case 'alphabetic':
                case 'ideographic':
                case 'bottom':
                    offset.y = -fontStyle.size / 3;
                    break;
            }
            switch (textAlign) {
                case 'right':
                    left = delta;
                    right = 0.05;
                    break;
                case 'center':
                    left = right = delta / 2;
                    break;
            }
            var d = getCoords(this, x + offset.x, y + offset.y);
            lineStr.push('<g_vml_:line from="', -left, ' 0" to="', right, ' 0.05" ', ' coordsize="100 100" coordorigin="0 0"', ' filled="', !stroke, '" stroked="', !!stroke, '" style="position:absolute;width:1px;height:1px;left:0px;top:0px;">');
            if (stroke) {
                appendStroke(this, lineStr);
            } else {
                // TODO: Fix the min and max params.
                appendFill(this, lineStr, {
                    x: -left,
                    y: 0
                }, {
                    x: right,
                    y: fontStyle.size
                });
            }
            var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' + m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0';
            var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z);
            lineStr.push('<g_vml_:skew on="t" matrix="', skewM, '" ', ' offset="', skewOffset, '" origin="', left, ' 0" />', '<g_vml_:path textpathok="true" />', '<g_vml_:textpath on="true" string="', encodeHtmlAttribute(text), '" style="v-text-align:', textAlign, ';font:', encodeHtmlAttribute(fontStyleString), '" /></g_vml_:line>');
            this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
        };
        contextPrototype.fillText = function(text, x, y, maxWidth) {
            this.drawText_(text, x, y, maxWidth, false);
        };
        contextPrototype.strokeText = function(text, x, y, maxWidth) {
            this.drawText_(text, x, y, maxWidth, true);
        };
        contextPrototype.measureText = function(text) {
            if (!this.textMeasureEl_) {
                var s = '<span style="position:absolute;' + 'top:-20000px;left:0;padding:0;margin:0;border:none;' + 'white-space:pre;"></span>';
                this.element_.insertAdjacentHTML('beforeEnd', s);
                this.textMeasureEl_ = this.element_.lastChild;
            }
            var doc = this.element_.ownerDocument;
            this.textMeasureEl_.innerHTML = '';
            this.textMeasureEl_.style.font = this.font;
            // Don't use innerHTML or innerText because they allow markup/whitespace.
            this.textMeasureEl_.appendChild(doc.createTextNode(text));
            return {
                width: this.textMeasureEl_.offsetWidth
            };
        };
        /* STUBS */
        contextPrototype.clip = function() {};
        // TODO: Implement
        contextPrototype.arcTo = function() {};
        // TODO: Implement
        contextPrototype.createPattern = function(image, repetition) {
            return new CanvasPattern_(image, repetition);
        };
        // Gradient / Pattern Stubs
        function CanvasGradient_(aType) {
            this.type_ = aType;
            this.x0_ = 0;
            this.y0_ = 0;
            this.r0_ = 0;
            this.x1_ = 0;
            this.y1_ = 0;
            this.r1_ = 0;
            this.colors_ = [];
        }
        CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
            aColor = processStyle(aColor);
            this.colors_.push({
                offset: aOffset,
                color: aColor.color,
                alpha: aColor.alpha
            });
        };
        function CanvasPattern_(image, repetition) {
            assertImageIsValid(image);
            switch (repetition) {
                case 'repeat':
                case null:
                case '':
                    this.repetition_ = 'repeat';
                    break;
                case 'repeat-x':
                case 'repeat-y':
                case 'no-repeat':
                    this.repetition_ = repetition;
                    break;
                default:
                    throwException('SYNTAX_ERR');
            }
            this.src_ = image.src;
            this.width_ = image.width;
            this.height_ = image.height;
        }
        function throwException(s) {
            throw new DOMException_(s);
        }
        function assertImageIsValid(img) {
            if (!img || img.nodeType != 1 || img.tagName != 'IMG') {
                throwException('TYPE_MISMATCH_ERR');
            }
            if (img.readyState != 'complete') {
                throwException('INVALID_STATE_ERR');
            }
        }
        function DOMException_(s) {
            this.code = this[s];
            this.message = s + ': DOM Exception ' + this.code;
        }
        var p = DOMException_.prototype = new Error();
        p.INDEX_SIZE_ERR = 1;
        p.DOMSTRING_SIZE_ERR = 2;
        p.HIERARCHY_REQUEST_ERR = 3;
        p.WRONG_DOCUMENT_ERR = 4;
        p.INVALID_CHARACTER_ERR = 5;
        p.NO_DATA_ALLOWED_ERR = 6;
        p.NO_MODIFICATION_ALLOWED_ERR = 7;
        p.NOT_FOUND_ERR = 8;
        p.NOT_SUPPORTED_ERR = 9;
        p.INUSE_ATTRIBUTE_ERR = 10;
        p.INVALID_STATE_ERR = 11;
        p.SYNTAX_ERR = 12;
        p.INVALID_MODIFICATION_ERR = 13;
        p.NAMESPACE_ERR = 14;
        p.INVALID_ACCESS_ERR = 15;
        p.VALIDATION_ERR = 16;
        p.TYPE_MISMATCH_ERR = 17;
        // set up externs
        G_vmlCanvasManager = G_vmlCanvasManager_;
        CanvasRenderingContext2D = CanvasRenderingContext2D_;
        CanvasGradient = CanvasGradient_;
        CanvasPattern = CanvasPattern_;
        DOMException = DOMException_;
    })();
}
// if

/* global G_vmlCanvasManager */
/**
 * Provides specific methods to draw with 2D Canvas element.
 */
Ext.define('Ext.draw.engine.Canvas', {
    extend: 'Ext.draw.Surface',
    isCanvas: true,
    requires: [
        //<feature legacyBrowser>
        'Ext.draw.engine.excanvas',
        //</feature>
        'Ext.draw.Animator',
        'Ext.draw.Color'
    ],
    config: {
        /**
         * @cfg {Boolean} highPrecision
         * True to have the Canvas use JavaScript Number instead of single precision floating point
         * for transforms.
         *
         * For example, when using data with big numbers to plot line series, the transformation
         * matrix of the canvas will have big elements. Due to the implementation of the SVGMatrix,
         * the elements are represented by 32-bits floats, which will work incorrectly.
         * To compensate for that, we enable the canvas context to perform all the transformations
         * in JavaScript.
         *
         * Do not use this if you are not encountering 32-bit floating point errors problem,
         * since this will result in a performance penalty.
         */
        highPrecision: false
    },
    statics: {
        contextOverrides: {
            /**
             * @ignore
             */
            setGradientBBox: function(bbox) {
                this.bbox = bbox;
            },
            /**
             * Fills the subpaths of the current default path or the given path with the current
             * fill style.
             * @ignore
             */
            fill: function() {
                var fillStyle = this.fillStyle,
                    fillGradient = this.fillGradient,
                    fillOpacity = this.fillOpacity,
                    alpha = this.globalAlpha,
                    bbox = this.bbox;
                if (fillStyle !== Ext.util.Color.RGBA_NONE && fillOpacity !== 0) {
                    if (fillGradient && bbox) {
                        this.fillStyle = fillGradient.generateGradient(this, bbox);
                    }
                    if (fillOpacity !== 1) {
                        this.globalAlpha = alpha * fillOpacity;
                    }
                    this.$fill();
                    if (fillOpacity !== 1) {
                        this.globalAlpha = alpha;
                    }
                    if (fillGradient && bbox) {
                        this.fillStyle = fillStyle;
                    }
                }
            },
            /**
             * Strokes the subpaths of the current default path or the given path with the current
             * stroke style.
             * @ignore
             */
            stroke: function() {
                var strokeStyle = this.strokeStyle,
                    strokeGradient = this.strokeGradient,
                    strokeOpacity = this.strokeOpacity,
                    alpha = this.globalAlpha,
                    bbox = this.bbox;
                if (strokeStyle !== Ext.util.Color.RGBA_NONE && strokeOpacity !== 0) {
                    if (strokeGradient && bbox) {
                        this.strokeStyle = strokeGradient.generateGradient(this, bbox);
                    }
                    if (strokeOpacity !== 1) {
                        this.globalAlpha = alpha * strokeOpacity;
                    }
                    this.$stroke();
                    if (strokeOpacity !== 1) {
                        this.globalAlpha = alpha;
                    }
                    if (strokeGradient && bbox) {
                        this.strokeStyle = strokeStyle;
                    }
                }
            },
            /**
             * @ignore
             */
            fillStroke: function(attr, transformFillStroke) {
                var ctx = this,
                    fillStyle = this.fillStyle,
                    fillOpacity = this.fillOpacity,
                    strokeStyle = this.strokeStyle,
                    strokeOpacity = this.strokeOpacity,
                    shadowColor = ctx.shadowColor,
                    shadowBlur = ctx.shadowBlur,
                    none = Ext.util.Color.RGBA_NONE;
                if (transformFillStroke === undefined) {
                    transformFillStroke = attr.transformFillStroke;
                }
                if (!transformFillStroke) {
                    attr.inverseMatrix.toContext(ctx);
                }
                if (fillStyle !== none && fillOpacity !== 0) {
                    ctx.fill();
                    ctx.shadowColor = none;
                    ctx.shadowBlur = 0;
                }
                if (strokeStyle !== none && strokeOpacity !== 0) {
                    ctx.stroke();
                }
                ctx.shadowColor = shadowColor;
                ctx.shadowBlur = shadowBlur;
            },
            /**
             * 2D Canvas context in IE (up to IE10, inclusive) doesn't support
             * the setLineDash method and the lineDashOffset property.
             * @param dashList An even number of non-negative numbers specifying a dash list.
             */
            setLineDash: function(dashList) {
                if (this.$setLineDash) {
                    this.$setLineDash(dashList);
                }
            },
            getLineDash: function() {
                if (this.$getLineDash) {
                    return this.$getLineDash();
                }
            },
            /**
             * Adds points to the subpath such that the arc described by the circumference of the
             * ellipse described by the arguments, starting at the given start angle and ending at
             * the given end angle, going in the given direction (defaulting to clockwise), is added
             * to the path, connected to the previous point by a straight line.
             * @ignore
             */
            ellipse: function(cx, cy, rx, ry, rotation, start, end, anticlockwise) {
                var cos = Math.cos(rotation),
                    sin = Math.sin(rotation);
                this.transform(cos * rx, sin * rx, -sin * ry, cos * ry, cx, cy);
                this.arc(0, 0, 1, start, end, anticlockwise);
                this.transform(cos / rx, -sin / ry, sin / rx, cos / ry, -(cos * cx + sin * cy) / rx, (sin * cx - cos * cy) / ry);
            },
            /**
             * Uses the given path commands to begin a new path on the canvas.
             * @ignore
             */
            appendPath: function(path) {
                var me = this,
                    i = 0,
                    j = 0,
                    commands = path.commands,
                    params = path.params,
                    ln = commands.length;
                me.beginPath();
                for (; i < ln; i++) {
                    switch (commands[i]) {
                        case 'M':
                            me.moveTo(params[j], params[j + 1]);
                            j += 2;
                            break;
                        case 'L':
                            me.lineTo(params[j], params[j + 1]);
                            j += 2;
                            break;
                        case 'C':
                            me.bezierCurveTo(params[j], params[j + 1], params[j + 2], params[j + 3], params[j + 4], params[j + 5]);
                            j += 6;
                            break;
                        case 'Z':
                            me.closePath();
                            break;
                    }
                }
            },
            save: function() {
                var toSave = this.toSave,
                    ln = toSave.length,
                    obj = ln && {},
                    // Don't allocate memory if we don't have to.
                    i = 0,
                    key;
                for (; i < ln; i++) {
                    key = toSave[i];
                    if (key in this) {
                        obj[key] = this[key];
                    }
                }
                this.state.push(obj);
                this.$save();
            },
            restore: function() {
                var obj = this.state.pop(),
                    key;
                if (obj) {
                    for (key in obj) {
                        this[key] = obj[key];
                    }
                }
                this.$restore();
            }
        }
    },
    splitThreshold: 3000,
    /**
     * @private
     * Properties to be saved/restored in the `save` and `restore` methods.
     */
    toSave: [
        'fillGradient',
        'strokeGradient'
    ],
    /**
     * @property element
     * @inheritdoc
     */
    element: {
        reference: 'element',
        children: [
            {
                reference: 'bodyElement',
                style: {
                    width: '100%',
                    height: '100%',
                    position: 'relative'
                }
            }
        ]
    },
    /**
     * @private
     *
     * Creates the canvas element.
     */
    createCanvas: function() {
        var canvas, overrides, ctx, name;
        canvas = Ext.Element.create({
            tag: 'canvas',
            cls: Ext.baseCSSPrefix + 'surface-canvas'
        });
        // Emulate Canvas in IE8 with VML.
        if (window.G_vmlCanvasManager) {
            G_vmlCanvasManager.initElement(canvas.dom);
            this.isVML = true;
        }
        overrides = Ext.draw.engine.Canvas.contextOverrides;
        ctx = canvas.dom.getContext('2d');
        if (ctx.ellipse) {
            delete overrides.ellipse;
        }
        ctx.state = [];
        ctx.toSave = this.toSave;
        // Saving references to the native Canvas context methods that we'll be overriding.
        for (name in overrides) {
            ctx['$' + name] = ctx[name];
        }
        Ext.apply(ctx, overrides);
        if (this.getHighPrecision()) {
            this.enablePrecisionCompensation(ctx);
        } else {
            this.disablePrecisionCompensation(ctx);
        }
        this.bodyElement.appendChild(canvas);
        this.canvases.push(canvas);
        this.contexts.push(ctx);
    },
    updateHighPrecision: function(highPrecision) {
        var contexts = this.contexts,
            ln = contexts.length,
            i, context;
        for (i = 0; i < ln; i++) {
            context = contexts[i];
            if (highPrecision) {
                this.enablePrecisionCompensation(context);
            } else {
                this.disablePrecisionCompensation(context);
            }
        }
    },
    precisionNames: [
        'rect',
        'fillRect',
        'strokeRect',
        'clearRect',
        'moveTo',
        'lineTo',
        'arc',
        'arcTo',
        'save',
        'restore',
        'updatePrecisionCompensate',
        'setTransform',
        'transform',
        'scale',
        'translate',
        'rotate',
        'quadraticCurveTo',
        'bezierCurveTo',
        'createLinearGradient',
        'createRadialGradient',
        'fillText',
        'strokeText',
        'drawImage'
    ],
    /**
     * @private
     * Clears canvas of compensation for canvas' use of single precision floating point.
     * @param {CanvasRenderingContext2D} ctx The canvas context.
     */
    disablePrecisionCompensation: function(ctx) {
        var regularOverrides = Ext.draw.engine.Canvas.contextOverrides,
            precisionOverrides = this.precisionNames,
            ln = precisionOverrides.length,
            i, name;
        for (i = 0; i < ln; i++) {
            name = precisionOverrides[i];
            if (!(name in regularOverrides)) {
                delete ctx[name];
            }
        }
        this.setDirty(true);
    },
    /**
     * @private
     * Compensate for canvas' use of single precision floating point.
     * @param {CanvasRenderingContext2D} ctx The canvas context.
     */
    enablePrecisionCompensation: function(ctx) {
        var surface = this,
            xx = 1,
            yy = 1,
            dx = 0,
            dy = 0,
            matrix = new Ext.draw.Matrix(),
            transStack = [],
            comp = {},
            regularOverrides = Ext.draw.engine.Canvas.contextOverrides,
            originalCtx = ctx.constructor.prototype,
            /**
             * @cfg {Object} precisionOverrides
             * @ignore
             */
            precisionOverrides = {
                toSave: surface.toSave,
                /**
                 * Adds a new closed subpath to the path, representing the given rectangle.
                 * @return {*}
                 * @ignore
                 */
                rect: function(x, y, w, h) {
                    return originalCtx.rect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
                },
                /**
                 * Paints the given rectangle onto the canvas, using the current fill style.
                 * @ignore
                 */
                fillRect: function(x, y, w, h) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.fillRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Paints the box that outlines the given rectangle onto the canvas, using
                 * the current stroke style.
                 * @ignore
                 */
                strokeRect: function(x, y, w, h) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.strokeRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Clears all pixels on the canvas in the given rectangle to transparent black.
                 * @ignore
                 */
                clearRect: function(x, y, w, h) {
                    return originalCtx.clearRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
                },
                /**
                 * Creates a new subpath with the given point.
                 * @ignore
                 */
                moveTo: function(x, y) {
                    return originalCtx.moveTo.call(this, x * xx + dx, y * yy + dy);
                },
                /**
                 * Adds the given point to the current subpath, connected to the previous one
                 * by a straight line.
                 * @ignore
                 */
                lineTo: function(x, y) {
                    return originalCtx.lineTo.call(this, x * xx + dx, y * yy + dy);
                },
                /**
                 * Adds points to the subpath such that the arc described by the circumference
                 * of the circle described by the arguments, starting at the given start angle
                 * and ending at the given end angle, going in the given direction (defaulting
                 * to clockwise), is added to the path, connected to the previous point
                 * by a straight line.
                 * @ignore
                 */
                arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.arc.call(this, x * xx + dx, y * xx + dy, radius * xx, startAngle, endAngle, anticlockwise);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Adds an arc with the given control points and radius to the current subpath,
                 * connected to the previous point by a straight line.  If two radii are provided,
                 * the first controls the width of the arc's ellipse, and the second controls
                 * the height. If only one is provided, or if they are the same, the arc is from
                 * a circle.
                 *
                 * In the case of an ellipse, the rotation argument controls the clockwise
                 * inclination of the ellipse relative to the x-axis.
                 * @ignore
                 */
                arcTo: function(x1, y1, x2, y2, radius) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.arcTo.call(this, x1 * xx + dx, y1 * yy + dy, x2 * xx + dx, y2 * yy + dy, radius * xx);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Pushes the context state to the state stack.
                 * @ignore
                 */
                save: function() {
                    transStack.push(matrix);
                    matrix = matrix.clone();
                    regularOverrides.save.call(this);
                    originalCtx.save.call(this);
                },
                /**
                 * Pops the state stack and restores the state.
                 * @ignore
                 */
                restore: function() {
                    matrix = transStack.pop();
                    regularOverrides.restore.call(this);
                    originalCtx.restore.call(this);
                    this.updatePrecisionCompensate();
                },
                /**
                 * @ignore
                 */
                updatePrecisionCompensate: function() {
                    matrix.precisionCompensate(surface.devicePixelRatio, comp);
                    xx = comp.xx;
                    yy = comp.yy;
                    dx = comp.dx;
                    dy = comp.dy;
                    originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0);
                },
                /**
                 * @ignore
                 */
                updatePrecisionCompensateRect: function() {
                    matrix.precisionCompensateRect(surface.devicePixelRatio, comp);
                    xx = comp.xx;
                    yy = comp.yy;
                    dx = comp.dx;
                    dy = comp.dy;
                    originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0);
                },
                /**
                 * Changes the transformation matrix to the matrix given by the arguments
                 * as described below.
                 * @ignore
                 */
                setTransform: function(x2x, x2y, y2x, y2y, newDx, newDy) {
                    matrix.set(x2x, x2y, y2x, y2y, newDx, newDy);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Changes the transformation matrix to apply the matrix given by the arguments
                 * as described below.
                 * @ignore
                 */
                transform: function(x2x, x2y, y2x, y2y, newDx, newDy) {
                    matrix.append(x2x, x2y, y2x, y2y, newDx, newDy);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Scales the transformation matrix.
                 * @return {*}
                 * @ignore
                 */
                scale: function(sx, sy) {
                    this.transform(sx, 0, 0, sy, 0, 0);
                },
                /**
                 * Translates the transformation matrix.
                 * @return {*}
                 * @ignore
                 */
                translate: function(dx, dy) {
                    this.transform(1, 0, 0, 1, dx, dy);
                },
                /**
                 * Rotates the transformation matrix.
                 * @return {*}
                 * @ignore
                 */
                rotate: function(radians) {
                    var cos = Math.cos(radians),
                        sin = Math.sin(radians);
                    this.transform(cos, sin, -sin, cos, 0, 0);
                },
                /**
                 * Adds the given point to the current subpath, connected to the previous one by a
                 * quadratic Bézier curve with the given control point.
                 * @return {*}
                 * @ignore
                 */
                quadraticCurveTo: function(cx, cy, x, y) {
                    originalCtx.quadraticCurveTo.call(this, cx * xx + dx, cy * yy + dy, x * xx + dx, y * yy + dy);
                },
                /**
                 * Adds the given point to the current subpath, connected to the previous one
                 * by a cubic Bézier curve with the given control points.
                 * @return {*}
                 * @ignore
                 */
                bezierCurveTo: function(c1x, c1y, c2x, c2y, x, y) {
                    originalCtx.bezierCurveTo.call(this, c1x * xx + dx, c1y * yy + dy, c2x * xx + dx, c2y * yy + dy, x * xx + dx, y * yy + dy);
                },
                /**
                 * Returns an object that represents a linear gradient that paints along the line
                 * given by the coordinates represented by the arguments.
                 * @return {*}
                 * @ignore
                 */
                createLinearGradient: function(x0, y0, x1, y1) {
                    var grad;
                    this.updatePrecisionCompensateRect();
                    grad = originalCtx.createLinearGradient.call(this, x0 * xx + dx, y0 * yy + dy, x1 * xx + dx, y1 * yy + dy);
                    this.updatePrecisionCompensate();
                    return grad;
                },
                /**
                 * Returns a CanvasGradient object that represents a radial gradient that paints
                 * along the cone given by the circles represented by the arguments. If either
                 * of the radii are negative, throws an IndexSizeError exception.
                 * @return {*}
                 * @ignore
                 */
                createRadialGradient: function(x0, y0, r0, x1, y1, r1) {
                    var grad;
                    this.updatePrecisionCompensateRect();
                    grad = originalCtx.createLinearGradient.call(this, x0 * xx + dx, y0 * xx + dy, r0 * xx, x1 * xx + dx, y1 * xx + dy, r1 * xx);
                    this.updatePrecisionCompensate();
                    return grad;
                },
                /**
                 * Fills the given text at the given position. If a maximum width is provided,
                 * the text will be scaled to fit that width if necessary.
                 * @ignore
                 */
                fillText: function(text, x, y, maxWidth) {
                    originalCtx.setTransform.apply(this, matrix.elements);
                    if (typeof maxWidth === 'undefined') {
                        originalCtx.fillText.call(this, text, x, y);
                    } else {
                        originalCtx.fillText.call(this, text, x, y, maxWidth);
                    }
                    this.updatePrecisionCompensate();
                },
                /**
                 * Strokes the given text at the given position. If a
                 * maximum width is provided, the text will be scaled to
                 * fit that width if necessary.
                 * @ignore
                 */
                strokeText: function(text, x, y, maxWidth) {
                    originalCtx.setTransform.apply(this, matrix.elements);
                    if (typeof maxWidth === 'undefined') {
                        originalCtx.strokeText.call(this, text, x, y);
                    } else {
                        originalCtx.strokeText.call(this, text, x, y, maxWidth);
                    }
                    this.updatePrecisionCompensate();
                },
                /**
                 * Fills the subpaths of the current default path or the given path with the current
                 * fill style.
                 * @ignore
                 */
                fill: function() {
                    var fillGradient = this.fillGradient,
                        bbox = this.bbox;
                    this.updatePrecisionCompensateRect();
                    if (fillGradient && bbox) {
                        this.fillStyle = fillGradient.generateGradient(this, bbox);
                    }
                    originalCtx.fill.call(this);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Strokes the subpaths of the current default path or the given path with the
                 * current stroke style.
                 * @ignore
                 */
                stroke: function() {
                    var strokeGradient = this.strokeGradient,
                        bbox = this.bbox;
                    this.updatePrecisionCompensateRect();
                    if (strokeGradient && bbox) {
                        this.strokeStyle = strokeGradient.generateGradient(this, bbox);
                    }
                    originalCtx.stroke.call(this);
                    this.updatePrecisionCompensate();
                },
                /**
                 * Draws the given image onto the canvas.  If the first argument isn't an img,
                 * canvas, or video element, throws a TypeMismatchError exception. If the image
                 * has no image data, throws an InvalidStateError exception. If the one of the
                 * source rectangle dimensions is zero, throws an IndexSizeError exception.
                 * If the image isn't yet fully decoded, then nothing is drawn.
                 * @return {*}
                 * @ignore
                 */
                drawImage: function(img_elem, arg1, arg2, arg3, arg4, dst_x, dst_y, dw, dh) {
                    switch (arguments.length) {
                        case 3:
                            return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy);
                        case 5:
                            return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy, arg3 * xx, arg4 * yy);
                        case 9:
                            return originalCtx.drawImage.call(this, img_elem, arg1, arg2, arg3, arg4, dst_x * xx + dx, dst_y * yy * dy, dw * xx, dh * yy);
                    }
                }
            };
        Ext.apply(ctx, precisionOverrides);
        this.setDirty(true);
    },
    /**
     * Normally, a surface will have a single canvas.
     * However, on certain platforms/browsers there's a limit to how big a canvas can be.
     * 'splitThreshold' is used to determine maximum width/height of a single canvas element.
     * When a surface is wider/taller than the splitThreshold, extra canvas element(s)
     * will be created and tiled inside the surface.
     */
    updateRect: function(rect) {
        this.callParent([
            rect
        ]);
        // eslint-disable-next-line vars-on-top
        var me = this,
            l = Math.floor(rect[0]),
            t = Math.floor(rect[1]),
            r = Math.ceil(rect[0] + rect[2]),
            b = Math.ceil(rect[1] + rect[3]),
            devicePixelRatio = me.devicePixelRatio,
            canvases = me.canvases,
            w = r - l,
            h = b - t,
            splitThreshold = Math.round(me.splitThreshold / devicePixelRatio),
            xSplits = me.xSplits = Math.ceil(w / splitThreshold),
            ySplits = me.ySplits = Math.ceil(h / splitThreshold),
            i, j, k, offsetX, offsetY, dom, width, height;
        for (j = 0 , offsetY = 0; j < ySplits; j++ , offsetY += splitThreshold) {
            for (i = 0 , offsetX = 0; i < xSplits; i++ , offsetX += splitThreshold) {
                k = j * xSplits + i;
                if (k >= canvases.length) {
                    me.createCanvas();
                }
                dom = canvases[k].dom;
                dom.style.left = offsetX + 'px';
                dom.style.top = offsetY + 'px';
                // The Canvas doesn't automatically support hi-DPI displays.
                // We have to actually create a larger canvas (more pixels)
                // while keeping its physical size the same.
                height = Math.min(splitThreshold, h - offsetY);
                if (height * devicePixelRatio !== dom.height) {
                    dom.height = height * devicePixelRatio;
                    dom.style.height = height + 'px';
                }
                width = Math.min(splitThreshold, w - offsetX);
                if (width * devicePixelRatio !== dom.width) {
                    dom.width = width * devicePixelRatio;
                    dom.style.width = width + 'px';
                }
                me.applyDefaults(me.contexts[k]);
            }
        }
        me.activeCanvases = k = xSplits * ySplits;
        while (canvases.length > k) {
            canvases.pop().destroy();
        }
        me.clear();
    },
    /**
     * @method clearTransform
     * @inheritdoc
     */
    clearTransform: function() {
        var me = this,
            xSplits = me.xSplits,
            ySplits = me.ySplits,
            contexts = me.contexts,
            splitThreshold = me.splitThreshold,
            devicePixelRatio = me.devicePixelRatio,
            i, j, k, ctx;
        for (i = 0; i < xSplits; i++) {
            for (j = 0; j < ySplits; j++) {
                k = j * xSplits + i;
                ctx = contexts[k];
                ctx.translate(-splitThreshold * i, -splitThreshold * j);
                ctx.scale(devicePixelRatio, devicePixelRatio);
                me.matrix.toContext(ctx);
            }
        }
    },
    /**
     * @method renderSprite
     * @inheritdoc
     */
    renderSprite: function(sprite) {
        var me = this,
            rect = me.getRect(),
            surfaceMatrix = me.matrix,
            parent = sprite.getParent(),
            matrix = Ext.draw.Matrix.fly([
                1,
                0,
                0,
                1,
                0,
                0
            ]),
            splitThreshold = me.splitThreshold / me.devicePixelRatio,
            xSplits = me.xSplits,
            ySplits = me.ySplits,
            offsetX, offsetY, ctx, bbox, width, height,
            left = 0,
            right,
            top = 0,
            bottom,
            w = rect[2],
            h = rect[3],
            i, j, k;
        while (parent && parent.isSprite) {
            matrix.prependMatrix(parent.matrix || parent.attr && parent.attr.matrix);
            parent = parent.getParent();
        }
        matrix.prependMatrix(surfaceMatrix);
        bbox = sprite.getBBox();
        if (bbox) {
            bbox = matrix.transformBBox(bbox);
        }
        sprite.preRender(me);
        if (sprite.attr.hidden || sprite.attr.globalAlpha === 0) {
            sprite.setDirty(false);
            return;
        }
        // Render this sprite on all Canvas elements it spans, skipping the rest.
        for (j = 0 , offsetY = 0; j < ySplits; j++ , offsetY += splitThreshold) {
            for (i = 0 , offsetX = 0; i < xSplits; i++ , offsetX += splitThreshold) {
                k = j * xSplits + i;
                ctx = me.contexts[k];
                width = Math.min(splitThreshold, w - offsetX);
                height = Math.min(splitThreshold, h - offsetY);
                left = offsetX;
                right = left + width;
                top = offsetY;
                bottom = top + height;
                if (bbox) {
                    if (bbox.x > right || bbox.x + bbox.width < left || bbox.y > bottom || bbox.y + bbox.height < top) {
                        
                        continue;
                    }
                }
                ctx.save();
                sprite.useAttributes(ctx, rect);
                if (false === sprite.render(me, ctx, [
                    left,
                    top,
                    width,
                    height
                ])) {
                    return false;
                }
                ctx.restore();
            }
        }
        sprite.setDirty(false);
    },
    flatten: function(size, surfaces) {
        var targetCanvas = document.createElement('canvas'),
            className = Ext.getClassName(this),
            ratio = this.devicePixelRatio,
            ctx = targetCanvas.getContext('2d'),
            surface, canvas, rect, i, j, xy;
        targetCanvas.width = Math.ceil(size.width * ratio);
        targetCanvas.height = Math.ceil(size.height * ratio);
        for (i = 0; i < surfaces.length; i++) {
            surface = surfaces[i];
            if (Ext.getClassName(surface) !== className) {
                
                continue;
            }
            rect = surface.getRect();
            for (j = 0; j < surface.canvases.length; j++) {
                canvas = surface.canvases[j];
                xy = canvas.getOffsetsTo(canvas.getParent());
                ctx.drawImage(canvas.dom, (rect[0] + xy[0]) * ratio, (rect[1] + xy[1]) * ratio);
            }
        }
        return {
            data: targetCanvas.toDataURL(),
            type: 'png'
        };
    },
    applyDefaults: function(ctx) {
        var none = Ext.util.Color.RGBA_NONE;
        ctx.strokeStyle = none;
        ctx.fillStyle = none;
        ctx.textAlign = 'start';
        ctx.textBaseline = 'alphabetic';
        ctx.miterLimit = 1;
    },
    /**
     * @method clear
     * @inheritdoc
     */
    clear: function() {
        var me = this,
            activeCanvases = me.activeCanvases,
            i, canvas, ctx;
        for (i = 0; i < activeCanvases; i++) {
            canvas = me.canvases[i].dom;
            ctx = me.contexts[i];
            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        }
        me.setDirty(true);
    },
    /**
     * Destroys the Canvas element and prepares it for Garbage Collection.
     */
    destroy: function() {
        var me = this,
            canvases = me.canvases,
            ln = canvases.length,
            i;
        for (i = 0; i < ln; i++) {
            me.contexts[i] = null;
            canvases[i].destroy();
            canvases[i] = null;
        }
        me.contexts = me.canvases = null;
        me.callParent();
    },
    privates: {
        initElement: function() {
            var me = this;
            me.callParent();
            me.canvases = [];
            me.contexts = [];
            me.activeCanvases = me.xSplits = me.ySplits = 0;
        }
    }
}, function() {
    var me = this,
        proto = me.prototype,
        splitThreshold = 1.0E10;
    if (Ext.os.is.Android4 && Ext.browser.is.Chrome) {
        splitThreshold = 3000;
    } else if (Ext.is.iOS) {
        splitThreshold = 2200;
    }
    proto.splitThreshold = splitThreshold;
});

/**
 * The container that holds and manages instances of the {@link Ext.draw.Surface}
 * in which {@link Ext.draw.sprite.Sprite sprites} are rendered.  Draw containers are 
 * used as the foundation for all of the chart classes but may also be created directly 
 * in order to create custom drawings.
 * 
 *     @example
 *     var drawContainer = Ext.create('Ext.draw.Container', {
 *         renderTo: Ext.getBody(),
 *         width:200,
 *         height:200,
 *         sprites: [{
 *             type: 'circle',
 *             fillStyle: '#79BB3F',
 *             r: 100,
 *             x: 100,
 *             y: 100
 *          }]
 *     });
 *
 *     // Uncomment to trigger a download of the painted circle.
 *     // drawContainer.download({
 *     //     filename: 'Circle',
 *     //     url: 'http://svg.sencha.io' // Default server the image data is sent to.
 *     // });
 *
 * In the previous example we created a draw container and configured it with a single 
 * sprite.  The *type* of the sprite is {@link Ext.draw.sprite.Circle circle}, so if you 
 * run this code you'll see a green circle.
 *
 * You can attach sprite event listeners to the draw container with the help of the
 * {@link Ext.draw.plugin.SpriteEvents} plugin.
 *
 * For more information on sprites, the core elements added to a draw container's 
 * surface, refer to the Ext.draw.sprite.Sprite documentation.
 * 
 * For more information on surfaces, the interface owned by the draw container used to 
 * manage all sprites, see the Ext.draw.Surface documentation.
 */
Ext.define('Ext.draw.Container', {
    extend: 'Ext.draw.ContainerBase',
    alternateClassName: 'Ext.draw.Component',
    xtype: 'draw',
    defaultType: 'surface',
    isDrawContainer: true,
    requires: [
        'Ext.draw.Surface',
        'Ext.draw.engine.Svg',
        'Ext.draw.engine.Canvas',
        'Ext.draw.gradient.GradientDefinition'
    ],
    /**
     * @cfg {String} [engine="Ext.draw.engine.Canvas"]
     * Defines the engine (type of surface) used to render draw container contents.  
     * 
     * The render engine is selected automatically depending on the platform used. Priority 
     * is given to the {@link Ext.draw.engine.Canvas} engine due to its performance advantage.
     *
     * You may also set the engine config to be `Ext.draw.engine.Svg` if so desired.
     */
    engine: 'Ext.draw.engine.Canvas',
    /**
     * @event spritemousemove
     * Fires when the mouse is moved on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemouseup
     * Fires when a mouseup event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemousedown
     * Fires when a mousedown event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemouseover
     * Fires when the mouse enters a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemouseout
     * Fires when the mouse exits a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spriteclick
     * Fires when a click event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritedblclick
     * Fires when a double click event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritetap
     * Fires when a tap event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event bodyresize
     * Fires when the size of the draw container body changes.
     * @param {Object} size The object containing 'width' and 'height' of the draw container's body.
     */
    config: {
        cls: [
            Ext.baseCSSPrefix + 'draw-container',
            Ext.baseCSSPrefix + 'unselectable'
        ],
        /**
         * @cfg {Function} [resizeHandler]
         * The resize function that can be configured to have a behavior,
         * e.g. resize draw surfaces based on new draw container dimensions.
         * The `resizeHandler` function takes a single parameter -
         * the size object with `width` and `height` properties.
         *
         * **Note:** Since resize events trigger {@link #renderFrame} calls automatically,
         * return `false` from the resize function, if it also calls `renderFrame`,
         * to prevent double rendering.
         */
        resizeHandler: null,
        /**
         * @cfg {Object[]} sprites
         * Defines a set of sprites to be added to the drawContainer surface.
         *
         * For example:
         *
         *      sprites: [{
         *           type: 'circle',
         *           fillStyle: '#79BB3F',
         *           r: 100,
         *           x: 100,
         *           y: 100
         *      }]
         * 
         */
        sprites: null,
        /**
         * @cfg {Object[]} gradients
         * Defines a set of gradients that can be used as color properties
         * (fillStyle and strokeStyle, but not shadowColor) in sprites.
         * The gradients array is an array of objects with the following properties:
         * - **id** - string - The unique name of the gradient.
         * - **type** - string, optional - The type of the gradient. Available types are: 'linear',
         * 'radial'. Defaults to 'linear'.
         * - **angle** - number, optional - The angle of the gradient in degrees.
         * - **stops** - array - An array of objects with 'color' and 'offset' properties, where
         * 'offset' is a real number from 0 to 1.
         *
         * For example:
         *
         *     gradients: [{
         *         id: 'gradientId1',
         *         type: 'linear',
         *         angle: 45,
         *         stops: [{
         *             offset: 0,
         *             color: 'red'
         *         }, {
         *            offset: 1,
         *            color: 'yellow'
         *         }]
         *     }, {
         *        id: 'gradientId2',
         *        type: 'radial',
         *        stops: [{
         *            offset: 0,
         *            color: '#555',
         *        }, {
         *            offset: 1,
         *            color: '#ddd',
         *        }]
         *     }]
         *
         * Then the sprites can use 'gradientId1' and 'gradientId2' by setting the color attributes
         * to those ids, for example:
         *
         *     sprite.setAttributes({
         *         fillStyle: 'url(#gradientId1)',
         *         strokeStyle: 'url(#gradientId2)'
         *     });
         */
        gradients: [],
        /**
         * @cfg {String} downloadServerUrl
         * The default URL used by the {@link #download} method.
         */
        downloadServerUrl: undefined,
        touchAction: {
            panX: false,
            panY: false,
            pinchZoom: false,
            doubleTapZoom: false
        },
        /**
         * @private
         * @cfg {Object} surfaceZIndexes A map of surface type name to zIndex.
         * The z-indexes to use for the various types of surfaces.
         */
        surfaceZIndexes: {
            main: 1
        }
    },
    /**
     * @private
     * @property {String} [defaultDownloadServerUrl="http://svg.sencha.io"]
     * The default URL used by the {@link #download} method if the {@link #downloadServerUrl}
     * config wasn't set.
     * To override this globally, set the `Ext.draw.Container.prototype.defaultDownloadServerUrl`.
     */
    defaultDownloadServerUrl: 'http://svg.sencha.io',
    /**
     * @property {Array} [supportedFormats=["png", "pdf", "jpeg", "gif"]]
     * A list of export types supported by the server.
     * @private
     */
    supportedFormats: [
        'png',
        'pdf',
        'jpeg',
        'gif'
    ],
    supportedOptions: {
        version: Ext.isNumber,
        data: Ext.isString,
        format: function(format) {
            return Ext.Array.indexOf(this.supportedFormats, format) >= 0;
        },
        filename: Ext.isString,
        width: Ext.isNumber,
        height: Ext.isNumber,
        scale: Ext.isNumber,
        pdf: Ext.isObject,
        jpeg: Ext.isObject
    },
    initAnimator: function() {
        this.frameCallbackId = Ext.draw.Animator.addFrameCallback('renderFrame', this);
    },
    applyDownloadServerUrl: function(url) {
        var defaultUrl = this.defaultDownloadServerUrl;
        if (!url) {
            url = defaultUrl;
            //<debug>
            // Skip this warning when unit testing.
            if (!window.jasmine) {
                Ext.log.warn('Using Sencha\'s download server could expose your data and pose ' + 'a security risk. Please see Ext.draw.Container#download method ' + 'docs for more info. (component id=' + this.getId() + ')');
            }
        }
        //</debug>
        return url;
    },
    applyGradients: function(gradients) {
        var result = [],
            i, n, gradient, offset;
        if (!Ext.isArray(gradients)) {
            return result;
        }
        for (i = 0 , n = gradients.length; i < n; i++) {
            gradient = gradients[i];
            if (!Ext.isObject(gradient)) {
                
                continue;
            }
            // ExtJS only supported linear gradients, so we didn't have to specify their type
            if (typeof gradient.type !== 'string') {
                gradient.type = 'linear';
            }
            if (gradient.angle) {
                gradient.degrees = gradient.angle;
                delete gradient.angle;
            }
            // Convert ExtJS stops object to Touch stops array
            if (Ext.isObject(gradient.stops)) {
                gradient.stops = (function(stops) {
                    var result = [],
                        stop;
                    for (offset in stops) {
                        stop = stops[offset];
                        stop.offset = offset / 100;
                        result.push(stop);
                    }
                    return result;
                })(gradient.stops);
            }
            result.push(gradient);
        }
        Ext.draw.gradient.GradientDefinition.add(result);
        return result;
    },
    applySprites: function(sprites) {
        var result, surface, sprite, i, ln;
        // Never update.
        if (!sprites) {
            return;
        }
        sprites = Ext.Array.from(sprites);
        result = [];
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            surface = sprite.surface;
            if (!(surface && surface.isSurface)) {
                if (Ext.isString(surface)) {
                    surface = this.getSurface(surface);
                    delete sprite.surface;
                } else {
                    surface = this.getSurface('main');
                }
            }
            sprite = surface.add(sprite);
            result.push(sprite);
        }
        return result;
    },
    resizeDelay: 500,
    // in milliseconds
    resizeTimerId: 0,
    lastResizeTime: null,
    /**
     * @private
     * @property
     * Last valid size.
     */
    size: null,
    /**
     * Triggers the {@link #resizeHandler} with the size of the draw container
     * element as the parameter.
     */
    handleResize: function(size, instantly) {
        // See the following:
        // Classic: Ext.draw.ContainerBase.reattachToBody
        //  Modern: Ext.draw.ContainerBase.initialize
        var me = this,
            el = me.element,
            resizeHandler = me.getResizeHandler() || me.defaultResizeHandler,
            resizeDelay = me.resizeDelay,
            lastResizeTime = me.lastResizeTime,
            defer, result;
        if (!el) {
            return;
        }
        size = size || el.getSize();
        if (!(size.width && size.height)) {
            return;
        }
        me.size = size;
        me.stopResizeTimer();
        // Only want to defer when multiple resize events happen in quick succession.
        // That way it doesn't feel luggy during an occasional resize, nor it's too straining
        // when continuously resizing.
        defer = !instantly && lastResizeTime && (Ext.Date.now() - lastResizeTime < resizeDelay);
        if (defer) {
            me.resizeTimerId = Ext.defer(me.handleResize, resizeDelay, me, [
                size,
                true
            ]);
            return;
        }
        me.fireEvent('bodyresize', me, size);
        Ext.callback(resizeHandler, null, [
            size
        ], 0, me);
        if (result !== false) {
            me.renderFrame();
        }
        me.lastResizeTime = Ext.Date.now();
    },
    /**
     * @private
     */
    stopResizeTimer: function() {
        if (this.resizeTimerId) {
            Ext.undefer(this.resizeTimerId);
            this.resizeTimerId = 0;
        }
    },
    defaultResizeHandler: function(size) {
        this.getItems().each(function(surface) {
            surface.setRect([
                0,
                0,
                size.width,
                size.height
            ]);
        });
    },
    /**
     * Get a surface by the given id or create one if it doesn't exist.
     * This will automatically call the {@link #resizeHandler}. Which
     * means that, if no custom resize handler has been provided, the
     * surface will be sized to match the container.
     * If the {@link #method!add} method is used, it is the responsibility
     * of the user to call the {@link #handleResize} method, to update
     * the size of all added surfaces.
     * @param {String} [id="main"]
     * @param {String} type
     * @return {Ext.draw.Surface}
     */
    getSurface: function(id, type) {
        var me = this,
            surfaces = me.getItems(),
            oldCount = surfaces.getCount(),
            zIndexes = me.getSurfaceZIndexes(),
            surface;
        id = id || 'main';
        type = type || id;
        surface = me.createSurface(id);
        if (type in zIndexes) {
            surface.element.setStyle('zIndex', zIndexes[type]);
        }
        if (surfaces.getCount() > oldCount) {
            // Immediately call resize handler of the draw container,
            // so that the newly created surface gets a size.
            me.handleResize(null, true);
        }
        return surface;
    },
    createSurface: function(id) {
        var me = this,
            surfaces = me.getItems(),
            surface;
        id = this.getId() + '-' + (id || 'main');
        surface = surfaces.get(id);
        if (!surface) {
            surface = me.add({
                xclass: me.engine,
                id: id
            });
        }
        return surface;
    },
    /**
     * Render all the surfaces in the container.
     */
    renderFrame: function() {
        var me = this,
            surfaces = me.getItems(),
            i, ln, item;
        for (i = 0 , ln = surfaces.length; i < ln; i++) {
            item = surfaces.items[i];
            if (item.isSurface) {
                item.renderFrame();
            }
        }
    },
    /**
     * @private
     * Returns a slice of the surfaces (items) array of the draw container,
     * optionally sorting them by zIndex.
     * Overridden in subclasses.
     */
    getSurfaces: function(sort) {
        var surfaces = Array.prototype.slice.call(this.items.items),
            zIndexes = this.getSurfaceZIndexes(),
            i, j, surface, zIndex;
        if (sort) {
            // Sort the surfaces by zIndex using insertion sort.
            for (j = 1; j < surfaces.length; j++) {
                surface = surfaces[j];
                zIndex = zIndexes[surface.type];
                i = j - 1;
                while (i >= 0 && zIndexes[surfaces[i].type] > zIndex) {
                    surfaces[i + 1] = surfaces[i];
                    i--;
                }
                surfaces[i + 1] = surface;
            }
        }
        return surfaces;
    },
    /**
     * Produces an image of the chart / drawing.
     * @param {String} [format] Possible options are 'image' (the method will return an 
     * Image object) and 'stream' (the method will return the image as a byte stream).  
     * If missing, the data URI of the drawing's (or chart's) image will be returned.
     * Note: for an SVG based drawing/chart in IE/Edge browsers the method will always
     * return SVG markup instead of a data URI, as 'img' elements won't accept a data
     * URI anyway in those browsers.
     * @return {Object}
     * @return {String} return.data Image element, byte stream or DataURL.
     * @return {String} return.type The type of the data (e.g. 'png' or 'svg').
     */
    getImage: function(format) {
        var size = this.bodyElement.getSize(),
            surfaces = this.getSurfaces(true),
            surface = surfaces[0],
            image, imageElement;
        if ((Ext.isIE || Ext.isEdge) && surface.isSVG) {
            // SVG data URLs don't work in IE/Edge as a source for an 'img' element,
            // so we need to render SVG the usual way.
            image = {
                data: surface.toSVG(size, surfaces),
                type: 'svg-markup'
            };
        } else {
            image = surface.flatten(size, surfaces);
            if (format === 'image') {
                imageElement = new Image();
                imageElement.src = image.data;
                image.data = imageElement;
                return image;
            }
            if (format === 'stream') {
                image.data = image.data.replace(/^data:image\/[^;]+/, 'data:application/octet-stream');
                return image;
            }
        }
        return image;
    },
    /**
     * Downloads an image or PDF of the chart / drawing or opens it in a separate 
     * browser tab/window if the download can't be triggered. The exact behavior is
     * platform and browser specific. For more consistent results on mobile devices use
     * the {@link #preview} method instead. This method doesn't work in IE8.
     *
     * Important: The default download mechanism sends image data to `http://svg.sencha.io`,
     * which is a server operated by Sencha. This can be changed by setting
     * the {@link #downloadServerUrl} config to the address of another server.
     *
     * You can deploy your own server by using the code from the `server` directory
     * in the Charts package. The server is Node.js based and uses PhantomJS to
     * generate images and PDFs from received data.
     *
     * The warning that the default download server is used can be suppressed
     * by explicitly setting the value of the {@link #downloadServerUrl} config
     * to `http://svg.sencha.io`.
     *
     * @param {Object} [config] The following config options are supported:
     *
     * @param {String} config.url The url to post the data to. Defaults to
     * the value of the {@link #downloadServerUrl} config.
     *
     * @param {String} config.format The format of image to export. See the
     * {@link #supportedFormats}. Defaults to 'png' on the Sencha IO server.
     * Note that you can't export to 'svg' format if the {@link Ext.draw.engine.Canvas Canvas}
     * {@link Ext.draw.Container#engine engine} is used.
     *
     * @param {Number} config.width A width to send to the server for
     * configuring the image width. Defaults to natural image width on
     * the Sencha IO server.
     *
     * @param {Number} config.height A height to send to the server for
     * configuring the image height. Defaults to natural image height on
     * the Sencha IO server.
     *
     * @param {String} config.filename The filename of the downloaded image.
     * Defaults to 'chart' on the Sencha IO server. The config.format is used
     * as a filename extension.
     *
     * @param {Number} config.scale The scaling of the downloaded image.
     * Defaults to 1 on the Sencha IO server. The server will try to determine the natural
     * size of the image unless the width/height configs have been set. If the
     * {@link Ext.draw.engine.Canvas Canvas} {@link Ext.draw.Container#engine engine} is
     * used the natural image size will depend on the value of the window.devicePixelRatio.
     * For example, for devices with devicePixelRatio of 2 the produced image will be
     * two times larger than for devices with devicePixelRatio of 1 for the same drawing.
     * This is done so that the users with devices with HiDPI screens get a downloaded
     * image that looks as crisp on their device as the original drawing.
     * If you want image size to be consistent across devices with different device
     * pixel ratios, you can set the value of this config to 1/devicePixelRatio.
     * This parameter is ignored by the Sencha IO server if config.format is set to 'svg'.
     *
     * @param {Object} config.pdf PDF specific options.
     * This config is only used if config.format is set to 'pdf'.
     * The given object should be in either this format:
     *
     *     {
     *       width: '200px',
     *       height: '300px',
     *       border: '0px'
     *     }
     *
     * or this format:
     *
     *     {
     *       format: 'A4',
     *       orientation: 'portrait',
     *       border: '1cm'
     *     }
     *
     * Supported dimension units are: 'mm', 'cm', 'in', 'px'. No unit means 'px'.
     * Supported formats are: 'A3', 'A4', 'A5', 'Legal', 'Letter', 'Tabloid'.
     * Orientation ('portrait', 'landscape') is optional and defaults to 'portrait'.
     *
     * @param {Object} config.jpeg JPEG specific options.
     * This config is only used if config.format is set to 'jpeg'.
     * The given object should be in this format:
     *
     *     {
     *       quality: 80
     *     }
     *
     * Where quality is an integer between 0 and 100.
     *
     * @return {Boolean} True if request was successfully sent to the server.
     */
    download: function(config) {
        var me = this,
            inputs = [],
            markup, name, value;
        if (Ext.isIE8) {
            return false;
        }
        config = config || {};
        config.version = 2;
        if (!config.data) {
            config.data = me.getImage().data;
        }
        for (name in config) {
            if (config.hasOwnProperty(name)) {
                value = config[name];
                if (name in me.supportedOptions) {
                    if (me.supportedOptions[name].call(me, value)) {
                        inputs.push({
                            tag: 'input',
                            type: 'hidden',
                            name: name,
                            value: Ext.String.htmlEncode(Ext.isObject(value) ? Ext.JSON.encode(value) : value)
                        });
                    } else //<debug>
                    {
                        Ext.log.error('Invalid value for image download option "' + name + '": ' + value);
                    }
                } else //</debug>
                //<debug>
                {
                    Ext.log.error('Invalid image download option: "' + name + '"');
                }
            }
        }
        //</debug>
        markup = Ext.dom.Helper.markup({
            tag: 'html',
            children: [
                {
                    tag: 'head'
                },
                {
                    tag: 'body',
                    children: [
                        {
                            tag: 'form',
                            method: 'POST',
                            action: config.url || me.getDownloadServerUrl(),
                            children: inputs
                        },
                        {
                            tag: 'script',
                            type: 'text/javascript',
                            children: 'document.getElementsByTagName("form")[0].submit();'
                        }
                    ]
                }
            ]
        });
        window.open('', 'ImageDownload_' + Date.now()).document.write(markup);
    },
    /**
     * @method preview
     * Displays an image of a Ext.draw.Container on screen.
     * On mobile devices this lets users tap-and-hold to bring up the menu
     * with image saving options.
     * Notes:
     * - some browsers won't save the preview image if it's SVG based
     *   (i.e. generated from a draw container that uses 'Ext.draw.engine.Svg' engine);
     * - some platforms may not have the means of viewing successfully saved SVG images;
     * - this method does not work on IE8.
     */
    doDestroy: function() {
        var me = this,
            callbackId = me.frameCallbackId;
        if (callbackId) {
            Ext.draw.Animator.removeFrameCallback(callbackId);
        }
        me.stopResizeTimer();
        me.callParent();
    }
}, function() {
    if (location.search.match('svg')) {
        Ext.draw.Container.prototype.engine = 'Ext.draw.engine.Svg';
    } else if ((Ext.os.is.BlackBerry && Ext.os.version.getMajor() === 10) || (Ext.browser.is.AndroidStock4 && (Ext.os.version.getMinor() === 1 || Ext.os.version.getMinor() === 2 || Ext.os.version.getMinor() === 3))) {
        // http://code.google.com/p/android/issues/detail?id=37529
        Ext.draw.Container.prototype.engine = 'Ext.draw.engine.Svg';
    }
});

/**
 *
 */
Ext.define('Ext.chart.theme.BaseTheme', {
    defaultsDivCls: 'x-component'
});

/**
 * Abstract class that provides default styles for non-specified things.
 * Should be sub-classed when creating new themes.
 * For example:
 *
 *     Ext.define('Ext.chart.theme.Custom', {
 *         extend: 'Ext.chart.theme.Base',
 *         singleton: true,
 *         alias: 'chart.theme.custom',
 *         config: {
 *             baseColor: '#ff9f00'
 *         }
 *     });
 *
 * Theme provided values will not override the values provided in an instance config.
 * However, if a theme provided value is an object, it will be merged with the value
 * from the instance config, unless the theme provided object has a '$default' key
 * set to 'true'.
 *
 * Certain chart theme configs (e.g. 'fontSize') may use the 'default' value to indicate
 * that they should inherit a value from the corresponding CSS style provided by
 * a framework theme. Additionally, one can use basic binary operators like multiplication,
 * addition and subtraction to derive from the default value, e.g. fontSize: 'default*1.3'.
 *
 * Important: the theme should not use the 'font' shorthand to specify the font of labels
 * and other text elements of a chart. Instead, individual font properties should be used:
 * 'fontStyle', 'fontVariant', 'fontWeight', 'fontSize' and 'fontFamily'.
 */
Ext.define('Ext.chart.theme.Base', {
    extend: 'Ext.chart.theme.BaseTheme',
    mixins: {
        factoryable: 'Ext.mixin.Factoryable'
    },
    requires: [
        'Ext.draw.Color'
    ],
    factoryConfig: {
        type: 'chart.theme'
    },
    isTheme: true,
    isBase: true,
    config: {
        /**
         * @cfg {String/Ext.util.Color} baseColor
         * The base color used to generate the {@link Ext.chart.AbstractChart#colors} of the theme.
         */
        baseColor: null,
        /**
         * @cfg {Array} colors
         *
         * Array of colors/gradients to be used by the theme.
         * Defaults to {@link #colorDefaults}.
         */
        colors: undefined,
        /**
         * @cfg {Object} gradients
         *
         * The gradient config to be used by series' sprites. E.g.:
         *
         *     {
         *       type: 'linear',
         *       degrees: 90
         *     }
         *
         * Please refer to the documentation for the {@link Ext.draw.gradient.Linear linear}
         * and {@link Ext.draw.gradient.Radial radial} gradients for all possible options.
         * The color {@link Ext.draw.gradient.Gradient#stops stops} for the gradients
         * will be generated by the theme based on the {@link #colors} config.
         */
        gradients: null,
        /**
         * @cfg {Object} chart
         * Theme defaults for the chart.
         * Can apply to all charts or just a specific type of chart.
         * For example:
         *
         *     chart: {
         *       defaults: {
         *         background: 'lightgray'
         *       },
         *       polar: {
         *         background: 'green'
         *       }
         *     }
         *
         * The values from the chart.defaults and chart.*type* configs (where *type* is a valid
         * chart xtype, e.g. '{@link Ext.chart.CartesianChart cartesian}' or
         * '{@link Ext.chart.PolarChart polar}') will be applied to corresponding chart configs.
         * E.g., the chart.defaults.background config will set the
         * {@link Ext.chart.AbstractChart#background} config of all charts, where the
         * chart.cartesian.flipXY config will only set the
         * {@link Ext.chart.CartesianChart#flipXY} config of all cartesian charts.
         */
        chart: {
            defaults: {
                captions: {
                    title: {
                        docked: 'top',
                        padding: 5,
                        style: {
                            textAlign: 'center',
                            fontFamily: 'default',
                            fontWeight: '500',
                            fillStyle: 'black',
                            fontSize: 'default*1.6'
                        }
                    },
                    subtitle: {
                        docked: 'top',
                        style: {
                            textAlign: 'center',
                            fontFamily: 'default',
                            fontWeight: 'normal',
                            fillStyle: 'black',
                            fontSize: 'default*1.3'
                        }
                    },
                    credits: {
                        docked: 'bottom',
                        padding: 5,
                        style: {
                            textAlign: 'left',
                            fontFamily: 'default',
                            fontWeight: 'lighter',
                            fillStyle: 'black',
                            fontSize: 'default'
                        }
                    }
                },
                background: 'white'
            }
        },
        /**
         * @cfg {Object} axis
         * Theme defaults for the axes.
         * Can apply to all axes or only axes with a specific position.
         * For example:
         *
         *     axis: {
         *       defaults: {
         *         style: {strokeStyle: 'red'}
         *       },
         *       left: {
         *         title: {fillStyle: 'green'}
         *       }
         *     }
         *
         * The values from the axis.defaults and axis.*position* configs (where *position*
         * is a valid axis {@link Ext.chart.axis.Axis#position}, e.g. 'bottom') will be
         * applied to corresponding {@link Ext.chart.axis.Axis axis} configs.
         * E.g., the axis.defaults.label config will apply to the {@link Ext.chart.axis.Axis#label}
         * config of all axes, where the axis.left.titleMargin config will only apply to the
         * {@link Ext.chart.axis.Axis#titleMargin} config of all axes positioned to the left.
         */
        axis: {
            defaults: {
                label: {
                    x: 0,
                    y: 0,
                    textBaseline: 'middle',
                    textAlign: 'center',
                    fontSize: 'default',
                    fontFamily: 'default',
                    fontWeight: 'default',
                    fillStyle: 'black'
                },
                title: {
                    fillStyle: 'black',
                    fontSize: 'default*1.23',
                    fontFamily: 'default',
                    fontWeight: 'default'
                },
                style: {
                    strokeStyle: 'black'
                },
                grid: {
                    strokeStyle: 'rgb(221, 221, 221)'
                }
            },
            top: {
                style: {
                    textPadding: 5
                }
            },
            bottom: {
                style: {
                    textPadding: 5
                }
            }
        },
        /**
         * @cfg {Object} series
         * Theme defaults for the series.
         * Can apply to all series or just a specific type of series.
         * For example:
         *
         *     series: {
         *       defaults: {
         *         style: {
         *           lineWidth: 2
         *         }
         *       },
         *       bar: {
         *         animation: {
         *           easing: 'bounceOut',
         *           duration: 1000
         *         }
         *       }
         *     }
         *
         * The values from the series.defaults and series.*type* configs (where *type*
         * is a valid series {@link Ext.chart.series.Series#type}, e.g. 'line') will be
         * applied to corresponding series configs.
         * E.g., the series.defaults.label config will apply to the
         * {@link Ext.chart.series.Series#label} config of all series, where the series.line.step
         * config will only apply to the
         * {@link Ext.chart.series.Line#step} config of {@link Ext.chart.series.Line line} series.
         */
        series: {
            defaults: {
                label: {
                    fillStyle: 'black',
                    strokeStyle: 'none',
                    fontFamily: 'default',
                    fontWeight: 'default',
                    fontSize: 'default*1.077',
                    textBaseline: 'middle',
                    textAlign: 'center'
                },
                labelOverflowPadding: 5
            }
        },
        /**
         * @cfg {Object} sprites
         * Default style for the custom chart sprites by type.
         * For example:
         *
         *     sprites: {
         *       text: {
         *         fontWeight: 300
         *       }
         *     }
         *
         * These sprite attribute overrides will apply to custom sprites of all charts
         * specified using the {@link Ext.draw.Container#sprites} config.
         * The overrides are specified by sprite type, e.g. sprites.text config
         * tells to apply given attributes to all {@link Ext.draw.sprite.Text text} sprites.
         */
        sprites: {
            text: {
                fontSize: 'default',
                fontWeight: 'default',
                fontFamily: 'default',
                fillStyle: 'black'
            }
        },
        /**
         * Style information for the {Ext.chart.legend.SpriteLegend sprite legend}.
         * If the {@link Ext.chart.legend.Legend DOM} legend is used, this config is ignored.
         * For additional details see {@link Ext.chart.AbstractChart#legend}.
         * @cfg {Object} legend
         * @cfg {Ext.chart.legend.sprite.Item} legend.item
         * @cfg {Object} legend.border See {@link Ext.chart.legend.SpriteLegend#border}.
         */
        legend: {
            label: {
                fontSize: 14,
                fontWeight: 'default',
                fontFamily: 'default',
                fillStyle: 'black'
            },
            border: {
                lineWidth: 1,
                radius: 4,
                fillStyle: 'none',
                strokeStyle: 'gray'
            },
            background: 'white'
        },
        /**
         * @private
         * An object with the following structure:
         * {
         *   fillStyle: [color, color, ...],
         *   strokeStyle: [color, color, ...],
         *   ...
         * }
         * If missing, generated from the other configs: 'baseColor, 'gradients', 'colors'.
         */
        seriesThemes: undefined,
        markerThemes: {
            type: [
                'circle',
                'cross',
                'plus',
                'square',
                'triangle',
                'diamond'
            ]
        },
        /**
         * @deprecated 5.0.1 Use the {@link Ext.draw.Container#gradients} config instead.
         */
        useGradients: false,
        /**
         * @deprecated 5.0.1 Use the {@link Ext.chart.AbstractChart#background} config instead.
         */
        background: null
    },
    colorDefaults: [
        '#94ae0a',
        '#115fa6',
        '#a61120',
        '#ff8809',
        '#ffd13e',
        '#a61187',
        '#24ad9a',
        '#7c7474',
        '#a66111'
    ],
    constructor: function(config) {
        this.initConfig(config);
        this.resolveDefaults();
    },
    defaultRegEx: /^default([+\-/*]\d+(?:\.\d+)?)?$/,
    defaultOperators: {
        '*': function(v1, v2) {
            return v1 * v2;
        },
        '+': function(v1, v2) {
            return v1 + v2;
        },
        '-': function(v1, v2) {
            return v1 - v2;
        }
    },
    resolveChartDefaults: function() {
        var chart = Ext.clone(this.getChart()),
            chartType, captionName, chartConfig, captionConfig;
        for (chartType in chart) {
            chartConfig = chart[chartType];
            if ('captions' in chartConfig) {
                for (captionName in chartConfig.captions) {
                    captionConfig = chartConfig.captions[captionName];
                    if (captionConfig) {
                        this.replaceDefaults(captionConfig.style);
                    }
                }
            }
        }
        this.setChart(chart);
    },
    resolveDefaults: function() {
        var me = this;
        Ext.onInternalReady(function() {
            var sprites = Ext.clone(me.getSprites()),
                legend = Ext.clone(me.getLegend()),
                axis = Ext.clone(me.getAxis()),
                series = Ext.clone(me.getSeries()),
                div, key, config;
            if (!me.superclass.defaults) {
                div = Ext.getBody().createChild({
                    tag: 'div',
                    cls: me.defaultsDivCls
                });
                me.superclass.defaults = {
                    fontFamily: div.getStyle('fontFamily'),
                    fontWeight: div.getStyle('fontWeight'),
                    fontSize: parseFloat(div.getStyle('fontSize')),
                    fontVariant: div.getStyle('fontVariant'),
                    fontStyle: div.getStyle('fontStyle')
                };
                div.destroy();
            }
            me.resolveChartDefaults();
            me.replaceDefaults(sprites.text);
            me.setSprites(sprites);
            me.replaceDefaults(legend.label);
            me.setLegend(legend);
            for (key in axis) {
                config = axis[key];
                me.replaceDefaults(config.label);
                me.replaceDefaults(config.title);
            }
            me.setAxis(axis);
            for (key in series) {
                config = series[key];
                me.replaceDefaults(config.label);
            }
            me.setSeries(series);
        });
    },
    replaceDefaults: function(target) {
        var me = this,
            defaults = me.superclass.defaults,
            defaultRegEx = me.defaultRegEx,
            key, value, match, binaryFn;
        if (Ext.isObject(target)) {
            for (key in defaults) {
                match = defaultRegEx.exec(target[key]);
                if (match) {
                    value = defaults[key];
                    match = match[1];
                    if (match) {
                        binaryFn = me.defaultOperators[match.charAt(0)];
                        value = Math.round(binaryFn(value, parseFloat(match.substr(1))));
                    }
                    target[key] = value;
                }
            }
        }
    },
    applyBaseColor: function(baseColor) {
        var midColor, midL;
        if (baseColor) {
            midColor = baseColor.isColor ? baseColor : Ext.util.Color.fromString(baseColor);
            midL = midColor.getHSL()[2];
            if (midL < 0.15) {
                midColor = midColor.createLighter(0.3);
            } else if (midL < 0.3) {
                midColor = midColor.createLighter(0.15);
            } else if (midL > 0.85) {
                midColor = midColor.createDarker(0.3);
            } else if (midL > 0.7) {
                midColor = midColor.createDarker(0.15);
            }
            this.setColors([
                midColor.createDarker(0.3).toString(),
                midColor.createDarker(0.15).toString(),
                midColor.toString(),
                midColor.createLighter(0.12).toString(),
                midColor.createLighter(0.24).toString(),
                midColor.createLighter(0.31).toString()
            ]);
        }
        return baseColor;
    },
    applyColors: function(newColors) {
        return newColors || this.colorDefaults;
    },
    updateUseGradients: function(useGradients) {
        if (useGradients) {
            this.updateGradients({
                type: 'linear',
                degrees: 90
            });
        }
    },
    updateBackground: function(background) {
        var chart;
        if (background) {
            chart = this.getChart();
            chart.defaults.background = background;
            this.setChart(chart);
        }
    },
    updateGradients: function(gradients) {
        var colors = this.getColors(),
            items = [],
            gradient, midColor, color, i, ln;
        if (Ext.isObject(gradients)) {
            for (i = 0 , ln = colors && colors.length || 0; i < ln; i++) {
                midColor = Ext.util.Color.fromString(colors[i]);
                if (midColor) {
                    color = midColor.createLighter(0.15).toString();
                    gradient = Ext.apply(Ext.Object.chain(gradients), {
                        stops: [
                            {
                                offset: 1,
                                color: midColor.toString()
                            },
                            {
                                offset: 0,
                                color: color.toString()
                            }
                        ]
                    });
                    items.push(gradient);
                }
            }
            this.setColors(items);
        }
    },
    applySeriesThemes: function(newSeriesThemes) {
        var colors, color;
        // Init the 'colors' config with solid colors generated from the 'baseColor'.
        this.getBaseColor();
        // Init the 'gradients' config with a hardcoded value, if the legacy 'useGradients'
        // config was set to 'true'. This in turn updates the 'colors' config.
        this.getUseGradients();
        // Init the 'gradients' config normally. This also updates the 'colors' config.
        this.getGradients();
        colors = this.getColors();
        // Final colors.
        if (!newSeriesThemes) {
            newSeriesThemes = {
                fillStyle: Ext.Array.clone(colors),
                strokeStyle: Ext.Array.map(colors, function(value) {
                    color = Ext.util.Color.fromString(value.stops ? value.stops[0].color : value);
                    return color.createDarker(0.15).toString();
                })
            };
        }
        return newSeriesThemes;
    }
});

/**
 * @private
 */
Ext.define('Ext.chart.theme.Default', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.default',
        'chart.theme.Default',
        'chart.theme.Base'
    ]
});

/**
 * @private
 */
Ext.define('Ext.chart.Util', {
    singleton: true,
    /**
     * @private
     * Given a `range` array of two (min/max) numbers and an arbitrary array of numbers
     * (`data`), updates the given `range`, if the range of `data` exceeds it.
     * Typically, one would start with the `[NaN, NaN]` range array instance, call the
     * method on multiple datasets with that range instance, then validate it with
     * {@link #validateRange}.
     * @param {Number[]} range
     * @param {Number[]} data
     */
    expandRange: function(range, data) {
        var length = data.length,
            min = range[0],
            max = range[1],
            i, value;
        for (i = 0; i < length; i++) {
            value = data[i];
            // `null` is a "finite" number in JavaScript
            // and greater than any negative number.
            if (value == null || !isFinite(value)) {
                
                continue;
            }
            if (value < min || !isFinite(min)) {
                min = value;
            }
            if (value > max || !isFinite(max)) {
                max = value;
            }
        }
        range[0] = min;
        range[1] = max;
    },
    defaultRange: [
        0,
        1
    ],
    /**
     * @private
     * Makes sure the range exists, is not zero, and its min/max values are finite numbers.
     * If this is not the case, the values from the provided `defaultRange`
     * are used.
     *
     * The range to validate. Never modified.
     * @param {Number[]} range
     * The default range to use, if the given range is not a valid data structure,
     * if both values are infinities, or if both values are the same and dangerously
     * close to either infinity (which makes expansion of the range by the value of
     * `padding` impossible).
     * If only a single value is infinity, the other value will be derived
     * from the finite value by incrementing/decrementing it by the span
     * of the default range towards the infinity.
     * For example, if the `defaultRange` is `[0, 1]`, we have:
     *
     *     [5, Infinity]   --> [5, 6]
     *     [3, -Infinity]  --> [2, 3]
     *     [-Infinity, -5] --> [-6, -5]
     *     [-3, -Infinity] --> [-4, -3]
     *
     * @param {Number[]} [defaultRange=[0, 1]]
     * A non-negative padding to use in case of identical min/max.
     * Note that the range span is not guaranteed to be `padding * 2` in this case,
     * if min/max are close to MIN_SAFE_INTEGER/MAX_SAFE_INTEGER.
     * @param {Number} [padding=0.5]
     * @return {Number[]}
     */
    validateRange: function(range, defaultRange, padding) {
        var isFin0, isFin1;
        defaultRange = defaultRange || this.defaultRange.slice();
        if (!(padding === 0 || padding > 0)) {
            padding = 0.5;
        }
        if (!range || range.length !== 2) {
            return defaultRange;
        }
        range = [
            range[0],
            range[1]
        ];
        if (!range[0]) {
            range[0] = 0;
        }
        if (!range[1]) {
            range[1] = 0;
        }
        if (padding && range[0] === range[1]) {
            range = [
                range[0] - padding,
                range[0] + padding
            ];
            // In case the range values are at Infinity, the expansion above by the value
            // of 'padding' won't do us much good, so we still have to fall back to the
            // 'defaultRange'.
            if (range[0] === range[1]) {
                return defaultRange;
            }
        }
        // Same sign infinities are ruled out at this point.
        isFin0 = isFinite(range[0]);
        isFin1 = isFinite(range[1]);
        if (!isFin0 && !isFin1) {
            return defaultRange;
        }
        // Different sign infinities are ruled out at this point.
        if (isFin0 && !isFin1) {
            range[1] = range[0] + Ext.Number.sign(range[1]) * (defaultRange[1] - defaultRange[0]);
        } else if (isFin1 && !isFin0) {
            range[0] = range[1] + Ext.Number.sign(range[0]) * (defaultRange[1] - defaultRange[0]);
        }
        // All infinities are ruled out at this point.
        return [
            Math.min(range[0], range[1]),
            Math.max(range[0], range[1])
        ];
    },
    applyAnimation: function(animation, oldAnimation) {
        if (!animation) {
            animation = {
                duration: 0
            };
        } else if (animation === true) {
            animation = {
                easing: 'easeInOut',
                duration: 500
            };
        }
        return oldAnimation ? Ext.apply({}, animation, oldAnimation) : animation;
    }
});

/**
 * @class Ext.chart.Markers
 * @extends Ext.draw.sprite.Instancing
 * 
 * Marker sprite. A specialized version of instancing sprite that groups instances.
 * Putting a marker is grouped by its category id. Clearing removes that category.
 */
Ext.define('Ext.chart.Markers', {
    extend: 'Ext.draw.sprite.Instancing',
    isMarkers: true,
    defaultCategory: 'default',
    constructor: function() {
        this.callParent(arguments);
        // `categories` maps category names to a map that maps instance index in category to its
        // global index: categoryName: {instanceIndexInCategory: globalInstanceIndex}
        this.categories = {};
        // The `revisions` map keeps revision numbers of instance categories.
        // When a marker (instance) is put (created or updated), it gets the revision
        // of the category. When a category is cleared, its revision is incremented,
        // but its instances are not removed.
        // An instance is only rendered if its revision matches category revision.
        // In other words, a marker has to be put again after its category has been cleared
        // or it won't render.
        this.revisions = {};
    },
    destroy: function() {
        this.categories = null;
        this.revisions = null;
        this.callParent();
    },
    getMarkerFor: function(category, index) {
        var categoryInstances;
        if (category in this.categories) {
            categoryInstances = this.categories[category];
            if (index in categoryInstances) {
                return this.get(categoryInstances[index]);
            }
        }
    },
    /**
     * Clears the markers in the category.
     * @param {String} category
     */
    clear: function(category) {
        category = category || this.defaultCategory;
        if (!(category in this.revisions)) {
            this.revisions[category] = 1;
        } else {
            this.revisions[category]++;
        }
    },
    clearAll: function() {
        this.callParent();
        this.categories = {};
        this.revisions = {};
    },
    /**
     * Puts a marker in the category with additional attributes.
     * @param {String} category
     * @param {Object} attr
     * @param {String|Number} index
     * @param {Boolean} [bypassNormalization]
     * @param {Boolean} [keepRevision]
     */
    putMarkerFor: function(category, attr, index, bypassNormalization, keepRevision) {
        var me = this,
            categoryInstances, instance;
        category = category || this.defaultCategory;
        categoryInstances = me.categories[category] || (me.categories[category] = {});
        if (index in categoryInstances) {
            me.setAttributesFor(categoryInstances[index], attr, bypassNormalization);
        } else {
            // get the index of the instance created on next line
            categoryInstances[index] = me.getCount();
            me.add(attr, bypassNormalization);
        }
        instance = me.get(categoryInstances[index]);
        if (instance) {
            instance.category = category;
            if (!keepRevision) {
                instance.revision = me.revisions[category] || (me.revisions[category] = 1);
            }
        }
    },
    /**
     *
     * @param {String} category
     * @param {Mixed} index
     * @param {Boolean} [isWithoutTransform]
     */
    getMarkerBBoxFor: function(category, index, isWithoutTransform) {
        var categoryInstances;
        if (category in this.categories) {
            categoryInstances = this.categories[category];
            if (index in categoryInstances) {
                return this.getBBoxFor(categoryInstances[index], isWithoutTransform);
            }
        }
    },
    getBBox: function() {
        return null;
    },
    render: function(surface, ctx, rect) {
        var me = this,
            surfaceRect = surface.getRect(),
            revisions = me.revisions,
            mat = me.attr.matrix,
            template = me.getTemplate(),
            templateAttr = template.attr,
            ln = me.instances.length,
            instance, i;
        mat.toContext(ctx);
        template.preRender(surface, ctx, rect);
        template.useAttributes(ctx, surfaceRect);
        for (i = 0; i < ln; i++) {
            instance = me.get(i);
            if (instance.hidden || instance.revision !== revisions[instance.category]) {
                
                continue;
            }
            ctx.save();
            template.attr = instance;
            template.useAttributes(ctx, surfaceRect);
            template.render(surface, ctx, rect);
            ctx.restore();
        }
        template.attr = templateAttr;
    }
});

/**
 * This is a modifier to place labels and callouts by additional attributes.
 */
Ext.define('Ext.chart.modifier.Callout', {
    extend: 'Ext.draw.modifier.Modifier',
    alternateClassName: 'Ext.chart.label.Callout',
    prepareAttributes: function(attr) {
        if (!attr.hasOwnProperty('calloutOriginal')) {
            attr.calloutOriginal = Ext.Object.chain(attr);
            // No __proto__, nor getPrototypeOf in IE8,
            // so manually saving a reference to 'attr' after chaining.
            attr.calloutOriginal.prototype = attr;
        }
        if (this._lower) {
            this._lower.prepareAttributes(attr.calloutOriginal);
        }
    },
    setAttrs: function(attr, changes) {
        var callout = attr.callout,
            origin = attr.calloutOriginal,
            bbox = attr.bbox.plain,
            width = (bbox.width || 0) + attr.labelOverflowPadding,
            height = (bbox.height || 0) + attr.labelOverflowPadding,
            dx, dy, rotationRads, x, y, calloutPlaceX, calloutPlaceY, calloutVertical, temp;
        if ('callout' in changes) {
            callout = changes.callout;
        }
        if ('callout' in changes || 'calloutPlaceX' in changes || 'calloutPlaceY' in changes || 'x' in changes || 'y' in changes) {
            rotationRads = 'rotationRads' in changes ? origin.rotationRads = changes.rotationRads : origin.rotationRads;
            x = 'x' in changes ? (origin.x = changes.x) : origin.x;
            y = 'y' in changes ? (origin.y = changes.y) : origin.y;
            calloutPlaceX = 'calloutPlaceX' in changes ? changes.calloutPlaceX : attr.calloutPlaceX;
            calloutPlaceY = 'calloutPlaceY' in changes ? changes.calloutPlaceY : attr.calloutPlaceY;
            calloutVertical = 'calloutVertical' in changes ? changes.calloutVertical : attr.calloutVertical;
            // Normalize Rotations
            rotationRads %= Math.PI * 2;
            if (Math.cos(rotationRads) < 0) {
                rotationRads = (rotationRads + Math.PI) % (Math.PI * 2);
            }
            if (rotationRads > Math.PI) {
                rotationRads -= Math.PI * 2;
            }
            if (calloutVertical) {
                rotationRads = rotationRads * (1 - callout) - Math.PI / 2 * callout;
                temp = width;
                width = height;
                height = temp;
            } else {
                rotationRads = rotationRads * (1 - callout);
            }
            changes.rotationRads = rotationRads;
            // Placing a label in the middle of a pie slice (x/y)
            // if callout doesn't exists (callout=0),
            // or outside the pie slice (calloutPlaceX/Y) if it does (callout=1).
            changes.x = x * (1 - callout) + calloutPlaceX * callout;
            changes.y = y * (1 - callout) + calloutPlaceY * callout;
            dx = calloutPlaceX - x;
            dy = calloutPlaceY - y;
            // Finding where the callout line intersects the bbox of the label
            // if it were to go to the center of the label,
            // and make that intersection point the end of the callout line.
            // Effectively, the end of the callout line traces label's bbox when chart is rotated.
            if (Math.abs(dy * width) > Math.abs(dx * height)) {
                // on top/bottom
                if (dy > 0) {
                    changes.calloutEndX = changes.x - (height / 2) * (dx / dy) * callout;
                    changes.calloutEndY = changes.y - (height / 2) * callout;
                } else {
                    changes.calloutEndX = changes.x + (height / 2) * (dx / dy) * callout;
                    changes.calloutEndY = changes.y + (height / 2) * callout;
                }
            } else {
                // on left/right
                if (dx > 0) {
                    changes.calloutEndX = changes.x - width / 2;
                    changes.calloutEndY = changes.y - (width / 2) * (dy / dx) * callout;
                } else {
                    changes.calloutEndX = changes.x + width / 2;
                    changes.calloutEndY = changes.y + (width / 2) * (dy / dx) * callout;
                }
            }
            // Since the length of the callout line is adjusted depending on the label's position
            // and dimensions, we hide the callout line if the length becomes negative.
            if (changes.calloutStartX && changes.calloutStartY) {
                changes.calloutHasLine = (dx > 0 && changes.calloutStartX < changes.calloutEndX) || (dx <= 0 && changes.calloutStartX > changes.calloutEndX) || (dy > 0 && changes.calloutStartY < changes.calloutEndY) || (dy <= 0 && changes.calloutStartY > changes.calloutEndY);
            } else {
                changes.calloutHasLine = true;
            }
        }
        return changes;
    },
    pushDown: function(attr, changes) {
        changes = this.callParent([
            attr.calloutOriginal,
            changes
        ]);
        return this.setAttrs(attr, changes);
    },
    popUp: function(attr, changes) {
        attr = attr.prototype;
        changes = this.setAttrs(attr, changes);
        if (this._upper) {
            return this._upper.popUp(attr, changes);
        } else {
            return Ext.apply(attr, changes);
        }
    }
});

/**
 * @class Ext.chart.sprite.Label
 * @extends Ext.draw.sprite.Text
 *
 * Sprite used to represent labels in series.
 *
 * Important: the actual default values are determined by the theme used.
 * Please see the `label` config of the {@link Ext.chart.theme.Base#axis}.
 */
Ext.define('Ext.chart.sprite.Label', {
    extend: 'Ext.draw.sprite.Text',
    alternateClassName: 'Ext.chart.label.Label',
    requires: [
        'Ext.chart.modifier.Callout'
    ],
    inheritableStatics: {
        def: {
            processors: {
                callout: 'limited01',
                // Meant to be set by the Callout modifier only.
                calloutHasLine: 'bool',
                // The position where the callout would end, if not for the label:
                // callout stops at the bounding box of the label,
                // so the actual point where the callout ends - calloutEndX/Y -
                // is calculated by the Callout modifier.
                calloutPlaceX: 'number',
                calloutPlaceY: 'number',
                // The start/end points used to render the callout line.
                calloutStartX: 'number',
                calloutStartY: 'number',
                calloutEndX: 'number',
                calloutEndY: 'number',
                calloutColor: 'color',
                calloutWidth: 'number',
                calloutVertical: 'bool',
                labelOverflowPadding: 'number',
                display: 'enums(none,under,over,rotate,insideStart,insideEnd,inside,outside)',
                orientation: 'enums(horizontal,vertical)',
                renderer: 'default'
            },
            defaults: {
                callout: 0,
                calloutHasLine: true,
                calloutPlaceX: 0,
                calloutPlaceY: 0,
                calloutStartX: 0,
                calloutStartY: 0,
                calloutEndX: 0,
                calloutEndY: 0,
                calloutWidth: 1,
                calloutVertical: false,
                calloutColor: 'black',
                labelOverflowPadding: 5,
                display: 'none',
                orientation: '',
                renderer: null
            },
            triggers: {
                callout: 'transform',
                calloutPlaceX: 'transform',
                calloutPlaceY: 'transform',
                labelOverflowPadding: 'transform',
                calloutRotation: 'transform',
                display: 'hidden'
            },
            updaters: {
                hidden: function(attr) {
                    attr.hidden = (attr.display === 'none');
                }
            }
        }
    },
    config: {
        /**
         * @cfg {Object} fx Animation configuration.
         */
        animation: {
            customDurations: {
                callout: 200
            }
        },
        /**
         * @cfg {String} field The store record field used by the label sprite.
         *
         * Note: the label sprite is typically used indirectly (by a Ext.chart.MarkerHolder
         * series sprite, via a Ext.chart.Markers sprite, where the latter is passed to the
         * label renderer), so to get to the label field one has to do:
         *
         *     renderer: function (text, sprite, config, data, index) {
         *         var field = sprite.getTemplate().getField();
         *     }
         *
         * To get the actual label sprite instance one can use:
         *
         *     sprite.get(index)
         *
         */
        field: null,
        /**
         * @cfg {Boolean|Object} calloutLine
         *
         * True to draw a line between the label and the chart with the default settings,
         * or an Object that defines the 'color', 'width' and 'length' properties of the line.
         * This config is only applicable when the label is displayed outside the chart.
         *
         * Default value: false.
         */
        calloutLine: true,
        /**
         * @cfg {Number} [hideLessThan=20]
         * Hides labels for pie slices with segment length less than this value (in pixels).
         */
        hideLessThan: 20
    },
    applyCalloutLine: function(calloutLine) {
        if (calloutLine) {
            return Ext.apply({}, calloutLine);
        }
        return calloutLine;
    },
    createModifiers: function() {
        var me = this,
            mods = me.callParent(arguments);
        mods.callout = new Ext.chart.modifier.Callout({
            sprite: me
        });
        mods.animation.setUpper(mods.callout);
        mods.callout.setUpper(mods.target);
    },
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            calloutColor = attr.calloutColor;
        ctx.save();
        ctx.globalAlpha *= attr.callout;
        if (ctx.globalAlpha > 0 && attr.calloutHasLine) {
            if (calloutColor && calloutColor.isGradient) {
                calloutColor = calloutColor.getStops()[0].color;
            }
            ctx.strokeStyle = calloutColor;
            ctx.fillStyle = calloutColor;
            ctx.lineWidth = attr.calloutWidth;
            ctx.beginPath();
            ctx.moveTo(me.attr.calloutStartX, me.attr.calloutStartY);
            ctx.lineTo(me.attr.calloutEndX, me.attr.calloutEndY);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(me.attr.calloutStartX, me.attr.calloutStartY, 1 * attr.calloutWidth, 0, 2 * Math.PI, true);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(me.attr.calloutEndX, me.attr.calloutEndY, 1 * attr.calloutWidth, 0, 2 * Math.PI, true);
            ctx.fill();
        }
        ctx.restore();
        Ext.draw.sprite.Text.prototype.render.apply(me, arguments);
    }
});

/**
 * Series is the abstract class containing the common logic to all chart series.
 * Series includes methods from Labels, Highlights, and Callouts mixins. This class
 * implements the logic of animating, hiding, showing all elements and returning the
 * color of the series to be used as a legend item.
 *
 * ## Listeners
 *
 * The series class supports listeners via the Observable syntax.
 *
 * For example:
 *
 *     Ext.create('Ext.chart.CartesianChart', {
 *         plugins: {
 *             chartitemevents: {
 *                 moveEvents: true
 *             }
 *         },
 *         store: {
 *             fields: ['pet', 'households', 'total'],
 *             data: [
 *                 {pet: 'Cats', households: 38, total: 93},
 *                 {pet: 'Dogs', households: 45, total: 79},
 *                 {pet: 'Fish', households: 13, total: 171}
 *             ]
 *         },
 *         axes: [{
 *             type: 'numeric',
 *             position: 'left'
 *         }, {
 *             type: 'category',
 *             position: 'bottom'
 *         }],
 *         series: [{
 *             type: 'bar',
 *             xField: 'pet',
 *             yField: 'households',
 *             listeners: {
 *                 itemmousemove: function (series, item, event) {
 *                     console.log('itemmousemove', item.category, item.field);
 *                 }
 *             }
 *         }, {
 *             type: 'line',
 *             xField: 'pet',
 *             yField: 'total',
 *             marker: true
 *         }]
 *     });
 *
 */
Ext.define('Ext.chart.series.Series', {
    requires: [
        'Ext.chart.Util',
        'Ext.chart.Markers',
        'Ext.chart.sprite.Label',
        'Ext.tip.ToolTip'
    ],
    mixins: [
        'Ext.mixin.Observable',
        'Ext.mixin.Bindable'
    ],
    isSeries: true,
    defaultBindProperty: 'store',
    /**
     * @property {String} type
     * The type of series. Set in subclasses.
     * @protected
     */
    type: null,
    /**
     * @property {String} seriesType
     * Default series sprite type.
     */
    seriesType: 'sprite',
    identifiablePrefix: 'ext-line-',
    observableType: 'series',
    darkerStrokeRatio: 0.15,
    /**
     * @event itemmousemove
     * Fires when the mouse is moved on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseup
     * Fires when a mouseup event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmousedown
     * Fires when a mousedown event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseover
     * Fires when the mouse enters a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseout
     * Fires when the mouse exits a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemclick
     * Fires when a click event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemdblclick
     * Fires when a double click event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtap
     * Fires when a tap event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event chartattached
     * Fires when the {@link Ext.chart.AbstractChart} has been attached to this series.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Ext.chart.series.Series} series
     */
    /**
     * @event chartdetached
     * Fires when the {@link Ext.chart.AbstractChart} has been detached from this series.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Ext.chart.series.Series} series
     */
    /**
     * @event storechange
     * Fires when the store of the series changes.
     * @param {Ext.chart.series.Series} series
     * @param {Ext.data.Store} newStore
     * @param {Ext.data.Store} oldStore
     */
    config: {
        /**
         * @private
         * @cfg {Object} chart The chart that the series is bound.
         */
        chart: null,
        /**
         * @cfg {String|String[]} title
         * The human-readable name of the series (displayed in the legend).
         * If the series is stacked (has multiple components in it) this
         * should be an array, where each string corresponds to a stacked component.
         */
        title: null,
        /**
         * @cfg {Function} renderer
         * A function that can be provided to set custom styling properties to each
         * rendered element. It receives `(sprite, config, rendererData, index)`
         * as parameters.
         *
         * @param {Object} sprite The sprite affected by the renderer.
         * The visual attributes are in `sprite.attr`.
         * The data field is available in `sprite.getField()`.
         * @param {Object} config The sprite configuration, which varies with the series
         * and the type of sprite. For instance, a Line chart sprite might have just the
         * `x` and `y` properties while a Bar chart sprite also has `width` and `height`.
         * A `type` might be present too. For instance to draw each marker and each segment
         * of a Line chart, the renderer is called with the `config.type` set to either
         * `marker` or `line`.
         * @param {Object} rendererData A record with different properties depending on
         * the type of chart. The only guaranteed property is `rendererData.store`, the
         * store used by the series. In some cases, a store may not exist: for instance
         * a Gauge chart may read its value directly from its configuration; in this case
         * rendererData.store is null and the value is available in rendererData.value.
         * @param {Number} index The index of the sprite. It is usually the index of the
         * store record associated with the sprite, in which case the record can be obtained
         * with `store.getData().items[index]`. If the chart is not associated with a store,
         * the index represents the index of the sprite within the series. For instance
         * a Gauge chart may have as many sprites as there are sectors in the background of
         * the gauge, plus one for the needle.
         *
         * @return {Object} The attributes that have been changed or added.
         * Note: it is usually possible to add or modify the attributes directly into the
         * `config` parameter and not return anything, but returning an object with only
         * those attributes that have been changed may allow for optimizations in the
         * rendering of some series. Example to draw every other marker in red:
         *
         *      renderer: function (sprite, config, rendererData, index) {
         *          if (config.type === 'marker') {
         *              return { strokeStyle: (index % 2 === 0 ? 'red' : 'black') };
         *          }
         *      }
         *
         * @controllable
         */
        renderer: null,
        /**
         * @cfg {Boolean} showInLegend
         * Whether to show this series in the legend.
         */
        showInLegend: true,
        /**
         * @private
         * Trigger drawlistener flag
         */
        triggerAfterDraw: false,
        /**
         * @private
         */
        theme: null,
        /**
         * @cfg {Object} style Custom style configuration for the sprite used in the series.
         * It overrides the style that is provided by the current theme.
         */
        style: {},
        /**
         * @cfg {Object} subStyle This is the cyclic used if the series has multiple sprites.
         */
        subStyle: {},
        /**
         * @private
         * @cfg {Object} themeStyle Style configuration that is provided by the current theme.
         * It is composed of five objects:
         * @cfg {Object} themeStyle.style Properties common to all the series,
         * for instance the 'lineWidth'.
         * @cfg {Object} themeStyle.subStyle Cyclic used if the series has multiple sprites.
         * @cfg {Object} themeStyle.label Sprite config for the labels,
         * for instance the font and color.
         * @cfg {Object} themeStyle.marker Sprite config for the markers,
         * for instance the size and stroke color.
         * @cfg {Object} themeStyle.markerSubStyle Cyclic used if series have multiple marker
         * sprites.
         */
        themeStyle: {},
        /**
         * @cfg {Array} colors
         * An array of color values which is used, in order of appearance, by the series.
         * Each series can request one or more colors from the array. Radar, Scatter or Line charts
         * require just one color each. Candlestick and OHLC require two
         * (1 for drops + 1 for rises). Pie charts and Stacked charts (like Bar or Pie charts)
         * require one color for each data category they represent, so one color for each slice
         * of a Pie chart or each segment (not bar) of a Bar chart.
         * It overrides the colors that are provided by the current theme.
         */
        colors: null,
        /**
         * @cfg {Boolean|Number} useDarkerStrokeColor
         * Colors for the series can be set directly through the 'colors' config, or indirectly
         * with the current theme or the 'colors' config that is set onto the chart. These colors
         * are used as "fill color". Set this config to true, if you want a darker color for the
         * strokes. Set it to false if you want to use the same color as the fill color.
         * Alternatively, you can set it to a number between 0 and 1 to control how much darker
         * the strokes should be.
         * Note: this should be initial config and cannot be changed later on.
         */
        useDarkerStrokeColor: true,
        /**
         * @cfg {Object} store The store to use for this series. If not specified,
         * the series will use the chart's {@link Ext.chart.AbstractChart#store store}.
         */
        store: null,
        /**
         * @cfg {Object} label
         * Object with the following properties:
         *
         * @cfg {String} label.display
         *
         * Specifies the presence and position of the labels.
         * The possible values depend on the series type.
         * For Line and Scatter series: 'under' | 'over' | 'rotate'.
         * For Bar and 3D Bar series: 'insideStart' | 'insideEnd' | 'outside'.
         * For Pie series: 'inside' | 'outside' | 'rotate' | 'horizontal' | 'vertical'.
         * Area, Radar and Candlestick series don't support labels.
         * For Area and Radar series please consider using {@link #tooltip tooltips} instead.
         * 3D Pie series currently always display labels 'outside'.
         * For all series: 'none' hides the labels.
         *
         * Default value: 'none'.
         *
         * @cfg {String} label.color
         *
         * The color of the label text.
         *
         * Default value: '#000' (black).
         *
         * @cfg {String|String[]} label.field
         *
         * The name(s) of the field(s) to be displayed in the labels. If your chart has 3 series
         * that correspond to the fields 'a', 'b', and 'c' of your model, and you only want to
         * display labels for the series 'c', you must still provide an array `[null, null, 'c']`.
         *
         * Default value: null.
         *
         * @cfg {String} label.font
         *
         * The font used for the labels.
         *
         * Default value: '14px Helvetica'.
         *
         * @cfg {String} label.orientation
         *
         * Either 'horizontal' or 'vertical'. If not set (default), the orientation is inferred
         * from the value of the flipXY property of the series.
         *
         * Default value: ''.
         *
         * @cfg {Function} label.renderer
         *
         * Optional function for formatting the label into a displayable value.
         *
         * The arguments to the method are:
         *
         *   - *`text`*, *`sprite`*, *`config`*, *`rendererData`*, *`index`*
         *
         *     Label's renderer is passed the same arguments as {@link #renderer}
         *     plus one extra 'text' argument which comes first.
         *
         * @return {Object|String} The attributes that have been changed or added,
         * or the text for the label.
         * Example to enclose every other label in parentheses:
         *
         *      renderer: function (text) {
         *          if (index % 2 == 0) {
         *              return '(' + text + ')'
         *          }
         *      }
         */
        label: null,
        /**
         * @cfg {Number} labelOverflowPadding
         * Extra distance value for which the labelOverflow listener is triggered.
         */
        labelOverflowPadding: null,
        /**
         * @cfg {Boolean} showMarkers
         * Whether markers should be displayed at the data points along the line. If true,
         * then the {@link #marker} config item will determine the markers' styling.
         */
        showMarkers: true,
        /**
         * @cfg {Object|Boolean} marker
         * The sprite template used by marker instances on the series.
         * If the value of the marker config is set to `true` or the type
         * of the sprite instance is not specified, the {@link Ext.draw.sprite.Circle}
         * sprite will be used.
         *
         * Examples:
         *
         *     marker: true
         *
         *     marker: {
         *         radius: 8
         *     }
         *
         *     marker: {
         *         type: 'arrow',
         *         animation: {
         *             duration: 200,
         *             easing: 'backOut'
         *         }
         *     }
         */
        marker: null,
        /**
         * @cfg {Object} markerSubStyle
         * This is cyclic used if series have multiple marker sprites.
         */
        markerSubStyle: null,
        /**
         * @protected
         * @cfg {Object} itemInstancing
         * The sprite template used to create sprite instances in the series.
         */
        itemInstancing: null,
        /**
         * @cfg {Object} background
         * Sets the background of the surface the series is attached.
         */
        background: null,
        /**
         * @protected
         * @cfg {Ext.draw.Surface} surface
         * The chart surface used to render series sprites.
         */
        surface: null,
        /**
         * @protected
         * @cfg {Object} overlaySurface
         * The surface used to render series labels.
         */
        overlaySurface: null,
        /**
         * @cfg {Boolean|Array} hidden
         */
        hidden: false,
        /**
         * @cfg {Boolean/Object} highlight
         * The sprite attributes that will be applied to the highlighted items in the series.
         * If set to 'true', the default highlight style from {@link #highlightCfg} will be used.
         * If the value of this config is an object, it will be merged with the
         * {@link #highlightCfg}. In case merging of 'highlight' and 'highlightCfg' configs
         * in not the desired behavior, provide the 'highlightCfg' instead.
         */
        highlight: false,
        /**
         * @protected
         * @cfg {Object} highlightCfg
         * The default style for the highlighted item.
         * Used when {@link #highlight} config was simply set to 'true' instead of specifying
         * a style.
         */
        highlightCfg: {
            // Make custom highlightCfg's in subclasses replace this one.
            merge: function(value) {
                return value;
            },
            $value: {
                fillStyle: 'yellow',
                strokeStyle: 'red'
            }
        },
        /**
         * @cfg {Object} animation The series animation configuration.
         * By default, the series is using the same animation the chart uses,
         * if it's own animation is not explicitly configured.
         */
        animation: null,
        /**
         * @cfg {Object} tooltip
         * Add tooltips to the visualization's markers. The config options for the 
         * tooltip are the same configuration used with {@link Ext.tip.ToolTip} plus a 
         * `renderer` config option and a `scope` for the renderer. For example:
         *
         *     tooltip: {
         *       trackMouse: true,
         *       width: 140,
         *       height: 28,
         *       renderer: function (toolTip, record, ctx) {
         *           toolTip.setHtml(record.get('name') + ': ' + record.get('data1') + ' views');
         *       }
         *     }
         *
         * Note that tooltips are shown for series markers and won't work
         * if the {@link #marker} is not configured.
         *
         * You can also configure
         * {@link Ext.chart.interactions.ItemHighlight#multiTooltips}
         * to display multiple tooltips for adjacent or overlapping Line series
         * data points within {@link Ext.chart.series.Line#selectionTolerance} radius.
         *
         * @cfg {Object} tooltip.scope The scope to use when the renderer function is 
         * called.  Defaults to the Series instance.
         * @cfg {Function} tooltip.renderer An 'interceptor' method which can be used to 
         * modify the tooltip attributes before it is shown.  The renderer function is 
         * passed the following params:
         * @cfg {Ext.tip.ToolTip} tooltip.renderer.toolTip The tooltip instance
         * @cfg {Ext.data.Model} tooltip.renderer.record The record instance for the 
         * chart item (sprite) currently targeted by the tooltip.
         * @cfg {Object} tooltip.renderer.ctx A data object with values relating to the 
         * currently targeted chart sprite
         * @cfg {String} tooltip.renderer.ctx.category The type of sprite passed to the 
         * renderer function (will be "items", "markers", or "labels" depending on the 
         * target sprite of the tooltip)
         * @cfg {String} tooltip.renderer.ctx.field The {@link #yField} for the series
         * @cfg {Number} tooltip.renderer.ctx.index The target sprite's index within the 
         * series' items
         * @cfg {Ext.data.Model} tooltip.renderer.ctx.record The record instance for the 
         * chart item (sprite) currently targeted by the tooltip.
         * @cfg {Ext.chart.series.Series} tooltip.renderer.ctx.series The series instance 
         * containing the tooltip's target sprite
         * @cfg {Ext.draw.sprite.Sprite} tooltip.renderer.ctx.sprite The sprite (item) 
         * target of the tooltip
         */
        tooltip: null
    },
    directions: [],
    sprites: null,
    /**
     * @private
     * Returns the number of colors this series needs.
     * A Pie chart needs one color per slice while a Stacked Bar chart needs one per segment.
     * An OHLC chart needs 2 colors (one for drops, one for rises), and most other charts
     * need just a single color.
     */
    themeColorCount: function() {
        return 1;
    },
    /**
     * @private
     * @property
     * Series, where the number of sprites (an so unique colors they require)
     * depends on the number of records in the store should set this to 'true'.
     */
    isStoreDependantColorCount: false,
    /**
     * @private
     * Returns the number of markers this series needs.
     * Currently, only the Line, Scatter and Radar series use markers - and they need
     * just one each.
     */
    themeMarkerCount: function() {
        return 0;
    },
    /**
     * @private
     * Each series has configs that tell which store record fields to use as data
     * for a certain dimension. For example, `xField`, `yField` for most cartesian series,
     * `angleField`, `radiusField` for polar series, `openField`, ..., `closeField`
     * for CandleStick series, etc. The field category is an array of capitalized config
     * names, minus the 'Field' part, to use as data for a certain dimension.
     * For example, for CandleStick series we have:
     *
     *     fieldCategoryY: ['Open', 'High', 'Low', 'Close']
     *
     * While for generic Cartesian series it is simply:
     *
     *     fieldCategoryY: ['Y']
     *
     * This method fetches the values of those configs, i.e. the actual record fields to use.
     *
     * The {@link #coordinate} method in turn will use the values from the `fieldCategory`
     * array to set data attributes of the series sprite. E.g., in case of CandleStick series,
     * the following attributes will be set based on the values in the `fieldCategoryY` array:
     *
     *     `dataOpen`, `dataHigh`, `dataLow`, `dataClose`
     *
     * Where the value of each attribute is a coordinated array of data from the corresponding
     * field.
     *
     * @param {String[]} fieldCategory
     * @return {String[]}
     */
    getFields: function(fieldCategory) {
        var me = this,
            fields = [],
            ln = fieldCategory.length,
            i, field;
        for (i = 0; i < ln; i++) {
            field = me['get' + fieldCategory[i] + 'Field']();
            if (Ext.isArray(field)) {
                fields.push.apply(fields, field);
            } else {
                fields.push(field);
            }
        }
        return fields;
    },
    applyAnimation: function(animation, oldAnimation) {
        var chart = this.getChart();
        if (!chart.isSettingSeriesAnimation) {
            this.isUserAnimation = true;
        }
        return Ext.chart.Util.applyAnimation(animation, oldAnimation);
    },
    updateAnimation: function(animation) {
        var sprites = this.getSprites(),
            itemsMarker, markersMarker, i, ln, sprite;
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            if (sprite.isMarkerHolder) {
                itemsMarker = sprite.getMarker('items');
                if (itemsMarker) {
                    itemsMarker.getTemplate().setAnimation(animation);
                }
                markersMarker = sprite.getMarker('markers');
                if (markersMarker) {
                    markersMarker.getTemplate().setAnimation(animation);
                }
            }
            sprite.setAnimation(animation);
        }
    },
    getAnimation: function() {
        var chart = this.getChart(),
            animation;
        if (chart && chart.animationSuspendCount) {
            animation = {
                duration: 0
            };
        } else {
            if (this.isUserAnimation) {
                animation = this.callParent();
            } else {
                animation = chart.getAnimation();
            }
        }
        return animation;
    },
    updateTitle: function() {
        var me = this,
            chart = me.getChart();
        if (chart && !chart.isInitializing) {
            chart.refreshLegendStore();
        }
    },
    applyHighlight: function(highlight, oldHighlight) {
        var me = this,
            highlightCfg = me.getHighlightCfg();
        if (Ext.isObject(highlight)) {
            highlight = Ext.merge({}, highlightCfg, highlight);
        } else if (highlight === true) {
            highlight = highlightCfg;
        }
        if (highlight) {
            highlight.type = 'highlight';
        }
        return highlight && Ext.merge({}, oldHighlight, highlight);
    },
    updateHighlight: function(highlight) {
        var me = this,
            sprites = me.sprites,
            highlightCfg = me.getHighlightCfg(),
            i, ln, sprite, items, markers;
        me.getStyle();
        // Make sure the 'markers' sprite has been created,
        // so that we can set the 'style' config of its 'highlight' modifier here.
        me.getMarker();
        if (!Ext.Object.isEmpty(highlight)) {
            me.addItemHighlight();
            for (i = 0 , ln = sprites.length; i < ln; i++) {
                sprite = sprites[i];
                if (sprite.isMarkerHolder) {
                    items = sprite.getMarker('items');
                    if (items) {
                        items.getTemplate().modifiers.highlight.setStyle(highlight);
                    }
                    markers = sprite.getMarker('markers');
                    if (markers) {
                        markers.getTemplate().modifiers.highlight.setStyle(highlight);
                    }
                }
            }
        } else if (!Ext.Object.equals(highlightCfg, this.defaultConfig.highlightCfg)) {
            this.addItemHighlight();
        }
    },
    updateHighlightCfg: function(highlightCfg) {
        // Make sure to add the 'itemhighlight' interaction to the series, if the default
        // highlight style changes, even if the 'highlight' config isn't set (defaults to false),
        // since we probably want to use item highlighting now or later, if we are changing
        // the default highlight style.
        // This updater will be triggered by the 'highlight' applier, and the 'addItemHighlight'
        // call here will in turn call 'getHighlight' down the call stack, which will return
        // 'undefined' since the value hasn't been processed yet. So we don't call
        // 'addItemHighlight' here during configuration and instead call it in the 'highlight'
        // updater, if it hasn't already been called ('highlight' config is set to 'false').
        if (!this.isConfiguring && !Ext.Object.equals(highlightCfg, this.defaultConfig.highlightCfg)) {
            this.addItemHighlight();
        }
    },
    applyItemInstancing: function(config, oldConfig) {
        if (config && oldConfig && (!config.type || config.type === oldConfig.type)) {
            // Have to merge to a new object, or the updater won't be called.
            config = Ext.merge({}, oldConfig, config);
        }
        if (config && !config.type) {
            config = null;
        }
        return config;
    },
    setAttributesForItem: function(item, change) {
        var sprite = item && item.sprite,
            i;
        if (sprite) {
            if (sprite.isMarkerHolder && item.category === 'items') {
                sprite.putMarker(item.category, change, item.index, false, true);
            }
            if (sprite.isMarkerHolder && item.category === 'markers') {
                sprite.putMarker(item.category, change, item.index, false, true);
            } else if (sprite.isInstancing) {
                sprite.setAttributesFor(item.index, change);
            } else if (Ext.isArray(sprite)) {
                // In some instances, like with the 3D pie series,
                // an item can be composed of multiple sprites
                // (e.g. 8 sprites are used to render a single 3D pie slice).
                for (i = 0; i < sprite.length; i++) {
                    sprite[i].setAttributes(change);
                }
            } else {
                sprite.setAttributes(change);
            }
        }
    },
    getBBoxForItem: function(item) {
        var sprite = item && item.sprite,
            result = null;
        if (sprite) {
            if (sprite.getMarker('items') && item.category === 'items') {
                result = sprite.getMarkerBBox(item.category, item.index);
            } else if (sprite instanceof Ext.draw.sprite.Instancing) {
                result = sprite.getBBoxFor(item.index);
            } else {
                result = sprite.getBBox();
            }
        }
        return result;
    },
    /**
     * @private
     * @property
     * The range of "coordinated" data.
     * Typically, for two directions ('X' and 'Y') the `dataRange` would look like this:
     *
     *     dataRange[0] - minX
     *     dataRange[1] - minY
     *     dataRange[2] - maxX
     *     dataRange[3] - maxY
     *
     * And the series' {@link #coordinate} method would be called like this:
     *
     *     coordinate('X', 0, 2)
     *     coordinate('Y', 1, 2)
     *
     * For numbers, coordinated data are numbers themselves.
     * For categories - their indexes.
     * For Date objects - their timestamps.
     * In other words, whatever source data we have, it has to be converted to numbers
     * before it can be plotted.
     */
    dataRange: null,
    constructor: function(config) {
        var me = this,
            id;
        config = config || {};
        // Backward compatibility with Ext.
        if (config.tips) {
            config = Ext.apply({
                tooltip: config.tips
            }, config);
        }
        // Backward compatibility with Touch.
        if (config.highlightCfg) {
            config = Ext.apply({
                highlight: config.highlightCfg
            }, config);
        }
        if ('id' in config) {
            id = config.id;
        } else if ('id' in me.config) {
            id = me.config.id;
        } else {
            id = me.getId();
        }
        me.setId(id);
        me.sprites = [];
        me.dataRange = [];
        me.mixins.observable.constructor.call(me, config);
        me.initBindable();
    },
    lookupViewModel: function(skipThis) {
        // Override the Bindable's method to redirect view model
        // lookup to the chart.
        var chart = this.getChart();
        return chart ? chart.lookupViewModel(skipThis) : null;
    },
    applyTooltip: function(tooltip, oldTooltip) {
        var config = Ext.apply({
                xtype: 'tooltip',
                renderer: Ext.emptyFn,
                constrainPosition: true,
                shrinkWrapDock: true,
                autoHide: true,
                hideDelay: 200,
                mouseOffset: [
                    20,
                    20
                ],
                trackMouse: true
            }, tooltip);
        return Ext.create(config);
    },
    updateTooltip: function() {
        // Tooltips can't work without the 'itemhighlight' or the 'itemedit' interaction.
        this.addItemHighlight();
    },
    // Adds the 'itemhighlight' interaction to the chart that owns the series.
    addItemHighlight: function() {
        var chart = this.getChart(),
            interactions, interaction, hasRequiredInteraction, i;
        if (!chart) {
            return;
        }
        interactions = chart.getInteractions();
        for (i = 0; i < interactions.length; i++) {
            interaction = interactions[i];
            if (interaction.isItemHighlight || interaction.isItemEdit) {
                hasRequiredInteraction = true;
                break;
            }
        }
        if (!hasRequiredInteraction) {
            interactions.push('itemhighlight');
            chart.setInteractions(interactions);
        }
    },
    showTooltip: function(item, event) {
        var me = this,
            tooltip = me.getTooltip();
        if (!tooltip) {
            return;
        }
        Ext.callback(tooltip.renderer, tooltip.scope, [
            tooltip,
            item.record,
            item
        ], 0, me);
        tooltip.showBy(event);
    },
    showTooltipAt: function(item, x, y) {
        var me = this,
            tooltip = me.getTooltip(),
            mouseOffset = tooltip.config.mouseOffset;
        if (!tooltip || !tooltip.showAt) {
            return;
        }
        if (mouseOffset) {
            x += mouseOffset[0];
            y += mouseOffset[1];
        }
        Ext.callback(tooltip.renderer, tooltip.scope, [
            tooltip,
            item.record,
            item
        ], 0, me);
        tooltip.showAt([
            x,
            y
        ]);
    },
    hideTooltip: function(item, immediate) {
        var me = this,
            tooltip = me.getTooltip();
        if (!tooltip) {
            return;
        }
        if (immediate) {
            tooltip.hide();
        } else {
            tooltip.delayHide();
        }
    },
    applyStore: function(store) {
        return store && Ext.StoreManager.lookup(store);
    },
    getStore: function() {
        return this._store || this.getChart() && this.getChart().getStore();
    },
    updateStore: function(newStore, oldStore) {
        var me = this,
            chart = me.getChart(),
            chartStore = chart && chart.getStore(),
            sprites, sprite, len, i;
        oldStore = oldStore || chartStore;
        if (oldStore && oldStore !== newStore) {
            oldStore.un({
                datachanged: 'onDataChanged',
                update: 'onDataChanged',
                scope: me
            });
        }
        if (newStore) {
            newStore.on({
                datachanged: 'onDataChanged',
                update: 'onDataChanged',
                scope: me
            });
            sprites = me.getSprites();
            for (i = 0 , len = sprites.length; i < len; i++) {
                sprite = sprites[i];
                if (sprite.setStore) {
                    sprite.setStore(newStore);
                }
            }
            me.onDataChanged();
        }
        me.fireEvent('storechange', me, newStore, oldStore);
    },
    onStoreChange: function(chart, newStore, oldStore) {
        if (!this._store) {
            this.updateStore(newStore, oldStore);
        }
    },
    defaultRange: [
        0,
        1
    ],
    /**
     * @private
     * @param direction {'X'/'Y'}
     * @param directionOffset
     * @param directionCount
     */
    coordinate: function(direction, directionOffset, directionCount) {
        var me = this,
            store = me.getStore(),
            hidden = me.getHidden(),
            items = store.getData().items,
            axis = me['get' + direction + 'Axis'](),
            dataRange = [
                NaN,
                NaN
            ],
            fieldCategory = me['fieldCategory' + direction] || [
                direction
            ],
            fields = me.getFields(fieldCategory),
            i, field, data,
            style = {},
            sprites = me.getSprites(),
            axisRange;
        if (sprites.length && !Ext.isBoolean(hidden) || !hidden) {
            for (i = 0; i < fieldCategory.length; i++) {
                field = fields[i];
                data = me.coordinateData(items, field, axis);
                Ext.chart.Util.expandRange(dataRange, data);
                style['data' + fieldCategory[i]] = data;
            }
            // We don't want to expand the range that has a span of 0 here
            // (e.g. [5, 5] that we'd get if all values for a field are 5).
            // We only want to do this in the Axis, when we calculate the
            // combined range.
            // This is because, if we try to expand the range of values here,
            // and we have multiple fields, the combined range for the axis
            // may not represent the actual range of the data.
            // E.g. if other fields have non-zero span ranges like [4.95, 5.03],
            // [4.91, 5.08], and if the `padding` param to `validateRange` is 0.5,
            // the range of the axis will end up being [4.5, 5.5], because the
            // [5, 5] range of one of the series was expanded to [4.5, 5.5]
            // which encompasses the rest of the ranges.
            dataRange = Ext.chart.Util.validateRange(dataRange, me.defaultRange, 0);
            // See `dataRange` docs.
            me.dataRange[directionOffset] = dataRange[0];
            me.dataRange[directionOffset + directionCount] = dataRange[1];
            style['dataMin' + direction] = dataRange[0];
            style['dataMax' + direction] = dataRange[1];
            if (axis) {
                axisRange = axis.getRange(true);
                axis.setBoundSeriesRange(axisRange);
            }
            for (i = 0; i < sprites.length; i++) {
                sprites[i].setAttributes(style);
            }
        }
    },
    /**
     * @private
     * This method will return an array containing data coordinated by a specific axis.
     * @param {Array} items Store records.
     * @param {String} field The field to fetch from each record.
     * @param {Ext.chart.axis.Axis} axis The axis used to lay out the data.
     * @return {Array}
     */
    coordinateData: function(items, field, axis) {
        var data = [],
            length = items.length,
            layout = axis && axis.getLayout(),
            i, x;
        for (i = 0; i < length; i++) {
            x = items[i].data[field];
            // An empty string (a valid discrete axis value) will be coordinated
            // by the axis layout (if axis is given), otherwise it will be converted
            // to zero (via +'').
            if (!Ext.isEmpty(x, true)) {
                if (layout) {
                    data[i] = layout.getCoordFor(x, field, i, items);
                } else {
                    x = +x;
                    // 'x' can be a category name here.
                    data[i] = Ext.isNumber(x) ? x : i;
                }
            } else {
                data[i] = x;
            }
        }
        return data;
    },
    updateLabelData: function() {
        var label = this.getLabel();
        if (!label) {
            return;
        }
        // eslint-disable-next-line vars-on-top, one-var
        var store = this.getStore(),
            items = store.getData().items,
            sprites = this.getSprites(),
            labelTpl = label.getTemplate(),
            labelFields = Ext.Array.from(labelTpl.getField()),
            i, j, ln, labels, sprite, field;
        if (!sprites.length || !labelFields.length) {
            return;
        }
        for (i = 0; i < sprites.length; i++) {
            sprite = sprites[i];
            if (!sprite.getField) {
                // The 'gauge' series is misnormer, its sprites
                // do not extend from the base Series sprite and
                // so do not have the 'field' config. They also
                // don't support labels in the traditional sense.
                
                continue;
            }
            labels = [];
            field = sprite.getField();
            if (Ext.Array.indexOf(labelFields, field) < 0) {
                field = labelFields[i];
            }
            for (j = 0 , ln = items.length; j < ln; j++) {
                labels.push(items[j].get(field));
            }
            sprite.setAttributes({
                labels: labels
            });
        }
    },
    /**
     * @private
     *
     * *** Data processing overview. ***
     *
     * The data is processed in the following order:
     *
     * 1) chart.processData()      - calls `processData` of all series
     * 2) series.processData()     - calls `processData` of all bound axes,
     *                               or jumps to (5) directly, if the series has no axis
     *                               in this direction
     * 3) axis.processData()       - calls the `processData` of its own layout
     * 4) axisLayout.processData() - calls `coordinateX/Y` of all bound series
     * 5) series.coordinateX/Y     - calls its own `coordinate` method in that direction
     * 6) series.coordinate        - calls its own `coordinateData` method using the right
     *                               record fields and axes
     * 7) series.coordinateData    - calls `getCoordFor` of the axis layout for the given
     *                               field
     * 8) layout.getCoordFor       - returns a numeric value for the given field value,
     *                               whatever its type may be
     *
     * The `dataX`, `dataY` attributes of the series' sprites are set by the
     * `series.coordinate` method using the data returned by the `coordinateData`.
     * `series.coordinate` also calculates the range of said data (via `expandRange`)
     * and sets the `dataMinX/Y`, `dataMaxX/Y` attributes of the series' sprites.
     */
    processData: function() {
        var me = this,
            directions = me.directions,
            direction, axis, name, i, ln;
        if (me.isProcessingData || !me.getStore()) {
            return;
        }
        me.isProcessingData = true;
        for (i = 0 , ln = directions.length; i < ln; i++) {
            direction = directions[i];
            axis = me['get' + direction + 'Axis']();
            if (axis) {
                axis.processData(me);
                
                continue;
            }
            name = 'coordinate' + direction;
            if (me[name]) {
                me[name]();
            }
        }
        me.updateLabelData();
        me.isProcessingData = false;
    },
    applyBackground: function(background) {
        var surface, result;
        if (this.getChart()) {
            surface = this.getSurface();
            surface.setBackground(background);
            result = surface.getBackground();
        } else {
            result = background;
        }
        return result;
    },
    updateChart: function(newChart, oldChart) {
        var me = this,
            store = me._store;
        if (oldChart) {
            oldChart.un('axeschange', 'onAxesChange', me);
            me.clearSprites();
            me.setSurface(null);
            me.setOverlaySurface(null);
            oldChart.unregister(me);
            me.onChartDetached(oldChart);
            if (!store) {
                me.updateStore(null);
            }
        }
        if (newChart) {
            me.setSurface(newChart.getSurface('series'));
            me.setOverlaySurface(newChart.getSurface('overlay'));
            newChart.on('axeschange', 'onAxesChange', me);
            // TODO: Gauge series should render correctly when chart's store is missing.
            // TODO: When store is initially missing the getAxes will return null here,
            // TODO: since applyAxes has actually triggered this series.updateChart call
            // TODO: indirectly.
            // TODO: Figure out why it doesn't go this route when a store is present.
            if (newChart.getAxes()) {
                me.onAxesChange(newChart);
            }
            me.onChartAttached(newChart);
            newChart.register(me);
            if (!store) {
                me.updateStore(newChart.getStore());
            }
        }
    },
    onAxesChange: function(chart, force) {
        if (chart.destroying || chart.destroyed) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            axes = chart.getAxes(),
            axis,
            directionToAxesMap = {},
            directionToFieldsMap = {},
            needHighPrecision = false,
            directions = this.directions,
            direction, i, ln;
        for (i = 0 , ln = directions.length; i < ln; i++) {
            direction = directions[i];
            directionToFieldsMap[direction] = me.getFields(me['fieldCategory' + direction]);
        }
        for (i = 0 , ln = axes.length; i < ln; i++) {
            axis = axes[i];
            direction = axis.getDirection();
            if (!directionToAxesMap[direction]) {
                directionToAxesMap[direction] = [
                    axis
                ];
            } else {
                directionToAxesMap[direction].push(axis);
            }
        }
        for (i = 0 , ln = directions.length; i < ln; i++) {
            direction = directions[i];
            if (!force && me['get' + direction + 'Axis']()) {
                
                continue;
            }
            if (directionToAxesMap[direction]) {
                axis = me.findMatchingAxis(directionToAxesMap[direction], directionToFieldsMap[direction]);
                if (axis) {
                    me['set' + direction + 'Axis'](axis);
                    if (axis.getNeedHighPrecision()) {
                        needHighPrecision = true;
                    }
                }
            }
        }
        this.getSurface().setHighPrecision(needHighPrecision);
    },
    /**
     * @private
     * Given the list of axes in a certain direction and a list of series fields in that
     * direction returns the first matching axis for the series in that direction,
     * or undefined if a match wasn't found.
     */
    findMatchingAxis: function(directionAxes, directionFields) {
        var axis, axisFields, i, j;
        for (i = 0; i < directionAxes.length; i++) {
            axis = directionAxes[i];
            axisFields = axis.getFields();
            if (!axisFields.length) {
                return axis;
            } else if (directionFields) {
                for (j = 0; j < directionFields.length; j++) {
                    if (Ext.Array.indexOf(axisFields, directionFields[j]) >= 0) {
                        return axis;
                    }
                }
            }
        }
    },
    onChartDetached: function(oldChart) {
        var me = this;
        me.fireEvent('chartdetached', oldChart, me);
        oldChart.un('storechange', 'onStoreChange', me);
    },
    onChartAttached: function(chart) {
        var me = this;
        me.fireEvent('chartattached', chart, me);
        chart.on('storechange', 'onStoreChange', me);
        me.processData();
    },
    updateOverlaySurface: function(overlaySurface) {
        var label = this.getLabel();
        if (overlaySurface && label) {
            overlaySurface.add(label);
        }
    },
    getLabel: function() {
        return this.labelMarker;
    },
    setLabel: function(label) {
        var me = this,
            chart = me.getChart(),
            marker = me.labelMarker,
            template;
        // The label sprite is reused unless the value of 'label' is falsy,
        // so that we can transition from one attribute set to another with an
        // animation, which is important for example during theme switching.
        if (!label && marker) {
            marker.getTemplate().destroy();
            marker.destroy();
            me.labelMarker = marker = null;
        }
        if (label) {
            if (!marker) {
                marker = me.labelMarker = new Ext.chart.Markers({
                    zIndex: 10
                });
                marker.setTemplate(new Ext.chart.sprite.Label());
                me.getOverlaySurface().add(marker);
            }
            template = marker.getTemplate();
            template.setAttributes(label);
            template.setConfig(label);
            if (label.field) {
                template.setField(label.field);
            }
            if (label.display) {
                marker.setAttributes({
                    hidden: label.display === 'none'
                });
            }
            marker.setDirty(true);
        }
        // Inform the label about the template change.
        me.updateLabelData();
        if (chart && !chart.isInitializing && !me.isConfiguring) {
            chart.redraw();
        }
    },
    createItemInstancingSprite: function(sprite, itemInstancing) {
        var me = this,
            markers = new Ext.chart.Markers(),
            config = Ext.apply({
                modifiers: 'highlight'
            }, itemInstancing),
            style = me.getStyle(),
            template, animation;
        markers.setAttributes({
            zIndex: Number.MAX_VALUE
        });
        markers.setTemplate(config);
        template = markers.getTemplate();
        template.setAttributes(style);
        animation = template.getAnimation();
        animation.on('animationstart', 'onSpriteAnimationStart', this);
        animation.on('animationend', 'onSpriteAnimationEnd', this);
        sprite.bindMarker('items', markers);
        me.getSurface().add(markers);
        return markers;
    },
    getDefaultSpriteConfig: function() {
        return {
            type: this.seriesType,
            renderer: this.getRenderer()
        };
    },
    updateRenderer: function(renderer) {
        var me = this,
            chart = me.getChart();
        if (chart && chart.isInitializing) {
            return;
        }
        // We have to be careful and not call the 'getSprites' method here, as this
        // method itself may have been called by the 'getSprites' method indirectly already.
        if (me.sprites.length) {
            me.sprites[0].setAttributes({
                renderer: renderer || null
            });
            if (chart && !chart.isInitializing) {
                chart.redraw();
            }
        }
    },
    updateShowMarkers: function(showMarkers) {
        var sprite = this.getSprite(),
            markers = sprite && sprite.getMarker('markers');
        if (markers) {
            markers.getTemplate().setAttributes({
                hidden: !showMarkers
            });
        }
    },
    createSprite: function() {
        var me = this,
            surface = me.getSurface(),
            itemInstancing = me.getItemInstancing(),
            sprite = surface.add(me.getDefaultSpriteConfig()),
            animation, label;
        sprite.setAttributes(me.getStyle());
        sprite.setSeries(me);
        if (itemInstancing) {
            me.createItemInstancingSprite(sprite, itemInstancing);
        }
        if (sprite.isMarkerHolder) {
            label = me.getLabel();
            if (label && label.getTemplate().getField()) {
                sprite.bindMarker('labels', label);
            }
        }
        if (sprite.setStore) {
            sprite.setStore(me.getStore());
        }
        animation = sprite.getAnimation();
        animation.on('animationstart', 'onSpriteAnimationStart', me);
        animation.on('animationend', 'onSpriteAnimationEnd', me);
        me.sprites.push(sprite);
        return sprite;
    },
    /**
     * @method
     * Returns the read-only array of sprites the are used to draw this series.
     */
    getSprites: null,
    /**
     * @private
     * Returns the first sprite. Convenience method for series that have
     * a single markerholder sprite.
     */
    getSprite: function() {
        var sprites = this.getSprites();
        return sprites && sprites[0];
    },
    /**
     * @private
     */
    withSprite: function(fn) {
        var sprite = this.getSprite();
        return sprite && fn(sprite) || undefined;
    },
    forEachSprite: function(fn) {
        var sprites = this.getSprites(),
            i, ln;
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            fn(sprites[i]);
        }
    },
    onDataChanged: function() {
        var me = this,
            chart = me.getChart(),
            chartStore = chart && chart.getStore(),
            seriesStore = me.getStore();
        if (seriesStore !== chartStore) {
            me.processData();
        }
    },
    isXType: function(xtype) {
        return xtype === 'series';
    },
    getItemId: function() {
        return this.getId();
    },
    applyThemeStyle: function(theme, oldTheme) {
        var me = this,
            fill, stroke;
        fill = theme && theme.subStyle && theme.subStyle.fillStyle;
        stroke = fill && theme.subStyle.strokeStyle;
        if (fill && !stroke) {
            theme.subStyle.strokeStyle = me.getStrokeColorsFromFillColors(fill);
        }
        fill = theme && theme.markerSubStyle && theme.markerSubStyle.fillStyle;
        stroke = fill && theme.markerSubStyle.strokeStyle;
        if (fill && !stroke) {
            theme.markerSubStyle.strokeStyle = me.getStrokeColorsFromFillColors(fill);
        }
        return Ext.apply(oldTheme || {}, theme);
    },
    applyStyle: function(style, oldStyle) {
        return Ext.apply({}, style, oldStyle);
    },
    applySubStyle: function(subStyle, oldSubStyle) {
        var name = Ext.ClassManager.getNameByAlias('sprite.' + this.seriesType),
            cls = Ext.ClassManager.get(name);
        if (cls && cls.def) {
            subStyle = cls.def.batchedNormalize(subStyle, true);
        }
        return Ext.merge({}, oldSubStyle, subStyle);
    },
    applyMarker: function(marker, oldMarker) {
        var type, cls;
        if (marker) {
            if (!Ext.isObject(marker)) {
                marker = {};
            }
            type = marker.type || 'circle';
            if (oldMarker && type === oldMarker.type) {
                marker = Ext.merge({}, oldMarker, marker);
            }
        }
        // Note: reusing the `oldMaker` like `Ext.merge(oldMarker, marker)`
        // isn't possible because the `updateMarker` won't be called.
        if (type) {
            cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + type));
        }
        if (cls && cls.def) {
            marker = cls.def.normalize(marker, true);
            marker.type = type;
        } else {
            marker = null;
            //<debug>
            Ext.log.warn('Invalid series marker type: ' + type);
        }
        //</debug>
        return marker;
    },
    updateMarker: function(marker) {
        var me = this,
            sprites = me.getSprites(),
            seriesSprite, markerSprite, markerTplConfig, i, ln;
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            seriesSprite = sprites[i];
            if (!seriesSprite.isMarkerHolder) {
                
                continue;
            }
            markerSprite = seriesSprite.getMarker('markers');
            if (marker) {
                if (!markerSprite) {
                    markerSprite = new Ext.chart.Markers();
                    seriesSprite.bindMarker('markers', markerSprite);
                    me.getOverlaySurface().add(markerSprite);
                }
                markerTplConfig = Ext.Object.merge({
                    modifiers: 'highlight'
                }, marker);
                markerSprite.setTemplate(markerTplConfig);
                markerSprite.getTemplate().getAnimation().setCustomDurations({
                    translationX: 0,
                    translationY: 0
                });
            } else if (markerSprite) {
                seriesSprite.releaseMarker('markers');
                me.getOverlaySurface().remove(markerSprite, true);
            }
            seriesSprite.setDirty(true);
        }
        // If we call, for example, `series.setMarker({type: 'circle'})` on a series
        // that has been already constructed, the newly added marker still has to be
        // themed, and the 'style' config of its 'highlight' modifier has to be set.
        if (!me.isConfiguring) {
            me.doUpdateStyles();
            me.updateHighlight(me.getHighlight());
        }
    },
    applyMarkerSubStyle: function(marker, oldMarker) {
        var type = (marker && marker.type) || (oldMarker && oldMarker.type) || 'circle',
            cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + type));
        if (cls && cls.def) {
            marker = cls.def.batchedNormalize(marker, true);
        }
        return Ext.merge(oldMarker || {}, marker);
    },
    updateHidden: function(hidden) {
        var me = this;
        me.getColors();
        me.getSubStyle();
        me.setSubStyle({
            hidden: hidden
        });
        me.processData();
        me.doUpdateStyles();
        if (!Ext.isArray(hidden)) {
            me.updateLegendStore(hidden);
        }
    },
    /**
     * @private
     * Updates chart's legend store when the value of the series' {@link #hidden} config
     * changes or when the {@link #setHiddenByIndex} method is called.
     * @param hidden Whether series (or its component) should be hidden or not.
     * @param index Used for stacked series.
     *              If present, only the component with the specified index will change
     *              visibility.
     */
    updateLegendStore: function(hidden, index) {
        var me = this,
            chart = me.getChart(),
            legendStore = chart && chart.getLegendStore(),
            id = me.getId(),
            record;
        if (legendStore) {
            if (arguments.length > 1) {
                record = legendStore.findBy(function(rec) {
                    return rec.get('series') === id && rec.get('index') === index;
                });
                if (record !== -1) {
                    record = legendStore.getAt(record);
                }
            } else {
                record = legendStore.findRecord('series', id);
            }
            if (record && record.get('disabled') !== hidden) {
                record.set('disabled', hidden);
            }
        }
    },
    /**
     *
     * @param {Number} index
     * @param {Boolean} value
     */
    setHiddenByIndex: function(index, value) {
        var me = this;
        if (Ext.isArray(me.getHidden())) {
            // Multi-sprite series like Pie and StackedCartesian.
            me.getHidden()[index] = value;
            me.updateHidden(me.getHidden());
            me.updateLegendStore(value, index);
        } else {
            me.setHidden(value);
        }
    },
    getStrokeColorsFromFillColors: function(colors) {
        var me = this,
            darker = me.getUseDarkerStrokeColor(),
            darkerRatio = (Ext.isNumber(darker) ? darker : me.darkerStrokeRatio),
            strokeColors;
        if (darker) {
            strokeColors = Ext.Array.map(colors, function(color) {
                color = Ext.isString(color) ? color : color.stops[0].color;
                color = Ext.util.Color.fromString(color);
                return color.createDarker(darkerRatio).toString();
            });
        } else {
            strokeColors = Ext.Array.clone(colors);
        }
        return strokeColors;
    },
    updateThemeColors: function(colors) {
        var me = this,
            theme = me.getThemeStyle(),
            fillColors = Ext.Array.clone(colors),
            strokeColors = me.getStrokeColorsFromFillColors(colors),
            newSubStyle = {
                fillStyle: fillColors,
                strokeStyle: strokeColors
            };
        theme.subStyle = Ext.apply(theme.subStyle || {}, newSubStyle);
        theme.markerSubStyle = Ext.apply(theme.markerSubStyle || {}, newSubStyle);
        me.doUpdateStyles();
        if (!me.isConfiguring) {
            me.getChart().refreshLegendStore();
        }
    },
    themeOnlyIfConfigured: {},
    updateTheme: function(theme) {
        var me = this,
            seriesTheme = theme.getSeries(),
            initialConfig = me.getInitialConfig(),
            defaultConfig = me.defaultConfig,
            configs = me.self.getConfigurator().configs,
            genericSeriesTheme = seriesTheme.defaults,
            specificSeriesTheme = seriesTheme[me.type],
            themeOnlyIfConfigured = me.themeOnlyIfConfigured,
            key, value, isObjValue, isUnusedConfig, initialValue, cfg;
        seriesTheme = Ext.merge({}, genericSeriesTheme, specificSeriesTheme);
        for (key in seriesTheme) {
            value = seriesTheme[key];
            cfg = configs[key];
            if (value !== null && value !== undefined && cfg) {
                initialValue = initialConfig[key];
                isObjValue = Ext.isObject(value);
                isUnusedConfig = initialValue === defaultConfig[key];
                if (isObjValue) {
                    if (isUnusedConfig && themeOnlyIfConfigured[key]) {
                        
                        continue;
                    }
                    value = Ext.merge({}, value, initialValue);
                }
                if (isUnusedConfig || isObjValue) {
                    me[cfg.names.set](value);
                }
            }
        }
    },
    /**
     * @private
     * When the chart's "colors" config changes, these colors are passed onto the series
     * where they are used with the same priority as theme colors, i.e. they do not override
     * the series' "colors" config, nor the series' "style" config, but they do override
     * the colors from the theme's "seriesThemes" config.
     */
    updateChartColors: function(colors) {
        var me = this;
        if (!me.getColors()) {
            me.updateThemeColors(colors);
        }
    },
    updateColors: function(colors) {
        var chart;
        this.updateThemeColors(colors);
        if (!this.isConfiguring) {
            chart = this.getChart();
            if (chart) {
                chart.refreshLegendStore();
            }
        }
    },
    updateStyle: function() {
        this.doUpdateStyles();
    },
    updateSubStyle: function() {
        this.doUpdateStyles();
    },
    updateThemeStyle: function() {
        this.doUpdateStyles();
    },
    doUpdateStyles: function() {
        var me = this,
            sprites = me.sprites,
            itemInstancing = me.getItemInstancing(),
            ln = sprites && sprites.length,
            // 'showMarkers' updater calls 'series.getSprites()',
            // which we don't want to call here.
            showMarkers = me.getConfig('showMarkers', true),
            // eslint-disable-line no-unused-vars
            style, sprite, marker, i;
        for (i = 0; i < ln; i++) {
            sprite = sprites[i];
            style = me.getStyleByIndex(i);
            if (itemInstancing) {
                sprite.getMarker('items').getTemplate().setAttributes(style);
            }
            sprite.setAttributes(style);
            marker = sprite.isMarkerHolder && sprite.getMarker('markers');
            if (marker) {
                marker.getTemplate().setAttributes(me.getMarkerStyleByIndex(i));
            }
        }
    },
    getStyleWithTheme: function() {
        var me = this,
            theme = me.getThemeStyle(),
            style = Ext.clone(me.getStyle());
        if (theme && theme.style) {
            Ext.applyIf(style, theme.style);
        }
        return style;
    },
    getSubStyleWithTheme: function() {
        var me = this,
            theme = me.getThemeStyle(),
            subStyle = Ext.clone(me.getSubStyle());
        if (theme && theme.subStyle) {
            Ext.applyIf(subStyle, theme.subStyle);
        }
        return subStyle;
    },
    getStyleByIndex: function(i) {
        var me = this,
            theme = me.getThemeStyle(),
            style, themeStyle, subStyle, themeSubStyle,
            result = {};
        style = me.getStyle();
        themeStyle = (theme && theme.style) || {};
        subStyle = me.styleDataForIndex(me.getSubStyle(), i);
        themeSubStyle = me.styleDataForIndex((theme && theme.subStyle), i);
        Ext.apply(result, themeStyle);
        Ext.apply(result, themeSubStyle);
        Ext.apply(result, style);
        Ext.apply(result, subStyle);
        return result;
    },
    getMarkerStyleByIndex: function(i) {
        var me = this,
            theme = me.getThemeStyle(),
            style, themeStyle, subStyle, themeSubStyle, markerStyle, themeMarkerStyle, markerSubStyle, themeMarkerSubStyle,
            result = {};
        style = me.getStyle();
        themeStyle = (theme && theme.style) || {};
        // 'series.updateHidden()' will update 'series.subStyle.hidden' config
        // with the value of the 'series.hidden' config.
        // But we also need to account for 'series.showMarkers' config
        // to determine whether the markers should be hidden or not.
        subStyle = me.styleDataForIndex(me.getSubStyle(), i);
        if (subStyle.hasOwnProperty('hidden')) {
            subStyle.hidden = subStyle.hidden || !this.getConfig('showMarkers', true);
        }
        themeSubStyle = me.styleDataForIndex((theme && theme.subStyle), i);
        markerStyle = me.getMarker();
        themeMarkerStyle = (theme && theme.marker) || {};
        markerSubStyle = me.getMarkerSubStyle();
        themeMarkerSubStyle = me.styleDataForIndex((theme && theme.markerSubStyle), i);
        Ext.apply(result, themeStyle);
        Ext.apply(result, themeSubStyle);
        Ext.apply(result, themeMarkerStyle);
        Ext.apply(result, themeMarkerSubStyle);
        Ext.apply(result, style);
        Ext.apply(result, subStyle);
        Ext.apply(result, markerStyle);
        Ext.apply(result, markerSubStyle);
        return result;
    },
    styleDataForIndex: function(style, i) {
        var value, name,
            result = {};
        if (style) {
            for (name in style) {
                value = style[name];
                if (Ext.isArray(value)) {
                    result[name] = value[i % value.length];
                } else {
                    result[name] = value;
                }
            }
        }
        return result;
    },
    /**
     * @method
     * For a given x/y point relative to the main rect, find a corresponding item from this
     * series, if any.
     * @param {Number} x
     * @param {Number} y
     * @param {Object} [target] optional target to receive the result
     * @return {Object} An object describing the item, or null if there is no matching item.
     * The exact contents of this object will vary by series type, but should always contain
     * at least the following:
     *
     * @return {Ext.data.Model} return.record the record of the item.
     * @return {Array} return.point the x/y coordinates relative to the chart box
     * of a single point for this data item, which can be used as e.g. a tooltip anchor
     * point.
     * @return {Ext.draw.sprite.Sprite} return.sprite the item's rendering Sprite.
     * @return {Number} return.subSprite the index if sprite is an instancing sprite.
     */
    getItemForPoint: Ext.emptyFn,
    /**
     * Returns a series item by index and (optional) category.
     * @param {Number} index The index of the item (matches store record index).
     * @param {String} [category] The category of item, e.g.: 'items', 'markers', 'sprites'.
     * @return {Object} item
     */
    getItemByIndex: function(index, category) {
        var me = this,
            sprites = me.getSprites(),
            sprite = sprites && sprites[0],
            item;
        if (!sprite) {
            return;
        }
        // 'category' is not defined, making our best guess here.
        if (category === undefined && sprite.isMarkerHolder) {
            category = me.getItemInstancing() ? 'items' : 'markers';
        } else if (!category || category === '' || category === 'sprites') {
            sprite = sprites[index];
        }
        if (sprite) {
            item = {
                series: me,
                category: category,
                index: index,
                record: me.getStore().getData().items[index],
                field: me.getYField(),
                sprite: sprite
            };
            return item;
        }
    },
    onSpriteAnimationStart: function(sprite) {
        this.fireEvent('animationstart', this, sprite);
    },
    onSpriteAnimationEnd: function(sprite) {
        this.fireEvent('animationend', this, sprite);
    },
    resolveListenerScope: function(defaultScope) {
        // Override the Observable's method to redirect listener scope
        // resolution to the chart.
        var me = this,
            namedScope = Ext._namedScopes[defaultScope],
            chart = me.getChart(),
            scope;
        if (!namedScope) {
            scope = chart ? chart.resolveListenerScope(defaultScope, false) : (defaultScope || me);
        } else if (namedScope.isThis) {
            scope = me;
        } else if (namedScope.isController) {
            scope = chart ? chart.resolveListenerScope(defaultScope, false) : me;
        } else if (namedScope.isSelf) {
            scope = chart ? chart.resolveListenerScope(defaultScope, false) : me;
            // Class body listener. No chart controller, nor chart container controller.
            if (scope === chart && !chart.getInheritedConfig('defaultListenerScope')) {
                scope = me;
            }
        }
        return scope;
    },
    /**
     * Provide legend information to target array.
     *
     * @param {Array} target
     *
     * The information consists:
     * @param {String} target.name
     * @param {String} target.mark
     * @param {Boolean} target.disabled
     * @param {String} target.series
     * @param {Number} target.index
     */
    provideLegendInfo: function(target) {
        var me = this,
            style = me.getSubStyleWithTheme(),
            fill = style.fillStyle;
        if (Ext.isArray(fill)) {
            fill = fill[0];
        }
        target.push({
            name: me.getTitle() || me.getYField() || me.getId(),
            mark: (Ext.isObject(fill) ? fill.stops && fill.stops[0].color : fill) || style.strokeStyle || 'black',
            disabled: me.getHidden(),
            series: me.getId(),
            index: 0
        });
    },
    clearSprites: function() {
        var sprites = this.sprites,
            sprite, i, ln;
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            if (sprite && sprite.isSprite) {
                sprite.destroy();
            }
        }
        this.sprites = [];
    },
    destroy: function() {
        var me = this,
            store = me._store,
            // Peek at the config so we don't create one just to destroy it
            tooltip = me.getConfig('tooltip', true);
        if (store && store.getAutoDestroy()) {
            Ext.destroy(store);
        }
        me.setChart(null);
        me.clearListeners();
        if (tooltip) {
            Ext.destroy(tooltip);
        }
        me.callParent();
    }
});

/**
 * @class Ext.chart.interactions.Abstract
 *
 * Defines a common abstract parent class for all interactions.
 *
 */
Ext.define('Ext.chart.interactions.Abstract', {
    xtype: 'interaction',
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    config: {
        /**
         * @cfg {Object} gesture
         * Maps gestures that should be used for starting/maintaining/ending the interaction
         * to corresponding class methods.
         * @private
         */
        gestures: {
            tap: 'onGesture'
        },
        /**
         * @cfg {Ext.chart.AbstractChart} chart The chart that the interaction is bound.
         */
        chart: null,
        /**
         * @cfg {Boolean} enabled 'true' if the interaction is enabled.
         */
        enabled: true
    },
    /**
     * Android device is emerging too many events so if we re-render every frame it will
     * take forever to finish a frame.
     * This throttle technique will limit the timespan between two frames.
     */
    throttleGap: 0,
    stopAnimationBeforeSync: false,
    constructor: function(config) {
        var me = this,
            id;
        config = config || {};
        if ('id' in config) {
            id = config.id;
        } else if ('id' in me.config) {
            id = me.config.id;
        } else {
            id = me.getId();
        }
        me.setId(id);
        me.mixins.observable.constructor.call(me, config);
    },
    updateChart: function(newChart, oldChart) {
        var me = this;
        if (oldChart === newChart) {
            return;
        }
        if (oldChart) {
            oldChart.unregister(me);
            me.removeChartListener(oldChart);
        }
        if (newChart) {
            newChart.register(me);
            me.addChartListener();
        }
    },
    updateEnabled: function(enabled) {
        var me = this,
            chart = me.getChart();
        if (chart) {
            if (enabled) {
                me.addChartListener();
            } else {
                me.removeChartListener(chart);
            }
        }
    },
    /**
     * @method
     * @protected
     * Placeholder method.
     */
    onGesture: Ext.emptyFn,
    /**
     * @protected
     * Find and return a single series item corresponding to the given event,
     * or null if no matching item is found.
     * @param {Event} e
     * @return {Object} the item object or null if none found.
     */
    getItemForEvent: function(e) {
        var me = this,
            chart = me.getChart(),
            chartXY = chart.getEventXY(e);
        return chart.getItemForPoint(chartXY[0], chartXY[1]);
    },
    /**
     * Find and return all series items corresponding to the given event.
     * @param {Event} e
     * @return {Array} array of matching item objects
     * @private
     * @deprecated 6.5.2 This method is deprecated
     */
    getItemsForEvent: function(e) {
        var me = this,
            chart = me.getChart(),
            chartXY = chart.getEventXY(e);
        return chart.getItemsForPoint(chartXY[0], chartXY[1]);
    },
    /**
     * @private
     */
    addChartListener: function() {
        var me = this,
            chart = me.getChart(),
            gestures = me.getGestures(),
            gesture;
        if (!me.getEnabled()) {
            return;
        }
        function insertGesture(name, fn) {
            chart.addElementListener(name, // wrap the handler so it does not fire if the event is locked
            // by another interaction
            me.listeners[name] = function(e) {
                var locks = me.getLocks(),
                    result;
                if (me.getEnabled() && (!(name in locks) || locks[name] === me)) {
                    result = (Ext.isFunction(fn) ? fn : me[fn]).apply(this, arguments);
                    if (result === false && e && e.stopPropagation) {
                        e.stopPropagation();
                    }
                    return result;
                }
            }, me);
        }
        me.listeners = me.listeners || {};
        for (gesture in gestures) {
            insertGesture(gesture, gestures[gesture]);
        }
    },
    removeChartListener: function(chart) {
        var me = this,
            gestures = me.getGestures(),
            gesture;
        function removeGesture(name) {
            var fn = me.listeners[name];
            if (fn) {
                chart.removeElementListener(name, fn);
                delete me.listeners[name];
            }
        }
        if (me.listeners) {
            for (gesture in gestures) {
                removeGesture(gesture);
            }
        }
    },
    lockEvents: function() {
        var me = this,
            locks = me.getLocks(),
            args = Array.prototype.slice.call(arguments),
            i = args.length;
        while (i--) {
            locks[args[i]] = me;
        }
    },
    unlockEvents: function() {
        var locks = this.getLocks(),
            args = Array.prototype.slice.call(arguments),
            i = args.length;
        while (i--) {
            delete locks[args[i]];
        }
    },
    getLocks: function() {
        var chart = this.getChart();
        return chart.lockedEvents || (chart.lockedEvents = {});
    },
    doSync: function() {
        var me = this,
            chart = me.getChart();
        if (me.syncTimer) {
            Ext.undefer(me.syncTimer);
            me.syncTimer = null;
        }
        if (me.stopAnimationBeforeSync) {
            chart.animationSuspendCount++;
        }
        chart.redraw();
        if (me.stopAnimationBeforeSync) {
            chart.animationSuspendCount--;
        }
        me.syncThrottle = Date.now() + me.throttleGap;
    },
    sync: function() {
        var me = this;
        if (me.throttleGap && Ext.frameStartTime < me.syncThrottle) {
            if (me.syncTimer) {
                return;
            }
            me.syncTimer = Ext.defer(function() {
                me.doSync();
            }, me.throttleGap);
        } else {
            me.doSync();
        }
    },
    getItemId: function() {
        return this.getId();
    },
    isXType: function(xtype) {
        return xtype === 'interaction';
    },
    destroy: function() {
        var me = this;
        me.setChart(null);
        delete me.listeners;
        me.callParent();
    }
}, function() {
    if (Ext.os.is.Android4) {
        this.prototype.throttleGap = 40;
    }
});

/**
 * Mixin that provides the functionality to place markers.
 */
Ext.define('Ext.chart.MarkerHolder', {
    extend: 'Ext.Mixin',
    requires: [
        'Ext.chart.Markers'
    ],
    mixinConfig: {
        id: 'markerHolder',
        after: {
            constructor: 'constructor',
            preRender: 'preRender'
        },
        before: {
            destroy: 'destroy'
        }
    },
    isMarkerHolder: true,
    // The combined transformation applied to the sprite by its parents.
    // Does not include the transformation matrix of the sprite itself.
    surfaceMatrix: null,
    // The inverse of the above transformation to go back to the original state.
    inverseSurfaceMatrix: null,
    deprecated: {
        6: {
            methods: {
                /**
                 * Returns the markers bound to the given name.
                 * @param {String} name The name of the marker (e.g., "items", "labels", etc.).
                 * @return {Ext.chart.Markers[]}
                 * @method getBoundMarker
                 * @deprecated 6.0 Use {@link #getMarker} instead.
                 */
                getBoundMarker: {
                    message: "Please use the 'getMarker' method instead.",
                    fn: function(name) {
                        var marker = this.boundMarkers[name];
                        return marker ? [
                            marker
                        ] : marker;
                    }
                }
            }
        }
    },
    constructor: function() {
        this.boundMarkers = {};
        this.cleanRedraw = false;
    },
    /**
     * Registers the given marker with the marker holder under the specified name.
     * @param {String} name The name of the marker (e.g., "items", "labels", etc.).
     * @param {Ext.chart.Markers} marker
     */
    bindMarker: function(name, marker) {
        var me = this,
            markers = me.boundMarkers;
        if (marker && marker.isMarkers) {
            //<debug>
            if (markers[name] && markers[name] === marker) {
                Ext.log.warn(me.getId(), " (MarkerHolder): the Markers instance '", marker.getId(), "' is already bound under the name '", name, "'.");
            }
            //</debug>
            me.releaseMarker(name);
            markers[name] = marker;
            marker.on('destroy', me.onMarkerDestroy, me);
        }
    },
    onMarkerDestroy: function(marker) {
        this.releaseMarker(marker);
    },
    /**
     * Unregisters the given marker or a marker with the given name.
     * Providing a name of the marker is more efficient as it avoids lookup.
     * @param marker {String/Ext.chart.Markers}
     * @return {Ext.chart.Markers} Released marker or null.
     */
    releaseMarker: function(marker) {
        var markers = this.boundMarkers,
            name;
        if (marker && marker.isMarkers) {
            for (name in markers) {
                if (markers[name] === marker) {
                    delete markers[name];
                    break;
                }
            }
        } else {
            name = marker;
            marker = markers[name];
            delete markers[name];
        }
        return marker || null;
    },
    /**
     * Returns the marker bound to the given name (or null). See {@link #bindMarker}.
     * @param {String} name The name of the marker (e.g., "items", "labels", etc.).
     * @return {Ext.chart.Markers}
     */
    getMarker: function(name) {
        return this.boundMarkers[name] || null;
    },
    preRender: function(surface, ctx, rect) {
        var me = this,
            id = me.getId(),
            boundMarkers = me.boundMarkers,
            parent = me.getParent(),
            name, marker, matrix;
        if (me.surfaceMatrix) {
            matrix = me.surfaceMatrix.set(1, 0, 0, 1, 0, 0);
        } else {
            matrix = me.surfaceMatrix = new Ext.draw.Matrix();
        }
        me.cleanRedraw = !me.attr.dirty;
        if (!me.cleanRedraw) {
            for (name in boundMarkers) {
                marker = boundMarkers[name];
                if (marker) {
                    marker.clear(id);
                }
            }
        }
        // Parent can be either a sprite (like a composite or instancing)
        // or a surface. First, climb up and apply transformations of the
        // parent sprites.
        while (parent && parent.attr && parent.attr.matrix) {
            matrix.prependMatrix(parent.attr.matrix);
            parent = parent.getParent();
        }
        // Finally, apply the transformation used by the surface.
        matrix.prependMatrix(parent.matrix);
        me.surfaceMatrix = matrix;
        me.inverseSurfaceMatrix = matrix.inverse(me.inverseSurfaceMatrix);
    },
    putMarker: function(name, attr, index, bypassNormalization, keepRevision) {
        var marker = this.boundMarkers[name];
        if (marker) {
            marker.putMarkerFor(this.getId(), attr, index, bypassNormalization, keepRevision);
        }
    },
    getMarkerBBox: function(name, index, isWithoutTransform) {
        var marker = this.boundMarkers[name];
        if (marker) {
            return marker.getMarkerBBoxFor(this.getId(), index, isWithoutTransform);
        }
    },
    destroy: function() {
        var boundMarkers = this.boundMarkers,
            name, marker;
        for (name in boundMarkers) {
            marker = boundMarkers[name];
            marker.destroy();
        }
    }
});

/**
 * @private
 * @class Ext.chart.axis.sprite.Axis
 * @extends Ext.draw.sprite.Sprite
 *
 * The axis sprite. Currently all types of the axis will be rendered with this sprite.
 */
Ext.define('Ext.chart.axis.sprite.Axis', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.axis',
    type: 'axis',
    mixins: {
        markerHolder: 'Ext.chart.MarkerHolder'
    },
    requires: [
        'Ext.draw.sprite.Text'
    ],
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Boolean} grid 'true' if the axis has a grid.
                 */
                grid: 'bool',
                /**
                 * @cfg {Boolean} axisLine 'true' if the main line of the axis is drawn.
                 */
                axisLine: 'bool',
                /**
                 * @cfg {Boolean} minorTicks 'true' if the axis has sub ticks.
                 */
                minorTicks: 'bool',
                /**
                 * @cfg {Number} minorTickSize The length of the minor ticks.
                 */
                minorTickSize: 'number',
                /**
                 * @cfg {Boolean} majorTicks 'true' if the axis has major ticks.
                 */
                majorTicks: 'bool',
                /**
                 * @cfg {Number} majorTickSize The length of the major ticks.
                 */
                majorTickSize: 'number',
                /**
                 * @cfg {Number} length The total length of the axis.
                 */
                length: 'number',
                /**
                 * @private
                 * @cfg {Number} startGap Axis start determined by the chart inset padding.
                 */
                startGap: 'number',
                /**
                 * @private
                 * @cfg {Number} endGap Axis end determined by the chart inset padding.
                 */
                endGap: 'number',
                /**
                 * @cfg {Number} dataMin The minimum value of the axis data.
                 */
                dataMin: 'number',
                /**
                 * @cfg {Number} dataMax The maximum value of the axis data.
                 */
                dataMax: 'number',
                /**
                 * @cfg {Number} visibleMin The minimum value that is displayed.
                 */
                visibleMin: 'number',
                /**
                 * @cfg {Number} visibleMax The maximum value that is displayed.
                 */
                visibleMax: 'number',
                /**
                 * @cfg {String} position The position of the axis on the chart.
                 */
                position: 'enums(left,right,top,bottom,angular,radial,gauge)',
                /**
                 * @cfg {Number} minStepSize The minimum step size between ticks.
                 */
                minStepSize: 'number',
                /**
                 * @private
                 * @cfg {Number} estStepSize The estimated step size between ticks.
                 */
                estStepSize: 'number',
                /**
                 * @private
                 * Unused.
                 */
                titleOffset: 'number',
                /**
                 * @cfg {Number} [textPadding=0]
                 * The padding around axis labels to determine collision.
                 * The default is 0 for all axes except horizontal axes of cartesian charts,
                 * where the default is 5 to prevent axis labels from blending one into another.
                 * This default is defined in the {@link Ext.chart.theme.Base#axis axis} config
                 * of the {@link Ext.chart.theme.Base Base} theme.
                 * You may want to change this default to a smaller number or 0, if you have
                 * horizontal axis labels rotated, which allows for more text to fit in.
                 */
                textPadding: 'number',
                /**
                 * @cfg {Number} min The minimum value of the axis.
                 * `min` and {@link #max} attributes represent the effective range of the axis
                 * after segmentation, layout, and range reconciliation between axes.
                 */
                min: 'number',
                /**
                 * @cfg {Number} max The maximum value of the axis.
                 * {@link #min} and `max` attributes represent the effective range of the axis
                 * after segmentation, layout, and range reconciliation between axes.
                 */
                max: 'number',
                /**
                 * @cfg {Number} centerX The central point of the angular axis on the x-axis.
                 */
                centerX: 'number',
                /**
                 * @cfg {Number} centerY The central point of the angular axis on the y-axis.
                 */
                centerY: 'number',
                /**
                 * @private
                 * @cfg {Number} radius
                 * Unused.
                 */
                radius: 'number',
                /**
                 * @private
                 */
                totalAngle: 'number',
                /**
                 * @cfg {Number} baseRotation The starting rotation of the angular axis.
                 */
                baseRotation: 'number',
                /**
                 * @private
                 * Unused.
                 */
                data: 'default',
                /**
                 * @cfg {Boolean} 'true' if the estimated step size is adjusted by text size.
                 */
                enlargeEstStepSizeByText: 'bool'
            },
            defaults: {
                grid: false,
                axisLine: true,
                minorTicks: false,
                minorTickSize: 3,
                majorTicks: true,
                majorTickSize: 5,
                length: 0,
                startGap: 0,
                endGap: 0,
                visibleMin: 0,
                visibleMax: 1,
                dataMin: 0,
                dataMax: 1,
                position: '',
                minStepSize: 0,
                estStepSize: 20,
                min: 0,
                max: 1,
                centerX: 0,
                centerY: 0,
                radius: 1,
                baseRotation: 0,
                data: null,
                titleOffset: 0,
                textPadding: 0,
                scalingCenterY: 0,
                scalingCenterX: 0,
                // Override default
                strokeStyle: 'black',
                enlargeEstStepSizeByText: false
            },
            triggers: {
                minorTickSize: 'bbox',
                majorTickSize: 'bbox',
                position: 'bbox,layout',
                axisLine: 'bbox,layout',
                minorTicks: 'layout',
                min: 'layout',
                max: 'layout',
                length: 'layout',
                minStepSize: 'layout',
                estStepSize: 'layout',
                data: 'layout',
                dataMin: 'layout',
                dataMax: 'layout',
                visibleMin: 'layout',
                visibleMax: 'layout',
                enlargeEstStepSizeByText: 'layout'
            },
            updaters: {
                layout: 'layoutUpdater'
            }
        }
    },
    config: {
        /**
         * @cfg {Object} label
         *
         * The label configuration object for the Axis. This object may include style attributes
         * like `spacing`, `padding`, `font` that receives a string or number and
         * returns a new string with the modified values.
         */
        label: null,
        /**
         * @cfg {Number} labelOffset
         * The distance between the label and the edge of a major tick.
         * Only applicable for 'gauge' and 'angular' axes.
         */
        labelOffset: 10,
        /**
         * @cfg {Object|Ext.chart.axis.layout.Layout} layout The layout configuration used by
         * the axis.
         */
        layout: null,
        /**
         * @cfg {Object|Ext.chart.axis.segmenter.Segmenter} segmenter The method of segmenter
         * used by the axis.
         */
        segmenter: null,
        /**
         * @cfg {Function} renderer Allows direct customisation of rendered axis sprites.
         */
        renderer: null,
        /**
         * @private
         * @cfg {Object} layoutContext Stores the context after calculating layout.
         */
        layoutContext: null,
        /**
         * @cfg {Ext.chart.axis.Axis} axis The axis represented by this sprite.
         */
        axis: null
    },
    thickness: 0,
    stepSize: 0,
    getBBox: function() {
        return null;
    },
    defaultRenderer: function(v) {
        // 'this' pointer in this case is a layoutContext
        return this.segmenter.renderer(v, this);
    },
    layoutUpdater: function() {
        var me = this,
            chart = me.getAxis().getChart();
        if (chart.isInitializing) {
            return;
        }
        // eslint-disable-next-line vars-on-top, one-var
        var attr = me.attr,
            layout = me.getLayout(),
            isRtl = chart.getInherited().rtl,
            dataRange = attr.dataMax - attr.dataMin,
            min = attr.dataMin + dataRange * attr.visibleMin,
            max = attr.dataMin + dataRange * attr.visibleMax,
            range = max - min,
            position = attr.position,
            context = {
                attr: attr,
                segmenter: me.getSegmenter(),
                renderer: me.defaultRenderer
            };
        if (position === 'left' || position === 'right') {
            attr.translationX = 0;
            attr.translationY = max * attr.length / range;
            attr.scalingX = 1;
            attr.scalingY = -attr.length / range;
            attr.scalingCenterY = 0;
            attr.scalingCenterX = 0;
            me.applyTransformations(true);
        } else if (position === 'top' || position === 'bottom') {
            if (isRtl) {
                attr.translationX = attr.length + min * attr.length / range + 1;
            } else {
                attr.translationX = -min * attr.length / range;
            }
            attr.translationY = 0;
            attr.scalingX = (isRtl ? -1 : 1) * attr.length / range;
            attr.scalingY = 1;
            attr.scalingCenterY = 0;
            attr.scalingCenterX = 0;
            me.applyTransformations(true);
        }
        if (layout) {
            layout.calculateLayout(context);
            me.setLayoutContext(context);
        }
    },
    iterate: function(snaps, fn) {
        var i, position, id, axis, floatingAxes, floatingValues,
            some = Ext.Array.some,
            abs = Math.abs,
            threshold, isTickVisible;
        if (snaps.getLabel) {
            // Discrete layout.
            if (snaps.min < snaps.from) {
                fn.call(this, snaps.min, snaps.getLabel(snaps.min), -1, snaps);
            }
            for (i = 0; i <= snaps.steps; i++) {
                fn.call(this, snaps.get(i), snaps.getLabel(i), i, snaps);
            }
            if (snaps.max > snaps.to) {
                fn.call(this, snaps.max, snaps.getLabel(snaps.max), snaps.steps + 1, snaps);
            }
        } else {
            axis = this.getAxis();
            floatingAxes = axis.floatingAxes;
            floatingValues = [];
            threshold = (snaps.to - snaps.from) / (snaps.steps + 1);
            if (axis.getFloating()) {
                for (id in floatingAxes) {
                    floatingValues.push(floatingAxes[id]);
                }
            }
            // Don't render ticks in axes intersection points.
            isTickVisible = function(position) {
                return !floatingValues.length || some(floatingValues, function(value) {
                    return abs(value - position) > threshold;
                });
            };
            if (snaps.min < snaps.from && isTickVisible(snaps.min)) {
                fn.call(this, snaps.min, snaps.min, -1, snaps);
            }
            for (i = 0; i <= snaps.steps; i++) {
                position = snaps.get(i);
                if (isTickVisible(position)) {
                    fn.call(this, position, position, i, snaps);
                }
            }
            if (snaps.max > snaps.to && isTickVisible(snaps.max)) {
                fn.call(this, snaps.max, snaps.max, snaps.steps + 1, snaps);
            }
        }
    },
    renderTicks: function(surface, ctx, layout, clipRect) {
        var me = this,
            attr = me.attr,
            docked = attr.position,
            matrix = attr.matrix,
            halfLineWidth = 0.5 * attr.lineWidth,
            xx = matrix.getXX(),
            dx = matrix.getDX(),
            yy = matrix.getYY(),
            dy = matrix.getDY(),
            majorTicks = layout.majorTicks,
            majorTickSize = attr.majorTickSize,
            minorTicks = layout.minorTicks,
            minorTickSize = attr.minorTickSize,
            gaugeAngles;
        /* eslint-disable no-inner-declarations, no-case-declarations */
        if (majorTicks) {
            switch (docked) {
                case 'right':
                    function getRightTickFn(size) {
                        return function(position, labelText, i) {
                            position = surface.roundPixel(position * yy + dy) + halfLineWidth;
                            ctx.moveTo(0, position);
                            ctx.lineTo(size, position);
                        };
                    };
                    me.iterate(majorTicks, getRightTickFn(majorTickSize));
                    if (minorTicks) {
                        me.iterate(minorTicks, getRightTickFn(minorTickSize));
                    };
                    break;
                case 'left':
                    function getLeftTickFn(size) {
                        return function(position, labelText, i) {
                            position = surface.roundPixel(position * yy + dy) + halfLineWidth;
                            ctx.moveTo(clipRect[2] - size, position);
                            ctx.lineTo(clipRect[2], position);
                        };
                    };
                    me.iterate(majorTicks, getLeftTickFn(majorTickSize));
                    if (minorTicks) {
                        me.iterate(minorTicks, getLeftTickFn(minorTickSize));
                    };
                    break;
                case 'bottom':
                    function getBottomTickFn(size) {
                        return function(position, labelText, i) {
                            position = surface.roundPixel(position * xx + dx) - halfLineWidth;
                            ctx.moveTo(position, 0);
                            ctx.lineTo(position, size);
                        };
                    };
                    me.iterate(majorTicks, getBottomTickFn(majorTickSize));
                    if (minorTicks) {
                        me.iterate(minorTicks, getBottomTickFn(minorTickSize));
                    };
                    break;
                case 'top':
                    function getTopTickFn(size) {
                        return function(position, labelText, i) {
                            position = surface.roundPixel(position * xx + dx) - halfLineWidth;
                            ctx.moveTo(position, clipRect[3]);
                            ctx.lineTo(position, clipRect[3] - size);
                        };
                    };
                    me.iterate(majorTicks, getTopTickFn(majorTickSize));
                    if (minorTicks) {
                        me.iterate(minorTicks, getTopTickFn(minorTickSize));
                    };
                    break;
                case 'angular':
                    me.iterate(majorTicks, function(position, labelText, i) {
                        position = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
                        ctx.moveTo(attr.centerX + (attr.length) * Math.cos(position), attr.centerY + (attr.length) * Math.sin(position));
                        ctx.lineTo(attr.centerX + (attr.length + majorTickSize) * Math.cos(position), attr.centerY + (attr.length + majorTickSize) * Math.sin(position));
                    });
                    break;
                case 'gauge':
                    gaugeAngles = me.getGaugeAngles();
                    me.iterate(majorTicks, function(position, labelText, i) {
                        position = (position - attr.min) / (attr.max - attr.min) * attr.totalAngle - attr.totalAngle + gaugeAngles.start;
                        ctx.moveTo(attr.centerX + (attr.length) * Math.cos(position), attr.centerY + (attr.length) * Math.sin(position));
                        ctx.lineTo(attr.centerX + (attr.length + majorTickSize) * Math.cos(position), attr.centerY + (attr.length + majorTickSize) * Math.sin(position));
                    });
                    break;
            }
        }
    },
    /* eslint-enable no-inner-declarations, no-case-declarations */
    renderLabels: function(surface, ctx, layoutContext, clipRect) {
        var me = this,
            attr = me.attr,
            halfLineWidth = 0.5 * attr.lineWidth,
            docked = attr.position,
            matrix = attr.matrix,
            textPadding = attr.textPadding,
            xx = matrix.getXX(),
            dx = matrix.getDX(),
            yy = matrix.getYY(),
            dy = matrix.getDY(),
            thickness = 0,
            majorTicks = layoutContext.majorTicks,
            tickPadding = Math.max(attr.majorTickSize, attr.minorTickSize) + attr.lineWidth,
            isBBoxIntersect = Ext.draw.Draw.isBBoxIntersect,
            label = me.getLabel(),
            font,
            labelOffset = me.getLabelOffset(),
            lastLabelText = null,
            textSize = 0,
            textCount = 0,
            segmenter = layoutContext.segmenter,
            renderer = me.getRenderer(),
            axis = me.getAxis(),
            title = axis.getTitle(),
            titleBBox = title && title.attr.text !== '' && title.getBBox(),
            labelInverseMatrix,
            lastBBox = null,
            bbox, fly, text, titlePadding, translation, gaugeAngles, angle;
        if (majorTicks && label && !label.attr.hidden) {
            font = label.attr.font;
            if (ctx.font !== font) {
                ctx.font = font;
            }
            // This can profoundly improve performance.
            label.setAttributes({
                translationX: 0,
                translationY: 0
            }, true);
            label.applyTransformations();
            labelInverseMatrix = label.attr.inverseMatrix.elements.slice(0);
            switch (docked) {
                case 'left':
                    titlePadding = titleBBox ? titleBBox.x + titleBBox.width : 0;
                    switch (label.attr.textAlign) {
                        case 'start':
                            translation = surface.roundPixel(titlePadding + dx) - halfLineWidth;
                            break;
                        case 'end':
                            translation = surface.roundPixel(clipRect[2] - tickPadding + dx) - halfLineWidth;
                            break;
                        default:
                            // 'center'
                            translation = surface.roundPixel(titlePadding + (clipRect[2] - titlePadding - tickPadding) / 2 + dx) - halfLineWidth;
                    };
                    label.setAttributes({
                        translationX: translation
                    }, true);
                    break;
                case 'right':
                    titlePadding = titleBBox ? clipRect[2] - titleBBox.x : 0;
                    switch (label.attr.textAlign) {
                        case 'start':
                            translation = surface.roundPixel(tickPadding + dx) + halfLineWidth;
                            break;
                        case 'end':
                            translation = surface.roundPixel(clipRect[2] - titlePadding + dx) + halfLineWidth;
                            break;
                        default:
                            // 'center'
                            translation = surface.roundPixel(tickPadding + (clipRect[2] - tickPadding - titlePadding) / 2 + dx) + halfLineWidth;
                    };
                    label.setAttributes({
                        translationX: translation
                    }, true);
                    break;
                case 'top':
                    titlePadding = titleBBox ? titleBBox.y + titleBBox.height : 0;
                    label.setAttributes({
                        translationY: surface.roundPixel(titlePadding + (clipRect[3] - titlePadding - tickPadding) / 2) - halfLineWidth
                    }, true);
                    break;
                case 'bottom':
                    titlePadding = titleBBox ? clipRect[3] - titleBBox.y : 0;
                    label.setAttributes({
                        translationY: surface.roundPixel(tickPadding + (clipRect[3] - tickPadding - titlePadding) / 2) + halfLineWidth
                    }, true);
                    break;
                case 'radial':
                    label.setAttributes({
                        translationX: attr.centerX
                    }, true);
                    break;
                case 'angular':
                    label.setAttributes({
                        translationY: attr.centerY
                    }, true);
                    break;
                case 'gauge':
                    label.setAttributes({
                        translationY: attr.centerY
                    }, true);
                    break;
            }
            // TODO: there are better ways to detect collision.
            if (docked === 'left' || docked === 'right') {
                me.iterate(majorTicks, function(position, labelText, i) {
                    if (labelText === undefined) {
                        return;
                    }
                    if (renderer) {
                        text = Ext.callback(renderer, null, [
                            axis,
                            labelText,
                            layoutContext,
                            lastLabelText
                        ], 0, axis);
                    } else {
                        text = segmenter.renderer(labelText, layoutContext, lastLabelText);
                    }
                    lastLabelText = labelText;
                    label.setAttributes({
                        text: String(text),
                        translationY: surface.roundPixel(position * yy + dy)
                    }, true);
                    label.applyTransformations();
                    thickness = Math.max(thickness, label.getBBox().width + tickPadding);
                    fly = Ext.draw.Matrix.fly(label.attr.matrix.elements.slice(0));
                    bbox = fly.prepend.apply(fly, labelInverseMatrix).transformBBox(label.getBBox(true));
                    if (lastBBox && !isBBoxIntersect(bbox, lastBBox, textPadding)) {
                        return;
                    }
                    surface.renderSprite(label);
                    lastBBox = bbox;
                    textSize += bbox.height;
                    textCount++;
                });
            } else if (docked === 'top' || docked === 'bottom') {
                me.iterate(majorTicks, function(position, labelText, i) {
                    if (labelText === undefined) {
                        return;
                    }
                    if (renderer) {
                        text = Ext.callback(renderer, null, [
                            axis,
                            labelText,
                            layoutContext,
                            lastLabelText
                        ], 0, axis);
                    } else {
                        text = segmenter.renderer(labelText, layoutContext, lastLabelText);
                    }
                    lastLabelText = labelText;
                    label.setAttributes({
                        text: String(text),
                        translationX: surface.roundPixel(position * xx + dx)
                    }, true);
                    label.applyTransformations();
                    thickness = Math.max(thickness, label.getBBox().height + tickPadding);
                    fly = Ext.draw.Matrix.fly(label.attr.matrix.elements.slice(0));
                    bbox = fly.prepend.apply(fly, labelInverseMatrix).transformBBox(label.getBBox(true));
                    if (lastBBox && !isBBoxIntersect(bbox, lastBBox, textPadding)) {
                        return;
                    }
                    surface.renderSprite(label);
                    lastBBox = bbox;
                    textSize += bbox.width;
                    textCount++;
                });
            } else if (docked === 'radial') {
                me.iterate(majorTicks, function(position, labelText, i) {
                    if (labelText === undefined) {
                        return;
                    }
                    if (renderer) {
                        text = Ext.callback(renderer, null, [
                            axis,
                            labelText,
                            layoutContext,
                            lastLabelText
                        ], 0, axis);
                    } else {
                        text = segmenter.renderer(labelText, layoutContext, lastLabelText);
                    }
                    lastLabelText = labelText;
                    if (typeof text !== 'undefined') {
                        label.setAttributes({
                            text: String(text),
                            translationX: attr.centerX - surface.roundPixel(position) / attr.max * attr.length * Math.cos(attr.baseRotation + Math.PI / 2),
                            translationY: attr.centerY - surface.roundPixel(position) / attr.max * attr.length * Math.sin(attr.baseRotation + Math.PI / 2)
                        }, true);
                        label.applyTransformations();
                        bbox = label.attr.matrix.transformBBox(label.getBBox(true));
                        if (lastBBox && !isBBoxIntersect(bbox, lastBBox)) {
                            return;
                        }
                        surface.renderSprite(label);
                        lastBBox = bbox;
                        textSize += bbox.width;
                        textCount++;
                    }
                });
            } else if (docked === 'angular') {
                labelOffset += attr.majorTickSize + attr.lineWidth * 0.5;
                me.iterate(majorTicks, function(position, labelText, i) {
                    if (labelText === undefined) {
                        return;
                    }
                    if (renderer) {
                        text = Ext.callback(renderer, null, [
                            axis,
                            labelText,
                            layoutContext,
                            lastLabelText
                        ], 0, axis);
                    } else {
                        text = segmenter.renderer(labelText, layoutContext, lastLabelText);
                    }
                    lastLabelText = labelText;
                    thickness = Math.max(thickness, Math.max(attr.majorTickSize, attr.minorTickSize) + (attr.lineCap !== 'butt' ? attr.lineWidth * 0.5 : 0));
                    if (typeof text !== 'undefined') {
                        angle = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
                        label.setAttributes({
                            text: String(text),
                            translationX: attr.centerX + (attr.length + labelOffset) * Math.cos(angle),
                            translationY: attr.centerY + (attr.length + labelOffset) * Math.sin(angle)
                        }, true);
                        label.applyTransformations();
                        bbox = label.attr.matrix.transformBBox(label.getBBox(true));
                        if (lastBBox && !isBBoxIntersect(bbox, lastBBox)) {
                            return;
                        }
                        surface.renderSprite(label);
                        lastBBox = bbox;
                        textSize += bbox.width;
                        textCount++;
                    }
                });
            } else if (docked === 'gauge') {
                gaugeAngles = me.getGaugeAngles();
                labelOffset += attr.majorTickSize + attr.lineWidth * 0.5;
                me.iterate(majorTicks, function(position, labelText, i) {
                    if (labelText === undefined) {
                        return;
                    }
                    if (renderer) {
                        text = Ext.callback(renderer, null, [
                            axis,
                            labelText,
                            layoutContext,
                            lastLabelText
                        ], 0, axis);
                    } else {
                        text = segmenter.renderer(labelText, layoutContext, lastLabelText);
                    }
                    lastLabelText = labelText;
                    if (typeof text !== 'undefined') {
                        angle = (position - attr.min) / (attr.max - attr.min) * attr.totalAngle - attr.totalAngle + gaugeAngles.start;
                        label.setAttributes({
                            text: String(text),
                            translationX: attr.centerX + (attr.length + labelOffset) * Math.cos(angle),
                            translationY: attr.centerY + (attr.length + labelOffset) * Math.sin(angle)
                        }, true);
                        label.applyTransformations();
                        bbox = label.attr.matrix.transformBBox(label.getBBox(true));
                        if (lastBBox && !isBBoxIntersect(bbox, lastBBox)) {
                            return;
                        }
                        surface.renderSprite(label);
                        lastBBox = bbox;
                        textSize += bbox.width;
                        textCount++;
                    }
                });
            }
            if (attr.enlargeEstStepSizeByText && textCount) {
                textSize /= textCount;
                textSize += tickPadding;
                textSize *= 2;
                if (attr.estStepSize < textSize) {
                    attr.estStepSize = textSize;
                }
            }
            if (Math.abs(me.thickness - thickness) > 1) {
                me.thickness = thickness;
                attr.bbox.plain.dirty = true;
                attr.bbox.transform.dirty = true;
                me.doThicknessChanged();
                return false;
            }
        }
    },
    renderAxisLine: function(surface, ctx, layout, clipRect) {
        var me = this,
            attr = me.attr,
            halfLineWidth = attr.lineWidth * 0.5,
            docked = attr.position,
            position, gaugeAngles;
        if (attr.axisLine && attr.length) {
            switch (docked) {
                case 'left':
                    position = surface.roundPixel(clipRect[2]) - halfLineWidth;
                    ctx.moveTo(position, -attr.endGap);
                    ctx.lineTo(position, attr.length + attr.startGap + 1);
                    break;
                case 'right':
                    ctx.moveTo(halfLineWidth, -attr.endGap);
                    ctx.lineTo(halfLineWidth, attr.length + attr.startGap + 1);
                    break;
                case 'bottom':
                    ctx.moveTo(-attr.startGap, halfLineWidth);
                    ctx.lineTo(attr.length + attr.endGap, halfLineWidth);
                    break;
                case 'top':
                    position = surface.roundPixel(clipRect[3]) - halfLineWidth;
                    ctx.moveTo(-attr.startGap, position);
                    ctx.lineTo(attr.length + attr.endGap, position);
                    break;
                case 'angular':
                    ctx.moveTo(attr.centerX + attr.length, attr.centerY);
                    ctx.arc(attr.centerX, attr.centerY, attr.length, 0, Math.PI * 2, true);
                    break;
                case 'gauge':
                    gaugeAngles = me.getGaugeAngles();
                    ctx.moveTo(attr.centerX + Math.cos(gaugeAngles.start) * attr.length, attr.centerY + Math.sin(gaugeAngles.start) * attr.length);
                    ctx.arc(attr.centerX, attr.centerY, attr.length, gaugeAngles.start, gaugeAngles.end, true);
                    break;
            }
        }
    },
    getGaugeAngles: function() {
        var me = this,
            angle = me.attr.totalAngle,
            offset;
        if (angle <= Math.PI) {
            offset = (Math.PI - angle) * 0.5;
        } else {
            offset = -(Math.PI * 2 - angle) * 0.5;
        }
        offset = Math.PI * 2 - offset;
        return {
            start: offset,
            end: offset - angle
        };
    },
    renderGridLines: function(surface, ctx, layout, clipRect) {
        var me = this,
            axis = me.getAxis(),
            attr = me.attr,
            matrix = attr.matrix,
            startGap = attr.startGap,
            endGap = attr.endGap,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            position = attr.position,
            alignment = axis.getGridAlignment(),
            majorTicks = layout.majorTicks,
            anchor, j, lastAnchor;
        if (attr.grid) {
            if (majorTicks) {
                if (position === 'left' || position === 'right') {
                    lastAnchor = attr.min * yy + dy + endGap + startGap;
                    me.iterate(majorTicks, function(position, labelText, i) {
                        anchor = position * yy + dy + endGap;
                        me.putMarker(alignment + '-' + (i % 2 ? 'odd' : 'even'), {
                            y: anchor,
                            height: lastAnchor - anchor
                        }, j = i, true);
                        lastAnchor = anchor;
                    });
                    j++;
                    anchor = 0;
                    me.putMarker(alignment + '-' + (j % 2 ? 'odd' : 'even'), {
                        y: anchor,
                        height: lastAnchor - anchor
                    }, j, true);
                } else if (position === 'top' || position === 'bottom') {
                    lastAnchor = attr.min * xx + dx + startGap;
                    if (startGap) {
                        me.putMarker(alignment + '-even', {
                            x: 0,
                            width: lastAnchor
                        }, -1, true);
                    }
                    me.iterate(majorTicks, function(position, labelText, i) {
                        anchor = position * xx + dx + startGap;
                        me.putMarker(alignment + '-' + (i % 2 ? 'odd' : 'even'), {
                            x: anchor,
                            width: lastAnchor - anchor
                        }, j = i, true);
                        lastAnchor = anchor;
                    });
                    j++;
                    anchor = attr.length + attr.startGap + attr.endGap;
                    me.putMarker(alignment + '-' + (j % 2 ? 'odd' : 'even'), {
                        x: anchor,
                        width: lastAnchor - anchor
                    }, j, true);
                } else if (position === 'radial') {
                    me.iterate(majorTicks, function(position, labelText, i) {
                        if (!position) {
                            return;
                        }
                        anchor = position / attr.max * attr.length;
                        me.putMarker(alignment + '-' + (i % 2 ? 'odd' : 'even'), {
                            scalingX: anchor,
                            scalingY: anchor
                        }, i, true);
                        lastAnchor = anchor;
                    });
                } else if (position === 'angular') {
                    me.iterate(majorTicks, function(position, labelText, i) {
                        if (!attr.length) {
                            return;
                        }
                        anchor = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
                        me.putMarker(alignment + '-' + (i % 2 ? 'odd' : 'even'), {
                            rotationRads: anchor,
                            rotationCenterX: 0,
                            rotationCenterY: 0,
                            scalingX: attr.length,
                            scalingY: attr.length
                        }, i, true);
                        lastAnchor = anchor;
                    });
                }
            }
        }
    },
    renderLimits: function(clipRect) {
        var me = this,
            attr = me.attr,
            axis = me.getAxis(),
            limits = Ext.Array.from(axis.getLimits());
        if (!limits.length || attr.dataMin === attr.dataMax) {
            if (axis.limits) {
                axis.limits.titles.attr.hidden = true;
            }
            return;
        }
        // eslint-disable-next-line vars-on-top, one-var
        var chart = axis.getChart(),
            innerPadding = chart.getInnerPadding(),
            limitsRect = axis.limits.surface.getRect(),
            matrix = attr.matrix,
            position = attr.position,
            chain = Ext.Object.chain,
            titles = axis.limits.titles,
            titleBBox, titlePosition, titleFlip, limit, value, i, ln, x, y;
        titles.attr.hidden = false;
        titles.instances = [];
        titles.position = 0;
        if (position === 'left' || position === 'right') {
            for (i = 0 , ln = limits.length; i < ln; i++) {
                limit = chain(limits[i]);
                if (!limit.line) {
                    limit.line = {};
                }
                value = Ext.isString(limit.value) ? axis.getCoordFor(limit.value) : limit.value;
                value = value * matrix.getYY() + matrix.getDY();
                limit.line.y = value + innerPadding.top;
                limit.line.strokeStyle = limit.line.strokeStyle || attr.strokeStyle;
                me.putMarker('horizontal-limit-lines', limit.line, i, true);
                if (limit.line.title) {
                    titles.add(limit.line.title);
                    titleBBox = titles.getBBoxFor(titles.position - 1);
                    titlePosition = limit.line.title.position || (position === 'left' ? 'start' : 'end');
                    switch (titlePosition) {
                        case 'start':
                            x = 10;
                            break;
                        case 'end':
                            x = limitsRect[2] - 10;
                            break;
                        case 'middle':
                            x = limitsRect[2] / 2;
                            break;
                    }
                    titles.setAttributesFor(titles.position - 1, {
                        x: x,
                        y: limit.line.y - titleBBox.height / 2,
                        textAlign: titlePosition,
                        fillStyle: limit.line.title.fillStyle || limit.line.strokeStyle
                    });
                }
            }
        } else if (position === 'top' || position === 'bottom') {
            for (i = 0 , ln = limits.length; i < ln; i++) {
                limit = chain(limits[i]);
                if (!limit.line) {
                    limit.line = {};
                }
                value = Ext.isString(limit.value) ? axis.getCoordFor(limit.value) : limit.value;
                value = value * matrix.getXX() + matrix.getDX();
                limit.line.x = value + innerPadding.left;
                limit.line.strokeStyle = limit.line.strokeStyle || attr.strokeStyle;
                me.putMarker('vertical-limit-lines', limit.line, i, true);
                if (limit.line.title) {
                    titles.add(limit.line.title);
                    titleBBox = titles.getBBoxFor(titles.position - 1);
                    titlePosition = limit.line.title.position || (position === 'top' ? 'end' : 'start');
                    switch (titlePosition) {
                        case 'start':
                            y = limitsRect[3] - titleBBox.width / 2 - 10;
                            break;
                        case 'end':
                            y = titleBBox.width / 2 + 10;
                            break;
                        case 'middle':
                            y = limitsRect[3] / 2;
                            break;
                    }
                    titles.setAttributesFor(titles.position - 1, {
                        x: limit.line.x + titleBBox.height / 2,
                        y: y,
                        fillStyle: limit.line.title.fillStyle || limit.line.strokeStyle,
                        rotationRads: Math.PI / 2
                    });
                }
            }
        } else if (position === 'radial') {
            for (i = 0 , ln = limits.length; i < ln; i++) {
                limit = chain(limits[i]);
                if (!limit.line) {
                    limit.line = {};
                }
                value = Ext.isString(limit.value) ? axis.getCoordFor(limit.value) : limit.value;
                if (value > attr.max) {
                    
                    continue;
                }
                value = value / attr.max * attr.length;
                limit.line.cx = attr.centerX;
                limit.line.cy = attr.centerY;
                limit.line.scalingX = value;
                limit.line.scalingY = value;
                limit.line.strokeStyle = limit.line.strokeStyle || attr.strokeStyle;
                me.putMarker('circular-limit-lines', limit.line, i, true);
                if (limit.line.title) {
                    titles.add(limit.line.title);
                    titleBBox = titles.getBBoxFor(titles.position - 1);
                    titles.setAttributesFor(titles.position - 1, {
                        x: attr.centerX,
                        y: attr.centerY - value - titleBBox.height / 2,
                        fillStyle: limit.line.title.fillStyle || limit.line.strokeStyle
                    });
                }
            }
        } else if (position === 'angular') {
            for (i = 0 , ln = limits.length; i < ln; i++) {
                limit = chain(limits[i]);
                if (!limit.line) {
                    limit.line = {};
                }
                value = Ext.isString(limit.value) ? axis.getCoordFor(limit.value) : limit.value;
                value = value / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
                limit.line.translationX = attr.centerX;
                limit.line.translationY = attr.centerY;
                limit.line.rotationRads = value;
                limit.line.rotationCenterX = 0;
                limit.line.rotationCenterY = 0;
                limit.line.scalingX = attr.length;
                limit.line.scalingY = attr.length;
                limit.line.strokeStyle = limit.line.strokeStyle || attr.strokeStyle;
                me.putMarker('radial-limit-lines', limit.line, i, true);
                if (limit.line.title) {
                    titles.add(limit.line.title);
                    titleBBox = titles.getBBoxFor(titles.position - 1);
                    titleFlip = ((value > -0.5 * Math.PI && value < 0.5 * Math.PI) || (value > 1.5 * Math.PI && value < 2 * Math.PI)) ? 1 : -1;
                    titles.setAttributesFor(titles.position - 1, {
                        x: attr.centerX + 0.5 * attr.length * Math.cos(value) + titleFlip * titleBBox.height / 2 * Math.sin(value),
                        y: attr.centerY + 0.5 * attr.length * Math.sin(value) - titleFlip * titleBBox.height / 2 * Math.cos(value),
                        rotationRads: titleFlip === 1 ? value : value - Math.PI,
                        fillStyle: limit.line.title.fillStyle || limit.line.strokeStyle
                    });
                }
            }
        } else if (position === 'gauge') {}
    },
    // TODO
    doThicknessChanged: function() {
        var axis = this.getAxis();
        if (axis) {
            axis.onThicknessChanged();
        }
    },
    render: function(surface, ctx, rect) {
        var me = this,
            layoutContext = me.getLayoutContext();
        if (layoutContext) {
            if (me.renderLabels(surface, ctx, layoutContext, rect) === false) {
                return false;
            }
            ctx.beginPath();
            me.renderTicks(surface, ctx, layoutContext, rect);
            me.renderAxisLine(surface, ctx, layoutContext, rect);
            me.renderGridLines(surface, ctx, layoutContext, rect);
            me.renderLimits(rect);
            ctx.stroke();
        }
    }
});

/**
 * @abstract
 * @class Ext.chart.axis.segmenter.Segmenter
 * 
 * Interface for a segmenter in an Axis. A segmenter defines the operations you can do to a specific
 * data type.
 * 
 * See {@link Ext.chart.axis.Axis}.
 * 
 */
Ext.define('Ext.chart.axis.segmenter.Segmenter', {
    config: {
        /**
         * @cfg {Ext.chart.axis.Axis} axis The axis that the Segmenter is bound.
         */
        axis: null
    },
    constructor: function(config) {
        this.initConfig(config);
    },
    /**
     * This method formats the value.
     * 
     * @param {*} value The value to format.
     * @param {Object} context Axis layout context.
     * @return {String}
     */
    renderer: function(value, context) {
        return String(value);
    },
    /**
     * Convert from any data into the target type.
     * @param {*} value The value to convert from
     * @return {*} The converted value. 
     */
    from: function(value) {
        return value;
    },
    /**
     * @method
     * Returns the difference between the min and max value based on the given unit scale.
     * 
     * @param {*} min The smaller value.
     * @param {*} max The larger value.
     * @param {*} unit The unit scale. Unit can be any type.
     * @return {Number} The number of `unit`s between min and max. It is the minimum n that
     * min + n * unit >= max.
     */
    diff: Ext.emptyFn,
    /**
     * @method
     * Align value with step of units.
     * For example, for the date segmenter, if the unit is "Month" and step is 3, the value will be
     * aligned by seasons.
     * 
     * @param {*} value The value to be aligned.
     * @param {Number} step The step of units.
     * @param {*} unit The unit.
     * @return {*} Aligned value.
     */
    align: Ext.emptyFn,
    /**
     * @method
     * Add `step` `unit`s to the value.
     * @param {*} value  The value to be added.
     * @param {Number} step The step of units. Negative value are allowed.
     * @param {*} unit The unit.
     */
    add: Ext.emptyFn,
    /**
     * @method
     * Given a start point and estimated step size of a range, determine the preferred step size.
     * 
     * @param {*} start The start point of range.
     * @param {*} estStepSize The estimated step size.
     * @return {Object} Return the step size by an object of step x unit.
     * @return {Number} return.step The step count of units.
     * @return {Number|Object} return.unit The unit.
     */
    preferredStep: Ext.emptyFn
});

/**
 * @class Ext.chart.axis.segmenter.Names
 * @extends Ext.chart.axis.segmenter.Segmenter
 * 
 * Names data type. Names will be calculated as their indices in the methods in this class.
 * The `preferredStep` always return `{ unit: 1, step: 1 }` to indicate "show every item".
 * 
 */
Ext.define('Ext.chart.axis.segmenter.Names', {
    extend: 'Ext.chart.axis.segmenter.Segmenter',
    alias: 'segmenter.names',
    renderer: function(value, context) {
        return value;
    },
    diff: function(min, max, unit) {
        return Math.floor(max - min);
    },
    align: function(value, step, unit) {
        return Math.floor(value);
    },
    add: function(value, step, unit) {
        return value + step;
    },
    preferredStep: function(min, estStepSize, minIdx, data) {
        return {
            unit: 1,
            step: 1
        };
    }
});

/**
 * @class Ext.chart.axis.segmenter.Numeric
 * @extends Ext.chart.axis.segmenter.Segmenter
 * 
 * Numeric data type.
 */
Ext.define('Ext.chart.axis.segmenter.Numeric', {
    extend: 'Ext.chart.axis.segmenter.Segmenter',
    alias: 'segmenter.numeric',
    isNumeric: true,
    renderer: function(value, context) {
        return value.toFixed(Math.max(0, context.majorTicks.unit.fixes));
    },
    diff: function(min, max, unit) {
        return Math.floor((max - min) / unit.scale);
    },
    align: function(value, step, unit) {
        var scaledStep = unit.scale * step;
        return Math.floor(value / scaledStep) * scaledStep;
    },
    add: function(value, step, unit) {
        return value + step * unit.scale;
    },
    preferredStep: function(min, estStepSize) {
        // Getting an order of magnitude of the estStepSize with a common logarithm.
        var order = Math.floor(Math.log(estStepSize) * Math.LOG10E),
            scale = Math.pow(10, order);
        estStepSize /= scale;
        if (estStepSize < 2) {
            estStepSize = 2;
        } else if (estStepSize < 5) {
            estStepSize = 5;
        } else if (estStepSize < 10) {
            estStepSize = 10;
            order++;
        }
        return {
            unit: {
                // When passed estStepSize is less than 1, its order of magnitude
                // is equal to -number_of_leading_zeros in the estStepSize.
                fixes: -order,
                // Number of fractional digits.
                scale: scale
            },
            step: estStepSize
        };
    },
    leadingZeros: function(n) {
        // For example:
        // leadingZeros(0.2) is 1,
        // leadingZeros(-0.01) is 2.
        return -Math.floor(Ext.Number.log10(Math.abs(n)));
    },
    /**
     * Wraps the provided estimated step size of a range without altering it into a step size
     * object.
     *
     * @param {*} min The start point of range.
     * @param {*} estStepSize The estimated step size.
     * @return {Object} Return the step size by an object of step x unit.
     * @return {Number} return.step The step count of units.
     * @return {Object} return.unit The unit.
     */
    exactStep: function(min, estStepSize) {
        var stepZeros = this.leadingZeros(estStepSize),
            scale = Math.pow(10, stepZeros);
        return {
            unit: {
                // add one decimal point if estStepSize is not a multiple of scale
                fixes: stepZeros + (estStepSize % scale === 0 ? 0 : 1),
                // Swap scale & step, if the estStepSize < 1,
                // or 'diff' method will give us rounding errors.
                scale: estStepSize < 1 ? estStepSize : 1
            },
            step: estStepSize < 1 ? 1 : estStepSize
        };
    },
    adjustByMajorUnit: function(step, scale, range) {
        var min = range[0],
            max = range[1],
            increment = step * scale,
            remainder, multiplier;
        multiplier = Math.max(1 / (min || 1), 1 / (increment || 1));
        multiplier = multiplier > 1 ? multiplier : 1;
        remainder = ((min * multiplier) % (increment * multiplier)) / multiplier;
        if (remainder !== 0) {
            range[0] = min - remainder + (min < 0 ? -increment : 0);
        }
        multiplier = Math.max(1 / (max || 1), 1 / (increment || 1));
        multiplier = multiplier > 1 ? multiplier : 1;
        remainder = ((max * multiplier) % (increment * multiplier)) / multiplier;
        if (remainder !== 0) {
            range[1] = max - remainder + (max > 0 ? increment : 0);
        }
    }
});

/**
 * @class Ext.chart.axis.segmenter.Time
 * @extends Ext.chart.axis.segmenter.Segmenter
 * 
 * Time data type.
 */
Ext.define('Ext.chart.axis.segmenter.Time', {
    extend: 'Ext.chart.axis.segmenter.Segmenter',
    alias: 'segmenter.time',
    config: {
        /**
         * @cfg {Object} step
         * @cfg {String} step.unit The unit of the step (Ext.Date.DAY, Ext.Date.MONTH, etc).
         * @cfg {Number} step.step The number of units for the step (1, 2, etc).
         * If specified, will override the result of {@link #preferredStep}.
         * For example:
         *     
         *     step: {
         *         unit: Ext.Date.HOUR,
         *         step: 1
         *     }
         */
        step: null
    },
    renderer: function(value, context) {
        var ExtDate = Ext.Date;
        switch (context.majorTicks.unit) {
            case 'y':
                return ExtDate.format(value, 'Y');
            case 'mo':
                return ExtDate.format(value, 'Y-m');
            case 'd':
                return ExtDate.format(value, 'Y-m-d');
        }
        return ExtDate.format(value, 'Y-m-d\nH:i:s');
    },
    from: function(value) {
        return new Date(value);
    },
    diff: function(min, max, unit) {
        if (isFinite(min)) {
            min = new Date(min);
        }
        if (isFinite(max)) {
            max = new Date(max);
        }
        return Ext.Date.diff(min, max, unit);
    },
    updateStep: function() {
        var axis = this.getAxis();
        if (axis && !this.isConfiguring) {
            axis.performLayout();
        }
    },
    align: function(date, step, unit) {
        if (unit === 'd' && step >= 7) {
            date = Ext.Date.align(date, 'd', step);
            date.setDate(date.getDate() - date.getDay() + 1);
            return date;
        } else {
            return Ext.Date.align(date, unit, step);
        }
    },
    add: function(value, step, unit) {
        return Ext.Date.add(new Date(value), unit, step);
    },
    timeBuckets: [
        {
            unit: Ext.Date.YEAR,
            steps: [
                1,
                2,
                5,
                10,
                20,
                50,
                100,
                200,
                500
            ]
        },
        {
            unit: Ext.Date.MONTH,
            steps: [
                1,
                3,
                6
            ]
        },
        {
            unit: Ext.Date.DAY,
            steps: [
                1,
                7,
                14
            ]
        },
        {
            unit: Ext.Date.HOUR,
            steps: [
                1,
                6,
                12
            ]
        },
        {
            unit: Ext.Date.MINUTE,
            steps: [
                1,
                5,
                15,
                30
            ]
        },
        {
            unit: Ext.Date.SECOND,
            steps: [
                1,
                5,
                15,
                30
            ]
        },
        {
            unit: Ext.Date.MILLI,
            steps: [
                1,
                2,
                5,
                10,
                20,
                50,
                100,
                200,
                500
            ]
        }
    ],
    /**
     * @private
     * Takes a time interval and figures out what is the smallest nice number of which
     * units (years, months, days, etc.) that can fully encompass that interval.
     * @param {Date} min
     * @param {Date} max
     * @return {Object}
     * @return {String} return.unit The unit.
     * @return {Number} return.step The number of units.
     */
    getTimeBucket: function(min, max) {
        var buckets = this.timeBuckets,
            unit, unitCount, steps, step, result, i, j;
        for (i = 0; i < buckets.length; i++) {
            unit = buckets[i].unit;
            unitCount = this.diff(min, max, unit);
            if (unitCount > 0) {
                steps = buckets[i].steps;
                for (j = 0; j < steps.length; j++) {
                    step = steps[j];
                    if (unitCount <= step) {
                        break;
                    }
                }
                result = {
                    unit: unit,
                    step: step
                };
                break;
            }
        }
        // If the interval is smaller then one millisecond ...
        if (!result) {
            // ... we can't go smaller than one millisecond.
            result = {
                unit: Ext.Date.MILLI,
                step: 1
            };
        }
        return result;
    },
    preferredStep: function(min, estStepSize) {
        var step = this.getStep();
        return step ? step : this.getTimeBucket(new Date(+min), new Date(+min + Math.ceil(estStepSize)));
    }
});

/**
 * @abstract
 * @class Ext.chart.axis.layout.Layout
 *
 * Interface used by Axis to process its data into a meaningful layout.
 */
Ext.define('Ext.chart.axis.layout.Layout', {
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    config: {
        /**
         * @cfg {Ext.chart.axis.Axis} axis The axis that the Layout is bound.
         */
        axis: null
    },
    constructor: function(config) {
        this.mixins.observable.constructor.call(this, config);
    },
    /**
     * Processes the data of the series bound to the axis.
     * @param {Ext.chart.series.Series} series The bound series.
     */
    processData: function(series) {
        var me = this,
            axis = me.getAxis(),
            direction = axis.getDirection(),
            boundSeries = axis.boundSeries,
            i, ln;
        if (series) {
            series['coordinate' + direction]();
        } else {
            for (i = 0 , ln = boundSeries.length; i < ln; i++) {
                boundSeries[i]['coordinate' + direction]();
            }
        }
    },
    /**
     * Calculates the position of major ticks for the axis.
     * @param {Object} context
     */
    calculateMajorTicks: function(context) {
        var me = this,
            attr = context.attr,
            range = attr.max - attr.min,
            zoom = range / Math.max(1, attr.length) * (attr.visibleMax - attr.visibleMin),
            viewMin = attr.min + range * attr.visibleMin,
            viewMax = attr.min + range * attr.visibleMax,
            estStepSize = attr.estStepSize * zoom,
            majorTicks = me.snapEnds(context, attr.min, attr.max, estStepSize);
        if (majorTicks) {
            me.trimByRange(context, majorTicks, viewMin, viewMax);
            context.majorTicks = majorTicks;
        }
    },
    /**
     * Calculates the position of sub ticks for the axis.
     * @param {Object} context
     */
    calculateMinorTicks: function(context) {
        if (this.snapMinorEnds) {
            context.minorTicks = this.snapMinorEnds(context);
        }
    },
    /**
     * Calculates the position of tick marks for the axis.
     * @param {Object} context
     * @return {*}
     */
    calculateLayout: function(context) {
        var me = this,
            attr = context.attr;
        if (attr.length === 0) {
            return null;
        }
        if (attr.majorTicks) {
            me.calculateMajorTicks(context);
            if (attr.minorTicks) {
                me.calculateMinorTicks(context);
            }
        }
    },
    /**
     * @method
     * Snaps the data bound to the axis to meaningful tick marks.
     * @param {Object} context
     * @param {Number} min
     * @param {Number} max
     * @param {Number} estStepSize
     */
    snapEnds: Ext.emptyFn,
    /**
     * Trims the layout of the axis by the defined minimum and maximum.
     * @param {Object} context
     * @param {Object} ticks Ticks object (e.g. major ticks) to be modified.
     * @param {Number} trimMin
     * @param {Number} trimMax
     */
    trimByRange: function(context, ticks, trimMin, trimMax) {
        var segmenter = context.segmenter,
            unit = ticks.unit,
            beginIdx = segmenter.diff(ticks.from, trimMin, unit),
            endIdx = segmenter.diff(ticks.from, trimMax, unit),
            begin = Math.max(0, Math.ceil(beginIdx / ticks.step)),
            end = Math.min(ticks.steps, Math.floor(endIdx / ticks.step));
        if (end < ticks.steps) {
            ticks.to = segmenter.add(ticks.from, end * ticks.step, unit);
        }
        if (ticks.max > trimMax) {
            ticks.max = ticks.to;
        }
        if (ticks.from < trimMin) {
            ticks.from = segmenter.add(ticks.from, begin * ticks.step, unit);
            while (ticks.from < trimMin) {
                begin++;
                ticks.from = segmenter.add(ticks.from, ticks.step, unit);
            }
        }
        if (ticks.min < trimMin) {
            ticks.min = ticks.from;
        }
        ticks.steps = end - begin;
    }
});

/**
 * @class Ext.chart.axis.layout.Discrete
 * @extends Ext.chart.axis.layout.Layout
 *
 * Simple processor for data that cannot be interpolated.
 */
Ext.define('Ext.chart.axis.layout.Discrete', {
    extend: 'Ext.chart.axis.layout.Layout',
    alias: 'axisLayout.discrete',
    isDiscrete: true,
    processData: function() {
        var me = this,
            axis = me.getAxis(),
            seriesList = axis.boundSeries,
            direction = axis.getDirection(),
            i, ln, series;
        me.labels = [];
        me.labelMap = {};
        for (i = 0 , ln = seriesList.length; i < ln; i++) {
            series = seriesList[i];
            if (series['get' + direction + 'Axis']() === axis) {
                series['coordinate' + direction]();
            }
        }
        // About the labels on Category axes (aka. axes with a Discrete layout)...
        //
        // When the data set from the store changes, series.processData() is called, which does
        // its thing at the series level and then calls series.updateLabelData() to update
        // the labels in the sprites that belong to the series. At the same time,
        // series.processData() calls axis.processData(), which also does its thing but at the axis
        // level, and also needs to update the labels for the sprite(s) that belong to the axis.
        // This is not that simple, however. So how are the axis labels rendered?
        // First, axis.sprite.Axis.render() calls renderLabels() which obtains the majorTicks
        // from the  axis.layout and iterate() through them. The majorTicks are an object returned
        // by snapEnds() below which provides a getLabel() function that returns the label
        // from the axis.layoutContext.data array. So now the question is: how are the labels
        // transferred from the axis.layout to the axis.layoutContext?
        // The easy response is: it's in calculateLayout() below. The issue is to call
        // calculateLayout() because it takes in an axis.layoutContext that can only be created
        // in axis.sprite.Axis.layoutUpdater(), which is a private "updater" function that is
        // called by all the sprite's "triggers". Of course, we don't want to call layoutUpdater()
        // directly from here, so instead we update the sprite's data attribute, which sets
        // the trigger which calls layoutUpdater() which calls calculateLayout() etc...
        // Note that the sprite's data attribute could be set to any value and it would still result
        // in the   trigger we need. For consistency, however, it is set to the labels.
        axis.getSprites()[0].setAttributes({
            data: me.labels
        });
        me.fireEvent('datachange', me.labels);
    },
    /**
     * @method calculateLayout
     * @inheritdoc
     */
    calculateLayout: function(context) {
        context.data = this.labels;
        this.callParent([
            context
        ]);
    },
    /**
     * @method calculateMajorTicks
     * @inheritdoc
     */
    calculateMajorTicks: function(context) {
        var me = this,
            attr = context.attr,
            data = context.data,
            range = attr.max - attr.min,
            viewMin = attr.min + range * attr.visibleMin,
            viewMax = attr.min + range * attr.visibleMax,
            out;
        out = me.snapEnds(context, Math.max(0, attr.min), Math.min(attr.max, data.length - 1), 1);
        if (out) {
            me.trimByRange(context, out, viewMin, viewMax);
            context.majorTicks = out;
        }
    },
    /**
     * @method snapEnds
     * @inheritdoc
     */
    snapEnds: function(context, min, max, estStepSize) {
        var data = context.data,
            steps;
        estStepSize = Math.ceil(estStepSize);
        steps = Math.floor((max - min) / estStepSize);
        return {
            min: min,
            max: max,
            from: min,
            to: steps * estStepSize + min,
            step: estStepSize,
            steps: steps,
            unit: 1,
            getLabel: function(currentStep) {
                return data[this.from + this.step * currentStep];
            },
            get: function(currentStep) {
                return this.from + this.step * currentStep;
            }
        };
    },
    /**
     * @method trimByRange
     * @inheritdoc
     */
    trimByRange: function(context, out, trimMin, trimMax) {
        var unit = out.unit,
            beginIdx = Math.ceil((trimMin - out.from) / unit) * unit,
            endIdx = Math.floor((trimMax - out.from) / unit) * unit,
            begin = Math.max(0, Math.ceil(beginIdx / out.step)),
            end = Math.min(out.steps, Math.floor(endIdx / out.step));
        if (end < out.steps) {
            out.to = end;
        }
        if (out.max > trimMax) {
            out.max = out.to;
        }
        if (out.from < trimMin && out.step > 0) {
            out.from = out.from + begin * out.step * unit;
            while (out.from < trimMin) {
                begin++;
                out.from += out.step * unit;
            }
        }
        if (out.min < trimMin) {
            out.min = out.from;
        }
        out.steps = end - begin;
    },
    getCoordFor: function(value, field, idx, items) {
        this.labels.push(value);
        return this.labels.length - 1;
    }
});

/**
 * Discrete layout that combines duplicate data points only if they have the same index.
 * For example:
 *
 *     @example
 *     Ext.create({
 *         xtype: 'cartesian',
 *         title: 'Weight vs Calories',
 *
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400,
 *
 *         store: {
 *              fields: ['month', 'weight', 'calories'],
 *              data: [
 *                  {
 *                      month: 'Jan',
 *                      weight: 185,
 *                      calories: 2650
 *                  },
 *                  {
 *                      month: 'Jan',
 *                      weight: 188,
 *                      calories: 2800
 *                  },
 *                  {
 *                      month: 'Feb',
 *                      weight: 188,
 *                      calories: 2800
 *                  },
 *                  {
 *                      month: 'Mar',
 *                      weight: 191,
 *                      calories: 2800
 *                  },
 *                  {
 *                      month: 'Apr',
 *                      weight: 189,
 *                      calories: 1500
 *                  },
 *                  {
 *                      month: 'May',
 *                      weight: 187,
 *                      calories: 1350
 *                  }
 *              ]
 *         },
 *
 *         axes: [{
 *             type: 'numeric',
 *             position: 'left',
 *             fields: ['weight'],
 *             minimum: 140
 *         }, {
 *             type: 'numeric',
 *             position: 'right',
 *             fields: ['calories'],
 *             minimum: 500,
 *             maximum: 3500
 *         }, {
 *             type: 'category',
 *             grid: true,
 *             layout: 'combineByIndex',
 *             fields: 'month',
 *             position: 'bottom',
 *             label: {
 *                 rotate: {
 *                     degrees: -45
 *                 }
 *             }
 *         }],
 *
 *         series: [{
 *             type: 'line',
 *             title: 'Weight',
 *             xField: 'month',
 *             yField: 'weight',
 *             smooth: true,
 *             marker: true
 *         }, {
 *             type: 'line',
 *             title: 'Calories',
 *             xField: 'month',
 *             yField: 'calories',
 *             smooth: true,
 *             marker: true
 *         }],
 *
 *         legend: {
 *             docked: 'bottom'
 *         }
 *
 *     });
 *
 * @since 6.5.0
 */
Ext.define('Ext.chart.axis.layout.CombineByIndex', {
    extend: 'Ext.chart.axis.layout.Discrete',
    alias: 'axisLayout.combineByIndex',
    getCoordFor: function(value, field, idx, items) {
        var labels = this.labels,
            result = idx;
        if (labels[idx] !== value) {
            result = labels.push(value) - 1;
        }
        return result;
    }
});

/**
 * Discrete processor that combines duplicate data points.
 */
Ext.define('Ext.chart.axis.layout.CombineDuplicate', {
    extend: 'Ext.chart.axis.layout.Discrete',
    alias: 'axisLayout.combineDuplicate',
    getCoordFor: function(value, field, idx, items) {
        var result;
        if (!(value in this.labelMap)) {
            result = this.labelMap[value] = this.labels.length;
            this.labels.push(value);
            return result;
        }
        return this.labelMap[value];
    }
});

/**
 * @class Ext.chart.axis.layout.Continuous
 * @extends Ext.chart.axis.layout.Layout
 * 
 * Processor for axis data that can be interpolated.
 */
Ext.define('Ext.chart.axis.layout.Continuous', {
    extend: 'Ext.chart.axis.layout.Layout',
    alias: 'axisLayout.continuous',
    isContinuous: true,
    config: {
        adjustMinimumByMajorUnit: false,
        adjustMaximumByMajorUnit: false
    },
    getCoordFor: function(value, field, idx, items) {
        return +value;
    },
    /**
     * @method snapEnds
     * @inheritdoc
     */
    snapEnds: function(context, min, max, estStepSize) {
        var segmenter = context.segmenter,
            axis = this.getAxis(),
            noAnimation = !axis.spriteAnimationCount,
            majorTickSteps = axis.getMajorTickSteps(),
            // if specific number of steps requested and the segmenter supports such segmentation
            bucket = majorTickSteps && segmenter.exactStep ? segmenter.exactStep(min, (max - min) / majorTickSteps) : segmenter.preferredStep(min, estStepSize),
            unit = bucket.unit,
            step = bucket.step,
            diffSteps = segmenter.diff(min, max, unit),
            steps = (majorTickSteps || diffSteps) + 1,
            from;
        // If 'majorTickSteps' config of the axis is set (is not 0), it means that
        // we want to split the range at that number of equal intervals (segmenter.exactStep),
        // and don't care if the resulting ticks are at nice round values or not.
        // So 'from' (aligned) step is equal to 'min' (unaligned step).
        // And 'to' is equal to 'max'.
        //
        // Another case where this is possible, is when the range between 'min' and
        // 'max' can be represented by n steps, where n is an integer.
        // For example, if the data values are [7, 17, 27, 37, 47], the data step is 10
        // and, if the calculated tick step (segmenter.preferredStep) is also 10,
        // there is no need to segmenter.align the 'min' to 0, so that the ticks are at
        // [0, 10, 20, 30, 40, 50], because the data points are already perfectly
        // spaced, so the ticks can be exactly at the data points without runing the
        // aesthetics.
        //
        // The 'noAnimation' check is required to prevent EXTJS-25413 from happening.
        // The segmentation described above is ideal for a static chart, but produces
        // unwanted effects during animation.
        if (majorTickSteps || (noAnimation && +segmenter.add(min, diffSteps, unit) === max)) {
            from = min;
        } else {
            from = segmenter.align(min, step, unit);
        }
        return {
            // min/max are NOT aligned to step
            min: segmenter.from(min),
            max: segmenter.from(max),
            // from/to are aligned to step
            from: from,
            to: segmenter.add(from, steps, unit),
            step: step,
            steps: steps,
            unit: unit,
            get: function(currentStep) {
                return segmenter.add(this.from, this.step * currentStep, this.unit);
            }
        };
    },
    snapMinorEnds: function(context) {
        var majorTicks = context.majorTicks,
            minorTickSteps = this.getAxis().getMinorTickSteps(),
            segmenter = context.segmenter,
            min = majorTicks.min,
            max = majorTicks.max,
            from = majorTicks.from,
            unit = majorTicks.unit,
            step = majorTicks.step / minorTickSteps,
            scaledStep = step * unit.scale,
            fromMargin = from - min,
            offset = Math.floor(fromMargin / scaledStep),
            extraSteps = offset + Math.floor((max - majorTicks.to) / scaledStep) + 1,
            steps = majorTicks.steps * minorTickSteps + extraSteps;
        return {
            min: min,
            max: max,
            from: min + fromMargin % scaledStep,
            to: segmenter.add(from, steps * step, unit),
            step: step,
            steps: steps,
            unit: unit,
            get: function(current) {
                // don't render minor tick in major tick position
                return (current % minorTickSteps + offset + 1 !== 0) ? segmenter.add(this.from, this.step * current, unit) : null;
            }
        };
    }
});

/**
 * @class Ext.chart.axis.Axis
 *
 * Defines axis for charts.
 *
 * Using the current model, the type of axis can be easily extended. By default, Sencha Charts 
 * provide three different types of axis:
 *
 *  * **numeric** - the data attached to this axis is numeric and continuous.
 *  * **time** - the data attached to this axis is (or gets converted into) a date/time value; 
 *               it is continuous.
 *  * **category** - the data attached to this axis belongs to a finite set. The data points
 *                   are evenly placed along the axis.
 *
 * The behavior of an axis can be easily changed by setting different types of axis layout and 
 * axis segmenter to the axis.
 *
 * Axis layout defines how the data points are placed. Using continuous layout, the data points 
 * will be distributed by the numeric value. Using discrete layout the data points will be spaced 
 * evenly. Furthermore, if you want to combine the data points with the duplicate values in a 
 * discrete layout, you should use combineDuplicate layout.
 *
 * Segmenter defines the way to segment data range. For example, if you have a Date-type data range 
 * from Jan 1, 1997 to Jan 1, 2017, the segmenter will segement the data range into years, months or
 * days based on the current zooming level.
 *
 * It is possible to write custom axis layouts and segmenters to extends this behavior by simply 
 * implementing interfaces {@link Ext.chart.axis.layout.Layout} and
 * {@link Ext.chart.axis.segmenter.Segmenter}.
 *
 * Here's an example for the axes part of a chart definition:
 * An example of axis for a series (in this case for an area chart that has multiple layers of 
 * yFields) could be:
 *
 *     axes: [{
 *         type: 'numeric',
 *         position: 'left',
 *         title: 'Number of Hits',
 *         grid: {
 *             odd: {
 *                 opacity: 1,
 *                 fill: '#ddd',
 *                 stroke: '#bbb',
 *                 lineWidth: 1
 *             }
 *         },
 *         minimum: 0
 *     }, {
 *         type: 'category',
 *         position: 'bottom',
 *         title: 'Month of the Year',
 *         grid: true,
 *         label: {
 *             rotate: {
 *                 degrees: 315
 *             }
 *         }
 *     }]
 *
 * In this case we use a `numeric` axis for displaying the values of the Area series and a 
 * `category` axis for displaying the names of the store elements. The numeric axis is placed 
 * on the left of the screen, while the category axis is placed at the bottom of the chart.
 * Both the category and numeric axes have `grid` set, which means that horizontal and vertical 
 * lines will cover the chart background. In the category axis the labels will be rotated so 
 * they can fit the space better.
 */
Ext.define('Ext.chart.axis.Axis', {
    xtype: 'axis',
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
    requires: [
        'Ext.chart.axis.sprite.Axis',
        'Ext.chart.axis.segmenter.*',
        'Ext.chart.axis.layout.*',
        'Ext.chart.Util'
    ],
    isAxis: true,
    /**
     * @event rangechange
     * Fires when the {@link Ext.chart.axis.Axis#range range} of the axis  changes.
     * @param {Ext.chart.axis.Axis} axis
     * @param {Array} range
     * @param {Array} oldRange
     */
    /**
     * @event visiblerangechange
     * Fires when the {@link #visibleRange} of the axis changes.
     * @param {Ext.chart.axis.Axis} axis
     * @param {Array} visibleRange
     */
    /**
     * @cfg {String} id
     * The **unique** id of this axis instance.
     */
    config: {
        /**
         * @cfg {String} position
         * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`, `radial`,
         * and `angular`.
         */
        position: 'bottom',
        /**
         * @cfg {Array} fields
         * An array containing the names of the record fields which should be mapped along the axis.
         * This is optional if the binding between series and fields is clear.
         */
        fields: [],
        /**
         * @cfg {Object} label
         *
         * The label configuration object for the Axis. This object may include style attributes
         * like `spacing`, `padding`, `font` that receives a string or number and
         * returns a new string with the modified values.
         *
         * For more supported values, see the configurations for {@link Ext.chart.sprite.Label}.
         */
        label: undefined,
        /**
         * @cfg {Object} grid
         * The grid configuration object for the Axis style. Can contain `stroke` or `fill`
         * attributes. Also may contain an `odd` or `even` property in which you only style things
         * on odd or even rows. For example:
         *
         *
         *     grid {
         *         odd: {
         *             stroke: '#555'
         *         },
         *         even: {
         *             stroke: '#ccc'
         *         }
         *     }
         */
        grid: false,
        /**
         * @cfg {Array|Object} limits
         * The limit lines configuration for the axis.
         * For example:
         *
         *     limits: [{
         *         value: 50,
         *         line: {
         *             strokeStyle: 'red',
         *             lineDash: [6, 3],
         *             title: {
         *                 text: 'Monthly minimum',
         *                 fontSize: 14
         *             }
         *         }
         *     }]
         */
        limits: null,
        /**
         * @cfg {Function} renderer Allows to change the text shown next to the tick.
         * @param {Ext.chart.axis.Axis} axis The axis.
         * @param {String/Number} label The label.
         * @param {Object} layoutContext The object that holds calculated positions
         * of axis' ticks based on current layout, segmenter, axis length and configuration.
         * @param {String/Number/null} lastLabel The last label (if any).
         * @return {String} The label to display.
         * @controllable
         */
        renderer: null,
        /**
         * @protected
         * @cfg {Ext.chart.AbstractChart} chart The Chart that the Axis is bound.
         */
        chart: null,
        /**
         * @cfg {Object} style
         * The style for the axis line and ticks.
         * Refer to the {@link Ext.chart.axis.sprite.Axis}
         */
        style: null,
        /**
         * @cfg {Number} margin
         * The margin of the axis. Used to control the spacing between axes in charts with multiple
         * axes. Unlike CSS where the margin is added on all 4 sides of an element, the `margin`
         * is the total space that is added horizontally for a vertical axis, vertically
         * for a horizontal axis, and radially for an angular axis.
         */
        margin: 0,
        /**
         * @cfg {Number} [titleMargin=4]
         * The margin around the axis title. Unlike CSS where the margin is added on all 4
         * sides of an element, the `titleMargin` is the total space that is added horizontally
         * for a vertical title and vertically for an horizontal title, with half the `titleMargin`
         * being added on either side.
         */
        titleMargin: 4,
        /**
         * @cfg {Object} background
         * The background config for the axis surface.
         */
        background: null,
        /**
         * @cfg {Number} minimum
         * The minimum value drawn by the axis. If not set explicitly, the axis
         * minimum will be calculated automatically.
         */
        minimum: NaN,
        /**
         * @cfg {Number} maximum
         * The maximum value drawn by the axis. If not set explicitly, the axis
         * maximum will be calculated automatically.
         */
        maximum: NaN,
        /**
         * @cfg {Boolean} reconcileRange
         * If 'true' the range of the axis will be a union of ranges
         * of all the axes with the same direction. Defaults to 'false'.
         */
        reconcileRange: false,
        /**
         * @cfg {Number} minZoom
         * The minimum zooming level for axis.
         */
        minZoom: 1,
        /**
         * @cfg {Number} maxZoom
         * The maximum zooming level for axis.
         */
        maxZoom: 10000,
        /**
         * @cfg {Object|Ext.chart.axis.layout.Layout} layout
         * The axis layout config. See {@link Ext.chart.axis.layout.Layout}
         */
        layout: 'continuous',
        /**
         * @cfg {Object|Ext.chart.axis.segmenter.Segmenter} segmenter
         * The segmenter config. See {@link Ext.chart.axis.segmenter.Segmenter}
         */
        segmenter: 'numeric',
        /**
         * @cfg {Boolean} hidden
         * Indicate whether to hide the axis.
         * If the axis is hidden, one of the axis line, ticks, labels or the title will be shown and
         * no margin will be taken.
         * The coordination mechanism works fine no matter if the axis is hidden.
         */
        hidden: false,
        /**
         * @cfg {Number} [majorTickSteps=0]
         * Forces the number of major ticks to the specified value.
         * Both {@link #minimum} and {@link #maximum} should be specified.
         */
        majorTickSteps: 0,
        /**
         * @cfg {Number} [minorTickSteps=0]
         * The number of small ticks between two major ticks.
         */
        minorTickSteps: 0,
        /**
         * @cfg {Boolean} adjustByMajorUnit
         * Whether to make the auto-calculated minimum and maximum of the axis
         * a multiple of the interval between the major ticks of the axis.
         * If {@link #majorTickSteps}, {@link #minimum} or {@link #maximum}
         * configs have been set, this config will be ignored.
         * Defaults to 'true'.
         * Note: this config has no effect if the axis is {@link #hidden}.
         */
        adjustByMajorUnit: true,
        /**
         * @cfg {String|Object} title
         * The title for the Axis.
         * If given a String, the 'text' attribute of the title sprite will be set,
         * otherwise the style will be set.
         */
        title: null,
        /**
         * @private
         * @cfg {Number} [expandRangeBy=0]
         */
        expandRangeBy: 0,
        /**
         * @private
         * @cfg {Number} length
         * Length of the axis position. Equals to the size of inner rect on the docking side
         * of this axis.
         * WARNING: Meant to be set automatically by chart. Do not set it manually.
         */
        length: 0,
        /**
         * @private
         * @cfg {Array} center
         * Center of the polar axis.
         * WARNING: Meant to be set automatically by chart. Do not set it manually.
         */
        center: null,
        /**
         * @private
         * @cfg {Number} radius
         * Radius of the polar axis.
         * WARNING: Meant to be set automatically by chart. Do not set it manually.
         */
        radius: null,
        /**
         * @private
         */
        totalAngle: Math.PI,
        /**
         * @private
         * @cfg {Number} rotation
         * Rotation of the polar axis in radians.
         * WARNING: Meant to be set automatically by chart. Do not set it manually.
         */
        rotation: null,
        /**
         * @cfg {Array} visibleRange
         * Specify the proportion of the axis to be rendered. The series bound to
         * this axis will be synchronized and transformed accordingly.
         */
        visibleRange: [
            0,
            1
        ],
        /**
         * @cfg {Boolean} needHighPrecision
         * Indicates that the axis needs high precision surface implementation.
         * See {@link Ext.draw.engine.Canvas#highPrecision}
         */
        needHighPrecision: false,
        /**
         * @cfg {Ext.chart.axis.Axis|String|Number} linkedTo
         * Axis (itself, its ID or index) that this axis is linked to.
         * When an axis is linked to a master axis, it will use the same data as the master axis.
         * It can be used to show additional info, or to ease reading the chart by duplicating
         * the scales.
         */
        linkedTo: null,
        /**
         * @cfg {Number|Object}
         * If `floating` is a number, then it's a percentage displacement of the axis from its
         * initial {@link #position} in the direction opposite to the axis' direction. For instance,
         * '{position:"left", floating:75}' displays a vertical  axis at 3/4 of the chart, starting
         * from the left. It is equivalent to '{position:"right", floating:25}'. If `floating` is
         * an object, then `floating.value` is the position of this axis along another axis, defined
         * by `floating.alongAxis`, where `alongAxis` is an ID, an
         * {@link Ext.chart.AbstractChart#axes} config index, or the other axis itself. `alongAxis`
         * must have an opposite {@link Ext.chart.axis.Axis#getAlignment alignment}.
         * For example:
         *
         *
         *      axes: [
         *          {
         *              title: 'Average Temperature (F)',
         *              type: 'numeric',
         *              position: 'left',
         *              id: 'temperature-vertical-axis',
         *              minimum: -30,
         *              maximum: 130
         *          },
         *          {
         *              title: 'Month (2013)',
         *              type: 'category',
         *              position: 'bottom',
         *              floating: {
         *                  value: 32,
         *                  alongAxis: 'temperature-vertical-axis'
         *              }
         *          }
         *      ]
         */
        floating: null
    },
    titleOffset: 0,
    spriteAnimationCount: 0,
    boundSeries: [],
    sprites: null,
    surface: null,
    /**
     * @private
     * @property {Array} range
     * The full data range of the axis. Should not be set directly, Clear it to `null`
     * and use `getRange` to update.
     */
    range: null,
    defaultRange: [
        0,
        1
    ],
    rangePadding: 0.5,
    xValues: [],
    yValues: [],
    masterAxis: null,
    applyRotation: function(rotation) {
        var twoPie = Math.PI * 2;
        return (rotation % twoPie + Math.PI) % twoPie - Math.PI;
    },
    updateRotation: function(rotation) {
        var sprites = this.getSprites(),
            position = this.getPosition();
        if (!this.getHidden() && position === 'angular' && sprites[0]) {
            sprites[0].setAttributes({
                baseRotation: rotation
            });
        }
    },
    applyTitle: function(title, oldTitle) {
        var surface;
        if (Ext.isString(title)) {
            title = {
                text: title
            };
        }
        if (!oldTitle) {
            oldTitle = Ext.create('sprite.text', title);
            if ((surface = this.getSurface())) {
                surface.add(oldTitle);
            }
        } else {
            oldTitle.setAttributes(title);
        }
        return oldTitle;
    },
    getAdjustByMajorUnit: function() {
        return !this.getHidden() && this.callParent();
    },
    applyFloating: function(floating, oldFloating) {
        if (floating === null) {
            floating = {
                value: null,
                alongAxis: null
            };
        } else if (Ext.isNumber(floating)) {
            floating = {
                value: floating,
                alongAxis: null
            };
        }
        if (Ext.isObject(floating)) {
            if (oldFloating && oldFloating.alongAxis) {
                delete this.getChart().getAxis(oldFloating.alongAxis).floatingAxes[this.getId()];
            }
            return floating;
        }
        return oldFloating;
    },
    constructor: function(config) {
        var me = this,
            id;
        me.sprites = [];
        me.labels = [];
        // Maps IDs of the axes that float along this axis to their floating values.
        me.floatingAxes = {};
        config = config || {};
        if (config.position === 'angular') {
            config.style = config.style || {};
            config.style.estStepSize = 1;
        }
        if ('id' in config) {
            id = config.id;
        } else if ('id' in me.config) {
            id = me.config.id;
        } else {
            id = me.getId();
        }
        me.setId(id);
        me.mixins.observable.constructor.apply(me, arguments);
    },
    /**
     * @private
     * @return {String}
     */
    getAlignment: function() {
        switch (this.getPosition()) {
            case 'left':
            case 'right':
                return 'vertical';
            case 'top':
            case 'bottom':
                return 'horizontal';
            case 'radial':
                return 'radial';
            case 'angular':
                return 'angular';
        }
    },
    /**
     * @private
     * @return {String}
     */
    getGridAlignment: function() {
        switch (this.getPosition()) {
            case 'left':
            case 'right':
                return 'horizontal';
            case 'top':
            case 'bottom':
                return 'vertical';
            case 'radial':
                return 'circular';
            case 'angular':
                return 'radial';
        }
    },
    /**
     * @private
     * Get the surface for drawing the series sprites
     */
    getSurface: function() {
        var me = this,
            chart = me.getChart(),
            surface, gridSurface;
        if (chart && !me.surface) {
            surface = me.surface = chart.getSurface(me.getId(), 'axis');
            gridSurface = me.gridSurface = chart.getSurface('main');
            gridSurface.waitFor(surface);
            me.getGrid();
            me.createLimits();
        }
        return me.surface;
    },
    createLimits: function() {
        var me = this,
            chart = me.getChart(),
            axisSprite = me.getSprites()[0],
            gridAlignment = me.getGridAlignment(),
            limits;
        if (me.getLimits() && gridAlignment) {
            gridAlignment = gridAlignment.replace('3d', '');
            me.limits = limits = {
                surface: chart.getSurface('overlay'),
                lines: new Ext.chart.Markers(),
                titles: new Ext.draw.sprite.Instancing()
            };
            limits.lines.setTemplate({
                xclass: 'grid.' + gridAlignment
            });
            limits.lines.getTemplate().setAttributes({
                strokeStyle: 'black'
            }, true);
            limits.surface.add(limits.lines);
            axisSprite.bindMarker(gridAlignment + '-limit-lines', me.limits.lines);
            me.limitTitleTpl = new Ext.draw.sprite.Text();
            limits.titles.setTemplate(me.limitTitleTpl);
            limits.surface.add(limits.titles);
        }
    },
    applyGrid: function(grid) {
        // Returning an empty object here if grid was set to 'true' so that
        // config merging in the theme works properly.
        if (grid === true) {
            return {};
        }
        return grid;
    },
    updateGrid: function(grid) {
        var me = this,
            chart = me.getChart(),
            gridSurface = me.gridSurface,
            axisSprite, gridAlignment, gridSprite;
        if (!chart) {
            me.on({
                chartattached: Ext.bind(me.updateGrid, me, [
                    grid
                ]),
                single: true
            });
            return;
        }
        axisSprite = me.getSprites()[0];
        gridAlignment = me.getGridAlignment();
        if (grid) {
            gridSprite = me.gridSpriteEven;
            if (!gridSprite) {
                gridSprite = me.gridSpriteEven = new Ext.chart.Markers();
                gridSprite.setTemplate({
                    xclass: 'grid.' + gridAlignment
                });
                gridSurface.add(gridSprite);
                axisSprite.bindMarker(gridAlignment + '-even', gridSprite);
            }
            if (Ext.isObject(grid)) {
                gridSprite.getTemplate().setAttributes(grid);
                if (Ext.isObject(grid.even)) {
                    gridSprite.getTemplate().setAttributes(grid.even);
                }
            }
            gridSprite = me.gridSpriteOdd;
            if (!gridSprite) {
                gridSprite = me.gridSpriteOdd = new Ext.chart.Markers();
                gridSprite.setTemplate({
                    xclass: 'grid.' + gridAlignment
                });
                gridSurface.add(gridSprite);
                axisSprite.bindMarker(gridAlignment + '-odd', gridSprite);
            }
            if (Ext.isObject(grid)) {
                gridSprite.getTemplate().setAttributes(grid);
                if (Ext.isObject(grid.odd)) {
                    gridSprite.getTemplate().setAttributes(grid.odd);
                }
            }
        }
    },
    updateMinorTickSteps: function(minorTickSteps) {
        var me = this,
            sprites = me.getSprites(),
            axisSprite = sprites && sprites[0],
            surface;
        if (axisSprite) {
            axisSprite.setAttributes({
                minorTicks: !!minorTickSteps
            });
            surface = me.getSurface();
            if (!me.isConfiguring && surface) {
                surface.renderFrame();
            }
        }
    },
    /**
     *
     * Mapping data value into coordinate.
     *
     * @param {*} value
     * @param {String} field
     * @param {Number} [idx]
     * @param {Ext.util.MixedCollection} [items]
     * @return {Number}
     */
    getCoordFor: function(value, field, idx, items) {
        return this.getLayout().getCoordFor(value, field, idx, items);
    },
    applyPosition: function(pos) {
        return pos.toLowerCase();
    },
    applyLength: function(length, oldLength) {
        return length > 0 ? length : oldLength;
    },
    applyLabel: function(label, oldLabel) {
        if (!oldLabel) {
            oldLabel = new Ext.draw.sprite.Text({});
        }
        if (label) {
            if (this.limitTitleTpl) {
                this.limitTitleTpl.setAttributes(label);
            }
            oldLabel.setAttributes(label);
        }
        return oldLabel;
    },
    applyLayout: function(layout, oldLayout) {
        layout = Ext.factory(layout, null, oldLayout, 'axisLayout');
        layout.setAxis(this);
        return layout;
    },
    applySegmenter: function(segmenter, oldSegmenter) {
        segmenter = Ext.factory(segmenter, null, oldSegmenter, 'segmenter');
        segmenter.setAxis(this);
        return segmenter;
    },
    updateMinimum: function() {
        this.range = null;
    },
    updateMaximum: function() {
        this.range = null;
    },
    hideLabels: function() {
        this.getSprites()[0].setDirty(true);
        this.setLabel({
            hidden: true
        });
    },
    showLabels: function() {
        this.getSprites()[0].setDirty(true);
        this.setLabel({
            hidden: false
        });
    },
    /**
     * Invokes renderFrame on this axis's surface(s)
     */
    renderFrame: function() {
        this.getSurface().renderFrame();
    },
    updateChart: function(newChart, oldChart) {
        var me = this,
            surface;
        if (oldChart) {
            oldChart.unregister(me);
            oldChart.un('serieschange', me.onSeriesChange, me);
            me.linkAxis();
            me.fireEvent('chartdetached', oldChart, me);
        }
        if (newChart) {
            newChart.on('serieschange', me.onSeriesChange, me);
            me.surface = null;
            surface = me.getSurface();
            me.getLabel().setSurface(surface);
            surface.add(me.getSprites());
            surface.add(me.getTitle());
            newChart.register(me);
            me.fireEvent('chartattached', newChart, me);
        }
    },
    applyBackground: function(background) {
        var rect = Ext.ClassManager.getByAlias('sprite.rect');
        return rect.def.normalize(background);
    },
    /**
     * @protected
     * Invoked when data has changed.
     */
    processData: function() {
        this.getLayout().processData();
        this.range = null;
    },
    getDirection: function() {
        return this.getChart().getDirectionForAxis(this.getPosition());
    },
    isSide: function() {
        var position = this.getPosition();
        return position === 'left' || position === 'right';
    },
    applyFields: function(fields) {
        return Ext.Array.from(fields);
    },
    applyVisibleRange: function(visibleRange, oldVisibleRange) {
        var temp;
        this.getChart();
        // If it is in reversed order swap them
        if (visibleRange[0] > visibleRange[1]) {
            temp = visibleRange[0];
            visibleRange[0] = visibleRange[1];
            visibleRange[0] = temp;
        }
        if (visibleRange[1] === visibleRange[0]) {
            visibleRange[1] += 1 / this.getMaxZoom();
        }
        if (visibleRange[1] > visibleRange[0] + 1) {
            visibleRange[0] = 0;
            visibleRange[1] = 1;
        } else if (visibleRange[0] < 0) {
            visibleRange[1] -= visibleRange[0];
            visibleRange[0] = 0;
        } else if (visibleRange[1] > 1) {
            visibleRange[0] -= visibleRange[1] - 1;
            visibleRange[1] = 1;
        }
        if (oldVisibleRange && visibleRange[0] === oldVisibleRange[0] && visibleRange[1] === oldVisibleRange[1]) {
            return undefined;
        }
        return visibleRange;
    },
    updateVisibleRange: function(visibleRange) {
        this.fireEvent('visiblerangechange', this, visibleRange);
    },
    onSeriesChange: function(chart) {
        var me = this,
            series = chart.getSeries(),
            boundSeries = [],
            linkedTo, masterAxis, getAxisMethod, i, ln;
        if (series) {
            getAxisMethod = 'get' + me.getDirection() + 'Axis';
            for (i = 0 , ln = series.length; i < ln; i++) {
                if (this === series[i][getAxisMethod]()) {
                    boundSeries.push(series[i]);
                }
            }
        }
        me.boundSeries = boundSeries;
        linkedTo = me.getLinkedTo();
        masterAxis = !Ext.isEmpty(linkedTo) && chart.getAxis(linkedTo);
        if (masterAxis) {
            me.linkAxis(masterAxis);
        } else {
            me.getLayout().processData();
        }
    },
    linkAxis: function(masterAxis) {
        var me = this;
        function link(action, slave, master) {
            master.getLayout()[action]('datachange', 'onDataChange', slave);
            master[action]('rangechange', 'onMasterAxisRangeChange', slave);
        }
        if (me.masterAxis) {
            if (!me.masterAxis.destroyed) {
                link('un', me, me.masterAxis);
            }
            me.masterAxis = null;
        }
        if (masterAxis) {
            if (masterAxis.type !== this.type) {
                Ext.Error.raise("Linked axes must be of the same type.");
            }
            link('on', me, masterAxis);
            me.onDataChange(masterAxis.getLayout().labels);
            me.onMasterAxisRangeChange(masterAxis, masterAxis.range);
            me.setStyle(Ext.apply({}, me.config.style, masterAxis.config.style));
            me.setTitle(Ext.apply({}, me.config.title, masterAxis.config.title));
            me.setLabel(Ext.apply({}, me.config.label, masterAxis.config.label));
            me.masterAxis = masterAxis;
        }
    },
    onDataChange: function(data) {
        this.getLayout().labels = data;
    },
    onMasterAxisRangeChange: function(masterAxis, range) {
        this.range = range;
    },
    applyRange: function(newRange) {
        if (!newRange) {
            return this.dataRange.slice(0);
        } else {
            return [
                newRange[0] === null ? this.dataRange[0] : newRange[0],
                newRange[1] === null ? this.dataRange[1] : newRange[1]
            ];
        }
    },
    /**
     * @private
     */
    setBoundSeriesRange: function(range) {
        var boundSeries = this.boundSeries,
            style = {},
            series, i, sprites, j, ln;
        style['range' + this.getDirection()] = range;
        for (i = 0 , ln = boundSeries.length; i < ln; i++) {
            series = boundSeries[i];
            if (series.getHidden() === true) {
                
                continue;
            }
            sprites = series.getSprites();
            for (j = 0; j < sprites.length; j++) {
                sprites[j].setAttributes(style);
            }
        }
    },
    /**
     * Get the range derived from all the bound series.
     * The range value is cached and returned the next time this method is called.
     * Set `recalculate` to `true` to recalculate the range, if changes to the
     * chart, its components or data are expected to affect the range.
     * @param {Boolean} [recalculate]
     * @return {Number[]}
     */
    getRange: function(recalculate) {
        var me = this,
            range = recalculate ? null : me.range,
            oldRange = me.oldRange,
            minimum, maximum;
        if (!range) {
            if (me.masterAxis) {
                range = me.masterAxis.range;
            } else {
                minimum = me.getMinimum();
                maximum = me.getMaximum();
                if (Ext.isNumber(minimum) && Ext.isNumber(maximum)) {
                    range = [
                        minimum,
                        maximum
                    ];
                } else {
                    range = me.calculateRange();
                }
                me.range = range;
            }
        }
        if (range && (!oldRange || range[0] !== oldRange[0] || range[1] !== oldRange[1])) {
            me.fireEvent('rangechange', me, range, oldRange);
            me.oldRange = range;
        }
        return range;
    },
    isSingleDataPoint: function(range) {
        return (range[0] + this.rangePadding) === 0 && (range[1] - this.rangePadding) === 0;
    },
    calculateRange: function() {
        var me = this,
            boundSeries = me.boundSeries,
            layout = me.getLayout(),
            segmenter = me.getSegmenter(),
            minimum = me.getMinimum(),
            maximum = me.getMaximum(),
            visibleRange = me.getVisibleRange(),
            getRangeMethod = 'get' + me.getDirection() + 'Range',
            expandRangeBy = me.getExpandRangeBy(),
            context, attr, majorTicks, series, i, ln, seriesRange,
            range = [
                NaN,
                NaN
            ];
        // For each series bound to this axis, ask the series for its min/max values
        // and use them to find the overall min/max.
        for (i = 0 , ln = boundSeries.length; i < ln; i++) {
            series = boundSeries[i];
            if (series.getHidden() === true) {
                
                continue;
            }
            seriesRange = series[getRangeMethod]();
            if (seriesRange) {
                Ext.chart.Util.expandRange(range, seriesRange);
            }
        }
        range = Ext.chart.Util.validateRange(range, me.defaultRange, me.rangePadding);
        // The second condition is there to account for a special case where we only have
        // a single data point, so the effective range of coordinated data is 0 (whatever
        // the actual value of that single data point is, it will be assigned an index of
        // zero, as the first and only data point). Since zero range is invalid, the
        // validateRange function above will expand the range by the value of the rangePadding,
        // which makes further expansion by the value of expandRangeBy unnecessary.
        if (expandRangeBy && (!me.isSingleDataPoint(range))) {
            range[0] -= expandRangeBy;
            range[1] += expandRangeBy;
        }
        if (isFinite(minimum)) {
            range[0] = minimum;
        }
        if (isFinite(maximum)) {
            range[1] = maximum;
        }
        // When series `fullStack` config is used, the values may add up to
        // slightly more than the value of the `fullStackTotal` config
        // because of a precision error.
        range[0] = Ext.Number.correctFloat(range[0]);
        range[1] = Ext.Number.correctFloat(range[1]);
        me.range = range;
        // It's important to call 'me.reconcileRange' after the 'range'
        // has been assigned to avoid circular calls.
        if (me.getReconcileRange()) {
            me.reconcileRange();
        }
        // TODO: Find a better way to do this.
        // TODO: The original design didn't take into account that the range of an axis
        // TODO: will depend not just on the range of the data of the bound series in the
        // TODO: direction of the axis, but also on the range of other axes with the
        // TODO: same direction and on the segmentation of the axis (interval between
        // TODO: major ticks).
        // TODO: While the fist omission was possible to retrofit rather gracefully
        // TODO: by adding the axis.reconcileRange method, the second one is harder to deal with.
        // TODO: The issue is that the resulting axis segmentation, which is a part of
        // TODO: the axis sprite layout has to be known before layout has begun.
        // TODO: Example for the logic below:
        // TODO: If we have a range of data of 0..34.5 the step will be 2 and we
        // TODO: will round up the max to 36 based on that step, but when the range is 0..36,
        // TODO: the step becomes 5, so we have to reconcile the range once again where max
        // TODO: becomes 40.
        if (range[0] !== range[1] && me.getAdjustByMajorUnit() && segmenter.adjustByMajorUnit && !me.getMajorTickSteps()) {
            attr = Ext.Object.chain(me.getSprites()[0].attr);
            attr.min = range[0];
            attr.max = range[1];
            attr.visibleMin = visibleRange[0];
            attr.visibleMax = visibleRange[1];
            context = {
                attr: attr,
                segmenter: segmenter
            };
            layout.calculateLayout(context);
            majorTicks = context.majorTicks;
            if (majorTicks) {
                segmenter.adjustByMajorUnit(majorTicks.step, majorTicks.unit.scale, range);
                attr.min = range[0];
                attr.max = range[1];
                context.majorTicks = null;
                layout.calculateLayout(context);
                majorTicks = context.majorTicks;
                segmenter.adjustByMajorUnit(majorTicks.step, majorTicks.unit.scale, range);
            } else if (!me.hasClearRangePending) {
                // Axis hasn't been rendered yet.
                me.hasClearRangePending = true;
                me.getChart().on('layout', 'clearRange', me);
            }
        }
        return range;
    },
    /**
     * @private
     */
    clearRange: function() {
        this.hasClearRangePending = null;
        this.range = null;
    },
    /**
     * Expands the range of the axis
     * based on the range of other axes with the same direction (if any).
     */
    reconcileRange: function() {
        var me = this,
            axes = me.getChart().getAxes(),
            direction = me.getDirection(),
            i, ln, axis, range;
        if (!axes) {
            return;
        }
        for (i = 0 , ln = axes.length; i < ln; i++) {
            axis = axes[i];
            range = axis.getRange();
            if (axis === me || axis.getDirection() !== direction || !range || !axis.getReconcileRange()) {
                
                continue;
            }
            if (range[0] < me.range[0]) {
                me.range[0] = range[0];
            }
            if (range[1] > me.range[1]) {
                me.range[1] = range[1];
            }
        }
    },
    applyStyle: function(style, oldStyle) {
        var cls = Ext.ClassManager.getByAlias('sprite.' + this.seriesType);
        if (cls && cls.def) {
            style = cls.def.normalize(style);
        }
        oldStyle = Ext.apply(oldStyle || {}, style);
        return oldStyle;
    },
    themeOnlyIfConfigured: {
        grid: true
    },
    updateTheme: function(theme) {
        var me = this,
            axisTheme = theme.getAxis(),
            position = me.getPosition(),
            initialConfig = me.getInitialConfig(),
            defaultConfig = me.defaultConfig,
            configs = me.self.getConfigurator().configs,
            genericAxisTheme = axisTheme.defaults,
            specificAxisTheme = axisTheme[position],
            themeOnlyIfConfigured = me.themeOnlyIfConfigured,
            key, value, isObjValue, isUnusedConfig, initialValue, cfg;
        axisTheme = Ext.merge({}, genericAxisTheme, specificAxisTheme);
        for (key in axisTheme) {
            value = axisTheme[key];
            cfg = configs[key];
            if (value !== null && value !== undefined && cfg) {
                initialValue = initialConfig[key];
                isObjValue = Ext.isObject(value);
                isUnusedConfig = initialValue === defaultConfig[key];
                if (isObjValue) {
                    if (isUnusedConfig && themeOnlyIfConfigured[key]) {
                        
                        continue;
                    }
                    value = Ext.merge({}, value, initialValue);
                }
                if (isUnusedConfig || isObjValue) {
                    me[cfg.names.set](value);
                }
            }
        }
    },
    updateCenter: function(center) {
        var me = this,
            sprites = me.getSprites(),
            axisSprite = sprites[0],
            centerX = center[0],
            centerY = center[1];
        if (axisSprite) {
            axisSprite.setAttributes({
                centerX: centerX,
                centerY: centerY
            });
        }
        if (me.gridSpriteEven) {
            me.gridSpriteEven.getTemplate().setAttributes({
                translationX: centerX,
                translationY: centerY,
                rotationCenterX: centerX,
                rotationCenterY: centerY
            });
        }
        if (me.gridSpriteOdd) {
            me.gridSpriteOdd.getTemplate().setAttributes({
                translationX: centerX,
                translationY: centerY,
                rotationCenterX: centerX,
                rotationCenterY: centerY
            });
        }
    },
    getSprites: function() {
        if (!this.getChart()) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            range = me.getRange(),
            position = me.getPosition(),
            chart = me.getChart(),
            animation = chart.getAnimation(),
            length = me.getLength(),
            axisClass = me.superclass,
            mainSprite, style, animationModifier;
        // If animation is false, then stop animation.
        if (animation === false) {
            animation = {
                duration: 0
            };
        }
        style = Ext.applyIf({
            position: position,
            axis: me,
            length: length,
            grid: me.getGrid(),
            hidden: me.getHidden(),
            titleOffset: me.titleOffset,
            layout: me.getLayout(),
            segmenter: me.getSegmenter(),
            totalAngle: me.getTotalAngle(),
            label: me.getLabel()
        }, me.getStyle());
        if (range) {
            style.min = range[0];
            style.max = range[1];
        }
        // If the sprites are not created.
        if (!me.sprites.length) {
            while (!axisClass.xtype) {
                axisClass = axisClass.superclass;
            }
            mainSprite = Ext.create('sprite.' + axisClass.xtype, style);
            animationModifier = mainSprite.getAnimation();
            animationModifier.setCustomDurations({
                baseRotation: 0
            });
            animationModifier.on('animationstart', 'onAnimationStart', me);
            animationModifier.on('animationend', 'onAnimationEnd', me);
            mainSprite.setLayout(me.getLayout());
            mainSprite.setSegmenter(me.getSegmenter());
            mainSprite.setLabel(me.getLabel());
            me.sprites.push(mainSprite);
            me.updateTitleSprite();
        } else {
            mainSprite = me.sprites[0];
            mainSprite.setAnimation(animation);
            mainSprite.setAttributes(style);
        }
        if (me.getRenderer()) {
            mainSprite.setRenderer(me.getRenderer());
        }
        return me.sprites;
    },
    /**
     * @private
     */
    performLayout: function() {
        if (this.isConfiguring) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            sprites = me.getSprites(),
            surface = me.getSurface(),
            chart = me.getChart(),
            sprite = sprites && sprites[0];
        if (chart && surface && sprite) {
            sprite.callUpdater(null, 'layout');
            // recalculate axis ticks
            chart.scheduleLayout();
        }
    },
    updateTitleSprite: function() {
        var me = this,
            length = me.getLength(),
            surface, thickness, title, position, margin, titleMargin, anchor;
        if (!me.sprites[0] || !Ext.isNumber(length)) {
            return;
        }
        thickness = this.sprites[0].thickness;
        surface = me.getSurface();
        title = me.getTitle();
        position = me.getPosition();
        margin = me.getMargin();
        titleMargin = me.getTitleMargin();
        anchor = surface.roundPixel(length / 2);
        if (title) {
            switch (position) {
                case 'top':
                    title.setAttributes({
                        x: anchor,
                        y: margin + titleMargin / 2,
                        textBaseline: 'top',
                        textAlign: 'center'
                    }, true);
                    title.applyTransformations();
                    me.titleOffset = title.getBBox().height + titleMargin;
                    break;
                case 'bottom':
                    title.setAttributes({
                        x: anchor,
                        y: thickness + titleMargin / 2,
                        textBaseline: 'top',
                        textAlign: 'center'
                    }, true);
                    title.applyTransformations();
                    me.titleOffset = title.getBBox().height + titleMargin;
                    break;
                case 'left':
                    title.setAttributes({
                        x: margin + titleMargin / 2,
                        y: anchor,
                        textBaseline: 'top',
                        textAlign: 'center',
                        rotationCenterX: margin + titleMargin / 2,
                        rotationCenterY: anchor,
                        rotationRads: -Math.PI / 2
                    }, true);
                    title.applyTransformations();
                    me.titleOffset = title.getBBox().width + titleMargin;
                    break;
                case 'right':
                    title.setAttributes({
                        x: thickness - margin + titleMargin / 2,
                        y: anchor,
                        textBaseline: 'bottom',
                        textAlign: 'center',
                        rotationCenterX: thickness + titleMargin / 2,
                        rotationCenterY: anchor,
                        rotationRads: Math.PI / 2
                    }, true);
                    title.applyTransformations();
                    me.titleOffset = title.getBBox().width + titleMargin;
                    break;
            }
        }
    },
    onThicknessChanged: function() {
        this.getChart().onThicknessChanged();
    },
    getThickness: function() {
        if (this.getHidden()) {
            return 0;
        }
        return (this.sprites[0] && this.sprites[0].thickness || 1) + this.titleOffset + this.getMargin();
    },
    onAnimationStart: function() {
        this.spriteAnimationCount++;
        if (this.spriteAnimationCount === 1) {
            this.fireEvent('animationstart', this);
        }
    },
    onAnimationEnd: function() {
        this.spriteAnimationCount--;
        if (this.spriteAnimationCount === 0) {
            this.fireEvent('animationend', this);
        }
    },
    // Methods used in ComponentQuery and controller
    getItemId: function() {
        return this.getId();
    },
    getAncestorIds: function() {
        return [
            this.getChart().getId()
        ];
    },
    isXType: function(xtype) {
        return xtype === 'axis';
    },
    // Override the Observable's method to redirect listener scope
    // resolution to the chart.
    resolveListenerScope: function(defaultScope) {
        var me = this,
            namedScope = Ext._namedScopes[defaultScope],
            chart = me.getChart(),
            scope;
        if (!namedScope) {
            scope = chart ? chart.resolveListenerScope(defaultScope, false) : (defaultScope || me);
        } else if (namedScope.isThis) {
            scope = me;
        } else if (namedScope.isController) {
            scope = chart ? chart.resolveListenerScope(defaultScope, false) : me;
        } else if (namedScope.isSelf) {
            scope = chart ? chart.resolveListenerScope(defaultScope, false) : me;
            // Class body listener. No chart controller, nor chart container controller.
            if (scope === chart && !chart.getInheritedConfig('defaultListenerScope')) {
                scope = me;
            }
        }
        return scope;
    },
    destroy: function() {
        var me = this;
        me.setChart(null);
        me.surface.destroy();
        me.surface = null;
        me.callParent();
    }
});

/**
 * The legend base class adapater for classic toolkit.
 */
Ext.define('Ext.chart.legend.LegendBase', {
    extend: 'Ext.view.View',
    config: {
        /* eslint-disable indent, max-len */
        tpl: [
            '<div class="',
            Ext.baseCSSPrefix,
            'legend-inner">',
            // for IE8 vertical centering
            '<div class="',
            Ext.baseCSSPrefix,
            'legend-container">',
            '<tpl for=".">',
            '<div class="',
            Ext.baseCSSPrefix,
            'legend-item">',
            '<span ',
            'class="',
            Ext.baseCSSPrefix,
            'legend-item-marker {[ values.disabled ? Ext.baseCSSPrefix + \'legend-item-inactive\' : \'\' ]}" ',
            'style="background:{mark};">',
            '</span>{name}',
            '</div>',
            '</tpl>',
            '</div>',
            '</div>'
        ],
        /* eslint-enable indent,max-len */
        // element that contains rows (see AbstractView)
        nodeContainerSelector: 'div.' + Ext.baseCSSPrefix + 'legend-inner',
        // row element (see AbstractView)
        itemSelector: 'div.' + Ext.baseCSSPrefix + 'legend-item',
        /**
         * @cfg {String} docked
         * The dock position of this component in its container. Can be `left`, `top`, `right`,
         * or `bottom`.
         */
        docked: 'bottom'
    },
    /**
         * @cfg dock
         * @hide
         */
    setDocked: function(docked) {
        // If we call the method 'updateDocked' instead of 'setDocked', the following error
        // is thrown: "Ext.Component#setDocked" is deprecated. Please use "setDock" instead.
        var me = this,
            panel = me.ownerCt;
        me.docked = me.dock = docked;
        switch (docked) {
            case 'top':
            case 'bottom':
                me.addCls(me.horizontalCls);
                me.removeCls(me.verticalCls);
                break;
            case 'left':
            case 'right':
                me.addCls(me.verticalCls);
                me.removeCls(me.horizontalCls);
                break;
        }
        if (panel) {
            panel.setDock(docked);
        }
    },
    setStore: function(store) {
        this.bindStore(store);
    },
    clearViewEl: function() {
        this.callParent(arguments);
        // The legend-container div is not removed automatically.
        Ext.removeNode(this.getNodeContainer());
    },
    onItemClick: function(record, item, index, e) {
        this.callParent(arguments);
        this.toggleItem(index);
    }
});

/**
 * This class provides a dataview-based chart legend.
 */
Ext.define('Ext.chart.legend.Legend', {
    extend: 'Ext.chart.legend.LegendBase',
    alternateClassName: 'Ext.chart.Legend',
    xtype: 'legend',
    alias: 'legend.dom',
    type: 'dom',
    isLegend: true,
    isDomLegend: true,
    config: {
        /**
         * @cfg {Array}
         * The rect of the legend relative to its container.
         */
        rect: null,
        /**
         * @cfg {Boolean} toggleable
         * `true` to allow series items to have their visibility
         * toggled by interaction with the legend items.
         */
        toggleable: true
    },
    /**
         * @cfg {Ext.chart.legend.store.Store} store
         * The {@link Ext.chart.legend.store.Store} to bind this legend to.
         * @private
         */
    baseCls: Ext.baseCSSPrefix + 'legend',
    horizontalCls: Ext.baseCSSPrefix + 'legend-horizontal',
    verticalCls: Ext.baseCSSPrefix + 'legend-vertical',
    toggleItem: function(index) {
        var disabledCount = 0,
            canToggle = true,
            disabled, store, count, record, i;
        if (!this.getToggleable()) {
            return;
        }
        store = this.getStore();
        if (store) {
            count = store.getCount();
            for (i = 0; i < count; i++) {
                record = store.getAt(i);
                if (record.get('disabled')) {
                    disabledCount++;
                }
            }
            canToggle = count - disabledCount > 1;
            record = store.getAt(index);
            if (record) {
                disabled = record.get('disabled');
                if (disabled || canToggle) {
                    // This will trigger AbstractChart.onLegendStoreUpdate.
                    record.set('disabled', !disabled);
                }
            }
        }
    },
    onResize: function(width, height, oldWidth, oldHeight) {
        var me = this,
            chart = me.chart;
        if (!me.isConfiguring) {
            if (chart) {
                chart.scheduleLayout();
            }
        }
    }
});

/**
 * @private
 */
Ext.define('Ext.chart.legend.sprite.Item', {
    extend: 'Ext.draw.sprite.Composite',
    alias: 'sprite.legenditem',
    type: 'legenditem',
    isLegendItem: true,
    requires: [
        'Ext.draw.sprite.Text',
        'Ext.draw.sprite.Circle'
    ],
    inheritableStatics: {
        def: {
            processors: {
                enabled: 'limited01',
                markerLabelGap: 'number'
            },
            animationProcessors: {
                enabled: null,
                markerLabelGap: null
            },
            defaults: {
                enabled: true,
                markerLabelGap: 5
            },
            triggers: {
                enabled: 'enabled',
                markerLabelGap: 'layout'
            },
            updaters: {
                layout: 'layoutUpdater',
                enabled: 'enabledUpdater'
            }
        }
    },
    config: {
        // Sprite's attributes are processed after initConfig.
        // So we need to init below configs lazily, as otherwise
        // adding sprites (created from those configs) to composite
        // will result in an attempt to access attributes that
        // composite doesn't have yet.
        label: {
            $value: {
                type: 'text'
            },
            lazy: true
        },
        marker: {
            $value: {
                type: 'circle'
            },
            lazy: true
        },
        legend: null,
        store: null,
        record: null,
        series: null
    },
    applyLabel: function(label, oldLabel) {
        var sprite;
        if (label) {
            if (label.isSprite && label.type === 'text') {
                sprite = label;
            } else {
                if (oldLabel && label.type === oldLabel.type) {
                    oldLabel.setConfig(label);
                    sprite = oldLabel;
                    this.scheduleUpdater(this.attr, 'layout');
                } else {
                    sprite = new Ext.draw.sprite.Text(label);
                }
            }
        }
        return sprite;
    },
    defaultMarkerSize: 10,
    updateLabel: function(label, oldLabel) {
        var me = this;
        me.removeSprite(oldLabel);
        label.setAttributes({
            textBaseline: 'middle'
        });
        me.addSprite(label);
        me.scheduleUpdater(me.attr, 'layout');
    },
    applyMarker: function(config) {
        var marker;
        if (config) {
            if (config.isSprite) {
                marker = config;
            } else {
                marker = this.createMarker(config);
            }
        }
        marker = this.resetMarker(marker, config);
        return marker;
    },
    createMarker: function(config) {
        var marker;
        // If marker attributes are animated, the attributes change over
        // time from default values to the values specified in the marker
        // config. But the 'legenditem' sprite needs final values
        // to properly layout its children.
        delete config.animation;
        if (config.type === 'image') {
            delete config.width;
            delete config.height;
        }
        marker = Ext.create('sprite.' + config.type, config);
        return marker;
    },
    resetMarker: function(sprite, config) {
        var size = config.size || this.defaultMarkerSize,
            bbox, max, scale;
        // Layout may not work properly,
        // if the marker sprite is transformed to begin with.
        sprite.setTransform([
            1,
            0,
            0,
            1,
            0,
            0
        ], true);
        if (config.type === 'image') {
            sprite.setAttributes({
                width: size,
                height: size
            });
        } else {
            // This should work with any sprite, irrespective of what attribute
            // is used to control sprite's size ('size', 'r', or something else).
            // However, the 'image' sprite above is a special case.
            bbox = sprite.getBBox();
            max = Math.max(bbox.width, bbox.height);
            scale = size / max;
            sprite.setAttributes({
                scalingX: scale,
                scalingY: scale
            });
        }
        return sprite;
    },
    updateMarker: function(marker, oldMarker) {
        var me = this;
        me.removeSprite(oldMarker);
        me.addSprite(marker);
        me.scheduleUpdater(me.attr, 'layout');
    },
    updateSurface: function(surface, oldSurface) {
        var me = this;
        me.callParent([
            surface,
            oldSurface
        ]);
        if (surface) {
            me.scheduleUpdater(me.attr, 'layout');
        }
    },
    enabledUpdater: function(attr) {
        var marker = this.getMarker();
        if (marker) {
            marker.setAttributes({
                globalAlpha: attr.enabled ? 1 : 0.3
            });
        }
    },
    layoutUpdater: function() {
        var me = this,
            attr = me.attr,
            label = me.getLabel(),
            marker = me.getMarker(),
            labelBBox, markerBBox, totalHeight;
        // Measuring bounding boxes of transformed marker and label
        // sprites and translating the sprites by required amount,
        // makes layout virtually bullet-proof to unaccounted for
        // changes in sprite attributes, whatever the sprite type may be.
        markerBBox = marker.getBBox();
        labelBBox = label.getBBox();
        totalHeight = Math.max(markerBBox.height, labelBBox.height);
        // Because we are getting an already transformed bounding box,
        // we want to add to that transformation, not replace it,
        // so setting translationX/Y attributes here would be inappropriate.
        marker.transform([
            1,
            0,
            0,
            1,
            -markerBBox.x,
            -markerBBox.y + (totalHeight - markerBBox.height) / 2
        ], true);
        label.transform([
            1,
            0,
            0,
            1,
            -labelBBox.x + markerBBox.width + attr.markerLabelGap,
            -labelBBox.y + (totalHeight - labelBBox.height) / 2
        ], true);
        me.bboxUpdater(attr);
    }
});

/**
 * @private
 */
Ext.define('Ext.chart.legend.sprite.Border', {
    extend: 'Ext.draw.sprite.Rect',
    alias: 'sprite.legendborder',
    type: 'legendborder',
    isLegendBorder: true
});

/**
 * @private
 * Singleton that provides methods used by the Ext.draw.Path
 * for hit testing and finding path intersection points.
 */
Ext.define('Ext.draw.PathUtil', function() {
    var abs = Math.abs,
        pow = Math.pow,
        cos = Math.cos,
        acos = Math.acos,
        sqrt = Math.sqrt,
        PI = Math.PI;
    // For extra info see: http://pomax.github.io/bezierinfo/
    return {
        singleton: true,
        requires: [
            'Ext.draw.overrides.hittest.Path',
            'Ext.draw.overrides.hittest.sprite.Path'
        ],
        /**
         * @private
         * Finds roots of a cubic equation in t, where t lies in the interval of [0,1].
         * Based on http://www.particleincell.com/blog/2013/cubic-line-intersection/
         * @param P {Number[]} Cubic equation coefficients.
         * @return {Number[]} Returns an array of parametric intersection locations along the cubic,
         *                  with -1 indicating an out-of-bounds intersection
         *                  (before or after the end point or in the imaginary plane).
         */
        cubicRoots: function(P) {
            var a = P[0],
                b = P[1],
                c = P[2],
                d = P[3];
            if (a === 0) {
                return this.quadraticRoots(b, c, d);
            }
            // eslint-disable-next-line vars-on-top, one-var
            var A = b / a,
                B = c / a,
                C = d / a,
                Q = (3 * B - pow(A, 2)) / 9,
                R = (9 * A * B - 27 * C - 2 * pow(A, 3)) / 54,
                D = pow(Q, 3) + pow(R, 2),
                // Polynomial discriminant.
                t = [],
                S, T, Im, th, i,
                sign = Ext.Number.sign;
            if (D >= 0) {
                // Complex or duplicate roots.
                S = sign(R + sqrt(D)) * pow(abs(R + sqrt(D)), 1 / 3);
                T = sign(R - sqrt(D)) * pow(abs(R - sqrt(D)), 1 / 3);
                t[0] = -A / 3 + (S + T);
                // Real root.
                t[1] = -A / 3 - (S + T) / 2;
                // Real part of complex root.
                t[2] = t[1];
                // Real part of complex root.
                Im = abs(sqrt(3) * (S - T) / 2);
                // Complex part of root pair.
                // Discard complex roots.
                if (Im !== 0) {
                    t[1] = -1;
                    t[2] = -1;
                }
            } else {
                // Distinct real roots.
                th = acos(R / sqrt(-pow(Q, 3)));
                t[0] = 2 * sqrt(-Q) * cos(th / 3) - A / 3;
                t[1] = 2 * sqrt(-Q) * cos((th + 2 * PI) / 3) - A / 3;
                t[2] = 2 * sqrt(-Q) * cos((th + 4 * PI) / 3) - A / 3;
            }
            // Discard out of spec roots.
            for (i = 0; i < 3; i++) {
                if (t[i] < 0 || t[i] > 1) {
                    t[i] = -1;
                }
            }
            return t;
        },
        /**
         * @private
         * Finds roots of a quadratic equation in t, where t lies in the interval of [0,1].
         * Takes three quadratic equation coefficients as parameters.
         * @param a {Number}
         * @param b {Number}
         * @param c {Number}
         * @return {Array}
         */
        quadraticRoots: function(a, b, c) {
            var D, rD, t, i;
            if (a === 0) {
                return this.linearRoot(b, c);
            }
            D = b * b - 4 * a * c;
            if (D === 0) {
                // One real root.
                t = [
                    -b / (2 * a)
                ];
            } else if (D > 0) {
                // Distinct real roots.
                rD = sqrt(D);
                t = [
                    (-b - rD) / (2 * a),
                    (-b + rD) / (2 * a)
                ];
            } else {
                // Complex roots.
                return [];
            }
            for (i = 0; i < t.length; i++) {
                if (t[i] < 0 || t[i] > 1) {
                    t[i] = -1;
                }
            }
            return t;
        },
        /**
         * @private
         * Finds roots of a linear equation in t, where t lies in the interval of [0,1].
         * Takes two linear equation coefficients as parameters.
         * @param a {Number}
         * @param b {Number}
         * @return {Array}
         */
        linearRoot: function(a, b) {
            var t = -b / a;
            if (a === 0 || t < 0 || t > 1) {
                return [];
            }
            return [
                t
            ];
        },
        /**
         * @private
         * Calculates the coefficients of a cubic function for the given coordinates.
         * @param P0 {Number}
         * @param P1 {Number}
         * @param P2 {Number}
         * @param P3 {Number}
         * @return {Array}
         */
        bezierCoeffs: function(P0, P1, P2, P3) {
            var Z = [];
            Z[0] = -P0 + 3 * P1 - 3 * P2 + P3;
            Z[1] = 3 * P0 - 6 * P1 + 3 * P2;
            Z[2] = -3 * P0 + 3 * P1;
            Z[3] = P0;
            return Z;
        },
        /**
         * @private
         * Computes intersection points between a cubic spline and a line segment.
         * Takes in x/y components of cubic control points and line segment start/end points
         * as parameters.
         * @param px1 {Number}
         * @param px2 {Number}
         * @param px3 {Number}
         * @param px4 {Number}
         * @param py1 {Number}
         * @param py2 {Number}
         * @param py3 {Number}
         * @param py4 {Number}
         * @param x1 {Number}
         * @param y1 {Number}
         * @param x2 {Number}
         * @param y2 {Number}
         * @return {Array} Array of intersection points, where each intersection point
         *                  is itself a two-item array [x,y].
         */
        cubicLineIntersections: function(px1, px2, px3, px4, py1, py2, py3, py4, x1, y1, x2, y2) {
            var P = [],
                intersections = [],
                // Finding line equation coefficients.
                A = y1 - y2,
                B = x2 - x1,
                C = x1 * (y2 - y1) - y1 * (x2 - x1),
                // Finding cubic Bezier curve equation coefficients.
                bx = this.bezierCoeffs(px1, px2, px3, px4),
                by = this.bezierCoeffs(py1, py2, py3, py4),
                i, r, s, t, tt, ttt, cx, cy;
            P[0] = A * bx[0] + B * by[0];
            // t^3
            P[1] = A * bx[1] + B * by[1];
            // t^2
            P[2] = A * bx[2] + B * by[2];
            // t
            P[3] = A * bx[3] + B * by[3] + C;
            // 1
            r = this.cubicRoots(P);
            // Verify the roots are in bounds of the linear segment.
            for (i = 0; i < r.length; i++) {
                t = r[i];
                if (t < 0 || t > 1) {
                    
                    continue;
                }
                tt = t * t;
                ttt = tt * t;
                cx = bx[0] * ttt + bx[1] * tt + bx[2] * t + bx[3];
                cy = by[0] * ttt + by[1] * tt + by[2] * t + by[3];
                // Above is intersection point assuming infinitely long line segment,
                // make sure we are also in bounds of the line.
                if ((x2 - x1) !== 0) {
                    // If not vertical line
                    s = (cx - x1) / (x2 - x1);
                } else {
                    s = (cy - y1) / (y2 - y1);
                }
                // In bounds?
                if (!(s < 0 || s > 1)) {
                    intersections.push([
                        cx,
                        cy
                    ]);
                }
            }
            return intersections;
        },
        /**
         * @private
         * Splits cubic Bezier curve into two cubic Bezier curves at point z,
         * where z belongs to a range of [0, 1].
         * Accepts cubic coefficients and point z as parameters.
         * @param P1 {Number}
         * @param P2 {Number}
         * @param P3 {Number}
         * @param P4 {Number}
         * @param z Point to split the given curve at.
         * @return {Array} Two-item array, where each item is itself an array
         *                  of cubic coefficients.
         */
        splitCubic: function(P1, P2, P3, P4, z) {
            var zz = z * z,
                zzz = z * zz,
                iz = z - 1,
                izz = iz * iz,
                izzz = iz * izz,
                // Common point for both curves.
                P = zzz * P4 - 3 * zz * iz * P3 + 3 * z * izz * P2 - izzz * P1;
            return [
                [
                    P1,
                    z * P2 - iz * P1,
                    zz * P3 - 2 * z * iz * P2 + izz * P1,
                    P
                ],
                [
                    P,
                    zz * P4 - 2 * z * iz * P3 + izz * P2,
                    z * P4 - iz * P3,
                    P4
                ]
            ];
        },
        /**
         * @private
         * Returns the dimension of a cubic Bezier curve in a single direction.
         * @param a {Number}
         * @param b {Number}
         * @param c {Number}
         * @param d {Number}
         * @return {Array} Two-item array representing cubic's range in the given direction.
         */
        cubicDimension: function(a, b, c, d) {
            var qa = 3 * (-a + 3 * (b - c) + d),
                qb = 6 * (a - 2 * b + c),
                qc = -3 * (a - b),
                x, y,
                min = Math.min(a, d),
                max = Math.max(a, d),
                delta;
            if (qa === 0) {
                if (qb === 0) {
                    return [
                        min,
                        max
                    ];
                } else {
                    x = -qc / qb;
                    if (0 < x && x < 1) {
                        y = this.interpolateCubic(a, b, c, d, x);
                        min = Math.min(min, y);
                        max = Math.max(max, y);
                    }
                }
            } else {
                delta = qb * qb - 4 * qa * qc;
                if (delta >= 0) {
                    delta = sqrt(delta);
                    x = (delta - qb) / 2 / qa;
                    if (0 < x && x < 1) {
                        y = this.interpolateCubic(a, b, c, d, x);
                        min = Math.min(min, y);
                        max = Math.max(max, y);
                    }
                    if (delta > 0) {
                        x -= delta / qa;
                        if (0 < x && x < 1) {
                            y = this.interpolateCubic(a, b, c, d, x);
                            min = Math.min(min, y);
                            max = Math.max(max, y);
                        }
                    }
                }
            }
            return [
                min,
                max
            ];
        },
        /**
         * @private
         * Calculates a value of a cubic function at the given point t. In other words
         * returns a * (1 - t) ^ 3 + 3 * b (1 - t) ^ 2 * t + 3 * c (1 - t) * t ^ 3 + d * t ^ 3
         * for given a, b, c, d and t, where t belongs to an interval of [0, 1].
         * @param a {Number}
         * @param b {Number}
         * @param c {Number}
         * @param d {Number}
         * @param t {Number}
         * @return {Number}
         */
        interpolateCubic: function(a, b, c, d, t) {
            var rate;
            if (t === 0) {
                return a;
            }
            if (t === 1) {
                return d;
            }
            rate = (1 - t) / t;
            return t * t * t * (d + rate * (3 * c + rate * (3 * b + rate * a)));
        },
        /**
         * @private
         * Computes intersection points between two cubic Bezier curve segments.
         * Takes x/y components of control points for two Bezier curve segments.
         * @param ax1 {Number}
         * @param ax2 {Number}
         * @param ax3 {Number}
         * @param ax4 {Number}
         * @param ay1 {Number}
         * @param ay2 {Number}
         * @param ay3 {Number}
         * @param ay4 {Number}
         * @param bx1 {Number}
         * @param bx2 {Number}
         * @param bx3 {Number}
         * @param bx4 {Number}
         * @param by1 {Number}
         * @param by2 {Number}
         * @param by3 {Number}
         * @param by4 {Number}
         * @return {Array} Array of intersection points, where each intersection point
         *                  is itself a two-item array [x,y].
         */
        cubicsIntersections: function(ax1, ax2, ax3, ax4, ay1, ay2, ay3, ay4, bx1, bx2, bx3, bx4, by1, by2, by3, by4) {
            /* eslint-disable max-len */
            var me = this,
                axDim = me.cubicDimension(ax1, ax2, ax3, ax4),
                ayDim = me.cubicDimension(ay1, ay2, ay3, ay4),
                bxDim = me.cubicDimension(bx1, bx2, bx3, bx4),
                byDim = me.cubicDimension(by1, by2, by3, by4),
                splitAx, splitAy, splitBx, splitBy,
                points = [];
            // Curves' bounding boxes don't intersect.
            if (axDim[0] > bxDim[1] || axDim[1] < bxDim[0] || ayDim[0] > byDim[1] || ayDim[1] < byDim[0]) {
                return [];
            }
            // Both curves occupy sub-pixel areas which is effectively their intersection point.
            if (abs(ay1 - ay2) < 1 && abs(ay3 - ay4) < 1 && abs(ax1 - ax4) < 1 && abs(ax2 - ax3) < 1 && abs(by1 - by2) < 1 && abs(by3 - by4) < 1 && abs(bx1 - bx4) < 1 && abs(bx2 - bx3) < 1) {
                return [
                    [
                        (ax1 + ax4) * 0.5,
                        (ay1 + ay2) * 0.5
                    ]
                ];
            }
            splitAx = me.splitCubic(ax1, ax2, ax3, ax4, 0.5);
            splitAy = me.splitCubic(ay1, ay2, ay3, ay4, 0.5);
            splitBx = me.splitCubic(bx1, bx2, bx3, bx4, 0.5);
            splitBy = me.splitCubic(by1, by2, by3, by4, 0.5);
            points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[0].concat(splitAy[0], splitBx[0], splitBy[0])));
            points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[0].concat(splitAy[0], splitBx[1], splitBy[1])));
            points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[1].concat(splitAy[1], splitBx[0], splitBy[0])));
            points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[1].concat(splitAy[1], splitBx[1], splitBy[1])));
            return points;
        },
        /* eslint-enable max-len */
        /**
         * @private
         * Returns the point [x,y] where two line segments intersect or null.
         * Takes x/y components of the start and end point of the segments as parameters.
         * Based on Paul Bourke's explanation:
         * http://paulbourke.net/geometry/pointlineplane/
         * @param x1 {Number}
         * @param y1 {Number}
         * @param x2 {Number}
         * @param y2 {Number}
         * @param x3 {Number}
         * @param y3 {Number}
         * @param x4 {Number}
         * @param y4 {Number}
         * @return {Number[]|null}
         */
        linesIntersection: function(x1, y1, x2, y2, x3, y3, x4, y4) {
            var d = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3),
                ua, ub;
            if (d === 0) {
                // Lines are parallel.
                return null;
            }
            ua = ((x4 - x3) * (y1 - y3) - (x1 - x3) * (y4 - y3)) / d;
            ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / d;
            if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
                return [
                    x1 + ua * (x2 - x1),
                    // x
                    y1 + ua * (y2 - y1)
                ];
            }
            // y
            return null;
        },
        // The intersection point is outside one or both segments.
        /**
         * @private
         * Checks if a point belongs to a line segment.
         * Takes x/y components of the start and end points of the segment and the point's
         * coordinates as parameters.
         * @param x1 {Number}
         * @param y1 {Number}
         * @param x2 {Number}
         * @param y2 {Number}
         * @param x {Number}
         * @param y {Number}
         * @return {Boolean}
         */
        pointOnLine: function(x1, y1, x2, y2, x, y) {
            var t, _;
            if (abs(x2 - x1) < abs(y2 - y1)) {
                _ = x1;
                x1 = y1;
                y1 = _;
                _ = x2;
                x2 = y2;
                y2 = _;
                _ = x;
                x = y;
                y = _;
            }
            t = (x - x1) / (x2 - x1);
            if (t < 0 || t > 1) {
                return false;
            }
            return abs(y1 + t * (y2 - y1) - y) < 4;
        },
        /**
         * @private
         * Checks if a point belongs to a cubic Bezier curve segment.
         * Takes x/y components of the control points of the segment and the point's
         * coordinates as parameters.
         * @param px1 {Number}
         * @param px2 {Number}
         * @param px3 {Number}
         * @param px4 {Number}
         * @param py1 {Number}
         * @param py2 {Number}
         * @param py3 {Number}
         * @param py4 {Number}
         * @param x {Number}
         * @param y {Number}
         * @return {Boolean}
         */
        pointOnCubic: function(px1, px2, px3, px4, py1, py2, py3, py4, x, y) {
            // Finding cubic Bezier curve equation coefficients.
            var me = this,
                bx = me.bezierCoeffs(px1, px2, px3, px4),
                by = me.bezierCoeffs(py1, py2, py3, py4),
                i, j, rx, ry, t;
            bx[3] -= x;
            by[3] -= y;
            rx = me.cubicRoots(bx);
            ry = me.cubicRoots(by);
            for (i = 0; i < rx.length; i++) {
                t = rx[i];
                for (j = 0; j < ry.length; j++) {
                    // TODO: for more accurate results tolerance should be dynamic
                    // TODO: based on the length and shape of the segment.
                    if (t >= 0 && t <= 1 && abs(t - ry[j]) < 0.05) {
                        return true;
                    }
                }
            }
            return false;
        }
    };
});

Ext.define('Ext.draw.overrides.hittest.All', {
    requires: [
        'Ext.draw.PathUtil',
        'Ext.draw.overrides.hittest.sprite.Instancing',
        'Ext.draw.overrides.hittest.Surface'
    ]
});

/**
 * This class uses `Ext.draw.sprite.Sprite` to render the chart legend.
 *
 * The DOM legend is essentially a data view docked inside a draw container, which a chart is.
 * The sprite legend, on the other hand, is not a foreign entity in a draw container,
 * and is rendered in a draw surface with sprites, just like series and axes.
 *
 * This means that:
 *
 * * it is styleable with chart themes
 * * it shows up in chart preview and chart download
 * * it renders markers exactly as they are in the series
 * * it can't be styled with CSS
 * * it doesn't scroll, instead the items are grouped into columns,
 *   and the legend grows in size as the number of items increases
 *
 */
Ext.define('Ext.chart.legend.SpriteLegend', {
    alias: 'legend.sprite',
    type: 'sprite',
    isLegend: true,
    isSpriteLegend: true,
    mixins: [
        'Ext.mixin.Observable'
    ],
    requires: [
        'Ext.chart.legend.sprite.Item',
        'Ext.chart.legend.sprite.Border',
        'Ext.draw.overrides.hittest.All',
        'Ext.draw.Animator'
    ],
    config: {
        /**
         * @cfg {'top'/'left'/'right'/'bottom'} docked
         * The position of the legend in the chart.
         */
        docked: 'bottom',
        /**
         * @cfg {Ext.chart.legend.store.Store} store
         * The {@link Ext.chart.legend.store.Store} to bind this legend to.
         * @private
         */
        store: null,
        /**
         * @cfg {Ext.chart.AbstractChart} chart
         * The chart that the store belongs to.
         */
        chart: null,
        /**
         * @cfg {Ext.draw.Surface} surface
         * The chart surface used to render legend sprites.
         * @protected
         */
        surface: null,
        /**
         * @cfg {Object} size
         * The size of the area occupied by the legend's sprites.
         * This is set by the legend itself and then used during chart layout
         * to make sure the 'legend' surface is big enough to accommodate
         * legend sprites.
         * @cfg {Number} size.width
         * @cfg {Number} size.height
         * @readonly
         */
        size: {
            width: 0,
            height: 0
        },
        /**
         * @cfg {Boolean} toggleable
         * `true` to allow series items to have their visibility
         * toggled by interaction with the legend items.
         */
        toggleable: true,
        /**
         * @cfg {Number} padding
         * The padding amount between legend items and legend border.
         */
        padding: 10,
        label: {
            preciseMeasurement: true
        },
        /**
         * The sprite to use as a legend item marker. By default a corresponding series
         * marker is used. If the series has no marker, the `circle` sprite
         * is used as a legend item marker, where its `fillStyle`, `strokeStyle` and
         * `lineWidth` match that of the series. The size of a legend item marker is
         * controlled by the `size` property, which to defaults to `10` (pixels).
         */
        marker: {},
        /**
         * @cfg {Object} border
         * The border that goes around legend item sprites.
         * The type of the sprite is determined by this config,
         * while the styling comes from a theme {@link Ext.chart.theme.Base #legend}.
         * If both this config and the theme provide values for the
         * same configs, the values from this config are used.
         * The sprite class used a legend border should have the `isLegendBorder`
         * property set to true on the prototype. The legend border sprite
         * should also have the `x`, `y`, `width` and `height` attributes
         * that determine it's position and dimensions.
         */
        border: {
            $value: {
                type: 'legendborder'
            },
            // The config should be processed at the time of the 'getSprites' call,
            // when we already have the legend surface, otherwise the border sprite
            // will not be added to the surface.
            lazy: true
        },
        /**
         * @cfg {Object} background
         * Sets the legend background.
         * This can be a gradient object, image, or color. This config works similarly
         * to the {@link Ext.chart.AbstractChart#background} config.
         */
        background: null,
        /**
         * @cfg {Boolean} hidden Toggles the visibility of the legend.
         */
        hidden: false
    },
    sprites: null,
    spriteZIndexes: {
        background: 0,
        border: 1,
        // Item sprites should have a higher zIndex than border,
        // or they won't react to clicks.
        item: 2
    },
    dockedValues: {
        left: true,
        right: true,
        top: true,
        bottom: true
    },
    constructor: function(config) {
        var me = this;
        me.oldSize = {
            width: 0,
            height: 0
        };
        me.getId();
        me.mixins.observable.constructor.call(me, config);
    },
    applyStore: function(store) {
        return store && Ext.StoreManager.lookup(store);
    },
    updateStore: function(store, oldStore) {
        var me = this;
        if (oldStore) {
            oldStore.un('datachanged', me.onDataChanged, me);
            oldStore.un('update', me.onDataUpdate, me);
        }
        if (store) {
            store.on('datachanged', me.onDataChanged, me);
            store.on('update', me.onDataUpdate, me);
            me.onDataChanged(store);
        }
        me.performLayout();
    },
    //<debug>
    applyDocked: function(docked) {
        if (!(docked in this.dockedValues)) {
            Ext.raise("Invalid 'docked' config value.");
        }
        return docked;
    },
    //</debug>
    updateDocked: function(docked) {
        this.isTop = docked === 'top';
        if (!this.isConfiguring) {
            this.layoutChart();
        }
    },
    updateHidden: function(hidden) {
        var surface;
        this.getChart();
        // 'chart' updater will set the surface
        surface = this.getSurface();
        if (surface) {
            surface.setHidden(hidden);
        }
        if (!this.isConfiguring) {
            this.layoutChart();
        }
    },
    /**
     * @private
     */
    layoutChart: function() {
        var chart;
        if (!this.isConfiguring) {
            chart = this.getChart();
            if (chart) {
                chart.scheduleLayout();
            }
        }
    },
    /**
     * @private
     * Calculates and returns the legend surface rect and adjusts the passed `chartRect`
     * accordingly. The first time this is called, the `SpriteLegend` will have zero size
     * (no width or height).
     * @param {Number[]} chartRect [left, top, width, height] components as an array.
     * @return {Number[]} [left, top, width, height] components as an array, or null.
     */
    computeRect: function(chartRect) {
        var rect, docked, size, height, width;
        if (this.getHidden()) {
            return null;
        }
        rect = [
            0,
            0,
            0,
            0
        ];
        docked = this.getDocked();
        size = this.getSize();
        height = size.height;
        width = size.width;
        switch (docked) {
            case 'top':
                rect[1] = chartRect[1];
                rect[2] = chartRect[2];
                rect[3] = height;
                chartRect[1] += height;
                chartRect[3] -= height;
                break;
            case 'bottom':
                chartRect[3] -= height;
                rect[1] = chartRect[3];
                rect[2] = chartRect[2];
                rect[3] = height;
                break;
            case 'left':
                chartRect[0] += width;
                chartRect[2] -= width;
                rect[2] = width;
                rect[3] = chartRect[3];
                break;
            case 'right':
                chartRect[2] -= width;
                rect[0] = chartRect[2];
                rect[2] = width;
                rect[3] = chartRect[3];
                break;
        }
        return rect;
    },
    applyBorder: function(config) {
        var border;
        if (config) {
            if (config.isSprite) {
                border = config;
            } else {
                border = Ext.create('sprite.' + config.type, config);
            }
        }
        if (border) {
            border.isLegendBorder = true;
            border.setAttributes({
                zIndex: this.spriteZIndexes.border
            });
        }
        return border;
    },
    updateBorder: function(border, oldBorder) {
        var surface = this.getSurface();
        this.borderSprite = null;
        if (surface) {
            if (oldBorder) {
                surface.remove(oldBorder);
            }
            if (border) {
                this.borderSprite = surface.add(border);
            }
        }
    },
    scheduleLayout: function() {
        if (!this.scheduledLayoutId) {
            this.scheduledLayoutId = Ext.draw.Animator.schedule('performLayout', this);
        }
    },
    cancelLayout: function() {
        Ext.draw.Animator.cancel(this.scheduledLayoutId);
        this.scheduledLayoutId = null;
    },
    performLayout: function() {
        var me = this,
            size = me.getSize(),
            gap = me.getPadding(),
            sprites = me.getSprites(),
            surface = me.getSurface(),
            background = me.getBackground(),
            surfaceRect = surface.getRect(),
            store = me.getStore(),
            ln = (sprites && sprites.length) || 0,
            i, sprite;
        if (!surface || !surfaceRect || !store) {
            return false;
        }
        me.cancelLayout();
        // eslint-disable-next-line vars-on-top, one-var
        var docked = me.getDocked(),
            surfaceWidth = surfaceRect[2],
            surfaceHeight = surfaceRect[3],
            border = me.borderSprite,
            bboxes = [],
            startX, // Coordinates of the top-left corner.
            startY, // of the first 'legenditem' sprite.
            columnSize, // Number of items in a column.
            columnCount, // Number of columns.
            columnWidth, itemsWidth, itemsHeight, paddedItemsWidth, // The horizontal span of all 'legenditem' sprites.
            paddedItemsHeight, // The vertical span of all 'legenditem' sprites.
            paddedBorderWidth, paddedBorderHeight, // eslint-disable-line no-unused-vars
            itemHeight, bbox, x, y;
        for (i = 0; i < ln; i++) {
            sprite = sprites[i];
            bbox = sprite.getBBox();
            bboxes.push(bbox);
        }
        if (bbox) {
            itemHeight = bbox.height;
        }
        switch (docked) {
            /*

             Horizontal legend.
             The outer box is the legend surface.
             The inner box is the legend border.
             There's a fixed amount of padding between all the items,
             denoted by ##. This amount is controlled by the 'padding' config
             of the legend.

             |-------------------------------------------------------------|
             |                             ##                              |
             |    |---------------------------------------------------|    |
             |    |        ##              ##               ##        |    |
             |    |     --------        -----------      --------     |    |
             | ## | ## | Item 0 |   ## | Item 2    | ## | Item 4 | ## | ## |
             |    |     --------        -----------      --------     |    |
             |    |        ##              ##               ##        |    |
             |    |     ----------      ---------                     |    |
             |    | ## | Item 1   | ## | Item 3  |                    |    |
             |    |     ----------      ---------                     |    |
             |    |        ##              ##                         |    |
             |    |---------------------------------------------------|    |
             |                             ##                              |
             |-------------------------------------------------------------|

             */
            case 'bottom':
            case 'top':
                // surface must have a width before we can proceed to layout top/bottom
                // docked legend.  width may be 0 if we are rendered into an inactive tab.
                // see https://sencha.jira.com/browse/EXTJS-22454
                if (!surfaceWidth) {
                    return false;
                };
                columnSize = 0;
                // Split legend items into columns until the width is suitable.
                do {
                    itemsWidth = 0;
                    columnWidth = 0;
                    columnCount = 0;
                    columnSize++;
                    for (i = 0; i < ln; i++) {
                        bbox = bboxes[i];
                        if (bbox.width > columnWidth) {
                            columnWidth = bbox.width;
                            columnWidth = Math.min(columnWidth, surfaceWidth - (gap * 4));
                        }
                        if ((i + 1) % columnSize === 0) {
                            itemsWidth += columnWidth;
                            columnWidth = 0;
                            columnCount++;
                        }
                    }
                    if (i % columnSize !== 0) {
                        itemsWidth += columnWidth;
                        columnCount++;
                    }
                    paddedItemsWidth = itemsWidth + (columnCount - 1) * gap;
                    paddedBorderWidth = paddedItemsWidth + gap * 4;
                } while (// if the column width is greater than available surface width,
                // set it to maximum available width
                paddedBorderWidth > surfaceWidth);
                paddedItemsHeight = itemHeight * columnSize + (columnSize - 1) * gap;
                break;
            /*

             Vertical legend.

             |-----------------------------------------------|
             |                     ##                        |
             |    |-------------------------------------|    |
             |    |        ##               ##          |    |
             |    |     --------        -----------     |    |
             |    | ## | Item 0 |   ## | Item 1    | ## |    |
             |    |     --------        -----------     |    |
             |    |        ##               ##          |    |
             |    |     ----------      ---------       |    |
             | ## | ## | Item 2   | ## | Item 3  |      | ## |
             |    |     ----------      ---------       |    |
             |    |        ##                           |    |
             |    |     --------                        |    |
             |    | ## | Item 4 |                       |    |
             |    |     --------                        |    |
             |    |        ##                           |    |
             |    |-------------------------------------|    |
             |                     ##                        |
             |-----------------------------------------------|

             */
            case 'right':
            case 'left':
                // surface must have a height before we can proceed to layout right/left
                // docked legend.  height may be 0 if we are rendered into an inactive tab.
                // see https://sencha.jira.com/browse/EXTJS-22454
                if (!surfaceHeight) {
                    return false;
                };
                columnSize = ln * 2;
                // Split legend items into columns until the height is suitable.
                do {
                    columnSize = (columnSize >> 1) + (columnSize % 2);
                    itemsWidth = 0;
                    itemsHeight = 0;
                    columnWidth = 0;
                    columnCount = 0;
                    for (i = 0; i < ln; i++) {
                        bbox = bboxes[i];
                        if (!columnCount) {
                            itemsHeight += bbox.height;
                        }
                        if (bbox.width > columnWidth) {
                            columnWidth = bbox.width;
                        }
                        if ((i + 1) % columnSize === 0) {
                            itemsWidth += columnWidth;
                            columnWidth = 0;
                            columnCount++;
                        }
                    }
                    if (i % columnSize !== 0) {
                        itemsWidth += columnWidth;
                        columnCount++;
                    }
                    paddedItemsWidth = itemsWidth + (columnCount - 1) * gap;
                    paddedItemsHeight = itemsHeight + (columnSize - 1) * gap;
                    paddedBorderWidth = paddedItemsWidth + gap * 4;
                    paddedBorderHeight = paddedItemsHeight + gap * 4;
                } while (// Integer division by 2, plus remainder.
                // itemsHeight is determined by the height of the first column.
                paddedItemsHeight > surfaceHeight);
                break;
        }
        startX = (surfaceWidth - paddedItemsWidth) / 2;
        startY = (surfaceHeight - paddedItemsHeight) / 2;
        x = 0;
        y = 0;
        columnWidth = 0;
        for (i = 0; i < ln; i++) {
            sprite = sprites[i];
            bbox = bboxes[i];
            sprite.setAttributes({
                translationX: startX + x,
                translationY: startY + y
            });
            if (bbox.width > columnWidth) {
                columnWidth = bbox.width;
            }
            if ((i + 1) % columnSize === 0) {
                x += columnWidth + gap;
                y = 0;
                columnWidth = 0;
            } else {
                y += bbox.height + gap;
            }
        }
        if (border) {
            border.setAttributes({
                hidden: !ln,
                x: startX - gap,
                y: startY - gap,
                width: paddedItemsWidth + gap * 2,
                height: paddedItemsHeight + gap * 2
            });
        }
        size.width = border.attr.width + gap * 2;
        size.height = border.attr.height + gap * 2;
        if (size.width !== me.oldSize.width || size.height !== me.oldSize.height) {
            // Do not simply assign size to oldSize, as we want them to be
            // separate objects.
            Ext.apply(me.oldSize, size);
            // Legend size has changed, so we return 'false' to cancel the current
            // chart layout (this method is called by chart's 'performLayout' method)
            // and manually start a new chart layout.
            me.getChart().scheduleLayout();
            return false;
        }
        if (background) {
            me.resizeBackground(surface, background);
        }
        surface.renderFrame();
        return true;
    },
    // Doesn't include the border sprite which also belongs to the 'legend'
    // surface. To get it, use the 'getBorder' method.
    getSprites: function() {
        this.updateSprites();
        return this.sprites;
    },
    /**
     * @private
     * Creates a 'legenditem' sprite in the given surface
     * using the legend store record data provided.
     * @param {Ext.draw.Surface} surface
     * @param {Ext.chart.legend.store.Item} record
     * @return {Ext.chart.legend.sprite.Item}
     */
    createSprite: function(surface, record) {
        var me = this,
            data = record.data,
            chart = me.getChart(),
            series = chart.get(data.series),
            seriesMarker = series.getMarker(),
            sprite = null,
            markerConfig, labelConfig, legendItemConfig;
        if (surface) {
            markerConfig = series.getMarkerStyleByIndex(data.index);
            markerConfig.fillStyle = data.mark;
            markerConfig.hidden = false;
            if (seriesMarker && seriesMarker.type) {
                markerConfig.type = seriesMarker.type;
            }
            Ext.apply(markerConfig, me.getMarker());
            markerConfig.surface = surface;
            labelConfig = me.getLabel();
            legendItemConfig = {
                type: 'legenditem',
                zIndex: me.spriteZIndexes.item,
                text: data.name,
                enabled: !data.disabled,
                marker: markerConfig,
                label: labelConfig,
                series: data.series,
                record: record
            };
            sprite = surface.add(legendItemConfig);
        }
        return sprite;
    },
    /**
     * @private
     * Creates legend item sprites and associates them with legend store records.
     * Updates attributes of the sprites when legend store data changes.
     */
    updateSprites: function() {
        var me = this,
            chart = me.getChart(),
            store = me.getStore(),
            surface = me.getSurface(),
            item, items, itemSprite, i, ln, sprites, unusedSprites, border;
        if (!(chart && store && surface)) {
            return;
        }
        me.sprites = sprites = me.sprites || [];
        items = store.getData().items;
        ln = items.length;
        for (i = 0; i < ln; i++) {
            item = items[i];
            itemSprite = sprites[i];
            if (itemSprite) {
                me.updateSprite(itemSprite, item);
            } else {
                itemSprite = me.createSprite(surface, item);
                surface.add(itemSprite);
                sprites.push(itemSprite);
            }
        }
        unusedSprites = Ext.Array.splice(sprites, i, sprites.length);
        for (i = 0 , ln = unusedSprites.length; i < ln; i++) {
            itemSprite = unusedSprites[i];
            itemSprite.destroy();
        }
        border = me.getBorder();
        if (border) {
            me.borderSprite = border;
        }
        me.updateTheme(chart.getTheme());
    },
    /**
     * @private
     * Updates the given legend item sprite based on store record data.
     * @param {Ext.chart.legend.sprite.Item} sprite
     * @param {Ext.chart.legend.store.Item} record
     */
    updateSprite: function(sprite, record) {
        var data = record.data,
            chart = this.getChart(),
            series = chart.get(data.series),
            marker, label, markerConfig;
        if (sprite) {
            label = sprite.getLabel();
            label.setAttributes({
                text: data.name
            });
            sprite.setAttributes({
                enabled: !data.disabled
            });
            sprite.setConfig({
                series: data.series,
                record: record
            });
            markerConfig = series.getMarkerStyleByIndex(data.index);
            markerConfig.fillStyle = data.mark;
            markerConfig.hidden = false;
            Ext.apply(markerConfig, this.getMarker());
            marker = sprite.getMarker();
            marker.setAttributes({
                fillStyle: markerConfig.fillStyle,
                strokeStyle: markerConfig.strokeStyle
            });
            sprite.layoutUpdater(sprite.attr);
        }
    },
    updateChart: function(newChart, oldChart) {
        var me = this;
        if (oldChart) {
            me.setSurface(null);
        }
        if (newChart) {
            me.setSurface(newChart.getSurface('legend'));
        }
    },
    updateSurface: function(surface, oldSurface) {
        if (oldSurface) {
            oldSurface.el.un('click', 'onClick', this);
            // The surface should not be destroyed here, just cleared.
            // E.g. we may remove the sprite legend only to add another one.
            oldSurface.removeAll(true);
        }
        if (surface) {
            surface.isLegendSurface = true;
            surface.el.on('click', 'onClick', this);
        }
    },
    onClick: function(event) {
        var chart = this.getChart(),
            surface = this.getSurface(),
            result, point;
        if (chart && chart.hasFirstLayout && surface) {
            point = surface.getEventXY(event);
            result = surface.hitTest(point);
            if (result && result.sprite) {
                this.toggleItem(result.sprite);
            }
        }
    },
    applyBackground: function(newBackground, oldBackground) {
        var me = this,
            // It's important to get the `chart` first here,
            // because the `surface` is set by the `chart` updater.
            chart = me.getChart(),
            surface = me.getSurface(),
            background;
        background = chart.refreshBackground(surface, newBackground, oldBackground);
        if (background) {
            background.setAttributes({
                zIndex: me.spriteZIndexes.background
            });
        }
        return background;
    },
    resizeBackground: function(surface, background) {
        var width = background.attr.width,
            height = background.attr.height,
            surfaceRect = surface.getRect();
        if (surfaceRect && (width !== surfaceRect[2] || height !== surfaceRect[3])) {
            background.setAttributes({
                width: surfaceRect[2],
                height: surfaceRect[3]
            });
        }
    },
    themeableConfigs: {
        background: true
    },
    updateTheme: function(theme) {
        var me = this,
            surface = me.getSurface(),
            sprites = surface.getItems(),
            legendTheme = theme.getLegend(),
            labelConfig = me.getLabel(),
            configs = me.self.getConfigurator().configs,
            themeableConfigs = me.themeableConfigs,
            themeOnlyIfConfigured = me.themeOnlyIfConfigured,
            initialConfig = me.getInitialConfig(),
            defaultConfig = me.defaultConfig,
            value, cfg, isObjValue, isUnusedConfig, initialValue, sprite, style, labelSprite, key, attr, i, ln;
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            if (sprite.isLegendItem) {
                style = legendTheme.label;
                if (style) {
                    attr = null;
                    for (key in style) {
                        if (!(key in labelConfig)) {
                            attr = attr || {};
                            attr[key] = style[key];
                        }
                    }
                    if (attr) {
                        labelSprite = sprite.getLabel();
                        labelSprite.setAttributes(attr);
                    }
                }
                
                continue;
            } else if (sprite.isLegendBorder) {
                style = legendTheme.border;
            } else {
                
                continue;
            }
            if (style) {
                attr = {};
                for (key in style) {
                    if (!(key in sprite.config)) {
                        attr[key] = style[key];
                    }
                }
                sprite.setAttributes(attr);
            }
        }
        value = legendTheme.background;
        cfg = configs.background;
        if (value !== null && value !== undefined && cfg) {}
        // TODO
        for (key in legendTheme) {
            if (!(key in themeableConfigs)) {
                
                continue;
            }
            value = legendTheme[key];
            cfg = configs[key];
            if (value !== null && value !== undefined && cfg) {
                initialValue = initialConfig[key];
                isObjValue = Ext.isObject(value);
                isUnusedConfig = initialValue === defaultConfig[key];
                if (isObjValue) {
                    if (isUnusedConfig && themeOnlyIfConfigured[key]) {
                        
                        continue;
                    }
                    value = Ext.merge({}, value, initialValue);
                }
                if (isUnusedConfig || isObjValue) {
                    me[cfg.names.set](value);
                }
            }
        }
    },
    onDataChanged: function(store) {
        this.updateSprites();
        this.scheduleLayout();
    },
    onDataUpdate: function(store, record) {
        var me = this,
            sprites = me.sprites,
            ln = sprites.length,
            i = 0,
            sprite, spriteRecord, match;
        for (; i < ln; i++) {
            sprite = sprites[i];
            spriteRecord = sprite.getRecord();
            if (spriteRecord === record) {
                match = sprite;
                break;
            }
        }
        if (match) {
            me.updateSprite(match, record);
            me.scheduleLayout();
        }
    },
    toggleItem: function(sprite) {
        var disabledCount = 0,
            canToggle = true,
            store, i, count, record, disabled;
        if (!this.getToggleable() || !sprite.isLegendItem) {
            return;
        }
        store = this.getStore();
        if (store) {
            count = store.getCount();
            for (i = 0; i < count; i++) {
                record = store.getAt(i);
                if (record.get('disabled')) {
                    disabledCount++;
                }
            }
            canToggle = count - disabledCount > 1;
            record = sprite.getRecord();
            if (record) {
                disabled = record.get('disabled');
                if (disabled || canToggle) {
                    // This will trigger AbstractChart.onLegendStoreUpdate.
                    record.set('disabled', !disabled);
                    sprite.setAttributes({
                        enabled: disabled
                    });
                }
            }
        }
    },
    destroy: function() {
        var me = this;
        me.destroying = true;
        me.cancelLayout();
        me.setChart(null);
        me.callParent();
    }
});

/**
 *  Chart captions can be used to place titles, subtitles, credits and other captions
 *  inside a chart. Please see the chart's {@link Ext.chart.AbstractChart#captions}
 *  config documentation for the general description of the way captions work, and
 *  refer to the documentation of this class' configs for details.
 */
Ext.define('Ext.chart.Caption', {
    mixins: [
        'Ext.mixin.Observable',
        'Ext.mixin.Bindable'
    ],
    isCaption: true,
    config: {
        /**
         * The weight controls the order in which the captions are created.
         * Captions with lower weights are created first.
         * This affects chart's layout. For example, if two captions are docked
         * to the 'top', the one with the lower weight will end up on top
         * of the other.
         */
        weight: 0,
        /**
         * @cfg {String} text
         * The text displayed by the caption.
         * Multi-line captions are allowed, e.g.:
         *
         *     captions: {
         *         title: {
         *             text: 'India\'s tiger population\n'
         *                 + 'from 1970 to 2015'
         *         }
         *     }
         *
         */
        text: '',
        /**
         * @cfg {'left'/'center'/'right'} [align='center']
         * Determines the horizontal alignment of the caption's text.
         */
        align: 'center',
        /**
         * @cfg {'series'/'chart'} [alignTo='series']
         * Whether to align the caption to the 'series' (default) or the 'chart'.
         */
        alignTo: 'series',
        /**
         * @cfg {Number} padding
         * The uniform padding applied to both top and bottom of the caption's text.
         */
        padding: 0,
        /**
         * @cfg {Boolean} [hidden=false]
         * Controls the visibility of the caption.
         */
        hidden: false,
        /**
         * @cfg {'top'/'bottom'} [docked='top']
         * The position of the caption in a chart.
         */
        docked: 'top',
        /**
         * @cfg {Object} style
         * Style attributes for the caption's text.
         * All attributes that are valid {@link Ext.draw.sprite.Text text sprite} attributes
         * are valid here. However, only font attributes (such as `fontSize`, `fontFamily`, ...),
         * color attributes (such as `fillStyle`) and `textAlign` attribute are guaranteed to
         * produce correct behavior. For example, transform attributes are not officially supported.
         */
        style: {
            fontSize: '14px',
            fontWeight: 'bold',
            fontFamily: 'Verdana, Aria, sans-serif'
        },
        /**
         * @private
         * @cfg {Ext.chart.AbstractChart} chart
         * The chart the label belongs to.
         */
        chart: null,
        /**
         * @private
         * The text sprite used to render caption's text.
         */
        sprite: {
            type: 'text',
            preciseMeasurement: true,
            zIndex: 10
        },
        //<debug>
        /**
         * @private
         * @cfg {Boolean} debug
         * Whether to show the bounding boxes or not.
         */
        debug: false,
        //</debug>
        /**
         * @private
         * The logical rect of the caption in the `surfaceName` surface.
         */
        rect: null
    },
    surfaceName: 'caption',
    constructor: function(config) {
        var me = this,
            id;
        if ('id' in config) {
            id = config.id;
        } else if ('id' in me.config) {
            id = me.config.id;
        } else {
            id = me.getId();
        }
        me.setId(id);
        me.mixins.observable.constructor.call(me, config);
        me.initBindable();
    },
    updateChart: function() {
        if (!this.isConfiguring) {
            // Re-create caption's sprite in another chart.
            this.setSprite({
                type: 'text'
            });
        }
    },
    applySprite: function(sprite) {
        var me = this,
            chart = me.getChart(),
            surface = me.surface = chart.getSurface(me.surfaceName);
        //<debug>
        me.rectSprite = surface.add({
            type: 'rect',
            fillStyle: 'yellow',
            strokeStyle: 'red'
        });
        //</debug>
        return sprite && surface.add(sprite);
    },
    updateSprite: function(sprite, oldSprite) {
        if (oldSprite) {
            oldSprite.destroy();
        }
    },
    updateText: function(text) {
        this.getSprite().setAttributes({
            text: text
        });
    },
    updateStyle: function(style) {
        this.getSprite().setAttributes(style);
    },
    //<debug>
    updateDebug: function(debug) {
        var me = this,
            sprite = me.getSprite();
        if (debug && !me.rectSprite) {
            me.rectSprite = me.surface.add({
                type: 'rect',
                fillStyle: 'yellow',
                strokeStyle: 'red'
            });
        }
        if (sprite) {
            sprite.setAttributes({
                debug: debug ? {
                    bbox: true
                } : null
            });
        }
        if (me.rectSprite) {
            me.rectSprite.setAttributes({
                hidden: !debug
            });
        }
        if (!me.isConfiguring) {
            me.surface.renderFrame();
        }
    },
    //</debug>
    updateRect: function(rect) {
        if (this.rectSprite) {
            this.rectSprite.setAttributes({
                x: rect[0],
                y: rect[1],
                width: rect[2],
                height: rect[3]
            });
        }
    },
    updateDocked: function() {
        var chart = this.getChart();
        if (chart && !this.isConfiguring) {
            chart.scheduleLayout();
        }
    },
    /**
     * @private
     * Computes and sets the caption's rect.
     * Shrinks the given chart rect to accomodate the caption.
     * The chart rect is [top, left, width, height] in chart's
     * body element coordinates.
     * The shrink rect is {left, top, right, bottom} in `caption`
     * surface coordinates.
     */
    computeRect: function(chartRect, shrinkRect) {
        if (this.getHidden()) {
            return null;
        }
        // eslint-disable-next-line vars-on-top
        var rect = [
                0,
                0,
                chartRect[2],
                0
            ],
            docked = this.getDocked(),
            padding = this.getPadding(),
            textSize = this.getSprite().getBBox(),
            height = textSize.height + padding * 2;
        switch (docked) {
            case 'top':
                rect[1] = shrinkRect.top;
                rect[3] = height;
                chartRect[1] += height;
                chartRect[3] -= height;
                shrinkRect.top += height;
                break;
            case 'bottom':
                chartRect[3] -= height;
                shrinkRect.bottom -= height;
                rect[1] = shrinkRect.bottom;
                rect[3] = height;
                break;
        }
        this.setRect(rect);
    },
    alignRect: function(seriesRect) {
        var surfaceRect = this.surface.getRect(),
            rect = this.getRect();
        rect[0] = seriesRect[0] - surfaceRect[0];
        rect[2] = seriesRect[2];
        // Slice to trigger the applier/updater.
        this.setRect(rect.slice());
    },
    performLayout: function() {
        var me = this,
            rect = me.getRect(),
            x = rect[0],
            y = rect[1],
            width = rect[2],
            height = rect[3],
            sprite = me.getSprite(),
            tx = sprite.attr.translationX,
            ty = sprite.attr.translationY,
            bbox = sprite.getBBox(),
            align = me.getAlign(),
            dx, dy;
        switch (align) {
            case 'left':
                dx = x - bbox.x;
                break;
            case 'right':
                dx = (x + width) - (bbox.x + bbox.width);
                break;
            case 'center':
                dx = x + (width - bbox.width) / 2 - bbox.x;
                break;
        }
        dy = y + (height - bbox.height) / 2 - bbox.y;
        sprite.setAttributes({
            translationX: tx + dx,
            translationY: ty + dy
        });
    },
    destroy: function() {
        var me = this;
        //<debug>
        if (me.rectSprite) {
            me.rectSprite.destroy();
        }
        //</debug>
        me.getSprite().destroy();
        me.callParent();
    }
});

/**
 * The data model for legend items.
 */
Ext.define('Ext.chart.legend.store.Item', {
    extend: 'Ext.data.Model',
    fields: [
        'id',
        'name',
        // The series title.
        'mark',
        // The color of the series.
        'disabled',
        // The state of the series.
        'series',
        // A reference to the series instance.
        // A sprite index, e.g. for stacked or pie series.
        // For such series an individual component of the series
        // is hidden or shown when the legend item is toggled.
        'index'
    ]
});

/**
 * The store type used for legend items.
 */
Ext.define('Ext.chart.legend.store.Store', {
    extend: 'Ext.data.Store',
    requires: [
        'Ext.chart.legend.store.Item'
    ],
    model: 'Ext.chart.legend.store.Item',
    isLegendStore: true,
    config: {
        autoDestroy: true
    }
});

/**
 * The Ext.chart package provides the capability to visualize data.
 * Each chart binds directly to a {@link Ext.data.Store store} enabling automatic
 * updates of the chart. A chart configuration object has some overall styling
 * options as well as an array of axes and series. A chart instance example could
 * look like this:
 *
 *     Ext.create('Ext.chart.CartesianChart', {
 *         width: 800,
 *         height: 600,
 *         animation: {
 *             easing: 'backOut',
 *             duration: 500
 *         },
 *         store: store1,
 *         legend: {
 *             position: 'right'
 *         },
 *         axes: [
 *             // ...some axes options...
 *         ],
 *         series: [
 *             // ...some series options...
 *         ]
 *     });
 *
 * In this example we set the `width` and `height` of a chart; We decide whether
 * our series are animated or not and we select a store to be bound to the chart;
 * We also set the legend to the right part of the chart.
 *
 * You can register certain interactions such as {@link Ext.chart.interactions.PanZoom}
 * on the chart by specifying an array of names or more specific config objects.
 * All the events will be wired automatically.
 *
 * You can also listen to series `itemXXX` events on both chart and series level.
 *
 * For example:
 *
 *     Ext.create('Ext.chart.CartesianChart', {
 *         plugins: {
 *             chartitemevents: {
 *                 moveEvents: true
 *             }
 *         },
 *         store: {
 *             fields: ['pet', 'households', 'total'],
 *             data: [
 *                 {pet: 'Cats', households: 38, total: 93},
 *                 {pet: 'Dogs', households: 45, total: 79},
 *                 {pet: 'Fish', households: 13, total: 171}
 *             ]
 *         },
 *         axes: [{
 *             type: 'numeric',
 *             position: 'left'
 *         }, {
 *             type: 'category',
 *             position: 'bottom'
 *         }],
 *         series: [{
 *             type: 'bar',
 *             xField: 'pet',
 *             yField: 'households',
 *             listeners: {
 *                 itemmousemove: function (series, item, event) {
 *                     console.log('itemmousemove', item.category, item.field);
 *                 }
 *             }
 *         }, {
 *             type: 'line',
 *             xField: 'pet',
 *             yField: 'total',
 *             marker: true
 *         }],
 *         listeners: { // Listen to itemclick events on all series.
 *             itemclick: function (chart, item, event) {
 *                 console.log('itemclick', item.category, item.field);
 *             }
 *         }
 *     });
 *
 * Important! It's generally a poor design choice to put interactive charts
 * inside scrollable views, in such cases it's not possible to tell
 * which component should respond to the interaction.
 * Since charts are typically interactive their default touch action config
 * looks as follows: {@link Ext.draw.Container#touchAction}.
 * If you do have a chart inside a scrollable view, even if it has no interactions,
 * you have to set its `touchAction` config to the following:
 *
 *     touchAction: {
 *         panX: true,
 *         panY: true
 *     }
 *
 * Otherwise, if a touch action started on a chart, a swipe will not scroll
 * the view.
 *
 * For more information about the axes and series configurations please check
 * the documentation of each series (Line, Bar, Pie, etc).
 *
 */
Ext.define('Ext.chart.AbstractChart', {
    extend: 'Ext.draw.Container',
    requires: [
        'Ext.chart.theme.Default',
        'Ext.chart.series.Series',
        'Ext.chart.interactions.Abstract',
        'Ext.chart.axis.Axis',
        'Ext.chart.Util',
        'Ext.data.StoreManager',
        'Ext.chart.legend.Legend',
        'Ext.chart.legend.SpriteLegend',
        'Ext.chart.Caption',
        'Ext.chart.legend.store.Store',
        'Ext.data.Store'
    ],
    isChart: true,
    defaultBindProperty: 'store',
    /**
     * @event beforerefresh
     * Fires before a refresh to the chart data is called.  If the `beforerefresh`
     * handler returns `false` the {@link #refresh} action will be canceled.
     * @param {Ext.chart.AbstractChart} this
     */
    /**
     * @event refresh
     * Fires after the chart data has been refreshed.
     * @param {Ext.chart.AbstractChart} this
     */
    /**
     * @event redraw
     * Fires after each {@link #event!redraw} call.
     * @param {Ext.chart.AbstractChart} this
     */
    /**
     * @private
     * @event layout
     * Fires after the final layout is done.
     * (Two layouts may be required to fully render a chart.
     * Typically for the initial render and every time thickness
     * of the chart's axes changes.)
     * @param {Ext.chart.AbstractChart} this
     */
    /**
     * @event itemhighlight
     * Fires when an item is highlighted.
     * @param {Ext.chart.AbstractChart} this
     * @param {Object} newItem The new highlight item.
     * @param {Object} oldItem The old highlight item.
     */
    /**
     * @event itemhighlightchange
     * Fires when an item's highlight changes.
     * @param this
     * @param {Object} newItem The new highlight item.
     * @param {Object} oldItem The old highlight item.
     */
    /**
     * @event itemmousemove
     * Fires when the mouse is moved on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseup
     * Fires when a mouseup event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmousedown
     * Fires when a mousedown event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseover
     * Fires when the mouse enters a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseout
     * Fires when the mouse exits a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemclick
     * Fires when a click event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemdblclick
     * Fires when a double click event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtap
     * Fires when a tap event occurs on a series item.
     * *Note*: This event requires the {@link Ext.chart.plugin.ItemEvents chartitemevents}
     * plugin be added to the chart.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event storechange
     * Fires when the store of the chart changes.
     * @param {Ext.chart.AbstractChart} chart
     * @param {Ext.data.Store} newStore
     * @param {Ext.data.Store} oldStore
     */
    config: {
        /**
         * @cfg {Ext.data.Store/String/Object} store
         * The data source to which the chart is bound.
         * Acceptable values for this property are:
         *
         *   - **any {@link Ext.data.Store Store} class / subclass**
         *   - **an {@link Ext.data.Store#storeId ID of a store}**
         *   - **a {@link Ext.data.Store Store} config object**.  When passing a config you can
         *     specify the store type by alias.  Passing a config object with a store type will
         *     dynamically create a new store of that type when the chart is instantiated.
         *
         * For example:
         *
         *     Ext.define('MyApp.store.Customer', {
         *         extend: 'Ext.data.Store',
         *         alias: 'store.customerstore',
         *
         *         fields: ['name', 'value']
         *     });
         *
         *
         *     Ext.create({
         *         xtype: 'cartesian',
         *         renderTo: document.body,
         *         height: 400,
         *         width: 400,
         *         store: {
         *             type: 'customerstore',
         *             data: [{
         *                 name: 'metric one',
         *                 value: 10
         *             }]
         *         },
         *         axes: [{
         *             type: 'numeric',
         *             position: 'left',
         *             title: {
         *                 text: 'Sample Values',
         *                 fontSize: 15
         *             },
         *             fields: 'value'
         *         }, {
         *             type: 'category',
         *             position: 'bottom',
         *             title: {
         *                 text: 'Sample Values',
         *                 fontSize: 15
         *             },
         *             fields: 'name'
         *         }],
         *         series: {
         *             type: 'bar',
         *             xField: 'name',
         *             yField: 'value'
         *         }
         *     });
         */
        store: 'ext-empty-store',
        /**
         * @cfg {String} [theme="default"]
         * The name of the theme to be used. A theme defines the colors and styles
         * used by the series, axes, markers and other chart components.
         * Please see the documentation for the {@link Ext.chart.theme.Base} class
         * for more information.
         *
         * Possible theme values are:
         *   - 'green', 'sky', 'red', 'purple', 'blue', 'yellow'
         *   - 'category1' to 'category6'
         *   - and the above theme names with the '-gradients' suffix, e.g. 'green-gradients'
         *
         * IMPORTANT: You should require the themes you use; for example, to use:
         *
         *     theme: 'blue'
         *
         * the `Ext.chart.theme.Blue` class should be required:
         *
         *     requires: 'Ext.chart.theme.Blue'
         *
         * To require all chart themes:
         *
         *     requires: 'Ext.chart.theme.*'
         */
        theme: 'default',
        /**
         * Chart captions can be used to place titles, subtitles, credits and other captions
         * inside a chart. For example:
         *
         *     captions: {
         *         title: {
         *             text: 'Consumer Price Index'
         *         },
         *         subtitle: {
         *             text: 'from 2007 to 2017'
         *         },
         *         credits: {
         *             text: 'Source: 'bls.gov'
         *         }
         *     }
         *
         * One can use any names for properties in the `captions` config, but the `title`,
         * `subtitle` and `credits` ones have a special meaning - they are automatically
         * themeable. The `title` and `subtitle` are automatically docked to the top of
         * a chart and the `credits` to the bottom. The `title` uses the largest and
         * the heaviest font, while the `credits` - the smallest and the lightest.
         *
         * Other captions besides those three can be easily defined as well:
         *
         *     captions: {
         *         myFancyCaption: {
         *             docked: 'bottom',
         *             align: 'left',
         *             style: {
         *                 fontSize: 18,
         *                 fontWeight: 'bold',
         *                 fontFamily: 'Verdana'
         *             }
         *         }
         *     }
         *
         * If a caption config only specifies text, a shorthand syntax is also possible:
         *
         *     captions: {
         *         title: 'Consumer Price Index'
         *     }
         *
         * @cfg {Object} captions
         * @cfg {Ext.chart.Caption} captions.title
         * @cfg {Ext.chart.Caption} captions.subtitle
         * @cfg {Ext.chart.Caption} captions.credits
         */
        captions: null,
        /**
         * @cfg {Object} style
         * The style for the chart component.
         */
        style: null,
        /**
         * @cfg {Boolean/Object} [animation=true]
         * Defaults to `easeInOut` easing with a 500ms duration.
         * See {@link Ext.draw.modifier.Animation} for possible configuration options.
         */
        animation: !Ext.isIE8,
        /**
         * @cfg {Ext.chart.series.Series/Array} series
         * Array of {@link Ext.chart.series.Series Series} instances or config objects.
         * For example:
         *
         *     series: [{
         *         type: 'column',
         *         axis: 'left',
         *         listeners: {
         *             'afterrender': function() {
         *                 console.log('afterrender');
         *             }
         *         },
         *         xField: 'category',
         *         yField: 'data1'
         *     }]
         */
        series: [],
        /**
         * @cfg {Ext.chart.axis.Axis/Array/Object} axes
         * Array of {@link Ext.chart.axis.Axis Axis} instances or config objects.
         * For example:
         *
         *     axes: [{
         *         type: 'numeric',
         *         position: 'left',
         *         title: 'Number of Hits',
         *         minimum: 0
         *     }, {
         *         type: 'category',
         *         position: 'bottom',
         *         title: 'Month of the Year'
         *     }]
         */
        axes: [],
        /**
         * @cfg {Ext.chart.legend.Legend/Ext.chart.legend.SpriteLegend/Boolean} legend
         * The legend config for the chart. If specified, a legend block will be shown
         * next to the chart.
         * Each legend item displays the {@link Ext.chart.series.Series#title title}
         * of the series, the color of the series and allows to toggle the visibility
         * of the series (at least one series should remain visible).
         *
         * Sencha Charts support two types of legends: sprite based and DOM based.
         *
         * The sprite based legend can be shown in chart {@link Ext.draw.Container#preview preview}
         * and is a part of the downloaded {@link Ext.draw.Container#download chart image}.
         * The sprite based legend is always displayed in full and takes as much space as necessary,
         * the legend items are split into columns to use the available space efficiently.
         * The sprite based legend is styled via a {@link Ext.chart.theme.Base chart theme}.
         *
         * The DOM based legend supports RTL.
         * It occupies a fixed width or height and scrolls when the content overflows.
         * The DOM based legend is styled via CSS rules.
         *
         * By default the sprite legend is used. The type can be explicitly specified:
         *
         *     legend: {
         *         type: 'dom',  // 'sprite' is another possible value
         *         docked: 'top'
         *     }
         *
         * If the legend config is set to `true`, the sprite legend will be used
         * docked to the bottom.
         */
        legend: null,
        /**
         * @cfg {Array} colors
         * Array of colors/gradients to override the color of items and legends.
         */
        colors: null,
        /**
         * @cfg {Object/Number/String} insetPadding
         * The amount of inset padding in pixels for the chart.
         * Inset padding is the padding from the boundary of the chart to any
         * of its contents.
         */
        insetPadding: {
            top: 10,
            left: 10,
            right: 10,
            bottom: 10
        },
        /**
         * @cfg {Object} background Set the chart background.
         * This can be a gradient object, image, or color.
         *
         * For example, if `background` were to be a color we could set the object as
         *
         *     background: '#ccc'
         *
         * You can specify an image by using:
         *
         *     background: {
         *         type: 'image',
         *         src: 'http://path.to.image/'
         *     }
         *
         * Also you can specify a gradient by using the gradient object syntax:
         *
         *     background: {
         *         type: 'linear',
         *         degrees: 0,
         *         stops: [
         *             {
         *                 offset: 0,
         *                 color: 'white'
         *             },
         *             {
         *                 offset: 1,
         *                 color: 'blue'
         *             }
         *         ]
         *     }
         */
        background: null,
        /**
         * @cfg {Array} interactions
         * Interactions are optional modules that can be plugged in to a chart
         * to allow the user to interact with the chart and its data in special ways.
         * The `interactions` config takes an Array of Object configurations,
         * each one corresponding to a particular interaction class identified
         * by a `type` property:
         *
         *     new Ext.chart.AbstractChart({
         *         renderTo: Ext.getBody(),
         *         width: 800,
         *         height: 600,
         *         store: store1,
         *         axes: [
         *             // ...some axes options...
         *         ],
         *         series: [
         *             // ...some series options...
         *         ],
         *         interactions: [{
         *             type: 'interactiontype'
         *             // ...additional configs for the interaction...
         *         }]
         *     });
         *
         * When adding an interaction which uses only its default configuration
         * (no extra properties other than `type`), you can alternately specify
         * only the type as a String rather than the full Object:
         *
         *     interactions: ['reset', 'rotate']
         *
         * The current supported interaction types include:
         *
         * - {@link Ext.chart.interactions.PanZoom panzoom} - allows pan and zoom of axes
         * - {@link Ext.chart.interactions.ItemHighlight itemhighlight} - allows highlighting
         * of series data points
         * - {@link Ext.chart.interactions.ItemInfo iteminfo} - allows displaying details of
         * a data point in a popup panel
         * - {@link Ext.chart.interactions.Rotate rotate} - allows rotation of pie and radar series
         *
         * See the documentation for each of those interaction classes to see how they
         * can be configured.
         *
         * Additional custom interactions can be registered using `'interactions.'` alias prefix.
         */
        interactions: [],
        /**
         * @private
         * The main area of the chart where grid and series are drawn.
         */
        mainRect: null,
        /**
         * @private
         * Override value.
         */
        resizeHandler: null,
        /**
         * @cfg {Object} highlightItem
         * The current highlight item in the chart.
         * The object must be the one that you get from item events.
         *
         * Note that series can also own highlight items.
         * This notion is separate from this one and should not be used at the same time.
         */
        highlightItem: null,
        /* eslint-disable indent */
        surfaceZIndexes: {
            background: 0,
            // Contains the backround 'rect' sprite.
            main: 1,
            // Contains grid lines and CrossZoom overlay 'rect' sprite.
            grid: 2,
            // Reserved.
            series: 3,
            // Contains series sprites.
            axis: 4,
            // No actual `axis` surface is created, but this zIndex is used
            // for all axis surfaces (one surface is created per axis).
            chart: 5,
            // Covers whole chart, minus the legend area.
            // Contains sprites defined in the `sprites` config,
            // title, subtitle and credits.
            caption: 6,
            // Contains title, subtitle and credits sprites.
            overlay: 7,
            // This surface will typically contain chart labels
            // and interaction sprites like crosshair lines.
            // With cartesian charts, equivalent in size to the `series` surface.
            // With polar charts, equivalent in size to the `chart` surface.
            legend: 8
        }
    },
    // `SpriteLegend` surface.
    /* eslint-enable indent */
    /**
     * @private
     */
    legendStore: null,
    /**
     * When this is non-zero, changes to sprite attributes apply instantly.
     * See {@link #getAnimation}.
     * @private
     */
    animationSuspendCount: 0,
    /**
     * @private
     */
    chartLayoutSuspendCount: 0,
    /**
     * @private
     */
    chartLayoutCount: 0,
    /**
     * @private
     */
    scheduledLayoutId: null,
    /**
     * @private
     */
    axisThicknessSuspendCount: 0,
    /**
     * @private
     * Indicates that thickness of one or more axes has changed,
     * at the time of {@link #performLayout} call. I.e. 'performLayout'
     * should be called again when current layout is done.
     */
    isThicknessChanged: false,
    constructor: function(config) {
        var me = this;
        me.itemListeners = {};
        me.surfaceMap = {};
        me.chartComponents = {};
        me.isInitializing = true;
        me.suspendChartLayout();
        me.animationSuspendCount++;
        me.callParent(arguments);
        me.isInitializing = false;
        me.getSurface('main');
        me.getSurface('chart').setFlipRtlText(me.getInherited().rtl);
        me.getSurface('overlay').waitFor(me.getSurface('series'));
        me.animationSuspendCount--;
        me.resumeChartLayout();
    },
    applyAnimation: function(animation, oldAnimation) {
        return Ext.chart.Util.applyAnimation(animation, oldAnimation);
    },
    updateAnimation: function() {
        if (this.isConfiguring) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var seriesList = this.getSeries(),
            ln = seriesList.length,
            i, series;
        this.isSettingSeriesAnimation = true;
        for (i = 0; i < ln; i++) {
            series = seriesList[i];
            // Don't update the series animation config, if it was set by
            // a user, unless 'suspendAnimation' was called.
            if (!series.isUserAnimation || this.animationSuspendCount) {
                series.setAnimation(series.getAnimation());
            }
        }
        this.isSettingSeriesAnimation = false;
    },
    getAnimation: function() {
        var result;
        if (this.animationSuspendCount) {
            result = {
                duration: 0
            };
        } else {
            result = this.callParent();
        }
        return result;
    },
    suspendAnimation: function() {
        this.animationSuspendCount++;
        if (this.animationSuspendCount === 1) {
            this.updateAnimation();
        }
    },
    resumeAnimation: function() {
        this.animationSuspendCount--;
        if (this.animationSuspendCount === 0) {
            this.updateAnimation();
        }
    },
    applyInsetPadding: function(padding, oldPadding) {
        var result;
        if (!Ext.isObject(padding)) {
            result = Ext.util.Format.parseBox(padding);
        } else if (!oldPadding) {
            result = padding;
        } else {
            result = Ext.apply(oldPadding, padding);
        }
        return result;
    },
    /**
     * Suspends chart's layout.
     */
    suspendChartLayout: function() {
        var me = this;
        me.chartLayoutSuspendCount++;
        if (me.chartLayoutSuspendCount === 1) {
            if (me.scheduledLayoutId) {
                me.layoutInSuspension = true;
                me.cancelChartLayout();
            } else {
                me.layoutInSuspension = false;
            }
        }
    },
    /**
     * Decrements chart's layout suspend count.
     * When the suspend count is decremented to zero,
     * a layout is scheduled.
     */
    resumeChartLayout: function() {
        var me = this;
        me.chartLayoutSuspendCount--;
        if (me.chartLayoutSuspendCount === 0) {
            if (me.layoutInSuspension) {
                me.scheduleLayout();
            }
        }
    },
    /**
     * Cancel a scheduled layout.
     */
    cancelChartLayout: function() {
        if (this.scheduledLayoutId) {
            Ext.draw.Animator.cancel(this.scheduledLayoutId);
            this.scheduledLayoutId = null;
            this.checkLayoutEnd();
        }
    },
    /**
     * Schedule a layout at next frame.
     */
    scheduleLayout: function() {
        var me = this;
        if (me.allowSchedule() && !me.scheduledLayoutId) {
            me.scheduledLayoutId = Ext.draw.Animator.schedule('doScheduleLayout', me);
        }
    },
    allowSchedule: function() {
        return true;
    },
    doScheduleLayout: function() {
        var me = this;
        me.scheduledLayoutId = null;
        if (me.chartLayoutSuspendCount) {
            me.layoutInSuspension = true;
        } else {
            me.performLayout();
        }
    },
    /**
     * Prevent axes from triggering chart layout when their thickness changes.
     * E.g. during an interaction that makes changes to the axes,
     * or when chart layout was triggered by something else,
     * for example a chart resize event.
     */
    suspendThicknessChanged: function() {
        this.axisThicknessSuspendCount++;
    },
    /**
     * Decrements axis thickness suspend count.
     * When axis thickness suspend count is decremented to zero,
     * chart layout is performed.
    */
    resumeThicknessChanged: function() {
        if (this.axisThicknessSuspendCount > 0) {
            this.axisThicknessSuspendCount--;
            if (this.axisThicknessSuspendCount === 0 && this.isThicknessChanged) {
                this.onThicknessChanged();
            }
        }
    },
    onThicknessChanged: function() {
        if (this.axisThicknessSuspendCount === 0) {
            this.isThicknessChanged = false;
            this.performLayout();
        } else {
            this.isThicknessChanged = true;
        }
    },
    applySprites: function(sprites) {
        var surface = this.getSurface('chart');
        sprites = Ext.Array.from(sprites);
        surface.removeAll(true);
        surface.add(sprites);
        return sprites;
    },
    initItems: function() {
        var items = this.items,
            i, ln, item;
        if (items && !items.isMixedCollection) {
            this.items = [];
            items = Ext.Array.from(items);
            for (i = 0 , ln = items.length; i < ln; i++) {
                item = items[i];
                if (item.type) {
                    Ext.raise("To add custom sprites to the chart use the 'sprites' config.");
                } else {
                    this.items.push(item);
                }
            }
        }
        // @noOptimize.callParent
        this.callParent();
    },
    // noOptimize is needed because in the ext build we have a parent method to call,
    // but in touch we do not so we need to suppress the cmd warning during optimized build
    applyBackground: function(newBackground, oldBackground) {
        var surface = this.getSurface('background');
        return this.refreshBackground(surface, newBackground, oldBackground);
    },
    /**
     * @private
     * The background updater. Used by both the chart and the sprite legend.
     * @param surface The surface to put the background in.
     * @param newBackground
     * @param oldBackground
     * @return {Ext.draw.sprite.Rect/Ext.draw.sprite.Sprite}
     */
    refreshBackground: function(surface, newBackground, oldBackground) {
        var width, height, isUpdateOld;
        if (newBackground) {
            if (oldBackground) {
                width = oldBackground.attr.width;
                height = oldBackground.attr.height;
                isUpdateOld = oldBackground.type === (newBackground.type || 'rect');
            }
            if (newBackground.isSprite) {
                oldBackground = newBackground;
            } else if (newBackground.type === 'image' && Ext.isString(newBackground.src)) {
                if (isUpdateOld) {
                    oldBackground.setAttributes({
                        src: newBackground.src
                    });
                } else {
                    surface.remove(oldBackground, true);
                    oldBackground = surface.add(newBackground);
                }
            } else {
                if (isUpdateOld) {
                    oldBackground.setAttributes({
                        fillStyle: newBackground
                    });
                } else {
                    surface.remove(oldBackground, true);
                    oldBackground = surface.add({
                        type: 'rect',
                        fillStyle: newBackground,
                        animation: {
                            customDurations: {
                                x: 0,
                                y: 0,
                                width: 0,
                                height: 0
                            }
                        }
                    });
                }
            }
        }
        if (width && height) {
            oldBackground.setAttributes({
                width: width,
                height: height
            });
        }
        oldBackground.setAnimation(this.getAnimation());
        return oldBackground;
    },
    defaultResizeHandler: function(size) {
        this.scheduleLayout();
        return false;
    },
    applyMainRect: function(newRect, rect) {
        if (!rect) {
            return newRect;
        }
        this.getSeries();
        this.getAxes();
        if (newRect[0] === rect[0] && newRect[1] === rect[1] && newRect[2] === rect[2] && newRect[3] === rect[3]) {
            return rect;
        } else {
            return newRect;
        }
    },
    register: function(component) {
        var map = this.chartComponents,
            id = component.getId();
        //<debug>
        if (id === undefined) {
            Ext.raise('Chart component id is undefined. ' + 'Please ensure the component has an id.');
        }
        if (id in map) {
            Ext.raise('Registering duplicate chart component id "' + id + '"');
        }
        //</debug>
        map[id] = component;
    },
    unregister: function(component) {
        var map = this.chartComponents,
            id = component.getId();
        delete map[id];
    },
    get: function(id) {
        return this.chartComponents[id];
    },
    /**
     * @method getAxis Returns an axis instance based on the type of data passed.
     * @param {String/Number/Ext.chart.axis.Axis} axis You may request an axis by passing
     * an id, the number of the array key returned by {@link #getAxes}, or an axis instance.
     * @return {Ext.chart.axis.Axis} The axis requested.
     */
    getAxis: function(axis) {
        if (axis instanceof Ext.chart.axis.Axis) {
            return axis;
        } else if (Ext.isNumber(axis)) {
            return this.getAxes()[axis];
        } else if (Ext.isString(axis)) {
            return this.get(axis);
        }
    },
    getSurface: function(id, type) {
        var me = this,
            map = me.surfaceMap,
            surface;
        id = id || 'main';
        type = type || id;
        surface = this.callParent([
            id,
            type
        ]);
        if (!map[type]) {
            map[type] = [];
        }
        if (Ext.Array.indexOf(map[type], surface) < 0) {
            surface.type = type;
            map[type].push(surface);
            surface.on('destroy', me.forgetSurface, me);
        }
        return surface;
    },
    forgetSurface: function(surface) {
        var map = this.surfaceMap,
            group, index;
        if (!map || this.destroying) {
            return;
        }
        group = map[surface.type];
        index = group ? Ext.Array.indexOf(group, surface) : -1;
        if (index >= 0) {
            group.splice(index, 1);
        }
    },
    applyAxes: function(newAxes, oldAxes) {
        var me = this,
            positions = {
                left: 'right',
                right: 'left'
            },
            result = [],
            axis, oldAxis, linkedTo, id, oldMap, series, i, j, ln;
        me.animationSuspendCount++;
        me.getStore();
        if (!oldAxes) {
            oldAxes = [];
            oldAxes.map = {};
        }
        oldMap = oldAxes.map;
        result.map = {};
        newAxes = Ext.Array.from(newAxes, true);
        for (i = 0 , ln = newAxes.length; i < ln; i++) {
            axis = newAxes[i];
            if (!axis) {
                
                continue;
            }
            if (axis instanceof Ext.chart.axis.Axis) {
                oldAxis = oldMap[axis.getId()];
                axis.setChart(me);
            } else {
                axis = Ext.Object.chain(axis);
                linkedTo = axis.linkedTo;
                id = axis.id;
                if (Ext.isNumber(linkedTo)) {
                    axis = Ext.merge({}, newAxes[linkedTo], axis);
                } else if (Ext.isString(linkedTo)) {
                    Ext.Array.each(newAxes, function(item) {
                        if (item.id === axis.linkedTo) {
                            axis = Ext.merge({}, item, axis);
                            return false;
                        }
                    });
                }
                axis.id = id;
                axis.chart = me;
                if (me.getInherited().rtl) {
                    axis.position = positions[axis.position] || axis.position;
                }
                id = axis.getId && axis.getId() || axis.id;
                axis = Ext.factory(axis, null, oldAxis = oldMap[id], 'axis');
            }
            if (axis) {
                result.push(axis);
                result.map[axis.getId()] = axis;
            }
        }
        me.axesChangeSeries = {};
        for (i in oldMap) {
            if (!result.map[i]) {
                oldAxis = oldMap[i];
                if (oldAxis && !oldAxis.destroyed) {
                    // At this point the series still have their `xAxis` and `yAxis` configs
                    // set to old axes. We need to update such series with new matching axes
                    // by calling their `onAxesChange` method.
                    for (j = 0 , ln = oldAxis.boundSeries.length; j < ln; j++) {
                        series = oldAxis.boundSeries[j];
                        me.axesChangeSeries[series.getId()] = series;
                    }
                    oldAxis.destroy();
                }
            }
        }
        me.animationSuspendCount--;
        return result;
    },
    updateAxes: function(axes) {
        var me = this,
            seriesMap = me.axesChangeSeries,
            series, id, i, ln, axis;
        for (id in seriesMap) {
            series = seriesMap[id];
            // `true` to force set series' axes, even if they are already set
            // (in this case to old axes that were just destroyed in the `axes` applier).
            series.onAxesChange(me, true);
        }
        // If changes to the `axes` config are made post chart creation, without making any
        // changes to the series afterwards, we need to figure out the new axes' `boundSeries`
        // manually, as the 'serieschange' event won't be fired in this case.
        for (i = 0 , ln = axes.length; i < ln; i++) {
            axis = axes[i];
            axis.onSeriesChange(me);
        }
        if (!me.isConfiguring && !me.destroying) {
            me.scheduleLayout();
        }
    },
    circularCopyArray: function(inArray, startIndex, count) {
        var outArray = [],
            i,
            len = inArray && inArray.length;
        if (len) {
            for (i = 0; i < count; i++) {
                outArray.push(inArray[(startIndex + i) % len]);
            }
        }
        return outArray;
    },
    circularCopyObject: function(inObject, startIndex, count) {
        var me = this,
            name, value,
            outObject = {};
        if (count) {
            for (name in inObject) {
                if (inObject.hasOwnProperty(name)) {
                    value = inObject[name];
                    if (Ext.isArray(value)) {
                        outObject[name] = me.circularCopyArray(value, startIndex, count);
                    } else {
                        outObject[name] = value;
                    }
                }
            }
        }
        return outObject;
    },
    getColors: function() {
        var me = this,
            configColors = me.config.colors,
            theme = me.getTheme();
        if (Ext.isArray(configColors) && configColors.length > 0) {
            configColors = me.applyColors(configColors);
        }
        return configColors || (theme && theme.getColors());
    },
    applyColors: function(newColors) {
        newColors = Ext.Array.map(newColors, function(color) {
            if (Ext.isString(color)) {
                return color;
            } else {
                return color.toString();
            }
        });
        return newColors;
    },
    updateColors: function(newColors) {
        var me = this,
            theme = me.getTheme(),
            colors = newColors || (theme && theme.getColors()),
            colorIndex = 0,
            series = me.getSeries(),
            seriesCount = series && series.length,
            i, seriesItem, seriesColors, seriesColorCount;
        if (colors.length) {
            for (i = 0; i < seriesCount; i++) {
                seriesItem = series[i];
                seriesColorCount = seriesItem.themeColorCount();
                seriesColors = me.circularCopyArray(colors, colorIndex, seriesColorCount);
                colorIndex += seriesColorCount;
                seriesItem.updateChartColors(seriesColors);
            }
        }
        if (!me.isConfiguring) {
            me.refreshLegendStore();
        }
    },
    applyTheme: function(theme) {
        if (theme && theme.isTheme) {
            return theme;
        }
        return Ext.Factory.chartTheme(theme);
    },
    updateGradients: function(gradients) {
        if (!Ext.isEmpty(gradients)) {
            this.updateTheme(this.getTheme());
        }
    },
    updateTheme: function(theme, oldTheme) {
        var me = this,
            axes = me.getAxes(),
            series = me.getSeries(),
            colors = me.getColors(),
            i;
        if (!series) {
            return;
        }
        me.updateChartTheme(theme);
        for (i = 0; i < axes.length; i++) {
            axes[i].updateTheme(theme);
        }
        for (i = 0; i < series.length; i++) {
            series[i].setTheme(theme);
        }
        me.updateSpriteTheme(theme);
        me.updateColors(colors);
        // It may be necessary to perform a layout here.
        // But instead of the 'chart.scheduleLayout' call, we can call
        // 'chart.redraw'. If after the redraw call the thickness
        // of any axis changes, this will automatically trigger
        // chart layout (see Ext.chart.axis.sprite.Axis.doThicknessChanged).
        // Otherwise, no layout is necessary.
        me.redraw();
        me.fireEvent('themechange', me, theme, oldTheme);
    },
    themeOnlyIfConfigured: {
        captions: true
    },
    updateChartTheme: function(theme) {
        var me = this,
            chartTheme = theme.getChart(),
            initialConfig = me.getInitialConfig(),
            defaultConfig = me.defaultConfig,
            configs = me.self.getConfigurator().configs,
            genericChartTheme = chartTheme.defaults,
            specificChartTheme = chartTheme[me.xtype],
            themeOnlyIfConfigured = me.themeOnlyIfConfigured,
            key, value, isObjValue, isUnusedConfig, initialValue, cfg;
        chartTheme = Ext.merge({}, genericChartTheme, specificChartTheme);
        for (key in chartTheme) {
            value = chartTheme[key];
            cfg = configs[key];
            if (value !== null && value !== undefined && cfg) {
                initialValue = initialConfig[key];
                isObjValue = Ext.isObject(value);
                isUnusedConfig = initialValue === defaultConfig[key];
                if (isObjValue) {
                    if (isUnusedConfig && themeOnlyIfConfigured[key]) {
                        
                        continue;
                    }
                    value = Ext.merge({}, value, initialValue);
                }
                if (isUnusedConfig || isObjValue) {
                    me[cfg.names.set](value);
                }
            }
        }
    },
    updateSpriteTheme: function(theme) {
        var me = this,
            chartSurface, sprites, styles, sprite, style, key, attr, isText, i, ln;
        me.getSprites();
        chartSurface = me.getSurface('chart');
        sprites = chartSurface.getItems();
        styles = theme.getSprites();
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            sprite = sprites[i];
            style = styles[sprite.type];
            if (style) {
                attr = {};
                isText = sprite.type === 'text';
                for (key in style) {
                    if (!(key in sprite.config)) {
                        // Setting individual font attributes will take over the 'font' shorthand
                        // attribute, but this behavior is undesireable for theming.
                        if (!(isText && key.indexOf('font') === 0 && sprite.config.font)) {
                            attr[key] = style[key];
                        }
                    }
                }
                sprite.setAttributes(attr);
            }
        }
    },
    /**
     * Adds a {@link Ext.chart.series.Series Series} to this chart.
     *
     * The Series (or array) passed will be added to the existing series. If an `id` is specified
     * in a new Series, any existing Series of that `id` will be updated.
     *
     * The chart will be redrawn in response to the change.
     *
     * @param {Object/Object[]/Ext.chart.series.Series/Ext.chart.series.Series[]} newSeries A
     * config object describing the Series to add, or an instantiated Series object. Or an array
     * of these.
     */
    addSeries: function(newSeries) {
        var series = this.getSeries();
        series = series.concat(Ext.Array.from(newSeries));
        this.setSeries(series);
    },
    /**
     * Remove a {@link Ext.chart.series.Series Series} from this chart.
     * The Series (or array) passed will be removed from the existing series.
     *
     * The chart will be redrawn in response to the change.
     *
     * @param {Ext.chart.series.Series/String} series The Series or the `id` of the Series
     * to remove. May be an array.
     */
    removeSeries: function(series) {
        var existingSeries = this.getSeries(),
            newSeries = [],
            removeMap = {},
            i, len, s;
        series = Ext.Array.from(series);
        // Build a map of the Series IDs that are to be removed
        for (i = 0 , len = series.length; i < len; i++) {
            s = series[i];
            // If they passed a Series Object
            if (typeof s !== 'string') {
                s = s.getId();
            }
            removeMap[s] = true;
        }
        // Build a new Series array that excludes those Series scheduled for removal
        for (i = 0 , len = existingSeries.length; i < len; i++) {
            if (!removeMap[existingSeries[i].getId()]) {
                newSeries.push(existingSeries[i]);
            }
        }
        this.setSeries(newSeries);
    },
    applySeries: function(newSeries, oldSeries) {
        var me = this,
            result = [],
            oldMap, oldSeriesItem, i, ln, series;
        me.animationSuspendCount++;
        me.getAxes();
        if (oldSeries) {
            oldMap = oldSeries.map;
        } else {
            oldSeries = [];
            oldMap = oldSeries.map = {};
        }
        result.map = {};
        newSeries = Ext.Array.from(newSeries, true);
        for (i = 0 , ln = newSeries.length; i < ln; i++) {
            series = newSeries[i];
            if (!series) {
                
                continue;
            }
            oldSeriesItem = oldMap[series.getId && series.getId() || series.id];
            // New Series instance passed in
            if (series instanceof Ext.chart.series.Series) {
                // Replacing
                if (oldSeriesItem && oldSeriesItem !== series) {
                    oldSeriesItem.destroy();
                }
                series.setChart(me);
            }
            // Series config object passed in
            else if (Ext.isObject(series)) {
                // Config object matched an existing Series item by id;
                // update its configuration
                if (oldSeriesItem) {
                    oldSeriesItem.setConfig(series);
                    series = oldSeriesItem;
                } else // Create a new Series
                {
                    if (Ext.isString(series)) {
                        series = {
                            type: series
                        };
                    }
                    series.chart = me;
                    series = Ext.create(series.xclass || ('series.' + series.type), series);
                }
            }
            result.push(series);
            result.map[series.getId()] = series;
        }
        for (i in oldMap) {
            if (!result.map[oldMap[i].id]) {
                oldMap[i].destroy();
            }
        }
        me.animationSuspendCount--;
        return result;
    },
    updateSeries: function(newSeries, oldSeries) {
        var me = this;
        if (me.destroying) {
            return;
        }
        me.animationSuspendCount++;
        me.fireEvent('serieschange', me, newSeries, oldSeries);
        if (!Ext.isEmpty(newSeries)) {
            me.updateTheme(me.getTheme());
        }
        me.refreshLegendStore();
        if (!me.isConfiguring && !me.destroying) {
            me.scheduleLayout();
        }
        me.animationSuspendCount--;
    },
    defaultLegendType: 'sprite',
    applyLegend: function(legend, oldLegend) {
        var me = this,
            result = null,
            alias;
        if (oldLegend && !(oldLegend.destroyed || oldLegend.destroying)) {
            if (me.legendStoreListeners) {
                me.legendStoreListeners.destroy();
            }
            if (me.legendStore) {
                me.legendStore.destroy();
            }
            oldLegend.destroy();
        }
        if (legend) {
            if (Ext.isBoolean(legend)) {
                result = Ext.create('legend.' + me.defaultLegendType, {
                    docked: 'bottom',
                    chart: me
                });
            } else {
                legend.docked = legend.docked || 'bottom';
                legend.chart = me;
                alias = 'legend.' + (legend.type || me.defaultLegendType);
                result = Ext.create(alias, legend);
            }
        }
        return result;
    },
    updateLegend: function(legend) {
        var me = this;
        // Probably has been already destroyed with the old legend,
        // but making sure.
        me.destroyLegendStore();
        if (legend) {
            me.getItems();
            legend.setStore(me.refreshLegendStore());
        }
        if (!me.isConfiguring) {
            me.scheduleLayout();
        }
    },
    captionApplier: function(caption, oldCaption) {
        var me = this,
            result;
        if (oldCaption && !(oldCaption.destroyed || oldCaption.destroying)) {
            oldCaption.destroy();
        }
        if (caption) {
            caption.chart = me;
            result = new Ext.chart.Caption(caption);
        }
        return result;
    },
    applyCaptions: function(captions, oldCaptions) {
        var map = {},
            caption, oldCaption, name, any;
        for (name in captions) {
            caption = captions[name];
            if (caption && !caption.length && !(caption.text && caption.text.length)) {
                caption = null;
            } else if (typeof caption === 'string') {
                caption = {
                    text: caption
                };
                // Initial config is used for proper theming (see `updateChartTheme`)
                // and config merging, however, mergin won't work as expected, if
                // the initial config value remains a string, so we modify it here.
                this.getInitialConfig().captions[name] = caption;
            }
            oldCaption = oldCaptions && oldCaptions[name];
            caption = this.captionApplier(caption, oldCaption);
            if (caption) {
                any = true;
                map[name] = caption;
            }
        }
        return any && map;
    },
    updateCaptions: function() {
        var me = this;
        if (!me.isConfiguring) {
            me.scheduleLayout();
        }
    },
    /**
     * Return the legend store that contains all the legend information.
     * This information is collected from all the series.
     * @return {Ext.chart.legend.store.Store}
     */
    getLegendStore: function() {
        var me = this,
            store = me.legendStore;
        if (!store) {
            store = me.legendStore = new Ext.chart.legend.store.Store({
                chart: me
            });
            me.legendStoreListeners = store.on({
                scope: me,
                update: 'onLegendStoreUpdate',
                destroyable: true
            });
        }
        return store;
    },
    destroyLegendStore: function() {
        var store = this.legendStore;
        if (store && !(store.destroyed || store.destroying)) {
            store.destroy();
        }
        this.legendStore = null;
    },
    refreshLegendStore: function() {
        var me = this,
            legendStore = me.getLegendStore(),
            series, seriesList, legendData, i, ln;
        if (legendStore) {
            seriesList = me.getSeries();
            legendData = [];
            for (i = 0 , ln = seriesList.length; i < ln; i++) {
                series = seriesList[i];
                if (series.getShowInLegend()) {
                    series.provideLegendInfo(legendData);
                }
            }
            legendStore.setData(legendData);
        }
        return legendStore;
    },
    onLegendStoreUpdate: function(store, record) {
        var me = this,
            series;
        if (record) {
            series = this.getSeries().map[record.get('series')];
            if (series) {
                series.setHiddenByIndex(record.get('index'), record.get('disabled'));
                me.redraw();
            }
        }
    },
    applyInteractions: function(interactions, oldInteractions) {
        var me = this,
            result = [],
            oldMap, interaction, i, ln;
        interactions = Ext.Array.from(interactions, true);
        if (!oldInteractions) {
            oldInteractions = [];
            oldInteractions.map = {};
        }
        oldMap = oldInteractions.map;
        result.map = {};
        for (i = 0 , ln = interactions.length; i < ln; i++) {
            interaction = interactions[i];
            if (!interaction) {
                
                continue;
            }
            // eslint-disable-next-line max-len
            interaction = Ext.factory(interaction, null, oldMap[interaction.getId && interaction.getId() || interaction.id], 'interaction');
            if (interaction) {
                interaction.setChart(me);
                result.push(interaction);
                result.map[interaction.getId()] = interaction;
            }
        }
        for (i in oldMap) {
            if (!result.map[i]) {
                oldMap[i].destroy();
            }
        }
        return result;
    },
    /**
     * Get an interaction by type.
     * @param {String} type The type of the interaction.
     * @return {Ext.chart.interactions.Abstract} The interaction. `null`
     * if not found.
     */
    getInteraction: function(type) {
        var interactions = this.getInteractions(),
            len = interactions && interactions.length,
            out = null,
            interaction, i;
        if (len) {
            for (i = 0; i < len; ++i) {
                interaction = interactions[i];
                if (interaction.type === type) {
                    out = interaction;
                    break;
                }
            }
        }
        return out;
    },
    applyStore: function(store) {
        return store && Ext.StoreManager.lookup(store);
    },
    updateStore: function(newStore, oldStore) {
        var me = this;
        if (oldStore && !oldStore.destroyed) {
            oldStore.un({
                datachanged: 'onDataChanged',
                update: 'onDataChanged',
                scope: me,
                order: 'after'
            });
            if (oldStore.autoDestroy) {
                oldStore.destroy();
            }
        }
        if (newStore) {
            newStore.on({
                datachanged: 'onDataChanged',
                update: 'onDataChanged',
                scope: me,
                order: 'after'
            });
        }
        me.fireEvent('storechange', me, newStore, oldStore);
        me.onDataChanged();
    },
    /**
     * Redraw the chart. If animations are set this will animate the chart too.
     * Note: the actual redraw is performed in a subclass.
     */
    redraw: function() {
        this.fireEvent('redraw', this);
    },
    /**
     * @private
     * Lays out chart components and triggers a {@link #event!redraw}.
     * Note: the actual layout is performed in a subclass.
     * A subclass should not perform a layout, if this parent method
     * returns `false`.
     * @return {Boolean}
     */
    performLayout: function() {
        if (this.destroying || this.destroyed) {
            //<debug>
            Ext.raise('Attempting to lay out a dead chart: ' + this.getId());
            //</debug>
            return false;
        }
        // Cancel subclass layout.
        // eslint-disable-next-line vars-on-top
        var me = this,
            legend = me.getLegend(),
            chartRect = me.getChartRect(true),
            background = me.getBackground(),
            result = true,
            legendRect;
        me.cancelChartLayout();
        //<debug>
        // Unlike the 'layout' event that is called after all chart layouts are done
        // and none are pending, this event fires before the start of each layout.
        me.fireEvent('beforelayout', me);
        //</debug>
        if (background) {
            me.getSurface('background').setRect(chartRect.slice());
            background.setAttributes({
                width: chartRect[2],
                height: chartRect[3]
            });
        }
        // The top docked legend is a special case and should be laid out after captions.
        if (legend && legend.isSpriteLegend && !legend.isTop) {
            legendRect = legend.computeRect(chartRect);
        }
        me.layoutCaptions(chartRect);
        if (legend && legend.isSpriteLegend && legend.isTop) {
            legendRect = legend.computeRect(chartRect);
        }
        if (legendRect) {
            me.getSurface('legend').setRect(legendRect);
            result = legend.performLayout();
        }
        me.getSurface('chart').setRect(chartRect);
        if (result) {
            me.hasFirstLayout = true;
        }
        return result;
    },
    layoutCaptions: function(chartRect) {
        var captions = this.getCaptions(),
            shrinkRect = {
                left: 0,
                top: 0,
                right: chartRect[2],
                bottom: chartRect[3]
            },
            caption, captionName, captionList, i, ln;
        if (captions) {
            captionList = [];
            for (captionName in captions) {
                captionList.push(captions[captionName]);
            }
            captionList.sort(function(a, b) {
                return a.getWeight() - b.getWeight();
            });
            for (i = 0 , ln = captionList.length; i < ln; i++) {
                caption = captionList[i];
                if (!i) {
                    this.getSurface(caption.surfaceName).setRect(chartRect.slice());
                }
                caption.computeRect(chartRect, shrinkRect);
            }
            this.captionList = captionList;
        }
    },
    /**
     * @private
     */
    checkLayoutEnd: function() {
        //        not running                not pending
        if (!this.chartLayoutCount && !this.scheduledLayoutId) {
            this.onLayoutEnd();
        }
    },
    /**
     * @private
     */
    onLayoutEnd: function() {
        var me = this;
        me.fireEvent('layout', me);
    },
    /**
     * @private
     * The area of the chart minus the legend, title, subtitle and credits.
     * Cache chart rect as element.getSize() results in
     * a relatively expensive call to the getComputedStyle().
     */
    getChartRect: function(isRecompute) {
        var me = this,
            chartRect, bodySize;
        if (isRecompute) {
            me.chartRect = null;
        }
        if (me.chartRect) {
            chartRect = me.chartRect;
        } else {
            bodySize = me.bodyElement.getSize();
            chartRect = me.chartRect = [
                0,
                0,
                bodySize.width,
                bodySize.height
            ];
        }
        return chartRect;
    },
    /**
     * @private
     * Converts page coordinates into chart's 'series' surface coordinates.
     */
    getEventXY: function(e) {
        return this.getSurface('series').getEventXY(e);
    },
    /**
     * Given an x/y point relative to the chart, find and return the first series item that
     * matches that point.
     * @param {Number} x
     * @param {Number} y
     * @return {Object} An object with `series` and `item` properties, or `false` if no item found.
     */
    getItemForPoint: function(x, y) {
        var me = this,
            seriesList = me.getSeries(),
            rect = me.getMainRect(),
            ln = seriesList.length,
            minDistance = Infinity,
            result = null,
            i, item;
        // The x,y here are already converted to the 'main' surface coordinates.
        // Series surface rect matches the main surface rect.
        if (!(me.hasFirstLayout && rect && x >= 0 && x <= rect[2] && y >= 0 && y <= rect[3])) {
            return null;
        }
        // Iterate in reverse order so that the series that render later (on top)
        // get hit tested first.
        for (i = ln - 1; i >= 0; i--) {
            item = seriesList[i].getItemForPoint(x, y);
            if (item) {
                // Imagine a chart with multiple series, e.g. 'line', 'scatter' and 'bar'.
                // For 'line' and 'scatter' series, the method will look for the nearest
                // marker, but for 'bar' series, it will look for the first bar that
                // contains the given point. For such series, the 'distance' information
                // is absent and meaningless.
                if (!item.distance) {
                    result = item;
                    break;
                }
                if (item.distance < minDistance) {
                    minDistance = item.distance;
                    result = item;
                }
            }
        }
        return result;
    },
    /**
     * @private
     * Given an x/y point relative to the chart, find and return all series items that match
     * that point.
     * @param {Number} x
     * @param {Number} y
     * @return {Array} An array of objects with `series` and `item` properties.
     * @deprecated 6.5.2 This method is deprecated
     */
    getItemsForPoint: function(x, y) {
        var me = this,
            seriesList = me.getSeries(),
            ln = seriesList.length,
            // If we haven't drawn yet, don't attempt to find any items.
            i = me.hasFirstLayout ? ln - 1 : -1,
            items = [],
            series, item;
        // Iterate from the end so that the series that are drawn later get hit tested first.
        for (; i >= 0; i--) {
            series = seriesList[i];
            item = series.getItemForPoint(x, y);
            if (item && (item.category === 'items' || item.category === 'markers')) {
                items.push(item);
            }
        }
        return items;
    },
    /**
     * @private
     */
    onDataChanged: function() {
        var me = this,
            rect, store, series, axes;
        if (me.isInitializing) {
            return;
        }
        rect = me.getMainRect();
        store = me.getStore();
        series = me.getSeries();
        axes = me.getAxes();
        if (!store || !axes || !series) {
            return;
        }
        if (!rect) {
            // The chart hasn't been rendered yet.
            me.on({
                redraw: me.onDataChanged,
                scope: me,
                single: true
            });
            return;
        }
        me.processData();
        me.redraw();
    },
    /**
     * @private
     * The number of records in the chart's store last time the data was changed.
     */
    recordCount: 0,
    /**
     * @private
     */
    processData: function() {
        var me = this,
            recordCount = me.getStore().getCount(),
            seriesList = me.getSeries(),
            ln = seriesList.length,
            isNeedUpdateColors = false,
            i = 0,
            series;
        for (; i < ln; i++) {
            series = seriesList[i];
            series.processData();
            if (!isNeedUpdateColors && series.isStoreDependantColorCount) {
                isNeedUpdateColors = true;
            }
        }
        if (isNeedUpdateColors && recordCount > me.recordCount) {
            me.updateColors(me.getColors());
            me.recordCount = recordCount;
        }
        // 'refreshLegendStore' will attemp to grab the 'series',
        // which are still configuring at this point.
        // The legend store will be refreshed inside the chart.series
        // updater anyway.
        if (!me.isConfiguring) {
            me.refreshLegendStore();
        }
    },
    /**
     * Changes the data store bound to this chart and refreshes it.
     * @param {Ext.data.Store} store The store to bind to this chart.
     */
    bindStore: function(store) {
        this.setStore(store);
    },
    applyHighlightItem: function(newHighlightItem, oldHighlightItem) {
        var i1, i2, s1, s2;
        if (newHighlightItem === oldHighlightItem) {
            return;
        }
        if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) {
            i1 = newHighlightItem;
            i2 = oldHighlightItem;
            s1 = i1.sprite && (i1.sprite[0] || i1.sprite);
            s2 = i2.sprite && (i2.sprite[0] || i2.sprite);
            if (s1 === s2 && i1.index === i2.index) {
                return;
            }
        }
        return newHighlightItem;
    },
    updateHighlightItem: function(newHighlightItem, oldHighlightItem) {
        var newHighlight, oldHighlight;
        if (oldHighlightItem) {
            oldHighlight = oldHighlightItem.series.getHighlight();
            if (oldHighlight) {
                oldHighlightItem.series.setAttributesForItem(oldHighlightItem, {
                    highlighted: false
                });
            }
        }
        if (newHighlightItem) {
            newHighlight = newHighlightItem.series.getHighlight();
            if (newHighlight) {
                newHighlightItem.series.setAttributesForItem(newHighlightItem, {
                    highlighted: true
                });
            }
        }
        if (oldHighlight || newHighlight) {
            this.fireEvent('itemhighlight', this, newHighlightItem, oldHighlightItem);
        }
    },
    destroyChart: function() {
        var me = this;
        // The order is important here.
        me.setInteractions(null);
        me.setAxes(null);
        me.setSeries(null);
        me.setLegend(null);
        me.setStore(null);
        me.cancelChartLayout();
    },
    /* ---------------------------------
     Methods needed for ComponentQuery
     ---------------------------------- */
    /**
     * @private
     * @param {Boolean} deep
     * @return {Array}
     */
    getRefItems: function(deep) {
        var me = this,
            series = me.getSeries(),
            axes = me.getAxes(),
            interaction = me.getInteractions(),
            legend = me.getLegend(),
            ans = [],
            i, ln;
        for (i = 0 , ln = series.length; i < ln; i++) {
            ans.push(series[i]);
            if (series[i].getRefItems) {
                ans.push.apply(ans, series[i].getRefItems(deep));
            }
        }
        for (i = 0 , ln = axes.length; i < ln; i++) {
            ans.push(axes[i]);
            if (axes[i].getRefItems) {
                ans.push.apply(ans, axes[i].getRefItems(deep));
            }
        }
        for (i = 0 , ln = interaction.length; i < ln; i++) {
            ans.push(interaction[i]);
            if (interaction[i].getRefItems) {
                ans.push.apply(ans, interaction[i].getRefItems(deep));
            }
        }
        if (legend) {
            ans.push(legend);
        }
        return ans;
    }
});

/**
 * @class Ext.chart.overrides.AbstractChart
 */
Ext.define('Ext.chart.overrides.AbstractChart', {
    override: 'Ext.chart.AbstractChart',
    updateLegend: function(legend, oldLegend) {
        this.callParent([
            legend,
            oldLegend
        ]);
        if (legend && legend.isDomLegend) {
            this.addDocked(legend);
        }
    },
    performLayout: function() {
        if (this.isVisible(true)) {
            return this.callParent();
        }
        this.cancelChartLayout();
        return false;
    },
    afterComponentLayout: function(width, height, oldWidth, oldHeight) {
        this.callParent([
            width,
            height,
            oldWidth,
            oldHeight
        ]);
        if (!this.hasFirstLayout) {
            this.scheduleLayout();
        }
    },
    allowSchedule: function() {
        return this.rendered;
    },
    doDestroy: function() {
        this.destroyChart();
        this.callParent();
    }
});

/**
 * @class Ext.chart.grid.HorizontalGrid
 * @extends Ext.draw.sprite.Sprite
 * 
 * Horizontal Grid sprite. Used in Cartesian Charts.
 */
Ext.define('Ext.chart.grid.HorizontalGrid', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'grid.horizontal',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                width: 'number',
                height: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                width: 1,
                height: 1,
                strokeStyle: '#DDD'
            }
        }
    },
    render: function(surface, ctx, rect) {
        var attr = this.attr,
            y = surface.roundPixel(attr.y),
            halfLineWidth = ctx.lineWidth * 0.5;
        ctx.beginPath();
        ctx.rect(rect[0] - surface.matrix.getDX(), y + halfLineWidth, +rect[2], attr.height);
        ctx.fill();
        ctx.beginPath();
        ctx.moveTo(rect[0] - surface.matrix.getDX(), y + halfLineWidth);
        ctx.lineTo(rect[0] + rect[2] - surface.matrix.getDX(), y + halfLineWidth);
        ctx.stroke();
    }
});

/**
 * @class Ext.chart.grid.VerticalGrid
 * @extends Ext.draw.sprite.Sprite
 * 
 * Vertical Grid sprite. Used in Cartesian Charts.
 */
Ext.define('Ext.chart.grid.VerticalGrid', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'grid.vertical',
    inheritableStatics: {
        def: {
            processors: {
                x: 'number',
                y: 'number',
                width: 'number',
                height: 'number'
            },
            defaults: {
                x: 0,
                y: 0,
                width: 1,
                height: 1,
                strokeStyle: '#DDD'
            }
        }
    },
    render: function(surface, ctx, rect) {
        var attr = this.attr,
            x = surface.roundPixel(attr.x),
            halfLineWidth = ctx.lineWidth * 0.5;
        ctx.beginPath();
        ctx.rect(x - halfLineWidth, rect[1] - surface.matrix.getDY(), attr.width, rect[3]);
        ctx.fill();
        ctx.beginPath();
        ctx.moveTo(x - halfLineWidth, rect[1] - surface.matrix.getDY());
        ctx.lineTo(x - halfLineWidth, rect[1] + rect[3] - surface.matrix.getDY());
        ctx.stroke();
    }
});

/**
 * Represents a chart that uses cartesian coordinates.
 * A cartesian chart has two directions, X direction and Y direction.
 * The series and axes are coordinated along these directions.
 * By default the x direction is horizontal and y direction is vertical,
 * You can swap the direction by setting the {@link #flipXY} config to `true`.
 *
 * Cartesian series often treats x direction an y direction differently.
 * In most cases, data on x direction are assumed to be monotonically increasing.
 * Based on this property, cartesian series can be trimmed and summarized properly
 * to gain a better performance.
 *
 * Please check out the summary for the {@link Ext.chart.AbstractChart} as well,
 * for helpful tips and important details.
 *
 */
Ext.define('Ext.chart.CartesianChart', {
    extend: 'Ext.chart.AbstractChart',
    alternateClassName: 'Ext.chart.Chart',
    requires: [
        'Ext.chart.grid.HorizontalGrid',
        'Ext.chart.grid.VerticalGrid'
    ],
    xtype: [
        'cartesian',
        'chart'
    ],
    isCartesian: true,
    config: {
        /**
         * @cfg {Boolean} flipXY Flip the direction of X and Y axis.
         * If flipXY is `true`, the X axes will be vertical and Y axes will be horizontal.
         * Note that {@link Ext.chart.axis.Axis#position positions} of chart axes have
         * to be updated accordingly: axes positioned to the `top` and `bottom` should
         * be positioned to the `left` or `right` and vice versa.
         */
        flipXY: false,
        /*

         While it may seem tedious to change the position config of all axes every time
         when the value of the flipXY config is changed, it's hard to predict the
         expectaction of the user here, as illustrated below.

         The 'num' and 'cat' here stand for the numeric and the category axis, respectively.
         And the right column shows the expected (subjective) result of setting the flipXY
         config of the chart to 'true'.

         As one can see, there's no single rule (e.g. position swapping, clockwise 90° chart
         rotation) that will produce a universally accepted result.
         So we are letting the user decide, instead of doing it for them.

         ---------------------------------------------
         |   flipXY: false       |    flipXY: true   |
         ---------------------------------------------
         |        ^              |      ^            |
         |        |     *        |      | * * *      |
         |   num1 |   * *        |  cat | * *        |
         |        | * * *        |      | *          |
         |        -------->      |      -------->    |
         |           cat         |         num1      |
         ---------------------------------------------
         |                       |         num1      |
         |       ^       ^       |      ^------->    |
         |       |     * |       |      | * * *      |
         |  num1 |   * * | num2  |  cat | * *        |
         |       | * * * |       |      | *          |
         |       -------->       |      -------->    |
         |          cat          |         num2      |
         ---------------------------------------------

         */
        innerRect: [
            0,
            0,
            1,
            1
        ],
        /**
         * @cfg {Object} innerPadding The amount of inner padding in pixels.
         * Inner padding is the padding from the innermost axes to the series.
         */
        innerPadding: {
            top: 0,
            left: 0,
            right: 0,
            bottom: 0
        }
    },
    applyInnerPadding: function(padding, oldPadding) {
        if (!Ext.isObject(padding)) {
            return Ext.util.Format.parseBox(padding);
        } else if (!oldPadding) {
            return padding;
        } else {
            return Ext.apply(oldPadding, padding);
        }
    },
    getDirectionForAxis: function(position) {
        var flipXY = this.getFlipXY(),
            direction;
        if (position === 'left' || position === 'right') {
            direction = flipXY ? 'X' : 'Y';
        } else {
            direction = flipXY ? 'Y' : 'X';
        }
        return direction;
    },
    /**
     * Layout the axes and series.
     */
    performLayout: function() {
        var me = this;
        if (me.callParent() === false) {
            return;
        }
        me.chartLayoutCount++;
        me.suspendAnimation();
        // 'chart' surface rect is the size of the chart's inner element
        // (see chart.getChartBox), i.e. the portion of the chart minus
        // the legend area (whether DOM or sprite based).
        // eslint-disable-next-line vars-on-top, one-var
        var chartRect = me.getSurface('chart').getRect(),
            left = chartRect[0],
            top = chartRect[1],
            width = chartRect[2],
            height = chartRect[3],
            captionList = me.captionList,
            axes = me.getAxes(),
            axis,
            seriesList = me.getSeries(),
            series, axisSurface, thickness,
            insetPadding = me.getInsetPadding(),
            innerPadding = me.getInnerPadding(),
            surface, gridSurface,
            // shrinkBox represents padding added on each side by
            // innerPadding & insetPadding configs and the legend.
            shrinkBox = Ext.apply({}, insetPadding),
            mainRect, innerWidth, innerHeight, elements, floating, floatingValue, matrix, i, ln,
            isRtl = me.getInherited().rtl,
            flipXY = me.getFlipXY(),
            caption;
        if (width <= 0 || height <= 0) {
            return;
        }
        me.suspendThicknessChanged();
        for (i = 0; i < axes.length; i++) {
            axis = axes[i];
            axisSurface = axis.getSurface();
            floating = axis.getFloating();
            floatingValue = floating ? floating.value : null;
            thickness = axis.getThickness();
            switch (axis.getPosition()) {
                case 'top':
                    axisSurface.setRect([
                        left,
                        top + shrinkBox.top + 1,
                        width,
                        thickness
                    ]);
                    break;
                case 'bottom':
                    axisSurface.setRect([
                        left,
                        top + height - (shrinkBox.bottom + thickness),
                        width,
                        thickness
                    ]);
                    break;
                case 'left':
                    axisSurface.setRect([
                        left + shrinkBox.left,
                        top,
                        thickness,
                        height
                    ]);
                    break;
                case 'right':
                    axisSurface.setRect([
                        left + width - (shrinkBox.right + thickness),
                        top,
                        thickness,
                        height
                    ]);
                    break;
            }
            if (floatingValue === null) {
                shrinkBox[axis.getPosition()] += thickness;
            }
        }
        width -= shrinkBox.left + shrinkBox.right;
        height -= shrinkBox.top + shrinkBox.bottom;
        mainRect = [
            left + shrinkBox.left,
            top + shrinkBox.top,
            width,
            height
        ];
        shrinkBox.left += innerPadding.left;
        shrinkBox.top += innerPadding.top;
        shrinkBox.right += innerPadding.right;
        shrinkBox.bottom += innerPadding.bottom;
        innerWidth = width - innerPadding.left - innerPadding.right;
        innerHeight = height - innerPadding.top - innerPadding.bottom;
        me.setInnerRect([
            shrinkBox.left,
            shrinkBox.top,
            innerWidth,
            innerHeight
        ]);
        if (innerWidth <= 0 || innerHeight <= 0) {
            return;
        }
        me.setMainRect(mainRect);
        me.getSurface().setRect(mainRect);
        for (i = 0 , ln = me.surfaceMap.grid && me.surfaceMap.grid.length; i < ln; i++) {
            gridSurface = me.surfaceMap.grid[i];
            gridSurface.setRect(mainRect);
            gridSurface.matrix.set(1, 0, 0, 1, innerPadding.left, innerPadding.top);
            gridSurface.matrix.inverse(gridSurface.inverseMatrix);
        }
        for (i = 0; i < axes.length; i++) {
            axis = axes[i];
            axis.getRange(true);
            axisSurface = axis.getSurface();
            matrix = axisSurface.matrix;
            elements = matrix.elements;
            switch (axis.getPosition()) {
                case 'top':
                case 'bottom':
                    elements[4] = shrinkBox.left;
                    axis.setLength(innerWidth);
                    break;
                case 'left':
                case 'right':
                    elements[5] = shrinkBox.top;
                    axis.setLength(innerHeight);
                    break;
            }
            axis.updateTitleSprite();
            matrix.inverse(axisSurface.inverseMatrix);
        }
        for (i = 0 , ln = seriesList.length; i < ln; i++) {
            series = seriesList[i];
            surface = series.getSurface();
            surface.setRect(mainRect);
            if (flipXY) {
                if (isRtl) {
                    surface.matrix.set(0, -1, -1, 0, innerPadding.left + innerWidth, innerPadding.top + innerHeight);
                } else {
                    surface.matrix.set(0, -1, 1, 0, innerPadding.left, innerPadding.top + innerHeight);
                }
            } else {
                surface.matrix.set(1, 0, 0, -1, innerPadding.left, innerPadding.top + innerHeight);
            }
            surface.matrix.inverse(surface.inverseMatrix);
            series.getOverlaySurface().setRect(mainRect);
        }
        if (captionList) {
            for (i = 0 , ln = captionList.length; i < ln; i++) {
                caption = captionList[i];
                if (caption.getAlignTo() === 'series') {
                    caption.alignRect(mainRect);
                }
                caption.performLayout();
            }
        }
        // In certain cases 'performLayout' override is not an option without major code duplication
        // 'afterChartLayout' can be a cleaner solution in such cases (because of the timing
        // of its call).
        me.afterChartLayout();
        // currently in cartesian charts only (used by Navigator)
        me.redraw();
        me.resumeAnimation();
        // 'resumeThicknessChanged' may trigger another layout, if the 'redraw' call above
        // resulted in a situation where an axis is no longer 'thick' enough to accommodate
        // the new labels. E.g. the labels were: 'Bob', 'Ann', 'Joe' and now they are 'Jonathan',
        // 'Rachael', 'Michael'. An axis has to be made thicker now, and another layout should be
        // performed. This second layout is not scheduled, but performed immediately, which will
        // increment the 'chartLayoutCount' again.
        me.resumeThicknessChanged();
        me.chartLayoutCount--;
        // 'checkLayoutEnd' will check if another layout is already running or scheduled and,
        // if neither is the case, will fire the 'layout' event, meaning we are totally done
        // with layout at this point.
        me.checkLayoutEnd();
    },
    afterChartLayout: Ext.emptyFn,
    refloatAxes: function() {
        var me = this,
            axes = me.getAxes(),
            axesCount = (axes && axes.length) || 0,
            axis, axisSurface, axisRect, floating, value, alongAxis, matrix,
            chartRect = me.getChartRect(),
            inset = me.getInsetPadding(),
            inner = me.getInnerPadding(),
            width = chartRect[2] - inset.left - inset.right,
            height = chartRect[3] - inset.top - inset.bottom,
            isHorizontal, i;
        for (i = 0; i < axesCount; i++) {
            axis = axes[i];
            floating = axis.getFloating();
            value = floating ? floating.value : null;
            if (value === null) {
                axis.floatingAtCoord = null;
                
                continue;
            }
            axisSurface = axis.getSurface();
            axisRect = axisSurface.getRect();
            if (!axisRect) {
                
                continue;
            }
            axisRect = axisRect.slice();
            alongAxis = me.getAxis(floating.alongAxis);
            if (alongAxis) {
                isHorizontal = alongAxis.getAlignment() === 'horizontal';
                if (Ext.isString(value)) {
                    value = alongAxis.getCoordFor(value);
                }
                alongAxis.floatingAxes[axis.getId()] = value;
                matrix = alongAxis.getSprites()[0].attr.matrix;
                if (isHorizontal) {
                    value = value * matrix.getXX() + matrix.getDX();
                    axis.floatingAtCoord = value + inner.left + inner.right;
                } else {
                    value = value * matrix.getYY() + matrix.getDY();
                    axis.floatingAtCoord = value + inner.top + inner.bottom;
                }
            } else {
                isHorizontal = axis.getAlignment() === 'horizontal';
                if (isHorizontal) {
                    axis.floatingAtCoord = value + inner.top + inner.bottom;
                } else {
                    axis.floatingAtCoord = value + inner.left + inner.right;
                }
                value = axisSurface.roundPixel(0.01 * value * (isHorizontal ? height : width));
            }
            switch (axis.getPosition()) {
                case 'top':
                    axisRect[1] = inset.top + inner.top + value - axisRect[3] + 1;
                    break;
                case 'bottom':
                    axisRect[1] = inset.top + inner.top + (alongAxis ? value : height - value);
                    break;
                case 'left':
                    axisRect[0] = inset.left + inner.left + value - axisRect[2];
                    break;
                case 'right':
                    axisRect[0] = inset.left + inner.left + (alongAxis ? value : width - value) - 1;
                    break;
            }
            axisSurface.setRect(axisRect);
        }
    },
    redraw: function() {
        var me = this,
            seriesList = me.getSeries(),
            axes = me.getAxes(),
            rect = me.getMainRect(),
            innerWidth, innerHeight,
            innerPadding = me.getInnerPadding(),
            sprites, xRange, yRange, isSide, attr, i, j, ln, axis, axisX, axisY, range, visibleRange,
            flipXY = me.getFlipXY(),
            zBase = 1000,
            zIndex, markersZIndex, series, sprite, markers;
        if (!rect) {
            return;
        }
        innerWidth = rect[2] - innerPadding.left - innerPadding.right;
        innerHeight = rect[3] - innerPadding.top - innerPadding.bottom;
        for (i = 0; i < seriesList.length; i++) {
            series = seriesList[i];
            axisX = series.getXAxis();
            if (axisX) {
                visibleRange = axisX.getVisibleRange();
                xRange = axisX.getRange();
                xRange = [
                    xRange[0] + (xRange[1] - xRange[0]) * visibleRange[0],
                    xRange[0] + (xRange[1] - xRange[0]) * visibleRange[1]
                ];
            } else {
                xRange = series.getXRange();
            }
            axisY = series.getYAxis();
            if (axisY) {
                visibleRange = axisY.getVisibleRange();
                yRange = axisY.getRange();
                yRange = [
                    yRange[0] + (yRange[1] - yRange[0]) * visibleRange[0],
                    yRange[0] + (yRange[1] - yRange[0]) * visibleRange[1]
                ];
            } else {
                yRange = series.getYRange();
            }
            attr = {
                visibleMinX: xRange[0],
                visibleMaxX: xRange[1],
                visibleMinY: yRange[0],
                visibleMaxY: yRange[1],
                innerWidth: innerWidth,
                innerHeight: innerHeight,
                flipXY: flipXY
            };
            sprites = series.getSprites();
            for (j = 0 , ln = sprites.length; j < ln; j++) {
                // All the series now share the same surface, so we must assign
                // the sprites a zIndex that depends on the index of their series.
                sprite = sprites[j];
                zIndex = sprite.attr.zIndex;
                if (zIndex < zBase) {
                    // Set the sprite's zIndex
                    zIndex += (i + 1) * 100 + zBase;
                    sprite.attr.zIndex = zIndex;
                    // If the sprite is a MarkerHolder, set zIndex of the bound markers as well.
                    // Do this for the 'items' markers only, as those are the only ones
                    // that go into the 'series' surface. 'labels' and 'markers' markers
                    // go into the 'overlay' surface instead.
                    markers = sprite.getMarker('items');
                    if (markers) {
                        markersZIndex = markers.attr.zIndex;
                        if (markersZIndex === Number.MAX_VALUE) {
                            markers.attr.zIndex = zIndex;
                        } else if (markersZIndex < zBase) {
                            markers.attr.zIndex = zIndex + markersZIndex;
                        }
                    }
                }
                sprite.setAttributes(attr, true);
            }
        }
        for (i = 0; i < axes.length; i++) {
            axis = axes[i];
            isSide = axis.isSide();
            sprites = axis.getSprites();
            range = axis.getRange();
            visibleRange = axis.getVisibleRange();
            attr = {
                dataMin: range[0],
                dataMax: range[1],
                visibleMin: visibleRange[0],
                visibleMax: visibleRange[1]
            };
            if (isSide) {
                attr.length = innerHeight;
                attr.startGap = innerPadding.bottom;
                attr.endGap = innerPadding.top;
            } else {
                attr.length = innerWidth;
                attr.startGap = innerPadding.left;
                attr.endGap = innerPadding.right;
            }
            for (j = 0 , ln = sprites.length; j < ln; j++) {
                sprites[j].setAttributes(attr, true);
            }
        }
        me.renderFrame();
        me.callParent();
    },
    renderFrame: function() {
        this.refloatAxes();
        this.callParent();
    }
});

/**
 * @class Ext.chart.grid.CircularGrid
 * @extends Ext.draw.sprite.Circle
 * 
 * Circular Grid sprite. Used by Radar chart to render a series of concentric circles.
 */
Ext.define('Ext.chart.grid.CircularGrid', {
    extend: 'Ext.draw.sprite.Circle',
    alias: 'grid.circular',
    inheritableStatics: {
        def: {
            defaults: {
                r: 1,
                strokeStyle: '#DDD'
            }
        }
    }
});

/**
 * @class Ext.chart.grid.RadialGrid
 * @extends Ext.draw.sprite.Path
 * 
 * Radial Grid sprite. Used by Radar chart to render a series of radial lines.
 * Represents the scale of the radar chart on the yField.
 */
Ext.define('Ext.chart.grid.RadialGrid', {
    extend: 'Ext.draw.sprite.Path',
    alias: 'grid.radial',
    inheritableStatics: {
        def: {
            processors: {
                startRadius: 'number',
                endRadius: 'number'
            },
            defaults: {
                startRadius: 0,
                endRadius: 1,
                scalingCenterX: 0,
                scalingCenterY: 0,
                strokeStyle: '#DDD'
            },
            triggers: {
                startRadius: 'path,bbox',
                endRadius: 'path,bbox'
            }
        }
    },
    render: function() {
        this.callParent(arguments);
    },
    updatePath: function(path, attr) {
        var startRadius = attr.startRadius,
            endRadius = attr.endRadius;
        path.moveTo(startRadius, 0);
        path.lineTo(endRadius, 0);
    }
});

/**
 * @class Ext.chart.PolarChart
 * @extends Ext.chart.AbstractChart
 * @xtype polar
 *
 * Represent a chart that uses polar coordinates.
 * A polar chart has two axes: an angular axis (which is a circle) and
 * a radial axis (a straight line from the center to the edge of the circle).
 * The angular axis is usually a Category axis while the radial axis is
 * typically numerical. 
 *
 * Pie charts and Radar charts are common examples of Polar charts.
 *
 * Please check out the summary for the {@link Ext.chart.AbstractChart} as well,
 * for helpful tips and important details.
 *
 */
Ext.define('Ext.chart.PolarChart', {
    extend: 'Ext.chart.AbstractChart',
    requires: [
        'Ext.chart.grid.CircularGrid',
        'Ext.chart.grid.RadialGrid'
    ],
    xtype: 'polar',
    isPolar: true,
    config: {
        /**
         * @cfg {Array} center Determines the center of the polar chart.
         * Updated when the chart performs layout.
         */
        center: [
            0,
            0
        ],
        /**
         * @cfg {Number} radius Determines the radius of the polar chart.
         * Updated when the chart performs layout.
         */
        radius: 0,
        /**
         * @cfg {Number} innerPadding The amount of inner padding in pixels.
         * Inner padding is the padding from the outermost angular axis to the series.
         */
        innerPadding: 0
    },
    getDirectionForAxis: function(position) {
        return position === 'radial' ? 'Y' : 'X';
    },
    updateCenter: function(center) {
        var me = this,
            axes = me.getAxes(),
            series = me.getSeries(),
            i, ln, axis, seriesItem;
        for (i = 0 , ln = axes.length; i < ln; i++) {
            axis = axes[i];
            axis.setCenter(center);
        }
        for (i = 0 , ln = series.length; i < ln; i++) {
            seriesItem = series[i];
            seriesItem.setCenter(center);
        }
    },
    applyInnerPadding: function(padding, oldPadding) {
        return Ext.isNumber(padding) ? padding : oldPadding;
    },
    updateInnerPadding: function() {
        if (!this.isConfiguring) {
            this.performLayout();
        }
    },
    doSetSurfaceRect: function(surface, rect) {
        var mainRect = this.getMainRect();
        surface.setRect(rect);
        surface.matrix.set(1, 0, 0, 1, mainRect[0] - rect[0], mainRect[1] - rect[1]);
        surface.inverseMatrix.set(1, 0, 0, 1, rect[0] - mainRect[0], rect[1] - mainRect[1]);
    },
    applyAxes: function(newAxes, oldAxes) {
        var me = this,
            firstSeries = Ext.Array.from(me.config.series)[0],
            i, ln, axis, foundAngular;
        if (firstSeries && firstSeries.type === 'radar' && newAxes && newAxes.length) {
            // For compatibility with ExtJS: add a default angular axis if it's missing
            for (i = 0 , ln = newAxes.length; i < ln; i++) {
                axis = newAxes[i];
                if (axis.position === 'angular') {
                    foundAngular = true;
                    break;
                }
            }
            if (!foundAngular) {
                newAxes.push({
                    type: 'category',
                    position: 'angular',
                    fields: firstSeries.xField || firstSeries.angleField,
                    style: {
                        estStepSize: 1
                    },
                    grid: true
                });
            }
        }
        return this.callParent([
            newAxes,
            oldAxes
        ]);
    },
    performLayout: function() {
        var me = this,
            applyThickness = true;
        try {
            me.chartLayoutCount++;
            me.suspendAnimation();
            if (this.callParent() === false) {
                applyThickness = false;
                // Animation will be decremented in finally block
                return;
            }
            me.suspendThicknessChanged();
            // eslint-disable-next-line vars-on-top, one-var
            var chartRect = me.getSurface('chart').getRect(),
                inset = me.getInsetPadding(),
                inner = me.getInnerPadding(),
                shrinkBox = Ext.apply({}, inset),
                width = Math.max(1, chartRect[2] - chartRect[0] - inset.left - inset.right),
                height = Math.max(1, chartRect[3] - chartRect[1] - inset.top - inset.bottom),
                mainRect = [
                    chartRect[0] + inset.left,
                    chartRect[1] + inset.top,
                    width + chartRect[0],
                    height + chartRect[1]
                ],
                seriesList = me.getSeries(),
                innerWidth = width - inner * 2,
                innerHeight = height - inner * 2,
                center = [
                    (chartRect[0] + innerWidth) * 0.5 + inner,
                    (chartRect[1] + innerHeight) * 0.5 + inner
                ],
                radius = Math.min(innerWidth, innerHeight) * 0.5,
                axes = me.getAxes(),
                angularAxes = [],
                radialAxes = [],
                seriesRadius = radius - inner,
                grid = me.surfaceMap.grid,
                captionList = me.captionList,
                i, ln, shrinkRadius, floating, floatingValue, // eslint-disable-line no-unused-vars
                gaugeSeries, gaugeRadius, side, series, axis, thickness, halfLineWidth, caption;
            me.setMainRect(mainRect);
            me.doSetSurfaceRect(me.getSurface(), mainRect);
            if (grid) {
                for (i = 0 , ln = grid.length; i < ln; i++) {
                    me.doSetSurfaceRect(grid[i], chartRect);
                }
            }
            for (i = 0 , ln = axes.length; i < ln; i++) {
                axis = axes[i];
                switch (axis.getPosition()) {
                    case 'angular':
                        angularAxes.push(axis);
                        break;
                    case 'radial':
                        radialAxes.push(axis);
                        break;
                }
            }
            for (i = 0 , ln = angularAxes.length; i < ln; i++) {
                axis = angularAxes[i];
                floating = axis.getFloating();
                floatingValue = floating ? floating.value : null;
                me.doSetSurfaceRect(axis.getSurface(), chartRect);
                thickness = axis.getThickness();
                for (side in shrinkBox) {
                    shrinkBox[side] += thickness;
                }
                width = chartRect[2] - shrinkBox.left - shrinkBox.right;
                height = chartRect[3] - shrinkBox.top - shrinkBox.bottom;
                shrinkRadius = Math.min(width, height) * 0.5;
                if (i === 0) {
                    seriesRadius = shrinkRadius - inner;
                }
                axis.setMinimum(0);
                axis.setLength(shrinkRadius);
                axis.getSprites();
                halfLineWidth = axis.sprites[0].attr.lineWidth * 0.5;
                for (side in shrinkBox) {
                    shrinkBox[side] += halfLineWidth;
                }
            }
            for (i = 0 , ln = radialAxes.length; i < ln; i++) {
                axis = radialAxes[i];
                me.doSetSurfaceRect(axis.getSurface(), chartRect);
                axis.setMinimum(0);
                axis.setLength(seriesRadius);
                axis.getSprites();
            }
            for (i = 0 , ln = seriesList.length; i < ln; i++) {
                series = seriesList[i];
                if (series.type === 'gauge' && !gaugeSeries) {
                    gaugeSeries = series;
                } else {
                    series.setRadius(seriesRadius);
                }
                me.doSetSurfaceRect(series.getSurface(), mainRect);
            }
            me.doSetSurfaceRect(me.getSurface('overlay'), chartRect);
            if (gaugeSeries) {
                gaugeSeries.setRect(mainRect);
                gaugeRadius = gaugeSeries.getRadius() - inner;
                me.setRadius(gaugeRadius);
                me.setCenter(gaugeSeries.getCenter());
                gaugeSeries.setRadius(gaugeRadius);
                if (axes.length && axes[0].getPosition() === 'gauge') {
                    axis = axes[0];
                    me.doSetSurfaceRect(axis.getSurface(), chartRect);
                    axis.setTotalAngle(gaugeSeries.getTotalAngle());
                    axis.setLength(gaugeRadius);
                }
            } else {
                me.setRadius(radius);
                me.setCenter(center);
            }
            if (captionList) {
                for (i = 0 , ln = captionList.length; i < ln; i++) {
                    caption = captionList[i];
                    if (caption.getAlignTo() === 'series') {
                        caption.alignRect(mainRect);
                    }
                    caption.performLayout();
                }
            }
            me.redraw();
        } finally {
            me.resumeAnimation();
            if (applyThickness) {
                me.resumeThicknessChanged();
            }
            me.chartLayoutCount--;
            me.checkLayoutEnd();
        }
    },
    refloatAxes: function() {
        var me = this,
            axes = me.getAxes(),
            mainRect = me.getMainRect(),
            floating, value, alongAxis, i, n, axis, radius;
        if (!mainRect) {
            return;
        }
        radius = 0.5 * Math.min(mainRect[2], mainRect[3]);
        for (i = 0 , n = axes.length; i < n; i++) {
            axis = axes[i];
            floating = axis.getFloating();
            value = floating ? floating.value : null;
            if (value !== null) {
                alongAxis = me.getAxis(floating.alongAxis);
                if (axis.getPosition() === 'angular') {
                    if (alongAxis) {
                        value = alongAxis.getLength() * value / alongAxis.getRange()[1];
                    } else {
                        value = 0.01 * value * radius;
                    }
                    axis.sprites[0].setAttributes({
                        length: value
                    }, true);
                } else {
                    if (alongAxis) {
                        if (Ext.isString(value)) {
                            value = alongAxis.getCoordFor(value);
                        }
                        value = value / (alongAxis.getRange()[1] + 1) * Math.PI * 2 - Math.PI * 1.5 + axis.getRotation();
                    } else {
                        value = Ext.draw.Draw.rad(value);
                    }
                    axis.sprites[0].setAttributes({
                        baseRotation: value
                    }, true);
                }
            }
        }
    },
    redraw: function() {
        var me = this,
            axes = me.getAxes(),
            axis,
            seriesList = me.getSeries(),
            series, i, ln;
        for (i = 0 , ln = axes.length; i < ln; i++) {
            axis = axes[i];
            axis.getSprites();
        }
        for (i = 0 , ln = seriesList.length; i < ln; i++) {
            series = seriesList[i];
            series.getSprites();
        }
        me.renderFrame();
        me.callParent();
    },
    renderFrame: function() {
        this.refloatAxes();
        this.callParent();
    }
});

/**
 * @class Ext.chart.SpaceFillingChart
 * @extends Ext.chart.AbstractChart
 *
 * Creates a chart that fills the entire area of the chart.
 * e.g. Gauge Charts
 */
Ext.define('Ext.chart.SpaceFillingChart', {
    extend: 'Ext.chart.AbstractChart',
    xtype: 'spacefilling',
    config: {},
    performLayout: function() {
        var me = this;
        try {
            me.chartLayoutCount++;
            me.suspendAnimation();
            if (me.callParent() === false) {
                // animationSuspendCount will still be decremented
                return;
            }
            // eslint-disable-next-line vars-on-top, one-var
            var chartRect = me.getSurface('chart').getRect(),
                padding = me.getInsetPadding(),
                width = chartRect[2] - padding.left - padding.right,
                height = chartRect[3] - padding.top - padding.bottom,
                mainRect = [
                    padding.left,
                    padding.top,
                    width,
                    height
                ],
                seriesList = me.getSeries(),
                series, i, ln;
            me.getSurface().setRect(mainRect);
            me.setMainRect(mainRect);
            for (i = 0 , ln = seriesList.length; i < ln; i++) {
                series = seriesList[i];
                series.getSurface().setRect(mainRect);
                if (series.setRect) {
                    series.setRect(mainRect);
                }
                series.getOverlaySurface().setRect(chartRect);
            }
            me.redraw();
        } finally {
            me.resumeAnimation();
            me.chartLayoutCount--;
            me.checkLayoutEnd();
        }
    },
    redraw: function() {
        var me = this,
            seriesList = me.getSeries(),
            series, i, ln;
        for (i = 0 , ln = seriesList.length; i < ln; i++) {
            series = seriesList[i];
            series.getSprites();
        }
        me.renderFrame();
        me.callParent();
    }
});

/**
 * @private
 * @class Ext.chart.axis.sprite.Axis3D
 * @extends Ext.chart.axis.sprite.Axis
 *
 * The {@link Ext.chart.axis.Axis3D 3D axis} sprite.
 * Only 3D cartesian axes are rendered with this sprite.
 */
Ext.define('Ext.chart.axis.sprite.Axis3D', {
    extend: 'Ext.chart.axis.sprite.Axis',
    alias: 'sprite.axis3d',
    type: 'axis3d',
    inheritableStatics: {
        def: {
            processors: {
                depth: 'number'
            },
            defaults: {
                depth: 0
            },
            triggers: {
                depth: 'layout'
            }
        }
    },
    config: {
        animation: {
            customDurations: {
                depth: 0
            }
        }
    },
    layoutUpdater: function() {
        var me = this,
            chart = me.getAxis().getChart();
        if (chart.isInitializing) {
            return;
        }
        // eslint-disable-next-line vars-on-top, one-var
        var attr = me.attr,
            layout = me.getLayout(),
            depth = layout.isDiscrete ? 0 : attr.depth,
            isRtl = chart.getInherited().rtl,
            min = attr.dataMin + (attr.dataMax - attr.dataMin) * attr.visibleMin,
            max = attr.dataMin + (attr.dataMax - attr.dataMin) * attr.visibleMax,
            context = {
                attr: attr,
                segmenter: me.getSegmenter(),
                renderer: me.defaultRenderer
            };
        if (attr.position === 'left' || attr.position === 'right') {
            attr.translationX = 0;
            attr.translationY = max * (attr.length - depth) / (max - min) + depth;
            attr.scalingX = 1;
            attr.scalingY = (-attr.length + depth) / (max - min);
            attr.scalingCenterY = 0;
            attr.scalingCenterX = 0;
            me.applyTransformations(true);
        } else if (attr.position === 'top' || attr.position === 'bottom') {
            if (isRtl) {
                attr.translationX = attr.length + min * attr.length / (max - min) + 1;
            } else {
                attr.translationX = -min * attr.length / (max - min);
            }
            attr.translationY = 0;
            attr.scalingX = (isRtl ? -1 : 1) * (attr.length - depth) / (max - min);
            attr.scalingY = 1;
            attr.scalingCenterY = 0;
            attr.scalingCenterX = 0;
            me.applyTransformations(true);
        }
        if (layout) {
            layout.calculateLayout(context);
            me.setLayoutContext(context);
        }
    },
    renderAxisLine: function(surface, ctx, layout, clipRect) {
        var me = this,
            attr = me.attr,
            halfLineWidth = attr.lineWidth * 0.5,
            layout = me.getLayout(),
            // eslint-disable-line no-redeclare
            depth = layout.isDiscrete ? 0 : attr.depth,
            docked = attr.position,
            position, gaugeAngles;
        if (attr.axisLine && attr.length) {
            switch (docked) {
                case 'left':
                    position = surface.roundPixel(clipRect[2]) - halfLineWidth;
                    ctx.moveTo(position, -attr.endGap + depth);
                    ctx.lineTo(position, attr.length + attr.startGap);
                    break;
                case 'right':
                    ctx.moveTo(halfLineWidth, -attr.endGap);
                    ctx.lineTo(halfLineWidth, attr.length + attr.startGap);
                    break;
                case 'bottom':
                    ctx.moveTo(-attr.startGap, halfLineWidth);
                    ctx.lineTo(attr.length - depth + attr.endGap, halfLineWidth);
                    break;
                case 'top':
                    position = surface.roundPixel(clipRect[3]) - halfLineWidth;
                    ctx.moveTo(-attr.startGap, position);
                    ctx.lineTo(attr.length + attr.endGap, position);
                    break;
                case 'angular':
                    ctx.moveTo(attr.centerX + attr.length, attr.centerY);
                    ctx.arc(attr.centerX, attr.centerY, attr.length, 0, Math.PI * 2, true);
                    break;
                case 'gauge':
                    gaugeAngles = me.getGaugeAngles();
                    ctx.moveTo(attr.centerX + Math.cos(gaugeAngles.start) * attr.length, attr.centerY + Math.sin(gaugeAngles.start) * attr.length);
                    ctx.arc(attr.centerX, attr.centerY, attr.length, gaugeAngles.start, gaugeAngles.end, true);
                    break;
            }
        }
    }
});

/**
 * @class Ext.chart.axis.Axis3D
 * @extends Ext.chart.axis.Axis
 * @xtype axis3d
 *
 * Defines a 3D axis for charts.
 *
 * A 3D axis has the same properties as the regular {@link Ext.chart.axis.Axis axis},
 * plus a notion of depth. The depth of the 3D axis is determined automatically
 * based on the depth of the bound series.
 *
 * This type of axis has the following limitations compared to the regular axis class:
 * - supported {@link Ext.chart.axis.Axis#position positions} are 'left' and 'bottom' only;
 * - floating axes are not supported.
 *
 * At the present moment only {@link Ext.chart.series.Bar3D} series can make use of the 3D axis.
 */
Ext.define('Ext.chart.axis.Axis3D', {
    extend: 'Ext.chart.axis.Axis',
    xtype: 'axis3d',
    requires: [
        'Ext.chart.axis.sprite.Axis3D'
    ],
    config: {
        /**
         * @private
         * The depth of the axis. Determined automatically.
         */
        depth: 0
    },
    /**
         * @cfg {String} position
         * Where to set the axis. Available options are `left` and `bottom`.
         */
    onSeriesChange: function(chart) {
        var me = this,
            eventName = 'depthchange',
            listenerName = 'onSeriesDepthChange',
            i, series;
        function toggle(action) {
            var boundSeries = me.boundSeries;
            for (i = 0; i < boundSeries.length; i++) {
                series = boundSeries[i];
                series[action](eventName, listenerName, me);
            }
        }
        // Remove 'depthchange' listeners from old bound series, if any.
        toggle('un');
        me.callParent(arguments);
        // Add 'depthchange' listeners to new bound series.
        toggle('on');
    },
    onSeriesDepthChange: function(series, depth) {
        var me = this,
            maxDepth = depth,
            boundSeries = me.boundSeries,
            i, item;
        if (depth > me.getDepth()) {
            maxDepth = depth;
        } else {
            for (i = 0; i < boundSeries.length; i++) {
                item = boundSeries[i];
                if (item !== series && item.getDepth) {
                    depth = item.getDepth();
                    if (depth > maxDepth) {
                        maxDepth = depth;
                    }
                }
            }
        }
        me.setDepth(maxDepth);
    },
    updateDepth: function(depth) {
        var me = this,
            sprites = me.getSprites(),
            attr = {
                depth: depth
            };
        if (sprites && sprites.length) {
            sprites[0].setAttributes(attr);
        }
        if (me.gridSpriteEven && me.gridSpriteOdd) {
            me.gridSpriteEven.getTemplate().setAttributes(attr);
            me.gridSpriteOdd.getTemplate().setAttributes(attr);
        }
    },
    getGridAlignment: function() {
        switch (this.getPosition()) {
            case 'left':
            case 'right':
                return 'horizontal3d';
            case 'top':
            case 'bottom':
                return 'vertical3d';
        }
    }
});

/**
 * @class Ext.chart.axis.Category
 * @extends Ext.chart.axis.Axis
 *
 * A type of axis that displays items in categories. This axis is generally used to
 * display categorical information like names of items, month names, quarters, etc.
 * but no quantitative values. For that other type of information
 * {@link Ext.chart.axis.Numeric Numeric} axis are more suitable.
 *
 * As with other axis you can set the position of the axis and its title. For example:
 *
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        innerPadding: '0 40 0 40',
 *        store: {
 *            fields: ['name', 'data1', 'data2', 'data3'],
 *            data: [{
 *                'name': 'metric one',
 *                'data1': 10,
 *                'data2': 12,
 *                'data3': 14
 *            }, {
 *                'name': 'metric two',
 *                'data1': 7,
 *                'data2': 8,
 *                'data3': 16
 *            }, {
 *                'name': 'metric three',
 *                'data1': 5,
 *                'data2': 2,
 *                'data3': 14
 *            }, {
 *                'name': 'metric four',
 *                'data1': 2,
 *                'data2': 14,
 *                'data3': 6
 *            }, {
 *                'name': 'metric five',
 *                'data1': 27,
 *                'data2': 38,
 *                'data3': 36
 *            }]
 *        },
 *        axes: {
 *            type: 'category',
 *            position: 'bottom',
 *            fields: ['name'],
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            }
 *        },
 *        series: {
 *            type: 'area',
 *            subStyle: {
 *                fill: ['#0A3F50', '#30BDA7', '#96D4C6']
 *            },
 *            xField: 'name',
 *            yField: ['data1', 'data2', 'data3']
 *        }
 *     });
 *
 * In this example with set the category axis to the bottom of the surface, bound the axis to
 * the `name` property and set as title "Sample Values".
 */
Ext.define('Ext.chart.axis.Category', {
    requires: [
        'Ext.chart.axis.layout.CombineDuplicate',
        'Ext.chart.axis.segmenter.Names'
    ],
    extend: 'Ext.chart.axis.Axis',
    alias: 'axis.category',
    type: 'category',
    isCategory: true,
    config: {
        layout: 'combineDuplicate',
        segmenter: 'names'
    }
});

/**
 * Category 3D Axis
 */
Ext.define('Ext.chart.axis.Category3D', {
    requires: [
        'Ext.chart.axis.layout.CombineDuplicate',
        'Ext.chart.axis.segmenter.Names'
    ],
    extend: 'Ext.chart.axis.Axis3D',
    alias: 'axis.category3d',
    type: 'category3d',
    config: {
        layout: 'combineDuplicate',
        segmenter: 'names'
    }
});

/**
 * @class Ext.chart.axis.Numeric
 * @extends Ext.chart.axis.Axis
 *
 * An axis to handle numeric values. This axis is used for quantitative data as
 * opposed to the category axis. You can set minimum and maximum values to the
 * axis so that the values are bound to that. If no values are set, then the
 * scale will auto-adjust to the values.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        store: {
 *            fields: ['name', 'data1', 'data2', 'data3'],
 *            data: [{
 *                'name': 1,
 *                'data1': 10,
 *                'data2': 12,
 *                'data3': 14
 *            }, {
 *                'name': 2,
 *                'data1': 7,
 *                'data2': 8,
 *                'data3': 16
 *            }, {
 *                'name': 3,
 *                'data1': 5,
 *                'data2': 2,
 *                'data3': 14
 *            }, {
 *                'name': 4,
 *                'data1': 2,
 *                'data2': 14,
 *                'data3': 6
 *            }, {
 *                'name': 5,
 *                'data1': 27,
 *                'data2': 38,
 *                'data3': 36
 *            }]
 *        },
 *        axes: {
 *            type: 'numeric',
 *            position: 'left',
 *            minimum: 0,
 *            fields: ['data1', 'data2', 'data3'],
 *            title: 'Sample Values',
 *            grid: {
 *                odd: {
 *                    opacity: 1,
 *                    fill: '#F2F2F2',
 *                    stroke: '#DDD',
 *                    'lineWidth': 1
 *                }
 *            }
 *        },
 *        series: {
 *            type: 'area',
 *            subStyle: {
 *                fill: ['#0A3F50', '#30BDA7', '#96D4C6']
 *            },
 *            xField: 'name',
 *            yField: ['data1', 'data2', 'data3']
 *        }
 *     });
 *
 * In this example we create an axis of Numeric type. We set a minimum value so that
 * even if all series have values greater than zero, the grid starts at zero. We bind
 * the axis onto the left part of the surface by setting _position_ to _left_.
 * We bind three different store fields to this axis by setting _fields_ to an array.
 * We set the title of the axis to _Number of Hits_ by using the _title_ property.
 * We use a _grid_ configuration to set odd background rows to a certain style and even rows
 * to be transparent/ignored.
 *
 */
Ext.define('Ext.chart.axis.Numeric', {
    extend: 'Ext.chart.axis.Axis',
    type: 'numeric',
    alias: [
        'axis.numeric',
        'axis.radial'
    ],
    // legacy charts compatibility
    requires: [
        'Ext.chart.axis.layout.Continuous',
        'Ext.chart.axis.segmenter.Numeric'
    ],
    config: {
        layout: 'continuous',
        segmenter: 'numeric',
        aggregator: 'double'
    }
});

/**
 * @class Ext.chart.axis.Numeric3D
 */
Ext.define('Ext.chart.axis.Numeric3D', {
    extend: 'Ext.chart.axis.Axis3D',
    alias: [
        'axis.numeric3d'
    ],
    type: 'numeric3d',
    requires: [
        'Ext.chart.axis.layout.Continuous',
        'Ext.chart.axis.segmenter.Numeric'
    ],
    config: {
        layout: 'continuous',
        segmenter: 'numeric',
        aggregator: 'double'
    }
});

/**
 * @class Ext.chart.axis.Time
 * @extends Ext.chart.axis.Numeric
 *
 * A type of axis whose units are measured in time values. Use this axis
 * for listing dates that you will want to group or dynamically change.
 * If you just want to display dates as categories then use the
 * Category class for axis instead.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        store: {
 *            fields: ['time', 'open', 'high', 'low', 'close'],
 *            data: [{
 *                'time': new Date('Jan 1 2010').getTime(),
 *                'open': 600,
 *                'high': 614,
 *                'low': 578,
 *                'close': 590
 *            }, {
 *                'time': new Date('Jan 2 2010').getTime(),
 *                'open': 590,
 *                'high': 609,
 *                'low': 580,
 *                'close': 580
 *            }, {
 *                'time': new Date('Jan 3 2010').getTime(),
 *                'open': 580,
 *                'high': 602,
 *                'low': 578,
 *                'close': 602
 *            }, {
 *                'time': new Date('Jan 4 2010').getTime(),
 *                'open': 602,
 *                'high': 614,
 *                'low': 586,
 *                'close': 586
 *            }]
 *        },
 *        axes: [{
 *            type: 'numeric',
 *            position: 'left',
 *            fields: ['open', 'high', 'low', 'close'],
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            },
 *            grid: true,
 *            minimum: 560,
 *            maximum: 640
 *        }, {
 *            type: 'time',
 *            position: 'bottom',
 *            fields: ['time'],
 *            fromDate: new Date('Dec 31 2009'),
 *            toDate: new Date('Jan 5 2010'),
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            },
 *            style: {
 *                axisLine: false
 *            }
 *        }],
 *        series: {
 *            type: 'candlestick',
 *            xField: 'time',
 *            openField: 'open',
 *            highField: 'high',
 *            lowField: 'low',
 *            closeField: 'close',
 *            style: {
 *                ohlcType: 'ohlc',
 *                dropStyle: {
 *                    fill: 'rgb(255, 128, 128)',
 *                    stroke: 'rgb(255, 128, 128)',
 *                    lineWidth: 3
 *                },
 *                raiseStyle: {
 *                    fill: 'rgb(48, 189, 167)',
 *                    stroke: 'rgb(48, 189, 167)',
 *                    lineWidth: 3
 *                }
 *            }
 *        }
 *     });
 */
Ext.define('Ext.chart.axis.Time', {
    extend: 'Ext.chart.axis.Numeric',
    alias: 'axis.time',
    type: 'time',
    requires: [
        'Ext.chart.axis.layout.Continuous',
        'Ext.chart.axis.segmenter.Time'
    ],
    config: {
        /**
         * @cfg {String} dateFormat
         * Indicates the format the date will be rendered in.
         * For example: 'M d' will render the dates as 'Jan 30'.
         * This config works by setting the {@link #renderer} config
         * to a function that uses {@link Ext.Date#format} to format the dates
         * using the given `dateFormat`.
         * If the {@link #renderer} config was set by the user, changes to this config
         * won't replace the user set renderer (until the user removes the renderer by
         * setting the `renderer` config to `null`). In this case the way the `dateFormat`
         * is used (if at all) is up to the user.
         */
        dateFormat: null,
        /**
         * @cfg {Date} fromDate The starting date for the time axis.
         */
        fromDate: null,
        /**
         * @cfg {Date} toDate The ending date for the time axis.
         */
        toDate: null,
        layout: 'continuous',
        segmenter: 'time',
        aggregator: 'time'
    },
    updateDateFormat: function(format) {
        var renderer = this.getRenderer();
        if (!renderer || renderer.isDefault) {
            renderer = function(axis, date) {
                return Ext.Date.format(new Date(date), format);
            };
            renderer.isDefault = true;
            this.setRenderer(renderer);
            this.performLayout();
        }
    },
    updateRenderer: function(renderer) {
        var dateFormat = this.getDateFormat();
        if (renderer) {
            this.performLayout();
        } else if (dateFormat) {
            // If the user removes custom `renderer` and `dateFormat` is set,
            // set the `renderer` to the default one based on `dateFormat`.
            this.updateDateFormat(dateFormat);
        }
    },
    updateFromDate: function(date) {
        this.setMinimum(+date);
    },
    updateToDate: function(date) {
        this.setMaximum(+date);
    },
    getCoordFor: function(value) {
        if (Ext.isString(value)) {
            value = new Date(value);
        }
        return +value;
    }
});

/**
 * @class Ext.chart.axis.Time3D
 */
Ext.define('Ext.chart.axis.Time3D', {
    extend: 'Ext.chart.axis.Numeric3D',
    alias: 'axis.time3d',
    type: 'time3d',
    requires: [
        'Ext.chart.axis.layout.Continuous',
        'Ext.chart.axis.segmenter.Time'
    ],
    config: {
        /**
         * @cfg {String/Boolean} dateFormat
         * Indicates the format the date will be rendered on.
         * For example: 'M d' will render the dates as 'Jan 30', etc.
         */
        dateFormat: null,
        /**
         * @cfg {Date} fromDate The starting date for the time axis.
         */
        fromDate: null,
        /**
         * @cfg {Date} toDate The ending date for the time axis.
         */
        toDate: null,
        layout: 'continuous',
        segmenter: 'time',
        aggregator: 'time'
    },
    updateDateFormat: function(format) {
        this.setRenderer(function(axis, date) {
            return Ext.Date.format(new Date(date), format);
        });
    },
    updateFromDate: function(date) {
        this.setMinimum(+date);
    },
    updateToDate: function(date) {
        this.setMaximum(+date);
    },
    getCoordFor: function(value) {
        if (Ext.isString(value)) {
            value = new Date(value);
        }
        return +value;
    }
});

/**
 * @class Ext.chart.grid.HorizontalGrid3D
 * @extends Ext.chart.grid.HorizontalGrid
 *
 * Horizontal 3D Grid sprite. Used in 3D Cartesian Charts.
 */
Ext.define('Ext.chart.grid.HorizontalGrid3D', {
    extend: 'Ext.chart.grid.HorizontalGrid',
    alias: 'grid.horizontal3d',
    inheritableStatics: {
        def: {
            processors: {
                depth: 'number'
            },
            defaults: {
                depth: 0
            }
        }
    },
    render: function(surface, ctx, rect) {
        var attr = this.attr,
            x = surface.roundPixel(attr.x),
            y = surface.roundPixel(attr.y),
            dx = surface.matrix.getDX(),
            halfLineWidth = ctx.lineWidth * 0.5,
            height = attr.height,
            depth = attr.depth,
            left, top;
        if (y <= rect[1]) {
            return;
        }
        // Horizontal stripe.
        left = rect[0] + depth - dx;
        top = y + halfLineWidth - depth;
        ctx.beginPath();
        ctx.rect(left, top, rect[2], height);
        ctx.fill();
        // Horizontal line.
        ctx.beginPath();
        ctx.moveTo(left, top);
        ctx.lineTo(left + rect[2], top);
        ctx.stroke();
        // Diagonal stripe.
        left = rect[0] + x - dx;
        top = y + halfLineWidth;
        ctx.beginPath();
        ctx.moveTo(left, top);
        ctx.lineTo(left + depth, top - depth);
        ctx.lineTo(left + depth, top - depth + height);
        ctx.lineTo(left, top + height);
        ctx.closePath();
        ctx.fill();
        // Diagonal line.
        ctx.beginPath();
        ctx.moveTo(left, top);
        ctx.lineTo(left + depth, top - depth);
        ctx.stroke();
    }
});

/**
 * @class Ext.chart.grid.VerticalGrid3D
 * @extends Ext.chart.grid.VerticalGrid
 * 
 * Vertical 3D Grid sprite. Used in 3D Cartesian Charts.
 */
Ext.define('Ext.chart.grid.VerticalGrid3D', {
    extend: 'Ext.chart.grid.VerticalGrid',
    alias: 'grid.vertical3d',
    inheritableStatics: {
        def: {
            processors: {
                depth: 'number'
            },
            defaults: {
                depth: 0
            }
        }
    },
    render: function(surface, ctx, clipRect) {
        var attr = this.attr,
            x = surface.roundPixel(attr.x),
            dy = surface.matrix.getDY(),
            halfLineWidth = ctx.lineWidth * 0.5,
            width = attr.width,
            depth = attr.depth,
            left, top;
        if (x >= clipRect[2]) {
            return;
        }
        // Vertical stripe.
        left = x - halfLineWidth + depth;
        top = clipRect[1] - depth - dy;
        ctx.beginPath();
        ctx.rect(left, top, width, clipRect[3]);
        ctx.fill();
        // Vertical line.
        ctx.beginPath();
        ctx.moveTo(left, top);
        ctx.lineTo(left, top + clipRect[3]);
        ctx.stroke();
        // Diagonal stripe.
        left = x - halfLineWidth;
        top = clipRect[3];
        ctx.beginPath();
        ctx.moveTo(left, top);
        ctx.lineTo(left + depth, top - depth);
        ctx.lineTo(left + depth + width, top - depth);
        ctx.lineTo(left + width, top);
        ctx.closePath();
        ctx.fill();
        // Diagonal line.
        left = x - halfLineWidth;
        top = clipRect[3];
        ctx.beginPath();
        ctx.moveTo(left, top);
        ctx.lineTo(left + depth, top - depth);
        ctx.stroke();
    }
});

/**
 * @class Ext.chart.interactions.CrossZoom
 * @extends Ext.chart.interactions.Abstract
 *
 * The CrossZoom interaction allows the user to zoom in on a selected area of the chart.
 *
 *     @example
 *     Ext.create({
 *         xtype: 'cartesian',
 *         renderTo: Ext.getBody(),
 *         width: 600,
 *         height: 400,
 *         insetPadding: 40,
 *         interactions: 'crosszoom',
 *         store: {
 *             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
 *             }]
 *         },
 *         axes: [{
 *             type: 'numeric',
 *             position: 'left',
 *             fields: ['data1'],
 *             title: {
 *                 text: 'Sample Values',
 *                 fontSize: 15
 *             },
 *             grid: true,
 *             minimum: 0
 *         }, {
 *             type: 'category',
 *             position: 'bottom',
 *             fields: ['name'],
 *             title: {
 *                 text: 'Sample Values',
 *                 fontSize: 15
 *             }
 *         }],
 *         series: [{
 *             type: 'line',
 *             highlight: {
 *                 size: 7,
 *                 radius: 7
 *             },
 *             style: {
 *                 stroke: 'rgb(143,203,203)'
 *             },
 *             xField: 'name',
 *             yField: 'data1',
 *             marker: {
 *                 type: 'path',
 *                 path: ['M', - 2, 0, 0, 2, 2, 0, 0, - 2, 'Z'],
 *                 stroke: 'blue',
 *                 lineWidth: 0
 *             }
 *         }, {
 *             type: 'line',
 *             highlight: {
 *                 size: 7,
 *                 radius: 7
 *             },
 *             fill: true,
 *             xField: 'name',
 *             yField: 'data3',
 *             marker: {
 *                 type: 'circle',
 *                 radius: 4,
 *                 lineWidth: 0
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.chart.interactions.CrossZoom', {
    extend: 'Ext.chart.interactions.Abstract',
    type: 'crosszoom',
    alias: 'interaction.crosszoom',
    isCrossZoom: true,
    config: {
        /**
         * @cfg {Object/Array} axes
         * Specifies which axes should be made navigable. The config value can take the following
         * formats:
         *
         * - An Object whose keys correspond to the {@link Ext.chart.axis.Axis#position position}
         *   of each axis that should be made navigable. Each key's value can either be an Object
         *   with further configuration options for each axis or simply `true` for a default
         *   set of options.
         *       {
         *           type: 'crosszoom',
         *           axes: {
         *               left: {
         *                   maxZoom: 5,
         *                   allowPan: false
         *               },
         *               bottom: true
         *           }
         *       }
         *
         *   If using the full Object form, the following options can be specified for each axis:
         *
         *   - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its
         *     natural size.
         *   - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
         *   - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
         *   - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
         *   - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
         *   - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
         *
         * - An Array of strings, each one corresponding to the
         *   {@link Ext.chart.axis.Axis#position position} of an axis that should be made navigable.
         *   The default options will be used for each named axis.
         *
         *       {
         *           type: 'crosszoom',
         *           axes: ['left', 'bottom']
         *       }
         *
         * If the `axes` config is not specified, it will default to making all axes navigable
         * with the default axis options.
         */
        axes: true,
        gestures: {
            dragstart: 'onGestureStart',
            drag: 'onGesture',
            dragend: 'onGestureEnd',
            dblclick: 'onDoubleTap'
        },
        undoButton: {}
    },
    stopAnimationBeforeSync: false,
    zoomAnimationInProgress: false,
    constructor: function() {
        this.callParent(arguments);
        this.zoomHistory = [];
    },
    applyAxes: function(axesConfig) {
        var result = {};
        if (axesConfig === true) {
            return {
                top: {},
                right: {},
                bottom: {},
                left: {}
            };
        } else if (Ext.isArray(axesConfig)) {
            // array of axis names - translate to full object form
            result = {};
            Ext.each(axesConfig, function(axis) {
                result[axis] = {};
            });
        } else if (Ext.isObject(axesConfig)) {
            Ext.iterate(axesConfig, function(key, val) {
                // axis name with `true` value -> translate to object
                if (val === true) {
                    result[key] = {};
                } else if (val !== false) {
                    result[key] = val;
                }
            });
        }
        return result;
    },
    applyUndoButton: function(button, oldButton) {
        var me = this;
        if (oldButton) {
            oldButton.destroy();
        }
        if (button) {
            return Ext.create('Ext.Button', Ext.apply({
                cls: [],
                text: 'Undo Zoom',
                disabled: true,
                handler: function() {
                    me.undoZoom();
                }
            }, button));
        }
    },
    getSurface: function() {
        return this.getChart() && this.getChart().getSurface('overlay');
    },
    setSeriesOpacity: function(opacity) {
        var surface = this.getChart() && this.getChart().getSurface('series');
        if (surface) {
            surface.element.setStyle('opacity', opacity);
        }
    },
    onGestureStart: function(e) {
        var me = this,
            chart = me.getChart(),
            surface = me.getSurface(),
            rect = chart.getInnerRect(),
            innerPadding = chart.getInnerPadding(),
            minX = innerPadding.left,
            maxX = minX + rect[2],
            minY = innerPadding.top,
            maxY = minY + rect[3],
            xy = chart.getEventXY(e),
            x = xy[0],
            y = xy[1];
        e.claimGesture();
        if (me.zoomAnimationInProgress) {
            return;
        }
        if (x > minX && x < maxX && y > minY && y < maxY) {
            me.gestureEvent = 'drag';
            me.lockEvents(me.gestureEvent);
            me.startX = x;
            me.startY = y;
            me.selectionRect = surface.add({
                type: 'rect',
                globalAlpha: 0.5,
                fillStyle: 'rgba(80,80,140,0.5)',
                strokeStyle: 'rgba(80,80,140,1)',
                lineWidth: 2,
                x: x,
                y: y,
                width: 0,
                height: 0,
                zIndex: 10000
            });
            me.setSeriesOpacity(0.8);
            return false;
        }
    },
    onGesture: function(e) {
        var me = this;
        if (me.zoomAnimationInProgress) {
            return;
        }
        if (me.getLocks()[me.gestureEvent] === me) {
            // eslint-disable-next-line vars-on-top, one-var
            var chart = me.getChart(),
                surface = me.getSurface(),
                rect = chart.getInnerRect(),
                innerPadding = chart.getInnerPadding(),
                minX = innerPadding.left,
                maxX = minX + rect[2],
                minY = innerPadding.top,
                maxY = minY + rect[3],
                xy = chart.getEventXY(e),
                x = xy[0],
                y = xy[1];
            if (x < minX) {
                x = minX;
            } else if (x > maxX) {
                x = maxX;
            }
            if (y < minY) {
                y = minY;
            } else if (y > maxY) {
                y = maxY;
            }
            me.selectionRect.setAttributes({
                width: x - me.startX,
                height: y - me.startY
            });
            if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) {
                me.selectionRect.setAttributes({
                    globalAlpha: 0.5
                });
            } else {
                me.selectionRect.setAttributes({
                    globalAlpha: 1
                });
            }
            surface.renderFrame();
            return false;
        }
    },
    onGestureEnd: function(e) {
        var me = this;
        if (me.zoomAnimationInProgress) {
            return;
        }
        if (me.getLocks()[me.gestureEvent] === me) {
            // eslint-disable-next-line vars-on-top, one-var
            var chart = me.getChart(),
                surface = me.getSurface(),
                rect = chart.getInnerRect(),
                innerPadding = chart.getInnerPadding(),
                minX = innerPadding.left,
                maxX = minX + rect[2],
                minY = innerPadding.top,
                maxY = minY + rect[3],
                rectWidth = rect[2],
                rectHeight = rect[3],
                xy = chart.getEventXY(e),
                x = xy[0],
                y = xy[1];
            if (x < minX) {
                x = minX;
            } else if (x > maxX) {
                x = maxX;
            }
            if (y < minY) {
                y = minY;
            } else if (y > maxY) {
                y = maxY;
            }
            if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) {
                surface.remove(me.selectionRect);
            } else {
                me.zoomBy([
                    Math.min(me.startX, x) / rectWidth,
                    1 - Math.max(me.startY, y) / rectHeight,
                    Math.max(me.startX, x) / rectWidth,
                    1 - Math.min(me.startY, y) / rectHeight
                ]);
                me.selectionRect.setAttributes({
                    x: Math.min(me.startX, x),
                    y: Math.min(me.startY, y),
                    width: Math.abs(me.startX - x),
                    height: Math.abs(me.startY - y)
                });
                me.selectionRect.setAnimation(chart.getAnimation() || {
                    duration: 0
                });
                me.selectionRect.setAttributes({
                    globalAlpha: 0,
                    x: 0,
                    y: 0,
                    width: rectWidth,
                    height: rectHeight
                });
                me.zoomAnimationInProgress = true;
                chart.suspendThicknessChanged();
                me.selectionRect.getAnimation().on('animationend', function() {
                    chart.resumeThicknessChanged();
                    surface.remove(me.selectionRect);
                    me.selectionRect = null;
                    me.zoomAnimationInProgress = false;
                });
            }
            surface.renderFrame();
            me.sync();
            me.unlockEvents(me.gestureEvent);
            me.setSeriesOpacity(1);
            if (!me.zoomAnimationInProgress) {
                surface.remove(me.selectionRect);
                me.selectionRect = null;
            }
        }
    },
    zoomBy: function(rect) {
        var me = this,
            axisConfigs = me.getAxes(),
            chart = me.getChart(),
            axes = chart.getAxes(),
            isRtl = chart.getInherited().rtl,
            zoomMap = {},
            config, axis, x1, x2, isSide, oldRange, i;
        if (isRtl) {
            rect = rect.slice();
            x1 = 1 - rect[0];
            x2 = 1 - rect[2];
            rect[0] = Math.min(x1, x2);
            rect[2] = Math.max(x1, x2);
        }
        for (i = 0; i < axes.length; i++) {
            axis = axes[i];
            config = axisConfigs[axis.getPosition()];
            if (config && config.allowZoom !== false) {
                isSide = axis.isSide();
                oldRange = axis.getVisibleRange();
                zoomMap[axis.getId()] = oldRange.slice(0);
                if (!isSide) {
                    axis.setVisibleRange([
                        (oldRange[1] - oldRange[0]) * rect[0] + oldRange[0],
                        (oldRange[1] - oldRange[0]) * rect[2] + oldRange[0]
                    ]);
                } else {
                    axis.setVisibleRange([
                        (oldRange[1] - oldRange[0]) * rect[1] + oldRange[0],
                        (oldRange[1] - oldRange[0]) * rect[3] + oldRange[0]
                    ]);
                }
            }
        }
        me.zoomHistory.push(zoomMap);
        me.getUndoButton().setDisabled(false);
    },
    undoZoom: function() {
        var zoomMap = this.zoomHistory.pop(),
            axes = this.getChart().getAxes(),
            axis, i;
        if (zoomMap) {
            for (i = 0; i < axes.length; i++) {
                axis = axes[i];
                if (zoomMap[axis.getId()]) {
                    axis.setVisibleRange(zoomMap[axis.getId()]);
                }
            }
        }
        this.getUndoButton().setDisabled(this.zoomHistory.length === 0);
        this.sync();
    },
    onDoubleTap: function(e) {
        this.undoZoom();
    },
    destroy: function() {
        this.setUndoButton(null);
        this.callParent();
    }
});

/**
 * The Crosshair interaction allows the user to get precise values for a specific point
 * on the chart. The values are obtained by single-touch dragging on the chart.
 *
 *     @example
 *     Ext.create('Ext.Container', {
 *         renderTo: Ext.getBody(),
 *         width: 600,
 *         height: 400,
 *         layout: 'fit',
 *         items: {
 *             xtype: 'cartesian',
 *             innerPadding: 20,
 *             interactions: {
 *                 type: 'crosshair',
 *                 axes: {
 *                     left: {
 *                         label: {
 *                             fillStyle: 'white'
 *                         },
 *                         rect: {
 *                             fillStyle: 'brown',
 *                             radius: 6
 *                         }
 *                     },
 *                     bottom: {
 *                         label: {
 *                             fontSize: '14px',
 *                             fontWeight: 'bold'
 *                         }
 *                     }
 *                 },
 *                 lines: {
 *                     horizontal: {
 *                         strokeStyle: 'brown',
 *                         lineWidth: 2,
 *                         lineDash: [20, 2, 2, 2, 2, 2, 2, 2]
 *                     }
 *                 }
 *             },
 *             store: {
 *                 fields: ['name', 'data'],
 *                 data: [
 *                     {name: 'apple', data: 300},
 *                     {name: 'orange', data: 900},
 *                     {name: 'banana', data: 800},
 *                     {name: 'pear', data: 400},
 *                     {name: 'grape', data: 500}
 *                 ]
 *             },
 *             axes: [{
 *                 type: 'numeric',
 *                 position: 'left',
 *                 fields: ['data'],
 *                 title: {
 *                     text: 'Value',
 *                     fontSize: 15
 *                 },
 *                 grid: true,
 *                 label: {
 *                     rotationRads: -Math.PI / 4
 *                 }
 *             }, {
 *                 type: 'category',
 *                 position: 'bottom',
 *                 fields: ['name'],
 *                 title: {
 *                     text: 'Category',
 *                     fontSize: 15
 *                 }
 *             }],
 *             series: {
 *                 type: 'line',
 *                 style: {
 *                     strokeStyle: 'black'
 *                 },
 *                 xField: 'name',
 *                 yField: 'data',
 *                 marker: {
 *                     type: 'circle',
 *                     radius: 5,
 *                     fillStyle: 'lightblue'
 *                 }
 *             }
 *         }
 *     });
 */
Ext.define('Ext.chart.interactions.Crosshair', {
    extend: 'Ext.chart.interactions.Abstract',
    requires: [
        'Ext.chart.grid.HorizontalGrid',
        'Ext.chart.grid.VerticalGrid',
        'Ext.chart.CartesianChart',
        'Ext.chart.axis.layout.Discrete'
    ],
    type: 'crosshair',
    alias: 'interaction.crosshair',
    config: {
        /**
         * @cfg {Object} axes
         * Specifies label text and label rect configs on per axis basis or as a single config
         * for all axes.
         *
         *     {
         *         type: 'crosshair',
         *         axes: {
         *             label: { fillStyle: 'white' },
         *             rect: { fillStyle: 'maroon'}
         *         }
         *     }
         *
         * In case per axis configuration is used, an object with keys corresponding
         * to the {@link Ext.chart.axis.Axis#position position} must be provided.
         *
         *     {
         *         type: 'crosshair',
         *         axes: {
         *             left: {
         *                 label: { fillStyle: 'white' },
         *                 rect: {
         *                     fillStyle: 'maroon',
         *                     radius: 4
         *                 }
         *             },
         *             bottom: {
         *                 label: {
         *                     fontSize: '14px',
         *                     fontWeight: 'bold'
         *                 },
         *                 rect: { fillStyle: 'white' }
         *             }
         *         }
         *
         * If the `axes` config is not specified, the following defaults will be used:
         * - `label` will use values from the {@link Ext.chart.axis.Axis#label label} config.
         * - `rect` will use the 'white' fillStyle.
         */
        axes: {
            top: {
                label: {},
                rect: {}
            },
            right: {
                label: {},
                rect: {}
            },
            bottom: {
                label: {},
                rect: {}
            },
            left: {
                label: {},
                rect: {}
            }
        },
        /**
         * @cfg {Object} lines
         * Specifies attributes of horizontal and vertical lines that make up the crosshair.
         * If this config is missing, black dashed lines will be used.
         *
         *     {
         *         horizontal: {
         *             strokeStyle: 'red',
         *             lineDash: [] // solid line
         *         },
         *         vertical: {
         *             lineWidth: 2,
         *             lineDash: [15, 5, 5, 5]
         *         }
         *     }
         */
        lines: {
            horizontal: {
                strokeStyle: 'black',
                lineDash: [
                    5,
                    5
                ]
            },
            vertical: {
                strokeStyle: 'black',
                lineDash: [
                    5,
                    5
                ]
            }
        },
        /**
         * @cfg {String} gesture
         * Specifies which gesture should be used for starting/maintaining/ending the interaction.
         */
        gesture: 'drag'
    },
    applyAxes: function(axesConfig, oldAxesConfig) {
        return Ext.merge(oldAxesConfig || {}, axesConfig);
    },
    applyLines: function(linesConfig, oldLinesConfig) {
        return Ext.merge(oldLinesConfig || {}, linesConfig);
    },
    updateChart: function(chart) {
        if (chart && !chart.isCartesian) {
            Ext.raise("Crosshair interaction can only be used on cartesian charts.");
        }
        this.callParent(arguments);
    },
    getGestures: function() {
        var me = this,
            gestures = {},
            gesture = me.getGesture();
        gestures[gesture] = 'onGesture';
        gestures[gesture + 'start'] = 'onGestureStart';
        gestures[gesture + 'end'] = 'onGestureEnd';
        gestures[gesture + 'cancel'] = 'onGestureCancel';
        return gestures;
    },
    onGestureStart: function(e) {
        var me = this,
            chart = me.getChart(),
            axesTheme = chart.getTheme().getAxis(),
            axisTheme,
            surface = chart.getSurface('overlay'),
            rect = chart.getInnerRect(),
            chartWidth = rect[2],
            chartHeight = rect[3],
            xy = chart.getEventXY(e),
            x = xy[0],
            y = xy[1],
            axes = chart.getAxes(),
            axesConfig = me.getAxes(),
            linesConfig = me.getLines(),
            axis, axisSurface, axisRect, axisWidth, axisHeight, axisPosition, axisAlignment, axisLabel, axisLabelConfig, crosshairLabelConfig, tickPadding, axisSprite, attr, lineWidth, halfLineWidth, title, titleBBox, horizontalLineCfg, verticalLineCfg, i;
        e.claimGesture();
        if (x > 0 && x < chartWidth && y > 0 && y < chartHeight) {
            me.lockEvents(me.getGesture());
            horizontalLineCfg = Ext.apply({
                xclass: 'Ext.chart.grid.HorizontalGrid',
                x: 0,
                y: y,
                width: chartWidth
            }, linesConfig.horizontal);
            verticalLineCfg = Ext.apply({
                xclass: 'Ext.chart.grid.VerticalGrid',
                x: x,
                y: 0,
                height: chartHeight
            }, linesConfig.vertical);
            me.axesLabels = me.axesLabels || {};
            for (i = 0; i < axes.length; i++) {
                axis = axes[i];
                axisSurface = axis.getSurface();
                axisRect = axisSurface.getRect();
                axisSprite = axis.getSprites()[0];
                axisWidth = axisRect[2];
                axisHeight = axisRect[3];
                axisPosition = axis.getPosition();
                axisAlignment = axis.getAlignment();
                title = axis.getTitle();
                titleBBox = title && title.attr.text !== '' && title.getBBox();
                attr = axisSprite.attr;
                lineWidth = attr.axisLine ? attr.lineWidth : 0;
                halfLineWidth = lineWidth / 2;
                tickPadding = Math.max(attr.majorTickSize, attr.minorTickSize) + lineWidth;
                axisLabel = me.axesLabels[axisPosition] = axisSurface.add({
                    type: 'composite'
                });
                axisLabel.labelRect = axisLabel.addSprite(Ext.apply({
                    type: 'rect',
                    fillStyle: 'white',
                    x: axisPosition === 'right' ? lineWidth : 0,
                    y: axisPosition === 'bottom' ? lineWidth : 0,
                    width: axisWidth - lineWidth - (axisAlignment === 'vertical' && titleBBox ? titleBBox.width : 0),
                    height: axisHeight - lineWidth - (axisAlignment === 'horizontal' && titleBBox ? titleBBox.height : 0),
                    translationX: axisPosition === 'left' && titleBBox ? titleBBox.width : 0,
                    translationY: axisPosition === 'top' && titleBBox ? titleBBox.height : 0
                }, axesConfig.rect || axesConfig[axisPosition].rect));
                if (axisAlignment === 'vertical' && !verticalLineCfg.strokeStyle) {
                    verticalLineCfg.strokeStyle = attr.strokeStyle;
                }
                if (axisAlignment === 'horizontal' && !horizontalLineCfg.strokeStyle) {
                    horizontalLineCfg.strokeStyle = attr.strokeStyle;
                }
                axisTheme = Ext.merge({}, axesTheme.defaults, axesTheme[axisPosition]);
                axisLabelConfig = Ext.apply({}, axis.config.label, axisTheme.label);
                crosshairLabelConfig = axesConfig.label || axesConfig[axisPosition].label;
                axisLabel.labelText = axisLabel.addSprite(Ext.apply(axisLabelConfig, crosshairLabelConfig, {
                    type: 'text',
                    x: me.calculateLabelTextPoint(false, axisPosition, tickPadding, titleBBox, axisWidth, halfLineWidth),
                    y: me.calculateLabelTextPoint(true, axisPosition, tickPadding, titleBBox, axisHeight, halfLineWidth)
                }));
            }
            me.horizontalLine = surface.add(horizontalLineCfg);
            me.verticalLine = surface.add(verticalLineCfg);
            return false;
        }
    },
    onGesture: function(e) {
        var me = this;
        if (me.getLocks()[me.getGesture()] !== me) {
            return;
        }
        // eslint-disable-next-line vars-on-top, one-var
        var chart = me.getChart(),
            surface = chart.getSurface('overlay'),
            rect = Ext.Array.slice(chart.getInnerRect()),
            padding = chart.getInnerPadding(),
            px = padding.left,
            py = padding.top,
            chartWidth = rect[2],
            chartHeight = rect[3],
            xy = chart.getEventXY(e),
            x = xy[0],
            y = xy[1],
            axes = chart.getAxes(),
            axis, axisPosition, axisAlignment, axisSurface, axisSprite, axisMatrix, axisLayoutContext, axisSegmenter, axisLabel, labelBBox, textPadding, xx, yy, dx, dy, xValue, yValue, text, i;
        if (x < 0) {
            x = 0;
        } else if (x > chartWidth) {
            x = chartWidth;
        }
        if (y < 0) {
            y = 0;
        } else if (y > chartHeight) {
            y = chartHeight;
        }
        x += px;
        y += py;
        for (i = 0; i < axes.length; i++) {
            axis = axes[i];
            axisPosition = axis.getPosition();
            axisAlignment = axis.getAlignment();
            axisSurface = axis.getSurface();
            axisSprite = axis.getSprites()[0];
            axisMatrix = axisSprite.attr.matrix;
            textPadding = axisSprite.attr.textPadding * 2;
            axisLabel = me.axesLabels[axisPosition];
            axisLayoutContext = axisSprite.getLayoutContext();
            axisSegmenter = axis.getSegmenter();
            if (axisLabel) {
                if (axisAlignment === 'vertical') {
                    yy = axisMatrix.getYY();
                    dy = axisMatrix.getDY();
                    yValue = (y - dy - py) / yy;
                    if (axis.getLayout() instanceof Ext.chart.axis.layout.Discrete) {
                        y = Math.round(yValue) * yy + dy + py;
                        yValue = axisSegmenter.from(Math.round(yValue));
                        yValue = axisSprite.attr.data[yValue];
                    } else {
                        yValue = axisSegmenter.from(yValue);
                    }
                    text = axisSegmenter.renderer(yValue, axisLayoutContext);
                    axisLabel.setAttributes({
                        translationY: y - py
                    });
                    axisLabel.labelText.setAttributes({
                        text: text
                    });
                    labelBBox = axisLabel.labelText.getBBox();
                    axisLabel.labelRect.setAttributes({
                        height: labelBBox.height + textPadding,
                        y: -(labelBBox.height + textPadding) / 2
                    });
                    axisSurface.renderFrame();
                } else {
                    xx = axisMatrix.getXX();
                    dx = axisMatrix.getDX();
                    xValue = (x - dx - px) / xx;
                    if (axis.getLayout() instanceof Ext.chart.axis.layout.Discrete) {
                        x = Math.round(xValue) * xx + dx + px;
                        xValue = axisSegmenter.from(Math.round(xValue));
                        xValue = axisSprite.attr.data[xValue];
                    } else {
                        xValue = axisSegmenter.from(xValue);
                    }
                    text = axisSegmenter.renderer(xValue, axisLayoutContext);
                    axisLabel.setAttributes({
                        translationX: x - px
                    });
                    axisLabel.labelText.setAttributes({
                        text: text
                    });
                    labelBBox = axisLabel.labelText.getBBox();
                    axisLabel.labelRect.setAttributes({
                        width: labelBBox.width + textPadding,
                        x: -(labelBBox.width + textPadding) / 2
                    });
                    axisSurface.renderFrame();
                }
            }
        }
        me.horizontalLine.setAttributes({
            y: y,
            strokeStyle: axisSprite.attr.strokeStyle
        });
        me.verticalLine.setAttributes({
            x: x,
            strokeStyle: axisSprite.attr.strokeStyle
        });
        surface.renderFrame();
        return false;
    },
    onGestureEnd: function(e) {
        var me = this,
            chart = me.getChart(),
            surface = chart.getSurface('overlay'),
            axes = chart.getAxes(),
            axis, axisPosition, axisSurface, axisLabel, i;
        surface.remove(me.verticalLine);
        surface.remove(me.horizontalLine);
        for (i = 0; i < axes.length; i++) {
            axis = axes[i];
            axisPosition = axis.getPosition();
            axisSurface = axis.getSurface();
            axisLabel = me.axesLabels[axisPosition];
            if (axisLabel) {
                delete me.axesLabels[axisPosition];
                axisSurface.remove(axisLabel);
            }
            axisSurface.renderFrame();
        }
        surface.renderFrame();
        me.unlockEvents(me.getGesture());
    },
    onGestureCancel: function(e) {
        this.onGestureEnd(e);
    },
    privates: {
        vertMap: {
            top: 'start',
            bottom: 'end'
        },
        horzMap: {
            left: 'start',
            right: 'end'
        },
        calculateLabelTextPoint: function(vertical, position, tickPadding, titleBBox, axisSize, halfLineWidth) {
            var titlePadding, sizeProp, pointProp;
            if (vertical) {
                pointProp = 'y';
                sizeProp = 'height';
                position = this.vertMap[position];
            } else {
                pointProp = 'x';
                sizeProp = 'width';
                position = this.horzMap[position];
            }
            switch (position) {
                case 'start':
                    titlePadding = titleBBox ? titleBBox[pointProp] + titleBBox[sizeProp] : 0;
                    return titlePadding + (axisSize - titlePadding - tickPadding) / 2 - halfLineWidth;
                case 'end':
                    titlePadding = titleBBox ? axisSize - titleBBox[pointProp] : 0;
                    return tickPadding + (axisSize - tickPadding - titlePadding) / 2 + halfLineWidth;
                default:
                    return 0;
            }
        }
    }
});

/**
 * @class Ext.chart.interactions.ItemHighlight
 * @extends Ext.chart.interactions.Abstract
 *
 * The 'itemhighlight' interaction allows the user to highlight series items in the chart.
 */
Ext.define('Ext.chart.interactions.ItemHighlight', {
    extend: 'Ext.chart.interactions.Abstract',
    type: 'itemhighlight',
    alias: 'interaction.itemhighlight',
    isItemHighlight: true,
    config: {
        gestures: {
            tap: 'onTapGesture',
            mousemove: 'onMouseMoveGesture',
            mousedown: 'onMouseDownGesture',
            mouseup: 'onMouseUpGesture',
            mouseleave: 'onMouseUpGesture'
        },
        /**
         * @cfg {Boolean} [sticky=false]
         * Disables mouse tracking.
         * Series items will only be highlighted/unhighlighted on mouse click.
         * This config has no effect on touch devices.
         */
        sticky: false,
        /**
         * @cfg {Boolean} [multiTooltips=false]
         * Enable displaying multiple tooltips for overlapping or adjacent series items within
         * {@link Ext.chart.series.Line#selectionTolerance} radius.
         * Default is to display a tooltip only for the last series item rendered.
         * When multiple tooltips are displayed, they may overlap partially or completely;
         * it is up to the developer to ensure tooltip positioning is satisfactory.
         * 
         * @since 6.6.0
         */
        multiTooltips: false
    },
    constructor: function(config) {
        this.callParent([
            config
        ]);
        this.stickyHighlightItem = null;
        this.tooltipItems = [];
    },
    destroy: function() {
        this.stickyHighlightItem = this.tooltipItems = null;
        this.callParent();
    },
    onMouseMoveGesture: function(e) {
        var me = this,
            tooltipItems = me.tooltipItems,
            isMousePointer = e.pointerType === 'mouse',
            tooltips = [],
            item, oldItem, items, tooltip, oldTooltip, i, len, j, jLen;
        if (me.getSticky()) {
            return true;
        }
        if (isMousePointer && me.stickyHighlightItem) {
            me.stickyHighlightItem = null;
            me.highlight(null);
        }
        if (me.isDragging) {
            if (tooltipItems.length && isMousePointer) {
                me.hideTooltips(tooltipItems);
                tooltipItems.length = 0;
            }
        } else if (!me.stickyHighlightItem) {
            if (me.getMultiTooltips()) {
                items = me.getItemsForEvent(e);
            } else {
                item = me.getItemForEvent(e);
                items = item ? [
                    item
                ] : [];
            }
            for (i = 0 , len = items.length; i < len; i++) {
                item = items[i];
                // Items are returned top to down, so first item is the top one.
                // Chart can only have one highlighted item.
                if (i === 0 && item !== me.getChart().getHighlightItem()) {
                    me.highlight(item);
                    me.sync();
                }
                tooltip = item.series.getTooltip();
                if (tooltip) {
                    tooltips.push(tooltip);
                }
            }
            if (isMousePointer) {
                // If we detected a mouse hit, show/refresh the tooltip
                if (items.length) {
                    for (i = 0 , len = items.length; i < len; i++) {
                        item = items[i];
                        tooltip = item.series.getTooltip();
                        if (tooltip) {
                            // If there were different previously active items
                            // that are not going to be included in current active items,
                            // ask them to hide their tooltips. Unless those are
                            // the same tooltip instances that we are about to show,
                            // in which case we are just going to reposition them.
                            for (j = 0 , jLen = tooltipItems.length; j < jLen; j++) {
                                oldItem = tooltipItems[j];
                                if (!Ext.Array.contains(items, oldItem)) {
                                    oldTooltip = oldItem.series.getTooltip();
                                    if (!Ext.Array.contains(tooltips, oldTooltip)) {
                                        oldItem.series.hideTooltip(oldItem, true);
                                    }
                                }
                            }
                            if (tooltip.getTrackMouse()) {
                                item.series.showTooltip(item, e);
                            } else {
                                me.showUntracked(item);
                            }
                        }
                    }
                    me.tooltipItems = items;
                } else // No mouse hit - schedule a hide for hideDelay ms.
                // If pointer enters another item within that time,
                // there will be no flickery reshow.
                {
                    me.hideTooltips(tooltipItems);
                    tooltipItems.length = 0;
                }
            }
            return false;
        }
    },
    highlight: function(item) {
        // This is its own function to make it easier for subclasses
        // to enhance the behavior. An alternative would be to listen
        // for the chart's 'itemhighlight' event.
        this.getChart().setHighlightItem(item);
    },
    showTooltip: function(e, item) {
        item.series.showTooltip(item, e);
        Ext.Array.include(this.tooltipItems, item);
    },
    showUntracked: function(item) {
        var marker = item.sprite.getMarker(item.category),
            surface, surfaceXY, isInverseY, itemBBox, matrix;
        if (marker) {
            surface = marker.getSurface();
            isInverseY = surface.matrix.elements[3] < 0;
            surfaceXY = surface.element.getXY();
            itemBBox = Ext.clone(marker.getBBoxFor(item.index));
            if (isInverseY) {
                // The item.category for bar series will be 'items'.
                // The item.category for line series will be 'markers'.
                // 'items' are in the 'series' surface, which is flipped vertically
                // for cartesian series.
                // 'markers' are in the 'overlay' surface, which isn't flipped.
                // So for 'markers' we already have the bbox in a coordinate system
                // with the origin at the top-left of the surface, but for 'items'
                // we need to do a conversion.
                if (surface.getInherited().rtl) {
                    matrix = surface.inverseMatrix.clone().flipX().translate(item.sprite.attr.innerWidth, 0, true);
                } else {
                    matrix = surface.inverseMatrix;
                }
                itemBBox = matrix.transformBBox(itemBBox);
            }
            itemBBox.x += surfaceXY[0];
            itemBBox.y += surfaceXY[1];
            item.series.showTooltipAt(item, itemBBox.x + itemBBox.width * 0.5, itemBBox.y + itemBBox.height * 0.5);
        }
    },
    onMouseDownGesture: function() {
        this.isDragging = true;
    },
    onMouseUpGesture: function() {
        this.isDragging = false;
    },
    isSameItem: function(a, b) {
        return a && b && a.series === b.series && a.field === b.field && a.index === b.index;
    },
    onTapGesture: function(e) {
        var me = this,
            item;
        // A click/tap on an item makes its highlight sticky.
        // It requires another click/tap to unhighlight.
        if (e.pointerType === 'mouse' && !me.getSticky()) {
            return;
        }
        item = me.getItemForEvent(e);
        if (me.isSameItem(me.stickyHighlightItem, item)) {
            item = null;
        }
        // toggle
        me.stickyHighlightItem = item;
        me.highlight(item);
    },
    privates: {
        hideTooltips: function(items, force) {
            var item, i, len;
            items = Ext.isArray(items) ? items : [
                items
            ];
            for (i = 0 , len = items.length; i < len; i++) {
                item = items[i];
                if (item && item.series && !item.series.destroyed) {
                    item.series.hideTooltip(item, force);
                }
            }
        }
    }
});

/**
 * @class Ext.chart.interactions.ItemEdit
 * @extends Ext.chart.interactions.ItemHighlight
 *
 * The 'itemedit' interaction allows the user to edit store data
 * by dragging series items in the chart.
 *
 * The 'itemedit' interaction extends the
 * {@link Ext.chart.interactions.ItemHighlight 'itemhighlight'} interaction,
 * so it also acts like one. If you need both interactions in a single chart,
 * 'itemedit' should be sufficient. Hovering/tapping will result in highlighting,
 * and dragging will result in editing.
 */
Ext.define('Ext.chart.interactions.ItemEdit', {
    extend: 'Ext.chart.interactions.ItemHighlight',
    requires: [
        'Ext.tip.ToolTip'
    ],
    type: 'itemedit',
    alias: 'interaction.itemedit',
    isItemEdit: true,
    config: {
        /**
         * @cfg {Object} [style=null]
         * The style that will be applied to the series item on dragging.
         * By default, series item will have no fill,
         * and will have a dashed stroke of the same color.
         */
        style: null,
        /**
         * @cfg {Function/String} [renderer=null]
         * A function that returns style attributes for the item that's being dragged.
         * This is useful if you want to give a visual feedback to the user when
         * they dragged to a certain point.
         *
         * @param {Object} [data] The following properties are available:
         *
         * @param {Object} data.target The object containing the xField/xValue or/and
         * yField/yValue properties, where the xField/yField specify the store records
         * being edited and the xValue/yValue the target values to be set when
         * the interaction ends. The object also contains the 'index' of the record
         * being edited.
         * @param {Object} data.style The style that is going to be used for the dragged item.
         * The attributes returned by the renderer will be applied on top of this style.
         * @param {Object} data.item The series item being dragged.
         * This is actually the {@link Ext.chart.AbstractChart#highlightItem}.
         *
         * @return {Object} The style attributes to be set on the dragged item.
         */
        renderer: null,
        /**
         * @cfg {Object/Boolean} [tooltip=true]
         */
        tooltip: true,
        gestures: {
            dragstart: 'onDragStart',
            drag: 'onDrag',
            dragend: 'onDragEnd'
        },
        cursors: {
            ewResize: 'ew-resize',
            nsResize: 'ns-resize',
            move: 'move'
        }
    },
    /**
         * @private
         * @cfg {Boolean} [sticky=false]
         */
    /**
     * @event beginitemedit
     * Fires when item edit operation (dragging) begins.
     * @param {Ext.chart.AbstractChart} chart The chart the interaction belongs to.
     * @param {Ext.chart.interactions.ItemEdit} interaction The interaction.
     * @param {Object} item The item that is about to be edited.
     */
    /**
     * @event enditemedit
     * Fires when item edit operation (dragging) ends.
     * @param {Ext.chart.AbstractChart} chart The chart the interaction belongs to.
     * @param {Ext.chart.interactions.ItemEdit} interaction The interaction.
     * @param {Object} item The item that was edited.
     * @param {Object} target The object containing target values the were used.
     */
    item: null,
    // Item being edited.
    applyTooltip: function(tooltip) {
        var config;
        if (tooltip) {
            config = Ext.apply({}, tooltip, {
                renderer: this.defaultTooltipRenderer,
                constrainPosition: true,
                shrinkWrapDock: true,
                autoHide: true,
                trackMouse: true,
                mouseOffset: [
                    20,
                    20
                ]
            });
            tooltip = new Ext.tip.ToolTip(config);
        }
        return tooltip;
    },
    defaultTooltipRenderer: function(tooltip, item, target, e) {
        var parts = [];
        if (target.xField) {
            parts.push(target.xField + ': ' + target.xValue);
        }
        if (target.yField) {
            parts.push(target.yField + ': ' + target.yValue);
        }
        tooltip.setHtml(parts.join('<br>'));
    },
    onDragStart: function(e) {
        var me = this,
            chart = me.getChart(),
            item = chart.getHighlightItem();
        e.claimGesture();
        if (item) {
            chart.fireEvent('beginitemedit', chart, me, me.item = item);
            // If ItemEdit interaction comes before other interactions
            // in the chart's 'interactions' config, this will
            // prevent other interactions hijacking the 'dragstart'
            // event. We only stop event propagation is there's
            // an item to edit under cursor/finger, otherwise we
            // let other interactions (e.g. 'panzoom') handle the event.
            return false;
        }
    },
    onDrag: function(e) {
        var me = this,
            chart = me.getChart(),
            item = chart.getHighlightItem(),
            type = item && item.sprite.type;
        if (item) {
            switch (type) {
                case 'barSeries':
                    return me.onDragBar(e);
                case 'scatterSeries':
                    return me.onDragScatter(e);
            }
        }
    },
    highlight: function(item) {
        var me = this,
            chart = me.getChart(),
            flipXY = chart.getFlipXY(),
            cursors = me.getCursors(),
            type = item && item.sprite.type,
            style = chart.el.dom.style;
        me.callParent([
            item
        ]);
        if (item) {
            switch (type) {
                case 'barSeries':
                    if (flipXY) {
                        style.cursor = cursors.ewResize;
                    } else {
                        style.cursor = cursors.nsResize;
                    };
                    break;
                case 'scatterSeries':
                    style.cursor = cursors.move;
                    break;
            }
        } else {
            chart.el.dom.style.cursor = 'default';
        }
    },
    onDragBar: function(e) {
        var me = this,
            chart = me.getChart(),
            isRtl = chart.getInherited().rtl,
            flipXY = chart.isCartesian && chart.getFlipXY(),
            item = chart.getHighlightItem(),
            marker = item.sprite.getMarker('items'),
            instance = marker.getMarkerFor(item.sprite.getId(), item.index),
            surface = item.sprite.getSurface(),
            surfaceRect = surface.getRect(),
            xy = surface.getEventXY(e),
            matrix = item.sprite.attr.matrix,
            renderer = me.getRenderer(),
            style, changes, params, positionY;
        if (flipXY) {
            positionY = isRtl ? surfaceRect[2] - xy[0] : xy[0];
        } else {
            positionY = surfaceRect[3] - xy[1];
        }
        style = {
            x: instance.x,
            y: positionY,
            width: instance.width,
            height: instance.height + (instance.y - positionY),
            radius: instance.radius,
            fillStyle: 'none',
            lineDash: [
                4,
                4
            ],
            zIndex: 100
        };
        Ext.apply(style, me.getStyle());
        if (Ext.isArray(item.series.getYField())) {
            // stacked bars
            positionY = positionY - instance.y - instance.height;
        }
        me.target = {
            index: item.index,
            yField: item.field,
            yValue: (positionY - matrix.getDY()) / matrix.getYY()
        };
        params = [
            chart,
            {
                target: me.target,
                style: style,
                item: item
            }
        ];
        changes = Ext.callback(renderer, null, params, 0, chart);
        if (changes) {
            Ext.apply(style, changes);
        }
        // The interaction works by putting another series item instance
        // under 'itemedit' ID with a slightly different style (default) or
        // whatever style the user provided.
        item.sprite.putMarker('items', style, 'itemedit');
        me.showTooltip(e, me.target, item);
        surface.renderFrame();
    },
    onDragScatter: function(e) {
        var me = this,
            chart = me.getChart(),
            isRtl = chart.getInherited().rtl,
            flipXY = chart.isCartesian && chart.getFlipXY(),
            item = chart.getHighlightItem(),
            marker = item.sprite.getMarker('markers'),
            instance = marker.getMarkerFor(item.sprite.getId(), item.index),
            surface = item.sprite.getSurface(),
            surfaceRect = surface.getRect(),
            xy = surface.getEventXY(e),
            matrix = item.sprite.attr.matrix,
            xAxis = item.series.getXAxis(),
            isEditableX = xAxis && xAxis.getLayout().isContinuous,
            renderer = me.getRenderer(),
            style, changes, params, positionX, positionY, hintX, hintY;
        if (flipXY) {
            positionY = isRtl ? surfaceRect[2] - xy[0] : xy[0];
        } else {
            positionY = surfaceRect[3] - xy[1];
        }
        if (isEditableX) {
            if (flipXY) {
                positionX = surfaceRect[3] - xy[1];
            } else {
                positionX = xy[0];
            }
        } else {
            positionX = instance.translationX;
        }
        if (isEditableX) {
            hintX = xy[0];
            hintY = xy[1];
        } else {
            if (flipXY) {
                hintX = xy[0];
                hintY = instance.translationY;
            } else // no change
            {
                hintX = instance.translationX;
                hintY = xy[1];
            }
        }
        // no change
        style = {
            translationX: hintX,
            translationY: hintY,
            scalingX: instance.scalingX,
            scalingY: instance.scalingY,
            r: instance.r,
            fillStyle: 'none',
            lineDash: [
                4,
                4
            ],
            zIndex: 100
        };
        Ext.apply(style, me.getStyle());
        me.target = {
            index: item.index,
            yField: item.field,
            yValue: (positionY - matrix.getDY()) / matrix.getYY()
        };
        if (isEditableX) {
            Ext.apply(me.target, {
                xField: item.series.getXField(),
                xValue: (positionX - matrix.getDX()) / matrix.getXX()
            });
        }
        params = [
            chart,
            {
                target: me.target,
                style: style,
                item: item
            }
        ];
        changes = Ext.callback(renderer, null, params, 0, chart);
        if (changes) {
            Ext.apply(style, changes);
        }
        // This marker acts as a visual hint while dragging.
        item.sprite.putMarker('markers', style, 'itemedit');
        me.showTooltip(e, me.target, item);
        surface.renderFrame();
    },
    showTooltip: function(e, target, item) {
        var tooltip = this.getTooltip(),
            config, chart;
        if (tooltip && Ext.toolkit !== 'modern') {
            config = tooltip.config;
            chart = this.getChart();
            Ext.callback(config.renderer, null, [
                tooltip,
                item,
                target,
                e
            ], 0, chart);
            // If trackMouse is set, a ToolTip shows by its pointerEvent
            tooltip.pointerEvent = e;
            if (tooltip.isVisible()) {
                // After show handling repositions according
                // to configuration. trackMouse uses the pointerEvent
                // If aligning to an element, it uses a currentTarget
                // flyweight which may be attached to any DOM element.
                tooltip.realignToTarget();
            } else {
                tooltip.show();
            }
        }
    },
    hideTooltip: function() {
        var tooltip = this.getTooltip();
        if (tooltip && Ext.toolkit !== 'modern') {
            tooltip.hide();
        }
    },
    onDragEnd: function(e) {
        var me = this,
            target = me.target,
            chart = me.getChart(),
            store = chart.getStore(),
            record;
        if (target) {
            record = store.getAt(target.index);
            if (target.yField) {
                record.set(target.yField, target.yValue, {
                    convert: false
                });
            }
            if (target.xField) {
                record.set(target.xField, target.xValue, {
                    convert: false
                });
            }
            if (target.yField || target.xField) {
                me.getChart().onDataChanged();
            }
            me.target = null;
        }
        me.hideTooltip();
        if (me.item) {
            chart.fireEvent('enditemedit', chart, me, me.item, target);
        }
        me.highlight(me.item = null);
    },
    destroy: function() {
        // Peek at the config, so we don't create one just to destroy it,
        // if a user has set 'tooltip' config to 'false'.
        var tooltip = this.getConfig('tooltip', true);
        Ext.destroy(tooltip);
        this.callParent();
    }
});

/**
 * The PanZoom interaction allows the user to navigate the data for one or more chart
 * axes by panning and/or zooming. Navigation can be limited to particular axes. Zooming is
 * performed by pinching on the chart or axis area; panning is performed by single-touch dragging.
 * The interaction only works with cartesian charts/series.
 *
 * For devices which do not support multiple-touch events, zooming can not be done via pinch
 * gestures; in this case the interaction will allow the user to perform both zooming and panning
 * using the same single-touch drag gesture.
 * {@link #modeToggleButton} provides a button to indicate and toggle between two modes.
 *
 *     @example
 *     Ext.create({
 *         renderTo: document.body,
 *         xtype: 'cartesian',
 *         width: 600,
 *         height: 400,
 *         insetPadding: 40,            
 *         interactions: [{
 *             type: 'panzoom',
 *             zoomOnPan: true
 *         }],
 *         store: {
 *             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
 *             }]
 *         },
 *         axes: [{
 *             type: 'numeric',
 *             position: 'left',
 *             fields: ['data1'],
 *             title: {
 *                 text: 'Sample Values',
 *                 fontSize: 15
 *             },
 *             grid: true,
 *             minimum: 0
 *         }, {
 *             type: 'category',
 *             position: 'bottom',
 *             fields: ['name'],
 *             title: {
 *                 text: 'Sample Values',
 *                 fontSize: 15
 *             }
 *         }],
 *         series: [{
 *             type: 'line',
 *             highlight: {
 *                 size: 7,
 *                 radius: 7
 *             },
 *             style: {
 *                 stroke: 'rgb(143,203,203)'
 *             },
 *             xField: 'name',
 *             yField: 'data1',
 *             marker: {
 *                 type: 'path',
 *                 path: ['M', - 2, 0, 0, 2, 2, 0, 0, - 2, 'Z'],
 *                 stroke: 'blue',
 *                 lineWidth: 0
 *             }
 *         }, {
 *             type: 'line',
 *             highlight: {
 *                 size: 7,
 *                 radius: 7
 *             },
 *             fill: true,
 *             xField: 'name',
 *             yField: 'data3',
 *             marker: {
 *                 type: 'circle',
 *                 radius: 4,
 *                 lineWidth: 0
 *             }
 *         }]
 *     });
 * 
 * The configuration object for the `panzoom` interaction type should specify which axes
 * will be made navigable via the `axes` config. See the {@link #axes} config documentation
 * for details on the allowed formats. If the `axes` config is not specified, it will default
 * to making all axes navigable with the default axis options.
 *
 */
Ext.define('Ext.chart.interactions.PanZoom', {
    extend: 'Ext.chart.interactions.Abstract',
    type: 'panzoom',
    alias: 'interaction.panzoom',
    requires: [
        'Ext.draw.Animator'
    ],
    config: {
        /**
         * @cfg {Object/Array} axes
         * Specifies which axes should be made navigable. The config value can take the following
         * formats:
         *
         * - An Object with keys corresponding to the {@link Ext.chart.axis.Axis#position position}
         *   of each axis that should be made navigable. Each key's value can either be an Object
         *   with further configuration options for each axis or simply `true` for a default set
         *   of options.
         *
         *       {
         *           type: 'panzoom',
         *           axes: {
         *               left: {
         *                   maxZoom: 5,
         *                   allowPan: false
         *               },
         *               bottom: true
         *           }
         *       }
         *
         *   If using the full Object form, the following options can be specified for each axis:
         *
         *   - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its
         *     natural size.
         *   - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
         *   - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
         *   - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
         *   - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
         *   - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
         *
         * - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position
         *   position} of an axis that should be made navigable. The default options will be used
         *   for each named axis.
         *
         *       {
         *           type: 'panzoom',
         *           axes: ['left', 'bottom']
         *       }
         *
         * If the `axes` config is not specified, it will default to making all axes navigable
         * with the default axis options.
         */
        axes: {
            top: {},
            right: {},
            bottom: {},
            left: {}
        },
        minZoom: null,
        maxZoom: null,
        /**
         * @cfg {Boolean} showOverflowArrows
         * If `true`, arrows will be conditionally shown at either end of each axis to indicate that
         * the axis is overflowing and can therefore be panned in that direction. Set this
         * to `false` to prevent the arrows from being displayed.
         */
        showOverflowArrows: true,
        /**
         * @cfg {Object} overflowArrowOptions
         * A set of optional overrides for the overflow arrow sprites' options. Only relevant when
         * {@link #showOverflowArrows} is `true`.
         */
        /**
         * @cfg {String} panGesture
         * Defines the gesture that initiates panning.
         * @private
         */
        panGesture: 'drag',
        /**
         * @cfg {String} zoomGesture
         * Defines the gesture that initiates zooming.
         * @private
         */
        zoomGesture: 'pinch',
        /**
         * @cfg {Boolean} zoomOnPanGesture
         * @deprecated 6.2 Please use {@link #zoomOnPan} instead.
         * If `true`, the pan gesture will zoom the chart.
         */
        zoomOnPanGesture: null,
        /**
         * @cfg {Boolean} zoomOnPan
         * If `true`, the pan gesture will zoom the chart.
         */
        zoomOnPan: false,
        /**
         * @cfg {Boolean} [doubleTapReset=false]
         * If `true`, the double tap on a chart will reset the current pan/zoom to show the whole
         * chart.
         */
        doubleTapReset: false,
        modeToggleButton: {
            xtype: 'segmentedbutton',
            width: 200,
            defaults: {
                ui: 'default-toolbar'
            },
            cls: Ext.baseCSSPrefix + 'panzoom-toggle',
            items: [
                {
                    text: 'Pan',
                    value: 'pan'
                },
                {
                    text: 'Zoom',
                    value: 'zoom'
                }
            ]
        },
        hideLabelInGesture: false
    },
    // Ext.os.is.Android
    stopAnimationBeforeSync: true,
    applyAxes: function(axesConfig, oldAxesConfig) {
        return Ext.merge(oldAxesConfig || {}, axesConfig);
    },
    updateZoomOnPan: function(zoomOnPan) {
        var button = this.getModeToggleButton();
        button.setValue(zoomOnPan ? 'zoom' : 'pan');
    },
    updateZoomOnPanGesture: function(zoomOnPanGesture) {
        this.setZoomOnPan(zoomOnPanGesture);
    },
    getZoomOnPanGesture: function() {
        return this.getZoomOnPan();
    },
    applyModeToggleButton: function(button, oldButton) {
        return Ext.factory(button, 'Ext.button.Segmented', oldButton);
    },
    updateModeToggleButton: function(button) {
        if (button) {
            button.on('change', 'onModeToggleChange', this);
        }
    },
    onModeToggleChange: function(segmentedButton, value) {
        this.setZoomOnPan(value === 'zoom');
    },
    getGestures: function() {
        var me = this,
            gestures = {},
            pan = me.getPanGesture(),
            zoom = me.getZoomGesture();
        gestures[zoom] = 'onZoomGestureMove';
        gestures[zoom + 'start'] = 'onZoomGestureStart';
        gestures[zoom + 'end'] = 'onZoomGestureEnd';
        gestures[pan] = 'onPanGestureMove';
        gestures[pan + 'start'] = 'onPanGestureStart';
        gestures[pan + 'end'] = 'onPanGestureEnd';
        gestures.doubletap = 'onDoubleTap';
        return gestures;
    },
    onDoubleTap: function(e) {
        var me = this,
            doubleTapReset = me.getDoubleTapReset(),
            chart, axes, axis, i, ln;
        if (doubleTapReset) {
            chart = me.getChart();
            axes = chart.getAxes();
            for (i = 0 , ln = axes.length; i < ln; i++) {
                axis = axes[i];
                axis.setVisibleRange([
                    0,
                    1
                ]);
            }
            chart.redraw();
        }
    },
    onPanGestureStart: function(e) {
        var me = this,
            chart, rect, xy;
        if (!e || !e.touches || e.touches.length < 2) {
            // Limit drags to single touch
            chart = me.getChart();
            rect = chart.getInnerRect();
            xy = chart.element.getXY();
            e.claimGesture();
            chart.suspendAnimation();
            me.startX = e.getX() - xy[0] - rect[0];
            me.startY = e.getY() - xy[1] - rect[1];
            me.oldVisibleRanges = null;
            me.hideLabels();
            chart.suspendThicknessChanged();
            me.lockEvents(me.getPanGesture());
            return false;
        }
    },
    onPanGestureMove: function(e) {
        var me = this,
            isMouse = e.pointerType === 'mouse',
            isZoomOnPan = isMouse && me.getZoomOnPan(),
            chart, rect, xy;
        if (me.getLocks()[me.getPanGesture()] === me) {
            // Limit drags to single touch.
            chart = me.getChart();
            rect = chart.getInnerRect();
            xy = chart.element.getXY();
            if (isZoomOnPan) {
                me.transformAxesBy(me.getZoomableAxes(e), 0, 0, (e.getX() - xy[0] - rect[0]) / me.startX, me.startY / (e.getY() - xy[1] - rect[1]));
            } else {
                me.transformAxesBy(me.getPannableAxes(e), e.getX() - xy[0] - rect[0] - me.startX, e.getY() - xy[1] - rect[1] - me.startY, 1, 1);
            }
            me.sync();
            return false;
        }
    },
    onPanGestureEnd: function(e) {
        var me = this,
            pan = me.getPanGesture(),
            chart;
        if (me.getLocks()[pan] === me) {
            chart = me.getChart();
            chart.resumeThicknessChanged();
            me.showLabels();
            me.sync();
            me.unlockEvents(pan);
            chart.resumeAnimation();
            return false;
        }
    },
    onZoomGestureStart: function(e) {
        if (e.touches && e.touches.length === 2) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                chart = me.getChart(),
                xy = chart.element.getXY(),
                rect = chart.getInnerRect(),
                x = xy[0] + rect[0],
                y = xy[1] + rect[1],
                newPoints = [
                    e.touches[0].point.x - x,
                    e.touches[0].point.y - y,
                    e.touches[1].point.x - x,
                    e.touches[1].point.y - y
                ],
                xDistance = Math.max(44, Math.abs(newPoints[2] - newPoints[0])),
                yDistance = Math.max(44, Math.abs(newPoints[3] - newPoints[1]));
            e.claimGesture();
            chart.suspendAnimation();
            chart.suspendThicknessChanged();
            me.lastZoomDistances = [
                xDistance,
                yDistance
            ];
            me.lastPoints = newPoints;
            me.oldVisibleRanges = null;
            me.hideLabels();
            me.lockEvents(me.getZoomGesture());
            return false;
        }
    },
    onZoomGestureMove: function(e) {
        var me = this;
        if (me.getLocks()[me.getZoomGesture()] === me) {
            // eslint-disable-next-line vars-on-top
            var chart = me.getChart(),
                rect = chart.getInnerRect(),
                xy = chart.element.getXY(),
                x = xy[0] + rect[0],
                y = xy[1] + rect[1],
                abs = Math.abs,
                lastPoints = me.lastPoints,
                newPoints = [
                    e.touches[0].point.x - x,
                    e.touches[0].point.y - y,
                    e.touches[1].point.x - x,
                    e.touches[1].point.y - y
                ],
                xDistance = Math.max(44, abs(newPoints[2] - newPoints[0])),
                yDistance = Math.max(44, abs(newPoints[3] - newPoints[1])),
                lastDistances = this.lastZoomDistances || [
                    xDistance,
                    yDistance
                ],
                zoomX = xDistance / lastDistances[0],
                zoomY = yDistance / lastDistances[1];
            me.transformAxesBy(me.getZoomableAxes(e), rect[2] * (zoomX - 1) / 2 + newPoints[2] - lastPoints[2] * zoomX, rect[3] * (zoomY - 1) / 2 + newPoints[3] - lastPoints[3] * zoomY, zoomX, zoomY);
            me.sync();
            return false;
        }
    },
    onZoomGestureEnd: function(e) {
        var me = this,
            zoom = me.getZoomGesture(),
            chart;
        if (me.getLocks()[zoom] === me) {
            chart = me.getChart();
            chart.resumeThicknessChanged();
            me.showLabels();
            me.sync();
            me.unlockEvents(zoom);
            chart.resumeAnimation();
            return false;
        }
    },
    hideLabels: function() {
        if (this.getHideLabelInGesture()) {
            this.eachInteractiveAxes(function(axis) {
                axis.hideLabels();
            });
        }
    },
    showLabels: function() {
        if (this.getHideLabelInGesture()) {
            this.eachInteractiveAxes(function(axis) {
                axis.showLabels();
            });
        }
    },
    isEventOnAxis: function(e, axis) {
        // TODO: right now this uses the current event position but really we want to only
        // use the gesture's start event. Pinch does not give that to us though.
        var rect = axis.getSurface().getRect();
        return rect[0] <= e.getX() && e.getX() <= rect[0] + rect[2] && rect[1] <= e.getY() && e.getY() <= rect[1] + rect[3];
    },
    getPannableAxes: function(e) {
        var me = this,
            axisConfigs = me.getAxes(),
            axes = me.getChart().getAxes(),
            i,
            ln = axes.length,
            result = [],
            isEventOnAxis = false,
            config;
        if (e) {
            for (i = 0; i < ln; i++) {
                if (this.isEventOnAxis(e, axes[i])) {
                    isEventOnAxis = true;
                    break;
                }
            }
        }
        for (i = 0; i < ln; i++) {
            config = axisConfigs[axes[i].getPosition()];
            if (config && config.allowPan !== false && (!isEventOnAxis || this.isEventOnAxis(e, axes[i]))) {
                result.push(axes[i]);
            }
        }
        return result;
    },
    getZoomableAxes: function(e) {
        var me = this,
            axisConfigs = me.getAxes(),
            axes = me.getChart().getAxes(),
            result = [],
            i,
            ln = axes.length,
            axis,
            isEventOnAxis = false,
            config;
        if (e) {
            for (i = 0; i < ln; i++) {
                if (this.isEventOnAxis(e, axes[i])) {
                    isEventOnAxis = true;
                    break;
                }
            }
        }
        for (i = 0; i < ln; i++) {
            axis = axes[i];
            config = axisConfigs[axis.getPosition()];
            if (config && config.allowZoom !== false && (!isEventOnAxis || this.isEventOnAxis(e, axis))) {
                result.push(axis);
            }
        }
        return result;
    },
    eachInteractiveAxes: function(fn) {
        var me = this,
            axisConfigs = me.getAxes(),
            axes = me.getChart().getAxes(),
            i;
        for (i = 0; i < axes.length; i++) {
            if (axisConfigs[axes[i].getPosition()]) {
                if (false === fn.call(this, axes[i])) {
                    return;
                }
            }
        }
    },
    transformAxesBy: function(axes, panX, panY, sx, sy) {
        var rect = this.getChart().getInnerRect(),
            axesCfg = this.getAxes(),
            oldVisibleRanges = this.oldVisibleRanges,
            result = false,
            axisCfg, i;
        if (!oldVisibleRanges) {
            this.oldVisibleRanges = oldVisibleRanges = {};
            this.eachInteractiveAxes(function(axis) {
                oldVisibleRanges[axis.getId()] = axis.getVisibleRange();
            });
        }
        if (!rect) {
            return;
        }
        for (i = 0; i < axes.length; i++) {
            axisCfg = axesCfg[axes[i].getPosition()];
            result = this.transformAxisBy(axes[i], oldVisibleRanges[axes[i].getId()], panX, panY, sx, sy, this.minZoom || axisCfg.minZoom, this.maxZoom || axisCfg.maxZoom) || result;
        }
        return result;
    },
    transformAxisBy: function(axis, oldVisibleRange, panX, panY, sx, sy, minZoom, maxZoom) {
        var me = this,
            visibleLength = oldVisibleRange[1] - oldVisibleRange[0],
            visibleRange = axis.getVisibleRange(),
            actualMinZoom = minZoom || me.getMinZoom() || axis.config.minZoom,
            actualMaxZoom = maxZoom || me.getMaxZoom() || axis.config.maxZoom,
            rect = me.getChart().getInnerRect(),
            left, right, isSide, pan, length;
        if (!rect) {
            return;
        }
        isSide = axis.isSide();
        length = isSide ? rect[3] : rect[2];
        pan = isSide ? -panY : panX;
        visibleLength /= isSide ? sy : sx;
        if (visibleLength < 0) {
            visibleLength = -visibleLength;
        }
        if (visibleLength * actualMinZoom > 1) {
            visibleLength = 1;
        }
        if (visibleLength * actualMaxZoom < 1) {
            visibleLength = 1 / actualMaxZoom;
        }
        left = oldVisibleRange[0];
        right = oldVisibleRange[1];
        visibleRange = visibleRange[1] - visibleRange[0];
        if (visibleLength === visibleRange && visibleRange === 1) {
            return;
        }
        axis.setVisibleRange([
            (oldVisibleRange[0] + oldVisibleRange[1] - visibleLength) * 0.5 - pan / length * visibleLength,
            (oldVisibleRange[0] + oldVisibleRange[1] + visibleLength) * 0.5 - pan / length * visibleLength
        ]);
        return Math.abs(left - axis.getVisibleRange()[0]) > 1.0E-10 || Math.abs(right - axis.getVisibleRange()[1]) > 1.0E-10;
    },
    destroy: function() {
        this.setModeToggleButton(null);
        this.callParent();
    }
});

/* eslint-disable max-len */
/**
 * @class Ext.chart.interactions.Rotate
 * @extends Ext.chart.interactions.Abstract
 *
 * The Rotate interaction allows the user to rotate a polar chart about its central point.
 *
 *     @example
 *     Ext.create('Ext.Container', {
 *         renderTo: Ext.getBody(),
 *         width: 600,
 *         height: 400,
 *         layout: 'fit',
 *         items: {
 *             xtype: 'polar',
 *             interactions: 'rotate',
 *             colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e"],
 *             store: {
 *                 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}
 *                 ]
 *             },
 *             series: {
 *                 type: 'pie',
 *                 label: {
 *                     field: 'name',
 *                     display: 'rotate'
 *                 },
 *                 xField: 'data3',
 *                 donut: 30
 *             }
 *         }
 *     });
 */
/* eslint-enable max-len */
Ext.define('Ext.chart.interactions.Rotate', {
    extend: 'Ext.chart.interactions.Abstract',
    type: 'rotate',
    alternateClassName: 'Ext.chart.interactions.RotatePie3D',
    alias: [
        'interaction.rotate',
        'interaction.rotatePie3d'
    ],
    /**
     * @event rotate
     * Fires on every tick of the rotation.
     * @param {Ext.chart.interactions.Rotate} this This interaction.
     * @param {Number} angle The new current rotation angle.
     */
    /**
     * @event rotatestart
     * Fires when a user initiates the rotation.
     * @param {Ext.chart.interactions.Rotate} this This interaction.
     * @param {Number} angle The new current rotation angle.
     */
    /**
     * @event rotateend
     * Fires after a user finishes the rotation.
     * @param {Ext.chart.interactions.Rotate} this This interaction.
     * @param {Number} angle The new current rotation angle.
     */
    /**
     * @deprecated 6.5.1 Use the 'rotateend' event instead.
     * @event rotationEnd
     * Fires after a user finishes the rotation
     * @param {Ext.chart.interactions.Rotate} this This interaction.
     * @param {Number} angle The new current rotation angle.
     */
    config: {
        /**
         * @cfg {String} gesture
         * Defines the gesture type that will be used to rotate the chart. Currently only
         * supports `pinch` for two-finger rotation and `drag` for single-finger rotation.
         * @private
         */
        gesture: 'rotate',
        gestures: {
            dragstart: 'onGestureStart',
            drag: 'onGesture',
            dragend: 'onGestureEnd'
        },
        /**
         * @cfg {Number} rotation
         * Saves the current rotation of the series. Accepts negative values
         * and values > 360 ( / 180 * Math.PI)
         * @private
         */
        rotation: 0
    },
    oldRotations: null,
    getAngle: function(e) {
        var me = this,
            chart = me.getChart(),
            xy = chart.getEventXY(e),
            center = chart.getCenter();
        return Math.atan2(xy[1] - center[1], xy[0] - center[0]);
    },
    onGestureStart: function(e) {
        var me = this;
        e.claimGesture();
        me.lockEvents('drag');
        me.angle = me.getAngle(e);
        me.oldRotations = {};
        me.getChart().suspendAnimation();
        me.fireEvent('rotatestart', me, me.getRotation());
        return false;
    },
    onGesture: function(e) {
        var me = this,
            angle = me.getAngle(e) - me.angle;
        if (me.getLocks().drag === me) {
            me.doRotateTo(angle, true);
            return false;
        }
    },
    /**
     * @private
     */
    doRotateTo: function(angle, relative) {
        var me = this,
            chart = me.getChart(),
            axes = chart.getAxes(),
            seriesList = chart.getSeries(),
            oldRotations = me.oldRotations,
            rotation, oldRotation, axis, series, id, i, ln;
        for (i = 0 , ln = axes.length; i < ln; i++) {
            axis = axes[i];
            id = axis.getId();
            oldRotation = oldRotations[id] || (oldRotations[id] = axis.getRotation());
            rotation = angle + (relative ? oldRotation : 0);
            axis.setRotation(rotation);
        }
        for (i = 0 , ln = seriesList.length; i < ln; i++) {
            series = seriesList[i];
            id = series.getId();
            oldRotation = oldRotations[id] || (oldRotations[id] = series.getRotation());
            // Unline axis's 'rotation', Polar series' 'rotation' is a public config and in degrees.
            rotation = Ext.draw.Draw.degrees(angle + (relative ? oldRotation : 0));
            series.setRotation(rotation);
        }
        me.setRotation(rotation);
        me.fireEvent('rotate', me, me.getRotation());
        me.sync();
    },
    /**
     * Rotates a polar chart about its center point to the specified angle.
     * @param {Number} angle The angle to rotate to.
     * @param {Boolean} [relative=false] Whether the rotation is relative to the current angle
     * or not.
     * @param {Boolean} [animate=false] Whether to animate the rotation or not.
     */
    rotateTo: function(angle, relative, animate) {
        var me = this,
            chart = me.getChart();
        if (!animate) {
            chart.suspendAnimation();
        }
        me.doRotateTo(angle, relative, animate);
        me.oldRotations = {};
        if (!animate) {
            chart.resumeAnimation();
        }
    },
    onGestureEnd: function(e) {
        var me = this;
        if (me.getLocks().drag === me) {
            me.onGesture(e);
            me.unlockEvents('drag');
            me.getChart().resumeAnimation();
            me.fireEvent('rotateend', me, me.getRotation());
            me.fireEvent('rotationEnd', me, me.getRotation());
            return false;
        }
    }
});

/**
 *
 */
Ext.define('Ext.chart.navigator.ContainerBase', {
    extend: 'Ext.panel.Panel'
});

/**
 *
 */
Ext.define('Ext.chart.navigator.NavigatorBase', {
    extend: 'Ext.chart.CartesianChart',
    onRender: function() {
        this.callParent();
        this.setupEvents();
    },
    // Note: 'applyDock' and 'updateDock' won't ever be called in Classic.
    // See the Classic Component's 'setDock' method, which is overridden here.
    setDocked: function(docked) {
        var me = this,
            ownerCt = me.getNavigatorContainer();
        if (!(docked === 'top' || docked === 'bottom')) {
            Ext.raise("Can only dock to 'top' or 'bottom'.");
        }
        if (docked !== me.dock) {
            if (ownerCt && ownerCt.moveDocked) {
                ownerCt.moveDocked(me, docked);
            } else {
                me.dock = docked;
            }
        }
        return me;
    },
    getDocked: function() {
        return this.dock;
    }
});

/**
 * The overlay sprite used by the {@link Ext.chart.navigator.Navigator} component
 * to render the selected visible range or a chart's horizontal axis.
 */
Ext.define('Ext.chart.navigator.sprite.RangeMask', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.rangemask',
    inheritableStatics: {
        def: {
            processors: {
                min: 'limited01',
                max: 'limited01',
                thumbOpacity: 'limited01'
            },
            defaults: {
                min: 0,
                max: 1,
                lineWidth: 2,
                miterLimit: 1,
                strokeStyle: '#787878',
                thumbOpacity: 1
            }
        }
    },
    getBBox: function(isWithoutTransform) {
        var me = this,
            attr = me.attr,
            bbox = attr.bbox;
        bbox.plain = {
            x: 0,
            y: 0,
            width: 1,
            height: 1
        };
        if (isWithoutTransform) {
            return bbox.plain;
        }
        return bbox.transform || (bbox.transform = attr.matrix.transformBBox(bbox.plain));
    },
    renderThumb: function(surface, ctx, x, y) {
        var me = this,
            shapeSprite = me.shapeSprite,
            textureSprite = me.textureSprite,
            thumbOpacity = me.attr.thumbOpacity,
            thumbAttributes = {
                opacity: thumbOpacity,
                translationX: x,
                translationY: y
            };
        if (!shapeSprite) {
            shapeSprite = me.shapeSprite = new Ext.draw.sprite.Rect({
                x: -9.5,
                y: -9.5,
                width: 19,
                height: 19,
                radius: 4,
                lineWidth: 1,
                fillStyle: {
                    type: 'linear',
                    degrees: 90,
                    stops: [
                        {
                            offset: 0,
                            color: '#EEE'
                        },
                        {
                            offset: 1,
                            color: '#FFF'
                        }
                    ]
                },
                strokeStyle: '#999'
            });
            textureSprite = me.textureSprite = new Ext.draw.sprite.Path({
                path: 'M -4, -5, -4, 5 M 0, -5, 0, 5 M 4, -5, 4, 5',
                strokeStyle: {
                    type: 'linear',
                    degrees: 90,
                    stops: [
                        {
                            offset: 0,
                            color: '#CCC'
                        },
                        {
                            offset: 1,
                            color: '#BBB'
                        }
                    ]
                },
                lineWidth: 2
            });
        }
        ctx.save();
        shapeSprite.setAttributes(thumbAttributes);
        shapeSprite.applyTransformations();
        textureSprite.setAttributes(thumbAttributes);
        textureSprite.applyTransformations();
        shapeSprite.useAttributes(ctx);
        shapeSprite.render(surface, ctx);
        textureSprite.useAttributes(ctx);
        textureSprite.render(surface, ctx);
        ctx.restore();
    },
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            matrix = attr.matrix.elements,
            sx = matrix[0],
            sy = matrix[3],
            tx = matrix[4],
            ty = matrix[5],
            min = attr.min,
            max = attr.max,
            // s_min and s_max are range values in screen coordinates (scaled and translated)
            s_min = min * sx + tx,
            s_max = max * sx + tx,
            s_y = Math.round(0.5 * sy + ty);
        // thumb position in screen coordinates (mid-height)
        ctx.beginPath();
        // Rect that represents the whole range.
        ctx.moveTo(tx, ty);
        ctx.lineTo(sx + tx, ty);
        ctx.lineTo(sx + tx, sy + ty);
        ctx.lineTo(tx, sy + ty);
        ctx.lineTo(tx, ty);
        // Rect that represents the visible range.
        ctx.moveTo(s_min, ty);
        ctx.lineTo(s_min, sy + ty);
        ctx.lineTo(s_max, sy + ty);
        ctx.lineTo(s_max, ty);
        ctx.lineTo(s_min, ty);
        ctx.fillStroke(attr, true);
        me.renderThumb(surface, ctx, Math.round(s_min), s_y);
        me.renderThumb(surface, ctx, Math.round(s_max), s_y);
    }
});

/**
 * The Navigator component is used to visually set the visible range of the x-axis
 * of a cartesian chart.
 *
 * This component is meant to be used with the Navigator Container
 * via its {@link Ext.chart.navigator.Container#navigator} config.
 *
 * IMPORTANT: even though the Navigator component is a kind of chart, it should not be
 * treated as such. Correct behavior is not guaranteed when using hidden/private configs.
 */
Ext.define('Ext.chart.navigator.Navigator', {
    extend: 'Ext.chart.navigator.NavigatorBase',
    isNavigator: true,
    requires: [
        'Ext.chart.navigator.sprite.RangeMask'
    ],
    config: {
        /**
         * @cfg {'bottom'/'top'} [docked='bottom']
         */
        docked: 'bottom',
        /**
         * @cfg {'series'/'chart'} [span='series']
         * Whether the navigator should span the 'series' (default) or the whole 'chart'.
         */
        span: 'series',
        insetPadding: 0,
        innerPadding: 0,
        /**
         * @cfg {Ext.chart.navigator.Container} navigatorContainer
         * 'parent' is reserved in Modern, 'container' is reserved in Classic,
         * so we use 'navigatorContainer' as a config name.
         * @private
         */
        navigatorContainer: null,
        /**
         * @cfg {String} axis (required)
         * The ID of the {@link #chart chart's} axis to link to.
         * The axis should be positioned to 'bottom' or 'top' in the chart.
         */
        axis: null,
        /**
         * @cfg {Number} [tolerance=20]
         * The maximum horizontal delta between the pointer/finger and the center of a navigator
         * thumb. Used for hit testing.
         */
        tolerance: 20,
        /**
         * @cfg {Number} [minimum=0.8]
         * The start of the visible range, where the visible range is a [0, 1] interval.
         */
        minimum: 0.8,
        /**
         * @cfg {Number} [maximum=1]
         * The end of the visible range, where the visible range is a [0, 1] interval.
         */
        maximum: 1,
        /**
         * @cfg {Number} [thumbGap=30]
         * Minimum gap between navigator thumbs in pixels.
         */
        thumbGap: 30,
        autoHideThumbs: true,
        width: '100%',
        /**
         * @cfg {Number} [height=75]
         * The height of the navigator component.
         */
        height: 75
    },
    /**
         * @cfg flipXY
         * @hide
         */
    /**
         * @cfg series
         * @hide
         */
    /**
         * @cfg axes
         * @hide
         */
    /**
         * @cfg store
         * @hide
         */
    /**
         * @cfg legend
         * @hide
         */
    /**
         * @cfg interactions
         * @hide
         */
    /**
         * @cfg highlightItem
         * @hide
         */
    /**
         * @cfg theme
         * @hide
         */
    /**
         * @cfg innerPadding
         * @hide
         */
    /**
         * @cfg insetPadding
         * @hide
         */
    dragType: null,
    constructor: function(config) {
        var me = this,
            visibleRange, overlay;
        config = config || {};
        visibleRange = [
            config.minimum || 0.8,
            config.maximum || 1
        ];
        me.callParent([
            config
        ]);
        overlay = me.overlaySurface;
        overlay.element.setStyle({
            zIndex: 100
        });
        me.rangeMask = overlay.add({
            type: 'rangemask',
            min: visibleRange[0],
            max: visibleRange[1],
            fillStyle: 'rgba(0, 0, 0, .25)'
        });
        me.onDragEnd();
        // Set 'thumbOpacity' of the range mask sprite to 0, if needed,
        // and apply animation modifier changes after that, so that the attribute is set
        // instantly.
        me.rangeMask.setAnimation({
            duration: 500,
            customDurations: {
                min: 0,
                max: 0,
                translationX: 0,
                translationY: 0,
                scalingX: 0,
                scalingY: 0,
                scalingCenterX: 0,
                scalingCenterY: 0,
                fillStyle: 0,
                strokeStyle: 0
            }
        });
        me.setVisibleRange(visibleRange);
    },
    createSurface: function(id) {
        var surface = this.callParent([
                id
            ]);
        if (id === 'overlay') {
            this.overlaySurface = surface;
        }
        return surface;
    },
    // Note: 'applyDock' and 'updateDock' won't ever be called in Classic.
    // See Classic NavigatorBase.
    applyAxis: function(axis) {
        return this.getNavigatorContainer().getChart().getAxis(axis);
    },
    updateAxis: function(axis, oldAxis) {
        var me = this,
            eventName = 'visiblerangechange',
            eventHandler = 'onAxisVisibleRangeChange';
        if (oldAxis) {
            oldAxis.un(eventName, eventHandler, me);
        }
        if (axis) {
            axis.on(eventName, eventHandler, me);
        }
        me.axis = axis;
    },
    getAxis: function() {
        // The superclass doesn't have the 'axis' config, but it has the same method,
        // which we override here to act as a getter for the config. The user is not
        // expected to use the original method in this subclass anyway.
        return this.axis;
    },
    onAxisVisibleRangeChange: function(axis, visibleRange) {
        this.setVisibleRange(visibleRange);
    },
    updateNavigatorContainer: function(navigatorContainer) {
        var me = this,
            oldChart = me.chart,
            chart = me.chart = navigatorContainer && navigatorContainer.getChart(),
            chartSeriesList = chart && chart.getSeries(),
            // 'legendStore' already exists in the base class.
            chartLegendStore = me.chartLegendStore,
            navigatorSeriesList = [],
            storeEventName = 'update',
            // 'onLegendStoreUpdate' already exists in the base class.
            storeEventHandler = 'onChartLegendStoreUpdate',
            chartSeries, navigatorSeries, seriesConfig, i;
        if (oldChart) {
            oldChart.un('layout', 'afterBoundChartLayout', me);
            oldChart.un('themechange', 'onChartThemeChange', me);
            oldChart.un('storechange', 'onChartStoreChange', me);
        }
        chart.on('layout', 'afterBoundChartLayout', me);
        for (i = 0; i < chartSeriesList.length; i++) {
            chartSeries = chartSeriesList[i];
            seriesConfig = me.getSeriesConfig(chartSeries);
            navigatorSeries = Ext.create('series.' + seriesConfig.type, seriesConfig);
            navigatorSeries.parentSeries = chartSeries;
            chartSeries.navigatorSeries = navigatorSeries;
            navigatorSeriesList.push(navigatorSeries);
        }
        if (chartLegendStore) {
            chartLegendStore.un(storeEventName, storeEventHandler, me);
            me.chartLegendStore = null;
        }
        if (chart) {
            me.setStore(chart.getStore());
            me.chartLegendStore = chartLegendStore = chart.getLegendStore();
            if (chartLegendStore) {
                chartLegendStore.on(storeEventName, storeEventHandler, me);
            }
            chart.on('themechange', 'onChartThemeChange', me);
            chart.on('storechange', 'onChartStoreChange', me);
            me.onChartThemeChange(chart, chart.getTheme());
        }
        me.setSeries(navigatorSeriesList);
    },
    onChartThemeChange: function(chart, theme) {
        this.setTheme(theme);
    },
    onChartStoreChange: function(chart, store) {
        this.setStore(store);
    },
    addCustomStyle: function(config, style, subStyle) {
        var fillStyle, strokeStyle;
        style = style || {};
        subStyle = subStyle || {};
        config.style = config.style || {};
        config.subStyle = config.subStyle || {};
        fillStyle = style && (style.fillStyle || style.fill);
        strokeStyle = style && (style.strokeStyle || style.stroke);
        if (fillStyle) {
            config.style.fillStyle = fillStyle;
        }
        if (strokeStyle) {
            config.style.strokeStyle = strokeStyle;
        }
        fillStyle = subStyle && (subStyle.fillStyle || subStyle.fill);
        strokeStyle = subStyle && (subStyle.strokeStyle || subStyle.stroke);
        if (fillStyle) {
            config.subStyle.fillStyle = fillStyle;
        }
        if (strokeStyle) {
            config.subStyle.strokeStyle = strokeStyle;
        }
        return config;
    },
    getSeriesConfig: function(chartSeries) {
        var me = this,
            style = chartSeries.getStyle(),
            config;
        if (chartSeries.isLine) {
            config = me.addCustomStyle({
                type: 'line',
                fill: true,
                xField: chartSeries.getXField(),
                yField: chartSeries.getYField(),
                smooth: chartSeries.getSmooth()
            }, style);
        } else if (chartSeries.isCandleStick) {
            config = me.addCustomStyle({
                type: 'line',
                fill: true,
                xField: chartSeries.getXField(),
                yField: chartSeries.getCloseField()
            }, style.raiseStyle);
        } else if (chartSeries.isArea || chartSeries.isBar) {
            config = me.addCustomStyle({
                type: 'area',
                xField: chartSeries.getXField(),
                yField: chartSeries.getYField()
            }, style, chartSeries.getSubStyle());
        } else {
            Ext.raise("Navigator only works with 'line', 'bar', 'candlestick' and 'area' series.");
        }
        config.style.fillOpacity = 0.2;
        return config;
    },
    onChartLegendStoreUpdate: function(store, record) {
        var me = this,
            chart = me.chart,
            series;
        if (chart && record) {
            series = chart.getSeries().map[record.get('series')];
            if (series && series.navigatorSeries) {
                series.navigatorSeries.setHiddenByIndex(record.get('index'), record.get('disabled'));
                me.redraw();
            }
        }
    },
    setupEvents: function() {
        // Called from NavigatorBase classes.
        var me = this,
            overlayEl = me.overlaySurface.element;
        overlayEl.on({
            scope: me,
            drag: 'onDrag',
            dragstart: 'onDragStart',
            dragend: 'onDragEnd',
            dragcancel: 'onDragEnd',
            mousemove: 'onMouseMove'
        });
    },
    onMouseMove: function(e) {
        var me = this,
            overlayEl = me.overlaySurface.element,
            style = overlayEl.dom.style,
            dragType = me.getDragType(e.pageX - overlayEl.getXY()[0]);
        switch (dragType) {
            case 'min':
            case 'max':
                style.cursor = 'ew-resize';
                break;
            case 'pan':
                style.cursor = 'move';
                break;
            default:
                style.cursor = 'default';
        }
    },
    getDragType: function(x) {
        var me = this,
            t = me.getTolerance(),
            width = me.overlaySurface.element.getSize().width,
            rangeMask = me.rangeMask,
            min = width * rangeMask.attr.min,
            max = width * rangeMask.attr.max,
            dragType;
        if (x > min + t && x < max - t) {
            dragType = 'pan';
        } else if (x <= min + t && x > min - t) {
            dragType = 'min';
        } else if (x >= max - t && x < max + t) {
            dragType = 'max';
        }
        return dragType;
    },
    onDragStart: function(e) {
        var me = this,
            x, dragType;
        // Limit drags to single touch.
        if (me.dragType || e && e.touches && e.touches.length > 1) {
            return;
        }
        x = e.touches[0].pageX - me.overlaySurface.element.getXY()[0];
        dragType = me.getDragType(x);
        me.rangeMask.attr.thumbOpacity = 1;
        if (dragType) {
            me.dragType = dragType;
            me.touchId = e.touches[0].identifier;
            me.dragX = x;
        }
    },
    onDrag: function(e) {
        if (e.touch.identifier !== this.touchId) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            overlayEl = me.overlaySurface.element,
            width = overlayEl.getSize().width,
            x = e.touches[0].pageX - overlayEl.getXY()[0],
            thumbGap = me.getThumbGap() / width,
            rangeMask = me.rangeMask,
            min = rangeMask.attr.min,
            max = rangeMask.attr.max,
            delta = max - min,
            dragType = me.dragType,
            drag = me.dragX,
            dx = (x - drag) / width;
        if (dragType === 'pan') {
            min += dx;
            max += dx;
            if (min < 0) {
                min = 0;
                max = delta;
            }
            if (max > 1) {
                max = 1;
                min = max - delta;
            }
        } else if (dragType === 'min') {
            min += dx;
            if (min < 0) {
                min = 0;
            }
            if (min > max - thumbGap) {
                min = max - thumbGap;
            }
        } else if (dragType === 'max') {
            max += dx;
            if (max > 1) {
                max = 1;
            }
            if (max < min + thumbGap) {
                max = min + thumbGap;
            }
        } else {
            return;
        }
        me.dragX = x;
        me.setVisibleRange([
            min,
            max
        ]);
    },
    onDragEnd: function() {
        var me = this,
            autoHideThumbs = me.getAutoHideThumbs();
        me.dragType = null;
        if (autoHideThumbs) {
            me.rangeMask.setAttributes({
                thumbOpacity: 0
            });
        }
    },
    updateMinimum: function(mininum) {
        if (!this.isConfiguring) {
            this.setVisibleRange([
                mininum,
                this.getMaximum()
            ]);
        }
    },
    updateMaximum: function(maximum) {
        if (!this.isConfiguring) {
            this.setVisibleRange([
                this.getMinimum(),
                maximum
            ]);
        }
    },
    getMinimum: function() {
        return this.rangeMask.attr.min;
    },
    getMaximum: function() {
        return this.rangeMask.attr.max;
    },
    setVisibleRange: function(visibleRange) {
        var me = this,
            chart = me.chart;
        me.axis.setVisibleRange(visibleRange);
        me.rangeMask.setAttributes({
            min: visibleRange[0],
            max: visibleRange[1]
        });
        me.getSurface('overlay').renderFrame();
        chart.suspendAnimation();
        chart.redraw();
        chart.resumeAnimation();
    },
    afterBoundChartLayout: function() {
        var me = this,
            spanSeries = me.getSpan() === 'series',
            mainRect = me.chart.getMainRect(),
            size = me.element.getSize();
        if (mainRect && spanSeries) {
            me.setInsetPadding({
                left: mainRect[0],
                right: size.width - mainRect[2] - mainRect[0],
                top: 0,
                bottom: 0
            });
            me.performLayout();
        }
    },
    afterChartLayout: function() {
        var me = this,
            size = me.overlaySurface.element.getSize();
        me.rangeMask.setAttributes({
            scalingCenterX: 0,
            scalingCenterY: 0,
            scalingX: size.width,
            scalingY: size.height
        });
    },
    doDestroy: function() {
        var chart = this.chart;
        if (chart && !chart.destroyed) {
            chart.un('layout', 'afterBoundChartLayout', this);
        }
        this.callParent();
    }
});

/**
 * The Navigator Container is a component used to lay out the chart and its
 * {@link Ext.chart.navigator.Navigator navigator}, where the navigator is docked
 * to the top/bottom, and the chart fills the rest of the container's space.
 *
 * For example:
 *
 *     @example
 *     Ext.create({
 *         xtype: 'chartnavigator',
 *         renderTo: Ext.getBody(),
 *         width: 600,
 *         height: 400,
 *
 *         chart: {
 *             xtype: 'cartesian',
 *
 *             store: {
 *                 data: (function () {
 *                     var data = [];
 *                     for (var i = 0; i < 360; i++) {
 *                         data.push({
 *                             x: i,
 *                             y: Math.sin(i / 45 * Math.PI)
 *                         });
 *                     }
 *                     return data;
 *                 })()
 *             },
 *             axes: [
 *                 {
 *                     id: 'navigable-axis',
 *
 *                     type: 'numeric',
 *                     position: 'bottom'
 *                 },
 *                 {
 *                     type: 'numeric',
 *                     position: 'left'
 *                 }
 *             ],
 *             series: {
 *                 type: 'line',
 *                 xField: 'x',
 *                 yField: 'y'
 *             }
 *         },
 *
 *         navigator: {
 *             axis: 'navigable-axis'
 *         }
 *     });
 *
 */
Ext.define('Ext.chart.navigator.Container', {
    // We are interested in the docking functionality that's available in
    // the Container in Modern and in the Panel in Classic.
    extend: 'Ext.chart.navigator.ContainerBase',
    requires: [
        'Ext.chart.CartesianChart',
        'Ext.chart.navigator.Navigator'
    ],
    xtype: 'chartnavigator',
    config: {
        /**
         * @cfg {Ext.chart.CartesianChart} chart
         * The chart to make navigable.
         */
        chart: null,
        /**
         * @cfg {Ext.chart.navigator.Navigator} navigator
         */
        navigator: {}
    },
    layout: 'fit',
    applyChart: function(chart, oldChart) {
        if (oldChart) {
            oldChart.destroy();
        }
        if (chart) {
            if (chart.isCartesian) {
                Ext.raise('Only cartesian charts are supported.');
            }
            if (!chart.isChart) {
                chart.$initParent = this;
                chart = new Ext.chart.CartesianChart(chart);
                delete chart.$initParent;
            }
        }
        return chart;
    },
    legendStore: null,
    surfaceRects: null,
    updateChart: function(chart, oldChart) {
        var me = this;
        if (chart) {
            me.legendStore = chart.getLegendStore();
            if (!me.items && me.initItems) {
                me.initItems();
            }
            me.add(chart);
        }
    },
    applyNavigator: function(navigator, oldNavigator) {
        var instance;
        if (oldNavigator) {
            oldNavigator.destroy();
        }
        if (navigator) {
            navigator.navigatorContainer = navigator.parent = this;
            instance = new Ext.chart.navigator.Navigator(navigator);
        }
        return instance;
    },
    preview: function() {
        this.getNavigator().preview(this.getImage());
    },
    download: function(config) {
        config = config || {};
        config.data = this.getImage().data;
        this.getNavigator().download(config);
    },
    setVisibleRange: function(visibleRange) {
        this.getNavigator().setVisibleRange(visibleRange);
    },
    getImage: function(format) {
        var me = this,
            chart = me.getChart(),
            navigator = me.getNavigator(),
            docked = navigator.getDocked(),
            chartImageSize = chart.bodyElement.getSize(),
            navigatorImageSize = navigator.bodyElement.getSize(),
            chartSurfaces = chart.getSurfaces(true),
            navigatorSurfaces = navigator.getSurfaces(true),
            size = {
                width: chartImageSize.width,
                height: chartImageSize.height + navigatorImageSize.height
            },
            image, imageElement, surfaces, surface;
        if (docked === 'top') {
            me.shiftSurfaces(chartSurfaces, 0, navigatorImageSize.height);
        } else {
            me.shiftSurfaces(navigatorSurfaces, 0, chartImageSize.height);
        }
        surfaces = chartSurfaces.concat(navigatorSurfaces);
        surface = surfaces[0];
        if ((Ext.isIE || Ext.isEdge) && surface.isSVG) {
            // SVG data URLs don't work in IE/Edge as a source for an 'img' element,
            // so we need to render SVG the usual way.
            image = {
                data: surface.toSVG(size, surfaces),
                type: 'svg-markup'
            };
        } else {
            image = surface.flatten(size, surfaces);
            if (format === 'image') {
                imageElement = new Image();
                imageElement.src = image.data;
                image.data = imageElement;
                return image;
            }
            if (format === 'stream') {
                image.data = image.data.replace(/^data:image\/[^;]+/, 'data:application/octet-stream');
                return image;
            }
        }
        me.unshiftSurfaces(surfaces);
        return image;
    },
    shiftSurfaces: function(surfaces, x, y) {
        var ln = surfaces.length,
            i = 0,
            surface;
        this.surfaceRects = {};
        for (; i < ln; i++) {
            surface = surfaces[i];
            this.shiftSurface(surface, x, y);
        }
    },
    shiftSurface: function(surface, x, y) {
        var rect = surface.getRect();
        this.surfaceRects[surface.getId()] = rect.slice();
        rect[0] += x;
        rect[1] += y;
    },
    unshiftSurfaces: function(surfaces) {
        var rects = this.surfaceRects,
            ln = surfaces.length,
            i = 0,
            surface, rect, oldRect;
        if (rects) {
            for (; i < ln; i++) {
                surface = surfaces[i];
                rect = surface.getRect();
                oldRect = rects[surface.getId()];
                if (oldRect) {
                    rect[0] = oldRect[0];
                    rect[1] = oldRect[1];
                }
            }
        }
        this.surfaceRects = null;
    }
});

/**
 * A chart {@link Ext.AbstractPlugin plugin} that adds ability to listen to chart series
 * items events. Item event listeners are passed two parameters: the target item and the
 * event itself. The item object has the following properties:
 *
 * * **category** - the category the item falls under: 'items' or 'markers'
 * * **field** - the store field used by this series item
 * * **index** - the index of the series item
 * * **record** - the store record associated with this series item
 * * **series** - the series the item belongs to
 * * **sprite** - the sprite used to represents this series item
 *
 * For example:
 *
 *     Ext.create('Ext.chart.CartesianChart', {
 *         plugins: {
 *             chartitemevents: {
 *                 moveEvents: true
 *             }
 *         },
 *         store: {
 *             fields: ['pet', 'households', 'total'],
 *             data: [
 *                 {pet: 'Cats', households: 38, total: 93},
 *                 {pet: 'Dogs', households: 45, total: 79},
 *                 {pet: 'Fish', households: 13, total: 171}
 *             ]
 *         },
 *         axes: [{
 *             type: 'numeric',
 *             position: 'left'
 *         }, {
 *             type: 'category',
 *             position: 'bottom'
 *         }],
 *         series: [{
 *             type: 'bar',
 *             xField: 'pet',
 *             yField: 'households',
 *             listeners: {
 *                 itemmousemove: function (series, item, event) {
*                      console.log('itemmousemove', item.category, item.field);
 *                 }
 *             }
 *         }, {
 *             type: 'line',
 *             xField: 'pet',
 *             yField: 'total',
 *             marker: true
 *         }],
 *         listeners: { // Listen to itemclick events on all series.
 *             itemclick: function (chart, item, event) {
 *                 console.log('itemclick', item.category, item.field);
 *             }
 *         }
 *     });
 *
 */
Ext.define('Ext.chart.plugin.ItemEvents', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.chartitemevents',
    /**
     * @cfg {Boolean} [moveEvents=false]
     * If `itemmousemove`, `itemmouseover` or `itemmouseout` event listeners are attached
     * to the chart, the plugin will detect those and will hit test series items on
     * every move. However, if the above item events are attached on the series level
     * only, this config has to be set to true, as the plugin won't perform a similar
     * detection on every series.
     */
    moveEvents: false,
    mouseMoveEvents: {
        mousemove: true,
        mouseover: true,
        mouseout: true
    },
    itemMouseMoveEvents: {
        itemmousemove: true,
        itemmouseover: true,
        itemmouseout: true
    },
    init: function(chart) {
        var handleEvent = 'handleEvent';
        this.chart = chart;
        chart.addElementListener({
            click: handleEvent,
            tap: handleEvent,
            dblclick: handleEvent,
            mousedown: handleEvent,
            mousemove: handleEvent,
            mouseup: handleEvent,
            mouseover: handleEvent,
            mouseout: handleEvent,
            // run our handlers before user code
            priority: 1001,
            scope: this
        });
    },
    hasItemMouseMoveListeners: function() {
        var listeners = this.chart.hasListeners,
            name;
        for (name in this.itemMouseMoveEvents) {
            if (name in listeners) {
                return true;
            }
        }
        return false;
    },
    handleEvent: function(e) {
        var me = this,
            chart = me.chart,
            isMouseMoveEvent = e.type in me.mouseMoveEvents,
            lastItem = me.lastItem,
            chartXY, item;
        if (isMouseMoveEvent && !me.hasItemMouseMoveListeners() && !me.moveEvents) {
            return;
        }
        chartXY = chart.getEventXY(e);
        item = chart.getItemForPoint(chartXY[0], chartXY[1]);
        if (isMouseMoveEvent && !Ext.Object.equals(item, lastItem)) {
            if (lastItem) {
                chart.fireEvent('itemmouseout', chart, lastItem, e);
                lastItem.series.fireEvent('itemmouseout', lastItem.series, lastItem, e);
            }
            if (item) {
                chart.fireEvent('itemmouseover', chart, item, e);
                item.series.fireEvent('itemmouseover', item.series, item, e);
            }
        }
        if (item) {
            chart.fireEvent('item' + e.type, chart, item, e);
            item.series.fireEvent('item' + e.type, item.series, item, e);
        }
        me.lastItem = item;
    }
});

/**
 * @abstract
 * @class Ext.chart.series.Cartesian
 * @extends Ext.chart.series.Series
 *
 * Common base class for series implementations that plot values using cartesian coordinates.
 *
 * @constructor
 */
Ext.define('Ext.chart.series.Cartesian', {
    extend: 'Ext.chart.series.Series',
    config: {
        /**
         * @cfg {String} xField
         * The field used to access the x axis value from the items from the data source.
         */
        xField: null,
        /**
         * @cfg {String|String[]} yField
         * The field(s) used to access the y-axis value(s) of the items from the data source.
         */
        yField: null,
        /**
         * @cfg {Ext.chart.axis.Axis|Number|String}
         * xAxis The chart axis the series is bound to in the 'X' direction.
         * Normally, this would be set automatically by the series.
         * For charts with multiple x-axes, this defines which x-axis is used by the series.
         * It refers to either axis' ID or the (zero-based) index of the axis
         * in the chart's {@link Ext.chart.AbstractChart#axes axes} config.
         */
        xAxis: null,
        /**
         * @cfg {Ext.chart.axis.Axis|Number|String}
         * yAxis The chart axis the series is bound to in the 'Y' direction.
         * Normally, this would be set automatically by the series.
         * For charts with multiple y-axes, this defines which y-axis is used by the series.
         * It refers to either axis' ID or the (zero-based) index of the axis
         * in the chart's {@link Ext.chart.AbstractChart#axes axes} config.
         */
        yAxis: null
    },
    directions: [
        'X',
        'Y'
    ],
    /**
     * @private
     *
     * Tells which store record fields should be used for a specific axis direction. E.g. for
     *
     *     fieldCategory<direction>: ['<fieldConfig1>', '<fieldConfig2>', ...]
     *
     * the field names from the following configs will be used:
     *
     *     series.<fieldConfig1>Field, series.<fieldConfig2>Field, ...
     *
     * See {@link Ext.chart.series.StackedCartesian#getFields}.
     *
     */
    fieldCategoryX: [
        'X'
    ],
    fieldCategoryY: [
        'Y'
    ],
    applyXAxis: function(newAxis, oldAxis) {
        return this.getChart().getAxis(newAxis) || oldAxis;
    },
    applyYAxis: function(newAxis, oldAxis) {
        return this.getChart().getAxis(newAxis) || oldAxis;
    },
    updateXAxis: function(axis) {
        axis.processData(this);
    },
    updateYAxis: function(axis) {
        axis.processData(this);
    },
    coordinateX: function() {
        return this.coordinate('X', 0, 2);
    },
    coordinateY: function() {
        return this.coordinate('Y', 1, 2);
    },
    getItemForPoint: function(x, y) {
        var me = this,
            sprite = me.getSprites()[0],
            store = me.getStore(),
            point;
        if (sprite && !me.getHidden()) {
            point = sprite.getNearestDataPoint(x, y);
        }
        return point ? {
            series: me,
            sprite: sprite,
            category: me.getItemInstancing() ? 'items' : 'markers',
            index: point.index,
            record: store.getData().items[point.index],
            field: me.getYField(),
            distance: point.distance
        } : null;
    },
    createSprite: function() {
        var me = this,
            sprite = me.callParent(),
            chart = me.getChart(),
            xAxis = me.getXAxis();
        sprite.setAttributes({
            flipXY: chart.getFlipXY(),
            xAxis: xAxis
        });
        if (sprite.setAggregator && xAxis && xAxis.getAggregator) {
            if (xAxis.getAggregator) {
                sprite.setAggregator({
                    strategy: xAxis.getAggregator()
                });
            } else {
                sprite.setAggregator({});
            }
        }
        return sprite;
    },
    getSprites: function() {
        var me = this,
            chart = this.getChart(),
            sprites = me.sprites;
        if (!chart) {
            return Ext.emptyArray;
        }
        if (!sprites.length) {
            me.createSprite();
        }
        return sprites;
    },
    getXRange: function() {
        return [
            this.dataRange[0],
            this.dataRange[2]
        ];
    },
    getYRange: function() {
        return [
            this.dataRange[1],
            this.dataRange[3]
        ];
    }
});

/**
 * @abstract
 * @extends Ext.chart.series.Cartesian
 * Abstract class for all the stacked cartesian series including area series
 * and bar series.
 */
Ext.define('Ext.chart.series.StackedCartesian', {
    extend: 'Ext.chart.series.Cartesian',
    config: {
        /**
         * @cfg {Boolean} [stacked=true]
         * `true` to display the series in its stacked configuration.
         */
        stacked: true,
        /**
         * @cfg {Boolean} [splitStacks=true]
         * `true` to stack negative/positive values in respective y-axis directions.
         */
        splitStacks: true,
        /**
         * @cfg {Boolean} [fullStack=false]
         * If `true`, the height of a stacked bar is always the full height of the chart,
         * with individual components viewed as shares of the whole determined by the
         * {@link #fullStackTotal} config.
         */
        fullStack: false,
        /**
         * @cfg {Boolean} [fullStackTotal=100]
         * If the {@link #fullStack} config is set to `true`, this will determine
         * the absolute total value of each stack.
         */
        fullStackTotal: 100,
        /**
         * @cfg {Array} hidden
         */
        hidden: []
    },
    /**
     * @private
     * @property
     * If `true`, each subsequent sprite has a lower zIndex so that the stroke of previous
     * sprite in the stack is not covered by the next sprite (which makes the very top
     * segment look odd in flat bar and area series, especially when wide strokes are used).
     */
    reversedSpriteZOrder: true,
    spriteAnimationCount: 0,
    themeColorCount: function() {
        var me = this,
            yField = me.getYField();
        return Ext.isArray(yField) ? yField.length : 1;
    },
    updateStacked: function() {
        this.processData();
    },
    updateSplitStacks: function() {
        this.processData();
    },
    coordinateY: function() {
        return this.coordinateStacked('Y', 1, 2);
    },
    coordinateStacked: function(direction, directionOffset, directionCount) {
        var me = this,
            store = me.getStore(),
            items = store.getData().items,
            itemCount = items.length,
            axis = me['get' + direction + 'Axis'](),
            hidden = me.getHidden(),
            splitStacks = me.getSplitStacks(),
            fullStack = me.getFullStack(),
            fullStackTotal = me.getFullStackTotal(),
            range = [
                0,
                0
            ],
            directions = me['fieldCategory' + direction],
            dataStart = [],
            posDataStart = [],
            negDataStart = [],
            dataEnd,
            stacked = me.getStacked(),
            sprites = me.getSprites(),
            coordinatedData = [],
            i, j, k, fields, fieldCount, posTotals, negTotals, fieldCategoriesItem, data, attr;
        if (!sprites.length) {
            return;
        }
        for (i = 0; i < directions.length; i++) {
            fieldCategoriesItem = directions[i];
            fields = me.getFields([
                fieldCategoriesItem
            ]);
            fieldCount = fields.length;
            for (j = 0; j < itemCount; j++) {
                dataStart[j] = 0;
                posDataStart[j] = 0;
                negDataStart[j] = 0;
            }
            for (j = 0; j < fieldCount; j++) {
                if (!hidden[j]) {
                    coordinatedData[j] = me.coordinateData(items, fields[j], axis);
                }
            }
            if (stacked && fullStack) {
                posTotals = [];
                if (splitStacks) {
                    negTotals = [];
                }
                for (j = 0; j < itemCount; j++) {
                    posTotals[j] = 0;
                    if (splitStacks) {
                        negTotals[j] = 0;
                    }
                    for (k = 0; k < fieldCount; k++) {
                        data = coordinatedData[k];
                        if (!data) {
                            // If the field is hidden there's no coordinated data for it.
                            
                            continue;
                        }
                        data = data[j];
                        if (data >= 0 || !splitStacks) {
                            posTotals[j] += data;
                        } else if (data < 0) {
                            negTotals[j] += data;
                        }
                    }
                }
            }
            // else not a valid number
            for (j = 0; j < fieldCount; j++) {
                attr = {};
                if (hidden[j]) {
                    attr['dataStart' + fieldCategoriesItem] = dataStart;
                    attr['data' + fieldCategoriesItem] = dataStart;
                    sprites[j].setAttributes(attr);
                    
                    continue;
                }
                data = coordinatedData[j];
                if (stacked) {
                    dataEnd = [];
                    for (k = 0; k < itemCount; k++) {
                        if (!data[k]) {
                            data[k] = 0;
                        }
                        if (data[k] >= 0 || !splitStacks) {
                            if (fullStack && posTotals[k]) {
                                data[k] *= fullStackTotal / posTotals[k];
                            }
                            dataStart[k] = posDataStart[k];
                            posDataStart[k] += data[k];
                            dataEnd[k] = posDataStart[k];
                        } else {
                            if (fullStack && negTotals[k]) {
                                data[k] *= fullStackTotal / negTotals[k];
                            }
                            dataStart[k] = negDataStart[k];
                            negDataStart[k] += data[k];
                            dataEnd[k] = negDataStart[k];
                        }
                    }
                    attr['dataStart' + fieldCategoriesItem] = dataStart;
                    attr['data' + fieldCategoriesItem] = dataEnd;
                    Ext.chart.Util.expandRange(range, dataStart);
                    Ext.chart.Util.expandRange(range, dataEnd);
                } else {
                    attr['dataStart' + fieldCategoriesItem] = dataStart;
                    attr['data' + fieldCategoriesItem] = data;
                    Ext.chart.Util.expandRange(range, data);
                }
                sprites[j].setAttributes(attr);
            }
        }
        range = Ext.chart.Util.validateRange(range, me.defaultRange);
        me.dataRange[directionOffset] = range[0];
        me.dataRange[directionOffset + directionCount] = range[1];
        attr = {};
        attr['dataMin' + direction] = range[0];
        attr['dataMax' + direction] = range[1];
        for (i = 0; i < sprites.length; i++) {
            sprites[i].setAttributes(attr);
        }
    },
    getFields: function(fieldCategory) {
        var me = this,
            fields = [],
            ln = fieldCategory.length,
            i, fieldsItem;
        for (i = 0; i < ln; i++) {
            fieldsItem = me['get' + fieldCategory[i] + 'Field']();
            if (Ext.isArray(fieldsItem)) {
                fields.push.apply(fields, fieldsItem);
            } else {
                fields.push(fieldsItem);
            }
        }
        return fields;
    },
    updateLabelOverflowPadding: function(labelOverflowPadding) {
        var me = this,
            label;
        if (!me.isConfiguring) {
            label = me.getLabel();
            if (label) {
                label.setAttributes({
                    labelOverflowPadding: labelOverflowPadding
                });
            }
        }
    },
    updateLabelData: function() {
        var me = this,
            label = me.getLabel();
        if (label) {
            label.setAttributes({
                labelOverflowPadding: me.getLabelOverflowPadding()
            });
        }
        me.callParent();
    },
    getSprites: function() {
        var me = this,
            chart = me.getChart(),
            fields = me.getFields(me.fieldCategoryY),
            itemInstancing = me.getItemInstancing(),
            sprites = me.sprites,
            hidden = me.getHidden(),
            spritesCreated = false,
            fieldCount = fields.length,
            i, sprite;
        if (!chart) {
            return [];
        }
        // Create one Ext.chart.series.sprite.StackedCartesian sprite per field.
        for (i = 0; i < fieldCount; i++) {
            sprite = sprites[i];
            if (!sprite) {
                sprite = me.createSprite();
                sprite.setAttributes({
                    zIndex: (me.reversedSpriteZOrder ? -1 : 1) * i
                });
                sprite.setField(fields[i]);
                spritesCreated = true;
                hidden.push(false);
                if (itemInstancing) {
                    sprite.getMarker('items').getTemplate().setAttributes(me.getStyleByIndex(i));
                } else {
                    sprite.setAttributes(me.getStyleByIndex(i));
                }
            }
        }
        if (spritesCreated) {
            me.updateHidden(hidden);
        }
        return sprites;
    },
    getItemForPoint: function(x, y) {
        var me = this,
            sprites = me.getSprites(),
            store = me.getStore(),
            hidden = me.getHidden(),
            minDistance = Infinity,
            item = null,
            spriteIndex = -1,
            pointIndex = -1,
            point, yField, sprite, i, ln;
        for (i = 0 , ln = sprites.length; i < ln; i++) {
            if (hidden[i]) {
                
                continue;
            }
            sprite = sprites[i];
            point = sprite.getNearestDataPoint(x, y);
            // Don't stop when the first matching point is found.
            // Keep looking for the nearest point.
            if (point) {
                if (point.distance < minDistance) {
                    minDistance = point.distance;
                    pointIndex = point.index;
                    spriteIndex = i;
                }
            }
        }
        if (spriteIndex > -1) {
            yField = me.getYField();
            item = {
                series: me,
                sprite: sprites[spriteIndex],
                category: me.getItemInstancing() ? 'items' : 'markers',
                index: pointIndex,
                record: store.getData().items[pointIndex],
                // Handle the case where we're stacked but a single segment
                field: typeof yField === 'string' ? yField : yField[spriteIndex],
                distance: minDistance
            };
        }
        return item;
    },
    provideLegendInfo: function(target) {
        var me = this,
            sprites = me.getSprites(),
            title = me.getTitle(),
            field = me.getYField(),
            hidden = me.getHidden(),
            single = sprites.length === 1,
            style, fill, i, name;
        for (i = 0; i < sprites.length; i++) {
            style = me.getStyleByIndex(i);
            fill = style.fillStyle;
            if (title) {
                if (Ext.isArray(title)) {
                    name = title[i];
                } else if (single) {
                    name = title;
                }
            }
            if (!title || !name) {
                if (Ext.isArray(field)) {
                    name = field[i];
                } else {
                    name = me.getId();
                }
            }
            target.push({
                name: name,
                mark: (Ext.isObject(fill) ? fill.stops && fill.stops[0].color : fill) || style.strokeStyle || 'black',
                disabled: hidden[i],
                series: me.getId(),
                index: i
            });
        }
    },
    onSpriteAnimationStart: function(sprite) {
        this.spriteAnimationCount++;
        if (this.spriteAnimationCount === 1) {
            this.fireEvent('animationstart');
        }
    },
    onSpriteAnimationEnd: function(sprite) {
        this.spriteAnimationCount--;
        if (this.spriteAnimationCount === 0) {
            this.fireEvent('animationend');
        }
    }
});

/**
 * Base class for all series sprites.
 * Defines attributes common to all series sprites, like data in x/y directions and its
 * min/max values, and configs, like the {@link Ext.chart.series.Series} instance that manages
 * the sprite.
 *
 */
Ext.define('Ext.chart.series.sprite.Series', {
    extend: 'Ext.draw.sprite.Sprite',
    mixins: {
        markerHolder: 'Ext.chart.MarkerHolder'
    },
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [dataMinX=0] Data minimum on the x-axis.
                 */
                dataMinX: 'number',
                /**
                 * @cfg {Number} [dataMaxX=1] Data maximum on the x-axis.
                 */
                dataMaxX: 'number',
                /**
                 * @cfg {Number} [dataMinY=0] Data minimum on the y-axis.
                 */
                dataMinY: 'number',
                /**
                 * @cfg {Number} [dataMaxY=1] Data maximum on the y-axis.
                 */
                dataMaxY: 'number',
                /**
                 * @cfg {Array} [rangeX=null] Data range derived from all the series bound
                 * to the x-axis.
                 */
                rangeX: 'data',
                /**
                 * @cfg {Array} [rangeY=null] Data range derived from all the series bound
                 * to the y-axis.
                 */
                rangeY: 'data',
                /**
                 * @cfg {Object} [dataX=null] Data items on the x-axis.
                 */
                dataX: 'data',
                /**
                 * @cfg {Object} [dataY=null] Data items on the y-axis.
                 */
                dataY: 'data',
                /**
                 * @cfg {Object} [labels=null] Labels used in the series.
                 */
                labels: 'default',
                /**
                 * @cfg {Number} [labelOverflowPadding=10] Padding around labels to determine
                 * overlap.
                 */
                labelOverflowPadding: 'number'
            },
            defaults: {
                dataMinX: 0,
                dataMaxX: 1,
                dataMinY: 0,
                dataMaxY: 1,
                rangeX: null,
                rangeY: null,
                dataX: null,
                dataY: null,
                labels: null,
                labelOverflowPadding: 10
            },
            triggers: {
                dataX: 'bbox',
                dataY: 'bbox',
                dataMinX: 'bbox',
                dataMaxX: 'bbox',
                dataMinY: 'bbox',
                dataMaxY: 'bbox'
            }
        }
    },
    config: {
        /**
         * @private
         * @cfg {Object} store The store that is passed to the renderer.
         */
        store: null,
        series: null,
        /**
         * @cfg {String} field The store field used by the series.
         */
        field: null
    }
});

/**
 * Cartesian sprite.
 */
Ext.define('Ext.chart.series.sprite.Cartesian', {
    extend: 'Ext.chart.series.sprite.Series',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [selectionTolerance=20]
                 * The distance from the event position to the sprite's data points to trigger
                 * interactions (used for 'iteminfo', etc).
                 */
                selectionTolerance: 'number',
                /**
                 * @cfg {Boolean} flipXY If flipXY is 'true', the series is flipped.
                 */
                flipXY: 'bool',
                renderer: 'default',
                // Visible range of data (pan/zoom) information.
                visibleMinX: 'number',
                visibleMinY: 'number',
                visibleMaxX: 'number',
                visibleMaxY: 'number',
                innerWidth: 'number',
                innerHeight: 'number'
            },
            defaults: {
                selectionTolerance: 20,
                flipXY: false,
                renderer: null,
                transformFillStroke: false,
                visibleMinX: 0,
                visibleMinY: 0,
                visibleMaxX: 1,
                visibleMaxY: 1,
                innerWidth: 1,
                innerHeight: 1
            },
            triggers: {
                dataX: 'dataX,bbox',
                dataY: 'dataY,bbox',
                visibleMinX: 'panzoom',
                visibleMinY: 'panzoom',
                visibleMaxX: 'panzoom',
                visibleMaxY: 'panzoom',
                innerWidth: 'panzoom',
                innerHeight: 'panzoom'
            },
            updaters: {
                dataX: function(attr) {
                    this.processDataX();
                    this.scheduleUpdater(attr, 'dataY', [
                        'dataY'
                    ]);
                },
                dataY: function() {
                    this.processDataY();
                },
                panzoom: function(attr) {
                    // dx, dy are deltas between min & max of coordinated data values.
                    var dx = attr.visibleMaxX - attr.visibleMinX,
                        dy = attr.visibleMaxY - attr.visibleMinY,
                        innerWidth = attr.flipXY ? attr.innerHeight : attr.innerWidth,
                        innerHeight = !attr.flipXY ? attr.innerHeight : attr.innerWidth,
                        surface = this.getSurface(),
                        isRtl = surface ? surface.getInherited().rtl : false;
                    attr.scalingCenterX = 0;
                    attr.scalingCenterY = 0;
                    attr.scalingX = innerWidth / dx;
                    attr.scalingY = innerHeight / dy;
                    // (attr.visibleMinY * attr.scalingY) will be the vertical position of
                    // our minimum data points, which we want to be at zero, so we offset
                    // by this amount.
                    attr.translationX = -(attr.visibleMinX * attr.scalingX);
                    attr.translationY = -(attr.visibleMinY * attr.scalingY);
                    if (isRtl && !attr.flipXY) {
                        attr.scalingX *= -1;
                        attr.translationX *= -1;
                        attr.translationX += innerWidth;
                    }
                    this.applyTransformations(true);
                }
            }
        }
    },
    processDataY: Ext.emptyFn,
    processDataX: Ext.emptyFn,
    updatePlainBBox: function(plain) {
        var attr = this.attr;
        plain.x = attr.dataMinX;
        plain.y = attr.dataMinY;
        plain.width = attr.dataMaxX - attr.dataMinX;
        plain.height = attr.dataMaxY - attr.dataMinY;
    },
    /**
     * Does a binary search of the data on the x-axis using the given key.
     * @param {String} key
     * @return {*}
     */
    binarySearch: function(key) {
        var dx = this.attr.dataX,
            start = 0,
            end = dx.length,
            mid, val;
        if (key <= dx[0]) {
            return start;
        }
        if (key >= dx[end - 1]) {
            return end - 1;
        }
        while (start + 1 < end) {
            mid = (start + end) >> 1;
            val = dx[mid];
            if (val === key) {
                return mid;
            } else if (val < key) {
                start = mid;
            } else {
                end = mid;
            }
        }
        return start;
    },
    render: function(surface, ctx, surfaceClipRect) {
        var me = this,
            attr = me.attr,
            margin = 1,
            // TODO: why do we need it?
            inverseMatrix = attr.inverseMatrix.clone(),
            dataClipRect;
        // The sprite's `attr.matrix` is stretching/shrinking data coordinates
        // to surface coordinates.
        // This matrix is set (indirectly) by the 'panzoom' updater.
        // The sprite's `attr.inverseMatrix` does the opposite.
        //
        // The `surface.matrix` of the 'series' surface of a cartesian chart flips the
        // surface content vertically, so that y=0 is at the bottom (look for
        // `surface.matrix.set` call in the CartesianChart.performLayout method).
        // This matrix is set in the 'performLayout' of the CartesianChart.
        // The `surface.inverseMatrix` flips the content back.
        //
        // By combining the inverse matrices of the series surface and the series sprite,
        // we essentially get a transformation that allows us to go from surface coordinates
        // in a final flipped drawing back to data points.
        //
        // For example
        //
        //     inverseMatrix.transformPoint([ 0, rect[3] ])
        //     inverseMatrix.transformPoint([ rect[2], 0 ])
        //
        // will return
        //
        //     [attr.dataMinX, attr.dataMinY]
        //     [attr.dataMaxX, attr.dataMaxY]
        //
        // because left/bottom and top/right of the series surface is where the first smallest
        // and last largest data points would be (given no pan/zoom), respectively.
        //
        // So the `dataClipRect` passed to the `renderClipped` call below is effectively
        // the visible rect in data (not surface!) coordinates.
        // It is important to note, that the all the scaling and translation is defined
        // by the sprite's matrix, the 'series' surface matrix does not contain scaling
        // or translation components, except for the vertical flipping.
        // This is important because there is a common pattern in chart series sprites
        // (MarkerHolders) - instead of using transform attributes for their Markers
        // (e.g. instances of a 'rect' sprite in case of 'bar' series), the attributes
        // that would position a sprite with no transformations are transformed.
        // For example, to draw a rect with coordinates TL(10, 10), BR(20, 40),
        // we could use the folling 'rect' sprite attributes:
        //
        //     {
        //         x: 0,
        //         y: 0
        //         width: 10,
        //         height: 30
        //
        //         translationX: 10,
        //         translationY: 10
        //
        // But the correct thing to do here is
        //
        //    {
        //        x: 10,
        //        y: 10,
        //        width: 10,
        //        height: 30
        //    }
        //
        // Similarly, if the sprite was scaled, the 'x', 'y', 'width', 'height' attributes
        // would have to account for that as well.
        //
        // This is done, so that the attribute values a marker gets by the time it renders,
        // are the final values, and are not affected later by other transforms, such as
        // surface matrix scaling, which could ruin the visual result, if the attributes
        // values are doctored to make lines align to the pixel grid (which is typically
        // the case).
        inverseMatrix.appendMatrix(surface.inverseMatrix);
        if (attr.dataX === null || attr.dataX === undefined) {
            return;
        }
        if (attr.dataY === null || attr.dataY === undefined) {
            return;
        }
        if (inverseMatrix.getXX() * inverseMatrix.getYX() || inverseMatrix.getXY() * inverseMatrix.getYY()) {
            Ext.Logger.warn('Cartesian Series sprite does not support rotation/sheering');
            return;
        }
        dataClipRect = inverseMatrix.transformList([
            [
                surfaceClipRect[0] - margin,
                surfaceClipRect[3] + margin
            ],
            // (left, height)
            [
                surfaceClipRect[0] + surfaceClipRect[2] + margin,
                -margin
            ]
        ]);
        // (width, top)
        dataClipRect = dataClipRect[0].concat(dataClipRect[1]);
        // TODO: RTL improvements:
        // TODO: produce such a dataClipRect here, so that we don't have to do:
        // TODO: min = Math.min(dataClipRect[0], dataClipRect[2])
        // TODO: max = Math.max(dataClipRect[0], dataClipRect[2])
        // TODO: inside each 'renderClipped' call
        me.renderClipped(surface, ctx, dataClipRect, surfaceClipRect);
    },
    /**
     * Render the given visible clip range.
     * @param {Ext.draw.Surface} surface A draw container surface.
     * @param {CanvasRenderingContext2D} ctx A context object that is API compatible with the native
     * [CanvasRenderingContext2D](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D).
     * @param {Number[]} dataClipRect The clip rect in data coordinates, roughly equivalent to
     * [attr.dataMinX, attr.dataMinY, attr.dataMaxX, attr.dataMaxY] for an untranslated/unscaled
     * surface/sprite.
     * @param {Number[]} surfaceClipRect The clip rect in surface coordinates:
     * [left, top, width, height].
     * @method
     */
    renderClipped: Ext.emptyFn,
    /**
     * Get the nearest item index from point (x, y). -1 as not found.
     * @param {Number} x
     * @param {Number} y
     * @return {Number} The index
     * @deprecated 6.5.2 Use {@link #getNearestDataPoint} instead.
     */
    getIndexNearPoint: function(x, y) {
        var result = this.getNearestDataPoint(x, y);
        return result ? result.index : -1;
    },
    /**
     * Given a point in 'series' surface element coordinates, returns the `index` of the
     * sprite's data point that is nearest to that point, along with the `distance`
     * between points.
     * If the `selectionTolerance` attribute of the sprite is not zero, only the data points
     * that are within that pixel distance from the given point will be checked.
     * In the event no such data points exist or the data is empty, `null` is returned.
     *
     * Notes:
     * 1) given a mouse/pointer event object, the surface coordinates of the event can be
     *    obtained with the `getEventXY` method of the chart;
     * 2) using `selectionTolerance` of zero is useful for series with no visible markers,
     *    such as the Area series, where this attribute becomes meaningless.
     *
     * @param {Number} x
     * @param {Number} y
     * @return {Object}
     */
    getNearestDataPoint: function(x, y) {
        var me = this,
            attr = me.attr,
            series = me.getSeries(),
            surface = me.getSurface(),
            items = me.boundMarkers.items,
            matrix = attr.matrix,
            dataX = attr.dataX,
            dataY = attr.dataY,
            selectionTolerance = attr.selectionTolerance,
            minDistance = Infinity,
            index = -1,
            result = null,
            distance, dx, dy, xy, i, ln, end, inc, bbox;
        // Notes:
        // Instead of converting the given point from surface coordinates to data coordinates
        // and then measuring the distances between it and the data points, we have to
        // convert all the data points to surface coordinates and measure the distances
        // between them and the given point. This is because the data coordinates can use
        // different scales, which makes distance measurement impossible.
        // For example, if the x-axis is a `category` axis, the categories will be assigned
        // indexes starting from 0, that's what the `attr.dataX` array will contain;
        // and if the y-axis is a `numeric` axis, the `attr.dataY` array will simply contain
        // the original values.
        //
        // Either 'items' or 'markers' will be highlighted. If a sprite has both (for example,
        // 'bar' series with the 'marker' config, where the bars are 'items' and marker instances
        // are 'markers'), only the 'items' (bars) will be highlighted.
        if (items) {
            ln = dataX.length;
            if (series.reversedSpriteZOrder) {
                i = ln - 1;
                end = -1;
                inc = -1;
            } else {
                i = 0;
                end = ln;
                inc = 1;
            }
            for (; i !== end; i += inc) {
                bbox = me.getMarkerBBox('items', i);
                // Transform the given surface element coordinates to logical coordinates
                // of the surface (the ones the bbox uses).
                xy = surface.inverseMatrix.transformPoint([
                    x,
                    y
                ]);
                if (Ext.draw.Draw.isPointInBBox(xy[0], xy[1], bbox)) {
                    index = i;
                    minDistance = 0;
                    // Return the first item that contains our touch point.
                    break;
                }
            }
        } else {
            // markers
            for (i = 0 , ln = dataX.length; i < ln; i++) {
                // Convert from data coordinates to coordinates within inner size rectangle.
                // See `panzoom` method for more details.
                xy = matrix.transformPoint([
                    dataX[i],
                    dataY[i]
                ]);
                // Flip back vertically and padding adjust (see `render` method comments).
                xy = surface.matrix.transformPoint(xy);
                // Essentially sprites go through the same two transformations when they render
                // data points.
                dx = x - xy[0];
                dy = y - xy[1];
                distance = Math.sqrt(dx * dx + dy * dy);
                if (selectionTolerance && distance > selectionTolerance) {
                    
                    continue;
                }
                if (distance < minDistance) {
                    minDistance = distance;
                    index = i;
                }
            }
        }
        // Keep looking for the nearest marker.
        if (index > -1) {
            result = {
                index: index,
                distance: minDistance
            };
        }
        return result;
    }
});

/**
 * @class Ext.chart.series.sprite.StackedCartesian
 * @extends Ext.chart.series.sprite.Cartesian
 *
 * Stacked cartesian sprite.
 */
Ext.define('Ext.chart.series.sprite.StackedCartesian', {
    extend: 'Ext.chart.series.sprite.Cartesian',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @private
                 * @cfg {Number} [groupCount=1] The number of items (e.g. bars) in a group.
                 */
                groupCount: 'number',
                /**
                 * @private
                 * @cfg {Number} [groupOffset=0] The group index of the series sprite.
                 */
                groupOffset: 'number',
                /**
                 * @private
                 * @cfg {Object} [dataStartY=null] The starting point of the data
                 * used in the series.
                 */
                dataStartY: 'data'
            },
            defaults: {
                selectionTolerance: 20,
                groupCount: 1,
                groupOffset: 0,
                dataStartY: null
            },
            triggers: {
                dataStartY: 'dataY,bbox'
            }
        }
    }
});

/**
 * @class Ext.chart.series.sprite.Area
 * @extends Ext.chart.series.sprite.StackedCartesian
 *
 * Area series sprite.
 */
Ext.define('Ext.chart.series.sprite.Area', {
    alias: 'sprite.areaSeries',
    extend: 'Ext.chart.series.sprite.StackedCartesian',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Boolean} [step=false] 'true' if the area is represented with steps
                 * instead of lines.
                 */
                step: 'bool'
            },
            defaults: {
                selectionTolerance: 0,
                step: false
            }
        }
    },
    renderClipped: function(surface, ctx, dataClipRect) {
        var me = this,
            store = me.getStore(),
            series = me.getSeries(),
            attr = me.attr,
            dataX = attr.dataX,
            dataY = attr.dataY,
            dataStartY = attr.dataStartY,
            matrix = attr.matrix,
            x, y, i, lastX, lastY, startX, startY,
            xx = matrix.elements[0],
            dx = matrix.elements[4],
            yy = matrix.elements[3],
            dy = matrix.elements[5],
            surfaceMatrix = me.surfaceMatrix,
            markerCfg = {},
            min = Math.min(dataClipRect[0], dataClipRect[2]),
            max = Math.max(dataClipRect[0], dataClipRect[2]),
            start = Math.max(0, this.binarySearch(min)),
            end = Math.min(dataX.length - 1, this.binarySearch(max) + 1),
            renderer = attr.renderer,
            rendererData = {
                store: store
            },
            rendererChanges;
        ctx.beginPath();
        startX = dataX[start] * xx + dx;
        startY = dataY[start] * yy + dy;
        ctx.moveTo(startX, startY);
        if (attr.step) {
            lastY = startY;
            for (i = start; i <= end; i++) {
                x = dataX[i] * xx + dx;
                y = dataY[i] * yy + dy;
                ctx.lineTo(x, lastY);
                ctx.lineTo(x, lastY = y);
            }
        } else {
            for (i = start; i <= end; i++) {
                x = dataX[i] * xx + dx;
                y = dataY[i] * yy + dy;
                ctx.lineTo(x, y);
            }
        }
        if (dataStartY) {
            if (attr.step) {
                lastX = dataX[end] * xx + dx;
                for (i = end; i >= start; i--) {
                    x = dataX[i] * xx + dx;
                    y = dataStartY[i] * yy + dy;
                    ctx.lineTo(lastX, y);
                    ctx.lineTo(lastX = x, y);
                }
            } else {
                for (i = end; i >= start; i--) {
                    x = dataX[i] * xx + dx;
                    y = dataStartY[i] * yy + dy;
                    ctx.lineTo(x, y);
                }
            }
        } else {
            ctx.lineTo(dataX[end] * xx + dx, y);
            ctx.lineTo(dataX[end] * xx + dx, dy);
            ctx.lineTo(startX, dy);
            ctx.lineTo(startX, dataY[i] * yy + dy);
        }
        if (attr.transformFillStroke) {
            attr.matrix.toContext(ctx);
        }
        ctx.fill();
        if (attr.transformFillStroke) {
            attr.inverseMatrix.toContext(ctx);
        }
        ctx.beginPath();
        ctx.moveTo(startX, startY);
        if (attr.step) {
            for (i = start; i <= end; i++) {
                x = dataX[i] * xx + dx;
                y = dataY[i] * yy + dy;
                ctx.lineTo(x, lastY);
                ctx.lineTo(x, lastY = y);
                markerCfg.translationX = surfaceMatrix.x(x, y);
                markerCfg.translationY = surfaceMatrix.y(x, y);
                if (renderer) {
                    // callback(fn, scope, args, delay, caller)
                    rendererChanges = Ext.callback(renderer, null, [
                        me,
                        markerCfg,
                        rendererData,
                        i
                    ], 0, series);
                    Ext.apply(markerCfg, rendererChanges);
                }
                me.putMarker('markers', markerCfg, i, !renderer);
            }
        } else {
            for (i = start; i <= end; i++) {
                x = dataX[i] * xx + dx;
                y = dataY[i] * yy + dy;
                ctx.lineTo(x, y);
                markerCfg.translationX = surfaceMatrix.x(x, y);
                markerCfg.translationY = surfaceMatrix.y(x, y);
                if (renderer) {
                    rendererChanges = Ext.callback(renderer, null, [
                        me,
                        markerCfg,
                        rendererData,
                        i
                    ], 0, series);
                    Ext.apply(markerCfg, rendererChanges);
                }
                me.putMarker('markers', markerCfg, i, !renderer);
            }
        }
        if (attr.transformFillStroke) {
            attr.matrix.toContext(ctx);
        }
        ctx.stroke();
    }
});

/**
 * @class Ext.chart.series.Area
 * @extends Ext.chart.series.StackedCartesian
 *
 * Creates an Area Chart.
 * 
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        insetPadding: 40,
 *        store: {
 *            fields: ['name', 'data1', 'data2', 'data3'],
 *            data: [{
 *                name: 'metric one',
 *                data1: 10,
 *                data2: 12,
 *                data3: 14
 *            }, {
 *                name: 'metric two',
 *                data1: 7,
 *                data2: 8,
 *                data3: 16
 *            }, {
 *                name: 'metric three',
 *                data1: 5,
 *                data2: 2,
 *                data3: 14
 *            }, {
 *                name: 'metric four',
 *                data1: 2,
 *                data2: 14,
 *                data3: 6
 *            }, {
 *                name: 'metric five',
 *                data1: 27,
 *                data2: 38,
 *                data3: 36
 *            }]
 *        },
 *        axes: [{
 *            type: 'numeric',
 *            position: 'left',
 *            fields: ['data1'],
 *            grid: true,
 *            minimum: 0
 *        }, {
 *            type: 'category',
 *            position: 'bottom',
 *            fields: ['name']
 *        }],
 *        series: {
 *            type: 'area',
 *            subStyle: {
 *                fill: ['#0A3F50', '#30BDA7', '#96D4C6']
 *            },
 *            xField: 'name',
 *            yField: ['data1', 'data2', 'data3']
 *        }
 *     });
 */
Ext.define('Ext.chart.series.Area', {
    extend: 'Ext.chart.series.StackedCartesian',
    alias: 'series.area',
    type: 'area',
    /**
     * @property seriesType
     * @inheritdoc
     */
    seriesType: 'areaSeries',
    isArea: true,
    requires: [
        'Ext.chart.series.sprite.Area'
    ],
    config: {
        /**
         * @cfg splitStacks
         * @inheritdoc
         */
        splitStacks: false
    }
});
/**
         * @cfg renderer
         * @inheritdoc
         * Area series renderers only affect markers.
         * For styling individual segments with a renderer it is possible to use
         * the Line series with {@link Ext.chart.series.Line#fill} config set to `true`,
         * which makes Line series look like Area series.
         */

/**
 * @class Ext.chart.series.sprite.Bar
 * @extends Ext.chart.series.sprite.StackedCartesian
 *
 * Draws a sprite used in the bar series.
 */
Ext.define('Ext.chart.series.sprite.Bar', {
    alias: 'sprite.barSeries',
    extend: 'Ext.chart.series.sprite.StackedCartesian',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [minBarWidth=2] The minimum bar width.
                 */
                minBarWidth: 'number',
                /**
                 * @cfg {Number} [maxBarWidth=100] The maximum bar width.
                 */
                maxBarWidth: 'number',
                /**
                 * @cfg {Number} [minGapWidth=5] The minimum gap between bars.
                 */
                minGapWidth: 'number',
                /**
                 * @cfg {Number} [radius=0] The degree of rounding for rounded bars.
                 */
                radius: 'number',
                /**
                 * @cfg {Number} [inGroupGapWidth=3] The gap between grouped bars.
                 */
                inGroupGapWidth: 'number'
            },
            defaults: {
                minBarWidth: 2,
                maxBarWidth: 100,
                minGapWidth: 5,
                inGroupGapWidth: 3,
                radius: 0
            }
        }
    },
    drawLabel: function(text, dataX, dataStartY, dataY, labelId) {
        var me = this,
            attr = me.attr,
            label = me.getMarker('labels'),
            labelTpl = label.getTemplate(),
            labelCfg = me.labelCfg || (me.labelCfg = {}),
            surfaceMatrix = me.surfaceMatrix,
            labelOverflowPadding = attr.labelOverflowPadding,
            labelDisplay = labelTpl.attr.display,
            labelOrientation = labelTpl.attr.orientation,
            isVerticalText = (labelOrientation === 'horizontal' && attr.flipXY) || (labelOrientation === 'vertical' && !attr.flipXY) || !labelOrientation,
            calloutLine = labelTpl.getCalloutLine(),
            labelY, halfText, labelBBox, calloutLineLength, changes, hasPendingChanges, params;
        // The coordinates below (data point converted to surface coordinates)
        // are just for the renderer to give it a notion of where the label will be positioned.
        // The actual position of the label will be different
        // (unless the renderer returns x/y coordinates in the changes object)
        // and depend on several things including the size of the text,
        // which has to be measured after the renderer call,
        // since text can be modified by the renderer.
        labelCfg.x = surfaceMatrix.x(dataX, dataY);
        labelCfg.y = surfaceMatrix.y(dataX, dataY);
        if (calloutLine) {
            calloutLineLength = calloutLine.length;
        } else {
            calloutLineLength = 0;
        }
        // Set defaults
        if (!attr.flipXY) {
            labelCfg.rotationRads = -Math.PI * 0.5;
        } else {
            labelCfg.rotationRads = 0;
        }
        labelCfg.calloutVertical = !attr.flipXY;
        // Check if we have a specific orientation specified, if so, set
        // the appropriate values.
        switch (labelOrientation) {
            case 'horizontal':
                labelCfg.rotationRads = 0;
                labelCfg.calloutVertical = false;
                break;
            case 'vertical':
                labelCfg.rotationRads = -Math.PI * 0.5;
                labelCfg.calloutVertical = true;
                break;
        }
        labelCfg.text = text;
        if (labelTpl.attr.renderer) {
            // The label instance won't exist on first render before the renderer is called,
            // it's only created later by `me.putMarker` after the renderer call. To make
            // sure the renderer always can access the label instance, we make this check here.
            if (!label.get(labelId)) {
                label.putMarkerFor('labels', {}, labelId);
            }
            params = [
                text,
                label,
                labelCfg,
                {
                    store: me.getStore()
                },
                labelId
            ];
            changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries());
            if (typeof changes === 'string') {
                labelCfg.text = changes;
            } else if (typeof changes === 'object') {
                if ('text' in changes) {
                    labelCfg.text = changes.text;
                }
                hasPendingChanges = true;
            }
        }
        labelBBox = me.getMarkerBBox('labels', labelId, true);
        if (!labelBBox) {
            me.putMarker('labels', labelCfg, labelId);
            labelBBox = me.getMarkerBBox('labels', labelId, true);
        }
        if (calloutLineLength > 0) {
            halfText = calloutLineLength;
        } else if (calloutLineLength === 0) {
            halfText = (isVerticalText ? labelBBox.width : labelBBox.height) / 2;
        } else {
            halfText = (isVerticalText ? labelBBox.width : labelBBox.height) / 2 + labelOverflowPadding;
        }
        if (dataStartY > dataY) {
            halfText = -halfText;
        }
        if (isVerticalText) {
            labelY = (labelDisplay === 'insideStart') ? dataStartY + halfText : dataY - halfText;
        } else {
            labelY = (labelDisplay === 'insideStart') ? dataStartY + labelOverflowPadding * 2 : dataY - labelOverflowPadding * 2;
        }
        labelCfg.x = surfaceMatrix.x(dataX, labelY);
        labelCfg.y = surfaceMatrix.y(dataX, labelY);
        labelY = (labelDisplay === 'insideStart') ? dataStartY : dataY;
        labelCfg.calloutStartX = surfaceMatrix.x(dataX, labelY);
        labelCfg.calloutStartY = surfaceMatrix.y(dataX, labelY);
        labelY = (labelDisplay === 'insideStart') ? dataStartY - halfText : dataY + halfText;
        labelCfg.calloutPlaceX = surfaceMatrix.x(dataX, labelY);
        labelCfg.calloutPlaceY = surfaceMatrix.y(dataX, labelY);
        labelCfg.calloutColor = (calloutLine && calloutLine.color) || me.attr.fillStyle;
        if (calloutLine) {
            if (calloutLine.width) {
                labelCfg.calloutWidth = calloutLine.width;
            }
        } else {
            labelCfg.calloutColor = 'none';
        }
        if (dataStartY > dataY) {
            halfText = -halfText;
        }
        if (Math.abs(dataY - dataStartY) <= halfText * 2 || labelDisplay === 'outside') {
            labelCfg.callout = 1;
        } else {
            labelCfg.callout = 0;
        }
        if (hasPendingChanges) {
            Ext.apply(labelCfg, changes);
        }
        me.putMarker('labels', labelCfg, labelId);
    },
    drawBar: function(ctx, surface, rect, left, top, right, bottom, index) {
        var me = this,
            itemCfg = {},
            renderer = me.attr.renderer,
            changes;
        itemCfg.x = left;
        itemCfg.y = top;
        itemCfg.width = right - left;
        itemCfg.height = bottom - top;
        itemCfg.radius = me.attr.radius;
        if (renderer) {
            changes = Ext.callback(renderer, null, [
                me,
                itemCfg,
                {
                    store: me.getStore()
                },
                index
            ], 0, me.getSeries());
            Ext.apply(itemCfg, changes);
        }
        me.putMarker('items', itemCfg, index, !renderer);
    },
    renderClipped: function(surface, ctx, dataClipRect) {
        if (this.cleanRedraw) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            attr = me.attr,
            dataX = attr.dataX,
            dataY = attr.dataY,
            dataText = attr.labels,
            dataStartY = attr.dataStartY,
            groupCount = attr.groupCount,
            groupOffset = attr.groupOffset - (groupCount - 1) * 0.5,
            inGroupGapWidth = attr.inGroupGapWidth,
            lineWidth = ctx.lineWidth,
            matrix = attr.matrix,
            xx = matrix.elements[0],
            yy = matrix.elements[3],
            dx = matrix.elements[4],
            dy = surface.roundPixel(matrix.elements[5]) - 1,
            maxBarWidth = Math.abs(xx) - attr.minGapWidth,
            minBarWidth = (Math.min(maxBarWidth, attr.maxBarWidth) - inGroupGapWidth * (groupCount - 1)) / groupCount,
            barWidth = surface.roundPixel(Math.max(attr.minBarWidth, minBarWidth)),
            surfaceMatrix = me.surfaceMatrix,
            left, right, bottom, top, i, center,
            halfLineWidth = 0.5 * attr.lineWidth,
            // Finding min/max so that bars render properly in both LTR and RTL modes.
            min = Math.min(dataClipRect[0], dataClipRect[2]),
            max = Math.max(dataClipRect[0], dataClipRect[2]),
            start = Math.max(0, Math.floor(min)),
            end = Math.min(dataX.length - 1, Math.ceil(max)),
            isDrawLabels = dataText && me.getMarker('labels'),
            yLow, yHi;
        // The scaling (xx) and translation (dx) here will already be such that the midpoints
        // of the first and last bars are not at the surface edges (which would mean that
        // bars are half-clipped), but padded, so that those bars are fully visible
        // (assuming no pan/zoom).
        for (i = start; i <= end; i++) {
            yLow = dataStartY ? dataStartY[i] : 0;
            yHi = dataY[i];
            center = dataX[i] * xx + dx + groupOffset * (barWidth + inGroupGapWidth);
            left = surface.roundPixel(center - barWidth / 2) + halfLineWidth;
            top = surface.roundPixel(yHi * yy + dy + lineWidth);
            right = surface.roundPixel(center + barWidth / 2) - halfLineWidth;
            bottom = surface.roundPixel(yLow * yy + dy + lineWidth);
            me.drawBar(ctx, surface, dataClipRect, left, top - halfLineWidth, right, bottom - halfLineWidth, i);
            // We want 0 values to be passed to the renderer
            if (isDrawLabels && dataText[i] != null) {
                me.drawLabel(dataText[i], center, bottom, top, i);
            }
            me.putMarker('markers', {
                translationX: surfaceMatrix.x(center, top),
                translationY: surfaceMatrix.y(center, top)
            }, i, true);
        }
    }
});

/**
 * @class Ext.chart.series.Bar
 * @extends Ext.chart.series.StackedCartesian
 * 
 * Creates a Bar or Column Chart (depending on the value of the
 * {@link Ext.chart.CartesianChart#flipXY flipXY} config).
 *
 * Note: 'bar' series is meant to be used with the
 * {@link Ext.chart.axis.Category 'category'} axis as its x-axis.
 * 
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        store: {
 *            fields: ['name', 'value'],
 *            data: [{
 *                name: 'metric one',
 *                value: 10
 *            }, {
 *                name: 'metric two',
 *                value: 7
 *            }, {
 *                name: 'metric three',
 *                value: 5
 *            }, {
 *                name: 'metric four',
 *                value: 2
 *            }, {
 *                name: 'metric five',
 *                value: 27
 *            }]
 *        },
 *        axes: [{
 *            type: 'numeric',
 *            position: 'left',
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            },
 *            fields: 'value'
 *        }, {
 *            type: 'category',
 *            position: 'bottom',
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            },
 *            fields: 'name'
 *        }],
 *        series: {
 *            type: 'bar',
 *            subStyle: {
 *                fill: ['#388FAD'],
 *                stroke: '#1F6D91'
 *            },
 *            xField: 'name',
 *            yField: 'value'
 *        }
 *     });
 */
Ext.define('Ext.chart.series.Bar', {
    extend: 'Ext.chart.series.StackedCartesian',
    alias: 'series.bar',
    type: 'bar',
    seriesType: 'barSeries',
    isBar: true,
    requires: [
        'Ext.chart.series.sprite.Bar',
        'Ext.draw.sprite.Rect'
    ],
    config: {
        /**
         * @private
         * @cfg {Object} itemInstancing Sprite template used for series.
         */
        itemInstancing: {
            type: 'rect',
            animation: {
                customDurations: {
                    x: 0,
                    y: 0,
                    width: 0,
                    height: 0,
                    radius: 0
                }
            }
        }
    },
    getItemForPoint: function(x, y) {
        var chart, padding, isRtl;
        if (this.getSprites().length) {
            chart = this.getChart();
            padding = chart.getInnerPadding();
            isRtl = chart.getInherited().rtl;
            // Convert the coordinates because the "items" sprites that draw
            // the bars ignore the chart's InnerPadding.
            arguments[0] = x + (isRtl ? padding.right : -padding.left);
            arguments[1] = y + padding.bottom;
            return this.callParent(arguments);
        }
    },
    updateXAxis: function(xAxis) {
        //<debug>
        if (!this.is3D && !xAxis.isCategory) {
            Ext.raise("'bar' series should be used with a 'category' axis. " + "Please refer to the bar series docs.");
        }
        //</debug>
        xAxis.setExpandRangeBy(0.5);
        this.callParent(arguments);
    },
    updateHidden: function(hidden) {
        this.callParent(arguments);
        this.updateStacked();
    },
    updateStacked: function(stacked) {
        var me = this,
            attributes = {},
            sprites = me.getSprites(),
            spriteCount = sprites.length,
            visibleSprites = [],
            visibleSpriteCount, i;
        for (i = 0; i < spriteCount; i++) {
            if (!sprites[i].attr.hidden) {
                visibleSprites.push(sprites[i]);
            }
        }
        visibleSpriteCount = visibleSprites.length;
        if (me.getStacked()) {
            attributes.groupCount = 1;
            attributes.groupOffset = 0;
            for (i = 0; i < visibleSpriteCount; i++) {
                visibleSprites[i].setAttributes(attributes);
            }
        } else {
            attributes.groupCount = visibleSpriteCount;
            for (i = 0; i < visibleSpriteCount; i++) {
                attributes.groupOffset = i;
                visibleSprites[i].setAttributes(attributes);
            }
        }
        me.callParent(arguments);
    }
});

/**
 * @class Ext.chart.series.sprite.Bar3D
 * @extends Ext.chart.series.sprite.Bar
 *
 * Draws a sprite used in {@link Ext.chart.series.Bar3D} series.
 */
Ext.define('Ext.chart.series.sprite.Bar3D', {
    extend: 'Ext.chart.series.sprite.Bar',
    alias: 'sprite.bar3dSeries',
    requires: [
        'Ext.draw.gradient.Linear'
    ],
    inheritableStatics: {
        def: {
            processors: {
                depthWidthRatio: 'number',
                /**
                 * @cfg {Number} [saturationFactor=1]
                 * The factor applied to the saturation of the bars.
                 */
                saturationFactor: 'number',
                /**
                 * @cfg {Number} [brightnessFactor=1]
                 * The factor applied to the brightness of the bars.
                 */
                brightnessFactor: 'number',
                /**
                 * @cfg {Number} [colorSpread=1]
                 * An attribute used to control how flat the bar gradient looks.
                 * A value of 0 essentially means no gradient (flat color).
                 */
                colorSpread: 'number'
            },
            defaults: {
                depthWidthRatio: 1 / 3,
                saturationFactor: 1,
                brightnessFactor: 1,
                colorSpread: 1,
                transformFillStroke: true
            },
            triggers: {
                groupCount: 'panzoom'
            },
            updaters: {
                panzoom: function(attr) {
                    var me = this,
                        dx = attr.visibleMaxX - attr.visibleMinX,
                        dy = attr.visibleMaxY - attr.visibleMinY,
                        innerWidth = attr.flipXY ? attr.innerHeight : attr.innerWidth,
                        innerHeight = !attr.flipXY ? attr.innerHeight : attr.innerWidth,
                        surface = me.getSurface(),
                        isRtl = surface ? surface.getInherited().rtl : false;
                    if (isRtl && !attr.flipXY) {
                        attr.translationX = innerWidth + attr.visibleMinX * innerWidth / dx;
                    } else {
                        attr.translationX = -attr.visibleMinX * innerWidth / dx;
                    }
                    attr.translationY = -attr.visibleMinY * (innerHeight - me.depth) / dy;
                    attr.scalingX = (isRtl && !attr.flipXY ? -1 : 1) * innerWidth / dx;
                    attr.scalingY = (innerHeight - me.depth) / dy;
                    attr.scalingCenterX = 0;
                    attr.scalingCenterY = 0;
                    me.applyTransformations(true);
                }
            }
        }
    },
    config: {
        showStroke: false
    },
    depth: 0,
    drawBar: function(ctx, surface, clip, left, top, right, bottom, index) {
        var me = this,
            attr = me.attr,
            itemCfg = {},
            renderer = attr.renderer,
            changes, depth, series, params;
        itemCfg.x = (left + right) * 0.5;
        itemCfg.y = top;
        itemCfg.width = (right - left) * 0.75;
        itemCfg.height = bottom - top;
        itemCfg.depth = depth = itemCfg.width * attr.depthWidthRatio;
        itemCfg.orientation = attr.flipXY ? 'horizontal' : 'vertical';
        itemCfg.saturationFactor = attr.saturationFactor;
        itemCfg.brightnessFactor = attr.brightnessFactor;
        itemCfg.colorSpread = attr.colorSpread;
        if (depth !== me.depth) {
            me.depth = depth;
            series = me.getSeries();
            series.fireEvent('depthchange', series, depth);
        }
        if (renderer) {
            params = [
                me,
                itemCfg,
                {
                    store: me.getStore()
                },
                index
            ];
            changes = Ext.callback(renderer, null, params, 0, me.getSeries());
            Ext.apply(itemCfg, changes);
        }
        me.putMarker('items', itemCfg, index, !renderer);
    }
});

/**
 * @class Ext.chart.sprite.Bar3D
 * @extends Ext.draw.sprite.Sprite
 *
 * A sprite that represents a 3D bar or column.
 * Used as an item template by the {@link Ext.chart.series.sprite.Bar3D} marker holder.
 *
 */
Ext.define('Ext.chart.sprite.Bar3D', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.bar3d',
    type: 'bar3d',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [x=0]
                 * The position of the sprite on the x-axis.
                 * Corresponds to the center of the front face of the box.
                 */
                x: 'number',
                /**
                 * @cfg {Number} [y=0]
                 * The position of the sprite on the y-axis.
                 * Corresponds to the top of the front face of the box.
                 */
                y: 'number',
                /**
                 * @cfg {Number} [width=8] The width of the box.
                 */
                width: 'number',
                /**
                 * @cfg {Number} [height=8] The height of the box.
                 */
                height: 'number',
                /**
                 * @cfg {Number} [depth=8] The depth of the box.
                 */
                depth: 'number',
                /**
                 * @cfg {String} [orientation='vertical'] The orientation of the box.
                 */
                orientation: 'enums(vertical,horizontal)',
                /**
                 * @cfg {Boolean} [showStroke=false]
                 * Whether to render the stroke or not.
                 */
                showStroke: 'bool',
                /**
                 * @cfg {Number} [saturationFactor=1]
                 * The factor applied to the saturation of the box.
                 */
                saturationFactor: 'number',
                /**
                 * @cfg {Number} [brightnessFactor=1]
                 * The factor applied to the brightness of the box.
                 */
                brightnessFactor: 'number',
                /**
                 * @cfg {Number} [colorSpread=1]
                 * An attribute used to control how flat the bar gradient looks.
                 * A value of 0 essentially means no gradient (flat color).
                 */
                colorSpread: 'number'
            },
            triggers: {
                x: 'bbox',
                y: 'bbox',
                width: 'bbox',
                height: 'bbox',
                depth: 'bbox',
                orientation: 'bbox'
            },
            defaults: {
                x: 0,
                y: 0,
                width: 8,
                height: 8,
                depth: 8,
                orientation: 'vertical',
                showStroke: false,
                saturationFactor: 1,
                brightnessFactor: 1,
                colorSpread: 1,
                lineJoin: 'bevel'
            }
        }
    },
    constructor: function(config) {
        this.callParent([
            config
        ]);
        this.topGradient = new Ext.draw.gradient.Linear({});
        this.rightGradient = new Ext.draw.gradient.Linear({});
        this.frontGradient = new Ext.draw.gradient.Linear({});
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr,
            x = attr.x,
            y = attr.y,
            width = attr.width,
            height = attr.height,
            depth = attr.depth;
        plain.x = x - width * 0.5;
        plain.width = width + depth;
        if (height > 0) {
            plain.y = y;
            plain.height = height + depth;
        } else {
            plain.y = y + depth;
            plain.height = height - depth;
        }
    },
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            center = attr.x,
            top = attr.y,
            bottom = top + attr.height,
            isNegative = top < bottom,
            halfWidth = attr.width * 0.5,
            depth = attr.depth,
            isHorizontal = attr.orientation === 'horizontal',
            isTransparent = attr.globalAlpha < 1,
            fillStyle = attr.fillStyle,
            color = Ext.util.Color.create(fillStyle.isGradient ? fillStyle.getStops()[0].color : fillStyle),
            saturationFactor = attr.saturationFactor,
            brightnessFactor = attr.brightnessFactor,
            colorSpread = attr.colorSpread,
            hsv = color.getHSV(),
            bbox = {},
            roundX, roundY, temp;
        if (!attr.showStroke) {
            ctx.strokeStyle = Ext.util.Color.RGBA_NONE;
        }
        if (isNegative) {
            temp = top;
            top = bottom;
            bottom = temp;
        }
        // Refresh gradients based on sprite's fillStyle and other attributes.
        me.topGradient.setDegrees(isHorizontal ? 0 : 80);
        me.topGradient.setStops([
            {
                offset: 0,
                color: Ext.util.Color.fromHSV(hsv[0], Ext.Number.constrain(hsv[1] * saturationFactor, 0, 1), Ext.Number.constrain((0.5 + colorSpread * 0.1) * brightnessFactor, 0, 1))
            },
            {
                offset: 1,
                color: Ext.util.Color.fromHSV(hsv[0], Ext.Number.constrain(hsv[1] * saturationFactor, 0, 1), Ext.Number.constrain((0.5 - colorSpread * 0.11) * brightnessFactor, 0, 1))
            }
        ]);
        me.rightGradient.setDegrees(isHorizontal ? 45 : 90);
        me.rightGradient.setStops([
            {
                offset: 0,
                color: Ext.util.Color.fromHSV(hsv[0], Ext.Number.constrain(hsv[1] * saturationFactor, 0, 1), Ext.Number.constrain((0.5 - colorSpread * 0.14) * brightnessFactor, 0, 1))
            },
            {
                offset: 1,
                color: Ext.util.Color.fromHSV(hsv[0], Ext.Number.constrain(hsv[1] * (1 + colorSpread * 0.4) * saturationFactor, 0, 1), Ext.Number.constrain((0.5 - colorSpread * 0.32) * brightnessFactor, 0, 1))
            }
        ]);
        if (isHorizontal) {
            // 0° angle looks like 90° angle because the chart is flipped
            me.frontGradient.setDegrees(0);
        } else {
            me.frontGradient.setRadians(Math.atan2(top - bottom, halfWidth * 2));
        }
        me.frontGradient.setStops([
            {
                offset: 0,
                color: Ext.util.Color.fromHSV(hsv[0], Ext.Number.constrain(hsv[1] * (1 - colorSpread * 0.1) * saturationFactor, 0, 1), Ext.Number.constrain((0.5 + colorSpread * 0.1) * brightnessFactor, 0, 1))
            },
            {
                offset: 1,
                color: Ext.util.Color.fromHSV(hsv[0], Ext.Number.constrain(hsv[1] * (1 + colorSpread * 0.1) * saturationFactor, 0, 1), Ext.Number.constrain((0.5 - colorSpread * 0.23) * brightnessFactor, 0, 1))
            }
        ]);
        if (isTransparent || isNegative) {
            // Bottom side.
            ctx.beginPath();
            ctx.moveTo(center - halfWidth, bottom);
            ctx.lineTo(center - halfWidth + depth, bottom + depth);
            ctx.lineTo(center + halfWidth + depth, bottom + depth);
            ctx.lineTo(center + halfWidth, bottom);
            ctx.closePath();
            bbox.x = center - halfWidth;
            bbox.y = top;
            bbox.width = halfWidth + depth;
            bbox.height = depth;
            // eslint-disable-next-line max-len
            ctx.fillStyle = (isHorizontal ? me.rightGradient : me.topGradient).generateGradient(ctx, bbox);
            ctx.fillStroke(attr);
        }
        if (isTransparent) {
            // Left side.
            ctx.beginPath();
            ctx.moveTo(center - halfWidth, top);
            ctx.lineTo(center - halfWidth + depth, top + depth);
            ctx.lineTo(center - halfWidth + depth, bottom + depth);
            ctx.lineTo(center - halfWidth, bottom);
            ctx.closePath();
            bbox.x = center + halfWidth;
            bbox.y = bottom;
            bbox.width = depth;
            bbox.height = top + depth - bottom;
            // eslint-disable-next-line max-len
            ctx.fillStyle = (isHorizontal ? me.topGradient : me.rightGradient).generateGradient(ctx, bbox);
            ctx.fillStroke(attr);
        }
        // Top side.
        roundY = surface.roundPixel(top);
        ctx.beginPath();
        ctx.moveTo(center - halfWidth, roundY);
        ctx.lineTo(center - halfWidth + depth, top + depth);
        ctx.lineTo(center + halfWidth + depth, top + depth);
        ctx.lineTo(center + halfWidth, roundY);
        ctx.closePath();
        bbox.x = center - halfWidth;
        bbox.y = top;
        bbox.width = halfWidth + depth;
        bbox.height = depth;
        // eslint-disable-next-line max-len
        ctx.fillStyle = (isHorizontal ? me.rightGradient : me.topGradient).generateGradient(ctx, bbox);
        ctx.fillStroke(attr);
        // Right side.
        roundX = surface.roundPixel(center + halfWidth);
        ctx.beginPath();
        ctx.moveTo(roundX, surface.roundPixel(top));
        ctx.lineTo(center + halfWidth + depth, top + depth);
        ctx.lineTo(center + halfWidth + depth, bottom + depth);
        ctx.lineTo(roundX, bottom);
        ctx.closePath();
        bbox.x = center + halfWidth;
        bbox.y = bottom;
        bbox.width = depth;
        bbox.height = top + depth - bottom;
        // eslint-disable-next-line max-len
        ctx.fillStyle = (isHorizontal ? me.topGradient : me.rightGradient).generateGradient(ctx, bbox);
        ctx.fillStroke(attr);
        // Front side.
        roundX = surface.roundPixel(center + halfWidth);
        roundY = surface.roundPixel(top);
        ctx.beginPath();
        ctx.moveTo(center - halfWidth, bottom);
        ctx.lineTo(center - halfWidth, roundY);
        ctx.lineTo(roundX, roundY);
        ctx.lineTo(roundX, bottom);
        ctx.closePath();
        bbox.x = center - halfWidth;
        bbox.y = bottom;
        bbox.width = halfWidth * 2;
        bbox.height = top - bottom;
        ctx.fillStyle = me.frontGradient.generateGradient(ctx, bbox);
        ctx.fillStroke(attr);
    }
});

/**
 * @class Ext.chart.series.Bar3D
 * @extends Ext.chart.series.Bar
 *
 * Creates a 3D Bar or 3D Column Chart (depending on the value of the
 * {@link Ext.chart.CartesianChart#flipXY flipXY} config).
 *
 * Note: 'bar3d' series is meant to be used with the
 * {@link Ext.chart.axis.Category 'category3d'} axis as its x-axis.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: Ext.getBody(),
 *        width: 600,
 *        height: 400,
 *        innerPadding: '0 10 0 10',
 *        store: {
 *            fields: ['name', 'apples', 'oranges'],
 *            data: [{
 *                name: 'Eric',
 *                apples: 10,
 *                oranges: 3
 *            }, {
 *                name: 'Mary',
 *                apples: 7,
 *                oranges: 2
 *            }, {
 *                name: 'John',
 *                apples: 5,
 *                oranges: 2
 *            }, {
 *                name: 'Bob',
 *                apples: 2,
 *                oranges: 3
 *            }, {
 *                name: 'Joe',
 *                apples: 19,
 *                oranges: 1
 *            }, {
 *                name: 'Macy',
 *                apples: 13,
 *                oranges: 4
 *            }]
 *        },
 *        axes: [{
 *            type: 'numeric3d',
 *            position: 'left',
 *            fields: ['apples', 'oranges'],
 *            title: {
 *                text: 'Inventory',
 *                fontSize: 15
 *            },
 *            grid: {
 *                odd: {
 *                    fillStyle: 'rgba(255, 255, 255, 0.06)'
 *                },
 *                even: {
 *                    fillStyle: 'rgba(0, 0, 0, 0.03)'
 *                }
 *            }
 *        }, {
 *            type: 'category3d',
 *            position: 'bottom',
 *            title: {
 *                text: 'People',
 *                fontSize: 15
 *            },
 *            fields: 'name'
 *        }],
 *        series: {
 *            type: 'bar3d',
 *            xField: 'name',
 *            yField: ['apples', 'oranges']
 *        }
 *     });
 */
Ext.define('Ext.chart.series.Bar3D', {
    extend: 'Ext.chart.series.Bar',
    requires: [
        'Ext.chart.series.sprite.Bar3D',
        'Ext.chart.sprite.Bar3D'
    ],
    alias: 'series.bar3d',
    type: 'bar3d',
    seriesType: 'bar3dSeries',
    is3D: true,
    config: {
        itemInstancing: {
            type: 'bar3d',
            animation: {
                customDurations: {
                    x: 0,
                    y: 0,
                    width: 0,
                    height: 0,
                    depth: 0
                }
            }
        },
        highlightCfg: {
            opacity: 0.8
        }
    },
    /**
     * For 3D series, it's quite the opposite. It would be extremely odd,
     * if top segments were rendered as if they were under the bottom ones.
     */
    reversedSpriteZOrder: false,
    updateXAxis: function(xAxis, oldXAxis) {
        //<debug>
        if (xAxis.type !== 'category3d') {
            Ext.raise("'bar3d' series should be used with a 'category3d' axis." + " Please refer to the 'bar3d' series docs.");
        }
        //</debug>
        this.callParent([
            xAxis,
            oldXAxis
        ]);
    },
    getDepth: function() {
        var sprite = this.getSprites()[0];
        return sprite ? (sprite.depth || 0) : 0;
    }
});

/**
 * BoxPlot series sprite that manages {@link Ext.chart.sprite.BoxPlot} instances.
 */
Ext.define('Ext.chart.series.sprite.BoxPlot', {
    alias: 'sprite.boxplotSeries',
    extend: 'Ext.chart.series.sprite.Cartesian',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number[]} [dataLow=null] Array of coordinated minimum values.
                 */
                dataLow: 'data',
                /**
                 * @cfg {Number[]} [dataQ1=null] Array of coordinated 1-st quartile values.
                 */
                dataQ1: 'data',
                /**
                 * @cfg {Number[]} [dataQ3=null] Array of coordinated 3-rd quartile values.
                 */
                dataQ3: 'data',
                /**
                 * @cfg {Number[]} [dataHigh=null] Array of coordinated maximum values.
                 */
                dataHigh: 'data',
                /**
                 * @cfg {Number} [minBoxWidth=2] The minimum box width.
                 */
                minBoxWidth: 'number',
                /**
                 * @cfg {Number} [maxBoxWidth=20] The maximum box width.
                 */
                maxBoxWidth: 'number',
                /**
                 * @cfg {Number} [minGapWidth=5] The minimum gap between boxes.
                 */
                minGapWidth: 'number'
            },
            aliases: {
                /**
                 * The `dataMedian` attribute can be used to set the value of
                 * the `dataY` attribute. E.g.:
                 *
                 *     sprite.setAttributes({
                 *         dataMedian: [...]
                 *     });
                 *
                 * To fetch the value of the attribute one has to use
                 *
                 *     sprite.attr.dataY // array of coordinated median values
                 *
                 * and not
                 *
                 *     sprite.attr.dataMedian // WRONG!
                 *
                 * `dataY` attribute is defined by the `Ext.chart.series.sprite.Series`.
                 *
                 * @cfg {Number[]} [dataMedian=null] Array of coordinated median values.
                 */
                dataMedian: 'dataY'
            },
            defaults: {
                minBoxWidth: 2,
                maxBoxWidth: 40,
                minGapWidth: 5
            }
        }
    },
    renderClipped: function(surface, ctx, dataClipRect) {
        if (this.cleanRedraw) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            attr = me.attr,
            series = me.getSeries(),
            renderer = attr.renderer,
            rendererData = {
                store: me.getStore()
            },
            itemCfg = {},
            dataX = attr.dataX,
            dataLow = attr.dataLow,
            dataQ1 = attr.dataQ1,
            dataMedian = attr.dataY,
            dataQ3 = attr.dataQ3,
            dataHigh = attr.dataHigh,
            min = Math.min(dataClipRect[0], dataClipRect[2]),
            max = Math.max(dataClipRect[0], dataClipRect[2]),
            start = Math.max(0, Math.floor(min)),
            end = Math.min(dataX.length - 1, Math.ceil(max)),
            // surfaceMatrix = me.surfaceMatrix,
            matrix = attr.matrix,
            xx = matrix.elements[0],
            // horizontal scaling can be < 0, if RTL
            yy = matrix.elements[3],
            dx = matrix.elements[4],
            dy = matrix.elements[5],
            // `xx` essentially represents the distance between data points in surface coordinates.
            maxBoxWidth = Math.abs(xx) - attr.minGapWidth,
            minBoxWidth = Math.min(maxBoxWidth, attr.maxBoxWidth),
            boxWidth = Math.round(Math.max(attr.minBoxWidth, minBoxWidth)),
            x, low, q1, median, q3, high, rendererParams, changes, i;
        if (renderer) {
            rendererParams = [
                me,
                itemCfg,
                rendererData
            ];
        }
        for (i = start; i <= end; i++) {
            x = dataX[i] * xx + dx;
            low = dataLow[i] * yy + dy;
            q1 = dataQ1[i] * yy + dy;
            median = dataMedian[i] * yy + dy;
            q3 = dataQ3[i] * yy + dy;
            high = dataHigh[i] * yy + dy;
            // --- Draw Box ---
            // Reuse 'itemCfg' object and 'rendererParams' arrays for better performance.
            itemCfg.x = x;
            itemCfg.low = low;
            itemCfg.q1 = q1;
            itemCfg.median = median;
            itemCfg.q3 = q3;
            itemCfg.high = high;
            itemCfg.boxWidth = boxWidth;
            if (renderer) {
                rendererParams[3] = i;
                changes = Ext.callback(renderer, null, rendererParams, 0, series);
                Ext.apply(itemCfg, changes);
            }
            me.putMarker('items', itemCfg, i, !renderer);
        }
    }
});

/**
 * A sprite that represents an individual box with whiskers.
 * This sprite is meant to be managed by the {@link Ext.chart.series.sprite.BoxPlot}
 * {@link Ext.chart.MarkerHolder MarkerHolder}, but can also be used independently:
 *
 *     @example
 *     new Ext.draw.Container({
 *         width: 100,
 *         height: 100,
 *         renderTo: Ext.getBody(),
 *         sprites: [{
 *             type: 'boxplot',
 *             translationX: 50,
 *             translationY: 50
 *         }]
 *     });
 *
 * IMPORTANT: the attributes that represent y-coordinates are in screen coordinates,
 * just like with any other sprite. For this particular sprite this means that, if 'low'
 * and 'high' attributes are 10 and 90, then the minimium whisker is rendered at the top
 * of a draw container {@link Ext.draw.Surface surface} at y = 10, and the maximum whisker
 * is rendered at the bottom at y = 90. But because the series surface is flipped vertically
 * in cartesian charts, this means that there minimum is rendered at the bottom and maximum
 * at the top, just as one would expect.
 */
Ext.define('Ext.chart.sprite.BoxPlot', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.boxplot',
    type: 'boxplot',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [x=0] The coordinate of the horizontal center of a boxplot.
                 */
                x: 'number',
                /**
                 * @cfg {Number} [low=-20] The y-coordinate of the whisker that represents
                 * the minimum.
                 */
                low: 'number',
                /**
                 * @cfg {Number} [q1=-10] The y-coordinate of the box edge that represents
                 * the 1-st quartile.
                 */
                q1: 'number',
                /**
                 * @cfg {Number} [median=0] The y-coordinate of the line that represents the median.
                 */
                median: 'number',
                /**
                 * @cfg {Number} [q3=10] The y-coordinate of the box edge that represents
                 * the 3-rd quartile.
                 */
                q3: 'number',
                /**
                 * @cfg {Number} [high=20] The y-coordinate of the whisker that represents
                 * the maximum.
                 */
                high: 'number',
                /**
                 * @cfg {Number} [boxWidth=12] The width of the box in pixels.
                 */
                boxWidth: 'number',
                /**
                 * @cfg {Number} [whiskerWidth=0.5] The length of the lines at the ends
                 * of the whiskers, as a ratio of `boxWidth`.
                 */
                whiskerWidth: 'number',
                /**
                 * @cfg {Boolean} [crisp=true] Whether to snap the rendered lines to the pixel grid
                 * of not. Generally, it's best to have this set to `true` (which is the default)
                 * for pixel perfect results (especially on non-HiDPI displays), but for boxplots
                 * with small `boxWidth` visible artifacts caused by pixel grid snapping may become
                 * noticeable, and setting this to `false` can be a remedy at the expense
                 * of clarity.
                 */
                crisp: 'bool'
            },
            triggers: {
                x: 'bbox',
                low: 'bbox',
                high: 'bbox',
                boxWidth: 'bbox',
                whiskerWidth: 'bbox',
                crisp: 'bbox'
            },
            defaults: {
                x: 0,
                low: -20,
                q1: -10,
                median: 0,
                q3: 10,
                high: 20,
                boxWidth: 12,
                whiskerWidth: 0.5,
                crisp: true,
                fillStyle: '#ccc',
                strokeStyle: '#000'
            }
        }
    },
    updatePlainBBox: function(plain) {
        var me = this,
            attr = me.attr,
            halfLineWidth = attr.lineWidth / 2,
            x = attr.x - attr.boxWidth / 2 - halfLineWidth,
            y = attr.high - halfLineWidth,
            width = attr.boxWidth + attr.lineWidth,
            height = attr.low - attr.high + attr.lineWidth;
        plain.x = x;
        plain.y = y;
        plain.width = width;
        plain.height = height;
    },
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr;
        attr.matrix.toContext(ctx);
        // enable sprite transformations
        if (attr.crisp) {
            me.crispRender(surface, ctx);
        } else {
            me.softRender(surface, ctx);
        }
        //<debug>
        // eslint-disable-next-line vars-on-top, one-var
        var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug;
        if (debug) {
            // This assumes no part of the sprite is rendered after this call.
            // If it is, we need to re-apply transformations.
            // But the bounding box should always be rendered as is, untransformed.
            this.attr.inverseMatrix.toContext(ctx);
            if (debug.bbox) {
                this.renderBBox(surface, ctx);
            }
        }
    },
    //</debug>
    /**
     * @private
     * Renders a single box with whiskers.
     * Changes to this method have to be reflected in the {@link #crispRender} as well.
     * @param surface
     * @param ctx
     */
    softRender: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            x = attr.x,
            low = attr.low,
            q1 = attr.q1,
            median = attr.median,
            q3 = attr.q3,
            high = attr.high,
            halfBoxWidth = attr.boxWidth / 2,
            halfWhiskerWidth = attr.boxWidth * attr.whiskerWidth / 2,
            dash = ctx.getLineDash();
        ctx.setLineDash([]);
        // Only stem can be dashed.
        // Box.
        ctx.beginPath();
        ctx.moveTo(x - halfBoxWidth, q3);
        ctx.lineTo(x + halfBoxWidth, q3);
        ctx.lineTo(x + halfBoxWidth, q1);
        ctx.lineTo(x - halfBoxWidth, q1);
        ctx.closePath();
        ctx.fillStroke(attr, true);
        // Stem.
        ctx.setLineDash(dash);
        ctx.beginPath();
        ctx.moveTo(x, q3);
        ctx.lineTo(x, high);
        ctx.moveTo(x, q1);
        ctx.lineTo(x, low);
        ctx.stroke();
        ctx.setLineDash([]);
        // Whiskers.
        ctx.beginPath();
        ctx.moveTo(x - halfWhiskerWidth, low);
        ctx.lineTo(x + halfWhiskerWidth, low);
        ctx.moveTo(x - halfBoxWidth, median);
        ctx.lineTo(x + halfBoxWidth, median);
        ctx.moveTo(x - halfWhiskerWidth, high);
        ctx.lineTo(x + halfWhiskerWidth, high);
        ctx.stroke();
    },
    alignLine: function(x, lineWidth) {
        lineWidth = lineWidth || this.attr.lineWidth;
        x = Math.round(x);
        if (lineWidth % 2 === 1) {
            x -= 0.5;
        }
        return x;
    },
    /**
     * @private
     * Renders a pixel-perfect single box with whiskers by aligning to the pixel grid.
     * Changes to this method have to be reflected in the {@link #softRender} as well.
     *
     * Note: crisp image is only guaranteed when `attr.lineWidth` is a whole number.
     * @param surface
     * @param ctx
     */
    crispRender: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            x = attr.x,
            low = me.alignLine(attr.low),
            q1 = me.alignLine(attr.q1),
            median = me.alignLine(attr.median),
            q3 = me.alignLine(attr.q3),
            high = me.alignLine(attr.high),
            halfBoxWidth = attr.boxWidth / 2,
            halfWhiskerWidth = attr.boxWidth * attr.whiskerWidth / 2,
            stemX = me.alignLine(x),
            boxLeft = me.alignLine(x - halfBoxWidth),
            boxRight = me.alignLine(x + halfBoxWidth),
            whiskerLeft = stemX + Math.round(-halfWhiskerWidth),
            whiskerRight = stemX + Math.round(halfWhiskerWidth),
            dash = ctx.getLineDash();
        ctx.setLineDash([]);
        // Only stem can be dashed.
        // Box.
        ctx.beginPath();
        ctx.moveTo(boxLeft, q3);
        ctx.lineTo(boxRight, q3);
        ctx.lineTo(boxRight, q1);
        ctx.lineTo(boxLeft, q1);
        ctx.closePath();
        ctx.fillStroke(attr, true);
        // Stem.
        ctx.setLineDash(dash);
        ctx.beginPath();
        ctx.moveTo(stemX, q3);
        ctx.lineTo(stemX, high);
        ctx.moveTo(stemX, q1);
        ctx.lineTo(stemX, low);
        ctx.stroke();
        ctx.setLineDash([]);
        // Whiskers.
        ctx.beginPath();
        ctx.moveTo(whiskerLeft, low);
        ctx.lineTo(whiskerRight, low);
        ctx.moveTo(boxLeft, median);
        ctx.lineTo(boxRight, median);
        ctx.moveTo(whiskerLeft, high);
        ctx.lineTo(whiskerRight, high);
        ctx.stroke();
    }
});

/**
 * A box plot chart is a useful tool for visializing data distribution within datasets.
 * For example, salary ranges for a set of occupations, or life expectancy for a set
 * of countries. A single box with whiskers displays the following values for a dataset:
 *
 * * minimum
 * * lower quartile (Q1)
 * * median (Q2)
 * * higher quartile (Q3)
 * * maximum
 *
 * For example:
 *
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian',
 *        width: 400,
 *        height: 400,
 *        renderTo: Ext.getBody(),
 *        insetPadding: '20 20 10 10',
 *        store: {
 *            data: [{
 *                category: 'Engineer IV',
 *                low: 110, q1: 130, median: 175, q3: 200, high: 225
 *            }, {
 *                category: 'Market',
 *                low: 75, q1: 125, median: 210, q3: 230, high: 255
 *            }]
 *        },
 *        axes: [
 *            {
 *                type: 'numeric',
 *                position: 'left',
 *                renderer: function (axis, text) {
 *                    return '$' + text + ' K'
 *                }
 *            },
 *            {
 *                type: 'category',
 *                position: 'bottom'
 *            }
 *        ],
 *        series: {
 *            type: 'boxplot',
 *            xField: 'category',
 *            style: {
 *                maxBoxWidth: 50,
 *                lineWidth: 2
 *            }
 *        }
 *     });
 *
 */
Ext.define('Ext.chart.series.BoxPlot', {
    extend: 'Ext.chart.series.Cartesian',
    alias: 'series.boxplot',
    type: 'boxplot',
    seriesType: 'boxplotSeries',
    isBoxPlot: true,
    requires: [
        'Ext.chart.series.sprite.BoxPlot',
        'Ext.chart.sprite.BoxPlot'
    ],
    config: {
        itemInstancing: {
            type: 'boxplot',
            animation: {
                // Setting the duration of these attributes to zero because
                // the 'data' attributes of the series sprite (MarkerHolder)
                // will be animated instead, and then changes applied to
                // the attributes of 'boxplot' instances instantly.
                customDurations: {
                    x: 0,
                    low: 0,
                    q1: 0,
                    median: 0,
                    q3: 0,
                    high: 0
                }
            }
        },
        /**
         * @cfg {String} [lowField='low']
         * The name of the store record field that represents the smallest value of a dataset.
         */
        lowField: 'low',
        /**
         * @cfg {String} [q1Field='q1']
         * The name of the store record field that represents the lower (1-st) quartile
         * value of a dataset.
         */
        q1Field: 'q1',
        /**
         * @cfg {String} [medianField='median']
         * The name of the store record field that represents the median of a dataset.
         */
        medianField: 'median',
        /**
         * @cfg {String} [q3Field='q3']
         * The name of the store record field that represents the upper (3-rd) quartile
         * value of a dataset.
         */
        q3Field: 'q3',
        /**
         * @cfg {String} [highField='high']
         * The name of the store record field that represents the largest value of a dataset.
         */
        highField: 'high'
    },
    fieldCategoryY: [
        'Low',
        'Q1',
        'Median',
        'Q3',
        'High'
    ],
    updateXAxis: function(xAxis) {
        xAxis.setExpandRangeBy(0.5);
        this.callParent(arguments);
    }
});

/**
 * Limited cache is a size limited cache container that stores limited number of objects.
 * 
 * When {@link #get} is called, the container will try to find the object in the list.
 * If failed it will call the {@link #feeder} to create that object. If there are too many
 * objects in the container, the old ones are removed.
 * 
 * __Note:__ This is not using a Least Recently Used policy due to simplicity and performance
 * consideration.
 * @private
 */
Ext.define('Ext.draw.LimitedCache', {
    config: {
        /**
         * @cfg {Number}
         * The amount limit of the cache.
         */
        limit: 40,
        /**
         * @cfg {Function}
         * Function that generates the object when look-up failed.
         * @return {Number}
         */
        feeder: function() {
            return 0;
        },
        /**
         * @cfg {Object}
         * The scope for {@link #feeder}
         */
        scope: null
    },
    cache: null,
    constructor: function(config) {
        this.cache = {};
        this.cache.list = [];
        this.cache.tail = 0;
        this.initConfig(config);
    },
    /**
     * Get a cached object.
     * @param {String} id
     * @return {Object}
     */
    get: function(id) {
        // TODO: Implement cache hit optimization
        var cache = this.cache,
            limit = this.getLimit(),
            feeder = this.getFeeder(),
            scope = this.getScope() || this;
        if (cache[id]) {
            return cache[id].value;
        }
        if (cache.list[cache.tail]) {
            delete cache[cache.list[cache.tail].cacheId];
        }
        cache[id] = cache.list[cache.tail] = {
            value: feeder.apply(scope, Array.prototype.slice.call(arguments, 1)),
            cacheId: id
        };
        cache.tail++;
        if (cache.tail === limit) {
            cache.tail = 0;
        }
        return cache[id].value;
    },
    /**
     * Clear all the objects.
     */
    clear: function() {
        this.cache = {};
        this.cache.list = [];
        this.cache.tail = 0;
    }
});

/**
 * This class we summarize the data and returns it when required.
 */
Ext.define("Ext.draw.SegmentTree", {
    config: {
        strategy: "double"
    },
    /**
     * @private
     * @param {Object} result
     * @param {Number} last
     * @param {Number} dataX
     * @param {Number} dataOpen
     * @param {Number} dataHigh
     * @param {Number} dataLow
     * @param {Number} dataClose
     */
    time: function(result, last, dataX, dataOpen, dataHigh, dataLow, dataClose) {
        var start = 0,
            lastOffset, lastOffsetEnd,
            minimum = new Date(dataX[result.startIdx[0]]),
            maximum = new Date(dataX[result.endIdx[last - 1]]),
            extDate = Ext.Date,
            units = [
                [
                    extDate.MILLI,
                    1,
                    'ms1',
                    null
                ],
                [
                    extDate.MILLI,
                    2,
                    'ms2',
                    'ms1'
                ],
                [
                    extDate.MILLI,
                    5,
                    'ms5',
                    'ms1'
                ],
                [
                    extDate.MILLI,
                    10,
                    'ms10',
                    'ms5'
                ],
                [
                    extDate.MILLI,
                    50,
                    'ms50',
                    'ms10'
                ],
                [
                    extDate.MILLI,
                    100,
                    'ms100',
                    'ms50'
                ],
                [
                    extDate.MILLI,
                    500,
                    'ms500',
                    'ms100'
                ],
                [
                    extDate.SECOND,
                    1,
                    's1',
                    'ms500'
                ],
                [
                    extDate.SECOND,
                    10,
                    's10',
                    's1'
                ],
                [
                    extDate.SECOND,
                    30,
                    's30',
                    's10'
                ],
                [
                    extDate.MINUTE,
                    1,
                    'mi1',
                    's10'
                ],
                [
                    extDate.MINUTE,
                    5,
                    'mi5',
                    'mi1'
                ],
                [
                    extDate.MINUTE,
                    10,
                    'mi10',
                    'mi5'
                ],
                [
                    extDate.MINUTE,
                    30,
                    'mi30',
                    'mi10'
                ],
                [
                    extDate.HOUR,
                    1,
                    'h1',
                    'mi30'
                ],
                [
                    extDate.HOUR,
                    6,
                    'h6',
                    'h1'
                ],
                [
                    extDate.HOUR,
                    12,
                    'h12',
                    'h6'
                ],
                [
                    extDate.DAY,
                    1,
                    'd1',
                    'h12'
                ],
                [
                    extDate.DAY,
                    7,
                    'd7',
                    'd1'
                ],
                [
                    extDate.MONTH,
                    1,
                    'mo1',
                    'd1'
                ],
                [
                    extDate.MONTH,
                    3,
                    'mo3',
                    'mo1'
                ],
                [
                    extDate.MONTH,
                    6,
                    'mo6',
                    'mo3'
                ],
                [
                    extDate.YEAR,
                    1,
                    'y1',
                    'mo3'
                ],
                [
                    extDate.YEAR,
                    5,
                    'y5',
                    'y1'
                ],
                [
                    extDate.YEAR,
                    10,
                    'y10',
                    'y5'
                ],
                [
                    extDate.YEAR,
                    100,
                    'y100',
                    'y10'
                ]
            ],
            unitIdx, currentUnit,
            plainStart = start,
            plainEnd = last,
            startIdxs = result.startIdx,
            endIdxs = result.endIdx,
            minIdxs = result.minIdx,
            maxIdxs = result.maxIdx,
            opens = result.open,
            closes = result.close,
            minXs = result.minX,
            minYs = result.minY,
            maxXs = result.maxX,
            maxYs = result.maxY,
            i, current;
        for (unitIdx = 0; last > start + 1 && unitIdx < units.length; unitIdx++) {
            minimum = new Date(dataX[startIdxs[0]]);
            currentUnit = units[unitIdx];
            minimum = extDate.align(minimum, currentUnit[0], currentUnit[1]);
            // eslint-disable-next-line max-len
            if (extDate.diff(minimum, maximum, currentUnit[0]) > dataX.length * 2 * currentUnit[1]) {
                
                continue;
            }
            if (currentUnit[3] && result.map['time_' + currentUnit[3]]) {
                lastOffset = result.map['time_' + currentUnit[3]][0];
                lastOffsetEnd = result.map['time_' + currentUnit[3]][1];
            } else {
                lastOffset = plainStart;
                lastOffsetEnd = plainEnd;
            }
            start = last;
            current = minimum;
            startIdxs[last] = startIdxs[lastOffset];
            endIdxs[last] = endIdxs[lastOffset];
            minIdxs[last] = minIdxs[lastOffset];
            maxIdxs[last] = maxIdxs[lastOffset];
            opens[last] = opens[lastOffset];
            closes[last] = closes[lastOffset];
            minXs[last] = minXs[lastOffset];
            minYs[last] = minYs[lastOffset];
            maxXs[last] = maxXs[lastOffset];
            maxYs[last] = maxYs[lastOffset];
            current = Ext.Date.add(current, currentUnit[0], currentUnit[1]);
            for (i = lastOffset + 1; i < lastOffsetEnd; i++) {
                if (dataX[endIdxs[i]] < +current) {
                    endIdxs[last] = endIdxs[i];
                    closes[last] = closes[i];
                    if (maxYs[i] > maxYs[last]) {
                        maxYs[last] = maxYs[i];
                        maxXs[last] = maxXs[i];
                        maxIdxs[last] = maxIdxs[i];
                    }
                    if (minYs[i] < minYs[last]) {
                        minYs[last] = minYs[i];
                        minXs[last] = minXs[i];
                        minIdxs[last] = minIdxs[i];
                    }
                } else {
                    last++;
                    startIdxs[last] = startIdxs[i];
                    endIdxs[last] = endIdxs[i];
                    minIdxs[last] = minIdxs[i];
                    maxIdxs[last] = maxIdxs[i];
                    opens[last] = opens[i];
                    closes[last] = closes[i];
                    minXs[last] = minXs[i];
                    minYs[last] = minYs[i];
                    maxXs[last] = maxXs[i];
                    maxYs[last] = maxYs[i];
                    current = Ext.Date.add(current, currentUnit[0], currentUnit[1]);
                }
            }
            if (last > start) {
                result.map['time_' + currentUnit[2]] = [
                    start,
                    last
                ];
            }
        }
    },
    /**
     * @private
     * @param {Object} result
     * @param {Number} position
     * @param {Number} dataX
     * @param {Number} dataOpen
     * @param {Number} dataHigh
     * @param {Number} dataLow
     * @param {Number} dataClose
     */
    "double": function(result, position, dataX, dataOpen, dataHigh, dataLow, dataClose) {
        var offset = 0,
            lastOffset,
            step = 1,
            i, startIdx, endIdx, minIdx, maxIdx, open, close, minX, minY, maxX, maxY;
        while (position > offset + 1) {
            lastOffset = offset;
            offset = position;
            step += step;
            for (i = lastOffset; i < offset; i += 2) {
                if (i === offset - 1) {
                    startIdx = result.startIdx[i];
                    endIdx = result.endIdx[i];
                    minIdx = result.minIdx[i];
                    maxIdx = result.maxIdx[i];
                    open = result.open[i];
                    close = result.close[i];
                    minX = result.minX[i];
                    minY = result.minY[i];
                    maxX = result.maxX[i];
                    maxY = result.maxY[i];
                } else {
                    startIdx = result.startIdx[i];
                    endIdx = result.endIdx[i + 1];
                    open = result.open[i];
                    close = result.close[i];
                    if (result.minY[i] <= result.minY[i + 1]) {
                        minIdx = result.minIdx[i];
                        minX = result.minX[i];
                        minY = result.minY[i];
                    } else {
                        minIdx = result.minIdx[i + 1];
                        minX = result.minX[i + 1];
                        minY = result.minY[i + 1];
                    }
                    if (result.maxY[i] >= result.maxY[i + 1]) {
                        maxIdx = result.maxIdx[i];
                        maxX = result.maxX[i];
                        maxY = result.maxY[i];
                    } else {
                        maxIdx = result.maxIdx[i + 1];
                        maxX = result.maxX[i + 1];
                        maxY = result.maxY[i + 1];
                    }
                }
                result.startIdx[position] = startIdx;
                result.endIdx[position] = endIdx;
                result.minIdx[position] = minIdx;
                result.maxIdx[position] = maxIdx;
                result.open[position] = open;
                result.close[position] = close;
                result.minX[position] = minX;
                result.minY[position] = minY;
                result.maxX[position] = maxX;
                result.maxY[position] = maxY;
                position++;
            }
            result.map['double_' + step] = [
                offset,
                position
            ];
        }
    },
    /**
     * @method
     * @private
     */
    none: Ext.emptyFn,
    /**
     * @private
     *
     * @param {Number} dataX
     * @param {Number} dataOpen
     * @param {Number} dataHigh
     * @param {Number} dataLow
     * @param {Number} dataClose
     * @return {Object}
     */
    aggregateData: function(dataX, dataOpen, dataHigh, dataLow, dataClose) {
        var length = dataX.length,
            startIdx = [],
            endIdx = [],
            minIdx = [],
            maxIdx = [],
            open = [],
            minX = [],
            minY = [],
            maxX = [],
            maxY = [],
            close = [],
            result = {
                startIdx: startIdx,
                endIdx: endIdx,
                minIdx: minIdx,
                maxIdx: maxIdx,
                open: open,
                minX: minX,
                minY: minY,
                maxX: maxX,
                maxY: maxY,
                close: close
            },
            i;
        for (i = 0; i < length; i++) {
            startIdx[i] = i;
            endIdx[i] = i;
            minIdx[i] = i;
            maxIdx[i] = i;
            open[i] = dataOpen[i];
            minX[i] = dataX[i];
            minY[i] = dataLow[i];
            maxX[i] = dataX[i];
            maxY[i] = dataHigh[i];
            close[i] = dataClose[i];
        }
        result.map = {
            original: [
                0,
                length
            ]
        };
        if (length) {
            this[this.getStrategy()](result, length, dataX, dataOpen, dataHigh, dataLow, dataClose);
        }
        return result;
    },
    /**
     * @private
     * @param {Object} items
     * @param {Number} start
     * @param {Number} end
     * @param {Number} key
     * @return {*}
     */
    binarySearchMin: function(items, start, end, key) {
        var dx = this.dataX,
            mid, val;
        if (key <= dx[items.startIdx[0]]) {
            return start;
        }
        if (key >= dx[items.startIdx[end - 1]]) {
            return end - 1;
        }
        while (start + 1 < end) {
            mid = (start + end) >> 1;
            val = dx[items.startIdx[mid]];
            if (val === key) {
                return mid;
            } else if (val < key) {
                start = mid;
            } else {
                end = mid;
            }
        }
        return start;
    },
    /**
     * @private
     * @param {Object} items
     * @param {Number} start
     * @param {Number} end
     * @param {Number} key
     * @return {*}
     */
    binarySearchMax: function(items, start, end, key) {
        var dx = this.dataX,
            mid, val;
        if (key <= dx[items.endIdx[0]]) {
            return start;
        }
        if (key >= dx[items.endIdx[end - 1]]) {
            return end - 1;
        }
        while (start + 1 < end) {
            mid = (start + end) >> 1;
            val = dx[items.endIdx[mid]];
            if (val === key) {
                return mid;
            } else if (val < key) {
                start = mid;
            } else {
                end = mid;
            }
        }
        return end;
    },
    constructor: function(config) {
        this.initConfig(config);
    },
    /**
     * Sets the data of the segment tree.
     * @param {Number} dataX
     * @param {Number} dataOpen
     * @param {Number} dataHigh
     * @param {Number} dataLow
     * @param {Number} dataClose
     */
    setData: function(dataX, dataOpen, dataHigh, dataLow, dataClose) {
        if (!dataHigh) {
            dataClose = dataLow = dataHigh = dataOpen;
        }
        this.dataX = dataX;
        this.dataOpen = dataOpen;
        this.dataHigh = dataHigh;
        this.dataLow = dataLow;
        this.dataClose = dataClose;
        if (dataX.length === dataHigh.length && dataX.length === dataLow.length) {
            this.cache = this.aggregateData(dataX, dataOpen, dataHigh, dataLow, dataClose);
        }
    },
    /**
     * Returns the minimum range of data that fits the given range and step size.
     *
     * @param {Number} min
     * @param {Number} max
     * @param {Number} estStep
     * @return {Object} The aggregation information.
     * @return {Number} return.start
     * @return {Number} return.end
     * @return {Object} return.data The aggregated data
     */
    getAggregation: function(min, max, estStep) {
        if (!this.cache) {
            return null;
        }
        // eslint-disable-next-line vars-on-top
        var minStep = Infinity,
            range = this.dataX[this.dataX.length - 1] - this.dataX[0],
            cacheMap = this.cache.map,
            result = cacheMap.original,
            name, positions, ln, step, minIdx, maxIdx;
        for (name in cacheMap) {
            positions = cacheMap[name];
            ln = positions[1] - positions[0] - 1;
            step = range / ln;
            if (estStep <= step && step < minStep) {
                result = positions;
                minStep = step;
            }
        }
        minIdx = Math.max(this.binarySearchMin(this.cache, result[0], result[1], min), result[0]);
        maxIdx = Math.min(this.binarySearchMax(this.cache, result[0], result[1], max) + 1, result[1]);
        return {
            data: this.cache,
            start: minIdx,
            end: maxIdx
        };
    }
});

/**
 *
 */
Ext.define('Ext.chart.series.sprite.Aggregative', {
    extend: 'Ext.chart.series.sprite.Cartesian',
    requires: [
        'Ext.draw.LimitedCache',
        'Ext.draw.SegmentTree'
    ],
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number[]} [dataHigh=null] Data items representing the high values
                 * of the aggregated data.
                 */
                dataHigh: 'data',
                /**
                 * @cfg {Number[]} [dataLow=null] Data items representing the low values
                 * of the aggregated data.
                 */
                dataLow: 'data',
                /**
                 * @cfg {Number[]} [dataClose=null] Data items representing the closing values
                 * of the aggregated data.
                 */
                dataClose: 'data'
            },
            aliases: {
                /**
                 * @cfg {Number[]} [dataOpen=null] Data items representing the opening values
                 * of the aggregated data.
                 */
                dataOpen: 'dataY'
            },
            defaults: {
                dataHigh: null,
                dataLow: null,
                dataClose: null
            }
        }
    },
    config: {
        aggregator: {}
    },
    applyAggregator: function(aggregator, oldAggr) {
        return Ext.factory(aggregator, Ext.draw.SegmentTree, oldAggr);
    },
    constructor: function() {
        this.callParent(arguments);
    },
    processDataY: function() {
        var me = this,
            attr = me.attr,
            high = attr.dataHigh,
            low = attr.dataLow,
            close = attr.dataClose,
            open = attr.dataY,
            aggregator;
        me.callParent(arguments);
        if (attr.dataX && open && open.length > 0) {
            aggregator = me.getAggregator();
            if (high) {
                aggregator.setData(attr.dataX, attr.dataY, high, low, close);
            } else {
                aggregator.setData(attr.dataX, attr.dataY);
            }
        }
    },
    getGapWidth: function() {
        return 1;
    },
    renderClipped: function(surface, ctx, dataClipRect, surfaceClipRect) {
        var me = this,
            min = Math.min(dataClipRect[0], dataClipRect[2]),
            max = Math.max(dataClipRect[0], dataClipRect[2]),
            aggregator = me.getAggregator(),
            aggregates = aggregator && aggregator.getAggregation(min, max, (max - min) / surfaceClipRect[2] * me.getGapWidth());
        if (aggregates) {
            me.dataStart = aggregates.data.startIdx[aggregates.start];
            me.dataEnd = aggregates.data.endIdx[aggregates.end - 1];
            me.renderAggregates(aggregates.data, aggregates.start, aggregates.end, surface, ctx, dataClipRect, surfaceClipRect);
        }
    }
});

/**
 * @class Ext.chart.series.sprite.CandleStick
 * @extends Ext.chart.series.sprite.Aggregative
 *
 * CandleStick series sprite.
 */
Ext.define('Ext.chart.series.sprite.CandleStick', {
    alias: 'sprite.candlestickSeries',
    extend: 'Ext.chart.series.sprite.Aggregative',
    inheritableStatics: {
        def: {
            processors: {
                raiseStyle: function(n, o) {
                    return Ext.merge({}, o || {}, n);
                },
                dropStyle: function(n, o) {
                    return Ext.merge({}, o || {}, n);
                },
                /**
                 * @cfg {Number} [barWidth=15] The bar width of the candles.
                 */
                barWidth: 'number',
                /**
                 * @cfg {Number} [padding=3] The amount of padding between candles.
                 */
                padding: 'number',
                /**
                 * @cfg {String} [ohlcType='candlestick'] Determines whether candlestick
                 * or ohlc is used.
                 */
                ohlcType: 'enums(candlestick,ohlc)'
            },
            defaults: {
                raiseStyle: {
                    strokeStyle: 'green',
                    fillStyle: 'green'
                },
                dropStyle: {
                    strokeStyle: 'red',
                    fillStyle: 'red'
                },
                barWidth: 15,
                padding: 3,
                lineJoin: 'miter',
                miterLimit: 5,
                ohlcType: 'candlestick'
            },
            triggers: {
                raiseStyle: 'raiseStyle',
                dropStyle: 'dropStyle'
            },
            updaters: {
                raiseStyle: function() {
                    var me = this,
                        tpl = me.raiseTemplate;
                    if (tpl) {
                        tpl.setAttributes(me.attr.raiseStyle);
                    }
                },
                dropStyle: function() {
                    var me = this,
                        tpl = me.dropTemplate;
                    if (tpl) {
                        tpl.setAttributes(me.attr.dropStyle);
                    }
                }
            }
        }
    },
    candlestick: function(ctx, open, high, low, close, mid, halfWidth) {
        var minOC = Math.min(open, close),
            maxOC = Math.max(open, close);
        // lower stick
        ctx.moveTo(mid, low);
        ctx.lineTo(mid, minOC);
        // body rect
        ctx.moveTo(mid + halfWidth, maxOC);
        ctx.lineTo(mid + halfWidth, minOC);
        ctx.lineTo(mid - halfWidth, minOC);
        ctx.lineTo(mid - halfWidth, maxOC);
        ctx.closePath();
        // upper stick
        ctx.moveTo(mid, high);
        ctx.lineTo(mid, maxOC);
    },
    ohlc: function(ctx, open, high, low, close, mid, halfWidth) {
        ctx.moveTo(mid, high);
        ctx.lineTo(mid, low);
        ctx.moveTo(mid, open);
        ctx.lineTo(mid - halfWidth, open);
        ctx.moveTo(mid, close);
        ctx.lineTo(mid + halfWidth, close);
    },
    constructor: function() {
        var me = this,
            Rect = Ext.draw.sprite.Rect;
        me.callParent(arguments);
        me.raiseTemplate = new Rect({
            parent: me
        });
        me.dropTemplate = new Rect({
            parent: me
        });
    },
    getGapWidth: function() {
        var attr = this.attr,
            barWidth = attr.barWidth,
            padding = attr.padding;
        return barWidth + padding;
    },
    renderAggregates: function(aggregates, start, end, surface, ctx, clip) {
        var me = this,
            attr = me.attr,
            ohlcType = attr.ohlcType,
            series = me.getSeries(),
            matrix = attr.matrix,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            halfWidth = Math.round(attr.barWidth * 0.5),
            dataX = attr.dataX,
            opens = aggregates.open,
            closes = aggregates.close,
            maxYs = aggregates.maxY,
            minYs = aggregates.minY,
            startIdxs = aggregates.startIdx,
            pixelAdjust = attr.lineWidth * surface.devicePixelRatio / 2,
            renderer = attr.renderer,
            rendererConfig = renderer && {},
            rendererParams, rendererChanges, open, high, low, close, mid, i, template;
        me.rendererData = me.rendererData || {
            store: me.getStore()
        };
        pixelAdjust -= Math.floor(pixelAdjust);
        // Render raises.
        ctx.save();
        template = me.raiseTemplate;
        template.useAttributes(ctx, clip);
        if (!renderer) {
            ctx.beginPath();
        }
        for (i = start; i < end; i++) {
            if (opens[i] <= closes[i]) {
                open = Math.round(opens[i] * yy + dy) + pixelAdjust;
                high = Math.round(maxYs[i] * yy + dy) + pixelAdjust;
                low = Math.round(minYs[i] * yy + dy) + pixelAdjust;
                close = Math.round(closes[i] * yy + dy) + pixelAdjust;
                mid = Math.round(dataX[startIdxs[i]] * xx + dx) + pixelAdjust;
                if (renderer) {
                    ctx.save();
                    ctx.beginPath();
                    rendererConfig.open = open;
                    rendererConfig.high = high;
                    rendererConfig.low = low;
                    rendererConfig.close = close;
                    rendererConfig.mid = mid;
                    rendererConfig.halfWidth = halfWidth;
                    rendererParams = [
                        me,
                        rendererConfig,
                        me.rendererData,
                        i
                    ];
                    rendererChanges = Ext.callback(renderer, null, rendererParams, 0, series);
                    Ext.apply(ctx, rendererChanges);
                }
                me[ohlcType](ctx, open, high, low, close, mid, halfWidth);
                if (renderer) {
                    ctx.fillStroke(template.attr);
                    ctx.restore();
                }
            }
        }
        if (!renderer) {
            ctx.fillStroke(template.attr);
        }
        ctx.restore();
        // Render drops.
        ctx.save();
        template = me.dropTemplate;
        template.useAttributes(ctx, clip);
        if (!renderer) {
            ctx.beginPath();
        }
        for (i = start; i < end; i++) {
            if (opens[i] > closes[i]) {
                open = Math.round(opens[i] * yy + dy) + pixelAdjust;
                high = Math.round(maxYs[i] * yy + dy) + pixelAdjust;
                low = Math.round(minYs[i] * yy + dy) + pixelAdjust;
                close = Math.round(closes[i] * yy + dy) + pixelAdjust;
                mid = Math.round(dataX[startIdxs[i]] * xx + dx) + pixelAdjust;
                if (renderer) {
                    ctx.save();
                    ctx.beginPath();
                    rendererConfig.open = open;
                    rendererConfig.high = high;
                    rendererConfig.low = low;
                    rendererConfig.close = close;
                    rendererConfig.mid = mid;
                    rendererConfig.halfWidth = halfWidth;
                    rendererParams = [
                        me,
                        rendererConfig,
                        me.rendererData,
                        i
                    ];
                    rendererChanges = Ext.callback(renderer, null, rendererParams, 0, me.getSeries());
                    Ext.apply(ctx, rendererChanges);
                }
                me[ohlcType](ctx, open, high, low, close, mid, halfWidth);
                if (renderer) {
                    ctx.fillStroke(template.attr);
                    ctx.restore();
                }
            }
        }
        if (!renderer) {
            ctx.fillStroke(template.attr);
        }
        ctx.restore();
    }
});

/**
 * @class Ext.chart.series.CandleStick
 * @extends Ext.chart.series.Cartesian
 * 
 * Creates a candlestick or OHLC Chart.
 *
 * CandleStick series are typically used to plot price movements of a security on an exchange
 * over time. The series can be used with the 'time' axis, but since exchanges often close
 * for weekends, and the price data has gaps for those days, it's more practical to use this series
 * with the 'category' axis to avoid rendering those data gaps. The 'category' axis has no notion
 * of time (and thus gaps) and treats every Date object (value of the 'xField') as a unique
 * category. However, it also means that it doesn't support the 'dateFormat' config,
 * which can be easily remedied with a 'renderer' that formats a Date object for use
 * as an axis label. For example:
 *
 *     @example
 *     new Ext.chart.CartesianChart({
 *         xtype: 'cartesian',
 *         renderTo: document.body,
 *         width: 700,
 *         height: 500,
 *         insetPadding: 20,
 *         innerPadding: '0 20 0 20',
 *
 *         store: {
 *             data: [
 *                 {
 *                     time: new Date('Nov 17 2016'),
 *                     o: 52.40, h: 52.74, l: 52.18, c: 52.29
 *                 },
 *                 {
 *                     time: new Date('Nov 18 2016'),
 *                     o: 51.87, h: 52.22, l: 51.51, c: 52.04
 *                 },
 *                 {
 *                     time: new Date('Nov 21 2016'),
 *                     o: 53.02, h: 53.40, l: 53.02, c: 53.33
 *                 },
 *                 {
 *                     time: new Date('Nov 22 2016'),
 *                     o: 53.48, h: 53.80, l: 53.13, c: 53.70
 *                 },
 *                 {
 *                     time: new Date('Nov 23 2016'),
 *                     o: 52.85, h: 53.39, l: 52.76, c: 53.28
 *                 },
 *                 {
 *                     time: new Date('Nov 25 2016'),
 *                     o: 53.28, h: 53.45, l: 53.20, c: 53.40
 *                 },
 *                 {
 *                     time: new Date('Nov 28 2016'),
 *                     o: 52.51, h: 52.58, l: 51.96, c: 52.00
 *                 },
 *                 {
 *                     time: new Date('Nov 29 2016'),
 *                     o: 51.25, h: 51.98, l: 51.10, c: 51.79
 *                 },
 *                 {
 *                     time: new Date('Nov 30 2016'),
 *                     o: 53.65, h: 54.56, l: 53.60, c: 54.17
 *                 },
 *                 {
 *                     time: new Date('Dec 01 2016'),
 *                     o: 55.26, h: 55.75, l: 54.94, c: 55.13
 *                 }
 *             ]
 *         },
 *         axes: [
 *             {
 *                 type: 'numeric',
 *                 position: 'left'
 *             },
 *             {
 *                 type: 'category',
 *                 position: 'bottom',
 *
 *                 renderer: function (axis, value) {
 *                     return Ext.Date.format(value, 'M j\nY');
 *                 }
 *             }
 *         ],
 *         series: {
 *             type: 'candlestick',
 *
 *             xField: 'time',
 *
 *             openField: 'o',
 *             highField: 'h',
 *             lowField: 'l',
 *             closeField: 'c',
 *
 *             style: {
 *                 barWidth: 10,
 *
 *                 dropStyle: {
 *                     fill: 'rgb(222, 87, 87)',
 *                     stroke: 'rgb(222, 87, 87)',
 *                     lineWidth: 3
 *                 },
 *                 raiseStyle: {
 *                     fill: 'rgb(48, 189, 167)',
 *                     stroke: 'rgb(48, 189, 167)',
 *                     lineWidth: 3
 *                 }
 *             }
 *         }
 *     });
 */
Ext.define('Ext.chart.series.CandleStick', {
    extend: 'Ext.chart.series.Cartesian',
    requires: [
        'Ext.chart.series.sprite.CandleStick'
    ],
    alias: 'series.candlestick',
    type: 'candlestick',
    seriesType: 'candlestickSeries',
    isCandleStick: true,
    config: {
        /**
         * @cfg {String} openField
         * The store record field name that represents the opening value of the given period.
         */
        openField: null,
        /**
         * @cfg {String} highField
         * The store record field name that represents the highest value of the time interval
         * represented.
         */
        highField: null,
        /**
         * @cfg {String} lowField
         * The store record field name that represents the lowest value of the time interval
         * represented.
         */
        lowField: null,
        /**
         * @cfg {String} closeField
         * The store record field name that represents the closing value of the given period.
         */
        closeField: null
    },
    fieldCategoryY: [
        'Open',
        'High',
        'Low',
        'Close'
    ],
    themeColorCount: function() {
        return 2;
    }
});

/**
 * @abstract
 * @class Ext.chart.series.Polar
 * @extends Ext.chart.series.Series
 *
 * Common base class for series implementations that plot values using polar coordinates.
 *
 * Polar charts accept angles in radians. You can calculate radians with the following
 * formula:
 *
 *      radians = degrees x Π/180
 */
Ext.define('Ext.chart.series.Polar', {
    extend: 'Ext.chart.series.Series',
    config: {
        /**
         * @cfg {Number} [rotation=0]
         * The angle in radians at which the first polar series item should start.
         */
        rotation: 0,
        /**
         * @cfg {Number} radius
         * @private
         * Use {@link Ext.chart.series.Pie#cfg!radiusFactor radiusFactor} instead.
         *
         * The internally used radius of the polar series. Set to `null` will fit the
         * polar series to the boundary.
         */
        radius: null,
        /**
         * @cfg {Array} center for the polar series.
         */
        center: [
            0,
            0
        ],
        /**
         * @cfg {Number} [offsetX=0]
         * The x-offset of center of the polar series related to the center of the boundary.
         */
        offsetX: 0,
        /**
         * @cfg {Number} [offsetY=0]
         * The y-offset of center of the polar series related to the center of the boundary.
         */
        offsetY: 0,
        /**
         * @cfg {Boolean} [showInLegend=true]
         * Whether to add the series elements as legend items.
         */
        showInLegend: true,
        /**
         * @private
         * @cfg {String} xField
         */
        xField: null,
        /**
         * @private
         * @cfg {String} yField
         */
        yField: null,
        /**
         * @cfg {String} angleField
         * The store record field name for the angular axes in radar charts,
         * or the size of the slices in pie charts.
         */
        angleField: null,
        /**
         * @cfg {String} radiusField
         * The store record field name for the radial axes in radar charts,
         * or the radius of the slices in pie charts.
         */
        radiusField: null,
        xAxis: null,
        yAxis: null
    },
    directions: [
        'X',
        'Y'
    ],
    fieldCategoryX: [
        'X'
    ],
    fieldCategoryY: [
        'Y'
    ],
    deprecatedConfigs: {
        field: 'angleField',
        lengthField: 'radiusField'
    },
    constructor: function(config) {
        var me = this,
            configurator = me.self.getConfigurator(),
            configs = configurator.configs,
            p;
        if (config) {
            for (p in me.deprecatedConfigs) {
                if (p in config && !(config in configs)) {
                    Ext.raise("'" + p + "' config has been deprecated. Please use the '" + me.deprecatedConfigs[p] + "' config instead.");
                }
            }
        }
        me.callParent([
            config
        ]);
    },
    getXField: function() {
        return this.getAngleField();
    },
    updateXField: function(value) {
        this.setAngleField(value);
    },
    getYField: function() {
        return this.getRadiusField();
    },
    updateYField: function(value) {
        this.setRadiusField(value);
    },
    applyXAxis: function(newAxis, oldAxis) {
        return this.getChart().getAxis(newAxis) || oldAxis;
    },
    applyYAxis: function(newAxis, oldAxis) {
        return this.getChart().getAxis(newAxis) || oldAxis;
    },
    getXRange: function() {
        return [
            this.dataRange[0],
            this.dataRange[2]
        ];
    },
    getYRange: function() {
        return [
            this.dataRange[1],
            this.dataRange[3]
        ];
    },
    themeColorCount: function() {
        var me = this,
            store = me.getStore(),
            count = store && store.getCount() || 0;
        return count;
    },
    isStoreDependantColorCount: true,
    getDefaultSpriteConfig: function() {
        return {
            type: this.seriesType,
            renderer: this.getRenderer(),
            centerX: 0,
            centerY: 0,
            rotationCenterX: 0,
            rotationCenterY: 0
        };
    },
    applyRotation: function(rotation) {
        return Ext.draw.sprite.AttributeParser.angle(Ext.draw.Draw.rad(rotation));
    },
    updateRotation: function(rotation) {
        var sprites = this.getSprites();
        if (sprites && sprites[0]) {
            sprites[0].setAttributes({
                baseRotation: rotation
            });
        }
    }
});

/**
 * Displays a gauge chart.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'polar', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        store: {
 *            fields: ['mph', 'fuel', 'temp', 'rpm'],
 *            data: [{
 *                mph: 65,
 *                fuel: 50,
 *                temp: 150,
 *                rpm: 6000
 *            }]
 *        },
 *        series: {
 *            type: 'gauge',
 *            colors: ['#1F6D91', '#90BCC9'],
 *            angleField: 'mph',
 *            needle: true,
 *            donut: 30
 *        }
 *     });
 */
Ext.define('Ext.chart.series.Gauge', {
    alias: 'series.gauge',
    extend: 'Ext.chart.series.Polar',
    type: 'gauge',
    seriesType: 'pieslice',
    requires: [
        'Ext.draw.sprite.Sector'
    ],
    config: {
        /**
         * @cfg {String} angleField
         * The store record field name to be used for the gauge value.
         * The values bound to this field name must be positive real numbers.
         */
        /**
         * @cfg {Boolean} needle
         * If true, display the gauge as a needle, otherwise as a sector.
         */
        needle: false,
        /**
         * @cfg {Number} needleLength
         * Percentage of the length of needle compared to the radius of the entire disk.
         */
        needleLength: 90,
        /**
         * @cfg {Number} needleWidth
         * Width of the needle in pixels.
         */
        needleWidth: 4,
        /**
         * @cfg {Number} donut
         * Percentage of the radius of the donut hole compared to the entire disk.
         */
        donut: 30,
        /**
         * @cfg {Boolean} showInLegend
         * Whether to add the gauge chart elements as legend items.
         */
        showInLegend: false,
        /**
         * @cfg {Number} value
         * Directly sets the displayed value of the gauge.
         * It is ignored if {@link #angleField} is provided.
         */
        value: null,
        /**
         * @cfg {Array} colors (required)
         * An array of color values which is used for the needle and the `sectors`.
         */
        colors: null,
        /**
         * @cfg {Array} sectors
         * Allows to paint sectors of different colors in the background of the gauge,
         * with optional labels.
         *
         * It can be an array of numbers (each between `minimum` and `maximum`) that
         * define the highest value of each sector. For N sectors, only (N-1) values are
         * needed because it is assumed that the first sector starts at `minimum` and the
         * last sector ends at `maximum`. Example: a water temperature gauge that is blue 
         * below 20C, red above 80C, gray in-between, and with an orange needle...
         *
         *      minimum: 0,
         *      maximum: 100,
         *      sectors: [20, 80],
         *      colors: ['orange', 'blue', 'lightgray', 'red']
         *
         * It can be also an array of objects, each with the following properties:
         * 
         * @cfg {Number} sectors.start The starting value of the sector. If omitted, it
         * uses the previous sector's `end` value or the chart's `minimum`.
         * @cfg {Number} sectors.end The ending value of the sector. If omitted, it uses
         * the `maximum` defined for the chart.
         * @cfg {String} sectors.label The label for this sector. Labels are styled using
         * the series' {@link Ext.chart.series.Series#label label} config.
         * @cfg {String} sectors.color The color of the sector. If omitted, it uses one
         * of the `colors` defined for the series or for the chart.
         * @cfg {Object} sectors.style An additional style object for the sector (for
         * instance to set the opacity or to draw a line of a different color around the
         * sector).
         *
         *      minimum: 0,
         *      maximum: 100,
         *      sectors: [{
         *              end: 20,
         *              label: 'Cold',
         *              color: 'aqua'
         *          },
         *          {
         *              end: 80,
         *              label: 'Temp.',
         *              color: 'lightgray',
         *              style: { strokeStyle:'black', strokeOpacity:1, lineWidth:1 }
         *          },
         *          {
         *              label: 'Hot',
         *              color: 'tomato'
         *          }]
         */
        sectors: null,
        /**
         * @cfg {Number} minimum
         * The minimum value of the gauge.
         */
        minimum: 0,
        /**
         * @cfg {Number} maximum
         * The maximum value of the gauge.
         */
        maximum: 100,
        rotation: 0,
        /**
        * @cfg {Number} totalAngle
        * The size of the sector that the series will occupy.
        */
        totalAngle: Math.PI / 2,
        rect: [
            0,
            0,
            1,
            1
        ],
        center: [
            0.5,
            0.75
        ],
        radius: 0.5,
        /**
         * @cfg {Boolean} wholeDisk Indicates whether to show the whole disk
         * or only the marked part.
         */
        wholeDisk: false
    },
    coordinateX: function() {
        return this.coordinate('X', 0, 2);
    },
    coordinateY: function() {
        return this.coordinate('Y', 1, 2);
    },
    updateNeedle: function(needle) {
        var me = this,
            sprites = me.getSprites(),
            angle = me.valueToAngle(me.getValue());
        if (sprites && sprites.length) {
            sprites[0].setAttributes({
                startAngle: (needle ? angle : 0),
                endAngle: angle,
                strokeOpacity: (needle ? 1 : 0),
                lineWidth: (needle ? me.getNeedleWidth() : 0)
            });
            me.doUpdateStyles();
        }
    },
    themeColorCount: function() {
        var me = this,
            store = me.getStore(),
            count = store && store.getCount() || 0;
        return count + (me.getNeedle() ? 0 : 1);
    },
    updateColors: function(colors, oldColors) {
        var me = this,
            sectors = me.getSectors(),
            sectorCount = sectors && sectors.length,
            sprites = me.getSprites(),
            newColors = Ext.Array.clone(colors),
            colorCount = colors && colors.length,
            i;
        if (!colorCount || !colors[0]) {
            return;
        }
        // Make sure the 'sectors' colors are not overridden.
        for (i = 0; i < sectorCount; i++) {
            newColors[i + 1] = sectors[i].color || newColors[i + 1] || colors[i % colorCount];
        }
        if (sprites.length) {
            sprites[0].setAttributes({
                strokeStyle: newColors[0]
            });
        }
        this.setSubStyle({
            fillStyle: newColors,
            strokeStyle: newColors
        });
        this.doUpdateStyles();
    },
    updateRect: function(rect) {
        var wholeDisk = this.getWholeDisk(),
            halfTotalAngle = wholeDisk ? Math.PI : this.getTotalAngle() / 2,
            donut = this.getDonut() / 100,
            width, height, radius;
        if (halfTotalAngle <= Math.PI / 2) {
            width = 2 * Math.sin(halfTotalAngle);
            height = 1 - donut * Math.cos(halfTotalAngle);
        } else {
            width = 2;
            height = 1 - Math.cos(halfTotalAngle);
        }
        radius = Math.min(rect[2] / width, rect[3] / height);
        this.setRadius(radius);
        this.setCenter([
            rect[2] / 2,
            radius + (rect[3] - height * radius) / 2
        ]);
    },
    updateCenter: function(center) {
        this.setStyle({
            centerX: center[0],
            centerY: center[1],
            rotationCenterX: center[0],
            rotationCenterY: center[1]
        });
        this.doUpdateStyles();
    },
    updateRotation: function(rotation) {
        this.setStyle({
            rotationRads: rotation - (this.getTotalAngle() + Math.PI) / 2
        });
        this.doUpdateStyles();
    },
    doUpdateShape: function(radius, donut) {
        var me = this,
            sectors = me.getSectors(),
            sectorCount = (sectors && sectors.length) || 0,
            needleLength = me.getNeedleLength() / 100,
            endRhoArray;
        // Initialize an array that contains the endRho for each sprite.
        // The first sprite is for the needle, the others for the gauge background sectors. 
        // Note: SubStyle arrays are handled in series.getStyleByIndex().
        endRhoArray = [
            radius * needleLength,
            radius
        ];
        while (sectorCount--) {
            endRhoArray.push(radius);
        }
        me.setSubStyle({
            endRho: endRhoArray,
            startRho: radius / 100 * donut
        });
        me.doUpdateStyles();
    },
    updateRadius: function(radius) {
        var donut = this.getDonut();
        this.doUpdateShape(radius, donut);
    },
    updateDonut: function(donut) {
        var radius = this.getRadius();
        this.doUpdateShape(radius, donut);
    },
    valueToAngle: function(value) {
        value = this.applyValue(value);
        return this.getTotalAngle() * (value - this.getMinimum()) / (this.getMaximum() - this.getMinimum());
    },
    applyValue: function(value) {
        return Math.min(this.getMaximum(), Math.max(value, this.getMinimum()));
    },
    updateValue: function(value) {
        var me = this,
            needle = me.getNeedle(),
            angle = me.valueToAngle(value),
            sprites = me.getSprites();
        sprites[0].getRendererData().value = value;
        sprites[0].setAttributes({
            startAngle: (needle ? angle : 0),
            endAngle: angle
        });
        me.doUpdateStyles();
    },
    processData: function() {
        var me = this,
            store = me.getStore(),
            record = store && store.first(),
            animation, duration, axis, min, max, xField, value;
        if (record) {
            xField = me.getXField();
            if (xField) {
                value = record.get(xField);
            }
        }
        axis = me.getXAxis();
        if (axis) {
            min = axis.getMinimum();
            max = axis.getMaximum();
            // Animating the axis here can lead to weird looking results.
            animation = axis.getSprites()[0].getAnimation();
            duration = animation.getDuration();
            animation.setDuration(0);
            if (Ext.isNumber(min)) {
                me.setMinimum(min);
            } else {
                axis.setMinimum(me.getMinimum());
            }
            if (Ext.isNumber(max)) {
                me.setMaximum(max);
            } else {
                axis.setMaximum(me.getMaximum());
            }
            animation.setDuration(duration);
        }
        if (!Ext.isNumber(value)) {
            value = me.getMinimum();
        }
        me.setValue(value);
    },
    getDefaultSpriteConfig: function() {
        return {
            type: this.seriesType,
            renderer: this.getRenderer(),
            animation: {
                customDurations: {
                    translationX: 0,
                    translationY: 0,
                    rotationCenterX: 0,
                    rotationCenterY: 0,
                    centerX: 0,
                    centerY: 0,
                    startRho: 0,
                    endRho: 0,
                    baseRotation: 0
                }
            }
        };
    },
    normalizeSectors: function(sectors) {
        // Make sure all the sectors in the array have a legit start and end.
        // Note: the array is modified in-place.
        var me = this,
            sectorCount = (sectors && sectors.length) || 0,
            i, value, start, end;
        if (sectorCount) {
            for (i = 0; i < sectorCount; i++) {
                value = sectors[i];
                if (typeof value === 'number') {
                    sectors[i] = {
                        start: (i > 0 ? sectors[i - 1].end : me.getMinimum()),
                        end: Math.min(value, me.getMaximum())
                    };
                    if (i === (sectorCount - 1) && sectors[i].end < me.getMaximum()) {
                        sectors[i + 1] = {
                            start: sectors[i].end,
                            end: me.getMaximum()
                        };
                    }
                } else {
                    if (typeof value.start === 'number') {
                        start = Math.max(value.start, me.getMinimum());
                    } else {
                        start = (i > 0 ? sectors[i - 1].end : me.getMinimum());
                    }
                    if (typeof value.end === 'number') {
                        end = Math.min(value.end, me.getMaximum());
                    } else {
                        end = me.getMaximum();
                    }
                    sectors[i].start = start;
                    sectors[i].end = end;
                }
            }
        } else {
            sectors = [
                {
                    start: me.getMinimum(),
                    end: me.getMaximum()
                }
            ];
        }
        return sectors;
    },
    getSprites: function() {
        var me = this,
            store = me.getStore(),
            value = me.getValue(),
            label = me.getLabel(),
            i, ln;
        // The store must be initialized, or the value must be set
        if (!store && !Ext.isNumber(value)) {
            return Ext.emptyArray;
        }
        // Return cached sprites
        // eslint-disable-next-line vars-on-top, one-var
        var chart = me.getChart(),
            animation = me.getAnimation() || chart && chart.getAnimation(),
            sprites = me.sprites,
            spriteIndex = 0,
            sprite, sectors, attr, rendererData,
            // Hack to avoid having the lineWidths overwritten by the one specified in the theme.
            // In fact, all the style properties from the needle and sectors should go
            // to the series subStyle.
            lineWidths = [];
        if (sprites && sprites.length) {
            sprites[0].setAnimation(animation);
            return sprites;
        }
        rendererData = {
            store: store,
            field: me.getXField(),
            // for backward compatibility only (deprecated in 5.5)
            angleField: me.getXField(),
            value: value,
            series: me
        };
        // Create needle sprite
        me.needleSprite = sprite = me.createSprite();
        sprite.setAttributes({
            zIndex: 10
        }, true);
        sprite.setRendererData(rendererData);
        sprite.setRendererIndex(spriteIndex++);
        lineWidths.push(me.getNeedleWidth());
        if (label) {
            label.getTemplate().setField(true);
        }
        // Enable labels
        // Create background sprite(s)
        sectors = me.normalizeSectors(me.getSectors());
        for (i = 0 , ln = sectors.length; i < ln; i++) {
            attr = {
                startAngle: me.valueToAngle(sectors[i].start),
                endAngle: me.valueToAngle(sectors[i].end),
                label: sectors[i].label,
                fillStyle: sectors[i].color,
                strokeOpacity: 0,
                doCallout: false,
                // Show labels inside sectors.
                labelOverflowPadding: -1
            };
            // Allow labels to overlap.
            Ext.apply(attr, sectors[i].style);
            sprite = me.createSprite();
            sprite.setRendererData(rendererData);
            sprite.setRendererIndex(spriteIndex++);
            sprite.setAttributes(attr, true);
            lineWidths.push(attr.lineWidth);
        }
        me.setSubStyle({
            lineWidth: lineWidths
        });
        me.doUpdateStyles();
        return sprites;
    },
    doUpdateStyles: function() {
        var me = this;
        me.callParent();
        if (me.sprites.length) {
            me.needleSprite.setAttributes({
                startRho: me.getNeedle() ? 0 : (me.getRadius() / 100 * me.getDonut())
            });
        }
    }
});

/**
 * @class Ext.chart.series.sprite.Line
 * @extends Ext.chart.series.sprite.Aggregative
 *
 * Line series sprite.
 */
Ext.define('Ext.chart.series.sprite.Line', {
    alias: 'sprite.lineSeries',
    extend: 'Ext.chart.series.sprite.Aggregative',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Object} [curve={type: 'linear'}]
                 * The type of curve that connects the data points.
                 *
                 * For example:
                 *
                 *     // The data points are connected by line segments.
                 *     // This is the default setting.
                 *     curve: {
                 *         type: 'linear'
                 *     }
                 *
                 *     // Cardinal spline interpolation is used to produce the curve
                 *     // that connects the data points. The `tension` parameter can
                 *     // be used to control the smoothness of the curve. A tension
                 *     // of 0 corresponds to infinite tension, which results in straight
                 *     // lines between data points. A tension of 1 corresponds to
                 *     // no tension, allowing the spline to take the path of least
                 *     // total bend. With tension values greater than 1, the curve
                 *     // behaves like a compressed spring, pushed to take a longer path.
                 *     // A cardinal spline with a tension of 0.5 is a special case.
                 *     // It is then called a Catmull-Rom spline. Catmull-Rom splines are
                 *     // thought to be esthetically pleasing and are quite common.
                 *     // Note: spline interpolation only works on gapless data.
                 *     curve: {
                 *         type: 'cardinal,
                 *         tension: 0.5
                 *     }
                 *
                 *     // Produces a natural cubic spline with the second derivative
                 *     // of the spline set to zero at the endpoints.
                 *     curve: {
                 *         type: 'natural'
                 *     }
                 *
                 *     // The data points are connected by alternating horizontal and
                 *     // vertical lines. The y-value changes after the x-value.
                 *     curve: {
                 *         type: 'step-after'
                 *     }
                 *
                 */
                curve: 'default',
                /**
                 * @cfg {Boolean} [fillArea=false]
                 * `true` if the sprite paints the area underneath the line.
                 */
                fillArea: 'bool',
                /**
                 * @cfg {"gap"/"connect"/"origin"} [nullStyle="gap"]
                 * Possible values:
                 * 'gap' - null points are rendered as gaps.
                 * 'connect' - non-null points are connected across null points, so that
                 * there is no gap, unless null points are at the beginning/end of the line.
                 * Only the visible data points are connected - if a visible data point
                 * is followed by a series of null points that go off screen and eventually
                 * terminate with a non-null point, the connection won't be made.
                 * 'origin' - null data points are rendered at the origin,
                 * which is the y-coordinate of a point where the x and y axes meet.
                 * This requires that at least the x-coordinate of a point is a valid value.
                 */
                nullStyle: 'enums(gap,connect,origin)',
                /**
                 * @cfg {Boolean} [preciseStroke=true]
                 * `true` if the line uses precise stroke.
                 */
                preciseStroke: 'bool',
                /**
                 * @private
                 * The x-axis associated with the Line series.
                 * We need to know the position of the x-axis to fill the area underneath
                 * the stroke properly.
                 */
                xAxis: 'default',
                /**
                 * @cfg {Number} [yCap=Math.pow(2, 20)]
                 * Absolute maximum y-value.
                 * Larger values will be capped to avoid rendering issues.
                 */
                // The 'default' processor is used here as we don't want this attribute to animate.
                yCap: 'default'
            },
            defaults: {
                curve: {
                    type: 'linear'
                },
                nullStyle: 'connect',
                fillArea: false,
                preciseStroke: true,
                xAxis: null,
                yCap: Math.pow(2, 20),
                yJump: 50
            },
            triggers: {
                dataX: 'dataX,bbox,curve',
                dataY: 'dataY,bbox,curve',
                curve: 'curve'
            },
            updaters: {
                curve: 'curveUpdater'
            }
        }
    },
    list: null,
    curveUpdater: function(attr) {
        var me = this,
            dataX = attr.dataX,
            dataY = attr.dataY,
            curve = attr.curve,
            smoothable = dataX && dataY && dataX.length > 2 && dataY.length > 2,
            type = curve.type;
        if (smoothable) {
            if (type === 'natural') {
                me.smoothX = Ext.draw.Draw.naturalSpline(dataX);
                me.smoothY = Ext.draw.Draw.naturalSpline(dataY);
            } else if (type === 'cardinal') {
                me.smoothX = Ext.draw.Draw.cardinalSpline(dataX, curve.tension);
                me.smoothY = Ext.draw.Draw.cardinalSpline(dataY, curve.tension);
            } else {
                smoothable = false;
            }
        }
        if (!smoothable) {
            delete me.smoothX;
            delete me.smoothY;
        }
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr,
            ymin = Math.min(0, attr.dataMinY),
            ymax = Math.max(0, attr.dataMaxY);
        plain.x = attr.dataMinX;
        plain.y = ymin;
        plain.width = attr.dataMaxX - attr.dataMinX;
        plain.height = ymax - ymin;
    },
    drawStrip: function(ctx, strip) {
        var i, ln;
        ctx.moveTo(strip[0], strip[1]);
        for (i = 2 , ln = strip.length; i < ln; i += 2) {
            ctx.lineTo(strip[i], strip[i + 1]);
        }
    },
    drawStraightStroke: function(surface, ctx, start, end, list, xAxis) {
        var me = this,
            attr = me.attr,
            nullStyle = attr.nullStyle,
            isConnect = nullStyle === 'connect',
            isOrigin = nullStyle === 'origin',
            renderer = attr.renderer,
            curve = attr.curve,
            step = curve.type === 'step-after',
            needMoveTo = true,
            ln = list.length,
            lineConfig = {
                type: 'line',
                smooth: false,
                step: step
            },
            rendererChanges, params, stripStartX, isValidX0, isValidX, isValidX1, isValidPoint0, isValidPoint, isValidPoint1, isGap, lastValidPoint, px, py, x, y, x0, y0, x1, y1, i,
            // 'strip' stores last continuous segment of the stroke,
            // which we may need to re-build, if there's a fill as well.
            // For example, if the renderer returned a style that needs
            // to be applied to the current step, or we reached a null
            // point in the data, where we have to fill the current continuous
            // segment, we build and close a path that will be filled, then
            // re-build the stroke path, using coordinates saved in the 'strip',
            // and render the stroke on top of the fill.
            strip = [];
        ctx.beginPath();
        for (i = 3; i < ln; i += 3) {
            x0 = list[i - 3];
            y0 = list[i - 2];
            x = list[i];
            y = list[i + 1];
            x1 = list[i + 3];
            y1 = list[i + 4];
            isValidX0 = Ext.isNumber(x0);
            isValidX = Ext.isNumber(x);
            isValidX1 = Ext.isNumber(x1);
            isValidPoint0 = isValidX0 && Ext.isNumber(y0);
            isValidPoint = isValidX && Ext.isNumber(y);
            isValidPoint1 = isValidX1 && Ext.isNumber(y1);
            if (isOrigin) {
                // If only the y-component isn't a valid number,
                // we can 'fix' it by setting it to value of y-origin.
                if (!isValidPoint0 && isValidX0) {
                    y0 = xAxis;
                    isValidPoint0 = true;
                }
                if (!isValidPoint && isValidX) {
                    y = xAxis;
                    isValidPoint = true;
                }
                if (!isValidPoint1 && isValidX1) {
                    y1 = xAxis;
                    isValidPoint1 = true;
                }
            }
            if (renderer) {
                lineConfig.x = x;
                lineConfig.y = y;
                lineConfig.x0 = x0;
                lineConfig.y0 = y0;
                params = [
                    me,
                    lineConfig,
                    me.rendererData,
                    start + i / 3
                ];
                // callback(fn, scope, args, delay, caller)
                rendererChanges = Ext.callback(renderer, null, params, 0, me.getSeries());
            }
            if (isGap && isConnect && isValidPoint0 && lastValidPoint) {
                px = lastValidPoint[0];
                py = lastValidPoint[1];
                if (needMoveTo) {
                    ctx.beginPath();
                    ctx.moveTo(px, py);
                    strip.push(px, py);
                    stripStartX = px;
                    needMoveTo = false;
                }
                if (step) {
                    ctx.lineTo(x0, py);
                    strip.push(x0, py);
                }
                ctx.lineTo(x0, y0);
                strip.push(x0, y0);
                lastValidPoint = [
                    x0,
                    y0
                ];
                isGap = false;
            }
            // Special case where we have an uninterrupted segment, followed
            // by a gap, then a valid point, then another gap. The uninterrupted
            // segment should be connenected with the dot situated between the gaps.
            if (isConnect && lastValidPoint && isValidPoint && !isValidPoint0) {
                x0 = lastValidPoint[0];
                y0 = lastValidPoint[1];
                isValidPoint0 = true;
            }
            // Remember last valid point to connect the gap
            // when the next valid point is encountered.
            if (isValidPoint) {
                lastValidPoint = [
                    x,
                    y
                ];
            }
            if (isValidPoint0 && isValidPoint) {
                if (needMoveTo) {
                    ctx.beginPath();
                    ctx.moveTo(x0, y0);
                    strip.push(x0, y0);
                    stripStartX = x0;
                    needMoveTo = false;
                }
            } else {
                isGap = true;
                
                continue;
            }
            if (step) {
                ctx.lineTo(x, y0);
                strip.push(x, y0);
            }
            ctx.lineTo(x, y);
            strip.push(x, y);
            // If the next point is a gap, then we need to fill what
            // has been already rendered so far. The same applies
            // if the renderer returned some changes to apply to
            // the current step.
            if (rendererChanges || !isValidPoint1) {
                ctx.save();
                Ext.apply(ctx, rendererChanges);
                rendererChanges = null;
                if (attr.fillArea) {
                    ctx.lineTo(x, xAxis);
                    ctx.lineTo(stripStartX, xAxis);
                    ctx.closePath();
                    ctx.fill();
                }
                // Draw the line on top of the filled area.
                ctx.beginPath();
                me.drawStrip(ctx, strip);
                strip = [];
                ctx.stroke();
                ctx.restore();
                ctx.beginPath();
                // Take note that the starting point of a path has been reset
                // (as a result of filling a sub-path) and needs to be set again
                // for the line to continue in a proper manner.
                needMoveTo = true;
            }
        }
    },
    calculateScale: function(count, end) {
        var power = 0,
            n = count;
        while (n < end && count > 0) {
            power++;
            n += count >> power;
        }
        return Math.pow(2, power > 0 ? power - 1 : power);
    },
    drawSmoothStroke: function(surface, ctx, start, end, list, xAxis) {
        var me = this,
            attr = me.attr,
            step = attr.step,
            matrix = attr.matrix,
            renderer = attr.renderer,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            smoothX = me.smoothX,
            smoothY = me.smoothY,
            scale = me.calculateScale(attr.dataX.length, end),
            cx1, cy1, cx2, cy2, x, y, x0, y0, i, j, changes, params,
            lineConfig = {
                type: 'line',
                smooth: true,
                step: step
            };
        ctx.beginPath();
        ctx.moveTo(smoothX[start * 3] * xx + dx, smoothY[start * 3] * yy + dy);
        for (i = 0 , j = start * 3 + 1; i < list.length - 3; i += 3 , j += 3 * scale) {
            cx1 = smoothX[j] * xx + dx;
            cy1 = smoothY[j] * yy + dy;
            cx2 = smoothX[j + 1] * xx + dx;
            cy2 = smoothY[j + 1] * yy + dy;
            x = surface.roundPixel(list[i + 3]);
            y = list[i + 4];
            x0 = surface.roundPixel(list[i]);
            y0 = list[i + 1];
            if (renderer) {
                lineConfig.x0 = x0;
                lineConfig.y0 = y0;
                lineConfig.cx1 = cx1;
                lineConfig.cy1 = cy1;
                lineConfig.cx2 = cx2;
                lineConfig.cy2 = cy2;
                lineConfig.x = x;
                lineConfig.y = y;
                params = [
                    me,
                    lineConfig,
                    me.rendererData,
                    start + i / 3 + 1
                ];
                changes = Ext.callback(renderer, null, params, 0, me.getSeries());
                ctx.save();
                Ext.apply(ctx, changes);
            }
            if (attr.fillArea) {
                ctx.moveTo(x0, y0);
                ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
                ctx.lineTo(x, xAxis);
                ctx.lineTo(x0, xAxis);
                ctx.lineTo(x0, y0);
                ctx.closePath();
                ctx.fill();
                ctx.beginPath();
            }
            // Draw the line on top of the filled area.
            ctx.moveTo(x0, y0);
            ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
            ctx.stroke();
            ctx.moveTo(x0, y0);
            ctx.closePath();
            if (renderer) {
                ctx.restore();
            }
            ctx.beginPath();
            ctx.moveTo(x, y);
        }
        // Prevent the last visible segment from being stroked twice
        // (second time by the ctx.fillStroke inside Path sprite 'render' method)
        ctx.beginPath();
    },
    drawLabel: function(text, dataX, dataY, labelId, rect) {
        var me = this,
            attr = me.attr,
            label = me.getMarker('labels'),
            labelTpl = label.getTemplate(),
            labelCfg = me.labelCfg || (me.labelCfg = {}),
            surfaceMatrix = me.surfaceMatrix,
            labelX, labelY,
            labelOverflowPadding = attr.labelOverflowPadding,
            halfHeight, labelBBox, changes, params, hasPendingChanges;
        // The coordinates below (data point converted to surface coordinates)
        // are just for the renderer to give it a notion of where the label will be positioned.
        // The actual position of the label will be different
        // (unless the renderer returns x/y coordinates in the changes object)
        // and depend on several things including the size of the text,
        // which has to be measured after the renderer call,
        // since text can be modified by the renderer.
        labelCfg.x = surfaceMatrix.x(dataX, dataY);
        labelCfg.y = surfaceMatrix.y(dataX, dataY);
        if (attr.flipXY) {
            labelCfg.rotationRads = Math.PI * 0.5;
        } else {
            labelCfg.rotationRads = 0;
        }
        labelCfg.text = text;
        if (labelTpl.attr.renderer) {
            params = [
                text,
                label,
                labelCfg,
                me.rendererData,
                labelId
            ];
            changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries());
            if (typeof changes === 'string') {
                labelCfg.text = changes;
            } else if (typeof changes === 'object') {
                if ('text' in changes) {
                    labelCfg.text = changes.text;
                }
                hasPendingChanges = true;
            }
        }
        labelBBox = me.getMarkerBBox('labels', labelId, true);
        if (!labelBBox) {
            me.putMarker('labels', labelCfg, labelId);
            labelBBox = me.getMarkerBBox('labels', labelId, true);
        }
        halfHeight = labelBBox.height / 2;
        labelX = dataX;
        switch (labelTpl.attr.display) {
            case 'under':
                labelY = dataY - halfHeight - labelOverflowPadding;
                break;
            case 'rotate':
                labelX += labelOverflowPadding;
                labelY = dataY - labelOverflowPadding;
                labelCfg.rotationRads = -Math.PI / 4;
                break;
            default:
                // 'over'
                labelY = dataY + halfHeight + labelOverflowPadding;
        }
        labelCfg.x = surfaceMatrix.x(labelX, labelY);
        labelCfg.y = surfaceMatrix.y(labelX, labelY);
        if (hasPendingChanges) {
            Ext.apply(labelCfg, changes);
        }
        me.putMarker('labels', labelCfg, labelId);
    },
    drawMarker: function(x, y, index) {
        var me = this,
            attr = me.attr,
            renderer = attr.renderer,
            surfaceMatrix = me.surfaceMatrix,
            markerCfg = {},
            changes, params;
        if (renderer && me.getMarker('markers')) {
            markerCfg.type = 'marker';
            markerCfg.x = x;
            markerCfg.y = y;
            params = [
                me,
                markerCfg,
                me.rendererData,
                index
            ];
            changes = Ext.callback(renderer, null, params, 0, me.getSeries());
            if (changes) {
                Ext.apply(markerCfg, changes);
            }
        }
        markerCfg.translationX = surfaceMatrix.x(x, y);
        markerCfg.translationY = surfaceMatrix.y(x, y);
        delete markerCfg.x;
        delete markerCfg.y;
        me.putMarker('markers', markerCfg, index, !renderer);
    },
    drawStroke: function(surface, ctx, start, end, list, xAxis) {
        var me = this,
            isSmooth = me.smoothX && me.smoothY;
        if (isSmooth) {
            me.drawSmoothStroke(surface, ctx, start, end, list, xAxis);
        } else {
            me.drawStraightStroke(surface, ctx, start, end, list, xAxis);
        }
    },
    renderAggregates: function(aggregates, start, end, surface, ctx, clip, rect) {
        var me = this,
            attr = me.attr,
            dataX = attr.dataX,
            dataY = attr.dataY,
            labels = attr.labels,
            xAxis = attr.xAxis,
            yCap = attr.yCap,
            isSmooth = attr.smooth && me.smoothX && me.smoothY,
            isDrawLabels = labels && me.getMarker('labels'),
            isDrawMarkers = me.getMarker('markers'),
            matrix = attr.matrix,
            pixel = surface.devicePixelRatio,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            list = me.list || (me.list = []),
            minXs = aggregates.minX,
            maxXs = aggregates.maxX,
            minYs = aggregates.minY,
            maxYs = aggregates.maxY,
            idx = aggregates.startIdx,
            isContinuousLine = true,
            isValidMinX, isValidMaxX, isValidMinY, isValidMaxY, xAxisOrigin, isVerticalX, x, y, i, index, minX, maxX, minY, maxY, lastPointX, lastPointY, firstPointX, firstPointY;
        me.rendererData = {
            store: me.getStore()
        };
        list.length = 0;
        // Say we have 7 y-items (attr.dataY): [20, 19, 17, 15, 11, 10, 14]
        //         and 7 x-items (attr.dataX): [0,   1,  2,  3,  4,  5,  6].
        // Then aggregates.startIdx is an aggregated index,
        // where every other item is skipped on each aggregation level:
        // [0, 1, 2, 3, 4, 5, 6,
        //  0, 2, 4, 6,
        //  0, 4,
        //  0]
        // aggregates.minY
        // [20, 19, 17, 15, 11, 10, 14,
        //  19, 15, 10, 14,
        //  15, 10,
        //  10]
        // aggregates.maxY
        // [20, 19, 17, 15, 11, 10, 14,
        //  20, 17, 11, 14,
        //  20, 14,
        //  20]
        // aggregates.minX is
        // [0, 1, 2, 3, 4, 5, 6,
        //  1, 3, 5, 6, // TODO: why this order for min?
        //  3, 5,       // TODO: why this inconsistency?
        //  5]
        // aggregates.maxX is
        // [0, 1, 2, 3, 4, 5, 6,
        //  0, 2, 4, 6,
        //  0, 6,
        //  0]
        // Create a list of the form [x0, y0, idx0, x1, y1, idx1, ...],
        // where each x,y pair is a coordinate representing original data point
        // at the idx position.
        for (i = start; i < end; i++) {
            minX = minXs[i];
            maxX = maxXs[i];
            minY = minYs[i];
            maxY = maxYs[i];
            isValidMinX = Ext.isNumber(minX);
            isValidMinY = Ext.isNumber(minY);
            isValidMaxX = Ext.isNumber(maxX);
            isValidMaxY = Ext.isNumber(maxY);
            if (minX < maxX) {
                list.push(isValidMinX ? (minX * xx + dx) : null, isValidMinY ? (minY * yy + dy) : null, idx[i]);
                list.push(isValidMaxX ? (maxX * xx + dx) : null, isValidMaxY ? (maxY * yy + dy) : null, idx[i]);
            } else if (minX > maxX) {
                list.push(isValidMaxX ? (maxX * xx + dx) : null, isValidMaxY ? (maxY * yy + dy) : null, idx[i]);
                list.push(isValidMinX ? (minX * xx + dx) : null, isValidMinY ? (minY * yy + dy) : null, idx[i]);
            } else {
                list.push(isValidMaxX ? (maxX * xx + dx) : null, isValidMaxY ? (maxY * yy + dy) : null, idx[i]);
            }
        }
        if (list.length) {
            for (i = 0; i < list.length; i += 3) {
                x = list[i];
                y = list[i + 1];
                if (Ext.isNumber(x) && Ext.isNumber(y)) {
                    if (y > yCap) {
                        y = yCap;
                    } else if (y < -yCap) {
                        y = -yCap;
                    }
                    list[i + 1] = y;
                } else {
                    isContinuousLine = false;
                    
                    continue;
                }
                index = list[i + 2];
                if (isDrawMarkers) {
                    me.drawMarker(x, y, index);
                }
                if (isDrawLabels && labels[index]) {
                    me.drawLabel(labels[index], x, y, index, rect);
                }
            }
            me.isContinuousLine = isContinuousLine;
            if (isSmooth && !isContinuousLine) {
                Ext.raise("Line smoothing in only supported for gapless data, " + "where all data points are finite numbers.");
            }
            if (xAxis) {
                isVerticalX = xAxis.getAlignment() === 'vertical';
                if (Ext.isNumber(xAxis.floatingAtCoord)) {
                    xAxisOrigin = (isVerticalX ? rect[2] : rect[3]) - xAxis.floatingAtCoord;
                } else {
                    xAxisOrigin = isVerticalX ? rect[0] : rect[1];
                }
            } else {
                xAxisOrigin = attr.flipXY ? rect[0] : rect[1];
            }
            if (attr.preciseStroke) {
                if (attr.fillArea) {
                    ctx.fill();
                }
                if (attr.transformFillStroke) {
                    attr.inverseMatrix.toContext(ctx);
                }
                me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);
                if (attr.transformFillStroke) {
                    attr.matrix.toContext(ctx);
                }
                ctx.stroke();
            } else {
                me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);
                if (isContinuousLine && isSmooth && attr.fillArea && !attr.renderer) {
                    lastPointX = dataX[dataX.length - 1] * xx + dx + pixel;
                    lastPointY = dataY[dataY.length - 1] * yy + dy;
                    firstPointX = dataX[0] * xx + dx - pixel;
                    firstPointY = dataY[0] * yy + dy;
                    // Fill the area from the series to the xAxis in case there
                    // are no gaps and no renderer is used, in which case the
                    // area would be filled per uninterrupted segment or per
                    // step, instead of being filled a single pass.
                    ctx.lineTo(lastPointX, lastPointY);
                    ctx.lineTo(lastPointX, xAxisOrigin - attr.lineWidth);
                    ctx.lineTo(firstPointX, xAxisOrigin - attr.lineWidth);
                    ctx.lineTo(firstPointX, firstPointY);
                }
                if (attr.transformFillStroke) {
                    attr.matrix.toContext(ctx);
                }
                // Prevent the reverse transform to fix floating point error.
                if (attr.fillArea) {
                    ctx.fillStroke(attr, true);
                } else {
                    ctx.stroke(true);
                }
            }
        }
    }
});

/**
 * @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
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        insetPadding: 40,
 *        store: {
 *            fields: ['name', 'data1', 'data2'],
 *            data: [{
 *                'name': 'metric one',
 *                'data1': 10,
 *                'data2': 14
 *            }, {
 *                'name': 'metric two',
 *                'data1': 7,
 *                'data2': 16
 *            }, {
 *                'name': 'metric three',
 *                'data1': 5,
 *                'data2': 14
 *            }, {
 *                'name': 'metric four',
 *                'data1': 2,
 *                'data2': 6
 *            }, {
 *                'name': 'metric five',
 *                'data1': 27,
 *                'data2': 36
 *            }]
 *        },
 *        axes: [{
 *            type: 'numeric',
 *            position: 'left',
 *            fields: ['data1'],
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            },
 *            grid: true,
 *            minimum: 0
 *        }, {
 *            type: 'category',
 *            position: 'bottom',
 *            fields: ['name'],
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            }
 *        }],
 *        series: [{
 *            type: 'line',
 *            style: {
 *                stroke: '#30BDA7',
 *                lineWidth: 2
 *            },
 *            xField: 'name',
 *            yField: 'data1',
 *            marker: {
 *                type: 'path',
 *                path: ['M', - 4, 0, 0, 4, 4, 0, 0, - 4, 'Z'],
 *                stroke: '#30BDA7',
 *                lineWidth: 2,
 *                fill: 'white'
 *            }
 *        }, {
 *            type: 'line',
 *            fill: true,
 *            style: {
 *                fill: '#96D4C6',
 *                fillOpacity: .6,
 *                stroke: '#0A3F50',
 *                strokeOpacity: .6,
 *            },
 *            xField: 'name',
 *            yField: 'data2',
 *            marker: {
 *                type: 'circle',
 *                radius: 4,
 *                lineWidth: 2,
 *                fill: 'white'
 *            }
 *        }]
 *     });
 *
 * 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` property of the store.
 * Both line series share the same axis, the left axis. You can set particular marker
 * configuration by adding properties onto the marker 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', {
    extend: 'Ext.chart.series.Cartesian',
    alias: 'series.line',
    type: 'line',
    seriesType: 'lineSeries',
    isLine: true,
    requires: [
        'Ext.chart.series.sprite.Line'
    ],
    config: {
        /**
         * @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 {Object} curve
         * The type of curve that connects the data points.
         * Please see {@link Ext.chart.series.sprite.Line#curve line sprite documentation}
         * for the full description.
         */
        curve: {
            type: 'linear'
        },
        /**
         * @cfg {Object} style
         * An object containing styles for the visualization lines. These styles will override
         * the theme styles.
         * Some options contained within the style object will are described next.
         */
        /**
         * @cfg {Boolean} smooth
         * `true` if the series' line should be smoothed.
         * Line smoothing only works with gapless data.
         * @deprecated 6.5.0 Use the {@link #curve} config instead.
         */
        smooth: null,
        /**
         * @cfg {Boolean} step
         * If set to `true`, the line uses steps instead of straight lines to connect the dots.
         * It is ignored if `smooth` is true.
         * @deprecated 6.5.0 Use the {@link #curve} config instead.
         */
        step: null,
        /**
         * @cfg {"gap"/"connect"/"origin"} [nullStyle="gap"]
         * Possible values:
         * 'gap' - null points are rendered as gaps.
         * 'connect' - non-null points are connected across null points, so that
         * there is no gap, unless null points are at the beginning/end of the line.
         * Only the visible data points are connected - if a visible data point
         * is followed by a series of null points that go off screen and eventually
         * terminate with a non-null point, the connection won't be made.
         * 'origin' - null data points are rendered at the origin,
         * which is the y-coordinate of a point where the x and y axes meet.
         * This requires that at least the x-coordinate of a point is a valid value.
         */
        nullStyle: 'gap',
        /**
         * @cfg {Boolean} fill
         * If set to `true`, the area underneath the line is filled with the color defined
         * as follows, listed by priority:
         * - The color that is configured for this series ({@link Ext.chart.series.Series#colors}).
         * - The color that is configured for this chart ({@link Ext.chart.AbstractChart#colors}).
         * - The fill color that is set in the {@link #style} config.
         * - The stroke color that is set in the {@link #style} config, or the same color
         * as the line.
         *
         * Note: Do not confuse `series.config.fill` (which is a boolean) with `series.style.fill'
         * (which is an alias for the `fillStyle` property and contains a color). For compatibility
         * with previous versions of the API, if `config.fill` is undefined but a `style.fill' color
         * is provided, `config.fill` is considered true. So the default value below must be
         * undefined, not false.
         */
        fill: undefined,
        aggregator: {
            strategy: 'double'
        }
    },
    themeMarkerCount: function() {
        return 1;
    },
    /**
     * @private
     * Override {@link Ext.chart.series.Series#getDefaultSpriteConfig}
     */
    getDefaultSpriteConfig: function() {
        var me = this,
            parentConfig = me.callParent(arguments),
            style = Ext.apply({}, me.getStyle()),
            styleWithTheme,
            fillArea = false;
        if (me.config.fill !== undefined) {
            // If config.fill is present but there is no fillStyle, then use the
            // strokeStyle to fill (and paint the area the same color as the line).
            if (me.config.fill) {
                fillArea = true;
                if (style.fillStyle === undefined) {
                    if (style.strokeStyle === undefined) {
                        styleWithTheme = me.getStyleWithTheme();
                        style.fillStyle = styleWithTheme.fillStyle;
                        style.strokeStyle = styleWithTheme.strokeStyle;
                    } else {
                        style.fillStyle = style.strokeStyle;
                    }
                }
            }
        } else {
            // For compatibility with previous versions of the API, if config.fill
            // is undefined but style.fillStyle is provided, we fill the area.
            if (style.fillStyle) {
                fillArea = true;
            }
        }
        // If we don't fill, then delete the fillStyle because that's what is used by
        // the Line sprite to fill below the line.
        if (!fillArea) {
            delete style.fillStyle;
        }
        style = Ext.apply(parentConfig || {}, style);
        return Ext.apply(style, {
            fillArea: fillArea,
            selectionTolerance: me.config.selectionTolerance
        });
    },
    updateFill: function(fill) {
        this.withSprite(function(sprite) {
            return sprite.setAttributes({
                fillArea: fill
            });
        });
    },
    updateCurve: function(curve) {
        this.withSprite(function(sprite) {
            return sprite.setAttributes({
                curve: curve
            });
        });
    },
    getCurve: function() {
        return this.withSprite(function(sprite) {
            return sprite.attr.curve;
        });
    },
    updateNullStyle: function(nullStyle) {
        this.withSprite(function(sprite) {
            return sprite.setAttributes({
                nullStyle: nullStyle
            });
        });
    },
    updateSmooth: function(smooth) {
        this.setCurve({
            type: smooth ? 'natural' : 'linear'
        });
    },
    updateStep: function(step) {
        this.setCurve({
            type: step ? 'step-after' : 'linear'
        });
    }
});

/**
 * @class Ext.chart.series.sprite.PieSlice
 *
 * Pie slice sprite.
 */
Ext.define('Ext.chart.series.sprite.PieSlice', {
    extend: 'Ext.draw.sprite.Sector',
    mixins: {
        markerHolder: 'Ext.chart.MarkerHolder'
    },
    alias: 'sprite.pieslice',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Boolean} [doCallout=true]
                 * 'true' if the pie series uses label callouts.
                 */
                doCallout: 'bool',
                /**
                 * @cfg {String} [label='']
                 * Label associated with the Pie sprite.
                 */
                label: 'string',
                // @deprecated Use series.label.orientation config instead.
                // @since 5.0.1
                rotateLabels: 'bool',
                /**
                 * @cfg {Number} [labelOverflowPadding=10]
                 * Padding around labels to determine overlap.
                 * Any negative number allows the labels to overlap.
                 */
                labelOverflowPadding: 'number',
                renderer: 'default'
            },
            defaults: {
                doCallout: true,
                rotateLabels: true,
                label: '',
                labelOverflowPadding: 10,
                renderer: null
            }
        }
    },
    config: {
        /**
         * @private
         * @cfg {Object} rendererData The object that is passed to the renderer.
         *
         * For instance when the PieSlice sprite is used in a Gauge chart, the object
         * contains the 'store' and 'angleField' properties, and the 'value' as well
         * for that one PieSlice that is used to draw the needle of the Gauge.
         */
        rendererData: null,
        rendererIndex: 0,
        series: null
    },
    setGradientBBox: function(ctx, rect) {
        var me = this,
            attr = me.attr,
            hasGradients = (attr.fillStyle && attr.fillStyle.isGradient) || (attr.strokeStyle && attr.strokeStyle.isGradient);
        if (hasGradients && !attr.constrainGradients) {
            // eslint-disable-next-line vars-on-top, one-var
            var midAngle = me.getMidAngle(),
                margin = attr.margin,
                cx = attr.centerX,
                cy = attr.centerY,
                r = attr.endRho,
                matrix = attr.matrix,
                scaleX = matrix.getScaleX(),
                scaleY = matrix.getScaleY(),
                w = scaleX * r,
                h = scaleY * r,
                bbox = {
                    width: w + w,
                    height: h + h
                };
            if (margin) {
                cx += margin * Math.cos(midAngle);
                cy += margin * Math.sin(midAngle);
            }
            bbox.x = matrix.x(cx, cy) - w;
            bbox.y = matrix.y(cx, cy) - h;
            ctx.setGradientBBox(bbox);
        } else {
            me.callParent([
                ctx,
                rect
            ]);
        }
    },
    render: function(surface, ctx, rect) {
        var me = this,
            attr = me.attr,
            itemCfg = {},
            changes;
        if (attr.renderer) {
            itemCfg = {
                type: 'sector',
                centerX: attr.centerX,
                centerY: attr.centerY,
                margin: attr.margin,
                startAngle: Math.min(attr.startAngle, attr.endAngle),
                endAngle: Math.max(attr.startAngle, attr.endAngle),
                startRho: Math.min(attr.startRho, attr.endRho),
                endRho: Math.max(attr.startRho, attr.endRho)
            };
            changes = Ext.callback(attr.renderer, null, [
                me,
                itemCfg,
                me.getRendererData(),
                me.getRendererIndex()
            ], 0, me.getSeries());
            me.setAttributes(changes);
            me.useAttributes(ctx, rect);
        }
        // Draw the sector
        me.callParent([
            surface,
            ctx,
            rect
        ]);
        // Draw the labels
        if (attr.label && me.getMarker('labels')) {
            me.placeLabel();
        }
    },
    placeLabel: function() {
        var me = this,
            attr = me.attr,
            attributeId = attr.attributeId,
            startAngle = Math.min(attr.startAngle, attr.endAngle),
            endAngle = Math.max(attr.startAngle, attr.endAngle),
            midAngle = (startAngle + endAngle) * 0.5,
            margin = attr.margin,
            centerX = attr.centerX,
            centerY = attr.centerY,
            sinMidAngle = Math.sin(midAngle),
            cosMidAngle = Math.cos(midAngle),
            startRho = Math.min(attr.startRho, attr.endRho) + margin,
            endRho = Math.max(attr.startRho, attr.endRho) + margin,
            midRho = (startRho + endRho) * 0.5,
            surfaceMatrix = me.surfaceMatrix,
            labelCfg = me.labelCfg || (me.labelCfg = {}),
            label = me.getMarker('labels'),
            labelTpl = label.getTemplate(),
            hideLessThan = labelTpl.getHideLessThan(),
            calloutLine = labelTpl.getCalloutLine(),
            labelBox, x, y, changes, params, calloutLineLength;
        if (calloutLine) {
            calloutLineLength = calloutLine.length || 40;
        } else {
            calloutLineLength = 0;
        }
        surfaceMatrix.appendMatrix(attr.matrix);
        labelCfg.text = attr.label;
        x = centerX + cosMidAngle * midRho;
        y = centerY + sinMidAngle * midRho;
        labelCfg.x = surfaceMatrix.x(x, y);
        labelCfg.y = surfaceMatrix.y(x, y);
        x = centerX + cosMidAngle * endRho;
        y = centerY + sinMidAngle * endRho;
        labelCfg.calloutStartX = surfaceMatrix.x(x, y);
        labelCfg.calloutStartY = surfaceMatrix.y(x, y);
        x = centerX + cosMidAngle * (endRho + calloutLineLength);
        y = centerY + sinMidAngle * (endRho + calloutLineLength);
        labelCfg.calloutPlaceX = surfaceMatrix.x(x, y);
        labelCfg.calloutPlaceY = surfaceMatrix.y(x, y);
        if (!attr.rotateLabels) {
            labelCfg.rotationRads = 0;
            //<debug>
            Ext.log.warn("'series.style.rotateLabels' config is deprecated. " + "Use 'series.label.orientation' config instead.");
        } else //</debug>
        {
            switch (labelTpl.attr.orientation) {
                case 'horizontal':
                    labelCfg.rotationRads = midAngle + Math.atan2(surfaceMatrix.y(1, 0) - surfaceMatrix.y(0, 0), surfaceMatrix.x(1, 0) - surfaceMatrix.x(0, 0)) + Math.PI / 2;
                    break;
                case 'vertical':
                    labelCfg.rotationRads = midAngle + Math.atan2(surfaceMatrix.y(1, 0) - surfaceMatrix.y(0, 0), surfaceMatrix.x(1, 0) - surfaceMatrix.x(0, 0));
                    break;
            }
        }
        labelCfg.calloutColor = (calloutLine && calloutLine.color) || me.attr.fillStyle;
        if (calloutLine) {
            if (calloutLine.width) {
                labelCfg.calloutWidth = calloutLine.width;
            }
        } else {
            labelCfg.calloutColor = 'none';
        }
        labelCfg.globalAlpha = attr.globalAlpha * attr.fillOpacity;
        // If a slice is empty, don't display the label.
        // This behavior can be overridden by a renderer.
        if (labelTpl.display !== 'none') {
            // eslint-disable-next-line eqeqeq
            labelCfg.hidden = (attr.startAngle == attr.endAngle);
        }
        if (labelTpl.attr.renderer) {
            // Note: the labels are 'put' by the Ext.chart.series.Pie.updateLabelData, so we can
            // be sure the label sprite instances will exist and can be accessed from the label
            // renderer on first render. For example, with 'bar' series this isn't the case,
            // so we make a check and create a label instance if necessary.
            params = [
                me.attr.label,
                label,
                labelCfg,
                me.getRendererData(),
                me.getRendererIndex()
            ];
            changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries());
            if (typeof changes === 'string') {
                labelCfg.text = changes;
            } else {
                Ext.apply(labelCfg, changes);
            }
        }
        me.putMarker('labels', labelCfg, attributeId);
        labelBox = me.getMarkerBBox('labels', attributeId, true);
        if (labelBox) {
            if (attr.doCallout && ((endAngle - startAngle) * endRho > hideLessThan || attr.highlighted)) {
                if (labelTpl.attr.display === 'outside') {
                    me.putMarker('labels', {
                        callout: 1
                    }, attributeId);
                } else if (labelTpl.attr.display === 'inside') {
                    me.putMarker('labels', {
                        callout: 0
                    }, attributeId);
                } else {
                    me.putMarker('labels', {
                        callout: 1 - me.sliceContainsLabel(attr, labelBox)
                    }, attributeId);
                }
            } else {
                me.putMarker('labels', {
                    globalAlpha: me.sliceContainsLabel(attr, labelBox)
                }, attributeId);
            }
        }
    },
    sliceContainsLabel: function(attr, bbox) {
        var padding = attr.labelOverflowPadding,
            middle = (attr.endRho + attr.startRho) / 2,
            outer = middle + (bbox.width + padding) / 2,
            inner = middle - (bbox.width + padding) / 2,
            sliceAngle, l1, l2, l3;
        if (padding < 0) {
            return 1;
        }
        if (bbox.width + padding * 2 > (attr.endRho - attr.startRho)) {
            return 0;
        }
        l1 = Math.sqrt(attr.endRho * attr.endRho - outer * outer);
        l2 = Math.sqrt(attr.endRho * attr.endRho - inner * inner);
        sliceAngle = Math.abs(attr.endAngle - attr.startAngle);
        l3 = (sliceAngle > Math.PI / 2 ? inner : Math.abs(Math.tan(sliceAngle / 2)) * inner);
        if (bbox.height + padding * 2 > Math.min(l1, l2, l3) * 2) {
            return 0;
        }
        return 1;
    }
});

/**
 * @class Ext.chart.series.Pie
 * @extends Ext.chart.series.Polar
 *
 * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display
 * quantitative information for different categories that also have a meaning as a whole.
 * As with all other series, the Pie Series must be appended in the *series* Chart array
 * configuration. See the Chart documentation for more information. A typical configuration
 * object for the pie series could be:
 *
 *     @example
 *     Ext.create({
 *        xtype: 'polar',
 *        renderTo: document.body,
 *        width: 400,
 *        height: 400,
 *        theme: 'green',
 *        interactions: ['rotate', 'itemhighlight'],
 *        store: {
 *            fields: ['name', 'data1'],
 *            data: [{
 *                name: 'metric one',
 *                data1: 14
 *            }, {
 *                name: 'metric two',
 *                data1: 16
 *            }, {
 *                name: 'metric three',
 *                data1: 14
 *            }, {
 *                name: 'metric four',
 *                data1: 6
 *            }, {
 *                name: 'metric five',
 *                data1: 36
 *            }]
 *        },
 *        series: {
 *            type: 'pie',
 *            highlight: true,
 *            angleField: 'data1',
 *            label: {
 *                field: 'name',
 *                display: 'rotate'
 *            },
 *            donut: 30
 *        }
 *     });
 *
 * In this configuration we set `pie` as the type for the series, then set the `highlight` config
 * to `true` (we can also specify an object with specific style properties for highlighting options)
 * which is triggered when hovering or tapping elements.
 * We set `data1` as the value of the `angleField` to determine the angular span for each pie slice.
 * We also set a label configuration object where we set the name of the store field
 * to be rendered as text for the label. The labels will also be displayed rotated.
 * And finally, we specify the donut hole radius for the pie series in percentages of the series
 * radius.
 *
 */
Ext.define('Ext.chart.series.Pie', {
    extend: 'Ext.chart.series.Polar',
    requires: [
        'Ext.chart.series.sprite.PieSlice'
    ],
    type: 'pie',
    alias: 'series.pie',
    seriesType: 'pieslice',
    isPie: true,
    config: {
        /**
         * @cfg {String} radiusField
         * The store record field name to be used for the pie slice lengths.
         * The values bound to this field name must be positive real numbers.
         */
        /**
         * @cfg {Number} donut Specifies the radius of the donut hole, as a percentage
         * of the chart's radius.
         * Defaults to 0 (no donut hole).
         */
        donut: 0,
        /**
         * @cfg {Number} rotation The starting angle of the pie slices.
         */
        rotation: 0,
        /**
         * @cfg {Boolean} clockwise
         * Whether the pie slices are displayed clockwise. Default's true.
         */
        clockwise: true,
        /**
         * @cfg {Number} [totalAngle=2*PI] The total angle of the pie series.
         */
        totalAngle: 2 * Math.PI,
        /**
         * @cfg {Array} hidden Determines which pie slices are hidden.
         */
        hidden: [],
        /**
         * @cfg {Number} [radiusFactor=100] Allows adjustment of the radius
         * by a specific percentage.
         */
        radiusFactor: 100,
        /**
         * @cfg {Ext.chart.series.sprite.PieSlice/Object} highlightCfg
         * Default highlight config for the pie series.
         * Slides highlighted pie sector outward by default.
         * 
         * highlightCfg accepts as its value a config object (or array of configs) for a 
         * {@link Ext.chart.series.sprite.PieSlice pie sprite}.
         * 
         * 
         * Example config:
         * 
         *     Ext.create('Ext.chart.PolarChart', {
         *         renderTo: document.body,
         *         width: 600,
         *         height: 400,
         *         innerPadding: 5,
         *         store: {
         *             fields: ['name', 'data1'],
         *             data: [{
         *                 name: 'metric one',
         *                 data1: 10
         *             }, {
         *                 name: 'metric two',
         *                 data1: 7
         *             }, {
         *                 name: 'metric three',
         *                 data1: 5
         *             }]
         *         },
         *         series: {
         *             type: 'pie',
         *             label: {
         *                 field: 'name',
         *                 display: 'rotate'
         *             },
         *             xField: 'data1',
         *             donut: 30,
         *             highlightCfg: {
         *                 margin: 10,
         *                 fillOpacity: .7
         *             }
         *         }
         *     });
         */
        highlightCfg: {
            margin: 20
        },
        style: {}
    },
    directions: [
        'X'
    ],
    applyLabel: function(newLabel, oldLabel) {
        if (Ext.isObject(newLabel) && !Ext.isString(newLabel.orientation)) {
            // Override default label orientation from '' to 'vertical'.
            Ext.apply(newLabel = Ext.Object.chain(newLabel), {
                orientation: 'vertical'
            });
        }
        return this.callParent([
            newLabel,
            oldLabel
        ]);
    },
    updateLabelData: function() {
        var me = this,
            store = me.getStore(),
            items = store.getData().items,
            sprites = me.getSprites(),
            label = me.getLabel(),
            labelField = label && label.getTemplate().getField(),
            hidden = me.getHidden(),
            i, ln, labels, sprite;
        if (sprites.length && labelField) {
            labels = [];
            for (i = 0 , ln = items.length; i < ln; i++) {
                labels.push(items[i].get(labelField));
            }
            for (i = 0 , ln = sprites.length; i < ln; i++) {
                sprite = sprites[i];
                sprite.setAttributes({
                    label: labels[i]
                });
                sprite.putMarker('labels', {
                    hidden: hidden[i]
                }, sprite.attr.attributeId);
            }
        }
    },
    coordinateX: function() {
        var me = this,
            store = me.getStore(),
            records = store.getData().items,
            recordCount = records.length,
            xField = me.getXField(),
            yField = me.getYField(),
            x,
            sumX = 0,
            unit, y,
            maxY = 0,
            hidden = me.getHidden(),
            summation = [],
            i,
            lastAngle = 0,
            totalAngle = me.getTotalAngle(),
            clockwise = me.getClockwise() ? 1 : -1,
            sprites = me.getSprites(),
            sprite, labels;
        if (!sprites) {
            return;
        }
        for (i = 0; i < recordCount; i++) {
            x = Math.abs(Number(records[i].get(xField))) || 0;
            y = yField && Math.abs(Number(records[i].get(yField))) || 0;
            if (!hidden[i]) {
                sumX += x;
                if (y > maxY) {
                    maxY = y;
                }
            }
            summation[i] = sumX;
            if (i >= hidden.length) {
                hidden[i] = false;
            }
        }
        hidden.length = recordCount;
        me.maxY = maxY;
        if (sumX !== 0) {
            unit = totalAngle / sumX;
        }
        for (i = 0; i < recordCount; i++) {
            sprites[i].setAttributes({
                startAngle: lastAngle,
                endAngle: lastAngle = (unit ? clockwise * summation[i] * unit : 0),
                globalAlpha: 1
            });
        }
        if (recordCount < sprites.length) {
            for (i = recordCount; i < sprites.length; i++) {
                sprite = sprites[i];
                labels = sprite.getMarker('labels');
                if (labels) {
                    // Don't want the 'labels' Markers and its 'template' sprite to be destroyed
                    // with the PieSlice MarkerHolder, as it is also used by other pie slices.
                    // So we release 'labels' before destroying the PieSlice.
                    // But first, we have to clear the instances of the 'labels'
                    // Markers created by the PieSlice MarkerHolder.
                    labels.clear(sprite.getId());
                    sprite.releaseMarker('labels');
                }
                sprite.destroy();
            }
            sprites.length = recordCount;
        }
        for (i = recordCount; i < sprites.length; i++) {
            sprites[i].setAttributes({
                startAngle: totalAngle,
                endAngle: totalAngle,
                globalAlpha: 0
            });
        }
    },
    updateCenter: function(center) {
        this.setStyle({
            translationX: center[0] + this.getOffsetX(),
            translationY: center[1] + this.getOffsetY()
        });
        this.doUpdateStyles();
    },
    updateRadius: function(radius) {
        this.setStyle({
            startRho: radius * this.getDonut() * 0.01,
            endRho: radius * this.getRadiusFactor() * 0.01
        });
        this.doUpdateStyles();
    },
    getStyleByIndex: function(i) {
        var me = this,
            store = me.getStore(),
            item = store.getAt(i),
            yField = me.getYField(),
            radius = me.getRadius(),
            style = {},
            startRho, endRho, y;
        if (item) {
            y = yField && Math.abs(Number(item.get(yField))) || 0;
            startRho = radius * me.getDonut() * 0.01;
            endRho = radius * me.getRadiusFactor() * 0.01;
            style = me.callParent([
                i
            ]);
            style.startRho = startRho;
            style.endRho = me.maxY ? (startRho + (endRho - startRho) * y / me.maxY) : endRho;
        }
        return style;
    },
    updateDonut: function(donut) {
        var radius = this.getRadius();
        this.setStyle({
            startRho: radius * donut * 0.01,
            endRho: radius * this.getRadiusFactor() * 0.01
        });
        this.doUpdateStyles();
    },
    // Subtract 90 degrees from rotation, so that `rotation` config's default
    // value of 0 makes first pie sector start at noon, rather than 3 o'clock.
    rotationOffset: -Math.PI / 2,
    updateRotation: function(rotation) {
        this.setStyle({
            rotationRads: rotation + this.rotationOffset
        });
        this.doUpdateStyles();
    },
    updateTotalAngle: function(totalAngle) {
        this.processData();
    },
    getSprites: function() {
        var me = this,
            chart = me.getChart(),
            store = me.getStore();
        if (!chart || !store) {
            return Ext.emptyArray;
        }
        me.getColors();
        me.getSubStyle();
        // eslint-disable-next-line vars-on-top, one-var
        var items = store.getData().items,
            length = items.length,
            animation = me.getAnimation() || chart && chart.getAnimation(),
            sprites = me.sprites,
            sprite,
            spriteCreated = false,
            spriteIndex = 0,
            label = me.getLabel(),
            labelTpl = label && label.getTemplate(),
            i, rendererData;
        rendererData = {
            store: store,
            field: me.getXField(),
            // for backward compatibility only (deprecated in 5.5)
            angleField: me.getXField(),
            radiusField: me.getYField(),
            series: me
        };
        for (i = 0; i < length; i++) {
            sprite = sprites[i];
            if (!sprite) {
                sprite = me.createSprite();
                if (me.getHighlight()) {
                    sprite.config.highlight = me.getHighlight();
                    sprite.addModifier('highlight', true);
                }
                if (labelTpl && labelTpl.getField()) {
                    labelTpl.setAttributes({
                        labelOverflowPadding: me.getLabelOverflowPadding()
                    });
                    labelTpl.getAnimation().setCustomDurations({
                        'callout': 200
                    });
                }
                sprite.setAttributes(me.getStyleByIndex(i));
                sprite.setRendererData(rendererData);
                spriteCreated = true;
            }
            sprite.setRendererIndex(spriteIndex++);
            sprite.setAnimation(animation);
        }
        if (spriteCreated) {
            me.doUpdateStyles();
        }
        return me.sprites;
    },
    betweenAngle: function(x, a, b) {
        var pp = Math.PI * 2,
            offset = this.rotationOffset;
        if (a === b) {
            return false;
        }
        if (!this.getClockwise()) {
            x *= -1;
            a *= -1;
            b *= -1;
            a -= offset;
            b -= offset;
        } else {
            a += offset;
            b += offset;
        }
        x -= a;
        b -= a;
        // Normalize, so that both x and b are in the [0,360) interval.
        x %= pp;
        b %= pp;
        x += pp;
        b += pp;
        x %= pp;
        b %= pp;
        // Because 360 * n angles will be normalized to 0,
        // we need to treat b ~= 0 as a special case.
        return x < b || Ext.Number.isEqual(b, 0, 1.0E-8);
    },
    getItemByIndex: function(index, category) {
        category = category || 'sprites';
        return this.callParent([
            index,
            category
        ]);
    },
    /**
     * Returns the pie slice for a given angle
     * @param {Number} angle The angle to search for the slice
     * @return {Object} An object containing the reocord, sprite, scope etc.
     */
    getItemForAngle: function(angle) {
        var me = this,
            sprites = me.getSprites(),
            attr, store, items, hidden, i, ln;
        angle %= Math.PI * 2;
        while (angle < 0) {
            angle += Math.PI * 2;
        }
        if (sprites) {
            store = me.getStore();
            items = store.getData().items;
            hidden = me.getHidden();
            for (i = 0 , ln = store.getCount(); i < ln; i++) {
                if (!hidden[i]) {
                    // Fortunately, item's id equals its index in the instances list.
                    attr = sprites[i].attr;
                    if (attr.startAngle <= angle && attr.endAngle >= angle) {
                        return {
                            series: me,
                            sprite: sprites[i],
                            index: i,
                            record: items[i],
                            field: me.getXField()
                        };
                    }
                }
            }
        }
        return null;
    },
    getItemForPoint: function(x, y) {
        var me = this,
            sprites = me.getSprites(),
            center = me.getCenter(),
            offsetX = me.getOffsetX(),
            offsetY = me.getOffsetY(),
            // Distance from the center of the series to the cursor.
            dx = x - center[0] + offsetX,
            dy = y - center[1] + offsetY,
            store = me.getStore(),
            donut = me.getDonut(),
            records = store.getData().items,
            direction = Math.atan2(dy, dx) - me.getRotation(),
            radius = Math.sqrt(dx * dx + dy * dy),
            startRadius = me.getRadius() * donut * 0.01,
            hidden = me.getHidden(),
            result = null,
            i, ln, attr, sprite;
        for (i = 0 , ln = records.length; i < ln; i++) {
            if (hidden[i]) {
                
                continue;
            }
            sprite = sprites[i];
            if (!sprite) {
                break;
            }
            attr = sprite.attr;
            if (radius >= startRadius + attr.margin && radius <= attr.endRho + attr.margin && me.betweenAngle(direction, attr.startAngle, attr.endAngle)) {
                result = {
                    series: me,
                    sprite: sprites[i],
                    index: i,
                    record: records[i],
                    field: me.getXField()
                };
                break;
            }
        }
        return result;
    },
    provideLegendInfo: function(target) {
        var me = this,
            store = me.getStore(),
            items, labelField, xField, hidden, i, style, fill;
        if (store) {
            items = store.getData().items;
            labelField = me.getLabel().getTemplate().getField();
            xField = me.getXField();
            hidden = me.getHidden();
            for (i = 0; i < items.length; i++) {
                style = me.getStyleByIndex(i);
                fill = style.fillStyle;
                if (Ext.isObject(fill)) {
                    fill = fill.stops && fill.stops[0].color;
                }
                target.push({
                    name: labelField ? String(items[i].get(labelField)) : xField + ' ' + i,
                    mark: fill || style.strokeStyle || 'black',
                    disabled: hidden[i],
                    series: me.getId(),
                    index: i
                });
            }
        }
    }
});

/**
 * @class Ext.chart.series.sprite.Pie3DPart
 * @extends Ext.draw.sprite.Path
 *
 * Pie3D series sprite.
 */
Ext.define('Ext.chart.series.sprite.Pie3DPart', {
    extend: 'Ext.draw.sprite.Path',
    mixins: {
        markerHolder: 'Ext.chart.MarkerHolder'
    },
    alias: 'sprite.pie3dPart',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [centerX=0]
                 * The central point of the series on the x-axis.
                 */
                centerX: 'number',
                /**
                 * @cfg {Number} [centerY=0]
                 * The central point of the series on the x-axis.
                 */
                centerY: 'number',
                /**
                 * @cfg {Number} [startAngle=0]
                 * The starting angle of the polar series.
                 */
                startAngle: 'number',
                /**
                 * @cfg {Number} [endAngle=Math.PI]
                 * The ending angle of the polar series.
                 */
                endAngle: 'number',
                /**
                 * @cfg {Number} [startRho=0]
                 * The starting radius of the polar series.
                 */
                startRho: 'number',
                /**
                 * @cfg {Number} [endRho=150]
                 * The ending radius of the polar series.
                 */
                endRho: 'number',
                /**
                 * @cfg {Number} [margin=0]
                 * Margin from the center of the pie. Used for donut.
                 */
                margin: 'number',
                /**
                 * @cfg {Number} [thickness=0]
                 * The thickness of the 3D pie part.
                 */
                thickness: 'number',
                /**
                 * @cfg {Number} [bevelWidth=5]
                 * The size of the 3D pie bevel.
                 */
                bevelWidth: 'number',
                /**
                 * @cfg {Number} [distortion=0]
                 * The distortion of the 3D pie part.
                 */
                distortion: 'number',
                /**
                 * @cfg {Object} [baseColor='white']
                 * The color of the 3D pie part before adding the 3D effect.
                 */
                baseColor: 'color',
                /**
                 * @cfg {Number} [colorSpread=0.7]
                 * An attribute used to control how flat the gradient of the sprite looks.
                 * A value of 0 essentially means no gradient (flat color).
                 */
                colorSpread: 'number',
                /**
                 * @cfg {Number} [baseRotation=0]
                 * The starting rotation of the polar series.
                 */
                baseRotation: 'number',
                /**
                 * @cfg {String} [part='top']
                 * The part of the 3D Pie represented by the sprite.
                 */
                part: 'enums(top,bottom,start,end,innerFront,innerBack,outerFront,outerBack)',
                /**
                 * @cfg {String} [label='']
                 * The label associated with the 'top' part of the sprite.
                 */
                label: 'string'
            },
            aliases: {
                rho: 'endRho'
            },
            triggers: {
                centerX: 'path,bbox',
                centerY: 'path,bbox',
                startAngle: 'path,partZIndex',
                endAngle: 'path,partZIndex',
                startRho: 'path',
                endRho: 'path,bbox',
                margin: 'path,bbox',
                thickness: 'path',
                distortion: 'path',
                baseRotation: 'path,partZIndex',
                baseColor: 'partZIndex,partColor',
                colorSpread: 'partColor',
                part: 'path,partZIndex',
                globalAlpha: 'canvas,alpha',
                fillOpacity: 'canvas,alpha'
            },
            defaults: {
                centerX: 0,
                centerY: 0,
                startAngle: Math.PI * 2,
                endAngle: Math.PI * 2,
                startRho: 0,
                endRho: 150,
                margin: 0,
                thickness: 35,
                distortion: 0.5,
                baseRotation: 0,
                baseColor: 'white',
                colorSpread: 0.5,
                miterLimit: 1,
                bevelWidth: 5,
                strokeOpacity: 0,
                part: 'top',
                label: ''
            },
            updaters: {
                alpha: 'alphaUpdater',
                partColor: 'partColorUpdater',
                partZIndex: 'partZIndexUpdater'
            }
        }
    },
    config: {
        renderer: null,
        rendererData: null,
        rendererIndex: 0,
        series: null
    },
    bevelParams: [],
    constructor: function(config) {
        this.callParent([
            config
        ]);
        this.bevelGradient = new Ext.draw.gradient.Linear({
            stops: [
                {
                    offset: 0,
                    color: 'rgba(255,255,255,0)'
                },
                {
                    offset: 0.7,
                    color: 'rgba(255,255,255,0.6)'
                },
                {
                    offset: 1,
                    color: 'rgba(255,255,255,0)'
                }
            ]
        });
    },
    updateRenderer: function() {
        this.setDirty(true);
    },
    updateRendererData: function() {
        this.setDirty(true);
    },
    updateRendererIndex: function() {
        this.setDirty(true);
    },
    alphaUpdater: function(attr) {
        var me = this,
            opacity = attr.globalAlpha,
            fillOpacity = attr.fillOpacity,
            oldOpacity = me.oldOpacity,
            oldFillOpacity = me.oldFillOpacity;
        // Update the path when the sprite becomes translucent or completely opaque.
        if ((opacity !== oldOpacity && (opacity === 1 || oldOpacity === 1)) || (fillOpacity !== oldFillOpacity && (fillOpacity === 1 || oldFillOpacity === 1))) {
            me.scheduleUpdater(attr, 'path', [
                'globalAlpha'
            ]);
            me.oldOpacity = opacity;
            me.oldFillOpacity = fillOpacity;
        }
    },
    partColorUpdater: function(attr) {
        var color = Ext.util.Color.fly(attr.baseColor),
            colorString = color.toString(),
            colorSpread = attr.colorSpread,
            fillStyle;
        switch (attr.part) {
            case 'top':
                fillStyle = new Ext.draw.gradient.Radial({
                    start: {
                        x: 0,
                        y: 0,
                        r: 0
                    },
                    end: {
                        x: 0,
                        y: 0,
                        r: 1
                    },
                    stops: [
                        {
                            offset: 0,
                            color: color.createLighter(0.1 * colorSpread)
                        },
                        {
                            offset: 1,
                            color: color.createDarker(0.1 * colorSpread)
                        }
                    ]
                });
                break;
            case 'bottom':
                fillStyle = new Ext.draw.gradient.Radial({
                    start: {
                        x: 0,
                        y: 0,
                        r: 0
                    },
                    end: {
                        x: 0,
                        y: 0,
                        r: 1
                    },
                    stops: [
                        {
                            offset: 0,
                            color: color.createDarker(0.2 * colorSpread)
                        },
                        {
                            offset: 1,
                            color: color.toString()
                        }
                    ]
                });
                break;
            case 'outerFront':
            case 'outerBack':
                fillStyle = new Ext.draw.gradient.Linear({
                    stops: [
                        {
                            offset: 0,
                            color: color.createDarker(0.15 * colorSpread).toString()
                        },
                        {
                            offset: 0.3,
                            color: colorString
                        },
                        {
                            offset: 0.8,
                            color: color.createLighter(0.2 * colorSpread).toString()
                        },
                        {
                            offset: 1,
                            color: color.createDarker(0.25 * colorSpread).toString()
                        }
                    ]
                });
                break;
            case 'start':
                fillStyle = new Ext.draw.gradient.Linear({
                    stops: [
                        {
                            offset: 0,
                            color: color.createDarker(0.1 * colorSpread).toString()
                        },
                        {
                            offset: 1,
                            color: color.createLighter(0.2 * colorSpread).toString()
                        }
                    ]
                });
                break;
            case 'end':
                fillStyle = new Ext.draw.gradient.Linear({
                    stops: [
                        {
                            offset: 0,
                            color: color.createDarker(0.1 * colorSpread).toString()
                        },
                        {
                            offset: 1,
                            color: color.createLighter(0.2 * colorSpread).toString()
                        }
                    ]
                });
                break;
            case 'innerFront':
            case 'innerBack':
                fillStyle = new Ext.draw.gradient.Linear({
                    stops: [
                        {
                            offset: 0,
                            color: color.createDarker(0.1 * colorSpread).toString()
                        },
                        {
                            offset: 0.2,
                            color: color.createLighter(0.2 * colorSpread).toString()
                        },
                        {
                            offset: 0.7,
                            color: colorString
                        },
                        {
                            offset: 1,
                            color: color.createDarker(0.1 * colorSpread).toString()
                        }
                    ]
                });
                break;
        }
        attr.fillStyle = fillStyle;
        attr.canvasAttributes.fillStyle = fillStyle;
    },
    partZIndexUpdater: function(attr) {
        var normalize = Ext.draw.sprite.AttributeParser.angle,
            rotation = attr.baseRotation,
            startAngle = attr.startAngle,
            endAngle = attr.endAngle,
            depth;
        switch (attr.part) {
            case 'top':
                attr.zIndex = 6;
                break;
            case 'outerFront':
                startAngle = normalize(startAngle + rotation);
                endAngle = normalize(endAngle + rotation);
                if (startAngle >= 0 && endAngle < 0) {
                    depth = Math.sin(startAngle);
                } else if (startAngle <= 0 && endAngle > 0) {
                    depth = Math.sin(endAngle);
                } else if (startAngle >= 0 && endAngle > 0) {
                    if (startAngle > endAngle) {
                        depth = 0;
                    } else {
                        depth = Math.max(Math.sin(startAngle), Math.sin(endAngle));
                    }
                } else {
                    depth = 1;
                };
                attr.zIndex = 4 + depth;
                break;
            case 'outerBack':
                attr.zIndex = 1;
                break;
            case 'start':
                attr.zIndex = 4 + Math.sin(normalize(startAngle + rotation));
                break;
            case 'end':
                attr.zIndex = 4 + Math.sin(normalize(endAngle + rotation));
                break;
            case 'innerFront':
                attr.zIndex = 2;
                break;
            case 'innerBack':
                attr.zIndex = 4 + Math.sin(normalize((startAngle + endAngle) / 2 + rotation));
                break;
            case 'bottom':
                attr.zIndex = 0;
                break;
        }
        attr.dirtyZIndex = true;
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr,
            part = attr.part,
            baseRotation = attr.baseRotation,
            centerX = attr.centerX,
            centerY = attr.centerY,
            rho, angle, x, y, sin, cos;
        if (part === 'start') {
            angle = attr.startAngle + baseRotation;
        } else if (part === 'end') {
            angle = attr.endAngle + baseRotation;
        }
        if (Ext.isNumber(angle)) {
            sin = Math.sin(angle);
            cos = Math.cos(angle);
            x = Math.min(centerX + cos * attr.startRho, centerX + cos * attr.endRho);
            y = centerY + sin * attr.startRho * attr.distortion;
            plain.x = x;
            plain.y = y;
            plain.width = cos * (attr.endRho - attr.startRho);
            plain.height = attr.thickness + sin * (attr.endRho - attr.startRho) * 2;
            return;
        }
        if (part === 'innerFront' || part === 'innerBack') {
            rho = attr.startRho;
        } else {
            rho = attr.endRho;
        }
        plain.width = rho * 2;
        plain.height = rho * attr.distortion * 2 + attr.thickness;
        plain.x = attr.centerX - rho;
        plain.y = attr.centerY - rho * attr.distortion;
    },
    updateTransformedBBox: function(transform) {
        if (this.attr.part === 'start' || this.attr.part === 'end') {
            return this.callParent(arguments);
        }
        return this.updatePlainBBox(transform);
    },
    updatePath: function(path) {
        if (!this.attr.globalAlpha) {
            return;
        }
        if (this.attr.endAngle < this.attr.startAngle) {
            return;
        }
        this[this.attr.part + 'Renderer'](path);
    },
    render: function(surface, ctx, rect) {
        var me = this,
            renderer = me.getRenderer(),
            attr = me.attr,
            part = attr.part,
            itemCfg, changes;
        if (!attr.globalAlpha || Ext.Number.isEqual(attr.startAngle, attr.endAngle, 1.0E-8)) {
            return;
        }
        if (renderer) {
            itemCfg = {
                type: 'pie3dPart',
                part: attr.part,
                margin: attr.margin,
                distortion: attr.distortion,
                centerX: attr.centerX,
                centerY: attr.centerY,
                baseRotation: attr.baseRotation,
                startAngle: attr.startAngle,
                endAngle: attr.endAngle,
                startRho: attr.startRho,
                endRho: attr.endRho
            };
            changes = Ext.callback(renderer, null, [
                me,
                itemCfg,
                me.getRendererData(),
                me.getRendererIndex()
            ], 0, me.getSeries());
            if (changes) {
                if (changes.part) {
                    // Can't let users change the nature of the sprite.
                    changes.part = part;
                }
                me.setAttributes(changes);
                me.useAttributes(ctx, rect);
            }
        }
        me.callParent([
            surface,
            ctx
        ]);
        me.bevelRenderer(surface, ctx);
        // Only the top part will have the label attribute (set by the series).
        if (attr.label && me.getMarker('labels')) {
            me.placeLabel();
        }
    },
    placeLabel: function() {
        var me = this,
            attr = me.attr,
            attributeId = attr.attributeId,
            margin = attr.margin,
            distortion = attr.distortion,
            centerX = attr.centerX,
            centerY = attr.centerY,
            baseRotation = attr.baseRotation,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            midAngle = (startAngle + endAngle) / 2,
            startRho = attr.startRho + margin,
            endRho = attr.endRho + margin,
            midRho = (startRho + endRho) / 2,
            sin = Math.sin(midAngle),
            cos = Math.cos(midAngle),
            surfaceMatrix = me.surfaceMatrix,
            label = me.getMarker('labels'),
            labelTpl = label.getTemplate(),
            calloutLine = labelTpl.getCalloutLine(),
            calloutLineLength = calloutLine && calloutLine.length || 40,
            labelCfg = {},
            rendererParams, rendererChanges, x, y;
        surfaceMatrix.appendMatrix(attr.matrix);
        labelCfg.text = attr.label;
        x = centerX + cos * midRho;
        y = centerY + sin * midRho * distortion;
        labelCfg.x = surfaceMatrix.x(x, y);
        labelCfg.y = surfaceMatrix.y(x, y);
        x = centerX + cos * endRho;
        y = centerY + sin * endRho * distortion;
        labelCfg.calloutStartX = surfaceMatrix.x(x, y);
        labelCfg.calloutStartY = surfaceMatrix.y(x, y);
        x = centerX + cos * (endRho + calloutLineLength);
        y = centerY + sin * (endRho + calloutLineLength) * distortion;
        labelCfg.calloutPlaceX = surfaceMatrix.x(x, y);
        labelCfg.calloutPlaceY = surfaceMatrix.y(x, y);
        labelCfg.calloutWidth = 2;
        if (labelTpl.attr.renderer) {
            rendererParams = [
                me.attr.label,
                label,
                labelCfg,
                me.getRendererData(),
                me.getRendererIndex()
            ];
            rendererChanges = Ext.callback(labelTpl.attr.renderer, null, rendererParams, 0, me.getSeries());
            if (typeof rendererChanges === 'string') {
                labelCfg.text = rendererChanges;
            } else {
                Ext.apply(labelCfg, rendererChanges);
            }
        }
        me.putMarker('labels', labelCfg, attributeId);
        me.putMarker('labels', {
            callout: 1
        }, attributeId);
    },
    bevelRenderer: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            bevelWidth = attr.bevelWidth,
            params = me.bevelParams,
            i;
        for (i = 0; i < params.length; i++) {
            ctx.beginPath();
            ctx.ellipse.apply(ctx, params[i]);
            ctx.save();
            ctx.lineWidth = bevelWidth;
            ctx.strokeOpacity = bevelWidth ? 1 : 0;
            ctx.strokeGradient = me.bevelGradient;
            ctx.stroke(attr);
            ctx.restore();
        }
    },
    lidRenderer: function(path, thickness) {
        var attr = this.attr,
            margin = attr.margin,
            distortion = attr.distortion,
            centerX = attr.centerX,
            centerY = attr.centerY,
            baseRotation = attr.baseRotation,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            midAngle = (startAngle + endAngle) / 2,
            startRho = attr.startRho,
            endRho = attr.endRho,
            sinEnd = Math.sin(endAngle),
            cosEnd = Math.cos(endAngle);
        centerX += Math.cos(midAngle) * margin;
        centerY += Math.sin(midAngle) * margin * distortion;
        path.ellipse(centerX, centerY + thickness, startRho, startRho * distortion, 0, startAngle, endAngle, false);
        path.lineTo(centerX + cosEnd * endRho, centerY + thickness + sinEnd * endRho * distortion);
        path.ellipse(centerX, centerY + thickness, endRho, endRho * distortion, 0, endAngle, startAngle, true);
        path.closePath();
    },
    topRenderer: function(path) {
        this.lidRenderer(path, 0);
    },
    bottomRenderer: function(path) {
        var attr = this.attr,
            none = Ext.util.Color.RGBA_NONE;
        if (attr.globalAlpha < 1 || attr.fillOpacity < 1 || attr.shadowColor !== none) {
            this.lidRenderer(path, attr.thickness);
        }
    },
    sideRenderer: function(path, position) {
        var attr = this.attr,
            margin = attr.margin,
            centerX = attr.centerX,
            centerY = attr.centerY,
            distortion = attr.distortion,
            baseRotation = attr.baseRotation,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            // eslint-disable-next-line max-len
            isFullPie = (!attr.startAngle && Ext.Number.isEqual(Math.PI * 2, attr.endAngle, 1.0E-7)),
            thickness = attr.thickness,
            startRho = attr.startRho,
            endRho = attr.endRho,
            angle = (position === 'start' && startAngle) || (position === 'end' && endAngle),
            sin = Math.sin(angle),
            cos = Math.cos(angle),
            isTranslucent = attr.globalAlpha < 1,
            isVisible = position === 'start' && cos < 0 || position === 'end' && cos > 0 || isTranslucent,
            midAngle;
        if (isVisible && !isFullPie) {
            midAngle = (startAngle + endAngle) / 2;
            centerX += Math.cos(midAngle) * margin;
            centerY += Math.sin(midAngle) * margin * distortion;
            path.moveTo(centerX + cos * startRho, centerY + sin * startRho * distortion);
            path.lineTo(centerX + cos * endRho, centerY + sin * endRho * distortion);
            path.lineTo(centerX + cos * endRho, centerY + sin * endRho * distortion + thickness);
            path.lineTo(centerX + cos * startRho, centerY + sin * startRho * distortion + thickness);
            path.closePath();
        }
    },
    startRenderer: function(path) {
        this.sideRenderer(path, 'start');
    },
    endRenderer: function(path) {
        this.sideRenderer(path, 'end');
    },
    rimRenderer: function(path, radius, isDonut, isFront) {
        var me = this,
            attr = me.attr,
            margin = attr.margin,
            centerX = attr.centerX,
            centerY = attr.centerY,
            distortion = attr.distortion,
            baseRotation = attr.baseRotation,
            normalize = Ext.draw.sprite.AttributeParser.angle,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            // It's critical to use non-normalized start and end angles
            // for middle angle calculation. Consider a situation where the
            // start angle is +170 degrees and the end engle is -170 degrees
            // after normalization (the middle angle is 0 then, but it should be 180 degrees).
            midAngle = normalize((startAngle + endAngle) / 2),
            thickness = attr.thickness,
            isTranslucent = attr.globalAlpha < 1,
            isAllFront, isAllBack, params;
        me.bevelParams = [];
        startAngle = normalize(startAngle);
        endAngle = normalize(endAngle);
        centerX += Math.cos(midAngle) * margin;
        centerY += Math.sin(midAngle) * margin * distortion;
        isAllFront = startAngle >= 0 && endAngle >= 0;
        isAllBack = startAngle <= 0 && endAngle <= 0;
        function renderLeftFrontChunk() {
            path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, Math.PI, startAngle, true);
            path.lineTo(centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius * distortion);
            params = [
                centerX,
                centerY,
                radius,
                radius * distortion,
                0,
                startAngle,
                Math.PI,
                false
            ];
            if (!isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
        function renderRightFrontChunk() {
            path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, 0, endAngle, false);
            path.lineTo(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion);
            params = [
                centerX,
                centerY,
                radius,
                radius * distortion,
                0,
                endAngle,
                0,
                true
            ];
            if (!isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
        function renderLeftBackChunk() {
            path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, Math.PI, endAngle, false);
            path.lineTo(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion);
            params = [
                centerX,
                centerY,
                radius,
                radius * distortion,
                0,
                endAngle,
                Math.PI,
                true
            ];
            if (isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
        function renderRightBackChunk() {
            path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, startAngle, 0, false);
            path.lineTo(centerX + radius, centerY);
            params = [
                centerX,
                centerY,
                radius,
                radius * distortion,
                0,
                0,
                startAngle,
                true
            ];
            if (isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
        if (isFront) {
            if (!isDonut || isTranslucent) {
                if (startAngle >= 0 && endAngle < 0) {
                    renderLeftFrontChunk();
                } else if (startAngle <= 0 && endAngle > 0) {
                    renderRightFrontChunk();
                } else if (startAngle <= 0 && endAngle < 0) {
                    if (startAngle > endAngle) {
                        path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, 0, Math.PI, false);
                        path.lineTo(centerX - radius, centerY);
                        params = [
                            centerX,
                            centerY,
                            radius,
                            radius * distortion,
                            0,
                            Math.PI,
                            0,
                            true
                        ];
                        if (!isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.closePath();
                    }
                } else {
                    // startAngle >= 0 && endAngle > 0
                    // obtuse horseshoe-like slice with the gap facing forward
                    if (startAngle > endAngle) {
                        renderLeftFrontChunk();
                        renderRightFrontChunk();
                    } else {
                        // acute slice facing forward
                        params = [
                            centerX,
                            centerY,
                            radius,
                            radius * distortion,
                            0,
                            startAngle,
                            endAngle,
                            false
                        ];
                        if (isAllFront && !isDonut || isAllBack && isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.lineTo(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion + thickness);
                        path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, endAngle, startAngle, true);
                        path.closePath();
                    }
                }
            }
        } else {
            if (isDonut || isTranslucent) {
                if (startAngle >= 0 && endAngle < 0) {
                    renderLeftBackChunk();
                } else if (startAngle <= 0 && endAngle > 0) {
                    renderRightBackChunk();
                } else if (startAngle <= 0 && endAngle < 0) {
                    if (startAngle > endAngle) {
                        renderLeftBackChunk();
                        renderRightBackChunk();
                    } else {
                        path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, startAngle, endAngle, false);
                        path.lineTo(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * distortion);
                        params = [
                            centerX,
                            centerY,
                            radius,
                            radius * distortion,
                            0,
                            endAngle,
                            startAngle,
                            true
                        ];
                        if (isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.closePath();
                    }
                } else {
                    // startAngle >= 0 && endAngle > 0
                    if (startAngle > endAngle) {
                        path.ellipse(centerX, centerY + thickness, radius, radius * distortion, 0, -Math.PI, 0, false);
                        path.lineTo(centerX + radius, centerY);
                        params = [
                            centerX,
                            centerY,
                            radius,
                            radius * distortion,
                            0,
                            0,
                            -Math.PI,
                            true
                        ];
                        if (isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.closePath();
                    }
                }
            }
        }
    },
    innerFrontRenderer: function(path) {
        this.rimRenderer(path, this.attr.startRho, true, true);
    },
    innerBackRenderer: function(path) {
        this.rimRenderer(path, this.attr.startRho, true, false);
    },
    outerFrontRenderer: function(path) {
        this.rimRenderer(path, this.attr.endRho, false, true);
    },
    outerBackRenderer: function(path) {
        this.rimRenderer(path, this.attr.endRho, false, false);
    }
});

/**
 * @class Ext.chart.series.Pie3D
 * @extends Ext.chart.series.Polar
 *
 * Creates a 3D Pie Chart.
 *
 * **Note:** Labels, legends, and lines are not currently available when using the
 * 3D Pie chart series.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'polar', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        theme: 'green',
 *        interactions: 'rotate',
 *        store: {
 *            fields: ['data3'],
 *            data: [{
 *                'data3': 14
 *            }, {
 *                'data3': 16
 *            }, {
 *                'data3': 14
 *            }, {
 *                'data3': 6
 *            }, {
 *                'data3': 36
 *            }]
 *        },
 *        series: {
 *            type: 'pie3d',
 *            angleField: 'data3',
 *            donut: 30
 *        }
 *     });
 */
Ext.define('Ext.chart.series.Pie3D', {
    extend: 'Ext.chart.series.Polar',
    requires: [
        'Ext.chart.series.sprite.Pie3DPart',
        'Ext.draw.PathUtil'
    ],
    type: 'pie3d',
    seriesType: 'pie3d',
    alias: 'series.pie3d',
    is3D: true,
    config: {
        rect: [
            0,
            0,
            0,
            0
        ],
        thickness: 35,
        distortion: 0.5,
        /**
         * @cfg {String} angleField (required)
         * The store record field name to be used for the pie angles.
         * The values bound to this field name must be positive real numbers.
         */
        /**
         * @private
         * @cfg {String} radiusField
         * Not supported.
         */
        /**
         * @cfg {Number} donut Specifies the radius of the donut hole, as a percentage
         * of the chart's radius.
         * Defaults to 0 (no donut hole).
         */
        donut: 0,
        /**
         * @cfg {Array} hidden Determines which pie slices are hidden.
         */
        hidden: [],
        // Populated by the coordinateX method.
        /**
         * @cfg {Object} highlightCfg Default {@link #highlight} config for the 3D pie series.
         * Slides highlighted pie sector outward.
         */
        highlightCfg: {
            margin: 20
        },
        /**
         * @cfg {Number} [rotation=0] The starting angle of the pie slices.
         */
        /**
         * @private
         * @cfg {Boolean/Object} [shadow=false]
         */
        shadow: false
    },
    // Subtract 90 degrees from rotation, so that `rotation` config's default
    // zero value makes first pie sector start at noon, rather than 3 o'clock.
    rotationOffset: -Math.PI / 2,
    setField: function(value) {
        return this.setXField(value);
    },
    getField: function() {
        return this.getXField();
    },
    updateRotation: function(rotation) {
        var attributes = {
                baseRotation: rotation + this.rotationOffset
            };
        this.forEachSprite(function(sprite) {
            sprite.setAttributes(attributes);
        });
    },
    updateColors: function(colors) {
        var chart;
        this.setSubStyle({
            baseColor: colors
        });
        if (!this.isConfiguring) {
            chart = this.getChart();
            if (chart) {
                chart.refreshLegendStore();
            }
        }
    },
    applyShadow: function(shadow) {
        if (shadow === true) {
            shadow = {
                shadowColor: 'rgba(0,0,0,0.8)',
                shadowBlur: 30
            };
        } else if (!Ext.isObject(shadow)) {
            shadow = {
                shadowColor: Ext.util.Color.RGBA_NONE
            };
        }
        return shadow;
    },
    updateShadow: function(shadow) {
        var me = this,
            sprites = me.getSprites(),
            spritesPerSlice = me.spritesPerSlice,
            ln = sprites && sprites.length,
            i, sprite;
        for (i = 1; i < ln; i += spritesPerSlice) {
            sprite = sprites[i];
            if (sprite.attr.part === 'bottom') {
                sprite.setAttributes(shadow);
            }
        }
    },
    // This is a temporary solution until the Series.getStyleByIndex is fixed
    // to give user styles the priority over theme ones. Also, for sprites of
    // this particular series, the fillStyle shouldn't be set directly. Instead,
    // the 'baseColor' attribute should be set, from which the stops of the
    // gradient (used for fillStyle) will be calculated. Themes can't handle
    // situations like that properly.
    getStyleByIndex: function(i) {
        var indexStyle = this.callParent([
                i
            ]),
            style = this.getStyle(),
            // 'fill' and 'color' are 'fillStyle' aliases
            // (see Ext.draw.sprite.Sprite.inheritableStatics.def.aliases)
            fillStyle = indexStyle.fillStyle || indexStyle.fill || indexStyle.color,
            strokeStyle = style.strokeStyle || style.stroke;
        if (fillStyle) {
            indexStyle.baseColor = fillStyle;
            delete indexStyle.fillStyle;
            delete indexStyle.fill;
            delete indexStyle.color;
        }
        if (strokeStyle) {
            indexStyle.strokeStyle = strokeStyle;
        }
        return indexStyle;
    },
    doUpdateStyles: function() {
        var me = this,
            sprites = me.getSprites(),
            spritesPerSlice = me.spritesPerSlice,
            ln = sprites && sprites.length,
            i = 0,
            j = 0,
            k, style;
        for (; i < ln; i += spritesPerSlice , j++) {
            style = me.getStyleByIndex(j);
            for (k = 0; k < spritesPerSlice; k++) {
                sprites[i + k].setAttributes(style);
            }
        }
    },
    coordinateX: function() {
        var me = this,
            store = me.getStore(),
            records = store.getData().items,
            recordCount = records.length,
            xField = me.getXField(),
            animation = me.getAnimation(),
            rotation = me.getRotation(),
            hidden = me.getHidden(),
            sprites = me.getSprites(true),
            spriteCount = sprites.length,
            spritesPerSlice = me.spritesPerSlice,
            center = me.getCenter(),
            offsetX = me.getOffsetX(),
            offsetY = me.getOffsetY(),
            radius = me.getRadius(),
            thickness = me.getThickness(),
            distortion = me.getDistortion(),
            renderer = me.getRenderer(),
            rendererData = me.getRendererData(),
            highlight = me.getHighlight(),
            // eslint-disable-line no-unused-vars
            lastAngle = 0,
            twoPi = Math.PI * 2,
            // To avoid adjacent start/end part blinking (z-index jitter)
            // when rotating a translucent pie chart.
            delta = 1.0E-10,
            endAngles = [],
            sum = 0,
            value, unit, sprite, style, i, j;
        for (i = 0; i < recordCount; i++) {
            value = Math.abs(+records[i].get(xField)) || 0;
            if (!hidden[i]) {
                sum += value;
            }
            endAngles[i] = sum;
            if (i >= hidden.length) {
                hidden[i] = false;
            }
        }
        if (sum === 0) {
            return;
        }
        // Angular value of 1 in radians.
        unit = 2 * Math.PI / sum;
        for (i = 0; i < recordCount; i++) {
            endAngles[i] *= unit;
        }
        for (i = 0; i < recordCount; i++) {
            style = this.getStyleByIndex(i);
            for (j = 0; j < spritesPerSlice; j++) {
                sprite = sprites[i * spritesPerSlice + j];
                sprite.setAnimation(animation);
                sprite.setAttributes({
                    centerX: center[0] + offsetX,
                    centerY: center[1] + offsetY - thickness / 2,
                    endRho: radius,
                    startRho: radius * me.getDonut() / 100,
                    baseRotation: rotation + me.rotationOffset,
                    startAngle: lastAngle,
                    endAngle: endAngles[i] - delta,
                    thickness: thickness,
                    distortion: distortion,
                    globalAlpha: 1
                });
                sprite.setAttributes(style);
                sprite.setConfig({
                    renderer: renderer,
                    rendererData: rendererData,
                    rendererIndex: i
                });
            }
            // if (highlight) {
            //     if (!sprite.modifiers.highlight) {
            //         debugger
            //         sprite.addModifier(highlight, true);
            //     }
            //     // sprite.modifiers.highlight.setConfig(highlight);
            // }
            lastAngle = endAngles[i];
        }
        for (i *= spritesPerSlice; i < spriteCount; i++) {
            sprite = sprites[i];
            sprite.setAnimation(animation);
            sprite.setAttributes({
                startAngle: twoPi,
                endAngle: twoPi,
                globalAlpha: 0,
                baseRotation: rotation + me.rotationOffset
            });
        }
    },
    updateHighlight: function(highlight, oldHighlight) {
        this.callParent([
            highlight,
            oldHighlight
        ]);
        this.forEachSprite(function(sprite) {
            if (highlight) {
                if (sprite.modifiers.highlight) {
                    sprite.modifiers.highlight.setConfig(highlight);
                } else {
                    sprite.config.highlight = highlight;
                    sprite.addModifier(highlight, true);
                }
            }
        });
    },
    updateLabelData: function() {
        var me = this,
            store = me.getStore(),
            items = store.getData().items,
            sprites = me.getSprites(),
            label = me.getLabel(),
            labelField = label && label.getTemplate().getField(),
            hidden = me.getHidden(),
            spritesPerSlice = me.spritesPerSlice,
            ln, labels, sprite,
            name = 'labels',
            i, // sprite index
            j;
        // record index
        if (sprites.length) {
            if (labelField) {
                labels = [];
                for (j = 0 , ln = items.length; j < ln; j++) {
                    labels.push(items[j].get(labelField));
                }
            }
            // Only set labels for the sprites that compose the top lid of the pie.
            for (i = 0 , j = 0 , ln = sprites.length; i < ln; i += spritesPerSlice , j++) {
                sprite = sprites[i];
                if (label) {
                    if (!sprite.getMarker(name)) {
                        sprite.bindMarker(name, label);
                    }
                    if (labels) {
                        sprite.setAttributes({
                            label: labels[j]
                        });
                    }
                    sprite.putMarker(name, {
                        hidden: hidden[j]
                    }, sprite.attr.attributeId);
                } else {
                    sprite.releaseMarker(name);
                }
            }
        }
    },
    // The radius here will normally be set by the PolarChart.performLayout,
    // where it's half the width or height (whichever is smaller) of the chart's rect.
    // But for 3D pie series we have to take the thickness of the pie and the
    // distortion into account to calculate the proper radius.
    // The passed value is never used (or derived from) since the radius config
    // is not really meant to be used directly, as it will be reset by the next layout.
    applyRadius: function() {
        var me = this,
            chart = me.getChart(),
            padding = chart.getInnerPadding(),
            rect = chart.getMainRect() || [
                0,
                0,
                1,
                1
            ],
            width = rect[2] - padding * 2,
            height = rect[3] - padding * 2 - me.getThickness(),
            horizontalRadius = width / 2,
            verticalRadius = horizontalRadius * me.getDistortion(),
            result;
        if (verticalRadius > height / 2) {
            result = height / (me.getDistortion() * 2);
        } else {
            result = horizontalRadius;
        }
        return Math.max(result, 0);
    },
    forEachSprite: function(fn) {
        var sprites = this.sprites,
            ln = sprites.length,
            i;
        for (i = 0; i < ln; i++) {
            fn(sprites[i], Math.floor(i / this.spritesPerSlice));
        }
    },
    updateRadius: function(radius) {
        var donut;
        // The side effects of the 'getChart' call will result
        // in the 'coordinateX' method call, which we want to have called
        // first, to coordinate the data and create sprites for pie slices,
        // before we set their attributes here.
        // updateChart -> onChartAttached -> processData -> coordinateX
        this.getChart();
        donut = this.getDonut();
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                endRho: radius,
                startRho: radius * donut / 100
            });
        });
    },
    updateDonut: function(donut) {
        var radius;
        // See 'updateRadius' comments.
        this.getChart();
        radius = this.getRadius();
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                startRho: radius * donut / 100
            });
        });
    },
    updateCenter: function(center) {
        var offsetX, offsetY, thickness;
        // See 'updateRadius' comments.
        this.getChart();
        offsetX = this.getOffsetX();
        offsetY = this.getOffsetY();
        thickness = this.getThickness();
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                centerX: center[0] + offsetX,
                centerY: center[1] + offsetY - thickness / 2
            });
        });
    },
    updateThickness: function(thickness) {
        var center, offsetY;
        // See 'updateRadius' comments.
        this.getChart();
        // Radius depends on thickness and distortion,
        // this will trigger its recalculation in the applier.
        this.setRadius();
        center = this.getCenter();
        offsetY = this.getOffsetY();
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                thickness: thickness,
                centerY: center[1] + offsetY - thickness / 2
            });
        });
    },
    updateDistortion: function(distortion) {
        // See 'updateRadius' comments.
        this.getChart();
        // Radius depends on thickness and distortion,
        // this will trigger its recalculation in the applier.
        this.setRadius();
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                distortion: distortion
            });
        });
    },
    updateOffsetX: function(offsetX) {
        var center;
        // See 'updateRadius' comments.
        this.getChart();
        center = this.getCenter();
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                centerX: center[0] + offsetX
            });
        });
    },
    updateOffsetY: function(offsetY) {
        var center, thickness;
        // See 'updateRadius' comments.
        this.getChart();
        center = this.getCenter();
        thickness = this.getThickness();
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                centerY: center[1] + offsetY - thickness / 2
            });
        });
    },
    updateAnimation: function(animation) {
        // See 'updateRadius' comments.
        this.getChart();
        this.forEachSprite(function(sprite) {
            sprite.setAnimation(animation);
        });
    },
    updateRenderer: function(renderer) {
        var rendererData;
        // See 'updateRadius' comments.
        this.getChart();
        rendererData = this.getRendererData();
        this.forEachSprite(function(sprite, itemIndex) {
            sprite.setConfig({
                renderer: renderer,
                rendererData: rendererData,
                rendererIndex: itemIndex
            });
        });
    },
    getRendererData: function() {
        return {
            store: this.getStore(),
            angleField: this.getXField(),
            radiusField: this.getYField(),
            series: this
        };
    },
    getSprites: function(createMissing) {
        var me = this,
            store = me.getStore(),
            sprites = me.sprites;
        if (!store) {
            return Ext.emptyArray;
        }
        if (sprites && !createMissing) {
            return sprites;
        }
        // eslint-disable-next-line vars-on-top, one-var
        var surface = me.getSurface(),
            records = store.getData().items,
            spritesPerSlice = me.spritesPerSlice,
            partCount = me.partNames.length,
            recordCount = records.length,
            sprite, i, j;
        for (i = 0; i < recordCount; i++) {
            if (!sprites[i * spritesPerSlice]) {
                for (j = 0; j < partCount; j++) {
                    sprite = surface.add({
                        type: 'pie3dPart',
                        part: me.partNames[j],
                        series: me
                    });
                    sprite.getAnimation().setDurationOn('baseRotation', 0);
                    sprites.push(sprite);
                }
            }
        }
        return sprites;
    },
    betweenAngle: function(x, a, b) {
        var pp = Math.PI * 2,
            offset = this.rotationOffset;
        a += offset;
        b += offset;
        x -= a;
        b -= a;
        // Normalize, so that both x and b are in the [0,360) interval.
        // Since 360 * n angles will be normalized to 0,
        // we need to treat b === 0 as a special case.
        x %= pp;
        b %= pp;
        x += pp;
        b += pp;
        x %= pp;
        b %= pp;
        return x < b || b === 0;
    },
    getItemForPoint: function(x, y) {
        var me = this,
            sprites = me.getSprites(),
            spritesPerSlice = me.spritesPerSlice,
            result = null,
            store, records, hidden, i, ln, sprite, topPartIndex;
        if (!sprites) {
            return result;
        }
        store = me.getStore();
        records = store.getData().items;
        hidden = me.getHidden();
        for (i = 0 , ln = records.length; i < ln; i++) {
            if (hidden[i]) {
                
                continue;
            }
            topPartIndex = i * spritesPerSlice;
            sprite = sprites[topPartIndex];
            // This is CPU intensive on mousemove (no visial slowdown
            // on a fast machine, but some throttling might be desirable
            // on slower machines).
            // On touch devices performance/battery hit is negligible.
            if (sprite.hitTest([
                x,
                y
            ])) {
                result = {
                    series: me,
                    sprite: sprites.slice(topPartIndex, topPartIndex + spritesPerSlice),
                    index: i,
                    record: records[i],
                    category: 'sprites',
                    field: me.getXField()
                };
                break;
            }
        }
        return result;
    },
    provideLegendInfo: function(target) {
        var me = this,
            store = me.getStore(),
            items, labelField, field, hidden, style, color, i;
        if (store) {
            items = store.getData().items;
            labelField = me.getLabel().getTemplate().getField();
            field = me.getField();
            hidden = me.getHidden();
            for (i = 0; i < items.length; i++) {
                style = me.getStyleByIndex(i);
                color = style.baseColor;
                target.push({
                    name: labelField ? String(items[i].get(labelField)) : field + ' ' + i,
                    mark: color || 'black',
                    disabled: hidden[i],
                    series: me.getId(),
                    index: i
                });
            }
        }
    }
}, function() {
    var proto = this.prototype,
        definition = Ext.chart.series.sprite.Pie3DPart.def.getInitialConfig().processors.part;
    proto.partNames = definition.replace(/^enums\(|\)/g, '').split(',');
    proto.spritesPerSlice = proto.partNames.length;
});

/**
 * Polar sprite.
 */
Ext.define('Ext.chart.series.sprite.Polar', {
    extend: 'Ext.chart.series.sprite.Series',
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [centerX=0] The central point of the series on the x-axis.
                 */
                centerX: 'number',
                /**
                 * @cfg {Number} [centerY=0] The central point of the series on the y-axis.
                 */
                centerY: 'number',
                /**
                 * @cfg {Number} [startAngle=0] The starting angle of the polar series.
                 */
                startAngle: 'number',
                /**
                 * @cfg {Number} [endAngle=Math.PI] The ending angle of the polar series.
                 */
                endAngle: 'number',
                /**
                 * @cfg {Number} [startRho=0] The starting radius of the polar series.
                 */
                startRho: 'number',
                /**
                 * @cfg {Number} [endRho=150] The ending radius of the polar series.
                 */
                endRho: 'number',
                /**
                 * @cfg {Number} [baseRotation=0] The starting rotation of the polar series.
                 */
                baseRotation: 'number'
            },
            defaults: {
                centerX: 0,
                centerY: 0,
                startAngle: 0,
                endAngle: Math.PI,
                startRho: 0,
                endRho: 150,
                baseRotation: 0
            },
            triggers: {
                centerX: 'bbox',
                centerY: 'bbox',
                startAngle: 'bbox',
                endAngle: 'bbox',
                startRho: 'bbox',
                endRho: 'bbox',
                baseRotation: 'bbox'
            }
        }
    },
    updatePlainBBox: function(plain) {
        var attr = this.attr;
        plain.x = attr.centerX - attr.endRho;
        plain.y = attr.centerY + attr.endRho;
        plain.width = attr.endRho * 2;
        plain.height = attr.endRho * 2;
    }
});

/**
 * @class Ext.chart.series.sprite.Radar
 * @extends Ext.chart.series.sprite.Polar
 * 
 * Radar series sprite.
 */
Ext.define('Ext.chart.series.sprite.Radar', {
    alias: 'sprite.radar',
    extend: 'Ext.chart.series.sprite.Polar',
    getDataPointXY: function(index) {
        var me = this,
            attr = me.attr,
            centerX = attr.centerX,
            centerY = attr.centerY,
            matrix = attr.matrix,
            minX = attr.dataMinX,
            maxX = attr.dataMaxX,
            dataX = attr.dataX,
            dataY = attr.dataY,
            endRho = attr.endRho,
            startRho = attr.startRho,
            baseRotation = attr.baseRotation,
            x, y, r, th, ox, oy, maxY;
        if (attr.rangeY) {
            maxY = attr.rangeY[1];
        } else {
            maxY = attr.dataMaxY;
        }
        th = (dataX[index] - minX) / (maxX - minX + 1) * 2 * Math.PI + baseRotation;
        r = dataY[index] / maxY * (endRho - startRho) + startRho;
        // Original coordinates.
        ox = centerX + Math.cos(th) * r;
        oy = centerY + Math.sin(th) * r;
        // Transformed coordinates.
        x = matrix.x(ox, oy);
        y = matrix.y(ox, oy);
        return [
            x,
            y
        ];
    },
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr,
            dataX = attr.dataX,
            length = dataX.length,
            surfaceMatrix = me.surfaceMatrix,
            markerCfg = {},
            i, x, y, xy;
        ctx.beginPath();
        for (i = 0; i < length; i++) {
            xy = me.getDataPointXY(i);
            x = xy[0];
            y = xy[1];
            if (i === 0) {
                ctx.moveTo(x, y);
            }
            ctx.lineTo(x, y);
            markerCfg.translationX = surfaceMatrix.x(x, y);
            markerCfg.translationY = surfaceMatrix.y(x, y);
            me.putMarker('markers', markerCfg, i, true);
        }
        ctx.closePath();
        ctx.fillStroke(attr);
    }
});

/**
 * @class Ext.chart.series.Radar
 * @extends Ext.chart.series.Polar
 *
 * Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different
 * quantitative values for a constrained number of categories.
 * As with all other series, the Radar series must be appended in the *series* Chart array
 * configuration. See the Chart documentation for more information. A typical configuration object
 * for the radar series could be:
 *
 *     @example
 *     Ext.create({
 *        xtype: 'polar',
 *        renderTo: document.body,
 *        width: 500,
 *        height: 400,
 *        interactions: 'rotate',
 *        store: {
 *            fields: ['name', 'data1'],
 *            data: [{
 *                'name': 'metric one',
 *                'data1': 8
 *            }, {
 *                'name': 'metric two',
 *                'data1': 10
 *            }, {
 *                'name': 'metric three',
 *                'data1': 12
 *            }, {
 *                'name': 'metric four',
 *                'data1': 1
 *            }, {
 *                'name': 'metric five',
 *                'data1': 13
 *            }]
 *        },
 *        series: {
 *            type: 'radar',
 *            angleField: 'name',
 *            radiusField: 'data1',
 *            style: {
 *                fillStyle: '#388FAD',
 *                fillOpacity: .1,
 *                strokeStyle: '#388FAD',
 *                strokeOpacity: .8,
 *                lineWidth: 1
 *            }
 *        },
 *        axes: [{
 *            type: 'numeric',
 *            position: 'radial',
 *            fields: 'data1',
 *            style: {
 *                estStepSize: 10
 *            },
 *            grid: true
 *        }, {
 *            type: 'category',
 *            position: 'angular',
 *            fields: 'name',
 *            style: {
 *                estStepSize: 1
 *            },
 *            grid: true
 *        }]
 *     });
 *
 */
Ext.define('Ext.chart.series.Radar', {
    extend: 'Ext.chart.series.Polar',
    type: 'radar',
    seriesType: 'radar',
    alias: 'series.radar',
    requires: [
        'Ext.chart.series.sprite.Radar'
    ],
    themeColorCount: function() {
        return 1;
    },
    isStoreDependantColorCount: false,
    themeMarkerCount: function() {
        return 1;
    },
    updateAngularAxis: function(axis) {
        axis.processData(this);
    },
    updateRadialAxis: function(axis) {
        axis.processData(this);
    },
    coordinateX: function() {
        return this.coordinate('X', 0, 2);
    },
    coordinateY: function() {
        return this.coordinate('Y', 1, 2);
    },
    updateCenter: function(center) {
        this.setStyle({
            translationX: center[0] + this.getOffsetX(),
            translationY: center[1] + this.getOffsetY()
        });
        this.doUpdateStyles();
    },
    updateRadius: function(radius) {
        this.setStyle({
            endRho: radius
        });
        this.doUpdateStyles();
    },
    updateRotation: function(rotation) {
        // Overrides base class method.
        var me = this,
            chart = me.getChart(),
            axes = chart.getAxes(),
            i, ln, axis;
        for (i = 0 , ln = axes.length; i < ln; i++) {
            axis = axes[i];
            axis.setRotation(rotation);
        }
        me.setStyle({
            rotationRads: rotation
        });
        me.doUpdateStyles();
    },
    updateTotalAngle: function(totalAngle) {
        this.processData();
    },
    getItemForPoint: function(x, y) {
        var me = this,
            sprite = me.sprites && me.sprites[0],
            attr = sprite.attr,
            dataX = attr.dataX,
            length = dataX.length,
            store = me.getStore(),
            marker = me.getMarker(),
            threshhold, item, xy, i, bbox, markers;
        if (me.getHidden()) {
            return null;
        }
        if (sprite && marker) {
            markers = sprite.getMarker('markers');
            for (i = 0; i < length; i++) {
                bbox = markers.getBBoxFor(i);
                threshhold = (bbox.width + bbox.height) * 0.25;
                xy = sprite.getDataPointXY(i);
                if (Math.abs(xy[0] - x) < threshhold && Math.abs(xy[1] - y) < threshhold) {
                    item = {
                        series: me,
                        sprite: sprite,
                        index: i,
                        category: 'markers',
                        record: store.getData().items[i],
                        field: me.getYField()
                    };
                    return item;
                }
            }
        }
        return me.callParent(arguments);
    },
    getDefaultSpriteConfig: function() {
        var config = this.callParent(),
            animation = {
                customDurations: {
                    translationX: 0,
                    translationY: 0,
                    rotationRads: 0,
                    // Prevent animation of 'dataMinX' and 'dataMaxX' attributes in order
                    // to react instantaniously to changes to the 'hidden' attribute.
                    dataMinX: 0,
                    dataMaxX: 0
                }
            };
        if (config.animation) {
            Ext.apply(config.animation, animation);
        } else {
            config.animation = animation;
        }
        return config;
    },
    getSprites: function() {
        var me = this,
            chart = me.getChart(),
            sprites = me.sprites;
        if (!chart) {
            return Ext.emptyArray;
        }
        if (!sprites.length) {
            me.createSprite();
        }
        return sprites;
    },
    provideLegendInfo: function(target) {
        var me = this,
            style = me.getSubStyleWithTheme(),
            fill = style.fillStyle;
        if (Ext.isArray(fill)) {
            fill = fill[0];
        }
        target.push({
            name: me.getTitle() || me.getYField() || me.getId(),
            mark: (Ext.isObject(fill) ? fill.stops && fill.stops[0].color : fill) || style.strokeStyle || 'black',
            disabled: me.getHidden(),
            series: me.getId(),
            index: 0
        });
    }
});

/**
 * @class Ext.chart.series.sprite.Scatter
 * @extends Ext.chart.series.sprite.Cartesian
 * 
 * Scatter series sprite.
 */
Ext.define('Ext.chart.series.sprite.Scatter', {
    alias: 'sprite.scatterSeries',
    extend: 'Ext.chart.series.sprite.Cartesian',
    renderClipped: function(surface, ctx, dataClipRect, surfaceClipRect) {
        if (this.cleanRedraw) {
            return;
        }
        // eslint-disable-next-line vars-on-top
        var me = this,
            attr = me.attr,
            dataX = attr.dataX,
            dataY = attr.dataY,
            labels = attr.labels,
            series = me.getSeries(),
            isDrawLabels = labels && me.getMarker('labels'),
            surfaceMatrix = me.surfaceMatrix,
            matrix = me.attr.matrix,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            markerCfg = {},
            changes, params,
            xScalingDirection = surface.getInherited().rtl && !attr.flipXY ? -1 : 1,
            left, right, top, bottom, x, y, i;
        if (attr.flipXY) {
            left = surfaceClipRect[1] - xx * xScalingDirection;
            right = surfaceClipRect[1] + surfaceClipRect[3] + xx * xScalingDirection;
            top = surfaceClipRect[0] - yy;
            bottom = surfaceClipRect[0] + surfaceClipRect[2] + yy;
        } else {
            left = surfaceClipRect[0] - xx * xScalingDirection;
            right = surfaceClipRect[0] + surfaceClipRect[2] + xx * xScalingDirection;
            top = surfaceClipRect[1] - yy;
            bottom = surfaceClipRect[1] + surfaceClipRect[3] + yy;
        }
        for (i = 0; i < dataX.length; i++) {
            x = dataX[i];
            y = dataY[i];
            x = x * xx + dx;
            y = y * yy + dy;
            if (left <= x && x <= right && top <= y && y <= bottom) {
                if (attr.renderer) {
                    markerCfg = {
                        type: 'markers',
                        translationX: surfaceMatrix.x(x, y),
                        translationY: surfaceMatrix.y(x, y)
                    };
                    params = [
                        me,
                        markerCfg,
                        {
                            store: me.getStore()
                        },
                        i
                    ];
                    changes = Ext.callback(attr.renderer, null, params, 0, series);
                    markerCfg = Ext.apply(markerCfg, changes);
                } else {
                    markerCfg.translationX = surfaceMatrix.x(x, y);
                    markerCfg.translationY = surfaceMatrix.y(x, y);
                }
                me.putMarker('markers', markerCfg, i, !attr.renderer);
                if (isDrawLabels && labels[i]) {
                    me.drawLabel(labels[i], x, y, i, surfaceClipRect);
                }
            }
        }
    },
    drawLabel: function(text, dataX, dataY, labelId, rect) {
        var me = this,
            attr = me.attr,
            label = me.getMarker('labels'),
            labelTpl = label.getTemplate(),
            labelCfg = me.labelCfg || (me.labelCfg = {}),
            surfaceMatrix = me.surfaceMatrix,
            labelX, labelY,
            labelOverflowPadding = attr.labelOverflowPadding,
            flipXY = attr.flipXY,
            halfHeight, labelBox, changes, params;
        labelCfg.text = text;
        labelBox = me.getMarkerBBox('labels', labelId, true);
        if (!labelBox) {
            me.putMarker('labels', labelCfg, labelId);
            labelBox = me.getMarkerBBox('labels', labelId, true);
        }
        if (flipXY) {
            labelCfg.rotationRads = Math.PI * 0.5;
        } else {
            labelCfg.rotationRads = 0;
        }
        halfHeight = labelBox.height / 2;
        labelX = dataX;
        switch (labelTpl.attr.display) {
            case 'under':
                labelY = dataY - halfHeight - labelOverflowPadding;
                break;
            case 'rotate':
                labelX += labelOverflowPadding;
                labelY = dataY - labelOverflowPadding;
                labelCfg.rotationRads = -Math.PI / 4;
                break;
            default:
                // 'over'
                labelY = dataY + halfHeight + labelOverflowPadding;
        }
        labelCfg.x = surfaceMatrix.x(labelX, labelY);
        labelCfg.y = surfaceMatrix.y(labelX, labelY);
        if (labelTpl.attr.renderer) {
            params = [
                text,
                label,
                labelCfg,
                {
                    store: me.getStore()
                },
                labelId
            ];
            changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries());
            if (typeof changes === 'string') {
                labelCfg.text = changes;
            } else {
                Ext.apply(labelCfg, changes);
            }
        }
        me.putMarker('labels', labelCfg, labelId);
    }
});

/**
 * @class Ext.chart.series.Scatter
 * @extends Ext.chart.series.Cartesian
 *
 * Creates a Scatter Chart. The scatter plot is useful when trying to display more than
 * two variables in the same visualization. These variables can be mapped into x, y coordinates
 * and also to an element's radius/size, color, etc. As with all other series, the Scatter Series
 * must be appended in the *series* Chart array configuration. See the Chart documentation for more
 * information on creating charts. A typical configuration object for the scatter could be:
 *
 *     @example
 *     Ext.create({
 *        xtype: 'cartesian', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        insetPadding: 40,
 *        interactions: ['itemhighlight'],
 *        store: {
 *            fields: ['name', 'data1', 'data2'],
 *            data: [{
 *                'name': 'metric one',
 *                'data1': 10,
 *                'data2': 14
 *            }, {
 *                'name': 'metric two',
 *                'data1': 7,
 *                'data2': 16
 *            }, {
 *                'name': 'metric three',
 *                'data1': 5,
 *                'data2': 14
 *            }, {
 *                'name': 'metric four',
 *                'data1': 2,
 *                'data2': 6
 *            }, {
 *                'name': 'metric five',
 *                'data1': 27,
 *                'data2': 36
 *            }]
 *        },
 *        axes: [{
 *            type: 'numeric',
 *            position: 'left',
 *            fields: ['data1'],
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            },
 *            grid: true,
 *            minimum: 0
 *        }, {
 *            type: 'category',
 *            position: 'bottom',
 *            fields: ['name'],
 *            title: {
 *                text: 'Sample Values',
 *                fontSize: 15
 *            }
 *        }],
 *        series: {
 *            type: 'scatter',
 *            highlight: {
 *                size: 12,
 *                radius: 12,
 *                fill: '#96D4C6',
 *                stroke: '#30BDA7'
 *            },
 *            fill: true,
 *            xField: 'name',
 *            yField: 'data2',
 *            marker: {
 *                type: 'circle',
 *                fill: '#30BDA7',
 *                radius: 10,
 *                lineWidth: 0
 *            }
 *        }
 *     });
 *
 * In this configuration we add three different categories of scatter series. Each of them is bound
 * to a different field of the same data store, `data1`, `data2` and `data3` respectively.
 * All x-fields for the series must be the same field, in this case `name`. Each scatter series
 * has a different styling configuration for markers, specified by the `marker` object. Finally
 * we set the left axis as axis to show the current values of the elements.
 *
 */
Ext.define('Ext.chart.series.Scatter', {
    extend: 'Ext.chart.series.Cartesian',
    alias: 'series.scatter',
    type: 'scatter',
    seriesType: 'scatterSeries',
    requires: [
        'Ext.chart.series.sprite.Scatter'
    ],
    config: {
        itemInstancing: null,
        marker: true
    },
    themeMarkerCount: function() {
        return 1;
    },
    provideLegendInfo: function(target) {
        var me = this,
            style = me.getMarkerStyleByIndex(0),
            fill = style.fillStyle;
        target.push({
            name: me.getTitle() || me.getYField() || me.getId(),
            mark: (Ext.isObject(fill) ? fill.stops && fill.stops[0].color : fill) || style.strokeStyle || 'black',
            disabled: me.getHidden(),
            series: me.getId(),
            index: 0
        });
    }
});

Ext.define('Ext.chart.theme.Blue', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.blue',
        'chart.theme.Blue'
    ],
    config: {
        baseColor: '#4d7fe6'
    }
});

Ext.define('Ext.chart.theme.BlueGradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.blue-gradients',
        'chart.theme.Blue:gradients'
    ],
    config: {
        baseColor: '#4d7fe6',
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Category1', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category1',
        'chart.theme.Category1'
    ],
    config: {
        colors: [
            '#f0a50a',
            '#c20024',
            '#2044ba',
            '#810065',
            '#7eae29'
        ]
    }
});

Ext.define('Ext.chart.theme.Category1Gradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category1-gradients',
        'chart.theme.Category1:gradients'
    ],
    config: {
        colors: [
            '#f0a50a',
            '#c20024',
            '#2044ba',
            '#810065',
            '#7eae29'
        ],
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Category2', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category2',
        'chart.theme.Category2'
    ],
    config: {
        colors: [
            '#6d9824',
            '#87146e',
            '#2a9196',
            '#d39006',
            '#1e40ac'
        ]
    }
});

Ext.define('Ext.chart.theme.Category2Gradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category2-gradients',
        'chart.theme.Category2:gradients'
    ],
    config: {
        colors: [
            '#6d9824',
            '#87146e',
            '#2a9196',
            '#d39006',
            '#1e40ac'
        ],
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Category3', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category3',
        'chart.theme.Category3'
    ],
    config: {
        colors: [
            '#fbbc29',
            '#ce2e4e',
            '#7e0062',
            '#158b90',
            '#57880e'
        ]
    }
});

Ext.define('Ext.chart.theme.Category3Gradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category3-gradients',
        'chart.theme.Category3:gradients'
    ],
    config: {
        colors: [
            '#fbbc29',
            '#ce2e4e',
            '#7e0062',
            '#158b90',
            '#57880e'
        ],
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Category4', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category4',
        'chart.theme.Category4'
    ],
    config: {
        colors: [
            '#ef5773',
            '#fcbd2a',
            '#4f770d',
            '#1d3eaa',
            '#9b001f'
        ]
    }
});

Ext.define('Ext.chart.theme.Category4Gradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category4-gradients',
        'chart.theme.Category4:gradients'
    ],
    config: {
        colors: [
            '#ef5773',
            '#fcbd2a',
            '#4f770d',
            '#1d3eaa',
            '#9b001f'
        ],
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Category5', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category5',
        'chart.theme.Category5'
    ],
    config: {
        colors: [
            '#7eae29',
            '#fdbe2a',
            '#910019',
            '#27b4bc',
            '#d74dbc'
        ]
    }
});

Ext.define('Ext.chart.theme.Category5Gradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category5-gradients',
        'chart.theme.Category5:gradients'
    ],
    config: {
        colors: [
            '#7eae29',
            '#fdbe2a',
            '#910019',
            '#27b4bc',
            '#d74dbc'
        ],
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Category6', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category6',
        'chart.theme.Category6'
    ],
    config: {
        colors: [
            '#44dce1',
            '#0b2592',
            '#996e05',
            '#7fb325',
            '#b821a1'
        ]
    }
});

Ext.define('Ext.chart.theme.Category6Gradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.category6-gradients',
        'chart.theme.Category6:gradients'
    ],
    config: {
        colors: [
            '#44dce1',
            '#0b2592',
            '#996e05',
            '#7fb325',
            '#b821a1'
        ],
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.DefaultGradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.default-gradients',
        'chart.theme.Base:gradients'
    ],
    config: {
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Green', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.green',
        'chart.theme.Green'
    ],
    config: {
        baseColor: '#b1da5a'
    }
});

Ext.define('Ext.chart.theme.GreenGradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.green-gradients',
        'chart.theme.Green:gradients'
    ],
    config: {
        baseColor: '#b1da5a',
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Midnight', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.midnight',
        'chart.theme.Midnight'
    ],
    config: {
        colors: [
            '#a837ff',
            '#4ac0f2',
            '#ff4d35',
            '#ff8809',
            '#61c102',
            '#ff37ea'
        ],
        chart: {
            defaults: {
                captions: {
                    title: {
                        docked: 'top',
                        padding: 5,
                        style: {
                            textAlign: 'center',
                            fontFamily: 'default',
                            fontWeight: 'bold',
                            fillStyle: 'rgb(224, 224, 227)',
                            fontSize: 'default*1.6'
                        }
                    },
                    subtitle: {
                        docked: 'top',
                        style: {
                            textAlign: 'center',
                            fontFamily: 'default',
                            fontWeight: 'normal',
                            fillStyle: 'rgb(224, 224, 227)',
                            fontSize: 'default*1.3'
                        }
                    },
                    credits: {
                        docked: 'bottom',
                        padding: 5,
                        style: {
                            textAlign: 'left',
                            fontFamily: 'default',
                            fontWeight: 'lighter',
                            fillStyle: 'rgb(224, 224, 227)',
                            fontSize: 'default'
                        }
                    }
                },
                background: 'rgb(52, 52, 53)'
            }
        },
        axis: {
            defaults: {
                style: {
                    strokeStyle: 'rgb(224, 224, 227)'
                },
                label: {
                    fillStyle: 'rgb(224, 224, 227)'
                },
                title: {
                    fillStyle: 'rgb(224, 224, 227)'
                },
                grid: {
                    strokeStyle: 'rgb(112, 112, 115)'
                }
            }
        },
        series: {
            defaults: {
                label: {
                    fillStyle: 'rgb(224, 224, 227)'
                }
            }
        },
        sprites: {
            text: {
                fillStyle: 'rgb(224, 224, 227)'
            }
        },
        legend: {
            label: {
                fillStyle: 'white'
            },
            border: {
                lineWidth: 2,
                fillStyle: 'rgba(255, 255, 255, 0.3)',
                strokeStyle: 'rgb(150, 150, 150)'
            },
            background: 'rgb(52, 52, 53)'
        }
    }
});

Ext.define('Ext.chart.theme.Muted', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.muted',
        'chart.theme.Muted'
    ],
    config: {
        colors: [
            '#8ca640',
            '#974144',
            '#4091ba',
            '#8e658e',
            '#3b8d8b',
            '#b86465',
            '#d2af69',
            '#6e8852',
            '#3dcc7e',
            '#a6bed1',
            '#cbaa4b',
            '#998baa'
        ]
    }
});

Ext.define('Ext.chart.theme.Purple', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.purple',
        'chart.theme.Purple'
    ],
    config: {
        baseColor: '#da5abd'
    }
});

Ext.define('Ext.chart.theme.PurpleGradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.purple-gradients',
        'chart.theme.Purple:gradients'
    ],
    config: {
        baseColor: '#da5abd',
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Red', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.red',
        'chart.theme.Red'
    ],
    config: {
        baseColor: '#e84b67'
    }
});

Ext.define('Ext.chart.theme.RedGradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.red-gradients',
        'chart.theme.Red:gradients'
    ],
    config: {
        baseColor: '#e84b67',
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Sky', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.sky',
        'chart.theme.Sky'
    ],
    config: {
        baseColor: '#4ce0e7'
    }
});

Ext.define('Ext.chart.theme.SkyGradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.sky-gradients',
        'chart.theme.Sky:gradients'
    ],
    config: {
        baseColor: '#4ce0e7',
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

Ext.define('Ext.chart.theme.Yellow', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.yellow',
        'chart.theme.Yellow'
    ],
    config: {
        baseColor: '#fec935'
    }
});

Ext.define('Ext.chart.theme.YellowGradients', {
    extend: 'Ext.chart.theme.Base',
    singleton: true,
    alias: [
        'chart.theme.yellow-gradients',
        'chart.theme.Yellow:gradients'
    ],
    config: {
        baseColor: '#fec935',
        gradients: {
            type: 'linear',
            degrees: 90
        }
    }
});

/**
 * A helper class to facilitate common operations on points and vectors.
 */
Ext.define('Ext.draw.Point', {
    requires: [
        'Ext.draw.Draw',
        'Ext.draw.Matrix'
    ],
    isPoint: true,
    x: 0,
    y: 0,
    length: 0,
    angle: 0,
    angleUnits: 'degrees',
    statics: {
        /**
         * @method
         * @static
         * Creates a flyweight Ext.draw.Point instance.
         * Takes the same parameters as the {@link Ext.draw.Point#method!constructor}.
         * Do not hold the instance of the flyweight point.
         *
         * @param {Number/Number[]/Object/Ext.draw.Point} point
         * @return {Ext.draw.Point}
         */
        fly: (function() {
            var point = null;
            return function(x, y) {
                if (!point) {
                    point = new Ext.draw.Point();
                }
                point.constructor(x, y);
                return point;
            };
        })()
    },
    /**
     * Creates a point.
     *
     *     new Ext.draw.Point(3, 4);
     *     new Ext.draw.Point(3); // both x and y equal 3
     *     new Ext.draw.Point([3, 4]);
     *     new Ext.draw.Point({x: 3, y: 4});
     *     new Ext.draw.Point(p); // where `p` is a Ext.draw.Point instance.
     *
     * @param {Number/Number[]/Object/Ext.draw.Point} x
     * @param {Number/Number[]/Object/Ext.draw.Point} y
     */
    constructor: function(x, y) {
        var me = this;
        if (typeof x === 'number') {
            me.x = x;
            if (typeof y === 'number') {
                me.y = y;
            } else {
                me.y = x;
            }
        } else if (Ext.isArray(x)) {
            me.x = x[0];
            me.y = x[1];
        } else if (x) {
            me.x = x.x;
            me.y = x.y;
        }
        me.calculatePolar();
    },
    calculateCartesian: function() {
        var me = this,
            length = me.length,
            angle = me.angle;
        if (me.angleUnits === 'degrees') {
            angle = Ext.draw.Draw.rad(angle);
        }
        me.x = Math.cos(angle) * length;
        me.y = Math.sin(angle) * length;
    },
    calculatePolar: function() {
        var me = this,
            x = me.x,
            y = me.y;
        me.length = Math.sqrt(x * x + y * y);
        me.angle = Math.atan2(y, x);
        if (me.angleUnits === 'degrees') {
            me.angle = Ext.draw.Draw.degrees(me.angle);
        }
    },
    /**
     * Sets the x-coordinate of the point.
     * @param {Number} x
     */
    setX: function(x) {
        this.x = x;
        this.calculatePolar();
    },
    /**
     * Sets the y-coordinate of the point.
     * @param {Number} y
     */
    setY: function(y) {
        this.y = y;
        this.calculatePolar();
    },
    /**
     * Sets coordinates of the point.
     * Takes the same parameters as the {@link #method!constructor}.
     * @param {Number/Number[]/Object/Ext.draw.Point} x
     * @param {Number/Number[]/Object/Ext.draw.Point} y
     */
    set: function(x, y) {
        this.constructor(x, y);
    },
    /**
     * Sets the angle of the vector (measured from the x-axis to the vector)
     * without changing its length.
     * @param {Number} angle
     */
    setAngle: function(angle) {
        this.angle = angle;
        this.calculateCartesian();
    },
    /**
     * Sets the length of the vector without changing its angle.
     * @param {Number} length
     */
    setLength: function(length) {
        this.length = length;
        this.calculateCartesian();
    },
    /**
     * Sets both the angle and the length of the vector.
     * A point can be thought of as a vector pointing from the origin to the point's location.
     * This can also be interpreted as setting coordinates of a point in the polar
     * coordinate system.
     * @param {Number} angle
     * @param {Number} length
     */
    setPolar: function(angle, length) {
        this.angle = angle;
        this.length = length;
        this.calculateCartesian();
    },
    /**
     * Returns a copy of the point.
     * @return {Ext.draw.Point}
     */
    clone: function() {
        return new Ext.draw.Point(this.x, this.y);
    },
    /**
     * Adds another vector to this one and returns the resulting vector
     * without changing this vector.
     * @param {Number/Number[]/Object/Ext.draw.Point} x
     * @param {Number/Number[]/Object/Ext.draw.Point} y
     * @return {Ext.draw.Point}
     */
    add: function(x, y) {
        var fly = Ext.draw.Point.fly(x, y);
        return new Ext.draw.Point(this.x + fly.x, this.y + fly.y);
    },
    /**
     * Subtracts another vector from this one and returns the resulting vector
     * without changing this vector.
     * @param {Number/Number[]/Object/Ext.draw.Point} x
     * @param {Number/Number[]/Object/Ext.draw.Point} y
     * @return {Ext.draw.Point}
     */
    sub: function(x, y) {
        var fly = Ext.draw.Point.fly(x, y);
        return new Ext.draw.Point(this.x - fly.x, this.y - fly.y);
    },
    /**
     * Returns the result of scalar multiplication of this vector by the given factor.
     * This vector is not modified.
     * @param {Number} n The factor.
     * @return {Ext.draw.Point}
     */
    mul: function(n) {
        return new Ext.draw.Point(this.x * n, this.y * n);
    },
    /**
     * Returns a vector which coordinates are the result of division of this vector's
     * coordinates by the given number. This vector is not modified.
     * This vector is not modified.
     * @param {Number} n The denominator.
     * @return {Ext.draw.Point}
     */
    div: function(n) {
        return new Ext.draw.Point(this.x / n, this.y / n);
    },
    /**
     * Returns the dot product of this vector and the given vector.
     * @param {Number/Number[]/Object/Ext.draw.Point} x
     * @param {Number/Number[]/Object/Ext.draw.Point} y
     * @return {Number}
     */
    dot: function(x, y) {
        var fly = Ext.draw.Point.fly(x, y);
        return this.x * fly.x + this.y * fly.y;
    },
    /**
     * Checks whether coordinates of the point match those of the point provided.
     * @param {Number/Number[]/Object/Ext.draw.Point} x
     * @param {Number/Number[]/Object/Ext.draw.Point} y
     * @return {Boolean}
     */
    equals: function(x, y) {
        var fly = Ext.draw.Point.fly(x, y);
        return this.x === fly.x && this.y === fly.y;
    },
    /**
     * Rotates the point by the given angle. This point is not modified.
     * @param {Number} angle The rotation angle.
     * @param {Ext.draw.Point} [center] The center of rotation (optional). Defaults to origin.
     * @return {Ext.draw.Point} The rotated point.
     */
    rotate: function(angle, center) {
        var sin, cos, cx, cy, point;
        if (this.angleUnits === 'degrees') {
            angle = Ext.draw.Draw.rad(angle);
            sin = Math.sin(angle);
            cos = Math.cos(angle);
        }
        if (center) {
            cx = center.x;
            cy = center.y;
        } else {
            cx = 0;
            cy = 0;
        }
        point = Ext.draw.Matrix.fly([
            cos,
            sin,
            -sin,
            cos,
            cx - cos * cx + cy * sin,
            cy - cos * cy + cx * -sin
        ]).transformPoint(this);
        return new Ext.draw.Point(point);
    },
    /**
     * Transforms the point from one coordinate system to another
     * using the transformation matrix provided. This point is not modified.
     * @param {Ext.draw.Matrix/Number[]} matrix A trasformation matrix or its elements.
     * @return {Ext.draw.Point}
     */
    transform: function(matrix) {
        if (matrix && matrix.isMatrix) {
            return new Ext.draw.Point(matrix.transformPoint(this));
        } else if (arguments.length === 6) {
            return new Ext.draw.Point(Ext.draw.Matrix.fly(arguments).transformPoint(this));
        } else {
            Ext.raise("Invalid parameters.");
        }
    },
    /**
     * Returns a new point with rounded x and y values. This point is not modified.
     * @return {Ext.draw.Point}
     */
    round: function() {
        return new Ext.draw.Point(Math.round(this.x), Math.round(this.y));
    },
    /**
     * Returns a new point with ceiled x and y values. This point is not modified.
     * @return {Ext.draw.Point}
     */
    ceil: function() {
        return new Ext.draw.Point(Math.ceil(this.x), Math.ceil(this.y));
    },
    /**
     * Returns a new point with floored x and y values. This point is not modified.
     * @return {Ext.draw.Point}
     */
    floor: function() {
        return new Ext.draw.Point(Math.floor(this.x), Math.floor(this.y));
    },
    /**
     * Returns a new point with absolute values of the x and y values of this point.
     * This point is not modified.
     * @return {Ext.draw.Point}
     */
    abs: function(x, y) {
        return new Ext.draw.Point(Math.abs(this.x), Math.abs(this.y));
    },
    /**
     * Normalizes the vector by changing its length to 1 without changing its angle.
     * The returned result is a normalized vector. This vector is not modified.
     * @param {Number} [factor=1] Multiplication factor. Defaults to 1.
     * @return {Ext.draw.Point}
     */
    normalize: function(factor) {
        var x = this.x,
            y = this.y,
            k = (factor || 1) / Math.sqrt(x * x + y * y);
        return new Ext.draw.Point(x * k, y * k);
    },
    /**
     * Returns the vector from the point perpendicular to the line (shortest distance).
     * Where line is specified using two points or the coordinates of those points.
     * @param {Ext.draw.Point} p1
     * @param {Ext.draw.Point} p2
     * @return {Ext.draw.Point}
     */
    getDistanceToLine: function(p1, p2) {
        var n, pp1;
        if (arguments.length === 4) {
            p1 = new Ext.draw.Point(arguments[0], arguments[1]);
            p2 = new Ext.draw.Point(arguments[2], arguments[3]);
        }
        // See http://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Vector_formulation
        n = p2.sub(p1).normalize();
        pp1 = p1.sub(this);
        return pp1.sub(n.mul(pp1.dot(n)));
    },
    /**
     * Checks if both x and y coordinates of the point are zero.
     * @return {Boolean}
     */
    isZero: function() {
        return this.x === 0 && this.y === 0;
    },
    /**
     * Checks if both x and y coordinates of the point are valid numbers.
     * @return {Boolean}
     */
    isNumber: function() {
        return Ext.isNumber(this.x) && Ext.isNumber(this.y);
    }
});

/**
 * A draw container {@link Ext.AbstractPlugin plugin} that adds ability to listen
 * to sprite events. For example:
 *
 *     var drawContainer = Ext.create('Ext.draw.Container', {
 *          plugins: {
 *              spriteevents: true
 *          },
 *          renderTo: Ext.getBody(),
 *          width: 200,
 *          height: 200,
 *          sprites: [{
 *               type: 'circle',
 *               fillStyle: '#79BB3F',
 *               r: 50,
 *               x: 100,
 *               y: 100
 *          }],
 *          listeners: {
 *              spriteclick: function (item, event) {
 *                  var sprite = item && item.sprite;
 *                  if (sprite) {
 *                      sprite.setAttributes({fillStyle: 'red'});
                        sprite.getSurface().renderFrame();
 *                  }
 *              }
 *          }
 *     });
 */
Ext.define('Ext.draw.plugin.SpriteEvents', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.spriteevents',
    requires: [
        'Ext.draw.overrides.hittest.All'
    ],
    /**
     * @event spritemousemove
     * Fires when the mouse is moved on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemouseup
     * Fires when a mouseup event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemousedown
     * Fires when a mousedown event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemouseover
     * Fires when the mouse enters a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritemouseout
     * Fires when the mouse exits a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spriteclick
     * Fires when a click event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritedblclick
     * Fires when a double click event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    /**
     * @event spritetap
     * Fires when a tap event occurs on a sprite.
     * @param {Object} sprite
     * @param {Event} event
     */
    mouseMoveEvents: {
        mousemove: true,
        mouseover: true,
        mouseout: true
    },
    spriteMouseMoveEvents: {
        spritemousemove: true,
        spritemouseover: true,
        spritemouseout: true
    },
    init: function(drawContainer) {
        var handleEvent = 'handleEvent';
        this.drawContainer = drawContainer;
        drawContainer.addElementListener({
            click: handleEvent,
            dblclick: handleEvent,
            mousedown: handleEvent,
            mousemove: handleEvent,
            mouseup: handleEvent,
            mouseover: handleEvent,
            mouseout: handleEvent,
            // run our handlers before user code
            priority: 1001,
            scope: this
        });
    },
    hasSpriteMouseMoveListeners: function() {
        var listeners = this.drawContainer.hasListeners,
            name;
        for (name in this.spriteMouseMoveEvents) {
            if (name in listeners) {
                return true;
            }
        }
        return false;
    },
    hitTestEvent: function(e) {
        var items = this.drawContainer.getItems(),
            surface, sprite, i;
        for (i = items.length - 1; i >= 0; i--) {
            surface = items.get(i);
            sprite = surface.hitTestEvent(e);
            if (sprite) {
                return sprite;
            }
        }
        return null;
    },
    handleEvent: function(e) {
        var me = this,
            drawContainer = me.drawContainer,
            isMouseMoveEvent = e.type in me.mouseMoveEvents,
            lastSprite = me.lastSprite,
            sprite;
        if (isMouseMoveEvent && !me.hasSpriteMouseMoveListeners()) {
            return;
        }
        sprite = me.hitTestEvent(e);
        if (isMouseMoveEvent && !Ext.Object.equals(sprite, lastSprite)) {
            if (lastSprite) {
                drawContainer.fireEvent('spritemouseout', lastSprite, e);
            }
            if (sprite) {
                drawContainer.fireEvent('spritemouseover', sprite, e);
            }
        }
        if (sprite) {
            drawContainer.fireEvent('sprite' + e.type, sprite, e);
        }
        me.lastSprite = sprite;
    }
});

/**
 * The ItemInfo interaction allows displaying detailed information about a series data
 * point in a popup panel.
 *
 * To attach this interaction to a chart, include an entry in the chart's
 * {@link Ext.chart.AbstractChart#interactions interactions} config with the `iteminfo` type:
 *
 *     new Ext.chart.AbstractChart({
 *         renderTo: Ext.getBody(),
 *         width: 800,
 *         height: 600,
 *         store: store1,
 *         axes: [ ...some axes options... ],
 *         series: [ ...some series options... ],
 *         interactions: [{
 *             type: 'iteminfo',
 *             listeners: {
 *                 show: function(me, item, panel) {
 *                     panel.setHtml('Stock Price: $' + item.record.get('price'));
 *                 }
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.chart.interactions.ItemInfo', {
    extend: 'Ext.chart.interactions.Abstract',
    type: 'iteminfo',
    alias: 'interaction.iteminfo',
    /**
     * @event show
     * Fires when the info panel is shown.
     * @param {Ext.chart.interactions.ItemInfo} this The interaction instance
     * @param {Object} item The item whose info is being displayed
     * @param {Ext.Panel} panel The panel for displaying the info
     */
    config: {
        /**
         * @cfg {Object} extjsGestures
         * Defines the gestures that should trigger the item info panel to be displayed in ExtJS.
         */
        extjsGestures: {
            'start': {
                event: 'click',
                handler: 'onInfoGesture'
            },
            'move': {
                event: 'mousemove',
                handler: 'onInfoGesture'
            },
            'end': {
                event: 'mouseleave',
                handler: 'onInfoGesture'
            }
        }
    },
    // TODO:ps The trigger above should be 'itemclick', not 'click'.
    item: null,
    onInfoGesture: function(e, element) {
        var me = this,
            item = me.getItemForEvent(e),
            tooltip = item && item.series.tooltip;
        if (tooltip) {
            tooltip.onMouseMove.call(tooltip, e);
        }
        if (item !== me.item) {
            if (item) {
                item.series.showTip(item);
            } else {
                me.item.series.hideTip(me.item);
            }
            me.item = item;
        }
        return false;
    }
});

