@elastic/charts
Version:
Elastic-Charts data visualization library
267 lines • 12.7 kB
JavaScript
"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