@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
|
@@ -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="
|
|
106
|
-
<input type="range" id="
|
|
107
|
-
<span id="
|
|
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="
|
|
112
|
-
<input type="range" id="
|
|
113
|
-
<span id="
|
|
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()">
|
|
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 {
|
|
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
|
|
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
|
|
303
|
-
const
|
|
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
|
-
|
|
291
|
+
// Generate bipartite graph using our generator
|
|
292
|
+
currentGraph = bipartiteGraph(setASize, setBSize, edgeDensity, Date.now());
|
|
307
293
|
|
|
308
|
-
//
|
|
309
|
-
|
|
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
|
-
|
|
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>
|
|
323
|
-
Set A: ${currentGraph.setA.length} nodes (${((
|
|
324
|
-
Set B: ${currentGraph.setB.length} nodes (${((
|
|
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
|
-
|
|
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 = ['
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
edges.push([i, (i + 1) % numNodes]);
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
166
|
+
return cycleGraph(numNodes);
|
|
160
167
|
case 'wheel':
|
|
161
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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
|
|
398
|
-
const
|
|
399
|
-
const
|
|
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
|
|
499
|
+
// Generate new graph
|
|
434
500
|
currentGraph = generateGraph(graphType, numNodes);
|
|
435
|
-
currentSeed = Math.floor(Math.random() * 1000);
|
|
436
501
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
580
|
+
null, // nodeMass
|
|
493
581
|
null, // nodeSize
|
|
494
582
|
null, // weight
|
|
495
583
|
dissuadeHubs,
|
|
496
584
|
linlog,
|
|
497
|
-
|
|
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
|
|
package/examples/index.html
CHANGED
|
@@ -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>
|