/**
 * 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. See
         * {@link Ext.chart.theme.series.Series}
         */
        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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 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 (= 0, ln = directions.length; i < ln; i++) {
            direction = directions[i];
            directionToFieldsMap[direction] = me.getFields(me['fieldCategory' + direction]);
        }
 
        for (= 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 (= 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 (= 0; i < directionAxes.length; i++) {
            axis = directionAxes[i];
            axisFields = axis.getFields();
 
            if (!axisFields.length) {
                return axis;
            }
            else if (directionFields) {
                for (= 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 (= 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 (= 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, labelMarker, category, categoryInstances, hidden;
 
        for (= 0; i < ln; i++) {
            sprite = sprites[i];
            category = sprite.id;
            hidden = sprite.attr.hidden;
 
            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));
            }
 
            labelMarker = sprite.getMarker('labels');
 
            if (Ext.isDefined(hidden) && labelMarker) {
                categoryInstances = labelMarker.categories[category];
 
                if (!Ext.Object.isEmpty(categoryInstances)) {
                    Ext.Object.eachValue(categoryInstances, function(value) {
                        labelMarker.setAttributesFor(value, { hidden: hidden });
                    });
                }
            }
        }
    },
 
    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[% 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 (= 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();
    }
});