helpers/axis.js

"use strict";

import * as d3 from "d3";
import Bar from "../controls/Bar/Bar";
import { getScale, getType, getDomain } from "../core/BaseConfig";
import constants, { AXES_ORIENTATION, AXIS_TYPE } from "../helpers/constants";
import styles from "../helpers/styles";
import utils from "../helpers/utils";
import { DEFAULT_TICK_FORMAT } from "../locale";
import { prepareHAxis } from "./datetimeBuckets";
import { shouldTruncateLabel, truncateLabel } from "./label";

/**
 * @module axis
 * @alias module:axis
 */

/**
 * Checks if Y or Y2 axis starts from Origin
 *
 * @private
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {string} yAxis - Y, Y2 etc
 * @returns {boolean} True if axis does not start from origin, false otherwise
 */
const hasNegativeLowerBound = (scale, yAxis = constants.Y_AXIS) =>
    d3.min(scale[yAxis].domain()) < 0 && d3.max(scale[yAxis].domain()) > 0;
/**
 * Parses the Y Axis lower and upper limits and returns it as an array for Y Axis reference line
 *
 * @private
 * @param {object} scale - d3 scale taking into account the input parameters
 * @returns {Array} x and y co-ordinate data for drawing a reference line
 */
const getReferenceLineData = (scale) => [
    {
        x: scale.x.domain()[0],
        y: 0
    },
    {
        x: scale.x.domain()[1],
        y: 0
    }
];
/**
 * Creates a simple reference line with x and y attributes
 *
 * @private
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {string} yAxis - Y, Y2 etc
 * @returns {d3.Line} A d3 line
 */
const createReferenceLine = (scale, yAxis) =>
    d3
        .line()
        .x((value) => scale.x(value.x))
        .y((value) => scale[yAxis](value.y));
/**
 * Create the d3 Axes - X, Y and Y2 and append into the canvas.
 * If axis.x.show, axis.y.show or axis.y2.show is set to false:
 * then the axis will be hidden
 *
 * @private
 * @param {object} axis - Axis scaled according to input parameters
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {object} config - config object derived from input JSON
 * @param {d3.selection} canvasSVG - d3 selection node of canvas svg
 * @returns {undefined} - returns nothing
 */
const createAxes = (axis, scale, config, canvasSVG) => {
    getAxesScale(axis, scale, config);
    prepareHAxis(scale, axis, config, prepareHorizontalAxis);
    canvasSVG
        .append("g")
        .classed(styles.axis, true)
        .classed(styles.axisX, true)
        .attr("aria-hidden", !config.axis.x.show)
        .attr(
            "transform",
            `translate(${getXAxisXPosition(config)}, ${getXAxisYPosition(
                config
            )})`
        )
        .call(axis.x)
        .call(resetD3FontSize);
    canvasSVG
        .append("g")
        .classed(styles.axis, true)
        .classed(styles.axisY, true)
        .attr("aria-hidden", !config.axis.y.show)
        .attr(
            "transform",
            `translate(${getYAxisXPosition(config)}, ${getYAxisYPosition(
                config
            )})`
        )
        .call(axis.y)
        .call(resetD3FontSize);
    if (hasY2Axis(config.axis)) {
        canvasSVG
            .append("g")
            .classed(styles.axis, true)
            .classed(styles.axisY2, true)
            .attr(
                "transform",
                `translate(${getY2AxisXPosition(config)}, ${getY2AxisYPosition(
                    config
                )})`
            )
            .call(axis.y2)
            .call(resetD3FontSize);
    }
};

/**
 * Create the axis for text labels
 *
 * @private
 * @param {object} axis - Axis scaled according to input parameters
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {object} config - config object derived from input JSON
 * @param {Array} canvasSVG - d3 object of canvas svg
 * @returns {undefined} - returns nothing
 */
const createXAxisInfoRow = (axis, scale, config, canvasSVG) => {
    getAxesScale(axis, scale, config);
    canvasSVG
        .append("g")
        .classed(styles.axis, true)
        .classed(styles.axisInfoRow, true)
        .attr("aria-hidden", true)
        .attr(
            "transform",
            `translate(${getXAxisXPosition(config)}, ${getAxisInfoRowYPosition(
                config
            )})`
        )
        .call(axis.axisInfoRow.x)
        .call(resetD3FontSize);
};

/**
 * Creates a horizontal reference line at 0, when Y Axis does not start at 0.
 *
 * @private
 * @param {object} axis - Axis scaled according to input parameters
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {object} config - config object derived from input JSON
 * @param {d3.selection} canvasSVG - d3 selection node of canvas svg
 * @returns {undefined} - returns nothing
 */
const createAxisReferenceLine = (axis, scale, config, canvasSVG) => {
    const transformAttribute = `translate(${getYAxisXPosition(
        config
    )}, ${getYAxisYPosition(config)})`;
    const setReferenceLineAttributes = (path, style) =>
        path
            .classed(styles.axisReferenceLine, true)
            .attr("aria-hidden", !hasNegativeLowerBound(scale, style))
            .attr("transform", transformAttribute)
            .attr(
                "d",
                createReferenceLine(scale, style)(getReferenceLineData(scale))
            );
    setReferenceLineAttributes(canvasSVG.append("path"), constants.Y_AXIS)
        .classed(styles.axis, true)
        .classed(styles.axisY, true);
    if (hasY2Axis(config.axis)) {
        setReferenceLineAttributes(canvasSVG.append("path"), constants.Y2_AXIS)
            .classed(styles.axis, true)
            .classed(styles.axisY2, true);
    }
};

/**
 * Prepares X,Y,Y2 and an optional axis info row (label row for Bar graphs) Axes according to their scale and available container width and height
 *
 * @private
 * @param {object} axis - Axis scaled according to input parameters
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {object} config - config object derived from input JSON
 * @returns {object} - Scaled axes object
 */
