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 from '../../../models'; import { matchMedian } from '../../../utils'; import currencyDict from '../../../../static/assets/currencies.json'; import getNivoScheme from '../NivoScheme'; import OrderTooltip from '../helpers/OrderTooltip'; import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext'; import { FederationContext, type UseFederationStoreType, } from '../../../contexts/FederationContext'; interface DepthChartProps { maxWidth: number; maxHeight: number; fillContainer?: boolean; elevation?: number; onOrderClicked?: (id: number, shortAlias: string) => void; } const DepthChart: React.FC = ({ maxWidth, maxHeight, fillContainer = false, elevation = 6, onOrderClicked = () => null, }) => { const { fav } = useContext(AppContext); const { federation, federationUpdatedAt } = useContext(FederationContext); 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(0); const [coordinatorFilter, setCoordinatorFilter] = useState('all'); 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); // as selected in BookControl setCoordinatorFilter(fav.coordinator); console.log(fav.coordinator); }, [fav.currency, fav.coordinator]); useEffect(() => { if (Object.values(federation.book).length > 0) { const enriched = Object.values(federation.book).map((order) => { if (order?.currency) { const limits = federation.getCoordinators()[0]?.limits; const originalPrice = (limits[order.currency]?.price ?? 0) * (1 + parseFloat(order.premium) / 100); const currencyPrice = (limits[currencyCode || 1]?.price ?? 0) * (1 + parseFloat(order.premium) / 100); const originalAmount = order.has_range && order.max_amount ? parseFloat(order.max_amount) : parseFloat(order.amount); const currencyAmount = (currencyPrice * originalAmount) / originalPrice; order.base_price = currencyPrice; order.satoshis_now = (100000000 * currencyAmount) / currencyPrice; } return order; }); setEnrichedOrders(enriched); } }, [federationUpdatedAt, currencyCode, coordinatorFilter]); useEffect(() => { if (enrichedOrders.length > 0) { generateSeries(); } }, [enrichedOrders, xRange]); useEffect(() => { if (xType === 'base_price') { const prices: number[] = enrichedOrders.map((order) => order?.base_price ?? 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 (federation.exchange.info?.last_day_nonkyc_btc_premium === undefined) { const premiums: number[] = enrichedOrders.map((order) => order?.premium ?? 0); setCenter(~~matchMedian(premiums)); } else { setCenter(federation.exchange.info?.last_day_nonkyc_btc_premium); } setXRange(8); setRangeSteps(0.5); } }, [enrichedOrders, xType, federationUpdatedAt, currencyCode, coordinatorFilter]); const generateSeries: () => void = () => { const sortedOrders: PublicOrder[] = xType === 'base_price' ? enrichedOrders .filter( (order: PublicOrder | null) => currencyCode === 0 || order?.currency === currencyCode, ) .filter( (order: PublicOrder | null) => coordinatorFilter === 'any' || (coordinatorFilter === 'robosats' && order?.federated) || order?.coordinatorShortAlias === coordinatorFilter, ) .sort( (order1: PublicOrder | null, order2: PublicOrder | null) => (order1?.base_price ?? 0) - (order2?.base_price ?? 0), ) : enrichedOrders .filter( (order: PublicOrder | null) => currencyCode === 0 || order?.currency === currencyCode, ) .filter( (order: PublicOrder | null) => coordinatorFilter === 'any' || (coordinatorFilter === 'robosats' && order?.federated) || order?.coordinatorShortAlias === coordinatorFilter, ) .sort( (order1: PublicOrder | null, order2: PublicOrder | null) => 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_price' ? order.base_price : order.premium, y: lastSumOrders, }, { // PublicOrder Point x: xType === 'base_price' ? order.base_price : 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_price') { return value.toString(); } return `${value}%`; }; const formatAxisY = (value: number): string => `${value}BTC`; const handleOnClick: PointMouseHandler = (point: Point) => { onOrderClicked(point.data?.order?.id, point.data?.order?.coordinatorShortAlias); }; const em = theme.typography.fontSize; return ( {center === undefined || enrichedOrders.length < 1 ? (
) : ( { setXRange(xRange + rangeSteps); }} > {xType === 'base_price' ? `${center} ${String(currencyDict[(currencyCode || 1) as keyof object])}` : `${String(center.toPrecision(3))}%`} { 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;