import React, {useCallback, useEffect, useRef, useState} from “react”;
import {
init,
dispose,
Chart,
IndicatorFigureStylesCallbackData,
Indicator,
IndicatorStyle,
KLineData,
utils,
ActionType,
} from “klinecharts”;
import {CandleChartProps} from “./CandleChart.props”;
import CandleChartToolbar from “./CandleChartToolbar”;
import {Box, Button, ButtonGroup, CircularProgress, Icon, IconButton, Stack, useTheme} from “@mui/material”;
import getMinutesTickSizeByInterval from “./utils/getMinutesTickSizeByInterval.util”;
import drawTrade from “./utils/drawTrade.util”;
import drawTradeLines from “./utils/drawTradeLines.util”;
import {BasketIcon, ScreenIcon} from “…/…/icons”;
import {FullScreen, useFullScreenHandle} from “react-full-screen”;
import dayjs from “dayjs”;
import {fetchSymbolsKlines} from “…/…/…/actions/binance”;
import {useSnackbar} from “notistack”;
import {useSelector} from “react-redux”;
import {AppState} from “…/…/…/store/store”;
import useComponentResizeListener from “…/…/…/hooks/componentResizeListener”;
import {chartStyles} from “./ChartStyles”;

interface Vol {
volume?: number
}

