@graphty/layout 1.1.1 → 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.
- package/.env.example +11 -0
- package/.github/workflows/ci.yml +3 -7
- package/CHANGELOG.md +14 -0
- package/DEPLOYMENT.md +59 -0
- package/README.md +54 -1
- package/dist/vitest.config.js +2 -2
- package/dist/vitest.config.js.map +1 -1
- package/examples/3d-force-directed.html +611 -0
- package/examples/3d-kamada-kawai.html +379 -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 +38 -32
- package/examples/bipartite-layout.html +16 -4
- package/examples/circular-layout.html +14 -2
- package/examples/forceatlas2-layout.html +118 -151
- package/examples/index.html +75 -0
- package/examples/kamada-kawai-layout.html +13 -1
- package/{dist → examples}/layout-helpers.js +58 -29
- package/examples/multipartite-layout.html +44 -54
- package/examples/planar-layout.html +13 -1
- package/examples/random-layout.html +13 -1
- package/examples/shell-layout.html +16 -2
- package/examples/spectral-layout.html +13 -1
- package/examples/spiral-layout.html +13 -1
- package/examples/spring-layout.html +15 -13
- package/package.json +6 -3
- package/src/algorithms/index.ts +6 -0
- package/src/algorithms/optimization/index.ts +12 -0
- package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
- package/src/algorithms/optimization/lbfgs.ts +68 -0
- package/src/algorithms/optimization/line-search.ts +50 -0
- package/src/algorithms/optimization/types.ts +8 -0
- package/src/algorithms/planarity/check.ts +38 -0
- package/src/algorithms/planarity/embedding.ts +216 -0
- package/src/algorithms/planarity/index.ts +12 -0
- package/src/algorithms/planarity/lr-test.ts +70 -0
- package/src/algorithms/planarity/special-graphs.ts +126 -0
- package/src/generators/basic.ts +93 -0
- package/src/generators/bipartite.ts +45 -0
- package/src/generators/grid.ts +42 -0
- package/src/generators/index.ts +15 -0
- package/src/generators/random.ts +39 -0
- package/src/generators/scale-free.ts +72 -0
- package/src/index.ts +18 -0
- package/src/layouts/basic/index.ts +5 -0
- package/src/layouts/basic/random.ts +32 -0
- package/src/layouts/force-directed/arf.ts +130 -0
- package/src/layouts/force-directed/forceatlas2.ts +407 -0
- package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
- package/src/layouts/force-directed/index.ts +9 -0
- package/src/layouts/force-directed/kamada-kawai.ts +112 -0
- package/src/layouts/force-directed/spring.ts +35 -0
- package/src/layouts/geometric/circular.ts +77 -0
- package/src/layouts/geometric/index.ts +7 -0
- package/src/layouts/geometric/shell.ts +80 -0
- package/src/layouts/geometric/spiral.ts +93 -0
- package/src/layouts/hierarchical/bfs.ts +81 -0
- package/src/layouts/hierarchical/bipartite.ts +94 -0
- package/src/layouts/hierarchical/index.ts +7 -0
- package/src/layouts/hierarchical/multipartite.ts +88 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/specialized/index.ts +6 -0
- package/src/layouts/specialized/planar.ts +65 -0
- package/src/layouts/specialized/spectral.ts +128 -0
- package/src/types/embedding.ts +11 -0
- package/src/types/graph.ts +13 -0
- package/src/types/index.ts +7 -0
- package/src/types/layout.ts +9 -0
- package/src/utils/graph.ts +77 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/numpy.ts +108 -0
- package/src/utils/params.ts +26 -0
- package/src/utils/random.ts +53 -0
- package/src/utils/rescale.ts +137 -0
- package/test/arf-layout.test.ts +1 -1
- package/test/bfs-layout.test.ts +1 -1
- package/test/bipartite-layout.test.ts +1 -1
- package/test/circular-layout.test.ts +145 -3
- package/test/forceatlas2-layout.test.ts +1 -1
- package/test/fruchterman-reingold-layout.test.ts +1 -1
- package/test/graph-generators.test.ts +1 -1
- package/test/kamada-kawai-layout.test.ts +273 -1
- package/test/multipartite-layout.test.ts +1 -1
- package/test/planar-layout.test.ts +1 -1
- package/test/random-layout.test.ts +1 -1
- package/test/rescale-layout.test.ts +1 -1
- package/test/shell-layout.test.ts +1 -1
- package/test/spectral-layout.test.ts +1 -1
- package/test/spiral-layout.test.ts +1 -1
- package/test/spring-layout.test.ts +1 -1
- package/vite.config.js +36 -0
- package/vitest.config.ts +2 -2
- package/dist/layout-helpers.d.ts +0 -123
- package/dist/layout-helpers.js.map +0 -1
- package/dist/layout.d.ts +0 -275
- package/dist/layout.js +0 -2280
- package/dist/layout.js.map +0 -1
- package/layout-helpers.ts +0 -559
- package/layout.ts +0 -2867
|
@@ -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>
|
|
@@ -191,16 +203,117 @@
|
|
|
191
203
|
|
|
192
204
|
<script type="module">
|
|
193
205
|
import {
|
|
194
|
-
forceatlas2Layout
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
forceatlas2Layout
|
|
207
|
+
} from "./layout.js";
|
|
208
|
+
|
|
209
|
+
import {
|
|
198
210
|
autoConfigureForce
|
|
199
|
-
} from
|
|
211
|
+
} from "./layout-helpers.js";
|
|
200
212
|
|
|
201
213
|
let currentGraph = null;
|
|
202
214
|
let currentConfig = null;
|
|
203
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
|
+
|
|
204
317
|
function generateGraph(type, numNodes) {
|
|
205
318
|
switch(type) {
|
|
206
319
|
case 'scale-free':
|
|
@@ -251,150 +364,7 @@
|
|
|
251
364
|
case 'random':
|
|
252
365
|
default:
|
|
253
366
|
return randomGraph(numNodes, 0.1, Date.now());
|
|
254
|
-
for(let i = 1; i < Math.min(5, numNodes); i++) {
|
|
255
|
-
edges.push([0, i]);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Add nodes with preferential attachment
|
|
259
|
-
for(let i = 5; i < numNodes; i++) {
|
|
260
|
-
// Number of connections for new node
|
|
261
|
-
const newEdges = Math.min(3, i);
|
|
262
|
-
|
|
263
|
-
// Track edge count per node for preferential attachment
|
|
264
|
-
const nodeDegrees = new Array(i).fill(0);
|
|
265
|
-
for(const edge of edges) {
|
|
266
|
-
nodeDegrees[edge[0]]++;
|
|
267
|
-
nodeDegrees[edge[1]]++;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Calculate total degree for probability
|
|
271
|
-
const totalDegree = nodeDegrees.reduce((sum, deg) => sum + deg, 0);
|
|
272
|
-
|
|
273
|
-
// Add edges based on preferential attachment
|
|
274
|
-
const added = new Set();
|
|
275
|
-
for(let j = 0; j < newEdges; j++) {
|
|
276
|
-
let targetNode;
|
|
277
|
-
do {
|
|
278
|
-
// Choose node with probability proportional to its degree
|
|
279
|
-
let rand = Math.random() * totalDegree;
|
|
280
|
-
targetNode = 0;
|
|
281
|
-
while(targetNode < i && rand > 0) {
|
|
282
|
-
rand -= nodeDegrees[targetNode];
|
|
283
|
-
if(rand > 0) targetNode++;
|
|
284
|
-
}
|
|
285
|
-
if(targetNode >= i) targetNode = Math.floor(Math.random() * i);
|
|
286
|
-
} while(added.has(targetNode));
|
|
287
|
-
|
|
288
|
-
edges.push([i, targetNode]);
|
|
289
|
-
added.add(targetNode);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
break;
|
|
293
|
-
|
|
294
|
-
case 'community':
|
|
295
|
-
// Create community structure with 3-4 communities
|
|
296
|
-
const numCommunities = Math.min(4, Math.ceil(numNodes / 10));
|
|
297
|
-
const communitySizes = [];
|
|
298
|
-
let remainingNodes = numNodes;
|
|
299
|
-
|
|
300
|
-
for(let i = 0; i < numCommunities - 1; i++) {
|
|
301
|
-
const size = Math.ceil(numNodes / numCommunities);
|
|
302
|
-
communitySizes.push(size);
|
|
303
|
-
remainingNodes -= size;
|
|
304
|
-
}
|
|
305
|
-
communitySizes.push(remainingNodes);
|
|
306
|
-
|
|
307
|
-
let nodeIndex = 0;
|
|
308
|
-
for(let c = 0; c < numCommunities; c++) {
|
|
309
|
-
const communityStart = nodeIndex;
|
|
310
|
-
const communityEnd = communityStart + communitySizes[c];
|
|
311
|
-
|
|
312
|
-
// Connect nodes within community (dense)
|
|
313
|
-
for(let i = communityStart; i < communityEnd; i++) {
|
|
314
|
-
for(let j = i + 1; j < communityEnd; j++) {
|
|
315
|
-
if(Math.random() < 0.7) { // High probability within community
|
|
316
|
-
edges.push([i, j]);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Connect to other communities (sparse)
|
|
322
|
-
if(c > 0) {
|
|
323
|
-
const prevCommunityStart = nodeIndex - communitySizes[c-1];
|
|
324
|
-
const bridgeEdges = Math.max(1, Math.floor(communitySizes[c] * 0.1));
|
|
325
|
-
for(let e = 0; e < bridgeEdges; e++) {
|
|
326
|
-
const source = communityStart + Math.floor(Math.random() * communitySizes[c]);
|
|
327
|
-
const target = prevCommunityStart + Math.floor(Math.random() * communitySizes[c-1]);
|
|
328
|
-
edges.push([source, target]);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
nodeIndex = communityEnd;
|
|
333
|
-
}
|
|
334
|
-
break;
|
|
335
|
-
|
|
336
|
-
case 'grid':
|
|
337
|
-
// Create a grid-like structure
|
|
338
|
-
const gridSize = Math.ceil(Math.sqrt(numNodes));
|
|
339
|
-
for(let i = 0; i < numNodes; i++) {
|
|
340
|
-
const row = Math.floor(i / gridSize);
|
|
341
|
-
const col = i % gridSize;
|
|
342
|
-
|
|
343
|
-
// Connect right
|
|
344
|
-
if(col < gridSize - 1 && i + 1 < numNodes) {
|
|
345
|
-
edges.push([i, i + 1]);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Connect down
|
|
349
|
-
if(row < gridSize - 1 && i + gridSize < numNodes) {
|
|
350
|
-
edges.push([i, i + gridSize]);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
break;
|
|
354
|
-
|
|
355
|
-
case 'random':
|
|
356
|
-
default:
|
|
357
|
-
// Random graph with controlled density
|
|
358
|
-
const density = 0.1; // 10% of possible edges
|
|
359
|
-
const possibleEdges = (numNodes * (numNodes - 1)) / 2;
|
|
360
|
-
const targetEdges = Math.ceil(possibleEdges * density);
|
|
361
|
-
|
|
362
|
-
// Ensure connected graph with spanning tree
|
|
363
|
-
for(let i = 1; i < numNodes; i++) {
|
|
364
|
-
const parent = Math.floor(Math.random() * i);
|
|
365
|
-
edges.push([parent, i]);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Add random edges up to target density
|
|
369
|
-
let additionalEdges = targetEdges - (numNodes - 1);
|
|
370
|
-
let attempts = 0;
|
|
371
|
-
|
|
372
|
-
while(additionalEdges > 0 && attempts < 1000) {
|
|
373
|
-
const a = Math.floor(Math.random() * numNodes);
|
|
374
|
-
const b = Math.floor(Math.random() * numNodes);
|
|
375
|
-
|
|
376
|
-
if(a !== b && !edges.some(e => (e[0] === a && e[1] === b) || (e[0] === b && e[1] === a))) {
|
|
377
|
-
edges.push([a, b]);
|
|
378
|
-
additionalEdges--;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
attempts++;
|
|
382
|
-
}
|
|
383
|
-
break;
|
|
384
367
|
}
|
|
385
|
-
|
|
386
|
-
// Create node masses based on degree
|
|
387
|
-
const masses = {};
|
|
388
|
-
for(const edge of edges) {
|
|
389
|
-
masses[edge[0]] = (masses[edge[0]] || 1) + 0.1;
|
|
390
|
-
masses[edge[1]] = (masses[edge[1]] || 1) + 0.1;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
nodes: () => nodes,
|
|
395
|
-
edges: () => edges,
|
|
396
|
-
masses: masses
|
|
397
|
-
};
|
|
398
368
|
}
|
|
399
369
|
|
|
400
370
|
function visualizeGraph(graph, positions) {
|
|
@@ -502,9 +472,6 @@
|
|
|
502
472
|
|
|
503
473
|
// Apply layout immediately
|
|
504
474
|
applyLayout();
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
visualizeGraph(currentGraph, positions);
|
|
508
475
|
|
|
509
476
|
} catch (error) {
|
|
510
477
|
console.error('Error in graph generation:', error);
|
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>
|
|
@@ -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>Kamada-Kawai Layout Test</title>
|
|
@@ -157,7 +169,7 @@
|
|
|
157
169
|
</div>
|
|
158
170
|
|
|
159
171
|
<script type="module">
|
|
160
|
-
import { kamadaKawaiLayout } from
|
|
172
|
+
import { kamadaKawaiLayout } from "./layout.js";
|
|
161
173
|
|
|
162
174
|
let currentGraph = null;
|
|
163
175
|
let distances = null;
|
|
@@ -409,49 +409,78 @@ function groupByKCore(nodes, edges) {
|
|
|
409
409
|
.map(([, nodeList]) => nodeList);
|
|
410
410
|
}
|
|
411
411
|
function groupByCommunity(nodes, edges, targetGroups) {
|
|
412
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
// Count communities
|
|
446
|
-
numCommunities = new Set(communities.values()).size;
|
|
462
|
+
|
|
463
|
+
if (!changed) break;
|
|
447
464
|
}
|
|
448
|
-
|
|
465
|
+
|
|
466
|
+
// Consolidate communities
|
|
449
467
|
const groups = new Map();
|
|
450
468
|
communities.forEach((comm, node) => {
|
|
451
|
-
if (!groups.has(comm))
|
|
452
|
-
groups.set(comm, []);
|
|
469
|
+
if (!groups.has(comm)) groups.set(comm, []);
|
|
453
470
|
groups.get(comm).push(node);
|
|
454
471
|
});
|
|
455
|
-
|
|
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;
|
|
456
485
|
}
|
|
457
486
|
//# sourceMappingURL=layout-helpers.js.map
|