Загрузка данных


import { MouseEventParams, Point, Time } from 'lightweight-charts';

import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';

import { ChartMouseEvents } from '@core/ChartMouseEvents';

import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { indicatorLabelById } from '@src/core/Indicators';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { CompareMode, IndicatorLabel, IndicatorsIds, OHLCConfig } from '@src/types';
import { getIndicatorSeriesLabel } from '@src/utils';

export interface LegendParams {
  config: OHLCConfig;
  eventManager: EventManager;
  indicators: BehaviorSubject<Map<IndicatorsIds, Indicator>>;
  subscribeChartEvent: ChartMouseEvents['subscribe'];
  mainSeries: BehaviorSubject<SeriesStrategies | null> | null;
  paneId: number;
  openIndicatorSettings: (id: IndicatorsIds, indicator: Indicator) => void;
}

export interface Ohlc {
  time: Time;
  open?: number;
  high?: number;
  low?: number;
  close?: number;
  value?: number;
  absoluteChange?: number;
  percentageChange?: number;
}

export interface CompareLegendItem {
  symbol: string;
  mode: CompareMode;
  value: string;
  color: string;
}

export interface LegendVM {
  vm: LegendModel;
}

export type LegendModel = {
  id: string;
  name: IndicatorLabel | string;
  isIndicator: boolean;
  values: Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>;
  remove?: () => void;
  settings?: () => void;
}[];

export const ohlcValuesToShowForMainSerie = [
  // 'time',
  'open',
  'high',
  'low',
  'close',
  'value',
  // 'absoluteChange',
  'percentageChange',
];

export const ohlcValuesToShowForIndicators = [
  // 'time',
  'open',
  // 'high',
  // 'low',
  // 'close',
  'value',
  // 'absoluteChange',
  // 'percentageChange'
];

export class Legend {
  private eventManager: EventManager;
  private indicators: Map<IndicatorsIds, Indicator> = new Map();
  private config: OHLCConfig;

  private mainSeries!: SeriesStrategies;
  private isChartHovered = false;
  private model$ = new BehaviorSubject<LegendModel>([]);
  private tooltipVisability = new BehaviorSubject<boolean>(false);
  private tooltipPos = new BehaviorSubject<null | Point>(null);
  private paneId: number;

  private openIndicatorSettings: (id: IndicatorsIds, indicator: Indicator) => void;
  private subscriptions = new Subscription();
  private indicatorSubscriptions = new Subscription();
  private mainSeriesSubscription = new Subscription();

  constructor({
    config,
    eventManager,
    indicators,
    subscribeChartEvent,
    mainSeries,
    paneId,
    openIndicatorSettings,
  }: LegendParams) {
    this.config = config;
    this.eventManager = eventManager;
    this.paneId = paneId;
    this.openIndicatorSettings = openIndicatorSettings;

    if (!mainSeries) {
      this.subscriptions.add(
        indicators.subscribe((value: Map<IndicatorsIds, Indicator>) => {
          this.indicators = value;
          this.handleIndicatorSeriesDataChange();
        }),
      );
    } else {
      this.subscriptions.add(
        combineLatest([mainSeries, indicators]).subscribe(([mainSerie, inds]) => {
          if (!mainSerie) {
            return;
          }
          this.mainSeries = mainSerie;
          this.indicators = inds;
          this.handleMainSeriesChange();
          this.handleIndicatorSeriesDataChange();
        }),
      );
    }
    this.subscriptions.add(subscribeChartEvent('crosshairMove', this.handleCrosshairMove));
  }

  public subscribeCursorPosition(cb: (point: Point | null) => void) {
    this.subscriptions.add(this.tooltipPos.subscribe(cb));
  }

  public subscribeCursorVisability(cb: (isVisible: boolean) => void) {
    this.subscriptions.add(this.tooltipVisability.subscribe(cb));
  }

  private handleIndicatorSeriesDataChange = () => {
    this.indicatorSubscriptions.unsubscribe();
    this.indicatorSubscriptions = new Subscription();

    for (const [_, indicator] of this.indicators) {
      this.indicatorSubscriptions.add(indicator.subscribeDataChange(this.updateWithLastCandle));
    }

    this.updateWithLastCandle();
  };

  private handleMainSeriesChange = () => {
    this.mainSeriesSubscription.unsubscribe();
    this.mainSeriesSubscription = new Subscription();

    const handler = () => this.updateWithLastCandle();

    this.mainSeries?.subscribeDataChanged(handler);

    this.mainSeriesSubscription.add(() => this.mainSeries?.unsubscribeDataChanged(handler));
  };

