Source: muze-axis/src/cartesian-axis/simple-axis.js

import {
    Store,
    mergeRecursive,
    getSmartComputedStyle,
    selectElement,
    generateGetterSetters,
    getUniqueId
} from 'muze-utils';
import { createScale } from '../scale-creator';
import { axisOrientationMap, BOTTOM, TOP } from '../enums/axis-orientation';
import { defaultConfig } from './default-config';
import { renderAxis } from '../axis-renderer';
import { DOMAIN, BAND } from '../enums/constants';
import {
    computeAxisDimensions,
    setOffset,
    registerChangeListeners,
    calculateContinousSpace
} from './helper';
import { PROPS } from './props';

export default class SimpleAxis {

    /**
     * Creates an instance of SimpleAxis.
     * @memberof SimpleAxis
     */
    constructor (config, dependencies) {
        this._id = getUniqueId();

        this._dependencies = dependencies;
        this._mount = null;
        this._range = [];
        this._domain = [];
        this._domainLock = false;
        this._rotationLock = false;
        this._axisDimensions = {};
        this._eventList = [];

        const defCon = mergeRecursive({}, this.constructor.defaultConfig());
        const simpleConfig = mergeRecursive(defCon, config);

        const bodyElem = selectElement('body');
        const classPrefix = simpleConfig.classPrefix;
        this._tickLabelStyle = getSmartComputedStyle(bodyElem, `${classPrefix}-ticks`);
        this._axisNameStyle = getSmartComputedStyle(bodyElem, `${classPrefix}-axis-name`);
        dependencies.labelManager.setStyle(this._tickLabelStyle);
        this._minTickDistance = dependencies.labelManager.getOriSize('ww');

        generateGetterSetters(this, PROPS);
        this.store(new Store({
            domain: this.domain(),
            range: this.range(),
            config: simpleConfig,
            mount: this.mount()
        }));
        this.config(simpleConfig);

        this._scale = this.createScale(this._config);
        this._axis = this.createAxis(this._config);

        registerChangeListeners(this);
    }

    /**
     * Returns the default configuration of simple axis
     *  @return {Object} default configurations
     */
    static defaultConfig () {
        return defaultConfig;
    }

    /**
     * Sets a fixed baseline for the first ticks so that they can render effectively within
     * the given area
     *
     * @param {*} tickText
     * @param {*} config
     * @param {*} labelManager
     */
    setFixedBaseline () {
        return this;
    }

    /**
     *
     *
     * @readonly
     * @memberof SimpleAxis
     */
    scale (...params) {
        if (params.length) {
            this._scale = params[0];
            return this;
        }
        return this._scale;
    }

    /**
     *
     *
     * @readonly
     * @memberof SimpleAxis
     */
    axis (...params) {
        if (params.length) {
            this._axis = params[0];
            return this;
        }
        return this._axis;
    }

