From 43b90a1834aaf647b2025d3d74878a502546d12b Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 21 Feb 2024 11:35:43 -0600 Subject: [PATCH] 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 --- .../Charts/PoolAssetReserveChart.tsx | 119 ---------- .../Charts/PoolPerformanceChart.tsx | 219 ++++++++++++++++++ centrifuge-app/src/components/Charts/utils.ts | 23 ++ .../PoolOverview/PoolPerfomance.tsx | 12 +- .../components/Portfolio/PortfolioValue.tsx | 17 +- .../src/pages/Pool/Overview/index.tsx | 56 +++-- 6 files changed, 284 insertions(+), 162 deletions(-) delete mode 100644 centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx create mode 100644 centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx create mode 100644 centrifuge-app/src/components/Charts/utils.ts diff --git a/centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx b/centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx deleted file mode 100644 index 45171f6414..0000000000 --- a/centrifuge-app/src/components/Charts/PoolAssetReserveChart.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Grid, Shelf, Stack, Text } from '@centrifuge/fabric' -import * as React from 'react' -import { useParams } from 'react-router' -import { Area, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' -import { useTheme } from 'styled-components' -import { daysBetween } from '../../utils/date' -import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' -import { useDailyPoolStates, usePool } from '../../utils/usePools' -import { Tooltips } from '../Tooltips' -import { CustomizedTooltip } from './Tooltip' - -type ChartData = { - day: Date - poolValue: number - assetValue: number - reserve: [number, number] -} - -function PoolAssetReserveChart() { - 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 data: ChartData[] = React.useMemo(() => { - return ( - poolStates?.map((day) => { - const assetValue = day.poolState.portfolioValuation.toDecimal().toNumber() - const poolValue = day.poolValue.toDecimal().toNumber() - return { day: new Date(day.timestamp), poolValue, assetValue, reserve: [assetValue, poolValue] } - }) || [] - ) - }, [poolStates]) - - if (poolStates && poolStates?.length < 1 && poolAge > 0) return No data available - - // querying chain for more accurate data, since data for today from subquery is not necessarily up to date - const todayPoolValue = pool?.value.toDecimal().toNumber() || 0 - const todayAssetValue = pool?.nav.latest.toDecimal().toNumber() || 0 - const today: ChartData = { - day: new Date(), - poolValue: todayPoolValue, - assetValue: todayAssetValue, - reserve: [todayAssetValue, todayPoolValue], - } - - const chartData = [...data.slice(0, data.length - 1), today] - - return ( - - - - {chartData?.length ? ( - - - { - 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' }} - /> - formatBalanceAbbreviated(tick, '', 0)} - /> - - } /> - - - - - - ) : ( - No data yet - )} - - - ) -} - -function CustomLegend({ data, currency }: { currency: string; data: ChartData }) { - const theme = useTheme() - - return ( - - - - - {formatBalance(data.poolValue, currency)} - - - - {formatBalance(data.assetValue, currency)} - - - - {formatBalance(data.reserve[1] - data.reserve[0], currency)} - - - - ) -} - -export default PoolAssetReserveChart diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx new file mode 100644 index 0000000000..69bd543d8a --- /dev/null +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -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 No data available + + // 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 ( + + + + + {chartData.length > 0 && + rangeFilters.map((rangeFilter, index) => ( + + setRange(rangeFilter)}> + + {rangeFilter.label} + + + + {index !== rangeFilters.length - 1 && ( + + )} + + ))} + + + + + {chartData?.length ? ( + + + + + + + + + { + 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()} + /> + formatBalanceAbbreviated(tick, '', 0)} + /> + + { + if (payload && payload?.length > 0) { + return ( + + {formatDate(payload[0].payload.day)} + {payload.map(({ value }, index) => ( + + NAV + + {typeof value === 'number' ? formatBalance(value, 'USD' || '') : '-'} + + + ))} + + ) + } + return null + }} + /> + + + + ) : ( + No data yet + )} + + + ) +} + +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 ( + + + + + NAV + + {formatBalance(data.nav, 'USD')} + + {/* + + NAV change + + 0 && 'statusOk'}> + {data.navChange > 0 && '+'} + {formatBalance(data.navChange, 'USD')} + {navChangePercentageChangeString} + + */} + + + ) +} + +export default PoolPerformanceChart diff --git a/centrifuge-app/src/components/Charts/utils.ts b/centrifuge-app/src/components/Charts/utils.ts new file mode 100644 index 0000000000..53ef7bfe81 --- /dev/null +++ b/centrifuge-app/src/components/Charts/utils.ts @@ -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 +} diff --git a/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx index e3458f8d6e..16c10a7cc8 100644 --- a/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx +++ b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx @@ -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 ( - - Pool Performance + + + + Pool performance + + + ) } diff --git a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx index ee4cbf88ed..bfe17f179d 100644 --- a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx +++ b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx @@ -2,6 +2,7 @@ import { Card, Stack, Text } from '@centrifuge/fabric' import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' import { formatDate } from '../../utils/date' import { formatBalance } from '../../utils/formatting' +import { getRangeNumber } from '../Charts/utils' import { useDailyPortfolioValue } from './usePortfolio' const chartColor = '#006ef5' @@ -103,19 +104,3 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad ) } - -const getRangeNumber = (rangeValue: string) => { - if (rangeValue === '30d') { - return 30 - } - if (rangeValue === '90d') { - return 90 - } - - 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 -} diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 0ece6789d6..47dbc37a12 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -137,30 +137,38 @@ export function PoolDetailOverview() { - - - }> - - - }> - - - - - - }> - - - - - - - }> - - - - - + {!isTinlakePool && ( + <> + + + }> + + + }> + + + + + + }> + + + + + + + }> + + + + + + + )} ) }