/* *
 *
 *  (c) 2010-2026 Highsoft AS
 *  Author: Torstein Hønsi
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 * */
'use strict';
import A from '../Animation/AnimationUtilities.js';
const { animObject } = A;
import AxisDefaults from './AxisDefaults.js';
const { xAxis, yAxis } = AxisDefaults;
import Color from '../Color/Color.js';
const { parse: color } = Color;
import D from '../Defaults.js';
const { defaultOptions } = D;
import F from '../Foundation.js';
const { registerEventOptions } = F;
import H from '../Globals.js';
const { deg2rad } = H;
import Tick from './Tick.js';
import { arrayMax, arrayMin, clamp, correctFloat, defined, destroyObjectProperties, erase, extend, fireEvent, getClosestDistance, isArray, isNumber, isString, merge, normalizeTickInterval, objectEach, pick, relativeLength, removeEvent, splat, syncTimeout } from '../../Shared/Utilities.js';
import { error, insertItem } from '../Utilities.js';
const getNormalizedTickInterval = (axis, tickInterval) => normalizeTickInterval(tickInterval, void 0, void 0, pick(axis.options.allowDecimals, 
// If the tick interval is greater than 0.5, avoid decimals, as
// linear axes are often used to render discrete values (#3363). If
// a tick amount is set, allow decimals by default, as it increases
// the chances for a good fit.
tickInterval < 0.5 || axis.tickAmount !== void 0), !!axis.tickAmount);
extend(defaultOptions, { xAxis, yAxis: merge(xAxis, yAxis) });
/* *
 *
 *  Class
 *
 * */
/**
 * Create a new axis object. Called internally when instantiating a new chart or
 * adding axes by {@link Highcharts.Chart#addAxis}.
 *
 * A chart can have from 0 axes (pie chart) to multiples. In a normal, single
 * series cartesian chart, there is one X axis and one Y axis.
 *
 * The X axis or axes are referenced by {@link Highcharts.Chart.xAxis}, which is
 * an array of Axis objects. If there is only one axis, it can be referenced
 * through `chart.xAxis[0]`, and multiple axes have increasing indices. The same
 * pattern goes for Y axes.
 *
 * If you need to get the axes from a series object, use the `series.xAxis` and
 * `series.yAxis` properties. These are not arrays, as one series can only be
 * associated to one X and one Y axis.
 *
 * A third way to reference the axis programmatically is by `id`. Add an `id` in
 * the axis configuration options, and get the axis by
 * {@link Highcharts.Chart#get}.
 *
 * Configuration options for the axes are given in options.xAxis and
 * options.yAxis.
 *
 * @class
 * @name Highcharts.Axis
 *
 * @param {Highcharts.Chart} chart
 * The Chart instance to apply the axis on.
 *
 * @param {Highcharts.AxisOptions} userOptions
 * Axis options
 */
