@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.
Files changed (57) hide show
  1. package/.env.example +11 -0
  2. package/.github/workflows/ci.yml +89 -14
  3. package/.releaserc.json +22 -0
  4. package/CHANGELOG.md +31 -0
  5. package/CLAUDE.md +104 -0
  6. package/CONTRIBUTING.md +1 -0
  7. package/DEPLOYMENT.md +59 -0
  8. package/README.md +662 -93
  9. package/dist/layout-helpers.d.ts +123 -0
  10. package/dist/layout-helpers.js +458 -0
  11. package/dist/layout-helpers.js.map +1 -0
  12. package/dist/layout.d.ts +275 -0
  13. package/dist/layout.js +2304 -0
  14. package/dist/layout.js.map +1 -0
  15. package/dist/vitest.config.d.ts +2 -0
  16. package/dist/vitest.config.js +30 -0
  17. package/dist/vitest.config.js.map +1 -0
  18. package/examples/3d-force-directed.html +611 -0
  19. package/examples/3d-kamada-kawai.html +394 -0
  20. package/examples/3d-layout-comparison.html +448 -0
  21. package/examples/3d-spherical-layout.html +319 -0
  22. package/examples/arf-layout.html +13 -1
  23. package/examples/bfs-layout.html +49 -39
  24. package/examples/bipartite-layout.html +89 -69
  25. package/examples/circular-layout.html +26 -35
  26. package/examples/forceatlas2-layout.html +134 -28
  27. package/examples/index.html +75 -0
  28. package/examples/kamada-kawai-layout.html +13 -1
  29. package/examples/multipartite-layout.html +73 -88
  30. package/examples/planar-layout.html +13 -1
  31. package/examples/random-layout.html +13 -1
  32. package/examples/shell-layout.html +65 -34
  33. package/examples/spectral-layout.html +13 -1
  34. package/examples/spiral-layout.html +13 -1
  35. package/examples/spring-layout.html +25 -3
  36. package/layout-helpers.ts +560 -0
  37. package/layout.ts +316 -14
  38. package/package.json +20 -6
  39. package/test/arf-layout.test.ts +443 -0
  40. package/test/bfs-layout.test.ts +427 -0
  41. package/test/bipartite-layout.test.ts +344 -0
  42. package/test/circular-layout.test.ts +442 -0
  43. package/test/forceatlas2-layout.test.ts +405 -0
  44. package/test/fruchterman-reingold-layout.test.ts +477 -0
  45. package/test/graph-generators.test.ts +450 -0
  46. package/test/kamada-kawai-layout.test.ts +623 -0
  47. package/test/multipartite-layout.test.ts +404 -0
  48. package/test/planar-layout.test.ts +266 -0
  49. package/test/random-layout.test.ts +254 -0
  50. package/test/rescale-layout.test.ts +373 -0
  51. package/test/shell-layout.test.ts +347 -0
  52. package/test/spectral-layout.test.ts +378 -0
  53. package/test/spiral-layout.test.ts +338 -0
  54. package/test/spring-layout.test.ts +241 -0
  55. package/vite.config.js +36 -0
  56. package/vitest.config.ts +30 -0
  57. package/.releaserc +0 -3
@@ -1,6 +1,18 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
+ <!-- Eruda Mobile Console -->
5
+ <script src="https://cdn.jsdelivr.net/npm/eruda@3/eruda.min.js"></script>
6
+ <script>
7
+ // Initialize Eruda console for mobile debugging
8
+ if (typeof eruda !== "undefined") {
9
+ eruda.init();
10
+ // Auto-show on mobile devices
11
+ if (/mobile|android|ios|iphone|ipad|ipod/i.test(navigator.userAgent.toLowerCase())) {
12
+ eruda.show();
13
+ }
14
+ }
15
+ </script>
4
16
  <meta charset="UTF-8">
5
17
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
18
  <title>Bipartite Layout Test</title>
@@ -102,15 +114,15 @@
102
114
  <h3>Graph Settings</h3>
