Skip to content

Commit 74e52c0

Browse files
committed
[Dashboard] Feature: Adds chain ID to total sponsored analytics (#5161)
<!-- start pr-codex --> ## PR-Codex overview This PR introduces an interface `UserOpStatsByChain` and updates the `TotalSponsoredChartCard` component to handle user operation statistics by blockchain. It enhances chart data handling and visualization, allowing for better representation of sponsored gas across different chains. ### Detailed summary - Added `UserOpStatsByChain` interface in `useApi.ts`. - Updated `TotalSponsoredChartCard` to use `UserOpStatsByChain[]`. - Modified chart data structure to accommodate multiple chains. - Implemented logic to categorize and display "others" in charts. - Adjusted CSV export to include unique chain data. - Improved chart rendering with dynamic colors and data keys based on chains. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 120f965 commit 74e52c0

File tree

2 files changed

+108
-48
lines changed

2 files changed

+108
-48
lines changed

apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,14 @@ export interface UserOpStats {
241241
sponsoredUsd: number;
242242
}
243243

244+
export interface UserOpStatsByChain {
245+
date: string;
246+
successful: number;
247+
failed: number;
248+
sponsoredUsd: number;
249+
chainId?: string;
250+
}
251+
244252
interface BillingProduct {
245253
name: string;
246254
id: string;

apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
44
import {
55
type ChartConfig,
66
ChartContainer,
7+
ChartLegend,
8+
ChartLegendContent,
79
ChartTooltip,
810
ChartTooltipContent,
911
} from "@/components/ui/chart";
10-
import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi";
12+
import type { UserOpStatsByChain } from "@3rdweb-sdk/react/hooks/useApi";
1113
import {
1214
EmptyChartState,
1315
LoadingChartState,
@@ -20,44 +22,91 @@ import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
2022
import { DocLink } from "components/shared/DocLink";
2123
import { format } from "date-fns";
2224
import { useMemo } from "react";
23-
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts";
25+
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
26+
import { useAllChainsData } from "../../../hooks/chains/allChains";
2427

25-
type ChartData = {
28+
type ChartData = Record<string, number> & {
2629
time: string; // human readable date
27-
sponsoredUsd: number;
2830
};
2931

30-
const chartConfig = {
31-
sponsoredUsd: {
32-
label: "Total Sponsored",
33-
color: "hsl(var(--chart-1))",
34-
},
35-
} satisfies ChartConfig;
36-
3732
export function TotalSponsoredChartCard(props: {
38-
userOpStats: UserOpStats[];
33+
userOpStats: UserOpStatsByChain[];
3934
isPending: boolean;
4035
}) {
4136
const { userOpStats } = props;
42-
const barChartData: ChartData[] = useMemo(() => {
43-
const chartDataMap: Map<string, ChartData> = new Map();
37+
const topChainsToShow = 10;
38+
const chainsStore = useAllChainsData();
39+
40+
const { chartConfig, chartData } = useMemo(() => {
41+
const _chartConfig: ChartConfig = {};
42+
const _chartDataMap: Map<string, ChartData> = new Map();
43+
const chainIdToVolumeMap: Map<string, number> = new Map();
44+
// for each stat, add it in _chartDataMap
45+
for (const stat of userOpStats) {
46+
const chartData = _chartDataMap.get(stat.date);
47+
const { chainId } = stat;
48+
const chain = chainsStore.idToChain.get(Number(chainId));
4449

45-
for (const data of userOpStats) {
46-
const chartData = chartDataMap.get(data.date);
50+
// if no data for current day - create new entry
4751
if (!chartData) {
48-
chartDataMap.set(data.date, {
49-
time: format(new Date(data.date), "MMM dd"),
50-
sponsoredUsd: data.sponsoredUsd,
51-
});
52+
_chartDataMap.set(stat.date, {
53+
time: format(new Date(stat.date), "MMM dd"),
54+
[chainId || "Unknown"]: Math.round(stat.sponsoredUsd * 100) / 100,
55+
} as ChartData);
5256
} else {
53-
chartData.sponsoredUsd += data.sponsoredUsd;
57+
chartData[chain?.name || chainId || "Unknown"] =
58+
(chartData[chain?.name || chainId || "Unknown"] || 0) +
59+
Math.round(stat.sponsoredUsd * 100) / 100;
5460
}
61+
62+
chainIdToVolumeMap.set(
63+
chain?.name || chainId || "Unknown",
64+
stat.sponsoredUsd + (chainIdToVolumeMap.get(chainId || "Unknown") || 0),
65+
);
5566
}
5667

57-
return Array.from(chartDataMap.values());
58-
}, [userOpStats]);
68+
const chainsSorted = Array.from(chainIdToVolumeMap.entries())
69+
.sort((a, b) => b[1] - a[1])
70+
.map((w) => w[0]);
71+
72+
const chainsToShow = chainsSorted.slice(0, topChainsToShow);
73+
const chainsToTagAsOthers = chainsSorted.slice(topChainsToShow);
74+
75+
// replace chainIdsToTagAsOther chainId with "other"
76+
for (const data of _chartDataMap.values()) {
77+
for (const chainId in data) {
78+
if (chainsToTagAsOthers.includes(chainId)) {
79+
data.others = (data.others || 0) + (data[chainId] || 0);
80+
delete data[chainId];
81+
}
82+
}
83+
}
5984

60-
const disableActions = props.isPending || barChartData.length === 0;
85+
chainsToShow.forEach((walletType, i) => {
86+
_chartConfig[walletType] = {
87+
label: chainsToShow[i],
88+
color: `hsl(var(--chart-${(i % 10) + 1}))`,
89+
};
90+
});
91+
92+
// Add Other
93+
chainsToShow.push("others");
94+
_chartConfig.others = {
95+
label: "Others",
96+
color: "hsl(var(--muted-foreground))",
97+
};
98+
99+
return {
100+
chartData: Array.from(_chartDataMap.values()),
101+
chartConfig: _chartConfig,
102+
};
103+
}, [userOpStats, chainsStore]);
104+
105+
const uniqueChainIds = Object.keys(chartConfig);
106+
const disableActions =
107+
props.isPending ||
108+
chartData.length === 0 ||
109+
chartData.every((data) => data.sponsoredUsd === 0);
61110

62111
return (
63112
<div className="relative w-full rounded-lg border border-border bg-muted/50 p-4 md:p-6">
@@ -70,17 +119,21 @@ export function TotalSponsoredChartCard(props: {
70119

71120
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
72121
<ExportToCSVButton
73-
disabled={disableActions}
74122
className="bg-background"
123+
fileName="Connect Wallets"
124+
disabled={disableActions}
75125
getData={async () => {
76-
const header = ["Date", "Total Sponsored"];
77-
const rows = barChartData.map((row) => [
78-
row.time,
79-
row.sponsoredUsd.toString(),
80-
]);
126+
// Shows the number of each type of wallet connected on all dates
127+
const header = ["Date", ...uniqueChainIds];
128+
const rows = chartData.map((data) => {
129+
const { time, ...rest } = data;
130+
return [
131+
time,
132+
...uniqueChainIds.map((w) => (rest[w] || 0).toString()),
133+
];
134+
});
81135
return { header, rows };
82136
}}
83-
fileName="Total Sponsored"
84137
/>
85138
</div>
86139

@@ -91,8 +144,8 @@ export function TotalSponsoredChartCard(props: {
91144
>
92145
{props.isPending ? (
93146
<LoadingChartState />
94-
) : barChartData.length === 0 ||
95-
barChartData.every((data) => data.sponsoredUsd === 0) ? (
147+
) : chartData.length === 0 ||
148+
chartData.every((data) => data.sponsoredUsd === 0) ? (
96149
<EmptyChartState>
97150
<div className="flex flex-col items-center justify-center">
98151
<span className="mb-6 text-lg">Sponsor gas for your users</span>
@@ -133,7 +186,7 @@ export function TotalSponsoredChartCard(props: {
133186
) : (
134187
<BarChart
135188
accessibilityLayer
136-
data={barChartData}
189+
data={chartData}
137190
margin={{
138191
top: 20,
139192
}}
@@ -148,21 +201,20 @@ export function TotalSponsoredChartCard(props: {
148201
/>
149202

150203
<ChartTooltip cursor={true} content={<ChartTooltipContent />} />
151-
152-
<Bar
153-
dataKey={"sponsoredUsd"}
154-
fill={"var(--color-sponsoredUsd)"}
155-
radius={8}
156-
>
157-
{barChartData.length < 50 && (
158-
<LabelList
159-
position="top"
160-
offset={12}
161-
className="invisible fill-foreground sm:visible"
162-
fontSize={12}
204+
<ChartLegend content={<ChartLegendContent />} />
205+
{uniqueChainIds.map((chainId) => {
206+
return (
207+
<Bar
208+
key={chainId}
209+
dataKey={chainId}
210+
fill={chartConfig[chainId]?.color}
211+
radius={4}
212+
stackId="a"
213+
strokeWidth={1.5}
214+
className="stroke-muted"
163215
/>
164-
)}
165-
</Bar>
216+
);
217+
})}
166218
</BarChart>
167219
)}
168220
</ChartContainer>

0 commit comments

Comments
 (0)