@devexperts/dxcharts-lite
Version:
250 lines (249 loc) • 13.4 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 { CHART_UUID, CanvasElement } from '../../canvas/canvas-bounds-container';
import { ChartBaseElement } from '../../model/chart-base-element';
import { uuid as generateUuid } from '../../utils/uuid.utils';
import { createHighLowOffsetCalculator } from '../chart/data-series.high-low-provider';
import { BarResizerComponent, RESIZER_HIT_TEST_EXTENSION } from '../resizer/bar-resizer.component';
import { createDefaultYExtentHighLowProvider, } from './extent/y-extent-component';
import { PaneHitTestController } from './pane-hit-test.controller';
import { PaneComponent } from './pane.component';
import { firstOf, flatMap, lastOf } from '../../utils/array.utils';
export class PaneManager extends ChartBaseElement {
/**
* Returns order of panes in the chart from top to bottom.
*/
get panesOrder() {
return this.canvasBoundsContainer.panesOrder;
}
constructor(chartBaseModel, dynamicObjectsCanvasModel, userInputListenerComponents, eventBus, mainScale, canvasBoundsContainer, config, canvasAnimation, canvasInputListener, drawingManager, cursorHandler, crossEventProducer, chartPanComponent, mainCanvasModel, yAxisLabelsCanvasModel, hitTestCanvasModel, chartResizeHandler) {
super();
this.chartBaseModel = chartBaseModel;
this.dynamicObjectsCanvasModel = dynamicObjectsCanvasModel;
this.userInputListenerComponents = userInputListenerComponents;
this.eventBus = eventBus;
this.mainScale = mainScale;
this.canvasBoundsContainer = canvasBoundsContainer;
this.config = config;
this.canvasAnimation = canvasAnimation;
this.canvasInputListener = canvasInputListener;
this.drawingManager = drawingManager;
this.cursorHandler = cursorHandler;
this.crossEventProducer = crossEventProducer;
this.chartPanComponent = chartPanComponent;
this.mainCanvasModel = mainCanvasModel;
this.yAxisLabelsCanvasModel = yAxisLabelsCanvasModel;
this.hitTestCanvasModel = hitTestCanvasModel;
this.chartResizeHandler = chartResizeHandler;
this.panes = {};
this.paneRemovedSubject = new Subject();
this.paneAddedSubject = new Subject();
this.dataSeriesAddedSubject = new Subject();
this.dataSeriesRemovedSubject = new Subject();
this.hitTestController = new PaneHitTestController(this.panes, this.dynamicObjectsCanvasModel);
const mainPane = this.createPane(CHART_UUID, {
useDefaultHighLow: false,
scale: this.mainScale,
});
mainPane.mainExtent.scale.autoScaleModel.setHighLowProvider('series', createDefaultYExtentHighLowProvider(mainPane.mainExtent));
mainScale.autoScaleModel.setHighLowPostProcessor('offsets', createHighLowOffsetCalculator(() => this.mainScale.getOffsets()));
}
addBounds(uuid, order) {
this.canvasBoundsContainer.addPaneBounds(uuid, order);
return () => this.canvasBoundsContainer.removePaneBounds(uuid);
}
/**
* Adds a resizer to the canvas bounds container for the given uuid.
* @param {string} uuid - The uuid of the pane to which the resizer is to be added.
* @returns {BarResizerComponent} - The BarResizerComponent instance that was added to the userInputListenerComponents array.
*/
addResizer(uuid) {
const resizerHT = this.canvasBoundsContainer.getBoundsHitTest(CanvasElement.PANE_UUID_RESIZER(uuid), {
extensionY: this.config.components.paneResizer.dragZone + RESIZER_HIT_TEST_EXTENSION,
});
const dragPredicate = () => this.chartBaseModel.mainVisualPoints.length !== 0;
const dragTick = () => {
this.canvasBoundsContainer.resizePaneVertically(uuid, this.canvasInputListener.getCurrentPoint().y);
this.eventBus.fireDraw([this.mainCanvasModel.canvasId, 'dynamicObjectsCanvas']);
};
const resizerId = CanvasElement.PANE_UUID_RESIZER(uuid);
const barResizerComponent = new BarResizerComponent(resizerId, () => this.canvasBoundsContainer.getBounds(resizerId), resizerHT, dragTick, dragPredicate, this.chartPanComponent, this.mainCanvasModel, this.drawingManager, this.canvasInputListener, this.canvasAnimation, this.config, this.canvasBoundsContainer, this.hitTestCanvasModel);
this.userInputListenerComponents.push(barResizerComponent);
return barResizerComponent;
}
get yExtents() {
return flatMap(Object.values(this.panes), c => c.yExtentComponents);
}
/**
* Returns the pane component that contains the given point.
* @param {Point} point - The point to check.
* @returns {PaneComponent | undefined} - The pane component that contains the point or undefined if no pane contains it.
*/
getPaneIfHit(point) {
const panes = Object.values(this.panes);
return panes.find(p => this.canvasBoundsContainer.getBoundsHitTest(CanvasElement.PANE_UUID(p.uuid))(point.x, point.y));
}
/**
* Creates sub-plot on the chart with y-axis
* @param uuid
* @param {AtLeastOne<YExtentCreationOptions>} options
* @returns
*/
createPane(uuid = generateUuid(), options) {
if (this.panes[uuid] !== undefined) {
return this.panes[uuid];
}
const paneComponent = new PaneComponent(this.chartBaseModel, this.mainCanvasModel, this.yAxisLabelsCanvasModel, this.dynamicObjectsCanvasModel, this.hitTestController, this.config, this.mainScale, this.drawingManager, this.chartPanComponent, this.canvasInputListener, this.canvasAnimation, this.cursorHandler, this.eventBus, this.canvasBoundsContainer, uuid, this.dataSeriesAddedSubject, this.dataSeriesRemovedSubject, this.hitTestCanvasModel, this.chartResizeHandler, options);
// TODO: is resizer should be added always?
if (this.config.components.paneResizer.visible) {
paneComponent.addChildEntity(this.addResizer(uuid));
}
paneComponent.addSubscription(this.addBounds(uuid, options === null || options === void 0 ? void 0 : options.order));
paneComponent.addSubscription(this.addCursors(uuid));
paneComponent.addSubscription(this.crossEventProducer.subscribeMouseOverHT(uuid, paneComponent.ht));
this.panes[uuid] = paneComponent;
paneComponent.activate();
this.recalculateState();
paneComponent.mainExtent.scale.autoScale(true);
this.paneAddedSubject.next(this.panes);
return paneComponent;
}
/**
* Moves the canvas bounds container up by calling the movePaneUp method with the uuid of the current object.
* @returns {void}
*/
movePaneUp(uuid) {
this.canvasBoundsContainer.movePaneUp(uuid);
}
/**
* Moves the canvas bounds container down by calling the movePaneDown method with the uuid of the current object.
* @returns {void}
*/
movePaneDown(uuid) {
this.canvasBoundsContainer.movePaneDown(uuid);
}
/**
* Checks if the current pane can move up.
* @returns {boolean} - Returns true if the current pane can move up, otherwise false.
*/
canMovePaneUp(uuid) {
var _a, _b;
const firstVisiblePane = firstOf(this.canvasBoundsContainer.panesOrder.filter(uuid => { var _a, _b; return (_b = (_a = this.panes[uuid]) === null || _a === void 0 ? void 0 : _a.visible) !== null && _b !== void 0 ? _b : false; }));
return uuid !== firstVisiblePane && ((_b = (_a = this.panes[uuid]) === null || _a === void 0 ? void 0 : _a.visible) !== null && _b !== void 0 ? _b : false);
}
/**
* Checks if the current pane can move down.
*
* @returns {boolean} - Returns true if the current pane is not the last one in the canvasBoundsContainer, otherwise returns false.
*/
canMovePaneDown(uuid) {
var _a, _b;
const lastVisiblePane = lastOf(this.canvasBoundsContainer.panesOrder.filter(uuid => { var _a, _b; return (_b = (_a = this.panes[uuid]) === null || _a === void 0 ? void 0 : _a.visible) !== null && _b !== void 0 ? _b : false; }));
return uuid !== lastVisiblePane && ((_b = (_a = this.panes[uuid]) === null || _a === void 0 ? void 0 : _a.visible) !== null && _b !== void 0 ? _b : false);
}
/**
* Removes pane from the chart and all related components
* @param uuid
*/
removePane(uuid) {
const pane = this.panes[uuid];
if (pane === undefined) {
return;
}
this.paneRemovedSubject.next(pane);
pane.disable();
pane.yExtentComponents.forEach(yExtentComponent => yExtentComponent.disable());
delete this.panes[uuid];
this.recalculateState();
}
/**
* Hides a pane from the chart and all related components
*/
hidePane(paneUUID) {
const pane = this.panes[paneUUID];
// hide pane only if we have more than one visible pane
if (pane === undefined || !pane.visible) {
return;
}
const paneResizerId = CanvasElement.PANE_UUID_RESIZER(paneUUID);
const resizer = this.userInputListenerComponents.find(el => el instanceof BarResizerComponent && el.id === paneResizerId);
resizer === null || resizer === void 0 ? void 0 : resizer.disable();
this.canvasBoundsContainer.hidePaneBounds(paneUUID);
this.recalculateState();
}
/**
* Shows a pane, use if the pane is hidden
*/
showPane(paneUUID) {
const pane = this.panes[paneUUID];
const paneResizerId = CanvasElement.PANE_UUID_RESIZER(paneUUID);
if (pane === undefined || pane.visible) {
return;
}
const resizer = this.userInputListenerComponents.find(el => el instanceof BarResizerComponent && el.id === paneResizerId);
resizer === null || resizer === void 0 ? void 0 : resizer.enable();
this.canvasBoundsContainer.showPaneBounds(paneUUID);
this.recalculateState();
}
/**
* Move data series to a certain pane, or create a new one if no pane is found
*/
moveDataSeriesToPane(dataSeries, initialPane, initialExtent, options) {
const { paneUUID, extent, direction, align, extentIdx, isForceKeepPane, index = 0 } = options;
const pane = paneUUID && this.panes[paneUUID];
const initialYAxisState = align ? Object.assign(Object.assign({}, initialExtent.yAxis.state), { align }) : undefined;
const onNewScale = extentIdx && extentIdx > 0;
if (!pane) {
const order = direction && direction === 'above' ? index : index + 1;
const newPane = this.createPane(paneUUID, {
order,
initialYAxisState,
inverse: initialExtent.scale.state.inverse,
lockToPriceRatio: initialExtent.scale.state.lockPriceToBarRatio,
});
newPane.moveDataSeriesToExistingExtentComponent(dataSeries, initialPane, initialExtent, newPane.mainExtent, isForceKeepPane);
!isForceKeepPane && initialPane.yExtentComponents.length === 0 && this.removePane(initialPane.uuid);
return;
}
if (extent && !onNewScale) {
pane.moveDataSeriesToExistingExtentComponent(dataSeries, initialPane, initialExtent, extent);
}
else {
pane.moveDataSeriesToNewExtentComponent(dataSeries, initialPane, initialExtent, align !== null && align !== void 0 ? align : initialExtent.yAxis.state.align);
}
!isForceKeepPane && initialPane.yExtentComponents.length === 0 && this.removePane(initialPane.uuid);
}
/**
* Adds cursors to the chart elements based on the provided uuid and cursor type.
* @private
* @param {string} uuid - The unique identifier for the chart element.
* @param {string} [cursor=this.config.components.chart.cursor] - The type of cursor to be added to the chart element.
* @returns {void}
*/
addCursors(uuid, cursor = this.config.components.chart.cursor) {
const pane = CanvasElement.PANE_UUID(uuid);
const paneResizer = CanvasElement.PANE_UUID_RESIZER(uuid);
this.cursorHandler.setCursorForCanvasEl(pane, cursor);
this.config.components.paneResizer.visible &&
this.cursorHandler.setCursorForCanvasEl(paneResizer, this.config.components.paneResizer.cursor, this.config.components.paneResizer.dragZone + RESIZER_HIT_TEST_EXTENSION);
return () => {
this.cursorHandler.removeCursorForCanvasEl(pane);
this.config.components.paneResizer.visible && this.cursorHandler.removeCursorForCanvasEl(paneResizer);
};
}
/**
* Recalculates the zoom Y of all pane components and fires a draw event on the event bus.
* @function
* @name recalculateState
* @memberof PaneManager
* @returns {void}
*/
recalculateState() {
Object.values(this.panes).forEach(state => state.scale.recalculateZoomY());
this.eventBus.fireDraw([this.mainCanvasModel.canvasId, 'dynamicObjectsCanvas']);
}
}