@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
@@ -3,51 +3,68 @@
3
3
  * Maintains exact same functionality as original implementation
4
4
  */
5
5
 
6
+ /**
7
+ * A deterministic random number generator based on a linear congruential generator (LCG) algorithm.
8
+ * Provides consistent random sequences when initialized with the same seed.
9
+ */
6
10
  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
- }
11
+ private seed: number;
12
+ private m: number;
13
+ private a: number;
14
+ private c: number;
15
+ private _state: number;
25
16
 
26
- rand(shape: number | number[] | null = null): number | number[] | number[][] {
27
- if (shape === null) {
28
- return this._next();
17
+ /**
18
+ * Creates a new random number generator instance.
19
+ * @param seed - Initial seed value for the generator. If not provided, uses a random seed.
20
+ */
21
+ constructor(seed?: number) {
22
+ this.seed = seed || Math.floor(Math.random() * 1000000);
23
+ this.m = 2 ** 35 - 31;
24
+ this.a = 185852;
25
+ this.c = 1;
26
+ this._state = this.seed % this.m;
29
27
  }
30
28
 
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;
29
+ /**
30
+ * Generates the next random number in the sequence.
31
+ * @returns A random number between 0 and 1.
32
+ */
33
+ _next(): number {
34
+ this._state = (this.a * this._state + this.c) % this.m;
35
+ return this._state / this.m;
37
36
  }
38
37
 
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
- }
38
+ /**
39
+ * Generates random numbers according to the specified shape.
40
+ * @param shape - Determines the output format: null for single number, number for 1D array, array for multi-dimensional array.
41
+ * @returns A random number, 1D array, or multi-dimensional array depending on the shape parameter.
42
+ */
43
+ rand(shape: number | number[] | null = null): number | number[] | number[][] {
44
+ if (shape === null) {
45
+ return this._next();
46
+ }
47
+
48
+ if (typeof shape === "number") {
49
+ const result: number[] = [];
50
+ for (let i = 0; i < shape; i++) {
51
+ result.push(this._next());
52
+ }
53
+ return result;
54
+ }
55
+
56
+ if (shape.length === 1) {
57
+ const result: number[] = [];
58
+ for (let i = 0; i < shape[0]; i++) {
59
+ result.push(this._next());
60
+ }
61
+ return result;
62
+ }
46
63
 
47
- const result: any[] = [];
48
- for (let i = 0; i < shape[0]; i++) {
49
- result.push(this.rand(shape.slice(1)));
64
+ const result: any[] = [];
65
+ for (let i = 0; i < shape[0]; i++) {
66
+ result.push(this.rand(shape.slice(1)));
67
+ }
68
+ return result;
50
69
  }
51
- return result;
52
- }
53
- }
70
+ }
@@ -2,211 +2,206 @@
2
2
  * Layout rescaling utilities
3
3
  */
4
4
 
5
- import { PositionMap } from '../types';
5
+ import { PositionMap } from "../types";
6
6
 
7
7
  /**
8
8
  * Returns scaled position array/dict to (-scale, scale) in all axes.
9
- *
10
9
  * @param pos - Position dictionary or array
11
10
  * @param scale - Scale factor for positions
12
11
  * @param center - Coordinate pair around which to center the layout
13
12
  * @returns Rescaled positions dictionary
14
13
  */
15
14
  export function rescaleLayout(
16
- pos: PositionMap | number[][],
17
- scale: number = 1,
18
- center: number[] | null = null
15
+ pos: PositionMap | number[][],
16
+ scale: number = 1,
17
+ center: number[] | null = null,
19
18
  ): 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
