@graphty/layout 1.1.0 → 1.1.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/.github/workflows/ci.yml +89 -14
- package/.releaserc.json +22 -0
- package/CHANGELOG.md +24 -0
- package/CLAUDE.md +104 -0
- package/CONTRIBUTING.md +1 -0
- package/README.md +609 -93
- package/dist/layout-helpers.d.ts +123 -0
- package/dist/layout-helpers.js +457 -0
- package/dist/layout-helpers.js.map +1 -0
- package/dist/layout.d.ts +275 -0
- package/dist/layout.js +2280 -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/bfs-layout.html +37 -39
- package/examples/bipartite-layout.html +77 -69
- package/examples/circular-layout.html +13 -34
- package/examples/forceatlas2-layout.html +122 -28
- package/examples/multipartite-layout.html +64 -51
- package/examples/shell-layout.html +53 -34
- package/examples/spring-layout.html +11 -1
- package/layout-helpers.ts +559 -0
- package/layout.ts +277 -1
- package/package.json +17 -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 +300 -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 +351 -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/vitest.config.ts +30 -0
- package/.releaserc +0 -3
|
@@ -153,12 +153,17 @@
|
|
|
153
153
|
</div>
|
|
154
154
|
|
|
155
155
|
<div class="control-group">
|
|
156
|
+
<label for="auto-configure">Auto-configure parameters:</label>
|
|
157
|
+
<input type="checkbox" id="auto-configure" checked>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="control-group" id="scaling-group">
|
|
156
161
|
<label for="scaling-ratio">Scaling Ratio:</label>
|
|
157
162
|
<input type="range" id="scaling-ratio" min="0.5" max="10" step="0.5" value="2">
|
|
158
163
|
<span id="scaling-ratio-value">2.0</span>
|
|
159
164
|
</div>
|
|
160
165
|
|
|
161
|
-
<div class="control-group">
|
|
166
|
+
<div class="control-group" id="gravity-group">
|
|
162
167
|
<label for="gravity">Gravity:</label>
|
|
163
168
|
<input type="range" id="gravity" min="0" max="5" step="0.1" value="1">
|
|
164
169
|
<span id="gravity-value">1.0</span>
|
|
@@ -185,19 +190,67 @@
|
|
|
185
190
|
</div>
|
|
186
191
|
|
|
187
192
|
<script type="module">
|
|
188
|
-
import {
|
|
193
|
+
import {
|
|
194
|
+
forceatlas2Layout,
|
|
195
|
+
randomGraph,
|
|
196
|
+
scaleFreeGraph,
|
|
197
|
+
gridGraph,
|
|
198
|
+
autoConfigureForce
|
|
199
|
+
} from '../dist/layout.js';
|
|
189
200
|
|
|
190
201
|
let currentGraph = null;
|
|
191
|
-
let
|
|
202
|
+
let currentConfig = null;
|
|
192
203
|
|
|
193
204
|
function generateGraph(type, numNodes) {
|
|
194
|
-
const nodes = Array.from({length: numNodes}, (_, i) => i);
|
|
195
|
-
const edges = [];
|
|
196
|
-
|
|
197
205
|
switch(type) {
|
|
198
206
|
case 'scale-free':
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
return scaleFreeGraph(numNodes, 2, Date.now());
|
|
208
|
+
|
|
209
|
+
case 'community':
|
|
210
|
+
// Use multiple smaller scale-free graphs
|
|
211
|
+
const numCommunities = Math.ceil(numNodes / 10);
|
|
212
|
+
const nodesPerCommunity = Math.floor(numNodes / numCommunities);
|
|
213
|
+
const nodes = [];
|
|
214
|
+
const edges = [];
|
|
215
|
+
|
|
216
|
+
// Create communities
|
|
217
|
+
for (let c = 0; c < numCommunities; c++) {
|
|
218
|
+
const start = c * nodesPerCommunity;
|
|
219
|
+
const end = Math.min(start + nodesPerCommunity, numNodes);
|
|
220
|
+
|
|
221
|
+
// Dense connections within community
|
|
222
|
+
for (let i = start; i < end; i++) {
|
|
223
|
+
nodes.push(i);
|
|
224
|
+
for (let j = i + 1; j < end; j++) {
|
|
225
|
+
if (Math.random() < 0.6) {
|
|
226
|
+
edges.push([i, j]);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Sparse connections between communities
|
|
232
|
+
if (c > 0) {
|
|
233
|
+
const prevStart = (c - 1) * nodesPerCommunity;
|
|
234
|
+
const prevEnd = Math.min(prevStart + nodesPerCommunity, numNodes);
|
|
235
|
+
for (let i = start; i < end && i < start + 2; i++) {
|
|
236
|
+
for (let j = prevStart; j < prevEnd && j < prevStart + 2; j++) {
|
|
237
|
+
if (Math.random() < 0.1) {
|
|
238
|
+
edges.push([i, j]);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { nodes: () => nodes, edges: () => edges };
|
|
246
|
+
|
|
247
|
+
case 'grid':
|
|
248
|
+
const size = Math.ceil(Math.sqrt(numNodes));
|
|
249
|
+
return gridGraph(size, size);
|
|
250
|
+
|
|
251
|
+
case 'random':
|
|
252
|
+
default:
|
|
253
|
+
return randomGraph(numNodes, 0.1, Date.now());
|
|
201
254
|
for(let i = 1; i < Math.min(5, numNodes); i++) {
|
|
202
255
|
edges.push([0, i]);
|
|
203
256
|
}
|
|
@@ -394,9 +447,10 @@
|
|
|
394
447
|
const x = margin + (pos[0] - minX) / rangeX * scaleX;
|
|
395
448
|
const y = margin + (pos[1] - minY) / rangeY * scaleY;
|
|
396
449
|
|
|
397
|
-
// Node size based on degree
|
|
398
|
-
const
|
|
399
|
-
const
|
|
450
|
+
// Node size based on degree
|
|
451
|
+
const edges = graph.edges();
|
|
452
|
+
const degree = edges.filter(([u, v]) => u === node || v === node).length;
|
|
453
|
+
const radius = 4 + Math.sqrt(degree) * 1.5;
|
|
400
454
|
|
|
401
455
|
// Color based on position/edge distribution
|
|
402
456
|
const hue = (node * 360 / nodes.length) % 360;
|
|
@@ -430,20 +484,24 @@
|
|
|
430
484
|
const graphType = document.getElementById('graph-type').value;
|
|
431
485
|
|
|
432
486
|
try {
|
|
433
|
-
// Generate new graph
|
|
487
|
+
// Generate new graph
|
|
434
488
|
currentGraph = generateGraph(graphType, numNodes);
|
|
435
|
-
currentSeed = Math.floor(Math.random() * 1000);
|
|
436
489
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
490
|
+
// Auto-configure parameters
|
|
491
|
+
currentConfig = autoConfigureForce(currentGraph);
|
|
492
|
+
|
|
493
|
+
// Update controls with recommended values
|
|
494
|
+
if (document.getElementById('auto-configure').checked) {
|
|
495
|
+
document.getElementById('iterations').value = currentConfig.iterations;
|
|
496
|
+
document.getElementById('iterations-value').textContent = currentConfig.iterations;
|
|
497
|
+
document.getElementById('scaling-ratio').value = currentConfig.scalingRatio;
|
|
498
|
+
document.getElementById('scaling-ratio-value').textContent = currentConfig.scalingRatio.toFixed(1);
|
|
499
|
+
document.getElementById('gravity').value = currentConfig.gravity;
|
|
500
|
+
document.getElementById('gravity-value').textContent = currentConfig.gravity.toFixed(1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Apply layout immediately
|
|
504
|
+
applyLayout();
|
|
447
505
|
});
|
|
448
506
|
|
|
449
507
|
visualizeGraph(currentGraph, positions);
|
|
@@ -470,9 +528,27 @@
|
|
|
470
528
|
};
|
|
471
529
|
|
|
472
530
|
async function runForceAtlas2() {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
531
|
+
const autoConfig = document.getElementById('auto-configure').checked;
|
|
532
|
+
|
|
533
|
+
let iterations, scalingRatio, gravity;
|
|
534
|
+
if (autoConfig && currentConfig) {
|
|
535
|
+
iterations = currentConfig.iterations;
|
|
536
|
+
scalingRatio = currentConfig.scalingRatio;
|
|
537
|
+
gravity = currentConfig.gravity;
|
|
538
|
+
|
|
539
|
+
// Update sliders to show auto values
|
|
540
|
+
document.getElementById('iterations').value = iterations;
|
|
541
|
+
document.getElementById('iterations-value').textContent = iterations;
|
|
542
|
+
document.getElementById('scaling-ratio').value = scalingRatio;
|
|
543
|
+
document.getElementById('scaling-ratio-value').textContent = scalingRatio.toFixed(1);
|
|
544
|
+
document.getElementById('gravity').value = gravity;
|
|
545
|
+
document.getElementById('gravity-value').textContent = gravity.toFixed(1);
|
|
546
|
+
} else {
|
|
547
|
+
iterations = parseInt(document.getElementById('iterations').value);
|
|
548
|
+
scalingRatio = parseFloat(document.getElementById('scaling-ratio').value);
|
|
549
|
+
gravity = parseFloat(document.getElementById('gravity').value);
|
|
550
|
+
}
|
|
551
|
+
|
|
476
552
|
const strongGravity = document.getElementById('strong-gravity').checked;
|
|
477
553
|
const dissuadeHubs = document.getElementById('dissuade-hubs').checked;
|
|
478
554
|
const linlog = document.getElementById('linlog').checked;
|
|
@@ -489,12 +565,12 @@
|
|
|
489
565
|
gravity,
|
|
490
566
|
false, // distributedAction
|
|
491
567
|
strongGravity,
|
|
492
|
-
|
|
568
|
+
null, // nodeMass
|
|
493
569
|
null, // nodeSize
|
|
494
570
|
null, // weight
|
|
495
571
|
dissuadeHubs,
|
|
496
572
|
linlog,
|
|
497
|
-
|
|
573
|
+
Date.now()
|
|
498
574
|
);
|
|
499
575
|
|
|
500
576
|
visualizeGraph(currentGraph, positions);
|
|
@@ -510,6 +586,24 @@
|
|
|
510
586
|
};
|
|
511
587
|
});
|
|
512
588
|
|
|
589
|
+
// Auto-config checkbox handler
|
|
590
|
+
document.getElementById('auto-configure').onchange = function() {
|
|
591
|
+
const groups = ['scaling-group', 'gravity-group'];
|
|
592
|
+
groups.forEach(groupId => {
|
|
593
|
+
const group = document.getElementById(groupId);
|
|
594
|
+
group.style.opacity = this.checked ? '0.5' : '1';
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (this.checked && currentConfig) {
|
|
598
|
+
document.getElementById('iterations').value = currentConfig.iterations;
|
|
599
|
+
document.getElementById('iterations-value').textContent = currentConfig.iterations;
|
|
600
|
+
document.getElementById('scaling-ratio').value = currentConfig.scalingRatio;
|
|
601
|
+
document.getElementById('scaling-ratio-value').textContent = currentConfig.scalingRatio.toFixed(1);
|
|
602
|
+
document.getElementById('gravity').value = currentConfig.gravity;
|
|
603
|
+
document.getElementById('gravity-value').textContent = currentConfig.gravity.toFixed(1);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
513
607
|
// Remove auto-update on change for these controls
|
|
514
608
|
document.getElementById('graph-type').onchange = null;
|
|
515
609
|
|
|
@@ -114,12 +114,11 @@
|
|
|
114
114
|
</div>
|
|
115
115
|
|
|
116
116
|
<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>
|
|
117
|
+
<label for="graph-type">Graph type:</label>
|
|
118
|
+
<select id="graph-type">
|
|
119
|
+
<option value="scale-free">Scale-Free (automatic layers)</option>
|
|
120
|
+
<option value="tree">Tree-like</option>
|
|
121
|
+
<option value="custom-layers">Custom Layers</option>
|
|
123
122
|
</select>
|
|
124
123
|
</div>
|
|
125
124
|
|
|
@@ -141,63 +140,75 @@
|
|
|
141
140
|
</select>
|
|
142
141
|
</div>
|
|
143
142
|
|
|
143
|
+
<div class="control-group">
|
|
144
|
+
<label for="layer-method">Layer assignment:</label>
|
|
145
|
+
<select id="layer-method">
|
|
146
|
+
<option value="bfs">BFS (distance-based)</option>
|
|
147
|
+
<option value="degree">Degree-based</option>
|
|
148
|
+
<option value="community">Community-based</option>
|
|
149
|
+
</select>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
144
152
|
<button onclick="applyLayout()">Apply Layout</button>
|
|
145
153
|
<div id="layers-info" class="layers-info"></div>
|
|
146
154
|
</div>
|
|
147
155
|
</div>
|
|
148
156
|
|
|
149
157
|
<script type="module">
|
|
150
|
-
import {
|
|
158
|
+
import {
|
|
159
|
+
multipartiteLayout,
|
|
160
|
+
scaleFreeGraph,
|
|
161
|
+
starGraph,
|
|
162
|
+
randomGraph,
|
|
163
|
+
groupNodes,
|
|
164
|
+
findBestRoot
|
|
165
|
+
} from '../dist/layout.js';
|
|
151
166
|
|
|
152
167
|
let currentGraph = null;
|
|
168
|
+
let currentLayers = null;
|
|
153
169
|
|
|
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
|
-
}
|
|
168
|
-
|
|
169
|
-
const edges = [];
|
|
170
|
+
function generateGraph(type, numLayers, nodesPerLayer) {
|
|
171
|
+
const totalNodes = numLayers * nodesPerLayer;
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
switch(type) {
|
|
174
|
+
case 'scale-free':
|
|
175
|
+
// Generate scale-free network
|
|
176
|
+
return scaleFreeGraph(totalNodes, 2, Date.now());
|
|
177
|
+
|
|
178
|
+
case 'tree':
|
|
179
|
+
// Generate tree-like structure
|
|
180
|
+
return starGraph(totalNodes);
|
|
181
|
+
|
|
182
|
+
case 'custom-layers':
|
|
183
|
+
default:
|
|
184
|
+
// Generate custom layered graph
|
|
185
|
+
const nodes = [];
|
|
186
|
+
const edges = [];
|
|
187
|
+
|
|
188
|
+
// Create nodes
|
|
189
|
+
for (let layer = 0; layer < numLayers; layer++) {
|
|
190
|
+
for (let node = 0; node < nodesPerLayer; node++) {
|
|
191
|
+
nodes.push(`L${layer}N${node}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Connect adjacent layers
|
|
175
196
|
for (let layer = 0; layer < numLayers - 1; layer++) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
197
|
+
for (let n1 = 0; n1 < nodesPerLayer; n1++) {
|
|
198
|
+
for (let n2 = 0; n2 < nodesPerLayer; n2++) {
|
|
199
|
+
if (Math.random() < 0.3) {
|
|
200
|
+
edges.push([
|
|
201
|
+
`L${layer}N${n1}`,
|
|
202
|
+
`L${layer + 1}N${n2}`
|
|
203
|
+
]);
|
|
183
204
|
}
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
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]);
|
|
205
|
+
}
|
|
199
206
|
}
|
|
200
207
|
}
|
|
208
|
+
|
|
209
|
+
return { nodes: () => nodes, edges: () => edges };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
201
212
|
break;
|
|
202
213
|
|
|
203
214
|
case 'hierarchical':
|
|
@@ -263,7 +274,8 @@
|
|
|
263
274
|
|
|
264
275
|
// Draw layer guidelines
|
|
265
276
|
const layerColors = ['#F44336', '#9C27B0', '#3F51B5', '#009688', '#FF9800'];
|
|
266
|
-
|
|
277
|
+
if (currentLayers) {
|
|
278
|
+
currentLayers.forEach((layerNodes, layerIdx) => {
|
|
267
279
|
const layerPositions = layerNodes.map(node => positions[node]);
|
|
268
280
|
|
|
269
281
|
if (layerPositions.length > 1) {
|
|
@@ -291,7 +303,8 @@
|
|
|
291
303
|
rect.setAttribute('stroke-dasharray', '5,5');
|
|
292
304
|
svg.appendChild(rect);
|
|
293
305
|
}
|
|
294
|
-
|
|
306
|
+
});
|
|
307
|
+
}
|
|
295
308
|
|
|
296
309
|
// Draw edges
|
|
297
310
|
const edges = graph.edges();
|
|
@@ -117,10 +117,12 @@
|
|
|
117
117
|
<div class="control-group">
|
|
118
118
|
<label for="shell-pattern">Shell pattern:</label>
|
|
119
119
|
<select id="shell-pattern">
|
|
120
|
-
<option value="
|
|
121
|
-
<option value="
|
|
122
|
-
<option value="
|
|
123
|
-
<option value="
|
|
120
|
+
<option value="degree">By Degree (hubs in center)</option>
|
|
121
|
+
<option value="k-core">By K-Core (density)</option>
|
|
122
|
+
<option value="bfs">By Distance (BFS)</option>
|
|
123
|
+
<option value="community">By Community</option>
|
|
124
|
+
<option value="manual-center">Manual: Center + Ring</option>
|
|
125
|
+
<option value="manual-triple">Manual: Three Rings</option>
|
|
124
126
|
</select>
|
|
125
127
|
</div>
|
|
126
128
|
|
|
@@ -129,46 +131,64 @@
|
|
|
129
131
|
</div>
|
|
130
132
|
|
|
131
133
|
<script type="module">
|
|
132
|
-
import {
|
|
134
|
+
import {
|
|
135
|
+
shellLayout,
|
|
136
|
+
scaleFreeGraph,
|
|
137
|
+
randomGraph,
|
|
138
|
+
groupNodes,
|
|
139
|
+
findBestRoot
|
|
140
|
+
} from '../dist/layout.js';
|
|
133
141
|
|
|
134
142
|
let currentGraph = null;
|
|
135
143
|
|
|
136
144
|
function generateGraph(numNodes) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
// Use scale-free graph for interesting shell patterns
|
|
146
|
+
if (numNodes < 50) {
|
|
147
|
+
return scaleFreeGraph(numNodes, 2, Date.now());
|
|
148
|
+
} else {
|
|
149
|
+
// For larger graphs, use random graph
|
|
150
|
+
return randomGraph(numNodes, 0.05, Date.now());
|
|
143
151
|
}
|
|
144
|
-
|
|
145
|
-
return { nodes: () => nodes, edges: () => edges };
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
function getShellConfiguration(pattern,
|
|
154
|
+
function getShellConfiguration(pattern, graph) {
|
|
155
|
+
const numNodes = graph.nodes().length;
|
|
156
|
+
|
|
149
157
|
switch(pattern) {
|
|
150
|
-
case '
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
case 'degree':
|
|
159
|
+
// Group by degree - hubs in center
|
|
160
|
+
return groupNodes(graph, 'degree', Math.min(4, Math.ceil(numNodes / 10)));
|
|
161
|
+
|
|
162
|
+
case 'k-core':
|
|
163
|
+
// Group by k-core decomposition
|
|
164
|
+
return groupNodes(graph, 'k-core');
|
|
165
|
+
|
|
166
|
+
case 'bfs':
|
|
167
|
+
// Group by distance from best root
|
|
168
|
+
const root = findBestRoot(graph);
|
|
169
|
+
return groupNodes(graph, 'bfs', 0, { root });
|
|
170
|
+
|
|
171
|
+
case 'community':
|
|
172
|
+
// Group by detected communities
|
|
173
|
+
return groupNodes(graph, 'community', Math.min(5, Math.ceil(numNodes / 20)));
|
|
174
|
+
|
|
175
|
+
case 'manual-center':
|
|
176
|
+
// Manual: center node + outer ring
|
|
177
|
+
const nodes = graph.nodes();
|
|
178
|
+
return [[nodes[0]], nodes.slice(1)];
|
|
179
|
+
|
|
180
|
+
case 'manual-triple':
|
|
181
|
+
// Manual: three equal rings
|
|
153
182
|
const third = Math.floor(numNodes / 3);
|
|
183
|
+
const allNodes = graph.nodes();
|
|
154
184
|
return [
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
allNodes.slice(0, third),
|
|
186
|
+
allNodes.slice(third, 2 * third),
|
|
187
|
+
allNodes.slice(2 * third)
|
|
158
188
|
];
|
|
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;
|
|
189
|
+
|
|
170
190
|
default:
|
|
171
|
-
return
|
|
191
|
+
return groupNodes(graph, 'degree', 3);
|
|
172
192
|
}
|
|
173
193
|
}
|
|
174
194
|
|
|
@@ -262,9 +282,8 @@
|
|
|
262
282
|
if(!currentGraph) return;
|
|
263
283
|
|
|
264
284
|
const pattern = document.getElementById('shell-pattern').value;
|
|
265
|
-
const numNodes = currentGraph.nodes().length;
|
|
266
285
|
|
|
267
|
-
const shells = getShellConfiguration(pattern,
|
|
286
|
+
const shells = getShellConfiguration(pattern, currentGraph);
|
|
268
287
|
const positions = shellLayout(currentGraph, shells, 1, [0, 0], 2);
|
|
269
288
|
|
|
270
289
|
visualizeGraph(currentGraph, positions);
|
|
@@ -274,7 +274,17 @@
|
|
|
274
274
|
if(!currentGraph) return;
|
|
275
275
|
|
|
276
276
|
const iterations = parseInt(document.getElementById('iterations').value);
|
|
277
|
-
const
|
|
277
|
+
const autoConfig = document.getElementById('auto-config').checked;
|
|
278
|
+
|
|
279
|
+
let k;
|
|
280
|
+
if (autoConfig && currentConfig) {
|
|
281
|
+
k = currentConfig.k;
|
|
282
|
+
// Update slider to show auto-configured value
|
|
283
|
+
document.getElementById('k-value').value = k;
|
|
284
|
+
document.getElementById('k-value-value').textContent = k.toFixed(2);
|
|
285
|
+
} else {
|
|
286
|
+
k = parseFloat(document.getElementById('k-value').value);
|
|
287
|
+
}
|
|
278
288
|
|
|
279
289
|
const positions = springLayout(currentGraph, k, null, null, iterations, 1, [0, 0], 2);
|
|
280
290
|
|