export const CandleChart = ({
candles,
uniqueId,
symbolName,
orders,
amount,
openPrice,
closePrice,
onScreenshotCreated,
chartInterval,
setChartInterval,
waiting,
setWaiting,
height,
showIntervalSelector,
}: CandleChartProps) => {
const chart = useRef<Chart|null>(null);
const paneId = useRef<string>(“”);
const [figureId, setFigureId] = useState<string>(“”);
const ref = useRef<HTMLDivElement>(null);
const handle = useFullScreenHandle();
const {enqueueSnackbar} = useSnackbar();
const precision = useSelector((state: AppState) => state.binancePrecision.futures[symbolName.toUpperCase()]);
const kline = useSelector((state: AppState) => state.binanceKline.futures[${symbolName.toLocaleLowerCase()}@kline_${chartInterval}]) || null;
const size = useComponentResizeListener(ref);
const theme = useTheme();
const darkMode = theme.palette.mode === “dark”;

const chartResize = useCallback(() => {
if (null !== chart.current) {
chart.current.resize();
}
}, [chart.current]);

useEffect(() => {
if (precision && null !== chart.current) {
chart.current.setPriceVolumePrecision(precision?.price || 4, precision?.quantity || 2);
}
}, [chart.current, precision]);

useEffect(() => {
if (kline && null !== chart.current) {
chart.current.updateData(kline);
}
}, [kline, chart.current]);

useEffect(() => {
chartResize();
}, [size]);

useEffect(() => {
// @ts-ignore
chart.current = init(chart-${uniqueId}, {styles: chartStyles(theme)});

return () => dispose(chart-${uniqueId});
}, [uniqueId]);

useEffect(() => {
if (chart.current) {
chart.current.setStyles({
grid: {
horizontal: {
color: theme.palette.grey[50],
},
vertical: {
color: theme.palette.grey[50],
},
},
xAxis: {
axisLine: {
color: theme.palette.grey[50],
},
},
yAxis: {
axisLine: {
color: theme.palette.grey[50],
},
},
separator: {
color: theme.palette.grey[50],
},
});
}
}, [theme]);

useEffect(() => {
if (null !== chart.current) {
window.addEventListener(“resize”, chartResize);
}

return () => window.removeEventListener(“resize”, chartResize);
}, []);

useEffect(() => {
chart.current?.applyNewData(candles);
chart.current?.overrideIndicator({
name: “VOL”,
shortName: “Объем”,
calcParams: [],
figures: [
{
key: “volume”,
title: “”,
type: “bar”,
baseValue: 0,
styles: (data: IndicatorFigureStylesCallbackData<Vol>, indicator: Indicator, defaultStyles: IndicatorStyle) => {
const kLineData = data.current.kLineData as KLineData;
let color: string;

if (kLineData.close > kLineData.open) {
color = utils.formatValue(indicator.styles, “bars[0].upColor”, (defaultStyles.bars)[0].upColor) as string;
} else if (kLineData.close < kLineData.open) {
color = utils.formatValue(indicator.styles, “bars[0].downColor”, (defaultStyles.bars)[0].downColor) as string;
} else {
color = utils.formatValue(indicator.styles, “bars[0].noChangeColor”, (defaultStyles.bars)[0].noChangeColor) as string;
}

return {color};
},
},
],
}, paneId.current);
chart.current?.createIndicator(“VOL”, false, {id: paneId.current});
}, [candles]);

useEffect(() => {
if (!orders || orders.length === 0 || candles.length === 0 || !amount || amount.length === 0) return;

const minTime = orders[0].time;
const maxTime = orders[orders.length - 1].time;
const needleTime = minTime + (maxTime - minTime) / 2;

chart.current?.scrollToTimestamp(needleTime + 50 * getMinutesTickSizeByInterval(chartInterval) * 60 * 1000);
drawTrade(chart, paneId, orders, chartInterval, amount);

if (openPrice && closePrice) {
let openTime = Infinity;
let closeTime = -Infinity;

orders.forEach(order => {
if (openTime > order.time) {
openTime = order.time;
}

if (closeTime < order.time) {
closeTime = order.time;
}
});

drawTradeLines(
chart,
openPrice,
openTime,
closePrice,
closeTime,
orders[0].position,
paneId,
precision.price,
precision.quantity,
);
}
}, [orders, candles, uniqueId]);

const removeFigure = () => {
chart.current?.removeOverlay({
id: figureId,
});
setFigureId(“”);
};

const onButtonClick = async() => {
const url = chart.current?.getConvertPictureUrl(true);

if (!url || !onScreenshotCreated) {
return;
}

onScreenshotCreated(url);
};

useEffect(() => {
if (candles.length === 0) return;
chart.current?.subscribeAction(ActionType.OnScroll, onScrollbarPositionChanged);

return (() => {
chart.current?.unsubscribeAction(ActionType.OnScroll, onScrollbarPositionChanged);
});
}, [chart.current, candles]);

let isLoading = false;

let onScrollbarPositionChanged = () => {
const chartWidth = chart.current?.getSize()?.width;
const barSpace = chart.current?.getBarSpace();
const rightDistance = chart.current?.getOffsetRightDistance();

const dataList = chart.current?.getDataList();
const from = chart.current?.getVisibleRange().realFrom;

if (!dataList || isLoading || !chartWidth || !barSpace) return;

if (rightDistance) {
if (0 < rightDistance && rightDistance !== 50) {
let startTime = dataList[dataList.length - 1].timestamp;

if (isLastCandleToday(startTime, chartInterval)) return;

isLoading = true;
setWaiting(true);
fetchSymbolsKlines(symbolName, chartInterval, startTime).then((data) => {
if (undefined === data) return;

const newCandles = data?.map((item: any[]) => ({
timestamp: item[0],
open: item[1],
high: item[2],
low: item[3],
close: item[4],
volume: item[5],
}));

if (!newCandles || newCandles.length === 0) {
isLoading = false;
setWaiting(false);

return;
} else {
const newData = […dataList, …newCandles];
chart.current?.applyNewData(newData, true);
const newRightDistance = (-barSpace * newCandles.length) + rightDistance;
chart.current?.setOffsetRightDistance(newRightDistance + 140);
}

isLoading = false;
setWaiting(false);
})
.catch((error) => {
enqueueSnackbar(error.message, {variant: “error”});
isLoading = false;
setWaiting(false);
});
}
}

if (from === 0) {
let endTime = dataList[0].timestamp;
let prevTime: number | null = null;
isLoading = true;
setWaiting(true);

fetchSymbolsKlines(symbolName, chartInterval, undefined, endTime).then((data) => {
if (undefined === data) return;

const newCandles = data?.map((item: any[]) => ({
timestamp: item[0],
open: item[1],
high: item[2],
low: item[3],
close: item[4],
volume: item[5],
}));

if (!newCandles || newCandles.length === 0) {
return;
} else {
if (prevTime === newCandles[newCandles.length - 1].timestamp) {
isLoading = false;
setWaiting(false);

return;
}

if (newCandles[newCandles.length - 1].timestamp === dataList[dataList.length - 1].timestamp) {
isLoading = false;
setWaiting(false);

return;
}

prevTime = newCandles[newCandles.length - 1].timestamp;
chart.current?.applyMoreData(newCandles);
}

isLoading = false;
setWaiting(false);
})
.catch((error) => {
enqueueSnackbar(error.message, {variant: “error”});
isLoading = false;
setWaiting(false);
});
}
};

const isLastCandleToday = (lastCandleTimestamp: number, interval: string) => {
const lastCandleDate = dayjs(lastCandleTimestamp);
const today = dayjs();

if (interval.includes(“m”) || interval.includes(“h”) || interval.includes(“d”)) {
return lastCandleDate.isSame(today, “day”);
}

return false;
};

return (<>
<FullScreen handle={handle}>
<Box height=“100%” sx={{borderRadius: “1rem”, position: “relative”, background: theme => theme.palette.background.paper}}>
{
showIntervalSelector &&
<Stack sx={{
padding: “0 5px”, width: “100%”, height: 40,
justifyContent: “space-between”, flexDirection: “row”, borderBottom: theme => 2px solid ${theme.palette.grey[50]},
}}>
<ButtonGroup>
{
[“1m”, “3m”, “5m”, “15m”, “30m”, “1h”, “4h”, “12h”, “1d”].map(interval => (
<Button
key={interval}
color=“inherit”
sx={{px: 1, py: .5,
color: interval === chartInterval ? darkMode ? “#fff” : “#677294” : “#999999”,
fontSize: “.75rem”, “&:hover”: {backgroundColor: darkMode ? “#2b2b2b !important” : “”},
}}
variant={“text”}
onClick={() => setChartInterval && setChartInterval(interval)}
>{interval}</Button>
))
}
</ButtonGroup>
<IconButton sx={{borderRadius: 2, fontFamily: “Mont”, mr: 1, color: darkMode ? “#999” : “#677294”, “&:hover”: {backgroundColor: darkMode ? “#2b2b2b !important” : “”}}} onClick={onButtonClick}>
<Icon sx={{width: 90}} component={ScreenIcon} />
</IconButton>
</Stack>
}
{
waiting ? (
<CircularProgress
style={{position: “absolute”, marginLeft: “1rem”}}
color=“inherit”
size={32}
/>
) : (<></>)
}
<Stack direction=“row” height={height && !handle.active ? 550 : “100%” || “calc(100% - 140px)”} width=“100%”>
<CandleChartToolbar
setFigureId={setFigureId}
chart={chart}
paneId={paneId}
handle={handle}
/>
<Box
ref={ref}
id={chart-${uniqueId}}
width=“calc(100% - 55px)”
height=“100%”
sx={{borderLeft: theme => 2px solid ${theme.palette.grey[50]}}}
>
{
figureId.length > 0 &&
<Stack
sx={{
backgroundColor: darkMode ? “#474747” : “#CBD4E3”,
borderRadius: 1,
position: “absolute”,
zIndex: 10,
right: 80,
top: 30,
border: theme => 2px solid ${theme.palette.grey[50]},
}}
spacing={2}
>
<IconButton sx={{borderRadius: 1}} onClick={removeFigure}>
<Icon component={BasketIcon} />
</IconButton>
</Stack>
}
</Box>
</Stack>

</Box>
</FullScreen>
</>);
};

CandleChart.displayName = “CandleChart”;

import {Icon, IconButton, Stack, useTheme} from “@mui/material”;
import {BasketIcon, FullScreenIcon} from “…/…/icons”;
import React from “react”;
import {CandleChartToolbarProps} from “./CandleChartToolbar.props”;
import RectTool from “./Tools/RectTool”;
import FibonacciLineTool from “./Tools/FibonacciLineTool”;
import HorizontalStraightLineTool from “./Tools/HorizontalStraightLineTool”;
import ParallelStraightLineTool from “./Tools/ParallelStraightLineTool”;
import RayLineTool from “./Tools/RayLineTool”;
import PriceLineTool from “./Tools/PriceLineTool”;
import SegmentTool from “./Tools/SegmentTool”;
import StraightLineTool from “./Tools/StraightLineTool”;
import RulerTool from “./Tools/RulerTool”;

const CandleChartToolbar = ({chart, paneId, setFigureId, handle}: CandleChartToolbarProps) => {
const darkMode = useTheme().palette.mode === “dark”;

const removeOverlay = () => {
chart.current?.removeOverlay();
setFigureId(“”);
};

const fullScreenHandle = () => {
if(handle.active) {
handle.exit();
} else {
handle.enter();
}
};

return <>
<Stack
className=“scroll-block”
direction=“column”
alignItems=“center”
spacing={1}
sx={{
width: “55px”,
height: “100%”,
overflowY: “scroll”,
scrollbarWidth: “thin”,
“&::-webkit-scrollbar”: {
width: “8px”,
},
“&::-webkit-scrollbar-track”: {
background: theme => theme.palette.background.paper,
},
“&::-webkit-scrollbar-thumb”: {
backgroundColor: theme => theme.palette.grey[300],
border: theme => 3px solid ${theme.palette.white.main},
},
}}
>
<PriceLineTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<HorizontalStraightLineTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<RayLineTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<SegmentTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<StraightLineTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<ParallelStraightLineTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<FibonacciLineTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<RulerTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>
<RectTool chart={chart} paneId={paneId} setFigureId={setFigureId}/>

<IconButton sx={{borderRadius: 2, “&:hover”: {backgroundColor: darkMode ? “#2b2b2b !important” : “”}}} style={{height: 40}} onClick={fullScreenHandle}>
<Icon component={FullScreenIcon} sx={{height: 18}} />
</IconButton>

<IconButton sx={{borderRadius: 2, “&:hover”: {backgroundColor: darkMode ? “#2b2b2b !important” : “”}}} onClick={removeOverlay}>
<Icon component={BasketIcon}/>
</IconButton>
</Stack>
</>;
};

export default CandleChartToolbar;

Мы рисуем фигуры, которые отрисовываем в CandleChartToolbar
нужно сделать сохранение того, что ты нарисовал в localStorage