103
115
 
104
116
  <div class="control-group">
105
- <label for="num-nodes">Total number of nodes:</label>
106
- <input type="range" id="num-nodes" min="4" max="20" value="12">
107
- <span id="num-nodes-value">12</span>
117
+ <label for="set-a-size">Set A size:</label>
118
+ <input type="range" id="set-a-size" min="2" max="15" value="5">
119
+ <span id="set-a-size-value">5</span>
108
120
  </div>
109
121
 
110
122
  <div class="control-group">
111
- <label for="partition-ratio">Partition ratio (A:B):</label>
112
- <input type="range" id="partition-ratio" min="0.2" max="0.8" step="0.1" value="0.5">
113
- <span id="partition-ratio-value">50%-50%</span>
123
+ <label for="set-b-size">Set B size:</label>
124
+ <input type="range" id="set-b-size" min="2" max="15" value="7">
125
+ <span id="set-b-size-value">7</span>
114
126
  </div>
115
127
 
116
128
  <div class="control-group">
@@ -119,7 +131,8 @@
119
131
  <span id="edge-density-value">0.3</span>
120
132
  </div>
121
133
 
122
- <button onclick="newGraph()">New Graph</button>
134
+ <button onclick="newGraph()">Generate Bipartite Graph</button>
135
+ <button onclick="generateMixedGraph()">Generate Random Graph</button>
123
136
  <div id="partition-info" class="partition-info"></div>
124
137
  </div>
125
138
 
@@ -149,44 +162,15 @@
149
162
  </div>
150
163
 
151
164
  <script type="module">
152
- import { bipartiteLayout } from '../dist/layout.js';
165
+ import {
166
+ bipartiteLayout,
167
+ bipartiteGraph,
168
+ detectBipartite,
169
+ randomGraph
170
+ } from "./layout.js";
153
171
 
154
172
  let currentGraph = null;
155
-
156
- function generateBipartiteGraph(totalNodes, partitionRatio, edgeDensity) {
157
- const setASize = Math.round(totalNodes * partitionRatio);
158
- const setBSize = totalNodes - setASize;
159
-
160
- const setA = Array.from({length: setASize}, (_, i) => `A${i}`);
161
- const setB = Array.from({length: setBSize}, (_, i) => `B${i}`);
162
- const allNodes = [...setA, ...setB];
163
-
164
- const edges = [];
165
- const maxEdges = setASize * setBSize;
166
- const targetEdges = Math.floor(maxEdges * edgeDensity);
167
-
168
- // Generate random bipartite edges (only between setA and setB)
169
- for (let i = 0; i < targetEdges; i++) {
170
- const nodeA = setA[Math.floor(Math.random() * setA.length)];
171
- const nodeB = setB[Math.floor(Math.random() * setB.length)];
172
-
173
- // Check if edge already exists
174
- const edgeExists = edges.some(([s, t]) =>
175
- (s === nodeA && t === nodeB) || (s === nodeB && t === nodeA)
176
- );
177
-
178
- if (!edgeExists) {
179
- edges.push([nodeA, nodeB]);
180
- }
181
- }
182
-
183
- return {
184
- nodes: () => allNodes,
185
- edges: () => edges,
186
- setA,
187
- setB
188
- };
189
- }
173
+ let detectedBipartite = null;
190
174
 
191
175
  function visualizeGraph(graph, positions) {
192
176
  const svg = document.getElementById('graph-svg');
@@ -237,8 +221,9 @@
237
221
  const x = margin + (pos[0] - minX) / rangeX * scaleX;
238
222
  const y = margin + (pos[1] - minY) / rangeY * scaleY;
239
223
 
240
- // Determine color based on set
241
- const isSetA = graph.setA.includes(node);
224
+ // Determine color based on set (use detected sets if available)
225
+ const setA = detectedBipartite ? detectedBipartite.setA : graph.setA;
226
+ const isSetA = setA.includes(node);
242
227
  const color = isSetA ? '#E91E63' : '#2196F3';
243
228
 
244
229
  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
@@ -299,30 +284,28 @@
299
284
  }
