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


import { createGraphicWidget } from '@widgets/Chart/creator';

import type { ChartSeriesType } from '@mx-charts/charts';

const NTB_CHART_SERIES_TYPE: ChartSeriesType = 'Line';

export const openNtbChart = (instrumentId: string | null | undefined) => {
  if (!instrumentId) {
    return;
  }

  createGraphicWidget({
    chartState: {
      savedInstrument: instrumentId,
    },
    moexChartState: {
      timeframe: '1d',
      initialInterval: '1Y',
      initialChartSeriesType: NTB_CHART_SERIES_TYPE,
    },
  });
};

















import { useContextMenuOverlay } from '@uikit/ContextMenuOverlay';
import { composeIssKey } from '@widgets/ntb/AnaliticsCommodities/utils/composeIssKey';
import { downloadHistory } from '@widgets/ntb/AnaliticsCommodities/utils/downloadHistory';
import { openNtbChart } from '@widgets/ntb/shared/services/openChart';

import type { CommoditiesTableDataItem } from '../types';
import type { ContextMenuItem } from '@uikit/ContextMenu/types';

type UseContextMenuProps = {
  onShowHistory: (secId: string | undefined) => void;
};

export const useContextMenu = ({ onShowHistory }: UseContextMenuProps) => {
  const {
    onContextMenu,
    menuProps,
    payload: instrument,
  } = useContextMenuOverlay<CommoditiesTableDataItem>({
    shouldOpen: (record) => !!record?.securityId,
  });

  const instrumentId = instrument ? composeIssKey(instrument) : null;

  const items: ContextMenuItem[] = [
    { key: 'openChart', label: 'Отобразить в виджете График', onClick: () => openNtbChart(instrumentId) },
    {
      key: 'showHistory',
      label: 'Отобразить исторические данные',
      onClick: () => onShowHistory(instrument?.securityId),
    },
    {
      key: 'downloadHistory',
      label: 'Экспорт истории в Excel',
      onClick: () => downloadHistory(instrument?.securityId),
    },
  ];

  return { onContextMenu, menuProps, items };
};


















import classNames from 'classnames';
import React, { FC } from 'react';

import { type ResizableTableProps, Table } from '@components/Table';
import { useWidgetIdContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { ContextMenuOverlay } from '@uikit/ContextMenuOverlay';
import { Filters } from '@widgets/ntb/shared/components/Filters';
import { useFilters, useHandleRowClick } from '@widgets/ntb/shared/hooks';

import { TableKeys } from '../../const';
import { composeIssKey } from '../../utils/composeIssKey';

import styles from './CommoditiesTable.module.scss';
import { commoditiesColumnsConfig } from './config';
import {
  COMMODITIES_FILTERS_CONFIG,
  COMMODITIES_TABLE_CONFIGURABLE,
  DEFAULT_FILTERS_STATE,
  INITIAL_TABLE_STATE,
} from './const';
import { useCommodities } from './hooks/useCommodities';
import { useContextMenu } from './hooks/useContextMenu';
import { useFilterOptions } from './hooks/useFilterOptions';

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

type CommoditiesTableProps = {
  hidden: boolean;
  setSettingsComponent: ResizableTableProps<CommoditiesTableDataItem>['setSettingsComponent'];
  onShowHistory: (secId: string | undefined) => void;
  showFilters: boolean;
};

export const CommoditiesTable: FC<CommoditiesTableProps> = ({
  onShowHistory,
  showFilters,
  hidden,
  setSettingsComponent,
}) => {
  const widgetId = useWidgetIdContext();

  const { filters, onChangeFilter, onResetFilters, hasActiveFilters } = useFilters(DEFAULT_FILTERS_STATE);
  const { options, loading: optionsLoading } = useFilterOptions(filters);

  const { data, loading } = useCommodities(filters);

  const { handleRowClick } = useHandleRowClick({
    getIssKey: composeIssKey,
  });

  const { onContextMenu, menuProps, items } = useContextMenu({ onShowHistory });

  return (
    <div className={classNames(styles.tableWrapper, hidden ? styles.hide : styles.show)}>
      {showFilters && (
        <Filters
          config={COMMODITIES_FILTERS_CONFIG}
          onChange={onChangeFilter}
          onReset={onResetFilters}
          hasActiveFilters={hasActiveFilters}
          filters={filters}
          options={options}
          loading={optionsLoading}
        />
      )}
      <Table<CommoditiesTableDataItem>
        isLoading={loading}
        dataSource={data}
        widgetId={widgetId}
        columns={commoditiesColumnsConfig}
        externalHash={TableKeys.Main}
        initialState={INITIAL_TABLE_STATE}
        setSettingsComponent={setSettingsComponent}
        onRowClick={handleRowClick}
        onContextMenuClick={onContextMenu}
        configurable={COMMODITIES_TABLE_CONFIGURABLE}
      />
      <ContextMenuOverlay
        items={items}
        {...menuProps}
      />
    </div>
  );
};

















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

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

import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { useWidgetHeaderName } from '@utils/useWidgetName';

import useChartComponentFacade from './hooks/useChartComponentFacade';

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

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

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

  const {
    dropDownOpen,
    setDropdownOpen,
    currentSymbolInfo,
    setIsWidgetHeaderContextMenuOpen,
    isWidgetHeaderContextMenuOpen,
    onDropInstrument,
    addInstrumentFromModal,
    isOver,
  } = useChartComponentFacade(props);

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

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

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

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

  const symbolInfo = indicativeData
    ? {
        symbolId: indicativeData.key,
        symbol: indicativeData.secId,
        symbolName: `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`,
      }
    : currentSymbolInfo;

  const instrumentName = symbolInfo.symbolName || contractsInstrumentName || symbolInfo.symbol || symbolInfo.symbolId;

  return (
    <DNDWrapper
      ref={dropRef}
      isOver={isOver}
      canDrop
    >
      <WidgetHeader
        {...props}
        dropdownOpen={dropDownOpen}
        setDropdownOpen={setDropdownOpen}
        handlerSaveAsExcel={features.hideSaveAsExcel ? undefined : null}
        addToWidgetNamePrefix={instrumentName}
        setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
        isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
        openInstrumentsModal={openInstrumentModal}
      />
      <WidgetContentWrapper {...props}>
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            height: '100%',
            pointerEvents: isDraggedOver ? 'none' : 'inherit',
          }}
        >
          <MoexChart
            symbolInfo={symbolInfo}
            indicativeData={indicativeData}
            widgetId={props.widgetId}
            addInstrumentFromModal={addInstrumentFromModal}
          />

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

