class Axis {
    /* *
     *
     *  Constructors
     *
     * */
    constructor(chart, userOptions, coll) {
        /**
         * Whether this axis is allowed to be clipped to the plot area.
         * @internal
         */
        this.clippable = true;
        this.init(chart, userOptions, coll);
    }
    /* *
     *
     *  Functions
     *
     * */
    /**
     * Overridable function to initialize the axis.
     *
     * @see {@link Axis}
     *
     * @function Highcharts.Axis#init
     *
     * @param {Highcharts.Chart} chart
     * The Chart instance to apply the axis on.
     *
     * @param {AxisOptions} userOptions
     * Axis options.
     *
     * @emits Highcharts.Axis#event:afterInit
     * @emits Highcharts.Axis#event:init
     */
    init(chart, userOptions, coll = this.coll) {
        const isXAxis = coll === 'xAxis', axis = this, horiz = axis.isZAxis || (chart.inverted ? !isXAxis : isXAxis);
        /**
         * The Chart that the axis belongs to.
         *
         * @name Highcharts.Axis#chart
         * @type {Highcharts.Chart}
         */
        axis.chart = chart;
        /**
         * Whether the axis is horizontal.
         *
         * @name Highcharts.Axis#horiz
         * @type {boolean|undefined}
         */
        axis.horiz = horiz;
        /**
         * Whether the axis is the x-axis.
         *
         * @name Highcharts.Axis#isXAxis
         * @type {boolean|undefined}
         */
        axis.isXAxis = isXAxis;
        /**
         * The collection where the axis belongs, for example `xAxis`, `yAxis`
         * or `colorAxis`. Corresponds to properties on Chart, for example
         * {@link Chart.xAxis}.
         *
         * @name Highcharts.Axis#coll
         * @type {string}
         */
        axis.coll = coll;
        fireEvent(this, 'init', { userOptions: userOptions });
        // Needed in setOptions
        axis.opposite = pick(userOptions.opposite, axis.opposite);
        /**
         * The side on which the axis is rendered. 0 is top, 1 is right, 2
         * is bottom and 3 is left.
         *
         * @name Highcharts.Axis#side
         * @type {number}
         */
        axis.side = pick(userOptions.side, axis.side, (horiz ?
            (axis.opposite ? 0 : 2) : // Top : bottom
            (axis.opposite ? 1 : 3)) // Right : left
        );
        /**
         * Current options for the axis after merge of defaults and user's
         * options.
         *
         * @name Highcharts.Axis#options
         * @type {Highcharts.AxisOptions}
         */
        axis.setOptions(userOptions);
        const options = axis.options, labelsOptions = options.labels;
        // Set the type and fire an event
        axis.type ?? (axis.type = options.type || 'linear');
        axis.uniqueNames ?? (axis.uniqueNames = options.uniqueNames ?? true);
        fireEvent(axis, 'afterSetType');
        /**
         * User's options for this axis without defaults.
         *
         * @name Highcharts.Axis#userOptions
         * @type {Highcharts.AxisOptions}
         */
        axis.userOptions = userOptions;
        axis.minPixelPadding = 0;
        /**
         * Whether the axis is reversed. Based on the `axis.reversed`,
         * option, but inverted charts have reversed xAxis by default.
         *
         * @name Highcharts.Axis#reversed
         * @type {boolean}
         */
        axis.reversed = pick(options.reversed, axis.reversed);
        axis.visible = options.visible;
        axis.zoomEnabled = options.zoomEnabled;
        // Initial categories
        axis.hasNames = this.type === 'category' || options.categories === true;
        /**
         * If categories are present for the axis, names are used instead of
         * numbers for that axis.
         *
         * Since Highcharts 3.0, categories can also be extracted by giving each
         * point a name and setting axis type to `category`. However, if you
         * have multiple series, best practice remains defining the `categories`
         * array.
         *
         * @see [xAxis.categories](/highcharts/xAxis.categories)
         *
         * @name Highcharts.Axis#categories
         * @type {Array<string>}
         * @readonly
         */
        axis.categories = (isArray(options.categories) && options.categories) ||
            (axis.hasNames ? [] : void 0);
        // Axis names and its map for quick access. Backwards mapping is much
        // faster than array searching (#7725). Preserve on update (#3830)
        axis.names || (axis.names = []);
        axis.namesMap || (axis.namesMap = {});
        // Placeholder for plotLines and plotBands groups
        axis.plotLinesAndBandsGroups = {};
        // Shorthand types
        axis.positiveValuesOnly = !!axis.logarithmic;
        // Flag, if axis is linked to another axis
        axis.isLinked = defined(options.linkedTo);
        /**
         * List of major ticks mapped by position on axis.
         *
         * @see {@link Highcharts.Tick}
         *
         * @name Highcharts.Axis#ticks
         * @type {Highcharts.Dictionary<Highcharts.Tick>}
         */
        axis.ticks = {};
        axis.labelEdge = [];
        /**
         * List of minor ticks mapped by position on the axis.
         *
         * @see {@link Highcharts.Tick}
         *
         * @name Highcharts.Axis#minorTicks
         * @type {Highcharts.Dictionary<Highcharts.Tick>}
         */
        axis.minorTicks = {};
        // List of plotLines/Bands
        axis.plotLinesAndBands = [];
        // Alternate bands
        axis.alternateBands = {};
        /**
         * The length of the axis in terms of pixels.
         *
         * @name Highcharts.Axis#len
         * @type {number}
         */
        axis.len ?? (axis.len = 0);
        axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
        axis.range = options.range;
        /**
         * The maximum value of the axis. In a logarithmic axis, this is the
         * logarithm of the real value, and the real value can be obtained from
         * {@link Axis#getExtremes}.
         *
         * @name Highcharts.Axis#max
         * @type {number|undefined}
         */
        axis.max = void 0;
        /**
         * The minimum value of the axis. In a logarithmic axis, this is the
         * logarithm of the real value, and the real value can be obtained from
         * {@link Axis#getExtremes}.
         *
         * @name Highcharts.Axis#min
         * @type {number|undefined}
         */
        axis.min = void 0;
        /**
         * The processed crosshair options.
         *
         * @name Highcharts.Axis#crosshair
         * @type {boolean|Highcharts.AxisCrosshairOptions}
         */
        const crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1]);
        axis.crosshair = crosshair === true ? {} : crosshair;
        // Register. Don't add it again on Axis.update().
        if (chart.axes.indexOf(axis) === -1) { //
            if (isXAxis) { // #2713
                chart.axes.splice(chart.xAxis.length, 0, axis);
            }
            else {
                chart.axes.push(axis);
            }
            insertItem(this, chart[this.coll]);
        }
        chart.orderItems(axis.coll);
        /**
         * All series associated to the axis.
         *
         * @name Highcharts.Axis#series
         * @type {Array<Highcharts.Series>}
         */
        axis.series = axis.series || []; // Populated by Series
        // Reversed axis
        if (chart.inverted &&
            !axis.isZAxis &&
            isXAxis &&
            !defined(axis.reversed)) {
            axis.reversed = true;
        }
        axis.labelRotation = isNumber(labelsOptions.rotation) ?
            labelsOptions.rotation :
            void 0;
        // Register event listeners
        registerEventOptions(axis, options);
        fireEvent(this, 'afterInit');
    }
    /**
     * Merge and set options.
     *
     * @internal
     * @function Highcharts.Axis#setOptions
     *
     * @param {Highcharts.AxisOptions} userOptions
     * Axis options.
     *
     * @emits Highcharts.Axis#event:afterSetOptions
     */
    setOptions(userOptions) {
        const sideSpecific = this.horiz ?
            // Top and bottom axis defaults
            {
                labels: {
                    autoRotation: [-45],
                    padding: 3
                },
                margin: 15
            } :
            // Left and right axis, title rotated 90 or 270 degrees
            // respectively
            {
                labels: {
                    padding: 1
                },
                title: {
                    rotation: 90 * this.side
                }
            };
        this.options = merge(sideSpecific, 
        // Merge in the default title for y-axis, which changes with
        // language settings
        this.coll === 'yAxis' ? {
            title: {
                text: this.chart.options.lang.yAxisTitle
            }
        } : {}, defaultOptions[this.coll], userOptions);
        fireEvent(this, 'afterSetOptions', { userOptions });
    }
    /**
     * The default label formatter. The context is a special config object for
     * the label. In apps, use the
     * [labels.formatter](https://api.highcharts.com/highcharts/xAxis.labels.formatter)
     * instead, except when a modification is needed.
     *
     * @function Highcharts.Axis#defaultLabelFormatter
     *
     * @param {Highcharts.AxisLabelsFormatterContextObject} this
     * Formatter context of axis label.
     *
     * @return {string}
     * The formatted label content.
     */
    defaultLabelFormatter() {
        const axis = this.axis, chart = this.chart, { numberFormatter } = chart, value = isNumber(this.value) ? this.value : NaN, time = axis.chart.time, categories = axis.categories, dateTimeLabelFormat = this.dateTimeLabelFormat, lang = defaultOptions.lang, numericSymbols = lang.numericSymbols, numSymMagnitude = lang.numericSymbolMagnitude || 1000, 
        // Make sure the same symbol is added for all labels on a linear
        // axis
        numericSymbolDetector = axis.logarithmic ?
            Math.abs(value) :
            axis.tickInterval;
        let i = numericSymbols?.length, multi, ret;
        if (categories) {
            ret = `${this.value}`;
        }
        else if (dateTimeLabelFormat) { // Datetime axis
            ret = time.dateFormat(dateTimeLabelFormat, value, true);
        }
        else if (i &&
            numericSymbols &&
            numericSymbolDetector >= 1000) {
            // Decide whether we should add a numeric symbol like k (thousands)
            // or M (millions). If we are to enable this in tooltip or other
            // places as well, we can move this logic to the numberFormatter and
            // enable it by a parameter.
            while (i-- && typeof ret === 'undefined') {
                multi = Math.pow(numSymMagnitude, i + 1);
                if (
                // Only accept a numeric symbol when the distance is more
                // than a full unit. So for example if the symbol is k, we
                // don't accept numbers like 0.5k.
                numericSymbolDetector >= multi &&
                    // Accept one decimal before the symbol. Accepts 0.5k but
                    // not 0.25k. How does this work with the previous?
                    (value * 10) % multi === 0 &&
                    numericSymbols[i] !== null &&
                    value !== 0) { // #5480
                    ret = numberFormatter(value / multi, -1, void 0, void 0, chart) + numericSymbols[i];
                }
            }
        }
        ret ?? (ret = numberFormatter(value, -1, void 0, 
        // Add thousands separators when 10 000 or more
        Math.abs(value) < 10000 ? '' : void 0, chart));
        return ret;
    }
    /**
     * Get the minimum and maximum for the series of each axis. The function
     * analyzes the axis series and updates `this.dataMin` and `this.dataMax`.
     *
     * @internal
     * @function Highcharts.Axis#getSeriesExtremes
     *
     * @emits Highcharts.Axis#event:afterGetSeriesExtremes
     * @emits Highcharts.Axis#event:getSeriesExtremes
     */
    getSeriesExtremes() {
        const axis = this;
        let xExtremes;
        fireEvent(this, 'getSeriesExtremes', null, function () {
            axis.hasVisibleSeries = false;
            // Reset properties in case we're redrawing (#3353)
            axis.dataMin = axis.dataMax = axis.threshold = void 0;
            axis.softThreshold = !axis.isXAxis;
            // Loop through this axis' series
            axis.series.forEach((series) => {
                if (series.reserveSpace()) {
                    const seriesOptions = series.options;
                    let xData, threshold = seriesOptions.threshold, seriesDataMin, seriesDataMax;
                    axis.hasVisibleSeries = true;
                    // Validate threshold in logarithmic axes
                    if (axis.positiveValuesOnly && (threshold || 0) <= 0) {
                        threshold = void 0;
                    }
                    // Get dataMin and dataMax for X axes
                    if (axis.isXAxis) {
                        xData = series.getColumn('x');
                        if (xData.length) {
                            xData = axis.logarithmic ?
                                xData.filter((x) => x > 0) :
                                xData;
                            xExtremes = series.getXExtremes(xData);
                            // If xData contains values which is not numbers,
                            // then filter them out. To prevent performance hit,
                            // we only do this after we have already found
                            // seriesDataMin because in most cases all data is
                            // valid. #5234.
                            seriesDataMin = xExtremes.min;
                            seriesDataMax = xExtremes.max;
                            if (!isNumber(seriesDataMin) &&
                                // #5010:
                                !(seriesDataMin instanceof Date)) {
                                xData = xData.filter(isNumber);
                                xExtremes = series.getXExtremes(xData);
                                // Do it again with valid data
                                seriesDataMin = xExtremes.min;
                                seriesDataMax = xExtremes.max;
                            }
                            if (xData.length) {
                                axis.dataMin = Math.min(pick(axis.dataMin, seriesDataMin), seriesDataMin);
                                axis.dataMax = Math.max(pick(axis.dataMax, seriesDataMax), seriesDataMax);
                            }
                        }
                        // Get dataMin and dataMax for Y axes, as well as handle
                        // stacking and processed data
                    }
                    else {
                        // Get this particular series extremes
                        const dataExtremes = series.applyExtremes();
                        // Get the dataMin and dataMax so far. If percentage is
                        // used, the min and max are always 0 and 100. If
                        // seriesDataMin and seriesDataMax is null, then series
                        // doesn't have active y data, we continue with nulls
                        if (isNumber(dataExtremes.dataMin)) {
                            seriesDataMin = dataExtremes.dataMin;
                            axis.dataMin = Math.min(pick(axis.dataMin, seriesDataMin), seriesDataMin);
                        }
                        if (isNumber(dataExtremes.dataMax)) {
                            seriesDataMax = dataExtremes.dataMax;
                            axis.dataMax = Math.max(pick(axis.dataMax, seriesDataMax), seriesDataMax);
                        }
                        // Adjust to threshold
                        if (defined(threshold)) {
                            axis.threshold = threshold;
                        }
                        // If any series has a hard threshold, it takes
                        // precedence
                        if (!seriesOptions.softThreshold ||
                            axis.positiveValuesOnly) {
                            axis.softThreshold = false;
                        }
                    }
                }
            });
        });
        fireEvent(this, 'afterGetSeriesExtremes');
    }
    /**
     * Translate from axis value to pixel position on the chart, or back. Use
     * the `toPixels` and `toValue` functions in applications.
     *
     * @internal
     * @function Highcharts.Axis#translate
     */
    translate(val, backwards, cvsCoord, old, handleLog, pointPlacement) {
        const axis = (this.linkedParent || this), // #1417
        localMin = (old && axis.old ? axis.old.min : axis.min);
        if (!isNumber(localMin)) {
            return NaN;
        }
        const minPixelPadding = axis.minPixelPadding, doPostTranslate = (axis.isOrdinal ||
            axis.brokenAxis?.hasBreaks ||
            (axis.logarithmic && handleLog)) && !!axis.lin2val;
        let sign = 1, cvsOffset = 0, localA = old && axis.old ? axis.old.transA : axis.transA, returnValue = 0;
        if (!localA) {
            localA = axis.transA;
        }
        // In vertical axes, the canvas coordinates start from 0 at the top like
        // in SVG.
        if (cvsCoord) {
            sign *= -1; // Canvas coordinates inverts the value
            cvsOffset = axis.len;
        }
        // Handle reversed axis
        if (axis.reversed) {
            sign *= -1;
            cvsOffset -= sign * (axis.sector || axis.len);
        }
        // From pixels to value
        if (backwards) { // Reverse translation
            val = val * sign + cvsOffset;
            val -= minPixelPadding;
            // From chart pixel to value:
            returnValue = val / localA + localMin;
            if (doPostTranslate) { // Log, ordinal and broken axis
                returnValue = axis.lin2val(returnValue);
            }
            // From value to pixels
        }
        else {
            if (doPostTranslate) { // Log, ordinal and broken axis
                val = axis.val2lin(val);
            }
            const value = sign * (val - localMin) * localA;
            returnValue = value +
                cvsOffset +
                (sign * minPixelPadding) +
                (isNumber(pointPlacement) ? localA * pointPlacement : 0);
            if (!axis.isRadial) {
                returnValue = correctFloat(returnValue);
            }
        }
        return returnValue;
    }
    /**
     * Translate a value in terms of axis units into pixels within the chart.
     *
     * @function Highcharts.Axis#toPixels
     *
     * @param {number|string} value
     * A value in terms of axis units. For datetime axes, a timestamp or
     * date/time string is accepted.
     *
     * @param {boolean} [paneCoordinates=false]
     * Whether to return the pixel coordinate relative to the chart or just the
     * axis/pane itself.
     *
     * @return {number}
     * Pixel position of the value on the chart or axis.
     */
    toPixels(value, paneCoordinates) {
        return this.translate(this.chart?.time.parse(value) ?? NaN, false, !this.horiz, void 0, true) + (paneCoordinates ? 0 : this.pos);
    }
    /**
     * Translate a pixel position along the axis to a value in terms of axis
     * units.
     *
     * @function Highcharts.Axis#toValue
     *
     * @param {number} pixel
     * The pixel value coordinate.
     *
     * @param {boolean} [paneCoordinates=false]
     * Whether the input pixel is relative to the chart or just the axis/pane
     * itself.
     *
     * @return {number}
     * The axis value.
     */
    toValue(pixel, paneCoordinates) {
        return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, void 0, true);
    }
    /**
     * Create the path for a plot line that goes from the given value on
     * this axis, across the plot to the opposite side. Also used internally for
     * grid lines and crosshairs.
     *
     * @function Highcharts.Axis#getPlotLinePath
     *
     * @param {Highcharts.AxisPlotLinePathOptionsObject} options
     * Options for the path.
     *
     * @return {Highcharts.SVGPathArray|null}
     * The SVG path definition for the plot line.
     */
    getPlotLinePath(options) {
        const axis = this, chart = axis.chart, axisLeft = axis.left, axisTop = axis.top, old = options.old, value = options.value, lineWidth = options.lineWidth, cHeight = (old && chart.oldChartHeight) || chart.chartHeight, cWidth = (old && chart.oldChartWidth) || chart.chartWidth, transB = axis.transB;
        let translatedValue = options.translatedValue, force = options.force, x1, y1, x2, y2, skip;
        // eslint-disable-next-line valid-jsdoc
        /**
         * Check if x is between a and b. If not, either move to a/b
         * or skip, depending on the force parameter.
         * @internal
         */
        function between(x, a, b) {
            if (force !== 'pass' && (x < a || x > b)) {
                if (force) {
                    x = clamp(x, a, b);
                }
                else {
                    skip = true;
                }
            }
            return x;
        }
        const evt = {
            value,
            lineWidth,
            old,
            force,
            acrossPanes: options.acrossPanes,
            translatedValue
        };
        fireEvent(this, 'getPlotLinePath', evt, function (e) {
            translatedValue = pick(translatedValue, axis.translate(value, void 0, void 0, old));
            // Keep the translated value within sane bounds, and avoid Infinity
            // to fail the isNumber test (#7709).
            translatedValue = clamp(translatedValue, -1e9, 1e9);
            x1 = x2 = translatedValue + transB;
            y1 = y2 = cHeight - translatedValue - transB;
            if (!isNumber(translatedValue)) { // No min or max
                skip = true;
                force = false; // #7175, don't force it when path is invalid
            }
            else if (axis.horiz) {
                y1 = axisTop;
                y2 = cHeight - axis.bottom + (axis.options.isInternal ?
                    0 :
                    (chart.scrollablePixelsY || 0)); // #20354, scrollablePixelsY shouldn't be used for navigator
                x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
            }
            else {
                x1 = axisLeft;
                x2 = cWidth - axis.right + (chart.scrollablePixelsX || 0);
                y1 = y2 = between(y1, axisTop, axisTop + axis.height);
            }
            e.path = skip && !force ?
                void 0 :
                chart.renderer.crispLine([['M', x1, y1], ['L', x2, y2]], lineWidth || 1);
        });
        return evt.path;
    }
    /**
     * Internal function to get the tick positions of a linear axis to round
     * values like whole tens or every five.
     *
     * @function Highcharts.Axis#getLinearTickPositions
     *
     * @param {number} tickInterval
     * The normalized tick interval.
     *
     * @param {number} min
     * Axis minimum.
     *
     * @param {number} max
     * Axis maximum.
     *
     * @return {Array<number>}
     * An array of axis values where ticks should be placed.
     */
    getLinearTickPositions(tickInterval, min, max) {
        const roundedMin = correctFloat(Math.floor(min / tickInterval) * tickInterval), roundedMax = correctFloat(Math.ceil(max / tickInterval) * tickInterval), tickPositions = [];
        let pos, lastPos, precision;
        // When the precision is higher than what we filter out in
        // correctFloat, skip it (#6183).
        if (correctFloat(roundedMin + tickInterval) === roundedMin) {
            precision = 20;
        }
        // For single points, add a tick regardless of the relative position
        // (#2662, #6274)
        if (this.single) {
            return [min];
        }
        // Populate the intermediate values
        pos = roundedMin;
        while (pos <= roundedMax) {
            // Place the tick on the rounded value
            tickPositions.push(pos);
            // Always add the raw tickInterval, not the corrected one.
            pos = correctFloat(pos + tickInterval, precision);
            // If the interval is not big enough in the current min - max range
            // to actually increase the loop variable, we need to break out to
            // prevent endless loop. Issue #619
            if (pos === lastPos) {
                break;
            }
            // Record the last value
            lastPos = pos;
        }
        return tickPositions;
    }
    /**
     * Resolve the new minorTicks/minorTickInterval options into the legacy
     * loosely typed minorTickInterval option.
     *
     * @function Highcharts.Axis#getMinorTickInterval
     *
     * @return {number|"auto"|null}
     * Legacy option
     */
    getMinorTickInterval() {
        const { minorTicks, minorTickInterval } = this.options;
        if (minorTicks === true) {
            return pick(minorTickInterval, 'auto');
        }
        if (minorTicks === false) {
            return;
        }
        return minorTickInterval;
    }
    /**
     * Internal function to return the minor tick positions. For logarithmic
     * axes, the same logic as for major ticks is reused.
     *
     * @function Highcharts.Axis#getMinorTickPositions
     *
     * @return {Array<number>}
     * An array of axis values where ticks should be placed.
     */
    getMinorTickPositions() {
        const axis = this, options = axis.options, tickPositions = axis.tickPositions, minorTickInterval = axis.minorTickInterval, pointRangePadding = axis.pointRangePadding || 0, min = (axis.min || 0) - pointRangePadding, // #1498
        max = (axis.max || 0) + pointRangePadding, // #1498
        range = axis.brokenAxis?.hasBreaks ?
            axis.brokenAxis.unitLength : max - min;
        let minorTickPositions = [], pos;
        // If minor ticks get too dense, they are hard to read, and may cause
        // long running script. So we don't draw them.
        if (range && range / minorTickInterval < axis.len / 3) { // #3875
            const logarithmic = axis.logarithmic;
            if (logarithmic) {
                // For each interval in the major ticks, compute the minor ticks
                // separately.
                this.paddedTicks.forEach(function (_pos, i, paddedTicks) {
                    if (i) {
                        minorTickPositions.push.apply(minorTickPositions, logarithmic.getLogTickPositions(minorTickInterval, paddedTicks[i - 1], paddedTicks[i], true));
                    }
                });
            }
            else if (axis.dateTime &&
                this.getMinorTickInterval() === 'auto') { // #1314
                minorTickPositions = minorTickPositions.concat(axis.getTimeTicks(axis.dateTime.normalizeTimeTickInterval(minorTickInterval), min, max, options.startOfWeek));
            }
            else {
                for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
                    // Very, very, tight grid lines (#5771)
                    if (pos === minorTickPositions[0]) {
                        break;
                    }
                    minorTickPositions.push(pos);
                }
            }
        }
        if (minorTickPositions.length !== 0) {
            axis.trimTicks(minorTickPositions); // #3652 #3743 #1498 #6330
        }
        return minorTickPositions;
    }
    /**
     * Adjust the min and max for the minimum range. Keep in mind that the
     * series data is not yet processed, so we don't have information on data
     * cropping and grouping, or updated `axis.pointRange` or
     * `series.pointRange`. The data can't be processed until we have finally
     * established min and max.
     *
     * @internal
     * @function Highcharts.Axis#adjustForMinRange
     */
    adjustForMinRange() {
        const axis = this, options = axis.options, logarithmic = axis.logarithmic, time = axis.chart.time;
        let { max, min, minRange } = axis, zoomOffset, spaceAvailable, closestDataRange, minArgs, maxArgs;
        // Set the automatic minimum range based on the closest point distance
        if (axis.isXAxis &&
            typeof minRange === 'undefined' &&
            !logarithmic) {
            if (defined(options.min) ||
                defined(options.max) ||
                defined(options.floor) ||
                defined(options.ceiling)) {
                // Setting it to null, as opposed to undefined, signals we don't
                // run this block again as per the condition above.
                minRange = null;
            }
            else {
                // Find the closest distance between raw data points, as opposed
                // to closestPointRange that applies to processed points
                // (cropped and grouped)
                closestDataRange = getClosestDistance(axis.series.map((s) => {
                    const xData = s.getColumn('x');
                    // If xIncrement, we only need to measure the two first
                    // points to get the distance. Saves processing time.
                    return s.xIncrement ? xData.slice(0, 2) : xData;
                })) || 0;
                minRange = Math.min(closestDataRange * 5, axis.dataMax - axis.dataMin);
            }
        }
        // If minRange is exceeded, adjust
        if (isNumber(max) &&
            isNumber(min) &&
            isNumber(minRange) &&
            max - min < minRange) {
            spaceAvailable =
                axis.dataMax - axis.dataMin >=
                    minRange;
            zoomOffset = (minRange - max + min) / 2;
            // If min and max options have been set, don't go beyond it
            minArgs = [
                min - zoomOffset,
                time.parse(options.min) ?? (min - zoomOffset)
            ];
            // If space is available, stay within the data range
            if (spaceAvailable) {
                minArgs[2] = logarithmic ?
                    logarithmic.log2lin(axis.dataMin) :
                    axis.dataMin;
            }
            min = arrayMax(minArgs);
            maxArgs = [
                min + minRange,
                time.parse(options.max) ?? (min + minRange)
            ];
            // If space is available, stay within the data range
            if (spaceAvailable) {
                maxArgs[2] = logarithmic ?
                    logarithmic.log2lin(axis.dataMax) :
                    axis.dataMax;
            }
            max = arrayMin(maxArgs);
            // Now if the max is adjusted, adjust the min back
            if (max - min < minRange) {
                minArgs[0] = max - minRange;
                minArgs[1] = time.parse(options.min) ?? (max - minRange);
                min = arrayMax(minArgs);
            }
        }
        // Record modified extremes
        axis.minRange = minRange;
        axis.min = min;
        axis.max = max;
    }
    /**
     * Find the closestPointRange across all series, including the single data
     * series.
     *
     * @internal
     * @function Highcharts.Axis#getClosest
     */
    getClosest() {
        let closestSingleDistance, closestDistance;
        if (this.categories) {
            closestDistance = 1;
        }
        else {
            const singleXs = [];
            this.series.forEach(function (series) {
                const seriesClosest = series.closestPointRange, xData = series.getColumn('x');
                if (xData.length === 1) {
                    singleXs.push(xData[0]);
                }
                else if (series.sorted &&
                    defined(seriesClosest) &&
                    series.reserveSpace()) {
                    closestDistance = defined(closestDistance) ?
                        Math.min(closestDistance, seriesClosest) :
                        seriesClosest;
                }
            });
            if (singleXs.length) {
                singleXs.sort((a, b) => a - b);
                closestSingleDistance = getClosestDistance([singleXs]);
            }
        }
        if (closestSingleDistance && closestDistance) {
            return Math.min(closestSingleDistance, closestDistance);
        }
        return closestSingleDistance || closestDistance;
    }
    /**
     * When a point name is given and no x, search for the name in the existing
     * categories, or if categories aren't provided, search names or create a
     * new category (#2522).
     *
     * @internal
     * @function Highcharts.Axis#nameToX
     *
     * @param {Highcharts.Point} point
     * The point to inspect.
     *
     * @return {number}
     * The X value that the point is given.
     */
    nameToX(point, nameX) {
        const explicitCategories = isArray(this.options.categories), { name, series } = point, names = explicitCategories ? this.categories : this.names, namesMap = this.namesMap;
        let x;
        series.requireSorting = false;
        if (!defined(nameX)) {
            nameX = this.uniqueNames && names ?
                (explicitCategories ?
                    names.indexOf(name) :
                    (namesMap[name] ?? -1)) :
                series.autoIncrement();
        }
        if (nameX === -1) { // Not found in current categories
            if (!explicitCategories && names) {
                x = names.length;
            }
        }
        else if (isNumber(nameX)) {
            x = nameX;
        }
        // Write the last point's name to the names array
        if (typeof x !== 'undefined') {
            this.names[x] = point.name;
            namesMap[point.name] = x;
        }
        else if (isNumber(point.x)) {
            x = point.x; // #17438
        }
        else {
            x = series.autoIncrement();
        }
        return x;
    }
    /**
     * When changes have been done to series data, update the axis.names.
     *
     * @internal
     * @function Highcharts.Axis#updateNames
     */
    updateNames() {
        const axis = this, { names, namesMap } = this, i = names.length;
        if (i > 0) {
            Object.keys(namesMap).forEach(function (key) {
                delete namesMap[key];
            });
            names.length = 0;
            this.minRange = this.userMinRange; // Reset
            (this.series || []).forEach((series) => {
                // Reset incrementer (#5928)
                series.xIncrement = null;
                delete series.xColumn;
                // When adding a series, points are not yet generated
                if (!series.points || series.isDirtyData) {
                    // When we're updating the series with data that is longer
                    // than it was, and cropThreshold is passed, we need to make
                    // sure that the axis.max is increased _before_ running the
                    // premature processData. Otherwise this early iteration of
                    // processData will crop the points to axis.max, and the
                    // names array will be too short (#5857).
                    axis.max = Math.max(axis.max || 0, series.dataTable.rowCount - 1);
                    series.processData();
                    series.generatePoints();
                }
                else {
                    const xData = series.getColumn('x');
                    series.points?.forEach((point) => {
                        point.x = xData[point.index];
                    });
                }
            });
        }
    }
    /**
     * Update translation information.
     *
     * @internal
     * @function Highcharts.Axis#setAxisTranslation
     *
     * @emits Highcharts.Axis#event:afterSetAxisTranslation
     */
    setAxisTranslation() {
        const axis = this, range = axis.max - axis.min, linkedParent = axis.linkedParent, hasCategories = !!axis.categories, isXAxis = axis.isXAxis;
        let pointRange = axis.axisPointRange || 0, closestPointRange, minPointOffset = 0, pointRangePadding = 0, ordinalCorrection, transA = axis.transA;
        // Adjust translation for padding. Y axis with categories need to go
        // through the same (#1784).
        if (isXAxis || hasCategories || pointRange) {
            // Get the closest points
            closestPointRange = axis.getClosest();
            if (linkedParent) {
                minPointOffset = linkedParent.minPointOffset;
                pointRangePadding = linkedParent.pointRangePadding;
            }
            else {
                axis.series.forEach(function (series) {
                    const seriesPointRange = hasCategories ?
                        1 :
                        (isXAxis ?
                            pick(series.options.pointRange, closestPointRange, 0) :
                            (axis.axisPointRange || 0)), // #2806
                    pointPlacement = series.options.pointPlacement;
                    pointRange = Math.max(pointRange, seriesPointRange);
                    if (!axis.single || hasCategories) {
                        // TODO: series should internally set x- and y-
                        // pointPlacement to simplify this logic.
                        const isPointPlacementAxis = series.is('xrange') ?
                            !isXAxis :
                            isXAxis;
                        // The `minPointOffset` is the value padding to the left
                        // of the axis in order to make room for points with a
                        // pointRange, typically columns, or line/scatter points
                        // on a category axis. When the `pointPlacement` option
                        // is 'between' or 'on', this padding does not apply.
                        minPointOffset = Math.max(minPointOffset, isPointPlacementAxis && isString(pointPlacement) ?
                            0 :
                            seriesPointRange / 2);
                        // Determine the total padding needed to the length of
                        // the axis to make room for the pointRange. If the
                        // series' pointPlacement is 'on', no padding is added.
                        pointRangePadding = Math.max(pointRangePadding, isPointPlacementAxis && pointPlacement === 'on' ?
                            0 :
                            seriesPointRange);
                    }
                });
            }
            // Record minPointOffset and pointRangePadding
            ordinalCorrection = (axis.ordinal?.slope && closestPointRange) ?
                axis.ordinal.slope / closestPointRange :
                1; // #988, #1853
            axis.minPointOffset = minPointOffset =
                minPointOffset * ordinalCorrection;
            axis.pointRangePadding =
                pointRangePadding = pointRangePadding * ordinalCorrection;
            // The `pointRange` is the width reserved for each point, like in a
            // column chart
            axis.pointRange = Math.min(pointRange, axis.single && hasCategories ? 1 : range);
            // The `closestPointRange` is the closest distance between points.
            // In columns it is mostly equal to pointRange, but in lines
            // pointRange is 0 while closestPointRange is some other value
            if (isXAxis) {
                axis.closestPointRange = closestPointRange;
            }
        }
        // Secondary values
        axis.translationSlope = axis.transA = transA =
            axis.staticScale ||
                axis.len / ((range + pointRangePadding) || 1);
        // Translation addend
        axis.transB = axis.horiz ? axis.left : axis.bottom;
        axis.minPixelPadding = transA * minPointOffset;
        fireEvent(this, 'afterSetAxisTranslation');
    }
    /**
     * @internal
     * @function Highcharts.Axis#minFromRange
     */
    minFromRange() {
        const { max, min } = this;
        return isNumber(max) && isNumber(min) && max - min || void 0;
    }
    /**
     * Set the tick positions to round values and optionally extend the extremes
     * to the nearest tick.
     *
     * @internal
     * @function Highcharts.Axis#setTickInterval
     *
     * @param {boolean} secondPass
     * TO-DO: parameter description
     *
     * @emits Highcharts.Axis#event:foundExtremes
     */
    setTickInterval(secondPass) {
        const axis = this, { categories, chart, dataMax, dataMin, dateTime, isXAxis, logarithmic, options, softThreshold } = axis, time = chart.time, threshold = isNumber(axis.threshold) ? axis.threshold : void 0, minRange = axis.minRange || 0, { ceiling, floor, linkedTo, softMax, softMin } = options, linkedParent = isNumber(linkedTo) && chart[axis.coll]?.[linkedTo], tickPixelIntervalOption = options.tickPixelInterval;
        let maxPadding = options.maxPadding, minPadding = options.minPadding, range = 0, linkedParentExtremes, 
        // Only non-negative tickInterval is valid, #12961
        tickIntervalOption = isNumber(options.tickInterval) && options.tickInterval >= 0 ?
            options.tickInterval : void 0, thresholdMin, thresholdMax, hardMin, hardMax;
        if (!dateTime && !categories && !linkedParent) {
            this.getTickAmount();
        }
        // Min or max set either by zooming/setExtremes or initial options
        hardMin = pick(axis.userMin, time.parse(options.min));
        hardMax = pick(axis.userMax, time.parse(options.max));
        // Linked axis gets the extremes from the parent axis
        if (linkedParent) {
            axis.linkedParent = linkedParent;
            linkedParentExtremes = linkedParent.getExtremes();
            axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
            axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
            if (this.type !== linkedParent.type) {
                // Can't link axes of different type
                error(11, true, chart);
            }
            // Initial min and max from the extreme data values
        }
        else {
            // Adjust to hard threshold
            if (softThreshold &&
                defined(threshold) &&
                isNumber(dataMax) &&
                isNumber(dataMin)) {
                if (dataMin >= threshold) {
                    thresholdMin = threshold;
                    minPadding = 0;
                }
                else if (dataMax <= threshold) {
                    thresholdMax = threshold;
                    maxPadding = 0;
                }
            }
            axis.min = pick(hardMin, thresholdMin, dataMin);
            axis.max = pick(hardMax, thresholdMax, dataMax);
        }
        if (isNumber(axis.max) && isNumber(axis.min)) {
            if (logarithmic) {
                if (axis.positiveValuesOnly &&
                    !secondPass &&
                    Math.min(axis.min, pick(dataMin, axis.min)) <= 0) { // #978
                    // Can't plot negative values on log axis
                    error(10, true, chart);
                }
                // The correctFloat cures #934, float errors on full tens. But
                // it was too aggressive for #4360 because of conversion back to
                // lin, therefore use precision 15.
                axis.min = correctFloat(logarithmic.log2lin(axis.min), 16);
                axis.max = correctFloat(logarithmic.log2lin(axis.max), 16);
            }
            // Handle zoomed range
            if (axis.range && isNumber(dataMin)) {
                // #618, #6773:
                axis.userMin = axis.min = hardMin = Math.max(dataMin, axis.minFromRange() || 0);
                axis.userMax = hardMax = axis.max;
                axis.range = void 0; // Don't use it when running setExtremes
            }
        }
        // Hook for Highcharts Stock Scroller and bubble axis padding
        fireEvent(axis, 'foundExtremes');
        // Adjust min and max for the minimum range
        axis.adjustForMinRange();
        if (isNumber(axis.min) && isNumber(axis.max)) {
            // Handle options for floor, ceiling, softMin and softMax (#6359)
            if (!isNumber(axis.userMin) &&
                isNumber(softMin) &&
                softMin < axis.min) {
                axis.min = hardMin = softMin; // #6894
            }
            if (!isNumber(axis.userMax) &&
                isNumber(softMax) &&
                softMax > axis.max) {
                axis.max = hardMax = softMax; // #6894
            }
            // Pad the values to get clear of the chart's edges. To avoid
            // tickInterval taking the padding into account, we do this after
            // computing tick interval (#1337).
            if (!categories &&
                !axis.axisPointRange &&
                !axis.stacking?.usePercentage &&
                !linkedParent) {
                range = axis.max - axis.min;
                if (range) {
                    if (!defined(hardMin) && minPadding) {
                        axis.min -= range * minPadding;
                    }
                    if (!defined(hardMax) && maxPadding) {
                        axis.max += range * maxPadding;
                    }
                }
            }
            if (!isNumber(axis.userMin) && isNumber(floor)) {
                axis.min = Math.max(axis.min, floor);
            }
            if (!isNumber(axis.userMax) && isNumber(ceiling)) {
                axis.max = Math.min(axis.max, ceiling);
            }
            // When the threshold is soft, adjust the extreme value only if the
            // data extreme and the padded extreme land on either side of the
            // threshold. For example, a series of [0, 1, 2, 3] would make the
            // yAxis add a tick for -1 because of the default `minPadding` and
            // `startOnTick` options. This is prevented by the `softThreshold`
            // option.
            if (softThreshold &&
                isNumber(dataMin) &&
                isNumber(dataMax)) {
                const numThreshold = threshold || 0;
                if (!defined(hardMin) &&
                    axis.min < numThreshold &&
                    dataMin >= numThreshold) {
                    axis.min = options.minRange ?
                        Math.min(numThreshold, axis.max - minRange) :
                        numThreshold;
                }
                else if (!defined(hardMax) &&
                    axis.max > numThreshold &&
                    dataMax <= numThreshold) {
                    axis.max = options.minRange ?
                        Math.max(numThreshold, axis.min + minRange) :
                        numThreshold;
                }
            }
            // If min is bigger than highest, or if max less than lowest value,
            // the chart should not render points. (#14417)
            if (!chart.polar && axis.min > axis.max) {
                if (defined(options.min)) {
                    axis.max = axis.min;
                }
                else if (defined(options.max)) {
                    axis.min = axis.max;
                }
            }
            range = axis.max - axis.min;
        }
        // Get tickInterval
        if (axis.min === axis.max ||
            !isNumber(axis.min) ||
            !isNumber(axis.max)) {
            axis.tickInterval = 1;
        }
        else if (linkedParent &&
            !tickIntervalOption &&
            tickPixelIntervalOption === linkedParent.options.tickPixelInterval) {
            axis.tickInterval = tickIntervalOption = linkedParent.tickInterval;
        }
        else {
            axis.tickInterval = pick(tickIntervalOption, this.tickAmount ?
                range / Math.max(this.tickAmount - 1, 1) :
                void 0, 
            // For categorized axis, 1 is default, for linear axis use
            // tickPix
            categories ?
                1 :
                // Don't let it be more than the data range
                range * tickPixelIntervalOption /
                    Math.max(axis.len, tickPixelIntervalOption));
        }
        // Now we're finished detecting min and max, crop and group series data.
        // This is in turn needed in order to find tick positions in ordinal
        // axes.
        if (isXAxis && !secondPass) {
            const hasExtremesChanged = axis.min !== axis.old?.min ||
                axis.max !== axis.old?.max;
            // First process all series assigned to that axis.
            axis.series.forEach(function (series) {
                // Allows filtering out points outside the plot area.
                series.forceCrop = series.forceCropping?.();
                series.processData(hasExtremesChanged);
            });
            // Then apply grouping if needed. The hasExtremesChanged helps to
            // decide if the data grouping should be skipped in the further
            // calculations #16319.
            fireEvent(this, 'postProcessData', { hasExtremesChanged });
        }
        // Set the translation factor used in translate function
        axis.setAxisTranslation();
        // Hook for ordinal axes and radial axes
        fireEvent(this, 'initialAxisTranslation');
        // In column-like charts, don't cramp in more ticks than there are
        // points (#1943, #4184)
        if (axis.pointRange && !tickIntervalOption) {
            axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval);
        }
        // Before normalizing the tick interval, handle minimum tick interval.
        // This applies only if tickInterval is not defined.
        const minTickInterval = pick(options.minTickInterval, 
        // In datetime axes, don't go below the data interval, except when
        // there are scatter-like series involved (#13369).
        dateTime &&
            !axis.series.some((s) => !s.sorted) ?
            axis.closestPointRange : 0);
        if (!tickIntervalOption &&
            minTickInterval &&
            axis.tickInterval < minTickInterval) {
            axis.tickInterval = minTickInterval;
        }
        // For linear axes, normalize the interval
        if (!dateTime && !logarithmic && !tickIntervalOption) {
            axis.tickInterval = getNormalizedTickInterval(axis, axis.tickInterval);
        }
        // Prevent ticks from getting so close that we can't draw the labels
        if (!this.tickAmount) {
            axis.tickInterval = axis.unsquish();
        }
        this.setTickPositions();
    }
    /**
     * Now we have computed the normalized tickInterval, get the tick positions.
     *
     * @internal
     * @function Highcharts.Axis#setTickPositions
     *
     * @emits Highcharts.Axis#event:afterSetTickPositions
     */
    setTickPositions() {
        const axis = this, options = this.options, tickPositionsOption = options.tickPositions, tickPositioner = options.tickPositioner, minorTickIntervalOption = this.getMinorTickInterval(), allowEndOnTick = !this.isPanning, startOnTick = allowEndOnTick && options.startOnTick, endOnTick = allowEndOnTick && options.endOnTick;
        let tickPositions = [], tickPositionerResult;
        // Set the tickmarkOffset
        this.tickmarkOffset = (this.categories &&
            options.tickmarkPlacement === 'between' &&
            this.tickInterval === 1) ? 0.5 : 0; // #3202
        // When there is only one point, or all points have the same value on
        // this axis, then min and max are equal and tickPositions.length is 0
        // or 1. In this case, add some padding in order to center the point,
        // but leave it with one tick. #1337.
        this.single =
            this.min === this.max &&
                defined(this.min) &&
                !this.tickAmount &&
                (
                // Data is on integer (#6563)
                this.min % 1 === 0 ||
                    // Between integers and decimals are not allowed (#6274)
                    options.allowDecimals !== false);
        /**
         * Contains the current positions that are laid out on the axis. The
         * positions are numbers in terms of axis values. In a category axis
         * they are integers, in a datetime axis they are also integers, but
         * designating milliseconds.
         *
         * This property is read only - for modifying the tick positions, use
         * the `tickPositioner` callback or [axis.tickPositions(
         * https://api.highcharts.com/highcharts/xAxis.tickPositions) option
         * instead.
         *
         * @name Highcharts.Axis#tickPositions
         * @type {Highcharts.AxisTickPositionsArray|undefined}
         */
        if (tickPositionsOption) {
            // Find the tick positions. Work on a copy (#1565)
            tickPositions = tickPositionsOption.slice();
        }
        else if (isNumber(this.min) && isNumber(this.max)) {
            // Too many ticks (#6405). Create a friendly warning and provide two
            // ticks so at least we can show the data series.
            if (!axis.ordinal?.positions &&
                ((this.max - this.min) /
                    this.tickInterval >
                    Math.max(2 * this.len, 200))) {
                tickPositions = [this.min, this.max];
                error(19, false, this.chart);
            }
            else if (axis.dateTime) {
                tickPositions = axis.getTimeTicks(axis.dateTime.normalizeTimeTickInterval(this.tickInterval, options.units), this.min, this.max, options.startOfWeek, axis.ordinal?.positions, this.closestPointRange, true);
            }
            else if (axis.logarithmic) {
                tickPositions = axis.logarithmic.getLogTickPositions(this.tickInterval, this.min, this.max);
            }
            else {
                const startingTickInterval = this.tickInterval;
                let adjustedTickInterval = startingTickInterval;
                while (adjustedTickInterval <= startingTickInterval * 2) {
                    tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max);
                    // If there are more tick positions than the set tickAmount,
                    // increase the tickInterval and continue until it fits.
                    // (#17100)
                    if (this.tickAmount &&
                        tickPositions.length > this.tickAmount) {
                        this.tickInterval = getNormalizedTickInterval(this, adjustedTickInterval *= 1.1);
                    }
                    else {
                        break;
                    }
                }
            }
            // Too dense ticks, keep only the first and last (#4477)
            if (tickPositions.length > this.len) {
                tickPositions = [
                    tickPositions[0],
                    tickPositions[tickPositions.length - 1]
                ];
                // Reduce doubled value (#7339)
                if (tickPositions[0] === tickPositions[1]) {
                    tickPositions.length = 1;
                }
            }
            // Run the tick positioner callback, that allows modifying auto tick
            // positions.
            if (tickPositioner) {
                // Make it available to the positioner
                this.tickPositions = tickPositions;
                tickPositionerResult = tickPositioner.apply(axis, [this.min, this.max, axis]);
                if (tickPositionerResult) {
                    tickPositions = tickPositionerResult;
                }
            }
        }
        // Ensure the old ticks are removed and new ones added (#17393)
        if (!this.isDirty &&
            tickPositions.length !==
                this.tickPositions?.length) {
            this.isDirty = true;
        }
        this.tickPositions = tickPositions;
        // Get minorTickInterval
        this.minorTickInterval =
            minorTickIntervalOption === 'auto' && this.tickInterval ?
                this.tickInterval / options.minorTicksPerMajor :
                minorTickIntervalOption;
        // Reset min/max or remove extremes based on start/end on tick
        this.paddedTicks = tickPositions.slice(0); // Used for logarithmic minor
        this.trimTicks(tickPositions, startOnTick, endOnTick);
        if (!this.isLinked && isNumber(this.min) && isNumber(this.max)) {
            // Subtract half a unit (#2619, #2846, #2515, #3390), but not in
            // case of multiple ticks (#6897)
            if (this.single &&
                tickPositions.length < 2 &&
                !this.categories &&
                !this.series.some((s) => (s.is('heatmap') && s.options.pointPlacement === 'between'))) {
                this.min -= 0.5;
                this.max += 0.5;
            }
            if (!tickPositionsOption && !tickPositionerResult) {
                this.adjustTickAmount();
            }
        }
        fireEvent(this, 'afterSetTickPositions');
    }
    /**
     * Handle startOnTick and endOnTick by either adapting to padding min/max or
     * rounded min/max. Also handle single data points.
     *
     * @internal
     * @function Highcharts.Axis#trimTicks
     *
     * @param {Array<number>} tickPositions
     * TO-DO: parameter description
     *
     * @param {boolean} [startOnTick]
     * TO-DO: parameter description
     *
     * @param {boolean} [endOnTick]
     * TO-DO: parameter description
     */
    trimTicks(tickPositions, startOnTick, endOnTick) {
        const roundedMin = tickPositions[0], roundedMax = tickPositions[tickPositions.length - 1], minPointOffset = (!this.isOrdinal && this.minPointOffset) || 0; // (#12716)
        fireEvent(this, 'trimTicks');
        if (!this.isLinked ||
            // Linked non-grid axes should trim ticks, #21743.
            // Grid axis has custom handling of ticks.
            !this.grid) {
            if (startOnTick && isNumber(roundedMin)) { // #6502
                this.min = roundedMin;
            }
            else if (tickPositions.length) {
                while (this.min - minPointOffset > tickPositions[0]) {
                    tickPositions.shift();
                }
            }
            if (endOnTick && isNumber(roundedMax)) { // #6502
                this.max = roundedMax;
            }
            else {
                while (this.max + minPointOffset <
                    tickPositions[tickPositions.length - 1]) {
                    tickPositions.pop();
                }
            }
            // If no tick are left, set one tick in the middle (#3195)
            if (tickPositions.length === 0 &&
                defined(roundedMin) &&
                !this.options.tickPositions) {
                tickPositions.push((roundedMax + roundedMin) / 2);
            }
        }
    }
    /**
     * Check if there are multiple axes in the same pane.
     *
     * @internal
     * @function Highcharts.Axis#alignToOthers
     *
     * @return {boolean|undefined}
     * True if there are other axes.
     */
    alignToOthers() {
        const axis = this, chart = axis.chart, alignedAxes = [this], options = axis.options, chartOptions = chart.options.chart, alignThresholds = (this.coll === 'yAxis' &&
            chartOptions.alignThresholds), thresholdAlignments = [];
        let hasOther;
        axis.thresholdAlignment = void 0;
        if ((
        // Only if alignTicks or alignThresholds is true
        (chartOptions.alignTicks !== false && options.alignTicks) ||
            alignThresholds) &&
            // Disabled when startOnTick or endOnTick are false (#7604)
            options.startOnTick !== false &&
            options.endOnTick !== false &&
            // Don't try to align ticks on a log axis, they are not evenly
            // spaced (#6021)
            !axis.logarithmic) {
            // Get a key identifying which pane the axis belongs to
            const getKey = (axis) => {
                const { horiz, options } = axis;
                return [
                    horiz ? options.left : options.top,
                    options.width,
                    options.height,
                    options.pane
                ].join(',');
            };
            const thisKey = getKey(this);
            chart[this.coll].forEach(function (otherAxis) {
                const { series } = otherAxis;
                if (
                // #4442
                series.length &&
                    series.some((s) => s.visible) &&
                    otherAxis !== axis &&
                    getKey(otherAxis) === thisKey) {
                    hasOther = true; // #4201
                    alignedAxes.push(otherAxis);
                }
            });
        }
        if (hasOther && alignThresholds) {
            // Handle alignThresholds. The `thresholdAlignments` array keeps
            // records of where each axis in the group wants its threshold, from
            // 0 which is on `axis.min`, to 1 which is on `axis.max`.
            alignedAxes.forEach((otherAxis) => {
                const threshAlign = otherAxis.getThresholdAlignment(axis);
                if (isNumber(threshAlign)) {
                    thresholdAlignments.push(threshAlign);
                }
            });
            // For each of the axes in the group, record the average
            // `thresholdAlignment`.
            const thresholdAlignment = thresholdAlignments.length > 1 ?
                thresholdAlignments.reduce((sum, n) => (sum += n), 0) / thresholdAlignments.length :
                void 0;
            alignedAxes.forEach((axis) => {
                axis.thresholdAlignment = thresholdAlignment;
            });
        }
        return hasOther;
    }
    /**
     * Where the axis wants its threshold, from 0 which is on `axis.min`, to 1 which
     * is on `axis.max`.
     *
     * @internal
     * @function Highcharts.Axis#getThresholdAlignment
     */
    getThresholdAlignment(callerAxis) {
        if (!isNumber(this.dataMin) ||
            (this !== callerAxis &&
                this.series.some((s) => 
                // The xAxis.isDirty check is for setExtremes (#23677)
                s.isDirty || s.isDirtyData || s.xAxis?.isDirty))) {
            this.getSeriesExtremes();
        }
        if (isNumber(this.threshold)) {
            let thresholdAlignment = clamp(((this.threshold - (this.dataMin || 0)) /
                ((this.dataMax || 0) - (this.dataMin || 0))), 0, 1);
            if (this.options.reversed) {
                thresholdAlignment = 1 - thresholdAlignment;
            }
            return thresholdAlignment;
        }
    }
    /**
     * Find the max ticks of either the x and y axis collection, and record it
     * in `this.tickAmount`.
     *
     * @internal
     * @function Highcharts.Axis#getTickAmount
     */
    getTickAmount() {
        const axis = this, options = this.options, tickPixelInterval = options.tickPixelInterval;
        let tickAmount = options.tickAmount;
        if (!defined(options.tickInterval) &&
            !tickAmount &&
            this.len < tickPixelInterval &&
            !this.isRadial &&
            !axis.logarithmic &&
            options.startOnTick &&
            options.endOnTick) {
            tickAmount = 2;
        }
        if (!tickAmount && this.alignToOthers()) {
            // Add 1 because 4 tick intervals require 5 ticks (including first
            // and last)
            tickAmount = Math.ceil(this.len / tickPixelInterval) + 1;
        }
        // For tick amounts of 2 and 3, compute five ticks and remove the
        // intermediate ones. This prevents the axis from adding ticks that are
        // too far away from the data extremes.
        if (tickAmount < 4) {
            this.finalTickAmt = tickAmount;
            tickAmount = 5;
        }
        this.tickAmount = tickAmount;
    }
    /**
     * When using multiple axes, adjust the number of ticks to match the highest
     * number of ticks in that group.
     *
     * @internal
     * @function Highcharts.Axis#adjustTickAmount
     */
    adjustTickAmount() {
        const axis = this, { finalTickAmt, max, min, options, tickPositions, tickAmount, thresholdAlignment } = axis, currentTickAmount = tickPositions?.length, threshold = pick(axis.threshold, axis.softThreshold ? 0 : null);
        let len, i, tickInterval = axis.tickInterval, thresholdTickIndex;
        const 
        // Extend the tickPositions by appending a position
        append = () => tickPositions.push(correctFloat(tickPositions[tickPositions.length - 1] +
            tickInterval)), 
        // Extend the tickPositions by prepending a position
        prepend = () => tickPositions.unshift(correctFloat(tickPositions[0] - tickInterval));
        // If `thresholdAlignment` is a number, it means the `alignThresholds`
        // option is true. The `thresholdAlignment` is a scalar value between 0
        // and 1 for where the threshold should be relative to `axis.min` and
        // `axis.max`. Now that we know the tick amount, convert this to the
        // tick index. Unless `thresholdAlignment` is exactly 0 or 1, avoid the
        // first or last tick because that would lead to series being clipped.
        if (isNumber(thresholdAlignment)) {
            thresholdTickIndex = thresholdAlignment === 0 ? 0 :
                thresholdAlignment === 1 ? tickAmount - 1 :
                    // Get the closest integer between 1 and
                    // `tickAmount - 2` (#23787)
                    Math.round(clamp(thresholdAlignment * (tickAmount - 1), 1, tickAmount - 2));
            if (options.reversed) {
                thresholdTickIndex = tickAmount - 1 - thresholdTickIndex;
            }
        }
        if (axis.hasData() && isNumber(min) && isNumber(max)) { // #14769
            // Adjust extremes and translation to the modified tick positions
            const adjustExtremes = () => {
                axis.transA *= (currentTickAmount - 1) / (tickAmount - 1);
                // Do not crop when ticks are not extremes (#9841)
                axis.min = options.startOnTick ?
                    tickPositions[0] :
                    Math.min(min, tickPositions[0]);
                axis.max = options.endOnTick ?
                    tickPositions[tickPositions.length - 1] :
                    Math.max(max, tickPositions[tickPositions.length - 1]);
            };
            // When the axis is subject to the alignThresholds option. Use
            // axis.threshold because the local threshold includes the
            // `softThreshold`.
            if (isNumber(thresholdTickIndex) && isNumber(axis.threshold)) {
                // Throw away the previously computed tickPositions and start
                // from scratch with only the threshold itself, then add ticks
                // below the threshold first, then fill up above the threshold.
                // If we are not able to fill up to axis.max, double the
                // tickInterval and run again.
                while (tickPositions[thresholdTickIndex] !== threshold ||
                    tickPositions.length !== tickAmount ||
                    tickPositions[0] > min ||
                    tickPositions[tickPositions.length - 1] < max) {
                    tickPositions.length = 0;
                    tickPositions.push(axis.threshold);
                    while (tickPositions.length < tickAmount) {
                        if (
                        // Start by prepending positions until the threshold
                        // is at the required index...
                        tickPositions[thresholdTickIndex] === void 0 ||
                            tickPositions[thresholdTickIndex] > axis.threshold) {
                            prepend();
                        }
                        else {
                            // ... then append positions until we have the
                            // required length
                            append();
                        }
                    }
                    // Safety vent
                    if (tickInterval > axis.tickInterval * 8) {
                        break;
                    }
                    tickInterval *= 2;
                }
                adjustExtremes();
            }
            else if (currentTickAmount < tickAmount) {
                while (tickPositions.length < tickAmount) {
                    // Extend evenly for both sides unless we're on the
                    // threshold (#3965)
                    if (tickPositions.length % 2 || min === threshold) {
                        append();
                    }
                    else {
                        prepend();
                    }
                }
                adjustExtremes();
            }
            // The finalTickAmt property is set in getTickAmount
            if (defined(finalTickAmt)) {
                i = len = tickPositions.length;
                while (i--) {
                    if (
                    // Remove every other tick
                    (finalTickAmt === 3 && i % 2 === 1) ||
                        // Remove all but first and last
                        (finalTickAmt <= 2 && i > 0 && i < len - 1)) {
                        tickPositions.splice(i, 1);
                    }
                }
                axis.finalTickAmt = void 0;
            }
        }
    }
    /**
     * Set the scale based on data min and max, user set min and max or options.
     *
     * @internal
     * @function Highcharts.Axis#setScale
     *
     * @emits Highcharts.Axis#event:afterSetScale
     */
    setScale() {
        const axis = this, { coll, stacking } = axis;
        let isDirtyData = false, isXAxisDirty = false;
        axis.series.forEach((series) => {
            isDirtyData = isDirtyData || series.isDirtyData || series.isDirty;
            // When x axis is dirty, we need new data extremes for y as
            // well:
            isXAxisDirty = (isXAxisDirty ||
                series.xAxis?.isDirty ||
                false);
        });
        // Set the new axisLength
        axis.setAxisSize();
        const isDirtyAxisLength = axis.len !== axis.old?.len;
        // Do we really need to go through all this?
        if (isDirtyAxisLength ||
            isDirtyData ||
            isXAxisDirty ||
            axis.isLinked ||
            axis.forceRedraw ||
            axis.userMin !== axis.old?.userMin ||
            axis.userMax !== axis.old?.userMax ||
            axis.alignToOthers()) {
            if (stacking && coll === 'yAxis') {
                stacking.buildStacks();
            }
            axis.forceRedraw = false;
            // #18066 delete minRange property to ensure that it will be
            // calculated again after dirty data in series
            if (!axis.userMinRange) {
                axis.minRange = void 0;
            }
            // Get data extremes if needed
            axis.getSeriesExtremes();
            // Get fixed positions based on tickInterval
            axis.setTickInterval();
            if (stacking && coll === 'xAxis') {
                stacking.buildStacks();
            }
            // Mark as dirty if it is not already set to dirty and extremes have
            // changed. #595.
            if (!axis.isDirty) {
                axis.isDirty =
                    isDirtyAxisLength ||
                        axis.min !== axis.old?.min ||
                        axis.max !== axis.old?.max;
            }
        }
        else if (stacking) {
            stacking.cleanStacks();
        }
        // Recalculate all extremes object when the data has changed. It is
        // required when vertical panning is enabled.
        if (isDirtyData) {
            delete axis.allExtremes;
        }
        fireEvent(this, 'afterSetScale');
    }
    /**
     * Set the minimum and maximum of the axes after render time. If the
     * `startOnTick` and `endOnTick` options are true, the minimum and maximum
     * values are rounded off to the nearest tick. To prevent this, these
     * options can be set to false before calling setExtremes. Also, setExtremes
     * will not allow a range lower than the `minRange` option, which by default
     * is the range of five points.
     *
     * @sample highcharts/members/axis-setextremes/
     *         Set extremes from a button
     * @sample highcharts/members/axis-setextremes-datetime/
     *         Set extremes on a datetime axis
     * @sample highcharts/members/axis-setextremes-off-ticks/
     *         Set extremes off ticks
     * @sample stock/members/axis-setextremes/
     *         Set extremes in Highcharts Stock
     *
     * @function Highcharts.Axis#setExtremes
     *
     * @param {number|string} [min]
     * The new minimum value. For datetime axes, date strings are accepted.
     *
     * @param {number|string} [max]
     * The new maximum value. For datetime axes, date strings are accepted.
     *
     * @param {boolean} [redraw=true]
     * Whether to redraw the chart or wait for an explicit call to
     * {@link Highcharts.Chart#redraw}
     *
     * @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation=true]
     * Enable or modify animations.
     *
     * @param {*} [eventArguments]
     * Arguments to be accessed in event handler.
     *
     * @emits Highcharts.Axis#event:setExtremes
     */
    setExtremes(min, max, redraw = true, animation, eventArguments) {
        const chart = this.chart;
        this.series.forEach((s) => {
            delete s.kdTree;
        });
        min = chart.time.parse(min);
        max = chart.time.parse(max);
        // Extend the arguments with min and max
        eventArguments = extend(eventArguments, { min, max });
        // Fire the event
        fireEvent(this, 'setExtremes', eventArguments, (e) => {
            this.userMin = e.min;
            this.userMax = e.max;
            this.eventArgs = e;
            if (redraw) {
                chart.redraw(animation);
            }
        });
    }
    /**
     * Update the axis metrics.
     *
     * @internal
     * @function Highcharts.Axis#setAxisSize
     */
    setAxisSize() {
        const chart = this.chart, options = this.options, 
        // [top, right, bottom, left]
        offsets = options.offsets || [0, 0, 0, 0], horiz = this.horiz, 
        // Check for percentage based input values. Rounding fixes problems
        // with column overflow and plot line filtering (#4898, #4899)
        width = this.width = Math.round(relativeLength(pick(options.width, chart.plotWidth - offsets[3] + offsets[1]), chart.plotWidth)), height = this.height = Math.round(relativeLength(pick(options.height, chart.plotHeight - offsets[0] + offsets[2]), chart.plotHeight)), top = this.top = Math.round(relativeLength(pick(options.top, chart.plotTop + offsets[0]), chart.plotHeight, chart.plotTop)), left = this.left = Math.round(relativeLength(pick(options.left, chart.plotLeft + offsets[3]), chart.plotWidth, chart.plotLeft));
        // Expose basic values to use in Series object and navigator
        this.bottom = chart.chartHeight - height - top;
        this.right = chart.chartWidth - width - left;
        // Direction agnostic properties
        this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905
        /**
         * The position of the axis in terms of pixels, compared to the chart
         * edge. In a horizontal axis it is the same as `chart.plotLeft` unless
         * the axis is explicitly positioned, and in a default vertical axis it
         * is the same as `chart.plotTop`.
         *
         * @name Highcharts.Axis#pos
         * @type {number}
         */
        this.pos = horiz ? left : top; // Distance from SVG origin
    }
    /**
     * Get the current extremes for the axis.
     *
     * @sample highcharts/members/axis-getextremes/
     *         Report extremes by click on a button
     *
     * @function Highcharts.Axis#getExtremes
     *
     * @return {Highcharts.ExtremesObject}
     * An object containing extremes information.
     */
    getExtremes() {
        const axis = this, log = axis.logarithmic;
        return {
            min: log ?
                correctFloat(log.lin2log(axis.min)) :
                axis.min,
            max: log ?
                correctFloat(log.lin2log(axis.max)) :
                axis.max,
            dataMin: axis.dataMin,
            dataMax: axis.dataMax,
            userMin: axis.userMin,
            userMax: axis.userMax
        };
    }
    /**
     * Get the zero plane either based on zero or on the min or max value.
     * Used in bar and area plots.
     *
     * @function Highcharts.Axis#getThreshold
     *
     * @param {number} threshold
     * The threshold in axis values.
     *
     * @return {number}
     * The translated threshold position in terms of pixels, and corrected to
     * stay within the axis bounds.
     */
    getThreshold(threshold) {
        const axis = this, log = axis.logarithmic, realMin = log ? log.lin2log(axis.min) : axis.min, realMax = log ? log.lin2log(axis.max) : axis.max;
        if (threshold === null || threshold === -Infinity) {
            threshold = realMin;
        }
        else if (threshold === Infinity) {
            threshold = realMax;
        }
        else if (realMin > threshold) {
            threshold = realMin;
        }
        else if (realMax < threshold) {
            threshold = realMax;
        }
        return axis.translate(threshold, 0, 1, 0, 1);
    }
    /**
     * Compute auto alignment for the axis label based on which side the axis is
     * on and the given rotation for the label.
     *
     * @internal
     * @function Highcharts.Axis#autoLabelAlign
     *
     * @param {number} rotation
     * The rotation in degrees as set by either the `rotation` or `autoRotation`
     * options.
     *
     * @return {Highcharts.AlignValue}
     * Can be `"center"`, `"left"` or `"right"`.
     */
    autoLabelAlign(rotation) {
        const angle = ((rotation - this.side * 90) % 360 + 360) % 360, evt = { align: 'center' };
        fireEvent(this, 'autoLabelAlign', evt, function (e) {
            if (angle > 15 && angle < 165) {
                e.align = 'right';
            }
            else if (angle > 195 && angle < 345) {
                e.align = 'left';
            }
        });
        return evt.align;
    }
    /**
     * Get the tick length and width for the axis based on axis options.
     *
     * @internal
     * @function Highcharts.Axis#tickSize
     *
     * @param {string} [prefix]
     * 'tick' or 'minorTick'
     *
     * @return {Array<number,number>|undefined}
     * An array of tickLength and tickWidth
     */
    tickSize(prefix) {
        const options = this.options, tickWidth = pick(options[prefix === 'tick' ? 'tickWidth' : 'minorTickWidth'], 
        // Default to 1 on linear and datetime X axes
        prefix === 'tick' && this.isXAxis && !this.categories ? 1 : 0);
        let tickLength = options[prefix === 'tick' ? 'tickLength' : 'minorTickLength'], tickSize;
        if (tickWidth && tickLength) {
            // Negate the length
            if (options[prefix + 'Position'] === 'inside') {
                tickLength = -tickLength;
            }
            tickSize = [tickLength, tickWidth];
        }
        const e = { tickSize, prefix };
        fireEvent(this, 'afterTickSize', e);
        return e.tickSize;
    }
    /**
     * Return the size of the labels.
     *
     * @internal
     * @function Highcharts.Axis#labelMetrics
     */
    labelMetrics() {
        const renderer = this.chart.renderer, ticks = this.ticks, tick = ticks[Object.keys(ticks)[0]] || {};
        return this.chart.renderer.fontMetrics(tick.label ||
            renderer.box);
    }
    /**
     * Prevent the ticks from getting so close we can't draw the labels. On a
     * horizontal axis, this is handled by rotating the labels, removing ticks
     * and adding ellipsis. On a vertical axis remove ticks and add ellipsis.
     *
     * @internal
     * @function Highcharts.Axis#unsquish
     */
    unsquish() {
        const labelOptions = this.options.labels, padding = labelOptions.padding || 0, horiz = this.horiz, tickInterval = this.tickInterval, slotSize = this.len / (((this.categories ? 1 : 0) +
            this.max -
            this.min) /
            tickInterval), rotationOption = labelOptions.rotation, 
        // We don't know the actual rendered line height at this point, but
        // it defaults to 0.8em
        lineHeight = correctFloat(this.labelMetrics().h * 0.8), range = Math.max(this.max - this.min, 0), 
        // Return the multiple of tickInterval that is needed to avoid
        // collision
        getStep = function (spaceNeeded) {
            let step = (spaceNeeded + 2 * padding) / (slotSize || 1);
            step = step > 1 ? Math.ceil(step) : 1;
            // Guard for very small or negative angles (#9835)
            if (step * tickInterval > range &&
                spaceNeeded !== Infinity &&
                slotSize !== Infinity &&
                range) {
                step = Math.ceil(range / tickInterval);
            }
            return correctFloat(step * tickInterval);
        };
        let newTickInterval = tickInterval, rotation, bestScore = Number.MAX_VALUE, autoRotation;
        if (horiz) {
            if (!labelOptions.staggerLines) {
                if (isNumber(rotationOption)) {
                    autoRotation = [rotationOption];
                }
                else if (slotSize < labelOptions.autoRotationLimit) {
                    autoRotation = labelOptions.autoRotation;
                }
            }
            if (autoRotation) {
                let step, score;
                // Loop over the given autoRotation options, and determine which
                // gives the best score. The best score is that with the lowest
                // number of steps and a rotation closest to horizontal.
                for (const rot of autoRotation) {
                    if (rot === rotationOption ||
                        (rot && rot >= -90 && rot <= 90)) { // #3891
                        step = getStep(Math.abs(lineHeight / Math.sin(deg2rad * rot)));
                        score = step + Math.abs(rot / 360);
                        if (score < bestScore) {
                            bestScore = score;
                            rotation = rot;
                            newTickInterval = step;
                        }
                    }
                }
            }
        }
        else { // #4411
            newTickInterval = getStep(lineHeight * 0.75);
        }
        this.autoRotation = autoRotation;
        this.labelRotation = pick(rotation, isNumber(rotationOption) ? rotationOption : 0);
        return labelOptions.step ? tickInterval : newTickInterval;
    }
    /**
     * Get the general slot width for labels/categories on this axis. This may
     * change between the pre-render (from Axis.getOffset) and the final tick
     * rendering and placement.
     *
     * @internal
     * @function Highcharts.Axis#getSlotWidth
     *
     * @param {Highcharts.Tick} [tick] Optionally, calculate the slot width
     * basing on tick label. It is used in highcharts-3d module, where the slots
     * has different widths depending on perspective angles.
     *
     * @return {number}
     * The pixel width allocated to each axis label.
     */
    getSlotWidth(tick) {
        // #5086, #1580, #1931
        const chart = this.chart, horiz = this.horiz, labelOptions = this.options.labels, slotCount = Math.max(this.tickPositions.length - (this.categories ? 0 : 1), 1), marginLeft = chart.margin[3];
        // Used by grid axis
        if (tick && isNumber(tick.slotWidth)) { // #13221, can be 0
            return tick.slotWidth;
        }
        if (horiz && labelOptions.step < 2 && !this.isRadial) {
            if (labelOptions.rotation) { // #4415
                return 0;
            }
            return ((this.staggerLines || 1) * this.len) / slotCount;
        }
        if (!horiz) {
            // #7028
            const cssWidth = labelOptions.style.width;
            if (cssWidth !== void 0) {
                return parseInt(String(cssWidth), 10);
            }
            // Skip marginLeft for opposite axis to avoid label cutoff, #22821
            if (!this.opposite && marginLeft) {
                return marginLeft - chart.spacing[3];
            }
        }
        // Last resort, a fraction of the available size
        return chart.chartWidth * 0.33;
    }
    /**
     * Render the axis labels and determine whether ellipsis or rotation need to
     * be applied.
     *
     * @internal
     * @function Highcharts.Axis#renderUnsquish
     */
    renderUnsquish() {
        const chart = this.chart, renderer = chart.renderer, tickPositions = this.tickPositions, ticks = this.ticks, labelOptions = this.options.labels, labelStyleOptions = labelOptions.style, horiz = this.horiz, slotWidth = this.getSlotWidth(), innerWidth = Math.max(1, Math.round(slotWidth - (horiz ?
            2 * (labelOptions.padding || 0) :
            labelOptions.distance || 0 // #21172
        ))), attr = {}, labelMetrics = this.labelMetrics(), lineClampOption = labelStyleOptions.lineClamp;
        let commonWidth, lineClamp = lineClampOption ?? (Math.floor(this.len / (tickPositions.length * labelMetrics.h)) || 1), maxLabelLength = 0;
        // Set rotation option unless it is "auto", like in gauges
        if (!isString(labelOptions.rotation)) {
            // #4443
            attr.rotation = labelOptions.rotation || 0;
        }
        // Get the longest label length
        tickPositions.forEach(function (tickPosition) {
            maxLabelLength = Math.max(maxLabelLength, ticks[tickPosition].label?.textPxLength || 0);
        });
        this.maxLabelLength = maxLabelLength;
        // Handle auto rotation on horizontal axis
        if (this.autoRotation) {
            // Apply rotation only if the label is too wide for the slot, and
            // the label is wider than its height.
            if (maxLabelLength > innerWidth &&
                maxLabelLength > labelMetrics.h) {
                attr.rotation = this.labelRotation;
            }
            else {
                this.labelRotation = 0;
            }
            // Handle word-wrap or ellipsis on vertical axis
        }
        else if (slotWidth) {
            // For word-wrap or ellipsis
            commonWidth = innerWidth;
        }
        // Add ellipsis if the label length is significantly longer than ideal
        if (attr.rotation) {
            commonWidth = (maxLabelLength > chart.chartHeight * 0.5 ?
                chart.chartHeight * 0.33 :
                maxLabelLength);
            if (!lineClampOption) {
                lineClamp = 1;
            }
        }
        // Set the explicit or automatic label alignment
        this.labelAlign = labelOptions.align ||
            this.autoLabelAlign(this.labelRotation || 0);
        if (this.labelAlign) {
            attr.align = this.labelAlign;
        }
        // Apply general and specific CSS
        tickPositions.forEach(function (pos) {
            const tick = ticks[pos], label = tick?.label, widthOption = labelStyleOptions.width, css = {};
            if (label) {
                // This needs to go before the CSS in old IE (#4502)
                label.attr(attr);
                if (tick.shortenLabel) {
                    tick.shortenLabel();
                }
                else if (commonWidth &&
                    !widthOption &&
                    // Setting width in this case messes with the bounding box
                    // (#7975)
                    labelStyleOptions.whiteSpace !== 'nowrap' &&
                    (
                    // Speed optimizing, #7656
                    commonWidth < (label.textPxLength || 0) ||
                        // Resetting CSS, #4928
                        label.element.tagName === 'DIV')) {
                    label.css(extend(css, {
                        width: `${commonWidth}px`,
                        lineClamp
                    }));
                    // Reset previously shortened label (#8210, #22961)
                }
                else {
                    const wasSquished = label.styles.width ||
                        label.textWidth || label.styles.lineClamp;
                    if (wasSquished && !css.width && !widthOption) {
                        label.css({
                            width: 'auto',
                            lineClamp: 0
                        });
                    }
                }
                tick.rotation = attr.rotation;
            }
        }, this);
        // Note: Why is this not part of getLabelPosition?
        this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0);
    }
    /**
     * Return true if the axis has associated data.
     *
     * @function Highcharts.Axis#hasData
     *
     * @return {boolean}
     * True if the axis has associated visible series and those series have
     * either valid data points or explicit `min` and `max` settings.
     */
    hasData() {
        return this.series.some(function (s) {
            return s.hasData();
        }) ||
            (this.options.showEmpty &&
                defined(this.min) &&
                defined(this.max));
    }
    /**
     * Adds the title defined in axis.options.title.
     *
     * @function Highcharts.Axis#addTitle
     *
     * @param {boolean} [display]
     * Whether or not to display the title.
     */
    addTitle(display) {
        const axis = this, renderer = axis.chart.renderer, horiz = axis.horiz, opposite = axis.opposite, options = axis.options, axisTitleOptions = options.title, styledMode = axis.chart.styledMode;
        let textAlign;
        if (!axis.axisTitle) {
            textAlign = axisTitleOptions.textAlign;
            if (!textAlign) {
                textAlign = (horiz ? {
                    low: 'left',
                    middle: 'center',
                    high: 'right'
                } : {
                    low: opposite ? 'right' : 'left',
                    middle: 'center',
                    high: opposite ? 'left' : 'right'
                })[axisTitleOptions.align];
            }
            axis.axisTitle = renderer
                .text(axisTitleOptions.text || '', 0, 0, axisTitleOptions.useHTML)
                .attr({
                zIndex: 7,
                rotation: axisTitleOptions.rotation || 0,
                align: textAlign
            })
                .addClass('highcharts-axis-title');
            // #7814, don't mutate style option
            if (!styledMode) {
                axis.axisTitle.css(merge(axisTitleOptions.style));
            }
            axis.axisTitle.add(axis.axisGroup);
            axis.axisTitle.isNew = true;
        }
        // Max width defaults to the length of the axis
        if (!styledMode &&
            !axisTitleOptions.style.width &&
            !axis.isRadial) {
            axis.axisTitle.css({
                width: axis.len + 'px'
            });
        }
        // Hide or show the title depending on whether showEmpty is set
        axis.axisTitle[display ? 'show' : 'hide'](display);
    }
    /**
     * Generates a tick for initial positioning.
     *
     * @internal
     * @function Highcharts.Axis#generateTick
     */
    generateTick(pos) {
        const axis = this, ticks = axis.ticks;
        if (!ticks[pos]) {
            ticks[pos] = new Tick(axis, pos);
        }
        else {
            ticks[pos].addLabel(); // Update labels depending on tick interval
        }
    }
    /**
     * Create the axisGroup and gridGroup elements on first iteration.
     *
     * @internal
     * @function Highcharts.Axis#getOffset
     *
     * @emits Highcharts.Axis#event:afterGetOffset
     */
    createGroups() {
        const { axisParent, // Used in color axis
        chart, coll, options } = this, renderer = chart.renderer;
        const createGroup = (name, suffix, zIndex) => renderer.g(name)
            .attr({ zIndex })
            .addClass(`highcharts-${coll.toLowerCase()}${suffix} ` +
            (this.isRadial ? `highcharts-radial-axis${suffix} ` : '') +
            (options.className || ''))
            .add(axisParent);
        if (!this.axisGroup) {
            this.gridGroup = createGroup('grid', '-grid', options.gridZIndex).clip(this.clippable ? chart.plotClipInner : void 0);
            this.axisGroup = createGroup('axis', '', options.zIndex);
            this.labelGroup = createGroup('axis-labels', '-labels', options.labels.zIndex);
        }
    }
    /**
     * Shuffle existing category ticks, like in bar race chart
     *
     * @internal
     */
    shuffleTicks() {
        const ticks = this.ticks, oldNames = this.old?.names;
        if (this.type === 'category' && oldNames) {
            oldNames.forEach((name, oldPos) => {
                const pos = this.namesMap[name];
                if (defined(pos) && oldPos !== pos) {
                    // Move tick instance
                    if (ticks[oldPos]) {
                        ticks[oldPos].pos = pos;
                        this.isDirty = true; // Mark for redraw
                    }
                    // Check if the existing tick in the new position has a
                    // new place to go
                    if (ticks[pos] &&
                        this.names.indexOf(oldNames[pos]) === -1) {
                        // Mark for destruction below
                        ticks[pos].pos = NaN;
                    }
                }
            });
            // Remap ticks to new positions
            const values = Object.values(ticks);
            Object.keys(ticks).forEach((key) => {
                delete ticks[key];
            });
            values.forEach((tick) => {
                if (!isNaN(tick.pos)) {
                    ticks[tick.pos] = tick;
                }
                else {
                    tick.destroy();
                }
            });
        }
    }
    /**
     * Render the tick labels to a preliminary position to get their sizes
     *
     * @internal
     * @function Highcharts.Axis#getOffset
     *
     * @emits Highcharts.Axis#event:afterGetOffset
     */
    getOffset() {
        const axis = this, { chart, horiz, options, side, ticks, tickPositions, coll } = axis, hasData = axis.hasData(), axisTitleOptions = options.title, labelOptions = options.labels, hasCrossing = isNumber(options.crossing), axisOffset = chart.axisOffset, clipOffset = chart.clipOffset, directionFactor = [-1, 1, 1, -1][side];
        let tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }, absTickRotCorrX = 0, showAxis, titleOffset = 0, titleOffsetOption, titleMargin = 0, labelOffset = 0, // Reset
        labelOffsetPadded, lineHeightCorrection, reserveSpaceDefault;
        // For reuse in Axis.render
        axis.showAxis = showAxis = hasData || options.showEmpty;
        // Set/reset staggerLines
        axis.staggerLines = (axis.horiz && labelOptions.staggerLines) || void 0;
        axis.createGroups();
        if (hasData || axis.isLinked) {
            // Shuffle existing category ticks
            axis.shuffleTicks();
            // Generate new ticks
            tickPositions.forEach(axis.generateTick.bind(axis));
            axis.renderUnsquish();
            tickRotCorr = axis.tickRotCorr;
            absTickRotCorrX = Math.abs(tickRotCorr.x);
            // Left side must be align: right and right side must
            // have align: left for labels
            reserveSpaceDefault = axis.reserveSpaceDefault = (side === 0 ||
                side === 2 ||
                ({ 1: 'left', 3: 'right' })[side] === axis.labelAlign);
            if (labelOptions.reserveSpace ??
                (hasCrossing ? false : null) ??
                (axis.labelAlign === 'center' ? true : null) ??
                reserveSpaceDefault) {
                tickPositions.forEach(function (pos) {
                    // Get the highest offset
                    labelOffset = Math.max(ticks[pos].getLabelSize(), labelOffset);
                });
            }
            if (axis.staggerLines) {
                labelOffset *= axis.staggerLines;
            }
            if (!horiz &&
                isNumber(axis.labelRotation) &&
                reserveSpaceDefault) {
                labelOffset -= absTickRotCorrX;
            }
            axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1);
        }
        else { // Doesn't have data
            objectEach(ticks, function (tick, n) {
                tick.destroy();
                delete ticks[n];
            });
        }
        if (axisTitleOptions?.text &&
            axisTitleOptions.enabled !== false) {
            axis.addTitle(showAxis);
            if (showAxis &&
                !hasCrossing &&
                axisTitleOptions.reserveSpace !== false) {
                axis.titleOffset = titleOffset =
                    axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
                titleOffsetOption = axisTitleOptions.offset;
                titleMargin = defined(titleOffsetOption) ?
                    0 :
                    pick(axisTitleOptions.margin, horiz ? 5 : 10);
            }
        }
        // Render the axis line
        axis.renderLine();
        // Handle automatic or user set offset
        axis.offset = directionFactor * (isNumber(options.offset) ?
            options.offset :
            axisOffset[side] ?
                axisOffset[side] + (options.margin || 0) :
                0);
        if (side === 0) {
            lineHeightCorrection = -axis.labelMetrics().h;
        }
        else if (side === 2) {
            lineHeightCorrection = tickRotCorr.y;
        }
        else {
            lineHeightCorrection = 0;
        }
        // Find the padded label offset
        labelOffsetPadded = Math.abs(labelOffset) + titleMargin;
        if (labelOffset) {
            labelOffsetPadded -= lineHeightCorrection;
            labelOffsetPadded += directionFactor * (horiz ?
                (labelOptions.y ??
                    (tickRotCorr.y +
                        directionFactor * labelOptions.distance)) :
                (labelOptions.x ?? (reserveSpaceDefault ?
                    directionFactor * (labelOptions.distance - absTickRotCorrX) :
                    tickRotCorr.x +
                        directionFactor * labelOptions.distance)));
            if (!horiz &&
                !reserveSpaceDefault &&
                axis.labelAlign === 'center' &&
                isNumber(axis.labelRotation)) {
                labelOffsetPadded += absTickRotCorrX;
            }
        }
        axis.axisTitleMargin = titleOffsetOption ?? labelOffsetPadded;
        axis.maxLabelDimensions = axis.getMaxLabelDimensions?.(ticks, tickPositions);
        // Due to GridAxis.tickSize, tickSize should be calculated after ticks
        // has rendered.
        if (coll !== 'colorAxis' && clipOffset) {
            const tickSize = this.tickSize('tick');
            axisOffset[side] = Math.max(axisOffset[side], (axis.axisTitleMargin || 0) + titleOffset +
                directionFactor * axis.offset, labelOffsetPadded, // #3027
            tickPositions?.length && tickSize ?
                tickSize[0] + directionFactor * axis.offset :
                0 // #4866
            );
            // Decide the clipping needed to keep the graph inside
            // the plot area and axis lines
            const clip = !axis.axisLine || options.offset ?
                0 :
                // #4308, #4371
                axis.axisLine.strokeWidth() / 2;
            clipOffset[side] = Math.max(clipOffset[side], clip);
        }
        fireEvent(this, 'afterGetOffset');
    }
    /**
     * Internal function to get the path for the axis line. Extended for polar
     * charts.
     *
     * @function Highcharts.Axis#getLinePath
     *
     * @param {number} lineWidth
     * The line width in pixels.
     *
     * @return {Highcharts.SVGPathArray}
     * The SVG path definition in array form.
     */
    getLinePath(lineWidth) {
        const chart = this.chart, opposite = this.opposite, offset = this.offset, horiz = this.horiz, lineLeft = this.left + (opposite ? this.width : 0) + offset, lineTop = chart.chartHeight - this.bottom -
            (opposite ? this.height : 0) + offset;
        if (opposite) {
            lineWidth *= -1; // Crispify the other way - #1480, #1687
        }
        return chart.renderer
            .crispLine([
            [
                'M',
                horiz ?
                    this.left :
                    lineLeft,
                horiz ?
                    lineTop :
                    this.top
            ],
            [
                'L',
                horiz ?
                    chart.chartWidth - this.right :
                    lineLeft,
                horiz ?
                    lineTop :
                    chart.chartHeight - this.bottom
            ]
        ], lineWidth);
    }
    /**
     * Render the axis line. Called internally when rendering and redrawing the
     * axis.
     *
     * @function Highcharts.Axis#renderLine
     */
    renderLine() {
        const { chart, offset = 0, options } = this;
        this.axisLine || (this.axisLine = chart.renderer.path()
            .addClass('highcharts-axis-line')
            .attr(chart.styledMode ? {} : {
            stroke: options.lineColor,
            'stroke-width': options.lineWidth,
            zIndex: 7
        })
            .clip(this.clippable && offset <= 0 ?
            chart.plotClipOuter :
            void 0)
            .add(this.axisGroup));
    }
    /**
     * Position the axis title.
     *
     * @internal
     * @function Highcharts.Axis#getTitlePosition
     *
     * @return {Highcharts.PositionObject}
     * X and Y positions for the title.
     */
    getTitlePosition(axisTitle) {
        // Compute anchor points for each of the title align options
        const horiz = this.horiz, axisLeft = this.left, axisTop = this.top, axisLength = this.len, axisTitleOptions = this.options.title, margin = horiz ? axisLeft : axisTop, opposite = this.opposite, offset = this.offset, xOption = axisTitleOptions.x, yOption = axisTitleOptions.y, fontMetrics = this.chart.renderer.fontMetrics(axisTitle), 
        // The part of a multiline text that is below the baseline of the
        // first line. Subtract 1 to preserve pixel-perfectness from the
        // old behavior (v5.0.12), where only one line was allowed.
        textHeightOvershoot = axisTitle ? Math.max(axisTitle.getBBox(false, 0).height - fontMetrics.h - 1, 0) : 0, 
        // The position in the length direction of the axis
        alongAxis = ({
            low: margin + (horiz ? 0 : axisLength),
            middle: margin + axisLength / 2,
            high: margin + (horiz ? axisLength : 0)
        })[axisTitleOptions.align], 
        // The position in the perpendicular direction of the axis
        offAxis = (horiz ? axisTop + this.height : axisLeft) +
            (horiz ? 1 : -1) * // Horizontal axis reverses the margin
                (opposite ? -1 : 1) * // So does opposite axes
                (this.axisTitleMargin || 0) +
            [
                -textHeightOvershoot, // Top
                textHeightOvershoot, // Right
                fontMetrics.f, // Bottom
                -textHeightOvershoot // Left
            ][this.side], titlePosition = {
            x: horiz ?
                alongAxis + xOption :
                offAxis + (opposite ? this.width : 0) + offset + xOption,
            y: horiz ?
                offAxis + yOption - (opposite ? this.height : 0) + offset :
                alongAxis + yOption
        };
        fireEvent(this, 'afterGetTitlePosition', { titlePosition: titlePosition });
        return titlePosition;
    }
    /**
     * Render a minor tick into the given position. If a minor tick already
     * exists in this position, move it.
     *
     * @function Highcharts.Axis#renderMinorTick
     *
     * @param {number} pos
     * The position in axis values.
     *
     * @param {boolean} slideIn
     * Whether the tick should animate in from last computed position
     */
    renderMinorTick(pos, slideIn) {
        const axis = this;
        const minorTicks = axis.minorTicks;
        if (!minorTicks[pos]) {
            minorTicks[pos] = new Tick(axis, pos, 'minor');
        }
        // Render new ticks in old position
        if (slideIn && minorTicks[pos].isNew) {
            minorTicks[pos].render(null, true);
        }
        minorTicks[pos].render(null, false, 1);
    }
    /**
     * Render a major tick into the given position. If a tick already exists
     * in this position, move it.
     *
     * @function Highcharts.Axis#renderTick
     *
     * @param {number} pos
     * The position in axis values.
     *
     * @param {number} i
     * The tick index.
     *
     * @param {boolean} slideIn
     * Whether the tick should animate in from last computed position
     */
    renderTick(pos, i, slideIn) {
        const axis = this, isLinked = axis.isLinked, ticks = axis.ticks;
        // Linked axes need an extra check to find out if
        if (!isLinked ||
            (pos >= axis.min && pos <= axis.max) ||
            axis.grid?.isColumn) {
            if (!ticks[pos]) {
                ticks[pos] = new Tick(axis, pos);
            }
            // NOTE this seems like overkill. Could be handled in tick.render by
            // setting old position in attr, then set new position in animate.
            // render new ticks in old position
            if (slideIn && ticks[pos].isNew) {
                // Start with negative opacity so that it is visible from
                // halfway into the animation
                ticks[pos].render(i, true, -1);
            }
            ticks[pos].render(i);
        }
    }
    /**
     * Render the axis.
     *
     * @internal
     * @function Highcharts.Axis#render
     *
     * @emits Highcharts.Axis#event:afterRender
     */
    render() {
        const axis = this, chart = axis.chart, log = axis.logarithmic, renderer = chart.renderer, options = axis.options, isLinked = axis.isLinked, tickPositions = axis.tickPositions, axisTitle = axis.axisTitle, ticks = axis.ticks, minorTicks = axis.minorTicks, alternateBands = axis.alternateBands, stackLabelOptions = options.stackLabels, alternateGridColor = options.alternateGridColor, crossing = options.crossing, tickmarkOffset = axis.tickmarkOffset, axisLine = axis.axisLine, showAxis = axis.showAxis, animation = animObject(renderer.globalAnimation);
        let from, to;
        // Reset
        axis.labelEdge.length = 0;
        axis.overlap = false;
        // Mark all elements inActive before we go over and mark the active ones
        [ticks, minorTicks, alternateBands].forEach(function (coll) {
            objectEach(coll, function (tick) {
                tick.isActive = false;
            });
        });
        // Crossing
        if (isNumber(crossing)) {
            const otherAxis = this.isXAxis ? chart.yAxis[0] : chart.xAxis[0], directionFactor = [1, -1, -1, 1][this.side];
            if (otherAxis) {
                let px = otherAxis.toPixels(crossing, true);
                if (axis.horiz) {
                    px = otherAxis.len - px;
                }
                axis.offset = directionFactor * px;
            }
        }
        // If the series has data draw the ticks. Else only the line and title
        if (axis.hasData() || isLinked) {
            const slideInTicks = axis.chart.hasRendered &&
                axis.old && isNumber(axis.old.min);
            // Minor ticks
            if (axis.minorTickInterval && !axis.categories) {
                axis.getMinorTickPositions().forEach(function (pos) {
                    axis.renderMinorTick(pos, slideInTicks);
                });
            }
            // Major ticks. Pull out the first item and render it last so that
            // we can get the position of the neighbor label. #808.
            if (tickPositions.length) { // #1300
                tickPositions.forEach(function (pos, i) {
                    axis.renderTick(pos, i, slideInTicks);
                });
                // In a categorized axis, the tick marks are displayed
                // between labels. So we need to add a tick mark and
                // grid line at the left edge of the X axis.
                if (tickmarkOffset && (axis.min === 0 || axis.single)) {
                    if (!ticks[-1]) {
                        ticks[-1] = new Tick(axis, -1, null, true);
                    }
                    ticks[-1].render(-1);
                }
            }
            // Alternate grid color
            if (alternateGridColor) {
                tickPositions.forEach(function (pos, i) {
                    to = typeof tickPositions[i + 1] !== 'undefined' ?
                        tickPositions[i + 1] + tickmarkOffset :
                        axis.max - tickmarkOffset;
                    if (i % 2 === 0 &&
                        pos < axis.max &&
                        to <= axis.max + (chart.polar ?
                            -tickmarkOffset :
                            tickmarkOffset)) { // #2248, #4660
                        if (!alternateBands[pos]) {
                            // Should be imported from PlotLineOrBand.js, but
                            // the dependency cycle with axis is a problem
                            alternateBands[pos] = new H.PlotLineOrBand(axis, {});
                        }
                        from = pos + tickmarkOffset; // #949
                        alternateBands[pos].options = {
                            from: log ? log.lin2log(from) : from,
                            to: log ? log.lin2log(to) : to,
                            color: alternateGridColor,
                            className: 'highcharts-alternate-grid'
                        };
                        alternateBands[pos].render();
                        alternateBands[pos].isActive = true;
                    }
                });
            }
            // Custom plot lines and bands
            if (!axis._addedPlotLB) { // Only first time
                axis._addedPlotLB = true;
                (options.plotLines || [])
                    .concat(options.plotBands || [])
                    .forEach(function (plotLineOptions) {
                    axis
                        .addPlotBandOrLine(plotLineOptions);
                });
            }
        } // End if hasData
        // Remove inactive ticks
        [ticks, minorTicks, alternateBands].forEach(function (coll) {
            const forDestruction = [], delay = animation.duration, destroyInactiveItems = function () {
                let i = forDestruction.length;
                while (i--) {
                    // When resizing rapidly, the same items
                    // may be destroyed in different timeouts,
                    // or the may be reactivated
                    if (coll[forDestruction[i]] &&
                        !coll[forDestruction[i]].isActive) {
                        coll[forDestruction[i]].destroy();
                        delete coll[forDestruction[i]];
                    }
                }
            };
            objectEach(coll, function (tick, pos) {
                if (!tick.isActive) {
                    // Render to zero opacity
                    tick.render(pos, false, 0);
                    tick.isActive = false;
                    forDestruction.push(pos);
                }
            });
            // When the objects are finished fading out, destroy them
            syncTimeout(destroyInactiveItems, coll === alternateBands ||
                !chart.hasRendered ||
                !delay ?
                0 :
                delay);
        });
        // Set the axis line path
        if (axisLine) {
            axisLine[axisLine.isPlaced ? 'animate' : 'attr']({
                d: this.getLinePath(axisLine.strokeWidth())
            });
            axisLine.isPlaced = true;
            // Show or hide the line depending on options.showEmpty
            axisLine[showAxis ? 'show' : 'hide'](showAxis);
        }
        if (axisTitle && showAxis) {
            axisTitle[axisTitle.isNew ? 'attr' : 'animate'](axis.getTitlePosition(axisTitle));
            axisTitle.isNew = false;
        }
        // Stacked totals
        if (stackLabelOptions?.enabled && axis.stacking) {
            axis.stacking.renderStackTotals();
        }
        // First time, save the existing state
        if (!this.old) {
            this.saveOld();
        }
        axis.isDirty = false;
        fireEvent(this, 'afterRender');
    }
    /**
     * Redraw the axis to reflect changes in the data or axis extremes. Called
     * internally from Highcharts.Chart#redraw.
     *
     * @internal
     * @function Highcharts.Axis#redraw
     */
    redraw() {
        if (this.visible) {
            // Render the axis
            this.render();
            // Move plot lines and bands
            this.plotLinesAndBands.forEach(function (plotLine) {
                plotLine.render();
            });
        }
        // Mark associated series as dirty and ready for redraw
        this.series.forEach(function (series) {
            series.isDirty = true;
        });
    }
    /**
     * Record old scaling for updating/animation.
     *
     * @private
     */
    saveOld() {
        this.old = isNumber(this.min) ? {
            len: this.len,
            max: this.max,
            min: this.min,
            names: this.names.slice(),
            transA: this.transA,
            userMax: this.userMax,
            userMin: this.userMin
        } : void 0;
    }
    /**
     * Returns an array of axis properties, that should be untouched during
     * reinitialization.
     *
     * @internal
     * @function Highcharts.Axis#getKeepProps
     */
    getKeepProps() {
        return (this.keepProps || Axis.keepProps);
    }
    /**
     * Destroys an Axis instance. See {@link Axis#remove} for the API endpoint
     * to fully remove the axis.
     *
     * @internal
     * @function Highcharts.Axis#destroy
     *
     * @param {boolean} [keepEvents]
     * Whether to preserve events, used internally in Axis.update.
     */
    destroy(keepEvents) {
        const axis = this, plotLinesAndBands = axis.plotLinesAndBands, eventOptions = this.eventOptions;
        fireEvent(this, 'destroy', { keepEvents: keepEvents });
        // Remove the events
        if (!keepEvents) {
            removeEvent(axis);
        }
        // Destroy collections
        [axis.ticks, axis.minorTicks, axis.alternateBands].forEach(function (coll) {
            destroyObjectProperties(coll);
        });
        if (plotLinesAndBands) {
            let i = plotLinesAndBands.length;
            while (i--) { // #1975
                plotLinesAndBands[i].destroy();
            }
        }
        // Destroy elements
        [
            'axisLine', 'axisTitle', 'axisGroup',
            'gridGroup', 'labelGroup', 'cross', 'scrollbar'
        ].forEach(function (prop) {
            if (axis[prop]) {
                axis[prop] = axis[prop].destroy();
            }
        });
        // Destroy each generated group for plotLines and plotBands
        for (const plotGroup in axis.plotLinesAndBandsGroups) { // eslint-disable-line guard-for-in
            axis.plotLinesAndBandsGroups[plotGroup] =
                axis.plotLinesAndBandsGroups[plotGroup].destroy();
        }
        // Delete all properties and fall back to the prototype.
        objectEach(axis, function (_val, key) {
            if (axis.getKeepProps().indexOf(key) === -1) {
                delete axis[key];
            }
        });
        this.eventOptions = eventOptions;
    }
    /**
     * Internal function to draw a crosshair.
     *
     * @function Highcharts.Axis#drawCrosshair
     *
     * @param {Highcharts.PointerEventObject} [e]
     * The event arguments from the modified pointer event, extended with
     * `chartX` and `chartY`
     *
     * @param {Highcharts.Point} [point]
     * The Point object if the crosshair snaps to points.
     *
     * @emits Highcharts.Axis#event:afterDrawCrosshair
     * @emits Highcharts.Axis#event:drawCrosshair
     */
    drawCrosshair(e, point) {
        const options = this.crosshair, snap = options?.snap ?? true, chart = this.chart, graphic = this.cross;
        let path, pos, categorized, crossOptions;
        fireEvent(this, 'drawCrosshair', { e: e, point: point });
        // Use last available event when updating non-snapped crosshairs without
        // mouse interaction (#5287)
        if (!e) {
            e = this.cross?.e;
        }
        if (
        // Disabled in options
        !options ||
            // Snap
            ((defined(point) || !snap) === false)) {
            this.hideCrosshair();
        }
        else {
            clearTimeout(this.crossShowTimer);
            // Get the path
            if (!snap) {
                pos = e &&
                    (this.horiz ?
                        e.chartX - this.pos :
                        this.len - e.chartY + this.pos);
            }
            else if (defined(point)) {
                // #3834
                pos = pick(this.coll !== 'colorAxis' ?
                    point.crosshairPos : // 3D axis extension
                    null, this.isXAxis ?
                    point.plotX :
                    this.len - point.plotY);
            }
            if (defined(pos)) {
                crossOptions = {
                    // Value, only used on radial
                    value: point && (this.isXAxis ?
                        point.x :
                        pick(point.stackY, point.y)),
                    translatedValue: pos
                };
                if (chart.polar) {
                    // Additional information required for crosshairs in
                    // polar chart
                    extend(crossOptions, {
                        isCrosshair: true,
                        chartX: e?.chartX,
                        chartY: e?.chartY,
                        point: point
                    });
                }
                path = this.getPlotLinePath(crossOptions) ||
                    null; // #3189
            }
            if (!defined(path)) {
                this.hideCrosshair();
                return;
            }
            categorized = this.categories && !this.isRadial;
            this.crossShowTimer = syncTimeout(() => {
                // Get the *current* crosshair, in case it was created by
                // another call while this one was delayed.
                let cross = this.cross;
                // Draw the cross
                if (!cross) {
                    this.cross = cross = chart.renderer
                        .path()
                        .addClass('highcharts-crosshair highcharts-crosshair-' +
                        (categorized ? 'category ' : 'thin ') +
                        (options.className || ''))
                        .attr({
                        zIndex: pick(options.zIndex, 2)
                    })
                        .clip(options.clip === false ?
                        void 0 :
                        chart.plotClipInner)
                        .add();
                    // Presentational attributes
                    if (!chart.styledMode) {
                        cross.attr({
                            stroke: options.color ||
                                (categorized ?
                                    color(
                                    // eslint-disable-next-line max-len
                                    'var(--highcharts-highlight-color-20)').setOpacity(0.25).get() :
                                    'var(--highcharts-neutral-color-20)'),
                            'stroke-width': pick(options.width, 1)
                        }).css({
                            'pointer-events': 'none'
                        });
                        if (options.dashStyle) {
                            cross.attr({
                                dashstyle: options.dashStyle
                            });
                        }
                    }
                }
                cross
                    .show()
                    .animate({ d: path }, animObject(options?.animation));
                if (categorized && !options.width) {
                    cross.attr({
                        'stroke-width': this.transA
                    });
                }
                if (this.cross) {
                    this.cross.e = e;
                }
            }, 
            // Only use delay if the crosshair is currently hidden
            (!graphic || graphic.attr('visibility') === 'hidden') ?
                options.showDelay || 0 :
                0);
        }
        fireEvent(this, 'afterDrawCrosshair', { e: e, point: point });
    }
    /**
     * Hide the crosshair if visible.
     *
     * @function Highcharts.Axis#hideCrosshair
     */
    hideCrosshair() {
        clearTimeout(this.crossShowTimer);
        if (this.cross) {
            this.cross.hide();
        }
        fireEvent(this, 'afterHideCrosshair');
    }
    /**
     * Update an axis object with a new set of options. The options are merged
     * with the existing options, so only new or altered options need to be
     * specified.
     *
     * @sample highcharts/members/axis-update/
     *         Axis update demo
     *
     * @function Highcharts.Axis#update
     *
     * @param {Highcharts.AxisOptions} options
     * The new options that will be merged in with existing options on the axis.
     *
     * @param {boolean} [redraw=true]
     * Whether to redraw the chart after the axis is altered. If doing more
     * operations on the chart, it is a good idea to set redraw to false and
     * call {@link Chart#redraw} after.
     */
    update(options, redraw) {
        const chart = this.chart;
        options = merge(this.userOptions, options);
        this.destroy(true);
        this.init(chart, options);
        chart.isDirtyBox = true;
        if (pick(redraw, true)) {
            chart.redraw();
        }
    }
    /**
     * Remove the axis from the chart.
     *
     * @sample highcharts/members/chart-addaxis/
     *         Add and remove axes
     *
     * @function Highcharts.Axis#remove
     *
     * @param {boolean} [redraw=true]
     * Whether to redraw the chart following the remove.
     */
    remove(redraw) {
        const chart = this.chart, coll = this.coll, axisSeries = this.series;
        let i = axisSeries.length;
        // Remove associated series (#2687)
        while (i--) {
            if (axisSeries[i]) {
                axisSeries[i].remove(false);
            }
        }
        // Remove the axis
        erase(chart.axes, this);
        erase(chart[coll] || [], this);
        chart.orderItems(coll);
        this.destroy();
        chart.isDirtyBox = true;
        if (pick(redraw, true)) {
            chart.redraw();
        }
    }
    /**
     * Update the axis title by options after render time.
     *
     * @sample highcharts/members/axis-settitle/
     *         Set a new Y axis title
     *
     * @function Highcharts.Axis#setTitle
     *
     * @param {Highcharts.AxisTitleOptions} titleOptions
     * The additional title options.
     *
     * @param {boolean} [redraw=true]
     * Whether to redraw the chart after setting the title.
     */
    setTitle(titleOptions, redraw) {
        this.update({ title: titleOptions }, redraw);
    }
    /**
     * Set new axis categories and optionally redraw.
     *
     * @sample highcharts/members/axis-setcategories/
     *         Set categories by click on a button
     *
     * @function Highcharts.Axis#setCategories
     *
     * @param {Array<string>} categories
     * The new categories.
     *
     * @param {boolean} [redraw=true]
     * Whether to redraw the chart.
     */
    setCategories(categories, redraw) {
        this.update({ categories: categories }, redraw);
    }
}
/* *
 *
 *  Static Properties
 *
 * */
