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


import { MoexChart, Timeframes } from 'moex-chart';

// eslint-disable-next-line import/no-unresolved -- MOEX_CHART
import { CompareManager } from 'moex-chart/dist/types/core/CompareManager';
import { useEffect, useRef, useState } from 'react';

import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';
import { ChartIndicativeData, ChartProps } from '@widgets/Chart/types';

import { MOEX_CHART_CONFIG } from '../constants';
import { dataSourceProvider, DataSourceProvider } from '../dataSourceProvide';

import type { IMoexChart } from 'moex-chart';

type TUseMoexChartProps = {
  symbol: string;
  indicativeData: ChartIndicativeData | undefined;
};

export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) => {
  const moexChartTimeFrame = useSelectProperties((wProps: Partial<ChartProps>) => wProps.moexChartState?.tf);

  const { updateProperties } = useChangeProperties<ChartProps>();

  const [isCompareOpen, setIsCompareOpen] = useState(false);

  const containerRef = useRef<HTMLDivElement | null>(null);
  const compareManagerRef = useRef<null | CompareManager>(null);
  const timeframeRef = useRef<Timeframes | undefined>(moexChartTimeFrame);
  const updateTimeframeRef = useRef<(tf: Timeframes) => void>(() => undefined);

  useEffect(() => {
    timeframeRef.current = moexChartTimeFrame;
  }, [moexChartTimeFrame]);

  updateTimeframeRef.current = (tf: Timeframes) => {
    if (timeframeRef.current === tf) {
      return;
    }

    timeframeRef.current = tf;

    updateProperties((state) => {
      if (state.moexChartState) {
        state.moexChartState.tf = tf;
        return;
      }

      state.moexChartState = {
        tf,
      };
    });
  };

  useEffect(() => {
    const container = containerRef.current;

    if (!container) {
      return undefined;
    }

    const initialTimeframe = timeframeRef.current || Timeframes['1m'];

    const charts: IMoexChart['snapshot']['charts'] = MOEX_CHART_CONFIG.snapshot.charts.map(
      (chartSnapshot): IMoexChart['snapshot']['charts'][number] => ({
        ...chartSnapshot,
        symbol,
        timeframe: initialTimeframe,
      }),
    );

    const chart = new MoexChart({
      ...MOEX_CHART_CONFIG,
      container,
      snapshot: {
        ...MOEX_CHART_CONFIG.snapshot,
        charts,
      },
      chartCollectionPreset: {
        ...MOEX_CHART_CONFIG.chartCollectionPreset,
        openCompareModal: () => setIsCompareOpen(true),
        getDataSource: DataSourceProvider.getDataSource(indicativeData, (tf) => {
          updateTimeframeRef.current(tf);
        }),
        startRealtime: (getSymbols, getTimeframe, update) =>
          dataSourceProvider.startRealtime({
            getSymbols,
            getTimeframe,
            update,
          }),
      },
    });

    compareManagerRef.current = chart.getCompareManager();

    return () => {
      compareManagerRef.current = null;
      chart.destroy();
    };
  }, [symbol, indicativeData]);

  return {
    containerRef,
    isCompareOpen,
    compareManagerRef,
    setIsCompareOpen,
  };
};

import { Timeframes } from 'moex-chart';

import { Contract } from '@modules/contracts';
import { WidgetContentBasicProps } from 'types/Widgets';

export type ChartIndicativeData = {
  secId: string;
  instrumentName: string;
  settlement: string;
  firmName: string;
  key: string;
};

export type ResolutionMap = {
  '1': string;
  '5': string;
  '10': string;
  '15': string;
  '30': string;
  '45': string;
  '60': string;
  '240': string;
  '1D': string;
  '7D': string;
  '1M': string;
  '3M': string;
};

export type ChartProps = {
  chartState: {
    savedInstrument: Contract['issKey'];
    interval: string;
    savedData: string;
  };
  indicativeData?: ChartIndicativeData;

  isMoexChartShow?: boolean;
  moexChartState?: {
    tf: Timeframes;
  };
};

