@graphty/layout 1.1.1 → 1.2.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 (100) hide show
  1. package/.env.example +11 -0
  2. package/.github/workflows/ci.yml +3 -7
  3. package/CHANGELOG.md +14 -0
  4. package/DEPLOYMENT.md +59 -0
  5. package/README.md +54 -1
  6. package/dist/vitest.config.js +2 -2
  7. package/dist/vitest.config.js.map +1 -1
  8. package/examples/3d-force-directed.html +611 -0
  9. package/examples/3d-kamada-kawai.html +379 -0
  10. package/examples/3d-layout-comparison.html +448 -0
  11. package/examples/3d-spherical-layout.html +319 -0
  12. package/examples/arf-layout.html +13 -1
  13. package/examples/bfs-layout.html +38 -32
  14. package/examples/bipartite-layout.html +16 -4
  15. package/examples/circular-layout.html +14 -2
  16. package/examples/forceatlas2-layout.html +118 -151
  17. package/examples/index.html +75 -0
  18. package/examples/kamada-kawai-layout.html +13 -1
  19. package/{dist → examples}/layout-helpers.js +58 -29
  20. package/examples/multipartite-layout.html +44 -54
  21. package/examples/planar-layout.html +13 -1
  22. package/examples/random-layout.html +13 -1
  23. package/examples/shell-layout.html +16 -2
  24. package/examples/spectral-layout.html +13 -1
  25. package/examples/spiral-layout.html +13 -1
  26. package/examples/spring-layout.html +15 -13
  27. package/package.json +6 -3
  28. package/src/algorithms/index.ts +6 -0
  29. package/src/algorithms/optimization/index.ts +12 -0
  30. package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
  31. package/src/algorithms/optimization/lbfgs.ts +68 -0
  32. package/src/algorithms/optimization/line-search.ts +50 -0
  33. package/src/algorithms/optimization/types.ts +8 -0
  34. package/src/algorithms/planarity/check.ts +38 -0
  35. package/src/algorithms/planarity/embedding.ts +216 -0
  36. package/src/algorithms/planarity/index.ts +12 -0
  37. package/src/algorithms/planarity/lr-test.ts +70 -0
  38. package/src/algorithms/planarity/special-graphs.ts +126 -0
  39. package/src/generators/basic.ts +93 -0
  40. package/src/generators/bipartite.ts +45 -0
  41. package/src/generators/grid.ts +42 -0
  42. package/src/generators/index.ts +15 -0
  43. package/src/generators/random.ts +39 -0
  44. package/src/generators/scale-free.ts +72 -0
  45. package/src/index.ts +18 -0
  46. package/src/layouts/basic/index.ts +5 -0
  47. package/src/layouts/basic/random.ts +32 -0
  48. package/src/layouts/force-directed/arf.ts +130 -0
  49. package/src/layouts/force-directed/forceatlas2.ts +407 -0
  50. package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
  51. package/src/layouts/force-directed/index.ts +9 -0
  52. package/src/layouts/force-directed/kamada-kawai.ts +112 -0
  53. package/src/layouts/force-directed/spring.ts +35 -0
  54. package/src/layouts/geometric/circular.ts +77 -0
  55. package/src/layouts/geometric/index.ts +7 -0
  56. package/src/layouts/geometric/shell.ts +80 -0
  57. package/src/layouts/geometric/spiral.ts +93 -0
  58. package/src/layouts/hierarchical/bfs.ts +81 -0
  59. package/src/layouts/hierarchical/bipartite.ts +94 -0
  60. package/src/layouts/hierarchical/index.ts +7 -0
  61. package/src/layouts/hierarchical/multipartite.ts +88 -0
  62. package/src/layouts/index.ts +9 -0
  63. package/src/layouts/specialized/index.ts +6 -0
  64. package/src/layouts/specialized/planar.ts +65 -0
  65. package/src/layouts/specialized/spectral.ts +128 -0
  66. package/src/types/embedding.ts +11 -0
  67. package/src/types/graph.ts +13 -0
  68. package/src/types/index.ts +7 -0
  69. package/src/types/layout.ts +9 -0
  70. package/src/utils/graph.ts +77 -0
  71. package/src/utils/index.ts +9 -0
  72. package/src/utils/numpy.ts +108 -0
  73. package/src/utils/params.ts +26 -0
  74. package/src/utils/random.ts +53 -0
  75. package/src/utils/rescale.ts +137 -0
  76. package/test/arf-layout.test.ts +1 -1
  77. package/test/bfs-layout.test.ts +1 -1
  78. package/test/bipartite-layout.test.ts +1 -1
  79. package/test/circular-layout.test.ts +145 -3
  80. package/test/forceatlas2-layout.test.ts +1 -1
  81. package/test/fruchterman-reingold-layout.test.ts +1 -1
  82. package/test/graph-generators.test.ts +1 -1
  83. package/test/kamada-kawai-layout.test.ts +273 -1
  84. package/test/multipartite-layout.test.ts +1 -1
  85. package/test/planar-layout.test.ts +1 -1
  86. package/test/random-layout.test.ts +1 -1
  87. package/test/rescale-layout.test.ts +1 -1
  88. package/test/shell-layout.test.ts +1 -1
  89. package/test/spectral-layout.test.ts +1 -1
  90. package/test/spiral-layout.test.ts +1 -1
  91. package/test/spring-layout.test.ts +1 -1
  92. package/vite.config.js +36 -0
  93. package/vitest.config.ts +2 -2
  94. package/dist/layout-helpers.d.ts +0 -123
  95. package/dist/layout-helpers.js.map +0 -1
  96. package/dist/layout.d.ts +0 -275
  97. package/dist/layout.js +0 -2280
  98. package/dist/layout.js.map +0 -1
  99. package/layout-helpers.ts +0 -559
  100. package/layout.ts +0 -2867
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Spectral layout algorithm using eigenvectors of the graph Laplacian
3
+ */
4
+
5
+ import { Graph, Node, Edge, PositionMap } from '../../types';
6
+ import { _processParams } from '../../utils/params';
7
+ import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph';
8
+ import { rescaleLayout } from '../../utils/rescale';
9
+
10
+ /**
11
+ * Position nodes in a spectral layout using eigenvectors of the graph Laplacian.
12
+ *
13
+ * @param G - Graph
14
+ * @param scale - Scale factor for positions
15
+ * @param center - Coordinate pair around which to center the layout
16
+ * @param dim - Dimension of layout
17
+ * @returns Positions dictionary keyed by node
18
+ */
19
+ export function spectralLayout(
20
+ G: Graph,
21
+ scale: number = 1,
22
+ center: number[] | null = null,
23
+ dim: number = 2
24
+ ): PositionMap {
25
+ const processed = _processParams(G, center, dim);
26
+ const graph = processed.G;
27
+ center = processed.center;
28
+
29
+ const nodes = getNodesFromGraph(graph);
30
+
31
+ if (nodes.length <= 2) {
32
+ if (nodes.length === 0) {
33
+ return {};
34
+ } else if (nodes.length === 1) {
35
+ return { [nodes[0]]: center };
36
+ } else {
37
+ return {
38
+ [nodes[0]]: center.map(v => v - scale),
39
+ [nodes[1]]: center.map(v => v + scale)
40
+ };
41
+ }
42
+ }
43
+
44
+ // Create adjacency matrix
45
+ const N = nodes.length;
46
+ const nodeIndices: Record<Node, number> = {};
47
+ nodes.forEach((node: Node, i: number) => { nodeIndices[node] = i; });
48
+
49
+ const A = Array(N).fill(0).map(() => Array(N).fill(0));
50
+ const edges = getEdgesFromGraph(graph);
51
+
52
+ for (const [source, target] of edges) {
53
+ const i = nodeIndices[source];
54
+ const j = nodeIndices[target];
55
+ A[i][j] = 1;
56
+ A[j][i] = 1; // Make symmetric for undirected graphs
57
+ }
58
+
59
+ // Create Laplacian matrix: L = D - A where D is degree matrix
60
+ const L = Array(N).fill(0).map(() => Array(N).fill(0));
61
+ for (let i = 0; i < N; i++) {
62
+ // Compute degree (sum of row)
63
+ L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
64
+ for (let j = 0; j < N; j++) {
65
+ L[i][j] -= A[i][j];
66
+ }
67
+ }
68
+
69
+ // Compute eigenvectors using power iteration method
70
+ // We need the smallest non-zero eigenvectors of L
71
+ const eigenvectors: number[][] = [];
72
+
73
+ // For each dimension, find an eigenvector
74
+ for (let d = 0; d < dim; d++) {
75
+ let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
76
+
77
+ // Orthogonalize against previous eigenvectors
78
+ for (const ev of eigenvectors) {
79
+ const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
80
+ vector = vector.map((val, idx) => val - dot * ev[idx]);
81
+ }
82
+
83
+ // Normalize
84
+ const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
85
+ vector = vector.map(val => val / norm);
86
+
87
+ // Apply shifted inverse iteration to find smallest non-zero eigenvector
88
+ // This is a simplification of the actual algorithm
89
+ for (let iter = 0; iter < 100; iter++) {
90
+ // Apply Laplacian
91
+ const newVec = Array(N).fill(0);
92
+ for (let i = 0; i < N; i++) {
93
+ for (let j = 0; j < N; j++) {
94
+ newVec[i] += L[i][j] * vector[j];
95
+ }
96
+ }
97
+
98
+ // Orthogonalize against the constant vector (eigenvector with eigenvalue 0)
99
+ const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
100
+ newVec.forEach((val, idx, arr) => { arr[idx] = val - mean; });
101
+
102
+ // Normalize
103
+ const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
104
+ if (newNorm < 1e-10) continue; // Skip if vector is close to zero
105
+
106
+ vector = newVec.map(val => val / newNorm);
107
+ }
108
+
109
+ eigenvectors.push(vector);
110
+ }
111
+
112
+ // Create position array from eigenvectors
113
+ const positions: number[][] = Array(N).fill(0).map(() => Array(dim).fill(0));
114
+ for (let i = 0; i < N; i++) {
115
+ for (let d = 0; d < dim; d++) {
116
+ positions[i][d] = eigenvectors[d][i];
117
+ }
118
+ }
119
+
120
+ // Rescale and create position dictionary
121
+ const scaledPositions = rescaleLayout(positions as any, scale);
122
+ const pos: PositionMap = {};
123
+ nodes.forEach((node: Node, i: number) => {
124
+ pos[node] = (scaledPositions as number[][])[i].map((val: number, j: number) => val + center[j]);
125
+ });
126
+
127
+ return pos;
128
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Embedding type for planar layout algorithms
3
+ */
4
+
5
+ import { Node, Position } from './index';
6
+
7
+ export interface Embedding {
8
+ nodeOrder: Node[];
9
+ faceList: Node[][];
10
+ nodePositions: Record<Node, Position>;
11
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Core graph type definitions
3
+ */
4
+
5
+ export type Node = string | number;
6
+
7
+ export type Edge = [Node, Node];
8
+
9
+ export type Graph = {
10
+ nodes: () => Node[];
11
+ edges: () => Edge[];
12
+ getEdgeData?: (source: Node, target: Node, attr: string) => any;
13
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Re-export all type definitions
3
+ */
4
+
5
+ export * from './graph';
6
+ export * from './layout';
7
+ export * from './embedding';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Layout-specific type definitions
3
+ */
4
+
5
+ import { Node } from './graph';
6
+
7
+ export type Position = number[];
8
+
9
+ export type PositionMap = Record<Node, Position>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Graph utility functions
3
+ */
4
+
5
+ import { Graph, Node, Edge } from '../types';
6
+
7
+ /**
8
+ * Extract nodes from a graph object
9
+ *
10
+ * @param G - Graph or list of nodes
11
+ * @returns Array of nodes
12
+ */
13
+ export function getNodesFromGraph(G: Graph | Node[]): Node[] {
14
+ if (Array.isArray(G)) {
15
+ return G;
16
+ }
17
+ return G.nodes();
18
+ }
19
+
20
+ /**
21
+ * Extract edges from a graph object
22
+ *
23
+ * @param G - Graph or list of nodes
24
+ * @returns Array of edges
25
+ */
26
+ export function getEdgesFromGraph(G: Graph | Node[]): Edge[] {
27
+ if (Array.isArray(G)) {
28
+ return [];
29
+ }
30
+ return G.edges();
31
+ }
32
+
33
+ /**
34
+ * Get the degree of a node in the graph
35
+ *
36
+ * @param graph - Graph object
37
+ * @param node - Node to get degree for
38
+ * @returns Degree of the node
39
+ */
40
+ export function getNodeDegree(graph: Graph, node: Node): number {
41
+ if (!graph.edges) return 0;
42
+
43
+ const edges = graph.edges();
44
+ let degree = 0;
45
+
46
+ for (const [source, target] of edges) {
47
+ if (source === node || target === node) {
48
+ degree++;
49
+ }
50
+ }
51
+
52
+ return degree;
53
+ }
54
+
55
+ /**
56
+ * Get the neighbors of a node in the graph
57
+ *
58
+ * @param graph - Graph object
59
+ * @param node - Node to get neighbors for
60
+ * @returns Array of neighbor nodes
61
+ */
62
+ export function getNeighbors(graph: Graph, node: Node): Node[] {
63
+ if (!graph.edges) return [];
64
+
65
+ const neighbors = new Set<Node>();
66
+ const edges = graph.edges();
67
+
68
+ for (const [source, target] of edges) {
69
+ if (source === node) {
70
+ neighbors.add(target);
71
+ } else if (target === node) {
72
+ neighbors.add(source);
73
+ }
74
+ }
75
+
76
+ return Array.from(neighbors);
77
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Re-export all utilities
3
+ */
4
+
5
+ export * from './numpy';
6
+ export * from './random';
7
+ export * from './graph';
8
+ export * from './params';
9
+ export * from './rescale';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * NumPy-like array manipulation functions
3
+ * Maintains exact same functionality as original implementation
4
+ */
5
+
6
+ export const np = {
7
+ zeros: function (shape: number | number[]): number | number[] | number[][] | any[] {
8
+ if (typeof shape === 'number') {
9
+ return Array(shape).fill(0);
10
+ }
11
+ if (shape.length === 1) {
12
+ return Array(shape[0]).fill(0);
13
+ }
14
+ return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
15
+ },
16
+
17
+ ones: function (shape: number | number[]): number | any[] {
18
+ if (typeof shape === 'number') {
19
+ return Array(shape).fill(1);
20
+ }
21
+ if (shape.length === 1) {
22
+ return Array(shape[0]).fill(1);
23
+ }
24
+ return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
25
+ },
26
+
27
+ linspace: function (start: number, stop: number, num: number): number[] {
28
+ const step = (stop - start) / (num - 1);
29
+ return Array.from({ length: num }, (_, i) => start + i * step);
30
+ },
31
+
32
+ array: function (arr: any): any[] {
33
+ return Array.isArray(arr) ? [...arr] : [arr];
34
+ },
35
+
36
+ repeat: function (a: any, repeats: number): any[] {
37
+ const result: any[] = [];
38
+ for (let i = 0; i < repeats; i++) {
39
+ result.push(...np.array(a));
40
+ }
41
+ return result;
42
+ },
43
+
44
+ mean: function (arr: number[] | number[][], axis: number | null = null): number | number[] {
45
+ if (axis === null) {
46
+ const flatArr = Array.isArray(arr[0])
47
+ ? (arr as number[][]).flat(Infinity) as number[]
48
+ : arr as number[];
49
+ const sum = flatArr.reduce((a, b) => a + b, 0);
50
+ return sum / flatArr.length;
51
+ }
52
+
53
+ if (axis === 0) {
54
+ const result: number[] = [];
55
+ const matrix = arr as number[][];
56
+ for (let i = 0; i < matrix[0].length; i++) {
57
+ let sum = 0;
58
+ for (let j = 0; j < matrix.length; j++) {
59
+ sum += matrix[j][i];
60
+ }
61
+ result.push(sum / matrix.length);
62
+ }
63
+ return result;
64
+ }
65
+
66
+ return (arr as number[][]).map(row => np.mean(row) as number);
67
+ },
68
+
69
+ add: function (a: number | number[], b: number | number[]): number | number[] {
70
+ if (!Array.isArray(a) && !Array.isArray(b)) {
71
+ return a + b;
72
+ }
73
+ if (!Array.isArray(a)) {
74
+ return (b as number[]).map(val => a + val);
75
+ }
76
+ if (!Array.isArray(b)) {
77
+ return (a as number[]).map(val => val + b);
78
+ }
79
+ return (a as number[]).map((val, i) => val + (b as number[])[i]);
80
+ },
81
+
82
+ subtract: function (a: number | number[], b: number | number[]): number | number[] {
83
+ if (!Array.isArray(a) && !Array.isArray(b)) {
84
+ return a - b;
85
+ }
86
+ if (!Array.isArray(a)) {
87
+ return (b as number[]).map(val => a - val);
88
+ }
89
+ if (!Array.isArray(b)) {
90
+ return (a as number[]).map(val => val - b);
91
+ }
92
+ return (a as number[]).map((val, i) => val - (b as number[])[i]);
93
+ },
94
+
95
+ max: function (arr: number | number[]): number {
96
+ if (!Array.isArray(arr)) return arr;
97
+ return Math.max(...(arr as number[]).flat(Infinity) as number[]);
98
+ },
99
+
100
+ min: function (arr: number | number[]): number {
101
+ if (!Array.isArray(arr)) return arr;
102
+ return Math.min(...(arr as number[]).flat(Infinity) as number[]);
103
+ },
104
+
105
+ norm: function (arr: number[]): number {
106
+ return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
107
+ }
108
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Parameter processing utilities
3
+ */
4
+
5
+ import { Graph, Node } from '../types';
6
+
7
+ /**
8
+ * Process and validate layout parameters
9
+ * Helper function similar to _process_params in Python version
10
+ *
11
+ * @param G - Graph object or array of nodes
12
+ * @param center - Center coordinates or null
13
+ * @param dim - Dimension of layout
14
+ * @returns Processed parameters
15
+ */
16
+ export function _processParams(G: Graph | Node[], center: number[] | null, dim: number): { G: Graph | Node[]; center: number[] } {
17
+ if (!center) {
18
+ center = Array(dim).fill(0);
19
+ }
20
+
21
+ if (center.length !== dim) {
22
+ throw new Error("length of center coordinates must match dimension of layout");
23
+ }
24
+
25
+ return { G, center };
26
+ }
@@ -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', () => {