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}
) : (
)}
)
}
export default SwapTokenChart