Skip to content

Commit 251bc5f

Browse files
committed
Refactor BalanceChart component for improved data handling and formatting
1 parent a890bb1 commit 251bc5f

File tree

1 file changed

+131
-63
lines changed

1 file changed

+131
-63
lines changed
Lines changed: 131 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,129 @@
1-
'use client'
1+
"use client";
22

3-
import { useState } from 'react'
4-
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, TooltipProps } from 'recharts'
5-
import { useSubscribeToAccountBalanceSubscription } from '@/lib/gql/urql'
3+
import { useState } from "react";
4+
import {
5+
LineChart,
6+
Line,
7+
XAxis,
8+
YAxis,
9+
CartesianGrid,
10+
Tooltip,
11+
ResponsiveContainer,
12+
TooltipProps,
13+
} from "recharts";
14+
import { useSubscribeToAccountBalanceSubscription } from "@/lib/gql/urql";
615

716
interface BalanceDataPoint {
8-
timestamp: string
9-
balance: number
10-
time: string // formatted time for display
17+
timestamp: string;
18+
timestampMs: number;
19+
balance: number;
20+
time: string; // formatted time for display
1121
}
1222

1323
interface BalanceChartProps {
14-
accountId: string
15-
initialBalance: number
16-
className?: string
24+
accountId: string;
25+
initialBalance: number;
26+
className?: string;
1727
}
1828

29+
const DAY_MS = 24 * 60 * 60 * 1000; // 24 hours
30+
1931
function formatCurrency(cents: number): string {
20-
return new Intl.NumberFormat('en-US', {
21-
style: 'currency',
22-
currency: 'USD',
23-
}).format(cents / 100)
32+
return new Intl.NumberFormat("en-US", {
33+
style: "currency",
34+
currency: "USD",
35+
}).format(cents / 100);
2436
}
2537

2638
function formatTime(date: Date): string {
27-
return new Intl.DateTimeFormat('en-US', {
28-
hour: '2-digit',
29-
minute: '2-digit',
30-
second: '2-digit',
31-
}).format(date)
39+
return new Intl.DateTimeFormat("en-US", {
40+
hour: "2-digit",
41+
minute: "2-digit",
42+
second: "2-digit",
43+
}).format(date);
3244
}
3345

34-
export function BalanceChart({ accountId, initialBalance, className }: BalanceChartProps) {
35-
const [balanceHistory, setBalanceHistory] = useState<BalanceDataPoint[]>(() => {
36-
const now = new Date()
37-
return [{
38-
timestamp: now.toISOString(),
39-
balance: initialBalance,
40-
time: formatTime(now)
41-
}]
42-
})
43-
const [isLive, setIsLive] = useState(false)
46+
export function BalanceChart({
47+
accountId,
48+
initialBalance,
49+
className,
50+
}: BalanceChartProps) {
51+
const [balanceHistory, setBalanceHistory] = useState<BalanceDataPoint[]>(
52+
() => {
53+
const now = new Date();
54+
const dayAgo = new Date(now.getTime() - DAY_MS);
55+
// Seed with a baseline point 24h ago so the time axis spans the full window
56+
return [
57+
{
58+
timestamp: dayAgo.toISOString(),
59+
timestampMs: dayAgo.getTime(),
60+
balance: initialBalance,
61+
time: formatTime(dayAgo),
62+
},
63+
{
64+
timestamp: now.toISOString(),
65+
timestampMs: now.getTime(),
66+
balance: initialBalance,
67+
time: formatTime(now),
68+
},
69+
];
70+
}
71+
);
72+
const [isLive, setIsLive] = useState(false);
4473

4574
// Subscribe to balance updates using GraphQL SSE subscription
4675
const [subscriptionResult] = useSubscribeToAccountBalanceSubscription(
4776
{ variables: { accountId } },
4877
(prev, data) => {
4978
if (data?.accountBalanceUpdated?.balance !== undefined) {
50-
const now = new Date()
79+
const now = new Date();
5180
const newDataPoint: BalanceDataPoint = {
5281
timestamp: now.toISOString(),
82+
timestampMs: now.getTime(),
5383
balance: data.accountBalanceUpdated.balance,
54-
time: formatTime(now)
55-
}
56-
57-
setBalanceHistory(prev => {
58-
// Keep only the last 20 data points to prevent the chart from becoming too cluttered
59-
const newHistory = [...prev, newDataPoint]
60-
return newHistory.slice(-20)
61-
})
62-
setIsLive(true)
84+
time: formatTime(now),
85+
};
86+
87+
setBalanceHistory((prev) => {
88+
// Keep only points from the last 24 hours (and cap to 500 to avoid unbounded growth)
89+
const cutoff = now.getTime() - DAY_MS;
90+
const newHistory = [...prev, newDataPoint].filter(
91+
(p) => p.timestampMs >= cutoff
92+
);
93+
return newHistory.slice(-500);
94+
});
95+
setIsLive(true);
6396
}
64-
return data
97+
return data;
6598
}
66-
)
99+
);
67100