300
285
 
301
286
  window.newGraph = function() {
302
- const totalNodes = parseInt(document.getElementById('num-nodes').value);
303
- const partitionRatio = parseFloat(document.getElementById('partition-ratio').value);
287
+ const setASize = parseInt(document.getElementById('set-a-size').value);
288
+ const setBSize = parseInt(document.getElementById('set-b-size').value);
304
289
  const edgeDensity = parseFloat(document.getElementById('edge-density').value);
305
290
 
306
- currentGraph = generateBipartiteGraph(totalNodes, partitionRatio, edgeDensity);
291
+ // Generate bipartite graph using our generator
292
+ currentGraph = bipartiteGraph(setASize, setBSize, edgeDensity, Date.now());
307
293
 
308
- // Just visualize with random positions initially
309
- const positions = {};
310
- currentGraph.nodes().forEach(node => {
311
- positions[node] = [
312
- (Math.random() - 0.5) * 1.5,
313
- (Math.random() - 0.5) * 1.5
314
- ];
315
- });
294
+ // Auto-detect bipartite structure
295
+ detectedBipartite = detectBipartite(currentGraph);
316
296
 
317
- visualizeGraph(currentGraph, positions);
297
+ // Apply layout immediately
298
+ applyLayout();
318
299
 
319
300
  // Update partition info
320
301
  const infoDiv = document.getElementById('partition-info');
302
+ const totalNodes = setASize + setBSize;
321
303
  infoDiv.innerHTML = `
322
- <strong>Partition:</strong><br>
323
- Set A: ${currentGraph.setA.length} nodes (${((currentGraph.setA.length / totalNodes) * 100).toFixed(1)}%)<br>
324
- Set B: ${currentGraph.setB.length} nodes (${((currentGraph.setB.length / totalNodes) * 100).toFixed(1)}%)<br>
325
- Edges: ${currentGraph.edges().length}
304
+ <strong>Generated Bipartite Graph:</strong><br>
305
+ Set A: ${currentGraph.setA.length} nodes (${((setASize / totalNodes) * 100).toFixed(1)}%)<br>
306
+ Set B: ${currentGraph.setB.length} nodes (${((setBSize / totalNodes) * 100).toFixed(1)}%)<br>
307
+ Edges: ${currentGraph.edges().length}<br>
308
+ <strong>Auto-detected:</strong> ${detectedBipartite ? 'Yes ✓' : 'No ✗'}
326
309
  `;
327
310
  };
328
311
 
@@ -332,29 +315,66 @@
332
315
  const alignment = document.getElementById('alignment').value;
333
316
  const aspectRatio = parseFloat(document.getElementById('aspect-ratio').value);
334
317
 
335
- const positions = bipartiteLayout(currentGraph, currentGraph.setA, alignment, 1, [0, 0], aspectRatio);
318
+ // Use detected sets if available, otherwise use generated sets
319
+ const setA = detectedBipartite ? detectedBipartite.setA : currentGraph.setA;
320
+ const positions = bipartiteLayout(currentGraph, setA, alignment, 1, [0, 0], aspectRatio);
336
321
 
337
322
  visualizeGraph(currentGraph, positions);
338
323
  };
339
324
 
340
325
  function setupControls() {
341
- const controls = ['num-nodes', 'partition-ratio', 'aspect-ratio', 'edge-density'];
326
+ const controls = ['set-a-size', 'set-b-size', 'aspect-ratio', 'edge-density'];
342
327
  controls.forEach(id => {
343
328
  const slider = document.getElementById(id);
344
329
  const valueSpan = document.getElementById(id + '-value');
345
330
  slider.oninput = function() {
346
- if (id === 'partition-ratio') {
347
- const ratio = parseFloat(this.value);
348
- valueSpan.textContent = `${(ratio * 100).toFixed(0)}%-${((1 - ratio) * 100).toFixed(0)}%`;
349
- } else {
350
- valueSpan.textContent = this.value;
351
- }
331
+ valueSpan.textContent = this.value;
352
332
  };
353
333
  });
354
334
 
355
335
  document.getElementById('alignment').onchange = null;
356
336
  }