import { Intervals, MoexChart, Timeframes } from '@mx-charts/charts';
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__, ChartSeriesType, IMoexChart, SymbolInfoInput } from '@mx-charts/charts';

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

interface SnapshotSettings {
  timeframe: Timeframes;
  interval?: Intervals;
  chartSeriesType?: ChartSeriesType;
}

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 initialChartSeriesTypeRef = useRef<ChartSeriesType | undefined>(moexChartState?.initialChartSeriesType);
  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]);

  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);

    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, interval, chartSeriesType }: SnapshotSettings,
  ): IMoexChart['snapshot'] => ({
    ...snapshot,
    charts: snapshot.charts.map((chartSnapshot) => ({
      ...chartSnapshot,
      ...currentSymbolInfoRef.current,
      timeframe,
      ...(interval ? { interval } : {}),
      ...(chartSeriesType ? { chartSeriesType } : {}),
    })),
  });

  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 initialChartSeriesType = initialChartSeriesTypeRef.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,
        interval: initialInterval,
        chartSeriesType: initialChartSeriesType,
      }),
      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 || initialChartSeriesType) {
      initialIntervalRef.current = undefined;
      initialChartSeriesTypeRef.current = undefined;

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

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

    return () => {
      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 { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';

import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';

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

import { DEFAULT_SYMBOL } from '../const';

import { useChartPublicContext } from './useChartPublicContext';

import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps, SelectedInstrument } from '../types';

import type { SymbolInfoInput } from '@mx-charts/charts';
import type { Dispatch, SetStateAction } from 'react';
import type { Widget } from 'types/Widgets';

interface UseChartComponentFacadeReturn {
  dropDownOpen: boolean;
  setDropdownOpen: Dispatch<SetStateAction<boolean>>;
  currentSymbolInfo: SymbolInfoInput;
  isWidgetHeaderContextMenuOpen: boolean;
  setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
  onDropInstrument: (symbolId: string, withUpdate?: boolean) => void;
  addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
  isOver: boolean;
}

type SaveContentPropsOptions = Partial<WidgetProperties['chartState']> & {
  withUpdate?: boolean;
  cleanIndicativeData?: boolean;
};

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

  const dispatch = useDispatch();

  const [isOver, setIsOver] = useState(false);
  const [dropDownOpen, setDropdownOpen] = useState(false);
  const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);

  const widgetProperties = useAppSelect(
    (state) => state.widgets.widgets.find(({ id }) => id === widgetId)?.widgetContentProps,
  ) as WidgetProperties | undefined;

  // TODO временно реф из-за непредсказуемого изменения если используется useState,
  // нужен рефакторинг и вернуть обратно useState
  const currentSymbolInfoRef = useRef<SymbolInfoInput>({
    symbolId: widgetProperties?.chartState?.savedInstrument ?? DEFAULT_SYMBOL,
    symbol: widgetProperties?.chartState?.savedSymbol,
    symbolName: widgetProperties?.chartState?.savedSymbolName,
  });

  const [currentSymbolInfo, setCurrentSymbolInfo] = useState<SymbolInfoInput>(currentSymbolInfoRef.current);

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

  const triggerRelatedWidgetsToUpdateRef = useRef(triggerRelatedWidgetsToUpdate);

  useEffect(() => {
    triggerRelatedWidgetsToUpdateRef.current = triggerRelatedWidgetsToUpdate;
  }, [triggerRelatedWidgetsToUpdate]);

  const saveContentProps = useCallback(
    (value: SaveContentPropsOptions): void => {
      const { withUpdate, cleanIndicativeData, ...chartStateValue } = value;

      const oldProps = (getStore().getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget | undefined)
        ?.widgetContentProps;

      if (!oldProps) {
        return;
      }

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

  const onInstrumentChange = useCallback(
    (symbolInfo: SymbolInfoInput, withUpdate?: boolean, unbind = true): void => {
      const symbolId = symbolInfo.symbolId.trim();

      if (!symbolId) {
        return;
      }

      setIsWidgetHeaderContextMenuOpen(false);

      const currentValue = currentSymbolInfoRef.current;
      const symbolIdChanged = currentValue.symbolId !== symbolId;

      const symbol = symbolInfo.symbol?.trim() || (symbolIdChanged ? undefined : currentValue.symbol);
      const symbolName = symbolInfo.symbolName?.trim() || (symbolIdChanged ? undefined : currentValue.symbolName);

      const nextSymbolInfo: SymbolInfoInput = {
        symbolId,
        symbol,
        symbolName,
      };

      const symbolChanged = currentValue.symbol !== symbol;
      const symbolNameChanged = currentValue.symbolName !== symbolName;

      if (!symbolIdChanged && !symbolChanged && !symbolNameChanged) {
        return;
      }

      currentSymbolInfoRef.current = nextSymbolInfo;
      setCurrentSymbolInfo(nextSymbolInfo);

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

      // при смене инструмента очищаем индикативные данные виджета график
      // т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
      saveContentProps({
        savedInstrument: symbolId,
        savedSymbol: symbol,
        savedSymbolName: symbolName,
        withUpdate,
        cleanIndicativeData: symbolIdChanged,
      });

      if (symbolIdChanged) {
        triggerRelatedWidgetsToUpdateRef.current(symbolId);
      }
    },
    [dispatch, saveContentProps, widgetId],
  );

  const addInstrumentFromModal = useCallback(
    (instruments: SelectedInstrument[]): void => {
      const instrument = instruments[0];

      if (!instrument?.issKey) {
        return;
      }

      onInstrumentChange({
        symbolId: instrument.issKey,
        symbol: instrument.symbol,
        symbolName: instrument.displayName,
      });
    },
    [onInstrumentChange],
  );

  const onInstrumentChangeFromBind = useCallback(
    (symbolInfo: SymbolInfoInput): void => {
      onInstrumentChange(symbolInfo, true, false);
    },
    [onInstrumentChange],
  );

  const onDropInstrument = useCallback(
    (symbolId: string, withUpdate?: boolean): void => {
      onInstrumentChange({ symbolId }, withUpdate);
    },
    [onInstrumentChange],
  );

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

  useEffect(() => {
    triggerRelatedWidgetsToUpdateRef.current(currentSymbolInfoRef.current.symbolId);
  }, []);

  useEffect(() => {
    const unsubscribe = communicator.listen(
      {
        messageType: CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT,
      },
      (message) => {
        const symbolId = (message as Record<number, string>)[widgetId];

        if (symbolId) {
          onInstrumentChange({ symbolId });
        }
      },
    );

    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();
    };
  }, [onInstrumentChange, widgetId]);

  return {
    dropDownOpen,
    setDropdownOpen,
    currentSymbolInfo,
    isWidgetHeaderContextMenuOpen,
    setIsWidgetHeaderContextMenuOpen,
    onDropInstrument,
    addInstrumentFromModal,
    isOver,
  };
}

















import React from 'react';

import { SymbolSearchModal } from '@widgets/Chart/components/MoexChart/components/SymbolSearchModal';

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

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

import type { SymbolInfoInput } from '@mx-charts/charts';

import '@mx-charts/charts/dist/styles.css';

interface TRProps {
  symbolInfo: SymbolInfoInput;
  indicativeData?: ChartIndicativeData;
  widgetId: number;
  addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
}

function MoexChart({ symbolInfo, indicativeData, widgetId, addInstrumentFromModal }: TRProps) {
  const {
    containerRef,
    isCompareOpen,
    isSymbolSearchOpen,
    compareManagerRef,
    setIsCompareOpen,
    setIsSymbolSearchOpen,
  } = useMoexChart({
    symbolInfo,
    indicativeData,
  });

  const handleSymbolChange = (instrument: SelectedInstrument): void => {
    addInstrumentFromModal([instrument]);
  };

  return (
    <div
      style={{
        flex: '1 1 0',
        minHeight: 0,
        minWidth: 0,
      }}
    >
      <div ref={containerRef} />

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

      {isSymbolSearchOpen && (
        <SymbolSearchModal
          widgetId={widgetId}
          isOpen={isSymbolSearchOpen}
          setOpen={setIsSymbolSearchOpen}
          onSymbolChange={handleSymbolChange}
        />
      )}
    </div>
  );
}

export default React.memo(MoexChart);