  private updateWithLastCandle = () => {
    if (this.isChartHovered) return;

    const model: LegendModel = [];

    if (this.mainSeries) {
      const series = new Map();

      const serieData = this.mainSeries.getLegendData();

      Object.entries(serieData).forEach(([key, sd]) => {
        series.set(`${key}`, {
          ...sd,
        });
      });

      model.push({
        id: `main-series-${this.paneId}`,
        name: this.mainSeries.options().title,
        values: series as Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>,
        isIndicator: false,
      });
    }

    if (!this.indicators) {
      return;
    }

    for (const [indicatorName, indicator] of this.indicators) {
      const indicatorSeries = new Map();

      for (const [serieName, serie] of indicator.getSeriesMap()) {
        if (!serie.isVisible()) {
          continue;
        }

        const serieData = serie.getLegendData();
        const value = serieData.value ?? serieData.close;

        if (!value) continue;

        indicatorSeries.set(serieName, { ...value, name: getIndicatorSeriesLabel(indicatorName, serieName) });
      }

      model.push({
        id: indicator.getId(),
        name: indicatorLabelById[indicatorName] ?? indicatorName,
        values: indicatorSeries as Partial<
          Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>
        >,
        isIndicator: true,
        remove: () => indicator.delete(),
        settings: indicator.hasSettings() ? () => this.openIndicatorSettings(indicatorName, indicator) : undefined,
      });
    }

    this.model$.next(model);
  };

  private handleCrosshairMove = (param: MouseEventParams) => {
    // todo: есть одинаковый код с updateWithLastCandle
    if (param.point === undefined || !param.time || param.point.x < 0 || param.point.y < 0) {
      this.tooltipVisability.next(false);

      this.isChartHovered = false;
      this.updateWithLastCandle();
      return;
    }

    if (this.paneId === param.paneIndex) {
      this.tooltipVisability.next(true);
    } else {
      this.tooltipVisability.next(false);
    }

    this.isChartHovered = true;

    const model: LegendModel = [];

    if (this.mainSeries) {
      const series = new Map();
      const serieData = this.mainSeries.getLegendData(param);

      Object.entries(serieData).forEach(([key, sd]) => {
        series.set(`${key}`, {
          ...sd,
        });
      });

      model.push({
        id: `main-series-${this.paneId}`,
        name: this.mainSeries.options().title,
        values: series as Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>,
        isIndicator: false,
      });
    }

    if (!this.indicators) {
      return;
    }

    for (const [indicatorName, indicator] of this.indicators) {
      const indicatorSeries = new Map();

      for (const [serieName, serie] of indicator.getSeriesMap()) {
        if (!serie.isVisible()) {
          continue;
        }

        const serieData = serie.getLegendData(param);
        const value = serieData.value ?? serieData.close;

        if (!value) continue;

        indicatorSeries.set(serieName, { ...value, name: getIndicatorSeriesLabel(indicatorName, serieName) });
      }

      model.push({
        id: indicator.getId(),
        name: indicatorLabelById[indicatorName] ?? indicatorName,
        values: indicatorSeries as Partial<
          Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>
        >,
        isIndicator: true,
        remove: () => indicator.delete(),
        settings: indicator.hasSettings() ? () => this.openIndicatorSettings(indicatorName, indicator) : undefined,
      });
    }

    this.model$.next(model);
    this.tooltipPos.next(param.point);
  };

  public getConfig(): OHLCConfig {
    return this.config;
  }

  public getLegendViewModel(): Observable<LegendModel> {
    return this.model$;
  }

  public destroy = () => {
    this.subscriptions.unsubscribe();
    this.indicatorSubscriptions.unsubscribe();
    this.mainSeriesSubscription.unsubscribe();

    this.model$.complete();
    this.tooltipVisability.complete();
    this.tooltipPos.complete();
  };
}




import { IChartApi, IPaneApi, Time } from 'lightweight-charts';

import { BehaviorSubject, Subscription } from 'rxjs';

import { ChartTooltip } from '@components/ChartTooltip';
import { LegendComponent } from '@components/Legend';
import { ChartMouseEvents } from '@core/ChartMouseEvents';
import { ContainerManager } from '@core/ContainerManager';
import { DataSource } from '@core/DataSource';
import { DOMModel } from '@core/DOMModel';
import { DrawingsManager, DrawingsNames } from '@core/DrawingsManager';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { Legend } from '@core/Legend';
import { ReactRenderer } from '@core/ReactRenderer';
import { TooltipService } from '@core/Tooltip';
import { UIRenderer } from '@core/UIRenderer';
import { IndicatorSettingsModal } from '@src/components/IndicatorSettingsModal';
import { indicatorLabelById } from '@src/core/Indicators';
import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesFactory, SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { IndicatorsIds, OHLCConfig, TooltipConfig } from '@src/types';
import { ensureDefined } from '@src/utils';

