@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.
- package/.env.example +11 -0
- package/.github/workflows/ci.yml +3 -7
- package/CHANGELOG.md +14 -0
- package/DEPLOYMENT.md +59 -0
- package/README.md +54 -1
- package/dist/vitest.config.js +2 -2
- package/dist/vitest.config.js.map +1 -1
- package/examples/3d-force-directed.html +611 -0
- package/examples/3d-kamada-kawai.html +379 -0
- package/examples/3d-layout-comparison.html +448 -0
- package/examples/3d-spherical-layout.html +319 -0
- package/examples/arf-layout.html +13 -1
- package/examples/bfs-layout.html +38 -32
- package/examples/bipartite-layout.html +16 -4
- package/examples/circular-layout.html +14 -2
- package/examples/forceatlas2-layout.html +118 -151
- package/examples/index.html +75 -0
- package/examples/kamada-kawai-layout.html +13 -1
- package/{dist → examples}/layout-helpers.js +58 -29
- package/examples/multipartite-layout.html +44 -54
- package/examples/planar-layout.html +13 -1
- package/examples/random-layout.html +13 -1
- package/examples/shell-layout.html +16 -2
- package/examples/spectral-layout.html +13 -1
- package/examples/spiral-layout.html +13 -1
- package/examples/spring-layout.html +15 -13
- package/package.json +6 -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 +108 -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 +145 -3
- 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 +273 -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/vite.config.js +36 -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 -2280
- package/dist/layout.js.map +0 -1
- package/layout-helpers.ts +0 -559
- package/layout.ts +0 -2867
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bipartite graph generation function
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Graph, Node, Edge } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a bipartite graph with two sets of nodes
|
|
9
|
+
* @param n1 - Number of nodes in first set
|
|
10
|
+
* @param n2 - Number of nodes in second set
|
|
11
|
+
* @param p - Probability of edge between nodes in different sets
|
|
12
|
+
* @param seed - Random seed for reproducibility
|
|
13
|
+
* @returns Graph object with bipartite structure and setA/setB properties
|
|
14
|
+
*/
|
|
15
|
+
export function bipartiteGraph(n1: number, n2: number, p: number, seed?: number): Graph & { setA: Node[], setB: Node[] } {
|
|
16
|
+
const setA: Node[] = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
17
|
+
const setB: Node[] = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
18
|
+
const nodes = [...setA, ...setB];
|
|
19
|
+
const edges: Edge[] = [];
|
|
20
|
+
|
|
21
|
+
// Simple deterministic pseudo-random if seed provided
|
|
22
|
+
let currentSeed = seed;
|
|
23
|
+
let random = seed !== undefined
|
|
24
|
+
? () => {
|
|
25
|
+
currentSeed = (currentSeed! * 9301 + 49297) % 233280;
|
|
26
|
+
return currentSeed / 233280;
|
|
27
|
+
}
|
|
28
|
+
: Math.random;
|
|
29
|
+
|
|
30
|
+
// Only connect nodes between sets
|
|
31
|
+
for (const a of setA) {
|
|
32
|
+
for (const b of setB) {
|
|
33
|
+
if (random() < p) {
|
|
34
|
+
edges.push([a, b]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
nodes: () => nodes,
|
|
41
|
+
edges: () => edges,
|
|
42
|
+
setA,
|
|
43
|
+
setB
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid graph generation function
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Graph, Node, Edge } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a grid graph with rows x cols nodes
|
|
9
|
+
* @param rows - Number of rows
|
|
10
|
+
* @param cols - Number of columns
|
|
11
|
+
* @returns Graph object with grid topology
|
|
12
|
+
*/
|
|
13
|
+
export function gridGraph(rows: number, cols: number): Graph {
|
|
14
|
+
const nodes: Node[] = [];
|
|
15
|
+
const edges: Edge[] = [];
|
|
16
|
+
|
|
17
|
+
// Create nodes
|
|
18
|
+
for (let i = 0; i < rows; i++) {
|
|
19
|
+
for (let j = 0; j < cols; j++) {
|
|
20
|
+
nodes.push(`${i},${j}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create edges
|
|
25
|
+
for (let i = 0; i < rows; i++) {
|
|
26
|
+
for (let j = 0; j < cols; j++) {
|
|
27
|
+
// Connect to right neighbor
|
|
28
|
+
if (j < cols - 1) {
|
|
29
|
+
edges.push([`${i},${j}`, `${i},${j + 1}`]);
|
|
30
|
+
}
|
|
31
|
+
// Connect to bottom neighbor
|
|
32
|
+
if (i < rows - 1) {
|
|
33
|
+
edges.push([`${i},${j}`, `${i + 1},${j}`]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
nodes: () => nodes,
|
|
40
|
+
edges: () => edges
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export all graph generation functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
completeGraph,
|
|
7
|
+
cycleGraph,
|
|
8
|
+
starGraph,
|
|
9
|
+
wheelGraph
|
|
10
|
+
} from './basic';
|
|
11
|
+
|
|
12
|
+
export { gridGraph } from './grid';
|
|
13
|
+
export { randomGraph } from './random';
|
|
14
|
+
export { bipartiteGraph } from './bipartite';
|
|
15
|
+
export { scaleFreeGraph } from './scale-free';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random graph generation function
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Graph, Node, Edge } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a random graph with n nodes and given edge probability
|
|
9
|
+
* @param n - Number of nodes
|
|
10
|
+
* @param p - Probability of edge between any two nodes (0-1)
|
|
11
|
+
* @param seed - Random seed for reproducibility
|
|
12
|
+
* @returns Graph object with random edges
|
|
13
|
+
*/
|
|
14
|
+
export function randomGraph(n: number, p: number, seed?: number): Graph {
|
|
15
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
16
|
+
const edges: Edge[] = [];
|
|
17
|
+
|
|
18
|
+
// Simple deterministic pseudo-random if seed provided
|
|
19
|
+
let currentSeed = seed;
|
|
20
|
+
let random = seed !== undefined
|
|
21
|
+
? () => {
|
|
22
|
+
currentSeed = (currentSeed! * 9301 + 49297) % 233280;
|
|
23
|
+
return currentSeed / 233280;
|
|
24
|
+
}
|
|
25
|
+
: Math.random;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
for (let j = i + 1; j < n; j++) {
|
|
29
|
+
if (random() < p) {
|
|
30
|
+
edges.push([i, j]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
nodes: () => nodes,
|
|
37
|
+
edges: () => edges
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale-free graph generation function
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Graph, Node, Edge } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a scale-free graph using Barabási-Albert model
|
|
9
|
+
* @param n - Total number of nodes
|
|
10
|
+
* @param m - Number of edges to attach from new node
|
|
11
|
+
* @param seed - Random seed for reproducibility
|
|
12
|
+
* @returns Graph object with scale-free properties
|
|
13
|
+
*/
|
|
14
|
+
export function scaleFreeGraph(n: number, m: number, seed?: number): Graph {
|
|
15
|
+
if (m >= n) {
|
|
16
|
+
throw new Error('m must be less than n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
20
|
+
const edges: Edge[] = [];
|
|
21
|
+
const degrees = new Array(n).fill(0);
|
|
22
|
+
|
|
23
|
+
// Simple deterministic pseudo-random if seed provided
|
|
24
|
+
let currentSeed = seed;
|
|
25
|
+
let random = seed !== undefined
|
|
26
|
+
? () => {
|
|
27
|
+
currentSeed = (currentSeed! * 9301 + 49297) % 233280;
|
|
28
|
+
return currentSeed / 233280;
|
|
29
|
+
}
|
|
30
|
+
: Math.random;
|
|
31
|
+
|
|
32
|
+
// Start with complete graph of m+1 nodes
|
|
33
|
+
for (let i = 0; i <= m; i++) {
|
|
34
|
+
for (let j = i + 1; j <= m; j++) {
|
|
35
|
+
edges.push([i, j]);
|
|
36
|
+
degrees[i]++;
|
|
37
|
+
degrees[j]++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add remaining nodes
|
|
42
|
+
for (let i = m + 1; i < n; i++) {
|
|
43
|
+
const targets = new Set<number>();
|
|
44
|
+
const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
|
|
45
|
+
|
|
46
|
+
// Choose m targets based on preferential attachment
|
|
47
|
+
while (targets.size < m) {
|
|
48
|
+
let r = random() * totalDegree;
|
|
49
|
+
let cumSum = 0;
|
|
50
|
+
|
|
51
|
+
for (let j = 0; j < i; j++) {
|
|
52
|
+
cumSum += degrees[j];
|
|
53
|
+
if (r <= cumSum && !targets.has(j)) {
|
|
54
|
+
targets.add(j);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add edges to targets
|
|
61
|
+
for (const target of targets) {
|
|
62
|
+
edges.push([i, target]);
|
|
63
|
+
degrees[i]++;
|
|
64
|
+
degrees[target]++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
nodes: () => nodes,
|
|
70
|
+
edges: () => edges
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for @graphty/layout
|
|
3
|
+
*
|
|
4
|
+
* This file exports the complete public API, maintaining
|
|
5
|
+
* compatibility with the original layout.ts file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export all types
|
|
9
|
+
export * from './types';
|
|
10
|
+
|
|
11
|
+
// Re-export utilities that are part of the public API
|
|
12
|
+
export { rescaleLayout, rescaleLayoutDict } from './utils/rescale';
|
|
13
|
+
|
|
14
|
+
// Re-export all layout algorithms
|
|
15
|
+
export * from './layouts';
|
|
16
|
+
|
|
17
|
+
// Re-export all graph generation functions
|
|
18
|
+
export * from './generators';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random layout algorithm
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Graph, Node, PositionMap } from '../../types';
|
|
6
|
+
import { _processParams } from '../../utils/params';
|
|
7
|
+
import { getNodesFromGraph } from '../../utils/graph';
|
|
8
|
+
import { RandomNumberGenerator } from '../../utils/random';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Position nodes uniformly at random in the unit square.
|
|
12
|
+
*
|
|
13
|
+
* @param G - Graph or list of nodes
|
|
14
|
+
* @param center - Coordinate pair around which to center the layout
|
|
15
|
+
* @param dim - Dimension of layout
|
|
16
|
+
* @param seed - Random seed for reproducible layouts
|
|
17
|
+
* @returns Positions dictionary keyed by node
|
|
18
|
+
*/
|
|
19
|
+
export function randomLayout(G: Graph | Node[], center: number[] | null = null, dim: number = 2, seed: number | null = null): PositionMap {
|
|
20
|
+
const processed = _processParams(G, center, dim);
|
|
21
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
22
|
+
center = processed.center;
|
|
23
|
+
|
|
24
|
+
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
25
|
+
const pos: PositionMap = {};
|
|
26
|
+
|
|
27
|
+
nodes.forEach((node: Node) => {
|
|
28
|
+
pos[node] = (rng.rand(dim) as number[]).map((val: number, i: number) => val + center[i]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return pos;
|
|
32
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Graph, Node, PositionMap } from '../../types';
|
|
2
|
+
import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph';
|
|
3
|
+
import { RandomNumberGenerator } from '../../utils/random';
|
|
4
|
+
import { randomLayout } from '../basic/random';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Layout algorithm with attractive and repulsive forces (ARF).
|
|
8
|
+
*
|
|
9
|
+
* @param G - Graph
|
|
10
|
+
* @param pos - Initial positions for nodes
|
|
11
|
+
* @param scaling - Scale factor for positions
|
|
12
|
+
* @param a - Strength of springs between connected nodes (should be > 1)
|
|
13
|
+
* @param maxIter - Maximum number of iterations
|
|
14
|
+
* @param seed - Random seed for initial positions
|
|
15
|
+
* @returns Positions dictionary keyed by node
|
|
16
|
+
*/
|
|
17
|
+
export function arfLayout(
|
|
18
|
+
G: Graph,
|
|
19
|
+
pos: PositionMap | null = null,
|
|
20
|
+
scaling: number = 1,
|
|
21
|
+
a: number = 1.1,
|
|
22
|
+
maxIter: number = 1000,
|
|
23
|
+
seed: number | null = null
|
|
24
|
+
): PositionMap {
|
|
25
|
+
if (a <= 1) {
|
|
26
|
+
throw new Error("The parameter a should be larger than 1");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const nodes = getNodesFromGraph(G);
|
|
30
|
+
const edges = getEdgesFromGraph(G);
|
|
31
|
+
|
|
32
|
+
if (nodes.length === 0) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Initialize positions if not provided
|
|
37
|
+
if (!pos) {
|
|
38
|
+
pos = randomLayout(G, null, 2, seed);
|
|
39
|
+
} else {
|
|
40
|
+
// Make sure all nodes have positions
|
|
41
|
+
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
42
|
+
const defaultPos: PositionMap = {};
|
|
43
|
+
nodes.forEach((node: Node) => {
|
|
44
|
+
if (!pos![node]) {
|
|
45
|
+
defaultPos[node] = [(rng.rand() as number), (rng.rand() as number)];
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
pos = { ...pos, ...defaultPos };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create node index mapping
|
|
52
|
+
const nodeIndex: Record<Node, number> = {};
|
|
53
|
+
nodes.forEach((node: Node, i: number) => {
|
|
54
|
+
nodeIndex[node] = i;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Create positions array
|
|
58
|
+
const positions: number[][] = nodes.map((node: Node) => [...pos![node]]);
|
|
59
|
+
|
|
60
|
+
// Initialize spring constant matrix
|
|
61
|
+
const N = nodes.length;
|
|
62
|
+
const K = Array(N).fill(0).map(() => Array(N).fill(1));
|
|
63
|
+
|
|
64
|
+
// Set diagonal to zero (no self-attraction)
|
|
65
|
+
for (let i = 0; i < N; i++) {
|
|
66
|
+
K[i][i] = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Set stronger attraction between connected nodes
|
|
70
|
+
for (const [source, target] of edges) {
|
|
71
|
+
if (source === target) continue;
|
|
72
|
+
|
|
73
|
+
const i = nodeIndex[source];
|
|
74
|
+
const j = nodeIndex[target];
|
|
75
|
+
K[i][j] = a;
|
|
76
|
+
K[j][i] = a;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Calculate rho (scale factor)
|
|
80
|
+
const rho = scaling * Math.sqrt(N);
|
|
81
|
+
|
|
82
|
+
// Optimization loop
|
|
83
|
+
const dt = 1e-3; // Time step
|
|
84
|
+
const etol = 1e-6; // Error tolerance
|
|
85
|
+
let error = etol + 1;
|
|
86
|
+
let nIter = 0;
|
|
87
|
+
|
|
88
|
+
while (error > etol && nIter < maxIter) {
|
|
89
|
+
// Calculate changes for each node
|
|
90
|
+
const change = Array(N).fill(0).map(() => [0, 0]);
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < N; i++) {
|
|
93
|
+
for (let j = 0; j < N; j++) {
|
|
94
|
+
if (i === j) continue;
|
|
95
|
+
|
|
96
|
+
// Calculate difference vector
|
|
97
|
+
const diff = positions[i].map((coord, dim) => coord - positions[j][dim]);
|
|
98
|
+
|
|
99
|
+
// Calculate distance (with minimum to avoid division by zero)
|
|
100
|
+
const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01;
|
|
101
|
+
|
|
102
|
+
// Calculate attractive and repulsive forces
|
|
103
|
+
for (let d = 0; d < diff.length; d++) {
|
|
104
|
+
change[i][d] += K[i][j] * diff[d] - (rho / dist) * diff[d];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Update positions
|
|
110
|
+
for (let i = 0; i < N; i++) {
|
|
111
|
+
for (let d = 0; d < positions[i].length; d++) {
|
|
112
|
+
positions[i][d] += change[i][d] * dt;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Calculate error (sum of force magnitudes)
|
|
117
|
+
error = change.reduce((sum, c) =>
|
|
118
|
+
sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
|
|
119
|
+
|
|
120
|
+
nIter++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Convert positions array back to object
|
|
124
|
+
const finalPos: PositionMap = {};
|
|
125
|
+
nodes.forEach((node: Node, i: number) => {
|
|
126
|
+
finalPos[node] = positions[i];
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return finalPos;
|
|
130
|
+
}
|