@graphty/layout 1.0.1 → 1.1.1

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 (51) hide show
  1. package/.github/workflows/ci.yml +105 -0
  2. package/.releaserc.json +22 -0
  3. package/CHANGELOG.md +24 -0
  4. package/CLAUDE.md +104 -0
  5. package/CONTRIBUTING.md +1 -0
  6. package/README.md +893 -34
  7. package/dist/layout-helpers.d.ts +123 -0
  8. package/dist/layout-helpers.js +457 -0
  9. package/dist/layout-helpers.js.map +1 -0
  10. package/dist/layout.d.ts +275 -0
  11. package/dist/layout.js +2280 -0
  12. package/dist/layout.js.map +1 -0
  13. package/dist/vitest.config.d.ts +2 -0
  14. package/dist/vitest.config.js +30 -0
  15. package/dist/vitest.config.js.map +1 -0
  16. package/examples/arf-layout.html +1 -1
  17. package/examples/bfs-layout.html +37 -39
  18. package/examples/bipartite-layout.html +77 -69
  19. package/examples/circular-layout.html +13 -34
  20. package/examples/forceatlas2-layout.html +122 -28
  21. package/examples/kamada-kawai-layout.html +1 -1
  22. package/examples/multipartite-layout.html +64 -51
  23. package/examples/planar-layout.html +1 -1
  24. package/examples/random-layout.html +1 -1
  25. package/examples/shell-layout.html +53 -34
  26. package/examples/spectral-layout.html +1 -1
  27. package/examples/spiral-layout.html +1 -1
  28. package/examples/spring-layout.html +12 -2
  29. package/layout-helpers.ts +559 -0
  30. package/{layout.js → layout.ts} +1261 -771
  31. package/package.json +22 -6
  32. package/test/arf-layout.test.ts +443 -0
  33. package/test/bfs-layout.test.ts +427 -0
  34. package/test/bipartite-layout.test.ts +344 -0
  35. package/test/circular-layout.test.ts +300 -0
  36. package/test/forceatlas2-layout.test.ts +405 -0
  37. package/test/fruchterman-reingold-layout.test.ts +477 -0
  38. package/test/graph-generators.test.ts +450 -0
  39. package/test/kamada-kawai-layout.test.ts +351 -0
  40. package/test/multipartite-layout.test.ts +404 -0
  41. package/test/planar-layout.test.ts +266 -0
  42. package/test/random-layout.test.ts +254 -0
  43. package/test/rescale-layout.test.ts +373 -0
  44. package/test/shell-layout.test.ts +347 -0
  45. package/test/spectral-layout.test.ts +378 -0
  46. package/test/spiral-layout.test.ts +338 -0
  47. package/test/spring-layout.test.ts +241 -0
  48. package/tsconfig.json +16 -0
  49. package/vitest.config.ts +30 -0
  50. package/.husky/commit-msg +0 -1
  51. package/.husky/prepare-commit-msg +0 -1
@@ -2,7 +2,7 @@
2
2
  * Layout
3
3
  * ======
4
4
  *
5
- * Node positioning algorithms for graph drawing in JavaScript.
5
+ * Node positioning algorithms for graph drawing in TypeScript.
6
6
  *
7
7
  * For `randomLayout()` the possible resulting shape
8
8
  * is a square of side [0, scale] (default: [0, 1])
@@ -14,144 +14,172 @@
14
14
  * Ported from NetworkX Python library.
15
15
  */
16
16
 
17
+ // Type definitions
18
+ export type Node = string | number;
19
+ export type Edge = [Node, Node];
20
+ export type Graph = {
21
+ nodes?: () => Node[];
22
+ edges?: () => Edge[];
23
+ getEdgeData?: (source: Node, target: Node, attr: string) => any;
24
+ };
25
+
26
+ type Position = number[];
27
+ type PositionMap = Record<Node, Position>;
28
+
29
+ interface Embedding {
30
+ nodeOrder: Node[];
31
+ faceList: Node[][];
32
+ nodePositions: Record<Node, Position>;
33
+ }
34
+
17
35
  // Utility array manipulation functions (NumPy-like)
