@memberjunction/ng-clustering 0.0.1 → 5.24.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 (42) hide show
  1. package/README.md +441 -27
  2. package/dist/__tests__/clustering-service.test.d.ts +2 -0
  3. package/dist/__tests__/clustering-service.test.d.ts.map +1 -0
  4. package/dist/__tests__/clustering-service.test.js +487 -0
  5. package/dist/__tests__/clustering-service.test.js.map +1 -0
  6. package/dist/__tests__/clustering-types.test.d.ts +2 -0
  7. package/dist/__tests__/clustering-types.test.d.ts.map +1 -0
  8. package/dist/__tests__/clustering-types.test.js +254 -0
  9. package/dist/__tests__/clustering-types.test.js.map +1 -0
  10. package/dist/__tests__/index.test.d.ts +2 -0
  11. package/dist/__tests__/index.test.d.ts.map +1 -0
  12. package/dist/__tests__/index.test.js +7 -0
  13. package/dist/__tests__/index.test.js.map +1 -0
  14. package/dist/__tests__/scatter-component-logic.test.d.ts +2 -0
  15. package/dist/__tests__/scatter-component-logic.test.d.ts.map +1 -0
  16. package/dist/__tests__/scatter-component-logic.test.js +494 -0
  17. package/dist/__tests__/scatter-component-logic.test.js.map +1 -0
  18. package/dist/lib/cluster-config-panel.component.d.ts +274 -0
  19. package/dist/lib/cluster-config-panel.component.d.ts.map +1 -0
  20. package/dist/lib/cluster-config-panel.component.js +732 -0
  21. package/dist/lib/cluster-config-panel.component.js.map +1 -0
  22. package/dist/lib/cluster-scatter.component.d.ts +572 -0
  23. package/dist/lib/cluster-scatter.component.d.ts.map +1 -0
  24. package/dist/lib/cluster-scatter.component.js +1410 -0
  25. package/dist/lib/cluster-scatter.component.js.map +1 -0
  26. package/dist/lib/clustering.module.d.ts +12 -0
  27. package/dist/lib/clustering.module.d.ts.map +1 -0
  28. package/dist/lib/clustering.module.js +38 -0
  29. package/dist/lib/clustering.module.js.map +1 -0
  30. package/dist/lib/clustering.service.d.ts +43 -0
  31. package/dist/lib/clustering.service.d.ts.map +1 -0
  32. package/dist/lib/clustering.service.js +253 -0
  33. package/dist/lib/clustering.service.js.map +1 -0
  34. package/dist/lib/clustering.types.d.ts +271 -0
  35. package/dist/lib/clustering.types.d.ts.map +1 -0
  36. package/dist/lib/clustering.types.js +74 -0
  37. package/dist/lib/clustering.types.js.map +1 -0
  38. package/dist/public-api.d.ts +6 -0
  39. package/dist/public-api.d.ts.map +1 -0
  40. package/dist/public-api.js +11 -0
  41. package/dist/public-api.js.map +1 -0
  42. package/package.json +41 -6