const getAxesScale = (axis, scale, config) => {
    let tickFormatToTrimTrailingZeros;

    // If suppressTrailingZeros is set to true and x axis type is set as
    // DEFAULT (normal number based axes values) and x axis's tick format
    // is not provided by the consumer, then invoke tickFormatter()
    // to insert '~' just before the default d3 tick format type
    // to suppress ticks values's trailing zeros
    if (
        config.axis.x.suppressTrailingZeros &&
        config.axis.x.type === AXIS_TYPE.DEFAULT &&
        utils.isUndefined(config.axis.x.ticks.format)
    ) {
        axis.x = isXAxisOrientationTop(config.axis.x.orientation)
            ? d3.axisTop(scale.x)
            : d3.axisBottom(scale.x);
        tickFormatToTrimTrailingZeros = tickFormatter(axis.x);
    }

    axis.x = prepareXAxis(
        scale.x,
        config.axis.x.ticks.values,
        getXAxisWidth(config),
        getAxisTickFormat(
            config.d3Locale,
            utils.isDefined(config.axis.x.ticks.format)
                ? config.axis.x.ticks.format
                : tickFormatToTrimTrailingZeros,
            config.axis.x.type
        ),
        config.axis.x.orientation
    );

    // Reset the tickFormatToTrimTrailingZeros to null, so that
    // if the y axis suppressTrailingZeros is set to false and
    // consumer has not defined its tick format,
    // we can ensure we are not using the same x axis's tick format
    // for y axis as well
    tickFormatToTrimTrailingZeros = null;

    axis.axisInfoRow.x = prepareXAxisInfoRow(
        scale.x,
        getAxisInfoOrientation(config.axis.x.orientation)
    );

    if (hasY2Axis(config.axis)) {
        // Y and Y2 axes - custom tick values. Takes priority
        // and ignores ticksCount if it is set. Will not work if only
        // Y2 ticks are provided.
        if (utils.isDefined(config.axis.y.ticks.values)) {
            // If suppressTrailingZeros is set to true and y axis's tick format
            // is not provided by the consumer, then invoke tickFormatter()
            // to insert '~' just before the default d3 tick format type
            // to suppress ticks values's trailing zeros
            if (
                config.axis.y.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y.ticks.format)
            ) {
                axis.y = d3.axisLeft(scale.y);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y);
            }
            axis.y = prepareYAxis(
                scale.y,
                config.axis.y.ticks.values,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y.ticks.format)
                        ? config.axis.y.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );

            // Reset the tickFormatToTrimTrailingZeros to null, so that
            // if the y2 axis suppressTrailingZeros is set to false and
            // consumer has not defined its tick format,
            // we can ensure we are not using the same y axis's tick format
            // for y2 axis as well
            tickFormatToTrimTrailingZeros = null;

            // If suppressTrailingZeros is set to true and y2 axis's tick format
            // is not provided by the consumer, then invoke tickFormatter()
            // to insert '~' just before the default d3 tick format type
            // to suppress ticks values's trailing zeros
            if (
                config.axis.y2.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y2.ticks.format)
            ) {
                axis.y2 = d3.axisRight(scale.y2);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y2);
            }
            axis.y2 = prepareY2Axis(
                scale.y2,
                config.axis.y2.ticks.values,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y2.ticks.format)
                        ? config.axis.y2.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
            return axis;
        }
        // Y and Y2 axes - ticksCount.
        else if (
            utils.isDefined(config.ticksCount) &&
            config.ticksCount <= constants.TICKSCOUNT_MAXLIMIT
        ) {
            const yTickValues = generateYAxesTickValues(
                config.axis.y.domain.lowerLimit,
                config.axis.y.domain.upperLimit,
                config.ticksCount,
                config.allowCalibration
            );

            const y2TickValues = generateYAxesTickValues(
                config.axis.y2.domain.lowerLimit,
                config.axis.y2.domain.upperLimit,
                config.ticksCount,
                config.allowCalibration
            );

            if (
                config.axis.y.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y.ticks.format)
            ) {
                axis.y = d3.axisLeft(scale.y);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y);
            }
            axis.y = prepareYAxis(
                scale.y,
                yTickValues,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y.ticks.format)
                        ? config.axis.y.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
            tickFormatToTrimTrailingZeros = null;

            if (
                config.axis.y2.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y2.ticks.format)
            ) {
                axis.y2 = d3.axisRight(scale.y2);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y2);
            }
            axis.y2 = prepareY2Axis(
                scale.y2,
                y2TickValues,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y2.ticks.format)
                        ? config.axis.y2.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
        }
        // Y and Y2 axes - If ticksCount is undefined or greater than
        // TICKS_MAXCOUNT AND if the Y2 is visible, then utilize a default value
        // for the ticksCount. This is based on the ranges of the Y & Y2 axes.
        else {
            const ticksCount = getAverageTicksCount(
                config.axis.y.domain.upperLimit -
                    config.axis.y.domain.lowerLimit,
                config.axis.y2.domain.upperLimit -
                    config.axis.y2.domain.lowerLimit
            );

            const yTickValues = generateYAxesTickValues(
                config.axis.y.domain.lowerLimit,
                config.axis.y.domain.upperLimit,
                ticksCount,
                config.allowCalibration
            );
            const y2TickValues = generateYAxesTickValues(
                config.axis.y2.domain.lowerLimit,
                config.axis.y2.domain.upperLimit,
                ticksCount,
                config.allowCalibration
            );

            if (
                config.axis.y.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y.ticks.format)
            ) {
                axis.y = d3.axisLeft(scale.y);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y);
            }
            axis.y = prepareYAxis(
                scale.y,
                yTickValues,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y.ticks.format)
                        ? config.axis.y.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
            tickFormatToTrimTrailingZeros = null;

            if (
                config.axis.y2.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y2.ticks.format)
            ) {
                axis.y2 = d3.axisRight(scale.y2);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y2);
            }
            axis.y2 = prepareY2Axis(
                scale.y2,
                y2TickValues,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y2.ticks.format)
                        ? config.axis.y2.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
        }
    }
    // Only single Y axis
    else {
        // Single Y axis - custom tick values
        if (utils.isDefined(config.axis.y.ticks.values)) {
            if (
                config.axis.y.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y.ticks.format)
            ) {
                axis.y = d3.axisLeft(scale.y);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y);
            }
            axis.y = prepareYAxis(
                scale.y,
                config.axis.y.ticks.values,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y.ticks.format)
                        ? config.axis.y.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
            // return axis;
        }
        // Single Y axis - ticksCount is defined
        else if (
            utils.isDefined(config.ticksCount) ||
            config.ticksCount <= constants.TICKSCOUNT_MAXLIMIT
        ) {
            const yTickValues = generateYAxesTickValues(
                config.axis.y.domain.lowerLimit,
                config.axis.y.domain.upperLimit,
                config.ticksCount,
                config.allowCalibration
            );

            if (
                config.axis.y.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y.ticks.format)
            ) {
                axis.y = d3.axisLeft(scale.y);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y);
            }
            axis.y = prepareYAxis(
                scale.y,
                yTickValues,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y.ticks.format)
                        ? config.axis.y.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
        }
        // Single Y axis - default case when
        // config.axis.y.ticks.values and ticksCount
        // are not defined
        else {
            if (
                config.axis.y.suppressTrailingZeros &&
                utils.isUndefined(config.axis.y.ticks.format)
            ) {
                axis.y = d3.axisLeft(scale.y);
                tickFormatToTrimTrailingZeros = tickFormatter(axis.y);
            }
            axis.y = prepareYAxis(
                scale.y,
                undefined,
                config.height,
                getAxisTickFormat(
                    config.locale,
                    utils.isDefined(config.axis.y.ticks.format)
                        ? config.axis.y.ticks.format
                        : tickFormatToTrimTrailingZeros
                )
            );
        }
    }
    return axis;
};

