argent-grid 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.github/workflows/pages.yml +6 -12
  3. package/.storybook/main.ts +20 -0
  4. package/.storybook/preview.ts +18 -0
  5. package/.storybook/tsconfig.json +24 -0
  6. package/AGENTS.md +70 -27
  7. package/README.md +51 -34
  8. package/angular.json +66 -0
  9. package/biome.json +66 -0
  10. package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
  11. package/docs/AG-GRID-COMPARISON.md +725 -0
  12. package/docs/CELL-RENDERER-GUIDE.md +241 -0
  13. package/docs/CONTEXT-MENU-GUIDE.md +371 -0
  14. package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
  15. package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
  16. package/docs/PERFORMANCE-REVIEW.md +571 -0
  17. package/docs/RESEARCH-STATUS.md +234 -0
  18. package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
  19. package/docs/STORYBOOK-REFACTOR.md +215 -0
  20. package/docs/STORYBOOK-STATUS.md +156 -0
  21. package/docs/TEST-COVERAGE-REPORT.md +276 -0
  22. package/docs/THEME-API-GUIDE.md +445 -0
  23. package/docs/THEME-API-PLAN.md +364 -0
  24. package/e2e/advanced.spec.ts +109 -0
  25. package/e2e/argentgrid.spec.ts +65 -0
  26. package/e2e/benchmark.spec.ts +52 -0
  27. package/e2e/cell-renderers.spec.ts +152 -0
  28. package/e2e/debug-streaming.spec.ts +31 -0
  29. package/e2e/dnd.spec.ts +73 -0
  30. package/e2e/screenshots.spec.ts +52 -0
  31. package/e2e/theming.spec.ts +35 -0
  32. package/e2e/visual.spec.ts +112 -0
  33. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  37. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  38. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  39. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  40. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  41. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  42. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  43. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  44. package/package.json +21 -7
  45. package/plan.md +56 -28
  46. package/playwright.config.ts +38 -0
  47. package/setup-vitest.ts +10 -13
  48. package/src/lib/argent-grid.module.ts +10 -12
  49. package/src/lib/components/argent-grid.component.css +281 -321
  50. package/src/lib/components/argent-grid.component.html +295 -207
  51. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  52. package/src/lib/components/argent-grid.component.ts +1193 -290
  53. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  54. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  55. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  56. package/src/lib/components/set-filter/set-filter.component.ts +307 -0
  57. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  58. package/src/lib/directives/click-outside.directive.ts +19 -0
  59. package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
  60. package/src/lib/rendering/canvas-renderer.ts +456 -452
  61. package/src/lib/rendering/live-data-handler.ts +110 -0
  62. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  63. package/src/lib/rendering/render/blit.spec.ts +16 -27
  64. package/src/lib/rendering/render/blit.ts +48 -36
  65. package/src/lib/rendering/render/cells.spec.ts +132 -0
  66. package/src/lib/rendering/render/cells.ts +167 -28
  67. package/src/lib/rendering/render/column-utils.ts +95 -0
  68. package/src/lib/rendering/render/hit-test.ts +50 -0
  69. package/src/lib/rendering/render/index.ts +88 -76
  70. package/src/lib/rendering/render/lines.ts +53 -47
  71. package/src/lib/rendering/render/primitives.ts +423 -0
  72. package/src/lib/rendering/render/theme.spec.ts +8 -12
  73. package/src/lib/rendering/render/theme.ts +7 -10
  74. package/src/lib/rendering/render/types.ts +3 -2
  75. package/src/lib/rendering/render/walk.spec.ts +35 -38
  76. package/src/lib/rendering/render/walk.ts +94 -64
  77. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  78. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  79. package/src/lib/rendering/utils/index.ts +1 -1
  80. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  81. package/src/lib/services/grid.service.spec.ts +1241 -201
  82. package/src/lib/services/grid.service.ts +1204 -235
  83. package/src/lib/themes/parts/color-schemes.ts +132 -0
  84. package/src/lib/themes/parts/icon-sets.ts +258 -0
  85. package/src/lib/themes/theme-builder.ts +347 -0
  86. package/src/lib/themes/theme-quartz.ts +72 -0
  87. package/src/lib/themes/types.ts +238 -0
  88. package/src/lib/types/ag-grid-types.ts +573 -14
  89. package/src/public-api.ts +39 -9
  90. package/src/stories/Advanced.stories.ts +249 -0
  91. package/src/stories/ArgentGrid.stories.ts +301 -0
  92. package/src/stories/Benchmark.stories.ts +76 -0
  93. package/src/stories/CellRenderers.stories.ts +395 -0
  94. package/src/stories/Filtering.stories.ts +292 -0
  95. package/src/stories/Grouping.stories.ts +290 -0
  96. package/src/stories/Streaming.stories.ts +57 -0
  97. package/src/stories/Theming.stories.ts +137 -0
  98. package/src/stories/Tooltips.stories.ts +381 -0
  99. package/src/stories/benchmark-wrapper.component.ts +355 -0
  100. package/src/stories/story-utils.ts +88 -0
  101. package/src/stories/streaming-wrapper.component.ts +441 -0
  102. package/tsconfig.json +1 -0
  103. package/tsconfig.storybook.json +10 -0
  104. package/vitest.config.ts +9 -9
  105. package/demo-app/README.md +0 -70
  106. package/demo-app/angular.json +0 -78
  107. package/demo-app/e2e/benchmark.spec.ts +0 -53
  108. package/demo-app/e2e/demo-page.spec.ts +0 -77
  109. package/demo-app/e2e/grid-features.spec.ts +0 -269
  110. package/demo-app/package-lock.json +0 -14023
  111. package/demo-app/package.json +0 -36
  112. package/demo-app/playwright-test-menu.js +0 -19
  113. package/demo-app/playwright.config.ts +0 -23
  114. package/demo-app/src/app/app.component.ts +0 -10
  115. package/demo-app/src/app/app.config.ts +0 -13
  116. package/demo-app/src/app/app.routes.ts +0 -7
  117. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  118. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  119. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  120. package/demo-app/src/index.html +0 -19
  121. package/demo-app/src/main.ts +0 -6
  122. package/demo-app/tsconfig.json +0 -31