- // Fix Bug #1: Default center should match dimensionality
32
- if (!center) {
33
- center = Array(dim).fill(0);
34
- }
35
-
36
- // Use the maximum dimension between positions and center
37
- const targetDim = Math.max(dim, center.length);
38
-
39
- // Calculate center of positions, handling NaN values (Bug #3)
40
- const posCenter = Array(dim).fill(0);
41
- const counts = Array(dim).fill(0);
42
-
43
- for (const p of posValues) {
44
- for (let i = 0; i < dim; i++) {
45
- if (!isNaN(p[i])) {
46
- posCenter[i] += p[i];
47
- counts[i]++;
48
- }
49
- }
50
- }
51
-
52
- // Average by actual count, not total length
53
- for (let i = 0; i < dim; i++) {
54
- posCenter[i] = counts[i] > 0 ? posCenter[i] / counts[i] : 0;
55
- }
56
-
57
- // Center positions
58
- let centeredPos: PositionMap | number[][] = {};
59
- if (Array.isArray(pos)) {
60
- centeredPos = pos.map(p => {
61
- const centered = Array(targetDim).fill(0);
62
- for (let i = 0; i < targetDim; i++) {
63
- centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
64
- }
65
- return centered;
66
- });
67
- } else {
68
- for (const [node, p] of Object.entries(pos)) {
69
- const centered = Array(targetDim).fill(0);
70
- for (let i = 0; i < targetDim; i++) {
71
- centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
72
- }
73
- (centeredPos as PositionMap)[node] = centered;
74
- }
75
- }
76
-
77
- // Find maximum distance from center, handling NaN values (Bug #3)
78
- let maxDistance = 0;
79
- const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
80
- for (const p of centeredValues) {
81
- // Calculate distance, treating NaN as 0 for distance calculation
82
- let sumSquares = 0;
83
- for (const val of p) {
84
- if (!isNaN(val)) {
85
- sumSquares += val * val;
86
- }
19
+ // Check if pos is empty
20
+ if (Array.isArray(pos)) {
21
+ if (pos.length === 0) {return [];}
22
+ } else {
23
+ if (Object.keys(pos).length === 0) {return {};}
87
24
  }
88
- const distance = Math.sqrt(sumSquares);
89
- if (!isNaN(distance)) {
90
- maxDistance = Math.max(maxDistance, distance);
25
+
26
+ // Extract position values
27
+ const posValues: number[][] = Array.isArray(pos) ? pos : Object.values(pos);
28
+ const dim = posValues[0].length;
29
+
30
+ // Fix Bug #1: Default center should match dimensionality
31
+ if (!center) {
32
+ center = Array(dim).fill(0);
91
33
  }
92
- }
93
34
 
94
- // Rescale
95
- let scaledPos: PositionMap | number[][] = Array.isArray(pos) ? [] : {};
35
+ // Use the maximum dimension between positions and center
36
+ const targetDim = Math.max(dim, center.length);
37
+
38
+ // Calculate center of positions, handling NaN values (Bug #3)
39
+ const posCenter = Array(dim).fill(0);
40
+ const counts = Array(dim).fill(0);
96
41
 
97
- if (maxDistance > 0) {
98
- const scaleFactor = scale / maxDistance;
42
+ for (const p of posValues) {
43
+ for (let i = 0; i < dim; i++) {
44
+ if (!isNaN(p[i])) {
45
+ posCenter[i] += p[i];
46
+ counts[i]++;
47
+ }
48
+ }
49
+ }
50
+
51
+ // Average by actual count, not total length
52
+ for (let i = 0; i < dim; i++) {
53
+ posCenter[i] = counts[i] > 0 ? posCenter[i] / counts[i] : 0;
54
+ }
99
55
 
56
+ // Center positions
57
+ let centeredPos: PositionMap | number[][] = {};
100
58
  if (Array.isArray(pos)) {
101
- (scaledPos as number[][]) = (centeredPos as number[][]).map((p, idx) =>
102
- p.map((val, i) => {
103
- // Check if this dimension existed in the original position
104
- const origPos = pos[idx];
105
- if (i >= origPos.length) {
106
- // This dimension was added, use center value
107
- return center![i];
108
- }
109
- // Preserve NaN values (Bug #3)
110
- if (isNaN(val)) return NaN;
111
- return val * scaleFactor + center![i];
112
- })
113
- );
114
- } else {
115
- for (const [node, p] of Object.entries(centeredPos as PositionMap)) {
116
- const origPos = pos[node];
117
- (scaledPos as PositionMap)[node] = p.map((val, i) => {
118
- // Check if this dimension existed in the original position
119
- if (i >= origPos.length) {
120
- // This dimension was added, use center value
121
- return center![i];
122
- }
123
- // Preserve NaN values (Bug #3)
124
- if (isNaN(val)) return NaN;
125
- return val * scaleFactor + center![i];
59
+ centeredPos = pos.map((p) => {
60
+ const centered = Array(targetDim).fill(0);
61
+ for (let i = 0; i < targetDim; i++) {
62
+ centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
63
+ }
64
+ return centered;
126
65
  });
127
- }
66
+ } else {
67
+ for (const [node, p] of Object.entries(pos)) {
68
+ const centered = Array(targetDim).fill(0);
69
+ for (let i = 0; i < targetDim; i++) {
70
+ centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
71
+ }
72
+ (centeredPos)[node] = centered;
73
+ }
128
74
  }
129
- } else {
130
- // All nodes at the same position - extend to target dimensionality
131
- if (Array.isArray(pos)) {
132
- (scaledPos as number[][]) = pos.map(p => {
133
- const result = Array(targetDim);
134
- for (let i = 0; i < targetDim; i++) {
135
- if (i < p.length) {
136
- result[i] = isNaN(p[i]) ? NaN : center![i];
137
- } else {
138
- result[i] = center![i];
139
- }
75
+
76
+ // Find maximum distance from center, handling NaN values (Bug #3)
77
+ let maxDistance = 0;
78
+ const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
79
+ for (const p of centeredValues) {
80
+ // Calculate distance, treating NaN as 0 for distance calculation
81
+ let sumSquares = 0;
82
+ for (const val of p) {
83
+ if (!isNaN(val)) {
84
+ sumSquares += val * val;
85
+ }
86
+ }
87
+ const distance = Math.sqrt(sumSquares);
88
+ if (!isNaN(distance)) {
89
+ maxDistance = Math.max(maxDistance, distance);
90
+ }
91
+ }
92
+
93
+ // Rescale
94
+ let scaledPos: PositionMap | number[][] = Array.isArray(pos) ? [] : {};
95
+
96
+ if (maxDistance > 0) {
97
+ const scaleFactor = scale / maxDistance;
98
+
99
+ if (Array.isArray(pos)) {
100
+ (scaledPos as number[][]) = (centeredPos as number[][]).map((p, idx) =>
101
+ p.map((val, i) => {
102
+ // Check if this dimension existed in the original position
103
+ const origPos = pos[idx];
104
+ if (i >= origPos.length) {
105
+ // This dimension was added, use center value
106
+ return center[i];
107
+ }
108
+ // Preserve NaN values (Bug #3)
109
+ if (isNaN(val)) {return NaN;}
110
+ return val * scaleFactor + center[i];
111
+ }),
112
+ );
113
+ } else {
114
+ for (const [node, p] of Object.entries(centeredPos as PositionMap)) {
115
+ const origPos = pos[node];
116
+ (scaledPos as PositionMap)[node] = p.map((val, i) => {
117
+ // Check if this dimension existed in the original position
118
+ if (i >= origPos.length) {
119
+ // This dimension was added, use center value
120
+ return center[i];
121
+ }
122
+ // Preserve NaN values (Bug #3)
123
+ if (isNaN(val)) {return NaN;}
124
+ return val * scaleFactor + center[i];
125
+ });
126
+ }
140
127
  }
141
- return result;
142
- });
143
128
  } else {
144
- for (const [node, p] of Object.entries(pos)) {
145
- const result = Array(targetDim);
146
- for (let i = 0; i < targetDim; i++) {
147
- if (i < p.length) {
148
- result[i] = isNaN(p[i]) ? NaN : center![i];
149
- } else {
150
- result[i] = center![i];
151
- }
129
+ // All nodes at the same position - extend to target dimensionality
130
+ if (Array.isArray(pos)) {
131
+ (scaledPos as number[][]) = pos.map((p) => {
132
+ const result = Array(targetDim);
133
+ for (let i = 0; i < targetDim; i++) {
134
+ if (i < p.length) {
135
+ result[i] = isNaN(p[i]) ? NaN : center[i];
136
+ } else {
137
+ result[i] = center[i];
138
+ }
139
+ }
140
+ return result;
141
+ });
142
+ } else {
143
+ for (const [node, p] of Object.entries(pos)) {
144
+ const result = Array(targetDim);
145
+ for (let i = 0; i < targetDim; i++) {
146
+ if (i < p.length) {
147
+ result[i] = isNaN(p[i]) ? NaN : center[i];
148
+ } else {
149
+ result[i] = center[i];
150
+ }
151
+ }
152
+ (scaledPos as PositionMap)[node] = result;
153
+ }
152
154
  }
153
- (scaledPos as PositionMap)[node] = result;
154
- }
155
155
  }
156
- }
157
156
 
158
- return scaledPos;
157
+ return scaledPos;
159
158
  }
160
159
 
161
160
  /**
162
161
  * Return a dictionary of scaled positions centered at (0, 0).
163
- *
164
162
  * @param pos - Dictionary of positions
165
163
  * @param scale - Scale factor for positions
166
164
  * @returns Dictionary of scaled positions
167
165
  */
168
- export function rescaleLayoutDict(
169
- pos: PositionMap,
170
- scale: number = 1
171
- ): PositionMap {
172
- if (Object.keys(pos).length === 0) {
173
- return {};
174
- }
175
-
176
- // Extract positions as array
177
- const posArray = Object.values(pos);
178
-
179
- // Find center of positions
180
- const center: number[] = [];
181
- for (let d = 0; d < posArray[0].length; d++) {
182
- center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
183
- }
184
-
185
- // Center positions
186
- const centeredPos: PositionMap = {};
187
- for (const [node, p] of Object.entries(pos)) {
188
- centeredPos[node] = p.map((val, d) => val - center[d]);
189
- }
190
-
191
- // Find maximum distance from center
192
- let maxDist = 0;
193
- for (const p of Object.values(centeredPos)) {
194
- const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
195
- maxDist = Math.max(maxDist, dist);
196
- }
197
-
198
- // Scale positions
199
- const scaledPos: PositionMap = {};
200
- if (maxDist > 0) {
201
- for (const [node, p] of Object.entries(centeredPos)) {
202
- scaledPos[node] = p.map(val => val * scale / maxDist);
166
+ export function rescaleLayoutDict(pos: PositionMap, scale: number = 1): PositionMap {
167
+ if (Object.keys(pos).length === 0) {
168
+ return {};
169
+ }
170
+
171
+ // Extract positions as array
172
+ const posArray = Object.values(pos);
173
+
174
+ // Find center of positions
175
+ const center: number[] = [];
176
+ for (let d = 0; d < posArray[0].length; d++) {
177
+ center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
178
+ }
179
+
180
+ // Center positions
181
+ const centeredPos: PositionMap = {};
182
+ for (const [node, p] of Object.entries(pos)) {
183
+ centeredPos[node] = p.map((val, d) => val - center[d]);
203
184
  }
204
- } else {
205
- // All points at the center
206
- for (const node of Object.keys(centeredPos)) {
207
- scaledPos[node] = Array(centeredPos[node].length).fill(0);
185
+
186
+ // Find maximum distance from center
187
+ let maxDist = 0;
188
+ for (const p of Object.values(centeredPos)) {
189
+ const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
190
+ maxDist = Math.max(maxDist, dist);
208
191
  }
209
- }
210
192
 
211
- return scaledPos;
212
- }
193
+ // Scale positions
194
+ const scaledPos: PositionMap = {};
195
+ if (maxDist > 0) {
196
+ for (const [node, p] of Object.entries(centeredPos)) {
197
+ scaledPos[node] = p.map((val) => (val * scale) / maxDist);
198
+ }
199
+ } else {
200
+ // All points at the center
201
+ for (const node of Object.keys(centeredPos)) {
202
+ scaledPos[node] = Array(centeredPos[node].length).fill(0);
203
+ }
204
+ }
205
+
206
+ return scaledPos;
207
+ }