import {
MouseEventHandler,
useCallback,
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 { formatCurrencyValue, formatNumericValue } from '../../utils/numbers'
import SheenLoader from '../shared/SheenLoader'
import { COLORS } from '../../styles/colors'
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,
SWAP_CHART_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 { SwapChartSettings, SwapHistoryItem } from 'types'
import useThemeWrapper from 'hooks/useThemeWrapper'
dayjs.extend(relativeTime)
export const handleFlipPrices = (
flip: boolean,
flipPrices: boolean,
inputToken: string | undefined,
outputToken: string | undefined,
swapChartSettings: SwapChartSettings[],
setSwapChartSettings: (settings: SwapChartSettings[]) => void,
) => {
if (!inputToken || !outputToken) return
if (!flipPrices && flip) {
setSwapChartSettings([
...swapChartSettings,
{ pair: `${inputToken}/${outputToken}`, flipPrices: true },
])
} else {
setSwapChartSettings(
swapChartSettings.filter(
(chart: SwapChartSettings) =>
!chart.pair.includes(inputToken) && !chart.pair.includes(outputToken),
),
)
}
}
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 (
{`${t( side, )} ${amount} ${symbol} at ${formatNumericValue( price, )} ${priceSymbol} for ${formatCurrencyValue(value)}`}
{coingeckoPercentageDifference ? ({coingeckoPercentageDifference.toFixed(2)}% {' '} {betterThanCoingecko ? 'better than' : 'worse than'} Coingecko
) : null} > ) }, [flipPrices, swapMarketName, swapTooltipCoingeckoPrice], ) const { data: coingeckoDataQuery, isLoading, isFetching, } = useQuery( ['swap-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 || !coingeckoDataQuery.length) return [] if (!flipPrices) { return coingeckoDataQuery } else { return coingeckoDataQuery.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 === 'ETH (Portal)' ? 'ETH' : inputBank.name, outputBank.name === 'ETH (Portal)' ? 'ETH' : 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 latestChartDataItem = useMemo(() => { if (!inputBank || !outputBank) return [] const price = !flipPrices ? outputBank.uiPrice / inputBank.uiPrice : inputBank.uiPrice / outputBank.uiPrice const item: ChartDataItem[] = [ { price, time: Date.now(), inputTokenPrice: inputBank.uiPrice, outputTokenPrice: outputBank.uiPrice, }, ] return item }, [flipPrices, inputBank, outputBank]) const chartData = useMemo(() => { if (!coingeckoData || !coingeckoData.length || coingeckoData.length < 2) 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) .concat(latestChartDataItem) } else return coingeckoData }, [coingeckoData, latestChartDataItem, swapHistoryPoints, showSwaps]) const handleMouseMove: CategoricalChartFunc = useCallback( (coords) => { if (coords.activePayload) { setMouseData(coords.activePayload[0].payload) } }, [setMouseData], ) const handleMouseLeave = useCallback(() => { setMouseData(undefined) }, [setMouseData]) useEffect(() => { if (!inputCoingeckoId || !outputCoingeckoId) return setBaseTokenId(inputCoingeckoId) setQuoteTokenId(outputCoingeckoId) }, [inputCoingeckoId, outputCoingeckoId]) const calculateChartChange = useCallback(() => { if (!chartData?.length) return 0 if (mouseData) { const index = chartData.findIndex((d) => d.time === mouseData.time) if (index === -1) return 0 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 ) } }, [chartData, mouseData]) return ({swapMarketName}
{dayjs(mouseData.time).format('DD MMM YY, h:mma')}
> ) : ( <>{dayjs(chartData[chartData.length - 1].time).format( 'DD MMM YY, h:mma', )}
> )}{t('chart-unavailable')}