/**
 * Inserts '~' just before the format type to suppress ticks values's trailing zeros when default d3 tick formatting is used
 *
 * @private
 * @param {object} axis - Scaled axes object
 * @returns {string} tick format string
 */
const tickFormatter = (axis) => {
    const defaultTickFormat = axis.scale().tickFormat().toString();

    // Return the default d3 tick format with the '~' character inserted just before the format type
    // Eg: defaultTickFormat: .1f
    // Value returned below will be: .1~f
    return `${defaultTickFormat.slice(
        0,
        defaultTickFormat.length - 1
    )}~${defaultTickFormat.slice(defaultTickFormat.length - 1)}`;
};

/**
 * Ticks can be formatted by passing the format string via input JSON.
 * For Empty tick labels consumer would pass format as "" (blank)
 * For formatting numbers (x,y,y2 axes ticks) use Python specifiers.
 * Ticks can also be formatted for date time inputs.
 *
 * @private
 * @see https://docs.python.org/2/library/string.html#format-specification-mini-language
 * @see https://github.com/d3/d3-time-format/blob/master/README.md#locales
 * @param {object} locale - d3 Locale object
 * @param {string} format - tick format string
 * @param {string} type - default or timeseries chart type
 * @returns {object} d3 locale object formatter
 */
const getAxisTickFormat = (locale, format, type = AXIS_TYPE.DEFAULT) => {
    if (format === "") {
        return format;
    }
    const _locale =
        type === AXIS_TYPE.TIME_SERIES
            ? d3.timeFormatDefaultLocale(locale)
            : d3.formatDefaultLocale(locale);

    if (utils.isEmpty(format)) {
        return DEFAULT_TICK_FORMAT;
    }
    return _locale.format(format);
};

/**
 * Gets the tick values with correct format.
 * If there are no tick values provided then null is returned
 * If the ticks values are in a ISO8601 format then a date object is returned
 * No processing is done, otherwise
 *
 * @private
 * @param {Array} ticks - Array of values that represent the tick values
 * @returns {(Array|null)} returns processed ticks, null otherwise
 */
const processTickValues = (ticks) => {
    if (utils.isEmpty(ticks)) {
        return null;
    }
    return ticks.map((t) => (utils.isDate(t) ? utils.parseDateTime(t) : t));
};

/**
 * Gets the number of ticks on the axis based on the upper and lower limits
 *
 * @private
 * @param {number} range - range of values (upperLimit - lowerLimits)
 * @returns {number} returns number of ticks for that range, based on a predefined set
 */
const getTicksCountFromRange = (range) => {
    let ticksCount;

    switch (true) {
        case range <= constants.AXISRANGE_ONE:
            ticksCount = constants.DEFAULT_TICKSCOUNT - 4;
            break;

        case range <= constants.AXISRANGE_TWO:
            ticksCount = constants.DEFAULT_TICKSCOUNT - 3;
            break;

        case range <= constants.AXISRANGE_THREE:
            ticksCount = constants.DEFAULT_TICKSCOUNT - 2;
            break;

        case range <= constants.AXISRANGE_FOUR:
            ticksCount = constants.DEFAULT_TICKSCOUNT - 1;
            break;

        default:
            ticksCount = constants.DEFAULT_TICKSCOUNT;
    }

    return ticksCount;
};

/**
 * Gets average number of ticks to be used based on the Y and Y2 axes
 * result from getTicksCountFromRange for Y and Y2 axes
 *
 * @private
 * @param {number} rangeY - Y axis range (upperLimit - lowerLimit)
 * @param {number} rangeY2 - Y2 axis range (upperLimit - lowerLimit)
 * @returns {number} returns number of ticks to be rendered between the upper limits & lower limits with
 */
const getAverageTicksCount = (rangeY, rangeY2) => {
    const yTicksCount = getTicksCountFromRange(rangeY);
    const y2TicksCount = getTicksCountFromRange(rangeY2);

    return Math.round((yTicksCount + y2TicksCount) / 2);
};

/**
 * Generates an array of tick values for to be used as the
 * tick labels on the Y & Y2 axis.
 *
 * @private
 * @param {number} lowerLimit - Lower limit of the Y or Y2 Axis
 * @param {number} upperLimit - Upper limit of the Y or Y2 Axis
 * @param {number} ticksCount - Number of ticks between the upper and lower limits
 * @param {boolean} allowCalibration - Whether this property is true or false
 * @returns {(Array)} returns array of values to be used as tick labels
 */
