@graphty/layout 1.2.1 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.2.2](https://github.com/graphty-org/layout/compare/v1.2.1...v1.2.2) (2025-07-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * numpy linspace error. improve test coverage ([2e820f1](https://github.com/graphty-org/layout/commit/2e820f181bd7242ef251c1833b10533699485209))
7
+
1
8
  ## [1.2.1](https://github.com/graphty-org/layout/compare/v1.2.0...v1.2.1) (2025-07-13)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphty/layout",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "graph layout algorithms based on networkx",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -25,6 +25,9 @@ export const np = {
25
25
  },
26
26
 
27
27
  linspace: function (start: number, stop: number, num: number): number[] {
28
+ if (num === 1) {
29
+ return [start];
30
+ }
28
31
  const step = (stop - start) / (num - 1);
29
32
  return Array.from({ length: num }, (_, i) => start + i * step);
30
33
  },
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Test utilities and helper functions
3
+ */
4
+
5
+ import { Graph, Node, Edge } from '../src/types';
6
+
7
+ /**
8
+ * Create a simple test graph for testing
9
+ */
10
+ export function createTestGraph(nodes: Node[], edges: Edge[]): Graph {
11
+ return {
12
+ nodes: () => nodes,
13
+ edges: () => edges,
14
+ adjacency: function() {
15
+ const adj = new Map<Node, Map<Node, number>>();
16
+
17
+ // Initialize adjacency for all nodes
18
+ for (const node of nodes) {
19
+ adj.set(node, new Map());
20
+ }
21
+
22
+ // Add edges
23
+ for (const [source, target] of edges) {
24
+ if (!adj.has(source)) adj.set(source, new Map());
25
+ if (!adj.has(target)) adj.set(target, new Map());
26
+
27
+ adj.get(source)!.set(target, 1);
28
+ adj.get(target)!.set(source, 1);
29
+ }
30
+
31
+ return adj;
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tests for graph utility functions
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ getNodesFromGraph,
8
+ getEdgesFromGraph,
9
+ getNodeDegree,
10
+ getNeighbors
11
+ } from '../src/utils/graph';
12
+ import { createTestGraph } from './test-utils';
13
+
14
+ describe('Graph Utils', () => {
15
+ describe('getNodesFromGraph', () => {
16
+ it('should return array as-is when given array of nodes', () => {
17
+ const nodes = [1, 2, 3, 'a', 'b'];
18
+ const result = getNodesFromGraph(nodes);
19
+ expect(result).toEqual(nodes);
20
+ expect(result).toBe(nodes); // same reference
21
+ });
22
+
23
+ it('should call nodes() method when given graph object', () => {
24
+ const graph = createTestGraph([1, 2, 3], [[1, 2], [2, 3]]);
25
+ const result = getNodesFromGraph(graph);
26
+ expect(result).toEqual([1, 2, 3]);
27
+ });
28
+
29
+ it('should handle empty array', () => {
30
+ const result = getNodesFromGraph([]);
31
+ expect(result).toEqual([]);
32
+ });
33
+
34
+ it('should handle empty graph', () => {
35
+ const graph = createTestGraph([], []);
36
+ const result = getNodesFromGraph(graph);
37
+ expect(result).toEqual([]);
38
+ });
39
+ });
40
+
41
+ describe('getEdgesFromGraph', () => {
42
+ it('should return empty array when given array of nodes', () => {
43
+ const nodes = [1, 2, 3, 'a', 'b'];
44
+ const result = getEdgesFromGraph(nodes);
45
+ expect(result).toEqual([]);
46
+ });
47
+
48
+ it('should call edges() method when given graph object', () => {
49
+ const edges = [[1, 2], [2, 3], [3, 1]];
50
+ const graph = createTestGraph([1, 2, 3], edges);
51
+ const result = getEdgesFromGraph(graph);
52
+ expect(result).toEqual(edges);
53
+ });
54
+
55
+ it('should handle empty array', () => {
56
+ const result = getEdgesFromGraph([]);
57
+ expect(result).toEqual([]);
58
+ });
59
+
60
+ it('should handle graph with no edges', () => {
61
+ const graph = createTestGraph([1, 2, 3], []);
62
+ const result = getEdgesFromGraph(graph);
63
+ expect(result).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe('getNodeDegree', () => {
68
+ it('should return 0 for graph without edges method', () => {
69
+ const graph = { nodes: () => [1, 2, 3] } as any;
70
+ const result = getNodeDegree(graph, 1);
71
+ expect(result).toBe(0);
72
+ });
73
+
74
+ it('should calculate degree correctly for connected node', () => {
75
+ const graph = createTestGraph([1, 2, 3, 4], [[1, 2], [1, 3], [1, 4], [2, 3]]);
76
+ expect(getNodeDegree(graph, 1)).toBe(3); // connected to 2, 3, 4
77
+ expect(getNodeDegree(graph, 2)).toBe(2); // connected to 1, 3
78
+ expect(getNodeDegree(graph, 3)).toBe(2); // connected to 1, 2
79
+ expect(getNodeDegree(graph, 4)).toBe(1); // connected to 1
80
+ });
81
+
82
+ it('should return 0 for isolated node', () => {
83
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
84
+ expect(getNodeDegree(graph, 3)).toBe(0);
85
+ });
86
+
87
+ it('should handle self-loops correctly', () => {
88
+ const graph = createTestGraph([1, 2], [[1, 1], [1, 2]]);
89
+ expect(getNodeDegree(graph, 1)).toBe(2); // self-loop + edge to 2
90
+ });
91
+
92
+ it('should handle node not in graph', () => {
93
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
94
+ expect(getNodeDegree(graph, 999)).toBe(0);
95
+ });
96
+
97
+ it('should handle empty graph', () => {
98
+ const graph = createTestGraph([], []);
99
+ expect(getNodeDegree(graph, 1)).toBe(0);
100
+ });
101
+ });
102
+
103
+ describe('getNeighbors', () => {
104
+ it('should return empty array for graph without edges method', () => {
105
+ const graph = { nodes: () => [1, 2, 3] } as any;
106
+ const result = getNeighbors(graph, 1);
107
+ expect(result).toEqual([]);
108
+ });
109
+
110
+ it('should return neighbors correctly', () => {
111
+ const graph = createTestGraph([1, 2, 3, 4], [[1, 2], [1, 3], [2, 4], [3, 4]]);
112
+
113
+ const neighbors1 = getNeighbors(graph, 1);
114
+ expect(neighbors1.sort()).toEqual([2, 3]);
115
+
116
+ const neighbors2 = getNeighbors(graph, 2);
117
+ expect(neighbors2.sort()).toEqual([1, 4]);
118
+
119
+ const neighbors4 = getNeighbors(graph, 4);
120
+ expect(neighbors4.sort()).toEqual([2, 3]);
121
+ });
122
+
123
+ it('should return empty array for isolated node', () => {
124
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
125
+ expect(getNeighbors(graph, 3)).toEqual([]);
126
+ });
127
+
128
+ it('should handle self-loops correctly', () => {
129
+ const graph = createTestGraph([1, 2], [[1, 1], [1, 2]]);
130
+ const neighbors = getNeighbors(graph, 1);
131
+ expect(neighbors.sort()).toEqual([1, 2]); // includes self and neighbor
132
+ });
133
+
134
+ it('should not duplicate neighbors in undirected edges', () => {
135
+ const graph = createTestGraph([1, 2, 3], [[1, 2], [2, 1], [1, 3]]); // bidirectional edge
136
+ const neighbors = getNeighbors(graph, 1);
137
+ expect(neighbors.sort()).toEqual([2, 3]); // should not duplicate node 2
138
+ });
139
+
140
+ it('should handle node not in graph', () => {
141
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
142
+ expect(getNeighbors(graph, 999)).toEqual([]);
143
+ });
144
+
145
+ it('should handle empty graph', () => {
146
+ const graph = createTestGraph([], []);
147
+ expect(getNeighbors(graph, 1)).toEqual([]);
148
+ });
149
+
150
+ it('should work with string node IDs', () => {
151
+ const graph = createTestGraph(['a', 'b', 'c'], [['a', 'b'], ['b', 'c']]);
152
+ expect(getNeighbors(graph, 'b').sort()).toEqual(['a', 'c']);
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for utils index exports
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+
7
+ describe('Utils Index', () => {
8
+ it('should export all utility functions from numpy', async () => {
9
+ const { np } = await import('../src/utils/index');
10
+ expect(np).toBeDefined();
11
+ expect(typeof np.zeros).toBe('function');
12
+ expect(typeof np.ones).toBe('function');
13
+ expect(typeof np.linspace).toBe('function');
14
+ expect(typeof np.array).toBe('function');
15
+ expect(typeof np.repeat).toBe('function');
16
+ expect(typeof np.mean).toBe('function');
17
+ expect(typeof np.add).toBe('function');
18
+ expect(typeof np.subtract).toBe('function');
19
+ expect(typeof np.max).toBe('function');
20
+ expect(typeof np.min).toBe('function');
21
+ expect(typeof np.norm).toBe('function');
22
+ });
23
+
24
+ it('should export RandomNumberGenerator class', async () => {
25
+ const { RandomNumberGenerator } = await import('../src/utils/index');
26
+ expect(RandomNumberGenerator).toBeDefined();
27
+ expect(typeof RandomNumberGenerator).toBe('function');
28
+
29
+ const rng = new RandomNumberGenerator(123);
30
+ expect(typeof rng.rand).toBe('function');
31
+ });
32
+
33
+ it('should export graph utility functions', async () => {
34
+ const { getNodesFromGraph, getEdgesFromGraph, getNodeDegree, getNeighbors } = await import('../src/utils/index');
35
+ expect(typeof getNodesFromGraph).toBe('function');
36
+ expect(typeof getEdgesFromGraph).toBe('function');
37
+ expect(typeof getNodeDegree).toBe('function');
38
+ expect(typeof getNeighbors).toBe('function');
39
+ });
40
+
41
+ it('should export parameter processing functions', async () => {
42
+ const { _processParams } = await import('../src/utils/index');
43
+ expect(typeof _processParams).toBe('function');
44
+ });
45
+
46
+ it('should export rescale functions', async () => {
47
+ const { rescaleLayout, rescaleLayoutDict } = await import('../src/utils/index');
48
+ expect(typeof rescaleLayout).toBe('function');
49
+ expect(typeof rescaleLayoutDict).toBe('function');
50
+ });
51
+
52
+ it('should allow importing everything with wildcard', async () => {
53
+ const utils = await import('../src/utils/index');
54
+
55
+ // Check that key exports are available
56
+ expect(utils.np).toBeDefined();
57
+ expect(utils.RandomNumberGenerator).toBeDefined();
58
+ expect(utils.getNodesFromGraph).toBeDefined();
59
+ expect(utils._processParams).toBeDefined();
60
+ expect(utils.rescaleLayout).toBeDefined();
61
+ });
62
+
63
+ it('should work with destructured imports', async () => {
64
+ // Test that the typical usage patterns work
65
+ const { np, RandomNumberGenerator, getNodesFromGraph } = await import('../src/utils/index');
66
+
67
+ // Quick functional test
68
+ expect(np.zeros(3)).toEqual([0, 0, 0]);
69
+
70
+ const rng = new RandomNumberGenerator(42);
71
+ const value = rng.rand() as number;
72
+ expect(typeof value).toBe('number');
73
+
74
+ const nodes = getNodesFromGraph([1, 2, 3]);
75
+ expect(nodes).toEqual([1, 2, 3]);
76
+ });
77
+ });
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Tests for NumPy-like utility functions
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { np } from '../src/utils/numpy';
7
+
8
+ describe('NumPy Utils', () => {
9
+ describe('zeros', () => {
10
+ it('should create 1D array of zeros', () => {
11
+ expect(np.zeros(3)).toEqual([0, 0, 0]);
12
+ expect(np.zeros(0)).toEqual([]);
13
+ expect(np.zeros(1)).toEqual([0]);
14
+ });
15
+
16
+ it('should create 1D array from single-element array shape', () => {
17
+ expect(np.zeros([3])).toEqual([0, 0, 0]);
18
+ expect(np.zeros([0])).toEqual([]);
19
+ });
20
+
21
+ it('should create 2D array of zeros', () => {
22
+ expect(np.zeros([2, 3])).toEqual([[0, 0, 0], [0, 0, 0]]);
23
+ expect(np.zeros([3, 2])).toEqual([[0, 0], [0, 0], [0, 0]]);
24
+ expect(np.zeros([1, 1])).toEqual([[0]]);
25
+ });
26
+
27
+ it('should create 3D array of zeros', () => {
28
+ const result = np.zeros([2, 2, 2]) as number[][][];
29
+ expect(result).toEqual([
30
+ [[0, 0], [0, 0]],
31
+ [[0, 0], [0, 0]]
32
+ ]);
33
+ });
34
+ });
35
+
36
+ describe('ones', () => {
37
+ it('should create 1D array of ones', () => {
38
+ expect(np.ones(3)).toEqual([1, 1, 1]);
39
+ expect(np.ones(0)).toEqual([]);
40
+ expect(np.ones(1)).toEqual([1]);
41
+ });
42
+
43
+ it('should create 1D array from single-element array shape', () => {
44
+ expect(np.ones([3])).toEqual([1, 1, 1]);
45
+ expect(np.ones([0])).toEqual([]);
46
+ });
47
+
48
+ it('should create 2D array of ones', () => {
49
+ expect(np.ones([2, 3])).toEqual([[1, 1, 1], [1, 1, 1]]);
50
+ expect(np.ones([3, 2])).toEqual([[1, 1], [1, 1], [1, 1]]);
51
+ });
52
+
53
+ it('should create 3D array of ones', () => {
54
+ const result = np.ones([2, 1, 2]) as number[][][];
55
+ expect(result).toEqual([
56
+ [[1, 1]],
57
+ [[1, 1]]
58
+ ]);
59
+ });
60
+ });
61
+
62
+ describe('linspace', () => {
63
+ it('should create linearly spaced array', () => {
64
+ const result = np.linspace(0, 1, 5);
65
+ expect(result).toHaveLength(5);
66
+ expect(result[0]).toBe(0);
67
+ expect(result[4]).toBe(1);
68
+ expect(result[2]).toBeCloseTo(0.5);
69
+ });
70
+
71
+ it('should handle single point', () => {
72
+ expect(np.linspace(5, 5, 1)).toEqual([5]);
73
+ });
74
+
75
+ it('should handle reverse range', () => {
76
+ const result = np.linspace(10, 0, 3);
77
+ expect(result).toEqual([10, 5, 0]);
78
+ });
79
+
80
+ it('should handle negative numbers', () => {
81
+ const result = np.linspace(-1, 1, 3);
82
+ expect(result).toEqual([-1, 0, 1]);
83
+ });
84
+ });
85
+
86
+ describe('array', () => {
87
+ it('should copy arrays', () => {
88
+ const input = [1, 2, 3];
89
+ const result = np.array(input);
90
+ expect(result).toEqual(input);
91
+ expect(result).not.toBe(input); // different reference
92
+ });
93
+
94
+ it('should wrap non-arrays', () => {
95
+ expect(np.array(5)).toEqual([5]);
96
+ expect(np.array('hello')).toEqual(['hello']);
97
+ expect(np.array(null)).toEqual([null]);
98
+ });
99
+
100
+ it('should handle nested arrays', () => {
101
+ const input = [[1, 2], [3, 4]];
102
+ const result = np.array(input);
103
+ expect(result).toEqual(input);
104
+ expect(result).not.toBe(input);
105
+ });
106
+ });
107
+
108
+ describe('repeat', () => {
109
+ it('should repeat single values', () => {
110
+ expect(np.repeat(5, 3)).toEqual([5, 5, 5]);
111
+ expect(np.repeat('a', 2)).toEqual(['a', 'a']);
112
+ });
113
+
114
+ it('should repeat arrays', () => {
115
+ expect(np.repeat([1, 2], 2)).toEqual([1, 2, 1, 2]);
116
+ expect(np.repeat([1, 2, 3], 3)).toEqual([1, 2, 3, 1, 2, 3, 1, 2, 3]);
117
+ });
118
+
119
+ it('should handle zero repeats', () => {
120
+ expect(np.repeat(5, 0)).toEqual([]);
121
+ expect(np.repeat([1, 2], 0)).toEqual([]);
122
+ });
123
+
124
+ it('should handle single repeat', () => {
125
+ expect(np.repeat([1, 2, 3], 1)).toEqual([1, 2, 3]);
126
+ });
127
+ });
128
+
129
+ describe('mean', () => {
130
+ it('should calculate mean of 1D array', () => {
131
+ expect(np.mean([1, 2, 3, 4, 5])).toBe(3);
132
+ expect(np.mean([2, 4, 6])).toBe(4);
133
+ expect(np.mean([10])).toBe(10);
134
+ });
135
+
136
+ it('should calculate mean of 2D array (flatten)', () => {
137
+ expect(np.mean([[1, 2], [3, 4]])).toBe(2.5);
138
+ expect(np.mean([[1, 1], [1, 1]])).toBe(1);
139
+ });
140
+
141
+ it('should calculate mean along axis 0 (columns)', () => {
142
+ const result = np.mean([[1, 2, 3], [4, 5, 6]], 0) as number[];
143
+ expect(result).toEqual([2.5, 3.5, 4.5]);
144
+ });
145
+
146
+ it('should calculate mean along axis 1 (rows)', () => {
147
+ const result = np.mean([[1, 2, 3], [4, 5, 6]], 1) as number[];
148
+ expect(result).toEqual([2, 5]);
149
+ });
150
+
151
+ it('should handle empty arrays', () => {
152
+ expect(np.mean([])).toBeNaN();
153
+ });
154
+
155
+ it('should handle nested empty arrays', () => {
156
+ expect(np.mean([[], []])).toBeNaN();
157
+ });
158
+ });
159
+
160
+ describe('add', () => {
161
+ it('should add two numbers', () => {
162
+ expect(np.add(5, 3)).toBe(8);
163
+ expect(np.add(-2, 7)).toBe(5);
164
+ });
165
+
166
+ it('should add number to array', () => {
167
+ expect(np.add(2, [1, 2, 3])).toEqual([3, 4, 5]);
168
+ expect(np.add([1, 2, 3], 10)).toEqual([11, 12, 13]);
169
+ });
170
+
171
+ it('should add two arrays element-wise', () => {
172
+ expect(np.add([1, 2, 3], [4, 5, 6])).toEqual([5, 7, 9]);
173
+ expect(np.add([10, 20], [1, 2])).toEqual([11, 22]);
174
+ });
175
+
176
+ it('should handle negative numbers', () => {
177
+ expect(np.add([-1, -2], [1, 2])).toEqual([0, 0]);
178
+ expect(np.add(5, [-3, -7])).toEqual([2, -2]);
179
+ });
180
+ });
181
+
182
+ describe('subtract', () => {
183
+ it('should subtract two numbers', () => {
184
+ expect(np.subtract(8, 3)).toBe(5);
185
+ expect(np.subtract(-2, 7)).toBe(-9);
186
+ });
187
+
188
+ it('should subtract number from array', () => {
189
+ expect(np.subtract(10, [1, 2, 3])).toEqual([9, 8, 7]);
190
+ expect(np.subtract([5, 6, 7], 2)).toEqual([3, 4, 5]);
191
+ });
192
+
193
+ it('should subtract two arrays element-wise', () => {
194
+ expect(np.subtract([5, 7, 9], [1, 2, 3])).toEqual([4, 5, 6]);
195
+ expect(np.subtract([10, 20], [3, 5])).toEqual([7, 15]);
196
+ });
197
+
198
+ it('should handle negative results', () => {
199
+ expect(np.subtract([1, 2], [5, 7])).toEqual([-4, -5]);
200
+ });
201
+ });
202
+
203
+ describe('max', () => {
204
+ it('should return single number as-is', () => {
205
+ expect(np.max(42)).toBe(42);
206
+ expect(np.max(-5)).toBe(-5);
207
+ });
208
+
209
+ it('should find max in 1D array', () => {
210
+ expect(np.max([1, 5, 3, 9, 2])).toBe(9);
211
+ expect(np.max([-10, -5, -20])).toBe(-5);
212
+ expect(np.max([7])).toBe(7);
213
+ });
214
+
215
+ it('should find max in nested arrays', () => {
216
+ expect(np.max([[1, 2], [3, 4]])).toBe(4);
217
+ expect(np.max([[10, 5], [2, 8], [1, 12]])).toBe(12);
218
+ });
219
+
220
+ it('should handle deeply nested arrays', () => {
221
+ expect(np.max([[[1, 2]], [[3, 4]]])).toBe(4);
222
+ });
223
+ });
224
+
225
+ describe('min', () => {
226
+ it('should return single number as-is', () => {
227
+ expect(np.min(42)).toBe(42);
228
+ expect(np.min(-5)).toBe(-5);
229
+ });
230
+
231
+ it('should find min in 1D array', () => {
232
+ expect(np.min([1, 5, 3, 9, 2])).toBe(1);
233
+ expect(np.min([-10, -5, -20])).toBe(-20);
234
+ expect(np.min([7])).toBe(7);
235
+ });
236
+
237
+ it('should find min in nested arrays', () => {
238
+ expect(np.min([[1, 2], [3, 4]])).toBe(1);
239
+ expect(np.min([[10, 5], [2, 8], [1, 12]])).toBe(1);
240
+ });
241
+
242
+ it('should handle deeply nested arrays', () => {
243
+ expect(np.min([[[1, 2]], [[3, 4]]])).toBe(1);
244
+ });
245
+ });
246
+
247
+ describe('norm', () => {
248
+ it('should calculate Euclidean norm', () => {
249
+ expect(np.norm([3, 4])).toBe(5); // 3-4-5 triangle
250
+ expect(np.norm([1, 0])).toBe(1);
251
+ expect(np.norm([0, 0])).toBe(0);
252
+ });
253
+
254
+ it('should handle negative values', () => {
255
+ expect(np.norm([-3, 4])).toBe(5);
256
+ expect(np.norm([-3, -4])).toBe(5);
257
+ });
258
+
259
+ it('should handle single element', () => {
260
+ expect(np.norm([5])).toBe(5);
261
+ expect(np.norm([-7])).toBe(7);
262
+ });
263
+
264
+ it('should handle higher dimensions', () => {
265
+ expect(np.norm([1, 2, 2])).toBeCloseTo(3); // sqrt(1 + 4 + 4) = 3
266
+ });
267
+
268
+ it('should handle empty array', () => {
269
+ expect(np.norm([])).toBe(0);
270
+ });
271
+ });
272
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Tests for parameter processing utilities
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { _processParams } from '../src/utils/params';
7
+ import { createTestGraph } from './test-utils';
8
+
9
+ describe('Parameter Utils', () => {
10
+ describe('_processParams', () => {
11
+ it('should use default center when null', () => {
12
+ const graph = createTestGraph([1, 2], [[1, 2]]);
13
+ const result = _processParams(graph, null, 2);
14
+
15
+ expect(result.G).toBe(graph);
16
+ expect(result.center).toEqual([0, 0]);
17
+ });
18
+
19
+ it('should use default center when undefined', () => {
20
+ const nodes = [1, 2, 3];
21
+ const result = _processParams(nodes, null, 3);
22
+
23
+ expect(result.G).toBe(nodes);
24
+ expect(result.center).toEqual([0, 0, 0]);
25
+ });
26
+
27
+ it('should preserve provided center', () => {
28
+ const graph = createTestGraph([1, 2], [[1, 2]]);
29
+ const center = [5, 10];
30
+ const result = _processParams(graph, center, 2);
31
+
32
+ expect(result.G).toBe(graph);
33
+ expect(result.center).toBe(center);
34
+ });
35
+
36
+ it('should work with array of nodes', () => {
37
+ const nodes = ['a', 'b', 'c'];
38
+ const center = [1, 2, 3];
39
+ const result = _processParams(nodes, center, 3);
40
+
41
+ expect(result.G).toBe(nodes);
42
+ expect(result.center).toBe(center);
43
+ });
44
+
45
+ it('should throw error when center length does not match dimension', () => {
46
+ const graph = createTestGraph([1, 2], [[1, 2]]);
47
+
48
+ expect(() => {
49
+ _processParams(graph, [1, 2], 3); // center has 2 elements, dim is 3
50
+ }).toThrow('length of center coordinates must match dimension of layout');
51
+ });
52
+
53
+ it('should throw error when center is too long', () => {
54
+ const nodes = [1, 2, 3];
55
+
56
+ expect(() => {
57
+ _processParams(nodes, [1, 2, 3, 4], 2); // center has 4 elements, dim is 2
58
+ }).toThrow('length of center coordinates must match dimension of layout');
59
+ });
60
+
61
+ it('should throw error when center is too short', () => {
62
+ const graph = createTestGraph([1], []);
63
+
64
+ expect(() => {
65
+ _processParams(graph, [1], 3); // center has 1 element, dim is 3
66
+ }).toThrow('length of center coordinates must match dimension of layout');
67
+ });
68
+
69
+ it('should work with 1D layouts', () => {
70
+ const graph = createTestGraph([1, 2], [[1, 2]]);
71
+ const result = _processParams(graph, [5], 1);
72
+
73
+ expect(result.center).toEqual([5]);
74
+ });
75
+
76
+ it('should work with high-dimensional layouts', () => {
77
+ const nodes = [1, 2, 3];
78
+ const center = [1, 2, 3, 4, 5];
79
+ const result = _processParams(nodes, center, 5);
80
+
81
+ expect(result.center).toBe(center);
82
+ });
83
+
84
+ it('should handle empty center array for 0D (edge case)', () => {
85
+ const graph = createTestGraph([1], []);
86
+ const result = _processParams(graph, [], 0);
87
+
88
+ expect(result.center).toEqual([]);
89
+ });
90
+
91
+ it('should create correct default center for various dimensions', () => {
92
+ const graph = createTestGraph([1], []);
93
+
94
+ expect(_processParams(graph, null, 1).center).toEqual([0]);
95
+ expect(_processParams(graph, null, 4).center).toEqual([0, 0, 0, 0]);
96
+ expect(_processParams(graph, null, 0).center).toEqual([]);
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Tests for random number generator utilities
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { RandomNumberGenerator } from '../src/utils/random';
7
+
8
+ describe('Random Utils', () => {
9
+ describe('RandomNumberGenerator', () => {
10
+ describe('constructor', () => {
11
+ it('should use provided seed', () => {
12
+ const rng = new RandomNumberGenerator(12345);
13
+ expect(rng['seed']).toBe(12345);
14
+ });
15
+
16
+ it('should generate random seed when none provided', () => {
17
+ const rng1 = new RandomNumberGenerator();
18
+ const rng2 = new RandomNumberGenerator();
19
+ // Seeds should be different (very high probability)
20
+ expect(rng1['seed']).not.toBe(rng2['seed']);
21
+ });
22
+
23
+ it('should initialize state properly', () => {
24
+ const rng = new RandomNumberGenerator(42);
25
+ expect(rng['_state']).toBe(42);
26
+ });
27
+
28
+ it('should handle large seeds', () => {
29
+ const largeSeed = 999999999;
30
+ const rng = new RandomNumberGenerator(largeSeed);
31
+ expect(rng['seed']).toBe(largeSeed);
32
+ });
33
+ });
34
+
35
+ describe('_next', () => {
36
+ it('should generate values between 0 and 1', () => {
37
+ const rng = new RandomNumberGenerator(123);
38
+ for (let i = 0; i < 100; i++) {
39
+ const value = rng['_next']();
40
+ expect(value).toBeGreaterThanOrEqual(0);
41
+ expect(value).toBeLessThan(1);
42
+ }
43
+ });
44
+
45
+ it('should be deterministic for same seed', () => {
46
+ const rng1 = new RandomNumberGenerator(456);
47
+ const rng2 = new RandomNumberGenerator(456);
48
+
49
+ const sequence1 = Array.from({ length: 10 }, () => rng1['_next']());
50
+ const sequence2 = Array.from({ length: 10 }, () => rng2['_next']());
51
+
52
+ expect(sequence1).toEqual(sequence2);
53
+ });
54
+
55
+ it('should produce different sequences for different seeds', () => {
56
+ const rng1 = new RandomNumberGenerator(111);
57
+ const rng2 = new RandomNumberGenerator(222);
58
+
59
+ const sequence1 = Array.from({ length: 10 }, () => rng1['_next']());
60
+ const sequence2 = Array.from({ length: 10 }, () => rng2['_next']());
61
+
62
+ expect(sequence1).not.toEqual(sequence2);
63
+ });
64
+ });
65
+
66
+ describe('rand', () => {
67
+ it('should return single number when no shape provided', () => {
68
+ const rng = new RandomNumberGenerator(789);
69
+ const value = rng.rand() as number;
70
+ expect(typeof value).toBe('number');
71
+ expect(value).toBeGreaterThanOrEqual(0);
72
+ expect(value).toBeLessThan(1);
73
+ });
74
+
75
+ it('should return single number when null shape provided', () => {
76
+ const rng = new RandomNumberGenerator(789);
77
+ const value = rng.rand(null) as number;
78
+ expect(typeof value).toBe('number');
79
+ expect(value).toBeGreaterThanOrEqual(0);
80
+ expect(value).toBeLessThan(1);
81
+ });
82
+
83
+ it('should return 1D array when given number shape', () => {
84
+ const rng = new RandomNumberGenerator(101112);
85
+ const values = rng.rand(5) as number[];
86
+ expect(Array.isArray(values)).toBe(true);
87
+ expect(values).toHaveLength(5);
88
+ values.forEach(v => {
89
+ expect(v).toBeGreaterThanOrEqual(0);
90
+ expect(v).toBeLessThan(1);
91
+ });
92
+ });
93
+
94
+ it('should return empty array for zero length', () => {
95
+ const rng = new RandomNumberGenerator(123);
96
+ expect(rng.rand(0)).toEqual([]);
97
+ });
98
+
99
+ it('should return 1D array when given single-element array shape', () => {
100
+ const rng = new RandomNumberGenerator(131415);
101
+ const values = rng.rand([4]) as number[];
102
+ expect(Array.isArray(values)).toBe(true);
103
+ expect(values).toHaveLength(4);
104
+ values.forEach(v => {
105
+ expect(v).toBeGreaterThanOrEqual(0);
106
+ expect(v).toBeLessThan(1);
107
+ });
108
+ });
109
+
110
+ it('should return 2D array when given 2D shape', () => {
111
+ const rng = new RandomNumberGenerator(161718);
112
+ const values = rng.rand([2, 3]) as number[][];
113
+ expect(Array.isArray(values)).toBe(true);
114
+ expect(values).toHaveLength(2);
115
+ values.forEach(row => {
116
+ expect(Array.isArray(row)).toBe(true);
117
+ expect(row).toHaveLength(3);
118
+ row.forEach(v => {
119
+ expect(v).toBeGreaterThanOrEqual(0);
120
+ expect(v).toBeLessThan(1);
121
+ });
122
+ });
123
+ });
124
+
125
+ it('should return 3D array when given 3D shape', () => {
126
+ const rng = new RandomNumberGenerator(192021);
127
+ const values = rng.rand([2, 2, 2]) as number[][][];
128
+ expect(Array.isArray(values)).toBe(true);
129
+ expect(values).toHaveLength(2);
130
+ values.forEach(plane => {
131
+ expect(Array.isArray(plane)).toBe(true);
132
+ expect(plane).toHaveLength(2);
133
+ plane.forEach(row => {
134
+ expect(Array.isArray(row)).toBe(true);
135
+ expect(row).toHaveLength(2);
136
+ row.forEach(v => {
137
+ expect(v).toBeGreaterThanOrEqual(0);
138
+ expect(v).toBeLessThan(1);
139
+ });
140
+ });
141
+ });
142
+ });
143
+
144
+ it('should handle edge cases with zeros in shape', () => {
145
+ const rng = new RandomNumberGenerator(222324);
146
+ expect(rng.rand([0])).toEqual([]);
147
+ expect(rng.rand([3, 0])).toEqual([[], [], []]);
148
+ expect(rng.rand([0, 3])).toEqual([]);
149
+ });
150
+
151
+ it('should be deterministic across calls with same seed', () => {
152
+ const rng1 = new RandomNumberGenerator(252627);
153
+ const rng2 = new RandomNumberGenerator(252627);
154
+
155
+ const values1 = rng1.rand([2, 3]) as number[][];
156
+ const values2 = rng2.rand([2, 3]) as number[][];
157
+
158
+ expect(values1).toEqual(values2);
159
+ });
160
+
161
+ it('should handle single element arrays', () => {
162
+ const rng = new RandomNumberGenerator(282930);
163
+ const value1D = rng.rand(1) as number[];
164
+ const value2D = rng.rand([1, 1]) as number[][];
165
+
166
+ expect(value1D).toHaveLength(1);
167
+ expect(value2D).toHaveLength(1);
168
+ expect(value2D[0]).toHaveLength(1);
169
+ });
170
+
171
+ it('should produce different values in sequence', () => {
172
+ const rng = new RandomNumberGenerator(313233);
173
+ const values = rng.rand(10) as number[];
174
+
175
+ // Check that not all values are the same (very high probability)
176
+ const uniqueValues = new Set(values);
177
+ expect(uniqueValues.size).toBeGreaterThan(1);
178
+ });
179
+ });
180
+
181
+ describe('integration tests', () => {
182
+ it('should work correctly for layout algorithm use cases', () => {
183
+ const rng = new RandomNumberGenerator(424344);
184
+
185
+ // Test typical usage patterns in layout algorithms
186
+ const initialPositions = rng.rand([5, 2]) as number[][];
187
+ expect(initialPositions).toHaveLength(5);
188
+ initialPositions.forEach(pos => {
189
+ expect(pos).toHaveLength(2);
190
+ expect(pos[0]).toBeGreaterThanOrEqual(0);
191
+ expect(pos[0]).toBeLessThan(1);
192
+ expect(pos[1]).toBeGreaterThanOrEqual(0);
193
+ expect(pos[1]).toBeLessThan(1);
194
+ });
195
+ });
196
+
197
+ it('should maintain quality of randomness over many calls', () => {
198
+ const rng = new RandomNumberGenerator(454647);
199
+ const values: number[] = [];
200
+
201
+ // Generate many values
202
+ for (let i = 0; i < 1000; i++) {
203
+ values.push(rng.rand() as number);
204
+ }
205
+
206
+ // Basic statistical tests
207
+ const mean = values.reduce((a, b) => a + b) / values.length;
208
+ expect(mean).toBeGreaterThan(0.4); // Should be around 0.5
209
+ expect(mean).toBeLessThan(0.6);
210
+
211
+ // Check distribution across range
212
+ const firstQuartile = values.filter(v => v < 0.25).length;
213
+ const secondQuartile = values.filter(v => v >= 0.25 && v < 0.5).length;
214
+ const thirdQuartile = values.filter(v => v >= 0.5 && v < 0.75).length;
215
+ const fourthQuartile = values.filter(v => v >= 0.75).length;
216
+
217
+ // Each quartile should have roughly 1/4 of values (allow some variance)
218
+ expect(firstQuartile).toBeGreaterThan(200);
219
+ expect(firstQuartile).toBeLessThan(300);
220
+ expect(secondQuartile).toBeGreaterThan(200);
221
+ expect(secondQuartile).toBeLessThan(300);
222
+ expect(thirdQuartile).toBeGreaterThan(200);
223
+ expect(thirdQuartile).toBeLessThan(300);
224
+ expect(fourthQuartile).toBeGreaterThan(200);
225
+ expect(fourthQuartile).toBeLessThan(300);
226
+ });
227
+ });
228
+ });
229
+ });