@graphty/layout 1.2.0 → 1.2.2
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 +3 -7
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/vitest.config.js +2 -2
- package/dist/vitest.config.js.map +1 -1
- package/examples/3d-kamada-kawai.html +4 -19
- package/examples/bfs-layout.html +26 -32
- package/examples/bipartite-layout.html +3 -3
- package/examples/forceatlas2-layout.html +106 -151
- package/{dist → examples}/layout-helpers.js +58 -30
- package/examples/multipartite-layout.html +32 -14
- package/examples/shell-layout.html +4 -2
- package/examples/spring-layout.html +1 -11
- package/package.json +3 -3
- package/src/algorithms/index.ts +6 -0
- package/src/algorithms/optimization/index.ts +12 -0
- package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
- package/src/algorithms/optimization/lbfgs.ts +68 -0
- package/src/algorithms/optimization/line-search.ts +50 -0
- package/src/algorithms/optimization/types.ts +8 -0
- package/src/algorithms/planarity/check.ts +38 -0
- package/src/algorithms/planarity/embedding.ts +216 -0
- package/src/algorithms/planarity/index.ts +12 -0
- package/src/algorithms/planarity/lr-test.ts +70 -0
- package/src/algorithms/planarity/special-graphs.ts +126 -0
- package/src/generators/basic.ts +93 -0
- package/src/generators/bipartite.ts +45 -0
- package/src/generators/grid.ts +42 -0
- package/src/generators/index.ts +15 -0
- package/src/generators/random.ts +39 -0
- package/src/generators/scale-free.ts +72 -0
- package/src/index.ts +18 -0
- package/src/layouts/basic/index.ts +5 -0
- package/src/layouts/basic/random.ts +32 -0
- package/src/layouts/force-directed/arf.ts +130 -0
- package/src/layouts/force-directed/forceatlas2.ts +407 -0
- package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
- package/src/layouts/force-directed/index.ts +9 -0
- package/src/layouts/force-directed/kamada-kawai.ts +112 -0
- package/src/layouts/force-directed/spring.ts +35 -0
- package/src/layouts/geometric/circular.ts +77 -0
- package/src/layouts/geometric/index.ts +7 -0
- package/src/layouts/geometric/shell.ts +80 -0
- package/src/layouts/geometric/spiral.ts +93 -0
- package/src/layouts/hierarchical/bfs.ts +81 -0
- package/src/layouts/hierarchical/bipartite.ts +94 -0
- package/src/layouts/hierarchical/index.ts +7 -0
- package/src/layouts/hierarchical/multipartite.ts +88 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/specialized/index.ts +6 -0
- package/src/layouts/specialized/planar.ts +65 -0
- package/src/layouts/specialized/spectral.ts +128 -0
- package/src/types/embedding.ts +11 -0
- package/src/types/graph.ts +13 -0
- package/src/types/index.ts +7 -0
- package/src/types/layout.ts +9 -0
- package/src/utils/graph.ts +77 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/numpy.ts +111 -0
- package/src/utils/params.ts +26 -0
- package/src/utils/random.ts +53 -0
- package/src/utils/rescale.ts +137 -0
- package/test/arf-layout.test.ts +1 -1
- package/test/bfs-layout.test.ts +1 -1
- package/test/bipartite-layout.test.ts +1 -1
- package/test/circular-layout.test.ts +1 -1
- package/test/forceatlas2-layout.test.ts +1 -1
- package/test/fruchterman-reingold-layout.test.ts +1 -1
- package/test/graph-generators.test.ts +1 -1
- package/test/kamada-kawai-layout.test.ts +1 -1
- package/test/multipartite-layout.test.ts +1 -1
- package/test/planar-layout.test.ts +1 -1
- package/test/random-layout.test.ts +1 -1
- package/test/rescale-layout.test.ts +1 -1
- package/test/shell-layout.test.ts +1 -1
- package/test/spectral-layout.test.ts +1 -1
- package/test/spiral-layout.test.ts +1 -1
- package/test/spring-layout.test.ts +1 -1
- package/test/test-utils.ts +34 -0
- package/test/utils-graph.test.ts +155 -0
- package/test/utils-index.test.ts +77 -0
- package/test/utils-numpy.test.ts +272 -0
- package/test/utils-params.test.ts +99 -0
- package/test/utils-random.test.ts +229 -0
- package/vitest.config.ts +2 -2
- package/dist/layout-helpers.d.ts +0 -123
- package/dist/layout-helpers.js.map +0 -1
- package/dist/layout.d.ts +0 -275
- package/dist/layout.js +0 -2304
- package/dist/layout.js.map +0 -1
- package/layout-helpers.ts +0 -560
- package/layout.ts +0 -2893
package/.github/workflows/ci.yml
CHANGED
|
@@ -38,16 +38,12 @@ jobs:
|
|
|
38
38
|
run: npm run test:coverage
|
|
39
39
|
if: matrix.node-version == '20.x'
|
|
40
40
|
|
|
41
|
-
- name: Upload coverage reports to
|
|
42
|
-
uses:
|
|
41
|
+
- name: Upload coverage reports to Coveralls
|
|
42
|
+
uses: coverallsapp/github-action@v2
|
|
43
43
|
if: matrix.node-version == '20.x'
|
|
44
44
|
with:
|
|
45
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
45
46
|
file: ./coverage/lcov.info
|
|
46
|
-
flags: unittests
|
|
47
|
-
name: codecov-umbrella
|
|
48
|
-
fail_ci_if_error: false
|
|
49
|
-
env:
|
|
50
|
-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
51
47
|
|
|
52
48
|
lint:
|
|
53
49
|
name: Lint
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.2.2](https://github.com/graphty-org/layout/compare/v1.2.1...v1.2.2) (2025-07-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* numpy linspace error. improve test coverage ([2e820f1](https://github.com/graphty-org/layout/commit/2e820f181bd7242ef251c1833b10533699485209))
|
|
7
|
+
|
|
8
|
+
## [1.2.1](https://github.com/graphty-org/layout/compare/v1.2.0...v1.2.1) (2025-07-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* resolve import errors and browser hang in shell layout example ([d65e436](https://github.com/graphty-org/layout/commit/d65e4360d9c42c885731ec952ea52978da61c7d5))
|
|
14
|
+
|
|
1
15
|
# [1.2.0](https://github.com/graphty-org/layout/compare/v1.1.1...v1.2.0) (2025-07-12)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Layout
|
|
2
2
|
|
|
3
3
|
[](https://github.com/graphty-org/layout/actions/workflows/ci.yml)
|
|
4
|
-
[](https://coveralls.io/github/graphty-org/layout?branch=main)
|
|
5
5
|
[](https://badge.fury.io/js/%40graphty%2Flayout)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
package/dist/vitest.config.js
CHANGED
|
@@ -7,8 +7,8 @@ export default defineConfig({
|
|
|
7
7
|
testTimeout: 30000,
|
|
8
8
|
coverage: {
|
|
9
9
|
provider: 'v8',
|
|
10
|
-
reporter: ['text', 'json', 'html'],
|
|
11
|
-
include: ['
|
|
10
|
+
reporter: ['text', 'json', 'html', 'lcov'],
|
|
11
|
+
include: ['src/**/*.ts'],
|
|
12
12
|
exclude: ['**/*.d.ts', '**/*.test.ts'],
|
|
13
13
|
all: true,
|
|
14
14
|
thresholds: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC;IAC1B,IAAI,EAAE;QACJ,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,WAAW;QACxB,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE;YACR,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC;IAC1B,IAAI,EAAE;QACJ,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,WAAW;QACxB,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE;YACR,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC1C,OAAO,EAAE,CAAC,aAAa,CAAC;YACxB,OAAO,EAAE,CAAC,WAAW,EAAE,cAAc,CAAC;YACtC,GAAG,EAAE,IAAI;YACT,UAAU,EAAE;gBACV,KAAK,EAAE,EAAE;gBACT,SAAS,EAAE,EAAE;gBACb,QAAQ,EAAE,EAAE;gBACZ,UAAU,EAAE,EAAE;aACf;SACF;QACD,OAAO,EAAE,CAAC,mBAAmB,CAAC;QAC9B,SAAS,EAAE,CAAC,SAAS,CAAC;KACvB;IACD,OAAO,EAAE;QACP,KAAK,EAAE;YACL,GAAG,EAAE,MAAM;SACZ;KACF;CACF,CAAC,CAAC"}
|
|
@@ -95,12 +95,6 @@
|
|
|
95
95
|
</label>
|
|
96
96
|
<button id="regenerate">Regenerate Graph</button>
|
|
97
97
|
<button id="relayout">Re-run Layout</button>
|
|
98
|
-
<label>
|
|
99
|
-
<input type="checkbox" id="showAxes" checked> Show Axes
|
|
100
|
-
</label>
|
|
101
|
-
<label>
|
|
102
|
-
<input type="checkbox" id="animateNodes"> Animate Nodes
|
|
103
|
-
</label>
|
|
104
98
|
</div>
|
|
105
99
|
<div id="status">
|
|
106
100
|
<div>Nodes: <span id="nodeCount">0</span></div>
|
|
@@ -327,7 +321,6 @@
|
|
|
327
321
|
const scaleLabel = document.getElementById('scaleLabel');
|
|
328
322
|
const regenerateBtn = document.getElementById('regenerate');
|
|
329
323
|
const relayoutBtn = document.getElementById('relayout');
|
|
330
|
-
const animateNodesCheckbox = document.getElementById('animateNodes');
|
|
331
324
|
|
|
332
325
|
graphSizeSlider.addEventListener('input', (e) => {
|
|
333
326
|
sizeLabel.textContent = e.target.value;
|
|
@@ -364,18 +357,10 @@
|
|
|
364
357
|
|
|
365
358
|
time += 0.01;
|
|
366
359
|
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
mesh.position.copy(mesh.userData.originalPosition);
|
|
372
|
-
mesh.position.y += offset;
|
|
373
|
-
});
|
|
374
|
-
} else {
|
|
375
|
-
nodeAnimations.forEach((mesh) => {
|
|
376
|
-
mesh.position.copy(mesh.userData.originalPosition);
|
|
377
|
-
});
|
|
378
|
-
}
|
|
360
|
+
// Keep nodes at their original positions
|
|
361
|
+
nodeAnimations.forEach((mesh) => {
|
|
362
|
+
mesh.position.copy(mesh.userData.originalPosition);
|
|
363
|
+
});
|
|
379
364
|
|
|
380
365
|
controls.update();
|
|
381
366
|
renderer.render(scene, camera);
|
package/examples/bfs-layout.html
CHANGED
|
@@ -169,61 +169,55 @@
|
|
|
169
169
|
</div>
|
|
170
170
|
|
|
171
171
|
<script type="module">
|
|
172
|
-
import {
|
|
173
|
-
bfsLayout,
|
|
174
|
-
starGraph,
|
|
175
|
-
gridGraph,
|
|
176
|
-
randomGraph,
|
|
177
|
-
scaleFreeGraph,
|
|
178
|
-
findBestRoot
|
|
179
|
-
} from "./layout.js";
|
|
172
|
+
import { bfsLayout, scaleFreeGraph, randomGraph, gridGraph, starGraph } from "./layout.js";
|
|
180
173
|
|
|
181
174
|
let currentGraph = null;
|
|
182
175
|
let bestRoot = null;
|
|
183
176
|
|
|
184
177
|
function generateGraph(type, numNodes) {
|
|
178
|
+
let graph;
|
|
185
179
|
switch(type) {
|
|
186
180
|
case 'tree':
|
|
187
181
|
// Generate tree-like structure using scale-free with low m
|
|
188
|
-
|
|
182
|
+
graph = scaleFreeGraph(numNodes, 1, Date.now());
|
|
183
|
+
break;
|
|
189
184
|
|
|
190
185
|
case 'connected':
|
|
191
186
|
// Generate connected random graph
|
|
192
|
-
|
|
187
|
+
graph = randomGraph(numNodes, 0.15, Date.now());
|
|
188
|
+
break;
|
|
193
189
|
|
|
194
190
|
case 'grid':
|
|
195
191
|
// Create grid
|
|
196
192
|
const size = Math.ceil(Math.sqrt(numNodes));
|
|
197
|
-
|
|
193
|
+
const gridG = gridGraph(size, size);
|
|
194
|
+
// Convert string node IDs to numbers for consistency
|
|
195
|
+
const nodeMap = new Map();
|
|
196
|
+
gridG.nodes().forEach((node, i) => nodeMap.set(node, i));
|
|
197
|
+
graph = {
|
|
198
|
+
nodes: () => Array.from({length: gridG.nodes().length}, (_, i) => i),
|
|
199
|
+
edges: () => gridG.edges().map(([u, v]) => [nodeMap.get(u), nodeMap.get(v)])
|
|
200
|
+
};
|
|
201
|
+
break;
|
|
198
202
|
|
|
199
203
|
case 'star':
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
default:
|
|
203
|
-
return randomGraph(numNodes, 0.2, Date.now());
|
|
204
|
-
edges.push([i, i + gridSize]);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
204
|
+
graph = starGraph(numNodes);
|
|
207
205
|
break;
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
for (let i = 1; i < numNodes; i++) {
|
|
212
|
-
edges.push([0, i]);
|
|
213
|
-
}
|
|
207
|
+
default:
|
|
208
|
+
graph = randomGraph(numNodes, 0.2, Date.now());
|
|
214
209
|
break;
|
|
215
210
|
}
|
|
216
211
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
edges
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.filter(([s, t]) => s === node || t === node)
|
|
224
|
-
.map(([s, t]) => s === node ? t : s);
|
|
225
|
-
}
|
|
212
|
+
// Add getNeighbors method to the graph
|
|
213
|
+
graph.getNeighbors = function(node) {
|
|
214
|
+
const edges = this.edges();
|
|
215
|
+
return edges
|
|
216
|
+
.filter(([s, t]) => s === node || t === node)
|
|
217
|
+
.map(([s, t]) => s === node ? t : s);
|
|
226
218
|
};
|
|
219
|
+
|
|
220
|
+
return graph;
|
|
227
221
|
}
|
|
228
222
|
|
|
229
223
|
function updateStartNodeOptions() {
|
|
@@ -165,9 +165,9 @@
|
|
|
165
165
|
import {
|
|
166
166
|
bipartiteLayout,
|
|
167
167
|
bipartiteGraph,
|
|
168
|
-
detectBipartite,
|
|
169
168
|
randomGraph
|
|
170
169
|
} from "./layout.js";
|
|
170
|
+
import { detectBipartite } from "./layout-helpers.js";
|
|
171
171
|
|
|
172
172
|
let currentGraph = null;
|
|
173
173
|
let detectedBipartite = null;
|
|
@@ -335,7 +335,7 @@
|
|
|
335
335
|
document.getElementById('alignment').onchange = null;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
window.generateMixedGraph = function() {
|
|
338
|
+
window.generateMixedGraph = async function() {
|
|
339
339
|
const totalNodes = parseInt(document.getElementById('set-a-size').value) +
|
|
340
340
|
parseInt(document.getElementById('set-b-size').value);
|
|
341
341
|
const edgeDensity = parseFloat(document.getElementById('edge-density').value);
|
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
`;
|
|
361
361
|
} else {
|
|
362
362
|
// Not bipartite - show with spring layout instead
|
|
363
|
-
const { springLayout } = await import('
|
|
363
|
+
const { springLayout } = await import('./layout.js');
|
|
364
364
|
const positions = springLayout(currentGraph);
|
|
365
365
|
visualizeGraph(currentGraph, positions);
|
|
366
366
|
|
|
@@ -203,16 +203,117 @@
|
|
|
203
203
|
|
|
204
204
|
<script type="module">
|
|
205
205
|
import {
|
|
206
|
-
forceatlas2Layout
|
|
207
|
-
randomGraph,
|
|
208
|
-
scaleFreeGraph,
|
|
209
|
-
gridGraph,
|
|
210
|
-
autoConfigureForce
|
|
206
|
+
forceatlas2Layout
|
|
211
207
|
} from "./layout.js";
|
|
212
208
|
|
|
209
|
+
import {
|
|
210
|
+
autoConfigureForce
|
|
211
|
+
} from "./layout-helpers.js";
|
|
212
|
+
|
|
213
213
|
let currentGraph = null;
|
|
214
214
|
let currentConfig = null;
|
|
215
215
|
|
|
216
|
+
// Graph generation utilities
|
|
217
|
+
function randomGraph(n, p, seed) {
|
|
218
|
+
const nodes = Array.from({length: n}, (_, i) => i);
|
|
219
|
+
const edges = [];
|
|
220
|
+
|
|
221
|
+
// Simple random graph with probability p for each edge
|
|
222
|
+
for (let i = 0; i < n; i++) {
|
|
223
|
+
for (let j = i + 1; j < n; j++) {
|
|
224
|
+
if (Math.random() < p) {
|
|
225
|
+
edges.push([i, j]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
nodes: () => nodes,
|
|
232
|
+
edges: () => edges
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function scaleFreeGraph(n, m, seed) {
|
|
237
|
+
const nodes = Array.from({length: n}, (_, i) => i);
|
|
238
|
+
const edges = [];
|
|
239
|
+
|
|
240
|
+
// Start with a complete graph of m+1 nodes
|
|
241
|
+
for (let i = 0; i <= m; i++) {
|
|
242
|
+
for (let j = i + 1; j <= m; j++) {
|
|
243
|
+
edges.push([i, j]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add remaining nodes with preferential attachment
|
|
248
|
+
for (let i = m + 1; i < n; i++) {
|
|
249
|
+
const degrees = new Map();
|
|
250
|
+
for (const [u, v] of edges) {
|
|
251
|
+
degrees.set(u, (degrees.get(u) || 0) + 1);
|
|
252
|
+
degrees.set(v, (degrees.get(v) || 0) + 1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const totalDegree = Array.from(degrees.values()).reduce((a, b) => a + b, 0);
|
|
256
|
+
const targets = new Set();
|
|
257
|
+
|
|
258
|
+
while (targets.size < Math.min(m, i)) {
|
|
259
|
+
let r = Math.random() * totalDegree;
|
|
260
|
+
let node = 0;
|
|
261
|
+
|
|
262
|
+
for (const [n, d] of degrees) {
|
|
263
|
+
r -= d;
|
|
264
|
+
if (r <= 0) {
|
|
265
|
+
node = n;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!targets.has(node)) {
|
|
271
|
+
targets.add(node);
|
|
272
|
+
edges.push([i, node]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
nodes: () => nodes,
|
|
279
|
+
edges: () => edges
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function gridGraph(rows, cols) {
|
|
284
|
+
const nodes = [];
|
|
285
|
+
const edges = [];
|
|
286
|
+
|
|
287
|
+
// Create nodes
|
|
288
|
+
for (let r = 0; r < rows; r++) {
|
|
289
|
+
for (let c = 0; c < cols; c++) {
|
|
290
|
+
nodes.push(r * cols + c);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Create edges
|
|
295
|
+
for (let r = 0; r < rows; r++) {
|
|
296
|
+
for (let c = 0; c < cols; c++) {
|
|
297
|
+
const node = r * cols + c;
|
|
298
|
+
|
|
299
|
+
// Right edge
|
|
300
|
+
if (c < cols - 1) {
|
|
301
|
+
edges.push([node, node + 1]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Down edge
|
|
305
|
+
if (r < rows - 1) {
|
|
306
|
+
edges.push([node, node + cols]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
nodes: () => nodes,
|
|
313
|
+
edges: () => edges
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
216
317
|
function generateGraph(type, numNodes) {
|
|
217
318
|
switch(type) {
|
|
218
319
|
case 'scale-free':
|
|
@@ -263,150 +364,7 @@
|
|
|
263
364
|
case 'random':
|
|
264
365
|
default:
|
|
265
366
|
return randomGraph(numNodes, 0.1, Date.now());
|
|
266
|
-
for(let i = 1; i < Math.min(5, numNodes); i++) {
|
|
267
|
-
edges.push([0, i]);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Add nodes with preferential attachment
|
|
271
|
-
for(let i = 5; i < numNodes; i++) {
|
|
272
|
-
// Number of connections for new node
|
|
273
|
-
const newEdges = Math.min(3, i);
|
|
274
|
-
|
|
275
|
-
// Track edge count per node for preferential attachment
|
|
276
|
-
const nodeDegrees = new Array(i).fill(0);
|
|
277
|
-
for(const edge of edges) {
|
|
278
|
-
nodeDegrees[edge[0]]++;
|
|
279
|
-
nodeDegrees[edge[1]]++;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Calculate total degree for probability
|
|
283
|
-
const totalDegree = nodeDegrees.reduce((sum, deg) => sum + deg, 0);
|
|
284
|
-
|
|
285
|
-
// Add edges based on preferential attachment
|
|
286
|
-
const added = new Set();
|
|
287
|
-
for(let j = 0; j < newEdges; j++) {
|
|
288
|
-
let targetNode;
|
|
289
|
-
do {
|
|
290
|
-
// Choose node with probability proportional to its degree
|
|
291
|
-
let rand = Math.random() * totalDegree;
|
|
292
|
-
targetNode = 0;
|
|
293
|
-
while(targetNode < i && rand > 0) {
|
|
294
|
-
rand -= nodeDegrees[targetNode];
|
|
295
|
-
if(rand > 0) targetNode++;
|
|
296
|
-
}
|
|
297
|
-
if(targetNode >= i) targetNode = Math.floor(Math.random() * i);
|
|
298
|
-
} while(added.has(targetNode));
|
|
299
|
-
|
|
300
|
-
edges.push([i, targetNode]);
|
|
301
|
-
added.add(targetNode);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
break;
|
|
305
|
-
|
|
306
|
-
case 'community':
|
|
307
|
-
// Create community structure with 3-4 communities
|
|
308
|
-
const numCommunities = Math.min(4, Math.ceil(numNodes / 10));
|
|
309
|
-
const communitySizes = [];
|
|
310
|
-
let remainingNodes = numNodes;
|
|
311
|
-
|
|
312
|
-
for(let i = 0; i < numCommunities - 1; i++) {
|
|
313
|
-
const size = Math.ceil(numNodes / numCommunities);
|
|
314
|
-
communitySizes.push(size);
|
|
315
|
-
remainingNodes -= size;
|
|
316
|
-
}
|
|
317
|
-
communitySizes.push(remainingNodes);
|
|
318
|
-
|
|
319
|
-
let nodeIndex = 0;
|
|
320
|
-
for(let c = 0; c < numCommunities; c++) {
|
|
321
|
-
const communityStart = nodeIndex;
|
|
322
|
-
const communityEnd = communityStart + communitySizes[c];
|
|
323
|
-
|
|
324
|
-
// Connect nodes within community (dense)
|
|
325
|
-
for(let i = communityStart; i < communityEnd; i++) {
|
|
326
|
-
for(let j = i + 1; j < communityEnd; j++) {
|
|
327
|
-
if(Math.random() < 0.7) { // High probability within community
|
|
328
|
-
edges.push([i, j]);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Connect to other communities (sparse)
|
|
334
|
-
if(c > 0) {
|
|
335
|
-
const prevCommunityStart = nodeIndex - communitySizes[c-1];
|
|
336
|
-
const bridgeEdges = Math.max(1, Math.floor(communitySizes[c] * 0.1));
|
|
337
|
-
for(let e = 0; e < bridgeEdges; e++) {
|
|
338
|
-
const source = communityStart + Math.floor(Math.random() * communitySizes[c]);
|
|
339
|
-
const target = prevCommunityStart + Math.floor(Math.random() * communitySizes[c-1]);
|
|
340
|
-
edges.push([source, target]);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
nodeIndex = communityEnd;
|
|
345
|
-
}
|
|
346
|
-
break;
|
|
347
|
-
|
|
348
|
-
case 'grid':
|
|
349
|
-
// Create a grid-like structure
|
|
350
|
-
const gridSize = Math.ceil(Math.sqrt(numNodes));
|
|
351
|
-
for(let i = 0; i < numNodes; i++) {
|
|
352
|
-
const row = Math.floor(i / gridSize);
|
|
353
|
-
const col = i % gridSize;
|
|
354
|
-
|
|
355
|
-
// Connect right
|
|
356
|
-
if(col < gridSize - 1 && i + 1 < numNodes) {
|
|
357
|
-
edges.push([i, i + 1]);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Connect down
|
|
361
|
-
if(row < gridSize - 1 && i + gridSize < numNodes) {
|
|
362
|
-
edges.push([i, i + gridSize]);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
break;
|
|
366
|
-
|
|
367
|
-
case 'random':
|
|
368
|
-
default:
|
|
369
|
-
// Random graph with controlled density
|
|
370
|
-
const density = 0.1; // 10% of possible edges
|
|
371
|
-
const possibleEdges = (numNodes * (numNodes - 1)) / 2;
|
|
372
|
-
const targetEdges = Math.ceil(possibleEdges * density);
|
|
373
|
-
|
|
374
|
-
// Ensure connected graph with spanning tree
|
|
375
|
-
for(let i = 1; i < numNodes; i++) {
|
|
376
|
-
const parent = Math.floor(Math.random() * i);
|
|
377
|
-
edges.push([parent, i]);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Add random edges up to target density
|
|
381
|
-
let additionalEdges = targetEdges - (numNodes - 1);
|
|
382
|
-
let attempts = 0;
|
|
383
|
-
|
|
384
|
-
while(additionalEdges > 0 && attempts < 1000) {
|
|
385
|
-
const a = Math.floor(Math.random() * numNodes);
|
|
386
|
-
const b = Math.floor(Math.random() * numNodes);
|
|
387
|
-
|
|
388
|
-
if(a !== b && !edges.some(e => (e[0] === a && e[1] === b) || (e[0] === b && e[1] === a))) {
|
|
389
|
-
edges.push([a, b]);
|
|
390
|
-
additionalEdges--;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
attempts++;
|
|
394
|
-
}
|
|
395
|
-
break;
|
|
396
367
|
}
|
|
397
|
-
|
|
398
|
-
// Create node masses based on degree
|
|
399
|
-
const masses = {};
|
|
400
|
-
for(const edge of edges) {
|
|
401
|
-
masses[edge[0]] = (masses[edge[0]] || 1) + 0.1;
|
|
402
|
-
masses[edge[1]] = (masses[edge[1]] || 1) + 0.1;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return {
|
|
406
|
-
nodes: () => nodes,
|
|
407
|
-
edges: () => edges,
|
|
408
|
-
masses: masses
|
|
409
|
-
};
|
|
410
368
|
}
|
|
411
369
|
|
|
412
370
|
function visualizeGraph(graph, positions) {
|
|
@@ -514,9 +472,6 @@
|
|
|
514
472
|
|
|
515
473
|
// Apply layout immediately
|
|
516
474
|
applyLayout();
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
visualizeGraph(currentGraph, positions);
|
|
520
475
|
|
|
521
476
|
} catch (error) {
|
|
522
477
|
console.error('Error in graph generation:', error);
|
|
@@ -409,50 +409,78 @@ function groupByKCore(nodes, edges) {
|
|
|
409
409
|
.map(([, nodeList]) => nodeList);
|
|
410
410
|
}
|
|
411
411
|
function groupByCommunity(nodes, edges, targetGroups) {
|
|
412
|
-
//
|
|
412
|
+
// Fast community detection using label propagation
|
|
413
413
|
const communities = new Map();
|
|
414
|
-
nodes.forEach((node, i) => communities.set(node, i));
|
|
415
|
-
// Build adjacency
|
|
416
414
|
const adj = new Map();
|
|
417
|
-
|
|
415
|
+
|
|
416
|
+
// Initialize each node in its own community
|
|
417
|
+
nodes.forEach((node, i) => {
|
|
418
|
+
communities.set(node, i);
|
|
419
|
+
adj.set(node, new Set());
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Build adjacency
|
|
418
423
|
edges.forEach(([u, v]) => {
|
|
419
424
|
adj.get(u).add(v);
|
|
420
425
|
adj.get(v).add(u);
|
|
421
426
|
});
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
427
|
+
|
|
428
|
+
// Label propagation algorithm
|
|
429
|
+
const maxIterations = 10; // Prevent infinite loops
|
|
430
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
431
|
+
let changed = false;
|
|
432
|
+
|
|
433
|
+
// Process nodes in random order
|
|
434
|
+
const shuffledNodes = nodes.slice().sort(() => Math.random() - 0.5);
|
|
435
|
+
|
|
436
|
+
for (const node of shuffledNodes) {
|
|
437
|
+
const neighbors = adj.get(node);
|
|
438
|
+
if (neighbors.size === 0) continue;
|
|
439
|
+
|
|
440
|
+
// Count community frequencies among neighbors
|
|
441
|
+
const commCounts = new Map();
|
|
442
|
+
neighbors.forEach(neighbor => {
|
|
443
|
+
const comm = communities.get(neighbor);
|
|
444
|
+
commCounts.set(comm, (commCounts.get(comm) || 0) + 1);
|
|
431
445
|
});
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
446
|
+
|
|
447
|
+
// Find most frequent community
|
|
448
|
+
let maxCount = 0;
|
|
449
|
+
let bestComm = communities.get(node);
|
|
450
|
+
commCounts.forEach((count, comm) => {
|
|
451
|
+
if (count > maxCount || (count === maxCount && comm < bestComm)) {
|
|
452
|
+
maxCount = count;
|
|
453
|
+
bestComm = comm;
|
|
438
454
|
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (bestComm !== communities.get(node)) {
|
|
458
|
+
communities.set(node, bestComm);
|
|
459
|
+
changed = true;
|
|
439
460
|
}
|
|
440
|
-
if (improved)
|
|
441
|
-
break;
|
|
442
461
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
// Count communities
|
|
446
|
-
const currentNumCommunities = new Set(communities.values()).size;
|
|
447
|
-
numCommunities = currentNumCommunities;
|
|
462
|
+
|
|
463
|
+
if (!changed) break;
|
|
448
464
|
}
|
|
449
|
-
|
|
465
|
+
|
|
466
|
+
// Consolidate communities
|
|
450
467
|
const groups = new Map();
|
|
451
468
|
communities.forEach((comm, node) => {
|
|
452
|
-
if (!groups.has(comm))
|
|
453
|
-
groups.set(comm, []);
|
|
469
|
+
if (!groups.has(comm)) groups.set(comm, []);
|
|
454
470
|
groups.get(comm).push(node);
|
|
455
471
|
});
|
|
456
|
-
|
|
472
|
+
|
|
473
|
+
let result = Array.from(groups.values());
|
|
474
|
+
|
|
475
|
+
// If we have too many groups, merge the smallest ones
|
|
476
|
+
while (result.length > targetGroups && result.length > 1) {
|
|
477
|
+
// Sort by size
|
|
478
|
+
result.sort((a, b) => a.length - b.length);
|
|
479
|
+
// Merge two smallest groups
|
|
480
|
+
const merged = result[0].concat(result[1]);
|
|
481
|
+
result = [merged, ...result.slice(2)];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return result;
|
|
457
485
|
}
|
|
458
486
|
//# sourceMappingURL=layout-helpers.js.map
|