/**
 * @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 (= 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 (= 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 + '-' + (% 2 ? 'odd' : 'even'), {
                            y: anchor,
                            height: lastAnchor - anchor
                        }, j = i, true);
                        lastAnchor = anchor;
                    });
                    j++;
                    anchor = 0;
                    me.putMarker(alignment + '-' + (% 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 + '-' + (% 2 ? 'odd' : 'even'), {
                            x: anchor,
                            width: lastAnchor - anchor
                        }, j = i, true);
                        lastAnchor = anchor;
                    });
                    j++;
                    anchor = attr.length + attr.startGap + attr.endGap;
                    me.putMarker(alignment + '-' + (% 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 + '-' + (% 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 + '-' + (% 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 (= 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 (= 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 (= 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 (= 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();
        }
    }
});