Skip to content

Commit 0178a32

Browse files
authored
Merge pull request #97 from rajbos/main
Add model breakdown visualization to Power User Requests chart (#24)
2 parents 44fe7f8 + 3f2f482 commit 0178a32

3 files changed

Lines changed: 109 additions & 10 deletions

File tree

src/App.tsx

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
getPowerUserDailyData,
3232
COPILOT_PLANS,
3333
getPowerUserDailyBreakdown,
34+
getUniqueModelsFromBreakdown,
3435
getLastDateFromData,
3536
getExceededRequestDetails,
3637
getUserExceededRequestSummary
@@ -69,6 +70,12 @@ function App() {
6970
return getPowerUserDailyBreakdown(data, [selectedPowerUser]);
7071
}, [selectedPowerUser, data, powerUserDailyBreakdown]);
7172

73+
// Get unique models from the power user breakdown data
74+
const getPowerUserModels = useCallback(() => {
75+
const breakdown = getFilteredPowerUserBreakdown();
76+
return getUniqueModelsFromBreakdown(breakdown);
77+
}, [getFilteredPowerUserBreakdown]);
78+
7279
const processFile = useCallback((file: File) => {
7380
if (!file) return;
7481

@@ -307,16 +314,16 @@ function App() {
307314

308315
// Generate colors for models in bar chart
309316
const getModelColors = useCallback(() => {
310-
// Use a set of predefined colors that are visually distinct
317+
// Use a set of predefined colors that are visually distinct from exceeding requests red (#ef4444)
311318
const colors = [
312319
"#4285F4", // Blue
313-
"#EA4335", // Red
320+
"#9C27B0", // Purple (changed from red to avoid confusion with exceeding requests)
314321
"#FBBC05", // Yellow
315322
"#34A853", // Green
316323
"#8E44AD", // Purple
317324
"#F39C12", // Orange
318325
"#16A085", // Teal
319-
"#E74C3C", // Red-Orange
326+
"#FF9800", // Amber (changed from red-orange to avoid confusion with exceeding requests)
320327
"#3498DB", // Light Blue
321328
"#1ABC9C" // Turquoise
322329
];
@@ -622,7 +629,7 @@ function App() {
622629
onClick={() => selectedPowerUser && handlePowerUserSelect(null)}
623630
title={selectedPowerUser ? 'Click to show all power users' : undefined}
624631
>
625-
Power User Requests Breakdown (Compliant vs Exceeding)
632+
Power User Requests Breakdown (By Model & Compliance)
626633
{selectedPowerUser && (
627634
<span className="text-sm font-normal text-muted-foreground ml-2">
628635
- {selectedPowerUser}
@@ -646,10 +653,21 @@ function App() {
646653
</div>
647654
<div className="h-[300px]">
648655
<ChartContainer
649-
config={{
650-
compliantRequests: { color: "#10b981" }, // green
651-
exceedingRequests: { color: "#ef4444" }, // red
652-
}}
656+
config={(() => {
657+
const models = getPowerUserModels();
658+
const modelColors = getModelColors();
659+
const config: Record<string, { color: string }> = {
660+
compliantRequests: { color: "#10b981" }, // green
661+
exceedingRequests: { color: "#ef4444" }, // red
662+
};
663+
664+
// Add each model with its color
665+
models.forEach((model, index) => {
666+
config[model] = { color: modelColors[model] || "#94a3b8" };
667+
});
668+
669+
return config;
670+
})()}
653671
className="h-full w-full"
654672
>
655673
<BarChart data={getFilteredPowerUserBreakdown()}>
@@ -739,7 +757,21 @@ function App() {
739757
/>
740758
<Legend content={(props) => <CustomLegend payload={props.payload} />} />
741759

742-
{/* Stacked bars for compliant and exceeding requests */}
760+
{/* Dynamic stacked bars for each model */}
761+
{getPowerUserModels().map((model) => {
762+
const modelColors = getModelColors();
763+
return (
764+
<Bar
765+
key={model}
766+
dataKey={model}
767+
name={model}
768+
stackId="models"
769+
fill={modelColors[model] || "#94a3b8"}
770+
/>
771+
);
772+
})}
773+
774+
{/* Keep the original compliant/exceeding bars but make them toggleable */}
743775
{visibleBars.includes('compliantRequests') && (
744776
<Bar
745777
dataKey="compliantRequests"

src/lib/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ export interface PowerUserDailyBreakdown {
283283
date: string;
284284
compliantRequests: number;
285285
exceedingRequests: number;
286+
// Model breakdown - each model will have its own field for recharts stacking
287+
[key: string]: string | number; // Allow dynamic model fields
286288
}
287289

288290

@@ -443,6 +445,16 @@ export function getPowerUserDailyBreakdown(data: CopilotUsageData[], powerUserNa
443445
};
444446
}
445447

448+
// Track model usage - create field name for this model
449+
const modelFieldName = item.model;
450+
if (!dailyBreakdown[date][modelFieldName]) {
451+
dailyBreakdown[date][modelFieldName] = 0;
452+
}
453+
454+
// Add to model-specific field
455+
dailyBreakdown[date][modelFieldName] = (dailyBreakdown[date][modelFieldName] as number) + item.requestsUsed;
456+
457+
// Keep the existing compliant/exceeding breakdown for backwards compatibility
446458
if (item.exceedsQuota) {
447459
dailyBreakdown[date].exceedingRequests += item.requestsUsed;
448460
} else {
@@ -454,6 +466,22 @@ export function getPowerUserDailyBreakdown(data: CopilotUsageData[], powerUserNa
454466
return Object.values(dailyBreakdown).sort((a, b) => a.date.localeCompare(b.date));
455467
}
456468

469+
// Helper function to extract all unique models from power user daily breakdown data
470+
export function getUniqueModelsFromBreakdown(breakdownData: PowerUserDailyBreakdown[]): string[] {
471+
const modelSet = new Set<string>();
472+
473+
breakdownData.forEach(dayData => {
474+
Object.keys(dayData).forEach(key => {
475+
// Skip non-model fields (date, compliantRequests, exceedingRequests)
476+
if (key !== 'date' && key !== 'compliantRequests' && key !== 'exceedingRequests') {
477+
modelSet.add(key);
478+
}
479+
});
480+
});
481+
482+
return Array.from(modelSet).sort();
483+
}
484+
457485
// Function to get the last date from CSV data
458486
export function getLastDateFromData(data: CopilotUsageData[]): string | null {
459487
if (!data.length) return null;

src/test/power-user-breakdown.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { getPowerUserDailyBreakdown, CopilotUsageData } from '../lib/utils';
2+
import { getPowerUserDailyBreakdown, getUniqueModelsFromBreakdown, CopilotUsageData } from '../lib/utils';
33

44
describe('Power User Daily Breakdown', () => {
55
const mockData: CopilotUsageData[] = [
@@ -127,4 +127,43 @@ describe('Power User Daily Breakdown', () => {
127127
expect(result[0].compliantRequests).toBe(1.5);
128128
expect(result[0].exceedingRequests).toBe(2.3);
129129
});
130+
131+
it('should include model-specific fields in daily breakdown', () => {
132+
const powerUserNames = ['power-user-1'];
133+
const result = getPowerUserDailyBreakdown(mockData, powerUserNames);
134+
135+
// Check that model fields are included
136+
const day1 = result.find(r => r.date === '2025-01-01');
137+
expect(day1).toBeDefined();
138+
expect(day1?.['gpt-4']).toBe(15); // From mockData, power-user-1 used gpt-4 for 15 requests on 2025-01-01
139+
expect(day1?.['claude-3']).toBe(8); // From mockData, power-user-1 used claude-3 for 8 requests on 2025-01-01
140+
141+
const day2 = result.find(r => r.date === '2025-01-02');
142+
expect(day2).toBeDefined();
143+
expect(day2?.['gpt-4']).toBe(12); // From mockData, power-user-1 used gpt-4 for 12 requests on 2025-01-02
144+
expect(day2?.['claude-3']).toBeUndefined(); // No claude-3 usage on 2025-01-02
145+
});
146+
147+
it('should aggregate model usage across multiple power users', () => {
148+
const powerUserNames = ['power-user-1', 'power-user-2'];
149+
const result = getPowerUserDailyBreakdown(mockData, powerUserNames);
150+
151+
const day1 = result.find(r => r.date === '2025-01-01');
152+
expect(day1).toBeDefined();
153+
// power-user-1: 15 gpt-4, power-user-2: 20 gpt-4 = 35 total
154+
expect(day1?.['gpt-4']).toBe(35);
155+
// power-user-1: 8 claude-3, power-user-2: 0 claude-3 = 8 total
156+
expect(day1?.['claude-3']).toBe(8);
157+
});
158+
159+
it('should extract unique models from breakdown data', () => {
160+
const powerUserNames = ['power-user-1', 'power-user-2'];
161+
const result = getPowerUserDailyBreakdown(mockData, powerUserNames);
162+
const models = getUniqueModelsFromBreakdown(result);
163+
164+
expect(models).toContain('gpt-4');
165+
expect(models).toContain('claude-3');
166+
expect(models).toHaveLength(2);
167+
expect(models).toEqual(['claude-3', 'gpt-4']); // Should be sorted
168+
});
130169
});

0 commit comments

Comments
 (0)