@devexperts/dxcharts-lite
Version:
937 lines (936 loc) • 42.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 { BehaviorSubject, Subject } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { arrayCompare, arrayRemove2, flat, moveInArrayMutable, reorderArray } from '../utils/array.utils';
import { isMobile } from '../utils/device/browser.utils';
import { calcTimeLabelBounds } from '../components/navigation_map/navigation-map.model';
import { calculateSymbolHeight } from '../utils/canvas/canvas-font-measure-tool.utils';
import { YAxisBoundsContainer } from './y-axis-bounds.container';
export const CHART_UUID = 'CHART';
class CanvasElement {
}
CanvasElement.CANVAS = 'CANVAS';
CanvasElement.N_MAP = 'N_MAP';
CanvasElement.X_AXIS = 'X_AXIS';
CanvasElement.N_MAP_KNOT_L = 'N_MAP_KNOT_L';
CanvasElement.N_MAP_KNOT_R = 'N_MAP_KNOT_R';
CanvasElement.N_MAP_BTN_L = 'N_MAP_BTN_L';
CanvasElement.N_MAP_BTN_R = 'N_MAP_BTN_R';
CanvasElement.N_MAP_SLIDER_WINDOW = 'N_MAP_SLIDER_WINDOW';
CanvasElement.N_MAP_CHART = 'N_MAP_CHART';
CanvasElement.N_MAP_LABEL_R = 'N_MAP_LABEL_R';
CanvasElement.N_MAP_LABEL_L = 'N_MAP_LABEL_L';
CanvasElement.PANE_UUID = (uuid) => 'PANE_' + uuid;
CanvasElement.PANE_UUID_Y_AXIS = (uuid, idx = 0) => 'PANE_' + uuid + '_Y_AXIS_' + idx;
CanvasElement.PANE_UUID_RESIZER = (uuid) => 'PANE_' + uuid + '_RESIZER';
CanvasElement.ALL_PANES = 'ALL_PANES';
CanvasElement.CHART_WITH_Y_AXIS = 'CHART_WITH_Y_AXIS';
CanvasElement.EVENTS = 'EVENTS';
/**
* @returns pane bounds for the main chart
*/
CanvasElement.CHART = CanvasElement.PANE_UUID(CHART_UUID);
/**
* @returns y-axis bounds for the main chart
*/
CanvasElement.Y_AXIS = CanvasElement.PANE_UUID_Y_AXIS(CHART_UUID);
export { CanvasElement };
export const DEFAULT_BOUNDS = { x: 0, y: 0, pageX: 0, pageY: 0, width: 0, height: 0 };
export const DEFAULT_MIN_PANE_HEIGHT = 20;
const N_MAP_H = 35;
const N_MAP_BUTTON_W = 15;
const KNOTS_W_MOBILE_MULTIPLIER = 1.5;
const N_MAP_KNOT_W = isMobile() ? 8 * KNOTS_W_MOBILE_MULTIPLIER : 8;
// additional x axis height padding for mobiles, used for better usability on mobile devices
export const X_AXIS_MOBILE_PADDING = isMobile() ? 5 : 0;
/**
* we need to check that: heightRatios - 1 < 0.000001 after calculations between decimals
*/
const PRECISION_DIFFERENCE = 0.000001;
/**
* This component listens EVENT_DRAW and recalculates bounds of canvas chart elements.
* {@link getBounds} method will always give actual placement of element you want.
*
* NOTE: this class was designed exclusively for {@link getBounds} method
* please think twice when adding additional logic here (does it affect element bounds? or it's smth else)
*/
export class CanvasBoundsContainer {
get graphsHeightRatio() {
return this._graphsHeightRatio;
}
constructor(config, eventBus, canvasModel, formatterFactory, chartResizeHandler) {
this.config = config;
this.eventBus = eventBus;
this.canvasModel = canvasModel;
this.formatterFactory = formatterFactory;
// holds all canvas element bounds
this.bounds = {};
// position of canvas on the whole page
this.canvasOnPageLocation = { x: 0, y: 0, pageX: 0, pageY: 0, width: 0, height: 0 };
// holds ordered "top to bottom" array of panes UUID's (studies in past)
this.panesOrder = [];
this.panesOrderChangedSubject = new Subject();
this.paneVisibilityChangedSubject = new Subject();
// both will be calculated based on font/content size
this.xAxisHeight = undefined;
this.yAxisWidths = {
right: [0],
left: [0],
};
this.leftRatio = 0;
this.rightRatio = 0;
this.boundsChangedSubject = new Subject();
this.barResizerChangedSubject = new Subject();
this._graphsHeightRatio = { [CHART_UUID]: 1 };
this.graphsHeightRatioChangedSubject = new Subject();
this.boundsChangedSubscriptions = {};
chartResizeHandler.canvasResized.subscribe(bcr => {
let calculatedBCR = bcr;
if (!calculatedBCR) {
calculatedBCR = this.canvasModel.canvas.getBoundingClientRect();
}
this.updateCanvasOnPageLocation(calculatedBCR);
this.recalculateBounds();
});
this.yAxisBoundsContainer = new YAxisBoundsContainer(this.config, this.canvasModel);
}
updateYAxisWidths() {
const widths = this.yAxisBoundsContainer.getYAxisWidths();
this.setYAxisWidths(widths);
}
/**
* Adds a pane to the list of panes and recalculates the height ratios of all panes.
* @param {string} uuid - The unique identifier of the pane to be added.
* @param {number} [order] - The order in which the pane should be added. If not provided, the pane will be added to the end of the list.
*/
addPaneBounds(uuid, order) {
if (this.panesOrder.indexOf(uuid) === -1) {
this.panesOrder.push(uuid);
if (order !== undefined) {
const idx = this.panesOrder.indexOf(uuid);
moveInArrayMutable(this.panesOrder, idx, order);
}
this.recalculatePanesHeightRatios();
this.panesOrderChangedSubject.next(this.panesOrder);
}
}
/**
* Overrides the height ratios of the chart with the provided height ratios.
* @param {Record<string, number>} heightRatios - An object containing the height ratios to be set.
* @returns {void}
* @throws {Error} If the sum of the height ratios is not equal to 1.
*/
overrideChartHeightRatios(heightRatios) {
const resultRatio = Object.assign(Object.assign({}, this.graphsHeightRatio), heightRatios);
const ratioSum = Object.values(resultRatio).reduce((sum, ratio) => sum + ratio, 0);
if (Math.abs(ratioSum - 1) < PRECISION_DIFFERENCE) {
this._graphsHeightRatio = resultRatio;
this.recalculateBounds();
}
else {
console.error(`Result ratio should be equal 1, but equal ${ratioSum}`);
}
}
/**
* Moves a pane up in the panesOrder array.
* @param {string} uuid - The unique identifier of the pane to be moved.
* @returns {void}
*/
movePaneUp(uuid) {
const idx = this.panesOrder.indexOf(uuid);
if (idx !== -1) {
moveInArrayMutable(this.panesOrder, idx, idx - 1);
this.recalculateBounds();
this.eventBus.fireDraw();
this.panesOrderChangedSubject.next(this.panesOrder);
}
}
/**
* Moves the pane down in the panesOrder array.
* @param {string} uuid - The unique identifier of the pane to be moved.
* @returns {void}
*/
movePaneDown(uuid) {
const idx = this.panesOrder.indexOf(uuid);
if (idx !== -1) {
moveInArrayMutable(this.panesOrder, idx, idx + 1);
this.recalculateBounds();
this.eventBus.fireDraw();
this.panesOrderChangedSubject.next(this.panesOrder);
}
}
/**
* Reorders current panes according to newPanesOrder
* if element in newPanesOrder doesn't exist in real panes order - it will ignored
* Example:
* panesOrder: ['1', '2', '3']
* newPanesOrder: ['3', '1']
* result: ['3', '2', '1']
*/
reorderPanes(newPanesOrder) {
this.panesOrder = reorderArray(this.panesOrder, newPanesOrder);
this.recalculateBounds();
this.panesOrderChangedSubject.next(this.panesOrder);
}
hidePaneBounds(uuid) {
this.graphsHeightRatio[uuid] = 0;
this.recalculatePanesHeightRatios();
this.paneVisibilityChangedSubject.next();
}
showPaneBounds(uuid) {
if (uuid === CHART_UUID) {
const [defaultChartHeightRatio] = getHeightRatios(this.panesOrder.length - 1);
this.graphsHeightRatio[uuid] = defaultChartHeightRatio;
}
else {
// when pane is hidden it has ratio of 0
// when we want pane to be visible again we want `recalculatePanesHeightRatios` function
// to treat it as a new pane
// to do so we need to delete its ratio (which is 0 because it is hidden) from graphsHeightRatio
// NOTE: CHART_UUID pane is exception, it is treated differently and should always have some ratio
delete this.graphsHeightRatio[uuid];
}
this.recalculatePanesHeightRatios();
this.paneVisibilityChangedSubject.next();
}
/**
* Removes the bounds of a pane with the given uuid from the canvas element.
* @param {string} uuid - The uuid of the pane to remove.
* @returns {void}
*/
removePaneBounds(uuid) {
arrayRemove2(this.panesOrder, uuid);
delete this.graphsHeightRatio[uuid];
delete this.bounds[CanvasElement.PANE_UUID(uuid)];
this.yAxisWidths.left
.concat(this.yAxisWidths.right)
.forEach((_, idx) => delete this.bounds[CanvasElement.PANE_UUID_Y_AXIS(uuid, idx)]);
delete this.bounds[CanvasElement.PANE_UUID_RESIZER(uuid)];
this.recalculatePanesHeightRatios();
this.panesOrderChangedSubject.next(this.panesOrder);
}
/**
* Recalculates the bounds of the chart elements based on the current configuration and canvas size.
* The function updates the bounds of the canvas, the main chart, the panes, the y-axis, and the chart with y-axis.
* It also updates the navigation map element bounds, all bounds page coordinates, and notifies the bounds subscribers.
* @function
* @name recalculateBounds
* @memberof ChartWidget
* @returns {void}
*/
recalculateBounds() {
const canvasW = this.canvasOnPageLocation.width;
const canvasH = this.canvasOnPageLocation.height;
const paneResizerHeight = this.config.components.paneResizer.height;
// whole canvas bounds
const canvas = this.getBounds(CanvasElement.CANVAS);
canvas.x = 0;
canvas.y = 0;
canvas.width = canvasW;
canvas.height = canvasH;
const yAxisWidths = this.yAxisWidths;
const nMap = this.getNavMapBounds(canvas);
const xAxis = this.getXAxisBounds(nMap, canvas);
const chartHeight = canvasH - xAxis.height - nMap.height;
const totalYAxisWidthLeft = yAxisWidths.left.reduce((sum, width) => sum + width, 0);
const totalYAxisWidthRight = yAxisWidths.right.reduce((sum, width) => sum + width, 0);
// main chart
const yAxisXLeft = 0;
const yAxisXRight = canvas.width - totalYAxisWidthRight;
const paneXStart = yAxisXLeft + totalYAxisWidthLeft;
const initialY = 0;
const chartWidth = canvas.width - totalYAxisWidthLeft - totalYAxisWidthRight;
let nextY = initialY;
// panes
const firstVisiblePaneIdx = this.panesOrder.findIndex(uuid => this.graphsHeightRatio[uuid] > 0);
this.panesOrder.forEach((uuid, index) => {
var _a;
const paneHeightRatio = this.graphsHeightRatio[this.panesOrder[index]];
// hide resizer for the first visible pane
const resizerUUID = CanvasElement.PANE_UUID_RESIZER(uuid);
const paneUUID = CanvasElement.PANE_UUID(uuid);
const resizerVisible = this.config.components.paneResizer.visible && index > firstVisiblePaneIdx && paneHeightRatio > 0;
if (resizerVisible) {
upsertBounds(this.bounds, resizerUUID, 0, nextY, canvas.width, paneResizerHeight, this.canvasOnPageLocation);
}
else {
upsertBounds(this.bounds, resizerUUID, 0, 0, 0, 0, this.canvasOnPageLocation);
}
const paneYStart = nextY + (resizerVisible ? paneResizerHeight : 0);
const paneBounds = upsertBounds(this.bounds, paneUUID, paneXStart, paneYStart, chartWidth, chartHeight * paneHeightRatio - (resizerVisible ? paneResizerHeight : 0), this.canvasOnPageLocation);
// y axis
if (this.config.components.yAxis.visible) {
const extents = this.yAxisBoundsContainer.extentsOrder.get(uuid);
if (extents === undefined) {
return;
}
const height = chartHeight * paneHeightRatio - (resizerVisible ? paneResizerHeight : 0);
let startXLeft = paneXStart - ((_a = yAxisWidths.left[0]) !== null && _a !== void 0 ? _a : 0); // we need to provide right to left order on the left y-axis side
let startXRight = yAxisXRight;
extents.left.forEach((extentIdx, i) => {
var _a;
upsertBounds(this.bounds, CanvasElement.PANE_UUID_Y_AXIS(uuid, extentIdx), startXLeft, paneYStart, yAxisWidths.left[i], height, this.canvasOnPageLocation);
startXLeft -= (_a = yAxisWidths.left[i + 1]) !== null && _a !== void 0 ? _a : 0;
});
extents.right.forEach((extentIdx, i) => {
upsertBounds(this.bounds, CanvasElement.PANE_UUID_Y_AXIS(uuid, extentIdx), startXRight, paneYStart, yAxisWidths.right[i], height, this.canvasOnPageLocation);
startXRight += yAxisWidths.right[i];
});
}
else {
upsertBounds(this.bounds, CanvasElement.PANE_UUID_Y_AXIS(uuid), 0, 0, 0, 0, this.canvasOnPageLocation);
}
nextY = paneBounds.y + paneBounds.height;
});
const allPanesBounds = this.getBounds(CanvasElement.ALL_PANES);
allPanesBounds.x = paneXStart;
allPanesBounds.y = initialY;
allPanesBounds.width = chartWidth;
allPanesBounds.height = nextY;
// chart with Y axis
const chartWithYAxis = this.getBounds(CanvasElement.CHART_WITH_Y_AXIS);
const chartBounds = this.getBounds(CanvasElement.CHART);
this.getEventsBounds(chartBounds);
this.copyBounds(chartBounds, chartWithYAxis);
chartWithYAxis.width = canvas.width;
this.recalculateNavigationMapElementBounds();
this.updateAllBoundsPageCoordinates();
this.notifyBoundsSubscribers();
}
/**
* Updates the canvasOnPageLocation property with the provided PickedDOMRect object.
* @param {PickedDOMRect} bcr - The PickedDOMRect object containing the new values for x, y, width, and height.
* @private
*/
updateCanvasOnPageLocation(bcr) {
this.canvasOnPageLocation = Object.assign(Object.assign({}, this.canvasOnPageLocation), { x: bcr.x, y: bcr.y, width: bcr.width, height: bcr.height });
}
/**
* Updates the page coordinates of all bounds.
* @private
*/
updateAllBoundsPageCoordinates() {
for (const name of Object.keys(this.bounds)) {
const bound = this.bounds[name];
bound.pageX = bound.x + this.canvasOnPageLocation.x;
bound.pageY = bound.y + this.canvasOnPageLocation.y;
}
}
/**
* Returns the bounds of the events component.
* @private
* @param {Bounds} chartPane - The bounds of the chart pane.
* @returns {Bounds} - The bounds of the events component.
*/
getEventsBounds(chartPane) {
const events = this.getBounds(CanvasElement.EVENTS);
if (this.config.components.events.visible) {
events.x = 0;
events.y = chartPane.y + chartPane.height - this.config.components.events.height;
events.width = chartPane.width;
events.height = this.config.components.events.height;
}
else {
this.applyDefaultBounds(events);
}
return events;
}
/**
* Returns the bounds of the navigation map element.
* @param {Bounds} canvas - The bounds of the canvas element.
* @returns {Bounds} - The bounds of the navigation map element.
* @private
*/
getNavMapBounds(canvas) {
const nMap = this.getBounds(CanvasElement.N_MAP);
if (this.config.components.navigationMap.visible) {
nMap.x = 0;
nMap.y = canvas.height - N_MAP_H;
nMap.width = canvas.width;
nMap.height = N_MAP_H;
}
else {
this.applyDefaultBounds(nMap);
}
return nMap;
}
/**
* Returns the bounds of the X axis based on the provided parameters.
* @private
* @param {Bounds} nMap - The bounds of the nMap.
* @param {Bounds} canvas - The bounds of the canvas.
* @returns {Bounds} - The bounds of the X axis.
*/
getXAxisBounds(nMap, canvas) {
const xAxis = this.getBounds(CanvasElement.X_AXIS);
const isSingleYAxis = this.yAxisWidths.left.length + this.yAxisWidths.right.length === 1;
const leftOffset = isSingleYAxis ? 0 : this.yAxisWidths.left.reduce((acc, width) => (acc += width), 0);
const rightOffset = isSingleYAxis ? 0 : this.yAxisWidths.right.reduce((acc, width) => (acc += width), 0);
if (this.config.components.xAxis.visible) {
const xAxisHeight = this.getXAxisHeight();
xAxis.x = leftOffset;
xAxis.y = canvas.height - xAxisHeight - nMap.height;
xAxis.width = canvas.width - rightOffset;
xAxis.height = xAxisHeight;
}
else {
this.applyDefaultBounds(xAxis);
}
return xAxis;
}
/**
* Calculates the height of the X axis based on the font size, font family and padding values.
* If the height has already been calculated, it returns the cached value.
* @returns {number} The height of the X axis.
*/
getXAxisHeight() {
var _a, _b;
if (!this.xAxisHeight) {
const font = this.config.components.xAxis.fontSize + 'px ' + this.config.components.xAxis.fontFamily;
const fontHeight = calculateSymbolHeight(font, this.canvasModel.ctx);
this.xAxisHeight =
fontHeight +
((_a = this.config.components.xAxis.padding.top) !== null && _a !== void 0 ? _a : 0) +
((_b = this.config.components.xAxis.padding.bottom) !== null && _b !== void 0 ? _b : 0) +
X_AXIS_MOBILE_PADDING;
}
return this.xAxisHeight;
}
/**
* Sets the width of the Y axis.
*
* @param {YAxisWidths} yAxisWidths - The width of the Y axis.
* @returns {void}
*/
setYAxisWidths(yAxisWidths) {
if (!arrayCompare(this.yAxisWidths.left, yAxisWidths.left) ||
!arrayCompare(this.yAxisWidths.right, yAxisWidths.right)) {
this.yAxisWidths = yAxisWidths;
this.recalculateBounds();
}
}
/**
*
* Sets the height of the X axis.
* @param {number} xAxisHeight - The height of the X axis.
* @returns {void}
*/
setXAxisHeight(xAxisHeight) {
if (xAxisHeight !== this.xAxisHeight) {
this.xAxisHeight = xAxisHeight;
this.recalculateBounds();
}
}
/**
* Sets the order of the panes.
* @param {string[]} panesOrder - An array of strings representing the order of the panes.
* @returns {void}
*/
setPanesOrder(panesOrder) {
this.panesOrder = panesOrder;
this.recalculateBounds();
}
/**
* Recalculates the height ratios of the panes by calling the calculateGraphsHeightRatios() and recalculateBounds() methods.
*/
recalculatePanesHeightRatios() {
this.calculateGraphsHeightRatios();
this.recalculateBounds();
}
/**
* Calculates the height ratios of the graphs in the chart.
* It first gets the height ratio of the main chart and then calculates the height ratios of the other graphs.
* It then calculates the free space available for the other graphs and distributes it among them based on their previous height ratios.
* If there are new graphs added, it calculates their height ratios based on the number of new graphs and the total number of graphs.
* @private
*/
calculateGraphsHeightRatios() {
let chartRatio = this.graphsHeightRatio[CHART_UUID];
// NOTE: pec stands for panesExceptMainChart
const pec = [];
pec.push(...this.panesOrder.filter(p => p !== CHART_UUID));
const pecRatios = pec.map(uuid => this.graphsHeightRatio[uuid] === undefined ? undefined : this.graphsHeightRatio[uuid]);
// we should count only visible panes, to escape wheight distribution for hidden panes
const visiblePecRatios = pecRatios.filter(ratio => ratio !== 0);
const visiblePecNumber = visiblePecRatios.length;
// we don't count as an old PEC panes that are not visible, because they don't whey in the final result
const oldPecNumber = visiblePecRatios.filter(ratio => ratio !== undefined).length;
// if ratio in undefined for a given pane it means that it's a new pane
const newPecNumber = pecRatios.filter(ratio => ratio === undefined).length;
const paneIsAdded = newPecNumber > 0;
let freeRatioForPec = 0;
let freeRatio = 0;
let ratioForOldPec = 1;
let ratioForNewPec = 0;
if (paneIsAdded) {
[ratioForOldPec, ratioForNewPec] = getHeightRatios(visiblePecNumber);
}
//#region chart height ratio logic
if (!this.graphsHeightRatio[CHART_UUID] || this.graphsHeightRatio[CHART_UUID] === 0) {
chartRatio = 0;
}
if (this.graphsHeightRatio[CHART_UUID] && this.graphsHeightRatio[CHART_UUID] !== 0) {
if (paneIsAdded) {
chartRatio = 1 * ratioForOldPec;
}
else {
// pec ratio values before recalculating and new chart ratio
const currentHeightRatioValues = Object.values(this.graphsHeightRatio).reduce((prev, curr) => (curr += prev));
// chart is hidden and one of the pec is removed
// since the chart height ratio is new, the currentHeightRatioValues could be > 1, it happens if make chart visible again
if (currentHeightRatioValues < 1 && this.graphsHeightRatio[CHART_UUID] !== 0) {
chartRatio += 1 - currentHeightRatioValues;
}
}
}
//#endregion
// this means we should keep in mind only new panes
if (oldPecNumber === 0) {
chartRatio = 1 - ratioForNewPec * newPecNumber;
}
freeRatio = 1 - chartRatio - ratioForNewPec * newPecNumber;
visiblePecRatios.forEach(ratio => {
if (ratio) {
freeRatio -= ratio * ratioForOldPec;
}
});
// || 1 to escape division by zero
// because there's might be no visible panes except CHART
freeRatioForPec = freeRatio / (visiblePecNumber || 1);
// distribute left free ratio between new and old panes
const proportions = pecRatios.map(ratio => {
// if ratio === 0 it means, that it's hidden
if (ratio === 0) {
return ratio;
}
if (!ratio) {
return ratioForNewPec + freeRatioForPec;
}
return ratio * ratioForOldPec + freeRatioForPec;
});
this._graphsHeightRatio = {};
this.graphsHeightRatio[CHART_UUID] = chartRatio;
proportions.forEach((ratio, index) => {
const name = pec[index];
this.graphsHeightRatio[name] = ratio;
});
}
/**
* Recalculates the bounds of the navigation map elements based on the current configuration and viewport.
* If the navigation map is visible, it calculates the bounds of the following elements:
* - Navigation map
* - Time labels
* - Left and right buttons
* - Knots
* - Slider
* - Chart
*
* The bounds of each element are calculated based on the current viewport and configuration values.
* If the time labels are visible, it calculates the width of the left and right time labels based on the first and last visible candle timestamps.
* The width of the time labels is used to calculate the position and size of the left and right buttons, as well as the position of the knots.
* The position and size of the knots are calculated based on the left and right ratios, which represent the position of the knots relative to the left and right buttons.
* The position and size of the slider and chart are calculated based on the position and size of the knots and buttons.
*/
recalculateNavigationMapElementBounds() {
var _a, _b, _c, _d;
if (!this.config.components.navigationMap.visible) {
return;
}
const nMap = this.getBounds(CanvasElement.N_MAP);
const { height, width } = this.config.components.navigationMap.knots;
const knotHeightFromConfig = height !== null && height !== void 0 ? height : 0;
const knotWidthFromConfig = isMobile() ? width * KNOTS_W_MOBILE_MULTIPLIER : width !== null && width !== void 0 ? width : 0;
const knotY = !knotHeightFromConfig ? nMap.y : nMap.y + (nMap.height - knotHeightFromConfig) / 2;
// time labels
const timeLabelsVisible = (_b = (_a = this.config.components.navigationMap) === null || _a === void 0 ? void 0 : _a.timeLabels) === null || _b === void 0 ? void 0 : _b.visible;
const calcLabelBounds = (timestamp) => {
return calcTimeLabelBounds(this.canvasModel.ctx, timestamp, this.formatterFactory, this.config)[0];
};
const candleSource = flat((_d = (_c = this.mainCandleSeries) === null || _c === void 0 ? void 0 : _c.getSeriesInViewport()) !== null && _d !== void 0 ? _d : []);
const leftTimeLabelWidth = timeLabelsVisible && candleSource.length ? calcLabelBounds(candleSource[0].candle.timestamp) : 0;
const rightTimeLabelWidth = timeLabelsVisible && candleSource.length
? calcLabelBounds(candleSource[candleSource.length - 1].candle.timestamp)
: 0;
const timeLabelWidth = Math.max(leftTimeLabelWidth, rightTimeLabelWidth);
if (timeLabelsVisible) {
const nMapLabelL = this.getBounds(CanvasElement.N_MAP_LABEL_L);
nMapLabelL.x = nMap.x;
nMapLabelL.y = nMap.y;
nMapLabelL.width = timeLabelWidth;
nMapLabelL.height = nMap.height;
const nMapLabelR = this.getBounds(CanvasElement.N_MAP_LABEL_R);
nMapLabelR.x = nMap.x + nMap.width - timeLabelWidth;
nMapLabelR.y = nMap.y;
nMapLabelR.width = timeLabelWidth;
nMapLabelR.height = nMap.height;
}
// buttons left and right
const nMapBtnL = this.getBounds(CanvasElement.N_MAP_BTN_L);
nMapBtnL.x = nMap.x + timeLabelWidth;
nMapBtnL.y = nMap.y;
nMapBtnL.width = N_MAP_BUTTON_W;
nMapBtnL.height = nMap.height;
const nMapBtnR = this.getBounds(CanvasElement.N_MAP_BTN_R);
nMapBtnR.x = nMap.x + nMap.width - N_MAP_BUTTON_W - timeLabelWidth;
nMapBtnR.y = nMap.y;
nMapBtnR.width = N_MAP_BUTTON_W;
nMapBtnR.height = nMap.height;
// knots
const navMapChartStart = nMapBtnL.x + nMapBtnL.width;
const navMapChartWidth = nMapBtnR.x - navMapChartStart;
const navMapChartEnd = navMapChartStart + navMapChartWidth;
const minSliderW = this.config.components.navigationMap.minSliderWindowWidth;
const knotW = knotWidthFromConfig !== null && knotWidthFromConfig !== void 0 ? knotWidthFromConfig : N_MAP_KNOT_W;
const knotH = knotHeightFromConfig !== null && knotHeightFromConfig !== void 0 ? knotHeightFromConfig : nMap.height;
const minDistanceBetweenKnotsX = knotW + minSliderW;
// Left drag button
const knotL = this.getBounds(CanvasElement.N_MAP_KNOT_L);
knotL.x = navMapChartStart + navMapChartWidth * this.leftRatio;
// limit left knot to min distance from right border
knotL.x = Math.min(knotL.x, navMapChartEnd - minDistanceBetweenKnotsX);
knotL.y = knotY;
knotL.width = knotW;
knotL.height = knotH;
// Right drag button
const knotR = this.getBounds(CanvasElement.N_MAP_KNOT_R);
knotR.x = navMapChartStart + navMapChartWidth * this.rightRatio - N_MAP_KNOT_W;
// limit right knot to min distance from left border
knotR.x = Math.max(knotR.x, navMapChartStart + minDistanceBetweenKnotsX);
knotR.y = knotY;
knotR.width = knotW;
knotR.height = knotH;
const distanceDiff = minDistanceBetweenKnotsX - (knotR.x - knotL.x);
// if distance between knots is less than min distance - move left knot start
if (distanceDiff > 0) {
knotL.x -= distanceDiff;
}
// slider
const slider = this.getBounds(CanvasElement.N_MAP_SLIDER_WINDOW);
slider.x = knotL.x + knotL.width;
slider.y = nMap.y;
slider.width = knotR.x - slider.x;
slider.height = nMap.height;
// chart
const nMapChart = this.getBounds(CanvasElement.N_MAP_CHART);
nMapChart.x = navMapChartStart;
nMapChart.y = nMap.y;
nMapChart.width = navMapChartWidth;
nMapChart.height = nMap.height;
}
/**
* Checks if the volumes are set to be visible and if they should be shown in a separate pane
*
* @returns {boolean} - Returns true if the volumes are set to be visible and if they should be shown in a separate pane, otherwise returns false
*/
isVolumesInSeparatePane() {
return this.config.components.volumes.visible && this.config.components.volumes.showSeparately;
}
/**
* Gets current canvas element bounds.
* @param {string} el - CanvasElement.ELEMENT_NAME
* @return {Bounds} bounds of element
*/
getBounds(el) {
let bounds = this.bounds[el];
if (bounds === undefined) {
bounds = this.copyOf(DEFAULT_BOUNDS);
this.bounds[el] = bounds;
}
return bounds;
}
/**
* Returns the position of CANVAS on whole page.
* Use it to calculate relative coordinates for mouse movement hit test - crosstool for example.
*/
getCanvasOnPageLocation() {
return this.canvasOnPageLocation;
}
/**
* Gets current panes bounds.
*/
getBoundsPanes() {
return this.panesOrder.reduce((acc, uuid) => (Object.assign(Object.assign({}, acc), { [uuid]: this.bounds[CanvasElement.PANE_UUID(uuid)] })), {});
}
/**
* Gets hit-test fn for canvas element.
* @param {string} el - CanvasElement.ELEMENT_NAME
* @param {Object} options - An object containing options for the hit test.
* @param {number} [options.extensionX=0] - The amount of extension in the x-axis.
* @param {number} [options.extensionY=0] - The amount of extension in the y-axis.
* @param {boolean} [options.wholePage=false] - Whether to test against the whole page or just the bounds object.
* @return {HitBoundsTest} hit-test fn
*/
getBoundsHitTest(el, options = DEFAULT_HIT_TEST_OPTIONS) {
const { extensionX, extensionY, wholePage } = Object.assign(Object.assign({}, DEFAULT_HIT_TEST_OPTIONS), options);
if (wholePage) {
return (x, y) => {
const bounds = this.getBounds(el);
const hit = x > bounds.pageX - extensionX &&
x < bounds.pageX + bounds.width + extensionX &&
y > bounds.pageY - extensionY &&
y < bounds.pageY + bounds.height + extensionY;
return hit;
};
}
else {
return (x, y) => {
const bounds = this.getBounds(el);
const hit = x > bounds.x - extensionX &&
x < bounds.x + bounds.width + extensionX &&
y > bounds.y - extensionY &&
y < bounds.y + bounds.height + extensionY;
return hit;
};
}
}
/**
* Returns a function that tests if a given point is inside a given bounds object.
* @param {Bounds} bounds - The bounds object to test against.
* @param {Object} options - An object containing options for the hit test.
* @param {number} [options.extensionX=0] - The amount of extension in the x-axis.
* @param {number} [options.extensionY=0] - The amount of extension in the y-axis.
* @param {boolean} [options.wholePage=false] - Whether to test against the whole page or just the bounds object.
* @returns {HitBoundsTest} - A function that takes in x and y coordinates and returns true if the point is inside the bounds object.
*/
static hitTestOf(bounds, options = DEFAULT_HIT_TEST_OPTIONS) {
const { extensionX, extensionY, wholePage } = Object.assign(Object.assign({}, DEFAULT_HIT_TEST_OPTIONS), options);
if (!wholePage) {
return (x, y) => {
const hit = x > bounds.x - extensionX &&
x < bounds.x + bounds.width + extensionX &&
y > bounds.y - extensionY &&
y < bounds.y + bounds.height + extensionY;
return hit;
};
}
else {
return (x, y) => {
const hit = x > bounds.pageX - extensionX &&
x < bounds.pageX + bounds.width + extensionX &&
y > bounds.pageY - extensionY &&
y < bounds.pageY + bounds.height + extensionY;
return hit;
};
}
}
/**
* This method indicates whether it's possible to render the chart, in particular if its width and height > 0.
*/
isChartBoundsAvailable() {
const canvasBounds = this.getBounds(CanvasElement.CANVAS);
return canvasBounds.width > 0 && canvasBounds.height > 0;
}
isAllBoundsAvailable() {
return Object.values(this.bounds).every(el => el.width >= 0 && el.height >= 0);
}
/**
* Resizes a pane vertically.
* @param {string} uuid - The unique identifier of the pane.
* @param {number} y - The amount of pixels to resize the pane by.
* @returns {void}
*/
resizePaneVertically(uuid, y) {
const idx = this.panesOrder.indexOf(uuid);
const bounds = this.getBounds(CanvasElement.PANE_UUID_RESIZER(uuid));
this.doResizePaneVertically(idx, bounds.y - y);
this.barResizerChangedSubject.next();
}
/**
* Resizes a pane vertically based on the provided index and delta in pixels.
* @param {number} idx - The index of the pane to be resized.
* @param {number} yDeltaPixels - The delta in pixels to resize the pane.
* @returns {void}
*/
doResizePaneVertically(idx, yDeltaPixels) {
// get prev visible pane index
let prevVisiblePaneIdx = idx - 1;
const prevPaneUUID = this.panesOrder[prevVisiblePaneIdx];
if (this._graphsHeightRatio[prevPaneUUID] <= 0) {
for (let i = 0; i < idx; i++) {
if (this._graphsHeightRatio[this.panesOrder[i]] > 0) {
prevVisiblePaneIdx = i;
}
}
}
const allPanesHeight = this.getBounds(CanvasElement.ALL_PANES).height;
const minAllowedPaneHeight = DEFAULT_MIN_PANE_HEIGHT;
const resultPaneHeight = allPanesHeight * this.graphsHeightRatio[this.panesOrder[idx]];
const dependResultPaneHeight = allPanesHeight * this.graphsHeightRatio[this.panesOrder[prevVisiblePaneIdx]];
// check if changes fit allowed minimal pane height
const fitPane = resultPaneHeight + yDeltaPixels > minAllowedPaneHeight;
const fitDependPane = dependResultPaneHeight - yDeltaPixels > minAllowedPaneHeight;
if (fitPane && fitDependPane) {
// convert pixels to percent
const yDeltaPercent = yDeltaPixels / allPanesHeight;
this.graphsHeightRatio[this.panesOrder[idx]] += yDeltaPercent;
this.graphsHeightRatio[this.panesOrder[prevVisiblePaneIdx]] -= yDeltaPercent;
this.recalculateBounds();
}
}
/**
* Notifies all subscribers about bounds changes
* @function
* @name notifyBoundsSubscribers
* @returns {void}
*/
notifyBoundsSubscribers() {
Object.keys(this.boundsChangedSubscriptions).forEach(el => {
const subject = this.boundsChangedSubscriptions[el];
subject.next(this.getBounds(el));
});
this.boundsChangedSubject.next();
}
/**
* Subscribe to element bounds changes.
* Multiple subscriptions will share the same subject.
* @param el - element name
*/
observeBoundsChanged(el) {
let sub = this.boundsChangedSubscriptions[el];
if (!sub) {
sub = new BehaviorSubject(this.getBounds(el));
this.boundsChangedSubscriptions[el] = sub;
}
return sub.pipe(map(b => this.copyOf(b)), distinctUntilChanged((b1, b2) => this.sameBounds(b1, b2)));
}
/**
* Returns an observable that emits when the bounds of the object change.
* @returns {Observable} An observable that emits when the bounds of the object change.
*/
observeAnyBoundsChanged() {
return this.boundsChangedSubject.asObservable();
}
/**
* Creates a copy of the provided bounds object.
*
* @private
* @param {Bounds} bounds - The bounds object to be copied.
* @returns {Bounds} - A new bounds object with the same properties as the original.
*/
copyOf(bounds) {
return Object.assign({}, bounds);
}
/**
* Copies the values of the `from` object to the `to` object.
* @param {Bounds} from - The object to copy the values from.
* @param {Bounds} to - The object to copy the values to.
*/
copyBounds(from, to) {
to.x = from.x;
to.y = from.y;
to.width = from.width;
to.height = from.height;
}
/**
* Checks if two Bounds objects have the same x, y, width and height values.
* @param {Bounds} b1 - The first Bounds object to compare.
* @param {Bounds} b2 - The second Bounds object to compare.
* @returns {boolean} - Returns true if both Bounds objects have the same x, y, width and height values, otherwise returns false.
*/
sameBounds(b1, b2) {
return b1.x === b2.x && b1.y === b2.y && b1.width === b2.width && b1.height === b2.height;
}
/**
* Applies default bounds to the provided bounds object.
* @param {Bounds} bounds - The bounds object to apply default bounds to.
* @returns {void}
* @private
*/
applyDefaultBounds(bounds) {
this.copyBounds(DEFAULT_BOUNDS, bounds);
}
/**
* Sets the main candle series for the CanvasBoundsContainer.
* @param {CandleSeriesModel} candleSeries - The candle series to be set as the main candle series.
* @returns {void}
* @deprecated This method should be removed as candleSeries is not a part of CanvasBoundsContainer.
*/
setMainCandleSeries(candleSeries) {
this.mainCandleSeries = candleSeries;
}
/**
* Returns the effective width of the Y axis.
*
* @function
* @name getEffectiveYAxisWidth
* @returns {number} The effective width of the Y axis.
*/
getEffectiveYAxisWidth() {
const yAxis = this.getBounds(CanvasElement.PANE_UUID_Y_AXIS(CHART_UUID));
return yAxis.width;
}
/**
* Returns the effective width of the chart.
*
* @function
* @returns {number} The effective width of the chart.
*/
getEffectiveChartWidth() {
const chart = this.getBounds(CanvasElement.PANE_UUID(CHART_UUID));
return chart.width;
}
/**
* Returns the effective height of the chart.
*
* @returns {number} The effective height of the chart.
*/
getEffectiveChartHeight() {
const chart = this.getBounds(CanvasElement.PANE_UUID(CHART_UUID));
return chart.height;
}
}
// paneCounter=chartHeightRatio: 0=1, 1=0.8, 2=0.6, 3=0.5, 4=0.4, 5=0.4
// ratios requirements table: https://confluence.in.devexperts.com/display/UI/Chart+Navigation#ChartNavigation-2Graphsadjustablesizes
const DEFAULT_RATIOS = {
0: 1,
1: 0.8,
2: 0.6,
3: 0.5,
4: 0.4,
5: 0.4,
6: 0.4,
};
// NOTE: pec stands for panes except main chart
export const getHeightRatios = (pecLength) => {
var _a;
const chartHeightRatio = (_a = DEFAULT_RATIOS[pecLength]) !== null && _a !== void 0 ? _a : 0.4;
const singlePecHeightRatio = (1 - chartHeightRatio) / pecLength;
return [chartHeightRatio, singlePecHeightRatio];
};
export const isInBounds = (point, bounds) => point.x > bounds.x && point.x < bounds.x + bounds.width && point.y > bounds.y && point.y < bounds.y + bounds.height;
export const isInVerticalBounds = (y, bounds) => y > bounds.y && y < bounds.y + bounds.height;
const upsertBounds = (storage, uuid, x, y, width, height, canvasOnPageLocation) => {
const existing = storage[uuid];
if (existing) {
existing.x = x;
existing.y = y;
existing.pageX = x + canvasOnPageLocation.x;
existing.pageY = y + canvasOnPageLocation.y;
existing.width = width;
existing.height = height;
return existing;
}
const newly = {
x,
y,
pageX: x + canvasOnPageLocation.x,
pageY: y + canvasOnPageLocation.y,
width,
height,
};
storage[uuid] = newly;
return newly;
};
export const limitYToBounds = (y, bounds) => Math.min(Math.max(y, bounds.y), bounds.y + bounds.height);
export const DEFAULT_HIT_TEST_OPTIONS = {
extensionX: 0,
extensionY: 0,
wholePage: false,
};
export const areBoundsChanged = (prev, next) => {
return prev.width === next.width && prev.height === next.height;
};