Загрузка данных
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;
};