Skip to content

Commit

Permalink
add sankey chart
Browse files Browse the repository at this point in the history
  • Loading branch information
groninge01 committed Sep 9, 2024
1 parent 72233d4 commit 47b9181
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 59 deletions.
136 changes: 77 additions & 59 deletions lib/modules/swap/SwapDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
import { NumberText } from '@/lib/shared/components/typography/NumberText'
import { useCurrency } from '@/lib/shared/hooks/useCurrency'
import { bn, fNum } from '@/lib/shared/utils/numbers'
import { HStack, VStack, Text, Tooltip, Box } from '@chakra-ui/react'
import {
HStack,
VStack,
Text,
Tooltip,
Box,
useDisclosure,
Button,
UseDisclosureProps,
} from '@chakra-ui/react'
import { useSwap } from './SwapProvider'
import { GqlSorSwapType } from '@/lib/shared/services/api/generated/graphql'
import { useUserSettings } from '../user/settings/UserSettingsProvider'
Expand All @@ -13,26 +22,30 @@ import { useTokens } from '../tokens/TokensProvider'
import { NativeWrapHandler } from './handlers/NativeWrap.handler'
import { InfoIcon } from '@/lib/shared/components/icons/InfoIcon'
import pluralize from 'pluralize'
import { SwapRoutesModal } from './modal/SwapRoutesModal'

