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


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

import { useCallback, useEffect, useRef, useState } from 'react';

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

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

import type {
  __CompareManager__,
  IMoexChart,
  SymbolInfoInput,
} from 'moex-chart';

interface TUseMoexChartProps {
  symbolInfo: SymbolInfoInput;
  indicativeData?: ChartIndicativeData;
}

export const useMoexChart = ({
  symbolInfo,
  indicativeData,
}: TUseMoexChartProps) => {
  const moexChartState = useSelectProperties(
    (widgetProperties: Partial<WidgetProperties>) =>
      widgetProperties.moexChartState,
  );

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

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

  const containerRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<MoexChart | null>(null);
  const compareManagerRef =
    useRef<__CompareManager__ | null>(null);

  const currentSymbolInfoRef =
    useRef<SymbolInfoInput>(symbolInfo);

  const timeframeRef = useRef<Timeframes | undefined>(
    moexChartState?.timeframe,
  );

  const savedDataRef = useRef<string | undefined>(
    moexChartState?.savedData,
  );

  const initialIntervalRef = useRef<Intervals | undefined>(
    moexChartState?.initialInterval,
  );

  const updateTimeframeRef = useRef<
    ((timeframe: Timeframes) => void) | null
  >(null);

  useEffect(() => {
    const currentSymbolInfo = currentSymbolInfoRef.current;

    if (
      currentSymbolInfo.symbolId === symbolInfo.symbolId &&
      currentSymbolInfo.symbol === symbolInfo.symbol &&
      currentSymbolInfo.symbolName === symbolInfo.symbolName
    ) {
      return;
    }

    currentSymbolInfoRef.current = symbolInfo;
    chartRef.current?.setSymbol(symbolInfo);
  }, [
    symbolInfo.symbol,
    symbolInfo.symbolId,
    symbolInfo.symbolName,
  ]);

  useEffect(() => {
    timeframeRef.current = moexChartState?.timeframe;
    savedDataRef.current = moexChartState?.savedData;
  }, [
    moexChartState?.savedData,
    moexChartState?.timeframe,
  ]);

  updateTimeframeRef.current = (
    timeframe: Timeframes,
  ): void => {
    if (timeframeRef.current === timeframe) {
      return;
    }

    timeframeRef.current = timeframe;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        timeframe,
      };
    });
  };

  const saveSnapshot = useCallback((): void => {
    const snapshot = chartRef.current?.getSnapshot();

    if (!snapshot) {
      return;
    }

    const savedData = JSON.stringify({
      ...snapshot,
      charts: snapshot.charts.map((chart) => ({
        ...chart,
        panes: chart.panes.map((pane) => ({
          ...pane,
          indicators: pane.indicators.map((indicator) => {
            const compareSeries =
              indicator.config?.series[0];

            const compareSymbolInfo =
              indicator.config?.symbolInfo;

            return {
              ...indicator,
              dataSource: undefined,
              config:
                indicator.indicatorType === undefined &&
                compareSymbolInfo &&
                indicator.config?.label &&
                compareSeries
                  ? {
                      symbolInfo: compareSymbolInfo,
                      label: indicator.config.label,
                      newPane: indicator.config.newPane,
                      series: [
                        {
                          name: compareSeries.name,
                          seriesOptions: {
                            priceScaleId:
                              compareSeries.seriesOptions
                                ?.priceScaleId,
                          },
                        },
                      ],
                    }
                  : undefined,
            };
          }),
        })),
      })),
    });

    if (savedDataRef.current === savedData) {
      return;
    }

    savedDataRef.current = savedData;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        timeframe:
          timeframeRef.current ?? Timeframes['1m'],
        savedData,
      };
    });
  }, [updateProperties]);

  const getSnapshotWithCurrentSettings = (
    snapshot:
      | typeof MOEX_CHART_CONFIG.snapshot
      | IMoexChart['snapshot'],
    timeframe: Timeframes,
    initialInterval?: Intervals,
  ): IMoexChart['snapshot'] => ({
    ...snapshot,
    charts: snapshot.charts.map((chartSnapshot) => ({
      ...chartSnapshot,
      ...currentSymbolInfoRef.current,
      timeframe,
      ...(initialInterval
        ? { interval: initialInterval }
        : {}),
    })),
  });

  const applySnapshot = (): void => {
    if (!savedDataRef.current || !chartRef.current) {
      return;
    }

    const savedSnapshot = JSON.parse(
      savedDataRef.current,
    ) as IMoexChart['snapshot'];

    const timeframe =
      timeframeRef.current ?? Timeframes['1m'];

    chartRef.current.setSnapshot(
      getSnapshotWithCurrentSettings(
        savedSnapshot,
        timeframe,
      ),
    );

    compareManagerRef.current =
      chartRef.current.getCompareManager();
  };

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

    if (!container) {
      return undefined;
    }

    const timeframe =
      timeframeRef.current ?? Timeframes['1m'];

    const initialInterval =
      initialIntervalRef.current;

    const savedSnapshot = savedDataRef.current
      ? (JSON.parse(
          savedDataRef.current,
        ) as IMoexChart['snapshot'])
      : MOEX_CHART_CONFIG.snapshot;

    const dataProvider = new DataSourceProvider();

    const chart = new MoexChart({
      ...MOEX_CHART_CONFIG,
      container,
      snapshot: getSnapshotWithCurrentSettings(
        savedSnapshot,
        timeframe,
        initialInterval,
      ),
      chartCollectionPreset: {
        ...MOEX_CHART_CONFIG.chartCollectionPreset,

        openCompareModal: () =>
          setIsCompareOpen(true),

        openSymbolSearchModal: () =>
          setIsSymbolSearchOpen(true),

        getDataSource: dataProvider.getDataSource(
          indicativeData,
          (nextTimeframe) => {
            updateTimeframeRef.current?.(
              nextTimeframe,
            );
          },
        ),

        startRealtime: (
          getSymbols,
          getTimeframe,
          update,
        ) =>
          dataProvider.startRealtime({
            getSymbols,
            getTimeframe,
            update,
            indicativeData,
          }),
      },
    });

    chartRef.current = chart;
    compareManagerRef.current =
      chart.getCompareManager();

    if (initialInterval) {
      initialIntervalRef.current = undefined;

      updateProperties((state) => {
        state.moexChartState = {
          ...state.moexChartState,
          initialInterval: undefined,
        };
      });
    }

    const intervalId = window.setInterval(() => {
      saveSnapshot();
    }, 1000);

    return () => {
      window.clearInterval(intervalId);

      saveSnapshot();

      chartRef.current = null;
      compareManagerRef.current = null;

      chart.destroy();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps -- график пересоздаётся только при изменении источника индикативных данных
  }, [indicativeData?.key]);

  return {
    containerRef,
    isCompareOpen,
    isSymbolSearchOpen,
    compareManagerRef,
    setIsCompareOpen,
    setIsSymbolSearchOpen,
    saveSnapshot,
    applySnapshot,
    hasSavedSnapshot: Boolean(
      moexChartState?.savedData,
    ),
  };
};



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

import type { Contract } from '@modules/contracts/types';
import type { WidgetProperties as BaseWidgetProperties } from '@modules/widgetProperties/types';
import type { Intervals, Timeframes } from 'moex-chart';

export interface WidgetProperties
  extends BaseWidgetProperties {
  chartState: {
    savedInstrument: Contract['issKey'] | null;
    savedSymbol?: Contract['symbol'];
    savedSymbolName?: Contract['displayName'];
    interval: string;
    savedData?: string;
  };

  indicativeData?: ChartIndicativeData;

  moexChartState?: {
    initialInterval?: Intervals;
    timeframe?: Timeframes;
    savedData?: string;
  };
}