@graphty/layout 1.1.0 → 1.2.0
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 +89 -14
- package/.releaserc.json +22 -0
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +104 -0
- package/CONTRIBUTING.md +1 -0
- package/DEPLOYMENT.md +59 -0
- package/README.md +662 -93
- package/dist/layout-helpers.d.ts +123 -0
- package/dist/layout-helpers.js +458 -0
- package/dist/layout-helpers.js.map +1 -0
- package/dist/layout.d.ts +275 -0
- package/dist/layout.js +2304 -0
- package/dist/layout.js.map +1 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +30 -0
- package/dist/vitest.config.js.map +1 -0
- package/examples/3d-force-directed.html +611 -0
- package/examples/3d-kamada-kawai.html +394 -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 +49 -39
- package/examples/bipartite-layout.html +89 -69
- package/examples/circular-layout.html +26 -35
- package/examples/forceatlas2-layout.html +134 -28
- package/examples/index.html +75 -0
- package/examples/kamada-kawai-layout.html +13 -1
- package/examples/multipartite-layout.html +73 -88
- package/examples/planar-layout.html +13 -1
- package/examples/random-layout.html +13 -1
- package/examples/shell-layout.html +65 -34
- package/examples/spectral-layout.html +13 -1
- package/examples/spiral-layout.html +13 -1
- package/examples/spring-layout.html +25 -3
- package/layout-helpers.ts +560 -0
- package/layout.ts +316 -14
- package/package.json +20 -6
- package/test/arf-layout.test.ts +443 -0
- package/test/bfs-layout.test.ts +427 -0
- package/test/bipartite-layout.test.ts +344 -0
- package/test/circular-layout.test.ts +442 -0
- package/test/forceatlas2-layout.test.ts +405 -0
- package/test/fruchterman-reingold-layout.test.ts +477 -0
- package/test/graph-generators.test.ts +450 -0
- package/test/kamada-kawai-layout.test.ts +623 -0
- package/test/multipartite-layout.test.ts +404 -0
- package/test/planar-layout.test.ts +266 -0
- package/test/random-layout.test.ts +254 -0
- package/test/rescale-layout.test.ts +373 -0
- package/test/shell-layout.test.ts +347 -0
- package/test/spectral-layout.test.ts +378 -0
- package/test/spiral-layout.test.ts +338 -0
- package/test/spring-layout.test.ts +241 -0
- package/vite.config.js +36 -0
- package/vitest.config.ts +30 -0
- package/.releaserc +0 -3
package/layout.ts
CHANGED
|
@@ -245,12 +245,12 @@ function randomLayout(G: Graph, center: number[] | null = null, dim: number = 2,
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
|
-
* Position nodes on a circle.
|
|
248
|
+
* Position nodes on a circle (2D) or sphere (3D).
|
|
249
249
|
*
|
|
250
250
|
* @param G - Graph or list of nodes
|
|
251
251
|
* @param scale - Scale factor for positions
|
|
252
252
|
* @param center - Coordinate pair around which to center the layout
|
|
253
|
-
* @param dim - Dimension of layout (
|
|
253
|
+
* @param dim - Dimension of layout (supports 2D circle or 3D sphere)
|
|
254
254
|
* @returns Positions dictionary keyed by node
|
|
255
255
|
*/
|
|
256
256
|
function circularLayout(G: Graph, scale: number = 1, center: number[] | null = null, dim: number = 2): PositionMap {
|
|
@@ -273,14 +273,41 @@ function circularLayout(G: Graph, scale: number = 1, center: number[] | null = n
|
|
|
273
273
|
return pos;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
if (dim === 2) {
|
|
277
|
+
// 2D circle layout
|
|
278
|
+
const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
|
|
278
279
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
nodes.forEach((node: Node, i: number) => {
|
|
281
|
+
const x: number = Math.cos(theta[i]) * scale + center[0];
|
|
282
|
+
const y: number = Math.sin(theta[i]) * scale + center[1];
|
|
283
|
+
pos[node] = [x, y];
|
|
284
|
+
});
|
|
285
|
+
} else if (dim === 3) {
|
|
286
|
+
// 3D sphere layout using Fibonacci spiral
|
|
287
|
+
const n = nodes.length;
|
|
288
|
+
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
289
|
+
|
|
290
|
+
nodes.forEach((node: Node, i: number) => {
|
|
291
|
+
// Use Fibonacci spiral for even distribution on sphere
|
|
292
|
+
const theta = 2 * Math.PI * i / goldenRatio;
|
|
293
|
+
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
294
|
+
|
|
295
|
+
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
296
|
+
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
297
|
+
const z = Math.cos(phi) * scale + center[2];
|
|
298
|
+
|
|
299
|
+
pos[node] = [x, y, z];
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
// For higher dimensions, fall back to random on hypersphere
|
|
303
|
+
const rng = new RandomNumberGenerator();
|
|
304
|
+
nodes.forEach((node: Node) => {
|
|
305
|
+
// Generate random point on unit hypersphere
|
|
306
|
+
const coords = Array(dim).fill(0).map(() => rng.rand() as number * 2 - 1);
|
|
307
|
+
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
308
|
+
pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
284
311
|
|
|
285
312
|
return pos;
|
|
286
313
|
}
|
|
@@ -1615,10 +1642,9 @@ function kamadaKawaiLayout(
|
|
|
1615
1642
|
|
|
1616
1643
|
// Initialize positions if not provided
|
|
1617
1644
|
if (!pos) {
|
|
1618
|
-
if (dim >=
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
pos = circularLayout(G, 1, [0, 0], dim);
|
|
1645
|
+
if (dim >= 2) {
|
|
1646
|
+
// Use circular/spherical layout for 2D and 3D
|
|
1647
|
+
pos = circularLayout(G, 1, center, dim);
|
|
1622
1648
|
} else {
|
|
1623
1649
|
// For 1D, use a linear layout
|
|
1624
1650
|
const posArray: PositionMap = {};
|
|
@@ -2570,6 +2596,273 @@ function rescaleLayoutDict(
|
|
|
2570
2596
|
return scaledPos;
|
|
2571
2597
|
}
|
|
2572
2598
|
|
|
2599
|
+
// Graph generation utilities
|
|
2600
|
+
/**
|
|
2601
|
+
* Create a complete graph with n nodes
|
|
2602
|
+
* @param n - Number of nodes
|
|
2603
|
+
* @returns Graph object with all nodes connected to all other nodes
|
|
2604
|
+
*/
|
|
2605
|
+
function completeGraph(n: number): Graph {
|
|
2606
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
2607
|
+
const edges: Edge[] = [];
|
|
2608
|
+
|
|
2609
|
+
for (let i = 0; i < n; i++) {
|
|
2610
|
+
for (let j = i + 1; j < n; j++) {
|
|
2611
|
+
edges.push([i, j]);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
return {
|
|
2616
|
+
nodes: () => nodes,
|
|
2617
|
+
edges: () => edges
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
/**
|
|
2622
|
+
* Create a cycle graph with n nodes
|
|
2623
|
+
* @param n - Number of nodes
|
|
2624
|
+
* @returns Graph object with nodes connected in a cycle
|
|
2625
|
+
*/
|
|
2626
|
+
function cycleGraph(n: number): Graph {
|
|
2627
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
2628
|
+
const edges: Edge[] = [];
|
|
2629
|
+
|
|
2630
|
+
for (let i = 0; i < n; i++) {
|
|
2631
|
+
edges.push([i, (i + 1) % n]);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
return {
|
|
2635
|
+
nodes: () => nodes,
|
|
2636
|
+
edges: () => edges
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/**
|
|
2641
|
+
* Create a star graph with n nodes (1 center + n-1 leaves)
|
|
2642
|
+
* @param n - Total number of nodes
|
|
2643
|
+
* @returns Graph object with star topology
|
|
2644
|
+
*/
|
|
2645
|
+
function starGraph(n: number): Graph {
|
|
2646
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
2647
|
+
const edges: Edge[] = [];
|
|
2648
|
+
|
|
2649
|
+
// Connect all nodes to node 0 (center)
|
|
2650
|
+
for (let i = 1; i < n; i++) {
|
|
2651
|
+
edges.push([0, i]);
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
return {
|
|
2655
|
+
nodes: () => nodes,
|
|
2656
|
+
edges: () => edges
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
/**
|
|
2661
|
+
* Create a wheel graph with n nodes (1 center + n-1 rim nodes)
|
|
2662
|
+
* @param n - Total number of nodes
|
|
2663
|
+
* @returns Graph object with wheel topology
|
|
2664
|
+
*/
|
|
2665
|
+
function wheelGraph(n: number): Graph {
|
|
2666
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
2667
|
+
const edges: Edge[] = [];
|
|
2668
|
+
|
|
2669
|
+
// Connect all rim nodes to center (node 0)
|
|
2670
|
+
for (let i = 1; i < n; i++) {
|
|
2671
|
+
edges.push([0, i]);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Connect rim nodes in a cycle
|
|
2675
|
+
for (let i = 1; i < n - 1; i++) {
|
|
2676
|
+
edges.push([i, i + 1]);
|
|
2677
|
+
}
|
|
2678
|
+
if (n > 2) {
|
|
2679
|
+
edges.push([n - 1, 1]);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
return {
|
|
2683
|
+
nodes: () => nodes,
|
|
2684
|
+
edges: () => edges
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
/**
|
|
2689
|
+
* Create a grid graph with rows x cols nodes
|
|
2690
|
+
* @param rows - Number of rows
|
|
2691
|
+
* @param cols - Number of columns
|
|
2692
|
+
* @returns Graph object with grid topology
|
|
2693
|
+
*/
|
|
2694
|
+
function gridGraph(rows: number, cols: number): Graph {
|
|
2695
|
+
const nodes: Node[] = [];
|
|
2696
|
+
const edges: Edge[] = [];
|
|
2697
|
+
|
|
2698
|
+
// Create nodes
|
|
2699
|
+
for (let i = 0; i < rows; i++) {
|
|
2700
|
+
for (let j = 0; j < cols; j++) {
|
|
2701
|
+
nodes.push(`${i},${j}`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// Create edges
|
|
2706
|
+
for (let i = 0; i < rows; i++) {
|
|
2707
|
+
for (let j = 0; j < cols; j++) {
|
|
2708
|
+
// Connect to right neighbor
|
|
2709
|
+
if (j < cols - 1) {
|
|
2710
|
+
edges.push([`${i},${j}`, `${i},${j + 1}`]);
|
|
2711
|
+
}
|
|
2712
|
+
// Connect to bottom neighbor
|
|
2713
|
+
if (i < rows - 1) {
|
|
2714
|
+
edges.push([`${i},${j}`, `${i + 1},${j}`]);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
return {
|
|
2720
|
+
nodes: () => nodes,
|
|
2721
|
+
edges: () => edges
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
/**
|
|
2726
|
+
* Create a random graph with n nodes and given edge probability
|
|
2727
|
+
* @param n - Number of nodes
|
|
2728
|
+
* @param p - Probability of edge between any two nodes (0-1)
|
|
2729
|
+
* @param seed - Random seed for reproducibility
|
|
2730
|
+
* @returns Graph object with random edges
|
|
2731
|
+
*/
|
|
2732
|
+
function randomGraph(n: number, p: number, seed?: number): Graph {
|
|
2733
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
2734
|
+
const edges: Edge[] = [];
|
|
2735
|
+
|
|
2736
|
+
// Simple deterministic pseudo-random if seed provided
|
|
2737
|
+
let currentSeed = seed;
|
|
2738
|
+
let random = seed !== undefined
|
|
2739
|
+
? () => {
|
|
2740
|
+
currentSeed = (currentSeed! * 9301 + 49297) % 233280;
|
|
2741
|
+
return currentSeed / 233280;
|
|
2742
|
+
}
|
|
2743
|
+
: Math.random;
|
|
2744
|
+
|
|
2745
|
+
for (let i = 0; i < n; i++) {
|
|
2746
|
+
for (let j = i + 1; j < n; j++) {
|
|
2747
|
+
if (random() < p) {
|
|
2748
|
+
edges.push([i, j]);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
return {
|
|
2754
|
+
nodes: () => nodes,
|
|
2755
|
+
edges: () => edges
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
/**
|
|
2760
|
+
* Create a bipartite graph with two sets of nodes
|
|
2761
|
+
* @param n1 - Number of nodes in first set
|
|
2762
|
+
* @param n2 - Number of nodes in second set
|
|
2763
|
+
* @param p - Probability of edge between nodes in different sets
|
|
2764
|
+
* @param seed - Random seed for reproducibility
|
|
2765
|
+
* @returns Graph object with bipartite structure and setA/setB properties
|
|
2766
|
+
*/
|
|
2767
|
+
function bipartiteGraph(n1: number, n2: number, p: number, seed?: number): Graph & { setA: Node[], setB: Node[] } {
|
|
2768
|
+
const setA: Node[] = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
2769
|
+
const setB: Node[] = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
2770
|
+
const nodes = [...setA, ...setB];
|
|
2771
|
+
const edges: Edge[] = [];
|
|
2772
|
+
|
|
2773
|
+
// Simple deterministic pseudo-random if seed provided
|
|
2774
|
+
let currentSeed = seed;
|
|
2775
|
+
let random = seed !== undefined
|
|
2776
|
+
? () => {
|
|
2777
|
+
currentSeed = (currentSeed! * 9301 + 49297) % 233280;
|
|
2778
|
+
return currentSeed / 233280;
|
|
2779
|
+
}
|
|
2780
|
+
: Math.random;
|
|
2781
|
+
|
|
2782
|
+
// Only connect nodes between sets
|
|
2783
|
+
for (const a of setA) {
|
|
2784
|
+
for (const b of setB) {
|
|
2785
|
+
if (random() < p) {
|
|
2786
|
+
edges.push([a, b]);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
return {
|
|
2792
|
+
nodes: () => nodes,
|
|
2793
|
+
edges: () => edges,
|
|
2794
|
+
setA,
|
|
2795
|
+
setB
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
* Create a scale-free graph using Barabási-Albert model
|
|
2801
|
+
* @param n - Total number of nodes
|
|
2802
|
+
* @param m - Number of edges to attach from new node
|
|
2803
|
+
* @param seed - Random seed for reproducibility
|
|
2804
|
+
* @returns Graph object with scale-free properties
|
|
2805
|
+
*/
|
|
2806
|
+
function scaleFreeGraph(n: number, m: number, seed?: number): Graph {
|
|
2807
|
+
if (m >= n) {
|
|
2808
|
+
throw new Error('m must be less than n');
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
|
|
2812
|
+
const edges: Edge[] = [];
|
|
2813
|
+
const degrees = new Array(n).fill(0);
|
|
2814
|
+
|
|
2815
|
+
// Simple deterministic pseudo-random if seed provided
|
|
2816
|
+
let currentSeed = seed;
|
|
2817
|
+
let random = seed !== undefined
|
|
2818
|
+
? () => {
|
|
2819
|
+
currentSeed = (currentSeed! * 9301 + 49297) % 233280;
|
|
2820
|
+
return currentSeed / 233280;
|
|
2821
|
+
}
|
|
2822
|
+
: Math.random;
|
|
2823
|
+
|
|
2824
|
+
// Start with complete graph of m+1 nodes
|
|
2825
|
+
for (let i = 0; i <= m; i++) {
|
|
2826
|
+
for (let j = i + 1; j <= m; j++) {
|
|
2827
|
+
edges.push([i, j]);
|
|
2828
|
+
degrees[i]++;
|
|
2829
|
+
degrees[j]++;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// Add remaining nodes
|
|
2834
|
+
for (let i = m + 1; i < n; i++) {
|
|
2835
|
+
const targets = new Set<number>();
|
|
2836
|
+
const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
|
|
2837
|
+
|
|
2838
|
+
// Choose m targets based on preferential attachment
|
|
2839
|
+
while (targets.size < m) {
|
|
2840
|
+
let r = random() * totalDegree;
|
|
2841
|
+
let cumSum = 0;
|
|
2842
|
+
|
|
2843
|
+
for (let j = 0; j < i; j++) {
|
|
2844
|
+
cumSum += degrees[j];
|
|
2845
|
+
if (r <= cumSum && !targets.has(j)) {
|
|
2846
|
+
targets.add(j);
|
|
2847
|
+
break;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// Add edges to targets
|
|
2853
|
+
for (const target of targets) {
|
|
2854
|
+
edges.push([i, target]);
|
|
2855
|
+
degrees[i]++;
|
|
2856
|
+
degrees[target]++;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
return {
|
|
2861
|
+
nodes: () => nodes,
|
|
2862
|
+
edges: () => edges
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2573
2866
|
// Export the layout functions
|
|
2574
2867
|
export {
|
|
2575
2868
|
randomLayout,
|
|
@@ -2587,5 +2880,14 @@ export {
|
|
|
2587
2880
|
forceatlas2Layout,
|
|
2588
2881
|
arfLayout,
|
|
2589
2882
|
rescaleLayout,
|
|
2590
|
-
rescaleLayoutDict
|
|
2883
|
+
rescaleLayoutDict,
|
|
2884
|
+
// Graph generation utilities
|
|
2885
|
+
completeGraph,
|
|
2886
|
+
cycleGraph,
|
|
2887
|
+
starGraph,
|
|
2888
|
+
wheelGraph,
|
|
2889
|
+
gridGraph,
|
|
2890
|
+
randomGraph,
|
|
2891
|
+
bipartiteGraph,
|
|
2892
|
+
scaleFreeGraph
|
|
2591
2893
|
};
|
package/package.json
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphty/layout",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "graph layout algorithms based on networkx",
|
|
5
|
-
"main": "layout.js",
|
|
5
|
+
"main": "dist/layout.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"directories": {
|
|
8
8
|
"example": "examples"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"test:ui": "vitest --ui",
|
|
13
|
+
"test:run": "vitest run",
|
|
14
|
+
"test:coverage": "vitest run --coverage",
|
|
12
15
|
"prepare": "husky",
|
|
13
16
|
"build": "tsc",
|
|
17
|
+
"build:examples": "npm run build && cp dist/layout.js examples/layout.js",
|
|
14
18
|
"watch": "tsc --watch",
|
|
15
|
-
"dev": "tsc --watch"
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"commit": "cz",
|
|
21
|
+
"serve": "npm run build:examples && vite",
|
|
22
|
+
"examples": "npm run build:examples && vite"
|
|
16
23
|
},
|
|
17
24
|
"repository": {
|
|
18
25
|
"type": "git",
|
|
@@ -37,8 +44,15 @@
|
|
|
37
44
|
"devDependencies": {
|
|
38
45
|
"@commitlint/cli": "^19.8.1",
|
|
39
46
|
"@commitlint/config-conventional": "^19.8.1",
|
|
47
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
48
|
+
"@semantic-release/git": "^10.0.1",
|
|
49
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
50
|
+
"@vitest/ui": "^3.2.4",
|
|
40
51
|
"cz-conventional-changelog": "^3.3.0",
|
|
52
|
+
"happy-dom": "^18.0.1",
|
|
41
53
|
"husky": "^9.1.7",
|
|
42
|
-
"
|
|
54
|
+
"semantic-release": "^24.2.7",
|
|
55
|
+
"typescript": "^5.3.3",
|
|
56
|
+
"vitest": "^3.2.4"
|
|
43
57
|
}
|
|
44
|
-
}
|
|
58
|
+
}
|