Skip to content

Commit

Permalink
Pool overview grapg changes
Browse files Browse the repository at this point in the history
  • Loading branch information
kattylucy committed Sep 10, 2024
1 parent 0b56e05 commit 679ef78
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 105 deletions.
223 changes: 131 additions & 92 deletions centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AnchorButton, Box, IconDownload, Select, Shelf, Stack, Tabs, TabsItem,
import * as React from 'react'
import { useParams } from 'react-router'
import { Bar, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import styled, { useTheme } from 'styled-components'
import { useTheme } from 'styled-components'
import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl'
import { daysBetween, formatDate } from '../../utils/date'
import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting'
Expand All @@ -15,9 +15,8 @@ import { getRangeNumber } from './utils'
type ChartData = {
day: Date
nav: number
juniorTokenPrice?: number
seniorTokenPrice?: number
price: number
juniorTokenPrice: number | null
seniorTokenPrice?: number | null
currency?: string
}

Expand All @@ -26,38 +25,29 @@ type Tranche = {
tokenPrice: number
}

const RangeFilterButton = styled(Stack)`
&:hover {
cursor: pointer;
}
`

const StyledTabs = styled(Tabs)`
padding: 8px;
`

const rangeFilters = [
{ value: 'all', label: 'All' },
{ value: '30d', label: '30 days' },
{ value: '90d', label: '90 days' },
{ value: 'ytd', label: 'Year to date' },
{ value: 'all', label: 'All' },
] as const
]

function calculateTranchePrices(pool: any) {
if (!pool?.tranches) return { juniorPrice: null, mezzaninePrice: null, seniorPrice: null }
if (!pool?.tranches) return { juniorPrice: null, seniorPrice: null }

const juniorTranche = pool.tranches.find((t: Tranche) => t.seniority === 0)
const seniorTranche = pool.tranches.find((t: Tranche) => t.seniority === 1)
const seniorTranche = pool.tranches.length > 1 ? pool.tranches.find((t: Tranche) => t.seniority === 1) : null

const juniorTokenPrice = juniorTranche ? Number(formatBalance(juniorTranche.tokenPrice, undefined, 5, 5)) : null
const seniorTokenPrice = seniorTranche ? Number(formatBalance(seniorTranche.tokenPrice, undefined, 5, 5)) : null

return { juniorTokenPrice, seniorTokenPrice }
}