export function OrderRoute() {
export function OrderRoute({ swapRoutesDisclosure }: { swapRoutesDisclosure: UseDisclosureProps }) {
const { simulationQuery } = useSwap()

const queryData = simulationQuery.data as SdkSimulateSwapResponse
const orderRouteVersion = queryData ? queryData.protocolVersion : 2
const hopCount = queryData.routes[0]?.hops?.length ?? 0

return (
<HStack justify="space-between" w="full">
<Text color="grayText">Order route</Text>
<HStack>
<Text color="grayText">
BV{orderRouteVersion}: {hopCount} {pluralize('hop', hopCount)}
</Text>
<Tooltip label="Balancer vault version and number of hops" fontSize="sm">
<InfoIcon />
</Tooltip>
<>
<HStack justify="space-between" w="full">
<Text color="grayText">Order route</Text>
<HStack>
<Text color="grayText">
BV{orderRouteVersion}: {hopCount} {pluralize('hop', hopCount)}
</Text>
<Tooltip label="Balancer vault version and number of hops" fontSize="sm">
<InfoIcon />
</Tooltip>
</HStack>
</HStack>
</HStack>
<Button onClick={swapRoutesDisclosure.onOpen}>View route</Button>
</>
)
}

Expand All @@ -41,8 +54,8 @@ export function SwapDetails() {
const { slippage, slippageDecimal } = useUserSettings()
const { usdValueForToken } = useTokens()
const { tokenInInfo, tokenOutInfo, swapType, tokenIn, tokenOut, handler } = useSwap()

const { priceImpactLevel, priceImpactColor, PriceImpactIcon, priceImpact } = usePriceImpact()
const swapRoutesDisclosure = useDisclosure()

const isDefaultSwap = handler instanceof DefaultSwapHandler
const isNativeWrapOrUnwrap = handler instanceof NativeWrapHandler
Expand Down Expand Up @@ -79,56 +92,61 @@ export function SwapDetails() {
You can change your slippage tolerance in your settings.`

return (
<VStack spacing="sm" align="start" w="full" fontSize="sm">
<HStack justify="space-between" w="full">
<Text color={priceImpactColor}>Price impact</Text>
<HStack>
{priceImpactLevel === 'unknown' ? (
<Text>Unknown</Text>
) : (
<NumberText color={priceImpactColor}>
-{toCurrency(priceImpacUsd, { abbreviated: false })} (-{priceImpactLabel})
</NumberText>
)}
<Tooltip
// eslint-disable-next-line max-len
label="This is the negative price impact of the swap based on the current market prices of the token in vs token out."
fontSize="sm"
>
{priceImpactLevel === 'low' ? (
<InfoIcon />
<>
<VStack spacing="sm" align="start" w="full" fontSize="sm">
<HStack justify="space-between" w="full">
<Text color={priceImpactColor}>Price impact</Text>
<HStack>
{priceImpactLevel === 'unknown' ? (
<Text>Unknown</Text>
) : (
<Box>
<PriceImpactIcon priceImpactLevel={priceImpactLevel} />
</Box>
<NumberText color={priceImpactColor}>
-{toCurrency(priceImpacUsd, { abbreviated: false })} (-{priceImpactLabel})
</NumberText>
)}
</Tooltip>
<Tooltip
// eslint-disable-next-line max-len
label="This is the negative price impact of the swap based on the current market prices of the token in vs token out."
fontSize="sm"
>
{priceImpactLevel === 'low' ? (
<InfoIcon />
) : (
<Box>
<PriceImpactIcon priceImpactLevel={priceImpactLevel} />
</Box>
)}
</Tooltip>
</HStack>
</HStack>
</HStack>
<HStack justify="space-between" w="full">
<Text color="grayText">Max slippage</Text>
<HStack>
<NumberText color="grayText">
-{toCurrency(maxSlippageUsd, { abbreviated: false })} (-{fNum('slippage', _slippage)})
</NumberText>
<Tooltip label={slippageLabel} fontSize="sm">
<InfoIcon />
</Tooltip>
<HStack justify="space-between" w="full">
<Text color="grayText">Max slippage</Text>
<HStack>
<NumberText color="grayText">
-{toCurrency(maxSlippageUsd, { abbreviated: false })} (-{fNum('slippage', _slippage)})
</NumberText>
<Tooltip label={slippageLabel} fontSize="sm">
<InfoIcon />
</Tooltip>
</HStack>
</HStack>
</HStack>
<HStack justify="space-between" w="full">
<Text color="grayText">{limitLabel}</Text>
<HStack>
<NumberText color="grayText">
{fNum('token', limitValue, { abbreviated: false })} {limitToken?.symbol}
</NumberText>
<Tooltip label={limitTooltip} fontSize="sm">
<InfoIcon />
</Tooltip>
<HStack justify="space-between" w="full">
<Text color="grayText">{limitLabel}</Text>
<HStack>
<NumberText color="grayText">
{fNum('token', limitValue, { abbreviated: false })} {limitToken?.symbol}
</NumberText>
<Tooltip label={limitTooltip} fontSize="sm">
<InfoIcon />
</Tooltip>
</HStack>
</HStack>
</HStack>

{isDefaultSwap && <OrderRoute />}
</VStack>
{isDefaultSwap && <OrderRoute swapRoutesDisclosure={swapRoutesDisclosure} />}
</VStack>
<SwapRoutesModal
isOpen={swapRoutesDisclosure.isOpen}
onClose={swapRoutesDisclosure.onClose}
/>
</>
)
}
117 changes: 117 additions & 0 deletions lib/modules/swap/charts/SwapRoutesChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { SdkSimulateSwapResponse } from '../swap.types'
import { useSwap } from '../SwapProvider'
import ReactECharts from 'echarts-for-react'
import { useTokens } from '../../../modules/tokens/TokensProvider'
import { GqlChain } from '@/lib/shared/services/api/generated/graphql'
import { renderToString } from 'react-dom/server'
import { Box, VStack } from '@chakra-ui/react'
import { uniqBy } from 'lodash'

type Params = {
data: {
source: string
target: string
value: number
}
value: number
dataIndex: number
}

function ToolTip({
params,
selectedChain,
poolInfoArray,
tokenInfoArray,
}: {
params: Params
selectedChain: GqlChain
poolInfoArray: { symbol: string }[]
tokenInfoArray: { symbol: string | undefined; address: string | undefined }[]
}) {
const sourceTokenInfo = tokenInfoArray.find(token => token.address === params.data.source)
const targetTokenInfo = tokenInfoArray.find(token => token.address === params.data.target)
const poolSymbol = poolInfoArray[params.dataIndex].symbol

return (
<VStack>
<Box>{poolSymbol}</Box>
<Box>
{sourceTokenInfo?.symbol} {'>>>'} {targetTokenInfo?.symbol}: {params.data.value}
</Box>
</VStack>
)
}

export function SwapRoutesChart() {
const { simulationQuery, selectedChain } = useSwap()
const { getToken } = useTokens()

const queryData = simulationQuery.data as SdkSimulateSwapResponse

const dataPaths = queryData.paths
.map(path => path.tokens.map(token => ({ name: token.address })))
.flat()

const uniqueTokenAddresses = uniqBy(dataPaths, 'name')
const data = uniqueTokenAddresses.map(token => ({ name: token.name }))

const links = queryData.routes
.map(route => {
const value = Number(route.hops[0].tokenInAmount)
return route.hops.map(hop => ({ source: hop.tokenIn, target: hop.tokenOut, value }))
})
.flat()

const tokenInfoArray = data.map(name => {
const token = getToken(name.name, selectedChain)
return { symbol: token?.symbol, address: token?.address }
})

const poolInfoArray = queryData.routes
.map(route => {
return route.hops.map(hop => ({
symbol: hop.pool.symbol,
}))
})
.flat()

const option = {
series: {
type: 'sankey',
layout: 'none',
emphasis: {
focus: 'adjacency',
},
data,
links,
label: {
formatter: function (params: any) {
return tokenInfoArray.find(token => token.address === params.data.name)?.symbol
},
rotate: 90,
position: 'inside',
align: 'center',
verticalAlign: 'middle',
},
right: '5%',
nodeWidth: 50,
draggable: false,
},
tooltip: {
trigger: 'item',
triggerOn: 'mousemove',
formatter: function (params: Params) {
return renderToString(
<ToolTip
params={params}
selectedChain={selectedChain}
poolInfoArray={poolInfoArray}
tokenInfoArray={tokenInfoArray}
/>
)
},
},
}

return <ReactECharts style={{ height: '600px', width: '1000px' }} option={option} />
}
33 changes: 33 additions & 0 deletions lib/modules/swap/modal/SwapRoutesModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client'

import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalProps,
Center,
} from '@chakra-ui/react'
import { SwapRoutesChart } from '../charts/SwapRoutesChart'

type Props = {
isOpen: boolean
onClose(): void
}

export function SwapRoutesModal({ isOpen, onClose }: Props & Omit<ModalProps, 'children'>) {
return (
<Modal isOpen={isOpen} onClose={onClose} isCentered size="full">
<ModalContent>
<ModalHeader>Swap routes</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Center>
<SwapRoutesChart />
</Center>
</ModalBody>
</ModalContent>
</Modal>
)
}

0 comments on commit 47b9181

Please sign in to comment.