Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
};
customHeader?: React.ReactNode;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
Expand All @@ -41,7 +42,9 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
chartClassName?: string;
isPending: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
hideLabel?: boolean;
emptyChartState?: React.ReactElement;
};

export function ThirdwebBarChart<TConfig extends ChartConfig>(
Expand All @@ -65,12 +68,14 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
</CardHeader>
)}

{props.customHeader && props.customHeader}

<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
) : (
<BarChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
Expand All @@ -88,6 +93,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
props.hideLabel !== undefined ? props.hideLabel : true
}
labelFormatter={props.toolTipLabelFormatter}
valueFormatter={props.toolTipValueFormatter}
/>
}
/>
Expand All @@ -96,25 +102,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
content={<ChartLegendContent className="pt-5" />}
/>
)}
{configKeys.map((key, idx) => (
{configKeys.map((key) => (
<Bar
key={key}
dataKey={key}
// if stacked then they should all be the same stackId
// if grouped then they should all be unique stackId (so the key works great)
stackId={variant === "stacked" ? "a" : key}
fill={`var(--color-${key})`}
// if stacked then we need to figure out the radius based on the index in the array
// if grouped then we can just use the same radius for all
radius={
variant === "stacked"
? idx === 0
? [0, 0, 4, 4]
: idx === configKeys.length - 1
? [4, 4, 0, 0]
: [0, 0, 0, 0]
: [4, 4, 4, 4]
}
fill={props.config[key]?.color}
radius={[4, 4, 4, 4]}
strokeWidth={1}
className="stroke-background"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { Meta, StoryObj } from "@storybook/react";
import type { EcosystemWalletStats } from "types/analytics";
import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard";

const meta = {
title: "Ecosystem/Analytics/EcosystemWalletUsersChartCard",
component: EcosystemWalletUsersChartCard,
decorators: [
(Story) => (
<div className="container max-w-7xl py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof EcosystemWalletUsersChartCard>;

export default meta;
type Story = StoryObj<typeof meta>;

const authMethods = [
"Email",
"Google",
"Apple",
"Discord",
"Twitter",
"GitHub",
"Facebook",
"Twitch",
"LinkedIn",
"TikTok",
"Coinbase",
"MetaMask",
];

function ecosystemWalletStatsStub(
length: number,
startDate = new Date(2024, 11, 1),
): EcosystemWalletStats[] {
const stats: EcosystemWalletStats[] = [];

for (let i = 0; i < length; i++) {
const date = new Date(startDate);
date.setDate(date.getDate() + i);
const formattedDate = date.toISOString().split("T")[0] || "";

// each day, we pick between 1 and 4 auth methods
const authMethodsToPick = Math.floor(Math.random() * 4) + 1;

for (let j = 0; j < authMethodsToPick; j++) {
const authMethod =
authMethods[Math.floor(Math.random() * authMethods.length)];
stats.push({
date: formattedDate,
authenticationMethod: authMethod || "MetaMask",
uniqueWalletsConnected: Math.floor(Math.random() * 1000) + 1,
});
}
}

return stats;
}

// Empty data state
export const EmptyData: Story = {
args: {
ecosystemWalletStats: [],
isPending: false,
},
};

// Loading state
export const Loading: Story = {
args: {
ecosystemWalletStats: [],
isPending: true,
},
};

// 30 days of data
export const ThirtyDaysData: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(30),
isPending: false,
},
};

// 60 days of data
export const SixtyDaysData: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(60),
isPending: false,
},
};

// 120 days of data
export const OneHundredTwentyDaysData: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(120),
isPending: false,
},
};

// Data with lots of authentication methods to test the "Others" category
export const ManyAuthMethods: Story = {
args: {
ecosystemWalletStats: (() => {
// Generate data with 15 different auth methods to test "Others" category
const basicData = ecosystemWalletStatsStub(30);

return basicData.map((item, index) => ({
...item,
authenticationMethod:
authMethods[index % authMethods.length] || "MetaMask",
}));
})(),
isPending: false,
},
};

// Zero values test
export const ZeroValues: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(30).map((stat) => ({
...stat,
uniqueWalletsConnected: 0,
})),
isPending: false,
},
};
Loading
Loading