@devexperts/dxcharts-lite
Version:
215 lines (214 loc) • 9.96 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/.
*/
/*
* 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();
}
}