"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScaleContinuous = void 0;
exports.limitLogScaleDomain = limitLogScaleDomain;
const d3_array_1 = require("d3-array");
const d3_scale_1 = require("d3-scale");
const constants_1 = require("./constants");
const get_linear_ticks_1 = require("../chart_types/xy_chart/utils/get_linear_ticks");
const screenspace_marker_scale_compressor_1 = require("../solvers/screenspace_marker_scale_compressor");
const common_1 = require("../utils/common");
const date_time_1 = require("../utils/data/date_time");
const SCALES = {
    [constants_1.ScaleType.Linear]: d3_scale_1.scaleLinear,
    [constants_1.ScaleType.Log]: d3_scale_1.scaleLog,
    [constants_1.ScaleType.Sqrt]: d3_scale_1.scaleSqrt,
    [constants_1.ScaleType.Time]: d3_scale_1.scaleUtc,
};
const defaultScaleOptions = {
    bandwidth: 0,
    minInterval: 0,
    timeZone: 'local',
    totalBarsInCluster: 1,
    barsPadding: 0,
    constrainDomainPadding: true,
    domainPixelPadding: 0,
    desiredTickCount: 10,
    isSingleValueHistogram: false,
    maximumFractionDigits: undefined,
    logBase: 10,
    logMinLimit: NaN,
    linearBase: 10,
};
const isUnitRange = ([r1, r2]) => r1 === 0 && r2 === 1;
class ScaleContinuous {
    bandwidth;
    totalBarsInCluster;
    bandwidthPadding;
    minInterval;
    step;
    type;
    domain;
    range;
    isInverted;
    linearBase;
    tickValues;
    timeZone;
    barsPadding;
    isSingleValueHistogram;
    unit;
    project;
    inverseProject;
    constructor({ type: scaleType = constants_1.ScaleType.Linear, domain: inputDomain, range, nice = false }, options) {
        const isBinary = scaleType === constants_1.ScaleType.LinearBinary;
        const type = isBinary ? constants_1.ScaleType.Linear : scaleType;
        const scaleOptions = (0, common_1.mergePartial)(defaultScaleOptions, options);
        const min = inputDomain.reduce((p, n) => Math.min(p, n), Infinity);
        const max = inputDomain.reduce((p, n) => Math.max(p, n), -Infinity);
        const properLogScale = type === constants_1.ScaleType.Log && min < max;
        const dataDomain = properLogScale ? limitLogScaleDomain([min, max], scaleOptions.logMinLimit) : inputDomain;
        const barsPadding = (0, common_1.clamp)(scaleOptions.barsPadding, 0, 1);
        const isNice = nice && type !== constants_1.ScaleType.Time;
        const totalRange = Math.abs(range[1] - range[0]);
        const pixelPadFits = 0 < scaleOptions.domainPixelPadding && scaleOptions.domainPixelPadding * 2 < totalRange;
        const isPixelPadded = pixelPadFits && type !== constants_1.ScaleType.Time && !isUnitRange(range);
        const minInterval = Math.abs(scaleOptions.minInterval);
        const bandwidth = scaleOptions.bandwidth * (1 - barsPadding);
        const bandwidthPadding = scaleOptions.bandwidth * barsPadding;
        this.barsPadding = barsPadding;
        this.bandwidth = bandwidth;
        this.bandwidthPadding = bandwidthPadding;
        this.type = type;
        this.range = range;
        this.linearBase = isBinary ? 2 : scaleOptions.linearBase;
        this.minInterval = minInterval;
        this.step = bandwidth + barsPadding + bandwidthPadding;
        this.timeZone = scaleOptions.timeZone;
        this.isInverted = dataDomain[0] > dataDomain[1];
        this.totalBarsInCluster = scaleOptions.totalBarsInCluster;
        this.isSingleValueHistogram = scaleOptions.isSingleValueHistogram;
        const d3Scale = SCALES[type]();
        d3Scale.domain(dataDomain);
        d3Scale.range(range);
        if (properLogScale)
            d3Scale.base(scaleOptions.logBase);
        if (isNice) {
            if (type === constants_1.ScaleType.Linear) {
                (0, get_linear_ticks_1.getNiceLinearTicks)(d3Scale, scaleOptions.desiredTickCount, this.linearBase);
            }
            else {
                d3Scale
                    .domain(dataDomain)
                    .nice(scaleOptions.desiredTickCount);
            }
        }
        const niceDomain = isNice ? d3Scale.domain() : dataDomain;
        const paddedDomain = isPixelPadded
            ? getPixelPaddedDomain(totalRange, niceDomain, scaleOptions.domainPixelPadding, scaleOptions.constrainDomainPadding)
            : niceDomain;
        d3Scale.domain(paddedDomain);
        if (isPixelPadded && isNice)
            d3Scale.nice(scaleOptions.desiredTickCount);
        const nicePaddedDomain = isPixelPadded && isNice ? d3Scale.domain() : paddedDomain;
        this.tickValues =
            type === constants_1.ScaleType.Time
                ? getTimeTicks(nicePaddedDomain, scaleOptions.desiredTickCount, scaleOptions.timeZone, scaleOptions.bandwidth === 0 ? 0 : scaleOptions.minInterval)
                : (type === constants_1.ScaleType.Linear
                    ? getLinearNonDenserTicks(nicePaddedDomain, scaleOptions.desiredTickCount, this.linearBase, scaleOptions.bandwidth === 0 ? 0 : scaleOptions.minInterval)
                    : d3Scale.ticks(scaleOptions.desiredTickCount)).filter(scaleOptions.maximumFractionDigits === undefined
                    ? () => true
                    : (n) => (0, common_1.round)(n, scaleOptions.maximumFractionDigits) === n);
        this.domain = nicePaddedDomain;
        this.project = (d) => d3Scale(d) ?? NaN;
        this.inverseProject = (d) => d3Scale.invert(d) ?? NaN;
    }
    scale(value) {
        return typeof value === 'number'
            ? this.project(value) + (this.bandwidthPadding / 2) * this.totalBarsInCluster
            : NaN;
    }
    pureScale(value) {
        return typeof value === 'number' ? this.project(this.bandwidth === 0 ? value : value + this.minInterval / 2) : NaN;
    }
    ticks() {
        return this.tickValues;
    }
    invert(value) {
        const invertedValue = this.inverseProject(value);
        return this.type === constants_1.ScaleType.Time
            ? (0, date_time_1.getMomentWithTz)(invertedValue, this.timeZone).valueOf()
            : Number(invertedValue);
    }
    invertWithStep(value, data) {
        if (data.length === 0) {
            return { withinBandwidth: false, value: NaN };
        }
        const invertedValue = this.invert(value);
        const bisectValue = this.bandwidth === 0 ? invertedValue + this.minInterval / 2 : invertedValue;
        const leftIndex = (0, d3_array_1.bisectLeft)(data, bisectValue);
        if (leftIndex === 0) {
            const [dataValue = NaN] = data;
            const withinBandwidth = invertedValue >= dataValue;
            return {
                withinBandwidth,
                value: dataValue +
                    (withinBandwidth ? 0 : -this.minInterval * Math.ceil((dataValue - invertedValue) / this.minInterval)),
            };
        }
        const currentValue = data[leftIndex - 1] ?? NaN;
        if (this.bandwidth === 0) {
            const nextValue = data[leftIndex] ?? NaN;
            const nextDiff = Math.abs(nextValue - invertedValue);
            const prevDiff = Math.abs(invertedValue - currentValue);
            return {
                withinBandwidth: true,
                value: nextDiff <= prevDiff ? nextValue : currentValue,
            };
        }
        const withinBandwidth = invertedValue - currentValue <= this.minInterval;
        return {
            withinBandwidth,
            value: currentValue +
                (withinBandwidth ? 0 : this.minInterval * Math.floor((invertedValue - currentValue) / this.minInterval)),
        };
    }
    isDegenerateDomain() {
        return this.domain.every((v) => v === this.domain[0]);
    }
    isSingleValue() {
        return this.isSingleValueHistogram || this.isDegenerateDomain();
    }
    isValueInDomain(value) {
        const [start = NaN, end = NaN] = this.domain;
        return (0, common_1.isFiniteNumber)(value) && start <= value && value <= end;
    }
}
exports.ScaleContinuous = ScaleContinuous;
function getTimeTicks(domain, desiredTickCount, timeZone, minInterval) {
    const [start, end] = domain;
    const startDomain = (0, date_time_1.getMomentWithTz)(start, timeZone);
    const endDomain = (0, date_time_1.getMomentWithTz)(end, timeZone);
    const offset = startDomain.utcOffset();
    const shiftedDomainMin = startDomain.add(offset, 'minutes').valueOf();
    const shiftedDomainMax = endDomain.add(offset, 'minutes').valueOf();
    const tzShiftedScale = (0, d3_scale_1.scaleUtc)().domain([shiftedDomainMin, shiftedDomainMax]);
    let currentCount = desiredTickCount;
    let rawTicks = tzShiftedScale.ticks(desiredTickCount);
    while (rawTicks.length > 2 &&
        currentCount > 0 &&
        (0, common_1.isDefined)(rawTicks[0]) &&
        (0, common_1.isDefined)(rawTicks[1]) &&
        rawTicks[1].valueOf() - rawTicks[0].valueOf() < minInterval) {
        currentCount--;
        rawTicks = tzShiftedScale.ticks(currentCount);
    }
    const timePerTick = (shiftedDomainMax - shiftedDomainMin) / rawTicks.length;
    const hasHourTicks = timePerTick < 1000 * 60 * 60 * 12;
    return rawTicks.map((d) => {
        const currentDateTime = (0, date_time_1.getMomentWithTz)(d, timeZone);
        const currentOffset = hasHourTicks ? offset : currentDateTime.utcOffset();
        return currentDateTime.subtract(currentOffset, 'minutes').valueOf();
    });
}
function getLinearNonDenserTicks(domain, desiredTickCount, base, minInterval) {
    const [start, stop] = domain;
    let currentCount = desiredTickCount;
    let ticks = (0, get_linear_ticks_1.getLinearTicks)(start, stop, desiredTickCount, base);
    while (ticks.length > 2 &&
        currentCount > 0 &&
        (0, common_1.isDefined)(ticks[0]) &&
        (0, common_1.isDefined)(ticks[1]) &&
        ticks[1] - ticks[0] < minInterval) {
        currentCount--;
        ticks = (0, get_linear_ticks_1.getLinearTicks)(start, stop, currentCount, base);
    }
    return ticks;
}
function limitLogScaleDomain([min, max], logMinLimit) {
    const absLimit = Math.abs(logMinLimit);
    const fallback = absLimit || constants_1.LOG_MIN_ABS_DOMAIN;
    if (absLimit > 0 && min > 0 && min < absLimit)
        return max > absLimit ? [absLimit, max] : [absLimit, absLimit];
    if (absLimit > 0 && max < 0 && max > -absLimit)
        return min < -absLimit ? [min, -absLimit] : [-absLimit, -absLimit];
    if (min === 0)
        return max > 0 ? [fallback, max] : max < 0 ? [-fallback, max] : [fallback, fallback];
    if (max === 0)
        return min > 0 ? [min, fallback] : min < 0 ? [min, -fallback] : [fallback, fallback];
    if (min < 0 && max > 0)
        return Math.abs(max) >= Math.abs(min) ? [fallback, max] : [min, -fallback];
    if (min > 0 && max < 0)
        return Math.abs(min) >= Math.abs(max) ? [min, fallback] : [-fallback, max];
    return [min, max];
}
function getPixelPaddedDomain(chartHeight, domain, desiredPixelPadding, constrainDomainPadding, intercept = 0) {
    const inverted = domain[1] < domain[0];
    const orderedDomain = inverted ? [domain[1], domain[0]] : domain;
    const { scaleMultiplier } = (0, screenspace_marker_scale_compressor_1.screenspaceMarkerScaleCompressor)(orderedDomain, [
        [desiredPixelPadding, desiredPixelPadding],
        [desiredPixelPadding, desiredPixelPadding],
    ], chartHeight);
    const baselinePaddedDomainLo = orderedDomain[0] - desiredPixelPadding / scaleMultiplier;
    const baselinePaddedDomainHigh = orderedDomain[1] + desiredPixelPadding / scaleMultiplier;
    const crossBelow = constrainDomainPadding && baselinePaddedDomainLo < intercept && orderedDomain[0] >= intercept;
    const crossAbove = constrainDomainPadding && baselinePaddedDomainHigh > 0 && orderedDomain[1] <= 0;
    const paddedDomainLo = crossBelow
        ? intercept
        : crossAbove
            ? orderedDomain[0] -
                desiredPixelPadding /
                    (0, screenspace_marker_scale_compressor_1.screenspaceMarkerScaleCompressor)([orderedDomain[0], intercept], [
                        [desiredPixelPadding, desiredPixelPadding],
                        [0, 0],
                    ], chartHeight).scaleMultiplier
            : baselinePaddedDomainLo;
    const paddedDomainHigh = crossBelow
        ? orderedDomain[1] +
            desiredPixelPadding /
                (0, screenspace_marker_scale_compressor_1.screenspaceMarkerScaleCompressor)([intercept, orderedDomain[1]], [
                    [0, 0],
                    [desiredPixelPadding, desiredPixelPadding],
                ], chartHeight).scaleMultiplier
        : crossAbove
            ? intercept
            : baselinePaddedDomainHigh;
    return inverted ? [paddedDomainHigh, paddedDomainLo] : [paddedDomainLo, paddedDomainHigh];
}
//# sourceMappingURL=scale_continuous.js.map