@@ -0,0 +1,494 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { CLUSTER_COLORS } from '../lib/clustering.types';
3
+ // Mock Angular core since the component uses @Component, @Input, etc.
4
+ const { mockDetectChanges } = vi.hoisted(() => ({
5
+ mockDetectChanges: vi.fn(),
6
+ }));
7
+ vi.mock('@angular/core', () => ({
8
+ Component: () => (target) => target,
9
+ Input: () => (_target, _key) => { },
10
+ Output: () => (_target, _key) => { },
11
+ ViewChild: () => (_target, _key) => { },
12
+ EventEmitter: class {
13
+ constructor() {
14
+ this.emit = vi.fn();
15
+ this.subscribe = vi.fn();
16
+ }
17
+ },
18
+ ElementRef: class {
19
+ },
20
+ ChangeDetectorRef: class {
21
+ },
22
+ AfterViewInit: class {
23
+ },
24
+ OnDestroy: class {
25
+ },
26
+ OnChanges: class {
27
+ },
28
+ SimpleChanges: class {
29
+ },
30
+ ViewEncapsulation: { None: 0 },
31
+ inject: () => ({ detectChanges: mockDetectChanges }),
32
+ }));
33
+ import { ClusterScatterComponent } from '../lib/cluster-scatter.component';
34
+ // ================================================================
35
+ // Helpers
36
+ // ================================================================
37
+ function makePoints(count, clusterId = 0) {
38
+ return Array.from({ length: count }, (_, i) => ({
39
+ X: 100 + i * 50,
40
+ Y: 200 + i * 30,
41
+ ClusterId: clusterId,
42
+ Label: `Point ${i}`,
43
+ VectorKey: `vec-${i}`,
44
+ Metadata: { index: i },
45
+ }));
46
+ }
47
+ function makeClusters(count) {
48
+ return Array.from({ length: count }, (_, i) => ({
49
+ Id: i,
50
+ Label: `Cluster ${i + 1}`,
51
+ Color: CLUSTER_COLORS[i % CLUSTER_COLORS.length],
52
+ MemberCount: 5,
53
+ }));
54
+ }
55
+ function createComponent() {
56
+ const comp = new ClusterScatterComponent();
57
+ return comp;
58
+ }
59
+ // ================================================================
60
+ // Tests
61
+ // ================================================================
62
+ describe('ClusterScatterComponent (pure logic)', () => {
63
+ let comp;
64
+ beforeEach(() => {
65
+ comp = createComponent();
66
+ });
67
+ // ================================================================
68
+ // Default property values
69
+ // ================================================================
70
+ describe('default property values', () => {
71
+ it('should have correct defaults', () => {
72
+ expect(comp.Points).toEqual([]);
73
+ expect(comp.Clusters).toEqual([]);
74
+ expect(comp.IsLoading).toBe(false);
75
+ expect(comp.DotRadius).toBe(5);
76
+ expect(comp.DotOpacity).toBe(0.75);
77
+ expect(comp.HighlightRadius).toBe(8);
78
+ expect(comp.ShowLegend).toBe(true);
79
+ expect(comp.ShowTooltip).toBe(true);
80
+ expect(comp.TooltipFields).toEqual([]);
81
+ expect(comp.ColorPalette).toEqual([]);
82
+ expect(comp.EnableZoom).toBe(true);
83
+ expect(comp.EnablePan).toBe(true);
84
+ expect(comp.AnimateTransitions).toBe(true);
85
+ expect(comp.MinZoom).toBe(0.5);
86
+ expect(comp.MaxZoom).toBe(10);
87
+ });
88
+ it('should have correct default ViewBox', () => {
89
+ expect(comp.ViewBox).toEqual([0, 0, 1000, 700]);
90
+ });
91
+ });
92
+ // ================================================================
93
+ // GetViewBoxString
94
+ // ================================================================
95
+ describe('GetViewBoxString', () => {
96
+ it('should return space-separated string', () => {
97
+ expect(comp.GetViewBoxString()).toBe('0 0 1000 700');
98
+ });
99
+ it('should reflect modified ViewBox', () => {
100
+ comp.ViewBox = [50, 100, 800, 600];
101
+ expect(comp.GetViewBoxString()).toBe('50 100 800 600');
102
+ });
103
+ });
104
+ // ================================================================
105
+ // GetPointColor
106
+ // ================================================================
107
+ describe('GetPointColor', () => {
108
+ it('should return gray for outlier points (ClusterId < 0)', () => {
109
+ const outlier = {
110
+ X: 0, Y: 0, ClusterId: -1, Label: '', VectorKey: 'o', Metadata: {},
111
+ };
112
+ expect(comp.GetPointColor(outlier)).toBe('#64748b');
113
+ });
114
+ it('should return cluster color from Clusters array', () => {
115
+ comp.Clusters = makeClusters(3);
116
+ const point = {
117
+ X: 0, Y: 0, ClusterId: 1, Label: '', VectorKey: 'p', Metadata: {},
118
+ };
119
+ expect(comp.GetPointColor(point)).toBe(CLUSTER_COLORS[1]);
120
+ });
121
+ it('should fall back to palette when cluster not found', () => {
122
+ comp.Clusters = []; // no clusters
123
+ const point = {
124
+ X: 0, Y: 0, ClusterId: 2, Label: '', VectorKey: 'p', Metadata: {},
125
+ };
126
+ expect(comp.GetPointColor(point)).toBe(CLUSTER_COLORS[2]);
127
+ });
128
+ it('should use custom ColorPalette when provided', () => {
129
+ comp.ColorPalette = ['#ff0000', '#00ff00', '#0000ff'];
130
+ comp.Clusters = []; // force palette fallback
131
+ const point = {
132
+ X: 0, Y: 0, ClusterId: 1, Label: '', VectorKey: 'p', Metadata: {},
133
+ };
134
+ expect(comp.GetPointColor(point)).toBe('#00ff00');
135
+ });
136
+ it('should wrap palette index for high cluster IDs', () => {
137
+ comp.Clusters = [];
138
+ const point = {
139
+ X: 0, Y: 0, ClusterId: 10, Label: '', VectorKey: 'p', Metadata: {},
140
+ };
141
+ // 10 % 10 = 0
142
+ expect(comp.GetPointColor(point)).toBe(CLUSTER_COLORS[0]);
143
+ });
144
+ });
145
+ // ================================================================
146
+ // GetPointRadius
147
+ // ================================================================
148
+ describe('GetPointRadius', () => {
149
+ it('should return DotRadius for non-selected points', () => {
150
+ const point = {
151
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
152
+ };
153
+ expect(comp.GetPointRadius(point)).toBe(5);
154
+ });
155
+ it('should return HighlightRadius for selected points', () => {
156
+ comp.SelectedPointIds = new Set(['p1']);
157
+ const point = {
158
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
159
+ };
160
+ expect(comp.GetPointRadius(point)).toBe(8);
161
+ });
162
+ it('should respect custom DotRadius and HighlightRadius', () => {
163
+ comp.DotRadius = 10;
164
+ comp.HighlightRadius = 15;
165
+ const p1 = {
166
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'a', Metadata: {},
167
+ };
168
+ const p2 = {
169
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'b', Metadata: {},
170
+ };
171
+ comp.SelectedPointIds = new Set(['b']);
172
+ expect(comp.GetPointRadius(p1)).toBe(10);
173
+ expect(comp.GetPointRadius(p2)).toBe(15);
174
+ });
175
+ });
176
+ // ================================================================
177
+ // GetPointOpacity
178
+ // ================================================================
179
+ describe('GetPointOpacity', () => {
180
+ it('should return DotOpacity when no point is hovered', () => {
181
+ const point = {
182
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
183
+ };
184
+ expect(comp.GetPointOpacity(point)).toBe(0.75);
185
+ });
186
+ it('should return full opacity for the hovered point', () => {
187
+ const point = {
188
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
189
+ };
190
+ comp.HoveredPoint = point;
191
+ expect(comp.GetPointOpacity(point)).toBe(0.75);
192
+ });
193
+ it('should dim non-hovered points when a point is hovered', () => {
194
+ const hovered = {
195
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'h', Metadata: {},
196
+ };
197
+ const other = {
198
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'o', Metadata: {},
199
+ };
200
+ comp.HoveredPoint = hovered;
201
+ expect(comp.GetPointOpacity(other)).toBe(0.75 * 0.5);
202
+ });
203
+ });
204
+ // ================================================================
205
+ // IsHighlighted / IsSelected
206
+ // ================================================================
207
+ describe('IsHighlighted', () => {
208
+ it('should return false when nothing is highlighted', () => {
209
+ const point = {
210
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
211
+ };
212
+ expect(comp.IsHighlighted(point)).toBe(false);
213
+ });
214
+ it('should return true for the highlighted point', () => {
215
+ comp.HighlightedKey = 'p1';
216
+ const point = {
217
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
218
+ };
219
+ expect(comp.IsHighlighted(point)).toBe(true);
220
+ });
221
+ });
222
+ describe('IsSelected', () => {
223
+ it('should return false when nothing is selected', () => {
224
+ const point = {
225
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
226
+ };
227
+ expect(comp.IsSelected(point)).toBe(false);
228
+ });
229
+ it('should return true for selected points', () => {
230
+ comp.SelectedPointIds = new Set(['p1', 'p2']);
231
+ const p1 = {
232
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},
233
+ };
234
+ const p3 = {
235
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p3', Metadata: {},
236
+ };
237
+ expect(comp.IsSelected(p1)).toBe(true);
238
+ expect(comp.IsSelected(p3)).toBe(false);
239
+ });
240
+ });
241
+ // ================================================================
242
+ // GetClusterLabel / GetClusterLabelById
243
+ // ================================================================
244
+ describe('GetClusterLabel', () => {
245
+ it('should return "Outlier" for negative cluster IDs', () => {
246
+ const point = {
247
+ X: 0, Y: 0, ClusterId: -1, Label: '', VectorKey: 'p', Metadata: {},
248
+ };
249
+ expect(comp.GetClusterLabel(point)).toBe('Outlier');
250
+ });
251
+ it('should return cluster label when cluster exists', () => {
252
+ comp.Clusters = [{ Id: 2, Label: 'Tech Companies', Color: '#000', MemberCount: 5 }];
253
+ const point = {
254
+ X: 0, Y: 0, ClusterId: 2, Label: '', VectorKey: 'p', Metadata: {},
255
+ };
256
+ expect(comp.GetClusterLabel(point)).toBe('Tech Companies');
257
+ });
258
+ it('should return fallback label when cluster not found', () => {
259
+ comp.Clusters = [];
260
+ const point = {
261
+ X: 0, Y: 0, ClusterId: 3, Label: '', VectorKey: 'p', Metadata: {},
262
+ };
263
+ expect(comp.GetClusterLabel(point)).toBe('Cluster 4');
264
+ });
265
+ });
266
+ describe('GetClusterLabelById', () => {
267
+ it('should return cluster label for valid ID', () => {
268
+ comp.Clusters = makeClusters(3);
269
+ expect(comp.GetClusterLabelById(0)).toBe('Cluster 1');
270
+ expect(comp.GetClusterLabelById(1)).toBe('Cluster 2');
271
+ });
272
+ it('should return empty string for unknown ID', () => {
273
+ comp.Clusters = [];
274
+ expect(comp.GetClusterLabelById(99)).toBe('');
275
+ });
276
+ });
277
+ // ================================================================
278
+ // GetClusterColor
279
+ // ================================================================
280
+ describe('GetClusterColor', () => {
281
+ it('should return gray for negative cluster IDs', () => {
282
+ expect(comp.GetClusterColor(-1)).toBe('#64748b');
283
+ });
284
+ it('should return cluster color when cluster exists', () => {
285
+ comp.Clusters = [{ Id: 0, Label: 'A', Color: '#ff0000', MemberCount: 3 }];
286
+ expect(comp.GetClusterColor(0)).toBe('#ff0000');
287
+ });
288
+ it('should fall back to palette for unknown cluster', () => {
289
+ comp.Clusters = [];
290
+ expect(comp.GetClusterColor(2)).toBe(CLUSTER_COLORS[2]);
291
+ });
292
+ });
293
+ // ================================================================
294
+ // Viewport Transform
295
+ // ================================================================
296
+ describe('GetViewportTransform / SetViewportTransform', () => {
297
+ it('should return default transform', () => {
298
+ const vt = comp.GetViewportTransform();
299
+ expect(vt.TranslateX).toBe(0);
300
+ expect(vt.TranslateY).toBe(0);
301
+ expect(vt.Scale).toBe(1);
302
+ });
303
+ it('should reflect modified ViewBox', () => {
304
+ comp.ViewBox = [100, 50, 500, 350];
305
+ const vt = comp.GetViewportTransform();
306
+ expect(vt.TranslateX).toBe(100);
307
+ expect(vt.TranslateY).toBe(50);
308
+ expect(vt.Scale).toBe(1000 / 500); // defaultWidth / ViewBox[2]
309
+ });
310
+ it('should restore a saved transform', () => {
311
+ const saved = {
312
+ TranslateX: 200,
313
+ TranslateY: 150,
314
+ Scale: 2,
315
+ };
316
+ comp.SetViewportTransform(saved);
317
+ expect(comp.ViewBox[0]).toBe(200);
318
+ expect(comp.ViewBox[1]).toBe(150);
319
+ expect(comp.ViewBox[2]).toBe(500); // 1000 / 2
320
+ expect(comp.ViewBox[3]).toBe(350); // 700 / 2
321
+ });
322
+ it('should round-trip correctly', () => {
323
+ const original = {
324
+ TranslateX: 75,
325
+ TranslateY: -30,
326
+ Scale: 1.5,
327
+ };
328
+ comp.SetViewportTransform(original);
329
+ const restored = comp.GetViewportTransform();
330
+ expect(restored.TranslateX).toBeCloseTo(75);
331
+ expect(restored.TranslateY).toBeCloseTo(-30);
332
+ expect(restored.Scale).toBeCloseTo(1.5);
333
+ });
334
+ });
335
+ // ================================================================
336
+ // GetVisiblePoints
337
+ // ================================================================
338
+ describe('GetVisiblePoints', () => {
339
+ it('should return all points within default viewport', () => {
340
+ comp.Points = makePoints(3);
341
+ // Default ViewBox: [0, 0, 1000, 700]
342
+ // Points at (100,200), (150,230), (200,260) - all inside
343
+ const visible = comp.GetVisiblePoints();
344
+ expect(visible).toHaveLength(3);
345
+ });
346
+ it('should exclude points outside viewport', () => {
347
+ comp.Points = [
348
+ { X: 500, Y: 350, ClusterId: 0, Label: 'Inside', VectorKey: 'a', Metadata: {} },
349
+ { X: 1500, Y: 350, ClusterId: 0, Label: 'Outside', VectorKey: 'b', Metadata: {} },
350
+ { X: -50, Y: 350, ClusterId: 0, Label: 'Outside2', VectorKey: 'c', Metadata: {} },
351
+ ];
352
+ const visible = comp.GetVisiblePoints();
353
+ expect(visible).toHaveLength(1);
354
+ expect(visible[0].VectorKey).toBe('a');
355
+ });
356
+ it('should respect modified ViewBox', () => {
357
+ comp.Points = [
358
+ { X: 500, Y: 350, ClusterId: 0, Label: '', VectorKey: 'a', Metadata: {} },
359
+ { X: 200, Y: 200, ClusterId: 0, Label: '', VectorKey: 'b', Metadata: {} },
360
+ ];
361
+ comp.ViewBox = [400, 300, 200, 100]; // tight box around point 'a'
362
+ const visible = comp.GetVisiblePoints();
363
+ expect(visible).toHaveLength(1);
364
+ expect(visible[0].VectorKey).toBe('a');
365
+ });
366
+ });
367
+ // ================================================================
368
+ // SelectPoints / ClearSelection / HighlightCluster
369
+ // ================================================================
370
+ describe('SelectPoints', () => {
371
+ it('should add to existing selection', () => {
372
+ comp.SelectedPointIds = new Set(['existing']);
373
+ comp.SelectPoints(['new1', 'new2']);
374
+ expect(comp.SelectedPointIds.has('existing')).toBe(true);
375
+ expect(comp.SelectedPointIds.has('new1')).toBe(true);
376
+ expect(comp.SelectedPointIds.has('new2')).toBe(true);
377
+ });
378
+ });
379
+ describe('ClearSelection', () => {
380
+ it('should clear all selections and highlighted key', () => {
381
+ comp.SelectedPointIds = new Set(['a', 'b']);
382
+ comp.HighlightedKey = 'a';
383
+ comp.ClearSelection();
384
+ expect(comp.SelectedPointIds.size).toBe(0);
385
+ expect(comp.HighlightedKey).toBeNull();
386
+ });
387
+ });
388
+ describe('HighlightCluster', () => {
389
+ it('should select all points in the specified cluster', () => {
390
+ comp.Points = [
391
+ { X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'a', Metadata: {} },
392
+ { X: 0, Y: 0, ClusterId: 1, Label: '', VectorKey: 'b', Metadata: {} },
393
+ { X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'c', Metadata: {} },
394
+ ];
395
+ comp.HighlightCluster(0);
396
+ expect(comp.SelectedPointIds.has('a')).toBe(true);
397
+ expect(comp.SelectedPointIds.has('c')).toBe(true);
398
+ expect(comp.SelectedPointIds.has('b')).toBe(false);
399
+ });
400
+ });
401
+ // ================================================================
402
+ // Detail Panel
403
+ // ================================================================
404
+ describe('CloseDetailPanel', () => {
405
+ it('should reset detail panel state', () => {
406
+ comp.ShowDetailPanel = true;
407
+ comp.SelectedPoint = makePoints(1)[0];
408
+ comp.DetailEntries = [{ Key: 'k', Value: 'v' }];
409
+ comp.ClusterMembers = makePoints(2);
410
+ comp.CloseDetailPanel();
411
+ expect(comp.ShowDetailPanel).toBe(false);
412
+ expect(comp.SelectedPoint).toBeNull();
413
+ expect(comp.DetailEntries).toEqual([]);
414
+ expect(comp.ClusterMembers).toEqual([]);
415
+ });
416
+ });
417
+ describe('ToggleClusterMembers', () => {
418
+ it('should toggle the expansion state', () => {
419
+ expect(comp.ClusterMembersExpanded).toBe(true);
420
+ comp.ToggleClusterMembers();
421
+ expect(comp.ClusterMembersExpanded).toBe(false);
422
+ comp.ToggleClusterMembers();
423
+ expect(comp.ClusterMembersExpanded).toBe(true);
424
+ });
425
+ });
426
+ // ================================================================
427
+ // TrackBy functions
428
+ // ================================================================
429
+ describe('TrackBy functions', () => {
430
+ it('TrackPointBy should return VectorKey', () => {
431
+ const point = {
432
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'abc', Metadata: {},
433
+ };
434
+ expect(comp.TrackPointBy(0, point)).toBe('abc');
435
+ });
436
+ it('TrackClusterBy should return cluster Id', () => {
437
+ const cluster = { Id: 5, Label: '', Color: '', MemberCount: 0 };
438
+ expect(comp.TrackClusterBy(0, cluster)).toBe(5);
439
+ });
440
+ it('TrackCentroidBy should return ClusterId', () => {
441
+ expect(comp.TrackCentroidBy(0, { ClusterId: 3 })).toBe(3);
442
+ });
443
+ it('TrackMemberBy should return VectorKey', () => {
444
+ const point = {
445
+ X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'xyz', Metadata: {},
446
+ };
447
+ expect(comp.TrackMemberBy(0, point)).toBe('xyz');
448
+ });
449
+ });
450
+ // ================================================================
451
+ // ResetZoom
452
+ // ================================================================
453
+ describe('ResetZoom', () => {
454
+ it('should reset ViewBox to defaults', () => {
455
+ comp.ViewBox = [100, 200, 500, 300];
456
+ comp.ResetZoom();
457
+ expect(comp.ViewBox).toEqual([0, 0, 1000, 700]);
458
+ });
459
+ });
460
+ // ================================================================
461
+ // ZoomToCluster
462
+ // ================================================================
463
+ describe('ZoomToCluster', () => {
464
+ it('should do nothing for empty cluster', () => {
465
+ comp.Points = makePoints(3, 0);
466
+ const originalViewBox = [...comp.ViewBox];
467
+ comp.ZoomToCluster(99); // no members
468
+ expect(comp.ViewBox).toEqual(originalViewBox);
469
+ });
470
+ it('should zoom into cluster bounds with padding', () => {
471
+ comp.Points = [
472
+ { X: 200, Y: 300, ClusterId: 1, Label: '', VectorKey: 'a', Metadata: {} },
473
+ { X: 400, Y: 500, ClusterId: 1, Label: '', VectorKey: 'b', Metadata: {} },
474
+ { X: 800, Y: 100, ClusterId: 0, Label: '', VectorKey: 'c', Metadata: {} },
475
+ ];
476
+ comp.ZoomToCluster(1);
477
+ // Should zoom to cluster 1 bounds (200-400 X, 300-500 Y) + 80 padding
478
+ expect(comp.ViewBox[0]).toBe(200 - 80); // minX - padding
479
+ expect(comp.ViewBox[1]).toBe(300 - 80); // minY - padding
480
+ expect(comp.ViewBox[2]).toBe(200 + 160); // range + 2*padding
481
+ expect(comp.ViewBox[3]).toBe(200 + 160);
482
+ });
483
+ it('should enforce minimum dimensions', () => {
484
+ comp.Points = [
485
+ { X: 500, Y: 350, ClusterId: 2, Label: '', VectorKey: 'a', Metadata: {} },
486
+ ];
487
+ comp.ZoomToCluster(2);
488
+ // Single point: range=0, so bounds would be tiny, minimum enforced
489
+ expect(comp.ViewBox[2]).toBeGreaterThanOrEqual(200); // min width
490
+ expect(comp.ViewBox[3]).toBeGreaterThanOrEqual(140); // min height
491
+ });
492
+ });
493
+ });
494
+ //# sourceMappingURL=scatter-component-logic.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scatter-component-logic.test.js","sourceRoot":"","sources":["../../src/__tests__/scatter-component-logic.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { ClusterPoint, ClusterInfo, CLUSTER_COLORS, ViewportTransform } from '../lib/clustering.types';\n\n// Mock Angular core since the component uses @Component, @Input, etc.\nconst { mockDetectChanges } = vi.hoisted(() => ({\n mockDetectChanges: vi.fn(),\n}));\n\nvi.mock('@angular/core', () => ({\n Component: () => (target: unknown) => target,\n Input: () => (_target: unknown, _key: string) => {},\n Output: () => (_target: unknown, _key: string) => {},\n ViewChild: () => (_target: unknown, _key: string) => {},\n EventEmitter: class { emit = vi.fn(); subscribe = vi.fn(); },\n ElementRef: class {},\n ChangeDetectorRef: class {},\n AfterViewInit: class {},\n OnDestroy: class {},\n OnChanges: class {},\n SimpleChanges: class {},\n ViewEncapsulation: { None: 0 },\n inject: () => ({ detectChanges: mockDetectChanges }),\n}));\n\nimport { ClusterScatterComponent } from '../lib/cluster-scatter.component';\n\n// ================================================================\n// Helpers\n// ================================================================\n\nfunction makePoints(count: number, clusterId: number = 0): ClusterPoint[] {\n return Array.from({ length: count }, (_, i) => ({\n X: 100 + i * 50,\n Y: 200 + i * 30,\n ClusterId: clusterId,\n Label: `Point ${i}`,\n VectorKey: `vec-${i}`,\n Metadata: { index: i },\n }));\n}\n\nfunction makeClusters(count: number): ClusterInfo[] {\n return Array.from({ length: count }, (_, i) => ({\n Id: i,\n Label: `Cluster ${i + 1}`,\n Color: CLUSTER_COLORS[i % CLUSTER_COLORS.length],\n MemberCount: 5,\n }));\n}\n\nfunction createComponent(): ClusterScatterComponent {\n const comp = new ClusterScatterComponent();\n return comp;\n}\n\n// ================================================================\n// Tests\n// ================================================================\n\ndescribe('ClusterScatterComponent (pure logic)', () => {\n let comp: ClusterScatterComponent;\n\n beforeEach(() => {\n comp = createComponent();\n });\n\n // ================================================================\n // Default property values\n // ================================================================\n\n describe('default property values', () => {\n it('should have correct defaults', () => {\n expect(comp.Points).toEqual([]);\n expect(comp.Clusters).toEqual([]);\n expect(comp.IsLoading).toBe(false);\n expect(comp.DotRadius).toBe(5);\n expect(comp.DotOpacity).toBe(0.75);\n expect(comp.HighlightRadius).toBe(8);\n expect(comp.ShowLegend).toBe(true);\n expect(comp.ShowTooltip).toBe(true);\n expect(comp.TooltipFields).toEqual([]);\n expect(comp.ColorPalette).toEqual([]);\n expect(comp.EnableZoom).toBe(true);\n expect(comp.EnablePan).toBe(true);\n expect(comp.AnimateTransitions).toBe(true);\n expect(comp.MinZoom).toBe(0.5);\n expect(comp.MaxZoom).toBe(10);\n });\n\n it('should have correct default ViewBox', () => {\n expect(comp.ViewBox).toEqual([0, 0, 1000, 700]);\n });\n });\n\n // ================================================================\n // GetViewBoxString\n // ================================================================\n\n describe('GetViewBoxString', () => {\n it('should return space-separated string', () => {\n expect(comp.GetViewBoxString()).toBe('0 0 1000 700');\n });\n\n it('should reflect modified ViewBox', () => {\n comp.ViewBox = [50, 100, 800, 600];\n expect(comp.GetViewBoxString()).toBe('50 100 800 600');\n });\n });\n\n // ================================================================\n // GetPointColor\n // ================================================================\n\n describe('GetPointColor', () => {\n it('should return gray for outlier points (ClusterId < 0)', () => {\n const outlier: ClusterPoint = {\n X: 0, Y: 0, ClusterId: -1, Label: '', VectorKey: 'o', Metadata: {},\n };\n expect(comp.GetPointColor(outlier)).toBe('#64748b');\n });\n\n it('should return cluster color from Clusters array', () => {\n comp.Clusters = makeClusters(3);\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 1, Label: '', VectorKey: 'p', Metadata: {},\n };\n expect(comp.GetPointColor(point)).toBe(CLUSTER_COLORS[1]);\n });\n\n it('should fall back to palette when cluster not found', () => {\n comp.Clusters = []; // no clusters\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 2, Label: '', VectorKey: 'p', Metadata: {},\n };\n expect(comp.GetPointColor(point)).toBe(CLUSTER_COLORS[2]);\n });\n\n it('should use custom ColorPalette when provided', () => {\n comp.ColorPalette = ['#ff0000', '#00ff00', '#0000ff'];\n comp.Clusters = []; // force palette fallback\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 1, Label: '', VectorKey: 'p', Metadata: {},\n };\n expect(comp.GetPointColor(point)).toBe('#00ff00');\n });\n\n it('should wrap palette index for high cluster IDs', () => {\n comp.Clusters = [];\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 10, Label: '', VectorKey: 'p', Metadata: {},\n };\n // 10 % 10 = 0\n expect(comp.GetPointColor(point)).toBe(CLUSTER_COLORS[0]);\n });\n });\n\n // ================================================================\n // GetPointRadius\n // ================================================================\n\n describe('GetPointRadius', () => {\n it('should return DotRadius for non-selected points', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n expect(comp.GetPointRadius(point)).toBe(5);\n });\n\n it('should return HighlightRadius for selected points', () => {\n comp.SelectedPointIds = new Set(['p1']);\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n expect(comp.GetPointRadius(point)).toBe(8);\n });\n\n it('should respect custom DotRadius and HighlightRadius', () => {\n comp.DotRadius = 10;\n comp.HighlightRadius = 15;\n const p1: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'a', Metadata: {},\n };\n const p2: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'b', Metadata: {},\n };\n comp.SelectedPointIds = new Set(['b']);\n\n expect(comp.GetPointRadius(p1)).toBe(10);\n expect(comp.GetPointRadius(p2)).toBe(15);\n });\n });\n\n // ================================================================\n // GetPointOpacity\n // ================================================================\n\n describe('GetPointOpacity', () => {\n it('should return DotOpacity when no point is hovered', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n expect(comp.GetPointOpacity(point)).toBe(0.75);\n });\n\n it('should return full opacity for the hovered point', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n comp.HoveredPoint = point;\n expect(comp.GetPointOpacity(point)).toBe(0.75);\n });\n\n it('should dim non-hovered points when a point is hovered', () => {\n const hovered: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'h', Metadata: {},\n };\n const other: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'o', Metadata: {},\n };\n comp.HoveredPoint = hovered;\n expect(comp.GetPointOpacity(other)).toBe(0.75 * 0.5);\n });\n });\n\n // ================================================================\n // IsHighlighted / IsSelected\n // ================================================================\n\n describe('IsHighlighted', () => {\n it('should return false when nothing is highlighted', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n expect(comp.IsHighlighted(point)).toBe(false);\n });\n\n it('should return true for the highlighted point', () => {\n comp.HighlightedKey = 'p1';\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n expect(comp.IsHighlighted(point)).toBe(true);\n });\n });\n\n describe('IsSelected', () => {\n it('should return false when nothing is selected', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n expect(comp.IsSelected(point)).toBe(false);\n });\n\n it('should return true for selected points', () => {\n comp.SelectedPointIds = new Set(['p1', 'p2']);\n const p1: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p1', Metadata: {},\n };\n const p3: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'p3', Metadata: {},\n };\n expect(comp.IsSelected(p1)).toBe(true);\n expect(comp.IsSelected(p3)).toBe(false);\n });\n });\n\n // ================================================================\n // GetClusterLabel / GetClusterLabelById\n // ================================================================\n\n describe('GetClusterLabel', () => {\n it('should return \"Outlier\" for negative cluster IDs', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: -1, Label: '', VectorKey: 'p', Metadata: {},\n };\n expect(comp.GetClusterLabel(point)).toBe('Outlier');\n });\n\n it('should return cluster label when cluster exists', () => {\n comp.Clusters = [{ Id: 2, Label: 'Tech Companies', Color: '#000', MemberCount: 5 }];\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 2, Label: '', VectorKey: 'p', Metadata: {},\n };\n expect(comp.GetClusterLabel(point)).toBe('Tech Companies');\n });\n\n it('should return fallback label when cluster not found', () => {\n comp.Clusters = [];\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 3, Label: '', VectorKey: 'p', Metadata: {},\n };\n expect(comp.GetClusterLabel(point)).toBe('Cluster 4');\n });\n });\n\n describe('GetClusterLabelById', () => {\n it('should return cluster label for valid ID', () => {\n comp.Clusters = makeClusters(3);\n expect(comp.GetClusterLabelById(0)).toBe('Cluster 1');\n expect(comp.GetClusterLabelById(1)).toBe('Cluster 2');\n });\n\n it('should return empty string for unknown ID', () => {\n comp.Clusters = [];\n expect(comp.GetClusterLabelById(99)).toBe('');\n });\n });\n\n // ================================================================\n // GetClusterColor\n // ================================================================\n\n describe('GetClusterColor', () => {\n it('should return gray for negative cluster IDs', () => {\n expect(comp.GetClusterColor(-1)).toBe('#64748b');\n });\n\n it('should return cluster color when cluster exists', () => {\n comp.Clusters = [{ Id: 0, Label: 'A', Color: '#ff0000', MemberCount: 3 }];\n expect(comp.GetClusterColor(0)).toBe('#ff0000');\n });\n\n it('should fall back to palette for unknown cluster', () => {\n comp.Clusters = [];\n expect(comp.GetClusterColor(2)).toBe(CLUSTER_COLORS[2]);\n });\n });\n\n // ================================================================\n // Viewport Transform\n // ================================================================\n\n describe('GetViewportTransform / SetViewportTransform', () => {\n it('should return default transform', () => {\n const vt = comp.GetViewportTransform();\n expect(vt.TranslateX).toBe(0);\n expect(vt.TranslateY).toBe(0);\n expect(vt.Scale).toBe(1);\n });\n\n it('should reflect modified ViewBox', () => {\n comp.ViewBox = [100, 50, 500, 350];\n const vt = comp.GetViewportTransform();\n expect(vt.TranslateX).toBe(100);\n expect(vt.TranslateY).toBe(50);\n expect(vt.Scale).toBe(1000 / 500); // defaultWidth / ViewBox[2]\n });\n\n it('should restore a saved transform', () => {\n const saved: ViewportTransform = {\n TranslateX: 200,\n TranslateY: 150,\n Scale: 2,\n };\n comp.SetViewportTransform(saved);\n\n expect(comp.ViewBox[0]).toBe(200);\n expect(comp.ViewBox[1]).toBe(150);\n expect(comp.ViewBox[2]).toBe(500); // 1000 / 2\n expect(comp.ViewBox[3]).toBe(350); // 700 / 2\n });\n\n it('should round-trip correctly', () => {\n const original: ViewportTransform = {\n TranslateX: 75,\n TranslateY: -30,\n Scale: 1.5,\n };\n comp.SetViewportTransform(original);\n const restored = comp.GetViewportTransform();\n\n expect(restored.TranslateX).toBeCloseTo(75);\n expect(restored.TranslateY).toBeCloseTo(-30);\n expect(restored.Scale).toBeCloseTo(1.5);\n });\n });\n\n // ================================================================\n // GetVisiblePoints\n // ================================================================\n\n describe('GetVisiblePoints', () => {\n it('should return all points within default viewport', () => {\n comp.Points = makePoints(3);\n // Default ViewBox: [0, 0, 1000, 700]\n // Points at (100,200), (150,230), (200,260) - all inside\n const visible = comp.GetVisiblePoints();\n expect(visible).toHaveLength(3);\n });\n\n it('should exclude points outside viewport', () => {\n comp.Points = [\n { X: 500, Y: 350, ClusterId: 0, Label: 'Inside', VectorKey: 'a', Metadata: {} },\n { X: 1500, Y: 350, ClusterId: 0, Label: 'Outside', VectorKey: 'b', Metadata: {} },\n { X: -50, Y: 350, ClusterId: 0, Label: 'Outside2', VectorKey: 'c', Metadata: {} },\n ];\n const visible = comp.GetVisiblePoints();\n expect(visible).toHaveLength(1);\n expect(visible[0].VectorKey).toBe('a');\n });\n\n it('should respect modified ViewBox', () => {\n comp.Points = [\n { X: 500, Y: 350, ClusterId: 0, Label: '', VectorKey: 'a', Metadata: {} },\n { X: 200, Y: 200, ClusterId: 0, Label: '', VectorKey: 'b', Metadata: {} },\n ];\n comp.ViewBox = [400, 300, 200, 100]; // tight box around point 'a'\n const visible = comp.GetVisiblePoints();\n expect(visible).toHaveLength(1);\n expect(visible[0].VectorKey).toBe('a');\n });\n });\n\n // ================================================================\n // SelectPoints / ClearSelection / HighlightCluster\n // ================================================================\n\n describe('SelectPoints', () => {\n it('should add to existing selection', () => {\n comp.SelectedPointIds = new Set(['existing']);\n comp.SelectPoints(['new1', 'new2']);\n expect(comp.SelectedPointIds.has('existing')).toBe(true);\n expect(comp.SelectedPointIds.has('new1')).toBe(true);\n expect(comp.SelectedPointIds.has('new2')).toBe(true);\n });\n });\n\n describe('ClearSelection', () => {\n it('should clear all selections and highlighted key', () => {\n comp.SelectedPointIds = new Set(['a', 'b']);\n comp.HighlightedKey = 'a';\n comp.ClearSelection();\n expect(comp.SelectedPointIds.size).toBe(0);\n expect(comp.HighlightedKey).toBeNull();\n });\n });\n\n describe('HighlightCluster', () => {\n it('should select all points in the specified cluster', () => {\n comp.Points = [\n { X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'a', Metadata: {} },\n { X: 0, Y: 0, ClusterId: 1, Label: '', VectorKey: 'b', Metadata: {} },\n { X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'c', Metadata: {} },\n ];\n comp.HighlightCluster(0);\n expect(comp.SelectedPointIds.has('a')).toBe(true);\n expect(comp.SelectedPointIds.has('c')).toBe(true);\n expect(comp.SelectedPointIds.has('b')).toBe(false);\n });\n });\n\n // ================================================================\n // Detail Panel\n // ================================================================\n\n describe('CloseDetailPanel', () => {\n it('should reset detail panel state', () => {\n comp.ShowDetailPanel = true;\n comp.SelectedPoint = makePoints(1)[0];\n comp.DetailEntries = [{ Key: 'k', Value: 'v' }];\n comp.ClusterMembers = makePoints(2);\n\n comp.CloseDetailPanel();\n\n expect(comp.ShowDetailPanel).toBe(false);\n expect(comp.SelectedPoint).toBeNull();\n expect(comp.DetailEntries).toEqual([]);\n expect(comp.ClusterMembers).toEqual([]);\n });\n });\n\n describe('ToggleClusterMembers', () => {\n it('should toggle the expansion state', () => {\n expect(comp.ClusterMembersExpanded).toBe(true);\n comp.ToggleClusterMembers();\n expect(comp.ClusterMembersExpanded).toBe(false);\n comp.ToggleClusterMembers();\n expect(comp.ClusterMembersExpanded).toBe(true);\n });\n });\n\n // ================================================================\n // TrackBy functions\n // ================================================================\n\n describe('TrackBy functions', () => {\n it('TrackPointBy should return VectorKey', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'abc', Metadata: {},\n };\n expect(comp.TrackPointBy(0, point)).toBe('abc');\n });\n\n it('TrackClusterBy should return cluster Id', () => {\n const cluster: ClusterInfo = { Id: 5, Label: '', Color: '', MemberCount: 0 };\n expect(comp.TrackClusterBy(0, cluster)).toBe(5);\n });\n\n it('TrackCentroidBy should return ClusterId', () => {\n expect(comp.TrackCentroidBy(0, { ClusterId: 3 })).toBe(3);\n });\n\n it('TrackMemberBy should return VectorKey', () => {\n const point: ClusterPoint = {\n X: 0, Y: 0, ClusterId: 0, Label: '', VectorKey: 'xyz', Metadata: {},\n };\n expect(comp.TrackMemberBy(0, point)).toBe('xyz');\n });\n });\n\n // ================================================================\n // ResetZoom\n // ================================================================\n\n describe('ResetZoom', () => {\n it('should reset ViewBox to defaults', () => {\n comp.ViewBox = [100, 200, 500, 300];\n comp.ResetZoom();\n expect(comp.ViewBox).toEqual([0, 0, 1000, 700]);\n });\n });\n\n // ================================================================\n // ZoomToCluster\n // ================================================================\n\n describe('ZoomToCluster', () => {\n it('should do nothing for empty cluster', () => {\n comp.Points = makePoints(3, 0);\n const originalViewBox = [...comp.ViewBox];\n comp.ZoomToCluster(99); // no members\n expect(comp.ViewBox).toEqual(originalViewBox);\n });\n\n it('should zoom into cluster bounds with padding', () => {\n comp.Points = [\n { X: 200, Y: 300, ClusterId: 1, Label: '', VectorKey: 'a', Metadata: {} },\n { X: 400, Y: 500, ClusterId: 1, Label: '', VectorKey: 'b', Metadata: {} },\n { X: 800, Y: 100, ClusterId: 0, Label: '', VectorKey: 'c', Metadata: {} },\n ];\n comp.ZoomToCluster(1);\n\n // Should zoom to cluster 1 bounds (200-400 X, 300-500 Y) + 80 padding\n expect(comp.ViewBox[0]).toBe(200 - 80); // minX - padding\n expect(comp.ViewBox[1]).toBe(300 - 80); // minY - padding\n expect(comp.ViewBox[2]).toBe(200 + 160); // range + 2*padding\n expect(comp.ViewBox[3]).toBe(200 + 160);\n });\n\n it('should enforce minimum dimensions', () => {\n comp.Points = [\n { X: 500, Y: 350, ClusterId: 2, Label: '', VectorKey: 'a', Metadata: {} },\n ];\n comp.ZoomToCluster(2);\n\n // Single point: range=0, so bounds would be tiny, minimum enforced\n expect(comp.ViewBox[2]).toBeGreaterThanOrEqual(200); // min width\n expect(comp.ViewBox[3]).toBeGreaterThanOrEqual(140); // min height\n });\n });\n});\n"]}