const generateYAxesTickValues = (
    lowerLimit,
    upperLimit,
    ticksCount = constants.DEFAULT_TICKSCOUNT,
    allowCalibration = true
) => {
    ticksCount = Math.abs(ticksCount);
    const tickValues = [];

    // use the d3,js nice function to round off the upper and lower limits
    // to multiples of 2, 5 or 10

    if (allowCalibration) {
        [lowerLimit, upperLimit] = d3
            .scaleLinear()
            .domain([lowerLimit, upperLimit])
            .nice()
            .domain();
    }

    tickValues.push(lowerLimit);
    tickValues.push(upperLimit);

    if (lowerLimit < 0) {
        tickValues.push(0);
    }

    const interval = (upperLimit - lowerLimit) / (ticksCount + 1);

    for (let index = 1; index <= ticksCount; index++) {
        tickValues.push(lowerLimit + interval * index);
    }

    return tickValues;
};

/**
 * Based on x axis orientation, sets the axis info row orientation.
 * If x axis orientation is top, axis info row orientation is bottom.
 * If x axis orientation is bottom, axis info row orientation is top.
 *
 * @private
 * @param {string} xAxisOrientation - x axis orientation
 * @returns {string} returns orientation for axis info row.
 */
const getAxisInfoOrientation = (xAxisOrientation) =>
    isXAxisOrientationTop(xAxisOrientation)
        ? AXES_ORIENTATION.X.BOTTOM
        : AXES_ORIENTATION.X.TOP;
/**
 * Creates the axis using the scale provided for X Axis using d3 svg axis.
 * If tickValues are provided then they are reserved precedence over ticks/tick counts.
 *
 * @private
 * @param {object} scale - d3 scale calculated using domain and range
 * @param {Array} tickValues - Array of values that represent the tick values
 * @param {number} width - Width of the canvas which will be used to tell d3 how many ticks to
 * keep in the X axis
 * @param {object} format - d3 locale object formatted to represent the tick.
 * @param {string} [orientation] - Axis orientation
 * @returns {object} d3 object which forms the x-axis scale
 */
const prepareXAxis = (
    scale,
    tickValues,
    width,
    format,
    orientation = AXES_ORIENTATION.X.BOTTOM
) => {
    let d3Axis = d3.axisBottom(scale);
    if (isXAxisOrientationTop(orientation)) {
        d3Axis = d3.axisTop(scale);
    }
    d3Axis
        .ticks(
            Math.max(width / constants.MAX_TICK_VARIANCE, constants.MIN_TICKS)
        )
        .tickValues(processTickValues(tickValues))
        .tickFormat(format);
    return d3Axis;
};

/**
 * Creates the axis using the scale provided for X Axis using d3 svg axis.
 *
 * @private
 * @param {object} scale - d3 scale calculated using domain and range
 * @param {string} [orientation] - Axis orientation
 * @returns {object} d3 object which forms the text label axis scale
 */
const prepareXAxisInfoRow = (
    scale,
    orientation = AXES_ORIENTATION.X.BOTTOM
) => {
    let d3Axis = d3.axisBottom(scale);
    if (isXAxisOrientationTop(orientation)) {
        d3Axis = d3.axisTop(scale);
    }
    d3Axis.tickValues([]);
    return d3Axis;
};

/**
 * Helper function to Create the axis using the scale provided for X Axis using d3 svg axis.
 *
 * @param {object} scale - d3 scale calculated using domain and range
 * @param {Array} tickValues - Array of values that represent the tick values
 * @param {object} config - config object derived from input JSON
 * @param {string} [orientation] - Axis orientation
 * @returns {object} - d3 Object which forms the axis scale
 */
const prepareHorizontalAxis = (scale, tickValues, config, orientation) =>
    prepareXAxis(
        scale.x,
        tickValues,
        getXAxisWidth(config),
        getAxisTickFormat(
            config.d3Locale,
            config.axis.x.ticks.format,
            config.axis.x.type
        ),
        orientation
    );

/**
 * Creates the axis using the scale provided for Y Axis using d3 svg axis
 *
 * @private
 * @param {object} scale - d3 scale calculated using domain and range
 * @param {Array} tickValues - Array of values that represent the tick values
 * @param {number} height - Height of the Y Axis to calculate the number of Y Axis ticks
 * @param {object} format - d3 locale object formatted to represent the tick.
 * @returns {object} d3 object which forms the y-axis scale
 */
const prepareYAxis = (scale, tickValues, height, format) =>
    d3
        .axisLeft(scale)
        .ticks(height / constants.DEFAULT_Y_AXIS_SPACING)
        .tickValues(tickValues)
        .tickFormat(format);

/**
 * Creates the axis using the scale provided for Y2 Axis using d3 svg axis
 *
 * @private
 * @param {object} scale - d3 scale calculated using domain and range
 * @param {Array} tickValues - Array of values that represent the tick values
 * @param {number} height - Height of the Y2 Axis to calculate the number of Y2 Axis ticks
 * @param {object} format - d3 locale object formatted to represent the tick.
 * @returns {object} d3 object which forms the y2-axis scale
 */
const prepareY2Axis = (scale, tickValues, height, format) =>
    d3
        .axisRight(scale)
        .ticks(height / constants.DEFAULT_Y_AXIS_SPACING)
        .tickValues(tickValues)
        .tickFormat(format);

/**
 * Returns the number of degrees the rotation of axis needs to be performed based on axis
 *
 * @private
 * @param {string} axis - X, Y or Y2 axis
 * @returns {number} amount of degrees the rotation needs to be performed
 */
const getRotationForAxis = (axis) => {
    switch (axis) {
        case constants.Y_AXIS:
            return -90;
        case constants.Y2_AXIS:
            return 90;
        default:
            return 0;
    }
};
/**
 * X Axis label's starting position below the graph
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the label
 */
const getXAxisLabelXPosition = (config) =>
    getXAxisXPosition(config) + getXAxisWidth(config) / 2;
/**
 * X Axis label's position vertically below the graph
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the label
 */
const getXAxisLabelYPosition = (config) =>
    isXAxisOrientationTop(config.axis.x.orientation)
        ? calculateVerticalPadding(config) - config.axisLabelHeights.x * 2
        : getXAxisYPosition(config) +
          config.axisLabelHeights.x * 2 +
          (config.padding.bottom - config.axisInfoRowLabelHeight) * 2;
