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