@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
@@ -38,16 +38,12 @@ jobs:
38
38
  run: npm run test:coverage
39
39
  if: matrix.node-version == '20.x'
40
40
 
41
- - name: Upload coverage reports to Codecov
42
- uses: codecov/codecov-action@v4
41
+ - name: Upload coverage reports to Coveralls
42
+ uses: coverallsapp/github-action@v2
43
43
  if: matrix.node-version == '20.x'
44
44
  with:
45
+ github-token: ${{ secrets.GITHUB_TOKEN }}
45
46
  file: ./coverage/lcov.info
46
- flags: unittests
47
- name: codecov-umbrella
48
- fail_ci_if_error: false
49
- env:
50
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
51
47
 
52
48
  lint:
53
49
  name: Lint
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.2.1](https://github.com/graphty-org/layout/compare/v1.2.0...v1.2.1) (2025-07-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * resolve import errors and browser hang in shell layout example ([d65e436](https://github.com/graphty-org/layout/commit/d65e4360d9c42c885731ec952ea52978da61c7d5))
7
+
1
8
  # [1.2.0](https://github.com/graphty-org/layout/compare/v1.1.1...v1.2.0) (2025-07-12)
2
9
 
3
10
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Layout
2
2
 
3
3
  [![CI](https://github.com/graphty-org/layout/actions/workflows/ci.yml/badge.svg)](https://github.com/graphty-org/layout/actions/workflows/ci.yml)
4
- [![codecov](https://codecov.io/gh/graphty-org/layout/branch/main/graph/badge.svg)](https://codecov.io/gh/graphty-org/layout)
4
+ [![Coverage Status](https://coveralls.io/repos/github/graphty-org/layout/badge.svg?branch=main)](https://coveralls.io/github/graphty-org/layout?branch=main)
5
5
  [![npm version](https://badge.fury.io/js/%40graphty%2Flayout.svg)](https://badge.fury.io/js/%40graphty%2Flayout)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
@@ -7,8 +7,8 @@ export default defineConfig({
7
7
  testTimeout: 30000,
8
8
  coverage: {
9
9
  provider: 'v8',
10
- reporter: ['text', 'json', 'html'],
11
- include: ['layout.ts'],
10
+ reporter: ['text', 'json', 'html', 'lcov'],
11
+ include: ['src/**/*.ts'],
12
12
  exclude: ['**/*.d.ts', '**/*.test.ts'],
13
13
  all: true,
14
14
  thresholds: {
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC;IAC1B,IAAI,EAAE;QACJ,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,WAAW;QACxB,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE;YACR,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAClC,OAAO,EAAE,CAAC,WAAW,CAAC;YACtB,OAAO,EAAE,CAAC,WAAW,EAAE,cAAc,CAAC;YACtC,GAAG,EAAE,IAAI;YACT,UAAU,EAAE;gBACV,KAAK,EAAE,EAAE;gBACT,SAAS,EAAE,EAAE;gBACb,QAAQ,EAAE,EAAE;gBACZ,UAAU,EAAE,EAAE;aACf;SACF;QACD,OAAO,EAAE,CAAC,mBAAmB,CAAC;QAC9B,SAAS,EAAE,CAAC,SAAS,CAAC;KACvB;IACD,OAAO,EAAE;QACP,KAAK,EAAE;YACL,GAAG,EAAE,MAAM;SACZ;KACF;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC;IAC1B,IAAI,EAAE;QACJ,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,WAAW;QACxB,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE;YACR,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC1C,OAAO,EAAE,CAAC,aAAa,CAAC;YACxB,OAAO,EAAE,CAAC,WAAW,EAAE,cAAc,CAAC;YACtC,GAAG,EAAE,IAAI;YACT,UAAU,EAAE;gBACV,KAAK,EAAE,EAAE;gBACT,SAAS,EAAE,EAAE;gBACb,QAAQ,EAAE,EAAE;gBACZ,UAAU,EAAE,EAAE;aACf;SACF;QACD,OAAO,EAAE,CAAC,mBAAmB,CAAC;QAC9B,SAAS,EAAE,CAAC,SAAS,CAAC;KACvB;IACD,OAAO,EAAE;QACP,KAAK,EAAE;YACL,GAAG,EAAE,MAAM;SACZ;KACF;CACF,CAAC,CAAC"}
@@ -95,12 +95,6 @@
95
95
  </label>
96
96
  <button id="regenerate">Regenerate Graph</button>
97
97
  <button id="relayout">Re-run Layout</button>
98
- <label>
99
- <input type="checkbox" id="showAxes" checked> Show Axes
100
- </label>
101
- <label>
102
- <input type="checkbox" id="animateNodes"> Animate Nodes
103
- </label>
104
98
  </div>
105
99
  <div id="status">
106
100
  <div>Nodes: <span id="nodeCount">0</span></div>
@@ -327,7 +321,6 @@
327
321
  const scaleLabel = document.getElementById('scaleLabel');
328
322
  const regenerateBtn = document.getElementById('regenerate');
329
323
  const relayoutBtn = document.getElementById('relayout');
330
- const animateNodesCheckbox = document.getElementById('animateNodes');
331
324
 
332
325
  graphSizeSlider.addEventListener('input', (e) => {
333
326
  sizeLabel.textContent = e.target.value;
@@ -364,18 +357,10 @@
364
357
 
365
358
  time += 0.01;
366
359
 
367
- // Animate nodes
368
- if (animateNodesCheckbox.checked) {
369
- nodeAnimations.forEach((mesh, i) => {
370
- const offset = Math.sin(time + i * 0.5) * 5;
371
- mesh.position.copy(mesh.userData.originalPosition);
372
- mesh.position.y += offset;
373
- });
374
- } else {
375
- nodeAnimations.forEach((mesh) => {
376
- mesh.position.copy(mesh.userData.originalPosition);
377
- });
378
- }
360
+ // Keep nodes at their original positions
361
+ nodeAnimations.forEach((mesh) => {
362
+ mesh.position.copy(mesh.userData.originalPosition);
363
+ });
379
364
 
380
365
  controls.update();
381
366
  renderer.render(scene, camera);
@@ -169,61 +169,55 @@
169
169
  </div>
170
170
 
171
171
  <script type="module">
172
- import {
173
- bfsLayout,
174
- starGraph,
175
- gridGraph,
176
- randomGraph,
177
- scaleFreeGraph,
178
- findBestRoot
179
- } from "./layout.js";
172
+ import { bfsLayout, scaleFreeGraph, randomGraph, gridGraph, starGraph } from "./layout.js";
180
173
 
181
174
  let currentGraph = null;
182
175
  let bestRoot = null;
183
176
 
184
177
  function generateGraph(type, numNodes) {
178
+ let graph;
185
179
  switch(type) {
186
180
  case 'tree':
187
181
  // Generate tree-like structure using scale-free with low m
188
- return scaleFreeGraph(numNodes, 1, Date.now());
182
+ graph = scaleFreeGraph(numNodes, 1, Date.now());
183
+ break;
189
184
 
190
185
  case 'connected':
191
186
  // Generate connected random graph
192
- return randomGraph(numNodes, 0.15, Date.now());
187
+ graph = randomGraph(numNodes, 0.15, Date.now());
188
+ break;
193
189
 
194
190
  case 'grid':
195
191
  // Create grid
196
192
  const size = Math.ceil(Math.sqrt(numNodes));
197
- return gridGraph(size, size);
193
+ const gridG = gridGraph(size, size);
194
+ // Convert string node IDs to numbers for consistency
195
+ const nodeMap = new Map();
196
+ gridG.nodes().forEach((node, i) => nodeMap.set(node, i));
197
+ graph = {
198
+ nodes: () => Array.from({length: gridG.nodes().length}, (_, i) => i),
199
+ edges: () => gridG.edges().map(([u, v]) => [nodeMap.get(u), nodeMap.get(v)])
200
+ };
201
+ break;
198
202
 
199
203
  case 'star':
200
- return starGraph(numNodes);
201
-
202
- default:
203
- return randomGraph(numNodes, 0.2, Date.now());
204
- edges.push([i, i + gridSize]);
205
- }
206
- }
204
+ graph = starGraph(numNodes);
207
205
  break;
208
206
 
209
- case 'star':
210
- // Star with center at node 0
211
- for (let i = 1; i < numNodes; i++) {
212
- edges.push([0, i]);
213
- }
207
+ default:
208
+ graph = randomGraph(numNodes, 0.2, Date.now());
214
209
  break;
215
210
  }
216
211
 
217
- return {
218
- nodes: () => nodes,
219
- edges: () => edges,
220
- // Simplified implementation to get neighbors
221
- getNeighbors: function(node) {
222
- return edges
223
- .filter(([s, t]) => s === node || t === node)
224
- .map(([s, t]) => s === node ? t : s);
225
- }
212
+ // Add getNeighbors method to the graph
213
+ graph.getNeighbors = function(node) {
214
+ const edges = this.edges();
215
+ return edges
216
+ .filter(([s, t]) => s === node || t === node)
217
+ .map(([s, t]) => s === node ? t : s);
226
218
  };
219
+
220
+ return graph;
227
221
  }
228
222
 
229
223
  function updateStartNodeOptions() {
@@ -165,9 +165,9 @@
165
165
  import {
166
166
  bipartiteLayout,
167
167
  bipartiteGraph,
168
- detectBipartite,
169
168
  randomGraph
170
169
  } from "./layout.js";
170
+ import { detectBipartite } from "./layout-helpers.js";
171
171
 
172
172
  let currentGraph = null;
173
173
  let detectedBipartite = null;
@@ -335,7 +335,7 @@
335
335
  document.getElementById('alignment').onchange = null;
336
336
  }
337
337
 
338
- window.generateMixedGraph = function() {
338
+ window.generateMixedGraph = async function() {
339
339
  const totalNodes = parseInt(document.getElementById('set-a-size').value) +
340
340
  parseInt(document.getElementById('set-b-size').value);
341
341
  const edgeDensity = parseFloat(document.getElementById('edge-density').value);
@@ -360,7 +360,7 @@
360
360
  `;
361
361
  } else {
362
362
  // Not bipartite - show with spring layout instead
363
- const { springLayout } = await import('../dist/layout.js');
363
+ const { springLayout } = await import('./layout.js');
364
364
  const positions = springLayout(currentGraph);
365
365
  visualizeGraph(currentGraph, positions);
366
366
 
@@ -203,16 +203,117 @@
203
203
 
204
204
  <script type="module">
205
205
  import {
206
- forceatlas2Layout,
207
- randomGraph,
208
- scaleFreeGraph,
209
- gridGraph,
210
- autoConfigureForce
206
+ forceatlas2Layout
211
207
  } from "./layout.js";
212
208
 
209
+ import {
210
+ autoConfigureForce
211
+ } from "./layout-helpers.js";
212
+
213
213
  let currentGraph = null;
214
214
  let currentConfig = null;
215
215
 
216
+ // Graph generation utilities
217
+ function randomGraph(n, p, seed) {
218
+ const nodes = Array.from({length: n}, (_, i) => i);
219
+ const edges = [];
220
+
221
+ // Simple random graph with probability p for each edge
222
+ for (let i = 0; i < n; i++) {
223
+ for (let j = i + 1; j < n; j++) {
224
+ if (Math.random() < p) {
225
+ edges.push([i, j]);
226
+ }
227
+ }
228
+ }
229
+
230
+ return {
231
+ nodes: () => nodes,
232
+ edges: () => edges
233
+ };
234
+ }
235
+
236
+ function scaleFreeGraph(n, m, seed) {
237
+ const nodes = Array.from({length: n}, (_, i) => i);
238
+ const edges = [];
239
+
240
+ // Start with a complete graph of m+1 nodes
241
+ for (let i = 0; i <= m; i++) {
242
+ for (let j = i + 1; j <= m; j++) {
243
+ edges.push([i, j]);
244
+ }
245
+ }
246
+
247
+ // Add remaining nodes with preferential attachment
248
+ for (let i = m + 1; i < n; i++) {
249
+ const degrees = new Map();
250
+ for (const [u, v] of edges) {
251
+ degrees.set(u, (degrees.get(u) || 0) + 1);
252
+ degrees.set(v, (degrees.get(v) || 0) + 1);
253
+ }
254
+
255
+ const totalDegree = Array.from(degrees.values()).reduce((a, b) => a + b, 0);
256
+ const targets = new Set();
257
+
258
+ while (targets.size < Math.min(m, i)) {
259
+ let r = Math.random() * totalDegree;
260
+ let node = 0;
261
+
262
+ for (const [n, d] of degrees) {
263
+ r -= d;
264
+ if (r <= 0) {
265
+ node = n;
266
+ break;
267
+ }
268
+ }
269
+
270
+ if (!targets.has(node)) {
271
+ targets.add(node);
272
+ edges.push([i, node]);
273
+ }
274
+ }
275
+ }
276
+
277
+ return {
278
+ nodes: () => nodes,
279
+ edges: () => edges
280
+ };
281
+ }
282
+
283
+ function gridGraph(rows, cols) {
284
+ const nodes = [];
285
+ const edges = [];
286
+
287
+ // Create nodes
288
+ for (let r = 0; r < rows; r++) {
289
+ for (let c = 0; c < cols; c++) {
290
+ nodes.push(r * cols + c);
291
+ }
292
+ }
293
+
294
+ // Create edges
295
+ for (let r = 0; r < rows; r++) {
296
+ for (let c = 0; c < cols; c++) {
297
+ const node = r * cols + c;
298
+
299
+ // Right edge
300
+ if (c < cols - 1) {
301
+ edges.push([node, node + 1]);
302
+ }
303
+
304
+ // Down edge
305
+ if (r < rows - 1) {
306
+ edges.push([node, node + cols]);
307
+ }
308
+ }
309
+ }
310
+
311
+ return {
312
+ nodes: () => nodes,
313
+ edges: () => edges
314
+ };
315
+ }
316
+
216
317
  function generateGraph(type, numNodes) {
217
318
  switch(type) {
218
319
  case 'scale-free':
@@ -263,150 +364,7 @@
263
364
  case 'random':
264
365
  default:
265
366
  return randomGraph(numNodes, 0.1, Date.now());
266
- for(let i = 1; i < Math.min(5, numNodes); i++) {
267
- edges.push([0, i]);
268
- }
269
-
270
- // Add nodes with preferential attachment
271
- for(let i = 5; i < numNodes; i++) {
272
- // Number of connections for new node
273
- const newEdges = Math.min(3, i);
274
-
275
- // Track edge count per node for preferential attachment
276
- const nodeDegrees = new Array(i).fill(0);
277
- for(const edge of edges) {
278
- nodeDegrees[edge[0]]++;
279
- nodeDegrees[edge[1]]++;
280
- }
281
-
282
- // Calculate total degree for probability
283
- const totalDegree = nodeDegrees.reduce((sum, deg) => sum + deg, 0);
284
-
285
- // Add edges based on preferential attachment
286
- const added = new Set();
287
- for(let j = 0; j < newEdges; j++) {
288
- let targetNode;
289
- do {
290
- // Choose node with probability proportional to its degree
291
- let rand = Math.random() * totalDegree;
292
- targetNode = 0;
293
- while(targetNode < i && rand > 0) {
294
- rand -= nodeDegrees[targetNode];
295
- if(rand > 0) targetNode++;
296
- }
297
- if(targetNode >= i) targetNode = Math.floor(Math.random() * i);
298
- } while(added.has(targetNode));
299
-
300
- edges.push([i, targetNode]);
301
- added.add(targetNode);
302
- }
303
- }
304
- break;
305
-
306
- case 'community':
307
- // Create community structure with 3-4 communities
308
- const numCommunities = Math.min(4, Math.ceil(numNodes / 10));
309
- const communitySizes = [];
310
- let remainingNodes = numNodes;
311
-
312
- for(let i = 0; i < numCommunities - 1; i++) {
313
- const size = Math.ceil(numNodes / numCommunities);
314
- communitySizes.push(size);
315
- remainingNodes -= size;
316
- }
317
- communitySizes.push(remainingNodes);
318
-
319
- let nodeIndex = 0;
320
- for(let c = 0; c < numCommunities; c++) {
321
- const communityStart = nodeIndex;
322
- const communityEnd = communityStart + communitySizes[c];
323
-
324
- // Connect nodes within community (dense)
325
- for(let i = communityStart; i < communityEnd; i++) {
326
- for(let j = i + 1; j < communityEnd; j++) {
327
- if(Math.random() < 0.7) { // High probability within community
328
- edges.push([i, j]);
329
- }
330
- }
331
- }
332
-
333
- // Connect to other communities (sparse)
334
- if(c > 0) {
335
- const prevCommunityStart = nodeIndex - communitySizes[c-1];
336
- const bridgeEdges = Math.max(1, Math.floor(communitySizes[c] * 0.1));
337
- for(let e = 0; e < bridgeEdges; e++) {
338
- const source = communityStart + Math.floor(Math.random() * communitySizes[c]);
339
- const target = prevCommunityStart + Math.floor(Math.random() * communitySizes[c-1]);
340
- edges.push([source, target]);
341
- }
342
- }
343
-
344
- nodeIndex = communityEnd;
345
- }
346
- break;
347
-
348
- case 'grid':
349
- // Create a grid-like structure
350
- const gridSize = Math.ceil(Math.sqrt(numNodes));
351
- for(let i = 0; i < numNodes; i++) {
352
- const row = Math.floor(i / gridSize);
353
- const col = i % gridSize;
354
-
355
- // Connect right
356
- if(col < gridSize - 1 && i + 1 < numNodes) {
357
- edges.push([i, i + 1]);
358
- }
359
-
360
- // Connect down
361
- if(row < gridSize - 1 && i + gridSize < numNodes) {
362
- edges.push([i, i + gridSize]);
363
- }
364
- }
365
- break;
366
-
367
- case 'random':
368
- default:
369
- // Random graph with controlled density
370
- const density = 0.1; // 10% of possible edges
371
- const possibleEdges = (numNodes * (numNodes - 1)) / 2;
372
- const targetEdges = Math.ceil(possibleEdges * density);
373
-
374
- // Ensure connected graph with spanning tree
375
- for(let i = 1; i < numNodes; i++) {
376
- const parent = Math.floor(Math.random() * i);
377
- edges.push([parent, i]);
378
- }
379
-
380
- // Add random edges up to target density
381
- let additionalEdges = targetEdges - (numNodes - 1);
382
- let attempts = 0;
383
-
384
- while(additionalEdges > 0 && attempts < 1000) {
385
- const a = Math.floor(Math.random() * numNodes);
386
- const b = Math.floor(Math.random() * numNodes);
387
-
388
- if(a !== b && !edges.some(e => (e[0] === a && e[1] === b) || (e[0] === b && e[1] === a))) {
389
- edges.push([a, b]);
390
- additionalEdges--;
391
- }
392
-
393
- attempts++;
394
- }
395
- break;
396
367
  }
397
-
398
- // Create node masses based on degree
399
- const masses = {};
400
- for(const edge of edges) {
401
- masses[edge[0]] = (masses[edge[0]] || 1) + 0.1;
402
- masses[edge[1]] = (masses[edge[1]] || 1) + 0.1;
403
- }
404
-
405
- return {
406
- nodes: () => nodes,
407
- edges: () => edges,
408
- masses: masses
409
- };
410
368
  }
411
369
 
412
370
  function visualizeGraph(graph, positions) {
@@ -514,9 +472,6 @@
514
472
 
515
473
  // Apply layout immediately
516
474
  applyLayout();
517
- });
518
-
519
- visualizeGraph(currentGraph, positions);
520
475
 
521
476
  } catch (error) {
522
477
  console.error('Error in graph generation:', error);
@@ -409,50 +409,78 @@ function groupByKCore(nodes, edges) {
409
409
  .map(([, nodeList]) => nodeList);
410
410
  }
411
411
  function groupByCommunity(nodes, edges, targetGroups) {
412
- // Simple community detection using modularity
412
+ // Fast community detection using label propagation
413
413
  const communities = new Map();
414
- nodes.forEach((node, i) => communities.set(node, i));
415
- // Build adjacency
416
414
  const adj = new Map();
417
- nodes.forEach(node => adj.set(node, new Set()));
415
+
416
+ // Initialize each node in its own community
417
+ nodes.forEach((node, i) => {
418
+ communities.set(node, i);
419
+ adj.set(node, new Set());
420
+ });
421
+
422
+ // Build adjacency
418
423
  edges.forEach(([u, v]) => {
419
424
  adj.get(u).add(v);
420
425
  adj.get(v).add(u);
421
426
  });
422
- // Iteratively merge communities
423
- let numCommunities = nodes.length;
424
- while (numCommunities > targetGroups) {
425
- let improved = false;
426
- for (const node of nodes) {
427
- const currentComm = communities.get(node);
428
- const neighborComms = new Set();
429
- adj.get(node).forEach(neighbor => {
430
- neighborComms.add(communities.get(neighbor));
427
+
428
+ // Label propagation algorithm
429
+ const maxIterations = 10; // Prevent infinite loops
430
+ for (let iter = 0; iter < maxIterations; iter++) {
431
+ let changed = false;
432
+
433
+ // Process nodes in random order
434
+ const shuffledNodes = nodes.slice().sort(() => Math.random() - 0.5);
435
+
436
+ for (const node of shuffledNodes) {
437
+ const neighbors = adj.get(node);
438
+ if (neighbors.size === 0) continue;
439
+
440
+ // Count community frequencies among neighbors
441
+ const commCounts = new Map();
442
+ neighbors.forEach(neighbor => {
443
+ const comm = communities.get(neighbor);
444
+ commCounts.set(comm, (commCounts.get(comm) || 0) + 1);
431
445
  });
432
- for (const targetComm of Array.from(neighborComms)) {
433
- if (targetComm !== currentComm) {
434
- // Simple decision: join most common neighbor community
435
- communities.set(node, targetComm);
436
- improved = true;
437
- break;
446
+
447
+ // Find most frequent community
448
+ let maxCount = 0;
449
+ let bestComm = communities.get(node);
450
+ commCounts.forEach((count, comm) => {
451
+ if (count > maxCount || (count === maxCount && comm < bestComm)) {
452
+ maxCount = count;
453
+ bestComm = comm;
438
454
  }
455
+ });
456
+
457
+ if (bestComm !== communities.get(node)) {
458
+ communities.set(node, bestComm);
459
+ changed = true;
439
460
  }
440
- if (improved)
441
- break;
442
461
  }
443
- if (!improved)
444
- break;
445
- // Count communities
446
- const currentNumCommunities = new Set(communities.values()).size;
447
- numCommunities = currentNumCommunities;
462
+
463
+ if (!changed) break;
448
464
  }
449
- // Group nodes by community
465
+
466
+ // Consolidate communities
450
467
  const groups = new Map();
451
468
  communities.forEach((comm, node) => {
452
- if (!groups.has(comm))
453
- groups.set(comm, []);
469
+ if (!groups.has(comm)) groups.set(comm, []);
454
470
  groups.get(comm).push(node);
455
471
  });
456
- return Array.from(groups.values());
472
+
473
+ let result = Array.from(groups.values());
474
+
475
+ // If we have too many groups, merge the smallest ones
476
+ while (result.length > targetGroups && result.length > 1) {
477
+ // Sort by size
478
+ result.sort((a, b) => a.length - b.length);
479
+ // Merge two smallest groups
480
+ const merged = result[0].concat(result[1]);
481
+ result = [merged, ...result.slice(2)];
482
+ }
483
+
484
+ return result;
457
485
  }
458
486
  //# sourceMappingURL=layout-helpers.js.map