Загрузка данных
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;
}
interface CircularReferenceInfo {
path: string;
reference: string;
type: string;
}
const findCircularReference = (value: unknown): CircularReferenceInfo | null => {
const visited = new WeakMap<object, string>();
const visit = (current: unknown, path: string): CircularReferenceInfo | null => {
if (current === null || typeof current !== 'object') {
return null;
}
const previousPath = visited.get(current);
if (previousPath) {
return {
path,
reference: previousPath,
type: current.constructor?.name ?? 'Object',
};
}
visited.set(current, path);
for (const [key, nestedValue] of Object.entries(current)) {
const result = visit(nestedValue, `${path}.${key}`);
if (result) {
return result;
}
}
return null;
};
return visit(value, 'snapshot');
};
const getValueType = (value: unknown): string => {
if (value === null) {
return 'null';
}
if (typeof value !== 'object') {
return typeof value;
}
return value.constructor?.name ?? 'Object';
};
const getValueKeys = (value: unknown): string[] => {
if (value === null || typeof value !== 'object') {
return [];
}
return Object.keys(value);
};
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]);
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 circularReference = findCircularReference(snapshot);
if (circularReference) {
console.error(
'[MoexChart][snapshot circular reference]',
JSON.stringify(circularReference, null, 2),
);
console.log(
'[MoexChart][snapshot structure]',
JSON.stringify(
{
snapshotKeys: Object.keys(snapshot),
charts: snapshot.charts.map((chart, chartIndex) => ({
chartIndex,
keys: Object.keys(chart),
symbolId: chart.symbolId,
symbol: chart.symbol,
symbolName: chart.symbolName,
timeframe: chart.timeframe,
panes: chart.panes.map((pane, paneIndex) => ({
paneIndex,
keys: Object.keys(pane),
id: pane.id,
isMain: pane.isMain,
indicators: pane.indicators.map((indicator, indicatorIndex) => ({
indicatorIndex,
keys: Object.keys(indicator),
type: 'indicatorType' in indicator ? indicator.indicatorType : 'compare',
valueTypes: Object.fromEntries(
Object.entries(indicator).map(([key, value]) => [key, getValueType(value)]),
),
})),
drawings: pane.drawings.map((drawing, drawingIndex) => ({
drawingIndex,
keys: Object.keys(drawing),
id: drawing.id,
drawingName: drawing.drawingName,
stateType: getValueType(drawing.state),
stateKeys: getValueKeys(drawing.state),
})),
})),
})),
},
null,
2,
),
);
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: 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 = 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),
};
};