argent-grid 0.2.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/AGENTS.md +70 -27
- package/e2e/advanced.spec.ts +1 -1
- package/e2e/benchmark.spec.ts +7 -7
- 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 +1 -1
- package/e2e/visual.spec.ts +30 -9
- 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 +5 -5
- package/plan.md +30 -34
- package/src/lib/components/argent-grid.component.css +258 -549
- package/src/lib/components/argent-grid.component.html +272 -306
- package/src/lib/components/argent-grid.component.ts +585 -135
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +2 -2
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +7 -2
- package/src/lib/rendering/canvas-renderer.spec.ts +148 -1
- package/src/lib/rendering/canvas-renderer.ts +177 -286
- package/src/lib/rendering/render/cells.ts +122 -5
- package/src/lib/rendering/render/column-utils.ts +27 -5
- package/src/lib/rendering/render/hit-test.ts +6 -11
- package/src/lib/rendering/render/index.ts +15 -6
- package/src/lib/rendering/render/lines.ts +12 -6
- package/src/lib/rendering/render/primitives.ts +269 -7
- package/src/lib/rendering/render/types.ts +2 -1
- package/src/lib/rendering/render/walk.ts +39 -19
- package/src/lib/services/grid.service.spec.ts +76 -0
- package/src/lib/services/grid.service.ts +451 -114
- package/src/lib/themes/theme-quartz.ts +2 -2
- package/src/lib/types/ag-grid-types.ts +500 -0
- package/src/stories/Advanced.stories.ts +78 -17
- package/src/stories/ArgentGrid.stories.ts +50 -26
- package/src/stories/Benchmark.stories.ts +17 -15
- package/src/stories/CellRenderers.stories.ts +205 -31
- package/src/stories/Filtering.stories.ts +56 -16
- package/src/stories/Grouping.stories.ts +86 -13
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +23 -10
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +69 -29
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Storybook stories
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const LOCATION_FLAGS: Record<string, string> = {
|
|
6
|
+
'New York': 'πΊπΈ New York',
|
|
7
|
+
'San Francisco': 'πΊπΈ San Francisco',
|
|
8
|
+
London: 'π¬π§ London',
|
|
9
|
+
Singapore: 'πΈπ¬ Singapore',
|
|
10
|
+
Remote: 'π Remote',
|
|
11
|
+
Berlin: 'π©πͺ Berlin',
|
|
12
|
+
Tokyo: 'π―π΅ Tokyo',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const DEPARTMENT_EMOJIS: Record<string, string> = {
|
|
16
|
+
Engineering: 'βοΈ Engineering',
|
|
17
|
+
Sales: 'π° Sales',
|
|
18
|
+
Marketing: 'π£ Marketing',
|
|
19
|
+
HR: 'π₯ HR',
|
|
20
|
+
Finance: 'π Finance',
|
|
21
|
+
Design: 'π¨ Design',
|
|
22
|
+
Operations: 'π’ Operations',
|
|
23
|
+
Support: 'π§ Support',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const ROLE_EMOJIS: Record<string, string> = {
|
|
27
|
+
Engineer: 'π» Engineer',
|
|
28
|
+
'Software Engineer': 'π» Software Engineer',
|
|
29
|
+
Manager: 'π Manager',
|
|
30
|
+
Director: 'π’ Director',
|
|
31
|
+
VP: 'π VP',
|
|
32
|
+
Intern: 'π Intern',
|
|
33
|
+
Analyst: 'π Analyst',
|
|
34
|
+
Lead: 'π₯ Lead',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Common value formatter for location columns to add flags
|
|
39
|
+
*/
|
|
40
|
+
export const locationValueFormatter = (params: any) => {
|
|
41
|
+
return LOCATION_FLAGS[params.value] ?? params.value;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Common value formatter for department columns to add emojis
|
|
46
|
+
*/
|
|
47
|
+
export const departmentValueFormatter = (params: any) => {
|
|
48
|
+
return DEPARTMENT_EMOJIS[params.value] ?? params.value;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Common value formatter for role columns to add emojis
|
|
53
|
+
*/
|
|
54
|
+
export const roleValueFormatter = (params: any) => {
|
|
55
|
+
return ROLE_EMOJIS[params.value] ?? params.value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Standard locations for mock data generation
|
|
60
|
+
*/
|
|
61
|
+
export const STORY_LOCATIONS = [
|
|
62
|
+
'New York',
|
|
63
|
+
'San Francisco',
|
|
64
|
+
'London',
|
|
65
|
+
'Singapore',
|
|
66
|
+
'Remote',
|
|
67
|
+
'Berlin',
|
|
68
|
+
'Tokyo',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Standard departments for mock data generation
|
|
73
|
+
*/
|
|
74
|
+
export const STORY_DEPARTMENTS = [
|
|
75
|
+
'Engineering',
|
|
76
|
+
'Sales',
|
|
77
|
+
'Marketing',
|
|
78
|
+
'HR',
|
|
79
|
+
'Finance',
|
|
80
|
+
'Design',
|
|
81
|
+
'Operations',
|
|
82
|
+
'Support',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Standard roles for mock data generation
|
|
87
|
+
*/
|
|
88
|
+
export const STORY_ROLES = ['Engineer', 'Manager', 'Director', 'VP', 'Intern', 'Analyst', 'Lead'];
|
|
@@ -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
|
+
}
|