/**
 * Properties to survive after destroy, needed for Axis.update (#4317,
 * #5773, #5881).
 * @internal
 */
Axis.keepProps = [
    'coll',
    'extKey',
    'hcEvents',
    'len',
    'names',
    'series',
    'userMax',
    'userMin'
];
/* *
 *
 *  Default Export
 *
 * */
export default Axis;
/* *
 *
 *  API Declarations
 *
 * */
/**
 * Options for the path on the Axis to be calculated.
 * @interface Highcharts.AxisPlotLinePathOptionsObject
 */ /**
* Axis value.
* @name Highcharts.AxisPlotLinePathOptionsObject#value
* @type {number|undefined}
*/ /**
* Line width used for calculation crisp line coordinates. Defaults to 1.
* @name Highcharts.AxisPlotLinePathOptionsObject#lineWidth
* @type {number|undefined}
*/ /**
* If `false`, the function will return null when it falls outside the axis
* bounds. If `true`, the function will return a path aligned to the plot area
* sides if it falls outside. If `pass`, it will return a path outside.
* @name Highcharts.AxisPlotLinePathOptionsObject#force
* @type {string|boolean|undefined}
*/ /**
* Used in Highcharts Stock. When `true`, plot paths
* (crosshair, plotLines, gridLines)
* will be rendered on all axes when defined on the first axis.
* @name Highcharts.AxisPlotLinePathOptionsObject#acrossPanes
* @type {boolean|undefined}
*/ /**
* Use old coordinates (for resizing and rescaling).
* If not set, defaults to `false`.
* @name Highcharts.AxisPlotLinePathOptionsObject#old
* @type {boolean|undefined}
*/ /**
* If given, return the plot line path of a pixel position on the axis.
* @name Highcharts.AxisPlotLinePathOptionsObject#translatedValue
* @type {number|undefined}
*/ /**
* Used in Polar axes. Reverse the positions for concatenation of polygonal
* plot bands
* @name Highcharts.AxisPlotLinePathOptionsObject#reverse
* @type {boolean|undefined}
*/
/**
 * Options for crosshairs on axes.
 *
 * @product highstock
 *
 * @typedef {Highcharts.XAxisCrosshairOptions|Highcharts.YAxisCrosshairOptions} Highcharts.AxisCrosshairOptions
 */
