Skip to content
This repository was archived by the owner on Apr 10, 2025. It is now read-only.

Commit a1c04fd

Browse files
authored
feat: better assets bar (#262)
* feat: better assets bar * chore: additional comments * fix: use tailwind native units
1 parent 928ee31 commit a1c04fd

File tree

1 file changed

+61
-78
lines changed

1 file changed

+61
-78
lines changed

src/pages/portfolio/ui/assets-table.tsx

Lines changed: 61 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -136,32 +136,59 @@ const NotConnectedNotice = () => {
136136
);
137137
};
138138

139-
const calculateAssetDistribution = async (balances: BalancesResponse[]) => {
139+
interface AssetDistribution {
140+
percentage: number;
141+
color: string;
142+
hasError: boolean;
143+
isOther: boolean;
144+
}
145+
146+
interface DistributionResult {
147+
distribution: AssetDistribution[];
148+
sortedBalances: BalancesResponse[];
149+
}
150+
151+
interface ValueWithMetadata {
152+
value: number;
153+
balance: BalancesResponse;
154+
hasError: boolean;
155+
}
156+
157+
/** Minimum percentage threshold for an asset to be shown individually. Assets below this are grouped into "Other". Unit: % */
158+
const SMALL_ASSET_THRESHOLD = 2;
159+
160+
/** HSL color saturation for asset bars. Unit: % */
161+
const COLOR_SATURATION = 95;
162+
163+
/** HSL color lightness for asset bars. Unit: % */
164+
const COLOR_LIGHTNESS = 53;
165+
166+
/** Angle used in the golden ratio color distribution algorithm to ensure visually distinct colors. Unit: degrees */
167+
const GOLDEN_RATIO_ANGLE = 137.5;
168+
169+
const calculateAssetDistribution = (balances: BalancesResponse[]): DistributionResult => {
140170
// First filter out NFTs and special assets
141171
const displayableBalances = balances.filter(balance => {
142172
const metadata = getMetadataFromBalancesResponse.optional(balance);
173+
// If we don't have a symbol, we can't display it
143174
if (!metadata?.symbol) {
144175
return false;
145176
}
146-
// Filter out LP NFTs, unbonding tokens, and auction tokens
147-
const isSpecialAsset =
148-
metadata.symbol.startsWith('lpNft') ||
149-
metadata.symbol.startsWith('unbond') ||
150-
metadata.symbol.startsWith('auction');
151-
return !isSpecialAsset;
152-
});
153177

154-
const validBalances = displayableBalances.filter(balance => {
155-
const valueView = getBalanceView.optional(balance);
156-
const metadata = getMetadataFromBalancesResponse.optional(balance);
157-
return valueView && metadata;
178+
// Filter out LP NFTs, unbonding tokens, and auction tokens
179+
return (
180+
!metadata.symbol.startsWith('lpNft') &&
181+
!metadata.symbol.startsWith('unbond') &&
182+
!metadata.symbol.startsWith('auction')
183+
);
158184
});
159185

160-
// Calculate values first
161-
const valuesWithMetadata = validBalances.map(balance => {
186+
// Calculate values and handle errors
187+
const valuesWithMetadata: ValueWithMetadata[] = displayableBalances.map(balance => {
162188
const valueView = getBalanceView.optional(balance);
163189
const metadata = getMetadataFromBalancesResponse.optional(balance);
164190
const amount = valueView?.valueView.value?.amount;
191+
165192
if (!amount || !metadata) {
166193
console.warn(
167194
'Missing amount or metadata for balance',
@@ -181,15 +208,7 @@ const calculateAssetDistribution = async (balances: BalancesResponse[]) => {
181208
);
182209

183210
if (Number.isNaN(formattedAmount)) {
184-
console.warn(
185-
'Failed to format amount for',
186-
metadata.symbol,
187-
'amount:',
188-
amount.toJsonString(),
189-
'exponent:',
190-
getDisplayDenomExponent(metadata),
191-
);
192-
return { value: 0, balance, hasError: true };
211+
throw new Error('Failed to format amount');
193212
}
194213

195214
return {
@@ -216,70 +235,43 @@ const calculateAssetDistribution = async (balances: BalancesResponse[]) => {
216235
return { distribution: [], sortedBalances: [] };
217236
}
218237

219-
// Take the first 4 bytes of the hash and convert to an integer
220-
const getHueFromHash = (buffer: ArrayBuffer) => {
221-
const view = new DataView(buffer);
222-
const num = view.getUint32(0, true); // true for little-endian
223-
// Offset the hue by the hash value to make UM naturally orange
224-
return (num * 83) % 360;
225-
};
226-
227-
const distributionWithMetadata = await Promise.all(
228-
valuesWithMetadata.map(async ({ value, balance, hasError }) => {
229-
const metadata = getMetadataFromBalancesResponse.optional(balance);
230-
const assetIdHex = metadata?.penumbraAssetId?.inner.toString() ?? '';
231-
232-
// Use Web Crypto API to hash the hex string
233-
const hashBuffer = await crypto.subtle.digest(
234-
'SHA-256',
235-
new TextEncoder().encode(assetIdHex),
236-
);
237-
const hue = getHueFromHash(hashBuffer);
238-
239-
const percentage = (value / totalValue) * 100;
240-
241-
return {
242-
percentage,
243-
color: `hsl(${hue}, 70%, 50%)`,
244-
balance,
245-
hasError,
246-
};
247-
}),
248-
);
249-
250-
// Sort by value percentage in descending order
251-
const sorted = distributionWithMetadata.sort((a, b) => b.percentage - a.percentage);
238+
// Sort by value percentage in descending order and calculate distribution
239+
const sorted = valuesWithMetadata
240+
.map((item, index) => ({
241+
...item,
242+
percentage: (item.value / totalValue) * 100,
243+
color: `hsl(${(index * GOLDEN_RATIO_ANGLE) % 360}, ${COLOR_SATURATION}%, ${COLOR_LIGHTNESS}%)`,
244+
}))
245+
.sort((a, b) => b.percentage - a.percentage);
252246

253-
// Group small assets (less than 2%) into "Other" category for distribution only
254-
const SMALL_ASSET_THRESHOLD = 2;
247+
// Split into main assets and small assets
255248
const mainAssets = sorted.filter(asset => asset.percentage >= SMALL_ASSET_THRESHOLD);
256249
const smallAssets = sorted.filter(asset => asset.percentage < SMALL_ASSET_THRESHOLD);
257250

258251
const otherPercentage = smallAssets.reduce((acc, asset) => acc + asset.percentage, 0);
259252

260-
const distributionWithOther = [
253+
const distribution: AssetDistribution[] = [
261254
...mainAssets.map(({ percentage, color, hasError }) => ({
262255
percentage,
263256
color,
264257
hasError,
265-
isOther: false as const,
258+
isOther: false,
266259
})),
267-
// Only add Other if there are small assets
268260
...(otherPercentage > 0
269261
? [
270262
{
271263
percentage: otherPercentage,
272-
color: 'hsl(0, 0%, 50%)', // Gray color for Other
264+
color: `hsl(0, 0%, ${COLOR_LIGHTNESS}%)`,
273265
hasError: false,
274-
isOther: true as const,
266+
isOther: true,
275267
},
276268
]
277269
: []),
278270
];
279271

280272
return {
281-
distribution: distributionWithOther,
282-
sortedBalances: sorted.map(({ balance }) => balance), // Keep all assets in the table
273+
distribution,
274+
sortedBalances: sorted.map(({ balance }) => balance),
283275
};
284276
};
285277

@@ -290,19 +282,11 @@ export const AssetsTable = observer(() => {
290282
const { data: balances, isLoading: balancesLoading } = useBalances(addressIndex.account);
291283
const { data: assets, isLoading: assetsLoading } = useAssets();
292284
const { data: chainId } = useChainId();
293-
const [distribution, setDistribution] = useState<{
294-
distribution: {
295-
percentage: number;
296-
color: string;
297-
hasError: boolean;
298-
isOther: boolean;
299-
}[];
300-
sortedBalances: BalancesResponse[];
301-
}>();
285+
const [distribution, setDistribution] = useState<DistributionResult>();
302286

303287
useEffect(() => {
304288
if (balances) {
305-
void calculateAssetDistribution(balances).then(setDistribution);
289+
setDistribution(calculateAssetDistribution(balances));
306290
}
307291
}, [balances]);
308292

@@ -314,7 +298,6 @@ export const AssetsTable = observer(() => {
314298
if (isLoading) {
315299
return <LoadingState />;
316300
}
317-
318301
if (!connected) {
319302
return <NotConnectedNotice />;
320303
}
@@ -328,15 +311,15 @@ export const AssetsTable = observer(() => {
328311
</Text>
329312

330313
{/* Asset distribution bar */}
331-
<div className='flex w-full h-2 mt-4 mb-6 rounded-full overflow-hidden'>
314+
<div className='flex w-full h-4 mt-4 mb-6 gap-[5px]'>
332315
{distribution.distribution.map((asset, index) => (
333316
<div
334317
key={index}
335318
style={{
336319
width: `${asset.percentage}%`,
337320
backgroundColor: asset.color,
338321
}}
339-
className='h-full first:rounded-l-full last:rounded-r-full'
322+
className='h-full rounded'
340323
/>
341324
))}
342325
</div>

0 commit comments

Comments
 (0)