68101
// Custom tooltip component for the chart
69-
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
102+
const CustomTooltip = ({
103+
active,
104+
payload,
105+
label,
106+
}: TooltipProps<number, string>) => {
70107
if (active && payload && payload.length) {
108+
const ts = typeof label === "number" ? label : Number(label);
109+
const labelTime = Number.isFinite(ts)
110+
? new Intl.DateTimeFormat("en-US", {
111+
hour: "2-digit",
112+
minute: "2-digit",
113+
second: "2-digit",
114+
}).format(new Date(ts))
115+
: String(label);
71116
return (
72117
<div className="bg-white p-3 border border-gray-200 rounded shadow-lg">
73-
<p className="text-sm text-gray-600">{`Time: ${label}`}</p>
118+
<p className="text-sm text-gray-600">{`Time: ${labelTime}`}</p>
74119
<p className="text-sm font-semibold">
75120
{`Balance: ${formatCurrency(payload[0].value || 0)}`}
76121
</p>
77122
</div>
78-
)
123+
);
79124
}
80-
return null
81-
}
125+
return null;
126+
};
82127

83128
return (
84129
<div className={className}>
@@ -87,11 +132,13 @@ export function BalanceChart({ accountId, initialBalance, className }: BalanceCh
87132
{isLive && (
88133
<div className="flex items-center gap-1">
89134
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
90-
<span className="text-xs text-green-600 font-medium">LIVE UPDATES</span>
135+
<span className="text-xs text-green-600 font-medium">
136+
LIVE UPDATES
137+
</span>
91138
</div>
92139
)}
93140
</div>
94-
141+
95142
<div className="h-64 w-full">
96143
<ResponsiveContainer width="100%" height="100%">
97144
<LineChart
@@ -104,42 +151,63 @@ export function BalanceChart({ accountId, initialBalance, className }: BalanceCh
104151
}}
105152
>
106153
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
107-
<XAxis
108-
dataKey="time"
154+
<XAxis
155+
dataKey="timestampMs"
156+
type="number"
157+
scale="time"
158+
domain={["dataMin", "dataMax"]}
159+
tickFormatter={(value) => {
160+
const n = typeof value === "number" ? value : Number(value);
161+
if (!Number.isFinite(n)) return "";
162+
const d = new Date(n);
163+
if (Number.isNaN(d.getTime())) return "";
164+
return new Intl.DateTimeFormat("en-US", {
165+
hour: "2-digit",
166+
minute: "2-digit",
167+
second: "2-digit",
168+
}).format(d);
169+
}}
109170
tick={{ fontSize: 12 }}
110171
interval="preserveStartEnd"
111172
/>
112-
<YAxis
173+
<YAxis
113174
tickFormatter={(value) => formatCurrency(value)}
114175
tick={{ fontSize: 12 }}
115-
domain={['dataMin - 100', 'dataMax + 100']}
176+
domain={["dataMin - 100", "dataMax + 100"]}
116177
/>
117178
<Tooltip content={<CustomTooltip />} />
118-
<Line
119-
type="monotone"
120-
dataKey="balance"
121-
stroke="#10b981"
179+
<Line
180+
type="monotone"
181+
dataKey="balance"
182+
stroke="#10b981"
122183
strokeWidth={2}
123-
dot={{ fill: '#10b981', strokeWidth: 2, r: 4 }}
124-
activeDot={{ r: 6, stroke: '#10b981', strokeWidth: 2, fill: '#fff' }}
184+
dot={{ fill: "#10b981", strokeWidth: 2, r: 4 }}
185+
activeDot={{
186+
r: 6,
187+
stroke: "#10b981",
188+
strokeWidth: 2,
189+
fill: "#fff",
190+
}}
125191
/>
126192
</LineChart>
127193
</ResponsiveContainer>
128194
</div>
129-
195+
130196
<div className="mt-2 text-xs text-muted-foreground">
131197
{isLive ? (
132-
<p>Real-time balance updates via SSE subscription • Showing last 20 data points</p>
198+
<p>
199+
Real-time balance updates via SSE subscription • Showing last 24 hours
200+
</p>
133201
) : (
134202
<p>Waiting for live balance updates...</p>
135203
)}
136204
</div>
137-
205+
138206
{subscriptionResult.error && (
139207
<p className="text-xs text-red-500 mt-1">
140208
Chart subscription error: {subscriptionResult.error.message}
141209
</p>
142210
)}
143211
</div>
144-
)
145-
}
212+
);
213+
}

0 commit comments

Comments
 (0)