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


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

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

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

export type WidgetProperties = {
  chartState: {
    savedInstrument: Contract['issKey'] | null;
    interval: string;
    savedData?: string;
  };
  indicativeData?: ChartIndicativeData;
  moexChartState?: {
    timeframe: Timeframes;
    // interval: Intervals;
    // dateFormat: DateFormat;
    // timeFormat: TimeFormat;
    savedData?: string;
  };
};

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

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

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

import { WidgetProperties } from '../../../properties/types';

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

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

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

export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) => {
  const moexChartState = useSelectProperties((wProps: Partial<WidgetProperties>) => wProps.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<null | __CompareManager__>(null);
  const currentSymbolRef = useRef(symbol);

  const timeframeRef = useRef<Timeframes | undefined>(moexChartState?.timeframe);
  const savedDataRef = useRef<string | undefined>(moexChartState?.savedData);
  const updateTimeframeRef = useRef<((tf: Timeframes) => void) | null>(null);

  useEffect(() => {
    if (!symbol || currentSymbolRef.current === symbol) {
      return;
    }

    currentSymbolRef.current = symbol;
    chartRef.current?.setSymbol(symbol);
  }, [symbol]);

  const setMainSymbol = (nextSymbol: string) => {
    const normalizedSymbol = nextSymbol.trim();

    if (!normalizedSymbol || currentSymbolRef.current === normalizedSymbol) {
      return;
    }

    currentSymbolRef.current = normalizedSymbol;
    chartRef.current?.setSymbol(normalizedSymbol);
  };

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

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

    timeframeRef.current = timeframe;

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

  const saveSnapshot = () => {
    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];

            return {
              ...indicator,
              dataSource: undefined, // dataSource пока не среиализуем
              config:
                indicator.indicatorType === undefined && indicator.config?.label && compareSeries
                  ? {
                      label: indicator.config.label,
                      newPane: indicator.config.newPane,
                      series: [
                        {
                          name: compareSeries.name,
                          seriesOptions: {
                            priceScaleId: compareSeries.seriesOptions?.priceScaleId,
                          },
                        },
                      ],
                    }
                  : undefined,
            };
          }),
        })),
      })),
    });

    savedDataRef.current = savedData;

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

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

    const savedSnapshot = JSON.parse(savedDataRef.current) as IMoexChart['snapshot'];
    const timeframe = timeframeRef.current || Timeframes['1m'];

    chartRef.current.setSnapshot({
      ...savedSnapshot,
      charts: savedSnapshot.charts.map((chartSnapshot) => ({
        ...chartSnapshot,
        symbol: currentSymbolRef.current,
        timeframe,
      })),
    });

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

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

    if (!container) {
      return undefined;
    }

    const timeframe = timeframeRef.current || Timeframes['1m'];
    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: {
        ...savedSnapshot,
        charts: savedSnapshot.charts.map((chartSnapshot) => ({
          ...chartSnapshot,
          symbol: currentSymbolRef.current,
          timeframe,
        })),
      },
      chartCollectionPreset: {
        ...MOEX_CHART_CONFIG.chartCollectionPreset,
        openCompareModal: () => setIsCompareOpen(true),
        openSymbolSearchModal: () => setIsSymbolSearchOpen(true),
        getDataSource: dataProvider.getDataSource(indicativeData, (tf) => {
          updateTimeframeRef.current?.(tf);
        }),
        startRealtime: (getSymbols, getTimeframe, update) =>
          dataProvider.startRealtime({
            getSymbols,
            getTimeframe,
            update,
          }),
      },
    });

    chartRef.current = chart;
    compareManagerRef.current = chart.getCompareManager();
    const intervalId = setInterval(() => {
      saveSnapshot();
    }, 1000);
    return () => {
      chartRef.current = null;
      clearInterval(intervalId);
      compareManagerRef.current = null;
      chart.destroy();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- исправим позже
  }, [indicativeData]);

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

import { useCallback } from 'react';
import { useDispatch } from 'react-redux';

import { addContentPropsToWidget, updateWidgetContentProps } from '@store/slices/widgets';

import { useWidgetIdContext } from '@terminal/desktop/workspaces/default/components/Widget/context';

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

type UpdateProperties<T> = {
  /**
   * @param updater - Функция для частичного обновления свойств виджета
   * @example updateProperties((draftWidgetContentProps) => {
   *  draftWidgetContentProps.a.b.c = 'some value'
   * })
   */
  (updater: (draftWidgetContentProps: Partial<T>) => void): void;
};

/** Обновление свойств виджета.
 * Для корректной типизации нужно передать тип свойств виджета
 */
export const useChangeProperties = <T extends WidgetProperties>() => {
  const dispatch = useDispatch();
  const widgetId = useWidgetIdContext();

  /** Отправка свойств на сохранение */
  const writeProperties = useCallback(
    (props: Partial<T>) => {
      dispatch(
        addContentPropsToWidget({
          id: widgetId,
          widgetContentProps: props,
        }),
      );
    },
    [dispatch, widgetId],
  );

  /** Частичное обновление свойств виджета */
  const updateProperties = useCallback<UpdateProperties<T>>(
    (updater) => {
      dispatch(updateWidgetContentProps({ id: widgetId, updater }));
    },
    [dispatch, widgetId],
  );

  return {
    updateProperties,
    writeProperties,
  };
};