@devexperts/dxcharts-lite
Version:
321 lines (320 loc) • 16.2 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 { 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);
}
}