@graphty/layout 1.0.1 → 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.
Files changed (51) hide show
  1. package/.github/workflows/ci.yml +105 -0
  2. package/.releaserc.json +22 -0
  3. package/CHANGELOG.md +24 -0
  4. package/CLAUDE.md +104 -0
  5. package/CONTRIBUTING.md +1 -0
  6. package/README.md +893 -34
  7. package/dist/layout-helpers.d.ts +123 -0
  8. package/dist/layout-helpers.js +457 -0
  9. package/dist/layout-helpers.js.map +1 -0
  10. package/dist/layout.d.ts +275 -0
  11. package/dist/layout.js +2280 -0
  12. package/dist/layout.js.map +1 -0
  13. package/dist/vitest.config.d.ts +2 -0
  14. package/dist/vitest.config.js +30 -0
  15. package/dist/vitest.config.js.map +1 -0
  16. package/examples/arf-layout.html +1 -1
  17. package/examples/bfs-layout.html +37 -39
  18. package/examples/bipartite-layout.html +77 -69
  19. package/examples/circular-layout.html +13 -34
  20. package/examples/forceatlas2-layout.html +122 -28
  21. package/examples/kamada-kawai-layout.html +1 -1
  22. package/examples/multipartite-layout.html +64 -51
  23. package/examples/planar-layout.html +1 -1
  24. package/examples/random-layout.html +1 -1
  25. package/examples/shell-layout.html +53 -34
  26. package/examples/spectral-layout.html +1 -1
  27. package/examples/spiral-layout.html +1 -1
  28. package/examples/spring-layout.html +12 -2
  29. package/layout-helpers.ts +559 -0
  30. package/{layout.js → layout.ts} +1261 -771
  31. package/package.json +22 -6
  32. package/test/arf-layout.test.ts +443 -0
  33. package/test/bfs-layout.test.ts +427 -0
  34. package/test/bipartite-layout.test.ts +344 -0
  35. package/test/circular-layout.test.ts +300 -0
  36. package/test/forceatlas2-layout.test.ts +405 -0
  37. package/test/fruchterman-reingold-layout.test.ts +477 -0
  38. package/test/graph-generators.test.ts +450 -0
  39. package/test/kamada-kawai-layout.test.ts +351 -0
  40. package/test/multipartite-layout.test.ts +404 -0
  41. package/test/planar-layout.test.ts +266 -0
  42. package/test/random-layout.test.ts +254 -0
  43. package/test/rescale-layout.test.ts +373 -0
  44. package/test/shell-layout.test.ts +347 -0
  45. package/test/spectral-layout.test.ts +378 -0
  46. package/test/spiral-layout.test.ts +338 -0
  47. package/test/spring-layout.test.ts +241 -0
  48. package/tsconfig.json +16 -0
  49. package/vitest.config.ts +30 -0
  50. package/.husky/commit-msg +0 -1
  51. package/.husky/prepare-commit-msg +0 -1
@@ -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 { forceatlas2Layout } from '../layout.js';
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 currentSeed = Math.floor(Math.random() * 1000);
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
- // Barabási–Albert model (preferential attachment)
200
- // Start with a small connected graph
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/mass
398
- const mass = graph.masses[node] || 1;
399
- const radius = 5 + Math.sqrt(mass) * 1.5;
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 and re-seed
487
+ // Generate new graph
434
488
  currentGraph = generateGraph(graphType, numNodes);
435
- currentSeed = Math.floor(Math.random() * 1000);
436
489
 
