Загрузка данных
import {
IChartApi,
IPaneApi,
IPriceScaleApi,
PriceScaleMode,
Time,
} from 'lightweight-charts';
import { Direction } from '@src/types';
import type {
PriceScaleSide,
PriceScaleSnapshot,
} from '@src/types/snapshot';
interface PriceScaleParams {
paneId: number;
side: PriceScaleSide;
chart: IChartApi;
pane: IPaneApi<Time>;
initialMode?: PriceScaleMode;
hasVisibleSeriesData: () => boolean;
}
const PRICE_SCALE_MINIMUM_WIDTH = 72;
export class PriceScale {
public readonly paneId: number;
public readonly side: PriceScaleSide;
private readonly chart: IChartApi;
private readonly pane: IPaneApi<Time>;
private readonly hasVisibleSeriesDataCallback: () => boolean;
private mode: PriceScaleMode;
private autoScaleEnabled = true;
private visible: boolean;
private initialized = false;
constructor({
paneId,
side,
chart,
pane,
initialMode = PriceScaleMode.Normal,
hasVisibleSeriesData,
}: PriceScaleParams) {
this.paneId = paneId;
this.side = side;
this.chart = chart;
this.pane = pane;
this.mode = initialMode;
this.visible = side === Direction.Right;
this.hasVisibleSeriesDataCallback = hasVisibleSeriesData;
}
public initialize(): void {
if (this.initialized) {
return;
}
const priceScale = this.getPriceScaleApi();
const options = priceScale.options();
this.autoScaleEnabled = options.autoScale ?? true;
this.initialized = true;
priceScale.applyOptions({
mode: this.mode,
autoScale: this.autoScaleEnabled,
visible: this.visible,
borderVisible: false,
minimumWidth: Math.max(
options.minimumWidth ?? 0,
PRICE_SCALE_MINIMUM_WIDTH,
),
});
}
public getMode(): PriceScaleMode {
return this.mode;
}
public setMode(mode: PriceScaleMode): void {
if (this.mode === mode) {
return;
}
this.mode = mode;
if (!this.initialized) {
return;
}
this.getPriceScaleApi().applyOptions({
mode,
autoScale: this.autoScaleEnabled,
});
}
public toggleLogarithmic(): void {
const nextMode =
this.mode === PriceScaleMode.Logarithmic
? PriceScaleMode.Normal
: PriceScaleMode.Logarithmic;
this.setMode(nextMode);
}
public isAutoScaleEnabled(): boolean {
return this.autoScaleEnabled;
}
public toggleAutoScale(): void {
this.autoScaleEnabled = !this.autoScaleEnabled;
if (this.initialized) {
this.getPriceScaleApi().setAutoScale(
this.autoScaleEnabled,
);
}
}
public enableAutoScale(): void {
if (this.autoScaleEnabled) {
return;
}
this.autoScaleEnabled = true;
if (this.initialized) {
this.getPriceScaleApi().setAutoScale(true);
}
}
public isVisible(): boolean {
return this.visible;
}
public setVisible(visible: boolean): void {
const visibilityChanged = this.visible !== visible;
this.visible = visible;
if (!this.initialized || !visibilityChanged) {
return;
}
this.getPriceScaleApi().applyOptions({
visible,
borderVisible: false,
});
}
public getWidth(): number {
if (
!this.initialized ||
!this.visible ||
this.pane.paneIndex() < 0
) {
return 0;
}
try {
return this.getPriceScaleApi().width();
} catch {
return 0;
}
}
public hasVisibleSeriesData(): boolean {
return this.hasVisibleSeriesDataCallback();
}
public getSnapshot(): PriceScaleSnapshot {
return {
side: this.side,
mode: this.mode,
};
}
private getPriceScaleApi(): IPriceScaleApi {
return this.chart.priceScale(
this.side,
this.pane.paneIndex(),
);
}
}
import { PriceScaleMode } from 'lightweight-charts';
import { PriceScaleControls as PriceScaleControlsView } from '@components/PriceScaleControls';
import { ReactRenderer } from '@core/ReactRenderer';
import { Direction } from '@src/types';
import { PriceScale } from './PriceScale';
interface PriceScaleControlsParams {
chartContainer: HTMLElement;
leftPriceScale: PriceScale;
rightPriceScale: PriceScale;
onPriceScaleChange: () => void;
}
export class PriceScaleControls {
private readonly chartContainer: HTMLElement;
private readonly leftPriceScale: PriceScale;
private readonly rightPriceScale: PriceScale;
private readonly onPriceScaleChange: () => void;
private readonly container: HTMLElement;
private readonly renderer: ReactRenderer;
private paneElement: HTMLElement | null = null;
private hoveredPriceScale: PriceScale | null = null;
constructor({
chartContainer,
leftPriceScale,
rightPriceScale,
onPriceScaleChange,
}: PriceScaleControlsParams) {
this.chartContainer = chartContainer;
this.leftPriceScale = leftPriceScale;
this.rightPriceScale = rightPriceScale;
this.onPriceScaleChange = onPriceScaleChange;
this.container = document.createElement('div');
this.container.className =
'moex-price-scale-controls';
this.container.style.position = 'absolute';
this.container.style.zIndex = '10';
this.container.style.pointerEvents = 'none';
this.renderer = new ReactRenderer(this.container);
}
public mount(paneElement: HTMLElement): void {
if (
this.paneElement === paneElement &&
this.container.parentElement ===
this.chartContainer
) {
this.refresh();
return;
}
this.unmount();
this.paneElement = paneElement;
this.chartContainer.appendChild(this.container);
this.chartContainer.addEventListener(
'pointermove',
this.handlePointerMove,
);
this.chartContainer.addEventListener(
'pointerleave',
this.handlePointerLeave,
);
this.refresh();
}
public refresh(): void {
if (!this.hoveredPriceScale) {
this.hide();
return;
}
this.render(this.hoveredPriceScale);
}
public destroy(): void {
this.unmount();
this.renderer.destroy();
this.container.remove();
}
private handlePointerMove = (
event: PointerEvent,
): void => {
if (!this.paneElement) {
return;
}
const chartRect =
this.chartContainer.getBoundingClientRect();
const paneRect =
this.paneElement.getBoundingClientRect();
const isInsidePane =
event.clientY >= paneRect.top &&
event.clientY < paneRect.bottom;
const nextPriceScale = isInsidePane
? this.getHoveredPriceScale(
event.clientX - chartRect.left,
chartRect.width,
)
: null;
if (
this.hoveredPriceScale === nextPriceScale
) {
if (nextPriceScale) {
this.updatePosition(nextPriceScale);
}
return;
}
this.hoveredPriceScale = nextPriceScale;
this.refresh();
};
private handlePointerLeave = (): void => {
this.hoveredPriceScale = null;
this.refresh();
};
private render(priceScale: PriceScale): void {
const width = this.getAvailableWidth(priceScale);
if (width === 0) {
this.hide();
return;
}
this.updatePosition(priceScale, width);
this.renderer.renderComponent(
<PriceScaleControlsView
isAutoScale={
priceScale.isAutoScaleEnabled()
}
isLogarithmic={
priceScale.getMode() ===
PriceScaleMode.Logarithmic
}
onToggleAutoScale={() => {
priceScale.toggleAutoScale();
this.refresh();
this.onPriceScaleChange();
}}
onToggleLogarithmic={() => {
priceScale.toggleLogarithmic();
this.refresh();
this.onPriceScaleChange();
}}
/>,
);
this.container.classList.add(
'moex-price-scale-controls_visible',
);
}
private hide(): void {
this.container.classList.remove(
'moex-price-scale-controls_visible',
);
}
private getHoveredPriceScale(
pointerX: number,
chartWidth: number,
): PriceScale | null {
const leftWidth = this.getAvailableWidth(
this.leftPriceScale,
);
if (
leftWidth > 0 &&
pointerX >= 0 &&
pointerX <= leftWidth
) {
return this.leftPriceScale;
}
const rightWidth = this.getAvailableWidth(
this.rightPriceScale,
);
if (
rightWidth > 0 &&
pointerX >= chartWidth - rightWidth &&
pointerX <= chartWidth
) {
return this.rightPriceScale;
}
return null;
}
private getAvailableWidth(
priceScale: PriceScale,
): number {
if (
!priceScale.isVisible() ||
!priceScale.hasVisibleSeriesData()
) {
return 0;
}
return priceScale.getWidth();
}
private updatePosition(
priceScale: PriceScale,
width = priceScale.getWidth(),
): void {
if (!this.paneElement || width === 0) {
return;
}
const chartRect =
this.chartContainer.getBoundingClientRect();
const paneRect =
this.paneElement.getBoundingClientRect();
const bottomOffset = Math.max(
0,
chartRect.bottom - paneRect.bottom,
);
this.container.style.width = `${width}px`;
this.container.style.bottom =
`${bottomOffset}px`;
if (priceScale.side === Direction.Left) {
this.container.style.left = '0';
this.container.style.right = '';
return;
}
this.container.style.left = '';
this.container.style.right = '0';
}
private unmount(): void {
this.chartContainer.removeEventListener(
'pointermove',
this.handlePointerMove,
);
this.chartContainer.removeEventListener(
'pointerleave',
this.handlePointerLeave,
);
this.paneElement = null;
this.hoveredPriceScale = null;
this.hide();
}
}