export type ChartContainerProps = WidgetContentBasicProps<ChartProps>;

import isEqual from 'lodash/isEqual';
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';

import { candlesController } from '@api/controllers/candles';
import { uiConfig } from '@configs/manager';
import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import {
  ChartingLibraryWidgetOptions,
  IChartingLibraryWidget,
  ResolutionString,
  widget,
} from '@modules/charting_library/charting_library';
import { Contract } from '@modules/contracts';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';

import { getState } from '@store/store';
import { chartToReqTimeConverter } from '@utils/chartToReqTimeConverter';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';

import { DAY_LINE_ON_CHART_BOARDS, DEFAULT_SYMBOL, MINUTE_CANDLES_ON_CHART_BOARDS } from '../const';
import useDatafeed from '../datafeed';
import { ResolutionMap } from '../types';

import { isIndicativeTicker } from '../utils/isIndicativeTicker';
import { mapResolutionIntervals } from '../utils/resolutionIntervals';
import { formatToDateTime } from '../utils/utils';

import { useChartPublicContext } from './useChartPublicContext';

import type { ChartContainerProps, ChartProps } from '../types';
import type { Candles } from 'types/Candles';
import type { ContractsType } from 'types/Contracts';
import type { Widget } from 'types/Widgets';

enum SeriesStyle {
  Bars = 0,
  Candles = 1,
  Line = 2,
  Area = 3,
  HeikenAshi = 8,
  HollowCandles = 9,
  Baseline = 10,
  HiLo = 12,
  Column = 13,
  Renko = 4,
  Kagi = 5,
  PointAndFigure = 6,
  LineBreak = 7,
};

interface UseChartComponentFacadeReturn {
  chartContainerRef: MutableRefObject<HTMLDivElement>;
  savedTvWidget: IChartingLibraryWidget | null;
  dropDownOpen: boolean;
  setDropdownOpen: Dispatch<SetStateAction<boolean>>;
  currentInstrument: MutableRefObject<string>;
  handlerSaveAsExcel: (() => void) | undefined;
  isWidgetHeaderContextMenuOpen: boolean;
  setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
  onDropInstruments: (val: string, withUpdate?: boolean) => void;
  addInstrumentFromModal: (instruments: Contract[]) => void;
  isOver: boolean;
}

const defaultOptions = {
  symbol: DEFAULT_SYMBOL,
  interval: '1' as ResolutionString,
  datafeedUrl: 'https://demo_feed.tradingview.com',
  libraryPath: `${uiConfig.staticContextPath || ''}/charting_library/`,
  chartsStorageUrl: 'https://saveload.tradingview.com',
  chartsStorageApiVersion: '1.1' as const,
  clientId: 'tradingview.com',
  userId: 'public_user_id',
  fullscreen: false,
  autosize: true,
  studiesOverrides: {},
};

