@graphty/layout 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.github/workflows/ci.yml +3 -7
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +1 -1
  4. package/dist/vitest.config.js +2 -2
  5. package/dist/vitest.config.js.map +1 -1
  6. package/examples/3d-kamada-kawai.html +4 -19
  7. package/examples/bfs-layout.html +26 -32
  8. package/examples/bipartite-layout.html +3 -3
  9. package/examples/forceatlas2-layout.html +106 -151
  10. package/{dist → examples}/layout-helpers.js +58 -30
  11. package/examples/multipartite-layout.html +32 -14
  12. package/examples/shell-layout.html +4 -2
  13. package/examples/spring-layout.html +1 -11
  14. package/package.json +3 -3
  15. package/src/algorithms/index.ts +6 -0
  16. package/src/algorithms/optimization/index.ts +12 -0
  17. package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
  18. package/src/algorithms/optimization/lbfgs.ts +68 -0
  19. package/src/algorithms/optimization/line-search.ts +50 -0
  20. package/src/algorithms/optimization/types.ts +8 -0
  21. package/src/algorithms/planarity/check.ts +38 -0
  22. package/src/algorithms/planarity/embedding.ts +216 -0
  23. package/src/algorithms/planarity/index.ts +12 -0
  24. package/src/algorithms/planarity/lr-test.ts +70 -0
  25. package/src/algorithms/planarity/special-graphs.ts +126 -0
  26. package/src/generators/basic.ts +93 -0
  27. package/src/generators/bipartite.ts +45 -0
  28. package/src/generators/grid.ts +42 -0
  29. package/src/generators/index.ts +15 -0
  30. package/src/generators/random.ts +39 -0
  31. package/src/generators/scale-free.ts +72 -0
  32. package/src/index.ts +18 -0
  33. package/src/layouts/basic/index.ts +5 -0
  34. package/src/layouts/basic/random.ts +32 -0
  35. package/src/layouts/force-directed/arf.ts +130 -0
  36. package/src/layouts/force-directed/forceatlas2.ts +407 -0
  37. package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
  38. package/src/layouts/force-directed/index.ts +9 -0
  39. package/src/layouts/force-directed/kamada-kawai.ts +112 -0
  40. package/src/layouts/force-directed/spring.ts +35 -0
  41. package/src/layouts/geometric/circular.ts +77 -0
  42. package/src/layouts/geometric/index.ts +7 -0
  43. package/src/layouts/geometric/shell.ts +80 -0
  44. package/src/layouts/geometric/spiral.ts +93 -0
  45. package/src/layouts/hierarchical/bfs.ts +81 -0
  46. package/src/layouts/hierarchical/bipartite.ts +94 -0
  47. package/src/layouts/hierarchical/index.ts +7 -0
  48. package/src/layouts/hierarchical/multipartite.ts +88 -0
  49. package/src/layouts/index.ts +9 -0
  50. package/src/layouts/specialized/index.ts +6 -0
  51. package/src/layouts/specialized/planar.ts +65 -0
  52. package/src/layouts/specialized/spectral.ts +128 -0
  53. package/src/types/embedding.ts +11 -0
  54. package/src/types/graph.ts +13 -0
  55. package/src/types/index.ts +7 -0
  56. package/src/types/layout.ts +9 -0
  57. package/src/utils/graph.ts +77 -0
  58. package/src/utils/index.ts +9 -0
  59. package/src/utils/numpy.ts +111 -0
  60. package/src/utils/params.ts +26 -0
  61. package/src/utils/random.ts +53 -0
  62. package/src/utils/rescale.ts +137 -0
  63. package/test/arf-layout.test.ts +1 -1
  64. package/test/bfs-layout.test.ts +1 -1
  65. package/test/bipartite-layout.test.ts +1 -1
  66. package/test/circular-layout.test.ts +1 -1
  67. package/test/forceatlas2-layout.test.ts +1 -1
  68. package/test/fruchterman-reingold-layout.test.ts +1 -1
  69. package/test/graph-generators.test.ts +1 -1
  70. package/test/kamada-kawai-layout.test.ts +1 -1
  71. package/test/multipartite-layout.test.ts +1 -1
  72. package/test/planar-layout.test.ts +1 -1
  73. package/test/random-layout.test.ts +1 -1
  74. package/test/rescale-layout.test.ts +1 -1
  75. package/test/shell-layout.test.ts +1 -1
  76. package/test/spectral-layout.test.ts +1 -1
  77. package/test/spiral-layout.test.ts +1 -1
  78. package/test/spring-layout.test.ts +1 -1
  79. package/test/test-utils.ts +34 -0
  80. package/test/utils-graph.test.ts +155 -0
  81. package/test/utils-index.test.ts +77 -0
  82. package/test/utils-numpy.test.ts +272 -0
  83. package/test/utils-params.test.ts +99 -0
  84. package/test/utils-random.test.ts +229 -0
  85. package/vitest.config.ts +2 -2
  86. package/dist/layout-helpers.d.ts +0 -123
  87. package/dist/layout-helpers.js.map +0 -1
  88. package/dist/layout.d.ts +0 -275
  89. package/dist/layout.js +0 -2304
  90. package/dist/layout.js.map +0 -1
  91. package/layout-helpers.ts +0 -560
  92. package/layout.ts +0 -2893
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Random number generator for seed-based randomization
3
+ * Maintains exact same functionality as original implementation
4
+ */
5
+
6
+ export class RandomNumberGenerator {
7
+ private seed: number;
8
+ private m: number;
9
+ private a: number;
10
+ private c: number;
11
+ private _state: number;
12
+
13
+ constructor(seed?: number) {
14
+ this.seed = seed || Math.floor(Math.random() * 1000000);
15
+ this.m = 2 ** 35 - 31;
16
+ this.a = 185852;
17
+ this.c = 1;
18
+ this._state = this.seed % this.m;
19
+ }
20
+
21
+ _next(): number {
22
+ this._state = (this.a * this._state + this.c) % this.m;
23
+ return this._state / this.m;
24
+ }
25
+
26
+ rand(shape: number | number[] | null = null): number | number[] | number[][] {
27
+ if (shape === null) {
28
+ return this._next();
29
+ }
30
+
31
+ if (typeof shape === 'number') {
32
+ const result: number[] = [];
33
+ for (let i = 0; i < shape; i++) {
34
+ result.push(this._next());
35
+ }
36
+ return result;
37
+ }
38
+
39
+ if (shape.length === 1) {
40
+ const result: number[] = [];
41
+ for (let i = 0; i < shape[0]; i++) {
42
+ result.push(this._next());
43
+ }
44
+ return result;
45
+ }
46
+
47
+ const result: any[] = [];
48
+ for (let i = 0; i < shape[0]; i++) {
49
+ result.push(this.rand(shape.slice(1)));
50
+ }
51
+ return result;
52
+ }
53
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Layout rescaling utilities
3
+ */
4
+
5
+ import { PositionMap } from '../types';
6
+
7
+ /**
8
+ * Returns scaled position array/dict to (-scale, scale) in all axes.
9
+ *
10
+ * @param pos - Position dictionary or array
11
+ * @param scale - Scale factor for positions
12
+ * @param center - Coordinate pair around which to center the layout
13
+ * @returns Rescaled positions dictionary
14
+ */
15
+ export function rescaleLayout(
16
+ pos: PositionMap | number[][],
17
+ scale: number = 1,
18
+ center: number[] = [0, 0]
19
+ ): PositionMap | number[][] {
20
+ // Check if pos is empty
21
+ if (Array.isArray(pos)) {
22
+ if (pos.length === 0) return [];
23
+ } else {
24
+ if (Object.keys(pos).length === 0) return {};
25
+ }
26
+
27
+ // Extract position values
28
+ const posValues: number[][] = Array.isArray(pos) ? pos : Object.values(pos);
29
+ const dim = posValues[0].length;
30
+
31
+ // Calculate center of positions
32
+ const posCenter = Array(dim).fill(0);
33
+ for (const p of posValues) {
34
+ for (let i = 0; i < dim; i++) {
35
+ posCenter[i] += p[i] / posValues.length;
36
+ }
37
+ }
38
+
39
+ // Center positions
40
+ let centeredPos: PositionMap | number[][] = {};
41
+ if (Array.isArray(pos)) {
42
+ centeredPos = pos.map(p => p.map((val, i) => val - posCenter[i]));
43
+ } else {
44
+ for (const [node, p] of Object.entries(pos)) {
45
+ (centeredPos as PositionMap)[node] = p.map((val, i) => val - posCenter[i]);
46
+ }
47
+ }
48
+
49
+ // Find maximum distance from center
50
+ let maxDistance = 0;
51
+ const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
52
+ for (const p of centeredValues) {
53
+ const distance = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
54
+ maxDistance = Math.max(maxDistance, distance);
55
+ }
56
+
57
+ // Rescale
58
+ let scaledPos: PositionMap | number[][] = Array.isArray(pos) ? [] : {};
59
+
60
+ if (maxDistance > 0) {
61
+ const scaleFactor = scale / maxDistance;
62
+
63
+ if (Array.isArray(pos)) {
64
+ (scaledPos as number[][]) = (centeredPos as number[][]).map(p =>
65
+ p.map((val, i) => val * scaleFactor + center[i])
66
+ );
67
+ } else {
68
+ for (const [node, p] of Object.entries(centeredPos as PositionMap)) {
69
+ (scaledPos as PositionMap)[node] = p.map((val, i) => val * scaleFactor + center[i]);
70
+ }
71
+ }
72
+ } else {
73
+ // All nodes at the same position
74
+ if (Array.isArray(pos)) {
75
+ (scaledPos as number[][]) = Array(pos.length).fill(0).map(() => [...center]);
76
+ } else {
77
+ for (const node of Object.keys(pos)) {
78
+ (scaledPos as PositionMap)[node] = [...center];
79
+ }
80
+ }
81
+ }
82
+
83
+ return scaledPos;
84
+ }
85
+
86
+ /**
87
+ * Return a dictionary of scaled positions centered at (0, 0).
88
+ *
89
+ * @param pos - Dictionary of positions
90
+ * @param scale - Scale factor for positions
91
+ * @returns Dictionary of scaled positions
92
+ */
93
+ export function rescaleLayoutDict(
94
+ pos: PositionMap,
95
+ scale: number = 1
96
+ ): PositionMap {
97
+ if (Object.keys(pos).length === 0) {
98
+ return {};
99
+ }
100
+
101
+ // Extract positions as array
102
+ const posArray = Object.values(pos);
103
+
104
+ // Find center of positions
105
+ const center: number[] = [];
106
+ for (let d = 0; d < posArray[0].length; d++) {
107
+ center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
108
+ }
109
+
110
+ // Center positions
111
+ const centeredPos: PositionMap = {};
112
+ for (const [node, p] of Object.entries(pos)) {
113
+ centeredPos[node] = p.map((val, d) => val - center[d]);
114
+ }
115
+
116
+ // Find maximum distance from center
117
+ let maxDist = 0;
118
+ for (const p of Object.values(centeredPos)) {
119
+ const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
120
+ maxDist = Math.max(maxDist, dist);
121
+ }
122
+
123
+ // Scale positions
124
+ const scaledPos: PositionMap = {};
125
+ if (maxDist > 0) {
126
+ for (const [node, p] of Object.entries(centeredPos)) {
127
+ scaledPos[node] = p.map(val => val * scale / maxDist);
128
+ }
129
+ } else {
130
+ // All points at the center
131
+ for (const node of Object.keys(centeredPos)) {
132
+ scaledPos[node] = Array(centeredPos[node].length).fill(0);
133
+ }
134
+ }
135
+
136
+ return scaledPos;
137
+ }
@@ -6,7 +6,7 @@ import {
6
6
  starGraph,
7
7
  gridGraph,
8
8
  randomGraph
9
- } from '../layout.ts';
9
+ } from '../src';
10
10
 
