import React, { useEffect, useState, useContext } from 'react'; import { ResponsiveLine, type Serie, type Datum, type PointTooltipProps, type PointMouseHandler, type Point, type CustomLayer, } from '@nivo/line'; import { Box, CircularProgress, Grid, IconButton, MenuItem, Paper, Select, useTheme, } from '@mui/material'; import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; import { type PublicOrder, type Order } from '../../../models'; import { matchMedian } from '../../../utils'; import currencyDict from '../../../../static/assets/currencies.json'; import getNivoScheme from '../NivoScheme'; import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext'; import OrderTooltip from '../helpers/OrderTooltip'; interface DepthChartProps { maxWidth: number; maxHeight: number; fillContainer?: boolean; elevation?: number; onOrderClicked?: (id: number) => void; } const DepthChart: React.FC = ({ maxWidth, maxHeight, fillContainer = false, elevation = 6, onOrderClicked = () => null, }) => { const { book, fav, info, limits, baseUrl } = useContext(AppContext); const { t } = useTranslation(); const theme = useTheme(); const [enrichedOrders, setEnrichedOrders] = useState([]); const [series, setSeries] = useState([]); const [rangeSteps, setRangeSteps] = useState(8); const [xRange, setXRange] = useState(8); const [xType, setXType] = useState('premium'); const [currencyCode, setCurrencyCode] = useState(1); const [center, setCenter] = useState(); const height = maxHeight < 10 ? 10 : maxHeight; const width = maxWidth < 10 ? 10 : maxWidth > 72.8 ? 72.8 : maxWidth; useEffect(() => { setCurrencyCode(fav.currency === 0 ? 1 : fav.currency); }, [fav.currency]); useEffect(() => { if (Object.keys(limits.list).length > 0) { const enriched = book.orders.map((order) => { // We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate // for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a // simple rule of three order.base_amount = (order.price * limits.list[currencyCode].price) / limits.list[order.currency].price; return order; }); setEnrichedOrders(enriched); } }, [limits.list, book.orders, currencyCode]); useEffect(() => { if (enrichedOrders.length > 0) { generateSeries(); } }, [enrichedOrders, xRange]); useEffect(() => { if (xType === 'base_amount') { const prices: number[] = enrichedOrders.map((order) => order?.base_amount || 0); const medianValue = ~~matchMedian(prices); const maxValue = prices.sort((a, b) => b - a).slice(0, 1)[0] || 1500; const maxRange = maxValue - medianValue; const rangeSteps = maxRange / 10; setCenter(medianValue); setXRange(maxRange); setRangeSteps(rangeSteps); } else { if (info.last_day_nonkyc_btc_premium === undefined) { const premiums: number[] = enrichedOrders.map((order) => order?.premium || 0); setCenter(~~matchMedian(premiums)); } else { setCenter(info.last_day_nonkyc_btc_premium); } setXRange(8); setRangeSteps(0.5); } }, [enrichedOrders, xType, info.last_day_nonkyc_btc_premium, currencyCode]); const generateSeries: () => void = () => { const sortedOrders: PublicOrder[] = xType === 'base_amount' ? enrichedOrders.sort( (order1, order2) => (order1?.base_amount || 0) - (order2?.base_amount || 0), ) : enrichedOrders.sort((order1, order2) => order1.premium - order2.premium); const sortedBuyOrders: PublicOrder[] = sortedOrders .filter((order) => order.type == 0) .reverse(); const sortedSellOrders: PublicOrder[] = sortedOrders.filter((order) => order.type == 1); const buySerie: Datum[] = generateSerie(sortedBuyOrders); const sellSerie: Datum[] = generateSerie(sortedSellOrders); const maxX: number = (center || 0) + xRange; const minX: number = (center || 0) - xRange; setSeries([ { id: 'buy', data: closeSerie(buySerie, maxX, minX), }, { id: 'sell', data: closeSerie(sellSerie, minX, maxX), }, ]); }; const generateSerie = (orders: PublicOrder[]): Datum[] => { if (center == undefined) { return []; } let sumOrders: number = 0; let serie: Datum[] = []; orders.forEach((order) => { const lastSumOrders = sumOrders; sumOrders += (order.satoshis_now || 0) / 100000000; const datum: Datum[] = [ { // Vertical Line x: xType === 'base_amount' ? order.base_amount : order.premium, y: lastSumOrders, }, { // PublicOrder Point x: xType === 'base_amount' ? order.base_amount : order.premium, y: sumOrders, order, }, ]; serie = [...serie, ...datum]; }); const inlineSerie = serie.filter((datum: Datum) => { return Number(datum.x) > center - xRange && Number(datum.x) < center + xRange; }); return inlineSerie; }; const closeSerie = (serie: Datum[], limitBottom: number, limitTop: number): Datum[] => { if (serie.length == 0) { return []; } // If the bottom is not 0, exdens the horizontal bottom line if (serie[0].y !== 0) { const startingPoint: Datum = { x: limitBottom, y: serie[0].y, }; serie.unshift(startingPoint); } // exdens the horizontal top line const endingPoint: Datum = { x: limitTop, y: serie[serie.length - 1].y, }; return [...serie, endingPoint]; }; const centerLine: CustomLayer = (props) => ( ); const generateTooltip: React.FunctionComponent = ( pointTooltip: PointTooltipProps, ) => { const order: PublicOrder = pointTooltip.point.data.order; return ; }; const formatAxisX = (value: number): string => { if (xType === 'base_amount') { return value.toString(); } return `${value}%`; }; const formatAxisY = (value: number): string => `${value}BTC`; const handleOnClick: PointMouseHandler = (point: Point) => { onOrderClicked(point.data?.order?.id); }; const em = theme.typography.fontSize; return ( {center == undefined || enrichedOrders.length < 1 ? (
) : ( { setXRange(xRange + rangeSteps); }} > {xType === 'base_amount' ? `${center} ${currencyDict[currencyCode]}` : `${center}%`} { setXRange(xRange - rangeSteps); }} disabled={xRange <= 1} > Number(value).toFixed(0)} lineWidth={3} theme={getNivoScheme(theme)} colors={[theme.palette.secondary.main, theme.palette.primary.main]} xScale={{ type: 'linear', min: center - xRange, max: center + xRange, }} layers={['axes', 'areas', 'crosshair', 'lines', centerLine, 'slices', 'mesh']} /> )}
); }; export default DepthChart;