/**
 * Y Axis label's starting position vertically beside the graph
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the label
 */
const getYAxisLabelXPosition = (config) => {
    // If y2-axis is true, y-axis label should move close to svg container else move closer to y-axis.
    if (hasY2Axis(config.axis)) {
        return config.padding.left - config.axisLabelWidths.y;
    } else {
        return config.padding.left;
    }
};
/**
 * Y Axis label's position distance away from the graph
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the label
 */
const getYAxisLabelYPosition = (config) =>
    getYAxisYPosition(config) +
    (getYAxisHeight(config) - config.padding.left / 2) / 2;
/**
 * Y Axis label shape starting position vertically beside the graph
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the label shape
 */
const getYAxisLabelShapeXPosition = (config) =>
    getYAxisLabelXPosition(config) + constants.BASE_LABEL_ICON_HEIGHT_PADDING;
/**
 * Y Axis label shape position distance away from the graph.
 * We are taking the Container height and adding it with
 * half of the width for label shape container so that it centers to the graph.
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @param {number} shapeCount - Number of shapes within shape container
 * @returns {number} Position for the label
 */
const getYAxisLabelShapeYPosition = (config, shapeCount) =>
    getYAxisLabelYPosition(config) +
    (shapeCount * constants.BASE_LABEL_ICON_HEIGHT_PADDING) / 1.5;
/**
 * Y2 Axis label shape position distance away from the graph.
 * We are taking the Container height and subtracting it with
 * Label shape container width to center it with respect to the
 * container in reverse order.
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @param {number} shapeCount - Number of shapes within shape container
 * @returns {number} Position for the label
 */
const getY2AxisLabelShapeYPosition = (config, shapeCount) =>
    getYAxisLabelYPosition(config) -
    (shapeCount * constants.BASE_LABEL_ICON_HEIGHT_PADDING) / 1.5;
/**
 * Y2 Axis label's starting position vertically beside the graph
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the label
 */
const getY2AxisLabelXPosition = (config) =>
    getY2AxisXPosition(config) +
    config.padding.right +
    config.axisLabelWidths.y2;
/**
 * Y2 Axis label shape starting position vertically beside the graph
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the label shape
 */
const getY2AxisLabelShapeXPosition = (config) =>
    getY2AxisLabelXPosition(config) - constants.BASE_LABEL_ICON_HEIGHT_PADDING;
/**
 * Calculates Vertical Padding according to X Axis orientation
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Vertical Padding for X Axis
 */
const calculateVerticalPadding = (config) => {
    if (!isXAxisOrientationTop(config.axis.x.orientation)) {
        return config.padding.bottom;
    } else if (!config.axisLabelHeights.x) {
        return config.padding.top;
    }
    return config.axisLabelHeights.x * 2 + config.padding.top;
};
/**
 * X Axis's starting position within the canvas
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the axis
 */
const getXAxisXPosition = (config) =>
    config.axisSizes.y + config.axisLabelWidths.y;
/**
 * X Axis's position vertically relative to the canvas
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the axis
 */
const getAxisInfoRowYPosition = (config) =>
    isXAxisOrientationTop(config.axis.x.orientation)
        ? getYAxisHeight(config) + calculateVerticalPadding(config)
        : calculateVerticalPadding(config);
/**
 * Axis Info Row's position vertically relative to the canvas
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the axis
 */
const getXAxisYPosition = (config) =>
    isXAxisOrientationTop(config.axis.x.orientation)
        ? calculateVerticalPadding(config)
        : getYAxisHeight(config) + calculateVerticalPadding(config);
/**
 * Y Axis's starting position relative to the canvas
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the axis
 */
const getYAxisXPosition = (config) => getXAxisXPosition(config);
/**
 * Y Axis's position vertically relative to the canvas
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the axis
 */
const getYAxisYPosition = (config) => calculateVerticalPadding(config);
/**
 * Y2 Axis's starting position relative to the canvas
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the axis
 */
const getY2AxisXPosition = (config) =>
    getYAxisXPosition(config) + getXAxisWidth(config);
/**
 * Y2 Axis's position vertically relative to the canvas
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Position for the axis
 */
const getY2AxisYPosition = (config) => calculateVerticalPadding(config);
/**
 * X Axis's width that will hold equally spaced ticks
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} X Axis width
 */
const getXAxisWidth = (config) =>
    config.canvasWidth -
    config.axisSizes.y -
    config.axisSizes.y2 -
    config.axisLabelWidths.y -
    config.axisLabelWidths.y2;
/**
 * Y Axis height for the axis and canvas region to clip the chart within
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Height of the canvas
 */
const getYAxisHeight = (config) => config.height;
/**
 * X Axis height for the axis and labels to display within
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {number} Height of the X Axis ticks, labels and numbers/datetimes
 */
const getXAxisHeight = (config) => {
    if (config.padding.hasCustomPadding) {
        return config.padding.bottom;
    }
    const scale = getScale(config.axis.x.type)
        .domain(config.axis.x.domain)
        .range([0, config.canvasWidth]);
    const axis = d3.axisBottom(scale);
    const dummy = d3.select("body").append("div");
    const svg = dummy.append("svg");
    const group = svg.append("g").call(axis);
    const height = group.node().getBoundingClientRect().height;
    dummy.remove();
    return height;
};
/**
 * X Axis range used to instruct d3 when creating a scale
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {Array} lower and upper bound forming the range
 */
const getXAxisRange = (config) => [0, getXAxisWidth(config)];
/**
 * Y Axis range used to instruct d3 when creating a scale
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {Array} lower and upper bound forming the range
 */
const getYAxisRange = (config) => [getYAxisHeight(config), 0];
/**
 * Dynamically generate the label width for axes
 *
 * @private
 * @param {string} label - Label text
 * @param {string} axis - x, y or y2
 * @param {object} config - config object derived from input JSON
 * @returns {number} label width
 */
