@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.
- package/.env.example +11 -0
- package/CHANGELOG.md +7 -0
- package/DEPLOYMENT.md +59 -0
- package/README.md +53 -0
- package/dist/layout-helpers.js +2 -1
- package/dist/layout-helpers.js.map +1 -1
- package/dist/layout.d.ts +2 -2
- package/dist/layout.js +38 -14
- package/dist/layout.js.map +1 -1
- 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 +13 -1
- package/examples/bipartite-layout.html +13 -1
- package/examples/circular-layout.html +14 -2
- package/examples/forceatlas2-layout.html +13 -1
- package/examples/index.html +75 -0
- package/examples/kamada-kawai-layout.html +13 -1
- package/examples/multipartite-layout.html +13 -41
- package/examples/planar-layout.html +13 -1
- package/examples/random-layout.html +13 -1
- package/examples/shell-layout.html +13 -1
- package/examples/spectral-layout.html +13 -1
- package/examples/spiral-layout.html +13 -1
- package/examples/spring-layout.html +14 -2
- package/layout-helpers.ts +2 -1
- package/layout.ts +39 -13
- package/package.json +5 -2
- package/test/circular-layout.test.ts +145 -3
- package/test/kamada-kawai-layout.test.ts +273 -1
- package/vite.config.js +36 -0
|
@@ -0,0 +1,319 @@
|
|
|
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 Spherical Layout - @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
|
+
}
|
|
47
|
+
button {
|
|
48
|
+
display: block;
|
|
49
|
+
margin: 5px 0;
|
|
50
|
+
padding: 5px 10px;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
}
|
|
53
|
+
label {
|
|
54
|
+
display: block;
|
|
55
|
+
margin: 5px 0;
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
58
|
+
</head>
|
|
59
|
+
<body>
|
|
60
|
+
<div id="container"></div>
|
|
61
|
+
<div id="info">
|
|
62
|
+
<h2>3D Spherical Layout</h2>
|
|
63
|
+
<p>Nodes distributed evenly on a sphere using Fibonacci spiral</p>
|
|
64
|
+
<p>Drag to rotate • Scroll to zoom</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div id="controls">
|
|
67
|
+
<label>
|
|
68
|
+
Graph Type:
|
|
69
|
+
<select id="graphType">
|
|
70
|
+
<option value="complete">Complete Graph</option>
|
|
71
|
+
<option value="cycle">Cycle Graph</option>
|
|
72
|
+
<option value="star">Star Graph</option>
|
|
73
|
+
<option value="wheel">Wheel Graph</option>
|
|
74
|
+
<option value="grid">Grid Graph</option>
|
|
75
|
+
<option value="random">Random Graph</option>
|
|
76
|
+
</select>
|
|
77
|
+
</label>
|
|
78
|
+
<label>
|
|
79
|
+
Node Count: <span id="nodeCountLabel">20</span>
|
|
80
|
+
<input type="range" id="nodeCount" min="5" max="50" value="20">
|
|
81
|
+
</label>
|
|
82
|
+
<label>
|
|
83
|
+
Sphere Radius: <span id="radiusLabel">200</span>
|
|
84
|
+
<input type="range" id="radius" min="50" max="300" value="200">
|
|
85
|
+
</label>
|
|
86
|
+
<button id="regenerate">Regenerate Graph</button>
|
|
87
|
+
<label>
|
|
88
|
+
<input type="checkbox" id="showSphere" checked> Show Sphere
|
|
89
|
+
</label>
|
|
90
|
+
<label>
|
|
91
|
+
<input type="checkbox" id="animateRotation" checked> Auto Rotate
|
|
92
|
+
</label>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Import map for module resolution -->
|
|
96
|
+
<script type="importmap">
|
|
97
|
+
{
|
|
98
|
+
"imports": {
|
|
99
|
+
"three": "https://unpkg.com/three@0.157.0/build/three.module.js",
|
|
100
|
+
"three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<script type="module">
|
|
106
|
+
import * as THREE from 'https://unpkg.com/three@0.157.0/build/three.module.js';
|
|
107
|
+
import { OrbitControls } from 'https://unpkg.com/three@0.157.0/examples/jsm/controls/OrbitControls.js';
|
|
108
|
+
import {
|
|
109
|
+
circularLayout,
|
|
110
|
+
completeGraph,
|
|
111
|
+
cycleGraph,
|
|
112
|
+
starGraph,
|
|
113
|
+
wheelGraph,
|
|
114
|
+
gridGraph,
|
|
115
|
+
randomGraph
|
|
116
|
+
} from './layout.js';
|
|
117
|
+
|
|
118
|
+
// Three.js setup
|
|
119
|
+
const scene = new THREE.Scene();
|
|
120
|
+
scene.background = new THREE.Color(0x0a0a0a);
|
|
121
|
+
|
|
122
|
+
const camera = new THREE.PerspectiveCamera(
|
|
123
|
+
75,
|
|
124
|
+
window.innerWidth / window.innerHeight,
|
|
125
|
+
0.1,
|
|
126
|
+
2000
|
|
127
|
+
);
|
|
128
|
+
camera.position.set(400, 300, 500);
|
|
129
|
+
|
|
130
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
131
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
132
|
+
document.getElementById('container').appendChild(renderer.domElement);
|
|
133
|
+
|
|
134
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
135
|
+
controls.enableDamping = true;
|
|
136
|
+
controls.dampingFactor = 0.05;
|
|
137
|
+
|
|
138
|
+
// Lighting
|
|
139
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
140
|
+
scene.add(ambientLight);
|
|
141
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
|
|
142
|
+
directionalLight.position.set(1, 1, 1);
|
|
143
|
+
scene.add(directionalLight);
|
|
144
|
+
|
|
145
|
+
// Graph visualization
|
|
146
|
+
let nodeGroup = new THREE.Group();
|
|
147
|
+
let edgeGroup = new THREE.Group();
|
|
148
|
+
let sphereMesh = null;
|
|
149
|
+
scene.add(nodeGroup);
|
|
150
|
+
scene.add(edgeGroup);
|
|
151
|
+
|
|
152
|
+
// Create sphere mesh
|
|
153
|
+
function createSphereMesh(radius) {
|
|
154
|
+
const geometry = new THREE.SphereGeometry(radius, 32, 32);
|
|
155
|
+
const material = new THREE.MeshBasicMaterial({
|
|
156
|
+
color: 0x444444,
|
|
157
|
+
wireframe: true,
|
|
158
|
+
transparent: true,
|
|
159
|
+
opacity: 0.2
|
|
160
|
+
});
|
|
161
|
+
return new THREE.Mesh(geometry, material);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Create graph based on type
|
|
165
|
+
function createGraph(type, nodeCount) {
|
|
166
|
+
switch (type) {
|
|
167
|
+
case 'complete':
|
|
168
|
+
return completeGraph(nodeCount);
|
|
169
|
+
case 'cycle':
|
|
170
|
+
return cycleGraph(nodeCount);
|
|
171
|
+
case 'star':
|
|
172
|
+
return starGraph(nodeCount);
|
|
173
|
+
case 'wheel':
|
|
174
|
+
return wheelGraph(nodeCount);
|
|
175
|
+
case 'grid':
|
|
176
|
+
const size = Math.floor(Math.sqrt(nodeCount));
|
|
177
|
+
return gridGraph(size, size);
|
|
178
|
+
case 'random':
|
|
179
|
+
return randomGraph(nodeCount, 0.3, Date.now());
|
|
180
|
+
default:
|
|
181
|
+
return completeGraph(nodeCount);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Visualize graph in 3D
|
|
186
|
+
function visualizeGraph(graph, radius) {
|
|
187
|
+
// Clear previous visualization
|
|
188
|
+
nodeGroup.clear();
|
|
189
|
+
edgeGroup.clear();
|
|
190
|
+
|
|
191
|
+
// Get 3D spherical layout
|
|
192
|
+
const positions = circularLayout(graph, radius, [0, 0, 0], 3);
|
|
193
|
+
|
|
194
|
+
// Create nodes
|
|
195
|
+
const nodeGeometry = new THREE.SphereGeometry(8, 16, 16);
|
|
196
|
+
const nodes = graph.nodes();
|
|
197
|
+
const nodeMeshes = {};
|
|
198
|
+
|
|
199
|
+
nodes.forEach((node, i) => {
|
|
200
|
+
const color = new THREE.Color();
|
|
201
|
+
color.setHSL(i / nodes.length, 0.7, 0.5);
|
|
202
|
+
|
|
203
|
+
const material = new THREE.MeshPhongMaterial({
|
|
204
|
+
color: color,
|
|
205
|
+
emissive: color,
|
|
206
|
+
emissiveIntensity: 0.3
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const mesh = new THREE.Mesh(nodeGeometry, material);
|
|
210
|
+
const [x, y, z] = positions[node];
|
|
211
|
+
mesh.position.set(x, y, z);
|
|
212
|
+
|
|
213
|
+
nodeGroup.add(mesh);
|
|
214
|
+
nodeMeshes[node] = mesh;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Create edges
|
|
218
|
+
const edges = graph.edges();
|
|
219
|
+
edges.forEach(([source, target]) => {
|
|
220
|
+
const sourcePos = positions[source];
|
|
221
|
+
const targetPos = positions[target];
|
|
222
|
+
|
|
223
|
+
const points = [
|
|
224
|
+
new THREE.Vector3(sourcePos[0], sourcePos[1], sourcePos[2]),
|
|
225
|
+
new THREE.Vector3(targetPos[0], targetPos[1], targetPos[2])
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
229
|
+
const material = new THREE.LineBasicMaterial({
|
|
230
|
+
color: 0x4444ff,
|
|
231
|
+
transparent: true,
|
|
232
|
+
opacity: 0.6
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const line = new THREE.Line(geometry, material);
|
|
236
|
+
edgeGroup.add(line);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Initial graph
|
|
241
|
+
let currentGraph = createGraph('complete', 20);
|
|
242
|
+
visualizeGraph(currentGraph, 200);
|
|
243
|
+
|
|
244
|
+
// Add sphere
|
|
245
|
+
sphereMesh = createSphereMesh(200);
|
|
246
|
+
scene.add(sphereMesh);
|
|
247
|
+
|
|
248
|
+
// Controls
|
|
249
|
+
const graphTypeSelect = document.getElementById('graphType');
|
|
250
|
+
const nodeCountSlider = document.getElementById('nodeCount');
|
|
251
|
+
const nodeCountLabel = document.getElementById('nodeCountLabel');
|
|
252
|
+
const radiusSlider = document.getElementById('radius');
|
|
253
|
+
const radiusLabel = document.getElementById('radiusLabel');
|
|
254
|
+
const regenerateBtn = document.getElementById('regenerate');
|
|
255
|
+
const showSphereCheckbox = document.getElementById('showSphere');
|
|
256
|
+
const animateCheckbox = document.getElementById('animateRotation');
|
|
257
|
+
|
|
258
|
+
nodeCountSlider.addEventListener('input', (e) => {
|
|
259
|
+
nodeCountLabel.textContent = e.target.value;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
radiusSlider.addEventListener('input', (e) => {
|
|
263
|
+
radiusLabel.textContent = e.target.value;
|
|
264
|
+
const radius = parseInt(e.target.value);
|
|
265
|
+
visualizeGraph(currentGraph, radius);
|
|
266
|
+
if (sphereMesh) {
|
|
267
|
+
scene.remove(sphereMesh);
|
|
268
|
+
sphereMesh = createSphereMesh(radius);
|
|
269
|
+
sphereMesh.visible = showSphereCheckbox.checked;
|
|
270
|
+
scene.add(sphereMesh);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
regenerateBtn.addEventListener('click', () => {
|
|
275
|
+
const type = graphTypeSelect.value;
|
|
276
|
+
const nodeCount = parseInt(nodeCountSlider.value);
|
|
277
|
+
const radius = parseInt(radiusSlider.value);
|
|
278
|
+
|
|
279
|
+
currentGraph = createGraph(type, nodeCount);
|
|
280
|
+
visualizeGraph(currentGraph, radius);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
graphTypeSelect.addEventListener('change', () => {
|
|
284
|
+
regenerateBtn.click();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
showSphereCheckbox.addEventListener('change', (e) => {
|
|
288
|
+
if (sphereMesh) {
|
|
289
|
+
sphereMesh.visible = e.target.checked;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Animation
|
|
294
|
+
function animate() {
|
|
295
|
+
requestAnimationFrame(animate);
|
|
296
|
+
|
|
297
|
+
if (animateCheckbox.checked) {
|
|
298
|
+
nodeGroup.rotation.y += 0.002;
|
|
299
|
+
edgeGroup.rotation.y += 0.002;
|
|
300
|
+
if (sphereMesh) {
|
|
301
|
+
sphereMesh.rotation.y += 0.002;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
controls.update();
|
|
306
|
+
renderer.render(scene, camera);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Handle window resize
|
|
310
|
+
window.addEventListener('resize', () => {
|
|
311
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
312
|
+
camera.updateProjectionMatrix();
|
|
313
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
animate();
|
|
317
|
+
</script>
|
|
318
|
+
</body>
|
|
319
|
+
</html>
|
package/examples/arf-layout.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>ARF Layout Test</title>
|
|
@@ -178,7 +190,7 @@
|
|
|
178
190
|
</div>
|
|
179
191
|
|
|
180
192
|
<script type="module">
|
|
181
|
-
import { arfLayout } from
|
|
193
|
+
import { arfLayout } from "./layout.js";
|
|
182
194
|
|
|
183
195
|
let currentGraph = null;
|
|
184
196
|
let currentSeed = Math.floor(Math.random() * 1000);
|
package/examples/bfs-layout.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>BFS Layout Test</title>
|
|
@@ -164,7 +176,7 @@
|
|
|
164
176
|
randomGraph,
|
|
165
177
|
scaleFreeGraph,
|
|
166
178
|
findBestRoot
|
|
167
|
-
} from
|
|
179
|
+
} from "./layout.js";
|
|
168
180
|
|
|
169
181
|
let currentGraph = null;
|
|
170
182
|
let bestRoot = null;
|
|
@@ -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>
|
|
@@ -155,7 +167,7 @@
|
|
|
155
167
|
bipartiteGraph,
|
|
156
168
|
detectBipartite,
|
|
157
169
|
randomGraph
|
|
158
|
-
} from
|
|
170
|
+
} from "./layout.js";
|
|
159
171
|
|
|
160
172
|
let currentGraph = null;
|
|
161
173
|
let detectedBipartite = null;
|
|
@@ -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>
|
|
@@ -142,7 +154,7 @@
|
|
|
142
154
|
cycleGraph,
|
|
143
155
|
wheelGraph,
|
|
144
156
|
randomGraph
|
|
145
|
-
} from
|
|
157
|
+
} from "./layout.js";
|
|
146
158
|
|
|
147
159
|
let currentGraph = null;
|
|
148
160
|
|
|
@@ -277,4 +289,4 @@
|
|
|
277
289
|
newGraph();
|
|
278
290
|
</script>
|
|
279
291
|
</body>
|
|
280
|
-
</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>
|
|
@@ -196,7 +208,7 @@
|
|
|
196
208
|
scaleFreeGraph,
|
|
197
209
|
gridGraph,
|
|
198
210
|
autoConfigureForce
|
|
199
|
-
} from
|
|
211
|
+
} from "./layout.js";
|
|
200
212
|
|
|
201
213
|
let currentGraph = null;
|
|
202
214
|
let currentConfig = null;
|
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;
|
|
@@ -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>Multipartite Layout Test</title>
|
|
@@ -162,7 +174,7 @@
|
|
|
162
174
|
randomGraph,
|
|
163
175
|
groupNodes,
|
|
164
176
|
findBestRoot
|
|
165
|
-
} from
|
|
177
|
+
} from "./layout.js";
|
|
166
178
|
|
|
167
179
|
let currentGraph = null;
|
|
168
180
|
let currentLayers = null;
|
|
@@ -209,46 +221,6 @@
|
|
|
209
221
|
return { nodes: () => nodes, edges: () => edges };
|
|
210
222
|
}
|
|
211
223
|
}
|
|
212
|
-
break;
|
|
213
|
-
|
|
214
|
-
case 'hierarchical':
|
|
215
|
-
// Hierarchical connections (each node connects to nodes in the next layer)
|
|
216
|
-
for (let layer = 0; layer < numLayers - 1; layer++) {
|
|
217
|
-
const currentLayer = layers[layer];
|
|
218
|
-
const nextLayer = layers[layer + 1];
|
|
219
|
-
|
|
220
|
-
currentLayer.forEach((node1, idx) => {
|
|
221
|
-
// Connect to 1-2 nodes in the next layer
|
|
222
|
-
const connections = Math.min(2, nextLayer.length);
|
|
223
|
-
for (let c = 0; c < connections; c++) {
|
|
224
|
-
const targetIdx = (idx + c) % nextLayer.length;
|
|
225
|
-
edges.push([node1, nextLayer[targetIdx]]);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
break;
|
|
230
|
-
|
|
231
|
-
case 'complete':
|
|
232
|
-
// Each layer is fully connected to the next
|
|
233
|
-
for (let layer = 0; layer < numLayers - 1; layer++) {
|
|
234
|
-
const currentLayer = layers[layer];
|
|
235
|
-
const nextLayer = layers[layer + 1];
|
|
236
|
-
|
|
237
|
-
currentLayer.forEach(node1 => {
|
|
238
|
-
nextLayer.forEach(node2 => {
|
|
239
|
-
edges.push([node1, node2]);
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
nodes: () => allNodes,
|
|
248
|
-
edges: () => edges,
|
|
249
|
-
layers
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
224
|
|
|
253
225
|
function visualizeGraph(graph, positions) {
|
|
254
226
|
const svg = document.getElementById('graph-svg');
|