-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Centrifuge App: Portfolio asset allocation (#1641)
- Loading branch information
1 parent
99dd40f
commit 62dcceb
Showing
12 changed files
with
278 additions
and
53 deletions.
There are no files selected for viewing
97 changes: 82 additions & 15 deletions
97
centrifuge-app/src/components/Portfolio/AssetAllocation.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,26 +1,93 @@ | ||
import { useBalances } from '@centrifuge/centrifuge-react' | ||
import { Box, Text } from '@centrifuge/fabric' | ||
import { Box, Shelf, Stack, Text } from '@centrifuge/fabric' | ||
import Decimal from 'decimal.js-light' | ||
import * as React from 'react' | ||
import { useAddress } from '../../utils/useAddress' | ||
import { useTheme } from 'styled-components' | ||
import { Dec } from '../../utils/Decimal' | ||
import { formatBalanceAbbreviated } from '../../utils/formatting' | ||
import { usePoolMetadataMulti, usePools } from '../../utils/usePools' | ||
import { LabelValueStack } from '../LabelValueStack' | ||
import { AssetClassChart } from './AssetClassChart' | ||
|
||
export function AssetAllocation() { | ||
const address = useAddress() | ||
const assetClassLabels = { | ||
privateCredit: 'Private Credit', | ||
publicCredit: 'Public Credit', | ||
} | ||
type AssetClass = 'publicCredit' | 'privateCredit' | ||
|
||
export function AssetAllocation({ address }: { address: string }) { | ||
const balances = useBalances(address) | ||
const pools = usePools() | ||
const theme = useTheme() | ||
const poolIds = new Set(balances?.tranches.map((t) => t.poolId)) | ||
const filteredPools = pools?.filter((p) => poolIds.has(p.id)) ?? [] | ||
const metas = usePoolMetadataMulti(filteredPools) | ||
const assetClasses = [...new Set(metas.map((m) => m.data?.pool?.asset?.class as string).filter(Boolean))] | ||
const valueByClass: Record<string, Decimal> = Object.fromEntries(assetClasses.map((item) => [item, Dec(0)])) | ||
let total = Dec(0) | ||
balances?.tranches.forEach((balance) => { | ||
const poolIndex = filteredPools.findIndex((p) => p.id === balance.poolId) | ||
const price = | ||
filteredPools[poolIndex]?.tranches.find((t) => t.id === balance.trancheId)?.tokenPrice?.toDecimal() ?? Dec(0) | ||
const asset = metas[poolIndex].data?.pool?.asset?.class | ||
const value = balance.balance.toDecimal().mul(price) | ||
total = total.add(value) | ||
valueByClass[asset!] = valueByClass[asset!]?.add(value) | ||
}) | ||
|
||
const shades = [600, 800, 200, 400] | ||
const shares = assetClasses | ||
.map((item, index) => { | ||
const nextShade = shades[index % shades.length] | ||
return { | ||
name: assetClassLabels[item as AssetClass] ?? item, | ||
value: valueByClass[item].toNumber(), | ||
color: theme.colors.accentScale[nextShade], | ||
labelColor: nextShade >= 500 ? 'white' : 'black', | ||
} | ||
}) | ||
.sort((a, b) => (b.value > a.value ? 1 : a === b ? 0 : -1)) | ||
|
||
return !!balances?.tranches && !!balances?.tranches.length ? ( | ||
<Box as="article"> | ||
<Stack gap={2}> | ||
<Text as="h2" variant="heading2"> | ||
Allocation | ||
</Text> | ||
<Box as="ul" role="list"> | ||
{balances?.tranches.map((tranche, index) => ( | ||
<Box as="li" key={`${tranche.trancheId}${index}`}> | ||
<Box> | ||
<Text>Asset Class</Text>{' '} | ||
</Box> | ||
</Box> | ||
))} | ||
</Box> | ||
</Box> | ||
<Shelf gap={8}> | ||
<AssetClassChart data={shares} currency="USD" total={total.toNumber()} /> | ||
<Shelf as="ul" alignSelf="stretch" alignItems="stretch" flex={1} gap={6}> | ||
{shares.map((cell, i) => ( | ||
<> | ||
{i > 0 && <Box width="1px" backgroundColor="borderSecondary" />} | ||
<LabelValueStack | ||
label={ | ||
<Box position="relative" ml={22}> | ||
{cell.name} | ||
<div | ||
style={{ | ||
width: 12, | ||
height: 12, | ||
borderRadius: '50%', | ||
backgroundColor: cell.color, | ||
position: 'absolute', | ||
top: '50%', | ||
right: 'calc(100% + 10px)', | ||
transform: 'translateY(-50%)', | ||
}} | ||
/> | ||
</Box> | ||
} | ||
value={ | ||
<Box ml={22}> | ||
<Text variant="heading2">{formatBalanceAbbreviated(cell.value, 'USD')}</Text> | ||
</Box> | ||
} | ||
key={i} | ||
/> | ||
</> | ||
))} | ||
</Shelf> | ||
</Shelf> | ||
</Stack> | ||
) : null | ||
} |
52 changes: 52 additions & 0 deletions
52
centrifuge-app/src/components/Portfolio/AssetClassChart.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,52 @@ | ||
import React from 'react' | ||
import { Cell, Pie, PieChart as RechartsPieChart, Tooltip, TooltipProps } from 'recharts' | ||
import { formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting' | ||
import { TooltipContainer, TooltipEntry, TooltipTitle } from '../Charts/CustomChartElements' | ||
|
||
type PieChartProps = { | ||
data: { name: string; value: number; color?: string }[] | ||
currency: string | ||
total: number | ||
} | ||
|
||
export function AssetClassChart({ data, currency, total }: PieChartProps) { | ||
return ( | ||
<RechartsPieChart width={150} height={150} style={{ fontFamily: 'Inter' }}> | ||
<Pie | ||
data={data} | ||
cx="50%" | ||
cy="50%" | ||
outerRadius={72} | ||
innerRadius={36} | ||
dataKey="value" | ||
nameKey="name" | ||
startAngle={-270} | ||
> | ||
{data.map((item, index) => ( | ||
<Cell | ||
key={`cell-${item.color}-${index}`} | ||
fill={item?.color || 'transparent'} | ||
stroke={item?.color! || 'transparent'} | ||
/> | ||
))} | ||
</Pie> | ||
<Tooltip content={<TooltipContent currency={currency} total={total} />} /> | ||
</RechartsPieChart> | ||
) | ||
} | ||
|
||
function TooltipContent({ payload, currency, total }: TooltipProps<any, any> & { currency: string; total: number }) { | ||
if (payload && payload.length > 0) { | ||
return ( | ||
<TooltipContainer> | ||
<TooltipTitle>{payload[0].name}</TooltipTitle> | ||
{payload.map(({ payload, name, value }) => ( | ||
<TooltipEntry name={formatBalanceAbbreviated(value, currency)} color={payload.color} key={name}> | ||
{formatPercentage((value / total) * 100)} | ||
</TooltipEntry> | ||
))} | ||
</TooltipContainer> | ||
) | ||
} | ||
return null | ||
} |
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
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
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
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
Oops, something went wrong.