=> {
+ let balances: TokenBalance[] = []
+ let nextToken: string | null = ''
+
+ while (nextToken !== null) {
+ try {
+ const url = `${indexerUrl}/v2/assets/${assetId}/balances`
+ const params: { next?: string } = nextToken ? { next: nextToken } : {}
+ const response = await axios.get(url, { params })
+ const data = response.data
+
+ balances = [...balances, ...data.balances]
+ nextToken = data['next-token'] || null
+ } catch {
+ nextToken = null
+ }
+ }
+
+ return balances
+ }
+
+ useEffect(() => {
+ const getBalances = async () => {
+ const balances = await fetchTokenBalances(assetId)
+ setTopWallets(balances)
+ }
+
+ getBalances()
+ }, [assetId])
+
+ const handleSearchSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ const parsedAssetId = parseInt(inputAssetId, 10)
+ if (!isNaN(parsedAssetId)) {
+ setAssetId(parsedAssetId)
+ }
+ }
+
+ const showHeatmap = () => {
+ const N = 50
+ const sortedWallets = topWallets
+ .map((wallet) => ({ ...wallet, amount: parseFloat(wallet.amount) }))
+ .sort((a, b) => b.amount - a.amount)
+ .slice(0, N)
+
+ const normalizedAmounts = sortedWallets.map((wallet) => Math.log10(wallet.amount + 1))
+ const rows = 5
+ const cols = 10
+
+ const heatmapData = Array.from({ length: rows }, (_, rowIndex) =>
+ normalizedAmounts.slice(rowIndex * cols, (rowIndex + 1) * cols)
+ )
+ return (
+
+ )
+ }
+
+ const showBubbleChart = () => {
+ const N = 50
+ const sortedWallets = topWallets
+ .map((wallet) => ({ ...wallet, amount: parseFloat(wallet.amount) }))
+ .sort((a, b) => b.amount - a.amount)
+ .slice(0, N)
+
+ const maxAmount = Math.max(...sortedWallets.map((wallet) => wallet.amount))
+ const bubbleData = sortedWallets.map((wallet, index) => ({
+ walletIndex: (index + 1).toString(),
+ amount: wallet.amount / maxAmount * 100,
+ }))
+
+ return (
+
+
index + 1),
+ y: bubbleData.map((data) => data.amount),
+ text: bubbleData.map(
+ (wallet) => `Wallet Index: ${wallet.walletIndex}
Token Amount: ${wallet.amount.toFixed(2)}`
+ ),
+ mode: 'markers',
+ marker: {
+ size: bubbleData.map((data) => data.amount / 2),
+ color: bubbleData.map((data) => data.amount),
+ colorscale: 'Viridis',
+ showscale: true,
+ colorbar: {
+ title: 'Token Amount',
+ titlefont: { ...fontConfig },
+ },
+ },
+ }]}
+ layout={{
+ title: `Whale Asset vs Small Wallets for ASA ${assetId}`,
+ xaxis: {
+ title: 'Wallet Index (Sorted by Amount)',
+ titlefont: { ...fontConfig, size: 14 },
+ tickfont: { ...fontConfig }
+ },
+ yaxis: {
+ title: 'Token Amount (Normalized)',
+ titlefont: { ...fontConfig, size: 14 },
+ tickfont: { ...fontConfig }
+ },
+ margin: { l: 40, r: 40, t: 80, b: 100 },
+ width: 1100,
+ height: 680,
+ paper_bgcolor: '#001324',
+ plot_bgcolor: '#001f3b',
+ titlefont: { ...fontConfig, size: 14 },
+ font: { ...fontConfig },
+ }}
+ />
+
+ )
+ }
+
+ return (
+
+
+ {showHeatmap()}
+ {showBubbleChart()}
+
+ )
+}
+
+export default Heatmap
\ No newline at end of file
diff --git a/src/features/layout/components/left-side-bar-menu.tsx b/src/features/layout/components/left-side-bar-menu.tsx
index 10c344d8..cbb13d1b 100644
--- a/src/features/layout/components/left-side-bar-menu.tsx
+++ b/src/features/layout/components/left-side-bar-menu.tsx
@@ -5,7 +5,7 @@ import { cn } from '@/features/common/utils'
import { Button } from '@/features/common/components/button'
import { useCallback, useMemo } from 'react'
import { useSelectedNetwork } from '@/features/network/data'
-import { Telescope, Settings, PanelLeftClose, PanelLeftOpen, ArrowLeft, Coins, FlaskConical } from 'lucide-react'
+import { Telescope, Settings, PanelLeftClose, PanelLeftOpen, ArrowLeft, Coins, FlaskConical, BarChart2 } from 'lucide-react'
import { ThemeToggle } from '@/features/settings/components/theme-toggle'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLayout } from '@/features/settings/data'
@@ -31,6 +31,7 @@ export function LeftSideBarMenu({ className }: Props) {
{ urlTemplate: Urls.Network.AppLab, icon: , text: 'App Lab' },
{ urlTemplate: Urls.Network.TransactionWizard, icon: , text: 'Txn Wizard' },
{ urlTemplate: Urls.Network.Fund, icon: , text: 'Fund' },
+ { urlTemplate: Urls.Network.Heatmap, icon: , text: 'Heatmap' },
]
const isExploreUrl = useMemo(() => {
const explorePaths = Object.values(Urls.Network.Explore)
diff --git a/src/routes/urls.ts b/src/routes/urls.ts
index 01c9baec..1405c56b 100644
--- a/src/routes/urls.ts
+++ b/src/routes/urls.ts
@@ -63,6 +63,7 @@ export const Urls = {
}),
}),
Fund: UrlTemplate`/fund`,
+ Heatmap: UrlTemplate`/heatmap`,
}),
AppLab: UrlTemplate`/app-lab`,
Settings: UrlTemplate`/settings`,