@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,448 @@
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 Layout Comparison - @graphty/layout</title>
19
+ <style>
20
+ body {
21
+ margin: 0;
22
+ font-family: Arial, sans-serif;
23
+ overflow: hidden;
24
+ background: #0a0a0a;
25
+ }
26
+ #container {
27
+ display: grid;
28
+ grid-template-columns: 1fr 1fr;
29
+ grid-template-rows: 1fr 1fr;
30
+ width: 100vw;
31
+ height: 100vh;
32
+ gap: 2px;
33
+ }
34
+ .viewport {
35
+ position: relative;
36
+ background: #111;
37
+ }
38
+ .viewport canvas {
39
+ width: 100% !important;
40
+ height: 100% !important;
41
+ }
42
+ .viewport-label {
43
+ position: absolute;
44
+ top: 10px;
45
+ left: 10px;
46
+ color: white;
47
+ background: rgba(0, 0, 0, 0.7);
48
+ padding: 8px 12px;
49
+ border-radius: 4px;
50
+ font-size: 14px;
51
+ font-weight: bold;
52
+ pointer-events: none;
53
+ }
54
+ #controls {
55
+ position: absolute;
56
+ top: 50%;
57
+ left: 50%;
58
+ transform: translate(-50%, -50%);
59
+ background: rgba(0, 0, 0, 0.9);
60
+ padding: 20px;
61
+ border-radius: 8px;
62
+ color: white;
63
+ border: 1px solid #333;
64
+ z-index: 100;
65
+ }
66
+ #info {
67
+ position: absolute;
68
+ bottom: 10px;
69
+ left: 10px;
70
+ color: white;
71
+ background: rgba(0, 0, 0, 0.7);
72
+ padding: 10px;
73
+ border-radius: 5px;
74
+ font-size: 12px;
75
+ }
76
+ button {
77
+ display: block;
78
+ margin: 10px 0;
79
+ padding: 8px 16px;
80
+ cursor: pointer;
81
+ width: 100%;
82
+ background: #2a2a2a;
83
+ color: white;
84
+ border: 1px solid #444;
85
+ border-radius: 4px;
86
+ }
87
+ button:hover {
88
+ background: #3a3a3a;
89
+ }
90
+ label {
91
+ display: block;
92
+ margin: 10px 0;
93
+ }
94
+ select {
95
+ width: 100%;
96
+ padding: 5px;
97
+ margin-top: 5px;
98
+ background: #2a2a2a;
99
+ color: white;
100
+ border: 1px solid #444;
101
+ border-radius: 4px;
102
+ }
103
+ .sync-indicator {
104
+ display: inline-block;
105
+ width: 8px;
106
+ height: 8px;
107
+ background: #4f4;
108
+ border-radius: 50%;
109
+ margin-left: 5px;
110
+ vertical-align: middle;
111
+ }
112
+ </style>
113
+ </head>
114
+ <body>
115
+ <div id="container">
116
+ <div class="viewport" id="viewport1">
117
+ <div class="viewport-label">Spherical Layout</div>
118
+ </div>
119
+ <div class="viewport" id="viewport2">
120
+ <div class="viewport-label">Kamada-Kawai Layout</div>
121
+ </div>
122
+ <div class="viewport" id="viewport3">
123
+ <div class="viewport-label">Spring Layout</div>
124
+ </div>
125
+ <div class="viewport" id="viewport4">
126
+ <div class="viewport-label">ForceAtlas2 Layout</div>
127
+ </div>
128
+ </div>
129
+
130
+ <div id="controls">
131
+ <h3>3D Layout Comparison <span class="sync-indicator"></span></h3>
132
+ <label>
133
+ Graph Type:
134
+ <select id="graphType">
135
+ <option value="grid">Grid Graph (4×4)</option>
136
+ <option value="complete">Complete Graph (K8)</option>
137
+ <option value="cycle">Cycle Graph (C20)</option>
138
+ <option value="star">Star Graph (S15)</option>
139
+ <option value="wheel">Wheel Graph (W12)</option>
140
+ <option value="bipartite">Bipartite Graph</option>
141
+ <option value="scalefree">Scale-Free Graph</option>
142
+ <option value="random">Random Graph</option>
143
+ </select>
144
+ </label>
145
+ <button id="regenerate">Generate New Graph</button>
146
+ <button id="syncCameras">Sync All Cameras</button>
147
+ <label>
148
+ <input type="checkbox" id="autoRotate" checked> Auto Rotate
149
+ </label>
150
+ </div>
151
+
152
+ <div id="info">
153
+ <div>Drag to rotate • Scroll to zoom • Click center panel to change graph</div>
154
+ <div>Nodes: <span id="nodeCount">0</span> • Edges: <span id="edgeCount">0</span></div>
155
+ </div>
156
+
157
+ <!-- Import map for module resolution -->
158
+ <script type="importmap">
159
+ {
160
+ "imports": {
161
+ "three": "https://unpkg.com/three@0.157.0/build/three.module.js",
162
+ "three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
163
+ }
164
+ }
165
+ </script>
166
+
167
+ <script type="module">
168
+ import * as THREE from 'https://unpkg.com/three@0.157.0/build/three.module.js';
169
+ import { OrbitControls } from 'https://unpkg.com/three@0.157.0/examples/jsm/controls/OrbitControls.js';
170
+ import {
171
+ circularLayout,
172
+ kamadaKawaiLayout,
173
+ springLayout,
174
+ forceatlas2Layout,
175
+ completeGraph,
176
+ cycleGraph,
177
+ starGraph,
178
+ wheelGraph,
179
+ gridGraph,
180
+ bipartiteGraph,
181
+ scaleFreeGraph,
182
+ randomGraph
183
+ } from './layout.js';
184
+
185
+ // Viewport configurations
186
+ const viewports = [
187
+ { id: 'viewport1', layout: 'spherical', scene: null, camera: null, renderer: null, controls: null },
188
+ { id: 'viewport2', layout: 'kamadaKawai', scene: null, camera: null, renderer: null, controls: null },
189
+ { id: 'viewport3', layout: 'spring', scene: null, camera: null, renderer: null, controls: null },
190
+ { id: 'viewport4', layout: 'forceatlas2', scene: null, camera: null, renderer: null, controls: null }
191
+ ];
192
+
193
+ // Shared graph data
194
+ let currentGraph = null;
195
+ let sharedNodeMaterial = null;
196
+ let sharedEdgeMaterial = null;
197
+
198
+ // Create materials
199
+ function createMaterials() {
200
+ sharedNodeMaterial = new THREE.MeshPhongMaterial({
201
+ vertexColors: true,
202
+ emissive: 0x222222,
203
+ shininess: 100
204
+ });
205
+
206
+ sharedEdgeMaterial = new THREE.LineBasicMaterial({
207
+ color: 0x4466ff,
208
+ transparent: true,
209
+ opacity: 0.3
210
+ });
211
+ }
212
+
213
+ // Initialize viewport
214
+ function initViewport(viewport) {
215
+ const container = document.getElementById(viewport.id);
216
+ const rect = container.getBoundingClientRect();
217
+
218
+ // Scene
219
+ viewport.scene = new THREE.Scene();
220
+ viewport.scene.background = new THREE.Color(0x111111);
221
+
222
+ // Camera
223
+ viewport.camera = new THREE.PerspectiveCamera(
224
+ 60,
225
+ rect.width / rect.height,
226
+ 0.1,
227
+ 2000
228
+ );
229
+ viewport.camera.position.set(300, 200, 400);
230
+
231
+ // Renderer
232
+ viewport.renderer = new THREE.WebGLRenderer({ antialias: true });
233
+ viewport.renderer.setSize(rect.width, rect.height);
234
+ container.appendChild(viewport.renderer.domElement);
235
+
236
+ // Controls
237
+ viewport.controls = new OrbitControls(viewport.camera, viewport.renderer.domElement);
238
+ viewport.controls.enableDamping = true;
239
+ viewport.controls.dampingFactor = 0.05;
240
+
241
+ // Lighting
242
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
243
+ viewport.scene.add(ambientLight);
244
+
245
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
246
+ directionalLight.position.set(1, 1, 1);
247
+ viewport.scene.add(directionalLight);
248
+
249
+ // Groups
250
+ viewport.nodeGroup = new THREE.Group();
251
+ viewport.edgeGroup = new THREE.Group();
252
+ viewport.scene.add(viewport.nodeGroup);
253
+ viewport.scene.add(viewport.edgeGroup);
254
+
255
+ }
256
+
257
+ // Create graph
258
+ function createGraph(type) {
259
+ switch (type) {
260
+ case 'complete':
261
+ return completeGraph(8);
262
+ case 'cycle':
263
+ return cycleGraph(20);
264
+ case 'star':
265
+ return starGraph(15);
266
+ case 'wheel':
267
+ return wheelGraph(12);
268
+ case 'grid':
269
+ return gridGraph(4, 4);
270
+ case 'bipartite':
271
+ return bipartiteGraph(8, 7, 0.4, Date.now());
272
+ case 'scalefree':
273
+ return scaleFreeGraph(20, 2, Date.now());
274
+ case 'random':
275
+ return randomGraph(20, 0.15, Date.now());
276
+ default:
277
+ return gridGraph(4, 4);
278
+ }
279
+ }
280
+
281
+ // Get layout positions
282
+ function getLayoutPositions(layoutType, graph) {
283
+ const scale = 150;
284
+ const center = [0, 0, 0];
285
+
286
+ switch (layoutType) {
287
+ case 'spherical':
288
+ return circularLayout(graph, scale, center, 3);
289
+ case 'kamadaKawai':
290
+ return kamadaKawaiLayout(graph, null, null, 'weight', scale/150, center, 3);
291
+ case 'spring':
292
+ return springLayout(graph, 1, null, null, 50, scale/150, center, 3);
293
+ case 'forceatlas2':
294
+ return forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, null, 3);
295
+ default:
296
+ return circularLayout(graph, scale, center, 3);
297
+ }
298
+ }
299
+
300
+ // Visualize graph in viewport
301
+ function visualizeInViewport(viewport, graph, positions) {
302
+ // Clear previous
303
+ viewport.nodeGroup.clear();
304
+ viewport.edgeGroup.clear();
305
+
306
+ const nodes = graph.nodes();
307
+ const edges = graph.edges();
308
+
309
+ // Calculate node colors based on degree
310
+ const degrees = {};
311
+ nodes.forEach(node => degrees[node] = 0);
312
+ edges.forEach(([u, v]) => {
313
+ degrees[u]++;
314
+ degrees[v]++;
315
+ });
316
+ const maxDegree = Math.max(...Object.values(degrees), 1);
317
+
318
+ // Create nodes
319
+ const nodeGeometry = new THREE.SphereGeometry(6, 16, 16);
320
+ const nodeColors = [];
321
+ const color = new THREE.Color();
322
+
323
+ nodes.forEach((node, i) => {
324
+ const degree = degrees[node];
325
+ color.setHSL(0.6 - (degree / maxDegree) * 0.6, 0.8, 0.5);
326
+
327
+ for (let j = 0; j < nodeGeometry.attributes.position.count; j++) {
328
+ nodeColors.push(color.r, color.g, color.b);
329
+ }
330
+ });
331
+
332
+ nodeGeometry.setAttribute('color', new THREE.Float32BufferAttribute(nodeColors, 3));
333
+
334
+ nodes.forEach((node, i) => {
335
+ const mesh = new THREE.Mesh(nodeGeometry.clone(), sharedNodeMaterial.clone());
336
+ const pos = positions[node];
337
+ mesh.position.set(pos[0], pos[1], pos[2]);
338
+ viewport.nodeGroup.add(mesh);
339
+ });
340
+
341
+ // Create edges
342
+ edges.forEach(([source, target]) => {
343
+ const sourcePos = positions[source];
344
+ const targetPos = positions[target];
345
+
346
+ const points = [
347
+ new THREE.Vector3(sourcePos[0], sourcePos[1], sourcePos[2]),
348
+ new THREE.Vector3(targetPos[0], targetPos[1], targetPos[2])
349
+ ];
350
+
351
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
352
+ const line = new THREE.Line(geometry, sharedEdgeMaterial);
353
+ viewport.edgeGroup.add(line);
354
+ });
355
+ }
356
+
357
+ // Update all viewports
358
+ function updateAllViewports() {
359
+ if (!currentGraph) return;
360
+
361
+ const nodes = currentGraph.nodes();
362
+ const edges = currentGraph.edges();
363
+ document.getElementById('nodeCount').textContent = nodes.length;
364
+ document.getElementById('edgeCount').textContent = edges.length;
365
+
366
+ viewports.forEach(viewport => {
367
+ const positions = getLayoutPositions(viewport.layout, currentGraph);
368
+ visualizeInViewport(viewport, currentGraph, positions);
369
+ });
370
+ }
371
+
372
+ // Sync cameras
373
+ function syncCameras() {
374
+ const sourceCamera = viewports[0].camera;
375
+ const sourceControls = viewports[0].controls;
376
+
377
+ viewports.slice(1).forEach(viewport => {
378
+ viewport.camera.position.copy(sourceCamera.position);
379
+ viewport.camera.quaternion.copy(sourceCamera.quaternion);
380
+ viewport.controls.target.copy(sourceControls.target);
381
+ viewport.controls.update();
382
+ });
383
+ }
384
+
385
+ // Initialize
386
+ createMaterials();
387
+ viewports.forEach(viewport => initViewport(viewport));
388
+
389
+ // Create initial graph
390
+ currentGraph = createGraph('grid');
391
+ updateAllViewports();
392
+
393
+ // Controls
394
+ const controlPanel = document.getElementById('controls');
395
+ const graphTypeSelect = document.getElementById('graphType');
396
+ const autoRotateCheckbox = document.getElementById('autoRotate');
397
+
398
+ document.getElementById('regenerate').addEventListener('click', () => {
399
+ currentGraph = createGraph(graphTypeSelect.value);
400
+ updateAllViewports();
401
+ });
402
+
403
+ document.getElementById('syncCameras').addEventListener('click', syncCameras);
404
+
405
+ graphTypeSelect.addEventListener('change', () => {
406
+ currentGraph = createGraph(graphTypeSelect.value);
407
+ updateAllViewports();
408
+ });
409
+
410
+ // Hide controls when clicking outside
411
+ let controlsVisible = true;
412
+ document.addEventListener('click', (e) => {
413
+ if (e.target.closest('#controls')) return;
414
+ controlsVisible = !controlsVisible;
415
+ controlPanel.style.display = controlsVisible ? 'block' : 'none';
416
+ });
417
+
418
+ // Animation loop
419
+ function animate() {
420
+ requestAnimationFrame(animate);
421
+
422
+ viewports.forEach(viewport => {
423
+ if (autoRotateCheckbox.checked) {
424
+ viewport.nodeGroup.rotation.y += 0.001;
425
+ viewport.edgeGroup.rotation.y += 0.001;
426
+ }
427
+
428
+ viewport.controls.update();
429
+ viewport.renderer.render(viewport.scene, viewport.camera);
430
+ });
431
+ }
432
+
433
+ // Handle window resize
434
+ window.addEventListener('resize', () => {
435
+ viewports.forEach(viewport => {
436
+ const container = document.getElementById(viewport.id);
437
+ const rect = container.getBoundingClientRect();
438
+
439
+ viewport.camera.aspect = rect.width / rect.height;
440
+ viewport.camera.updateProjectionMatrix();
441
+ viewport.renderer.setSize(rect.width, rect.height);
442
+ });
443
+ });
444
+
445
+ animate();
446
+ </script>
447
+ </body>
448
+ </html>