/**
 * @typedef {"navigator"|"pan"|"rangeSelectorButton"|"rangeSelectorInput"|"scrollbar"|"traverseUpButton"|"zoom"} Highcharts.AxisExtremesTriggerValue
 */
/**
 * @callback Highcharts.AxisEventCallbackFunction
 *
 * @param {Highcharts.Axis} this
 */
/**
 * @callback Highcharts.AxisLabelsFormatterCallbackFunction
 *
 * @param {Highcharts.AxisLabelsFormatterContextObject} this
 *
 * @param {Highcharts.AxisLabelsFormatterContextObject} ctx
 * Since v12.6.0, the formatter context passed as an extra argument for arrow
 * functions.
 *
 * @return {string}
 */
/**
 * @interface Highcharts.AxisLabelsFormatterContextObject
 */ /**
* The axis item of the label
* @name Highcharts.AxisLabelsFormatterContextObject#axis
* @type {Highcharts.Axis}
*/ /**
* The chart instance.
* @name Highcharts.AxisLabelsFormatterContextObject#chart
* @type {Highcharts.Chart}
*/ /**
* Default formatting of date/time labels.
* @name Highcharts.AxisLabelsFormatterContextObject#dateTimeLabelFormat
* @type {string|undefined}
*/ /**
* Whether the label belongs to the first tick on the axis.
* @name Highcharts.AxisLabelsFormatterContextObject#isFirst
* @type {boolean}
*/ /**
* Whether the label belongs to the last tick on the axis.
* @name Highcharts.AxisLabelsFormatterContextObject#isLast
* @type {boolean}
*/ /**
* The position on the axis in terms of axis values. For category axes, a
* zero-based index. For datetime axes, the JavaScript time in milliseconds
* since 1970.
* @name Highcharts.AxisLabelsFormatterContextObject#pos
* @type {number}
*/ /**
* The preformatted text as the result of the default formatting. For example
* dates will be formatted as strings, and numbers with language-specific comma
* separators, thousands separators and numeric symbols like `k` or `M`.
* @name Highcharts.AxisLabelsFormatterContextObject#text
* @type {string|undefined}
*/ /**
* The Tick instance.
* @name Highcharts.AxisLabelsFormatterContextObject#tick
* @type {Highcharts.Tick}
*/ /**
* This can be either a numeric value or a category string.
* @name Highcharts.AxisLabelsFormatterContextObject#value
* @type {number|string}
*/
/**
 * Options for axes.
 *
 * @typedef {Highcharts.XAxisOptions|Highcharts.YAxisOptions|Highcharts.ZAxisOptions} Highcharts.AxisOptions
 */