const getAxisLabelWidth = (label, axis, config) => {
    let width;
    const dummy = d3.select("body").append("div");
    const svg = dummy.append("svg");
    const grouper = svg
        .append("g")
        .attr("transform", `rotate(${getRotationForAxis(axis)})`);
    buildAxisLabel(grouper, label);

    // To avoid overlapping, (for y-axis shape and y-axis) we are setting default width, when space is passed as y-axis label.
    if (
        utils.isDefined(config) &&
        hasY2Axis(config.axis) &&
        label.trim().length === 0
    ) {
        width = constants.DEFAULT_CHARACTER_SVG_ELEMENT_WIDTH;
    } else {
        width = grouper.node().getBoundingClientRect().width;
    }
    dummy.remove();
    return width;
};

/**
 * Dynamically generate the label height for axes
 *
 * @private
 * @param {string} label - Label text
 * @returns {number} label height
 */
const getAxisLabelHeight = (label) => {
    const dummy = d3.select("body").append("div");
    const svg = dummy.append("svg");
    const grouper = svg.append("g");
    buildAxisLabel(grouper, label);
    const height = grouper.node().getBoundingClientRect().height;
    dummy.remove();
    return height;
};

/**
 * Dynamically generate the label width for y axes
 *
 * @private
 * @param {string} id - y or y2
 * @param {object} config - config object derived from input JSON
 * @returns {number} label width
 */
const getYAxisWidth = (id, config) => {
    if (config.padding.hasCustomPadding) {
        return config.padding.left;
    }
    const scale = d3
        .scaleLinear()
        .domain([
            config.axis[id].domain.lowerLimit,
            config.axis[id].domain.upperLimit
        ])
        .range([config.height, 0]);
    const axis = d3.axisLeft(scale);
    const dummy = d3.select("body").append("div");
    const svg = dummy.append("svg");
    const yAxisSVG = svg.append("g").call(axis);
    const width = yAxisSVG.node().getBoundingClientRect().width;
    dummy.remove();
    return width;
};

/**
 * Generate the label width for y2 axes.
 *
 * @private
 * @param {object} config - config object derived from input JSON.
 * @returns {number} label width
 */
const getY2AxisWidth = (config) => {
    if (config.padding.hasCustomPadding) {
        return config.padding.right;
    }
    return hasY2Axis(config.axis)
        ? getYAxisWidth(constants.Y2_AXIS, config)
        : 20;
};

/**
 * Checks if X Axis orientation is set to top
 *
 * @private
 * @param {string} xAxisOrientation - X Axis orientation
 * @returns {boolean} - true if X Axis orientation is set to top, false if it is bottom(default)
 */
const isXAxisOrientationTop = (xAxisOrientation) =>
    xAxisOrientation === AXES_ORIENTATION.X.TOP;
/**
 * Calculates axes sizes, specifically:
 *  X Axis: Height
 *  Y Axis: Width
 *  Y2 Axis: Width
 *
 *  @private
 *  @param {object} config - config object derived from input JSON
 *  @returns {undefined} - returns nothing
 */
const calculateAxesSize = (config) => {
    config.axisSizes = {};
    config.axisSizes.y =
        getYAxisWidth(constants.Y_AXIS, config) + config.padding.left;
    config.axisSizes.y2 = getY2AxisWidth(config) + config.padding.right;
    config.axisSizes.x = getXAxisHeight(config);
};

/**
 * Calculates axes label sizes, specifically:
 *  X Axis Label: Height
 *  Y Axis Label: Width
 *  Y2 Axis Label: Width
 *
 *  @private
 *  @param {object} config - config object derived from input JSON
 *  @returns {undefined} - returns nothing
 */
const calculateAxesLabelSize = (config) => {
    config.axisLabelHeights = {};
    config.axisLabelWidths = {};
    config.axisLabelHeights.x = 0;
    config.axisLabelWidths.y = 0;
    config.axisLabelWidths.y2 = 0;
    config.axisInfoRowLabelHeight = 0;
    if (config.showLabel) {
        if (config.axis.x.label) {
            config.axisLabelHeights.x = getAxisLabelHeight(config.axis.x.label);
        }
        if (config.axis.y.label) {
            config.axisLabelWidths.y = getAxisLabelWidth(
                config.axis.y.label,
                constants.Y_AXIS,
                config
            );
        }
        if (hasY2Axis(config.axis) && config.axis.y2.label) {
            config.axisLabelWidths.y2 = hasY2Axis(config.axis)
                ? getAxisLabelWidth(
                      config.axis.y2.label,
                      constants.Y2_AXIS,
                      config
                  )
                : 0;
        }
    }
};

/**
 * Returns the mid value of the axis domain relative to the lower bound
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @param {string} yAxis - Y, Y2 etc
 * @returns {number} returns a number representing the mid value of y axes domain
 */
const getMidPoint = (config, yAxis) => {
    const axisMidValue =
        (config.axis[yAxis].domain.upperLimit -
            config.axis[yAxis].domain.lowerLimit) /
        2;
    return config.axis[yAxis].domain.lowerLimit + axisMidValue;
};

/**
 * Calculates the lower part of the outlier based on data points.
 * If the content has any data points that are outside the lower and upper bounds set
 * in the vertical axis then we adjust the axis bounds to support that outlier value.
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {Array} List of lower bound values for each of the vertical axis
 */
const getLowerOutlierStretchFactorList = (config) => {
    const lowerStretchFactors = [];
    const getMinValue = (config, yAxis, axisMinValue) => {
        const dataRangeMinValue = config.axis[yAxis].dataRange.min;
        return dataRangeMinValue < axisMinValue
            ? dataRangeMinValue
            : axisMinValue;
    };
    const getLowerStretchFactor = (yAxis) => {
        const axisMinValue = config.axis[yAxis].domain.lowerLimit;
        const axisMidPoint = getMidPoint(config, yAxis);
        const lowerStretchFactor = Math.abs(
            (axisMidPoint - getMinValue(config, yAxis, axisMinValue)) /
                (axisMidPoint - axisMinValue)
        );
        return lowerStretchFactor > 1 ? lowerStretchFactor : 1;
    };
    lowerStretchFactors.push(getLowerStretchFactor(constants.Y_AXIS));
    if (hasY2Axis(config.axis)) {
        lowerStretchFactors.push(getLowerStretchFactor(constants.Y2_AXIS));
    }
    return lowerStretchFactors;
};

