UNPKG

@devexperts/dxcharts-lite

Version:
211 lines (210 loc) 9.41 kB
/* * Copyright (C) 2019 - 2026 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 { replaceMinusSign, MathUtils } from '../../utils/math.utils'; import { PriceIncrementsUtils } from '../../utils/price-increments.utils'; import { TREASURY_32ND } from '../chart/price-formatters/treasury-price.formatter'; 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, yAxisConfig) { 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; this.yAxisConfig = yAxisConfig; /** * 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())); this.treasuryFormat = this.yAxisConfig && this.yAxisConfig.treasuryFormat; } 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 = replaceMinusSign(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 = replaceMinusSign(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) { var _a; // provided increment if (this.increment) { return this.increment; } // auto-generated increment if (!isNaN(valueLength)) { const calculatedIncrement = ((_a = this.treasuryFormat) === null || _a === void 0 ? void 0 : _a.enabled) ? TREASURY_32ND : 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; } }