@@ -0,0 +1,441 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
3
+ import { ArgentGridComponent, ArgentGridModule, ColDef, GridApi, themeQuartz } from '../public-api';
4
+
5
+ interface Stock {
6
+ id: string;
7
+ symbol: string;
8
+ name: string;
9
+ price: number;
10
+ change: number;
11
+ changePct: number;
12
+ history: number[];
13
+ volume: number;
14
+ marketCap: number;
15
+ }
16
+
17
+ @Component({
18
+ selector: 'app-streaming-wrapper',
19
+ standalone: true,
20
+ imports: [CommonModule, ArgentGridModule],
21
+ template: `
22
+ <div class="streaming-container">
23
+ <div class="header">
24
+ <div class="title">
25
+ <h2>Live Stock Market Feed</h2>
26
+ <div class="status-badge" [class.active]="isRunning">
27
+ <span class="dot"></span>
28
+ {{ isRunning ? 'Streaming Live' : 'Paused' }}
29
+ </div>
30
+ </div>
31
+ <div class="controls">
32
+ <button (click)="checkData()">Check Data</button>
33
+ <button (click)="toggleStreaming()" [class.pause]="isRunning">
34
+ {{ isRunning ? 'Pause Stream' : 'Start Stream' }}
35
+ </button>
36
+ <div class="stats">
37
+ <span class="stat-label">Message Rate:</span>
38
+ <span class="stat-value">{{ messageRate | number:'1.1-1' }} msgs/sec</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <argent-grid
44
+ #grid
45
+ [columnDefs]="columnDefs"
46
+ [rowData]="rowData"
47
+ [height]="height"
48
+ [width]="width"
49
+ [theme]="theme"
50
+ [gridOptions]="gridOptions"
51
+ (gridReady)="onGridReady($event)"
52
+ />
53
+ </div>
54
+ `,
55
+ styles: [
56
+ `
57
+ .streaming-container {
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 16px;
61
+ padding: 20px;
62
+ background: #f8fafc;
63
+ height: 600px;
64
+ box-sizing: border-box;
65
+ }
66
+ .header {
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ background: white;
71
+ padding: 16px 24px;
72
+ border-radius: 8px;
73
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
74
+ }
75
+ .title h2 {
76
+ margin: 0;
77
+ font-size: 1.25rem;
78
+ color: #1e293b;
79
+ }
80
+ .status-badge {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: 6px;
84
+ font-size: 12px;
85
+ font-weight: 500;
86
+ color: #64748b;
87
+ margin-top: 4px;
88
+ }
89
+ .status-badge.active {
90
+ color: #10b981;
91
+ }
92
+ .dot {
93
+ width: 8px;
94
+ height: 8px;
95
+ background: #cbd5e1;
96
+ border-radius: 50%;
97
+ }
98
+ .active .dot {
99
+ background: #10b981;
100
+ box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
101
+ animation: pulse 2s infinite;
102
+ }
103
+ @keyframes pulse {
104
+ 0% { transform: scale(0.95); opacity: 0.5; }
105
+ 50% { transform: scale(1.05); opacity: 1; }
106
+ 100% { transform: scale(0.95); opacity: 0.5; }
107
+ }
108
+ .controls {
109
+ display: flex;
110
+ gap: 20px;
111
+ align-items: center;
112
+ }
113
+ button {
114
+ padding: 8px 20px;
115
+ background: #3b82f6;
116
+ color: white;
117
+ border: none;
118
+ border-radius: 6px;
119
+ font-weight: 600;
120
+ cursor: pointer;
121
+ transition: all 0.2s;
122
+ }
123
+ button:hover {
124
+ background: #2563eb;
125
+ }
126
+ button.pause {
127
+ background: #ef4444;
128
+ }
129
+ button.pause:hover {
130
+ background: #dc2626;
131
+ }
132
+ .stats {
133
+ font-size: 14px;
134
+ }
135
+ .stat-label {
136
+ color: #64748b;
137
+ margin-right: 4px;
138
+ }
139
+ .stat-value {
140
+ color: #1e293b;
141
+ font-weight: 700;
142
+ font-family: monospace;
143
+ }
144
+ `,
145
+ ],
146
+ })
147
+ export class StreamingWrapperComponent implements OnInit, OnDestroy {
148
+ @ViewChild('grid') gridComponent!: ArgentGridComponent;
149
+
150
+ @Input() updateFrequency = 100; // ms
151
+ @Input() batchSize = 10;
152
+
153
+ columnDefs: ColDef<Stock>[] = [
154
+ { field: 'symbol', headerName: 'Symbol', width: 100, pinned: 'left', sortable: true },
155
+ { field: 'name', headerName: 'Name', width: 200, sortable: true },
156
+ {
157
+ field: 'price',
158
+ headerName: 'Price',
159
+ width: 120,
160
+ sortable: true,
161
+ cellRenderer: (params: any) => {
162
+ const arrow = params.data.change >= 0 ? '▲' : '▼';
163
+ return `${arrow} $${params.value.toFixed(2)}`;
164
+ },
165
+ cellStyle: (params: any) => ({
166
+ color: params.data.change >= 0 ? '#16a34a' : '#dc2626',
167
+ }),
168
+ tooltipValueGetter: (params: any) => {
169
+ const d = params.data;
170
+ const sign = d.change >= 0 ? '+' : '';
171
+ return `${d.name}\nPrice: $${d.price.toFixed(2)}\nChange: ${sign}$${d.change.toFixed(2)} (${sign}${d.changePct.toFixed(2)}%)\nVolume: ${d.volume.toLocaleString()}`;
172
+ },
173
+ },
174
+ {
175
+ field: 'change',
176
+ headerName: 'Change',
177
+ width: 100,
178
+ cellRenderer: (params: any) => {
179
+ const val = params.value;
180
+ const arrow = val >= 0 ? '▲' : '▼';
181
+ const sign = val >= 0 ? '+' : '';
182
+ return `${arrow} ${sign}${val.toFixed(2)}`;
183
+ },
184
+ cellStyle: (params: any) => ({
185
+ color: params.value >= 0 ? '#16a34a' : '#dc2626',
186
+ }),
187
+ },
188
+ {
189
+ field: 'changePct',
190
+ headerName: '% Change',
191
+ width: 110,
192
+ cellRenderer: (params: any) => {
193
+ const val = params.value;
194
+ const arrow = val >= 0 ? '▲' : '▼';
195
+ const sign = val >= 0 ? '+' : '';
196
+ return `${arrow} ${sign}${val.toFixed(2)}%`;
197
+ },
198
+ cellStyle: (params: any) => ({
199
+ color: params.value >= 0 ? '#16a34a' : '#dc2626',
200
+ }),
201
+ },
202
+ {
203
+ field: 'history',
204
+ headerName: 'Trend',
205
+ width: 140,
206
+ sortable: false,
207
+ sparklineOptions: {
208
+ type: 'bar',
209
+ column: {
210
+ fill: '#3b82f6',
211
+ stroke: '#2563eb',
212
+ strokeWidth: 0,
213
+ padding: 0.15,
214
+ },
215
+ padding: { top: 4, bottom: 4, left: 4, right: 4 },
216
+ },
217
+ },
218
+ {
219
+ field: 'volume',
220
+ headerName: 'Volume',
221
+ width: 140,
222
+ sortable: true,
223
+ valueFormatter: (params: any) => params.value.toLocaleString(),
224
+ },
225
+ {
226
+ field: 'marketCap',
227
+ headerName: 'Mkt Cap',
228
+ width: 140,
229
+ sortable: true,
230
+ cellRenderer: (params: any) => {
231
+ const val = params.value;
232
+ if (val >= 1e12) return `$${(val / 1e12).toFixed(2)}T`;
233
+ if (val >= 1e9) return `$${(val / 1e9).toFixed(1)}B`;
234
+ return `$${(val / 1e6).toFixed(1)}M`;
235
+ },
236
+ },
237
+ ];
238
+
239
+ rowData: Stock[] = [];
240
+ height = '100%';
241
+ width = '100%';
242
+ theme = themeQuartz;
243
+
244
+ gridOptions = {
245
+ getRowId: (params: any) => params.data.symbol,
246
+ defaultColDef: {
247
+ resizable: true,
248
+ },
249
+ };
250
+
251
+ private gridApi?: GridApi<Stock>;
252
+ private intervalId: any;
253
+ private rateIntervalId: any;
254
+ private flushIntervalId: any;
255
+ isRunning = false;
256
+ updateCount = 0;
257
+ messageRate = 0;
258
+ private lastUpdateCount = 0;
259
+
260
+ // Transaction throttling - buffer updates and apply in batches
261
+ private pendingUpdates: Stock[] = [];
262
+ private flushIntervalMs = 200; // Apply transactions at most every 500ms
263
+
264
+ ngOnInit(): void {
265
+ this.rowData = this.generateInitialData();
266
+ }
267
+
268
+ onGridReady(api: GridApi<Stock>): void {
269
+ this.gridApi = api;
270
+ console.log('Grid Ready, rowData count:', this.rowData.length);
271
+ this.startStreaming();
272
+ }
273
+
274
+ toggleStreaming(): void {
275
+ if (this.isRunning) {
276
+ this.stopStreaming();
277
+ } else {
278
+ this.startStreaming();
279
+ }
280
+ }
281
+
282
+ private startStreaming(): void {
283
+ this.isRunning = true;
284
+ this.intervalId = setInterval(() => {
285
+ this.updateStocks();
286
+ }, this.updateFrequency);
287
+
288
+ // Flush pending updates every 500ms
289
+ this.flushIntervalId = setInterval(() => {
290
+ this.flushPendingUpdates();
291
+ }, this.flushIntervalMs);
292
+
293
+ // Calculate message rate every second
294
+ this.rateIntervalId = setInterval(() => {
295
+ this.messageRate = this.updateCount - this.lastUpdateCount;
296
+ this.lastUpdateCount = this.updateCount;
297
+ }, 1000);
298
+ }
299
+
300
+ private stopStreaming(): void {
301
+ this.isRunning = false;
302
+ if (this.intervalId) {
303
+ clearInterval(this.intervalId);
304
+ }
305
+ if (this.rateIntervalId) {
306
+ clearInterval(this.rateIntervalId);
307
+ }
308
+ if (this.flushIntervalId) {
309
+ clearInterval(this.flushIntervalId);
310
+ }
311
+ // Flush any remaining updates before stopping
312
+ this.flushPendingUpdates();
313
+ }
314
+
315
+ forceRender(): void {
316
+ if (this.gridComponent) {
317
+ console.log('Forcing grid render...');
318
+ this.gridComponent.refresh();
319
+ }
320
+ }
321
+
322
+ checkData(): void {
323
+ if (this.gridApi) {
324
+ console.log('Current Row Data:', this.gridApi.getRowData());
325
+ console.log('Displayed Count:', this.gridApi.getDisplayedRowCount());
326
+ }
327
+ }
328
+
329
+ private generateInitialData(): Stock[] {
330
+ const symbols = [
331
+ { s: 'AAPL', n: 'Apple Inc.', p: 185.92, m: 2.89e12 },
332
+ { s: 'MSFT', n: 'Microsoft Corp.', p: 406.32, m: 3.02e12 },
333
+ { s: 'GOOGL', n: 'Alphabet Inc.', p: 142.65, m: 1.79e12 },
334
+ { s: 'AMZN', n: 'Amazon.com Inc.', p: 174.45, m: 1.81e12 },
335
+ { s: 'NVDA', n: 'NVIDIA Corp.', p: 788.17, m: 1.94e12 },
336
+ { s: 'META', n: 'Meta Platforms Inc.', p: 484.03, m: 1.24e12 },
337
+ { s: 'TSLA', n: 'Tesla Inc.', p: 191.97, m: 611.3e9 },
338
+ { s: 'BRK.B', n: 'Berkshire Hathaway', p: 408.84, m: 882.4e9 },
339
+ { s: 'V', n: 'Visa Inc.', p: 282.43, m: 581.2e9 },
340
+ { s: 'JPM', n: 'JPMorgan Chase & Co.', p: 184.21, m: 531.5e9 },
341
+ { s: 'UNH', n: 'UnitedHealth Group', p: 525.44, m: 485.1e9 },
342
+ { s: 'LLY', n: 'Eli Lilly & Co.', p: 769.72, m: 730.8e9 },
343
+ { s: 'XOM', n: 'Exxon Mobil Corp.', p: 105.21, m: 417.3e9 },
344
+ { s: 'MA', n: 'Mastercard Inc.', p: 471.22, m: 439.1e9 },
345
+ { s: 'AVGO', n: 'Broadcom Inc.', p: 1304.11, m: 605.4e9 },
346
+ { s: 'HD', n: 'Home Depot Inc.', p: 372.44, m: 369.2e9 },
347
+ { s: 'PG', n: 'Procter & Gamble', p: 160.21, m: 377.1e9 },
348
+ { s: 'COST', n: 'Costco Wholesale', p: 742.11, m: 329.4e9 },
349
+ { s: 'CVX', n: 'Chevron Corp.', p: 154.32, m: 289.1e9 },
350
+ { s: 'ABBV', n: 'AbbVie Inc.', p: 178.44, m: 315.2e9 },
351
+ ];
352
+
353
+ // Expand to 100 stocks for more activity
354
+ const allStocks: Stock[] = [];
355
+ for (let i = 0; i < 5; i++) {
356
+ symbols.forEach((sym) => {
357
+ const symbol = i === 0 ? sym.s : `${sym.s}_${i}`;
358
+ const name = i === 0 ? sym.n : `${sym.n} Class ${i}`;
359
+ const price = sym.p * (0.8 + Math.random() * 0.4);
360
+ const history = Array.from({ length: 20 }, () => price * (0.95 + Math.random() * 0.1));
361
+
362
+ allStocks.push({
363
+ id: symbol,
364
+ symbol,
365
+ name,
366
+ price,
367
+ change: 0,
368
+ changePct: 0,
369
+ history,
370
+ volume: Math.floor(Math.random() * 10000000) + 1000000,
371
+ marketCap: sym.m * (0.8 + Math.random() * 0.4),
372
+ });
373
+ });
374
+ }
375
+
376
+ return allStocks;
377
+ }
378
+
379
+ private updateStocks(): void {
380
+ if (!this.gridApi || !this.rowData.length) return;
381
+
382
+ const updates: Stock[] = [];
383
+ const indicesToUpdate = new Set<number>();
384
+
385
+ // Pick unique random stocks to update
386
+ while (indicesToUpdate.size < Math.min(this.batchSize, this.rowData.length)) {
387
+ indicesToUpdate.add(Math.floor(Math.random() * this.rowData.length));
388
+ }
389
+
390
+ // Create a NEW array for rowData to ensure OnPush detects change
391
+ const newRowData = [...this.rowData];
392
+
393
+ indicesToUpdate.forEach((idx) => {
394
+ const stock = newRowData[idx];
395
+
396
+ // Realistic price movement (Random Walk with slight mean reversion)
397
+ const volatility = 0.002; // 0.2% per update
398
+ const drift = 0.00001; // slight upward drift
399
+ const change = stock.price * (volatility * (Math.random() - 0.5) + drift);
400
+
401
+ const newPrice = Math.max(0.01, stock.price + change);
402
+ const dayChange = newPrice - (stock.price - stock.change);
403
+ const dayChangePct = (dayChange / (newPrice - dayChange)) * 100;
404
+
405
+ // Update history
406
+ const newHistory = [...stock.history.slice(1), newPrice];
407
+
408
+ const updatedStock = {
409
+ ...stock,
410
+ price: newPrice,
411
+ change: dayChange,
412
+ changePct: dayChangePct,
413
+ history: newHistory,
414
+ volume: stock.volume + Math.floor(Math.random() * 1000),
415
+ };
416
+
417
+ newRowData[idx] = updatedStock;
418
+ updates.push(updatedStock);
419
+ });
420
+
421
+ this.rowData = newRowData;
422
+
423
+ // Buffer updates for throttled applyTransaction
424
+ this.pendingUpdates.push(...updates);
425
+ this.updateCount += updates.length;
426
+ }
427
+
428
+ private flushPendingUpdates(): void {
429
+ if (!this.gridApi || this.pendingUpdates.length === 0) return;
430
+
431
+ // Apply all buffered updates in a single transaction
432
+ const updatesToApply = [...this.pendingUpdates];
433
+ this.pendingUpdates = [];
434
+
435
+ this.gridApi.applyTransaction({ update: updatesToApply });
436
+ }
437
+
438
+ ngOnDestroy(): void {
439
+ this.stopStreaming();
440
+ }
441
+ }
package/tsconfig.json CHANGED
@@ -23,6 +23,7 @@
23
23
  "forceConsistentCasingInFileNames": true,
