Загрузка данных
import { getThemeStore } from '@src/theme';
import { IndicatorConfig, IndicatorSerie } from '@src/types';
type SeriesOptionsWithColor = {
color?: string;
visible?: boolean;
};
export function createIndicatorConfigWithUniqueColors(
config: IndicatorConfig,
usedColors: readonly string[],
): IndicatorConfig {
const reservedColors = new Set(usedColors.map(normalizeColor));
return {
...config,
settings: config.settings ? [...config.settings] : undefined,
seriesLabels: config.seriesLabels ? { ...config.seriesLabels } : undefined,
series: config.series.map((serie) => {
const clonedSerie = cloneIndicatorSerie(serie);
if (!isVisibleLineSerie(serie)) {
return clonedSerie;
}
const currentColor = getSerieColor(serie);
const nextColor =
currentColor && !reservedColors.has(normalizeColor(currentColor))
? currentColor
: getNextIndicatorColor(Array.from(reservedColors));
reservedColors.add(normalizeColor(nextColor));
return {
...clonedSerie,
seriesOptions: {
...clonedSerie.seriesOptions,
color: nextColor,
},
};
}),
};
}
export function getIndicatorConfigColors(config: IndicatorConfig): string[] {
return config.series.reduce<string[]>((colors, serie) => {
if (!isVisibleLineSerie(serie)) {
return colors;
}
const color = getSerieColor(serie);
if (color) {
colors.push(color);
}
return colors;
}, []);
}
function getNextIndicatorColor(usedColors: readonly string[]): string {
const normalizedUsedColors = new Set(usedColors.map(normalizeColor));
const freeColor = getInitialIndicatorColors().find((color) => !normalizedUsedColors.has(normalizeColor(color)));
if (freeColor) {
return freeColor;
}
let index = normalizedUsedColors.size;
let generatedColor = generateColorByIndex(index);
while (normalizedUsedColors.has(normalizeColor(generatedColor))) {
index += 1;
generatedColor = generateColorByIndex(index);
}
return generatedColor;
}
function getInitialIndicatorColors(): string[] {
const { colors } = getThemeStore();
return getUniqueColors([
colors.indicatorLineSma,
colors.indicatorLineEma,
colors.chartCandleWickUp,
'#2962FF',
'#FF6D00',
'#00C853',
'#D500F9',
'#FF1744',
'#00B8D4',
'#FFD600',
'#7C4DFF',
'#64DD17',
'#FF4081',
]);
}
function getUniqueColors(colors: readonly string[]): string[] {
const uniqueColors = new Set<string>();
return colors.filter((color) => {
const normalizedColor = normalizeColor(color);
if (uniqueColors.has(normalizedColor)) {
return false;
}
uniqueColors.add(normalizedColor);
return true;
});
}
function generateColorByIndex(index: number): string {
const goldenAngle = 137.508;
const hue = Math.round((index * goldenAngle) % 360);
return `hsl(${hue}deg 72% 56%)`;
}
function isVisibleLineSerie(serie: IndicatorSerie): boolean {
const seriesOptions = serie.seriesOptions as SeriesOptionsWithColor | undefined;
return serie.name === 'Line' && seriesOptions?.visible !== false;
}
function getSerieColor(serie: IndicatorSerie): string | null {
const seriesOptions = serie.seriesOptions as SeriesOptionsWithColor | undefined;
const color = seriesOptions?.color;
return typeof color === 'string' && color.trim() ? color : null;
}
function cloneIndicatorSerie(serie: IndicatorSerie): IndicatorSerie {
return {
...serie,
seriesOptions: serie.seriesOptions ? { ...serie.seriesOptions } : undefined,
priceScaleOptions: serie.priceScaleOptions ? { ...serie.priceScaleOptions } : undefined,
};
}
function normalizeColor(color: string): string {
return color.trim().toLowerCase();
}
import { IChartApi } from 'lightweight-charts';
import { BehaviorSubject, map, Observable } from 'rxjs';
import { DataSource } from '@core/DataSource';
import { DOMModel } from '@core/DOMModel';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { PaneManager } from '@core/PaneManager';
import { DOMObject } from '@src/core/DOMObject';
import { indicatorsMap as indicatorsConfigMap } from '@src/core/Indicators';
import {
createIndicatorConfigWithUniqueColors,
getIndicatorConfigColors,
} from '@src/core/Indicators/colors';
import { ChartTypeOptions, IndicatorConfig } from '@src/types';
import { IndicatorSnapshot } from '@src/types/snapshot';
interface SeriesParams {
eventManager: EventManager;
dataSource: DataSource;
lwcChart: IChartApi;
paneManager: PaneManager;
DOM: DOMModel;
initialIndicators?: IndicatorSnapshot[];
chartOptions?: ChartTypeOptions;
}
export class IndicatorManager {
private eventManager: EventManager;
private lwcChart: IChartApi;
private chartOptions?: ChartTypeOptions;
private entities$: BehaviorSubject<Indicator[]> = new BehaviorSubject<Indicator[]>([]);
private indicatorsMap$: BehaviorSubject<Map<string, Indicator>> = new BehaviorSubject(new Map());
private DOM: DOMModel;
private dataSource: DataSource;
private paneManager: PaneManager;
constructor({ eventManager, dataSource, lwcChart, DOM, chartOptions, initialIndicators, paneManager }: SeriesParams) {
this.eventManager = eventManager;
this.lwcChart = lwcChart;
this.chartOptions = chartOptions;
this.DOM = DOM;
this.dataSource = dataSource;
this.paneManager = paneManager;
this.indicatorsMap$ = new BehaviorSubject<Map<string, Indicator>>(new Map());
initialIndicators?.forEach((ind) => {
this.addIndicator(ind);
});
}
public addEntity<T extends Indicator>(
factory: (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => T,
): T {
return this.DOM.setEntity(factory);
}
public addIndicator(snap: Partial<IndicatorSnapshot>): void {
if (!snap.indicatorType) {
console.error('[IndicatorManager] Не был получен тип индиктора');
return;
}
const indicatorsMap = new Map(this.indicatorsMap$.value);
const id = snap.id ?? `${snap.indicatorType}-${crypto.randomUUID()}`;
const defaultConfig = indicatorsConfigMap[snap.indicatorType];
if (!snap.config && !defaultConfig) {
console.error(`[IndicatorManager] Не был получен конфиг индикатора ${snap.indicatorType}`);
return;
}
const baseConfig = snap.config ?? (defaultConfig as IndicatorConfig);
const config = snap.config
? baseConfig
: createIndicatorConfigWithUniqueColors(baseConfig, this.getUsedIndicatorColors());
const associatedPane =
snap.paneId !== undefined
? (this.paneManager.getPaneById(snap.paneId) ?? this.paneManager.addPane())
: config.newPane
? this.paneManager.addPane()
: this.paneManager.getMainPane();
const indicatorToSet = this.addEntity<Indicator>(
(zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
new Indicator({
id,
paneId: associatedPane.getId(),
zIndex,
onDelete: this.deleteIndicator,
moveUp,
moveDown,
mainSymbol$: this.eventManager.getSymbol(),
lwcChart: this.lwcChart,
dataSource: this.dataSource,
associatedPane,
config,
type: snap.indicatorType,
chartOptions: this.chartOptions,
}),
);
indicatorsMap.set(id, indicatorToSet);
this.indicatorsMap$.next(indicatorsMap);
this.entities$.next(Array.from(indicatorsMap.values()));
}
public getIndicators() {
return this.indicatorsMap$.pipe(map((indicators) => Array.from(indicators.keys())));
}
public removeEntity(entity: DOMObject): void {
this.DOM.removeEntity(entity);
}
public deleteIndicator = (id: string) => {
const indicatorsMap = new Map(this.indicatorsMap$.value);
const entity = indicatorsMap.get(id);
if (!entity) {
return;
}
this.removeEntity(entity);
entity.destroy();
indicatorsMap.delete(id);
this.indicatorsMap$.next(indicatorsMap);
this.entities$.next(Array.from(indicatorsMap.values()));
};
public entities(): Observable<Indicator[]> {
return this.entities$.asObservable();
}
private getUsedIndicatorColors(): string[] {
const usedColors: string[] = [];
this.indicatorsMap$.value.forEach((indicator) => {
usedColors.push(...getIndicatorConfigColors(indicator.getConfig()));
});
return usedColors;
}
}