18
36
  const np = {
19
- zeros: function(shape) {
37
+ zeros: function (shape: number | number[]): number | number[] | number[][] | any[] {
20
38
  if (typeof shape === 'number') {
21
39
  return Array(shape).fill(0);
22
40
  }
23
41
  if (shape.length === 1) {
24
42
  return Array(shape[0]).fill(0);
25
43
  }
26
- return Array(shape[0]).fill().map(() => this.zeros(shape.slice(1)));
44
+ return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
27
45
  },
28
-
29
- ones: function(shape) {
46
+
47
+ ones: function (shape: number | number[]): number | any[] {
30
48
  if (typeof shape === 'number') {
31
49
  return Array(shape).fill(1);
32
50
  }
33
51
  if (shape.length === 1) {
34
52
  return Array(shape[0]).fill(1);
35
53
  }
36
- return Array(shape[0]).fill().map(() => this.ones(shape.slice(1)));
54
+ return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
37
55
  },
38
-
39
- linspace: function(start, stop, num) {
56
+
57
+ linspace: function (start: number, stop: number, num: number): number[] {
40
58
  const step = (stop - start) / (num - 1);
41
- return Array.from({length: num}, (_, i) => start + i * step);
59
+ return Array.from({ length: num }, (_, i) => start + i * step);
42
60
  },
43
-
44
- array: function(arr) {
61
+
62
+ array: function (arr: any): any[] {
45
63
  return Array.isArray(arr) ? [...arr] : [arr];
46
64
  },
47
-
48
- repeat: function(a, repeats) {
49
- const result = [];
65
+
66
+ repeat: function (a: any, repeats: number): any[] {
67
+ const result: any[] = [];
50
68
  for (let i = 0; i < repeats; i++) {
51
69
  result.push(...np.array(a));
52
70
  }
53
71
  return result;
54
72
  },
55
-
56
- mean: function(arr, axis = null) {
73
+
74
+ mean: function (arr: number[] | number[][], axis: number | null = null): number | number[] {
57
75
  if (axis === null) {
58
- const sum = arr.flat(Infinity).reduce((a, b) => a + b, 0);
59
- return sum / arr.flat(Infinity).length;
76
+ const flatArr = Array.isArray(arr[0])
77
+ ? (arr as number[][]).flat(Infinity) as number[]
78
+ : arr as number[];
79
+ const sum = flatArr.reduce((a, b) => a + b, 0);
80
+ return sum / flatArr.length;
60
81
  }
61
-
82
+
62
83
  if (axis === 0) {
63
- const result = [];
64
- for (let i = 0; i < arr[0].length; i++) {
84
+ const result: number[] = [];
85
+ const matrix = arr as number[][];
86
+ for (let i = 0; i < matrix[0].length; i++) {
65
87
  let sum = 0;
66
- for (let j = 0; j < arr.length; j++) {
67
- sum += arr[j][i];
88
+ for (let j = 0; j < matrix.length; j++) {
89
+ sum += matrix[j][i];
68
90
  }
69
- result.push(sum / arr.length);
91
+ result.push(sum / matrix.length);
70
92
  }
71
93
  return result;
72
94
  }
73
-
74
- return arr.map(row => np.mean(row));
95
+
96
+ return (arr as number[][]).map(row => np.mean(row) as number);
75
97
  },
76
-
77
- add: function(a, b) {
98
+
99
+ add: function (a: number | number[], b: number | number[]): number | number[] {
78
100
  if (!Array.isArray(a) && !Array.isArray(b)) {
79
101
  return a + b;
80
102
  }
81
103
  if (!Array.isArray(a)) {
82
- return b.map(val => a + val);
104
+ return (b as number[]).map(val => a + val);
83
105
  }
84
106
  if (!Array.isArray(b)) {
85
- return a.map(val => val + b);
107
+ return (a as number[]).map(val => val + b);
86
108
  }
87
- return a.map((val, i) => val + b[i]);
109
+ return (a as number[]).map((val, i) => val + (b as number[])[i]);
88
110
  },
89
-
90
- subtract: function(a, b) {
111
+
112
+ subtract: function (a: number | number[], b: number | number[]): number | number[] {
91
113
  if (!Array.isArray(a) && !Array.isArray(b)) {
92
114
  return a - b;
93
115
  }
94
116
  if (!Array.isArray(a)) {
95
- return b.map(val => a - val);
117
+ return (b as number[]).map(val => a - val);
96
118
  }
97
119
  if (!Array.isArray(b)) {
98
- return a.map(val => val - b);
120
+ return (a as number[]).map(val => val - b);
99
121
  }
100
- return a.map((val, i) => val - b[i]);
122
+ return (a as number[]).map((val, i) => val - (b as number[])[i]);
101
123
  },
102
-
103
- max: function(arr) {
124
+
125
+ max: function (arr: number | number[]): number {
104
126
  if (!Array.isArray(arr)) return arr;
105
- return Math.max(...arr.flat(Infinity));
127
+ return Math.max(...(arr as number[]).flat(Infinity) as number[]);
106
128
  },
107
-
108
- min: function(arr) {
129
+
130
+ min: function (arr: number | number[]): number {
109
131
  if (!Array.isArray(arr)) return arr;
110
- return Math.min(...arr.flat(Infinity));
132
+ return Math.min(...(arr as number[]).flat(Infinity) as number[]);
111
133
  },
112
-
113
- norm: function(arr) {
134
+
135
+ norm: function (arr: number[]): number {
114
136
  return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
115
137
  }
116
138
  };
117
139
 
118
140
  // Random number generator (for seed-based randomization)
119
141
  class RandomNumberGenerator {
120
- constructor(seed) {
142
+ private seed: number;
143
+ private m: number;
144
+ private a: number;
145
+ private c: number;
146
+ private _state: number;
147
+
148
+ constructor(seed?: number) {
121
149
  this.seed = seed || Math.floor(Math.random() * 1000000);
122
- this.m = 2**35 - 31;
150
+ this.m = 2 ** 35 - 31;
123
151
  this.a = 185852;
124
152
  this.c = 1;
125
153
  this._state = this.seed % this.m;
126
154
  }
127
-
128
- _next() {
155
+
156
+ _next(): number {
129
157
  this._state = (this.a * this._state + this.c) % this.m;
130
158
  return this._state / this.m;
131
159
  }
132
-
133
- rand(shape = null) {
160
+
161
+ rand(shape: number | number[] | null = null): number | number[] | number[][] {
134
162
  if (shape === null) {
135
163
  return this._next();
136
164
  }
137
-
165
+
138
166
  if (typeof shape === 'number') {
139
- const result = [];
167
+ const result: number[] = [];
140
168
  for (let i = 0; i < shape; i++) {
141
169
  result.push(this._next());
142
170
  }
143
171
  return result;
144
172
  }
145
-
173
+
146
174
  if (shape.length === 1) {
147
- const result = [];
175
+ const result: number[] = [];
148
176
  for (let i = 0; i < shape[0]; i++) {
149
177
  result.push(this._next());
150
178
  }
151
179
  return result;
152
180
  }
153
-
154
- const result = [];
181
+
182
+ const result: any[] = [];
155
183
  for (let i = 0; i < shape[0]; i++) {
156
184
  result.push(this.rand(shape.slice(1)));
157
185
  }
@@ -159,122 +187,142 @@ class RandomNumberGenerator {
159
187
  }
160
188
  }
161
189
 
190
+ /**
191
+ * Extract nodes from a graph object
192
+ *
193
+ * @param G - Graph or list of nodes
194
+ * @returns Array of nodes
195
+ */
196
+ function getNodesFromGraph(G: Graph): Node[] {
197
+ return G.nodes ? G.nodes() : G as Node[];
198
+ }
199
+
200
+ /**
201
+ * Extract edges from a graph object
202
+ *
203
+ * @param G - Graph or list of nodes
204
+ * @returns Array of edges
205
+ */
206
+ function getEdgesFromGraph(G: Graph): Edge[] {
207
+ return G.edges ? G.edges() : [] as Edge[];
208
+ }
209
+
162
210
  // Helper function similar to _process_params in Python version
163
- function _processParams(G, center, dim) {
211
+ function _processParams(G: Graph, center: number[] | null, dim: number): { G: Graph; center: number[] } {
164
212
  if (!center) {
165
213
  center = Array(dim).fill(0);
166
214
  }
167
-
215
+
168
216
  if (center.length !== dim) {
169
217
  throw new Error("length of center coordinates must match dimension of layout");
170
218
  }
171
-
219
+
172
220
  return { G, center };
173
221
  }
174
222
 
175
223
  /**
176
224
  * Position nodes uniformly at random in the unit square.
177
225
  *
178
- * @param {Object} G - Graph or list of nodes
179
- * @param {Array|null} center - Coordinate pair around which to center the layout
180
- * @param {number} dim - Dimension of layout
181
- * @param {number|null} seed - Random seed for reproducible layouts
182
- * @returns {Object} Positions dictionary keyed by node
226
+ * @param G - Graph or list of nodes
227
+ * @param center - Coordinate pair around which to center the layout
228
+ * @param dim - Dimension of layout
229
+ * @param seed - Random seed for reproducible layouts
230
+ * @returns Positions dictionary keyed by node
183
231
  */
184
- function randomLayout(G, center = null, dim = 2, seed = null) {
232
+ function randomLayout(G: Graph, center: number[] | null = null, dim: number = 2, seed: number | null = null): PositionMap {
185
233
  const processed = _processParams(G, center, dim);
186
- const nodes = processed.G.nodes ? processed.G.nodes() : processed.G;
234
+ const nodes = getNodesFromGraph(processed.G);
187
235
  center = processed.center;
188
-
189
- const rng = new RandomNumberGenerator(seed);
190
- const pos = {};
191
-
192
- nodes.forEach(node => {
193
- pos[node] = rng.rand(dim).map((val, i) => val + center[i]);
236
+
237
+ const rng = new RandomNumberGenerator(seed ?? undefined);
238
+ const pos: PositionMap = {};
239
+
240
+ nodes.forEach((node: Node) => {
241
+ pos[node] = (rng.rand(dim) as number[]).map((val: number, i: number) => val + center[i]);
194
242
  });
195
-
243
+
196
244
  return pos;
197
245
  }
198
246
 
199
247
  /**
200
248
  * Position nodes on a circle.
201
249
  *
202
- * @param {Object} G - Graph or list of nodes
203
- * @param {number} scale - Scale factor for positions
204
- * @param {Array|null} center - Coordinate pair around which to center the layout
205
- * @param {number} dim - Dimension of layout (currently only supports dim=2)
206
- * @returns {Object} Positions dictionary keyed by node
250
+ * @param G - Graph or list of nodes
251
+ * @param scale - Scale factor for positions
252
+ * @param center - Coordinate pair around which to center the layout
253
+ * @param dim - Dimension of layout (currently only supports dim=2)
254
+ * @returns Positions dictionary keyed by node
207
255
  */
208
- function circularLayout(G, scale = 1, center = null, dim = 2) {
256
+ function circularLayout(G: Graph, scale: number = 1, center: number[] | null = null, dim: number = 2): PositionMap {
209
257
  if (dim < 2) {
210
258
  throw new Error("cannot handle dimensions < 2");
211
259
  }
212
-
260
+
213
261
  const processed = _processParams(G, center, dim);
214
- const nodes = processed.G.nodes ? processed.G.nodes() : processed.G;
262
+ const nodes = getNodesFromGraph(processed.G);
215
263
  center = processed.center;
216
-
217
- const pos = {};
218
-
264
+
265
+ const pos: PositionMap = {};
266
+
219
267
  if (nodes.length === 0) {
220
268
  return pos;
221
269
  }
222
-
270
+
223
271
  if (nodes.length === 1) {
224
272
  pos[nodes[0]] = center;
225
273
  return pos;
226
274
  }
227
-
275
+
228
276
  // Calculate positions on a circle
229
277
  const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
230
-
231
- nodes.forEach((node, i) => {
232
- const x = Math.cos(theta[i]) * scale + center[0];
233
- const y = Math.sin(theta[i]) * scale + center[1];
234
- pos[node] = Array(dim).fill(0).map((_, j) => j === 0 ? x : j === 1 ? y : 0);
278
+
279
+ nodes.forEach((node: Node, i: number) => {
280
+ const x: number = Math.cos(theta[i]) * scale + center[0];
281
+ const y: number = Math.sin(theta[i]) * scale + center[1];
282
+ pos[node] = Array(dim).fill(0).map((_, j: number) => j === 0 ? x : j === 1 ? y : 0);
235
283
  });
236
-
284
+
237
285
  return pos;
238
286
  }
239
287
 
240
288
  /**
241
289
  * Position nodes in concentric circles.
242
290
  *
243
- * @param {Object} G - Graph or list of nodes
244
- * @param {Array|null} nlist - List of node lists for each shell
245
- * @param {number} scale - Scale factor for positions
246
- * @param {Array|null} center - Coordinate pair around which to center the layout
247
- * @param {number} dim - Dimension of layout (currently only supports dim=2)
248
- * @returns {Object} Positions dictionary keyed by node
291
+ * @param G - Graph or list of nodes
292
+ * @param nlist - List of node lists for each shell
293
+ * @param scale - Scale factor for positions
294
+ * @param center - Coordinate pair around which to center the layout
295
+ * @param dim - Dimension of layout (currently only supports dim=2)
296
+ * @returns Positions dictionary keyed by node
249
297
  */
250
- function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
298
+ function shellLayout(G: Graph, nlist: Node[][] | null = null, scale: number = 1, center: number[] | null = null, dim: number = 2): PositionMap {
251
299
  if (dim !== 2) {
252
300
  throw new Error("can only handle 2 dimensions");
253
301
  }
254
-
302
+
255
303
  const processed = _processParams(G, center, dim);
256
- const nodes = processed.G.nodes ? processed.G.nodes() : processed.G;
304
+ const nodes = getNodesFromGraph(processed.G);
257
305
  center = processed.center;
258
-
259
- const pos = {};
260
-
306
+
307
+ const pos: PositionMap = {};
308
+
261
309
  if (nodes.length === 0) {
262
310
  return pos;
263
311
  }
264
-
312
+
265
313
  if (nodes.length === 1) {
266
314
  pos[nodes[0]] = center;
267
315
  return pos;
268
316
  }
269
-
317
+
270
318
  // If no nlist is specified, put all nodes in a single shell
271
319
  if (!nlist) {
272
320
  nlist = [nodes];
273
321
  }
274
-
322
+
275
323
  const radiusBump = scale / nlist.length;
276
- let radius;
277
-
324
+ let radius: number;
325
+
278
326
  if (nlist[0].length === 1) {
279
327
  // Single node at center
280
328
  radius = 0;
@@ -284,28 +332,28 @@ function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
284
332
  // Start at radius 1
285
333
  radius = radiusBump;
286
334
  }
287
-
335
+
288
336
  for (let i = 0; i < nlist.length; i++) {
289
337
  const shell = nlist[i];
290
338
  if (shell.length === 0) continue;
291
-
339
+
292
340
  if (shell.length === 1 && i === 0) {
293
341
  // Already handled the case of a single center node
294
342
  continue;
295
343
  }
296
-
344
+
297
345
  // Calculate positions on a circle
298
346
  const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
299
-
300
- shell.forEach((node, j) => {
347
+
348
+ shell.forEach((node: Node, j) => {
301
349
  const x = Math.cos(theta[j]) * radius + center[0];
302
350
  const y = Math.sin(theta[j]) * radius + center[1];
303
351
  pos[node] = [x, y];
304
352
  });
305
-
353
+
306
354
  radius += radiusBump;
307
355
  }
308
-
356
+
309
357
  return pos;
310
358
  }
311
359
 
@@ -323,8 +371,17 @@ function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
323
371
  * @param {number} seed - Random seed for initial positions
324
372
  * @returns {Object} Positions dictionary keyed by node
325
373
  */
326
- function springLayout(G, k = null, pos = null, fixed = null, iterations = 50,
327
- scale = 1, center = null, dim = 2, seed = null) {
374
+ function springLayout(
375
+ G: Graph,
376
+ k: number | null = null,
377
+ pos: PositionMap | null = null,
378
+ fixed: Node[] | null = null,
379
+ iterations: number = 50,
380
+ scale: number = 1,
381
+ center: number[] | null = null,
382
+ dim: number = 2,
383
+ seed: number | null = null
384
+ ): PositionMap {
328
385
  // Legacy compatibility alias
329
386
  return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
330
387
  }
@@ -343,81 +400,90 @@ function springLayout(G, k = null, pos = null, fixed = null, iterations = 50,
343
400
  * @param {number} seed - Random seed for initial positions
344
401
  * @returns {Object} Positions dictionary keyed by node
345
402
  */
346
- function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterations = 50,
347
- scale = 1, center = null, dim = 2, seed = null) {
403
+ function fruchtermanReingoldLayout(
404
+ G: Graph,
405
+ k: number | null = null,
406
+ pos: PositionMap | null = null,
407
+ fixed: Node[] | null = null,
408
+ iterations: number = 50,
409
+ scale: number = 1,
410
+ center: number[] | null = null,
411
+ dim: number = 2,
412
+ seed: number | null = null
413
+ ): PositionMap {
348
414
  const processed = _processParams(G, center, dim);
349
415
  let graph = processed.G;
350
416
  center = processed.center;
351
-
352
- const nodes = graph.nodes ? graph.nodes() : graph;
353
- const edges = graph.edges ? graph.edges() : [];
354
-
417
+
418
+ const nodes = getNodesFromGraph(graph);
419
+ const edges = getEdgesFromGraph(graph);
420
+
355
421
  if (nodes.length === 0) {
356
422
  return {};
357
423
  }
358
-
424
+
359
425
  if (nodes.length === 1) {
360
- const singlePos = {};
426
+ const singlePos: PositionMap = {};
361
427
  singlePos[nodes[0]] = center;
362
428
  return singlePos;
363
429
  }
364
-
430
+
365
431
  // Set up initial positions
366
- let positions = {};
432
+ let positions: PositionMap = {};
367
433
  if (pos) {
368
434
  // Use provided positions
369
435
  for (const node of nodes) {
370
436
  if (pos[node]) {
371
437
  positions[node] = [...pos[node]];
372
438
  } else {
373
- const rng = new RandomNumberGenerator(seed);
374
- positions[node] = rng.rand(dim);
439
+ const rng = new RandomNumberGenerator(seed ?? undefined);
440
+ positions[node] = rng.rand(dim) as number[];
375
441
  }
376
442
  }
377
443
  } else {
378
444
  // Random initial positions
379
- const rng = new RandomNumberGenerator(seed);
445
+ const rng = new RandomNumberGenerator(seed ?? undefined);
380
446
  for (const node of nodes) {
381
- positions[node] = rng.rand(dim);
447
+ positions[node] = rng.rand(dim) as number[];
382
448
  }
383
449
  }
384
-
450
+
385
451
  // Set up fixed nodes
386
452
  const fixedNodes = new Set(fixed || []);
387
-
453
+
388
454
  // Optimal distance between nodes
389
455
  if (!k) {
390
456
  k = 1.0 / Math.sqrt(nodes.length);
391
457
  }
392
-
458
+
393
459
  // Initialize temperature
394
460
  let t = 0.1;
395
461
  // Calculate temperature reduction
396
462
  const dt = t / (iterations + 1);
397
-
463
+
398
464
  // Simple cooling schedule
399
465
  for (let i = 0; i < iterations; i++) {
400
466
  // Calculate repulsive forces
401
- const displacement = {};
467
+ const displacement: Record<Node, number[]> = {};
402
468
  for (const node of nodes) {
403
469
  displacement[node] = Array(dim).fill(0);
404
470
  }
405
-
471
+
406
472
  // Repulsive forces between nodes
407
473
  for (let v1i = 0; v1i < nodes.length; v1i++) {
408
474
  const v1 = nodes[v1i];
409
475
  for (let v2i = v1i + 1; v2i < nodes.length; v2i++) {
410
476
  const v2 = nodes[v2i];
411
-
477
+
412
478
  // Difference vector
413
479
  const delta = positions[v1].map((p, i) => p - positions[v2][i]);
414
-
480
+
415
481
  // Distance
416
482
  const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
417
-
483
+
418
484
  // Force
419
485
  const force = (k * k) / distance;
420
-
486
+
421
487
  // Add force to displacement
422
488
  for (let j = 0; j < dim; j++) {
423
489
  const direction = delta[j] / distance;
@@ -426,18 +492,18 @@ function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterat
426
492
  }
427
493
  }
428
494
  }
429
-
495
+
430
496
  // Attractive forces between connected nodes
431
497
  for (const [source, target] of edges) {
432
498
  // Difference vector
433
499
  const delta = positions[source].map((p, i) => p - positions[target][i]);
434
-
500
+
435
501
  // Distance
436
502
  const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
437
-
503
+
438
504
  // Force
439
505
  const force = (distance * distance) / k;
440
-
506
+
441
507
  // Add force to displacement
442
508
  for (let j = 0; j < dim; j++) {
443
509
  const direction = delta[j] / distance;
@@ -445,57 +511,62 @@ function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterat
445
511
  displacement[target][j] += direction * force;
446
512
  }
447
513
  }
448
-
514
+
449
515
  // Update positions
450
516
  for (const node of nodes) {
451
517
  if (fixedNodes.has(node)) continue;
452
-
518
+
453
519
  // Calculate displacement magnitude
454
520
  const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
455
-
521
+
456
522
  // Limit maximum displacement by temperature
457
523
  const limitedMagnitude = Math.min(magnitude, t);
458
-
524
+
459
525
  // Update position
460
526
  for (let j = 0; j < dim; j++) {
461
527
  const direction = magnitude === 0 ? 0 : displacement[node][j] / magnitude;
462
528
  positions[node][j] += direction * limitedMagnitude;
463
529
  }
464
530
  }
465
-
531
+
466
532
  // Cool temperature
467
533
  t -= dt;
468
534
  }
469
-
535
+
470
536
  // Rescale positions
471
537
  if (!fixed) {
472
- positions = rescaleLayout(positions, scale, center);
538
+ positions = rescaleLayout(positions, scale, center) as PositionMap;
473
539
  }
474
-
540
+
475
541
  return positions;
476
542
  }
477
543
 
478
544
  /**
479
545
  * Position nodes in a spectral layout using eigenvectors of the graph Laplacian.
480
546
  *
481
- * @param {Object} G - Graph
482
- * @param {number} scale - Scale factor for positions
483
- * @param {Array|null} center - Coordinate pair around which to center the layout
484
- * @param {number} dim - Dimension of layout
485
- * @returns {Object} Positions dictionary keyed by node
547
+ * @param G - Graph
548
+ * @param scale - Scale factor for positions
549
+ * @param center - Coordinate pair around which to center the layout
550
+ * @param dim - Dimension of layout
551
+ * @returns Positions dictionary keyed by node
486
552
  */
487
- function spectralLayout(G, scale = 1, center = null, dim = 2) {
553
+ function spectralLayout(
554
+ G: Graph,
555
+ scale: number = 1,
556
+ center: number[] | null = null,
557
+ dim: number = 2
558
+ ): PositionMap {
488
559
  const processed = _processParams(G, center, dim);
489
560
  const graph = processed.G;
490
561
  center = processed.center;
491
-
492
- const nodes = graph.nodes ? graph.nodes() : graph;
493
-
562
+
563
+ const nodes = getNodesFromGraph(graph);
564
+
494
565
  if (nodes.length <= 2) {
495
566
  if (nodes.length === 0) {
496
567
  return {};
497
568
  } else if (nodes.length === 1) {
498
- return {[nodes[0]]: center};
569
+ return { [nodes[0]]: center };
499
570
  } else {
500
571
  return {
501
572
  [nodes[0]]: center.map(v => v - scale),
@@ -503,24 +574,24 @@ function spectralLayout(G, scale = 1, center = null, dim = 2) {
503
574
  };
504
575
  }
505
576
  }
506
-
577
+
507
578
  // Create adjacency matrix
508
579
  const N = nodes.length;
509
- const nodeIndices = {};
510
- nodes.forEach((node, i) => { nodeIndices[node] = i; });
511
-
512
- const A = Array(N).fill().map(() => Array(N).fill(0));
513
- const edges = graph.edges ? graph.edges() : [];
514
-
580
+ const nodeIndices: Record<Node, number> = {};
581
+ nodes.forEach((node: Node, i: number) => { nodeIndices[node] = i; });
582
+
583
+ const A = Array(N).fill(0).map(() => Array(N).fill(0));
584
+ const edges = getEdgesFromGraph(graph);
585
+
515
586
  for (const [source, target] of edges) {
516
587
  const i = nodeIndices[source];
517
588
  const j = nodeIndices[target];
518
589
  A[i][j] = 1;
519
590
  A[j][i] = 1; // Make symmetric for undirected graphs
520
591
  }
521
-
592
+
522
593
  // Create Laplacian matrix: L = D - A where D is degree matrix
523
- const L = Array(N).fill().map(() => Array(N).fill(0));
594
+ const L = Array(N).fill(0).map(() => Array(N).fill(0));
524
595
  for (let i = 0; i < N; i++) {
525
596
  // Compute degree (sum of row)
526
597
  L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
@@ -528,25 +599,25 @@ function spectralLayout(G, scale = 1, center = null, dim = 2) {
528
599
  L[i][j] -= A[i][j];
529
600
  }
530
601
  }
531
-
602
+
532
603
  // Compute eigenvectors using power iteration method
533
604
  // We need the smallest non-zero eigenvectors of L
534
- const eigenvectors = [];
535
-
605
+ const eigenvectors: number[][] = [];
606
+
536
607
  // For each dimension, find an eigenvector
537
608
  for (let d = 0; d < dim; d++) {
538
- let vector = Array(N).fill().map(() => Math.random() - 0.5);
539
-
609
+ let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
610
+
540
611
  // Orthogonalize against previous eigenvectors
541
612
  for (const ev of eigenvectors) {
542
613
  const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
543
614
  vector = vector.map((val, idx) => val - dot * ev[idx]);
544
615
  }
545
-
616
+
546
617
  // Normalize
547
618
  const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
548
619
  vector = vector.map(val => val / norm);
549
-
620
+
550
621
  // Apply shifted inverse iteration to find smallest non-zero eigenvector
551
622
  // This is a simplification of the actual algorithm
552
623
  for (let iter = 0; iter < 100; iter++) {
@@ -557,72 +628,79 @@ function spectralLayout(G, scale = 1, center = null, dim = 2) {
557
628
  newVec[i] += L[i][j] * vector[j];
558
629
  }
559
630
  }
560
-
631
+
561
632
  // Orthogonalize against the constant vector (eigenvector with eigenvalue 0)
562
633
  const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
563
634
  newVec.forEach((val, idx, arr) => { arr[idx] = val - mean; });
564
-
635
+
565
636
  // Normalize
566
637
  const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
567
638
  if (newNorm < 1e-10) continue; // Skip if vector is close to zero
568
-
639
+
569
640
  vector = newVec.map(val => val / newNorm);
570
641
  }
571
-
642
+
572
643
  eigenvectors.push(vector);
573
644
  }
574
-
645
+
575
646
  // Create position array from eigenvectors
576
- const positions = Array(N).fill().map(() => Array(dim).fill(0));
647
+ const positions: number[][] = Array(N).fill(0).map(() => Array(dim).fill(0));
577
648
  for (let i = 0; i < N; i++) {
578
649
  for (let d = 0; d < dim; d++) {
579
650
  positions[i][d] = eigenvectors[d][i];
580
651
  }
581
652
  }
582
-
653
+
583
654
  // Rescale and create position dictionary
584
- const scaledPositions = rescaleLayout(positions, scale);
585
- const pos = {};
586
- nodes.forEach((node, i) => {
587
- pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
655
+ const scaledPositions = rescaleLayout(positions as any, scale);
656
+ const pos: PositionMap = {};
657
+ nodes.forEach((node: Node, i: number) => {
658
+ pos[node] = (scaledPositions as number[][])[i].map((val: number, j: number) => val + center[j]);
588
659
  });
589
-
660
+
590
661
  return pos;
591
662
  }
592
663
 
593
664
  /**
594
665
  * Position nodes in a spiral layout.
595
666
  *
596
- * @param {Object} G - Graph or list of nodes
597
- * @param {number} scale - Scale factor for positions
598
- * @param {Array|null} center - Coordinate pair around which to center the layout
599
- * @param {number} dim - Dimension of layout
600
- * @param {number} resolution - Controls the spacing between spiral elements
601
- * @param {boolean} equidistant - Whether to place nodes equidistant from each other
602
- * @returns {Object} Positions dictionary keyed by node
667
+ * @param G - Graph or list of nodes
668
+ * @param scale - Scale factor for positions
669
+ * @param center - Coordinate pair around which to center the layout
670
+ * @param dim - Dimension of layout
671
+ * @param resolution - Controls the spacing between spiral elements
672
+ * @param equidistant - Whether to place nodes equidistant from each other
673
+ * @returns Positions dictionary keyed by node
603
674
  */
604
- function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) {
675
+ function spiralLayout(
676
+ G: Graph,
677
+ scale: number = 1,
678
+ center: number[] | null = null,
679
+ dim: number = 2,
680
+ resolution: number = 0.35,
681
+ equidistant: boolean = false
682
+ ): PositionMap {
605
683
  if (dim !== 2) {
606
684
  throw new Error("can only handle 2 dimensions");
607
685
  }
608
-
686
+
609
687
  const processed = _processParams(G, center || [0, 0], dim);
610
- const nodes = processed.G.nodes ? processed.G.nodes() : processed.G;
688
+ const nodes = getNodesFromGraph(processed.G);
611
689
  center = processed.center;
612
-
613
- const pos = {};
614
-
690
+
691
+ const pos: PositionMap = {};
692
+
615
693
  if (nodes.length === 0) {
616
694
  return pos;
617
695
  }
618
-
696
+
619
697
  if (nodes.length === 1) {
620
698
  pos[nodes[0]] = [...center];
621
699
  return pos;
622
700
  }
623
-
624
- let positions = [];
625
-
701
+
702
+ let positions: number[][] = [];
703
+
626
704
  if (equidistant) {
627
705
  // Create equidistant points along the spiral
628
706
  // This matches the Python implementation logic
@@ -630,7 +708,7 @@ function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, e
630
708
  const step = 0.5;
631
709
  let theta = resolution;
632
710
  theta += chord / (step * theta);
633
-
711
+
634
712
  for (let i = 0; i < nodes.length; i++) {
635
713
  const r = step * theta;
636
714
  theta += chord / r;
@@ -638,53 +716,60 @@ function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, e
638
716
  }
639
717
  } else {
640
718
  // Create points with equal angle but increasing distance
641
- const dist = Array.from({length: nodes.length}, (_, i) => parseFloat(i));
719
+ const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
642
720
  const angle = dist.map(d => resolution * d);
643
-
721
+
644
722
  positions = dist.map((d, i) => [
645
723
  Math.cos(angle[i]) * d,
646
724
  Math.sin(angle[i]) * d
647
725
  ]);
648
726
  }
649
-
727
+
650
728
  // Convert position array to position matrix for rescaling
651
- const posArray = [];
729
+ const posArray: number[][] = [];
652
730
  for (let i = 0; i < positions.length; i++) {
653
731
  posArray.push(positions[i]);
654
732
  }
655
-
733
+
656
734
  // Rescale positions and add center offset
657
- const scaledPositions = rescaleLayout(posArray, scale);
735
+ const scaledPositions = rescaleLayout(posArray as any, scale) as any;
658
736
  for (let i = 0; i < scaledPositions.length; i++) {
659
737
  scaledPositions[i][0] += center[0];
660
738
  scaledPositions[i][1] += center[1];
661
739
  }
662
-
740
+
663
741
  // Create position dictionary
664
742
  for (let i = 0; i < nodes.length; i++) {
665
743
  pos[nodes[i]] = scaledPositions[i];
666
744
  }
667
-
745
+
668
746
  return pos;
669
747
  }
670
748
 
671
749
  /**
672
750
  * Rescale node positions to fit in the specified scale and center.
673
751
  *
674
- * @param {Object} pos - Dictionary of positions keyed by node
675
- * @param {number} scale - Scale factor for positions
676
- * @param {Array} center - Coordinate pair around which to center the layout
677
- * @returns {Object} Rescaled positions dictionary
752
+ * @param pos - Dictionary or array of positions
753
+ * @param scale - Scale factor for positions
754
+ * @param center - Coordinate pair around which to center the layout
755
+ * @returns Rescaled positions dictionary
678
756
  */
679
- function rescaleLayout(pos, scale = 1, center = [0, 0]) {
680
- if (Object.keys(pos).length === 0) {
681
- return {};
757
+ function rescaleLayout(
758
+ pos: PositionMap | number[][],
759
+ scale: number = 1,
760
+ center: number[] = [0, 0]
761
+ ): PositionMap | number[][] {
762
+ // Check if pos is empty
763
+ if (Array.isArray(pos)) {
764
+ if (pos.length === 0) return [];
765
+ } else {
766
+ if (Object.keys(pos).length === 0) return {};
682
767
  }
683
-
768
+
684
769
  // Extract position values
685
- const posValues = Object.values(pos);
770
+ const posValues: number[][] = Array.isArray(pos) ? pos : Object.values(pos);
686
771
  const dim = posValues[0].length;
687
-
772
+
688
773
  // Calculate center of positions
689
774
  const posCenter = Array(dim).fill(0);
690
775
  for (const p of posValues) {
@@ -692,79 +777,103 @@ function rescaleLayout(pos, scale = 1, center = [0, 0]) {
692
777
  posCenter[i] += p[i] / posValues.length;
693
778
  }
694
779
  }
695
-
780
+
696
781
  // Center positions
697
- const centeredPos = {};
698
- for (const [node, p] of Object.entries(pos)) {
699
- centeredPos[node] = p.map((val, i) => val - posCenter[i]);
782
+ let centeredPos: PositionMap | number[][] = {};
783
+ if (Array.isArray(pos)) {
784
+ centeredPos = pos.map(p => p.map((val, i) => val - posCenter[i]));
785
+ } else {
786
+ for (const [node, p] of Object.entries(pos)) {
787
+ (centeredPos as PositionMap)[node] = p.map((val, i) => val - posCenter[i]);
788
+ }
700
789
  }
701
-
790
+
702
791
  // Find maximum distance from center
703
792
  let maxDistance = 0;
704
- for (const p of Object.values(centeredPos)) {
793
+ const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
794
+ for (const p of centeredValues) {
705
795
  const distance = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
706
796
  maxDistance = Math.max(maxDistance, distance);
707
797
  }
708
-
798
+
709
799
  // Rescale
710
- const scaledPos = {};
800
+ let scaledPos: PositionMap | number[][] = Array.isArray(pos) ? [] : {};
801
+
711
802
  if (maxDistance > 0) {
712
803
  const scaleFactor = scale / maxDistance;
713
- for (const [node, p] of Object.entries(centeredPos)) {
714
- scaledPos[node] = p.map((val, i) => val * scaleFactor + center[i]);
804
+
805
+ if (Array.isArray(pos)) {
806
+ (scaledPos as number[][]) = (centeredPos as number[][]).map(p =>
807
+ p.map((val, i) => val * scaleFactor + center[i])
808
+ );
809
+ } else {
810
+ for (const [node, p] of Object.entries(centeredPos as PositionMap)) {
811
+ (scaledPos as PositionMap)[node] = p.map((val, i) => val * scaleFactor + center[i]);
812
+ }
715
813
  }
716
814
  } else {
717
815
  // All nodes at the same position
718
- for (const node of Object.keys(centeredPos)) {
719
- scaledPos[node] = [...center];
816
+ if (Array.isArray(pos)) {
817
+ (scaledPos as number[][]) = Array(pos.length).fill(0).map(() => [...center]);
818
+ } else {
819
+ for (const node of Object.keys(pos)) {
820
+ (scaledPos as PositionMap)[node] = [...center];
821
+ }
720
822
  }
721
823
  }
722
-
824
+
723
825
  return scaledPos;
724
826
  }
725
827
 
726
828
  /**
727
829
  * Position nodes in two straight lines (bipartite layout).
728
830
  *
729
- * @param {Object} G - Graph or list of nodes
730
- * @param {Array} nodes - Nodes in one node set of the graph
731
- * @param {string} align - The alignment of nodes: 'vertical' or 'horizontal'
732
- * @param {number} scale - Scale factor for positions
733
- * @param {Array|null} center - Coordinate pair around which to center the layout
734
- * @param {number} aspectRatio - The ratio of the width to the height of the layout
735
- * @returns {Object} Positions dictionary keyed by node
831
+ * @param G - Graph or list of nodes
832
+ * @param nodes - Nodes in one node set of the graph
833
+ * @param align - The alignment of nodes: 'vertical' or 'horizontal'
834
+ * @param scale - Scale factor for positions
835
+ * @param center - Coordinate pair around which to center the layout
836
+ * @param aspectRatio - The ratio of the width to the height of the layout
837
+ * @returns Positions dictionary keyed by node
736
838
  */
737
- function bipartiteLayout(G, nodes = null, align = 'vertical', scale = 1, center = null, aspectRatio = 4/3) {
839
+ function bipartiteLayout(
840
+ G: Graph,
841
+ nodes: Node[] | null = null,
842
+ align: 'vertical' | 'horizontal' = 'vertical',
843
+ scale: number = 1,
844
+ center: number[] | null = null,
845
+ aspectRatio: number = 4 / 3
846
+ ): PositionMap {
738
847
  if (align !== 'vertical' && align !== 'horizontal') {
739
848
  throw new Error("align must be either vertical or horizontal");
740
849
  }
741
-
850
+
742
851
  const processed = _processParams(G, center || [0, 0], 2);
743
852
  const graph = processed.G;
744
853
  center = processed.center;
745
-
746
- const allNodes = graph.nodes ? graph.nodes() : graph;
747
-
854
+
855
+ const allNodes = getNodesFromGraph(graph);
856
+
748
857
  if (allNodes.length === 0) {
749
858
  return {};
750
859
  }
751
-
860
+
752
861
  // If nodes not provided, try to determine bipartite sets
753
862
  if (!nodes) {
754
863
  // A simple heuristic for bipartite detection: use nodes with even/odd indices
755
864
  // This is a simplification, in Python NetworkX has bipartite.sets()
756
- nodes = allNodes.filter((_, i) => i % 2 === 0);
865
+ nodes = allNodes.filter((_: Node, i: number): boolean => i % 2 === 0);
757
866
  }
758
-
867
+
759
868
  const left = new Set(nodes);
760
- const right = new Set(allNodes.filter(n => !left.has(n)));
761
-
869
+ const right: Set<Node> = new Set(allNodes.filter((n: Node) => !left.has(n)));
870
+
762
871
  const height = 1;
763
872
  const width = aspectRatio * height;
764
873
  const offset = [width / 2, height / 2];
765
-
766
- const pos = {};
767
-
874
+
875
+ const pos: PositionMap = {};
876
+
768
877
  // Position nodes in the left set
769
878
  const leftNodes = [...left];
770
879
  leftNodes.forEach((node, i) => {
@@ -772,7 +881,7 @@ function bipartiteLayout(G, nodes = null, align = 'vertical', scale = 1, center
772
881
  const y = i * height / (leftNodes.length || 1);
773
882
  pos[node] = [x, y];
774
883
  });
775
-
884
+
776
885
  // Position nodes in the right set
777
886
  const rightNodes = [...right];
778
887
  rightNodes.forEach((node, i) => {
@@ -780,16 +889,16 @@ function bipartiteLayout(G, nodes = null, align = 'vertical', scale = 1, center
780
889
  const y = i * height / (rightNodes.length || 1);
781
890
  pos[node] = [x, y];
782
891
  });
783
-
892
+
784
893
  // Center positions around the origin and apply offset
785
894
  for (const node in pos) {
786
895
  pos[node][0] -= offset[0];
787
896
  pos[node][1] -= offset[1];
788
897
  }
789
-
898
+
790
899
  // Rescale positions
791
- const scaledPos = rescaleLayout(pos, scale, center);
792
-
900
+ const scaledPos = rescaleLayout(pos, scale, center) as PositionMap;
901
+
793
902
  // Handle horizontal alignment
794
903
  if (align === 'horizontal') {
795
904
  for (const node in scaledPos) {
@@ -798,37 +907,43 @@ function bipartiteLayout(G, nodes = null, align = 'vertical', scale = 1, center
798
907
  scaledPos[node][1] = temp;
799
908
  }
800
909
  }
801
-
910
+
802
911
  return scaledPos;
803
912
  }
804
913
 
805
914
  /**
806
915
  * Position nodes in layers of straight lines (multipartite layout).
807
916
  *
808
- * @param {Object} G - Graph or list of nodes
809
- * @param {Object|string} subsetKey - Object mapping layers to node sets, or node attribute name
810
- * @param {string} align - The alignment of nodes: 'vertical' or 'horizontal'
811
- * @param {number} scale - Scale factor for positions
812
- * @param {Array|null} center - Coordinate pair around which to center the layout
813
- * @returns {Object} Positions dictionary keyed by node
917
+ * @param G - Graph or list of nodes
918
+ * @param subsetKey - Object mapping layers to node sets, or node attribute name
919
+ * @param align - The alignment of nodes: 'vertical' or 'horizontal'
920
+ * @param scale - Scale factor for positions
921
+ * @param center - Coordinate pair around which to center the layout
922
+ * @returns Positions dictionary keyed by node
814
923
  */
815
- function multipartiteLayout(G, subsetKey = 'subset', align = 'vertical', scale = 1, center = null) {
924
+ function multipartiteLayout(
925
+ G: Graph,
926
+ subsetKey: Record<number | string, Node | Node[]> | string = 'subset',
927
+ align: 'vertical' | 'horizontal' = 'vertical',
928
+ scale: number = 1,
929
+ center: number[] | null = null
930
+ ): PositionMap {
816
931
  if (align !== 'vertical' && align !== 'horizontal') {
817
932
  throw new Error("align must be either vertical or horizontal");
818
933
  }
819
-
934
+
820
935
  const processed = _processParams(G, center || [0, 0], 2);
821
936
  const graph = processed.G;
822
937
  center = processed.center;
823
-
824
- const allNodes = graph.nodes ? graph.nodes() : graph;
825
-
938
+
939
+ const allNodes = getNodesFromGraph(graph);
940
+
826
941
  if (allNodes.length === 0) {
827
942
  return {};
828
943
  }
829
-
944
+
830
945
  // Convert subsetKey to a layer mapping if it's a string
831
- let layers = {};
946
+ let layers: Record<number | string, Node[]> = {};
832
947
  if (typeof subsetKey === 'string') {
833
948
  // In JS we don't have access to node attributes directly
834
949
  // This is a simplification - in a real implementation we would need
@@ -838,17 +953,24 @@ function multipartiteLayout(G, subsetKey = 'subset', align = 'vertical', scale =
838
953
  layers = { 0: allNodes };
839
954
  } else {
840
955
  // subsetKey is already a mapping of layers to nodes
841
- layers = subsetKey;
956
+ // Convert single nodes to arrays
957
+ for (const [key, value] of Object.entries(subsetKey)) {
958
+ if (Array.isArray(value)) {
959
+ layers[key] = value;
960
+ } else {
961
+ layers[key] = [value];
962
+ }
963
+ }
842
964
  }
843
-
965
+
844
966
  const layerCount = Object.keys(layers).length;
845
- let pos = {};
846
-
967
+ let pos: PositionMap = {};
968
+
847
969
  // Process each layer
848
970
  Object.entries(layers).forEach(([layer, nodes], layerIdx) => {
849
971
  const layerNodes = Array.isArray(nodes) ? nodes : [nodes];
850
972
  const layerSize = layerNodes.length;
851
-
973
+
852
974
  layerNodes.forEach((node, nodeIdx) => {
853
975
  // Place nodes in a grid: layerIdx determines x-coordinate (column)
854
976
  // nodeIdx determines y-coordinate (row position within column)
@@ -857,10 +979,10 @@ function multipartiteLayout(G, subsetKey = 'subset', align = 'vertical', scale =
857
979
  pos[node] = [x, y];
858
980
  });
859
981
  });
860
-
982
+
861
983
  // Rescale positions
862
- pos = rescaleLayout(pos, scale, center);
863
-
984
+ pos = rescaleLayout(pos, scale, center) as PositionMap;
985
+
864
986
  // Handle horizontal alignment
865
987
  if (align === 'horizontal') {
866
988
  for (const node in pos) {
@@ -869,50 +991,56 @@ function multipartiteLayout(G, subsetKey = 'subset', align = 'vertical', scale =
869
991
  pos[node][1] = temp;
870
992
  }
871
993
  }
872
-
994
+
873
995
  return pos;
874
996
  }
875
997
 
876
998
  /**
877
999
  * Position nodes according to breadth-first search algorithm.
878
1000
  *
879
- * @param {Object} G - Graph
880
- * @param {any} start - Starting node for bfs
881
- * @param {string} align - The alignment of layers: 'vertical' or 'horizontal'
882
- * @param {number} scale - Scale factor for positions
883
- * @param {Array|null} center - Coordinate pair around which to center the layout
884
- * @returns {Object} Positions dictionary keyed by node
1001
+ * @param G - Graph
1002
+ * @param start - Starting node for bfs
1003
+ * @param align - The alignment of layers: 'vertical' or 'horizontal'
1004
+ * @param scale - Scale factor for positions
1005
+ * @param center - Coordinate pair around which to center the layout
1006
+ * @returns Positions dictionary keyed by node
885
1007
  */
886
- function bfsLayout(G, start, align = 'vertical', scale = 1, center = null) {
1008
+ function bfsLayout(
1009
+ G: Graph,
1010
+ start: Node,
1011
+ align: 'vertical' | 'horizontal' = 'vertical',
1012
+ scale: number = 1,
1013
+ center: number[] | null = null
1014
+ ): PositionMap {
887
1015
  const processed = _processParams(G, center || [0, 0], 2);
888
1016
  const graph = processed.G;
889
1017
  center = processed.center;
890
-
891
- const allNodes = graph.nodes ? graph.nodes() : graph;
892
-
1018
+
1019
+ const allNodes = getNodesFromGraph(graph);
1020
+
893
1021
  if (allNodes.length === 0) {
894
1022
  return {};
895
1023
  }
896
-
1024
+
897
1025
  // Compute BFS layers
898
- const layers = {};
899
- const visited = new Set();
1026
+ const layers: Record<number, Node[]> = {};
1027
+ const visited = new Set<Node>();
900
1028
  let currentLayer = 0;
901
-
1029
+
902
1030
  // Starting layer
903
1031
  layers[currentLayer] = [start];
904
1032
  visited.add(start);
905
-
1033
+
906
1034
  // BFS traversal
907
1035
  while (Object.values(layers).flat().length < allNodes.length) {
908
- const nextLayer = [];
1036
+ const nextLayer: Node[] = [];
909
1037
  const currentNodes = layers[currentLayer];
910
-
1038
+
911
1039
  for (const node of currentNodes) {
912
1040
  // Get neighbors - this is a simplified approach
913
1041
  // In a real implementation, we would get neighbors from the graph
914
1042
  const neighbors = getNeighbors(graph, node);
915
-
1043
+
916
1044
  for (const neighbor of neighbors) {
917
1045
  if (!visited.has(neighbor)) {
918
1046
  nextLayer.push(neighbor);
@@ -920,71 +1048,80 @@ function bfsLayout(G, start, align = 'vertical', scale = 1, center = null) {
920
1048
  }
921
1049
  }
922
1050
  }
923
-
1051
+
924
1052
  if (nextLayer.length === 0) {
925
1053
  // No more connected nodes
926
- const unvisited = allNodes.filter(node => !visited.has(node));
1054
+ const unvisited: Node[] = allNodes.filter((node: Node) => !visited.has(node));
927
1055
  if (unvisited.length > 0) {
928
1056
  throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
929
1057
  }
930
1058
  break;
931
1059
  }
932
-
1060
+
933
1061
  currentLayer++;
934
1062
  layers[currentLayer] = nextLayer;
935
1063
  }
936
-
1064
+
937
1065
  // Use multipartite_layout to position the layers
938
1066
  return multipartiteLayout(graph, layers, align, scale, center);
939
-
1067
+
940
1068
  // Helper function to get neighbors
941
- function getNeighbors(graph, node) {
1069
+ function getNeighbors(graph: Graph, node: Node): Node[] {
942
1070
  if (!graph.edges) return [];
943
-
1071
+
944
1072
  return graph.edges()
945
- .filter(edge => edge[0] === node || edge[1] === node)
946
- .map(edge => edge[0] === node ? edge[1] : edge[0]);
1073
+ .filter((edge: Edge) => edge[0] === node || edge[1] === node)
1074
+ .map((edge: Edge): Node => edge[0] === node ? edge[1] : edge[0]);
947
1075
  }
948
1076
  }
949
1077
 
950
1078
  /**
951
1079
  * Position nodes without edge intersections (planar layout).
952
1080
  *
953
- * @param {Object} G - Graph
954
- * @param {number} scale - Scale factor for positions
955
- * @param {Array|null} center - Coordinate pair around which to center the layout
956
- * @param {number} dim - Dimension of layout (must be 2)
957
- * @returns {Object} Positions dictionary keyed by node
1081
+ * @param G - Graph
1082
+ * @param scale - Scale factor for positions
1083
+ * @param center - Coordinate pair around which to center the layout
1084
+ * @param dim - Dimension of layout (must be 2)
1085
+ * @returns Positions dictionary keyed by node
958
1086
  */
959
- function planarLayout(G, scale = 1, center = null, dim = 2) {
1087
+ function planarLayout(
1088
+ G: Graph,
1089
+ scale: number = 1,
1090
+ center: number[] | null = null,
1091
+ dim: number = 2
1092
+ ): PositionMap {
960
1093
  if (dim !== 2) {
961
1094
  throw new Error("can only handle 2 dimensions");
962
1095
  }
963
-
1096
+
964
1097
  const processed = _processParams(G, center || [0, 0], dim);
965
1098
  const graph = processed.G;
966
1099
  center = processed.center;
967
-
968
- const nodes = graph.nodes ? graph.nodes() : graph;
969
- const edges = graph.edges ? graph.edges() : [];
970
-
1100
+
1101
+ const nodes = getNodesFromGraph(graph);
1102
+ const edges = getEdgesFromGraph(graph);
1103
+
971
1104
  if (nodes.length === 0) {
972
1105
  return {};
973
1106
  }
974
1107
 
975
1108
  // Check if graph is planar and get embedding
976
1109
  const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges);
977
-
1110
+
978
1111
  if (!isPlanar) {
979
1112
  throw new Error("G is not planar.");
980
1113
  }
981
-
1114
+
1115
+ if (!embedding) {
1116
+ throw new Error("Failed to generate planar embedding.");
1117
+ }
1118
+
982
1119
  // Convert embedding to positions
983
1120
  let pos = combinatorialEmbeddingToPos(embedding, nodes);
984
-
1121
+
985
1122
  // Rescale the positions
986
- pos = rescaleLayout(pos, scale, center);
987
-
1123
+ pos = rescaleLayout(pos, scale, center) as PositionMap;
1124
+
988
1125
  return pos;
989
1126
  }
990
1127
 
@@ -992,23 +1129,27 @@ function planarLayout(G, scale = 1, center = null, dim = 2) {
992
1129
  * Check if graph is planar using a simplified version of Boyer-Myrvold algorithm.
993
1130
  * Returns planarity and embedding information.
994
1131
  *
995
- * @param {Object} G - Graph
996
- * @param {Array} nodes - List of nodes
997
- * @param {Array} edges - List of edges
998
- * @returns {Object} Object containing isPlanar flag and embedding
1132
+ * @param G - Graph
1133
+ * @param nodes - List of nodes
1134
+ * @param edges - List of edges
1135
+ * @returns Object containing isPlanar flag and embedding
999
1136
  */
1000
- function checkPlanarity(G, nodes, edges) {
1137
+ function checkPlanarity(
1138
+ G: Graph,
1139
+ nodes: Node[],
1140
+ edges: Edge[]
1141
+ ): { isPlanar: boolean; embedding: Embedding | null } {
1001
1142
  // For small graphs (n <= 4), all are planar
1002
1143
  if (nodes.length <= 4) {
1003
1144
  return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
1004
1145
  }
1005
-
1146
+
1006
1147
  // For K5 (complete graph with 5 nodes) and K3,3 (complete bipartite with 3,3 nodes)
1007
1148
  // these are not planar by Kuratowski's theorem
1008
1149
  if (isK5(nodes, edges) || isK33(nodes, edges)) {
1009
1150
  return { isPlanar: false, embedding: null };
1010
1151
  }
1011
-
1152
+
1012
1153
  // For other graphs, use LR algorithm (Left-Right Planarity Test)
1013
1154
  const result = lrPlanarityTest(nodes, edges);
1014
1155
  return result;
@@ -1017,95 +1158,98 @@ function checkPlanarity(G, nodes, edges) {
1017
1158
  /**
1018
1159
  * Check if graph is K5 (complete graph with 5 nodes)
1019
1160
  *
1020
- * @param {Array} nodes - List of nodes
1021
- * @param {Array} edges - List of edges
1022
- * @returns {boolean} True if graph is K5
1161
+ * @param nodes - List of nodes
1162
+ * @param edges - List of edges
1163
+ * @returns True if graph is K5
1023
1164
  */
1024
- function isK5(nodes, edges) {
1165
+ function isK5(nodes: Node[], edges: Edge[]): boolean {
1025
1166
  if (nodes.length !== 5) return false;
1026
-
1167
+
1027
1168
  // K5 has exactly 10 edges
1028
1169
  if (edges.length !== 10) return false;
1029
-
1170
+
1030
1171
  // Check if every pair of distinct nodes is connected
1031
1172
  for (let i = 0; i < nodes.length; i++) {
1032
1173
  for (let j = i + 1; j < nodes.length; j++) {
1033
1174
  const hasEdge = edges.some(
1034
- e => (e[0] === nodes[i] && e[1] === nodes[j]) ||
1035
- (e[0] === nodes[j] && e[1] === nodes[i])
1175
+ e => (e[0] === nodes[i] && e[1] === nodes[j]) ||
1176
+ (e[0] === nodes[j] && e[1] === nodes[i])
1036
1177
  );
1037
1178
  if (!hasEdge) return false;
1038
1179
  }
1039
1180
  }
1040
-
1181
+
1041
1182
  return true;
1042
1183
  }
1043
1184
 
1044
1185
  /**
1045
1186
  * Check if graph is K3,3 (complete bipartite with 3,3 nodes)
1046
1187
  *
1047
- * @param {Array} nodes - List of nodes
1048
- * @param {Array} edges - List of edges
1049
- * @returns {boolean} True if graph is K3,3
1188
+ * @param nodes - List of nodes
1189
+ * @param edges - List of edges
1190
+ * @returns True if graph is K3,3
1050
1191
  */
1051
- function isK33(nodes, edges) {
1192
+ function isK33(nodes: Node[], edges: Edge[]): boolean {
1052
1193
  if (nodes.length !== 6) return false;
1053
-
1194
+
1054
1195
  // K3,3 has exactly 9 edges
1055
1196
  if (edges.length !== 9) return false;
1056
-
1197
+
1057
1198
  // Try to find a bipartite partition
1058
1199
  const nodePartitions = tryFindBipartitePartition(nodes, edges);
1059
1200
  if (!nodePartitions) return false;
1060
-
1201
+
1061
1202
  const [part1, part2] = nodePartitions;
1062
-
1203
+
1063
1204
  // Check if both partitions have size 3
1064
1205
  if (part1.length !== 3 || part2.length !== 3) return false;
1065
-
1206
+
1066
1207
  // Check if every node in part1 is connected to every node in part2
1067
1208
  for (const n1 of part1) {
1068
1209
  for (const n2 of part2) {
1069
1210
  const hasEdge = edges.some(
1070
- e => (e[0] === n1 && e[1] === n2) ||
1071
- (e[0] === n2 && e[1] === n1)
1211
+ e => (e[0] === n1 && e[1] === n2) ||
1212
+ (e[0] === n2 && e[1] === n1)
1072
1213
  );
1073
1214
  if (!hasEdge) return false;
1074
1215
  }
1075
1216
  }
1076
-
1217
+
1077
1218
  return true;
1078
1219
  }
1079
1220
 
1080
1221
  /**
1081
1222
  * Try to find a bipartite partition of the nodes
1082
1223
  *
1083
- * @param {Array} nodes - List of nodes
1084
- * @param {Array} edges - List of edges
1085
- * @returns {Array|null} Array of two partitions, or null if not bipartite
1224
+ * @param nodes - List of nodes
1225
+ * @param edges - List of edges
1226
+ * @returns Array of two partitions, or null if not bipartite
1086
1227
  */
1087
- function tryFindBipartitePartition(nodes, edges) {
1088
- const colorMap = {};
1089
- const adjList = {};
1090
-
1228
+ function tryFindBipartitePartition(
1229
+ nodes: Node[],
1230
+ edges: Edge[]
1231
+ ): [Node[], Node[]] | null {
1232
+ const colorMap: Record<Node, number> = {};
1233
+ const adjList: Record<Node, Node[]> = {};
1234
+
1091
1235
  // Create adjacency list
1092
1236
  for (const node of nodes) {
1093
1237
  adjList[node] = [];
1094
1238
  }
1095
-
1239
+
1096
1240
  for (const [u, v] of edges) {
1097
1241
  adjList[u].push(v);
1098
1242
  adjList[v].push(u);
1099
1243
  }
1100
-
1244
+
1101
1245
  // BFS to color nodes
1102
- const queue = [nodes[0]];
1246
+ const queue: Node[] = [nodes[0]];
1103
1247
  colorMap[nodes[0]] = 0;
1104
-
1248
+
1105
1249
  while (queue.length > 0) {
1106
- const node = queue.shift();
1250
+ const node = queue.shift()!;
1107
1251
  const nodeColor = colorMap[node];
1108
-
1252
+
1109
1253
  for (const neighbor of adjList[node]) {
1110
1254
  if (colorMap[neighbor] === undefined) {
1111
1255
  colorMap[neighbor] = 1 - nodeColor; // Toggle color (0/1)
@@ -1116,11 +1260,11 @@ function tryFindBipartitePartition(nodes, edges) {
1116
1260
  }
1117
1261
  }
1118
1262
  }
1119
-
1263
+
1120
1264
  // Create partitions
1121
- const part0 = [];
1122
- const part1 = [];
1123
-
1265
+ const part0: Node[] = [];
1266
+ const part1: Node[] = [];
1267
+
1124
1268
  for (const node of nodes) {
1125
1269
  if (colorMap[node] === 0) {
1126
1270
  part0.push(node);
@@ -1128,121 +1272,127 @@ function tryFindBipartitePartition(nodes, edges) {
1128
1272
  part1.push(node);
1129
1273
  }
1130
1274
  }
1131
-
1275
+
1132
1276
  return [part0, part1];
1133
1277
  }
1134
1278
 
1135
1279
  /**
1136
1280
  * Left-Right Planarity Test for general graphs
1137
1281
  *
1138
- * @param {Array} nodes - List of nodes
1139
- * @param {Array} edges - List of edges
1140
- * @returns {Object} Object containing isPlanar flag and embedding
1282
+ * @param nodes - List of nodes
1283
+ * @param edges - List of edges
1284
+ * @returns Object containing isPlanar flag and embedding
1141
1285
  */
1142
- function lrPlanarityTest(nodes, edges) {
1286
+ function lrPlanarityTest(
1287
+ nodes: Node[],
1288
+ edges: Edge[]
1289
+ ): { isPlanar: boolean; embedding: Embedding | null } {
1143
1290
  // Create adjacency list for the graph
1144
- const adjList = {};
1291
+ const adjList: Record<Node, Node[]> = {};
1145
1292
  for (const node of nodes) {
1146
1293
  adjList[node] = [];
1147
1294
  }
1148
-
1295
+
1149
1296
  for (const [u, v] of edges) {
1150
1297
  adjList[u].push(v);
1151
1298
  adjList[v].push(u);
1152
1299
  }
1153
-
1300
+
1154
1301
  // Step 1: Perform DFS to get an st-numbering (ordering of nodes)
1155
- const visited = new Set();
1156
- const ordering = [];
1157
-
1158
- function dfs(node) {
1302
+ const visited = new Set<Node>();
1303
+ const ordering: Node[] = [];
1304
+
1305
+ function dfs(node: Node): void {
1159
1306
  visited.add(node);
1160
1307
  ordering.push(node);
1161
-
1308
+
1162
1309
  for (const neighbor of adjList[node]) {
1163
1310
  if (!visited.has(neighbor)) {
1164
1311
  dfs(neighbor);
1165
1312
  }
1166
1313
  }
1167
1314
  }
1168
-
1315
+
1169
1316
  // Start DFS from first node
1170
1317
  dfs(nodes[0]);
1171
-
1318
+
1172
1319
  // If the graph is disconnected, it's still planar but we need to handle each component
1173
1320
  if (ordering.length < nodes.length) {
1174
1321
  // Create a simple triangulation embedding for disconnected graphs
1175
1322
  return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
1176
1323
  }
1177
-
1324
+
1178
1325
  // Step 2: For a general implementation, we'll use a simplified approach
1179
1326
  // since this would normally require implementing the entire LR algorithm
1180
-
1327
+
1181
1328
  // For this implementation, since we can't fully implement Boyer-Myrvold,
1182
1329
  // we'll create a reasonable planar embedding for most planar graphs
1183
-
1330
+
1184
1331
  // We assume the graph is planar if it's sparse enough (|E| <= 3|V| - 6)
1185
1332
  // This is a necessary but not sufficient condition for planar graphs
1186
1333
  if (edges.length > 3 * nodes.length - 6) {
1187
1334
  return { isPlanar: false, embedding: null };
1188
1335
  }
1189
-
1336
+
1190
1337
  // Create a planar embedding using a triangulation approach
1191
1338
  const embedding = createTriangulationEmbedding(nodes, edges);
1192
-
1339
+
1193
1340
  return { isPlanar: true, embedding };
1194
1341
  }
1195
1342
 
1196
1343
  /**
1197
1344
  * Create a triangulation-based embedding for a planar graph
1198
1345
  *
1199
- * @param {Array} nodes - List of nodes
1200
- * @param {Array} edges - List of edges
1201
- * @returns {Object} Embedding object
1346
+ * @param nodes - List of nodes
1347
+ * @param edges - List of edges
1348
+ * @returns Embedding object
1202
1349
  */
1203
- function createTriangulationEmbedding(nodes, edges) {
1350
+ function createTriangulationEmbedding(
1351
+ nodes: Node[],
1352
+ edges: Edge[]
1353
+ ): Embedding {
1204
1354
  // Create a simple embedding using the incremental approach
1205
- const embedding = {
1355
+ const embedding: Embedding = {
1206
1356
  nodeOrder: [...nodes],
1207
1357
  faceList: [],
1208
1358
  nodePositions: {}
1209
1359
  };
1210
-
1360
+
1211
1361
  // Create a map of adjacent nodes
1212
- const adjMap = {};
1362
+ const adjMap: Record<Node, Set<Node>> = {};
1213
1363
  for (const node of nodes) {
1214
- adjMap[node] = new Set();
1364
+ adjMap[node] = new Set<Node>();
1215
1365
  }
1216
-
1366
+
1217
1367
  for (const [u, v] of edges) {
1218
1368
  adjMap[u].add(v);
1219
1369
  adjMap[v].add(u);
1220
1370
  }
1221
-
1371
+
1222
1372
  // Create outer face as a cycle (if possible)
1223
1373
  const outerFace = findCycle(nodes, edges, adjMap) || nodes;
1224
1374
  embedding.faceList.push(outerFace);
1225
-
1375
+
1226
1376
  // Position nodes on a convex polygon (outer face)
1227
1377
  const n = outerFace.length;
1228
1378
  for (let i = 0; i < n; i++) {
1229
1379
  const angle = 2 * Math.PI * i / n;
1230
1380
  embedding.nodePositions[outerFace[i]] = [Math.cos(angle), Math.sin(angle)];
1231
1381
  }
1232
-
1382
+
1233
1383
  // Position interior nodes using barycentric coordinates
1234
1384
  const interiorNodes = nodes.filter(node => !embedding.nodePositions[node]);
1235
-
1385
+
1236
1386
  for (const node of interiorNodes) {
1237
1387
  const neighbors = Array.from(adjMap[node]);
1238
-
1388
+
1239
1389
  if (neighbors.length === 0) {
1240
1390
  // Isolated node, place at center
1241
1391
  embedding.nodePositions[node] = [0, 0];
1242
1392
  } else {
1243
1393
  // Average position of neighbors that have positions
1244
1394
  let xSum = 0, ySum = 0, count = 0;
1245
-
1395
+
1246
1396
  for (const neighbor of neighbors) {
1247
1397
  if (embedding.nodePositions[neighbor]) {
1248
1398
  xSum += embedding.nodePositions[neighbor][0];
@@ -1250,13 +1400,13 @@ function createTriangulationEmbedding(nodes, edges) {
1250
1400
  count++;
1251
1401
  }
1252
1402
  }
1253
-
1403
+
1254
1404
  if (count > 0) {
1255
1405
  // Place slightly away from center to avoid overlaps
1256
1406
  const jitter = 0.1 * Math.random();
1257
1407
  embedding.nodePositions[node] = [
1258
- xSum/count + jitter * (Math.random() - 0.5),
1259
- ySum/count + jitter * (Math.random() - 0.5)
1408
+ xSum / count + jitter * (Math.random() - 0.5),
1409
+ ySum / count + jitter * (Math.random() - 0.5)
1260
1410
  ];
1261
1411
  } else {
1262
1412
  // No neighbors have positions yet, place randomly inside unit circle
@@ -1266,31 +1416,35 @@ function createTriangulationEmbedding(nodes, edges) {
1266
1416
  }
1267
1417
  }
1268
1418
  }
1269
-
1419
+
1270
1420
  return embedding;
1271
1421
  }
1272
1422
 
1273
1423
  /**
1274
1424
  * Find a simple cycle in the graph (for outer face)
1275
1425
  *
1276
- * @param {Array} nodes - List of nodes
1277
- * @param {Array} edges - List of edges
1278
- * @param {Object} adjMap - Adjacency map
1279
- * @returns {Array|null} Cycle as array of nodes, or null if none found
1426
+ * @param nodes - List of nodes
1427
+ * @param edges - List of edges
1428
+ * @param adjMap - Adjacency map
1429
+ * @returns Cycle as array of nodes, or null if none found
1280
1430
  */
1281
- function findCycle(nodes, edges, adjMap) {
1431
+ function findCycle(
1432
+ nodes: Node[],
1433
+ edges: Edge[],
1434
+ adjMap: Record<Node, Set<Node>>
1435
+ ): Node[] | null {
1282
1436
  if (nodes.length === 0) return null;
1283
1437
  if (nodes.length <= 2) return nodes; // Not a real cycle but handle it
1284
-
1438
+
1285
1439
  // Try to find a Hamiltonian cycle for simplicity (for small graphs)
1286
1440
  if (nodes.length <= 8) {
1287
- const visited = new Set();
1288
- const path = [];
1289
-
1290
- function hamiltonianCycleDFS(node) {
1441
+ const visited = new Set<Node>();
1442
+ const path: Node[] = [];
1443
+
1444
+ function hamiltonianCycleDFS(node: Node): boolean {
1291
1445
  path.push(node);
1292
1446
  visited.add(node);
1293
-
1447
+
1294
1448
  if (path.length === nodes.length) {
1295
1449
  // Check if it's a cycle (last node connects to first)
1296
1450
  if (adjMap[node].has(path[0])) {
@@ -1301,7 +1455,7 @@ function findCycle(nodes, edges, adjMap) {
1301
1455
  path.pop();
1302
1456
  return false;
1303
1457
  }
1304
-
1458
+
1305
1459
  for (const neighbor of adjMap[node]) {
1306
1460
  if (!visited.has(neighbor)) {
1307
1461
  if (hamiltonianCycleDFS(neighbor)) {
@@ -1309,55 +1463,55 @@ function findCycle(nodes, edges, adjMap) {
1309
1463
  }
1310
1464
  }
1311
1465
  }
1312
-
1466
+
1313
1467
  visited.delete(node);
1314
1468
  path.pop();
1315
1469
  return false;
1316
1470
  }
1317
-
1471
+
1318
1472
  if (hamiltonianCycleDFS(nodes[0])) {
1319
1473
  return path;
1320
1474
  }
1321
1475
  }
1322
-
1476
+
1323
1477
  // Fallback: try to find any cycle using DFS
1324
- const visited = new Set();
1325
- const parent = {};
1326
- let cycleFound = null;
1327
-
1328
- function findCycleDFS(node, parentNode) {
1478
+ const visited = new Set<Node>();
1479
+ const parent: Record<Node, Node | null> = {};
1480
+ let cycleFound: Node[] | null = null;
1481
+
1482
+ function findCycleDFS(node: Node, parentNode: Node | null): boolean {
1329
1483
  visited.add(node);
1330
-
1484
+
1331
1485
  for (const neighbor of adjMap[node]) {
1332
1486
  if (neighbor === parentNode) continue;
1333
-
1487
+
1334
1488
  if (visited.has(neighbor)) {
1335
1489
  // Found a cycle
1336
1490
  cycleFound = constructCycle(node, neighbor, parent);
1337
1491
  return true;
1338
1492
  }
1339
-
1493
+
1340
1494
  parent[neighbor] = node;
1341
1495
  if (findCycleDFS(neighbor, node)) {
1342
1496
  return true;
1343
1497
  }
1344
1498
  }
1345
-
1499
+
1346
1500
  return false;
1347
1501
  }
1348
-
1349
- function constructCycle(u, v, parent) {
1350
- const cycle = [v, u];
1502
+
1503
+ function constructCycle(u: Node, v: Node, parent: Record<Node, Node | null>): Node[] {
1504
+ const cycle: Node[] = [v, u];
1351
1505
  let current = u;
1352
-
1506
+
1353
1507
  while (parent[current] !== undefined && parent[current] !== v) {
1354
- current = parent[current];
1508
+ current = parent[current]!;
1355
1509
  cycle.push(current);
1356
1510
  }
1357
-
1511
+
1358
1512
  return cycle;
1359
1513
  }
1360
-
1514
+
1361
1515
  // Try to find a cycle
1362
1516
  for (const node of nodes) {
1363
1517
  if (!visited.has(node)) {
@@ -1367,20 +1521,23 @@ function findCycle(nodes, edges, adjMap) {
1367
1521
  }
1368
1522
  }
1369
1523
  }
1370
-
1524
+
1371
1525
  return cycleFound || nodes; // Fallback to all nodes if no cycle found
1372
1526
  }
1373
1527
 
1374
1528
  /**
1375
1529
  * Convert a combinatorial embedding to node positions
1376
1530
  *
1377
- * @param {Object} embedding - The embedding object
1378
- * @param {Array} nodes - List of nodes
1379
- * @returns {Object} Dictionary mapping nodes to positions
1531
+ * @param embedding - The embedding object
1532
+ * @param nodes - List of nodes
1533
+ * @returns Dictionary mapping nodes to positions
1380
1534
  */
1381
- function combinatorialEmbeddingToPos(embedding, nodes) {
1382
- const pos = {};
1383
-
1535
+ function combinatorialEmbeddingToPos(
1536
+ embedding: Embedding,
1537
+ nodes: Node[]
1538
+ ): PositionMap {
1539
+ const pos: PositionMap = {};
1540
+
1384
1541
  // Use the positions from the embedding
1385
1542
  for (const node of nodes) {
1386
1543
  if (embedding.nodePositions[node]) {
@@ -1390,53 +1547,64 @@ function combinatorialEmbeddingToPos(embedding, nodes) {
1390
1547
  pos[node] = [0, 0];
1391
1548
  }
1392
1549
  }
1393
-
1550
+
1394
1551
  return pos;
1395
1552
  }
1396
1553
 
1554
+ // Type definitions for distance structure used in Kamada-Kawai
1555
+ type DistanceMap = Record<Node, Record<Node, number>>;
1556
+
1397
1557
  /**
1398
1558
  * Position nodes using Kamada-Kawai path-length cost-function.
1399
1559
  *
1400
- * @param {Object} G - NetworkX graph or list of nodes
1401
- * @param {Object} dist - A two-level dictionary of optimal distances between nodes
1402
- * @param {Object} pos - Initial positions for nodes
1403
- * @param {string} weight - The edge attribute used for edge weights
1404
- * @param {number} scale - Scale factor for positions
1405
- * @param {Array|null} center - Coordinate pair around which to center the layout
1406
- * @param {number} dim - Dimension of layout
1407
- * @returns {Object} Positions dictionary keyed by node
1560
+ * @param G - NetworkX graph or list of nodes
1561
+ * @param dist - A two-level dictionary of optimal distances between nodes
1562
+ * @param pos - Initial positions for nodes
1563
+ * @param weight - The edge attribute used for edge weights
1564
+ * @param scale - Scale factor for positions
1565
+ * @param center - Coordinate pair around which to center the layout
1566
+ * @param dim - Dimension of layout
1567
+ * @returns Positions dictionary keyed by node
1408
1568
  */
1409
- function kamadaKawaiLayout(G, dist = null, pos = null, weight = 'weight', scale = 1, center = null, dim = 2) {
1569
+ function kamadaKawaiLayout(
1570
+ G: Graph,
1571
+ dist: DistanceMap | null = null,
1572
+ pos: PositionMap | null = null,
1573
+ weight: string = 'weight',
1574
+ scale: number = 1,
1575
+ center: number[] | null = null,
1576
+ dim: number = 2
1577
+ ): PositionMap {
1410
1578
  const processed = _processParams(G, center, dim);
1411
1579
  const graph = processed.G;
1412
1580
  center = processed.center;
1413
-
1414
- const nodes = graph.nodes ? graph.nodes() : graph;
1415
-
1581
+
1582
+ const nodes = getNodesFromGraph(graph);
1583
+
1416
1584
  if (nodes.length === 0) {
1417
1585
  return {};
1418
1586
  }
1419
-
1587
+
1420
1588
  if (nodes.length === 1) {
1421
- return {[nodes[0]]: center};
1589
+ return { [nodes[0]]: center };
1422
1590
  }
1423
-
1591
+
1424
1592
  // Initialize distance matrix
1425
1593
  if (!dist) {
1426
- dist = _compute_shortest_path_distances(graph, weight);
1594
+ dist = _computeShortestPathDistances(graph, weight);
1427
1595
  }
1428
-
1596
+
1429
1597
  // Convert distances to a matrix
1430
- const nodesArray = Array.from(nodes);
1598
+ const nodesArray: Node[] = Array.from(nodes);
1431
1599
  const nNodes = nodesArray.length;
1432
- const distMatrix = Array(nNodes).fill().map(() => Array(nNodes).fill(1e6));
1433
-
1600
+ const distMatrix: number[][] = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6));
1601
+
1434
1602
  for (let i = 0; i < nNodes; i++) {
1435
1603
  const nodeI = nodesArray[i];
1436
1604
  distMatrix[i][i] = 0;
1437
-
1605
+
1438
1606
  if (!dist[nodeI]) continue;
1439
-
1607
+
1440
1608
  for (let j = 0; j < nNodes; j++) {
1441
1609
  const nodeJ = nodesArray[j];
1442
1610
  if (dist[nodeI][nodeJ] !== undefined) {
@@ -1444,7 +1612,7 @@ function kamadaKawaiLayout(G, dist = null, pos = null, weight = 'weight', scale
1444
1612
  }
1445
1613
  }
1446
1614
  }
1447
-
1615
+
1448
1616
  // Initialize positions if not provided
1449
1617
  if (!pos) {
1450
1618
  if (dim >= 3) {
@@ -1453,70 +1621,78 @@ function kamadaKawaiLayout(G, dist = null, pos = null, weight = 'weight', scale
1453
1621
  pos = circularLayout(G, 1, [0, 0], dim);
1454
1622
  } else {
1455
1623
  // For 1D, use a linear layout
1456
- const posArray = {};
1624
+ const posArray: PositionMap = {};
1457
1625
  nodesArray.forEach((node, i) => {
1458
1626
  posArray[node] = [i / (nNodes - 1 || 1)];
1459
1627
  });
1460
1628
  pos = posArray;
1461
1629
  }
1462
1630
  }
1463
-
1631
+
1464
1632
  // Convert positions to array for computation
1465
- const posArray = new Array(nNodes);
1633
+ const posArray: number[][] = new Array(nNodes);
1466
1634
  for (let i = 0; i < nNodes; i++) {
1467
1635
  const node = nodesArray[i];
1468
1636
  posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
1469
-
1637
+
1470
1638
  // Ensure correct dimensionality
1471
1639
  while (posArray[i].length < dim) {
1472
1640
  posArray[i].push(0);
1473
1641
  }
1474
1642
  }
1475
-
1643
+
1476
1644
  // Run the Kamada-Kawai algorithm
1477
1645
  const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
1478
-
1646
+
1479
1647
  // Convert positions array back to dictionary and rescale
1480
- const finalPos = {};
1648
+ const finalPos: PositionMap = {};
1481
1649
  for (let i = 0; i < nNodes; i++) {
1482
1650
  finalPos[nodesArray[i]] = newPositions[i];
1483
1651
  }
1484
-
1485
- return rescaleLayout(finalPos, scale, center);
1652
+
1653
+ return rescaleLayout(finalPos, scale, center) as PositionMap;
1486
1654
  }
1487
1655
 
1488
1656
  /**
1489
1657
  * Compute all-pairs shortest path distances for the graph
1490
1658
  *
1491
- * @param {Object} G - NetworkX graph
1492
- * @param {string} weight - Edge attribute for weight
1493
- * @returns {Object} Dictionary of dictionaries of shortest path distances
1659
+ * @param G - NetworkX graph
1660
+ * @param weight - Edge attribute for weight
1661
+ * @returns Dictionary of dictionaries of shortest path distances
1494
1662
  */
1495
- function _compute_shortest_path_distances(G, weight) {
1496
- const distances = {};
1497
- const nodes = G.nodes ? G.nodes() : G;
1498
- const edges = G.edges ? G.edges() : [];
1499
-
1663
+ function _computeShortestPathDistances(
1664
+ G: Graph,
1665
+ weight: string
1666
+ ): DistanceMap {
1667
+ const distances: DistanceMap = {};
1668
+ const nodes = G.nodes ? G.nodes() : G as Node[];
1669
+ const edges = G.edges ? G.edges() : [] as Edge[];
1670
+
1500
1671
  // Initialize distances with direct edges
1501
1672
  for (const node of nodes) {
1502
1673
  distances[node] = {};
1503
1674
  distances[node][node] = 0;
1504
-
1675
+
1505
1676
  for (const other of nodes) {
1506
1677
  if (node !== other) {
1507
1678
  distances[node][other] = Infinity;
1508
1679
  }
1509
1680
  }
1510
1681
  }
1511
-
1682
+
1512
1683
  // Add direct edges
1513
1684
  for (const [source, target] of edges) {
1514
1685
  // In a real implementation, we would get the weight from the graph
1515
- // For now, assume weight = 1
1516
- distances[source][target] = 1;
1517
- distances[target][source] = 1; // Assuming undirected graph
1686
+ // For now, assume weight = 1 or use weight attribute if available
1687
+ let edgeWeight = 1;
1688
+ if (G.getEdgeData) {
1689
+ edgeWeight = G.getEdgeData(source, target, weight) || 1;
1690
+ }
1691
+
1692
+ distances[source][target] = edgeWeight;
1693
+ distances[target][source] = edgeWeight; // Assuming undirected graph
1518
1694
  }
1519
-
1695
+
1520
1696
  // Floyd-Warshall algorithm for all-pairs shortest paths
1521
1697
  for (const k of nodes) {
1522
1698
  for (const i of nodes) {
@@ -1527,113 +1703,122 @@ function _compute_shortest_path_distances(G, weight) {
1527
1703
  }
1528
1704
  }
1529
1705
  }
1530
-
1706
+
1531
1707
  return distances;
1532
1708
  }
1533
1709
 
1534
1710
  /**
1535
1711
  * Solve the Kamada-Kawai layout optimization problem
1536
1712
  *
1537
- * @param {Array} distMatrix - Matrix of desired distances between nodes
1538
- * @param {Array} positions - Initial node positions
1539
- * @param {number} dim - Dimension of layout
1540
- * @returns {Array} Optimized node positions
1713
+ * @param distMatrix - Matrix of desired distances between nodes
1714
+ * @param positions - Initial node positions
1715
+ * @param dim - Dimension of layout
1716
+ * @returns Optimized node positions
1541
1717
  */
1542
- function _kamadaKawaiSolve(distMatrix, positions, dim) {
1718
+ function _kamadaKawaiSolve(
1719
+ distMatrix: number[][],
1720
+ positions: number[][],
1721
+ dim: number
1722
+ ): number[][] {
1543
1723
  // Implementation of L-BFGS optimization for Kamada-Kawai
1544
1724
  const nNodes = positions.length;
1545
1725
  const meanWeight = 1e-3;
1546
-
1726
+
1547
1727
  // Convert distances to inverse distances (with protection against division by zero)
1548
- const invDistMatrix = distMatrix.map(row =>
1728
+ const invDistMatrix = distMatrix.map(row =>
1549
1729
  row.map(d => d === 0 ? 0 : 1 / (d + 1e-3))
1550
1730
  );
1551
-
1731
+
1552
1732
  // Flatten positions for optimization
1553
1733
  let posVec = positions.flat();
1554
-
1734
+
1555
1735
  // Optimization parameters
1556
1736
  const maxIter = 500;
1557
1737
  const gtol = 1e-5;
1558
1738
  const m = 10; // L-BFGS memory size
1559
-
1739
+
1560
1740
  // Implement a simplified L-BFGS-B algorithm
1561
1741
  let alpha = 1.0;
1562
- const oldValues = [];
1563
- const oldGrads = [];
1564
-
1742
+ const oldValues: number[][] = [];
1743
+ const oldGrads: number[][] = [];
1744
+
1565
1745
  for (let iter = 0; iter < maxIter; iter++) {
1566
1746
  // Calculate cost and gradient
1567
1747
  const [cost, grad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
1568
-
1748
+
1569
1749
  // Compute search direction using L-BFGS approximation
1570
1750
  const direction = _lbfgsDirection(grad, oldValues, oldGrads, m);
1571
-
1751
+
1572
1752
  // Simple line search for step size
1573
1753
  alpha = _backtrackingLineSearch(
1574
- posVec, direction, cost, grad,
1575
- (x) => _kamadaKawaiCostfn(x, invDistMatrix, meanWeight, dim)[0],
1754
+ posVec, direction, cost, grad,
1755
+ (x: number[]) => _kamadaKawaiCostfn(x, invDistMatrix, meanWeight, dim)[0],
1576
1756
  alpha
1577
1757
  );
1578
-
1758
+
1579
1759
  // Save current position and gradient for next iteration
1580
1760
  const oldPos = [...posVec];
1581
-
1761
+
1582
1762
  // Update position
1583
1763
  for (let i = 0; i < posVec.length; i++) {
1584
1764
  posVec[i] += alpha * direction[i];
1585
1765
  }
1586
-
1766
+
1587
1767
  // Calculate new gradient
1588
1768
  const [, newGrad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
1589
-
1769
+
1590
1770
  // Update L-BFGS memory
1591
1771
  oldValues.push(posVec.map((val, i) => val - oldPos[i]));
1592
1772
  oldGrads.push(newGrad.map((val, i) => val - grad[i]));
1593
-
1773
+
1594
1774
  // Keep only m most recent updates
1595
1775
  if (oldValues.length > m) {
1596
1776
  oldValues.shift();
1597
1777
  oldGrads.shift();
1598
1778
  }
1599
-
1779
+
1600
1780
  // Check convergence
1601
1781
  const gradNorm = Math.sqrt(newGrad.reduce((sum, g) => sum + g * g, 0));
1602
1782
  if (gradNorm < gtol) {
1603
1783
  break;
1604
1784
  }
1605
1785
  }
1606
-
1786
+
1607
1787
  // Reshape result back into positions array
1608
- const result = [];
1788
+ const result: number[][] = [];
1609
1789
  for (let i = 0; i < nNodes; i++) {
1610
1790
  result.push(posVec.slice(i * dim, (i + 1) * dim));
1611
1791
  }
1612
-
1792
+
1613
1793
  return result;
1614
1794
  }
1615
1795
 
1616
1796
  /**
1617
1797
  * Cost function and gradient for Kamada-Kawai layout algorithm
1618
1798
  *
1619
- * @param {Array} posVec - Flattened position array
1620
- * @param {Array} invDist - Inverse distance matrix
1621
- * @param {number} meanWeight - Weight for centering positions
1622
- * @param {number} dim - Dimension of layout
1623
- * @returns {Array} Array with [cost, gradient]
1799
+ * @param posVec - Flattened position array
1800
+ * @param invDist - Inverse distance matrix
1801
+ * @param meanWeight - Weight for centering positions
1802
+ * @param dim - Dimension of layout
1803
+ * @returns Array with [cost, gradient]
1624
1804
  */
1625
- function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
1805
+ function _kamadaKawaiCostfn(
1806
+ posVec: number[],
1807
+ invDist: number[][],
1808
+ meanWeight: number,
1809
+ dim: number
1810
+ ): [number, number[]] {
1626
1811
  const nNodes = invDist.length;
1627
- const positions = [];
1628
-
1812
+ const positions: number[][] = [];
1813
+
1629
1814
  // Reshape flat vector into positions array
1630
1815
  for (let i = 0; i < nNodes; i++) {
1631
1816
  positions.push(posVec.slice(i * dim, (i + 1) * dim));
1632
1817
  }
1633
-
1818
+
1634
1819
  // Calculate cost
1635
1820
  let cost = 0;
1636
-
1821
+
1637
1822
  // Add mean position penalty term
1638
1823
  const sumPos = Array(dim).fill(0);
1639
1824
  for (let i = 0; i < nNodes; i++) {
@@ -1642,31 +1827,31 @@ function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
1642
1827
  }
1643
1828
  }
1644
1829
  cost += 0.5 * meanWeight * sumPos.reduce((sum, val) => sum + val * val, 0);
1645
-
1830
+
1646
1831
  // Add distance penalty terms
1647
1832
  for (let i = 0; i < nNodes; i++) {
1648
1833
  for (let j = i + 1; j < nNodes; j++) {
1649
1834
  // Calculate actual distance
1650
1835
  const diff = positions[i].map((val, d) => val - positions[j][d]);
1651
1836
  const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0));
1652
-
1837
+
1653
1838
  // Add penalty for difference between actual and ideal distance
1654
1839
  const idealInvDist = invDist[i][j];
1655
1840
  const offset = distance * idealInvDist - 1.0;
1656
1841
  cost += 0.5 * offset * offset;
1657
1842
  }
1658
1843
  }
1659
-
1844
+
1660
1845
  // Calculate gradient
1661
1846
  const grad = new Array(posVec.length).fill(0);
1662
-
1847
+
1663
1848
  // Add gradient of mean position penalty
1664
1849
  for (let i = 0; i < nNodes; i++) {
1665
1850
  for (let d = 0; d < dim; d++) {
1666
1851
  grad[i * dim + d] += meanWeight * sumPos[d];
1667
1852
  }
1668
1853
  }
1669
-
1854
+
1670
1855
  // Add gradient of distance penalties
1671
1856
  for (let i = 0; i < nNodes; i++) {
1672
1857
  for (let j = i + 1; j < nNodes; j++) {
@@ -1674,11 +1859,11 @@ function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
1674
1859
  const diff = positions[i].map((val, d) => val - positions[j][d]);
1675
1860
  const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 1e-10;
1676
1861
  const direction = diff.map(d => d / distance);
1677
-
1862
+
1678
1863
  // Calculate contribution to gradient
1679
1864
  const idealInvDist = invDist[i][j];
1680
1865
  const offset = distance * idealInvDist - 1.0;
1681
-
1866
+
1682
1867
  for (let d = 0; d < dim; d++) {
1683
1868
  const force = idealInvDist * offset * direction[d];
1684
1869
  grad[i * dim + d] += force;
@@ -1686,36 +1871,41 @@ function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
1686
1871
  }
1687
1872
  }
1688
1873
  }
1689
-
1874
+
1690
1875
  return [cost, grad];
1691
1876
  }
1692
1877
 
1693
1878
  /**
1694
1879
  * Compute the search direction using L-BFGS approximation
1695
1880
  *
1696
- * @param {Array} grad - Current gradient
1697
- * @param {Array} sList - List of position differences (s_k)
1698
- * @param {Array} yList - List of gradient differences (y_k)
1699
- * @param {number} m - Memory size
1700
- * @returns {Array} Direction vector
1881
+ * @param grad - Current gradient
1882
+ * @param sList - List of position differences (s_k)
1883
+ * @param yList - List of gradient differences (y_k)
1884
+ * @param m - Memory size
1885
+ * @returns Direction vector
1701
1886
  */
1702
- function _lbfgsDirection(grad, sList, yList, m) {
1887
+ function _lbfgsDirection(
1888
+ grad: number[],
1889
+ sList: number[][],
1890
+ yList: number[][],
1891
+ m: number
1892
+ ): number[] {
1703
1893
  if (sList.length === 0) {
1704
1894
  // First iteration - use negative gradient
1705
1895
  return grad.map(g => -g);
1706
1896
  }
1707
-
1897
+
1708
1898
  const q = grad.slice();
1709
1899
  const alpha = Array(sList.length).fill(0);
1710
- const rho = [];
1711
-
1900
+ const rho: number[] = [];
1901
+
1712
1902
  // Compute rho values
1713
1903
  for (let i = 0; i < sList.length; i++) {
1714
1904
  const s = sList[i];
1715
1905
  const y = yList[i];
1716
1906
  rho.push(1 / y.reduce((sum, val, j) => sum + val * s[j], 0));
1717
1907
  }
1718
-
1908
+
1719
1909
  // Forward pass
1720
1910
  for (let i = sList.length - 1; i >= 0; i--) {
1721
1911
  const s = sList[i];
@@ -1724,19 +1914,19 @@ function _lbfgsDirection(grad, sList, yList, m) {
1724
1914
  q[j] -= alpha[i] * yList[i][j];
1725
1915
  }
1726
1916
  }
1727
-
1917
+
1728
1918
  // Scale initial Hessian approximation
1729
1919
  let gamma = 1;
1730
1920
  if (sList.length > 0 && yList.length > 0) {
1731
1921
  const y = yList[yList.length - 1];
1732
1922
  const s = sList[sList.length - 1];
1733
- gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) /
1734
- y.reduce((sum, val) => sum + val * val, 0);
1923
+ gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) /
1924
+ y.reduce((sum, val) => sum + val * val, 0);
1735
1925
  }
1736
-
1926
+
1737
1927
  // Initialize direction with scaled negative gradient
1738
1928
  const direction = q.map(val => -gamma * val);
1739
-
1929
+
1740
1930
  // Backward pass
1741
1931
  for (let i = 0; i < sList.length; i++) {
1742
1932
  const s = sList[i];
@@ -1746,204 +1936,218 @@ function _lbfgsDirection(grad, sList, yList, m) {
1746
1936
  direction[j] += s[j] * (alpha[i] - beta);
1747
1937
  }
1748
1938
  }
1749
-
1939
+
1750
1940
  return direction;
1751
1941
  }
1752
1942
 
1753
1943
  /**
1754
1944
  * Backtracking line search to find step size
1755
1945
  *
1756
- * @param {Array} x - Current position
1757
- * @param {Array} direction - Search direction
1758
- * @param {number} f - Function value at current position
1759
- * @param {Array} grad - Gradient at current position
1760
- * @param {Function} func - Function to evaluate cost
1761
- * @param {number} alpha0 - Initial step size
1762
- * @returns {number} Optimal step size
1946
+ * @param x - Current position
1947
+ * @param direction - Search direction
1948
+ * @param f - Function value at current position
1949
+ * @param grad - Gradient at current position
1950
+ * @param func - Function to evaluate cost
1951
+ * @param alpha0 - Initial step size
1952
+ * @returns Optimal step size
1763
1953
  */
1764
- function _backtrackingLineSearch(x, direction, f, grad, func, alpha0) {
1954
+ function _backtrackingLineSearch(
1955
+ x: number[],
1956
+ direction: number[],
1957
+ f: number,
1958
+ grad: number[],
1959
+ func: (x: number[]) => number,
1960
+ alpha0: number
1961
+ ): number {
1765
1962
  const c1 = 1e-4;
1766
1963
  const c2 = 0.9;
1767
1964
  const initialSlope = grad.reduce((sum, g, i) => sum + g * direction[i], 0);
1768
-
1965
+
1769
1966
  if (initialSlope >= 0) {
1770
1967
  return 1e-8; // Direction is not a descent direction
1771
1968
  }
1772
-
1969
+
1773
1970
  let alpha = alpha0;
1774
1971
  const maxIter = 20;
1775
-
1972
+
1776
1973
  for (let i = 0; i < maxIter; i++) {
1777
1974
  // Try step
1778
1975
  const newX = x.map((val, i) => val + alpha * direction[i]);
1779
1976
  const newF = func(newX);
1780
-
1977
+
1781
1978
  // Check sufficient decrease condition (Armijo condition)
1782
1979
  if (newF <= f + c1 * alpha * initialSlope) {
1783
1980
  return alpha;
1784
1981
  }
1785
-
1982
+
1786
1983
  // Reduce step size
1787
1984
  alpha *= c2;
1788
1985
  }
1789
-
1986
+
1790
1987
  return alpha; // Return last alpha even if not optimal
1791
1988
  }
1792
1989
 
1793
1990
  /**
1794
1991
  * Position nodes using the ForceAtlas2 force-directed algorithm.
1795
1992
  *
1796
- * @param {Object} G - Graph
1797
- * @param {Object} pos - Initial positions for nodes
1798
- * @param {number} maxIter - Maximum number of iterations
1799
- * @param {number} jitterTolerance - Controls tolerance for node speed adjustments
1800
- * @param {number} scalingRatio - Scaling of attraction and repulsion forces
1801
- * @param {number} gravity - Attraction to center to prevent disconnected components from drifting
1802
- * @param {boolean} distributedAction - Distributes attraction force among nodes
1803
- * @param {boolean} strongGravity - Uses a stronger gravity model
1804
- * @param {Object} nodeMass - Dictionary mapping nodes to their masses
1805
- * @param {Object} nodeSize - Dictionary mapping nodes to their sizes
1806
- * @param {string} weight - Edge attribute for weight
1807
- * @param {boolean} dissuadeHubs - Whether to prevent hub nodes from clustering
1808
- * @param {boolean} linlog - Whether to use logarithmic attraction
1809
- * @param {number} seed - Random seed for initial positions
1810
- * @param {number} dim - Dimension of layout
1811
- * @returns {Object} Positions dictionary keyed by node
1993
+ * @param G - Graph
1994
+ * @param pos - Initial positions for nodes
1995
+ * @param maxIter - Maximum number of iterations
1996
+ * @param jitterTolerance - Controls tolerance for node speed adjustments
1997
+ * @param scalingRatio - Scaling of attraction and repulsion forces
1998
+ * @param gravity - Attraction to center to prevent disconnected components from drifting
1999
+ * @param distributedAction - Distributes attraction force among nodes
2000
+ * @param strongGravity - Uses a stronger gravity model
2001
+ * @param nodeMass - Dictionary mapping nodes to their masses
2002
+ * @param nodeSize - Dictionary mapping nodes to their sizes
2003
+ * @param weight - Edge attribute for weight
2004
+ * @param dissuadeHubs - Whether to prevent hub nodes from clustering
2005
+ * @param linlog - Whether to use logarithmic attraction
2006
+ * @param seed - Random seed for initial positions
2007
+ * @param dim - Dimension of layout
2008
+ * @returns Positions dictionary keyed by node
1812
2009
  */
1813
2010
  function forceatlas2Layout(
1814
- G,
1815
- pos = null,
1816
- maxIter = 100,
1817
- jitterTolerance = 1.0,
1818
- scalingRatio = 2.0,
1819
- gravity = 1.0,
1820
- distributedAction = false,
1821
- strongGravity = false,
1822
- nodeMass = null,
1823
- nodeSize = null,
1824
- weight = null,
1825
- dissuadeHubs = false,
1826
- linlog = false,
1827
- seed = null,
1828
- dim = 2
1829
- ) {
2011
+ G: Graph,
2012
+ pos: PositionMap | null = null,
2013
+ maxIter: number = 100,
2014
+ jitterTolerance: number = 1.0,
2015
+ scalingRatio: number = 2.0,
2016
+ gravity: number = 1.0,
2017
+ distributedAction: boolean = false,
2018
+ strongGravity: boolean = false,
2019
+ nodeMass: Record<Node, number> | null = null,
2020
+ nodeSize: Record<Node, number> | null = null,
2021
+ weight: string | null = null,
2022
+ dissuadeHubs: boolean = false,
2023
+ linlog: boolean = false,
2024
+ seed: number | null = null,
2025
+ dim: number = 2
2026
+ ): PositionMap {
1830
2027
  const processed = _processParams(G, null, dim);
1831
2028
  const graph = processed.G;
1832
-
1833
- const nodes = graph.nodes ? graph.nodes() : graph;
1834
-
2029
+
2030
+ const nodes = getNodesFromGraph(graph);
2031
+
1835
2032
  if (nodes.length === 0) {
1836
2033
  return {};
1837
2034
  }
1838
-
2035
+
1839
2036
  // Initialize random number generator
1840
- const rng = new RandomNumberGenerator(seed);
1841
-
2037
+ const rng = new RandomNumberGenerator(seed ?? undefined);
2038
+
1842
2039
  // Initialize positions if not provided
1843
- let pos_arr;
2040
+ let posArray: number[][];
1844
2041
  if (pos === null) {
1845
2042
  pos = {};
1846
- pos_arr = new Array(nodes.length);
2043
+ posArray = new Array(nodes.length);
1847
2044
  for (let i = 0; i < nodes.length; i++) {
1848
- pos_arr[i] = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
1849
- pos[nodes[i]] = pos_arr[i];
2045
+ posArray[i] = Array(dim).fill(0).map(() => rng.rand() as number * 2 - 1);
2046
+ pos[nodes[i]] = posArray[i];
1850
2047
  }
1851
2048
  } else if (Object.keys(pos).length === nodes.length) {
1852
2049
  // Use provided positions
1853
- pos_arr = new Array(nodes.length);
2050
+ posArray = new Array(nodes.length);
1854
2051
  for (let i = 0; i < nodes.length; i++) {
1855
- pos_arr[i] = [...pos[nodes[i]]];
2052
+ posArray[i] = [...pos[nodes[i]]];
1856
2053
  }
1857
2054
  } else {
1858
2055
  // Some nodes don't have positions, initialize within the range of existing positions
1859
- let min_pos = Array(dim).fill(Number.POSITIVE_INFINITY);
1860
- let max_pos = Array(dim).fill(Number.NEGATIVE_INFINITY);
1861
-
2056
+ let minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
2057
+ let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
2058
+
1862
2059
  // Find min and max of existing positions
1863
2060
  for (const node in pos) {
1864
2061
  for (let d = 0; d < dim; d++) {
1865
- min_pos[d] = Math.min(min_pos[d], pos[node][d]);
1866
- max_pos[d] = Math.max(max_pos[d], pos[node][d]);
2062
+ minPos[d] = Math.min(minPos[d], pos[node][d]);
2063
+ maxPos[d] = Math.max(maxPos[d], pos[node][d]);
1867
2064
  }
1868
2065
  }
1869
-
1870
- pos_arr = new Array(nodes.length);
2066
+
2067
+ posArray = new Array(nodes.length);
1871
2068
  for (let i = 0; i < nodes.length; i++) {
1872
2069
  const node = nodes[i];
1873
2070
  if (pos[node]) {
1874
- pos_arr[i] = [...pos[node]];
2071
+ posArray[i] = [...pos[node]];
1875
2072
  } else {
1876
- pos_arr[i] = Array(dim).fill(0).map((_, d) =>
1877
- min_pos[d] + rng.rand() * (max_pos[d] - min_pos[d])
2073
+ posArray[i] = Array(dim).fill(0).map((_, d) =>
2074
+ minPos[d] + (rng.rand() as number) * (maxPos[d] - minPos[d])
1878
2075
  );
1879
- pos[node] = pos_arr[i];
2076
+ pos[node] = posArray[i];
1880
2077
  }
1881
2078
  }
1882
2079
  }
1883
-
2080
+
1884
2081
  // Initialize mass and size arrays
1885
2082
  const mass = new Array(nodes.length).fill(0);
1886
2083
  const size = new Array(nodes.length).fill(0);
1887
-
2084
+
1888
2085
  // Flag to track whether to adjust for node sizes
1889
2086
  const adjustSizes = nodeSize !== null;
1890
-
2087
+
1891
2088
  // Set node masses and sizes
1892
2089
  for (let i = 0; i < nodes.length; i++) {
1893
2090
  const node = nodes[i];
1894
- mass[i] = nodeMass && nodeMass[node] ?
1895
- nodeMass[node] :
2091
+ mass[i] = nodeMass && nodeMass[node] ?
2092
+ nodeMass[node] :
1896
2093
  (graph.edges ? getNodeDegree(graph, node) + 1 : 1);
1897
-
2094
+
1898
2095
  size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
1899
2096
  }
1900
-
2097
+
1901
2098
  // Create adjacency matrix
1902
2099
  const n = nodes.length;
1903
- const A = Array(n).fill().map(() => Array(n).fill(0));
1904
-
2100
+ const A = Array(n).fill(0).map(() => Array(n).fill(0));
2101
+
1905
2102
  // Populate adjacency matrix with edge weights
1906
- const edges = graph.edges ? graph.edges() : [];
1907
- const nodeIndices = {};
2103
+ const edges = graph.edges ? graph.edges() : [] as Edge[];
2104
+ const nodeIndices: Record<Node, number> = {};
1908
2105
  nodes.forEach((node, i) => { nodeIndices[node] = i; });
1909
-
2106
+
1910
2107
  for (const [source, target] of edges) {
1911
2108
  const i = nodeIndices[source];
1912
2109
  const j = nodeIndices[target];
1913
-
2110
+
1914
2111
  // Use edge weight if provided, otherwise default to 1
1915
2112
  let edgeWeight = 1;
1916
2113
  if (weight && graph.getEdgeData) {
1917
2114
  edgeWeight = graph.getEdgeData(source, target, weight) || 1;
1918
2115
  }
1919
-
2116
+
1920
2117
  A[i][j] = edgeWeight;
1921
2118
  A[j][i] = edgeWeight; // For undirected graphs
1922
2119
  }
1923
-
2120
+
1924
2121
  // Initialize force arrays
1925
- const gravities = Array(n).fill().map(() => Array(dim).fill(0));
1926
- const attraction = Array(n).fill().map(() => Array(dim).fill(0));
1927
- const repulsion = Array(n).fill().map(() => Array(dim).fill(0));
1928
-
2122
+ const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
2123
+ const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
2124
+ const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
2125
+
1929
2126
  // Simulation parameters
1930
2127
  let speed = 1;
1931
2128
  let speedEfficiency = 1;
1932
2129
  let swing = 1;
1933
2130
  let traction = 1;
1934
-
2131
+
1935
2132
  // Helper function to estimate factor for force scaling
1936
- function estimateFactor(n, swing, traction, speed, speedEfficiency, jitterTolerance) {
2133
+ function estimateFactor(
2134
+ n: number,
2135
+ swing: number,
2136
+ traction: number,
2137
+ speed: number,
2138
+ speedEfficiency: number,
2139
+ jitterTolerance: number
2140
+ ): [number, number] {
1937
2141
  // Optimal jitter parameters
1938
2142
  const optJitter = 0.05 * Math.sqrt(n);
1939
2143
  const minJitter = Math.sqrt(optJitter);
1940
2144
  const maxJitter = 10;
1941
2145
  const minSpeedEfficiency = 0.05;
1942
-
2146
+
1943
2147
  // Estimate jitter based on current state
1944
2148
  const other = Math.min(maxJitter, optJitter * traction / (n * n));
1945
2149
  let jitter = jitterTolerance * Math.max(minJitter, other);
1946
-
2150
+
1947
2151
  // Adjust speed efficiency based on swing/traction ratio
1948
2152
  if (swing / traction > 2.0) {
1949
2153
  if (speedEfficiency > minSpeedEfficiency) {
@@ -1951,12 +2155,12 @@ function forceatlas2Layout(
1951
2155
  }
1952
2156
  jitter = Math.max(jitter, jitterTolerance);
1953
2157
  }
1954
-
2158
+
1955
2159
  // Calculate target speed
1956
- let targetSpeed = swing === 0 ?
1957
- Number.POSITIVE_INFINITY :
2160
+ let targetSpeed = swing === 0 ?
2161
+ Number.POSITIVE_INFINITY :
1958
2162
  jitter * speedEfficiency * traction / swing;
1959
-
2163
+
1960
2164
  // Further adjust speed efficiency
1961
2165
  if (swing > jitter * traction) {
1962
2166
  if (speedEfficiency > minSpeedEfficiency) {
@@ -1965,14 +2169,14 @@ function forceatlas2Layout(
1965
2169
  } else if (speed < 1000) {
1966
2170
  speedEfficiency *= 1.3;
1967
2171
  }
1968
-
2172
+
1969
2173
  // Limit the speed increase
1970
2174
  const maxRise = 0.5;
1971
2175
  speed = speed + Math.min(targetSpeed - speed, maxRise * speed);
1972
-
2176
+
1973
2177
  return [speed, speedEfficiency];
1974
2178
  }
1975
-
2179
+
1976
2180
  // Main simulation loop
1977
2181
  for (let iter = 0; iter < maxIter; iter++) {
1978
2182
  // Reset forces for this iteration
@@ -1983,38 +2187,38 @@ function forceatlas2Layout(
1983
2187
  gravities[i][d] = 0;
1984
2188
  }
1985
2189
  }
1986
-
2190
+
1987
2191
  // Compute pairwise differences and distances
1988
- const diff = Array(n).fill().map(() =>
1989
- Array(n).fill().map(() => Array(dim).fill(0))
2192
+ const diff = Array(n).fill(0).map(() =>
2193
+ Array(n).fill(0).map(() => Array(dim).fill(0))
1990
2194
  );
1991
-
1992
- const distance = Array(n).fill().map(() => Array(n).fill(0));
1993
-
2195
+
2196
+ const distance = Array(n).fill(0).map(() => Array(n).fill(0));
2197
+
1994
2198
  for (let i = 0; i < n; i++) {
1995
2199
  for (let j = 0; j < n; j++) {
1996
2200
  if (i === j) continue;
1997
-
2201
+
1998
2202
  for (let d = 0; d < dim; d++) {
1999
- diff[i][j][d] = pos_arr[i][d] - pos_arr[j][d];
2203
+ diff[i][j][d] = posArray[i][d] - posArray[j][d];
2000
2204
  }
2001
-
2205
+
2002
2206
  distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
2003
2207
  // Prevent division by zero
2004
2208
  if (distance[i][j] < 0.01) distance[i][j] = 0.01;
2005
2209
  }
2006
2210
  }
2007
-
2211
+
2008
2212
  // Calculate attraction forces
2009
2213
  if (linlog) {
2010
2214
  // Logarithmic attraction model
2011
2215
  for (let i = 0; i < n; i++) {
2012
2216
  for (let j = 0; j < n; j++) {
2013
2217
  if (i === j || A[i][j] === 0) continue;
2014
-
2218
+
2015
2219
  const dist = distance[i][j];
2016
2220
  const factor = -Math.log(1 + dist) / dist * A[i][j];
2017
-
2221
+
2018
2222
  for (let d = 0; d < dim; d++) {
2019
2223
  const force = factor * diff[i][j][d];
2020
2224
  attraction[i][d] += force;
@@ -2026,7 +2230,7 @@ function forceatlas2Layout(
2026
2230
  for (let i = 0; i < n; i++) {
2027
2231
  for (let j = 0; j < n; j++) {
2028
2232
  if (i === j || A[i][j] === 0) continue;
2029
-
2233
+
2030
2234
  for (let d = 0; d < dim; d++) {
2031
2235
  const force = -diff[i][j][d] * A[i][j];
2032
2236
  attraction[i][d] += force;
@@ -2034,7 +2238,7 @@ function forceatlas2Layout(
2034
2238
  }
2035
2239
  }
2036
2240
  }
2037
-
2241
+
2038
2242
  // Apply distributed attraction if enabled
2039
2243
  if (distributedAction) {
2040
2244
  for (let i = 0; i < n; i++) {
@@ -2043,46 +2247,46 @@ function forceatlas2Layout(
2043
2247
  }
2044
2248
  }
2045
2249
  }
2046
-
2250
+
2047
2251
  // Calculate repulsion forces
2048
2252
  for (let i = 0; i < n; i++) {
2049
2253
  for (let j = 0; j < n; j++) {
2050
2254
  if (i === j) continue;
2051
-
2255
+
2052
2256
  let dist = distance[i][j];
2053
-
2257
+
2054
2258
  // Adjust distance for node sizes if needed
2055
2259
  if (adjustSizes) {
2056
2260
  dist -= size[i] - size[j];
2057
2261
  dist = Math.max(dist, 0.01); // Prevent negative or zero distances
2058
2262
  }
2059
-
2263
+
2060
2264
  const distSquared = dist * dist;
2061
2265
  const massProduct = mass[i] * mass[j];
2062
2266
  const factor = (massProduct / distSquared) * scalingRatio;
2063
-
2267
+
2064
2268
  for (let d = 0; d < dim; d++) {
2065
2269
  const direction = diff[i][j][d] / dist;
2066
2270
  repulsion[i][d] += direction * factor;
2067
2271
  }
2068
2272
  }
2069
2273
  }
2070
-
2274
+
2071
2275
  // Calculate gravity forces
2072
2276
  // First find the center of mass
2073
2277
  const centerOfMass = Array(dim).fill(0);
2074
2278
  for (let i = 0; i < n; i++) {
2075
2279
  for (let d = 0; d < dim; d++) {
2076
- centerOfMass[d] += pos_arr[i][d] / n;
2280
+ centerOfMass[d] += posArray[i][d] / n;
2077
2281
  }
2078
2282
  }
2079
-
2283
+
2080
2284
  for (let i = 0; i < n; i++) {
2081
2285
  const posCentered = Array(dim);
2082
2286
  for (let d = 0; d < dim; d++) {
2083
- posCentered[d] = pos_arr[i][d] - centerOfMass[d];
2287
+ posCentered[d] = posArray[i][d] - centerOfMass[d];
2084
2288
  }
2085
-
2289
+
2086
2290
  if (strongGravity) {
2087
2291
  // Strong gravity model
2088
2292
  for (let d = 0; d < dim; d++) {
@@ -2091,7 +2295,7 @@ function forceatlas2Layout(
2091
2295
  } else {
2092
2296
  // Regular gravity model
2093
2297
  const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
2094
-
2298
+
2095
2299
  if (dist > 0.01) {
2096
2300
  for (let d = 0; d < dim; d++) {
2097
2301
  const direction = posCentered[d] / dist;
@@ -2100,31 +2304,31 @@ function forceatlas2Layout(
2100
2304
  }
2101
2305
  }
2102
2306
  }
2103
-
2307
+
2104
2308
  // Calculate total forces and update positions
2105
- const update = Array(n).fill().map(() => Array(dim).fill(0));
2309
+ const update = Array(n).fill(0).map(() => Array(dim).fill(0));
2106
2310
  let totalSwing = 0;
2107
2311
  let totalTraction = 0;
2108
-
2312
+
2109
2313
  for (let i = 0; i < n; i++) {
2110
2314
  for (let d = 0; d < dim; d++) {
2111
2315
  update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
2112
2316
  }
2113
-
2317
+
2114
2318
  // Calculate swing and traction for this node
2115
- const oldPos = [...pos_arr[i]];
2319
+ const oldPos = [...posArray[i]];
2116
2320
  const newPos = oldPos.map((p, d) => p + update[i][d]);
2117
-
2321
+
2118
2322
  const swingVector = oldPos.map((p, d) => p - newPos[d]);
2119
2323
  const tractionVector = oldPos.map((p, d) => p + newPos[d]);
2120
-
2324
+
2121
2325
  const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
2122
2326
  const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
2123
-
2327
+
2124
2328
  totalSwing += mass[i] * swingMagnitude;
2125
2329
  totalTraction += 0.5 * mass[i] * tractionMagnitude;
2126
2330
  }
2127
-
2331
+
2128
2332
  // Update speed and efficiency
2129
2333
  [speed, speedEfficiency] = estimateFactor(
2130
2334
  n,
@@ -2134,18 +2338,18 @@ function forceatlas2Layout(
2134
2338
  speedEfficiency,
2135
2339
  jitterTolerance
2136
2340
  );
2137
-
2341
+
2138
2342
  // Apply forces to update positions
2139
2343
  let totalMovement = 0;
2140
-
2344
+
2141
2345
  for (let i = 0; i < n; i++) {
2142
2346
  let factor;
2143
-
2347
+
2144
2348
  if (adjustSizes) {
2145
2349
  // Calculate displacement magnitude
2146
2350
  const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
2147
2351
  const swinging = mass[i] * df;
2148
-
2352
+
2149
2353
  // Determine scaling factor with size adjustments
2150
2354
  factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
2151
2355
  factor = Math.min(factor * df, 10) / df;
@@ -2154,34 +2358,34 @@ function forceatlas2Layout(
2154
2358
  const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
2155
2359
  factor = speed / (1 + Math.sqrt(speed * swinging));
2156
2360
  }
2157
-
2361
+
2158
2362
  // Apply factor to update position
2159
2363
  for (let d = 0; d < dim; d++) {
2160
2364
  const movement = update[i][d] * factor;
2161
- pos_arr[i][d] += movement;
2365
+ posArray[i][d] += movement;
2162
2366
  totalMovement += Math.abs(movement);
2163
2367
  }
2164
2368
  }
2165
-
2369
+
2166
2370
  // Check for convergence
2167
2371
  if (totalMovement < 1e-10) {
2168
2372
  break;
2169
2373
  }
2170
2374
  }
2171
-
2375
+
2172
2376
  // Create position dictionary
2173
- const positions = {};
2377
+ const positions: PositionMap = {};
2174
2378
  for (let i = 0; i < n; i++) {
2175
- positions[nodes[i]] = pos_arr[i];
2379
+ positions[nodes[i]] = posArray[i];
2176
2380
  }
2177
-
2178
- return rescaleLayout(positions);
2179
-
2381
+
2382
+ return rescaleLayout(positions) as PositionMap;
2383
+
2180
2384
  // Helper function to get node degree
2181
- function getNodeDegree(graph, node) {
2385
+ function getNodeDegree(graph: Graph, node: Node): number {
2182
2386
  if (!graph.edges) return 0;
2183
-
2184
- return graph.edges().filter(edge =>
2387
+
2388
+ return graph.edges().filter((edge: Edge) =>
2185
2389
  edge[0] === node || edge[1] === node
2186
2390
  ).length;
2187
2391
  }
@@ -2190,158 +2394,168 @@ function forceatlas2Layout(
2190
2394
  /**
2191
2395
  * Layout algorithm with attractive and repulsive forces (ARF).
2192
2396
  *
2193
- * @param {Object} G - Graph
2194
- * @param {Object} pos - Initial positions for nodes
2195
- * @param {number} scaling - Scale factor for positions
2196
- * @param {number} a - Strength of springs between connected nodes (should be > 1)
2197
- * @param {number} maxIter - Maximum number of iterations
2198
- * @param {number|null} seed - Random seed for initial positions
2199
- * @returns {Object} Positions dictionary keyed by node
2397
+ * @param G - Graph
2398
+ * @param pos - Initial positions for nodes
2399
+ * @param scaling - Scale factor for positions
2400
+ * @param a - Strength of springs between connected nodes (should be > 1)
2401
+ * @param maxIter - Maximum number of iterations
2402
+ * @param seed - Random seed for initial positions
2403
+ * @returns Positions dictionary keyed by node
2200
2404
  */
2201
- function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1000, seed = null) {
2405
+ function arfLayout(
2406
+ G: Graph,
2407
+ pos: PositionMap | null = null,
2408
+ scaling: number = 1,
2409
+ a: number = 1.1,
2410
+ maxIter: number = 1000,
2411
+ seed: number | null = null
2412
+ ): PositionMap {
2202
2413
  if (a <= 1) {
2203
2414
  throw new Error("The parameter a should be larger than 1");
2204
2415
  }
2205
-
2206
- const nodes = G.nodes ? G.nodes() : G;
2207
- const edges = G.edges ? G.edges() : [];
2208
-
2416
+
2417
+ const nodes = getNodesFromGraph(G);
2418
+ const edges = getEdgesFromGraph(G);
2419
+
2209
2420
  if (nodes.length === 0) {
2210
2421
  return {};
2211
2422
  }
2212
-
2423
+
2213
2424
  // Initialize positions if not provided
2214
2425
  if (!pos) {
2215
2426
  pos = randomLayout(G, null, 2, seed);
2216
2427
  } else {
2217
2428
  // Make sure all nodes have positions
2218
- const rng = new RandomNumberGenerator(seed);
2219
- const defaultPos = nodes.reduce((acc, node) => {
2220
- if (!pos[node]) {
2221
- acc[node] = [rng.rand(), rng.rand()];
2429
+ const rng = new RandomNumberGenerator(seed ?? undefined);
2430
+ const defaultPos: PositionMap = {};
2431
+ nodes.forEach((node: Node) => {
2432
+ if (!pos![node]) {
2433
+ defaultPos[node] = [(rng.rand() as number), (rng.rand() as number)];
2222
2434
  }
2223
- return acc;
2224
- }, {});
2225
- pos = {...pos, ...defaultPos};
2435
+ });
2436
+ pos = { ...pos, ...defaultPos };
2226
2437
  }
2227
-
2438
+
2228
2439
  // Create node index mapping
2229
- const nodeIndex = {};
2230
- nodes.forEach((node, i) => {
2440
+ const nodeIndex: Record<Node, number> = {};
2441
+ nodes.forEach((node: Node, i: number) => {
2231
2442
  nodeIndex[node] = i;
2232
2443
  });
2233
-
2444
+
2234
2445
  // Create positions array
2235
- const positions = nodes.map(node => [...pos[node]]);
2236
-
2446
+ const positions: number[][] = nodes.map((node: Node) => [...pos![node]]);
2447
+
2237
2448
  // Initialize spring constant matrix
2238
2449
  const N = nodes.length;
2239
- const K = Array(N).fill().map(() => Array(N).fill(1));
2240
-
2450
+ const K = Array(N).fill(0).map(() => Array(N).fill(1));
2451
+
2241
2452
  // Set diagonal to zero (no self-attraction)
2242
2453
  for (let i = 0; i < N; i++) {
2243
2454
  K[i][i] = 0;
2244
2455
  }
2245
-
2456
+
2246
2457
  // Set stronger attraction between connected nodes
2247
2458
  for (const [source, target] of edges) {
2248
2459
  if (source === target) continue;
2249
-
2460
+
2250
2461
  const i = nodeIndex[source];
2251
2462
  const j = nodeIndex[target];
2252
2463
  K[i][j] = a;
2253
2464
  K[j][i] = a;
2254
2465
  }
2255
-
2466
+
2256
2467
  // Calculate rho (scale factor)
2257
2468
  const rho = scaling * Math.sqrt(N);
2258
-
2469
+
2259
2470
  // Optimization loop
2260
2471
  const dt = 1e-3; // Time step
2261
2472
  const etol = 1e-6; // Error tolerance
2262
2473
  let error = etol + 1;
2263
2474
  let nIter = 0;
2264
-
2475
+
2265
2476
  while (error > etol && nIter < maxIter) {
2266
2477
  // Calculate changes for each node
2267
- const change = Array(N).fill().map(() => [0, 0]);
2268
-
2478
+ const change = Array(N).fill(0).map(() => [0, 0]);
2479
+
2269
2480
  for (let i = 0; i < N; i++) {
2270
2481
  for (let j = 0; j < N; j++) {
2271
2482
  if (i === j) continue;
2272
-
2483
+
2273
2484
  // Calculate difference vector
2274
2485
  const diff = positions[i].map((coord, dim) => coord - positions[j][dim]);
2275
-
2486
+
2276
2487
  // Calculate distance (with minimum to avoid division by zero)
2277
2488
  const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01;
2278
-
2489
+
2279
2490
  // Calculate attractive and repulsive forces
2280
2491
  for (let d = 0; d < diff.length; d++) {
2281
2492
  change[i][d] += K[i][j] * diff[d] - (rho / dist) * diff[d];
2282
2493
  }
2283
2494
  }
2284
2495
  }
2285
-
2496
+
2286
2497
  // Update positions
2287
2498
  for (let i = 0; i < N; i++) {
2288
2499
  for (let d = 0; d < positions[i].length; d++) {
2289
2500
  positions[i][d] += change[i][d] * dt;
2290
2501
  }
2291
2502
  }
2292
-
2503
+
2293
2504
  // Calculate error (sum of force magnitudes)
2294
- error = change.reduce((sum, c) =>
2505
+ error = change.reduce((sum, c) =>
2295
2506
  sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
2296
-
2507
+
2297
2508
  nIter++;
2298
2509
  }
2299
-
2510
+
2300
2511
  // Convert positions array back to object
2301
- const finalPos = {};
2302
- nodes.forEach((node, i) => {
2512
+ const finalPos: PositionMap = {};
2513
+ nodes.forEach((node: Node, i: number) => {
2303
2514
  finalPos[node] = positions[i];
2304
2515
  });
2305
-
2516
+
2306
2517
  return finalPos;
2307
2518
  }
2308
2519
 
2309
2520
  /**
2310
2521
  * Return a dictionary of scaled positions keyed by node.
2311
2522
  *
2312
- * @param {Object} pos - Dictionary of positions keyed by node
2313
- * @param {number} scale - Scale factor for positions
2314
- * @returns {Object} Dictionary of scaled positions
2523
+ * @param pos - Dictionary of positions keyed by node
2524
+ * @param scale - Scale factor for positions
2525
+ * @returns Dictionary of scaled positions
2315
2526
  */
2316
- function rescaleLayoutDict(pos, scale = 1) {
2527
+ function rescaleLayoutDict(
2528
+ pos: PositionMap,
2529
+ scale: number = 1
2530
+ ): PositionMap {
2317
2531
  if (Object.keys(pos).length === 0) {
2318
2532
  return {};
2319
2533
  }
2320
-
2534
+
2321
2535
  // Extract positions as array
2322
2536
  const posArray = Object.values(pos);
2323
-
2537
+
2324
2538
  // Find center of positions
2325
- const center = [];
2539
+ const center: number[] = [];
2326
2540
  for (let d = 0; d < posArray[0].length; d++) {
2327
2541
  center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
2328
2542
  }
2329
-
2543
+
2330
2544
  // Center positions
2331
- const centeredPos = {};
2545
+ const centeredPos: PositionMap = {};
2332
2546
  for (const [node, p] of Object.entries(pos)) {
2333
2547
  centeredPos[node] = p.map((val, d) => val - center[d]);
2334
2548
  }
2335
-
2549
+
2336
2550
  // Find maximum distance from center
2337
2551
  let maxDist = 0;
2338
2552
  for (const p of Object.values(centeredPos)) {
2339
2553
  const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
2340
2554
  maxDist = Math.max(maxDist, dist);
2341
2555
  }
2342
-
2556
+
2343
2557
  // Scale positions
2344
- const scaledPos = {};
2558
+ const scaledPos: PositionMap = {};
2345
2559
  if (maxDist > 0) {
2346
2560
  for (const [node, p] of Object.entries(centeredPos)) {
2347
2561
  scaledPos[node] = p.map(val => val * scale / maxDist);
@@ -2352,10 +2566,277 @@ function rescaleLayoutDict(pos, scale = 1) {
2352
2566
  scaledPos[node] = Array(centeredPos[node].length).fill(0);
2353
2567
  }
2354
2568
  }
2355
-
2569
+
2356
2570
  return scaledPos;
2357
2571
  }
2358
2572
 
2573
+ // Graph generation utilities
2574
+ /**
2575
+ * Create a complete graph with n nodes
2576
+ * @param n - Number of nodes
2577
+ * @returns Graph object with all nodes connected to all other nodes
2578
+ */
2579
+ function completeGraph(n: number): Graph {
2580
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
2581
+ const edges: Edge[] = [];
2582
+
2583
+ for (let i = 0; i < n; i++) {
2584
+ for (let j = i + 1; j < n; j++) {
2585
+ edges.push([i, j]);
2586
+ }
2587
+ }
2588
+
2589
+ return {
2590
+ nodes: () => nodes,
2591
+ edges: () => edges
2592
+ };
2593
+ }
2594
+
2595
+ /**
2596
+ * Create a cycle graph with n nodes
2597
+ * @param n - Number of nodes
2598
+ * @returns Graph object with nodes connected in a cycle
2599
+ */
2600
+ function cycleGraph(n: number): Graph {
2601
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
2602
+ const edges: Edge[] = [];
2603
+
2604
+ for (let i = 0; i < n; i++) {
2605
+ edges.push([i, (i + 1) % n]);
2606
+ }
2607
+
2608
+ return {
2609
+ nodes: () => nodes,
2610
+ edges: () => edges
2611
+ };
2612
+ }
2613
+
2614
+ /**
2615
+ * Create a star graph with n nodes (1 center + n-1 leaves)
2616
+ * @param n - Total number of nodes
2617
+ * @returns Graph object with star topology
2618
+ */
2619
+ function starGraph(n: number): Graph {
2620
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
2621
+ const edges: Edge[] = [];
2622
+
2623
+ // Connect all nodes to node 0 (center)
2624
+ for (let i = 1; i < n; i++) {
2625
+ edges.push([0, i]);
2626
+ }
2627
+
2628
+ return {
2629
+ nodes: () => nodes,
2630
+ edges: () => edges
2631
+ };
2632
+ }
2633
+
2634
+ /**
2635
+ * Create a wheel graph with n nodes (1 center + n-1 rim nodes)
2636
+ * @param n - Total number of nodes
2637
+ * @returns Graph object with wheel topology
2638
+ */
2639
+ function wheelGraph(n: number): Graph {
2640
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
2641
+ const edges: Edge[] = [];
2642
+
2643
+ // Connect all rim nodes to center (node 0)
2644
+ for (let i = 1; i < n; i++) {
2645
+ edges.push([0, i]);
2646
+ }
2647
+
2648
+ // Connect rim nodes in a cycle
2649
+ for (let i = 1; i < n - 1; i++) {
2650
+ edges.push([i, i + 1]);
2651
+ }
2652
+ if (n > 2) {
2653
+ edges.push([n - 1, 1]);
2654
+ }
2655
+
2656
+ return {
2657
+ nodes: () => nodes,
2658
+ edges: () => edges
2659
+ };
2660
+ }
2661
+
2662
+ /**
2663
+ * Create a grid graph with rows x cols nodes
2664
+ * @param rows - Number of rows
2665
+ * @param cols - Number of columns
2666
+ * @returns Graph object with grid topology
2667
+ */
2668
+ function gridGraph(rows: number, cols: number): Graph {
2669
+ const nodes: Node[] = [];
2670
+ const edges: Edge[] = [];
2671
+
2672
+ // Create nodes
2673
+ for (let i = 0; i < rows; i++) {
2674
+ for (let j = 0; j < cols; j++) {
2675
+ nodes.push(`${i},${j}`);
2676
+ }
2677
+ }
2678
+
2679
+ // Create edges
2680
+ for (let i = 0; i < rows; i++) {
2681
+ for (let j = 0; j < cols; j++) {
2682
+ // Connect to right neighbor
2683
+ if (j < cols - 1) {
2684
+ edges.push([`${i},${j}`, `${i},${j + 1}`]);
2685
+ }
2686
+ // Connect to bottom neighbor
2687
+ if (i < rows - 1) {
2688
+ edges.push([`${i},${j}`, `${i + 1},${j}`]);
2689
+ }
2690
+ }
2691
+ }
2692
+
2693
+ return {
2694
+ nodes: () => nodes,
2695
+ edges: () => edges
2696
+ };
2697
+ }
2698
+
2699
+ /**
2700
+ * Create a random graph with n nodes and given edge probability
2701
+ * @param n - Number of nodes
2702
+ * @param p - Probability of edge between any two nodes (0-1)
2703
+ * @param seed - Random seed for reproducibility
2704
+ * @returns Graph object with random edges
2705
+ */
2706
+ function randomGraph(n: number, p: number, seed?: number): Graph {
2707
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
2708
+ const edges: Edge[] = [];
2709
+
2710
+ // Simple deterministic pseudo-random if seed provided
2711
+ let currentSeed = seed;
2712
+ let random = seed !== undefined
2713
+ ? () => {
2714
+ currentSeed = (currentSeed! * 9301 + 49297) % 233280;
2715
+ return currentSeed / 233280;
2716
+ }
2717
+ : Math.random;
2718
+
2719
+ for (let i = 0; i < n; i++) {
2720
+ for (let j = i + 1; j < n; j++) {
2721
+ if (random() < p) {
2722
+ edges.push([i, j]);
2723
+ }
2724
+ }
2725
+ }
2726
+
2727
+ return {
2728
+ nodes: () => nodes,
2729
+ edges: () => edges
2730
+ };
2731
+ }
2732
+
2733
+ /**
2734
+ * Create a bipartite graph with two sets of nodes
2735
+ * @param n1 - Number of nodes in first set
2736
+ * @param n2 - Number of nodes in second set
2737
+ * @param p - Probability of edge between nodes in different sets
2738
+ * @param seed - Random seed for reproducibility
2739
+ * @returns Graph object with bipartite structure and setA/setB properties
2740
+ */
2741
+ function bipartiteGraph(n1: number, n2: number, p: number, seed?: number): Graph & { setA: Node[], setB: Node[] } {
2742
+ const setA: Node[] = Array.from({ length: n1 }, (_, i) => `A${i}`);
2743
+ const setB: Node[] = Array.from({ length: n2 }, (_, i) => `B${i}`);
2744
+ const nodes = [...setA, ...setB];
2745
+ const edges: Edge[] = [];
2746
+
2747
+ // Simple deterministic pseudo-random if seed provided
2748
+ let currentSeed = seed;
2749
+ let random = seed !== undefined
2750
+ ? () => {
2751
+ currentSeed = (currentSeed! * 9301 + 49297) % 233280;
2752
+ return currentSeed / 233280;
2753
+ }
2754
+ : Math.random;
2755
+
2756
+ // Only connect nodes between sets
2757
+ for (const a of setA) {
2758
+ for (const b of setB) {
2759
+ if (random() < p) {
2760
+ edges.push([a, b]);
2761
+ }
2762
+ }
2763
+ }
2764
+
2765
+ return {
2766
+ nodes: () => nodes,
2767
+ edges: () => edges,
2768
+ setA,
2769
+ setB
2770
+ };
2771
+ }
2772
+
2773
+ /**
2774
+ * Create a scale-free graph using Barabási-Albert model
2775
+ * @param n - Total number of nodes
2776
+ * @param m - Number of edges to attach from new node
2777
+ * @param seed - Random seed for reproducibility
2778
+ * @returns Graph object with scale-free properties
2779
+ */
2780
+ function scaleFreeGraph(n: number, m: number, seed?: number): Graph {
2781
+ if (m >= n) {
2782
+ throw new Error('m must be less than n');
2783
+ }
2784
+
2785
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
2786
+ const edges: Edge[] = [];
2787
+ const degrees = new Array(n).fill(0);
2788
+
2789
+ // Simple deterministic pseudo-random if seed provided
2790
+ let currentSeed = seed;
2791
+ let random = seed !== undefined
2792
+ ? () => {
2793
+ currentSeed = (currentSeed! * 9301 + 49297) % 233280;
2794
+ return currentSeed / 233280;
2795
+ }
2796
+ : Math.random;
2797
+
2798
+ // Start with complete graph of m+1 nodes
2799
+ for (let i = 0; i <= m; i++) {
2800
+ for (let j = i + 1; j <= m; j++) {
2801
+ edges.push([i, j]);
2802
+ degrees[i]++;
2803
+ degrees[j]++;
2804
+ }
2805
+ }
2806
+
2807
+ // Add remaining nodes
2808
+ for (let i = m + 1; i < n; i++) {
2809
+ const targets = new Set<number>();
2810
+ const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
2811
+
2812
+ // Choose m targets based on preferential attachment
2813
+ while (targets.size < m) {
2814
+ let r = random() * totalDegree;
2815
+ let cumSum = 0;
2816
+
2817
+ for (let j = 0; j < i; j++) {
2818
+ cumSum += degrees[j];
2819
+ if (r <= cumSum && !targets.has(j)) {
2820
+ targets.add(j);
2821
+ break;
2822
+ }
2823
+ }
2824
+ }
2825
+
2826
+ // Add edges to targets
2827
+ for (const target of targets) {
2828
+ edges.push([i, target]);
2829
+ degrees[i]++;
2830
+ degrees[target]++;
2831
+ }
2832
+ }
2833
+
2834
+ return {
2835
+ nodes: () => nodes,
2836
+ edges: () => edges
2837
+ };
2838
+ }
2839
+
2359
2840
  // Export the layout functions
2360
2841
  export {
2361
2842
  randomLayout,
@@ -2373,5 +2854,14 @@ export {
2373
2854
  forceatlas2Layout,
2374
2855
  arfLayout,
2375
2856
  rescaleLayout,
2376
- rescaleLayoutDict
2857
+ rescaleLayoutDict,
2858
+ // Graph generation utilities
2859
+ completeGraph,
2860
+ cycleGraph,
2861
+ starGraph,
2862
+ wheelGraph,
2863
+ gridGraph,
2864
+ randomGraph,
2865
+ bipartiteGraph,
2866
+ scaleFreeGraph
2377
2867
  };