@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.
- package/.github/workflows/ci.yml +3 -7
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/vitest.config.js +2 -2
- package/dist/vitest.config.js.map +1 -1
- package/examples/3d-kamada-kawai.html +4 -19
- package/examples/bfs-layout.html +26 -32
- package/examples/bipartite-layout.html +3 -3
- package/examples/forceatlas2-layout.html +106 -151
- package/{dist → examples}/layout-helpers.js +58 -30
- package/examples/multipartite-layout.html +32 -14
- package/examples/shell-layout.html +4 -2
- package/examples/spring-layout.html +1 -11
- package/package.json +3 -3
- package/src/algorithms/index.ts +6 -0
- package/src/algorithms/optimization/index.ts +12 -0
- package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
- package/src/algorithms/optimization/lbfgs.ts +68 -0
- package/src/algorithms/optimization/line-search.ts +50 -0
- package/src/algorithms/optimization/types.ts +8 -0
- package/src/algorithms/planarity/check.ts +38 -0
- package/src/algorithms/planarity/embedding.ts +216 -0
- package/src/algorithms/planarity/index.ts +12 -0
- package/src/algorithms/planarity/lr-test.ts +70 -0
- package/src/algorithms/planarity/special-graphs.ts +126 -0
- package/src/generators/basic.ts +93 -0
- package/src/generators/bipartite.ts +45 -0
- package/src/generators/grid.ts +42 -0
- package/src/generators/index.ts +15 -0
- package/src/generators/random.ts +39 -0
- package/src/generators/scale-free.ts +72 -0
- package/src/index.ts +18 -0
- package/src/layouts/basic/index.ts +5 -0
- package/src/layouts/basic/random.ts +32 -0
- package/src/layouts/force-directed/arf.ts +130 -0
- package/src/layouts/force-directed/forceatlas2.ts +407 -0
- package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
- package/src/layouts/force-directed/index.ts +9 -0
- package/src/layouts/force-directed/kamada-kawai.ts +112 -0
- package/src/layouts/force-directed/spring.ts +35 -0
- package/src/layouts/geometric/circular.ts +77 -0
- package/src/layouts/geometric/index.ts +7 -0
- package/src/layouts/geometric/shell.ts +80 -0
- package/src/layouts/geometric/spiral.ts +93 -0
- package/src/layouts/hierarchical/bfs.ts +81 -0
- package/src/layouts/hierarchical/bipartite.ts +94 -0
- package/src/layouts/hierarchical/index.ts +7 -0
- package/src/layouts/hierarchical/multipartite.ts +88 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/specialized/index.ts +6 -0
- package/src/layouts/specialized/planar.ts +65 -0
- package/src/layouts/specialized/spectral.ts +128 -0
- package/src/types/embedding.ts +11 -0
- package/src/types/graph.ts +13 -0
- package/src/types/index.ts +7 -0
- package/src/types/layout.ts +9 -0
- package/src/utils/graph.ts +77 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/numpy.ts +111 -0
- package/src/utils/params.ts +26 -0
- package/src/utils/random.ts +53 -0
- package/src/utils/rescale.ts +137 -0
- package/test/arf-layout.test.ts +1 -1
- package/test/bfs-layout.test.ts +1 -1
- package/test/bipartite-layout.test.ts +1 -1
- package/test/circular-layout.test.ts +1 -1
- package/test/forceatlas2-layout.test.ts +1 -1
- package/test/fruchterman-reingold-layout.test.ts +1 -1
- package/test/graph-generators.test.ts +1 -1
- package/test/kamada-kawai-layout.test.ts +1 -1
- package/test/multipartite-layout.test.ts +1 -1
- package/test/planar-layout.test.ts +1 -1
- package/test/random-layout.test.ts +1 -1
- package/test/rescale-layout.test.ts +1 -1
- package/test/shell-layout.test.ts +1 -1
- package/test/spectral-layout.test.ts +1 -1
- package/test/spiral-layout.test.ts +1 -1
- package/test/spring-layout.test.ts +1 -1
- package/test/test-utils.ts +34 -0
- package/test/utils-graph.test.ts +155 -0
- package/test/utils-index.test.ts +77 -0
- package/test/utils-numpy.test.ts +272 -0
- package/test/utils-params.test.ts +99 -0
- package/test/utils-random.test.ts +229 -0
- package/vitest.config.ts +2 -2
- package/dist/layout-helpers.d.ts +0 -123
- package/dist/layout-helpers.js.map +0 -1
- package/dist/layout.d.ts +0 -275
- package/dist/layout.js +0 -2304
- package/dist/layout.js.map +0 -1
- package/layout-helpers.ts +0 -560
- 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
|
+
}
|
package/test/arf-layout.test.ts
CHANGED
package/test/bfs-layout.test.ts
CHANGED
|
@@ -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
|
+
});
|