export interface PaneParams {
  id: number;
  lwcChart: IChartApi;
  eventManager: EventManager;
  DOM: DOMModel;
  isMainPane: boolean;
  ohlcConfig: OHLCConfig;
  dataSource: DataSource | null; // todo: deal with dataSource. На каких то пейнах он нужен, на каких то нет
  basedOn?: Pane; // Pane на котором находится главная серия, или серия, по которой строятся серии на текущем пейне
  subscribeChartEvent: ChartMouseEvents['subscribe'];
  tooltipConfig: TooltipConfig;
  onDelete: () => void;
  chartContainer: HTMLElement;
  modalRenderer: ModalRenderer;
}

// todo: Pane, ему должна принадлежать mainSerie, а также IndicatorManager и drawingsManager, mouseEvents. Также перекинуть соответствующие/необходимые свойства из чарта, и из чарта удалить
// todo: Учитывать, что есть линейка, которая рисуется одна для всех пейнов
// todo: в CompareManage, при создании нового пейна для сравнения - инициализируем новый dataSource, принадлежащий только конкретному пейну. Убираем возможность добавлять индикаторы на такие пейны
// todo: на каждый символ свой DataSource (учитывать что есть MainPane и "главный" DataSource, который инициализиурется во время старта moexChart)
// todo: сделать два разных представления для compare, в зависимости от отображения на главном пейне или на второстепенном

export class Pane {
  private readonly id: number;
  private isMain: boolean;
  private mainSeries: BehaviorSubject<SeriesStrategies | null> = new BehaviorSubject<SeriesStrategies | null>(null); // Main Series. Exists in a single copy
  private legend!: Legend;
  private tooltip: TooltipService | undefined;

  private indicatorsMap: BehaviorSubject<Map<IndicatorsIds, Indicator>> = new BehaviorSubject<
    Map<IndicatorsIds, Indicator>
  >(new Map());

  private lwcPane: IPaneApi<Time>;
  private lwcChart: IChartApi;

  private eventManager: EventManager;
  private drawingsManager: DrawingsManager;

  private legendContainer!: HTMLElement;
  private paneOverlayContainer!: HTMLElement;
  private legendRenderer!: UIRenderer;
  private tooltipRenderer: UIRenderer | undefined;
  private modalRenderer: ModalRenderer;

  private mainSerieSub!: Subscription;
  private subscribeChartEvent: ChartMouseEvents['subscribe'];
  private onDelete: () => void;
  private subscriptions = new Subscription();

  constructor({
    lwcChart,
    eventManager,
    dataSource,
    DOM,
    isMainPane,
    ohlcConfig,
    id,
    basedOn,
    subscribeChartEvent,
    tooltipConfig,
    onDelete,
    chartContainer,
    modalRenderer,
  }: PaneParams) {
    this.onDelete = onDelete;
    this.eventManager = eventManager;
    this.lwcChart = lwcChart;
    this.modalRenderer = modalRenderer;
    this.subscribeChartEvent = subscribeChartEvent;
    this.isMain = isMainPane ?? false;
    this.id = id;

    this.initializeLegend({ ohlcConfig });

    if (isMainPane) {
      this.lwcPane = this.lwcChart.panes()[this.id];
    } else {
      this.lwcPane = this.lwcChart.addPane(true);
    }

    this.tooltip = new TooltipService({
      config: tooltipConfig,
      legend: this.legend,
      paneOverlayContainer: this.paneOverlayContainer,
    });

    this.tooltipRenderer = new ReactRenderer(this.paneOverlayContainer);

    this.tooltipRenderer.renderComponent(
      <ChartTooltip
        formatObs={this.eventManager.getChartOptionsModel()}
        timeframeObs={this.eventManager.getTimeframeObs()}
        viewModel={this.tooltip.getTooltipViewModel()}
        // ohlcConfig={this.legend.getConfig()}
        ohlcConfig={ohlcConfig}
        tooltipConfig={this.tooltip.getConfig()}
      />,
    );

    if (dataSource) {
      this.initializeMainSerie({ lwcChart, dataSource });
    } else if (basedOn) {
      this.mainSeries = basedOn?.getMainSerie();
    } else {
      console.error('[Pane]: There is no any mainSerie for new pane');
    }

    this.drawingsManager = new DrawingsManager({
      // todo: менеджер дровингов должен быть один на чарт, не на пейн
      eventManager,
      DOM,
      mainSeries$: this.mainSeries.asObservable(),
      lwcChart,
      container: chartContainer,
    });

    this.subscriptions.add(
      // todo: переедет в пейн
      this.drawingsManager.entities().subscribe((drawings) => {
        const hasRuler = drawings.some((drawing) => drawing.id.startsWith(DrawingsNames.ruler));
        this.legendContainer.style.display = hasRuler ? 'none' : '';
      }),
    );
  }

  public getMainSerie = () => {
    return this.mainSeries;
  };

  public getId = () => {
    return this.id;
  };

