UNPKG

@devexperts/dxcharts-lite

Version:
250 lines (249 loc) 13.4 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/. */ 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']); } }