export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
  const { widgetId } = props;

  const [isOver, setIsOver] = useState<boolean>(false);

  const chartContainerRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement>;
  const tvWidgetRef = useRef<IChartingLibraryWidget | null>(null);
  const dispatch = useDispatch();
  const indicativeData = props.widgetContentProps?.indicativeData;

  const chartPrevStateRef = useRef<object>({});

  // используется для определения источника отображаемого в виджете инструмента (из привязки или нет)
  const isInstrumentChangedFromBinding = useRef<boolean>(false);
  const prevTimeframe = useRef<string>('');

  const lastIntervalChangeTime = useRef<number | null>(null);
  const timeDiffOfIntervalChangeTime = useRef<number | null>(null);

  const [savedTvWidget, setTvWidget] = useState<IChartingLibraryWidget | null>(null);
  /**
   * Копия chartIsReady тк некоторые useCallback хранят старое значение переменной.
   * Полностью не переходим на реф тк стейт используется в useEffect.
   * Рекомендуется использовать реф, за исключением если требуется срабатывать useEffect на chartIsReady.
   */
  const isChartIsReadyFromRef = useRef<boolean>(false);
  const [chartIsReady, setChartIsReadyFromUseState] = useState(false);
  const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);

  const setChartIsReady = useCallback((isReady: boolean) => {
    setChartIsReadyFromUseState(isReady);
    isChartIsReadyFromRef.current = isReady;
  }, []);

  useEffect(() => {
    tvWidgetRef.current = savedTvWidget;
  }, [savedTvWidget]);

  const language = useAppSelect((state) => state.localisation.localisation);

  const { chartState } = useAppSelect((state) => state.widgets.widgets.find(({ id }) => id === widgetId))
    ?.widgetContentProps as ChartProps;

  // TODO временно реф из-за непредсказуемого изменения если используется useState,
  // нужен рефакторинг и вернуть обратно useState
  const currentInstrument = useRef<ContractsType['issKey']>(chartState?.savedInstrument ?? defaultOptions.symbol);

  const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
    widgetId,
  });

  // Функция работает внутри интервала, поэтому используем не хуковый доступ к стору
  const saveContentProps = (
    val: Partial<ChartProps['chartState']> & { withUpdate?: boolean; cleanIndicativeData?: boolean },
  ): void => {
    // если вызываем функцию с withUpdate, то оправляем в стор и делаем запрос на изменения
    const { withUpdate, cleanIndicativeData, ...chartStateVal } = val;
    const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;

    if (oldProps) {
      dispatch(
        addContentPropsToWidget({
          id: widgetId,
          withoutSend: !withUpdate,
          widgetContentProps: {
            chartState: { ...oldProps.chartState, ...chartStateVal },
            // при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
            indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
          },
        }),
      );
    }
  };

  const handlerSaveAsExcel = () => {
    const chart = savedTvWidget?.activeChart();
    const visibleRange = chart ? chart.getVisibleRange() : null;

    const checkedInterval = chart?.resolution();

    // Интервал из которого фактически будут формироваться свечи на графике
    const interval = mapResolutionIntervals((checkedInterval ?? defaultOptions.interval) as keyof ResolutionMap);
    const timezoneTitle = tvWidgetRef.current?.activeChart().getTimezoneApi().getTimezone()?.title;
    let timezoneAsNumber;

    if (timezoneTitle === 'Биржа') {
      timezoneAsNumber = 3;
    } else if (timezoneTitle && timezoneTitle.includes(':')) {
      // если часовой пояс с дробным числом в формате UTC то по договоренности с бэком берем только целую часть
      timezoneAsNumber = Number(
        timezoneTitle
          .split('UTC' as string)[1]
          .split(')')[0]
          .split(':')[0],
      );
    } else {
      timezoneAsNumber = timezoneTitle ? Number(timezoneTitle.split('UTC')[1].split(')')[0]) : 3;
    }

    if (visibleRange) {
      const startDateTime = formatToDateTime(visibleRange.from);
      const endDateTime = formatToDateTime(visibleRange.to);

      const data: Candles = {
        secId: currentInstrument.current.replace(/:/g, '.'),
        from: startDateTime,
        till: endDateTime,
        interval: chartToReqTimeConverter(interval ?? defaultOptions.interval),
        value: chartToReqTimeConverter(checkedInterval ?? defaultOptions.interval),
        offset: timezoneAsNumber,
      };

      candlesController
        .upload(data)
        .then((response) => {
          // eslint-disable-next-line @typescript-eslint/no-shadow -- TODO исправить reasign
          const { data, headers } = response;

          const blob = new Blob([data]);
          const fileName = headers['content-disposition']?.match(/filename="(.+)"/)?.[1] || 'default_filename';

          const link = document.createElement('a');
          link.href = window.URL.createObjectURL(blob);
          link.download = fileName;
          link.click();
        })
        .catch((error) => {
          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('Error uploading data:', error);
        });
    } else {
      // eslint-disable-next-line no-console -- Обработка исключения
      console.error('Chart is undefined or visible range is not available.');
    }
  };

  useEffect(() => {
    triggerRelatedWidgetsToUpdate(currentInstrument.current);

    const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;

    if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
      return;
    }

    if (currentInstrument.current !== savedActiveChart.symbol()) {
      savedActiveChart.setSymbol(currentInstrument.current);
    }
    // [currentInstrument.current] в зависимостях
    // т.к. здесь не нужен ререндер потому что достаточно прокинуть в TV новое значение
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, [currentInstrument.current, chartIsReady]);

  const onInstrumentChangeCallback = useCallback((prevInstr: string, newInstr: string) => {
    if (prevInstr === newInstr || !isChartIsReadyFromRef.current) {
      return;
    }

    const newBoardInLowerCase = newInstr.split(':')[1]?.toLowerCase() ?? '';
    const prevBoardInLowerCase = prevInstr.split(':')[1]?.toLowerCase() ?? '';

    // для двух специфичных случаев точечно изменяем тип линии и таймфрейм
    if (
      ((prevInstr === DEFAULT_SYMBOL ||
        !DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase) ||
        !MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase)) &&
        DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase)) ||
      (MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase) &&
        DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase))
    ) {
      tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
      tvWidgetRef.current?.activeChart().setResolution('1D' as ResolutionString);
    }

    if (
      MINUTE_CANDLES_ON_CHART_BOARDS.includes(newBoardInLowerCase) &&
      DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase)
    ) {
      tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Candles);
      tvWidgetRef.current?.activeChart().setResolution('1' as ResolutionString);
    }

    // для индикатива при выборе из Поиска инструментов и после перезагрузки устанавливаем
    // нужные настройки графика
    if (isIndicativeTicker(newInstr)) {
      tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
      tvWidgetRef.current?.activeChart().setResolution('60' as ResolutionString);
    }
  }, []);

  const onInstrumentChange = useCallback(
    (newVal: string, withUpdate?: boolean, unbind = true): void => {
      const prev = currentInstrument.current;
      currentInstrument.current = newVal;

      if (unbind) {
        dispatch(unbindWidgets({ widgetId }));
      }

      // при смене инструмента очищаем индикативные данные виджета график
      // т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
      saveContentProps({ savedInstrument: newVal, withUpdate, cleanIndicativeData: true });
      setIsWidgetHeaderContextMenuOpen(false);
      onInstrumentChangeCallback(prev, newVal);
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
    [onInstrumentChangeCallback, chartIsReady],
  );

  const addInstrumentFromModal = useCallback(
    (instruments: Pick<Contract, 'issKey'>[]) => {
      const instr = instruments[0];

      isInstrumentChangedFromBinding.current = false;

      if (instr.issKey !== null) {
        onInstrumentChange(instr.issKey);
      }
    },
    [onInstrumentChange],
  );

  const onInstrumentChangeFromBind = useCallback(
    (instrumentId: string) => {
      isInstrumentChangedFromBinding.current = true;
      onInstrumentChange(instrumentId, true, false);
    },
    [onInstrumentChange],
  );

  const onDropInstruments = useCallback((val: string, withUpdate?: boolean): void => {
    isInstrumentChangedFromBinding.current = false;
    onInstrumentChange(val, withUpdate);
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, []);

  useChartPublicContext({
    setCurrInstrument: onInstrumentChangeFromBind,
    widgetId,
    getMasterInstrumentFromPublicContext,
  });

  const { datafeed, windowIntervalRequester } = useDatafeed(
    tvWidgetRef,
    prevTimeframe,
    timeDiffOfIntervalChangeTime,
    isInstrumentChangedFromBinding,
    indicativeData,
  );

  const createWidget = () => {
    if (!chartContainerRef.current) {
      return;
    }
    const todayData = new Date();
    const halfYearAgo = new Date();
    halfYearAgo.setMonth(halfYearAgo.getMonth() - 6);

    const threeMonthsAgo = new Date();
    threeMonthsAgo.setDate(threeMonthsAgo.getDate() - 90);
    const oneDayAgo = new Date();
    oneDayAgo.setDate(oneDayAgo.getDate() - 1);

    const thisYearStartDate = new Date();
    thisYearStartDate.setMonth(0);
    thisYearStartDate.setDate(1);

    const diffBetween = todayData.getTime() - thisYearStartDate.getTime();
    const dayDiff = diffBetween / 1000 / 3600 / 24;
    const minutesDiff = diffBetween / 1000 / 60;

    const ytlResolution = `${dayDiff > 0 ? dayDiff : minutesDiff}${dayDiff > 0 ? 'D' : ''}` as ResolutionString;

    const widgetOptions: ChartingLibraryWidgetOptions = {
      symbol: currentInstrument.current ?? chartState?.savedInstrument ?? (defaultOptions.symbol as string),
      // BEWARE: no trailing slash is expected in feed URL
      // tslint:disable-next-line:no-any
      datafeed,
      interval: (chartState?.interval ?? defaultOptions.interval) as ChartingLibraryWidgetOptions['interval'],
      container: chartContainerRef.current,
      library_path: defaultOptions.libraryPath as string,
      locale: language,
      timezone: 'Europe/Moscow',
      charts_storage_url: defaultOptions.chartsStorageUrl,
      charts_storage_api_version: defaultOptions.chartsStorageApiVersion,
      client_id: defaultOptions.clientId,
      time_frames: [
        { text: '1D', resolution: '60' as ResolutionString, description: '1d' },
        { text: '5d', resolution: '60' as ResolutionString, description: '5d' },
        { text: '1M', resolution: 'D' as ResolutionString, description: '1M' },
        { text: '6M', resolution: 'D' as ResolutionString, description: '6M' },
        {
          text: ytlResolution,
          resolution: '7D' as ResolutionString,
          description: 'YTL',
          title: 'YTD',
        },
        { text: '1Y', resolution: '7D' as ResolutionString, description: '6M' },
        { text: '5Y', resolution: '1M' as ResolutionString, description: '6M' },
        {
          text: '1000y',
          resolution: '7D' as ResolutionString,
          description: 'All',
          title: 'All',
        },
      ],

      user_id: defaultOptions.userId,
      fullscreen: defaultOptions.fullscreen,
      autosize: defaultOptions.autosize,
      symbol_search_request_delay: 1000,
      enabled_features: ['study_templates', 'hide_left_toolbar_by_default', 'header_compare', 'header_indicators'],
      disabled_features: ['use_localstorage_for_settings'],
      custom_css_url: 'css/customstyles.css',
      theme: 'Dark',
      save_load_adapter: {
        charts: [],
        studyTemplates: [],
        drawingTemplates: [],
        getAllCharts() {
          return Promise.resolve(this.charts);
        },

        removeChart(id: number) {
          for (let i = 0; i < this.charts.length; i += 1) {
            if (this.charts[i].id === id) {
              this.charts.splice(i, 1);
              return Promise.resolve();
            }
          }

          return Promise.reject();
        },

        saveChart(chartData: Record<string, unknown>) {
          if (!chartData.id) {
            // eslint-disable-next-line no-param-reassign -- Ругается.
            chartData.id = Math.random().toString();
          } else {
            this.removeChart(chartData.id);
          }

          // eslint-disable-next-line no-param-reassign -- Ругается.
          chartData.timestamp = new Date().valueOf();

          this.charts.push(chartData);

          return Promise.resolve(chartData.id);
        },

        getChartContent(id: number) {
          for (let i = 0; i < this.charts.length; i += 1) {
            if (this.charts[i].id === id) {
              return Promise.resolve(this.charts[i].content);
            }
          }

          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('error');

          return Promise.reject();
        },

        removeStudyTemplate(studyTemplateData: Record<string, unknown>) {
          for (let i = 0; i < this.studyTemplates.length; i += 1) {
            if (this.studyTemplates[i].name === studyTemplateData.name) {
              this.studyTemplates.splice(i, 1);
              return Promise.resolve();
            }
          }

          return Promise.reject();
        },

        getStudyTemplateContent(studyTemplateData: Record<string, unknown>) {
          for (let i = 0; i < this.studyTemplates.length; i += 1) {
            if (this.studyTemplates[i].name === studyTemplateData.name) {
              return Promise.resolve(this.studyTemplates[i]);
            }
          }

          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('st: error');

          return Promise.reject();
        },

        saveStudyTemplate(studyTemplateData: Record<string, unknown>) {
          for (let i = 0; i < this.studyTemplates.length; i += 1) {
            if (this.studyTemplates[i].name === studyTemplateData.name) {
              this.studyTemplates.splice(i, 1);
              break;
            }
          }

          this.studyTemplates.push(studyTemplateData);
          return Promise.resolve();
        },

        getAllStudyTemplates() {
          return Promise.resolve(this.studyTemplates);
        },

        removeDrawingTemplate(toolName: string, templateName: string) {
          for (let i = 0; i < this.drawingTemplates.length; i += 1) {
            if (this.drawingTemplates[i].name === templateName) {
              this.drawingTemplates.splice(i, 1);
              return Promise.resolve();
            }
          }

          return Promise.reject();
        },

        loadDrawingTemplate(toolName: string, templateName: string) {
          for (let i = 0; i < this.drawingTemplates.length; i += 1) {
            if (this.drawingTemplates[i].name === templateName) {
              return Promise.resolve(this.drawingTemplates[i].content);
            }
          }

          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('drawing: error');

          return Promise.reject();
        },

        saveDrawingTemplate(toolName: string, templateName: string, content: unknown) {
          for (let i = 0; i < this.drawingTemplates.length; i += 1) {
            if (this.drawingTemplates[i].name === templateName) {
              this.drawingTemplates.splice(i, 1);
              break;
            }
          }

          this.drawingTemplates.push({ name: templateName, content });
          return Promise.resolve();
        },

        getDrawingTemplates() {
          return Promise.resolve(this.drawingTemplates.map((template: Record<string, unknown>) => template.name));
        },
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Ругается.
      } as any,
      overrides: {
        'mainSeriesProperties.lineStyle.color': '#576DDB',
        'paneProperties.legendProperties.showSeriesTitle': false,
      },
    };
    if (chartState && chartState.savedData) {
      try {
        widgetOptions.saved_data = JSON.parse(chartState.savedData);
      } catch {
        widgetOptions.saved_data = undefined;
      }
    }

    // eslint-disable-next-line new-cap -- Ругается
    const tvWidget = new widget(widgetOptions);
    tvWidget.onChartReady(() => {
      setChartIsReady(true);
      tvWidget.chart().getTimezoneApi().setTimezone('Europe/Moscow');
      tvWidget
        ?.activeChart()
        ?.onIntervalChanged()
        ?.subscribe(null, (newInterval) => {
          const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;
          prevTimeframe.current = oldProps.chartState.interval;

          const now = Date.now();

          if (lastIntervalChangeTime.current) {
            timeDiffOfIntervalChangeTime.current = now - lastIntervalChangeTime.current;
          }

          lastIntervalChangeTime.current = now;

          if (oldProps) {
            dispatch(
              addContentPropsToWidget({
                id: widgetId,
                widgetContentProps: {
                  chartState: {
                    ...oldProps.chartState,
                    interval: newInterval,
                  },
                },
              }),
            );
          }
        });
      // tvWidget.chart().getTimezoneApi().setTimezone("Europe/Moscow");

      tvWidget
        .activeChart()
        .onSymbolChanged()
        .subscribe(null, () => {
          // если инструмент из индикативныз котировок то его не надо отдельно созранять через onInstrumentChange
          if (indicativeData) {
            return;
          }

          if (currentInstrument.current === tvWidget.activeChart().symbol()) {
            return;
          }

          // если инструмент не из привязки то обновляем значение и отвязываем привязку
          if (!isInstrumentChangedFromBinding.current) {
            onInstrumentChange(tvWidget.activeChart().symbol(), false, true);
          }
        });
    });

    setTvWidget(tvWidget);

    const counter = setInterval(() => {
      if (!chartContainerRef.current || !tvWidgetRef.current) {
        if (!chartContainerRef.current) {
          clearInterval(counter);
        }
        return;
      }
      tvWidget?.save((state) => {
        if (chartPrevStateRef.current && !isEqual(state, chartPrevStateRef.current)) {
          chartPrevStateRef.current = state;
          saveContentProps({
            interval: tvWidget.symbolInterval().interval,
            savedData: JSON.stringify(state),
          });
        }
      });
    }, 1000);
  };

  const [dropDownOpen, setDropdownOpen] = useState(false);

  useEffect(() => {
    const unsubscribe = communicator.listen({ messageType: CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT }, (message) => {
      const issKey = (message as Record<number, string>)[widgetId];
      if (issKey) {
        addInstrumentFromModal([{ issKey }]);
      }
    });
    const unsubscribeHighlighter = communicator.listen({ messageType: HIGHLIGHT_WIDGET_EVENT }, (message) => {
      const typedMessage = message as Record<number, boolean>;

      if (Object.keys(typedMessage).includes(String(widgetId))) {
        setIsOver(typedMessage[widgetId]);
      }
    });
    return () => {
      unsubscribe();
      unsubscribeHighlighter();
    };
  }, [addInstrumentFromModal, widgetId]);

  useEffect(
    () => () => {
      Object.values(windowIntervalRequester).forEach(clearInterval);
    },
    [windowIntervalRequester],
  );

  useEffect(
    () => () => {
      if (savedTvWidget !== null) {
        savedTvWidget.remove();
        setTvWidget(null);
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
    [],
  );

  useEffect(() => {
    if (savedTvWidget?.getLanguage() === language) {
      return;
    }
    createWidget();
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, [language]);

  /** Переключение инструмента и вида графика при indicativeData */
  useEffect(() => {
    const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;

    if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
      return;
    }

    onInstrumentChangeCallback(
      chartState?.savedInstrument ?? (defaultOptions.symbol as string),
      currentInstrument.current,
    );

    if (indicativeData) {
      savedActiveChart.setSymbol(indicativeData.key);
      savedActiveChart.setChartType(SeriesStyle.Line);
      savedActiveChart.setResolution('60' as ResolutionString);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, [chartIsReady, savedTvWidget, indicativeData]);

  return {
    chartContainerRef,
    savedTvWidget,
    dropDownOpen,
    setDropdownOpen,
    currentInstrument,
    handlerSaveAsExcel,
    isWidgetHeaderContextMenuOpen,
    setIsWidgetHeaderContextMenuOpen,
    onDropInstruments,
    addInstrumentFromModal,
    isOver,
  };
}



import React, { FC, lazy, useContext, useEffect, useState } from 'react';

import { DNDWrapper } from '@components/DNDWrapper';
import { InstrumentSearch } from '@components/InstrumentSearch';
import WidgetContentWrapper from '@components/WidgetContentWrapper';
import WidgetHeader from '@components/WidgetHeader';
import { useDropInstrument } from '@hooks/dnd';

import { useAppSelect } from '@hooks/useAppSelector';
import { useChangeProperties } from '@modules/widgetProperties';
import { getIsShowMoexChartInChart } from '@store/selectors/cachedData';
import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { Switch } from '@uikit/Switch';
import { useWidgetHeaderName } from '@utils/useWidgetName';

import useChartComponentFacade from './hooks/useChartComponentFacade';
import { ChartContainerProps, ChartProps } from './types';

const LazyTREntry = lazy(() => import('./components/MoexChart/MoexChart'));

export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
  const indicativeData = props.widgetContentProps?.indicativeData;

  const { updateProperties } = useChangeProperties<ChartProps>();

  const isShowMoexChartInChart = useAppSelect(getIsShowMoexChartInChart);

  const [isMoexChartShow, setIsMoexChartShow] = useState(props.widgetContentProps?.isMoexChartShow);

  const {
    chartContainerRef,
    dropDownOpen,
    setDropdownOpen,
    currentInstrument,
    handlerSaveAsExcel,
    setIsWidgetHeaderContextMenuOpen,
    isWidgetHeaderContextMenuOpen,
    onDropInstruments,
    addInstrumentFromModal,
    isOver,
  } = useChartComponentFacade(props);

  /* на дроп обновляем название бумаги в сторе
    и делаем апдейт на бэк, чтобы сохранить изменения
    при обновлении страницы */
  const { dropRef, isDragged } = useDropInstrument((dragData) => {
    onDropInstruments(dragData.properties.issKey, true);
  });

  const [isOpenEmptyAction, setIsOpenEmptyAction] = useState<boolean>(false);

  const openInstrumentModal = () => {
    setIsOpenEmptyAction(true);
    setIsWidgetHeaderContextMenuOpen(false);
  };

  // TODO: Зависит от сеток
  const { isDraggedOver } = useContext(WidgetEnvContext);
  const contractsInstrumentName = useWidgetHeaderName(currentInstrument.current);

  const instrumentName = indicativeData
    ? `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`
    : contractsInstrumentName;

  useEffect(() => {
    updateProperties((state) => {
      state.isMoexChartShow = isMoexChartShow;
    });
  }, [isMoexChartShow, updateProperties]);

  return (
    <DNDWrapper
      ref={dropRef}
      isOver={isOver}
      canDrop
    >
      <WidgetHeader
        {...props}
        dropdownOpen={dropDownOpen}
        setDropdownOpen={setDropdownOpen}
        itemsSearchIcon={[true]}
        handlerSaveAsExcel={indicativeData ? null : handlerSaveAsExcel}
        addToWidgetNamePrefix={instrumentName}
        setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
        isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
        openInstrumentsModal={openInstrumentModal}
      />
      <WidgetContentWrapper {...props}>
        <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
          {true && (
            <>
              <div style={{ padding: '8px 16px' }}>
                <Switch
                  onChange={setIsMoexChartShow}
                  value={isMoexChartShow}
                  checkedChildren="Moex chart"
                  unCheckedChildren="Traiding view chart"
                />
              </div>

              <LazyTREntry
                fullName={currentInstrument.current}
                widgetId={props.widgetId}
                isMoexChartShow={isMoexChartShow}
              />
            </>
          )}

          <div
            className="TVChartContainer"
            style={{
              width: '100%',
              height: '100%',
              pointerEvents: isDragged || isDraggedOver ? 'none' : 'inherit',
              display: !isMoexChartShow ? 'block' : 'none',
            }}
            ref={chartContainerRef}
          />

          {isOpenEmptyAction && (
            <InstrumentSearch
              setOpen={setIsOpenEmptyAction}
              isOpen={isOpenEmptyAction}
              variant="single"
              widgetId={props.widgetId}
              withNRD={false}
              addInstruments={addInstrumentFromModal}
            />
          )}
        </div>
      </WidgetContentWrapper>
    </DNDWrapper>
  );
};


import React from 'react';

import { ChartIndicativeData } from '../../types';

import { CompareModal } from './components/CompareModal';
import { useMoexChart } from './hooks';

import 'moex-chart/dist/styles.css';

import styles from './styles.module.scss';

type TRProps = {
  fullName: string;
  indicativeData?: ChartIndicativeData | undefined;
  widgetId: number;
  isMoexChartShow: boolean | undefined;
};

export default React.memo(({ fullName, indicativeData, widgetId, isMoexChartShow }: TRProps) => {
  const { containerRef, isCompareOpen, compareManagerRef, setIsCompareOpen } = useMoexChart({
    indicativeData,
    symbol: fullName,
  });

  return (
    <div style={{ maxHeight: 'calc(100% - 74px)', display: isMoexChartShow ? 'block' : 'none' }}>
      <div
        className={styles.wrap}
        ref={containerRef}
      />

      {isCompareOpen && (
        <CompareModal
          onClose={() => setIsCompareOpen(false)}
          compareManager={compareManagerRef}
          widgetId={widgetId}
          isOpen={isCompareOpen}
          setOpen={setIsCompareOpen}
        />
      )}
    </div>
  );
});