Загрузка данных
import { PriceScaleMode } from 'lightweight-charts';
import type { Pane } from '@core/Pane';
export interface PriceScaleTarget {
paneId: number;
priceScaleId: string;
}
export interface PriceScaleModeSnapshot extends PriceScaleTarget {
mode: PriceScaleMode;
}
interface PriceScaleModeControllerParams {
getPaneById: (paneId: number) => Pane | undefined;
initialModes?: PriceScaleModeSnapshot[];
onChange?: () => void;
}
export class PriceScaleModeController {
private readonly getPaneById: (paneId: number) => Pane | undefined;
private readonly onChange?: () => void;
private readonly savedModes = new Map<number, Map<string, PriceScaleMode>>();
private readonly overrideModes = new Map<number, Map<string, PriceScaleMode>>();
constructor({ getPaneById, initialModes = [], onChange }: PriceScaleModeControllerParams) {
this.getPaneById = getPaneById;
this.onChange = onChange;
this.setSnapshot(initialModes);
}
public getActiveMode(target: PriceScaleTarget): PriceScaleMode {
return this.getOverrideMode(target) ?? this.getSavedMode(target);
}
public getSavedMode(target: PriceScaleTarget): PriceScaleMode {
return this.savedModes.get(target.paneId)?.get(target.priceScaleId) ?? PriceScaleMode.Normal;
}
public setSavedMode(target: PriceScaleTarget, mode: PriceScaleMode): void {
if (this.getSavedMode(target) === mode) {
return;
}
this.setMode(this.savedModes, target, mode);
this.applyMode(target);
this.onChange?.();
}
public toggleSavedMode(target: PriceScaleTarget, mode: PriceScaleMode): void {
this.setSavedMode(target, this.getSavedMode(target) === mode ? PriceScaleMode.Normal : mode);
}
public setOverrideMode(target: PriceScaleTarget, mode: PriceScaleMode): void {
if (mode === PriceScaleMode.Normal) {
this.clearOverrideMode(target);
return;
}
if (this.getOverrideMode(target) === mode) {
return;
}
this.setMode(this.overrideModes, target, mode);
this.applyMode(target);
this.onChange?.();
}
public clearOverrideMode(target: PriceScaleTarget): void {
if (!this.getOverrideMode(target)) {
return;
}
this.removeMode(this.overrideModes, target);
this.applyMode(target);
this.onChange?.();
}
public applyAll(): void {
this.getTargets(this.savedModes).forEach((target) => this.applyMode(target));
this.getTargets(this.overrideModes).forEach((target) => this.applyMode(target));
}
public getSnapshot(): PriceScaleModeSnapshot[] {
const snapshot: PriceScaleModeSnapshot[] = [];
this.savedModes.forEach((paneModes, paneId) => {
if (!this.getPaneById(paneId)) {
return;
}
paneModes.forEach((mode, priceScaleId) => {
snapshot.push({
paneId,
priceScaleId,
mode,
});
});
});
return snapshot;
}
public setSnapshot(snapshot: PriceScaleModeSnapshot[]): void {
this.savedModes.clear();
snapshot.forEach(({ paneId, priceScaleId, mode }) => {
this.setMode(this.savedModes, { paneId, priceScaleId }, mode);
});
}
public destroy(): void {
this.savedModes.clear();
this.overrideModes.clear();
}
private getOverrideMode(target: PriceScaleTarget): PriceScaleMode | undefined {
return this.overrideModes.get(target.paneId)?.get(target.priceScaleId);
}
private setMode(
modes: Map<number, Map<string, PriceScaleMode>>,
target: PriceScaleTarget,
mode: PriceScaleMode,
): void {
if (mode === PriceScaleMode.Normal) {
this.removeMode(modes, target);
return;
}
const paneModes = modes.get(target.paneId) ?? new Map<string, PriceScaleMode>();
paneModes.set(target.priceScaleId, mode);
modes.set(target.paneId, paneModes);
}
private removeMode(modes: Map<number, Map<string, PriceScaleMode>>, target: PriceScaleTarget): void {
const paneModes = modes.get(target.paneId);
paneModes?.delete(target.priceScaleId);
if (paneModes?.size === 0) {
modes.delete(target.paneId);
}
}
private getTargets(modes: Map<number, Map<string, PriceScaleMode>>): PriceScaleTarget[] {
const targets: PriceScaleTarget[] = [];
modes.forEach((paneModes, paneId) => {
paneModes.forEach((_, priceScaleId) => {
targets.push({ paneId, priceScaleId });
});
});
return targets;
}
private applyMode(target: PriceScaleTarget): void {
const pane = this.getPaneById(target.paneId);
if (!pane) {
return;
}
pane.getPriceScale(target.priceScaleId).applyOptions({
mode: this.getActiveMode(target),
});
}
}
import { IPriceScaleApi, PriceScaleMode } from 'lightweight-charts';
import { PriceScaleControls } from '@components/PriceScaleControls';
import { ReactRenderer } from '@core/ReactRenderer';
import { Direction } from '@src/types';
import type { PriceScaleTarget } from './controller';
interface PriceScaleControlsControllerParams {
getPaneElement: () => HTMLElement | null;
getPriceScale: (priceScaleId: Direction) => IPriceScaleApi;
getPriceScaleTarget: (priceScaleId: Direction) => PriceScaleTarget;
getSavedPriceScaleMode: (target: PriceScaleTarget) => PriceScaleMode;
togglePriceScaleMode: (target: PriceScaleTarget, mode: PriceScaleMode) => void;
}
export class PriceScaleControlsController {
private readonly getPaneElement: () => HTMLElement | null;
private readonly getPriceScale: (priceScaleId: Direction) => IPriceScaleApi;
private readonly getPriceScaleTarget: (priceScaleId: Direction) => PriceScaleTarget;
private readonly getSavedPriceScaleMode: (target: PriceScaleTarget) => PriceScaleMode;
private readonly togglePriceScaleMode: (target: PriceScaleTarget, mode: PriceScaleMode) => void;
private readonly container: HTMLElement;
private readonly renderer: ReactRenderer;
private paneElement: HTMLElement | null = null;
private hoveredPriceScaleId: Direction | null = null;
constructor({
getPaneElement,
getPriceScale,
getPriceScaleTarget,
getSavedPriceScaleMode,
togglePriceScaleMode,
}: PriceScaleControlsControllerParams) {
this.getPaneElement = getPaneElement;
this.getPriceScale = getPriceScale;
this.getPriceScaleTarget = getPriceScaleTarget;
this.getSavedPriceScaleMode = getSavedPriceScaleMode;
this.togglePriceScaleMode = togglePriceScaleMode;
this.container = document.createElement('div');
this.container.className = 'moex-price-scale-controls';
this.container.style.position = 'absolute';
this.container.style.bottom = 'var(--space-0500)';
this.container.style.zIndex = '10';
this.container.style.pointerEvents = 'none';
this.renderer = new ReactRenderer(this.container);
}
public mount(): void {
const paneElement = this.getPaneElement();
if (!paneElement) {
return;
}
if (this.paneElement === paneElement && this.container.parentElement === paneElement) {
this.update();
return;
}
this.unmount();
this.paneElement = paneElement;
this.paneElement.style.position = 'relative';
this.paneElement.appendChild(this.container);
this.paneElement.addEventListener('pointermove', this.handlePointerMove);
this.paneElement.addEventListener('pointerleave', this.handlePointerLeave);
this.update();
}
public update(): void {
if (!this.hoveredPriceScaleId) {
this.hide();
return;
}
this.render(this.hoveredPriceScaleId);
}
public resetAutoScale(): void {
this.setAutoScale(Direction.Left);
this.setAutoScale(Direction.Right);
this.update();
}
public destroy(): void {
this.unmount();
this.renderer.destroy();
this.container.remove();
}
private handlePointerMove = (event: PointerEvent): void => {
if (!this.paneElement) {
return;
}
const paneRect = this.paneElement.getBoundingClientRect();
const pointerX = event.clientX - paneRect.left;
const nextPriceScaleId = this.getHoveredPriceScaleId(pointerX, paneRect.width);
if (this.hoveredPriceScaleId === nextPriceScaleId) {
if (nextPriceScaleId) {
this.updatePosition(nextPriceScaleId);
}
return;
}
this.hoveredPriceScaleId = nextPriceScaleId;
this.update();
};
private handlePointerLeave = (): void => {
this.hoveredPriceScaleId = null;
this.update();
};
private render(priceScaleId: Direction): void {
if (!this.isPriceScaleAvailable(priceScaleId)) {
this.hide();
return;
}
this.updatePosition(priceScaleId);
const target = this.getPriceScaleTarget(priceScaleId);
const savedMode = this.getSavedPriceScaleMode(target);
this.renderer.renderComponent(
<PriceScaleControls
isAutoScale={this.isAutoScale(priceScaleId)}
isLogarithmic={savedMode === PriceScaleMode.Logarithmic}
onAutoScale={() => {
this.setAutoScale(priceScaleId);
this.update();
}}
onToggleLogarithmic={() => {
this.togglePriceScaleMode(target, PriceScaleMode.Logarithmic);
this.update();
}}
onRefresh={() => {
this.update();
}}
/>,
);
this.container.classList.add('moex-price-scale-controls_visible');
}
private hide(): void {
this.container.classList.remove('moex-price-scale-controls_visible');
}
private getHoveredPriceScaleId(pointerX: number, paneWidth: number): Direction | null {
const leftWidth = this.getPriceScaleWidth(Direction.Left);
const rightWidth = this.getPriceScaleWidth(Direction.Right);
if (this.isPriceScaleAvailable(Direction.Left) && pointerX >= 0 && pointerX <= leftWidth) {
return Direction.Left;
}
if (this.isPriceScaleAvailable(Direction.Right) && pointerX >= paneWidth - rightWidth && pointerX <= paneWidth) {
return Direction.Right;
}
return null;
}
private updatePosition(priceScaleId: Direction): void {
const width = this.getPriceScaleWidth(priceScaleId);
this.container.style.width = `${width}px`;
if (priceScaleId === Direction.Left) {
this.container.style.left = '0';
this.container.style.right = '';
return;
}
this.container.style.left = '';
this.container.style.right = '0';
}
private setAutoScale(priceScaleId: Direction): void {
this.getPriceScale(priceScaleId).setAutoScale(true);
}
private isAutoScale(priceScaleId: Direction): boolean {
return this.getPriceScale(priceScaleId).options().autoScale ?? true;
}
private isPriceScaleAvailable(priceScaleId: Direction): boolean {
const priceScale = this.getPriceScale(priceScaleId);
const options = priceScale.options();
return options.visible !== false && priceScale.width() > 0;
}
private getPriceScaleWidth(priceScaleId: Direction): number {
return this.getPriceScale(priceScaleId).width();
}
private unmount(): void {
if (!this.paneElement) {
return;
}
this.paneElement.removeEventListener('pointermove', this.handlePointerMove);
this.paneElement.removeEventListener('pointerleave', this.handlePointerLeave);
this.paneElement = null;
}
}
export { PriceScaleModeController } from './controller';
export { PriceScaleControlsController } from './controls';
export type { PriceScaleModeSnapshot, PriceScaleTarget } from './controller';
.moex-price-scale-controls {
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.moex-price-scale-controls_visible {
opacity: 1;
}
.moex-price-scale-controls_visible .moex-price-scale-controls-content {
pointer-events: auto;
}