357
337
 
338
+ window.generateMixedGraph = function() {
339
+ const totalNodes = parseInt(document.getElementById('set-a-size').value) +
340
+ parseInt(document.getElementById('set-b-size').value);
341
+ const edgeDensity = parseFloat(document.getElementById('edge-density').value);
342
+
343
+ // Generate random graph that might or might not be bipartite
344
+ currentGraph = randomGraph(totalNodes, edgeDensity * 0.3, Date.now());
345
+
346
+ // Try to detect bipartite structure
347
+ detectedBipartite = detectBipartite(currentGraph);
348
+
349
+ if (detectedBipartite) {
350
+ // If bipartite, use bipartite layout
351
+ applyLayout();
352
+
353
+ const infoDiv = document.getElementById('partition-info');
354
+ infoDiv.innerHTML = `
355
+ <strong>Random Graph (Bipartite Detected!):</strong><br>
356
+ Set A: ${detectedBipartite.setA.length} nodes<br>
357
+ Set B: ${detectedBipartite.setB.length} nodes<br>
358
+ Edges: ${currentGraph.edges().length}<br>
359
+ <strong>Auto-detected:</strong> Yes ✓
360
+ `;
361
+ } else {
362
+ // Not bipartite - show with spring layout instead
363
+ const { springLayout } = await import('../dist/layout.js');
364
+ const positions = springLayout(currentGraph);
365
+ visualizeGraph(currentGraph, positions);
366
+
367
+ const infoDiv = document.getElementById('partition-info');
368
+ infoDiv.innerHTML = `
369
+ <strong>Random Graph (Not Bipartite):</strong><br>
370
+ Nodes: ${currentGraph.nodes().length}<br>
371
+ Edges: ${currentGraph.edges().length}<br>
372
+ <strong>Auto-detected:</strong> No ✗<br>
373
+ <em>Using spring layout instead</em>
374
+ `;
375
+ }
376
+ };
377
+
358
378
  setupControls();
359
379
  newGraph();
360
380
  </script>
@@ -1,6 +1,18 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
+ <!-- Eruda Mobile Console -->
5
+ <script src="https://cdn.jsdelivr.net/npm/eruda@3/eruda.min.js"></script>
6
+ <script>
7
+ // Initialize Eruda console for mobile debugging
8
+ if (typeof eruda !== "undefined") {
9
+ eruda.init();
10
+ // Auto-show on mobile devices
11
+ if (/mobile|android|ios|iphone|ipad|ipod/i.test(navigator.userAgent.toLowerCase())) {
12
+ eruda.show();
13
+ }
14
+ }
15
+ </script>
4
16
  <meta charset="UTF-8">
5
17
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
18
  <title>Circular Layout Test</title>
@@ -136,51 +148,30 @@
136
148
  </div>
137
149
 
138
150
  <script type="module">
139
- import { circularLayout } from '../dist/layout.js';
151
+ import {
152
+ circularLayout,
153
+ completeGraph,
154
+ cycleGraph,
155
+ wheelGraph,
156
+ randomGraph
157
+ } from "./layout.js";
140
158
 
141
159
  let currentGraph = null;
142
160
 
