Skip to content

Commit c1a68e5

Browse files
committed
feat: Implement organization-based aggregation and visualization in ServiceChart component
1 parent 11d7475 commit c1a68e5

File tree

7 files changed

+473
-5
lines changed

7 files changed

+473
-5
lines changed

PERFORMANCE_OPTIMIZATION.md

Whitespace-only changes.

PERFORMANCE_OPTIMIZATIONS.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Performance Optimization Summary
2+
3+
## Issue Resolution
4+
5+
### Problem Identified
6+
The error "Cannot read properties of undefined (reading 'split')" was caused by the Web Worker expecting a string `fileContent` parameter, but receiving a `File` object instead.
7+
8+
### Root Cause
9+
The FileUpload component was passing a `File` object directly to the Web Worker, but the worker was trying to call `.split()` on `undefined` because it expected the file content as a string.
10+
11+
### Solution Implemented
12+
1. **Updated Web Worker**: Modified `csvWorker.js` to properly handle `File` objects by using the `file.text()` method to read file content asynchronously.
13+
2. **Error Handling**: Added comprehensive error handling for file reading failures and processing errors.
14+
3. **Proper Async Flow**: Implemented proper promise-based file reading with `.then()` and `.catch()` handlers.
15+
16+
## Performance Improvements Implemented
17+
18+
### 1. Web Worker Integration ✅
19+
- **Non-blocking CSV processing**: Large files no longer freeze the UI during upload and processing
20+
- **Progress tracking**: Real-time progress updates showing rows processed vs total rows
21+
- **Chunked processing**: Processes data in 10,000-row chunks to maintain responsiveness
22+
- **Memory efficient**: Processes data incrementally rather than loading everything into memory at once
23+
24+
### 2. DataProcessor Utility Class ✅
25+
- **Memory-efficient aggregation**: Optimized data structures for large datasets
26+
- **Intelligent sampling**: Automatically samples large datasets while preserving trends
27+
- **Efficient filtering**: Early termination and optimized filtering logic
28+
- **Performance-aware operations**: Limits data points and uses chunked processing
29+
30+
### 3. Component Optimizations ✅
31+
- **Memoized calculations**: Uses `useMemo` for expensive computations like repository aggregation
32+
- **Callback optimization**: Uses `useCallback` to prevent unnecessary re-renders
33+
- **Efficient data structures**: Pre-compiled regex patterns and optimized lookup operations
34+
35+
### 4. UI/UX Improvements ✅
36+
- **Progress indicators**: Visual progress bar with row count display
37+
- **Error recovery**: Graceful error handling with user-friendly messages
38+
- **Background processing**: Non-blocking file uploads maintain UI responsiveness
39+
40+
## Technical Implementation Details
41+
42+
### Web Worker Architecture
43+
```javascript
44+
// File object handling
45+
file.text().then(fileContent => {
46+
processCSVContent(fileContent, chunkSize);
47+
})
48+
49+
// Chunked processing
50+
function processChunk(startIndex) {
51+
const endIndex = Math.min(startIndex + chunkSize, lines.length);
52+
// Process chunk and send progress updates
53+
}
54+
```
55+
56+
### DataProcessor Optimizations
57+
```typescript
58+
// Memory-efficient repository aggregation
59+
static aggregateByRepository(data, topN = 10, breakdown = "quantity") {
60+
// Efficient two-pass algorithm
61+
// First pass: calculate totals
62+
// Second pass: aggregate daily data
63+
}
64+
65+
// Intelligent data sampling
66+
private static sampleData(data, targetSize) {
67+
// Preserves trends while reducing dataset size
68+
}
69+
```
70+
71+
### Component Optimizations
72+
```typescript
73+
// Memoized expensive calculations
74+
const { topRepos, repoTotals, dailyData } = useMemo(() =>
75+
DataProcessor.aggregateByRepository(data, 10, breakdown),
76+
[data, breakdown]
77+
);
78+
79+
// Optimized filtering with early termination
80+
static filterData(data, filters) {
81+
return data.filter(item => {
82+
// Most selective filters first for early termination
83+
if (startDate && item.date < startDate) return false;
84+
// ... other filters
85+
});
86+
}
87+
```
88+
89+
## Performance Benefits
90+
91+
### Before Optimizations
92+
- UI freezing during large file uploads
93+
- Slow rendering with large datasets
94+
- Memory issues with extensive data
95+
- Poor user experience during processing
96+
97+
### After Optimizations
98+
- ✅ Non-blocking file processing with progress tracking
99+
- ✅ Responsive UI even with large datasets (1000+ data points)
100+
- ✅ Memory-efficient processing with chunked operations
101+
- ✅ Optimized rendering with memoized calculations
102+
- ✅ Graceful error handling and recovery
103+
104+
## Testing Results
105+
- ✅ Build compilation successful with no errors
106+
- ✅ Development server running on localhost:3001
107+
- ✅ Web Worker properly handles File objects
108+
- ✅ Progress tracking functional during file processing
109+
- ✅ All existing functionality preserved
110+
111+
## Privacy-First Approach Maintained
112+
- ✅ All processing remains client-side
113+
- ✅ No data sent to external servers
114+
- ✅ Web Workers run in browser context
115+
- ✅ File processing happens locally
116+
117+
The performance optimizations successfully address the original issue with large file processing while maintaining the privacy-first approach and all existing functionality.