437
- // Just visualize without layout initially
438
- const positions = {};
439
- currentGraph.nodes().forEach(node => {
440
- // Random positions in a circle
441
- const angle = Math.random() * Math.PI * 2;
442
- const radius = Math.random() * 0.5;
443
- positions[node] = [
444
- Math.cos(angle) * radius,
445
- Math.sin(angle) * radius
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 iterations = parseInt(document.getElementById('iterations').value);
474
- const scalingRatio = parseFloat(document.getElementById('scaling-ratio').value);
475
- const gravity = parseFloat(document.getElementById('gravity').value);
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
- currentGraph.masses, // nodeMass
568
+ null, // nodeMass
493
569
  null, // nodeSize
494
570
  null, // weight
495
571
  dissuadeHubs,
496
572
  linlog,
497
- currentSeed
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
 
@@ -157,7 +157,7 @@
157
157
  </div>
158
158
 
159
159
  <script type="module">
160
- import { kamadaKawaiLayout } from '../layout.js';
160
+ import { kamadaKawaiLayout } from '../dist/layout.js';
161
161
 
162
162
  let currentGraph = null;
163
163
  let distances = null;
@@ -114,12 +114,11 @@
114
114
  </div>
115
115
 
116
116
  <div class="control-group">
117
- <label for="edge-type">Connection type:</label>
118
- <select id="edge-type">
119
- <option value="adjacent">Adjacent layers</option>
120
- <option value="random">Random between layers</option>
121
- <option value="hierarchical">Hierarchical</option>
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 { multipartiteLayout } from '../layout.js';
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 generateMultipartiteGraph(numLayers, nodesPerLayer, edgeType) {
155
- const layers = {};
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
- // Generate edges based on type
172
- switch(edgeType) {
173
- case 'adjacent':
174
- // Connect only adjacent layers
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
- const currentLayer = layers[layer];
177
- const nextLayer = layers[layer + 1];
178
-
179
- currentLayer.forEach(node1 => {
180
- nextLayer.forEach(node2 => {
181
- if (Math.random() < 0.4) { // 40% probability
182
- edges.push([node1, node2]);
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
- Object.entries(graph.layers).forEach(([layerIdx, layerNodes]) => {
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();
@@ -161,7 +161,7 @@
161
161
  </div>
162
162
 
163
163
  <script type="module">
164
- import { planarLayout } from '../layout.js';
164
+ import { planarLayout } from '../dist/layout.js';
165
165
 
166
166
  let currentGraph = null;
167
167
 
@@ -142,7 +142,7 @@
142
142
  </div>
143
143
 
144
144
  <script type="module">
145
- import { randomLayout } from '../layout.js';
145
+ import { randomLayout } from '../dist/layout.js';
146
146
 
147
147
  let currentGraph = null;
148
148
 
@@ -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="auto">Automatic</option>
121
- <option value="centered">Center + Ring</option>
122
- <option value="triple">Three Rings</option>
123
- <option value="nested">Nested Rings</option>
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 { shellLayout } from '../layout.js';
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
- const nodes = Array.from({length: numNodes}, (_, i) => i);
138
- const edges = [];
139
-
140
- // Generate simple connected graph
141
- for(let i = 1; i < numNodes; i++) {
142
- edges.push([Math.floor(Math.random() * i), i]);
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, numNodes) {
154
+ function getShellConfiguration(pattern, graph) {
155
+ const numNodes = graph.nodes().length;
156
+
149
157
  switch(pattern) {
150
- case 'centered':
151
- return [[0], Array.from({length: numNodes - 1}, (_, i) => i + 1)];
152
- case 'triple':
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
- Array.from({length: third}, (_, i) => i),
156
- Array.from({length: third}, (_, i) => i + third),
157
- Array.from({length: numNodes - 2 * third}, (_, i) => i + 2 * third)
185
+ allNodes.slice(0, third),
186
+ allNodes.slice(third, 2 * third),
187
+ allNodes.slice(2 * third)
158
188
  ];
159
- case 'nested':
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 null; // Auto-configuration
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, numNodes);
286
+ const shells = getShellConfiguration(pattern, currentGraph);
268
287
  const positions = shellLayout(currentGraph, shells, 1, [0, 0], 2);
269
288
 
270
289
  visualizeGraph(currentGraph, positions);
@@ -135,7 +135,7 @@
135
135
  </div>
136
136
 
137
137
  <script type="module">
138
- import { spectralLayout } from '../layout.js';
138
+ import { spectralLayout } from '../dist/layout.js';
139
139
 
140
140
  let currentGraph = null;
141
141
 
@@ -171,7 +171,7 @@
171
171
  </div>
172
172
 
173
173
  <script type="module">
174
- import { spiralLayout } from '../layout.js';
174
+ import { spiralLayout } from '../dist/layout.js';
175
175
 
176
176
  let currentGraph = null;
177
177
 
@@ -141,7 +141,7 @@
141
141
  </div>
142
142
 
143
143
  <script type="module">
144
- import { springLayout } from '../layout.js';
144
+ import { springLayout } from '../dist/layout.js';
145
145
 
146
146
  let currentGraph = null;
147
147
 
@@ -274,7 +274,17 @@
274
274
  if(!currentGraph) return;
275
275
 
276
276
  const iterations = parseInt(document.getElementById('iterations').value);
277
- const k = parseFloat(document.getElementById('k-value').value);
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