Skip to content

Commit c55cc0a

Browse files
authored
Merge pull request #73 from rajbos/main
[Feature] Show last date available in CSV and limit chart domain (#12)
2 parents a6ad1b4 + 4147416 commit c55cc0a

File tree

7 files changed

+535
-7
lines changed

7 files changed

+535
-7
lines changed

src/App.tsx

Lines changed: 163 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ import {
1919
ModelUsageSummary,
2020
DailyModelData,
2121
PowerUserSummary,
22+
PowerUserDailyBreakdown,
2223
aggregateDataByDay,
2324
parseCSV,
2425
getModelUsageSummary,
2526
getDailyModelData,
2627
getPowerUsers,
27-
getPowerUserDailyData
28+
getPowerUserDailyData,
29+
getPowerUserDailyBreakdown,
30+
getLastDateFromData
2831
} from "@/lib/utils";
2932

3033
function App() {
@@ -34,10 +37,27 @@ function App() {
3437
const [modelSummary, setModelSummary] = useState<ModelUsageSummary[]>([]);
3538
const [dailyModelData, setDailyModelData] = useState<DailyModelData[]>([]);
3639
const [powerUserSummary, setPowerUserSummary] = useState<PowerUserSummary | null>(null);
40+
const [powerUserDailyBreakdown, setPowerUserDailyBreakdown] = useState<PowerUserDailyBreakdown[]>([]);
41+
const [selectedPowerUser, setSelectedPowerUser] = useState<string | null>(null);
42+
const [lastDateAvailable, setLastDateAvailable] = useState<string | null>(null);
3743
const [isDragging, setIsDragging] = useState(false);
3844
const [isProcessing, setIsProcessing] = useState(false);
3945
const fileInputRef = useRef<HTMLInputElement>(null);
4046

47+
const handlePowerUserSelect = useCallback((userName: string | null) => {
48+
setSelectedPowerUser(userName);
49+
}, []);
50+
51+
// Generate filtered power user daily breakdown based on selected user
52+
const getFilteredPowerUserBreakdown = useCallback(() => {
53+
if (!selectedPowerUser || !data) {
54+
return powerUserDailyBreakdown;
55+
}
56+
57+
// Filter the original data to only include the selected user, then regenerate breakdown
58+
return getPowerUserDailyBreakdown(data, [selectedPowerUser]);
59+
}, [selectedPowerUser, data, powerUserDailyBreakdown]);
60+
4161
const processFile = useCallback((file: File) => {
4262
if (!file) return;
4363

@@ -94,6 +114,18 @@ function App() {
94114
const powerUsers = getPowerUsers(parsedData);
95115
setPowerUserSummary(powerUsers);
96116

117+
// Get power user daily breakdown for the stacked bar chart
118+
const powerUserNames = powerUsers.powerUsers.map(user => user.user);
119+
const powerUserBreakdown = getPowerUserDailyBreakdown(parsedData, powerUserNames);
120+
setPowerUserDailyBreakdown(powerUserBreakdown);
121+
122+
// Get the last date available in the CSV
123+
const lastDate = getLastDateFromData(parsedData);
124+
setLastDateAvailable(lastDate);
125+
126+
// Reset selected power user when new data is loaded
127+
setSelectedPowerUser(null);
128+
97129
setIsProcessing(false);
98130
toast.success(`Loaded ${parsedData.length} records successfully`);
99131
} catch (error) {
@@ -133,6 +165,9 @@ function App() {
133165
setModelSummary([]);
134166
setDailyModelData([]);
135167
setPowerUserSummary(null);
168+
setPowerUserDailyBreakdown([]);
169+
setSelectedPowerUser(null);
170+
setLastDateAvailable(null);
136171
}
137172
};
138173

@@ -453,7 +488,8 @@ function App() {
453488
<XAxis
454489
dataKey="date"
455490
tick={{ fill: 'var(--foreground)' }}
456-
tickLine={{ stroke: 'var(--border)' }}
491+
tickLine={{ stroke: 'var(--border)' }}
492+
domain={['dataMin', lastDateAvailable || 'dataMax']}
457493
/>
458494
<YAxis
459495
tick={{ fill: 'var(--foreground)' }}
@@ -488,6 +524,103 @@ function App() {
488524
</div>
489525
</Card>
490526

527+
{/* Power User Requests Breakdown - Stacked Bar Chart */}
528+
<Card className="p-4">
529+
<div className="flex items-center justify-between mb-3">
530+
<h3
531+
className={`text-md font-medium ${selectedPowerUser ? 'cursor-pointer hover:text-blue-600 transition-colors' : ''}`}
532+
onClick={() => selectedPowerUser && handlePowerUserSelect(null)}
533+
title={selectedPowerUser ? 'Click to show all power users' : undefined}
534+
>
535+
Power User Requests Breakdown (Compliant vs Exceeding)
536+
{selectedPowerUser && (
537+
<span className="text-sm font-normal text-muted-foreground ml-2">
538+
- {selectedPowerUser}
539+
</span>
540+
)}
541+
</h3>
542+
{selectedPowerUser && (
543+
<Button
544+
variant="outline"
545+
size="sm"
546+
onClick={() => handlePowerUserSelect(null)}
547+
>
548+
Show All
549+
</Button>
550+
)}
551+
</div>
552+
<div className="h-[300px]">
553+
<ChartContainer
554+
config={{
555+
compliantRequests: { color: "#10b981" }, // green
556+
exceedingRequests: { color: "#ef4444" }, // red
557+
}}
558+
className="h-full w-full"
559+
>
560+
<BarChart data={getFilteredPowerUserBreakdown()}>
561+
<CartesianGrid strokeDasharray="3 3" opacity={0.2} />
562+
<XAxis
563+
dataKey="date"
564+
tick={{ fill: 'var(--foreground)' }}
565+
tickLine={{ stroke: 'var(--border)' }}
566+
domain={['dataMin', lastDateAvailable || 'dataMax']}
567+
/>
568+
<YAxis
569+
tick={{ fill: 'var(--foreground)' }}
570+
tickLine={{ stroke: 'var(--border)' }}
571+
/>
572+
<ChartTooltip
573+
content={({ active, payload, label }) => {
574+
if (active && payload && payload.length) {
575+
const compliant = payload.find(p => p.dataKey === 'compliantRequests')?.value || 0;
576+
const exceeding = payload.find(p => p.dataKey === 'exceedingRequests')?.value || 0;
577+
const total = Number(compliant) + Number(exceeding);
578+
579+
return (
580+
<div className="border rounded-lg bg-background shadow-lg p-3 text-xs">
581+
<div className="font-medium mb-2">{label}</div>
582+
<div className="space-y-2">
583+
<div className="grid grid-cols-2 gap-2">
584+
<div className="flex items-center gap-1.5">
585+
<div className="w-2 h-2 rounded-full bg-[#10b981]" />
586+
<span>Compliant:</span>
587+
</div>
588+
<div className="text-right">{Number(compliant).toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</div>
589+
<div className="flex items-center gap-1.5">
590+
<div className="w-2 h-2 rounded-full bg-[#ef4444]" />
591+
<span>Exceeding:</span>
592+
</div>
593+
<div className="text-right">{Number(exceeding).toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</div>
594+
<div className="font-medium">Total:</div>
595+
<div className="text-right font-medium">{Number(total).toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</div>
596+
</div>
597+
</div>
598+
</div>
599+
);
600+
}
601+
return null;
602+
}}
603+
/>
604+
<Legend />
605+
606+
{/* Stacked bars for compliant and exceeding requests */}
607+
<Bar
608+
dataKey="compliantRequests"
609+
name="Compliant Requests"
610+
stackId="requests"
611+
fill="#10b981"
612+
/>
613+
<Bar
614+
dataKey="exceedingRequests"
615+
name="Exceeding Requests"
616+
stackId="requests"
617+
fill="#ef4444"
618+
/>
619+
</BarChart>
620+
</ChartContainer>
621+
</div>
622+
</Card>
623+
491624
{/* Individual Power Users List */}
492625
<Card className="p-4">
493626
<h3 className="text-md font-medium mb-3">Individual Power Users</h3>
@@ -497,14 +630,22 @@ function App() {
497630
<TableRow>
498631
<TableHead>User</TableHead>
499632
<TableHead className="text-right">Total Requests</TableHead>
633+
<TableHead className="text-right">Exceeding Requests</TableHead>
500634
<TableHead className="text-right">Models Used</TableHead>
501635
</TableRow>
502636
</TableHeader>
503637
<TableBody>
504638
{powerUserSummary.powerUsers.map((user) => (
505639
<TableRow key={user.user}>
506-
<TableCell className="font-medium">{user.user}</TableCell>
640+
<TableCell
641+
className={`font-medium cursor-pointer hover:text-blue-600 transition-colors ${selectedPowerUser === user.user ? 'text-blue-600 font-bold' : ''}`}
642+
onClick={() => handlePowerUserSelect(user.user)}
643+
title="Click to filter chart to this user"
644+
>
645+
{user.user}
646+
</TableCell>
507647
<TableCell className="text-right">{user.totalRequests.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</TableCell>
648+
<TableCell className="text-right">{user.exceedingRequests.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</TableCell>
508649
<TableCell className="text-right">{Object.keys(user.requestsByModel).length}</TableCell>
509650
</TableRow>
510651
))}
@@ -559,7 +700,14 @@ function App() {
559700
</div>
560701

561702
<div>
562-
<h2 className="text-2xl font-semibold mb-2">Daily Usage Overview</h2>
703+
<div className="flex justify-between items-center mb-2">
704+
<h2 className="text-2xl font-semibold">Daily Usage Overview</h2>
705+
{lastDateAvailable && (
706+
<div className="text-sm text-muted-foreground">
707+
Data available through: <span className="font-medium">{lastDateAvailable}</span>
708+
</div>
709+
)}
710+
</div>
563711
<Separator className="mb-6" />
564712
<div className="bg-card p-4 rounded-lg border mb-8">
565713
<ChartContainer
@@ -574,7 +722,8 @@ function App() {
574722
<XAxis
575723
dataKey="date"
576724
tick={{ fill: 'var(--foreground)' }}
577-
tickLine={{ stroke: 'var(--border)' }}
725+
tickLine={{ stroke: 'var(--border)' }}
726+
domain={['dataMin', lastDateAvailable || 'dataMax']}
578727
/>
579728
<YAxis
580729
tick={{ fill: 'var(--foreground)' }}
@@ -640,7 +789,14 @@ function App() {
640789
</div>
641790

642791
{/* Bar Chart - Requests per Model per Day */}
643-
<h2 className="text-2xl font-semibold mb-2">Requests per Model per Day</h2>
792+
<div className="flex justify-between items-center mb-2">
793+
<h2 className="text-2xl font-semibold">Requests per Model per Day</h2>
794+
{lastDateAvailable && (
795+
<div className="text-sm text-muted-foreground">
796+
Data available through: <span className="font-medium">{lastDateAvailable}</span>
797+
</div>
798+
)}
799+
</div>
644800
<Separator className="mb-6" />
645801
<div className="bg-card p-4 rounded-lg border">
646802
<ChartContainer
@@ -653,6 +809,7 @@ function App() {
653809
dataKey="date"
654810
tick={{ fill: 'var(--foreground)' }}
655811
tickLine={{ stroke: 'var(--border)' }}
812+
domain={['dataMin', lastDateAvailable || 'dataMax']}
656813
/>
657814
<YAxis
658815
tick={{ fill: 'var(--foreground)' }}

src/lib/utils.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export function getDailyModelData(data: CopilotUsageData[]): DailyModelData[] {
218218
export interface PowerUserData {
219219
user: string;
220220
totalRequests: number;
221+
exceedingRequests: number;
221222
requestsByModel: Record<string, number>;
222223
dailyActivity: Array<{
223224
date: string;
@@ -232,6 +233,12 @@ export interface PowerUserSummary {
232233
powerUserModelSummary: ModelUsageSummary[];
233234
}
234235

236+
export interface PowerUserDailyBreakdown {
237+
date: string;
238+
compliantRequests: number;
239+
exceedingRequests: number;
240+
}
241+
235242

236243

237244
export function getPowerUsers(data: CopilotUsageData[]): PowerUserSummary {
@@ -265,6 +272,11 @@ export function getPowerUsers(data: CopilotUsageData[]): PowerUserSummary {
265272
requestsByModel[item.model] = (requestsByModel[item.model] || 0) + item.requestsUsed;
266273
});
267274

275+
// Calculate exceeding requests
276+
const exceedingRequests = userRequests
277+
.filter(item => item.exceedsQuota)
278+
.reduce((sum, item) => sum + item.requestsUsed, 0);
279+
268280
// Aggregate daily activity
269281
const dailyActivity: Record<string, number> = {};
270282
userRequests.forEach(item => {
@@ -279,6 +291,7 @@ export function getPowerUsers(data: CopilotUsageData[]): PowerUserSummary {
279291
return {
280292
user: userName,
281293
totalRequests: userTotals[userName],
294+
exceedingRequests,
282295
requestsByModel,
283296
dailyActivity: dailyActivityArray
284297
};
@@ -317,3 +330,42 @@ export function getPowerUserDailyData(powerUsers: PowerUserData[]): Array<{
317330
.map(([date, requests]) => ({ date, requests }))
318331
.sort((a, b) => a.date.localeCompare(b.date));
319332
}
333+
334+
export function getPowerUserDailyBreakdown(data: CopilotUsageData[], powerUserNames: string[]): PowerUserDailyBreakdown[] {
335+
// Filter data to only include power users
336+
const powerUserData = data.filter(item => powerUserNames.includes(item.user));
337+
338+
const dailyBreakdown: Record<string, PowerUserDailyBreakdown> = {};
339+
340+
powerUserData.forEach(item => {
341+
const date = item.timestamp.toISOString().split('T')[0];
342+
343+
if (!dailyBreakdown[date]) {
344+
dailyBreakdown[date] = {
345+
date,
346+
compliantRequests: 0,
347+
exceedingRequests: 0,
348+
};
349+
}
350+
351+
if (item.exceedsQuota) {
352+
dailyBreakdown[date].exceedingRequests += item.requestsUsed;
353+
} else {
354+
dailyBreakdown[date].compliantRequests += item.requestsUsed;
355+
}
356+
});
357+
358+
// Convert to array and sort by date
359+
return Object.values(dailyBreakdown).sort((a, b) => a.date.localeCompare(b.date));
360+
}
361+
362+
// Function to get the last date from CSV data
363+
export function getLastDateFromData(data: CopilotUsageData[]): string | null {
364+
if (!data.length) return null;
365+
366+
// Get all dates and find the maximum
367+
const dates = data.map(item => item.timestamp.toISOString().split('T')[0]);
368+
const sortedDates = dates.sort((a, b) => a.localeCompare(b));
369+
370+
return sortedDates[sortedDates.length - 1];
371+
}

src/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import App from './App.tsx'
44
import "./main.css"
55
import "./styles/theme.css"
66
import "./index.css"
7-
import "@github/spark/spark"
7+
// @github/spark/spark import is excluded and handled externally
88

99
createRoot(document.getElementById('root')!).render(
1010
<App />

0 commit comments

Comments
 (0)