function PoolPerformanceChart() {
const theme = useTheme()
const [selectedTabIndex, setSelectedTabIndex] = React.useState(0)
const chartColor = theme.colors.accentPrimary
const chartColor = theme.colors.textGold
const { pid: poolId } = useParams<{ pid: string }>()

if (!poolId) throw new Error('Pool not found')
Expand Down Expand Up @@ -97,15 +87,37 @@ function PoolPerformanceChart() {

const data: ChartData[] = React.useMemo(
() =>
truncatedPoolStates?.map((day) => {
truncatedPoolStates?.map((day: any) => {
const nav = day.poolState.netAssetValue.toDecimal().toNumber()
const price = (isSingleTranche && Object.values(day.tranches)[0].price?.toFloat()) || null

const trancheKeys = Object.keys(day.tranches)
const juniorTrancheKey = trancheKeys[0]
const seniorTrancheKey = trancheKeys[1] || null

const juniorTokenPrice = (day.tranches[juniorTrancheKey]?.price as any)?.toFloat() || null
const seniorTokenPrice = seniorTrancheKey
? (day.tranches[seniorTrancheKey]?.price as any)?.toFloat() || null
: null

if (day.timestamp && new Date(day.timestamp).toDateString() === new Date().toDateString()) {
return { day: new Date(day.timestamp), nav: todayAssetValue, price: Number(todayPrice) }
const tranchePrices = calculateTranchePrices(pool)

return {
day: new Date(day.timestamp),
nav: todayAssetValue,
juniorTokenPrice: tranchePrices.juniorTokenPrice ?? null,
seniorTokenPrice: tranchePrices.seniorTokenPrice ?? null,
}
}

return {
day: new Date(day.timestamp),
nav: Number(nav),
juniorTokenPrice,
seniorTokenPrice,
}
return { day: new Date(day.timestamp), nav: Number(nav), price: Number(price) }
}) || [],
[isSingleTranche, truncatedPoolStates, todayAssetValue, todayPrice]
[isSingleTranche, truncatedPoolStates, todayAssetValue, todayPrice, pool]

Check warning on line 120 in centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx

View workflow job for this annotation

GitHub Actions / build-app

React Hook React.useMemo has unnecessary dependencies: 'isSingleTranche' and 'todayPrice'. Either exclude them or remove the dependency array

Check warning on line 120 in centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

React Hook React.useMemo has unnecessary dependencies: 'isSingleTranche' and 'todayPrice'. Either exclude them or remove the dependency array
)

const today = {
Expand All @@ -122,10 +134,19 @@ function PoolPerformanceChart() {
return undefined
}

const filteredData = chartData.map((data) => ({
day: data.day,
tokenPrice: data.price,
}))
const filteredData = chartData.map((data) => {
const base = {
day: data.day,
nav: data.nav,
juniorTokenPrice: data.juniorTokenPrice,
}
if (data.seniorTokenPrice) {
return {
...base,
seniorTokenPrice: data.seniorTokenPrice,
}
} else return { ...base }
})

return getCSVDownloadUrl(filteredData as any)
}, [chartData])
Expand All @@ -134,14 +155,19 @@ function PoolPerformanceChart() {
if (!chartData) return [0, 100]

const min =
chartData?.reduce((prev, curr) => {
return prev.price! < curr.price! ? prev : curr
}, chartData[0])?.price || 0
chartData.reduce((prev, curr) => {
const currMin = Math.min(curr.juniorTokenPrice ?? Infinity, curr.seniorTokenPrice ?? Infinity)
const prevMin = Math.min(prev.juniorTokenPrice ?? Infinity, prev.seniorTokenPrice ?? Infinity)
return currMin < prevMin ? curr : prev
}, chartData[0])?.juniorTokenPrice ?? 0

const max =
chartData?.reduce((prev, curr) => {
return prev.price! > curr.price! ? prev : curr
}, chartData[0])?.price || 1
chartData.reduce((prev, curr) => {
const currMax = Math.max(curr.juniorTokenPrice ?? -Infinity, curr.seniorTokenPrice ?? -Infinity)
const prevMax = Math.max(prev.juniorTokenPrice ?? -Infinity, prev.seniorTokenPrice ?? -Infinity)
return currMax > prevMax ? curr : prev
}, chartData[0])?.juniorTokenPrice ?? 1

return [min, max]
}, [chartData])

Expand All @@ -165,8 +191,6 @@ function PoolPerformanceChart() {
return result
}

console.log(rangeFilters)

return (
<Stack gap={2} padding={20}>
<Stack flexDirection="row" justifyContent="space-between" alignItems="center" mb={12}>
Expand All @@ -191,31 +215,8 @@ function PoolPerformanceChart() {
Download
</AnchorButton>
</Stack>
<Stack>
<CustomLegend selectedTabIndex={selectedTabIndex} data={today} />
<Shelf justifyContent="flex-end">
{chartData.length > 0 &&
rangeFilters.map((rangeFilter, index) => (
<React.Fragment key={rangeFilter.label}>
<RangeFilterButton gap={1} onClick={() => setRange(rangeFilter)}>
<Text variant="body3" whiteSpace="nowrap">
<Text variant={rangeFilter.value === range.value && 'emphasized'}>{rangeFilter.label}</Text>
</Text>
<Box
width="100%"
backgroundColor={rangeFilter.value === range.value ? '#000000' : '#E0E0E0'}
height="2px"
/>
</RangeFilterButton>
{index !== rangeFilters.length - 1 && (
<Box width="24px" backgroundColor="#E0E0E0" height="2px" alignSelf="flex-end" />
)}
</React.Fragment>
))}
</Shelf>
</Stack>

<Shelf gap={4} width="100%" color="textSecondary">
<CustomLegend selectedTabIndex={selectedTabIndex} data={today} setRange={setRange} />
<Shelf gap={4} width="100%" color="textSecondary" mt={4}>
{chartData?.length ? (
<ResponsiveContainer width="100%" height={200} minHeight={200} maxHeight={200}>
<ComposedChart data={chartData} margin={{ left: -36 }}>
Expand All @@ -238,19 +239,20 @@ function PoolPerformanceChart() {
<YAxis
stroke="none"
tickLine={false}
style={{ fontSize: '10px', fill: theme.colors.textSecondary }}
style={{ fontSize: '10px', fill: theme.colors.textPrimary }}
tickFormatter={(tick: number) => formatBalanceAbbreviated(tick, '', 0)}
yAxisId="left"
width={80}
/>
<YAxis
stroke="none"
tickLine={false}
style={{ fontSize: '10px', fill: theme.colors.textSecondary }}
style={{ fontSize: '10px', fill: theme.colors.textPrimary }}
tickFormatter={(tick: number) => formatBalanceAbbreviated(tick, '', 6)}
yAxisId="right"
orientation="right"
domain={priceRange}
hide={true}
/>
<CartesianGrid stroke={theme.colors.borderPrimary} vertical={false} />
<Tooltip
Expand All @@ -259,28 +261,64 @@ function PoolPerformanceChart() {
return (
<TooltipContainer>
<TooltipTitle>{formatDate(payload[0].payload.day)}</TooltipTitle>
{payload.map(({ name, value }, index) => (
<Shelf justifyContent="space-between" pl="4px" key={index}>
<Text variant="label2">
{name === 'nav' ? 'NAV' : name === 'price' ? 'Token price' : 'Cash'}
</Text>
<Text variant="label2">
{name === 'nav' && typeof value === 'number'
? formatBalance(value, 'USD')
: typeof value === 'number'
? formatBalance(value, 'USD', 6)
: '-'}
</Text>
</Shelf>
))}
{payload.map(({ name, value }, index) => {
let label = ''
if (name === 'nav') label = 'NAV'
else if (name === 'juniorTokenPrice') label = 'Junior Token Price'
else if (name === 'seniorTokenPrice') label = 'Senior Token Price'
else label = 'Cash'

return (
<Shelf justifyContent="space-between" pl="4px" key={index}>
<Text color="textPrimary" variant="label2">
{label}
</Text>
<Text color="textPrimary" variant="label2">
{typeof value === 'number'
? formatBalance(
value,
name === 'nav' ? pool.currency.symbol ?? 'USD' : '',
name === 'juniorTokenPrice' || name === 'seniorTokenPrice' ? 6 : 0
)
: '-'}
</Text>
</Shelf>
)
})}
</TooltipContainer>
)
}
return null
}}
/>
<Bar type="monotone" dataKey="nav" strokeWidth={0} fillOpacity={1} fill="#dbe5ff" yAxisId="left" />
<Line type="monotone" dataKey="price" stroke="#FFC012" strokeWidth={2} dot={false} yAxisId="right" />
<Bar
type="monotone"
dataKey="nav"
strokeWidth={0}
fillOpacity={1}
fill={theme.colors.backgroundTertiary}
yAxisId="left"
/>
<Line
type="monotone"
dataKey="juniorTokenPrice"
stroke={theme.colors.textGold}
strokeWidth={2}
dot={false}
yAxisId="right"
name="juniorTokenPrice"
/>
{chartData.some((d) => d.seniorTokenPrice !== null) && (
<Line
type="monotone"
dataKey="seniorTokenPrice"
stroke={theme.colors.backgroundInverted}
strokeWidth={2}
dot={false}
yAxisId="right"
name="seniorTokenPrice"
/>
)}
</ComposedChart>
</ResponsiveContainer>
) : (
Expand All @@ -293,18 +331,16 @@ function PoolPerformanceChart() {

function CustomLegend({
data,
selectedTabIndex,
setRange,
}: {
data: {
currency: string
nav: number
juniorTokenPrice: number
seniorTokenPrice?: number | null
}
selectedTabIndex: number
setRange: (value: { value: string; label: string }) => void
}) {
const theme = useTheme()

const Dot = ({ color }: { color: string }) => (
<Box width="8px" height="8px" borderRadius="50%" backgroundColor={color} marginRight="4px" />
)
Expand All @@ -315,32 +351,35 @@ function CustomLegend({
label: `NAV ${data.currency}`,
value: formatBalance(data.nav),
type: 'nav',
show: true,
},
{
color: 'textGold',
label: 'Junior token price',
value: data.juniorTokenPrice ?? 0,
type: 'singleTrancheTokenPrice',
show: true,
},
{
color: 'textPrimary',
label: 'Senior token price',
value: data.seniorTokenPrice ?? 0,
type: 'singleTrancheTokenPrice',
show: !!data.seniorTokenPrice,
},
]

const accordionData = [
{ label: '30 days', value: '30d' },
{ label: '90 days', value: '90d' },
{ label: 'Year to date', value: 'ytd' },
{ label: 'All', value: 'all' },
]
const toggleRange = (e: any) => {
const value = e.target.value
const range = rangeFilters.find((range) => range.value === value)
setRange(range ?? rangeFilters[0])
}

return (
<Box display="flex" justifyContent="space-between">
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box display="flex" justifyContent="space-evenly">
{buildData.map((item, index) => {
if (!item.show) return

Check warning on line 382 in centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx

View workflow job for this annotation

GitHub Actions / build-app

Array.prototype.map() expects a return value from arrow function

Check warning on line 382 in centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx

View workflow job for this annotation

GitHub Actions / ff-prod / build-app

Array.prototype.map() expects a return value from arrow function
return (
<Stack key={index} pl={1} display="flex" marginRight="20px">
<Box display="flex" alignItems="center">
Expand All @@ -353,7 +392,7 @@ function CustomLegend({
})}
</Box>
<Box>
<Select options={accordionData} />
<Select options={rangeFilters} onChange={toggleRange} hideBorder />
</Box>
</Box>
)
Expand All @@ -364,7 +403,7 @@ const CustomTick = ({ x, y, payload }: any) => {
return (
<g transform={`translate(${x},${y})`}>
<text
style={{ fontSize: '10px', fill: theme.colors.textSecondary, letterSpacing: '-0.5px' }}
style={{ fontSize: '10px', fill: theme.colors.textPrimary, letterSpacing: '-0.5px' }}
x={0}
y={0}
dy={16}
Expand Down
Loading

0 comments on commit 679ef78

Please sign in to comment.