24
24
  "resolveJsonModule": true,
25
25
  "paths": {
26
+ "exceljs": ["node_modules/exceljs/index.d.ts"],
26
27
  "argent-grid": ["dist"],
27
28
  "argent-grid/*": ["dist/*"]
28
29
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist-storybook",
5
+ "baseUrl": ".",
6
+ "paths": {}
7
+ },
8
+ "include": ["src/**/*.ts", "src/storybook-app/**/*.ts"],
9
+ "exclude": ["node_modules", "dist", "dist-storybook"]
10
+ }
package/vitest.config.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { defineConfig } from 'vitest/config';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
3
+ import { defineConfig } from 'vitest/config';
4
4
 
5
5
  export default defineConfig({
6
6
  plugins: [
@@ -20,7 +20,7 @@ export default defineConfig({
20
20
  }
21
21
  return match;
22
22
  });
23
-
23
+
24
24
  newCode = newCode.replace(/styleUrls:\s*\[\s*['"]([^'"]+)['"]\s*\]/g, (match, p1) => {
25
25
  const stylePath = path.resolve(path.dirname(id), p1);
26
26
  if (fs.existsSync(stylePath)) {
@@ -33,11 +33,11 @@ export default defineConfig({
33
33
  }
34
34
  return match;
35
35
  });
36
-
36
+
37
37
  return { code: newCode, map: null };
38
38
  }
39
- }
40
- }
39
+ },
40
+ },
41
41
  ],
