UNPKG

@devexperts/dxcharts-lite

Version:
215 lines (214 loc) 9.96 kB
/* * 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/. */ /* * Copyright (C) 2019 - 2024 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 { animationFrameScheduler, merge, Subject } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; import { CanvasElement } from '../../../canvas/canvas-bounds-container'; import { ChartBaseElement } from '../../../model/chart-base-element'; import { flat } from '../../../utils/array.utils'; import { animationFrameThrottledPrior } from '../../../utils/performance/request-animation-frame-throttle.utils'; import { uuid } from '../../../utils/uuid.utils'; import { calcLabelsYCoordinates } from './labels-positions-calculator'; export const LabelsGroups = { MAIN: 'MAIN', }; /** * Different labels on Y axis: * - last candle price * - bid/ask * - countdown * - prev.day price * - high low in viewport * - drawings * - studies * - ... * Anything but the base labels which are generated in other component {@link YAxisBaseLabelsModel} */ export class FancyYAxisLabelsModel extends ChartBaseElement { get labelsProviders() { return this._labelsProviders; } constructor(eventBus, scale, canvasBoundsContainer, state, canvasModel, paneUUID, updateYAxisWidth, chartResizeHandler) { super(); this.eventBus = eventBus; this.scale = scale; this.canvasBoundsContainer = canvasBoundsContainer; this.state = state; this.canvasModel = canvasModel; this.paneUUID = paneUUID; this.updateYAxisWidth = updateYAxisWidth; this.chartResizeHandler = chartResizeHandler; this.orderedLabels = []; /** * an easier way to manage custom y-axis labels, than y-axis labels providers, but doesn't support overlapping avoidance */ this.customLabels = {}; this._labelsProviders = {}; this.labelsPositionRecalculatedSubject = new Subject(); this.animFrameId = `anim_cache_${uuid()}`; this.initModel(); } /** * This method is used to activate the chart and subscribe to the observables that will trigger the update of the labels. * It calls the parent method doActivate() and adds a new Rx subscription to the merge of the following observables: * - canvasBoundsContainer.observeBoundsChanged(CanvasElement.CHART) * - canvasBoundsContainer.barResizerChangedSubject * - scale changed * When any of these observables emit a new value, the updateLabels() method is called. * @protected */ doActivate() { super.doActivate(); this.addRxSubscription(merge(this.canvasBoundsContainer.barResizerChangedSubject, this.scale.changed, merge(this.canvasBoundsContainer.observeBoundsChanged(CanvasElement.PANE_UUID(this.paneUUID)), this.chartResizeHandler.canvasResized).pipe(throttleTime(50, animationFrameScheduler, { trailing: true, leading: true }))).subscribe(() => { this.updateLabels(); })); } /** * Initializes the model by calling the methods to initialize the label groups and recalculate the labels. * Then, it fires the draw event on the canvas model. * @private */ initModel() { this.initLabelsGroups(); this.recalculateLabels(); this.canvasModel.fireDraw(); } /** * Initializes the labels groups. * If there are labels providers, it creates a group for each one and adds the providers to their respective groups. * @returns {void} */ initLabelsGroups() { for (const groupName of Object.keys(this.labelsProviders)) { this.createGroup(groupName); Object.entries(this.labelsProviders[groupName]).forEach(([id, provider]) => { this.addToGroup(provider, groupName, id); }); } } /** * Updates YAxis ordered labels. * @param adjustYAxisWidth - provide "true", if you need to adjust width after new labels will be calculated. */ updateLabels(adjustYAxisWidth = false) { // Updating labels is an expensive operation, and depends on chart data. // Animation frame is needed here, because recalculation can be proceeded while rendering, and make render process fregmented. this.recalculateLabels(); animationFrameThrottledPrior(this.animFrameId, () => { adjustYAxisWidth && this.updateYAxisWidth(); this.canvasModel.fireDraw(); }); } /** * Recalculates the labels based on the current configuration and label providers. * It generates label groups and calculates their coordinates based on their weight and the label height. * The coordinates are generated in the order they were passed to the generator. * The ordered labels are then updated with the new coordinates and reversed. * @returns {void} */ recalculateLabels() { var _a, _b; this.orderedLabels = []; const labelHeight = this.state.fontSize + ((_a = this.state.labelBoxMargin.top) !== null && _a !== void 0 ? _a : 0) + ((_b = this.state.labelBoxMargin.bottom) !== null && _b !== void 0 ? _b : 0); for (const providers of Object.values(this.labelsProviders)) { // generated label groups const labelGroups = flat(Object.values(providers).map(c => c.getUnorderedLabels())); const labelsPointsAndWeight = flat(labelGroups.map(g => g.labels)).map(label => { var _a; return ({ y: label.y, weight: (_a = label.labelWeight) !== null && _a !== void 0 ? _a : Number.POSITIVE_INFINITY, }); }); // coordinates are generated in order they were passed to generator const updatedCoordinates = calcLabelsYCoordinates(labelsPointsAndWeight, labelHeight); labelGroups.forEach(labelGroup => { const points = updatedCoordinates.splice(0, labelGroup.labels.length); this.orderedLabels.push(this.updateLabelsCoordinates(labelGroup, points)); }); } this.orderedLabels = this.orderedLabels.reverse(); } /** * Creates a new group with the given name if it doesn't exist yet. * @param {string} groupName - The name of the group to be created. * @returns {void} */ createGroup(groupName) { !this.labelsProviders[groupName] && (this.labelsProviders[groupName] = {}); } /** * Adds a YAxisLabelsProvider component to a group. * * @param {YAxisLabelsProvider} component - The component to add to the group. * @param {string} groupName - The name of the group to add the component to. * @param {string} id - The id of the component to add to the group. * @returns {void} */ addToGroup(component, groupName, id) { if (this.labelsProviders[groupName]) { if (!Object.values(this.labelsProviders[groupName]).includes(component)) { this.labelsProviders[groupName][id] = component; } } } /** * Updates the coordinates of the labels in a LabelGroup object based on an array of points. * @param {LabelGroup} labels - The LabelGroup object containing the labels to be updated. * @param {number[]} points - An array of points to be used to update the y-coordinate of each label. * @returns {LabelGroup} - A new LabelGroup object with updated label coordinates. */ updateLabelsCoordinates(labels, points) { return Object.assign(Object.assign({}, labels), { labels: labels.labels.map((label, idx) => (Object.assign(Object.assign({}, label), { // save original y lineY: label.y, y: points[idx] }))) }); } /** * Returns an Observable that emits a void value whenever the labels position is recalculated. * The Observable is created from the labelsPositionRecalculatedSubject Subject. * @returns {Observable<void>} An Observable that emits a void value whenever the labels position is recalculated. */ observeLabelsPositionsRecalculated() { return this.labelsPositionRecalculatedSubject.asObservable(); } /** * Registers a Y axis labels provider for a given group name and ID. * * @param {string} groupName - The name of the group to which the provider belongs. * @param {YAxisLabelsProvider} provider - The provider to be registered. * @param {string} id - The ID of the provider to be registered. * @returns {void} */ registerYAxisLabelsProvider(groupName, provider, id) { var _a; const providers = (_a = this.labelsProviders[groupName]) !== null && _a !== void 0 ? _a : {}; providers[id] = provider; this.labelsProviders[groupName] = providers; this.initModel(); } /** * Unregisters a Y axis labels provider. * * @param {string} groupName - The name of the group to which the provider belongs. * @param {string} id - The ID of the provider to unregister. * * @returns {void} */ unregisterYAxisLabelsProvider(groupName, id) { var _a; const providers = (_a = this.labelsProviders[groupName]) !== null && _a !== void 0 ? _a : {}; delete providers[id]; // if group is empty, delete it too if (this.labelsProviders[groupName] && Object.keys(this.labelsProviders[groupName]).length === 0) { delete this.labelsProviders[groupName]; } this.initModel(); } }