import { useEffect, useMemo, useState } from 'react' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { AreaChart, Area, XAxis, YAxis, Tooltip as RechartsTooltip, ResponsiveContainer, Text, ReferenceDot, ReferenceDotProps, } from 'recharts' import FlipNumbers from 'react-flip-numbers' import ContentBox from '../shared/ContentBox' import { formatNumericValue } from '../../utils/numbers' import SheenLoader from '../shared/SheenLoader' import { COLORS } from '../../styles/colors' import { useTheme } from 'next-themes' import Change from '../shared/Change' import ChartRangeButtons from '../shared/ChartRangeButtons' import { useViewport } from 'hooks/useViewport' import { formatTokenSymbol } from 'utils/tokens' import { useQuery } from '@tanstack/react-query' import { ChartDataItem, fetchChartData } from 'apis/coingecko' import mangoStore from '@store/mangoStore' import useJupiterSwapData from './useJupiterSwapData' import useLocalStorageState from 'hooks/useLocalStorageState' import { ANIMATION_SETTINGS_KEY } from 'utils/constants' import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { useTranslation } from 'next-i18next' import { ArrowsRightLeftIcon, EyeIcon, EyeSlashIcon, NoSymbolIcon, } from '@heroicons/react/20/solid' import FormatNumericValue from '@components/shared/FormatNumericValue' import { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart' import { interpolateNumber } from 'd3-interpolate' import { IconButton } from '@components/shared/Button' import Tooltip from '@components/shared/Tooltip' import { SwapHistoryItem } from 'types' import { Bank } from '@blockworks-foundation/mango-v4' dayjs.extend(relativeTime) const CustomizedLabel = ({ chartData, x, y, value, index, }: { chartData: ChartDataItem[] x?: number y?: string | number value?: number index?: number }) => { const { width } = useViewport() const [min, max] = useMemo(() => { if (chartData.length) { const prices = chartData.map((d) => d.price) return [Math.min(...prices), Math.max(...prices)] } return ['', ''] }, [chartData]) const [minIndex, maxIndex] = useMemo(() => { const minIndex = chartData.findIndex((d) => d.price === min) const maxIndex = chartData.findIndex((d) => d.price === max) return [minIndex, maxIndex] }, [min, max, chartData]) if (value && (minIndex === index || maxIndex === index)) { return ( width / 3 ? 'end' : 'start'} className="font-mono" > {formatNumericValue(value)} ) } else return
} interface ExtendedReferenceDotProps extends ReferenceDotProps { swapHistory: SwapHistoryItem[] inputBank: Bank | undefined flipPrices: boolean } const SwapHistoryArrows = (props: ExtendedReferenceDotProps) => { const { cx, cy, x, swapHistory, inputBank, flipPrices } = props const swapDetails = swapHistory.find( (swap) => dayjs(swap.block_datetime).unix() * 1000 === x ) const side = swapDetails?.swap_in_symbol === inputBank?.name ? 'sell' : 'buy' const buy = { pathCoords: 'M11 0.858312L0.857867 15.0004H21.1421L11 0.858312Z', fill: 'var(--up)', yOffset: 1, } const sell = { pathCoords: 'M11 14.1427L21.1421 0.000533306L0.857865 0.000529886L11 14.1427Z', fill: 'var(--down)', yOffset: -11, } const sideArrowProps = side === 'buy' ? (!flipPrices ? buy : sell) : !flipPrices ? sell : buy return cx && cy ? ( ) : (
) } const SwapTokenChart = () => { const { t } = useTranslation('common') const inputBank = mangoStore((s) => s.swap.inputBank) const outputBank = mangoStore((s) => s.swap.outputBank) const { inputCoingeckoId, outputCoingeckoId } = useJupiterSwapData() const [baseTokenId, setBaseTokenId] = useState(inputCoingeckoId) const [quoteTokenId, setQuoteTokenId] = useState(outputCoingeckoId) const [mouseData, setMouseData] = useState() const [daysToShow, setDaysToShow] = useState('1') const [flipPrices, setFlipPrices] = useState(false) const { theme } = useTheme() const [animationSettings] = useLocalStorageState( ANIMATION_SETTINGS_KEY, INITIAL_ANIMATION_SETTINGS ) const swapHistory = mangoStore((s) => s.mangoAccount.swapHistory.data) const loadSwapHistory = mangoStore((s) => s.mangoAccount.swapHistory.loading) const [showSwaps, setShowSwaps] = useState(false) const coingeckoDataQuery = useQuery( ['chart-data', baseTokenId, quoteTokenId, daysToShow], () => fetchChartData(baseTokenId, quoteTokenId, daysToShow), { cacheTime: 1000 * 60 * 15, staleTime: 1000 * 60 * 1, enabled: !!baseTokenId && !!quoteTokenId, refetchOnWindowFocus: false, } ) const coingeckoData = useMemo(() => { if (!coingeckoDataQuery?.data?.length) return [] if (!flipPrices) { return coingeckoDataQuery.data } else { return coingeckoDataQuery.data.map((d: ChartDataItem) => { const price = d.inputTokenPrice / d.outputTokenPrice === d.price ? d.outputTokenPrice / d.inputTokenPrice : d.inputTokenPrice / d.outputTokenPrice return { ...d, price: price } }) } }, [flipPrices, coingeckoDataQuery]) const chartSwapTimes = useMemo(() => { if ( loadSwapHistory || !swapHistory || !swapHistory.length || !inputBank || !outputBank ) return [] const chartSymbols = [inputBank.name, outputBank.name] return swapHistory .filter( (swap) => chartSymbols.includes(swap.swap_in_symbol) && chartSymbols.includes(swap.swap_out_symbol) ) .map((val) => dayjs(val.block_datetime).unix() * 1000) }, [swapHistory, loadSwapHistory, inputBank, outputBank]) const swapHistoryPoints = useMemo(() => { if (!coingeckoData.length || !chartSwapTimes.length) return [] return chartSwapTimes.map((x) => { const makeChartDataItem = { inputTokenPrice: 1, outputTokenPrice: 1 } const index = coingeckoData.findIndex((d) => d.time > x) // find index of data point with x value greater than highlight x if (index === 0) { return { time: x, price: coingeckoData[0].price, ...makeChartDataItem } // return first data point y value if highlight x is less than first data point x } else if (index === -1) { return { time: x, price: coingeckoData[coingeckoData.length - 1].price, ...makeChartDataItem, } // return last data point y value if highlight x is greater than last data point x } else { const x0 = coingeckoData[index - 1].time const x1 = coingeckoData[index].time const y0 = coingeckoData[index - 1].price const y1 = coingeckoData[index].price const interpolateY = interpolateNumber(y0, y1) // create interpolate function for y values const y = interpolateY((x - x0) / (x1 - x0)) // estimate y value at highlight x using interpolate function return { time: x, price: y, ...makeChartDataItem } } }) }, [coingeckoData, chartSwapTimes]) const chartData = useMemo(() => { if (!coingeckoData.length) return [] const minTime = coingeckoData[0].time const maxTime = coingeckoData[coingeckoData.length - 1].time if (swapHistoryPoints.length && showSwaps) { const swapPoints = swapHistoryPoints.filter( (point) => point.time >= minTime && point.time <= maxTime ) return coingeckoData.concat(swapPoints).sort((a, b) => a.time - b.time) } else return coingeckoData }, [coingeckoData, swapHistoryPoints, showSwaps]) const handleMouseMove: CategoricalChartFunc = (coords) => { if (coords.activePayload) { setMouseData(coords.activePayload[0].payload) } } const handleMouseLeave = () => { setMouseData(undefined) } useEffect(() => { if (!inputCoingeckoId || !outputCoingeckoId) return if (['usd-coin', 'tether'].includes(outputCoingeckoId)) { setBaseTokenId(inputCoingeckoId) setQuoteTokenId(outputCoingeckoId) } else { setBaseTokenId(outputCoingeckoId) setQuoteTokenId(inputCoingeckoId) } }, [inputCoingeckoId, outputCoingeckoId]) const calculateChartChange = () => { if (chartData?.length) { if (mouseData) { const index = chartData.findIndex((d) => d.time === mouseData.time) return ( ((chartData[index]['price'] - chartData[0]['price']) / chartData[0]['price']) * 100 ) } else return ( ((chartData[chartData.length - 1]['price'] - chartData[0]['price']) / chartData[0]['price']) * 100 ) } return 0 } const swapMarketName = useMemo(() => { if (!inputBank || !outputBank) return '' const inputSymbol = formatTokenSymbol(inputBank.name?.toUpperCase()) const outputSymbol = formatTokenSymbol(outputBank.name?.toUpperCase()) return ['usd-coin', 'tether'].includes(inputCoingeckoId || '') ? !flipPrices ? `${outputSymbol}/${inputSymbol}` : `${inputSymbol}/${outputSymbol}` : !flipPrices ? `${inputSymbol}/${outputSymbol}` : `${outputSymbol}/${inputSymbol}` }, [flipPrices, inputBank, inputCoingeckoId, outputBank]) return ( {coingeckoDataQuery?.isLoading || coingeckoDataQuery.isFetching ? ( <>
) : chartData?.length && baseTokenId && quoteTokenId ? (
{inputBank && outputBank ? (

{swapMarketName}

setFlipPrices(!flipPrices)} hideBg >
) : null} {mouseData ? ( <>
{animationSettings['number-scroll'] ? ( ) : ( )}

{dayjs(mouseData.time).format('DD MMM YY, h:mma')}

) : ( <>
{animationSettings['number-scroll'] ? ( ) : ( )}

{dayjs(chartData[chartData.length - 1].time).format( 'DD MMM YY, h:mma' )}

)}
setShowSwaps(!showSwaps)} > {showSwaps ? ( ) : ( )} setDaysToShow(v)} />
} /> = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] } stopOpacity={0.25} /> = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] } stopOpacity={0} /> = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] } strokeWidth={1.5} fill="url(#gradientArea)" label={} /> {showSwaps && swapHistoryPoints.length ? swapHistoryPoints.map((point, index) => ( } /> )) : null}
) : (

{t('chart-unavailable')}

)} ) } export default SwapTokenChart