@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>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>
|
|
@@ -114,12 +126,11 @@
|
|
|
114
126
|
</div>
|
|
115
127
|
|
|
116
128
|
<div class="control-group">
|
|
117
|
-
<label for="
|
|
118
|
-
<select id="
|
|
119
|
-
<option value="
|
|
120
|
-
<option value="
|
|
121
|
-
<option value="
|
|
122
|
-
<option value="complete">Complete</option>
|
|
129
|
+
<label for="graph-type">Graph type:</label>
|
|
130
|
+
<select id="graph-type">
|
|
131
|
+
<option value="scale-free">Scale-Free (automatic layers)</option>
|
|
132
|
+
<option value="tree">Tree-like</option>
|
|
133
|
+
<option value="custom-layers">Custom Layers</option>
|
|
123
134
|
</select>
|
|
124
135
|
</div>
|
|
125
136
|
|
|
@@ -141,102 +152,74 @@
|
|
|
141
152
|
</select>
|
|
142
153
|
</div>
|
|
143
154
|
|
|
155
|
+
<div class="control-group">
|
|
156
|
+
<label for="layer-method">Layer assignment:</label>
|
|
157
|
+
<select id="layer-method">
|
|
158
|
+
<option value="bfs">BFS (distance-based)</option>
|
|
159
|
+
<option value="degree">Degree-based</option>
|
|
160
|
+
<option value="community">Community-based</option>
|
|
161
|
+
</select>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
144
164
|
<button onclick="applyLayout()">Apply Layout</button>
|
|
145
165
|
<div id="layers-info" class="layers-info"></div>
|
|
146
166
|
</div>
|
|
147
167
|
</div>
|
|
148
168
|
|
|
149
169
|
<script type="module">
|
|
150
|
-
import {
|
|
170
|
+
import {
|
|
171
|
+
multipartiteLayout,
|
|
172
|
+
scaleFreeGraph,
|
|
173
|
+
starGraph,
|
|
174
|
+
randomGraph,
|
|
175
|
+
groupNodes,
|
|
176
|
+
findBestRoot
|
|
177
|
+
} from "./layout.js";
|
|
151
178
|
|
|
152
179
|
let currentGraph = null;
|
|
180
|
+
let currentLayers = null;
|
|
153
181
|
|
|
154
|
-
function
|
|
155
|
-
const
|
|
156
|
-
const allNodes = [];
|
|
157
|
-
|
|
158
|
-
// Create layers
|
|
159
|
-
for (let layer = 0; layer < numLayers; layer++) {
|
|
160
|
-
const layerNodes = [];
|
|
161
|
-
for (let node = 0; node < nodesPerLayer; node++) {
|
|
162
|
-
const nodeName = `L${layer}N${node}`;
|
|
163
|
-
layerNodes.push(nodeName);
|
|
164
|
-
allNodes.push(nodeName);
|
|
165
|
-
}
|
|
166
|
-
layers[layer] = layerNodes;
|
|
167
|
-
}
|
|
182
|
+
function generateGraph(type, numLayers, nodesPerLayer) {
|
|
183
|
+
const totalNodes = numLayers * nodesPerLayer;
|
|
168
184
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
case 'random':
|
|
190
|
-
// Random connections between all layers
|
|
191
|
-
for (let i = 0; i < allNodes.length * 0.8; i++) {
|
|
192
|
-
const node1 = allNodes[Math.floor(Math.random() * allNodes.length)];
|
|
193
|
-
const node2 = allNodes[Math.floor(Math.random() * allNodes.length)];
|
|
194
|
-
|
|
195
|
-
// Avoid self-loops and duplicates
|
|
196
|
-
if (node1 !== node2 && !edges.some(([s, t]) =>
|
|
197
|
-
(s === node1 && t === node2) || (s === node2 && t === node1))) {
|
|
198
|
-
edges.push([node1, node2]);
|
|
185
|
+
switch(type) {
|
|
186
|
+
case 'scale-free':
|
|
187
|
+
// Generate scale-free network
|
|
188
|
+
return scaleFreeGraph(totalNodes, 2, Date.now());
|
|
189
|
+
|
|
190
|
+
case 'tree':
|
|
191
|
+
// Generate tree-like structure
|
|
192
|
+
return starGraph(totalNodes);
|
|
193
|
+
|
|
194
|
+
case 'custom-layers':
|
|
195
|
+
default:
|
|
196
|
+
// Generate custom layered graph
|
|
197
|
+
const nodes = [];
|
|
198
|
+
const edges = [];
|
|
199
|
+
|
|
200
|
+
// Create nodes
|
|
201
|
+
for (let layer = 0; layer < numLayers; layer++) {
|
|
202
|
+
for (let node = 0; node < nodesPerLayer; node++) {
|
|
203
|
+
nodes.push(`L${layer}N${node}`);
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
case 'hierarchical':
|
|
204
|
-
// Hierarchical connections (each node connects to nodes in the next layer)
|
|
206
|
+
|
|
207
|
+
// Connect adjacent layers
|
|
205
208
|
for (let layer = 0; layer < numLayers - 1; layer++) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
edges.push([node1, nextLayer[targetIdx]]);
|
|
209
|
+
for (let n1 = 0; n1 < nodesPerLayer; n1++) {
|
|
210
|
+
for (let n2 = 0; n2 < nodesPerLayer; n2++) {
|
|
211
|
+
if (Math.random() < 0.3) {
|
|
212
|
+
edges.push([
|
|
213
|
+
`L${layer}N${n1}`,
|
|
214
|
+
`L${layer + 1}N${n2}`
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
215
217
|
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
break;
|
|
219
|
-
|
|
220
|
-
case 'complete':
|
|
221
|
-
// Each layer is fully connected to the next
|
|
222
|
-
for (let layer = 0; layer < numLayers - 1; layer++) {
|
|
223
|
-
const currentLayer = layers[layer];
|
|
224
|
-
const nextLayer = layers[layer + 1];
|
|
225
|
-
|
|
226
|
-
currentLayer.forEach(node1 => {
|
|
227
|
-
nextLayer.forEach(node2 => {
|
|
228
|
-
edges.push([node1, node2]);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
218
|
+
}
|
|
231
219
|
}
|
|
232
|
-
|
|
220
|
+
|
|
221
|
+
return { nodes: () => nodes, edges: () => edges };
|
|
233
222
|
}
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
nodes: () => allNodes,
|
|
237
|
-
edges: () => edges,
|
|
238
|
-
layers
|
|
239
|
-
};
|
|
240
223
|
}
|
|
241
224
|
|
|
242
225
|
function visualizeGraph(graph, positions) {
|
|
@@ -263,7 +246,8 @@
|
|
|
263
246
|
|
|
264
247
|
// Draw layer guidelines
|
|
265
248
|
const layerColors = ['#F44336', '#9C27B0', '#3F51B5', '#009688', '#FF9800'];
|
|
266
|
-
|
|
249
|
+
if (currentLayers) {
|
|
250
|
+
currentLayers.forEach((layerNodes, layerIdx) => {
|
|
267
251
|
const layerPositions = layerNodes.map(node => positions[node]);
|
|
268
252
|
|
|
269
253
|
if (layerPositions.length > 1) {
|
|
@@ -291,7 +275,8 @@
|
|
|
291
275
|
rect.setAttribute('stroke-dasharray', '5,5');
|
|
292
276
|
svg.appendChild(rect);
|
|
293
277
|
}
|
|
294
|
-
|
|
278
|
+
});
|
|
279
|
+
}
|
|
295
280
|
|
|
296
281
|
// Draw edges
|
|
297
282
|
const edges = graph.edges();
|
|
@@ -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>Planar Layout Test</title>
|
|
@@ -161,7 +173,7 @@
|
|
|
161
173
|
</div>
|
|
162
174
|
|
|
163
175
|
<script type="module">
|
|
164
|
-
import { planarLayout } from
|
|
176
|
+
import { planarLayout } from "./layout.js";
|
|
165
177
|
|
|
166
178
|
let currentGraph = null;
|
|
167
179
|
|
|
@@ -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>Random Layout Test</title>
|
|
@@ -142,7 +154,7 @@
|
|
|
142
154
|
</div>
|
|
143
155
|
|
|
144
156
|
<script type="module">
|
|
145
|
-
import { randomLayout } from
|
|
157
|
+
import { randomLayout } from "./layout.js";
|
|
146
158
|
|
|
147
159
|
let currentGraph = null;
|
|
148
160
|
|
|
@@ -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>Shell Layout Test</title>
|
|
@@ -117,10 +129,12 @@
|
|
|
117
129
|
<div class="control-group">
|
|
118
130
|
<label for="shell-pattern">Shell pattern:</label>
|
|
119
131
|
<select id="shell-pattern">
|
|
120
|
-
<option value="
|
|
121
|
-
<option value="
|
|
122
|
-
<option value="
|
|
123
|
-
<option value="
|
|
132
|
+
<option value="degree">By Degree (hubs in center)</option>
|
|
133
|
+
<option value="k-core">By K-Core (density)</option>
|
|
134
|
+
<option value="bfs">By Distance (BFS)</option>
|
|
135
|
+
<option value="community">By Community</option>
|
|
136
|
+
<option value="manual-center">Manual: Center + Ring</option>
|
|
137
|
+
<option value="manual-triple">Manual: Three Rings</option>
|
|
124
138
|
</select>
|
|
125
139
|
</div>
|
|
126
140
|
|
|
@@ -129,46 +143,64 @@
|
|
|
129
143
|
</div>
|
|
130
144
|
|
|
131
145
|
<script type="module">
|
|
132
|
-
import {
|
|
146
|
+
import {
|
|
147
|
+
shellLayout,
|
|
148
|
+
scaleFreeGraph,
|
|
149
|
+
randomGraph,
|
|
150
|
+
groupNodes,
|
|
151
|
+
findBestRoot
|
|
152
|
+
} from "./layout.js";
|
|
133
153
|
|
|
134
154
|
let currentGraph = null;
|
|
135
155
|
|
|
136
156
|
function generateGraph(numNodes) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
// Use scale-free graph for interesting shell patterns
|
|
158
|
+
if (numNodes < 50) {
|
|
159
|
+
return scaleFreeGraph(numNodes, 2, Date.now());
|
|
160
|
+
} else {
|
|
161
|
+
// For larger graphs, use random graph
|
|
162
|
+
return randomGraph(numNodes, 0.05, Date.now());
|
|
143
163
|
}
|
|
144
|
-
|
|
145
|
-
return { nodes: () => nodes, edges: () => edges };
|
|
146
164
|
}
|
|
147
165
|
|
|
148
|
-
function getShellConfiguration(pattern,
|
|
166
|
+
function getShellConfiguration(pattern, graph) {
|
|
167
|
+
const numNodes = graph.nodes().length;
|
|
168
|
+
|
|
149
169
|
switch(pattern) {
|
|
150
|
-
case '
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
case 'degree':
|
|
171
|
+
// Group by degree - hubs in center
|
|
172
|
+
return groupNodes(graph, 'degree', Math.min(4, Math.ceil(numNodes / 10)));
|
|
173
|
+
|
|
174
|
+
case 'k-core':
|
|
175
|
+
// Group by k-core decomposition
|
|
176
|
+
return groupNodes(graph, 'k-core');
|
|
177
|
+
|
|
178
|
+
case 'bfs':
|
|
179
|
+
// Group by distance from best root
|
|
180
|
+
const root = findBestRoot(graph);
|
|
181
|
+
return groupNodes(graph, 'bfs', 0, { root });
|
|
182
|
+
|
|
183
|
+
case 'community':
|
|
184
|
+
// Group by detected communities
|
|
185
|
+
return groupNodes(graph, 'community', Math.min(5, Math.ceil(numNodes / 20)));
|
|
186
|
+
|
|
187
|
+
case 'manual-center':
|
|
188
|
+
// Manual: center node + outer ring
|
|
189
|
+
const nodes = graph.nodes();
|
|
190
|
+
return [[nodes[0]], nodes.slice(1)];
|
|
191
|
+
|
|
192
|
+
case 'manual-triple':
|
|
193
|
+
// Manual: three equal rings
|
|
153
194
|
const third = Math.floor(numNodes / 3);
|
|
195
|
+
const allNodes = graph.nodes();
|
|
154
196
|
return [
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
197
|
+
allNodes.slice(0, third),
|
|
198
|
+
allNodes.slice(third, 2 * third),
|
|
199
|
+
allNodes.slice(2 * third)
|
|
158
200
|
];
|
|
159
|
-
|
|
160
|
-
const shells = [];
|
|
161
|
-
let remaining = numNodes;
|
|
162
|
-
let start = 0;
|
|
163
|
-
while(remaining > 0) {
|
|
164
|
-
const shellSize = Math.min(Math.ceil(remaining / 2), remaining);
|
|
165
|
-
shells.push(Array.from({length: shellSize}, (_, i) => i + start));
|
|
166
|
-
start += shellSize;
|
|
167
|
-
remaining -= shellSize;
|
|
168
|
-
}
|
|
169
|
-
return shells;
|
|
201
|
+
|
|
170
202
|
default:
|
|
171
|
-
return
|
|
203
|
+
return groupNodes(graph, 'degree', 3);
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
206
|
|
|
@@ -262,9 +294,8 @@
|
|
|
262
294
|
if(!currentGraph) return;
|
|
263
295
|
|
|
264
296
|
const pattern = document.getElementById('shell-pattern').value;
|
|
265
|
-
const numNodes = currentGraph.nodes().length;
|
|
266
297
|
|
|
267
|
-
const shells = getShellConfiguration(pattern,
|
|
298
|
+
const shells = getShellConfiguration(pattern, currentGraph);
|
|
268
299
|
const positions = shellLayout(currentGraph, shells, 1, [0, 0], 2);
|
|
269
300
|
|
|
270
301
|
visualizeGraph(currentGraph, positions);
|
|
@@ -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>Spectral Layout Test</title>
|
|
@@ -135,7 +147,7 @@
|
|
|
135
147
|
</div>
|
|
136
148
|
|
|
137
149
|
<script type="module">
|
|
138
|
-
import { spectralLayout } from
|
|
150
|
+
import { spectralLayout } from "./layout.js";
|
|
139
151
|
|
|
140
152
|
let currentGraph = null;
|
|
141
153
|
|
|
@@ -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>Spiral Layout Test</title>
|
|
@@ -171,7 +183,7 @@
|
|
|
171
183
|
</div>
|
|
172
184
|
|
|
173
185
|
<script type="module">
|
|
174
|
-
import { spiralLayout } from
|
|
186
|
+
import { spiralLayout } from "./layout.js";
|
|
175
187
|
|
|
176
188
|
let currentGraph = null;
|
|
177
189
|
|
|
@@ -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>Spring Layout Test</title>
|
|
@@ -141,7 +153,7 @@
|
|
|
141
153
|
</div>
|
|
142
154
|
|
|
143
155
|
<script type="module">
|
|
144
|
-
import { springLayout } from
|
|
156
|
+
import { springLayout } from "./layout.js";
|
|
145
157
|
|
|
146
158
|
let currentGraph = null;
|
|
147
159
|
|
|
@@ -274,7 +286,17 @@
|
|
|
274
286
|
if(!currentGraph) return;
|
|
275
287
|
|
|
276
288
|
const iterations = parseInt(document.getElementById('iterations').value);
|
|
277
|
-
const
|
|
289
|
+
const autoConfig = document.getElementById('auto-config').checked;
|
|
290
|
+
|
|
291
|
+
let k;
|
|
292
|
+
if (autoConfig && currentConfig) {
|
|
293
|
+
k = currentConfig.k;
|
|
294
|
+
// Update slider to show auto-configured value
|
|
295
|
+
document.getElementById('k-value').value = k;
|
|
296
|
+
document.getElementById('k-value-value').textContent = k.toFixed(2);
|
|
297
|
+
} else {
|
|
298
|
+
k = parseFloat(document.getElementById('k-value').value);
|
|
299
|
+
}
|
|
278
300
|
|
|
279
301
|
const positions = springLayout(currentGraph, k, null, null, iterations, 1, [0, 0], 2);
|
|
280
302
|
|
|
@@ -298,4 +320,4 @@
|
|
|
298
320
|
newGraph();
|
|
299
321
|
</script>
|
|
300
322
|
</body>
|
|
301
|
-
</html>
|
|
323
|
+
</html>
|