Загрузка данных
import { MoexChart, Timeframes } from 'moex-chart';
// eslint-disable-next-line import/no-unresolved -- MOEX_CHART
import { CompareManager } from 'moex-chart/dist/types/core/CompareManager';
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, DataSourceProvider } from '../dataSourceProvide';
import type { IMoexChart } from 'moex-chart';
type TUseMoexChartProps = {
symbol: string;
indicativeData: ChartIndicativeData | undefined;
};
export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) => {
const moexChartTimeFrame = useSelectProperties((wProps: Partial<ChartProps>) => wProps.moexChartState?.tf);
const { updateProperties } = useChangeProperties<ChartProps>();
const [isCompareOpen, setIsCompareOpen] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const compareManagerRef = useRef<null | CompareManager>(null);
const timeframeRef = useRef<Timeframes | undefined>(moexChartTimeFrame);
const updateTimeframeRef = useRef<(tf: Timeframes) => void>(() => undefined);
useEffect(() => {
timeframeRef.current = moexChartTimeFrame;
}, [moexChartTimeFrame]);
updateTimeframeRef.current = (tf: Timeframes) => {
if (timeframeRef.current === tf) {
return;
}
timeframeRef.current = tf;
updateProperties((state) => {
if (state.moexChartState) {
state.moexChartState.tf = tf;
return;
}
state.moexChartState = {
tf,
};
});
};
useEffect(() => {
const container = containerRef.current;
if (!container) {
return undefined;
}
const initialTimeframe = timeframeRef.current || Timeframes['1m'];
const charts: IMoexChart['snapshot']['charts'] = MOEX_CHART_CONFIG.snapshot.charts.map(
(chartSnapshot): IMoexChart['snapshot']['charts'][number] => ({
...chartSnapshot,
symbol,
timeframe: initialTimeframe,
}),
);
const chart = new MoexChart({
...MOEX_CHART_CONFIG,
container,
snapshot: {
...MOEX_CHART_CONFIG.snapshot,
charts,
},
chartCollectionPreset: {
...MOEX_CHART_CONFIG.chartCollectionPreset,
openCompareModal: () => setIsCompareOpen(true),
getDataSource: DataSourceProvider.getDataSource(indicativeData, (tf) => {
updateTimeframeRef.current(tf);
}),
startRealtime: (getSymbols, getTimeframe, update) =>
dataSourceProvider.startRealtime({
getSymbols,
getTimeframe,
update,
}),
},
});
compareManagerRef.current = chart.getCompareManager();
return () => {
compareManagerRef.current = null;
chart.destroy();
};
}, [symbol, indicativeData]);
return {
containerRef,
isCompareOpen,
compareManagerRef,
setIsCompareOpen,
};
};
import { Timeframes } from 'moex-chart';
import { Contract } from '@modules/contracts';
import { WidgetContentBasicProps } from 'types/Widgets';
export type ChartIndicativeData = {
secId: string;
instrumentName: string;
settlement: string;
firmName: string;
key: string;
};
export type ResolutionMap = {
'1': string;
'5': string;
'10': string;
'15': string;
'30': string;
'45': string;
'60': string;
'240': string;
'1D': string;
'7D': string;
'1M': string;
'3M': string;
};
export type ChartProps = {
chartState: {
savedInstrument: Contract['issKey'];
interval: string;
savedData: string;
};
indicativeData?: ChartIndicativeData;
isMoexChartShow?: boolean;
moexChartState?: {
tf: Timeframes;
};
};
export type ChartContainerProps = WidgetContentBasicProps<ChartProps>;
import isEqual from 'lodash/isEqual';
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { candlesController } from '@api/controllers/candles';
import { uiConfig } from '@configs/manager';
import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import {
ChartingLibraryWidgetOptions,
IChartingLibraryWidget,
ResolutionString,
widget,
} from '@modules/charting_library/charting_library';
import { Contract } from '@modules/contracts';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';
import { getState } from '@store/store';
import { chartToReqTimeConverter } from '@utils/chartToReqTimeConverter';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';
import { DAY_LINE_ON_CHART_BOARDS, DEFAULT_SYMBOL, MINUTE_CANDLES_ON_CHART_BOARDS } from '../const';
import useDatafeed from '../datafeed';
import { ResolutionMap } from '../types';
import { isIndicativeTicker } from '../utils/isIndicativeTicker';
import { mapResolutionIntervals } from '../utils/resolutionIntervals';
import { formatToDateTime } from '../utils/utils';
import { useChartPublicContext } from './useChartPublicContext';
import type { ChartContainerProps, ChartProps } from '../types';
import type { Candles } from 'types/Candles';
import type { ContractsType } from 'types/Contracts';
import type { Widget } from 'types/Widgets';
enum SeriesStyle {
Bars = 0,
Candles = 1,
Line = 2,
Area = 3,
HeikenAshi = 8,
HollowCandles = 9,
Baseline = 10,
HiLo = 12,
Column = 13,
Renko = 4,
Kagi = 5,
PointAndFigure = 6,
LineBreak = 7,
};
interface UseChartComponentFacadeReturn {
chartContainerRef: MutableRefObject<HTMLDivElement>;
savedTvWidget: IChartingLibraryWidget | null;
dropDownOpen: boolean;
setDropdownOpen: Dispatch<SetStateAction<boolean>>;
currentInstrument: MutableRefObject<string>;
handlerSaveAsExcel: (() => void) | undefined;
isWidgetHeaderContextMenuOpen: boolean;
setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
onDropInstruments: (val: string, withUpdate?: boolean) => void;
addInstrumentFromModal: (instruments: Contract[]) => void;
isOver: boolean;
}
const defaultOptions = {
symbol: DEFAULT_SYMBOL,
interval: '1' as ResolutionString,
datafeedUrl: 'https://demo_feed.tradingview.com',
libraryPath: `${uiConfig.staticContextPath || ''}/charting_library/`,
chartsStorageUrl: 'https://saveload.tradingview.com',
chartsStorageApiVersion: '1.1' as const,
clientId: 'tradingview.com',
userId: 'public_user_id',
fullscreen: false,
autosize: true,
studiesOverrides: {},
};
export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
const { widgetId } = props;
const [isOver, setIsOver] = useState<boolean>(false);
const chartContainerRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement>;
const tvWidgetRef = useRef<IChartingLibraryWidget | null>(null);
const dispatch = useDispatch();
const indicativeData = props.widgetContentProps?.indicativeData;
const chartPrevStateRef = useRef<object>({});
// используется для определения источника отображаемого в виджете инструмента (из привязки или нет)
const isInstrumentChangedFromBinding = useRef<boolean>(false);
const prevTimeframe = useRef<string>('');
const lastIntervalChangeTime = useRef<number | null>(null);
const timeDiffOfIntervalChangeTime = useRef<number | null>(null);
const [savedTvWidget, setTvWidget] = useState<IChartingLibraryWidget | null>(null);
/**
* Копия chartIsReady тк некоторые useCallback хранят старое значение переменной.
* Полностью не переходим на реф тк стейт используется в useEffect.
* Рекомендуется использовать реф, за исключением если требуется срабатывать useEffect на chartIsReady.
*/
const isChartIsReadyFromRef = useRef<boolean>(false);
const [chartIsReady, setChartIsReadyFromUseState] = useState(false);
const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);
const setChartIsReady = useCallback((isReady: boolean) => {
setChartIsReadyFromUseState(isReady);
isChartIsReadyFromRef.current = isReady;
}, []);
useEffect(() => {
tvWidgetRef.current = savedTvWidget;
}, [savedTvWidget]);
const language = useAppSelect((state) => state.localisation.localisation);
const { chartState } = useAppSelect((state) => state.widgets.widgets.find(({ id }) => id === widgetId))
?.widgetContentProps as ChartProps;
// TODO временно реф из-за непредсказуемого изменения если используется useState,
// нужен рефакторинг и вернуть обратно useState
const currentInstrument = useRef<ContractsType['issKey']>(chartState?.savedInstrument ?? defaultOptions.symbol);
const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
widgetId,
});
// Функция работает внутри интервала, поэтому используем не хуковый доступ к стору
const saveContentProps = (
val: Partial<ChartProps['chartState']> & { withUpdate?: boolean; cleanIndicativeData?: boolean },
): void => {
// если вызываем функцию с withUpdate, то оправляем в стор и делаем запрос на изменения
const { withUpdate, cleanIndicativeData, ...chartStateVal } = val;
const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;
if (oldProps) {
dispatch(
addContentPropsToWidget({
id: widgetId,
withoutSend: !withUpdate,
widgetContentProps: {
chartState: { ...oldProps.chartState, ...chartStateVal },
// при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
},
}),
);
}
};
const handlerSaveAsExcel = () => {
const chart = savedTvWidget?.activeChart();
const visibleRange = chart ? chart.getVisibleRange() : null;
const checkedInterval = chart?.resolution();
// Интервал из которого фактически будут формироваться свечи на графике
const interval = mapResolutionIntervals((checkedInterval ?? defaultOptions.interval) as keyof ResolutionMap);
const timezoneTitle = tvWidgetRef.current?.activeChart().getTimezoneApi().getTimezone()?.title;
let timezoneAsNumber;
if (timezoneTitle === 'Биржа') {
timezoneAsNumber = 3;
} else if (timezoneTitle && timezoneTitle.includes(':')) {
// если часовой пояс с дробным числом в формате UTC то по договоренности с бэком берем только целую часть
timezoneAsNumber = Number(
timezoneTitle
.split('UTC' as string)[1]
.split(')')[0]
.split(':')[0],
);
} else {
timezoneAsNumber = timezoneTitle ? Number(timezoneTitle.split('UTC')[1].split(')')[0]) : 3;
}
if (visibleRange) {
const startDateTime = formatToDateTime(visibleRange.from);
const endDateTime = formatToDateTime(visibleRange.to);
const data: Candles = {
secId: currentInstrument.current.replace(/:/g, '.'),
from: startDateTime,
till: endDateTime,
interval: chartToReqTimeConverter(interval ?? defaultOptions.interval),
value: chartToReqTimeConverter(checkedInterval ?? defaultOptions.interval),
offset: timezoneAsNumber,
};
candlesController
.upload(data)
.then((response) => {
// eslint-disable-next-line @typescript-eslint/no-shadow -- TODO исправить reasign
const { data, headers } = response;
const blob = new Blob([data]);
const fileName = headers['content-disposition']?.match(/filename="(.+)"/)?.[1] || 'default_filename';
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
link.click();
})
.catch((error) => {
// eslint-disable-next-line no-console -- Обработка исключения
console.error('Error uploading data:', error);
});
} else {
// eslint-disable-next-line no-console -- Обработка исключения
console.error('Chart is undefined or visible range is not available.');
}
};
useEffect(() => {
triggerRelatedWidgetsToUpdate(currentInstrument.current);
const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;
if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
return;
}
if (currentInstrument.current !== savedActiveChart.symbol()) {
savedActiveChart.setSymbol(currentInstrument.current);
}
// [currentInstrument.current] в зависимостях
// т.к. здесь не нужен ререндер потому что достаточно прокинуть в TV новое значение
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, [currentInstrument.current, chartIsReady]);
const onInstrumentChangeCallback = useCallback((prevInstr: string, newInstr: string) => {
if (prevInstr === newInstr || !isChartIsReadyFromRef.current) {
return;
}
const newBoardInLowerCase = newInstr.split(':')[1]?.toLowerCase() ?? '';
const prevBoardInLowerCase = prevInstr.split(':')[1]?.toLowerCase() ?? '';
// для двух специфичных случаев точечно изменяем тип линии и таймфрейм
if (
((prevInstr === DEFAULT_SYMBOL ||
!DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase) ||
!MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase)) &&
DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase)) ||
(MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase) &&
DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase))
) {
tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
tvWidgetRef.current?.activeChart().setResolution('1D' as ResolutionString);
}
if (
MINUTE_CANDLES_ON_CHART_BOARDS.includes(newBoardInLowerCase) &&
DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase)
) {
tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Candles);
tvWidgetRef.current?.activeChart().setResolution('1' as ResolutionString);
}
// для индикатива при выборе из Поиска инструментов и после перезагрузки устанавливаем
// нужные настройки графика
if (isIndicativeTicker(newInstr)) {
tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
tvWidgetRef.current?.activeChart().setResolution('60' as ResolutionString);
}
}, []);
const onInstrumentChange = useCallback(
(newVal: string, withUpdate?: boolean, unbind = true): void => {
const prev = currentInstrument.current;
currentInstrument.current = newVal;
if (unbind) {
dispatch(unbindWidgets({ widgetId }));
}
// при смене инструмента очищаем индикативные данные виджета график
// т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
saveContentProps({ savedInstrument: newVal, withUpdate, cleanIndicativeData: true });
setIsWidgetHeaderContextMenuOpen(false);
onInstrumentChangeCallback(prev, newVal);
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
[onInstrumentChangeCallback, chartIsReady],
);
const addInstrumentFromModal = useCallback(
(instruments: Pick<Contract, 'issKey'>[]) => {
const instr = instruments[0];
isInstrumentChangedFromBinding.current = false;
if (instr.issKey !== null) {
onInstrumentChange(instr.issKey);
}
},
[onInstrumentChange],
);
const onInstrumentChangeFromBind = useCallback(
(instrumentId: string) => {
isInstrumentChangedFromBinding.current = true;
onInstrumentChange(instrumentId, true, false);
},
[onInstrumentChange],
);
const onDropInstruments = useCallback((val: string, withUpdate?: boolean): void => {
isInstrumentChangedFromBinding.current = false;
onInstrumentChange(val, withUpdate);
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, []);
useChartPublicContext({
setCurrInstrument: onInstrumentChangeFromBind,
widgetId,
getMasterInstrumentFromPublicContext,
});
const { datafeed, windowIntervalRequester } = useDatafeed(
tvWidgetRef,
prevTimeframe,
timeDiffOfIntervalChangeTime,
isInstrumentChangedFromBinding,
indicativeData,
);
const createWidget = () => {
if (!chartContainerRef.current) {
return;
}
const todayData = new Date();
const halfYearAgo = new Date();
halfYearAgo.setMonth(halfYearAgo.getMonth() - 6);
const threeMonthsAgo = new Date();
threeMonthsAgo.setDate(threeMonthsAgo.getDate() - 90);
const oneDayAgo = new Date();
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
const thisYearStartDate = new Date();
thisYearStartDate.setMonth(0);
thisYearStartDate.setDate(1);
const diffBetween = todayData.getTime() - thisYearStartDate.getTime();
const dayDiff = diffBetween / 1000 / 3600 / 24;
const minutesDiff = diffBetween / 1000 / 60;
const ytlResolution = `${dayDiff > 0 ? dayDiff : minutesDiff}${dayDiff > 0 ? 'D' : ''}` as ResolutionString;
const widgetOptions: ChartingLibraryWidgetOptions = {
symbol: currentInstrument.current ?? chartState?.savedInstrument ?? (defaultOptions.symbol as string),
// BEWARE: no trailing slash is expected in feed URL
// tslint:disable-next-line:no-any
datafeed,
interval: (chartState?.interval ?? defaultOptions.interval) as ChartingLibraryWidgetOptions['interval'],
container: chartContainerRef.current,
library_path: defaultOptions.libraryPath as string,
locale: language,
timezone: 'Europe/Moscow',
charts_storage_url: defaultOptions.chartsStorageUrl,
charts_storage_api_version: defaultOptions.chartsStorageApiVersion,
client_id: defaultOptions.clientId,
time_frames: [
{ text: '1D', resolution: '60' as ResolutionString, description: '1d' },
{ text: '5d', resolution: '60' as ResolutionString, description: '5d' },
{ text: '1M', resolution: 'D' as ResolutionString, description: '1M' },
{ text: '6M', resolution: 'D' as ResolutionString, description: '6M' },
{
text: ytlResolution,
resolution: '7D' as ResolutionString,
description: 'YTL',
title: 'YTD',
},
{ text: '1Y', resolution: '7D' as ResolutionString, description: '6M' },
{ text: '5Y', resolution: '1M' as ResolutionString, description: '6M' },
{
text: '1000y',
resolution: '7D' as ResolutionString,
description: 'All',
title: 'All',
},
],
user_id: defaultOptions.userId,
fullscreen: defaultOptions.fullscreen,
autosize: defaultOptions.autosize,
symbol_search_request_delay: 1000,
enabled_features: ['study_templates', 'hide_left_toolbar_by_default', 'header_compare', 'header_indicators'],
disabled_features: ['use_localstorage_for_settings'],
custom_css_url: 'css/customstyles.css',
theme: 'Dark',
save_load_adapter: {
charts: [],
studyTemplates: [],
drawingTemplates: [],
getAllCharts() {
return Promise.resolve(this.charts);
},
removeChart(id: number) {
for (let i = 0; i < this.charts.length; i += 1) {
if (this.charts[i].id === id) {
this.charts.splice(i, 1);
return Promise.resolve();
}
}
return Promise.reject();
},
saveChart(chartData: Record<string, unknown>) {
if (!chartData.id) {
// eslint-disable-next-line no-param-reassign -- Ругается.
chartData.id = Math.random().toString();
} else {
this.removeChart(chartData.id);
}
// eslint-disable-next-line no-param-reassign -- Ругается.
chartData.timestamp = new Date().valueOf();
this.charts.push(chartData);
return Promise.resolve(chartData.id);
},
getChartContent(id: number) {
for (let i = 0; i < this.charts.length; i += 1) {
if (this.charts[i].id === id) {
return Promise.resolve(this.charts[i].content);
}
}
// eslint-disable-next-line no-console -- Обработка исключения
console.error('error');
return Promise.reject();
},
removeStudyTemplate(studyTemplateData: Record<string, unknown>) {
for (let i = 0; i < this.studyTemplates.length; i += 1) {
if (this.studyTemplates[i].name === studyTemplateData.name) {
this.studyTemplates.splice(i, 1);
return Promise.resolve();
}
}
return Promise.reject();
},
getStudyTemplateContent(studyTemplateData: Record<string, unknown>) {
for (let i = 0; i < this.studyTemplates.length; i += 1) {
if (this.studyTemplates[i].name === studyTemplateData.name) {
return Promise.resolve(this.studyTemplates[i]);
}
}
// eslint-disable-next-line no-console -- Обработка исключения
console.error('st: error');
return Promise.reject();
},
saveStudyTemplate(studyTemplateData: Record<string, unknown>) {
for (let i = 0; i < this.studyTemplates.length; i += 1) {
if (this.studyTemplates[i].name === studyTemplateData.name) {
this.studyTemplates.splice(i, 1);
break;
}
}
this.studyTemplates.push(studyTemplateData);
return Promise.resolve();
},
getAllStudyTemplates() {
return Promise.resolve(this.studyTemplates);
},
removeDrawingTemplate(toolName: string, templateName: string) {
for (let i = 0; i < this.drawingTemplates.length; i += 1) {
if (this.drawingTemplates[i].name === templateName) {
this.drawingTemplates.splice(i, 1);
return Promise.resolve();
}
}
return Promise.reject();
},
loadDrawingTemplate(toolName: string, templateName: string) {
for (let i = 0; i < this.drawingTemplates.length; i += 1) {
if (this.drawingTemplates[i].name === templateName) {
return Promise.resolve(this.drawingTemplates[i].content);
}
}
// eslint-disable-next-line no-console -- Обработка исключения
console.error('drawing: error');
return Promise.reject();
},
saveDrawingTemplate(toolName: string, templateName: string, content: unknown) {
for (let i = 0; i < this.drawingTemplates.length; i += 1) {
if (this.drawingTemplates[i].name === templateName) {
this.drawingTemplates.splice(i, 1);
break;
}
}
this.drawingTemplates.push({ name: templateName, content });
return Promise.resolve();
},
getDrawingTemplates() {
return Promise.resolve(this.drawingTemplates.map((template: Record<string, unknown>) => template.name));
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Ругается.
} as any,
overrides: {
'mainSeriesProperties.lineStyle.color': '#576DDB',
'paneProperties.legendProperties.showSeriesTitle': false,
},
};
if (chartState && chartState.savedData) {
try {
widgetOptions.saved_data = JSON.parse(chartState.savedData);
} catch {
widgetOptions.saved_data = undefined;
}
}
// eslint-disable-next-line new-cap -- Ругается
const tvWidget = new widget(widgetOptions);
tvWidget.onChartReady(() => {
setChartIsReady(true);
tvWidget.chart().getTimezoneApi().setTimezone('Europe/Moscow');
tvWidget
?.activeChart()
?.onIntervalChanged()
?.subscribe(null, (newInterval) => {
const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;
prevTimeframe.current = oldProps.chartState.interval;
const now = Date.now();
if (lastIntervalChangeTime.current) {
timeDiffOfIntervalChangeTime.current = now - lastIntervalChangeTime.current;
}
lastIntervalChangeTime.current = now;
if (oldProps) {
dispatch(
addContentPropsToWidget({
id: widgetId,
widgetContentProps: {
chartState: {
...oldProps.chartState,
interval: newInterval,
},
},
}),
);
}
});
// tvWidget.chart().getTimezoneApi().setTimezone("Europe/Moscow");
tvWidget
.activeChart()
.onSymbolChanged()
.subscribe(null, () => {
// если инструмент из индикативныз котировок то его не надо отдельно созранять через onInstrumentChange
if (indicativeData) {
return;
}
if (currentInstrument.current === tvWidget.activeChart().symbol()) {
return;
}
// если инструмент не из привязки то обновляем значение и отвязываем привязку
if (!isInstrumentChangedFromBinding.current) {
onInstrumentChange(tvWidget.activeChart().symbol(), false, true);
}
});
});
setTvWidget(tvWidget);
const counter = setInterval(() => {
if (!chartContainerRef.current || !tvWidgetRef.current) {
if (!chartContainerRef.current) {
clearInterval(counter);
}
return;
}
tvWidget?.save((state) => {
if (chartPrevStateRef.current && !isEqual(state, chartPrevStateRef.current)) {
chartPrevStateRef.current = state;
saveContentProps({
interval: tvWidget.symbolInterval().interval,
savedData: JSON.stringify(state),
});
}
});
}, 1000);
};
const [dropDownOpen, setDropdownOpen] = useState(false);
useEffect(() => {
const unsubscribe = communicator.listen({ messageType: CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT }, (message) => {
const issKey = (message as Record<number, string>)[widgetId];
if (issKey) {
addInstrumentFromModal([{ issKey }]);
}
});
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();
};
}, [addInstrumentFromModal, widgetId]);
useEffect(
() => () => {
Object.values(windowIntervalRequester).forEach(clearInterval);
},
[windowIntervalRequester],
);
useEffect(
() => () => {
if (savedTvWidget !== null) {
savedTvWidget.remove();
setTvWidget(null);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
[],
);
useEffect(() => {
if (savedTvWidget?.getLanguage() === language) {
return;
}
createWidget();
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, [language]);
/** Переключение инструмента и вида графика при indicativeData */
useEffect(() => {
const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;
if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
return;
}
onInstrumentChangeCallback(
chartState?.savedInstrument ?? (defaultOptions.symbol as string),
currentInstrument.current,
);
if (indicativeData) {
savedActiveChart.setSymbol(indicativeData.key);
savedActiveChart.setChartType(SeriesStyle.Line);
savedActiveChart.setResolution('60' as ResolutionString);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
}, [chartIsReady, savedTvWidget, indicativeData]);
return {
chartContainerRef,
savedTvWidget,
dropDownOpen,
setDropdownOpen,
currentInstrument,
handlerSaveAsExcel,
isWidgetHeaderContextMenuOpen,
setIsWidgetHeaderContextMenuOpen,
onDropInstruments,
addInstrumentFromModal,
isOver,
};
}
import React, { FC, lazy, useContext, useEffect, useState } from 'react';
import { DNDWrapper } from '@components/DNDWrapper';
import { InstrumentSearch } from '@components/InstrumentSearch';
import WidgetContentWrapper from '@components/WidgetContentWrapper';
import WidgetHeader from '@components/WidgetHeader';
import { useDropInstrument } from '@hooks/dnd';
import { useAppSelect } from '@hooks/useAppSelector';
import { useChangeProperties } from '@modules/widgetProperties';
import { getIsShowMoexChartInChart } from '@store/selectors/cachedData';
import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { Switch } from '@uikit/Switch';
import { useWidgetHeaderName } from '@utils/useWidgetName';
import useChartComponentFacade from './hooks/useChartComponentFacade';
import { ChartContainerProps, ChartProps } from './types';
const LazyTREntry = lazy(() => import('./components/MoexChart/MoexChart'));
export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
const indicativeData = props.widgetContentProps?.indicativeData;
const { updateProperties } = useChangeProperties<ChartProps>();
const isShowMoexChartInChart = useAppSelect(getIsShowMoexChartInChart);
const [isMoexChartShow, setIsMoexChartShow] = useState(props.widgetContentProps?.isMoexChartShow);
const {
chartContainerRef,
dropDownOpen,
setDropdownOpen,
currentInstrument,
handlerSaveAsExcel,
setIsWidgetHeaderContextMenuOpen,
isWidgetHeaderContextMenuOpen,
onDropInstruments,
addInstrumentFromModal,
isOver,
} = useChartComponentFacade(props);
/* на дроп обновляем название бумаги в сторе
и делаем апдейт на бэк, чтобы сохранить изменения
при обновлении страницы */
const { dropRef, isDragged } = useDropInstrument((dragData) => {
onDropInstruments(dragData.properties.issKey, true);
});
const [isOpenEmptyAction, setIsOpenEmptyAction] = useState<boolean>(false);
const openInstrumentModal = () => {
setIsOpenEmptyAction(true);
setIsWidgetHeaderContextMenuOpen(false);
};
// TODO: Зависит от сеток
const { isDraggedOver } = useContext(WidgetEnvContext);
const contractsInstrumentName = useWidgetHeaderName(currentInstrument.current);
const instrumentName = indicativeData
? `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`
: contractsInstrumentName;
useEffect(() => {
updateProperties((state) => {
state.isMoexChartShow = isMoexChartShow;
});
}, [isMoexChartShow, updateProperties]);
return (
<DNDWrapper
ref={dropRef}
isOver={isOver}
canDrop
>
<WidgetHeader
{...props}
dropdownOpen={dropDownOpen}
setDropdownOpen={setDropdownOpen}
itemsSearchIcon={[true]}
handlerSaveAsExcel={indicativeData ? null : handlerSaveAsExcel}
addToWidgetNamePrefix={instrumentName}
setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
openInstrumentsModal={openInstrumentModal}
/>
<WidgetContentWrapper {...props}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{true && (
<>
<div style={{ padding: '8px 16px' }}>
<Switch
onChange={setIsMoexChartShow}
value={isMoexChartShow}
checkedChildren="Moex chart"
unCheckedChildren="Traiding view chart"
/>
</div>
<LazyTREntry
fullName={currentInstrument.current}
widgetId={props.widgetId}
isMoexChartShow={isMoexChartShow}
/>
</>
)}
<div
className="TVChartContainer"
style={{
width: '100%',
height: '100%',
pointerEvents: isDragged || isDraggedOver ? 'none' : 'inherit',
display: !isMoexChartShow ? 'block' : 'none',
}}
ref={chartContainerRef}
/>
{isOpenEmptyAction && (
<InstrumentSearch
setOpen={setIsOpenEmptyAction}
isOpen={isOpenEmptyAction}
variant="single"
widgetId={props.widgetId}
withNRD={false}
addInstruments={addInstrumentFromModal}
/>
)}
</div>
</WidgetContentWrapper>
</DNDWrapper>
);
};
import React from 'react';
import { ChartIndicativeData } from '../../types';
import { CompareModal } from './components/CompareModal';
import { useMoexChart } from './hooks';
import 'moex-chart/dist/styles.css';
import styles from './styles.module.scss';
type TRProps = {
fullName: string;
indicativeData?: ChartIndicativeData | undefined;
widgetId: number;
isMoexChartShow: boolean | undefined;
};
export default React.memo(({ fullName, indicativeData, widgetId, isMoexChartShow }: TRProps) => {
const { containerRef, isCompareOpen, compareManagerRef, setIsCompareOpen } = useMoexChart({
indicativeData,
symbol: fullName,
});
return (
<div style={{ maxHeight: 'calc(100% - 74px)', display: isMoexChartShow ? 'block' : 'none' }}>
<div
className={styles.wrap}
ref={containerRef}
/>
{isCompareOpen && (
<CompareModal
onClose={() => setIsCompareOpen(false)}
compareManager={compareManagerRef}
widgetId={widgetId}
isOpen={isCompareOpen}
setOpen={setIsCompareOpen}
/>
)}
</div>
);
});