@graphty/layout 1.2.0 → 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 (86) hide show
  1. package/.github/workflows/ci.yml +3 -7
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +1 -1
  4. package/dist/vitest.config.js +2 -2
  5. package/dist/vitest.config.js.map +1 -1
  6. package/examples/3d-kamada-kawai.html +4 -19
  7. package/examples/bfs-layout.html +26 -32
  8. package/examples/bipartite-layout.html +3 -3
  9. package/examples/forceatlas2-layout.html +106 -151
  10. package/{dist → examples}/layout-helpers.js +58 -30
  11. package/examples/multipartite-layout.html +32 -14
  12. package/examples/shell-layout.html +4 -2
  13. package/examples/spring-layout.html +1 -11
  14. package/package.json +3 -3
  15. package/src/algorithms/index.ts +6 -0
  16. package/src/algorithms/optimization/index.ts +12 -0
  17. package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
  18. package/src/algorithms/optimization/lbfgs.ts +68 -0
  19. package/src/algorithms/optimization/line-search.ts +50 -0
  20. package/src/algorithms/optimization/types.ts +8 -0
  21. package/src/algorithms/planarity/check.ts +38 -0
  22. package/src/algorithms/planarity/embedding.ts +216 -0
  23. package/src/algorithms/planarity/index.ts +12 -0
  24. package/src/algorithms/planarity/lr-test.ts +70 -0
  25. package/src/algorithms/planarity/special-graphs.ts +126 -0
  26. package/src/generators/basic.ts +93 -0
  27. package/src/generators/bipartite.ts +45 -0
  28. package/src/generators/grid.ts +42 -0
  29. package/src/generators/index.ts +15 -0
  30. package/src/generators/random.ts +39 -0
  31. package/src/generators/scale-free.ts +72 -0
  32. package/src/index.ts +18 -0
  33. package/src/layouts/basic/index.ts +5 -0
  34. package/src/layouts/basic/random.ts +32 -0
  35. package/src/layouts/force-directed/arf.ts +130 -0
  36. package/src/layouts/force-directed/forceatlas2.ts +407 -0
  37. package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
  38. package/src/layouts/force-directed/index.ts +9 -0
  39. package/src/layouts/force-directed/kamada-kawai.ts +112 -0
  40. package/src/layouts/force-directed/spring.ts +35 -0
  41. package/src/layouts/geometric/circular.ts +77 -0
  42. package/src/layouts/geometric/index.ts +7 -0
  43. package/src/layouts/geometric/shell.ts +80 -0
  44. package/src/layouts/geometric/spiral.ts +93 -0
  45. package/src/layouts/hierarchical/bfs.ts +81 -0
  46. package/src/layouts/hierarchical/bipartite.ts +94 -0
  47. package/src/layouts/hierarchical/index.ts +7 -0
  48. package/src/layouts/hierarchical/multipartite.ts +88 -0
  49. package/src/layouts/index.ts +9 -0
  50. package/src/layouts/specialized/index.ts +6 -0
  51. package/src/layouts/specialized/planar.ts +65 -0
  52. package/src/layouts/specialized/spectral.ts +128 -0
  53. package/src/types/embedding.ts +11 -0
  54. package/src/types/graph.ts +13 -0
  55. package/src/types/index.ts +7 -0
  56. package/src/types/layout.ts +9 -0
  57. package/src/utils/graph.ts +77 -0
  58. package/src/utils/index.ts +9 -0
  59. package/src/utils/numpy.ts +108 -0
  60. package/src/utils/params.ts +26 -0
  61. package/src/utils/random.ts +53 -0
  62. package/src/utils/rescale.ts +137 -0
  63. package/test/arf-layout.test.ts +1 -1
  64. package/test/bfs-layout.test.ts +1 -1
  65. package/test/bipartite-layout.test.ts +1 -1
  66. package/test/circular-layout.test.ts +1 -1
  67. package/test/forceatlas2-layout.test.ts +1 -1
  68. package/test/fruchterman-reingold-layout.test.ts +1 -1
  69. package/test/graph-generators.test.ts +1 -1
  70. package/test/kamada-kawai-layout.test.ts +1 -1
  71. package/test/multipartite-layout.test.ts +1 -1
  72. package/test/planar-layout.test.ts +1 -1
  73. package/test/random-layout.test.ts +1 -1
  74. package/test/rescale-layout.test.ts +1 -1
  75. package/test/shell-layout.test.ts +1 -1
  76. package/test/spectral-layout.test.ts +1 -1
  77. package/test/spiral-layout.test.ts +1 -1
  78. package/test/spring-layout.test.ts +1 -1
  79. package/vitest.config.ts +2 -2
  80. package/dist/layout-helpers.d.ts +0 -123
  81. package/dist/layout-helpers.js.map +0 -1
  82. package/dist/layout.d.ts +0 -275
  83. package/dist/layout.js +0 -2304
  84. package/dist/layout.js.map +0 -1
  85. package/layout-helpers.ts +0 -560
  86. package/layout.ts +0 -2893
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Bipartite 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 { rescaleLayout } from '../../utils/rescale';
9
+
10
+ /**
11
+ * Position nodes in two straight lines (bipartite layout).
12
+ *
13
+ * @param G - Graph or list of nodes
14
+ * @param nodes - Nodes in one node set of the graph
15
+ * @param align - The alignment of nodes: 'vertical' or 'horizontal'
16
+ * @param scale - Scale factor for positions
17
+ * @param center - Coordinate pair around which to center the layout
18
+ * @param aspectRatio - The ratio of the width to the height of the layout
19
+ * @returns Positions dictionary keyed by node
20
+ */
21
+ export function bipartiteLayout(
22
+ G: Graph,
23
+ nodes: Node[] | null = null,
24
+ align: 'vertical' | 'horizontal' = 'vertical',
25
+ scale: number = 1,
26
+ center: number[] | null = null,
27
+ aspectRatio: number = 4 / 3
28
+ ): PositionMap {
29
+ if (align !== 'vertical' && align !== 'horizontal') {
30
+ throw new Error("align must be either vertical or horizontal");
31
+ }
32
+
33
+ const processed = _processParams(G, center || [0, 0], 2);
34
+ const graph = processed.G;
35
+ center = processed.center;
36
+
37
+ const allNodes = getNodesFromGraph(graph);
38
+
39
+ if (allNodes.length === 0) {
40
+ return {};
41
+ }
42
+
43
+ // If nodes not provided, try to determine bipartite sets
44
+ if (!nodes) {
45
+ // A simple heuristic for bipartite detection: use nodes with even/odd indices
46
+ // This is a simplification, in Python NetworkX has bipartite.sets()
47
+ nodes = allNodes.filter((_: Node, i: number): boolean => i % 2 === 0);
48
+ }
49
+
50
+ const left = new Set(nodes);
51
+ const right: Set<Node> = new Set(allNodes.filter((n: Node) => !left.has(n)));
52
+
53
+ const height = 1;
54
+ const width = aspectRatio * height;
55
+ const offset = [width / 2, height / 2];
56
+
57
+ const pos: PositionMap = {};
58
+
59
+ // Position nodes in the left set
60
+ const leftNodes = [...left];
61
+ leftNodes.forEach((node, i) => {
62
+ const x = 0;
63
+ const y = i * height / (leftNodes.length || 1);
64
+ pos[node] = [x, y];
65
+ });
66
+
67
+ // Position nodes in the right set
68
+ const rightNodes = [...right];
69
+ rightNodes.forEach((node, i) => {
70
+ const x = width;
71
+ const y = i * height / (rightNodes.length || 1);
72
+ pos[node] = [x, y];
73
+ });
74
+
75
+ // Center positions around the origin and apply offset
76
+ for (const node in pos) {
77
+ pos[node][0] -= offset[0];
78
+ pos[node][1] -= offset[1];
79
+ }
80
+
81
+ // Rescale positions
82
+ const scaledPos = rescaleLayout(pos, scale, center) as PositionMap;
83
+
84
+ // Handle horizontal alignment
85
+ if (align === 'horizontal') {
86
+ for (const node in scaledPos) {
87
+ const temp = scaledPos[node][0];
88
+ scaledPos[node][0] = scaledPos[node][1];
89
+ scaledPos[node][1] = temp;
90
+ }
91
+ }
92
+
93
+ return scaledPos;
94
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Hierarchical layout algorithms
3
+ */
4
+
5
+ export { bipartiteLayout } from './bipartite';
6
+ export { multipartiteLayout } from './multipartite';
7
+ export { bfsLayout } from './bfs';
@@ -0,0 +1,88 @@
1
+ import type { Graph, Node, PositionMap } from '../../types';
2
+ import { getNodesFromGraph, getNeighbors } from '../../utils/graph';
3
+ import { _processParams } from '../../utils/params';
4
+ import { rescaleLayout } from '../../utils/rescale';
5
+
6
+ /**
7
+ * Position nodes in layers of straight lines (multipartite layout).
8
+ *
9
+ * @param G - Graph or list of nodes
10
+ * @param subsetKey - Object mapping layers to node sets, or node attribute name
11
+ * @param align - The alignment of nodes: 'vertical' or 'horizontal'
12
+ * @param scale - Scale factor for positions
13
+ * @param center - Coordinate pair around which to center the layout
14
+ * @returns Positions dictionary keyed by node
15
+ */
16
+ export function multipartiteLayout(
17
+ G: Graph,
18
+ subsetKey: Record<number | string, Node | Node[]> | string = 'subset',
19
+ align: 'vertical' | 'horizontal' = 'vertical',
20
+ scale: number = 1,
21
+ center: number[] | null = null
22
+ ): PositionMap {
23
+ if (align !== 'vertical' && align !== 'horizontal') {
24
+ throw new Error("align must be either vertical or horizontal");
25
+ }
26
+
27
+ const processed = _processParams(G, center || [0, 0], 2);
28
+ const graph = processed.G;
29
+ center = processed.center;
30
+
31
+ const allNodes = getNodesFromGraph(graph);
32
+
33
+ if (allNodes.length === 0) {
34
+ return {};
35
+ }
36
+
37
+ // Convert subsetKey to a layer mapping if it's a string
38
+ let layers: Record<number | string, Node[]> = {};
39
+ if (typeof subsetKey === 'string') {
40
+ // In JS we don't have access to node attributes directly
41
+ // This is a simplification - in a real implementation we would need
42
+ // to access node attributes from the graph
43
+ console.warn("Using string subsetKey requires node attributes, using default partitioning");
44
+ // Create a simple partitioning as fallback
45
+ layers = { 0: allNodes };
46
+ } else {
47
+ // subsetKey is already a mapping of layers to nodes
48
+ // Convert single nodes to arrays
49
+ for (const [key, value] of Object.entries(subsetKey)) {
50
+ if (Array.isArray(value)) {
51
+ layers[key] = value;
52
+ } else {
53
+ layers[key] = [value];
54
+ }
55
+ }
56
+ }
57
+
58
+ const layerCount = Object.keys(layers).length;
59
+ let pos: PositionMap = {};
60
+
61
+ // Process each layer
62
+ Object.entries(layers).forEach(([layer, nodes], layerIdx) => {
63
+ const layerNodes = Array.isArray(nodes) ? nodes : [nodes];
64
+ const layerSize = layerNodes.length;
65
+
66
+ layerNodes.forEach((node, nodeIdx) => {
67
+ // Place nodes in a grid: layerIdx determines x-coordinate (column)
68
+ // nodeIdx determines y-coordinate (row position within column)
69
+ const x = layerIdx - (layerCount - 1) / 2;
70
+ const y = nodeIdx - (layerSize - 1) / 2;
71
+ pos[node] = [x, y];
72
+ });
73
+ });
74
+
75
+ // Rescale positions
76
+ pos = rescaleLayout(pos, scale, center) as PositionMap;
77
+
78
+ // Handle horizontal alignment
79
+ if (align === 'horizontal') {
80
+ for (const node in pos) {
81
+ const temp = pos[node][0];
82
+ pos[node][0] = pos[node][1];
83
+ pos[node][1] = temp;
84
+ }
85
+ }
86
+
87
+ return pos;
88
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Re-export all layout algorithms
3
+ */
4
+
5
+ export * from './basic';
6
+ export * from './geometric';
7
+ export * from './force-directed';
8
+ export * from './hierarchical';
9
+ export * from './specialized';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Specialized layout algorithms
3
+ */
4
+
5
+ export { spectralLayout } from './spectral';
6
+ export { planarLayout } from './planar';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Planar layout algorithm
3
+ */
4
+
5
+ import { Graph, Node, Edge, PositionMap } from '../../types';
6
+ import { _processParams } from '../../utils/params';
7
+ import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph';
8
+ import { rescaleLayout } from '../../utils/rescale';
9
+ import { checkPlanarity, combinatorialEmbeddingToPos } from '../../algorithms/planarity';
10
+
11
+ /**
12
+ * Position nodes without edge intersections (planar layout).
13
+ *
14
+ * @param G - Graph
15
+ * @param scale - Scale factor for positions
16
+ * @param center - Coordinate pair around which to center the layout
17
+ * @param dim - Dimension of layout (must be 2)
18
+ * @returns Positions dictionary keyed by node
19
+ */
20
+ export function planarLayout(
21
+ G: Graph,
22
+ scale: number = 1,
23
+ center: number[] | null = null,
24
+ dim: number = 2
25
+ ): PositionMap {
26
+ if (dim !== 2) {
27
+ throw new Error("can only handle 2 dimensions");
28
+ }
29
+
30
+ const processed = _processParams(G, center || [0, 0], dim);
31
+
32
+ // Planar layout requires a proper Graph, not just a list of nodes
33
+ if (Array.isArray(processed.G)) {
34
+ throw new Error('Planar layout requires a Graph with edges, not just a list of nodes');
35
+ }
36
+
37
+ const graph = processed.G;
38
+ center = processed.center;
39
+
40
+ const nodes = getNodesFromGraph(graph);
41
+ const edges = getEdgesFromGraph(graph);
42
+
43
+ if (nodes.length === 0) {
44
+ return {};
45
+ }
46
+
47
+ // Check if graph is planar and get embedding
48
+ const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges);
49
+
50
+ if (!isPlanar) {
51
+ throw new Error("G is not planar.");
52
+ }
53
+
54
+ if (!embedding) {
55
+ throw new Error("Failed to generate planar embedding.");
56
+ }
57
+
58
+ // Convert embedding to positions
59
+ let pos = combinatorialEmbeddingToPos(embedding, nodes);
60
+
61
+ // Rescale the positions
62
+ pos = rescaleLayout(pos, scale, center) as PositionMap;
63
+
64
+ return pos;
65
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Spectral layout algorithm using eigenvectors of the graph Laplacian
3
+ */
4
+
5
+ import { Graph, Node, Edge, PositionMap } from '../../types';
6
+ import { _processParams } from '../../utils/params';
7
+ import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph';
8
+ import { rescaleLayout } from '../../utils/rescale';
9
+
10
+ /**
11
+ * Position nodes in a spectral layout using eigenvectors of the graph Laplacian.
12
+ *
13
+ * @param G - Graph
14
+ * @param scale - Scale factor for positions
15
+ * @param center - Coordinate pair around which to center the layout
16
+ * @param dim - Dimension of layout
17
+ * @returns Positions dictionary keyed by node
18
+ */
19
+ export function spectralLayout(
20
+ G: Graph,
21
+ scale: number = 1,
22
+ center: number[] | null = null,
23
+ dim: number = 2
24
+ ): PositionMap {
25
+ const processed = _processParams(G, center, dim);
26
+ const graph = processed.G;
27
+ center = processed.center;
28
+
29
+ const nodes = getNodesFromGraph(graph);
30
+
31
+ if (nodes.length <= 2) {
32
+ if (nodes.length === 0) {
33
+ return {};
34
+ } else if (nodes.length === 1) {
35
+ return { [nodes[0]]: center };
36
+ } else {
37
+ return {
38
+ [nodes[0]]: center.map(v => v - scale),
39
+ [nodes[1]]: center.map(v => v + scale)
40
+ };
41
+ }
42
+ }
43
+
44
+ // Create adjacency matrix
45
+ const N = nodes.length;
46
+ const nodeIndices: Record<Node, number> = {};
47
+ nodes.forEach((node: Node, i: number) => { nodeIndices[node] = i; });
48
+
49
+ const A = Array(N).fill(0).map(() => Array(N).fill(0));
50
+ const edges = getEdgesFromGraph(graph);
51
+
52
+ for (const [source, target] of edges) {
53
+ const i = nodeIndices[source];
54
+ const j = nodeIndices[target];
55
+ A[i][j] = 1;
56
+ A[j][i] = 1; // Make symmetric for undirected graphs
57
+ }
58
+
59
+ // Create Laplacian matrix: L = D - A where D is degree matrix
60
+ const L = Array(N).fill(0).map(() => Array(N).fill(0));
61
+ for (let i = 0; i < N; i++) {
62
+ // Compute degree (sum of row)
63
+ L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
64
+ for (let j = 0; j < N; j++) {
65
+ L[i][j] -= A[i][j];
66
+ }
67
+ }
68
+
69
+ // Compute eigenvectors using power iteration method
70
+ // We need the smallest non-zero eigenvectors of L
71
+ const eigenvectors: number[][] = [];
72
+
73
+ // For each dimension, find an eigenvector
74
+ for (let d = 0; d < dim; d++) {
75
+ let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
76
+
77
+ // Orthogonalize against previous eigenvectors
78
+ for (const ev of eigenvectors) {
79
+ const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
80
+ vector = vector.map((val, idx) => val - dot * ev[idx]);
81
+ }
82
+
83
+ // Normalize
84
+ const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
85
+ vector = vector.map(val => val / norm);
86
+
87
+ // Apply shifted inverse iteration to find smallest non-zero eigenvector
88
+ // This is a simplification of the actual algorithm
89
+ for (let iter = 0; iter < 100; iter++) {
90
+ // Apply Laplacian
91
+ const newVec = Array(N).fill(0);
92
+ for (let i = 0; i < N; i++) {
93
+ for (let j = 0; j < N; j++) {
94
+ newVec[i] += L[i][j] * vector[j];
95
+ }
96
+ }
97
+
98
+ // Orthogonalize against the constant vector (eigenvector with eigenvalue 0)
99
+ const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
100
+ newVec.forEach((val, idx, arr) => { arr[idx] = val - mean; });
101
+
102
+ // Normalize
103
+ const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
104
+ if (newNorm < 1e-10) continue; // Skip if vector is close to zero
105
+
106
+ vector = newVec.map(val => val / newNorm);
107
+ }
108
+
109
+ eigenvectors.push(vector);
110
+ }
111
+
112
+ // Create position array from eigenvectors
113
+ const positions: number[][] = Array(N).fill(0).map(() => Array(dim).fill(0));
114
+ for (let i = 0; i < N; i++) {
115
+ for (let d = 0; d < dim; d++) {
116
+ positions[i][d] = eigenvectors[d][i];
117
+ }
118
+ }
119
+
120
+ // Rescale and create position dictionary
121
+ const scaledPositions = rescaleLayout(positions as any, scale);
122
+ const pos: PositionMap = {};
123
+ nodes.forEach((node: Node, i: number) => {
124
+ pos[node] = (scaledPositions as number[][])[i].map((val: number, j: number) => val + center[j]);
125
+ });
126
+
127
+ return pos;
128
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Embedding type for planar layout algorithms
3
+ */
4
+
5
+ import { Node, Position } from './index';
6
+
7
+ export interface Embedding {
8
+ nodeOrder: Node[];
9
+ faceList: Node[][];
10
+ nodePositions: Record<Node, Position>;
11
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Core graph type definitions
3
+ */
4
+
5
+ export type Node = string | number;
6
+
7
+ export type Edge = [Node, Node];
8
+
9
+ export type Graph = {
10
+ nodes: () => Node[];
11
+ edges: () => Edge[];
12
+ getEdgeData?: (source: Node, target: Node, attr: string) => any;
13
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Re-export all type definitions
3
+ */
4
+
5
+ export * from './graph';
6
+ export * from './layout';
7
+ export * from './embedding';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Layout-specific type definitions
3
+ */
4
+
5
+ import { Node } from './graph';
6
+
7
+ export type Position = number[];
8
+
9
+ export type PositionMap = Record<Node, Position>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Graph utility functions
3
+ */
4
+
5
+ import { Graph, Node, Edge } from '../types';
6
+
7
+ /**
8
+ * Extract nodes from a graph object
9
+ *
10
+ * @param G - Graph or list of nodes
11
+ * @returns Array of nodes
12
+ */
13
+ export function getNodesFromGraph(G: Graph | Node[]): Node[] {
14
+ if (Array.isArray(G)) {
15
+ return G;
16
+ }
17
+ return G.nodes();
18
+ }
19
+
20
+ /**
21
+ * Extract edges from a graph object
22
+ *
23
+ * @param G - Graph or list of nodes
24
+ * @returns Array of edges
25
+ */
26
+ export function getEdgesFromGraph(G: Graph | Node[]): Edge[] {
27
+ if (Array.isArray(G)) {
28
+ return [];
29
+ }
30
+ return G.edges();
31
+ }
32
+
33
+ /**
34
+ * Get the degree of a node in the graph
35
+ *
36
+ * @param graph - Graph object
37
+ * @param node - Node to get degree for
38
+ * @returns Degree of the node
39
+ */
40
+ export function getNodeDegree(graph: Graph, node: Node): number {
41
+ if (!graph.edges) return 0;
42
+
43
+ const edges = graph.edges();
44
+ let degree = 0;
45
+
46
+ for (const [source, target] of edges) {
47
+ if (source === node || target === node) {
48
+ degree++;
49
+ }
50
+ }
51
+
52
+ return degree;
53
+ }
54
+
55
+ /**
56
+ * Get the neighbors of a node in the graph
57
+ *
58
+ * @param graph - Graph object
59
+ * @param node - Node to get neighbors for
60
+ * @returns Array of neighbor nodes
61
+ */
62
+ export function getNeighbors(graph: Graph, node: Node): Node[] {
63
+ if (!graph.edges) return [];
64
+
65
+ const neighbors = new Set<Node>();
66
+ const edges = graph.edges();
67
+
68
+ for (const [source, target] of edges) {
69
+ if (source === node) {
70
+ neighbors.add(target);
71
+ } else if (target === node) {
72
+ neighbors.add(source);
73
+ }
74
+ }
75
+
76
+ return Array.from(neighbors);
77
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Re-export all utilities
3
+ */
4
+
5
+ export * from './numpy';
6
+ export * from './random';
7
+ export * from './graph';
8
+ export * from './params';
9
+ export * from './rescale';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * NumPy-like array manipulation functions
3
+ * Maintains exact same functionality as original implementation
4
+ */
5
+
6
+ export const np = {
7
+ zeros: function (shape: number | number[]): number | number[] | number[][] | any[] {
8
+ if (typeof shape === 'number') {
9
+ return Array(shape).fill(0);
10
+ }
11
+ if (shape.length === 1) {
12
+ return Array(shape[0]).fill(0);
13
+ }
14
+ return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
15
+ },
16
+
17
+ ones: function (shape: number | number[]): number | any[] {
18
+ if (typeof shape === 'number') {
19
+ return Array(shape).fill(1);
20
+ }
21
+ if (shape.length === 1) {
22
+ return Array(shape[0]).fill(1);
23
+ }
24
+ return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
25
+ },
26
+
27
+ linspace: function (start: number, stop: number, num: number): number[] {
28
+ const step = (stop - start) / (num - 1);
29
+ return Array.from({ length: num }, (_, i) => start + i * step);
30
+ },
31
+
32
+ array: function (arr: any): any[] {
33
+ return Array.isArray(arr) ? [...arr] : [arr];
34
+ },
35
+
36
+ repeat: function (a: any, repeats: number): any[] {
37
+ const result: any[] = [];
38
+ for (let i = 0; i < repeats; i++) {
39
+ result.push(...np.array(a));
40
+ }
41
+ return result;
42
+ },
43
+
44
+ mean: function (arr: number[] | number[][], axis: number | null = null): number | number[] {
45
+ if (axis === null) {
46
+ const flatArr = Array.isArray(arr[0])
47
+ ? (arr as number[][]).flat(Infinity) as number[]
48
+ : arr as number[];
49
+ const sum = flatArr.reduce((a, b) => a + b, 0);
50
+ return sum / flatArr.length;
51
+ }
52
+
53
+ if (axis === 0) {
54
+ const result: number[] = [];
55
+ const matrix = arr as number[][];
56
+ for (let i = 0; i < matrix[0].length; i++) {
57
+ let sum = 0;
58
+ for (let j = 0; j < matrix.length; j++) {
59
+ sum += matrix[j][i];
60
+ }
61
+ result.push(sum / matrix.length);
62
+ }
63
+ return result;
64
+ }
65
+
66
+ return (arr as number[][]).map(row => np.mean(row) as number);
67
+ },
68
+
69
+ add: function (a: number | number[], b: number | number[]): number | number[] {
70
+ if (!Array.isArray(a) && !Array.isArray(b)) {
71
+ return a + b;
72
+ }
73
+ if (!Array.isArray(a)) {
74
+ return (b as number[]).map(val => a + val);
75
+ }
76
+ if (!Array.isArray(b)) {
77
+ return (a as number[]).map(val => val + b);
78
+ }
79
+ return (a as number[]).map((val, i) => val + (b as number[])[i]);
80
+ },
81
+
82
+ subtract: function (a: number | number[], b: number | number[]): number | number[] {
83
+ if (!Array.isArray(a) && !Array.isArray(b)) {
84
+ return a - b;
85
+ }
86
+ if (!Array.isArray(a)) {
87
+ return (b as number[]).map(val => a - val);
88
+ }
89
+ if (!Array.isArray(b)) {
90
+ return (a as number[]).map(val => val - b);
91
+ }
92
+ return (a as number[]).map((val, i) => val - (b as number[])[i]);
93
+ },
94
+
95
+ max: function (arr: number | number[]): number {
96
+ if (!Array.isArray(arr)) return arr;
97
+ return Math.max(...(arr as number[]).flat(Infinity) as number[]);
98
+ },
99
+
100
+ min: function (arr: number | number[]): number {
101
+ if (!Array.isArray(arr)) return arr;
102
+ return Math.min(...(arr as number[]).flat(Infinity) as number[]);
103
+ },
104
+
105
+ norm: function (arr: number[]): number {
106
+ return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
107
+ }
108
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Parameter processing utilities
3
+ */
4
+
5
+ import { Graph, Node } from '../types';
6
+
7
+ /**
8
+ * Process and validate layout parameters
9
+ * Helper function similar to _process_params in Python version
10
+ *
11
+ * @param G - Graph object or array of nodes
12
+ * @param center - Center coordinates or null
13
+ * @param dim - Dimension of layout
14
+ * @returns Processed parameters
15
+ */
16
+ export function _processParams(G: Graph | Node[], center: number[] | null, dim: number): { G: Graph | Node[]; center: number[] } {
17
+ if (!center) {
18
+ center = Array(dim).fill(0);
19
+ }
20
+
21
+ if (center.length !== dim) {
22
+ throw new Error("length of center coordinates must match dimension of layout");
23
+ }
24
+
25
+ return { G, center };
26
+ }