Загрузка данных
import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { EventManager } from '@core';
import { DOMModel } from '@core/DOMModel';
import { Drawing } from '@core/Drawings';
import { ISeriesDrawing } from '@core/Drawings/common';
import { Ray } from '@core/Drawings/ray';
import { TrendLine } from '@core/Drawings/trendLine';
import { VolumeProfile } from '@core/Drawings/volumeProfile';
import { drawingLabelById, DrawingsNames } from '@src/constants';
import { AxisLine } from '@src/core/Drawings/axisLine';
import { Diapson } from '@src/core/Drawings/diapson';
import { Rectangle } from '@src/core/Drawings/rectangle';
import { Ruler } from '@src/core/Drawings/ruler';
import { SliderPosition } from '@src/core/Drawings/sliderPosition';
import { Traectory } from '@src/core/Drawings/traectory';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool } from '@src/types';
interface DrawingsManagerParams {
eventManager: EventManager;
mainSeries$: Observable<SeriesStrategies | null>;
lwcChart: IChartApi;
// chartOptions?: ChartTypeOptions;
DOM: DOMModel;
container: HTMLElement;
}
export interface DrawingParams {
chart: IChartApi;
series: ISeriesApi<SeriesType>;
eventManager: EventManager;
container: HTMLElement;
removeSelf: () => void;
}
export interface DrawingConfig {
singleInstance?: boolean;
construct: (params: DrawingParams) => ISeriesDrawing;
}
export const drawingsMap: Record<DrawingsNames, DrawingConfig> = {
[DrawingsNames.trendLine]: {
construct: ({ chart, series, container, eventManager, removeSelf }) => {
return new TrendLine(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.ray]: {
construct: ({ chart, series, container, eventManager, removeSelf }) => {
return new Ray(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.horizontalLine]: {
construct: ({ chart, series, eventManager, container, removeSelf }) => {
return new AxisLine(chart, series, {
direction: 'horizontal',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.horizontalRay]: {
construct: ({ chart, series, container, eventManager, removeSelf }) => {
return new TrendLine(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.verticalLine]: {
construct: ({ chart, series, eventManager, container, removeSelf }) => {
return new AxisLine(chart, series, {
direction: 'vertical',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.sliderLong]: {
construct: ({ chart, series, eventManager, container, removeSelf }) => {
return new SliderPosition(chart, series, {
side: 'long',
container,
formatObservable: eventManager.getChartOptionsModel(),
resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
removeSelf,
});
},
},
[DrawingsNames.sliderShort]: {
construct: ({ chart, series, eventManager, container, removeSelf }) => {
return new SliderPosition(chart, series, {
side: 'short',
container,
formatObservable: eventManager.getChartOptionsModel(),
resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
removeSelf,
});
},
},
[DrawingsNames.diapsonDates]: {
construct: ({ chart, series, container, eventManager, removeSelf }) => {
return new Diapson(chart, series, {
rangeMode: 'date',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.diapsonPrices]: {
construct: ({ chart, series, container, eventManager, removeSelf }) => {
return new Diapson(chart, series, {
rangeMode: 'price',
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.fixedProfile]: {
construct: ({ chart, series, container, eventManager, removeSelf }) => {
return new VolumeProfile(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.rectangle]: {
construct: ({ chart, series, eventManager, container, removeSelf }) => {
return new Rectangle(chart, series, {
container,
formatObservable: eventManager.getChartOptionsModel(),
removeSelf,
});
},
},
[DrawingsNames.ruler]: {
singleInstance: true,
construct: ({ chart, series, eventManager, removeSelf }) => {
return new Ruler(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
resetTriggers: [eventManager.getTimeframeObs(), eventManager.getInterval()],
removeSelf,
});
},
},
[DrawingsNames.traectory]: {
construct: ({ chart, series, eventManager, removeSelf, container }) => {
return new Traectory(chart, series, {
formatObservable: eventManager.getChartOptionsModel(),
container,
removeSelf,
});
},
},
};
export class DrawingsManager {
private eventManager: EventManager;
private lwcChart: IChartApi;
private drawingsQty = 0; // todo: replace with hash
private DOM: DOMModel;
private container: HTMLElement;
private mainSeries: SeriesStrategies | null = null;
private subscriptions = new Subscription();
private drawings$ = new BehaviorSubject<Drawing[]>([]);
private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
private endlessMode$ = new BehaviorSubject(false);
private recreateScheduled = false;
constructor({ eventManager, mainSeries$, lwcChart, DOM, container }: DrawingsManagerParams) {
this.DOM = DOM;
this.eventManager = eventManager;
this.lwcChart = lwcChart;
this.container = container;
this.subscriptions.add(
mainSeries$.subscribe((s) => {
if (!s) return;
this.mainSeries = s;
this.drawings$.value.forEach((drawing) => drawing.rebind(s));
}),
);
window.addEventListener('pointerup', this.updateActiveTool);
this.container.addEventListener('click', this.updateActiveTool);
}
private updateActiveTool = (): void => {
const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());
if (hasPendingDrawing) {
return;
}
const activeTool = this.activeTool$.value;
if (activeTool !== 'crosshair' && this.endlessMode$.value) {
if (this.recreateScheduled) {
return;
}
this.recreateScheduled = true;
queueMicrotask(() => {
this.recreateScheduled = false;
const currentTool = this.activeTool$.value;
const hasPendingAfterTick = this.drawings$.value.some((drawing) => drawing.isCreationPending());
if (currentTool === 'crosshair') {
return;
}
if (!this.endlessMode$.value) {
return;
}
if (hasPendingAfterTick) {
return;
}
this.createDrawing(currentTool);
});
return;
}
this.activeTool$.next('crosshair');
};
private removeDrawing = (id: string) => {
const drawings = this.drawings$.value;
const objectToRemove = drawings.find((d) => d.id === id);
if (!objectToRemove) {
return;
}
this.removeDrawings([objectToRemove]);
};
private removeDrawingsByName(name: string): void {
const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.id.startsWith(name));
this.removeDrawings(drawingsToRemove);
}
private removePendingDrawings(): void {
const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.isCreationPending());
this.removeDrawings(drawingsToRemove);
}
private removeDrawings(drawingsToRemove: Drawing[]): void {
if (!drawingsToRemove.length) {
return;
}
drawingsToRemove.forEach((drawing) => {
drawing.destroy();
this.DOM.removeEntity(drawing);
});
this.drawings$.next(this.drawings$.value.filter((drawing) => !drawingsToRemove.includes(drawing)));
this.updateActiveTool();
}
public addDrawingForce = (name: DrawingsNames) => {
this.removePendingDrawings();
this.activeTool$.next(name);
this.createDrawing(name);
};
public createDrawing = (name: DrawingsNames) => {
if (!this.mainSeries) {
throw new Error('[Drawings] main series is not defined');
}
this.removePendingDrawings();
this.activeTool$.next(name);
const config = drawingsMap[name];
if (config.singleInstance) {
this.removeDrawingsByName(name);
}
const drawingId = `${name}${this.drawingsQty}`;
const construct = (chart: IChartApi, series: ISeriesApi<SeriesType>) =>
config.construct({
chart,
series,
eventManager: this.eventManager,
container: this.container,
removeSelf: () => this.removeDrawing(drawingId),
});
const drawing = (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
new Drawing({
lwcChart: this.lwcChart,
mainSeries: this.mainSeries as SeriesStrategies,
id: drawingId,
name: drawingLabelById[name],
onDelete: this.removeDrawing,
zIndex,
moveDown,
moveUp,
construct,
});
const entity = this.DOM.setEntity<Drawing>(drawing);
this.drawingsQty++;
this.drawings$.next([...this.drawings$.value, entity]);
};
public setEndlessDrawingMode = (value: boolean) => {
this.endlessMode$.next(value);
};
public isEndlessDrawingsMode(): Observable<boolean> {
return this.endlessMode$.asObservable();
}
public getActiveTool(): Observable<ActiveDrawingTool> {
return this.activeTool$.asObservable();
}
public activateCrosshair(): void {
this.removePendingDrawings();
this.activeTool$.next('crosshair');
}
public entities(): Observable<Drawing[]> {
return this.drawings$.asObservable();
}
public getDrawings() {}
public hideAll() {}
public destroy() {
window.removeEventListener('pointerup', this.updateActiveTool);
this.container.removeEventListener('click', this.updateActiveTool);
this.subscriptions.unsubscribe();
this.drawings$.complete();
this.activeTool$.complete();
this.endlessMode$.complete();
}
}