143
161
  function generateGraph(type, numNodes) {
144
- const nodes = Array.from({length: numNodes}, (_, i) => i);
145
- const edges = [];
146
-
147
162
  switch(type) {
148
163
  case 'complete':
149
- for(let i = 0; i < numNodes; i++) {
150
- for(let j = i + 1; j < numNodes; j++) {
151
- edges.push([i, j]);
152
- }
153
- }
154
- break;
164
+ return completeGraph(numNodes);
155
165
  case 'cycle':
156
- for(let i = 0; i < numNodes; i++) {
157
- edges.push([i, (i + 1) % numNodes]);
158
- }
159
- break;
166
+ return cycleGraph(numNodes);
160
167
  case 'wheel':
161
- // Center connected to all
162
- for(let i = 1; i < numNodes; i++) {
163
- edges.push([0, i]);
164
- }
165
- // Outer cycle
166
- for(let i = 1; i < numNodes; i++) {
167
- edges.push([i, i === numNodes - 1 ? 1 : i + 1]);
168
- }
169
- break;
168
+ return wheelGraph(numNodes);
170
169
  case 'random':
171
170
  default:
172
- const numEdges = Math.floor(numNodes * 1.2);
173
- for(let i = 0; i < numEdges; i++) {
174
- const a = Math.floor(Math.random() * numNodes);
175
- const b = Math.floor(Math.random() * numNodes);
176
- if(a !== b && !edges.some(e => (e[0] === a && e[1] === b) || (e[0] === b && e[1] === a))) {
177
- edges.push([a, b]);
178
- }
179
- }
180
- break;
171
+ // Random graph with ~1.2 edges per node
172
+ const edgeProbability = Math.min(2.4 / (numNodes - 1), 1);
173
+ return randomGraph(numNodes, edgeProbability, Date.now());
181
174
  }
182
-
183
- return { nodes: () => nodes, edges: () => edges };
184
175
  }
185
176
 
