@graphty/layout 1.2.8 → 1.3.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 (185) hide show
  1. package/README.md +394 -404
  2. package/dist/layout.js +881 -821
  3. package/dist/layout.js.map +1 -1
  4. package/dist/src/algorithms/index.d.ts +2 -2
  5. package/dist/src/algorithms/index.js +2 -2
  6. package/dist/src/algorithms/index.js.map +1 -1
  7. package/dist/src/algorithms/optimization/index.d.ts +4 -4
  8. package/dist/src/algorithms/optimization/index.js +3 -3
  9. package/dist/src/algorithms/optimization/index.js.map +1 -1
  10. package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +2 -5
  11. package/dist/src/algorithms/optimization/kamada-kawai-solver.js +6 -9
  12. package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -1
  13. package/dist/src/algorithms/optimization/lbfgs.d.ts +0 -1
  14. package/dist/src/algorithms/optimization/lbfgs.js +3 -5
  15. package/dist/src/algorithms/optimization/lbfgs.js.map +1 -1
  16. package/dist/src/algorithms/optimization/line-search.d.ts +0 -1
  17. package/dist/src/algorithms/optimization/line-search.js +0 -1
  18. package/dist/src/algorithms/optimization/line-search.js.map +1 -1
  19. package/dist/src/algorithms/optimization/types.d.ts +1 -1
  20. package/dist/src/algorithms/planarity/check.d.ts +3 -3
  21. package/dist/src/algorithms/planarity/check.js +7 -7
  22. package/dist/src/algorithms/planarity/check.js.map +1 -1
  23. package/dist/src/algorithms/planarity/embedding.d.ts +3 -5
  24. package/dist/src/algorithms/planarity/embedding.js +20 -16
  25. package/dist/src/algorithms/planarity/embedding.js.map +1 -1
  26. package/dist/src/algorithms/planarity/index.d.ts +4 -4
  27. package/dist/src/algorithms/planarity/index.js +4 -4
  28. package/dist/src/algorithms/planarity/index.js.map +1 -1
  29. package/dist/src/algorithms/planarity/lr-test.d.ts +3 -3
  30. package/dist/src/algorithms/planarity/lr-test.js +5 -5
  31. package/dist/src/algorithms/planarity/lr-test.js.map +1 -1
  32. package/dist/src/algorithms/planarity/special-graphs.d.ts +1 -4
  33. package/dist/src/algorithms/planarity/special-graphs.js +18 -15
  34. package/dist/src/algorithms/planarity/special-graphs.js.map +1 -1
  35. package/dist/src/generators/basic.d.ts +1 -1
  36. package/dist/src/generators/basic.js +4 -4
  37. package/dist/src/generators/basic.js.map +1 -1
  38. package/dist/src/generators/bipartite.d.ts +1 -1
  39. package/dist/src/generators/bipartite.js +2 -2
  40. package/dist/src/generators/bipartite.js.map +1 -1
  41. package/dist/src/generators/grid.d.ts +1 -1
  42. package/dist/src/generators/grid.js +1 -1
  43. package/dist/src/generators/grid.js.map +1 -1
  44. package/dist/src/generators/index.d.ts +5 -5
  45. package/dist/src/generators/index.js +5 -5
  46. package/dist/src/generators/index.js.map +1 -1
  47. package/dist/src/generators/random.d.ts +1 -1
  48. package/dist/src/generators/random.js +2 -2
  49. package/dist/src/generators/random.js.map +1 -1
  50. package/dist/src/generators/scale-free.d.ts +1 -1
  51. package/dist/src/generators/scale-free.js +4 -4
  52. package/dist/src/generators/scale-free.js.map +1 -1
  53. package/dist/src/index.d.ts +4 -4
  54. package/dist/src/index.js +4 -4
  55. package/dist/src/layouts/basic/index.d.ts +1 -1
  56. package/dist/src/layouts/basic/index.js +1 -1
  57. package/dist/src/layouts/basic/random.d.ts +1 -2
  58. package/dist/src/layouts/basic/random.js +3 -4
  59. package/dist/src/layouts/basic/random.js.map +1 -1
  60. package/dist/src/layouts/force-directed/arf.d.ts +1 -2
  61. package/dist/src/layouts/force-directed/arf.js +13 -8
  62. package/dist/src/layouts/force-directed/arf.js.map +1 -1
  63. package/dist/src/layouts/force-directed/forceatlas2.d.ts +1 -2
  64. package/dist/src/layouts/force-directed/forceatlas2.js +58 -35
  65. package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -1
  66. package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +11 -12
  67. package/dist/src/layouts/force-directed/fruchterman-reingold.js +17 -17
  68. package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -1
  69. package/dist/src/layouts/force-directed/index.d.ts +5 -5
  70. package/dist/src/layouts/force-directed/index.js +5 -5
  71. package/dist/src/layouts/force-directed/index.js.map +1 -1
  72. package/dist/src/layouts/force-directed/kamada-kawai.d.ts +2 -3
  73. package/dist/src/layouts/force-directed/kamada-kawai.js +12 -10
  74. package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -1
  75. package/dist/src/layouts/force-directed/spring.d.ts +11 -12
  76. package/dist/src/layouts/force-directed/spring.js +11 -12
  77. package/dist/src/layouts/force-directed/spring.js.map +1 -1
  78. package/dist/src/layouts/geometric/circular.d.ts +1 -2
  79. package/dist/src/layouts/geometric/circular.js +10 -9
  80. package/dist/src/layouts/geometric/circular.js.map +1 -1
  81. package/dist/src/layouts/geometric/index.d.ts +3 -3
  82. package/dist/src/layouts/geometric/index.js +3 -3
  83. package/dist/src/layouts/geometric/shell.d.ts +1 -2
  84. package/dist/src/layouts/geometric/shell.js +5 -5
  85. package/dist/src/layouts/geometric/shell.js.map +1 -1
  86. package/dist/src/layouts/geometric/spiral.d.ts +1 -2
  87. package/dist/src/layouts/geometric/spiral.js +5 -9
  88. package/dist/src/layouts/geometric/spiral.js.map +1 -1
  89. package/dist/src/layouts/hierarchical/bfs.d.ts +2 -3
  90. package/dist/src/layouts/hierarchical/bfs.js +5 -6
  91. package/dist/src/layouts/hierarchical/bfs.js.map +1 -1
  92. package/dist/src/layouts/hierarchical/bipartite.d.ts +2 -3
  93. package/dist/src/layouts/hierarchical/bipartite.js +8 -9
  94. package/dist/src/layouts/hierarchical/bipartite.js.map +1 -1
  95. package/dist/src/layouts/hierarchical/index.d.ts +3 -3
  96. package/dist/src/layouts/hierarchical/index.js +3 -3
  97. package/dist/src/layouts/hierarchical/index.js.map +1 -1
  98. package/dist/src/layouts/hierarchical/multipartite.d.ts +2 -3
  99. package/dist/src/layouts/hierarchical/multipartite.js +7 -8
  100. package/dist/src/layouts/hierarchical/multipartite.js.map +1 -1
  101. package/dist/src/layouts/index.d.ts +5 -5
  102. package/dist/src/layouts/index.js +5 -5
  103. package/dist/src/layouts/index.js.map +1 -1
  104. package/dist/src/layouts/specialized/index.d.ts +2 -2
  105. package/dist/src/layouts/specialized/index.js +2 -2
  106. package/dist/src/layouts/specialized/index.js.map +1 -1
  107. package/dist/src/layouts/specialized/planar.d.ts +3 -3
  108. package/dist/src/layouts/specialized/planar.js +8 -8
  109. package/dist/src/layouts/specialized/planar.js.map +1 -1
  110. package/dist/src/layouts/specialized/spectral.d.ts +1 -2
  111. package/dist/src/layouts/specialized/spectral.js +30 -20
  112. package/dist/src/layouts/specialized/spectral.js.map +1 -1
  113. package/dist/src/types/embedding.d.ts +1 -1
  114. package/dist/src/types/index.d.ts +3 -3
  115. package/dist/src/types/index.js +3 -3
  116. package/dist/src/types/index.js.map +1 -1
  117. package/dist/src/types/layout.d.ts +1 -1
  118. package/dist/src/utils/graph.d.ts +1 -5
  119. package/dist/src/utils/graph.js +4 -6
  120. package/dist/src/utils/graph.js.map +1 -1
  121. package/dist/src/utils/index.d.ts +5 -5
  122. package/dist/src/utils/index.js +5 -5
  123. package/dist/src/utils/index.js.map +1 -1
  124. package/dist/src/utils/numpy.js +22 -16
  125. package/dist/src/utils/numpy.js.map +1 -1
  126. package/dist/src/utils/params.d.ts +1 -2
  127. package/dist/src/utils/params.js +0 -1
  128. package/dist/src/utils/params.js.map +1 -1
  129. package/dist/src/utils/random.d.ts +17 -0
  130. package/dist/src/utils/random.js +18 -1
  131. package/dist/src/utils/random.js.map +1 -1
  132. package/dist/src/utils/rescale.d.ts +1 -3
  133. package/dist/src/utils/rescale.js +12 -10
  134. package/dist/src/utils/rescale.js.map +1 -1
  135. package/dist/vitest.config.js +14 -14
  136. package/dist/vitest.config.js.map +1 -1
  137. package/package.json +29 -24
  138. package/src/algorithms/index.ts +2 -2
  139. package/src/algorithms/optimization/index.ts +4 -8
  140. package/src/algorithms/optimization/kamada-kawai-solver.ts +170 -179
  141. package/src/algorithms/optimization/lbfgs.ts +41 -48
  142. package/src/algorithms/optimization/line-search.ts +27 -28
  143. package/src/algorithms/optimization/types.ts +2 -2
  144. package/src/algorithms/planarity/check.ts +22 -21
  145. package/src/algorithms/planarity/embedding.ts +159 -165
  146. package/src/algorithms/planarity/index.ts +4 -8
  147. package/src/algorithms/planarity/lr-test.ts +46 -45
  148. package/src/algorithms/planarity/special-graphs.ts +73 -83
  149. package/src/generators/basic.ts +57 -57
  150. package/src/generators/bipartite.ts +37 -31
  151. package/src/generators/grid.ts +28 -28
  152. package/src/generators/index.ts +5 -11
  153. package/src/generators/random.ts +26 -25
  154. package/src/generators/scale-free.ts +56 -55
  155. package/src/index.ts +5 -5
  156. package/src/layouts/basic/index.ts +1 -1
  157. package/src/layouts/basic/random.ts +20 -16
  158. package/src/layouts/force-directed/arf.ts +107 -105
  159. package/src/layouts/force-directed/forceatlas2.ts +365 -351
  160. package/src/layouts/force-directed/fruchterman-reingold.ts +133 -134
  161. package/src/layouts/force-directed/index.ts +5 -5
  162. package/src/layouts/force-directed/kamada-kawai.ts +78 -80
  163. package/src/layouts/force-directed/spring.ts +24 -25
  164. package/src/layouts/geometric/circular.ts +63 -57
  165. package/src/layouts/geometric/index.ts +3 -3
  166. package/src/layouts/geometric/shell.ts +58 -53
  167. package/src/layouts/geometric/spiral.ts +60 -64
  168. package/src/layouts/hierarchical/bfs.ts +58 -59
  169. package/src/layouts/hierarchical/bipartite.ts +62 -63
  170. package/src/layouts/hierarchical/index.ts +3 -3
  171. package/src/layouts/hierarchical/multipartite.ts +62 -63
  172. package/src/layouts/index.ts +5 -5
  173. package/src/layouts/specialized/index.ts +2 -2
  174. package/src/layouts/specialized/planar.ts +42 -41
  175. package/src/layouts/specialized/spectral.ts +110 -99
  176. package/src/types/embedding.ts +5 -5
  177. package/src/types/graph.ts +4 -4
  178. package/src/types/index.ts +3 -3
  179. package/src/types/layout.ts +2 -2
  180. package/src/utils/graph.ts +31 -35
  181. package/src/utils/index.ts +5 -5
  182. package/src/utils/numpy.ts +96 -92
  183. package/src/utils/params.ts +14 -11
  184. package/src/utils/random.ts +57 -40
  185. package/src/utils/rescale.ts +169 -174