  public setIndicator(indicatorName: IndicatorsIds, indicator: Indicator): void {
    const map = this.indicatorsMap.value;

    map.set(indicatorName, indicator);

    this.indicatorsMap.next(map);
  }

  public removeIndicator(indicatorName: IndicatorsIds): void {
    const map = this.indicatorsMap.value;

    map.delete(indicatorName);

    this.indicatorsMap.next(map);

    if (map.size === 0 && !this.isMain) {
      this.destroy();
    }
  }

  public getDrawingManager(): DrawingsManager {
    return this.drawingsManager;
  }

  private initializeLegend({ ohlcConfig }: { ohlcConfig: OHLCConfig }) {
    const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
    this.legendContainer = legendContainer;
    this.paneOverlayContainer = paneOverlayContainer;
    this.legendRenderer = new ReactRenderer(legendContainer);

    requestAnimationFrame(() => {
      setTimeout(() => {
        const lwcPaneElement = this.lwcPane.getHTMLElement();
        if (!lwcPaneElement) return;
        lwcPaneElement.style.position = 'relative';
        lwcPaneElement.appendChild(legendContainer);
        lwcPaneElement.appendChild(paneOverlayContainer);
      }, 0);
    });

    // todo: переписать код ниже под логику пейнов
    // /*
    //   Внутри lightweight-chart DOM построен как таблица из 3 td
    //   [0] left priceScale, [1] center chart, [2] right priceScale
    //   Кладём легенду в td[1] и тогда легенда сама будет адаптироваться при изменении ширины шкал
    // */
    // requestAnimationFrame(() => {
    //   const root = chartAreaContainer.querySelector('.tv-lightweight-charts');
    //   console.log(root)
    //   const table = root?.querySelector('table');
    //   console.log(table)
    //
    //   const htmlCollectionOfPanes = table?.getElementsByTagName('td')
    //   console.log(htmlCollectionOfPanes)
    //
    //   const centerId = htmlCollectionOfPanes?.[1];
    //   console.log(centerId)
    //
    //   if (centerId && legendContainer && legendContainer.parentElement !== centerId) {
    //     centerId.appendChild(legendContainer);
    //   }
    // });
    // /*
    //   Внутри lightweight-chart DOM построен как таблица из 3 td
    //   [0] left priceScale, [1] center chart, [2] right priceScale
    //   Кладём легенду в td[1] и тогда легенда сама будет адаптироваться при изменении ширины шкал
    // */
    // requestAnimationFrame(() => {
    //   const root = chartAreaContainer.querySelector('.tv-lightweight-charts');
    //   const table = root?.querySelector('table');
    //   const centerId = table?.getElementsByTagName('td')?.[1];
    //
    //   if (centerId && legendContainer && legendContainer.parentElement !== centerId) {
    //     centerId.appendChild(legendContainer);
    //   }
    // });

    this.legend = new Legend({
      config: ohlcConfig,
      indicators: this.indicatorsMap,
      eventManager: this.eventManager,
      subscribeChartEvent: this.subscribeChartEvent,
      mainSeries: this.isMain ? this.mainSeries : null,
      paneId: this.id,
      openIndicatorSettings: (indicatorId, indicator) => {
        let settings = indicator.getSettings();

        this.modalRenderer.renderComponent(
          <IndicatorSettingsModal
            fields={indicator.getSettingsConfig()}
            values={settings}
            onChange={(nextSettings) => {
              settings = nextSettings;
            }}
          />,
          {
            size: 'sm',
            title: indicatorLabelById[indicatorId],
            onSave: () => indicator.updateSettings(settings),
          },
        );
      },
      // todo: throw isMainPane
    });

    this.legendRenderer.renderComponent(
      <LegendComponent
        ohlcConfig={this.legend.getConfig()}
        viewModel={this.legend.getLegendViewModel()}
      />,
    );
  }

  private initializeMainSerie({ lwcChart, dataSource }: { lwcChart: IChartApi; dataSource: DataSource }) {
    this.mainSerieSub = this.eventManager.subscribeSeriesSelected((nextSeries) => {
      this.mainSeries.value?.destroy();

      const next = ensureDefined(SeriesFactory.create(nextSeries))({
        lwcChart,
        dataSource,
        mainSymbol$: this.eventManager.getSymbol(),
        mainSerie$: this.mainSeries,
      });

      this.mainSeries.next(next);
    });
  }

  public destroy() {
    this.subscriptions.unsubscribe();
    this.tooltip?.destroy();
    this.legend?.destroy();
    this.legendRenderer.destroy();

    this.tooltipRenderer?.destroy();
    this.indicatorsMap.complete();

    this.mainSerieSub?.unsubscribe();
    try {
      this.lwcChart.removePane(this.id);
    } catch (e) {
      console.log(e);
    }

    this.onDelete();
  }
}