@devexperts/dxcharts-lite
Version:
205 lines (204 loc) • 8.99 kB
JavaScript
/*
* Copyright (C) 2019 - 2025 Devexperts Solutions IE Limited
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Subject } from 'rxjs';
import { logValueToUnit, percentToUnit, calcLogValue } from '../../model/scaling/viewport.model';
import { AnimationFrameCache } from '../../utils/performance/animation-frame-cache.utils';
import { identity } from '../../utils/function.utils';
import { MathUtils } from '../../utils/math.utils';
import { PriceIncrementsUtils } from '../../utils/price-increments.utils';
const PIXEL_OFFSET = 0;
const DEFAULT_REGULAR_INCREMENT = 0.01;
/**
* Generator of axes labels.
*/
export class NumericAxisLabelsGenerator {
constructor(increment, startEndProvider, lengthProvider, valueFormatter, withZero = false, axisTypeProvider, baseLineProvider, labelFilter = identity, singleLabelHeightPixels = 23) {
this.increment = increment;
this.startEndProvider = startEndProvider;
this.lengthProvider = lengthProvider;
this.valueFormatter = valueFormatter;
this.withZero = withZero;
this.axisTypeProvider = axisTypeProvider;
this.baseLineProvider = baseLineProvider;
this.labelFilter = labelFilter;
this.singleLabelHeightPixels = singleLabelHeightPixels;
/**
* Multipliers which are using for price increments to
* calculate horizontal grid and price lines step.
*/
this.gridDistanceMultipliers = [2, 4, 5, 10];
this.lastSingleLabelHeightValue = 0;
this.distanceBetweenLabelsChangeSubject = new Subject();
this.newGeneratedLabelsSubject = new Subject();
// optimization to not recalculate labels on same viewport
// TODO rework, generate 1-2 screens outside viewport as well
this.lastStart = 0;
this.lastEnd = 0;
this.labelsCache = new AnimationFrameCache(() => this.labelFilter(this.doGenerateLabels()));
}
generateRegularLabels(min, max, singleLabelHeightValue) {
const newLabels = [];
this.withZero && newLabels.push({ value: 0, text: '0' });
let value = MathUtils.roundToNearest(min, singleLabelHeightValue);
while (value < max) {
// Adjust value to increment
const adjustedValue = MathUtils.roundToNearest(value, singleLabelHeightValue);
const labelText = this.valueFormatter(adjustedValue);
newLabels.push({
value: adjustedValue,
text: labelText,
});
value = MathUtils.roundDecimal(value + singleLabelHeightValue);
}
return newLabels;
}
generatePercentLabels(min, max, singleLabelHeightValue) {
const newLabels = [];
const baseLine = this.baseLineProvider();
let value = MathUtils.roundToNearest(min, singleLabelHeightValue);
while (value < max) {
// Adjust value to increment
const adjustedValue = MathUtils.roundToNearest(value, singleLabelHeightValue);
const valueUnit = percentToUnit(adjustedValue, baseLine);
const labelText = this.valueFormatter(valueUnit);
newLabels.push({
value: adjustedValue,
text: labelText,
});
value = MathUtils.roundDecimal(value + singleLabelHeightValue);
}
return newLabels;
}
generateLogarithmLabels(min, max, singleLabelHeightValue) {
const newLabels = [];
let value = MathUtils.roundToNearest(min, singleLabelHeightValue);
while (value < max) {
const realValue = logValueToUnit(value);
const labelText = this.valueFormatter(realValue);
newLabels.push({
value,
text: labelText,
});
value = MathUtils.roundDecimal(value + singleLabelHeightValue);
}
return newLabels;
}
doGenerateLabels() {
var _a;
const lengthPixels = this.lengthProvider();
if (lengthPixels <= 0) {
return [];
}
const [min, max] = this.calculateMinMax();
const axisStep = this.getAxisStep(min, max, lengthPixels);
if (!this.lastSingleLabelHeightValue) {
this.lastSingleLabelHeightValue = axisStep;
}
let newLabels;
const axisType = this.axisTypeProvider();
if (axisType === 'logarithmic') {
newLabels = this.generateLogarithmLabels(min, max, axisStep);
}
else if (axisType === 'percent') {
newLabels = this.generatePercentLabels(min, max, axisStep);
}
else {
newLabels = this.generateRegularLabels(min, max, axisStep);
}
if (this.lastSingleLabelHeightValue !== axisStep && this.labelsCache.cache) {
const currentLabels = (_a = this.labelsCache.getLastCachedValue()) !== null && _a !== void 0 ? _a : [];
this.distanceBetweenLabelsChangeSubject.next([currentLabels, newLabels]);
this.lastSingleLabelHeightValue = axisStep;
}
this.newGeneratedLabelsSubject.next(newLabels);
return newLabels;
}
calculateMinMax() {
const [min, max] = this.startEndProvider();
const lengthPixels = this.lengthProvider();
const labelBounds = NumericAxisLabelsGenerator.getLabelBounds(min, max, lengthPixels);
return [labelBounds[0], labelBounds[1]];
}
getAxisStep(min, max, lengthPixels) {
const valueRange = max - min;
const labelsCount = lengthPixels / this.singleLabelHeightPixels;
const increment = this.calculateIncrement(valueRange);
const singleLabelHeightValue = valueRange / labelsCount;
return this.calculateAxisStep(singleLabelHeightValue, increment);
}
observeDistanceBetweenLabelsChanged() {
return this.distanceBetweenLabelsChangeSubject.asObservable();
}
observeLabelsChanged() {
return this.newGeneratedLabelsSubject.asObservable();
}
calculateIncrement(valueLength) {
// provided increment
if (this.increment) {
return this.increment;
}
// auto-generated increment
if (!isNaN(valueLength)) {
const calculatedIncrement = PriceIncrementsUtils.autoDetectIncrementOfValueRange(valueLength);
return this.adjustIncrementOnAxisType(calculatedIncrement);
}
return this.adjustIncrementOnAxisType(DEFAULT_REGULAR_INCREMENT);
}
adjustIncrementOnAxisType(increment) {
switch (this.axisTypeProvider()) {
case 'percent':
return increment;
case 'logarithmic':
const [logMin] = this.calculateMinMax();
const regularMin = logValueToUnit(logMin);
const regularIncrementedValue = regularMin + increment;
const incrementedLogValue = calcLogValue(regularIncrementedValue);
return incrementedLogValue - logMin;
case 'regular':
return increment;
}
}
// TODO rework, generator should act as model and update itself on scaleChanged
// TODO this should be called 1-2 times and very rarely, there should be 1-3 viewport of labels
generateNumericLabels() {
const [start, end] = this.startEndProvider();
if (start !== this.lastStart || end !== this.lastEnd) {
this.labelsCache.invalidate();
}
this.lastStart = start;
this.lastEnd = end;
return this.labelsCache.calculateOrGet();
}
static getLabelBounds(start, end, lengthPixels) {
const offset = Math.abs((end - start) * (PIXEL_OFFSET / lengthPixels));
return [start - offset, end + offset];
}
/**
* Calculates the distance between two axis labels as:
* - Take increment (0.01 for price or 1 for natural number);
* - Take step which was calculated as (chart height / max lines count provided by config (or default 10));
* - Multiplying increment with gridDistanceMultipliers until it will greater then step
* @param step
* @param increment
*/
calculateAxisStep(step, increment) {
if (increment === 0) {
console.error('NumericAxisLabelsGenerator failed at calculateAxisStep: increment = 0');
return 0;
}
let distance = increment;
let currentNumberOrder = increment;
let multipliersPointer = 0;
while (distance < step && distance > 0) {
if (multipliersPointer >= this.gridDistanceMultipliers.length) {
multipliersPointer = 0;
currentNumberOrder *= 10;
}
distance = currentNumberOrder * this.gridDistanceMultipliers[multipliersPointer++];
}
return distance;
}
}