/**
 * Updates the x axis domain values.
 *
 * @private
 * @param {object} config - config object derived from input JSON
 */
const updateXAxisDomain = (config) => {
    config.axis.x.domain = getDomain(
        config.axis.x.type,
        config.axis.x.lowerLimit,
        config.axis.x.upperLimit
    );
};
/**
 * Calculates the upper part of the outlier based on data points.
 * If the content has any data points that are outside the lower and upper bounds set
 * in the vertical axis then we adjust the axis bounds to support that outlier value.
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {Array} List of upper bound values for each of the vertical axis
 */
const getUpperOutlierStretchFactorList = (config) => {
    const upperStretchFactors = [];
    const getMaxValue = (config, yAxis, axisMaxValue) => {
        const dataRangeMaxValue = config.axis[yAxis].dataRange.max;
        return dataRangeMaxValue > axisMaxValue
            ? dataRangeMaxValue
            : axisMaxValue;
    };
    const getUpperStretchFactor = (yAxis) => {
        const axisMaxValue = config.axis[yAxis].domain.upperLimit;
        const axisMidPoint = getMidPoint(config, yAxis);
        const upperStretchFactor = Math.abs(
            (getMaxValue(config, yAxis, axisMaxValue) - axisMidPoint) /
                (axisMaxValue - axisMidPoint)
        );
        return upperStretchFactor > 1 ? upperStretchFactor : 1;
    };
    upperStretchFactors.push(getUpperStretchFactor(constants.Y_AXIS));
    if (hasY2Axis(config.axis)) {
        upperStretchFactors.push(getUpperStretchFactor(constants.Y2_AXIS));
    }
    return upperStretchFactors;
};

/**
 * Determines if the values provided exceed the lower and upper bounds provided in the Y or Y2 axes
 * If the values exceed the bounds then the range and domain are adjusted accordingly.
 * There is no outlier check for X axis, for now, due to the possibility that X axis can be a timeseries.
 *
 * @private
 * @param {object} config - config object derived from input JSON
 * @returns {object} stretch factor determines the new upper and lower limit.
 */
const determineOutlierStretchFactor = (config) => {
    const sortOutlier = (firstValue, secondValue) => secondValue - firstValue;
    return {
        upperLimit: getUpperOutlierStretchFactorList(config).sort(
            sortOutlier
        )[0],
        lowerLimit: getLowerOutlierStretchFactorList(config).sort(
            sortOutlier
        )[0]
    };
};

/**
 * Checks if the Axis label needs to be truncated and returns the truncated value
 *
 * @param {string} label - Axis label display property
 * @param {number} charLimit  - label character limit on axis
 * @returns {string} if more than character limit then truncates,
 * normal label otherwise
 */
const formatLabel = (label, charLimit) =>
    shouldTruncateLabel(label, Math.abs(charLimit))
        ? truncateLabel(label, Math.abs(charLimit))
        : label;

/**
 * Returns the d3 html element after appending axis label text
 *
 * @param {Array} group - d3 html element
 * @param {string} label - Label text
 * @param {number} charLimit - character limit of label with respect to axis size
 * @returns {Array} d3 html element
 */
const buildAxisLabel = (group, label, charLimit) =>
    group
        .append("text")
        .attr("text-anchor", "middle")
        .append("tspan")
        .text(formatLabel(label, charLimit));
/**
 * Decides true if the input JSON y2.show is enabled and if y2 axis points are provided
 *
 * @private
 * @param {object} axis - x, y and y2 axes values
 * @returns {boolean} True if enabled
 */
const hasY2Axis = (axis) => utils.isDefined(axis.y2) && axis.y2.show;
/**
 * D3 adds font-size and font-family by default to axis, to use/inherit consumer fonts
 * we have to override the properties.
 * https://github.com/d3/d3-axis/issues/36
 *
 * @private
 * @param {d3.selection} axisD3Element - Axis element post rendering in D3
 * @returns {undefined} returns nothing
 */
const resetD3FontSize = (axisD3Element) =>
    axisD3Element.attr("font-size", null).attr("font-family", null);
/**
 * Updates the x, y, y2 (if enabled) and axis info row(if enabled) positions on resize
 *
 * @private
 * @param {object} axis - Axis scaled according to input parameters
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {object} config - config object derived from input JSON
 * @param {d3.selection} canvasSVG - d3 selection node of canvas svg
 * @returns {undefined} - returns nothing
 */
const translateAxes = (axis, scale, config, canvasSVG) => {
    getAxesScale(axis, scale, config);
    prepareHAxis(scale, axis, config, prepareHorizontalAxis);
    canvasSVG
        .select(`.${styles.axisX}`)
        .transition()
        .call(constants.d3Transition(config.settingsDictionary.transition))
        .attr(
            "transform",
            `translate(${getXAxisXPosition(config)},${getXAxisYPosition(
                config
            )})`
        )
        .call(axis.x);
    canvasSVG
        .select(`.${styles.axisY}`)
        .transition()
        .call(constants.d3Transition(config.settingsDictionary.transition))
        .attr(
            "transform",
            `translate(${getYAxisXPosition(config)}, ${getYAxisYPosition(
                config
            )})`
        )
        .call(axis.y);
    if (hasY2Axis(config.axis)) {
        canvasSVG
            .select(`.${styles.axisY2}`)
            .transition()
            .call(constants.d3Transition(config.settingsDictionary.transition))
            .attr(
                "transform",
                `translate(${getY2AxisXPosition(config)}, ${getY2AxisYPosition(
                    config
                )})`
            )
            .call(axis.y2);
    }
    canvasSVG
        .select(`.${styles.axisInfoRow}`)
        .transition()
        .call(constants.d3Transition(config.settingsDictionary.transition))
        .attr(
            "transform",
            `translate(${getXAxisXPosition(config)}, ${getAxisInfoRowYPosition(
                config
            )})`
        )
        .call(axis.axisInfoRow.x);
};
/**
 * Updates the Y axis reference line when resized. This is also called
 * when a content is loaded.
 *
 * @private
 * @param {object} axis - Axis scaled according to input parameters
 * @param {object} scale - d3 scale taking into account the input parameters
 * @param {object} config - config object derived from input JSON
 * @param {d3.selection} canvasSVG - d3 selection node of canvas svg
 * @returns {undefined} - returns nothing
 */