@@ -1,12 +1,11 @@
1
- import type { Graph, Node, Edge, PositionMap } from '../../types';
2
- import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph';
3
- import { _processParams } from '../../utils/params';
4
- import { rescaleLayout } from '../../utils/rescale';
5
- import { RandomNumberGenerator } from '../../utils/random';
1
+ import type { Edge, Graph, Node, PositionMap } from "../../types";
2
+ import { getEdgesFromGraph,getNodesFromGraph } from "../../utils/graph";
3
+ import { _processParams } from "../../utils/params";
4
+ import { RandomNumberGenerator } from "../../utils/random";
5
+ import { rescaleLayout } from "../../utils/rescale";
6
6
 
7
7
  /**
8
8
  * Position nodes using the ForceAtlas2 force-directed algorithm.
9
- *
10
9
  * @param G - Graph
11
10
  * @param pos - Initial positions for nodes
12
11
  * @param maxIter - Maximum number of iterations
@@ -25,402 +24,417 @@ import { RandomNumberGenerator } from '../../utils/random';
25
24
  * @returns Positions dictionary keyed by node
26
25
  */
27
26
  export function forceatlas2Layout(
28
- G: Graph,
29
- pos: PositionMap | null = null,
30
- maxIter: number = 100,
31
- jitterTolerance: number = 1.0,
32
- scalingRatio: number = 2.0,
33
- gravity: number = 1.0,
34
- distributedAction: boolean = false,
35
- strongGravity: boolean = false,
36
- nodeMass: Record<Node, number> | null = null,
37
- nodeSize: Record<Node, number> | null = null,
38
- weight: string | null = null,
39
- dissuadeHubs: boolean = false,
40
- linlog: boolean = false,
41
- seed: number | null = null,
42
- dim: number = 2
27
+ G: Graph,
28
+ pos: PositionMap | null = null,
29
+ maxIter: number = 100,
30
+ jitterTolerance: number = 1.0,
31
+ scalingRatio: number = 2.0,
32
+ gravity: number = 1.0,
33
+ distributedAction: boolean = false,
34
+ strongGravity: boolean = false,
35
+ nodeMass: Record<Node, number> | null = null,
36
+ nodeSize: Record<Node, number> | null = null,
37
+ weight: string | null = null,
38
+ dissuadeHubs: boolean = false,
39
+ linlog: boolean = false,
40
+ seed: number | null = null,
41
+ dim: number = 2,
43
42
  ): PositionMap {
44
- const processed = _processParams(G, null, dim);
45
- const graph = processed.G;
43
+ const processed = _processParams(G, null, dim);
44
+ const graph = processed.G;
46
45
 
47
- const nodes = getNodesFromGraph(graph);
46
+ const nodes = getNodesFromGraph(graph);
48
47
 
49
- if (nodes.length === 0) {
50
- return {};
51
- }
52
-
53
- // Initialize random number generator
54
- const rng = new RandomNumberGenerator(seed ?? undefined);
55
-
56
- // Initialize positions if not provided
57
- let posArray: number[][];
58
- if (pos === null) {
59
- pos = {};
60
- posArray = new Array(nodes.length);
61
- for (let i = 0; i < nodes.length; i++) {
62
- posArray[i] = Array(dim).fill(0).map(() => rng.rand() as number * 2 - 1);
63
- pos[nodes[i]] = posArray[i];
64
- }
65
- } else if (Object.keys(pos).length === nodes.length) {
66
- // Use provided positions
67
- posArray = new Array(nodes.length);
68
- for (let i = 0; i < nodes.length; i++) {
69
- const nodePos = pos[nodes[i]];
70
- posArray[i] = new Array(dim);
71
- for (let d = 0; d < dim; d++) {
72
- posArray[i][d] = d < nodePos.length ? nodePos[d] : (rng.rand() as number * 2 - 1);
73
- }
74
- }
75
- } else {
76
- // Some nodes don't have positions, initialize within the range of existing positions
77
- let minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
78
- let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
79
-
80
- // Find min and max of existing positions
81
- for (const node in pos) {
82
- const nodePos = pos[node];
83
- for (let d = 0; d < dim; d++) {
84
- if (d < nodePos.length) {
85
- minPos[d] = Math.min(minPos[d], nodePos[d]);
86
- maxPos[d] = Math.max(maxPos[d], nodePos[d]);
87
- }
88
- }
48
+ if (nodes.length === 0) {
49
+ return {};
89
50
  }
90
51
 
91
- // If min/max are still infinity for some dimensions, use default range
92
- for (let d = 0; d < dim; d++) {
93
- if (!isFinite(minPos[d]) || !isFinite(maxPos[d])) {
94
- minPos[d] = -1;
95
- maxPos[d] = 1;
96
- }
97
- }
52
+ // Initialize random number generator
53
+ const rng = new RandomNumberGenerator(seed ?? undefined);
54
+
55
+ // Initialize positions if not provided
56
+ let posArray: number[][];
57
+ if (pos === null) {
58
+ pos = {};
59
+ posArray = new Array(nodes.length);
60
+ for (let i = 0; i < nodes.length; i++) {
61
+ posArray[i] = Array(dim)
62
+ .fill(0)
63
+ .map(() => (rng.rand() as number) * 2 - 1);
64
+ pos[nodes[i]] = posArray[i];
65
+ }
66
+ } else if (Object.keys(pos).length === nodes.length) {
67
+ // Use provided positions
68
+ posArray = new Array(nodes.length);
69
+ for (let i = 0; i < nodes.length; i++) {
70
+ const nodePos = pos[nodes[i]];
71
+ posArray[i] = new Array(dim);
72
+ for (let d = 0; d < dim; d++) {
73
+ posArray[i][d] = d < nodePos.length ? nodePos[d] : (rng.rand() as number) * 2 - 1;
74
+ }
75
+ }
76
+ } else {
77
+ // Some nodes don't have positions, initialize within the range of existing positions
78
+ const minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
79
+ const maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
80
+
81
+ // Find min and max of existing positions
82
+ for (const node in pos) {
83
+ const nodePos = pos[node];
84
+ for (let d = 0; d < dim; d++) {
85
+ if (d < nodePos.length) {
86
+ minPos[d] = Math.min(minPos[d], nodePos[d]);
87
+ maxPos[d] = Math.max(maxPos[d], nodePos[d]);
88
+ }
89
+ }
90
+ }
98
91
 
99
- posArray = new Array(nodes.length);
100
- for (let i = 0; i < nodes.length; i++) {
101
- const node = nodes[i];
102
- if (pos[node]) {
103
- const nodePos = pos[node];
104
- posArray[i] = new Array(dim);
92
+ // If min/max are still infinity for some dimensions, use default range
105
93
  for (let d = 0; d < dim; d++) {
106
- posArray[i][d] = d < nodePos.length ? nodePos[d] : (rng.rand() as number * 2 - 1);
94
+ if (!isFinite(minPos[d]) || !isFinite(maxPos[d])) {
95
+ minPos[d] = -1;
96
+ maxPos[d] = 1;
97
+ }
107
98
  }
108
- } else {
109
- posArray[i] = Array(dim).fill(0).map((_, d) =>
110
- minPos[d] + (rng.rand() as number) * (maxPos[d] - minPos[d])
111
- );
112
- pos[node] = posArray[i];
113
- }
114
- }
115
- }
116
-
117
- // Initialize mass and size arrays
118
- const mass = new Array(nodes.length).fill(0);
119
- const size = new Array(nodes.length).fill(0);
120
-
121
- // Flag to track whether to adjust for node sizes
122
- const adjustSizes = nodeSize !== null;
123
-
124
- // Set node masses and sizes
125
- for (let i = 0; i < nodes.length; i++) {
126
- const node = nodes[i];
127
- mass[i] = nodeMass && nodeMass[node] ?
128
- nodeMass[node] :
129
- (Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1);
130
-
131
- size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
132
- }
133
-
134
- // Create adjacency matrix
135
- const n = nodes.length;
136
- const A = Array(n).fill(0).map(() => Array(n).fill(0));
137
-
138
- // Populate adjacency matrix with edge weights
139
- const edges = Array.isArray(graph) ? [] as Edge[] : graph.edges();
140
- const nodeIndices: Record<Node, number> = {};
141
- nodes.forEach((node, i) => { nodeIndices[node] = i; });
142
-
143
- for (const [source, target] of edges) {
144
- const i = nodeIndices[source];
145
- const j = nodeIndices[target];
146
-
147
- // Use edge weight if provided, otherwise default to 1
148
- let edgeWeight = 1;
149
- if (weight && !Array.isArray(graph) && graph.getEdgeData) {
150
- edgeWeight = graph.getEdgeData(source, target, weight) || 1;
151
- }
152
99
 
153
- A[i][j] = edgeWeight;
154
- A[j][i] = edgeWeight; // For undirected graphs
155
- }
156
-
157
- // Initialize force arrays
158
- const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
159
- const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
160
- const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
161
-
162
- // Simulation parameters
163
- let speed = 1;
164
- let speedEfficiency = 1;
165
- let swing = 1;
166
- let traction = 1;
167
-
168
- // Helper function to estimate factor for force scaling
169
- function estimateFactor(
170
- n: number,
171
- swing: number,
172
- traction: number,
173
- speed: number,
174
- speedEfficiency: number,
175
- jitterTolerance: number
176
- ): [number, number] {
177
- // Optimal jitter parameters
178
- const optJitter = 0.05 * Math.sqrt(n);
179
- const minJitter = Math.sqrt(optJitter);
180
- const maxJitter = 10;
181
- const minSpeedEfficiency = 0.05;
182
-
183
- // Estimate jitter based on current state
184
- const other = Math.min(maxJitter, optJitter * traction / (n * n));
185
- let jitter = jitterTolerance * Math.max(minJitter, other);
186
-
187
- // Adjust speed efficiency based on swing/traction ratio
188
- if (swing / traction > 2.0) {
189
- if (speedEfficiency > minSpeedEfficiency) {
190
- speedEfficiency *= 0.5;
191
- }
192
- jitter = Math.max(jitter, jitterTolerance);
100
+ posArray = new Array(nodes.length);
101
+ for (let i = 0; i < nodes.length; i++) {
102
+ const node = nodes[i];
103
+ if (pos[node]) {
104
+ const nodePos = pos[node];
105
+ posArray[i] = new Array(dim);
106
+ for (let d = 0; d < dim; d++) {
107
+ posArray[i][d] = d < nodePos.length ? nodePos[d] : (rng.rand() as number) * 2 - 1;
108
+ }
109
+ } else {
110
+ posArray[i] = Array(dim)
111
+ .fill(0)
112
+ .map((_, d) => minPos[d] + (rng.rand() as number) * (maxPos[d] - minPos[d]));
113
+ pos[node] = posArray[i];
114
+ }
115
+ }
193
116
  }
194
117
 
195
- // Calculate target speed
196
- let targetSpeed = swing === 0 ?
197
- Number.POSITIVE_INFINITY :
198
- jitter * speedEfficiency * traction / swing;
199
-
200
- // Further adjust speed efficiency
201
- if (swing > jitter * traction) {
202
- if (speedEfficiency > minSpeedEfficiency) {
203
- speedEfficiency *= 0.7;
204
- }
205
- } else if (speed < 1000) {
206
- speedEfficiency *= 1.3;
207
- }
118
+ // Initialize mass and size arrays
119
+ const mass = new Array(nodes.length).fill(0);
120
+ const size = new Array(nodes.length).fill(0);
208
121
 
209
- // Limit the speed increase
210
- const maxRise = 0.5;
211
- speed = speed + Math.min(targetSpeed - speed, maxRise * speed);
122
+ // Flag to track whether to adjust for node sizes
123
+ const adjustSizes = nodeSize !== null;
212
124
 
213
- return [speed, speedEfficiency];
214
- }
125
+ // Set node masses and sizes
126
+ for (let i = 0; i < nodes.length; i++) {
127
+ const node = nodes[i];
128
+ mass[i] =
129
+ nodeMass && nodeMass[node] ? nodeMass[node] : Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1;
215
130
 
216
- // Main simulation loop
217
- for (let iter = 0; iter < maxIter; iter++) {
218
- // Reset forces for this iteration
219
- for (let i = 0; i < n; i++) {
220
- for (let d = 0; d < dim; d++) {
221
- attraction[i][d] = 0;
222
- repulsion[i][d] = 0;
223
- gravities[i][d] = 0;
224
- }
131
+ size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
225
132
  }
226
133
 
227
- // Compute pairwise differences and distances
228
- const diff = Array(n).fill(0).map(() =>
229
- Array(n).fill(0).map(() => Array(dim).fill(0))
230
- );
231
-
232
- const distance = Array(n).fill(0).map(() => Array(n).fill(0));
233
-
234
- for (let i = 0; i < n; i++) {
235
- for (let j = 0; j < n; j++) {
236
- if (i === j) continue;
237
-
238
- for (let d = 0; d < dim; d++) {
239
- diff[i][j][d] = posArray[i][d] - posArray[j][d];
134
+ // Create adjacency matrix
135
+ const n = nodes.length;
136
+ const A = Array(n)
137
+ .fill(0)
138
+ .map(() => Array(n).fill(0));
139
+
140
+ // Populate adjacency matrix with edge weights
141
+ const edges = Array.isArray(graph) ? ([] as Edge[]) : graph.edges();
142
+ const nodeIndices: Record<Node, number> = {};
143
+ nodes.forEach((node, i) => {
144
+ nodeIndices[node] = i;
145
+ });
146
+
147
+ for (const [source, target] of edges) {
148
+ const i = nodeIndices[source];
149
+ const j = nodeIndices[target];
150
+
151
+ // Use edge weight if provided, otherwise default to 1
152
+ let edgeWeight = 1;
153
+ if (weight && !Array.isArray(graph) && graph.getEdgeData) {
154
+ edgeWeight = graph.getEdgeData(source, target, weight) || 1;
240
155
  }
241
156
 
242
- distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
243
- // Prevent division by zero
244
- if (distance[i][j] < 0.01) distance[i][j] = 0.01;
245
- }
157
+ A[i][j] = edgeWeight;
158
+ A[j][i] = edgeWeight; // For undirected graphs
246
159
  }
247
160
 
248
- // Calculate attraction forces
249
- if (linlog) {
250
- // Logarithmic attraction model
251
- for (let i = 0; i < n; i++) {
252
- for (let j = 0; j < n; j++) {
253
- if (i === j || A[i][j] === 0) continue;
161
+ // Initialize force arrays
162
+ const gravities = Array(n)
163
+ .fill(0)
164
+ .map(() => Array(dim).fill(0));
165
+ const attraction = Array(n)
166
+ .fill(0)
167
+ .map(() => Array(dim).fill(0));
168
+ const repulsion = Array(n)
169
+ .fill(0)
170
+ .map(() => Array(dim).fill(0));
171
+
172
+ // Simulation parameters
173
+ let speed = 1;
174
+ let speedEfficiency = 1;
175
+ const swing = 1;
176
+ const traction = 1;
177
+
178
+ // Helper function to estimate factor for force scaling
179
+ function estimateFactor(
180
+ n: number,
181
+ swing: number,
182
+ traction: number,
183
+ speed: number,
184
+ speedEfficiency: number,
185
+ jitterTolerance: number,
186
+ ): [number, number] {
187
+ // Optimal jitter parameters
188
+ const optJitter = 0.05 * Math.sqrt(n);
189
+ const minJitter = Math.sqrt(optJitter);
190
+ const maxJitter = 10;
191
+ const minSpeedEfficiency = 0.05;
192
+
193
+ // Estimate jitter based on current state
194
+ const other = Math.min(maxJitter, (optJitter * traction) / (n * n));
195
+ let jitter = jitterTolerance * Math.max(minJitter, other);
196
+
197
+ // Adjust speed efficiency based on swing/traction ratio
198
+ if (swing / traction > 2.0) {
199
+ if (speedEfficiency > minSpeedEfficiency) {
200
+ speedEfficiency *= 0.5;
201
+ }
202
+ jitter = Math.max(jitter, jitterTolerance);
203
+ }
254
204
 
255
- const dist = distance[i][j];
256
- const factor = -Math.log(1 + dist) / dist * A[i][j];
205
+ // Calculate target speed
206
+ const targetSpeed = swing === 0 ? Number.POSITIVE_INFINITY : (jitter * speedEfficiency * traction) / swing;
257
207
 
258
- for (let d = 0; d < dim; d++) {
259
- const force = factor * diff[i][j][d];
260
- attraction[i][d] += force;
261
- }
262
- }
263
- }
264
- } else {
265
- // Linear attraction model
266
- for (let i = 0; i < n; i++) {
267
- for (let j = 0; j < n; j++) {
268
- if (i === j || A[i][j] === 0) continue;
269
-
270
- for (let d = 0; d < dim; d++) {
271
- const force = -diff[i][j][d] * A[i][j];
272
- attraction[i][d] += force;
273
- }
208
+ // Further adjust speed efficiency
209
+ if (swing > jitter * traction) {
210
+ if (speedEfficiency > minSpeedEfficiency) {
211
+ speedEfficiency *= 0.7;
212
+ }
213
+ } else if (speed < 1000) {
214
+ speedEfficiency *= 1.3;
274
215
  }
275
- }
216
+
217
+ // Limit the speed increase
218
+ const maxRise = 0.5;
219
+ speed = speed + Math.min(targetSpeed - speed, maxRise * speed);
220
+
221
+ return [speed, speedEfficiency];
276
222
  }
277
223
 
278
- // Apply distributed attraction if enabled
279
- if (distributedAction) {
280
- for (let i = 0; i < n; i++) {
281
- for (let d = 0; d < dim; d++) {
282
- attraction[i][d] /= mass[i];
224
+ // Main simulation loop
225
+ for (let iter = 0; iter < maxIter; iter++) {
226
+ // Reset forces for this iteration
227
+ for (let i = 0; i < n; i++) {
228
+ for (let d = 0; d < dim; d++) {
229
+ attraction[i][d] = 0;
230
+ repulsion[i][d] = 0;
231
+ gravities[i][d] = 0;
232
+ }
283
233
  }
284
- }
285
- }
286
234
 
287
- // Calculate repulsion forces
288
- for (let i = 0; i < n; i++) {
289
- for (let j = 0; j < n; j++) {
290
- if (i === j) continue;
235
+ // Compute pairwise differences and distances
236
+ const diff = Array(n)
237
+ .fill(0)
238
+ .map(() =>
239
+ Array(n)
240
+ .fill(0)
241
+ .map(() => Array(dim).fill(0)),
242
+ );
243
+
244
+ const distance = Array(n)
245
+ .fill(0)
246
+ .map(() => Array(n).fill(0));
247
+
248
+ for (let i = 0; i < n; i++) {
249
+ for (let j = 0; j < n; j++) {
250
+ if (i === j) {continue;}
251
+
252
+ for (let d = 0; d < dim; d++) {
253
+ diff[i][j][d] = posArray[i][d] - posArray[j][d];
254
+ }
255
+
256
+ distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
257
+ // Prevent division by zero
258
+ if (distance[i][j] < 0.01) {distance[i][j] = 0.01;}
259
+ }
260
+ }
291
261
 
292
- let dist = distance[i][j];
262
+ // Calculate attraction forces
263
+ if (linlog) {
264
+ // Logarithmic attraction model
265
+ for (let i = 0; i < n; i++) {
266
+ for (let j = 0; j < n; j++) {
267
+ if (i === j || A[i][j] === 0) {continue;}
268
+
269
+ const dist = distance[i][j];
270
+ const factor = (-Math.log(1 + dist) / dist) * A[i][j];
271
+
272
+ for (let d = 0; d < dim; d++) {
273
+ const force = factor * diff[i][j][d];
274
+ attraction[i][d] += force;
275
+ }
276
+ }
277
+ }
278
+ } else {
279
+ // Linear attraction model
280
+ for (let i = 0; i < n; i++) {
281
+ for (let j = 0; j < n; j++) {
282
+ if (i === j || A[i][j] === 0) {continue;}
283
+
284
+ for (let d = 0; d < dim; d++) {
285
+ const force = -diff[i][j][d] * A[i][j];
286
+ attraction[i][d] += force;
287
+ }
288
+ }
289
+ }
290
+ }
293
291
 
294
- // Adjust distance for node sizes if needed
295
- if (adjustSizes) {
296
- dist -= size[i] - size[j];
297
- dist = Math.max(dist, 0.01); // Prevent negative or zero distances
292
+ // Apply distributed attraction if enabled
293
+ if (distributedAction) {
294
+ for (let i = 0; i < n; i++) {
295
+ for (let d = 0; d < dim; d++) {
296
+ attraction[i][d] /= mass[i];
297
+ }
298
+ }
298
299
  }
299
300
 
300
- const distSquared = dist * dist;
301
- const massProduct = mass[i] * mass[j];
302
- const factor = (massProduct / distSquared) * scalingRatio;
301
+ // Calculate repulsion forces
302
+ for (let i = 0; i < n; i++) {
303
+ for (let j = 0; j < n; j++) {
304
+ if (i === j) {continue;}
303
305
 
304
- for (let d = 0; d < dim; d++) {
305
- const direction = diff[i][j][d] / dist;
306
- repulsion[i][d] += direction * factor;
307
- }
308
- }
309
- }
306
+ let dist = distance[i][j];
310
307
 
311
- // Calculate gravity forces
312
- // First find the center of mass
313
- const centerOfMass = Array(dim).fill(0);
314
- for (let i = 0; i < n; i++) {
315
- for (let d = 0; d < dim; d++) {
316
- centerOfMass[d] += posArray[i][d] / n;
317
- }
318
- }
308
+ // Adjust distance for node sizes if needed
309
+ if (adjustSizes) {
310
+ dist -= size[i] - size[j];
311
+ dist = Math.max(dist, 0.01); // Prevent negative or zero distances
312
+ }
319
313
 
320
- for (let i = 0; i < n; i++) {
321
- const posCentered = Array(dim);
322
- for (let d = 0; d < dim; d++) {
323
- posCentered[d] = posArray[i][d] - centerOfMass[d];
324
- }
314
+ const distSquared = dist * dist;
315
+ const massProduct = mass[i] * mass[j];
316
+ const factor = (massProduct / distSquared) * scalingRatio;
325
317
 
326
- if (strongGravity) {
327
- // Strong gravity model
328
- for (let d = 0; d < dim; d++) {
329
- gravities[i][d] = -gravity * mass[i] * posCentered[d];
318
+ for (let d = 0; d < dim; d++) {
319
+ const direction = diff[i][j][d] / dist;
320
+ repulsion[i][d] += direction * factor;
321
+ }
322
+ }
330
323
  }
331
- } else {
332
- // Regular gravity model
333
- const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
334
-
335
- if (dist > 0.01) {
336
- for (let d = 0; d < dim; d++) {
337
- const direction = posCentered[d] / dist;
338
- gravities[i][d] = -gravity * mass[i] * direction;
339
- }
324
+
325
+ // Calculate gravity forces
326
+ // First find the center of mass
327
+ const centerOfMass = Array(dim).fill(0);
328
+ for (let i = 0; i < n; i++) {
329
+ for (let d = 0; d < dim; d++) {
330
+ centerOfMass[d] += posArray[i][d] / n;
331
+ }
340
332
  }
341
- }
342
- }
343
333
 
344
- // Calculate total forces and update positions
345
- const update = Array(n).fill(0).map(() => Array(dim).fill(0));
346
- let totalSwing = 0;
347
- let totalTraction = 0;
334
+ for (let i = 0; i < n; i++) {
335
+ const posCentered = Array(dim);
336
+ for (let d = 0; d < dim; d++) {
337
+ posCentered[d] = posArray[i][d] - centerOfMass[d];
338
+ }
339
+
340
+ if (strongGravity) {
341
+ // Strong gravity model
342
+ for (let d = 0; d < dim; d++) {
343
+ gravities[i][d] = -gravity * mass[i] * posCentered[d];
344
+ }
345
+ } else {
346
+ // Regular gravity model
347
+ const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
348
+
349
+ if (dist > 0.01) {
350
+ for (let d = 0; d < dim; d++) {
351
+ const direction = posCentered[d] / dist;
352
+ gravities[i][d] = -gravity * mass[i] * direction;
353
+ }
354
+ }
355
+ }
356
+ }
348
357
 
349
- for (let i = 0; i < n; i++) {
350
- for (let d = 0; d < dim; d++) {
351
- update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
352
- }
358
+ // Calculate total forces and update positions
359
+ const update = Array(n)
360
+ .fill(0)
361
+ .map(() => Array(dim).fill(0));
362
+ let totalSwing = 0;
363
+ let totalTraction = 0;
353
364
 
354
- // Calculate swing and traction for this node
355
- const oldPos = [...posArray[i]];
356
- const newPos = oldPos.map((p, d) => p + update[i][d]);
365
+ for (let i = 0; i < n; i++) {
366
+ for (let d = 0; d < dim; d++) {
367
+ update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
368
+ }
357
369
 
358
- const swingVector = oldPos.map((p, d) => p - newPos[d]);
359
- const tractionVector = oldPos.map((p, d) => p + newPos[d]);
370
+ // Calculate swing and traction for this node
371
+ const oldPos = [...posArray[i]];
372
+ const newPos = oldPos.map((p, d) => p + update[i][d]);
360
373
 
361
- const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
362
- const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
374
+ const swingVector = oldPos.map((p, d) => p - newPos[d]);
375
+ const tractionVector = oldPos.map((p, d) => p + newPos[d]);
363
376
 
364
- totalSwing += mass[i] * swingMagnitude;
365
- totalTraction += 0.5 * mass[i] * tractionMagnitude;
366
- }
377
+ const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
378
+ const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
379
+
380
+ totalSwing += mass[i] * swingMagnitude;
381
+ totalTraction += 0.5 * mass[i] * tractionMagnitude;
382
+ }
367
383
 
368
- // Update speed and efficiency
369
- [speed, speedEfficiency] = estimateFactor(
370
- n,
371
- totalSwing,
372
- totalTraction,
373
- speed,
374
- speedEfficiency,
375
- jitterTolerance
376
- );
384
+ // Update speed and efficiency
385
+ [speed, speedEfficiency] = estimateFactor(
386
+ n,
387
+ totalSwing,
388
+ totalTraction,
389
+ speed,
390
+ speedEfficiency,
391
+ jitterTolerance,
392
+ );
377
393
 
378
- // Apply forces to update positions
379
- let totalMovement = 0;
394
+ // Apply forces to update positions
395
+ let totalMovement = 0;
396
+
397
+ for (let i = 0; i < n; i++) {
398
+ let factor;
399
+
400
+ if (adjustSizes) {
401
+ // Calculate displacement magnitude
402
+ const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
403
+ const swinging = mass[i] * df;
404
+
405
+ // Determine scaling factor with size adjustments
406
+ factor = (0.1 * speed) / (1 + Math.sqrt(speed * swinging));
407
+ factor = Math.min(factor * df, 10) / df;
408
+ } else {
409
+ // Standard scaling factor
410
+ const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
411
+ factor = speed / (1 + Math.sqrt(speed * swinging));
412
+ }
413
+
414
+ // Apply factor to update position
415
+ for (let d = 0; d < dim; d++) {
416
+ const movement = update[i][d] * factor;
417
+ posArray[i][d] += movement;
418
+ totalMovement += Math.abs(movement);
419
+ }
420
+ }
380
421
 
381
- for (let i = 0; i < n; i++) {
382
- let factor;
383
-
384
- if (adjustSizes) {
385
- // Calculate displacement magnitude
386
- const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
387
- const swinging = mass[i] * df;
388
-
389
- // Determine scaling factor with size adjustments
390
- factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
391
- factor = Math.min(factor * df, 10) / df;
392
- } else {
393
- // Standard scaling factor
394
- const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
395
- factor = speed / (1 + Math.sqrt(speed * swinging));
396
- }
397
-
398
- // Apply factor to update position
399
- for (let d = 0; d < dim; d++) {
400
- const movement = update[i][d] * factor;
401
- posArray[i][d] += movement;
402
- totalMovement += Math.abs(movement);
403
- }
422
+ // Check for convergence
423
+ if (totalMovement < 1e-10) {
424
+ break;
425
+ }
404
426
  }
405
427
 
406
- // Check for convergence
407
- if (totalMovement < 1e-10) {
408
- break;
428
+ // Create position dictionary
429
+ const positions: PositionMap = {};
430
+ for (let i = 0; i < n; i++) {
431
+ positions[nodes[i]] = posArray[i];
409
432
  }
410
- }
411
-
412
- // Create position dictionary
413
- const positions: PositionMap = {};
414
- for (let i = 0; i < n; i++) {
415
- positions[nodes[i]] = posArray[i];
416
- }
417
433
 
418
- return rescaleLayout(positions) as PositionMap;
434
+ return rescaleLayout(positions) as PositionMap;
419
435
  }
420
436
 
421
437
  // Helper function to get node degree
422
438
  function getNodeDegree(graph: Graph, node: Node): number {
423
- return graph.edges().filter((edge: Edge) =>
424
- edge[0] === node || edge[1] === node
425
- ).length;
426
- }
439
+ return graph.edges().filter((edge: Edge) => edge[0] === node || edge[1] === node).length;
440
+ }