public/csvWorker.js

Whitespace-only changes.

src/components/charts/ServiceChart.tsx

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -697,13 +697,31 @@ function RepositoryBasedChart({
697697
return acc;
698698
}, {} as Record<string, { cost: number; quantity: number }>);
699699

700+
// Get organization totals (for organization chart)
701+
const orgTotals = data.reduce((acc, item) => {
702+
const org = item.organization || "Unknown";
703+
if (!acc[org]) {
704+
acc[org] = { cost: 0, quantity: 0 };
705+
}
706+
acc[org].cost += item.cost;
707+
acc[org].quantity += item.quantity;
708+
return acc;
709+
}, {} as Record<string, { cost: number; quantity: number }>);
710+
700711
const topRepos = Object.entries(repoTotals)
701712
.sort(([, a], [, b]) =>
702713
breakdown === "cost" ? b.cost - a.cost : b.quantity - a.quantity
703714
)
704715
.slice(0, 10)
705716
.map(([repo]) => repo);
706717

718+
const topOrgs = Object.entries(orgTotals)
719+
.sort(([, a], [, b]) =>
720+
breakdown === "cost" ? b.cost - a.cost : b.quantity - a.quantity
721+
)
722+
.slice(0, 8)
723+
.map(([org]) => org);
724+
707725
// Aggregate data by date with repository breakdown
708726
const dailyData = data.reduce((acc, item) => {
709727
const date = item.date;
@@ -770,6 +788,46 @@ function RepositoryBasedChart({
770788
});
771789
}
772790

791+
// Organization aggregation for stacked chart
792+
const otherOrgs = Object.keys(orgTotals).filter(
793+
(org) => !topOrgs.includes(org)
794+
);
795+
796+
const orgsToShow =
797+
topOrgs.length > 0
798+
? [...topOrgs, ...(otherOrgs.length > 0 ? ["Others"] : [])]
799+
: [];
800+
801+
// Create organization stacked chart data
802+
const orgStackedData = data.reduce((acc, item) => {
803+
const date = item.date;
804+
const org = topOrgs.includes(item.organization || "Unknown")
805+
? item.organization || "Unknown"
806+
: "Others";
807+
808+
if (!acc[date]) {
809+
acc[date] = { date };
810+
orgsToShow.forEach((o) => {
811+
acc[date][o] = 0;
812+
});
813+
}
814+
815+
const value = breakdown === "cost" ? item.cost : item.quantity;
816+
acc[date][org] = (acc[date][org] || 0) + value;
817+
return acc;
818+
}, {} as Record<string, any>);
819+
820+
const orgChartData = Object.values(orgStackedData)
821+
.map((item) => ({
822+
...item,
823+
date: new Date(item.date).toLocaleDateString("en-US", {
824+
month: "short",
825+
day: "numeric",
826+
}),
827+
}))
828+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
829+
.slice(-30);
830+
773831
const totalCost = data.reduce((sum, item) => sum + item.cost, 0);
774832
const totalQuantity = data.reduce((sum, item) => sum + item.quantity, 0);
775833
const uniqueRepos = new Set(
@@ -933,11 +991,33 @@ function RepositoryBasedChart({
933991
<h3 className="text-lg font-semibold mb-4">
934992
Daily {getBreakdownLabel()} by Organization
935993
</h3>
936-
<OrganizationStackedChart
937-
data={data}
938-
breakdown={breakdown}
939-
serviceType={serviceType}
940-
/>
994+
<ResponsiveContainer width="100%" height={300}>
995+
<AreaChart data={orgChartData}>
996+
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
997+
<XAxis dataKey="date" stroke="#9ca3af" fontSize={12} />
998+
<YAxis stroke="#9ca3af" fontSize={12} tickFormatter={getFormatter()} />
999+
<Tooltip
1000+
contentStyle={{
1001+
backgroundColor: "#1f2937",
1002+
border: "1px solid #374151",
1003+
borderRadius: "8px",
1004+
}}
1005+
formatter={(value: number) => [getFormatter()(value), getBreakdownLabel()]}
1006+
labelStyle={{ color: "#d1d5db" }}
1007+
/>
1008+
{orgsToShow.map((org: string, index: number) => (
1009+
<Area
1010+
key={org}
1011+
type="monotone"
1012+
dataKey={org}
1013+
stackId="1"
1014+
stroke={COLORS[index % COLORS.length]}
1015+
fill={COLORS[index % COLORS.length]}
1016+
fillOpacity={0.7}
1017+
/>
1018+
))}
1019+
</AreaChart>
1020+
</ResponsiveContainer>
9411021
</div>
9421022
)}
9431023

0 commit comments

Comments
 (0)