@graphty/layout 1.1.1 → 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.
@@ -0,0 +1,611 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
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>
16
+ <meta charset="UTF-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
18
+ <title>3D Force-Directed Layouts - @graphty/layout</title>
19
+ <style>
20
+ body {
21
+ margin: 0;
22
+ font-family: Arial, sans-serif;
23
+ overflow: hidden;
24
+ }
25
+ #container {
26
+ width: 100vw;
27
+ height: 100vh;
28
+ }
29
+ #info {
30
+ position: absolute;
31
+ top: 10px;
32
+ left: 10px;
33
+ color: white;
34
+ background: rgba(0, 0, 0, 0.7);
35
+ padding: 10px;
36
+ border-radius: 5px;
37
+ }
38
+ #controls {
39
+ position: absolute;
40
+ top: 10px;
41
+ right: 10px;
42
+ background: rgba(0, 0, 0, 0.7);
43
+ padding: 10px;
44
+ border-radius: 5px;
45
+ color: white;
46
+ max-width: 250px;
47
+ }
48
+ button {
49
+ display: block;
50
+ margin: 5px 0;
51
+ padding: 5px 10px;
52
+ cursor: pointer;
53
+ width: 100%;
54
+ }
55
+ label {
56
+ display: block;
57
+ margin: 8px 0;
58
+ }
59
+ select, input[type="range"] {
60
+ width: 100%;
61
+ }
62
+ .algorithm-params {
63
+ margin-top: 10px;
64
+ padding-top: 10px;
65
+ border-top: 1px solid #555;
66
+ }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <div id="container"></div>
71
+ <div id="info">
72
+ <h2>3D Force-Directed Layouts</h2>
73
+ <p>Compare different force-directed algorithms in 3D</p>
74
+ <p>Drag to rotate • Scroll to zoom</p>
75
+ </div>
76
+ <div id="controls">
77
+ <label>
78
+ Algorithm:
79
+ <select id="algorithm">
80
+ <option value="spring">Spring Layout</option>
81
+ <option value="forceatlas2">ForceAtlas2</option>
82
+ <option value="arf">ARF Layout</option>
83
+ </select>
84
+ </label>
85
+ <label>
86
+ Graph Type:
87
+ <select id="graphType">
88
+ <option value="random">Random Graph</option>
89
+ <option value="scalefree">Scale-Free Graph</option>
90
+ <option value="complete">Complete Graph</option>
91
+ <option value="bipartite">Bipartite Graph</option>
92
+ <option value="grid">Grid Graph</option>
93
+ </select>
94
+ </label>
95
+ <label>
96
+ Node Count: <span id="nodeCountLabel">25</span>
97
+ <input type="range" id="nodeCount" min="10" max="50" value="25">
98
+ </label>
99
+ <button id="regenerate">Generate New Graph</button>
100
+
101
+ <div class="algorithm-params">
102
+ <h4>Algorithm Parameters</h4>
103
+ <label>
104
+ Iterations: <span id="iterationsLabel">50</span>
105
+ <input type="range" id="iterations" min="10" max="200" value="50">
106
+ </label>
107
+ <label id="springConstantControl">
108
+ Spring Constant: <span id="springLabel">1</span>
109
+ <input type="range" id="springConstant" min="0.1" max="5" step="0.1" value="1">
110
+ </label>
111
+ <label id="gravityControl">
112
+ Gravity: <span id="gravityLabel">1</span>
113
+ <input type="range" id="gravity" min="0" max="10" step="0.5" value="1">
114
+ </label>
115
+ <label id="attractionControl">
116
+ Attraction: <span id="attractionLabel">0.1</span>
117
+ <input type="range" id="attraction" min="0" max="1" step="0.05" value="0.1">
118
+ </label>
119
+ </div>
120
+
121
+ <label>
122
+ <input type="checkbox" id="showParticles"> Show Force Particles
123
+ </label>
124
+ <label>
125
+ <input type="checkbox" id="animateLayout" checked> Animate Transition
126
+ </label>
127
+ </div>
128
+
129
+ <!-- Import map for module resolution -->
130
+ <script type="importmap">
131
+ {
132
+ "imports": {
133
+ "three": "https://unpkg.com/three@0.157.0/build/three.module.js",
134
+ "three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
135
+ }
136
+ }
137
+ </script>
138
+
139
+ <script type="module">
140
+ console.log('Script starting...');
141
+
142
+ import * as THREE from 'https://unpkg.com/three@0.157.0/build/three.module.js';
143
+ import { OrbitControls } from 'https://unpkg.com/three@0.157.0/examples/jsm/controls/OrbitControls.js';
144
+ import {
145
+ springLayout,
146
+ forceatlas2Layout,
147
+ arfLayout,
148
+ randomGraph,
149
+ scaleFreeGraph,
150
+ completeGraph,
151
+ bipartiteGraph,
152
+ gridGraph
153
+ } from './layout.js';
154
+
155
+ console.log('All modules imported successfully');
156
+ console.log('Layout functions available:', {
157
+ springLayout: typeof springLayout,
158
+ randomGraph: typeof randomGraph
159
+ });
160
+
161
+ // Test graph generation
162
+ console.log('Testing graph generation...');
163
+ const testGraph = randomGraph(5, 0.5, 42);
164
+ console.log('Test graph created:', {
165
+ nodes: testGraph.nodes(),
166
+ edges: testGraph.edges(),
167
+ nodeCount: testGraph.nodes().length,
168
+ edgeCount: testGraph.edges().length
169
+ });
170
+
171
+ // Test layout function
172
+ console.log('Testing spring layout...');
173
+ const testPositions = springLayout(testGraph, 1, null, null, 10, 1, [0, 0, 0], 3);
174
+ console.log('Test layout positions:', testPositions);
175
+ console.log('Number of positioned nodes:', Object.keys(testPositions).length);
176
+
177
+ try {
178
+
179
+ // Three.js setup
180
+ console.log('Setting up Three.js scene...');
181
+ const scene = new THREE.Scene();
182
+ scene.background = new THREE.Color(0x0a0a0a);
183
+ scene.fog = new THREE.Fog(0x0a0a0a, 500, 1500);
184
+
185
+ const camera = new THREE.PerspectiveCamera(
186
+ 75,
187
+ window.innerWidth / window.innerHeight,
188
+ 0.1,
189
+ 2000
190
+ );
191
+ camera.position.set(400, 300, 500);
192
+
193
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
194
+ renderer.setSize(window.innerWidth, window.innerHeight);
195
+ document.getElementById('container').appendChild(renderer.domElement);
196
+
197
+ const controls = new OrbitControls(camera, renderer.domElement);
198
+ controls.enableDamping = true;
199
+ controls.dampingFactor = 0.05;
200
+
201
+ // Lighting
202
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
203
+ scene.add(ambientLight);
204
+
205
+ const pointLight1 = new THREE.PointLight(0xff0044, 1, 500);
206
+ pointLight1.position.set(200, 200, 200);
207
+ scene.add(pointLight1);
208
+
209
+ const pointLight2 = new THREE.PointLight(0x0044ff, 1, 500);
210
+ pointLight2.position.set(-200, -200, -200);
211
+ scene.add(pointLight2);
212
+
213
+ // Graph visualization
214
+ let nodeGroup = new THREE.Group();
215
+ let edgeGroup = new THREE.Group();
216
+ let particleGroup = new THREE.Group();
217
+ scene.add(nodeGroup);
218
+ scene.add(edgeGroup);
219
+ scene.add(particleGroup);
220
+
221
+ console.log('Three.js setup complete');
222
+
223
+ // Animation state
224
+ let currentPositions = {};
225
+ let targetPositions = {};
226
+ let animationProgress = 0;
227
+ let isAnimating = false;
228
+
229
+ // Create graph based on type
230
+ function createGraph(type, nodeCount) {
231
+ console.log(`Creating graph: type=${type}, nodeCount=${nodeCount}`);
232
+ let graph;
233
+ switch (type) {
234
+ case 'random':
235
+ graph = randomGraph(nodeCount, 0.15, Date.now());
236
+ break;
237
+ case 'scalefree':
238
+ graph = scaleFreeGraph(nodeCount, 2, Date.now());
239
+ break;
240
+ case 'complete':
241
+ graph = completeGraph(Math.min(nodeCount, 10)); // Limit complete graph size
242
+ break;
243
+ case 'bipartite':
244
+ const n1 = Math.floor(nodeCount / 2);
245
+ const n2 = nodeCount - n1;
246
+ graph = bipartiteGraph(n1, n2, 0.3, Date.now());
247
+ break;
248
+ case 'grid':
249
+ const size = Math.floor(Math.sqrt(nodeCount));
250
+ graph = gridGraph(size, size);
251
+ break;
252
+ default:
253
+ graph = randomGraph(nodeCount, 0.15, Date.now());
254
+ }
255
+
256
+ console.log(`Graph created:`, {
257
+ nodes: graph.nodes?.()?.length || 'no nodes method',
258
+ edges: graph.edges?.()?.length || 'no edges method',
259
+ nodesList: graph.nodes?.() || 'no nodes',
260
+ edgesList: graph.edges?.() || 'no edges'
261
+ });
262
+
263
+ return graph;
264
+ }
265
+
266
+ // Get layout based on algorithm
267
+ function getLayout(algorithm, graph, params) {
268
+ console.log(`Getting layout: algorithm=${algorithm}, params=`, params);
269
+ const center = [0, 0, 0];
270
+ const dim = 3;
271
+
272
+ let positions;
273
+ switch (algorithm) {
274
+ case 'spring':
275
+ console.log('Calling springLayout with params:', {
276
+ springConstant: params.springConstant,
277
+ iterations: params.iterations,
278
+ center,
279
+ dim
280
+ });
281
+ positions = springLayout(
282
+ graph,
283
+ params.springConstant,
284
+ null,
285
+ null,
286
+ params.iterations,
287
+ 1,
288
+ center,
289
+ dim
290
+ );
291
+ break;
292
+ case 'forceatlas2':
293
+ positions = forceatlas2Layout(
294
+ graph,
295
+ null,
296
+ params.iterations,
297
+ 1.0,
298
+ 2.0,
299
+ params.gravity,
300
+ false,
301
+ false,
302
+ null,
303
+ null,
304
+ null,
305
+ false,
306
+ false,
307
+ null,
308
+ dim
309
+ );
310
+ break;
311
+ case 'arf':
312
+ positions = arfLayout(
313
+ graph,
314
+ null,
315
+ 1,
316
+ 1.1,
317
+ params.iterations
318
+ );
319
+ break;
320
+ default:
321
+ positions = springLayout(graph, 1, null, null, 50, 1, center, dim);
322
+ }
323
+
324
+ console.log('Layout positions generated:', positions);
325
+ console.log('Number of positioned nodes:', Object.keys(positions).length);
326
+
327
+ return positions;
328
+ }
329
+
330
+ // Create force particles
331
+ function createForceParticles(positions) {
332
+ particleGroup.clear();
333
+
334
+ if (!document.getElementById('showParticles').checked) return;
335
+
336
+ const particleGeometry = new THREE.SphereGeometry(2, 8, 8);
337
+ const particleMaterial = new THREE.MeshBasicMaterial({
338
+ color: 0xffff00,
339
+ transparent: true,
340
+ opacity: 0.6
341
+ });
342
+
343
+ const scale = 200; // Same scale as nodes
344
+ Object.values(positions).forEach(pos => {
345
+ for (let i = 0; i < 3; i++) {
346
+ const particle = new THREE.Mesh(particleGeometry, particleMaterial);
347
+ const offset = (Math.random() - 0.5) * 50;
348
+ particle.position.set(
349
+ pos[0] * scale + offset,
350
+ pos[1] * scale + offset,
351
+ pos[2] * scale + offset
352
+ );
353
+ particleGroup.add(particle);
354
+ }
355
+ });
356
+ }
357
+
358
+ // Visualize graph
359
+ function visualizeGraph(graph, positions, animate = true) {
360
+ console.log('Visualizing graph with positions:', positions);
361
+ const nodes = graph.nodes();
362
+ const edges = graph.edges();
363
+ console.log(`Visualizing: ${nodes.length} nodes, ${edges.length} edges`);
364
+
365
+ // Store target positions
366
+ targetPositions = { ...positions };
367
+
368
+ // Initialize current positions if needed
369
+ if (Object.keys(currentPositions).length === 0 || !animate) {
370
+ currentPositions = { ...targetPositions };
371
+ updateVisualization(graph, currentPositions);
372
+ } else {
373
+ // Start animation
374
+ animationProgress = 0;
375
+ isAnimating = true;
376
+ }
377
+
378
+ createForceParticles(positions);
379
+ }
380
+
381
+ // Update visualization with current positions
382
+ function updateVisualization(graph, positions) {
383
+ console.log('updateVisualization called with positions:', positions);
384
+ // Clear previous visualization
385
+ nodeGroup.clear();
386
+ edgeGroup.clear();
387
+
388
+ const nodes = graph.nodes();
389
+ const edges = graph.edges();
390
+ const nodeMeshes = {};
391
+
392
+ console.log(`Creating visualization for ${nodes.length} nodes and ${edges.length} edges`);
393
+
394
+ // Calculate node sizes based on degree
395
+ const degrees = {};
396
+ nodes.forEach(node => degrees[node] = 0);
397
+ edges.forEach(([u, v]) => {
398
+ degrees[u]++;
399
+ degrees[v]++;
400
+ });
401
+
402
+ const maxDegree = Math.max(...Object.values(degrees), 1);
403
+
404
+ // Create nodes
405
+ nodes.forEach((node, i) => {
406
+ const degree = degrees[node];
407
+ const size = 5 + (degree / maxDegree) * 15;
408
+
409
+ const geometry = new THREE.SphereGeometry(size, 32, 16);
410
+ const color = new THREE.Color();
411
+ color.setHSL(i / nodes.length, 0.7, 0.5);
412
+
413
+ const material = new THREE.MeshPhongMaterial({
414
+ color: color,
415
+ emissive: color,
416
+ emissiveIntensity: 0.3,
417
+ shininess: 100
418
+ });
419
+
420
+ const mesh = new THREE.Mesh(geometry, material);
421
+ const pos = positions[node] || [0, 0, 0];
422
+ // Scale up positions for better visibility in 3D space
423
+ const scale = 200;
424
+ const scaledPos = [pos[0] * scale, pos[1] * scale, pos[2] * scale];
425
+ console.log(`Node ${node} positioned at [${pos[0]}, ${pos[1]}, ${pos[2]}] -> scaled to [${scaledPos[0]}, ${scaledPos[1]}, ${scaledPos[2]}]`);
426
+ mesh.position.set(scaledPos[0], scaledPos[1], scaledPos[2]);
427
+
428
+ nodeGroup.add(mesh);
429
+ nodeMeshes[node] = mesh;
430
+ });
431
+
432
+ console.log(`Added ${nodeGroup.children.length} node meshes to scene`);
433
+
434
+ // Create edges
435
+ const scale = 200; // Same scale as nodes
436
+ edges.forEach(([source, target]) => {
437
+ const sourcePos = positions[source] || [0, 0, 0];
438
+ const targetPos = positions[target] || [0, 0, 0];
439
+
440
+ const points = [
441
+ new THREE.Vector3(sourcePos[0] * scale, sourcePos[1] * scale, sourcePos[2] * scale),
442
+ new THREE.Vector3(targetPos[0] * scale, targetPos[1] * scale, targetPos[2] * scale)
443
+ ];
444
+
445
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
446
+ const material = new THREE.LineBasicMaterial({
447
+ color: 0x4466ff,
448
+ transparent: true,
449
+ opacity: 0.4
450
+ });
451
+
452
+ const line = new THREE.Line(geometry, material);
453
+ edgeGroup.add(line);
454
+ });
455
+ }
456
+
457
+ // Initial setup
458
+ let currentGraph = createGraph('random', 25);
459
+ let currentAlgorithm = 'spring';
460
+
461
+ // Controls
462
+ const algorithmSelect = document.getElementById('algorithm');
463
+ const graphTypeSelect = document.getElementById('graphType');
464
+ const nodeCountSlider = document.getElementById('nodeCount');
465
+ const iterationsSlider = document.getElementById('iterations');
466
+ const springConstantSlider = document.getElementById('springConstant');
467
+ const gravitySlider = document.getElementById('gravity');
468
+ const attractionSlider = document.getElementById('attraction');
469
+ const animateCheckbox = document.getElementById('animateLayout');
470
+
471
+ // Update parameter visibility based on algorithm
472
+ function updateParameterVisibility() {
473
+ const algorithm = algorithmSelect.value;
474
+ document.getElementById('springConstantControl').style.display =
475
+ (algorithm === 'spring' || algorithm === 'arf') ? 'block' : 'none';
476
+ document.getElementById('gravityControl').style.display =
477
+ algorithm === 'forceatlas2' ? 'block' : 'none';
478
+ document.getElementById('attractionControl').style.display =
479
+ algorithm === 'arf' ? 'block' : 'none';
480
+ }
481
+
482
+ // Event listeners
483
+ algorithmSelect.addEventListener('change', () => {
484
+ currentAlgorithm = algorithmSelect.value;
485
+ updateParameterVisibility();
486
+ applyLayout();
487
+ });
488
+
489
+ nodeCountSlider.addEventListener('input', (e) => {
490
+ document.getElementById('nodeCountLabel').textContent = e.target.value;
491
+ });
492
+
493
+ iterationsSlider.addEventListener('input', (e) => {
494
+ document.getElementById('iterationsLabel').textContent = e.target.value;
495
+ });
496
+
497
+ springConstantSlider.addEventListener('input', (e) => {
498
+ document.getElementById('springLabel').textContent = e.target.value;
499
+ });
500
+
501
+ gravitySlider.addEventListener('input', (e) => {
502
+ document.getElementById('gravityLabel').textContent = e.target.value;
503
+ });
504
+
505
+ attractionSlider.addEventListener('input', (e) => {
506
+ document.getElementById('attractionLabel').textContent = e.target.value;
507
+ });
508
+
509
+ document.getElementById('regenerate').addEventListener('click', () => {
510
+ const type = graphTypeSelect.value;
511
+ const nodeCount = parseInt(nodeCountSlider.value);
512
+ currentGraph = createGraph(type, nodeCount);
513
+ applyLayout();
514
+ });
515
+
516
+ document.getElementById('showParticles').addEventListener('change', () => {
517
+ if (!document.getElementById('showParticles').checked) {
518
+ particleGroup.clear();
519
+ } else {
520
+ createForceParticles(targetPositions);
521
+ }
522
+ });
523
+
524
+ // Apply layout with current parameters
525
+ function applyLayout() {
526
+ const params = {
527
+ iterations: parseInt(iterationsSlider.value),
528
+ springConstant: parseFloat(springConstantSlider.value),
529
+ gravity: parseFloat(gravitySlider.value),
530
+ attraction: parseFloat(attractionSlider.value)
531
+ };
532
+
533
+ const positions = getLayout(currentAlgorithm, currentGraph, params);
534
+ visualizeGraph(currentGraph, positions, animateCheckbox.checked);
535
+ }
536
+
537
+ // Smooth position interpolation
538
+ function lerp(start, end, t) {
539
+ return start + (end - start) * t;
540
+ }
541
+
542
+ function easeInOutCubic(t) {
543
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
544
+ }
545
+
546
+ // Animation loop
547
+ function animate() {
548
+ requestAnimationFrame(animate);
549
+
550
+ // Animate layout transition
551
+ if (isAnimating && animateCheckbox.checked) {
552
+ animationProgress += 0.02;
553
+ if (animationProgress >= 1) {
554
+ animationProgress = 1;
555
+ isAnimating = false;
556
+ currentPositions = { ...targetPositions };
557
+ }
558
+
559
+ const t = easeInOutCubic(animationProgress);
560
+ const interpolatedPositions = {};
561
+
562
+ for (const node in targetPositions) {
563
+ const current = currentPositions[node] || [0, 0, 0];
564
+ const target = targetPositions[node];
565
+ interpolatedPositions[node] = [
566
+ lerp(current[0], target[0], t),
567
+ lerp(current[1], target[1], t),
568
+ lerp(current[2], target[2], t)
569
+ ];
570
+ }
571
+
572
+ updateVisualization(currentGraph, interpolatedPositions);
573
+ }
574
+
575
+ // Animate particles
576
+ particleGroup.children.forEach((particle, i) => {
577
+ particle.position.y += Math.sin(Date.now() * 0.001 + i) * 0.5;
578
+ particle.material.opacity = 0.3 + Math.sin(Date.now() * 0.002 + i) * 0.3;
579
+ });
580
+
581
+ controls.update();
582
+ renderer.render(scene, camera);
583
+ }
584
+
585
+ // Handle window resize
586
+ window.addEventListener('resize', () => {
587
+ camera.aspect = window.innerWidth / window.innerHeight;
588
+ camera.updateProjectionMatrix();
589
+ renderer.setSize(window.innerWidth, window.innerHeight);
590
+ });
591
+
592
+ // Initialize
593
+ console.log('Initializing application...');
594
+ updateParameterVisibility();
595
+ console.log('About to call applyLayout()...');
596
+ applyLayout();
597
+ console.log('Starting animation loop...');
598
+ animate();
599
+
600
+ } catch (error) {
601
+ console.error('Error during setup:', error);
602
+ console.error('Error stack:', error.stack);
603
+ // Show error on page
604
+ const errorDiv = document.createElement('div');
605
+ errorDiv.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255,0,0,0.9); color: white; padding: 20px; border-radius: 10px; z-index: 1000; max-width: 80%; text-align: center;';
606
+ errorDiv.innerHTML = '<h3>Error Loading 3D Demo</h3><p>' + error.message + '</p><p style="font-size: 12px;">' + error.stack + '</p>';
607
+ document.body.appendChild(errorDiv);
608
+ }
609
+ </script>
610
+ </body>
611
+ </html>