11
11
  describe('ARF Layout', () => {
12
12
  describe('Basic functionality', () => {
@@ -7,7 +7,7 @@ import {
7
7
  wheelGraph,
8
8
  gridGraph,
9
9
  randomGraph
10
- } from '../layout.ts';
10
+ } from '../src';
11
11
 
12
12
  describe('BFS Layout', () => {
13
13
  describe('Basic functionality', () => {
@@ -2,7 +2,7 @@ import { describe, it, assert } from 'vitest';
2
2
  import {
3
3
  bipartiteLayout,
4
4
  bipartiteGraph
5
- } from '../layout.ts';
5
+ } from '../src';
6
6
 
7
7
  describe('Bipartite Layout', () => {
8
8
  describe('Basic functionality', () => {
@@ -7,7 +7,7 @@ import {
7
7
  wheelGraph,
8
8
  gridGraph,
9
9
  randomGraph
10
- } from '../layout.js';
10
+ } from '../src';
11
11
 
12
12
  describe('Circular Layout', () => {
13
13
  describe('Basic functionality', () => {
@@ -7,7 +7,7 @@ import {
7
7
  gridGraph,
8
8
  randomGraph,
9
9
  scaleFreeGraph
10
- } from '../layout.ts';
10
+ } from '../src';
11
11
 
12
12
  describe('ForceAtlas2 Layout', () => {
13
13
  describe('Basic functionality', () => {
@@ -6,7 +6,7 @@ import {
6
6
  starGraph,
7
7
  gridGraph,
8
8
  randomGraph
9
- } from '../layout.ts';
9
+ } from '../src';
10
10
 
11
11
  describe('Fruchterman-Reingold Layout', () => {
12
12
  describe('Basic functionality', () => {
@@ -9,7 +9,7 @@ import {
9
9
  bipartiteGraph,
10
10
  scaleFreeGraph,
11
11
  circularLayout
12
- } from '../layout.ts';
12
+ } from '../src';
13
13
 
14
14
  describe('Graph Generators', () => {
15
15
  describe('completeGraph', () => {
@@ -6,7 +6,7 @@ import {
6
6
  starGraph,
7
7
  gridGraph,
8
8
  randomGraph
9
- } from '../layout.js';
9
+ } from '../src';
10
10
 
11
11
  describe('Kamada-Kawai Layout', () => {
12
12
  describe('Basic functionality', () => {
@@ -4,7 +4,7 @@ import {
4
4
  completeGraph,
5
5
  cycleGraph,
6
6
  randomGraph
7
- } from '../layout.ts';
7
+ } from '../src';
8
8
 
9
9
  describe('Multipartite Layout', () => {
10
10
  describe('Basic functionality', () => {
@@ -6,7 +6,7 @@ import {
6
6
  starGraph,
7
7
  gridGraph,
8
8
  wheelGraph
9
- } from '../layout.ts';
9
+ } from '../src';
10
10
 
11
11
  describe('Planar Layout', () => {
12
12
  describe('Basic functionality', () => {
@@ -5,7 +5,7 @@ import {
5
5
  cycleGraph,
6
6
  starGraph,
7
7
  gridGraph
8
- } from '../layout.ts';
8
+ } from '../src';
9
9
 
10
10
  describe('Random Layout', () => {
11
11
  describe('Basic functionality', () => {
@@ -6,7 +6,7 @@ import {
6
6
  randomLayout,
7
7
  completeGraph,
8
8
  starGraph
9
- } from '../layout.ts';
9
+ } from '../src';
10
10
 
11
11
  describe('Rescale Layout', () => {
12
12
  describe('Dictionary format (rescaleLayout)', () => {
@@ -7,7 +7,7 @@ import {
7
7
  wheelGraph,
8
8
  gridGraph,
9
9
  randomGraph
10
- } from '../layout.ts';
10
+ } from '../src';
11
11
 
12
12
  describe('Shell Layout', () => {
13
13
  describe('Basic functionality', () => {
@@ -6,7 +6,7 @@ import {
6
6
  starGraph,
7
7
  gridGraph,
8
8
  randomGraph
9
- } from '../layout.ts';
9
+ } from '../src';
10
10
 
11
11
  describe('Spectral Layout', () => {
12
12
  describe('Basic functionality', () => {
@@ -6,7 +6,7 @@ import {
6
6
  starGraph,
7
7
  gridGraph,
8
8
  randomGraph
9
- } from '../layout.ts';
9
+ } from '../src';
10
10
 
11
11
  describe('Spiral Layout', () => {
12
12
  describe('Basic functionality', () => {
@@ -5,7 +5,7 @@ import {
5
5
  cycleGraph,
6
6
  starGraph,
7
7
  randomGraph
8
- } from '../layout.ts';
8
+ } from '../src';
9
9
 
10
10
  describe('Spring Layout', () => {
11
11
  describe('Basic functionality', () => {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Test utilities and helper functions
3
+ */
4
+
5
+ import { Graph, Node, Edge } from '../src/types';
6
+
7
+ /**
8
+ * Create a simple test graph for testing
9
+ */
10
+ export function createTestGraph(nodes: Node[], edges: Edge[]): Graph {
11
+ return {
12
+ nodes: () => nodes,
13
+ edges: () => edges,
14
+ adjacency: function() {
15
+ const adj = new Map<Node, Map<Node, number>>();
16
+
17
+ // Initialize adjacency for all nodes
18
+ for (const node of nodes) {
19
+ adj.set(node, new Map());
20
+ }
21
+
22
+ // Add edges
23
+ for (const [source, target] of edges) {
24
+ if (!adj.has(source)) adj.set(source, new Map());
25
+ if (!adj.has(target)) adj.set(target, new Map());
26
+
27
+ adj.get(source)!.set(target, 1);
28
+ adj.get(target)!.set(source, 1);
29
+ }
30
+
31
+ return adj;
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tests for graph utility functions
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ getNodesFromGraph,
8
+ getEdgesFromGraph,
9
+ getNodeDegree,
10
+ getNeighbors
11
+ } from '../src/utils/graph';
12
+ import { createTestGraph } from './test-utils';
13
+
14
+ describe('Graph Utils', () => {
15
+ describe('getNodesFromGraph', () => {
16
+ it('should return array as-is when given array of nodes', () => {
17
+ const nodes = [1, 2, 3, 'a', 'b'];
18
+ const result = getNodesFromGraph(nodes);
19
+ expect(result).toEqual(nodes);
20
+ expect(result).toBe(nodes); // same reference
21
+ });
22
+
23
+ it('should call nodes() method when given graph object', () => {
24
+ const graph = createTestGraph([1, 2, 3], [[1, 2], [2, 3]]);
25
+ const result = getNodesFromGraph(graph);
26
+ expect(result).toEqual([1, 2, 3]);
27
+ });
28
+
29
+ it('should handle empty array', () => {
30
+ const result = getNodesFromGraph([]);
31
+ expect(result).toEqual([]);
32
+ });
33
+
34
+ it('should handle empty graph', () => {
35
+ const graph = createTestGraph([], []);
36
+ const result = getNodesFromGraph(graph);
37
+ expect(result).toEqual([]);
38
+ });
39
+ });
40
+
41
+ describe('getEdgesFromGraph', () => {
42
+ it('should return empty array when given array of nodes', () => {
43
+ const nodes = [1, 2, 3, 'a', 'b'];
44
+ const result = getEdgesFromGraph(nodes);
45
+ expect(result).toEqual([]);
46
+ });
47
+
48
+ it('should call edges() method when given graph object', () => {
49
+ const edges = [[1, 2], [2, 3], [3, 1]];
50
+ const graph = createTestGraph([1, 2, 3], edges);
51
+ const result = getEdgesFromGraph(graph);
52
+ expect(result).toEqual(edges);
53
+ });
54
+
55
+ it('should handle empty array', () => {
56
+ const result = getEdgesFromGraph([]);
57
+ expect(result).toEqual([]);
58
+ });
59
+
60
+ it('should handle graph with no edges', () => {
61
+ const graph = createTestGraph([1, 2, 3], []);
62
+ const result = getEdgesFromGraph(graph);
63
+ expect(result).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe('getNodeDegree', () => {
68
+ it('should return 0 for graph without edges method', () => {
69
+ const graph = { nodes: () => [1, 2, 3] } as any;
70
+ const result = getNodeDegree(graph, 1);
71
+ expect(result).toBe(0);
72
+ });
73
+
74
+ it('should calculate degree correctly for connected node', () => {
75
+ const graph = createTestGraph([1, 2, 3, 4], [[1, 2], [1, 3], [1, 4], [2, 3]]);
76
+ expect(getNodeDegree(graph, 1)).toBe(3); // connected to 2, 3, 4
77
+ expect(getNodeDegree(graph, 2)).toBe(2); // connected to 1, 3
78
+ expect(getNodeDegree(graph, 3)).toBe(2); // connected to 1, 2
79
+ expect(getNodeDegree(graph, 4)).toBe(1); // connected to 1
80
+ });
81
+
82
+ it('should return 0 for isolated node', () => {
83
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
84
+ expect(getNodeDegree(graph, 3)).toBe(0);
85
+ });
86
+
87
+ it('should handle self-loops correctly', () => {
88
+ const graph = createTestGraph([1, 2], [[1, 1], [1, 2]]);
89
+ expect(getNodeDegree(graph, 1)).toBe(2); // self-loop + edge to 2
90
+ });
91
+
92
+ it('should handle node not in graph', () => {
93
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
94
+ expect(getNodeDegree(graph, 999)).toBe(0);
95
+ });
96
+
97
+ it('should handle empty graph', () => {
98
+ const graph = createTestGraph([], []);
99
+ expect(getNodeDegree(graph, 1)).toBe(0);
100
+ });
101
+ });
102
+
103
+ describe('getNeighbors', () => {
104
+ it('should return empty array for graph without edges method', () => {
105
+ const graph = { nodes: () => [1, 2, 3] } as any;
106
+ const result = getNeighbors(graph, 1);
107
+ expect(result).toEqual([]);
108
+ });
109
+
110
+ it('should return neighbors correctly', () => {
111
+ const graph = createTestGraph([1, 2, 3, 4], [[1, 2], [1, 3], [2, 4], [3, 4]]);
112
+
113
+ const neighbors1 = getNeighbors(graph, 1);
114
+ expect(neighbors1.sort()).toEqual([2, 3]);
115
+
116
+ const neighbors2 = getNeighbors(graph, 2);
117
+ expect(neighbors2.sort()).toEqual([1, 4]);
118
+
119
+ const neighbors4 = getNeighbors(graph, 4);
120
+ expect(neighbors4.sort()).toEqual([2, 3]);
121
+ });
122
+
123
+ it('should return empty array for isolated node', () => {
124
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
125
+ expect(getNeighbors(graph, 3)).toEqual([]);
126
+ });
127
+
128
+ it('should handle self-loops correctly', () => {
129
+ const graph = createTestGraph([1, 2], [[1, 1], [1, 2]]);
130
+ const neighbors = getNeighbors(graph, 1);
131
+ expect(neighbors.sort()).toEqual([1, 2]); // includes self and neighbor
132
+ });
133
+
134
+ it('should not duplicate neighbors in undirected edges', () => {
135
+ const graph = createTestGraph([1, 2, 3], [[1, 2], [2, 1], [1, 3]]); // bidirectional edge
136
+ const neighbors = getNeighbors(graph, 1);
137
+ expect(neighbors.sort()).toEqual([2, 3]); // should not duplicate node 2
138
+ });
139
+
140
+ it('should handle node not in graph', () => {
141
+ const graph = createTestGraph([1, 2, 3], [[1, 2]]);
142
+ expect(getNeighbors(graph, 999)).toEqual([]);
143
+ });
144
+
145
+ it('should handle empty graph', () => {
146
+ const graph = createTestGraph([], []);
147
+ expect(getNeighbors(graph, 1)).toEqual([]);
148
+ });
149
+
150
+ it('should work with string node IDs', () => {
151
+ const graph = createTestGraph(['a', 'b', 'c'], [['a', 'b'], ['b', 'c']]);
152
+ expect(getNeighbors(graph, 'b').sort()).toEqual(['a', 'c']);
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for utils index exports
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+
7
+ describe('Utils Index', () => {
8
+ it('should export all utility functions from numpy', async () => {
9
+ const { np } = await import('../src/utils/index');
10
+ expect(np).toBeDefined();
11
+ expect(typeof np.zeros).toBe('function');
12
+ expect(typeof np.ones).toBe('function');
13
+ expect(typeof np.linspace).toBe('function');
14
+ expect(typeof np.array).toBe('function');
15
+ expect(typeof np.repeat).toBe('function');
16
+ expect(typeof np.mean).toBe('function');
17
+ expect(typeof np.add).toBe('function');
18
+ expect(typeof np.subtract).toBe('function');
19
+ expect(typeof np.max).toBe('function');
20
+ expect(typeof np.min).toBe('function');
21
+ expect(typeof np.norm).toBe('function');
22
+ });
23
+
24
+ it('should export RandomNumberGenerator class', async () => {
25
+ const { RandomNumberGenerator } = await import('../src/utils/index');
26
+ expect(RandomNumberGenerator).toBeDefined();
27
+ expect(typeof RandomNumberGenerator).toBe('function');
28
+
29
+ const rng = new RandomNumberGenerator(123);
30
+ expect(typeof rng.rand).toBe('function');
31
+ });
32
+
33
+ it('should export graph utility functions', async () => {
34
+ const { getNodesFromGraph, getEdgesFromGraph, getNodeDegree, getNeighbors } = await import('../src/utils/index');
35
+ expect(typeof getNodesFromGraph).toBe('function');
36
+ expect(typeof getEdgesFromGraph).toBe('function');
37
+ expect(typeof getNodeDegree).toBe('function');
38
+ expect(typeof getNeighbors).toBe('function');
39
+ });
40
+
41
+ it('should export parameter processing functions', async () => {
42
+ const { _processParams } = await import('../src/utils/index');
43
+ expect(typeof _processParams).toBe('function');
44
+ });
45
+
46
+ it('should export rescale functions', async () => {
47
+ const { rescaleLayout, rescaleLayoutDict } = await import('../src/utils/index');
48
+ expect(typeof rescaleLayout).toBe('function');
49
+ expect(typeof rescaleLayoutDict).toBe('function');
50
+ });
51
+
52
+ it('should allow importing everything with wildcard', async () => {
53
+ const utils = await import('../src/utils/index');
54
+
55
+ // Check that key exports are available
56
+ expect(utils.np).toBeDefined();
57
+ expect(utils.RandomNumberGenerator).toBeDefined();
58
+ expect(utils.getNodesFromGraph).toBeDefined();
59
+ expect(utils._processParams).toBeDefined();
60
+ expect(utils.rescaleLayout).toBeDefined();
61
+ });
62
+
63
+ it('should work with destructured imports', async () => {
64
+ // Test that the typical usage patterns work
65
+ const { np, RandomNumberGenerator, getNodesFromGraph } = await import('../src/utils/index');
66
+
67
+ // Quick functional test
68
+ expect(np.zeros(3)).toEqual([0, 0, 0]);
69
+
70
+ const rng = new RandomNumberGenerator(42);
71
+ const value = rng.rand() as number;
72
+ expect(typeof value).toBe('number');
73
+
74
+ const nodes = getNodesFromGraph([1, 2, 3]);
75
+ expect(nodes).toEqual([1, 2, 3]);
76
+ });
77
+ });