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


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

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 } from '../dataSourceProvide';

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

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

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

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

  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 timeframeRef = useRef<Timeframes | undefined>(moexChartState?.tf);
  const savedDataRef = useRef<string | undefined>(moexChartState?.savedData);
  const updateTimeframeRef = useRef<((tf: Timeframes) => void) | null>(null);

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

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

    timeframeRef.current = tf;

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

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

    if (!snapshot) {
      return;
    }

    const savedData = JSON.stringify({
      ...snapshot,
      charts: snapshot.charts.map((c) => ({
        ...c,
        panes: c.panes.map((p) => ({
          ...p,
          indicators: p.indicators.map((i) => ({
            ...i,
            dataSource: undefined, // dataSource пока не среиализуем
            config: undefined, // config пока не среиализуем
          })),
        })),
      })),
    });
    savedDataRef.current = savedData;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        tf: 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,
        timeframe,
      })),
    });

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

  useEffect(() => {
    if (!isMoexChartShow) {
      return;
    }
    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,
          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 -- исправим позже
  }, [symbol, indicativeData, isMoexChartShow]);

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


import React from 'react';

import { InstrumentSearch } from '@components/InstrumentSearch';

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

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

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

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

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

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

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

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

      {isSymbolSearchOpen && (
        <InstrumentSearch
          widgetId={widgetId}
          variant="single"
          isOpen={isSymbolSearchOpen}
          setOpen={setIsSymbolSearchOpen}
          // Временный костыль, пока не завезем свой поиск интструментов
          addInstruments={() => {
            // nothing
          }}
        />
      )}
    </div>
  );
});

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

import {
  AllFiltersVariants,
  FilterOptionsType,
  FiltersOptionsRecordType,
  FilterValuesRecordType,
  FilterValueType,
} from './filters';

export type HookBaseType<T, U> = {
  data: T;
  methods: U;
};

/** Поиск может быть двух видов либо одиночный, либо множественный */
export type SearchVariant = 'single' | 'multiply';

export type FiltersDataType = {
  selectedFilters?: FilterValuesRecordType;
  filterOptions?: FiltersOptionsRecordType;
  isExpandedFilters: boolean;
  searchString: string;
  isShowClearButton: boolean;
  isActiveOnly: boolean;
};

export type FiltersMethodsType = {
  handleFilterValue: (type: AllFiltersVariants, value: FilterValueType) => void;
  toggleFilters: () => void;
  handleSearchValue: (str: string) => void;
  clearFilters: (onlyAdditional?: boolean) => void;
  getOptionsForSelect: (type: AllFiltersVariants) => FilterOptionsType[];
  toggleIsActive: () => void;
};

export type TableDataType = {
  filteredInstruments?: Contract[];
  selectedInstrumentRows: Contract[] | [];
  isAllFiltersOff: boolean;
};

export type TableMethodsType = {
  selectInstrumentHandler: (inst: Contract) => void;
  removeInstrumentFromSelected: (inst: Contract) => void;
};

export type ActionsDataType = {
  isButtonActive: boolean;
};

export type ActionsMethodsType = {
  acceptHandler: () => void;
  cancelHandler: () => void;
};

export type UseDataReturnType = {
  filters: HookBaseType<FiltersDataType, FiltersMethodsType>;
  actions: HookBaseType<ActionsDataType, ActionsMethodsType>;
  table: HookBaseType<TableDataType, TableMethodsType>;
};

export type InstrumentSearchType = {
  variant: SearchVariant;
  widgetId: number;
  setOpen: (bool: boolean) => void;
  isOpen: boolean;
  addInstruments: (inst: Contract[]) => void;
  // withNRD?: boolean;

  /**
   * @deprecated
   *
   * TODO: убрать, когда разработаем свою модалку для moex_chart
   */
  customActionsFooterHandlers?: {
    handlePercent: (symbol: string) => void;
    handleNewScale: (symbol: string) => void;
    handleNewPanel: (symbol: string) => void;
  };
  isNewScaleDisabled?: boolean;
};

export type InstrumentType = Record<AllFiltersVariants, string | number | null | boolean>;


export type Contract = {
  assetCode: string;
  /** То же самое что boardId */
  board: string;
  ccy: null | string;
  currencyType: null | string;
  group: null | string;
  groupType: null | string;
  initialFaceValue: null | string;
  instrFullName: string;
  instrGroupType: string;
  instrId: number;
  instrIsin: null | string;
  instrIssuerId: null | number;
  instrIssuerName: null | string;
  instrName: string | null;
  instrSubtypeId: number | null;
  instrSubtypeName: string | null;
  instrType: string | null;
  isRepayment: boolean;
  issKey: string | null;
  key: string | null;
  latName: string;
  month: string | null;
  optionType: null | string;
  repoTerm: null | number;
  repoTermMin: number | null;
  repoTermMax: number | null;
  repoType: null | string;
  secName: null | string;
  shortName: string | null;
  /** То же самое что secId */
  symbol: string;
  term: null | string;
  termName?: string | null;
  tf: string | null;
  underlyingAsset: string;
  underlyingType?: string | null;
  volToday: number | null;
  year: string | null;
  displayName: string;
  isPrimaryBoard?: boolean | null;
  boardName: string | null;
  lastDelDate: null | number;
  country: string | null;
  faceUnit: string | null;
  isActive?: boolean;
  instrIssuerShortName?: string | null;
};