Skip to content

Commit 4147416

Browse files
Copilotrajbos
andauthored
Add stacked bar chart for power user requests breakdown with compliant/exceeding split (#14)
* Initial plan * Add stacked bar chart for power user requests breakdown with compliant/exceeding split Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> * Add user filtering functionality to power user breakdown chart Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> * Add exceeding requests column to Individual Power Users table Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com>
1 parent 9519527 commit 4147416

5 files changed

Lines changed: 449 additions & 1 deletion

File tree

src/App.tsx

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ import {
1919
ModelUsageSummary,
2020
DailyModelData,
2121
PowerUserSummary,
22+
PowerUserDailyBreakdown,
2223
aggregateDataByDay,
2324
parseCSV,
2425
getModelUsageSummary,
2526
getDailyModelData,
2627
getPowerUsers,
2728
getPowerUserDailyData,
29+
getPowerUserDailyBreakdown,
2830
getLastDateFromData
2931
} from "@/lib/utils";
3032

@@ -35,11 +37,27 @@ function App() {
3537
const [modelSummary, setModelSummary] = useState<ModelUsageSummary[]>([]);
3638
const [dailyModelData, setDailyModelData] = useState<DailyModelData[]>([]);
3739
const [powerUserSummary, setPowerUserSummary] = useState<PowerUserSummary | null>(null);
40+
const [powerUserDailyBreakdown, setPowerUserDailyBreakdown] = useState<PowerUserDailyBreakdown[]>([]);
41+
const [selectedPowerUser, setSelectedPowerUser] = useState<string | null>(null);
3842
const [lastDateAvailable, setLastDateAvailable] = useState<string | null>(null);
3943
const [isDragging, setIsDragging] = useState(false);
4044
const [isProcessing, setIsProcessing] = useState(false);
4145
const fileInputRef = useRef<HTMLInputElement>(null);
4246

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+
4361
const processFile = useCallback((file: File) => {
4462
if (!file) return;
4563

@@ -96,10 +114,18 @@ function App() {
96114
const powerUsers = getPowerUsers(parsedData);
97115
setPowerUserSummary(powerUsers);
98116

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+
99122
// Get the last date available in the CSV
100123
const lastDate = getLastDateFromData(parsedData);
101124
setLastDateAvailable(lastDate);
102125

126+
// Reset selected power user when new data is loaded
127+
setSelectedPowerUser(null);
128+
103129
setIsProcessing(false);
104130
toast.success(`Loaded ${parsedData.length} records successfully`);
105131
} catch (error) {
@@ -139,6 +165,8 @@ function App() {
139165
setModelSummary([]);
140166
setDailyModelData([]);
141167
setPowerUserSummary(null);
168+
setPowerUserDailyBreakdown([]);
169+
setSelectedPowerUser(null);
142170
setLastDateAvailable(null);
143171
}
144172
};
@@ -496,6 +524,103 @@ function App() {
496524
</div>
497525
</Card>
498526

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+
499624
{/* Individual Power Users List */}
500625
<Card className="p-4">
501626
<h3 className="text-md font-medium mb-3">Individual Power Users</h3>
@@ -505,14 +630,22 @@ function App() {
505630
<TableRow>
506631
<TableHead>User</TableHead>
507632
<TableHead className="text-right">Total Requests</TableHead>
633+
<TableHead className="text-right">Exceeding Requests</TableHead>
508634
<TableHead className="text-right">Models Used</TableHead>
509635
</TableRow>
510636
</TableHeader>
511637
<TableBody>
512638
{powerUserSummary.powerUsers.map((user) => (
513639
<TableRow key={user.user}>
514-
<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>
515647
<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>
516649
<TableCell className="text-right">{Object.keys(user.requestsByModel).length}</TableCell>
517650
</TableRow>
518651
))}

src/lib/utils.ts

Lines changed: 41 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
};
@@ -318,6 +331,34 @@ export function getPowerUserDailyData(powerUsers: PowerUserData[]): Array<{
318331
.sort((a, b) => a.date.localeCompare(b.date));
319332
}
320333

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+
321362
// Function to get the last date from CSV data
322363
export function getLastDateFromData(data: CopilotUsageData[]): string | null {
323364
if (!data.length) return null;

0 commit comments

Comments
 (0)