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.
- package/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +70 -27
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +112 -0
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +21 -7
- package/plan.md +56 -28
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +281 -321
- package/src/lib/components/argent-grid.component.html +295 -207
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +1193 -290
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +307 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
- package/src/lib/rendering/canvas-renderer.ts +456 -452
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +167 -28
- package/src/lib/rendering/render/column-utils.ts +95 -0
- package/src/lib/rendering/render/hit-test.ts +50 -0
- package/src/lib/rendering/render/index.ts +88 -76
- package/src/lib/rendering/render/lines.ts +53 -47
- package/src/lib/rendering/render/primitives.ts +423 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +3 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +94 -64
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1241 -201
- package/src/lib/services/grid.service.ts +1204 -235
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +573 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +249 -0
- package/src/stories/ArgentGrid.stories.ts +301 -0
- package/src/stories/Benchmark.stories.ts +76 -0
- package/src/stories/CellRenderers.stories.ts +395 -0
- package/src/stories/Filtering.stories.ts +292 -0
- package/src/stories/Grouping.stories.ts +290 -0
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +137 -0
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +355 -0
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- 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
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
|
+
});
|
package/demo-app/README.md
DELETED
|
@@ -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 |
|
package/demo-app/angular.json
DELETED
|
@@ -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
|
-
});
|