TargetProcess/tauCharts

View on GitHub
src/scales/logarithmic.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import {BaseScale} from './base';
import {TauChartError, errorCodes} from '../error';
import {DataFrame} from '../data-frame';
import * as d3Array from 'd3-array';
import * as d3Scale from 'd3-scale';
const d3 = {
    ...d3Array,
    ...d3Scale,
};
import {
    ScaleConfig
} from '../definitions';

export class LogarithmicScale extends BaseScale {

    vars: number[];

    constructor(xSource: DataFrame, scaleConfig: ScaleConfig) {

        super(xSource, scaleConfig);

        var props = this.scaleConfig;
        var domain = d3.extent(this.vars);

        var min = Number.isFinite(props.min) ? props.min : domain[0];
        var max = Number.isFinite(props.max) ? props.max : domain[1];

        domain = [
            Math.min(...[min, domain[0]].filter(Number.isFinite)),
            Math.max(...[max, domain[1]].filter(Number.isFinite))
        ];
        throwIfCrossesZero(domain);

        if (props.nice) {
            domain = niceLog10(domain) as [number, number];
        }

        this.vars = domain;

        this.addField('scaleType', 'logarithmic')
            .addField('discrete', false);
    }

    isInDomain(x) {
        var domain = this.domain();
        var min = domain[0];
        var max = domain[domain.length - 1];
        return (!Number.isNaN(min) && !Number.isNaN(max) && (x <= max) && (x >= min));
    }

    create(interval: [number, number]) {

        var domain = this.vars;
        throwIfCrossesZero(domain);

        var d3Scale = extendLogScale(d3.scaleLog())
            .domain(domain)
            .range(interval);
        d3Scale.stepSize = (() => 0);

        return this.toBaseScale(d3Scale, interval);
    }
}

function log10(x: number) {
    return Math.log(x) / Math.LN10;
}

function throwIfCrossesZero(domain: number[]) {
    if (domain[0] * domain[1] <= 0) {
        throw new TauChartError(
            'Logarithmic scale domain cannot cross zero.',
            errorCodes.INVALID_LOG_DOMAIN
        );
    }
}

function extendLogScale(scale) {
    var d3ScaleCopy = scale.copy;

    // NOTE: D3 log scale ticks count is not configurable
    // and returns 10 ticks per each exponent.
    // So here we make it return 10 ticks per each
    // step of 1, 2 or more exponents, according to
    // necessary ticks count.
    scale.ticks = function (n) {

        var ticksPerExp = 10;
        var ticks = [];
        var extent = d3.extent(scale.domain()) as [number, number];
        var lowExp = Math.floor(log10(extent[0]));
        var topExp = Math.ceil(log10(extent[1]));

        var step = Math.ceil(
            (topExp - lowExp) * ticksPerExp /
            (Math.ceil(n / ticksPerExp) * ticksPerExp)
        );

        for (let e = lowExp; e <= topExp; e += step) {
            for (let t = 1; t <= ticksPerExp; t++) {
                let tick = Math.pow(t, step) * Math.pow(10, e);
                tick = parseFloat(tick.toExponential(0));
                if (tick >= extent[0] && tick <= extent[1]) {
                    ticks.push(tick);
                }
            }
        }

        return ticks;
    };
    scale.copy = function () {
        var copy = d3ScaleCopy.call(scale);
        extendLogScale(copy);
        return copy;
    };

    return scale;
}

function niceLog10(domain: [number, number]): [number, number] {

    var isPositive = domain[0] > 0;
    var absDomain = domain.map((d) => Math.abs(d));
    var top = Math.max(...absDomain);
    var low = Math.min(...absDomain);

    var lowExp = low.toExponential().split('e');
    var topExp = top.toExponential().split('e');
    var niceLow = parseFloat(Math.floor(Number(lowExp[0])) + 'e' + lowExp[1]);
    var niceTop = parseFloat(Math.ceil(Number(topExp[0])) + 'e' + topExp[1]);

    return (
        isPositive ?
            [niceLow, niceTop] :
            [-niceTop, -niceLow]
    );
}