/**
 * @callback Highcharts.AxisPointBreakEventCallbackFunction
 *
 * @param {Highcharts.Axis} this
 *
 * @param {Highcharts.AxisPointBreakEventObject} evt
 */
/**
 * @interface Highcharts.AxisPointBreakEventObject
 */ /**
* @name Highcharts.AxisPointBreakEventObject#brk
* @type {Highcharts.Dictionary<number>}
*/ /**
* @name Highcharts.AxisPointBreakEventObject#point
* @type {Highcharts.Point}
*/ /**
* @name Highcharts.AxisPointBreakEventObject#preventDefault
* @type {Function}
*/ /**
* @name Highcharts.AxisPointBreakEventObject#target
* @type {Highcharts.SVGElement}
*/ /**
* @name Highcharts.AxisPointBreakEventObject#type
* @type {"pointBreak"|"pointInBreak"}
*/
/**
 * @callback Highcharts.AxisSetExtremesEventCallbackFunction
 *
 * @param {Highcharts.Axis} this
 *
 * @param {Highcharts.AxisSetExtremesEventObject} evt
 */
/**
 * @interface Highcharts.AxisSetExtremesEventObject
 * @extends Highcharts.ExtremesObject
 */ /**
* @name Highcharts.AxisSetExtremesEventObject#preventDefault
* @type {Function}
*/ /**
* @name Highcharts.AxisSetExtremesEventObject#target
* @type {Highcharts.SVGElement}
*/ /**
* @name Highcharts.AxisSetExtremesEventObject#trigger
* @type {Highcharts.AxisExtremesTriggerValue|string}
*/ /**
* @name Highcharts.AxisSetExtremesEventObject#type
* @type {"setExtremes"}
*/
/**
 * @callback Highcharts.AxisTickPositionerCallbackFunction
 *
 * @param {Highcharts.Axis} this
 *
 * @param {number} min
 * Current minimum value.
 *
 * @param {number} max
 * Current maximum value.
 *
 * @param {Highcharts.Axis} [ctx]
 * Since v12.6.0, the axis context passed as an extra argument for arrow
 * functions.
 *
 * @return {Highcharts.AxisTickPositionsArray|undefined}
 */