186
177
  function visualizeGraph(graph, positions) {
@@ -298,4 +289,4 @@
298
289
  newGraph();
299
290
  </script>
300
291
  </body>
301
- </html>
292
+ </html>
@@ -1,6 +1,18 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
+ <!-- Eruda Mobile Console -->
5
+ <script src="https://cdn.jsdelivr.net/npm/eruda@3/eruda.min.js"></script>
6
+ <script>
7
+ // Initialize Eruda console for mobile debugging
8
+ if (typeof eruda !== "undefined") {
9
+ eruda.init();
10
+ // Auto-show on mobile devices
11
+ if (/mobile|android|ios|iphone|ipad|ipod/i.test(navigator.userAgent.toLowerCase())) {
12
+ eruda.show();
13
+ }
14
+ }
15
+ </script>
4
16
  <meta charset="UTF-8">
5
17
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
18
  <title>ForceAtlas2 Layout Test</title>
@@ -153,12 +165,17 @@
153
165
  </div>
154
166
 
155
167
  <div class="control-group">
168
+ <label for="auto-configure">Auto-configure parameters:</label>
169
+ <input type="checkbox" id="auto-configure" checked>
170
+ </div>
171
+
172
+ <div class="control-group" id="scaling-group">
156
173
  <label for="scaling-ratio">Scaling Ratio:</label>
157
174
  <input type="range" id="scaling-ratio" min="0.5" max="10" step="0.5" value="2">
158
175
  <span id="scaling-ratio-value">2.0</span>
159
176
  </div>
160
177
 
161
- <div class="control-group">
178
+ <div class="control-group" id="gravity-group">
162
179
  <label for="gravity">Gravity:</label>
163
180
  <input type="range" id="gravity" min="0" max="5" step="0.1" value="1">
164
181
  <span id="gravity-value">1.0</span>
@@ -185,19 +202,67 @@
185
202
  </div>
186
203
 
187
204
  <script type="module">
188
- import { forceatlas2Layout } from '../dist/layout.js';
205
+ import {
206
+ forceatlas2Layout,
207
+ randomGraph,
208
+ scaleFreeGraph,
209
+ gridGraph,
210
+ autoConfigureForce
211
+ } from "./layout.js";
189
212
 
190
213
  let currentGraph = null;
191
- let currentSeed = Math.floor(Math.random() * 1000);
214
+ let currentConfig = null;
192
215
 
193
216
  function generateGraph(type, numNodes) {
194
- const nodes = Array.from({length: numNodes}, (_, i) => i);
195
- const edges = [];
196
-
197
217
  switch(type) {
198
218
  case 'scale-free':
199
- // Barabási–Albert model (preferential attachment)
200
- // Start with a small connected graph
219
+ return scaleFreeGraph(numNodes, 2, Date.now());
220
+
221
+ case 'community':
222
+ // Use multiple smaller scale-free graphs
223
+ const numCommunities = Math.ceil(numNodes / 10);
224
+ const nodesPerCommunity = Math.floor(numNodes / numCommunities);
225
+ const nodes = [];
226
+ const edges = [];
227
+
228
+ // Create communities
229
+ for (let c = 0; c < numCommunities; c++) {
230
+ const start = c * nodesPerCommunity;
231
+ const end = Math.min(start + nodesPerCommunity, numNodes);
232
+
233
+ // Dense connections within community
234
+ for (let i = start; i < end; i++) {
235
+ nodes.push(i);
236
+ for (let j = i + 1; j < end; j++) {
237
+ if (Math.random() < 0.6) {
238
+ edges.push([i, j]);
239
+ }
240
+ }
241
+ }
242
+
243
+ // Sparse connections between communities
244
+ if (c > 0) {
245
+ const prevStart = (c - 1) * nodesPerCommunity;
246
+ const prevEnd = Math.min(prevStart + nodesPerCommunity, numNodes);
247
+ for (let i = start; i < end && i < start + 2; i++) {
248
+ for (let j = prevStart; j < prevEnd && j < prevStart + 2; j++) {
249
+ if (Math.random() < 0.1) {
250
+ edges.push([i, j]);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ return { nodes: () => nodes, edges: () => edges };
258
+
259
+ case 'grid':
260
+ const size = Math.ceil(Math.sqrt(numNodes));
261
+ return gridGraph(size, size);
262
+
263
+ case 'random':
264
+ default:
265
+ return randomGraph(numNodes, 0.1, Date.now());
201
266
  for(let i = 1; i < Math.min(5, numNodes); i++) {
202
267
  edges.push([0, i]);
203
268
  }
@@ -394,9 +459,10 @@
394
459
  const x = margin + (pos[0] - minX) / rangeX * scaleX;
395
460
  const y = margin + (pos[1] - minY) / rangeY * scaleY;
396
461
 
397
- // Node size based on degree/mass
398
- const mass = graph.masses[node] || 1;
399
- const radius = 5 + Math.sqrt(mass) * 1.5;
462
+ // Node size based on degree
463
+ const edges = graph.edges();
464
+ const degree = edges.filter(([u, v]) => u === node || v === node).length;
465
+ const radius = 4 + Math.sqrt(degree) * 1.5;
400
466
 
401
467
  // Color based on position/edge distribution
402
468
  const hue = (node * 360 / nodes.length) % 360;
@@ -430,20 +496,24 @@
430
496
  const graphType = document.getElementById('graph-type').value;
431
497
 
432
498
  try {
433
- // Generate new graph and re-seed
499
+ // Generate new graph
434
500
  currentGraph = generateGraph(graphType, numNodes);
435
- currentSeed = Math.floor(Math.random() * 1000);
436
501
 
437
- // Just visualize without layout initially
438
- const positions = {};
439
- currentGraph.nodes().forEach(node => {
440
- // Random positions in a circle
441
- const angle = Math.random() * Math.PI * 2;
442
- const radius = Math.random() * 0.5;
443
- positions[node] = [
444
- Math.cos(angle) * radius,
445
- Math.sin(angle) * radius
446
- ];
502
+ // Auto-configure parameters
503
+ currentConfig = autoConfigureForce(currentGraph);
504
+
505
+ // Update controls with recommended values
506
+ if (document.getElementById('auto-configure').checked) {
507
+ document.getElementById('iterations').value = currentConfig.iterations;
508
+ document.getElementById('iterations-value').textContent = currentConfig.iterations;
509
+ document.getElementById('scaling-ratio').value = currentConfig.scalingRatio;
510
+ document.getElementById('scaling-ratio-value').textContent = currentConfig.scalingRatio.toFixed(1);
511
+ document.getElementById('gravity').value = currentConfig.gravity;
512
+ document.getElementById('gravity-value').textContent = currentConfig.gravity.toFixed(1);
513
+ }
514
+
515
+ // Apply layout immediately
516
+ applyLayout();
447
517
  });
448
518
 
449
519
  visualizeGraph(currentGraph, positions);
@@ -470,9 +540,27 @@
470
540
  };
471
541
 
472
542
  async function runForceAtlas2() {
473
- const iterations = parseInt(document.getElementById('iterations').value);
474
- const scalingRatio = parseFloat(document.getElementById('scaling-ratio').value);
475
- const gravity = parseFloat(document.getElementById('gravity').value);
543
+ const autoConfig = document.getElementById('auto-configure').checked;
544
+
545
+ let iterations, scalingRatio, gravity;
546
+ if (autoConfig && currentConfig) {
547
+ iterations = currentConfig.iterations;
548
+ scalingRatio = currentConfig.scalingRatio;
549
+ gravity = currentConfig.gravity;
550
+
551
+ // Update sliders to show auto values
552
+ document.getElementById('iterations').value = iterations;
553
+ document.getElementById('iterations-value').textContent = iterations;
554
+ document.getElementById('scaling-ratio').value = scalingRatio;
555
+ document.getElementById('scaling-ratio-value').textContent = scalingRatio.toFixed(1);
556
+ document.getElementById('gravity').value = gravity;
557
+ document.getElementById('gravity-value').textContent = gravity.toFixed(1);
558
+ } else {
559
+ iterations = parseInt(document.getElementById('iterations').value);
560
+ scalingRatio = parseFloat(document.getElementById('scaling-ratio').value);
561
+ gravity = parseFloat(document.getElementById('gravity').value);
562
+ }
563
+
476
564
  const strongGravity = document.getElementById('strong-gravity').checked;
477
565
  const dissuadeHubs = document.getElementById('dissuade-hubs').checked;
478
566
  const linlog = document.getElementById('linlog').checked;
@@ -489,12 +577,12 @@
489
577
  gravity,
490
578
  false, // distributedAction
491
579
  strongGravity,
492
- currentGraph.masses, // nodeMass
580
+ null, // nodeMass
493
581
  null, // nodeSize
494
582
  null, // weight
495
583
  dissuadeHubs,
496
584
  linlog,
497
- currentSeed
585
+ Date.now()
498
586
  );
499
587
 
500
588
  visualizeGraph(currentGraph, positions);
@@ -510,6 +598,24 @@
510
598
  };
511
599
  });
512
600
 
601
+ // Auto-config checkbox handler
602
+ document.getElementById('auto-configure').onchange = function() {
603
+ const groups = ['scaling-group', 'gravity-group'];
604
+ groups.forEach(groupId => {
605
+ const group = document.getElementById(groupId);
606
+ group.style.opacity = this.checked ? '0.5' : '1';
607
+ });
608
+
609
+ if (this.checked && currentConfig) {
610
+ document.getElementById('iterations').value = currentConfig.iterations;
611
+ document.getElementById('iterations-value').textContent = currentConfig.iterations;
612
+ document.getElementById('scaling-ratio').value = currentConfig.scalingRatio;
613
+ document.getElementById('scaling-ratio-value').textContent = currentConfig.scalingRatio.toFixed(1);
614
+ document.getElementById('gravity').value = currentConfig.gravity;
615
+ document.getElementById('gravity-value').textContent = currentConfig.gravity.toFixed(1);
616
+ }
617
+ };
618
+
513
619
  // Remove auto-update on change for these controls
514
620
  document.getElementById('graph-type').onchange = null;
515
621
 
@@ -1,6 +1,18 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
+ <!-- Eruda Mobile Console -->
5
+ <script src="https://cdn.jsdelivr.net/npm/eruda@3/eruda.min.js"></script>
6
+ <script>
7
+ // Initialize Eruda console for mobile debugging
8
+ if (typeof eruda !== "undefined") {
9
+ eruda.init();
10
+ // Auto-show on mobile devices
11
+ if (/mobile|android|ios|iphone|ipad|ipod/i.test(navigator.userAgent.toLowerCase())) {
12
+ eruda.show();
13
+ }
14
+ }
15
+ </script>
4
16
  <meta charset="UTF-8">
5
17
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
18
  <title>NetworkX Layout Tests</title>
@@ -71,6 +83,38 @@
71
83
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
72
84
  padding: 20px;
73
85
  }
86
+ .section-header {
87
+ text-align: center;
88
+ margin: 40px 0 20px 0;
89
+ color: #333;
90
+ }
91
+ .section-header h2 {
92
+ color: #2c5aa0;
93
+ margin-bottom: 10px;
94
+ }
95
+ .section-header p {
96
+ color: #666;
97
+ margin: 0;
98
+ }
99
+ .layout-card.threed {
100
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
101
+ color: white;
102
+ }
103
+ .layout-card.threed .layout-title {
104
+ color: #fff;
105
+ }
106
+ .layout-card.threed .layout-description {
107
+ color: #f0f0f0;
108
+ }
109
+ .layout-card.threed .test-button {
110
+ background: rgba(255, 255, 255, 0.2);
111
+ color: white;
112
+ border: 2px solid rgba(255, 255, 255, 0.3);
113
+ }
114
+ .layout-card.threed .test-button:hover {
115
+ background: rgba(255, 255, 255, 0.3);
116
+ border-color: rgba(255, 255, 255, 0.5);
117
+ }
74
118
  </style>
75
119
  </head>
76
120
  <body>
@@ -79,6 +123,11 @@
79
123
  <p>Interactive tests for graph layout algorithms in JavaScript</p>
80
124
  </div>
81
125
 
126
+ <div class="section-header">
127
+ <h2>2D Layout Algorithms</h2>
128
+ <p>Classic graph layout algorithms for 2D visualization</p>
129
+ </div>
130
+
82
131
  <div class="layout-grid">
83
132
  <div class="layout-card">
84
133
  <div class="layout-title">Random Layout</div>
@@ -159,6 +208,32 @@
159
208
  </div>
160
209
  </div>
161
210
 
211
+ <div class="section-header">
212
+ <h2>3D Layout Algorithms</h2>
213
+ <p>Interactive 3D visualizations with Three.js - drag to rotate, scroll to zoom</p>
214
+ </div>
215
+
216
+ <div class="layout-grid">
217
+ <div class="layout-card threed">
218
+ <div class="layout-title">3D Force-Directed Layouts</div>
219
+ <div class="layout-description">Compare spring, ForceAtlas2, and ARF algorithms in immersive 3D space with interactive controls</div>
220
+ <a href="3d-force-directed.html" class="test-button">Explore 3D</a>
221
+ </div>
222
+
223
+ <div class="layout-card threed">
224
+ <div class="layout-title">3D Kamada-Kawai Layout</div>
225
+ <div class="layout-description">Stress-minimization algorithm in 3D with real-time stress calculation and graph generation</div>
226
+ <a href="3d-kamada-kawai.html" class="test-button">Explore 3D</a>
227
+ </div>
228
+
229
+ <div class="layout-card threed">
230
+ <div class="layout-title">3D Spherical Layout</div>
231
+ <div class="layout-description">Nodes positioned on a 3D sphere surface with auto-rotation and customizable graph types</div>
232
+ <a href="3d-spherical-layout.html" class="test-button">Explore 3D</a>
233
+ </div>
234
+
235
+ </div>
236
+
162
237
  <div class="footer">
163
238
  <p>Ported from NetworkX Python library - Interactive tests for JavaScript</p>
164
239
  </div>