-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: pool performance chart (#1969)
* feat: pool performance chart * address pr feedback * hide sections for tinlake pools * add interval logic * hide nav change * address pr feedback * revert env * remove unnecessary width and height
- Loading branch information
JP
committed
Feb 22, 2024
1 parent
8328f94
commit 43b90a1
Showing
6 changed files
with
284 additions
and
162 deletions.
There are no files selected for viewing
119 changes: 0 additions & 119 deletions
119
centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx
This file was deleted.
Oops, something went wrong.
219 changes: 219 additions & 0 deletions
219
centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import { Box, Grid, Shelf, Stack, Text } from '@centrifuge/fabric' | ||
import * as React from 'react' | ||
import { useParams } from 'react-router' | ||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' | ||
import styled, { useTheme } from 'styled-components' | ||
import { daysBetween, formatDate } from '../../utils/date' | ||
import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' | ||
import { useDailyPoolStates, usePool } from '../../utils/usePools' | ||
import { TooltipContainer, TooltipTitle } from './Tooltip' | ||
import { getRangeNumber } from './utils' | ||
|
||
type ChartData = { | ||
day: Date | ||
nav: number | ||
} | ||
|
||
const RangeFilterButton = styled(Stack)` | ||
&:hover { | ||
cursor: pointer; | ||
} | ||
` | ||
|
||
const rangeFilters = [ | ||
{ value: '30d', label: '30 days' }, | ||
{ value: '90d', label: '90 days' }, | ||
{ value: 'ytd', label: 'Year to date' }, | ||
{ value: 'all', label: 'All' }, | ||
] as const | ||
|
||
const chartColor = '#A4D5D8' | ||
|
||
function PoolPerformanceChart() { | ||
const theme = useTheme() | ||
const { pid: poolId } = useParams<{ pid: string }>() | ||
const { poolStates } = useDailyPoolStates(poolId) || {} | ||
const pool = usePool(poolId) | ||
const poolAge = pool.createdAt ? daysBetween(pool.createdAt, new Date()) : 0 | ||
|
||
const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'ytd', label: 'Year to date' }) | ||
const rangeNumber = getRangeNumber(range.value, poolAge) | ||
|
||
const data: ChartData[] = React.useMemo( | ||
() => | ||
poolStates?.map((day) => { | ||
const nav = | ||
day.poolState.portfolioValuation.toDecimal().toNumber() + day.poolState.totalReserve.toDecimal().toNumber() | ||
|
||
return { day: new Date(day.timestamp), nav } | ||
}) || [], | ||
[poolStates] | ||
) | ||
|
||
if (poolStates && poolStates?.length < 1 && poolAge > 0) return <Text variant="body2">No data available</Text> | ||
|
||
// querying chain for more accurate data, since data for today from subquery is not necessarily up to date | ||
const todayAssetValue = pool?.nav.latest.toDecimal().toNumber() || 0 | ||
const todayReserve = pool?.reserve.total.toDecimal().toNumber() || 0 | ||
|
||
const chartData = data.slice(-rangeNumber) | ||
|
||
const today = { | ||
day: new Date(), | ||
nav: todayReserve + todayAssetValue, | ||
navChange: chartData.length > 0 ? todayReserve + todayAssetValue - chartData[0]?.nav : 0, | ||
} | ||
|
||
const getXAxisInterval = () => { | ||
if (rangeNumber <= 30) return 5 | ||
if (rangeNumber > 30 && rangeNumber <= 90) { | ||
return 14 | ||
} | ||
if (rangeNumber > 90 && rangeNumber <= 180) { | ||
return 30 | ||
} | ||
return 45 | ||
} | ||
|
||
return ( | ||
<Stack gap={2}> | ||
<Stack> | ||
<CustomLegend 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"> | ||
{chartData?.length ? ( | ||
<ResponsiveContainer width="100%" height="100%" minHeight="200px"> | ||
<AreaChart data={chartData} margin={{ left: -36 }}> | ||
<defs> | ||
<linearGradient id="colorPoolValue" x1="0" y1="0" x2="0" y2="1"> | ||
<stop offset="5%" stopColor={chartColor} stopOpacity={0.4} /> | ||
<stop offset="95%" stopColor={chartColor} stopOpacity={0.2} /> | ||
</linearGradient> | ||
</defs> | ||
<XAxis | ||
dataKey="day" | ||
tickLine={false} | ||
type="category" | ||
tickFormatter={(tick: number) => { | ||
if (data.length > 180) { | ||
return new Date(tick).toLocaleString('en-US', { month: 'short' }) | ||
} | ||
return new Date(tick).toLocaleString('en-US', { day: 'numeric', month: 'short' }) | ||
}} | ||
style={{ fontSize: '10px', fill: theme.colors.textSecondary, letterSpacing: '-0.5px' }} | ||
dy={4} | ||
interval={getXAxisInterval()} | ||
/> | ||
<YAxis | ||
stroke="none" | ||
tickLine={false} | ||
style={{ fontSize: '10px', fill: theme.colors.textSecondary }} | ||
tickFormatter={(tick: number) => formatBalanceAbbreviated(tick, '', 0)} | ||
/> | ||
<CartesianGrid stroke={theme.colors.borderSecondary} vertical={false} /> | ||
<Tooltip | ||
content={({ payload }) => { | ||
if (payload && payload?.length > 0) { | ||
return ( | ||
<TooltipContainer> | ||
<TooltipTitle>{formatDate(payload[0].payload.day)}</TooltipTitle> | ||
{payload.map(({ value }, index) => ( | ||
<Shelf justifyContent="space-between" pl="4px" key={index}> | ||
<Text variant="label2">NAV</Text> | ||
<Text variant="label2"> | ||
{typeof value === 'number' ? formatBalance(value, 'USD' || '') : '-'} | ||
</Text> | ||
</Shelf> | ||
))} | ||
</TooltipContainer> | ||
) | ||
} | ||
return null | ||
}} | ||
/> | ||
<Area type="monotone" dataKey="nav" strokeWidth={0} fillOpacity={1} fill="url(#colorPoolValue)" /> | ||
</AreaChart> | ||
</ResponsiveContainer> | ||
) : ( | ||
<Text variant="label1">No data yet</Text> | ||
)} | ||
</Shelf> | ||
</Stack> | ||
) | ||
} | ||
|
||
function CustomLegend({ | ||
data, | ||
}: { | ||
data: { | ||
day: Date | ||
nav: number | ||
navChange: number | ||
} | ||
}) { | ||
const theme = useTheme() | ||
|
||
// const navChangePercentageChange = (data.navChange / data.nav) * 100 | ||
// const navChangePercentageChangeString = | ||
// data.navChange === data.nav || navChangePercentageChange === 0 | ||
// ? '' | ||
// : ` (${navChangePercentageChange > 0 ? '+' : ''}${navChangePercentageChange.toFixed(2)}%)` | ||
|
||
return ( | ||
<Shelf bg="backgroundPage" width="100%" gap={2}> | ||
<Grid pb={2} gridTemplateColumns="fit-content(100%) fit-content(100%)" width="100%" gap={8}> | ||
<Stack | ||
borderLeftWidth="3px" | ||
pl={1} | ||
borderLeftStyle="solid" | ||
borderLeftColor={theme.colors.accentPrimary} | ||
gap="4px" | ||
> | ||
<Text variant="body3" color="textSecondary"> | ||
NAV | ||
</Text> | ||
<Text variant="body1">{formatBalance(data.nav, 'USD')}</Text> | ||
</Stack> | ||
{/* <Stack | ||
borderLeftWidth="3px" | ||
pl={1} | ||
borderLeftStyle="solid" | ||
borderLeftColor={theme.colors.textPrimary} | ||
gap="4px" | ||
> | ||
<Text variant="body3" color="textSecondary"> | ||
NAV change | ||
</Text> | ||
<Text variant="body1" color={data.navChange > 0 && 'statusOk'}> | ||
{data.navChange > 0 && '+'} | ||
{formatBalance(data.navChange, 'USD')} | ||
{navChangePercentageChangeString} | ||
</Text> | ||
</Stack> */} | ||
</Grid> | ||
</Shelf> | ||
) | ||
} | ||
|
||
export default PoolPerformanceChart |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
export const getRangeNumber = (rangeValue: string, poolAge?: number) => { | ||
if (rangeValue === '30d') { | ||
return 30 | ||
} | ||
if (rangeValue === '90d') { | ||
return 90 | ||
} | ||
|
||
if (rangeValue === 'ytd') { | ||
const today = new Date() | ||
const januaryFirst = new Date(today.getFullYear(), 0, 1) | ||
const timeDifference = new Date(today).getTime() - new Date(januaryFirst).getTime() | ||
const daysSinceJanuary1 = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) | ||
|
||
return daysSinceJanuary1 | ||
} | ||
|
||
if (rangeValue === 'all' && poolAge) { | ||
return poolAge | ||
} | ||
|
||
return 30 | ||
} |
12 changes: 9 additions & 3 deletions
12
centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,15 @@ | ||
import { Card } from '@centrifuge/fabric' | ||
import { Card, Stack, Text } from '@centrifuge/fabric' | ||
import PoolPerformanceChart from '../Charts/PoolPerformanceChart' | ||
|
||
export const PoolPerformance = () => { | ||
return ( | ||
<Card width="100%" height="100%"> | ||
Pool Performance | ||
<Card p={3}> | ||
<Stack gap={2}> | ||
<Text fontSize="18px" fontWeight="500"> | ||
Pool performance | ||
</Text> | ||
<PoolPerformanceChart /> | ||
</Stack> | ||
</Card> | ||
) | ||
} |
Oops, something went wrong.