@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,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,5 @@
1
+ /**
2
+ * Basic layout algorithms
3
+ */
4
+
5
+ export { randomLayout } from './random';
@@ -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
+ }