42
42
  test: {
43
43
  globals: true,
@@ -48,8 +48,8 @@ export default defineConfig({
48
48
  provider: 'v8',
49
49
  reporter: ['text', 'json', 'html'],
50
50
  include: ['src/**/*.ts'],
51
- exclude: ['src/**/*.spec.ts', 'src/**/types/**']
51
+ exclude: ['src/**/*.spec.ts', 'src/**/types/**'],
52
52
  },
53
- testTimeout: 10000
54
- }
55
- });
53
+ testTimeout: 10000,
54
+ },
55
+ });
@@ -1,70 +0,0 @@
1
- # ArgentGrid Demo App
2
-
3
- Live demo showcasing the canvas-based high-performance Angular grid.
4
-
5
- ## Quick Start
6
-
7
- ```bash
8
- # Install dependencies
9
- npm install
10
-
11
- # Start dev server
12
- npm start
13
-
14
- # Build for production
15
- npm run build
16
-
17
- # Build for GitHub Pages
18
- npm run build:gh-pages
19
- ```
20
-
21
- ## E2E Testing (Playwright)
22
-
23
- End-to-end tests are located in the `e2e/` folder. They verify critical visual and interactive features that cannot be easily tested with unit tests (like Canvas rendering and Drag & Drop).
24
-
25
- **Prerequisites:**
26
- Ensure the dev server is running (`npm start`).
27
-
28
- **Run All Tests:**
29
- ```bash
30
- npx playwright test
31
- ```
32
-
33
- **Run with UI:**
34
- ```bash
35
- npx playwright test --ui
36
- ```
37
-
38
- **Key Scenarios Covered:**
39
- - Row Grouping & Hierarchical Expansion
40
- - Floating Filters & Quick Clear
41
- - Inline Cell Editing (Enter/Escape/Tab)
42
- - Column Pinning (Sticky Columns)
43
- - Column Re-ordering via Drag & Drop
44
-
45
- ## Demo Features
46
-
47
- - **100K rows** - Standard load test
48
- - **500K rows** - Heavy load test
49
- - **1M rows** - Extreme stress test
50
- - **Real-time FPS** - Monitor rendering performance
51
- - **Canvas rendering** - Zero DOM overhead
52
-
53
- ## Live Demo
54
-
55
- https://hainanzhao.github.io/ArgentGrid/
56
-
57
- ## Tech Stack
58
-
59
- - Angular 18
60
- - Canvas 2D API
61
- - RequestAnimationFrame for 60fps
62
- - Zero dependencies beyond Angular
63
-
64
- ## Performance Targets
65
-
66
- | Rows | Load Time | FPS | Memory |
67
- |------|-----------|-----|--------|
68
- | 100K | < 500ms | 60 | ~50MB |
69
- | 500K | < 2s | 60 | ~150MB |
70
- | 1M | < 5s | 55-60 | ~300MB |
@@ -1,78 +0,0 @@
1
- {
2
- "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
- "version": 1,
4
- "newProjectRoot": "projects",
5
- "projects": {
6
- "argent-grid-demo": {
7
- "projectType": "application",
8
- "schematics": {},
9
- "root": "",
10
- "sourceRoot": "src",
11
- "prefix": "app",
12
- "architect": {
13
- "build": {
14
- "builder": "@angular-devkit/build-angular:application",
15
- "options": {
16
- "outputPath": "dist/argent-grid-demo",
17
- "index": "src/index.html",
18
- "browser": "src/main.ts",
19
- "polyfills": [],
20
- "tsConfig": "tsconfig.json",
21
- "preserveSymlinks": true,
22
- "sourceMap": true,
23
- "assets": [
24
- {
25
- "glob": "**/*",
26
- "input": "public"
27
- }
28
- ],
29
- "styles": [],
30
- "scripts": []
31
- },
32
- "configurations": {
33
- "production": {
34
- "budgets": [
35
- {
36
- "type": "initial",
37
- "maximumWarning": "500kB",
38
- "maximumError": "2MB"
39
- },
40
- {
41
- "type": "anyComponentStyle",
42
- "maximumWarning": "10kB",
43
- "maximumError": "20kB"
44
- }
45
- ],
46
- "outputHashing": "all",
47
- "sourceMap": true
48
- },
49
- "development": {
50
- "optimization": false,
51
- "extractLicenses": false,
52
- "sourceMap": true
53
- }
54
- },
55
- "defaultConfiguration": "production"
56
- },
57
- "serve": {
58
- "builder": "@angular-devkit/build-angular:dev-server",
59
- "configurations": {
60
- "production": {
61
- "buildTarget": "argent-grid-demo:build:production"
62
- },
63
- "development": {
64
- "buildTarget": "argent-grid-demo:build:development"
65
- }
66
- },
67
- "defaultConfiguration": "development"
68
- },
69
- "extract-i18n": {
70
- "builder": "@angular-devkit/build-angular:extract-i18n"
71
- }
72
- }
73
- }
74
- },
75
- "cli": {
76
- "analytics": false
77
- }
78
- }
@@ -1,53 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- test.describe('ArgentGrid Performance Benchmark', () => {
4
- test('should run the benchmark and report results', async ({ page }) => {
5
- // Navigate to the demo page
6
- await page.goto('/', { waitUntil: 'networkidle' });
7
-
8
- // Wait for the grid to be ready
9
- await page.waitForSelector('argent-grid', { timeout: 10000 });
10
-
11
- // Click the Benchmark button
12
- console.log('Starting benchmark...');
13
- await page.click('.btn-benchmark');
14
-
15
- // Wait for benchmark to complete (isBenchmarking becomes false)
16
- // The benchmark-results section appears when finished
17
- await page.waitForSelector('.benchmark-results', { timeout: 30000 });
18
-
19
- // Extract results
20
- const results = await page.evaluate(() => {
21
- const items = document.querySelectorAll('.result-item');
22
- const data: Record<string, string> = {};
23
- items.forEach(item => {
24
- const label = item.querySelector('label')?.textContent?.replace(':', '') || 'unknown';
25
- const value = item.querySelector('span')?.textContent || '0';
26
- data[label] = value;
27
- });
28
- return data;
29
- });
30
-
31
- console.log('-------------------------------------------');
32
- console.log('🚀 ArgentGrid Performance Benchmark Results');
33
- console.log('-------------------------------------------');
34
- Object.entries(results).forEach(([label, value]) => {
35
- console.log(`${label.padEnd(20)}: ${value}`);
36
- });
37
- console.log('-------------------------------------------');
38
-
39
- // Optional assertions based on targets
40
- const initialRender = parseFloat(results['Initial Render']);
41
- const scrollFrame = parseFloat(results['Avg Scroll Frame']);
42
-
43
- console.log(`Checking against targets...`);
44
- console.log(`Initial Render: ${initialRender}ms (Target: < 30ms)`);
45
- console.log(`Avg Scroll Frame: ${scrollFrame}ms (Target: < 4ms)`);
46
-
47
- // We don't fail the test if targets aren't met yet, but we report it
48
- if (initialRender > 30) console.warn('⚠️ Initial render is slower than target (30ms)');
49
- if (scrollFrame > 4) console.warn('⚠️ Scroll frame time is slower than target (4ms)');
50
-
51
- expect(results['Total Test Time']).toBeDefined();
52
- });
53
- });