const translateAxisReferenceLine = (axis, scale, config, canvasSVG) => {
    const setTranslate = (path, style) =>
        path
            .transition()
            .call(constants.d3Transition(config.settingsDictionary.transition))
            .attr("aria-hidden", false)
            .attr(
                "d",
                createReferenceLine(scale, style)(getReferenceLineData(scale))
            );
    if (hasNegativeLowerBound(scale, constants.Y_AXIS)) {
        setTranslate(
            canvasSVG.select(
                `path.${styles.axis}.${styles.axisY}.${styles.axisReferenceLine}`
            ),
            constants.Y_AXIS
        );
    }
    if (
        hasY2Axis(config.axis) &&
        hasNegativeLowerBound(scale, constants.Y2_AXIS)
    ) {
        setTranslate(
            canvasSVG.select(
                `path.${styles.axis}.${styles.axisY2}.${styles.axisReferenceLine}`
            ),
            constants.Y2_AXIS
        );
    }
};
/**
 * Calculates current min and max value ranges.
 * if the input is bar content and is being cascaded on top of other bars,
 * then we need to calculate top and bottom domain values by summing cascaded bars value ranges
 *
 * @private
 * @param {object} input - Object containing min and max data point values
 * @param {Array} content - array of target objects
 * @param {string} axis - y or y2
 * @returns {object} - Object with min and max value ranges
 */
const getCurMinMaxValueRange = (input, content, axis) => {
    if (input instanceof Bar) {
        let min = 0;
        let max = 0;
        const groupedBars = content.filter((value) => {
            if (value instanceof Bar) {
                return value.config.group === input.config.group;
            }
            return false;
        });
        groupedBars.forEach((bar) => {
            max += bar.valuesRange[axis].max;
            min += bar.valuesRange[axis].min;
        });
        return {
            min,
            max
        };
    }
    return {
        min: input.valuesRange[axis].min,
        max: input.valuesRange[axis].max
    };
};
/**
 * Calculates the axes - x, y and y2 data range.
 * For each data point provided, we need to set the min and max ranges.
 * Data point sets [n]
 *  Data points [n]
 *      x, y, y2 [1]
 *
 * @private
 * @param {object} input - input content object
 * @param {string} axis - y or y2
 * @param {object} config - config object derived from input JSON
 * @param {Array} content - array of target objects
 * @returns {undefined} - returns nothing
 */
const getAxesDataRange = (
    input,
    axis = constants.Y_AXIS,
    config,
    content = []
) => {
    if (utils.isEmpty(config.axis.y.dataRange)) {
        config.axis.y.dataRange = {};
    }
    if (hasY2Axis(config.axis) && utils.isEmpty(config.axis.y2.dataRange)) {
        config.axis.y2.dataRange = {};
    }
    if (utils.isEmpty(input) || utils.isEmpty(input.valuesRange)) {
        return;
    }
    const curRange = getCurMinMaxValueRange(input, content, axis);
    const prevMin = config.axis[axis].dataRange.oldMin;
    const prevMax = config.axis[axis].dataRange.oldMax;
    const isRangeModified =
        !(prevMin && prevMax) ||
        !(prevMin <= curRange.min || prevMax >= curRange.max);
    config.axis[axis].dataRange.isRangeModified = isRangeModified;
    if (isRangeModified) {
        config.axis[axis].dataRange.oldMin = config.axis[axis].dataRange.min;
        config.axis[axis].dataRange.oldMax = config.axis[axis].dataRange.max;
        config.axis[axis].dataRange.min = curRange.min;
        config.axis[axis].dataRange.max = curRange.max;
    }
};
/**
 * Checks if provided input has valid axis type
 *
 * @param {string} x - input x value
 * @param {string} xAxisType - x axis type
 * @returns {boolean} - returns true if valid, false if invalid
 */
const isValidAxisType = (x, xAxisType) =>
    ((utils.isDate(x) || utils.isDateInstance(x)) &&
        getType(xAxisType) === AXIS_TYPE.TIME_SERIES) ||
    (!utils.isDate(x) &&
        !utils.isDateInstance(x) &&
        getType(xAxisType) !== AXIS_TYPE.TIME_SERIES);

/**
 * @enum {Function}
 */
export {
    prepareXAxis,
    prepareHorizontalAxis,
    prepareYAxis,
    prepareY2Axis,
    getAxisTickFormat,
    getRotationForAxis,
    getXAxisLabelXPosition,
    getXAxisLabelYPosition,
    getYAxisLabelXPosition,
    getYAxisLabelShapeXPosition,
    getYAxisLabelYPosition,
    getY2AxisLabelXPosition,
    getY2AxisLabelShapeXPosition,
    getYAxisLabelShapeYPosition,
    getY2AxisLabelShapeYPosition,
    getXAxisXPosition,
    getXAxisYPosition,
    getYAxisXPosition,
    getYAxisYPosition,
    getY2AxisXPosition,
    getY2AxisYPosition,
    getXAxisWidth,
    getYAxisHeight,
    getXAxisHeight,
    getXAxisRange,
    getYAxisRange,
    getAxisLabelWidth,
    getAxisLabelHeight,
    getYAxisWidth,
    calculateAxesSize,
    calculateAxesLabelSize,
    determineOutlierStretchFactor,
    buildAxisLabel,
    getAxesScale,
    createAxes,
    createXAxisInfoRow,
    createAxisReferenceLine,
    getAxesDataRange,
    processTickValues,
    generateYAxesTickValues,
    hasY2Axis,
    translateAxes,
    translateAxisReferenceLine,
    isValidAxisType,
    resetD3FontSize,
    calculateVerticalPadding,
    isXAxisOrientationTop,
    getAxisInfoRowYPosition,
    updateXAxisDomain,
    getTicksCountFromRange,
    getAverageTicksCount,
    formatLabel
};