/**
 * @interface Highcharts.AxisTickPositionsArray
 * @augments Array<number>
 */
/**
 * @typedef {"high"|"low"|"middle"} Highcharts.AxisTitleAlignValue
 */
/**
 * @typedef {Highcharts.XAxisTitleOptions|Highcharts.YAxisTitleOptions|Highcharts.ZAxisTitleOptions} Highcharts.AxisTitleOptions
 */
/**
 * @typedef {"linear"|"logarithmic"|"datetime"|"category"|"treegrid"} Highcharts.AxisTypeValue
 */
/**
 * The returned object literal from the {@link Highcharts.Axis#getExtremes}
 * function.
 *
 * @interface Highcharts.ExtremesObject
 */ /**
* The maximum value of the axis' associated series.
* @name Highcharts.ExtremesObject#dataMax
* @type {number}
*/ /**
* The minimum value of the axis' associated series.
* @name Highcharts.ExtremesObject#dataMin
* @type {number}
*/ /**
* The maximum axis value, either automatic or set manually. If the `max` option
* is not set, `maxPadding` is 0 and `endOnTick` is false, this value will be
* the same as `dataMax`.
* @name Highcharts.ExtremesObject#max
* @type {number}
*/ /**
* The minimum axis value, either automatic or set manually. If the `min` option
* is not set, `minPadding` is 0 and `startOnTick` is false, this value will be
* the same as `dataMin`.
* @name Highcharts.ExtremesObject#min
* @type {number}
*/ /**
* The user defined maximum, either from the `max` option or from a zoom or
* `setExtremes` action.
* @name Highcharts.ExtremesObject#userMax
* @type {number|undefined}
*/ /**
* The user defined minimum, either from the `min` option or from a zoom or
* `setExtremes` action.
* @name Highcharts.ExtremesObject#userMin
* @type {number|undefined}
*/
/**
 * Formatter function for the text of a crosshair label.
 *
 * @callback Highcharts.XAxisCrosshairLabelFormatterCallbackFunction
 *
 * @param {Highcharts.Axis} this
 * Axis context
 *
 * @param {number} value
 * Y value of the data point
 *
 * @param {Highcharts.Axis} [ctx]
 * Since v12.6.0, the axis context passed as an extra argument for arrow
 * functions.
 *
 * @return {string}
 */
''; // Keeps doclets above in JS file