    /**
     *
     *
     * @param {*} d
     *
     * @memberof SimpleAxis
     */
    domain (...domain) {
        if (domain.length) {
            this.scale().domain(domain[0]);
            this._domain = this.scale().domain();
            this.smartTicks(this.setTickConfig());
            this.store().commit(DOMAIN, this._domain);
            this.logicalSpace(null);
            return this;
        }
        return this._domain;
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    dependencies () {
        return this._dependencies;
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    createScale (config) {
        const {
            base,
            padding,
            interpolator,
            exponent
        } = config;
        const range = this.range();
        const scale = createScale({
            padding,
            interpolator,
            exponent,
            base,
            range,
            type: this.constructor.type()
        });

        return scale;
    }

    getTickFormatter (tickFormat, numberFormat) {
        if (tickFormat) {
            return ticks => (val, i) => tickFormat(numberFormat(val), i, ticks);
        }
        return () => val => numberFormat(val);
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    createAxis (config) {
        const {
            tickFormat,
            numberFormat,
            orientation
        } = config;
        const axisClass = axisOrientationMap[orientation];

        if (axisClass) {
            const axis = axisClass(this.scale());
            this.formatter = this.getTickFormatter(tickFormat, numberFormat);

            return axis;
        }
        return null;
    }

    /**
     *
     *
     * @memberof SimpleAxis
     */
    setTickConfig () {
        return this;
    }

    /**
     *
     *
     * @param {*} axisTickLabels
     * @param {*} labelWidth
     *
     * @memberof SimpleAxis
     */
    setRotationConfig (axisTickLabels, labelWidth) {
        const { orientation } = this.config();

        if (orientation === TOP || orientation === BOTTOM) {
            const range = this.range();
            const length = Math.abs(range[0] - range[1]);
            this.config({ labels: { rotation: 0 } });
            if (length > 0 && axisTickLabels.length * (labelWidth + this._minTickDistance.width) > length) {
                this.config({ labels: { rotation: -90 } });
            }
        }
        return this;
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    adjustRange () {
        return this;
    }

    getScaleValue (domainVal) {
        if (domainVal === null || domainVal === undefined) {
            return undefined;
        }
        return this.scale()(domainVal);
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    getTickSize () {
        return this.axis().tickSize();
    }

    /**
     * Gets the space occupied by the parts of an axis
     *
     * @return {Object} object with details about sizes of the axis.
     * @memberof SimpleAxis
     */
    getAxisDimensions () {
        this.axisDimensions(computeAxisDimensions(this));
        return this.axisDimensions();
    }

    /**
     * Gets the space occupied by the axis
     *
     * @return {Object} object with details about size of the axis.
     * @memberof SimpleAxis
     */
    getLogicalSpace () {
        if (!this.logicalSpace()) {
            this.logicalSpace(calculateContinousSpace(this));
            setOffset(this);
            this.logicalSpace();
        }
        return this.logicalSpace();
    }

    /**
     * Returns the value from the domain when given a value from the range.
     * @param {number} value Value from the range.
     * @return {number} Value
     */
    invert (...value) {
        const values = value.map(d => this.scale().invert(d)) || [];
        return value.length === 1 ? values[0] : values;
    }

    /**
     * Gets the nearest range value from the given range values.
     * @param {number} v1 Start range value
     * @param {number} v2 End range value
     * @return {Array} range values
     */
    getNearestRange (v1, v2) {
        let p1;
        let p2;
        let extent;
        const {
            type
        } = this.config();
        const scale = this.scale();
        const range = scale.range();
        const reverse = range[0] > range[1];

        if (type === BAND) {
            extent = scale.invertExtent(v1, v2);
            p1 = scale(reverse ? extent[extent.length - 1] : extent[0]);
            p2 = scale(reverse ? extent[0] : extent[extent.length - 1]) + scale.bandwidth();
            return [p1, p2];
        }
        return [v1, v2];
    }

    /**
     * This method is used to assign a domain to the axis.
     *
     * @param {Array} domain the domain of the scale
     * @memberof SimpleAxis
     */
    updateDomainBounds (domain) {
        let currentDomain = this.domain();
        if (this.config().domain) {
            currentDomain = this.config().domain;
        } else {
            if (currentDomain.length === 0) {
                currentDomain = domain;
            }
            if (domain.length) {
                currentDomain = [Math.min(currentDomain[0], domain[0]), Math.max(currentDomain[1], domain[1])];
            }
        }

        return this.domain(currentDomain);
    }

    /**
     *
     *
     * @param {*} domain
     *
     * @memberof SimpleAxis
     */
    updateDomainCache (domain) {
        if (this._domainLock === false) {
            this.domain([]);
            this._domainLock = true;
        }
        const cachedDomain = [];
        domain && domain.forEach((d) => {
            d !== undefined && d !== null && cachedDomain.push(d);
        });
        return this.updateDomainBounds(cachedDomain);
    }

    getMinTickDifference () {
        return this.domain();
    }

    getFormattedTickValues (tickValues) {
        return tickValues;
    }

    /**
     *
     *
     * @param {*} tickValues
     *
     * @memberof SimpleAxis
     */
    setTickValues () {
        const {
            tickValues
        } = this.config();

        if (tickValues) {
            tickValues instanceof Array && this.axis().tickValues(tickValues);
            return this;
        }
        return this;
    }

    /**
     * This method returns the width in pixels for one
     * unit along the axis. It is only applicable to band scale
     * and returns undefined for other scale type.
     *
     * @return {number} the width of one band along band scale axis
     * @memberof SimpleAxis
     */
    getUnitWidth () {
        return 0;
    }

    /**
     * This method returns an object that can be used to
     * reconstruct this instance.
     *
     * @return {Object} the serializable props of axis
     * @memberof SimpleAxis
     */
    serialize () {
        return {
            name: this.name,
            type: this.type,
            range: this.range(),
            config: this.config()
        };
    }

    /**
     * Returns the id of the axis.
     * @return {string} Unique identifier of the axis.
     */
    get id () {
        return this._id;
    }

    registerEvent (event, fn) {
        this._eventList.push({ name: event, action: fn });
    }

    /**
     *
     *
     * @param {*} fn
     * @memberof SimpleAxis
     */
    on (event, fn) {
        event = event || 'update';
        this.registerEvent(event, fn);
    }

    /**
     * This method is used to render the axis inside
     * the supplied svg container.
     *
     * @param {SVGElement} svg the svg element in which to render the path
     * @memberof SimpleAxis
     */
    /* istanbul ignore next */render () {
        if (this.mount()) {
            renderAxis(this);
        }
        return this;
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    remove () {
        this.store().unsubscribeAll();
        selectElement(this.mount()).remove();
        return this;
    }

    /**
     *
     *
     * @memberof SimpleAxis
     */
    unsubscribe () {
        this.store().unsubscribeAll();
        return this;
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    isReverse () {
        const range = this.range();
        return range[0] > range[1];
    }

    /**
     *
     *
     *
     * @memberof SimpleAxis
     */
    getPixelToValueRatio () {
        const scale = this.scale();
        const range = scale.range();
        const domain = scale.domain();

        return Math.abs(range[1] - range[0]) / (domain[1] - domain[0]);
    }
}