UNPKG

@devexperts/dxcharts-lite

Version:
321 lines (320 loc) 16.2 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 { distinctUntilChanged } from 'rxjs/operators'; import { areBoundsChanged, CanvasElement, } from '../../canvas/canvas-bounds-container'; import { ChartBaseElement } from '../../model/chart-base-element'; import { SyncedByXScaleModel } from '../../model/scale.model'; import { firstOf, flatMap, lastOf } from '../../utils/array.utils'; import { cloneUnsafe } from '../../utils/object.utils'; import { createCandlesOffsetProvider } from '../chart/data-series.high-low-provider'; import { GridComponent } from '../grid/grid.component'; import { YAxisComponent } from '../y_axis/y-axis.component'; import { createDefaultYExtentHighLowProvider, YExtentComponent, } from './extent/y-extent-component'; import { merge } from '../../utils/merge.utils'; export class PaneComponent extends ChartBaseElement { get scale() { return this.mainExtent.scale; } get yAxis() { return this.mainExtent.yAxis; } get dataSeries() { return flatMap(this.yExtentComponents, c => Array.from(c.dataSeries)); } get visible() { return this.canvasBoundsContainer.graphsHeightRatio[this.uuid] > 0; } constructor(chartBaseModel, mainCanvasModel, yAxisLabelsCanvasModel, dynamicObjectsCanvasModel, hitTestController, config, mainScale, drawingManager, chartPanComponent, canvasInputListener, canvasAnimation, cursorHandler, eventBus, canvasBoundsContainer, uuid, seriesAddedSubject, seriesRemovedSubject, hitTestCanvasModel, chartResizeHandler, options) { super(); this.chartBaseModel = chartBaseModel; this.mainCanvasModel = mainCanvasModel; this.yAxisLabelsCanvasModel = yAxisLabelsCanvasModel; this.dynamicObjectsCanvasModel = dynamicObjectsCanvasModel; this.hitTestController = hitTestController; this.config = config; this.mainScale = mainScale; this.drawingManager = drawingManager; this.chartPanComponent = chartPanComponent; this.canvasInputListener = canvasInputListener; this.canvasAnimation = canvasAnimation; this.cursorHandler = cursorHandler; this.eventBus = eventBus; this.canvasBoundsContainer = canvasBoundsContainer; this.uuid = uuid; this.seriesAddedSubject = seriesAddedSubject; this.seriesRemovedSubject = seriesRemovedSubject; this.hitTestCanvasModel = hitTestCanvasModel; this.chartResizeHandler = chartResizeHandler; this.yExtentComponents = []; this.yExtentComponentsChangedSubject = new Subject(); this.getYAxisBounds = () => { return this.canvasBoundsContainer.getBounds(CanvasElement.PANE_UUID_Y_AXIS(this.uuid)); }; this.valueFormatter = (value, dataSeries) => { return this.mainExtent.valueFormatter(value, dataSeries); }; const yExtentComponent = this.createExtentComponent(options); this.mainExtent = yExtentComponent; this.ht = this.canvasBoundsContainer.getBoundsHitTest(CanvasElement.PANE_UUID(uuid), { // this is needed to reduce cross event fire zone, so cross event won't be fired when hover resizer // maybe we need to rework it, this isn't perfect: top and bottom panes will have small no-hover area extensionY: -this.config.components.paneResizer.dragZone, }); } /** * Method that activates the canvas bounds container and recalculates the zoom Y of the scale model. * @protected * @function * @returns {void} */ doActivate() { super.doActivate(); this.addRxSubscription(this.canvasBoundsContainer .observeBoundsChanged(CanvasElement.PANE_UUID(this.uuid)) .pipe(distinctUntilChanged(areBoundsChanged)) .subscribe(() => { this.yExtentComponents.forEach(c => c.scale.recalculateZoomY()); this.dynamicObjectsCanvasModel.fireDraw(); })); } toY(price) { var _a, _b; return (_b = (_a = this.mainExtent.mainDataSeries) === null || _a === void 0 ? void 0 : _a.view.toY(price)) !== null && _b !== void 0 ? _b : this.scale.toY(price); } /** * Creates a new GridComponent instance with the provided parameters. * @param {string} uuid - The unique identifier of the pane. * @param {ScaleModel} scale - The scale model used to calculate the scale of the grid. * @param {YAxisConfig} yAxisState - y Axis Config * @param {() => NumericAxisLabel[]} yAxisLabelsGetter * @param {() => Unit} yAxisBaselineGetter * @returns {GridComponent} - The newly created GridComponent instance. */ createGridComponent(uuid, extentIdx, scale, yAxisState, yAxisLabelsGetter, yAxisBaselineGetter) { var _a, _b; const chartPaneId = CanvasElement.PANE_UUID(uuid); const mainExtentIdx = (_b = (_a = this.mainExtent) === null || _a === void 0 ? void 0 : _a.idx) !== null && _b !== void 0 ? _b : 0; const gridComponent = new GridComponent(this.mainCanvasModel, scale, this.config, yAxisState, `PANE_${uuid}_${extentIdx}_grid_drawer`, this.drawingManager, () => this.canvasBoundsContainer.getBounds(chartPaneId), () => this.canvasBoundsContainer.getBounds(chartPaneId), () => [], yAxisLabelsGetter, extentIdx, yAxisBaselineGetter, () => this.config.components.grid.visible, mainExtentIdx); return gridComponent; } /** * Creates a handler for Y-axis panning of the chart. * @private * @param {string} uuid - The unique identifier of the chart pane. * @param {ScaleModel} scale - The scale model of the chart. * @returns [Unsubscriber, DragNDropYComponent] */ createYPanHandler(uuid, scale) { const chartPaneId = CanvasElement.PANE_UUID(uuid); const dragNDropComponent = this.chartPanComponent.chartAreaPanHandler.registerChartYPanHandler(scale, this.canvasBoundsContainer.getBoundsHitTest(chartPaneId)); return [ () => { this.chartPanComponent.chartAreaPanHandler.removeChildEntity(dragNDropComponent); dragNDropComponent.disable(); }, dragNDropComponent, ]; } addCursors(extentIdx, yAxisComponent) { const paneYAxis = CanvasElement.PANE_UUID_Y_AXIS(this.uuid, extentIdx); this.cursorHandler.setCursorForCanvasEl(paneYAxis, yAxisComponent.state.cursor); return () => this.cursorHandler.removeCursorForCanvasEl(paneYAxis); } createExtentComponent(options) { var _a, _b; const extentIdx = this.yExtentComponents.length; const chartPaneId = CanvasElement.PANE_UUID(this.uuid); const getBounds = () => this.canvasBoundsContainer.getBounds(chartPaneId); const scaleModel = (_a = options === null || options === void 0 ? void 0 : options.scale) !== null && _a !== void 0 ? _a : new SyncedByXScaleModel(this.mainScale, this.config, getBounds, this.canvasAnimation); const initialYAxisState = options === null || options === void 0 ? void 0 : options.initialYAxisState; const [unsub, dragNDrop] = this.createYPanHandler(this.uuid, scaleModel); // creating partially resolved constructor except formatter & dataSeriesProvider - bcs it's not possible to provide formatter // before y-extent is created const createYAxisComponent = (formatter, dataSeriesProvider) => new YAxisComponent(this.eventBus, this.config, this.mainCanvasModel, this.yAxisLabelsCanvasModel, scaleModel, this.canvasInputListener, this.canvasBoundsContainer, this.chartPanComponent, this.cursorHandler, formatter, dataSeriesProvider, this.uuid, extentIdx, this.hitTestCanvasModel, this.chartResizeHandler, initialYAxisState); const yExtentComponent = new YExtentComponent(this.config.components.yAxis, this.uuid, extentIdx, this, this.chartBaseModel, this.canvasBoundsContainer, this.hitTestController, this.dynamicObjectsCanvasModel, scaleModel, createYAxisComponent, dragNDrop); yExtentComponent.addSubscription(unsub); yExtentComponent.addSubscription(this.addCursors(extentIdx, yExtentComponent.yAxis)); (options === null || options === void 0 ? void 0 : options.paneFormatters) && yExtentComponent.setValueFormatters(options.paneFormatters); yExtentComponent.yAxis.togglePriceScaleInverse(options === null || options === void 0 ? void 0 : options.inverse); yExtentComponent.scale.setLockPriceToBarRatio(options === null || options === void 0 ? void 0 : options.lockToPriceRatio); const useDefaultHighLow = (_b = options === null || options === void 0 ? void 0 : options.useDefaultHighLow) !== null && _b !== void 0 ? _b : true; if (useDefaultHighLow) { scaleModel.autoScaleModel.setHighLowProvider('default', createCandlesOffsetProvider(() => ({ top: 10, bottom: 10, left: 0, right: 0, visible: true }), createDefaultYExtentHighLowProvider(yExtentComponent))); } const gridComponent = this.createGridComponent(this.uuid, yExtentComponent.idx, scaleModel, yExtentComponent.yAxis.state, () => yExtentComponent.yAxis.model.baseLabelsModel.labels, () => yExtentComponent.toY(yExtentComponent.getBaseline())); yExtentComponent.addChildEntity(gridComponent); yExtentComponent.activate(); this.yExtentComponents.push(yExtentComponent); this.canvasBoundsContainer.updateYAxisWidths(); this.yExtentComponentsChangedSubject.next(); return yExtentComponent; } removeExtentComponents(extentComponents) { extentComponents.forEach(extentComponent => extentComponent.disable()); this.yExtentComponents = this.yExtentComponents.filter(current => !extentComponents.map(excluded => excluded.idx).includes(current.idx)); // re-index extents this.yExtentComponents.forEach((c, idx) => { c.yAxis.setExtentIdx(idx); c.idx = idx; c.yAxis.updateCursor(); }); this.canvasBoundsContainer.updateYAxisWidths(); this.yExtentComponentsChangedSubject.next(); } /** * Create new pane extent and attach data series to it */ moveDataSeriesToNewExtentComponent(dataSeries, initialPane, initialExtent, align = 'right') { const yAxisConfigCopy = cloneUnsafe(initialExtent.yAxis.state); const scaleConfigCopy = cloneUnsafe(initialExtent.scale.state); const initialYAxisState = merge(yAxisConfigCopy, this.config.components.yAxis, { overrideExisting: false, addIfMissing: true, }); const extent = this.createExtentComponent({ initialYAxisState, inverse: scaleConfigCopy.inverse, lockToPriceRatio: scaleConfigCopy.lockPriceToBarRatio, }); extent.yAxis.setYAxisAlign(align); dataSeries.forEach(series => series.moveToExtent(extent)); initialExtent.dataSeries.size === 0 && initialPane.removeExtentComponents([initialExtent]); } /** * Attach data series to existing y axis extent */ moveDataSeriesToExistingExtentComponent(dataSeries, initialPane, initialExtent, extentComponent, // in some cases extent should not be deleted right after data series move, // because the next data series could be moved to it isForceKeepExtent) { dataSeries.forEach(series => series.moveToExtent(extentComponent)); !isForceKeepExtent && initialExtent.dataSeries.size === 0 && initialPane.removeExtentComponents([initialExtent]); this.yExtentComponentsChangedSubject.next(); } /** * This method updates the view by calling the doAutoScale method of the scaleModel and firing the Draw event using the eventBus. * @private */ updateView() { this.yExtentComponents.forEach(c => { c.scale.doAutoScale(); c.yAxis.model.labelsGenerator.generateNumericLabels(); }); this.canvasBoundsContainer.updateYAxisWidths(); this.eventBus.fireDraw(); } /** * Merges all the y-axis extents on the pane into one. */ mergeYExtents() { for (let i = 1; i < this.yExtentComponents.length; i++) { const extent = this.yExtentComponents[i]; extent.dataSeries.forEach(s => s.moveToExtent(this.mainExtent)); extent.disable(); } this.canvasBoundsContainer.updateYAxisWidths(); this.yExtentComponents = [this.mainExtent]; } /** * Returns the bounds of the pane component. */ getBounds() { return this.mainExtent.getBounds(); } /** * Creates a new DataSeriesModel object. * @returns {DataSeriesModel} - The newly created DataSeriesModel object. */ createDataSeries() { var _a; return (_a = this.mainExtent) === null || _a === void 0 ? void 0 : _a.createDataSeries(); } /** * Adds a new data series to the chart. * @param {DataSeriesModel} series - The data series to be added. * @returns {void} */ addDataSeries(series) { this.mainExtent.addDataSeries(series); this.updateView(); } /** * Removes a data series from the chart. * * @param {DataSeriesModel} series - The data series to be removed. * @returns {void} */ removeDataSeries(series) { this.mainExtent.removeDataSeries(series); this.updateView(); } // TODO hack, remove when each pane will have separate y-axis component /** * Returns the type of the y-axis component for the current pane. * * @returns {PriceAxisType} The 'regular' type of the y-axis component for the current pane. * */ getAxisType() { return 'regular'; } /** * Moves the canvas bounds container up by calling the movePaneUp method with the uuid of the current object. * @returns {void} * @deprecated Use `paneManager.movePaneUp()` instead */ moveUp() { this.canvasBoundsContainer.movePaneUp(this.uuid); } /** * Moves the canvas bounds container down by calling the movePaneDown method with the uuid of the current object. * @returns {void} * @deprecated Use `paneManager.movePaneDown()` instead */ moveDown() { this.canvasBoundsContainer.movePaneDown(this.uuid); } /** * Checks if the current pane can move up. * @returns {boolean} - Returns true if the current pane can move up, otherwise false. * @deprecated Use `paneManager.canMovePaneUp()` instead */ canMoveUp() { const firstVisiblePane = firstOf(this.canvasBoundsContainer.panesOrder.filter(uuid => this.canvasBoundsContainer.graphsHeightRatio[uuid] > 0)); return this.uuid !== firstVisiblePane && this.visible; } /** * 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. * @deprecated Use `paneManager.canMovePaneDown()` instead */ canMoveDown() { const lastVisiblePane = lastOf(this.canvasBoundsContainer.panesOrder.filter(uuid => this.canvasBoundsContainer.graphsHeightRatio[uuid] > 0)); return this.uuid !== lastVisiblePane && this.visible; } get regularFormatter() { return this.mainExtent.formatters.regular; } /** * Sets the pane value formatters for the current instance. * @param {YExtentFormatters} formatters - The pane value formatters to be set. */ setPaneValueFormatters(formatters) { this.mainExtent.setValueFormatters(formatters); } /** * Returns the regular value from Y coordinate. * @param {number} y - The Y coordinate. * @returns {number} - The regular value. */ regularValueFromY(y) { return this.mainExtent.regularValueFromY(y); } }