@graphty/layout 1.2.2 → 1.2.3
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/CHANGELOG.md +7 -0
- package/README.md +317 -237
- package/build-examples-script.js +26 -0
- package/examples/layout-helpers.js +23 -486
- package/examples/layout.js +2304 -0
- package/package.json +2 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// Read the compiled JavaScript from dist
|
|
9
|
+
const distLayoutPath = path.join(__dirname, 'dist', 'layout.js');
|
|
10
|
+
const examplesLayoutPath = path.join(__dirname, 'examples', 'layout.js');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Check if dist file exists
|
|
14
|
+
if (!fs.existsSync(distLayoutPath)) {
|
|
15
|
+
console.error('Error: dist/layout.js not found. Please run "npm run build" first.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Copy the bundled layout.js to examples directory
|
|
20
|
+
fs.copyFileSync(distLayoutPath, examplesLayoutPath);
|
|
21
|
+
console.log('Successfully copied dist/layout.js to examples/layout.js');
|
|
22
|
+
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error building examples:', error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
@@ -1,486 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* const positions = shellLayout(graph, shells);
|
|
25
|
-
*
|
|
26
|
-
* // For multipartite layout
|
|
27
|
-
* const layers = groupNodes(graph, 'bfs', 0); // 0 = auto
|
|
28
|
-
* const positions = multipartiteLayout(graph, layers);
|
|
29
|
-
*/
|
|
30
|
-
export function groupNodes(graph, method = 'degree', numGroups = 3, options = {}) {
|
|
31
|
-
const nodes = graph.nodes?.() || [];
|
|
32
|
-
const edges = graph.edges?.() || [];
|
|
33
|
-
switch (method) {
|
|
34
|
-
case 'degree':
|
|
35
|
-
return groupByDegree(nodes, edges, numGroups);
|
|
36
|
-
case 'bfs':
|
|
37
|
-
return groupByDistance(nodes, edges, options.root);
|
|
38
|
-
case 'k-core':
|
|
39
|
-
return groupByKCore(nodes, edges);
|
|
40
|
-
case 'community':
|
|
41
|
-
return groupByCommunity(nodes, edges, numGroups);
|
|
42
|
-
default:
|
|
43
|
-
return [nodes]; // Single group fallback
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Bipartite Detection & Handling
|
|
48
|
-
* ==============================
|
|
49
|
-
*/
|
|
50
|
-
/**
|
|
51
|
-
* Detect if graph is bipartite and find the two sets.
|
|
52
|
-
*
|
|
53
|
-
* @param graph - Input graph
|
|
54
|
-
* @returns Bipartite sets or null if not bipartite
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* const result = detectBipartite(graph);
|
|
58
|
-
* if (result) {
|
|
59
|
-
* const positions = bipartiteLayout(graph, result.setA);
|
|
60
|
-
* }
|
|
61
|
-
*/
|
|
62
|
-
export function detectBipartite(graph) {
|
|
63
|
-
const nodes = graph.nodes?.() || [];
|
|
64
|
-
const edges = graph.edges?.() || [];
|
|
65
|
-
if (nodes.length === 0)
|
|
66
|
-
return { setA: [], setB: [] };
|
|
67
|
-
// Build adjacency list
|
|
68
|
-
const adj = new Map();
|
|
69
|
-
nodes.forEach(node => adj.set(node, []));
|
|
70
|
-
edges.forEach(([u, v]) => {
|
|
71
|
-
adj.get(u).push(v);
|
|
72
|
-
adj.get(v).push(u);
|
|
73
|
-
});
|
|
74
|
-
// Try to 2-color the graph
|
|
75
|
-
const colors = new Map();
|
|
76
|
-
for (const start of nodes) {
|
|
77
|
-
if (colors.has(start))
|
|
78
|
-
continue;
|
|
79
|
-
const queue = [start];
|
|
80
|
-
colors.set(start, 0);
|
|
81
|
-
while (queue.length > 0) {
|
|
82
|
-
const node = queue.shift();
|
|
83
|
-
const nodeColor = colors.get(node);
|
|
84
|
-
for (const neighbor of adj.get(node)) {
|
|
85
|
-
if (!colors.has(neighbor)) {
|
|
86
|
-
colors.set(neighbor, 1 - nodeColor);
|
|
87
|
-
queue.push(neighbor);
|
|
88
|
-
}
|
|
89
|
-
else if (colors.get(neighbor) === nodeColor) {
|
|
90
|
-
return null; // Not bipartite
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Separate into sets
|
|
96
|
-
const setA = [];
|
|
97
|
-
const setB = [];
|
|
98
|
-
nodes.forEach(node => {
|
|
99
|
-
if (colors.get(node) === 0)
|
|
100
|
-
setA.push(node);
|
|
101
|
-
else
|
|
102
|
-
setB.push(node);
|
|
103
|
-
});
|
|
104
|
-
return { setA, setB };
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Layout Analysis & Optimization
|
|
108
|
-
* ==============================
|
|
109
|
-
*/
|
|
110
|
-
/**
|
|
111
|
-
* Find the best root node for tree-like layouts (BFS, hierarchical).
|
|
112
|
-
*
|
|
113
|
-
* @param graph - Input graph
|
|
114
|
-
* @returns Best root node
|
|
115
|
-
*/
|
|
116
|
-
export function findBestRoot(graph) {
|
|
117
|
-
const nodes = graph.nodes?.() || [];
|
|
118
|
-
const edges = graph.edges?.() || [];
|
|
119
|
-
if (nodes.length === 0)
|
|
120
|
-
return null;
|
|
121
|
-
// Build adjacency list
|
|
122
|
-
const adj = new Map();
|
|
123
|
-
nodes.forEach(node => adj.set(node, new Set()));
|
|
124
|
-
edges.forEach(([u, v]) => {
|
|
125
|
-
adj.get(u).add(v);
|
|
126
|
-
adj.get(v).add(u);
|
|
127
|
-
});
|
|
128
|
-
// Find node with minimum eccentricity (center of graph)
|
|
129
|
-
let bestNode = nodes[0];
|
|
130
|
-
let bestEccentricity = Infinity;
|
|
131
|
-
for (const start of nodes) {
|
|
132
|
-
// BFS to find max distance
|
|
133
|
-
const distances = new Map();
|
|
134
|
-
const queue = [start];
|
|
135
|
-
distances.set(start, 0);
|
|
136
|
-
let maxDist = 0;
|
|
137
|
-
while (queue.length > 0) {
|
|
138
|
-
const node = queue.shift();
|
|
139
|
-
const dist = distances.get(node);
|
|
140
|
-
adj.get(node).forEach(neighbor => {
|
|
141
|
-
if (!distances.has(neighbor)) {
|
|
142
|
-
distances.set(neighbor, dist + 1);
|
|
143
|
-
maxDist = Math.max(maxDist, dist + 1);
|
|
144
|
-
queue.push(neighbor);
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
if (maxDist < bestEccentricity) {
|
|
149
|
-
bestEccentricity = maxDist;
|
|
150
|
-
bestNode = start;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return bestNode;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Check if a graph is planar (can be drawn without edge crossings).
|
|
157
|
-
*
|
|
158
|
-
* @param graph - Input graph
|
|
159
|
-
* @returns true if planar
|
|
160
|
-
*/
|
|
161
|
-
export function isPlanar(graph) {
|
|
162
|
-
const n = graph.nodes?.()?.length || 0;
|
|
163
|
-
const m = graph.edges?.()?.length || 0;
|
|
164
|
-
// Quick check: Euler's formula
|
|
165
|
-
if (n >= 3 && m > 3 * n - 6)
|
|
166
|
-
return false;
|
|
167
|
-
// For accurate check, would need to try planarLayout
|
|
168
|
-
// This is a simplified heuristic
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Auto-configure force-directed layout parameters.
|
|
173
|
-
*
|
|
174
|
-
* @param graph - Input graph
|
|
175
|
-
* @returns Recommended parameters
|
|
176
|
-
*/
|
|
177
|
-
export function autoConfigureForce(graph) {
|
|
178
|
-
const n = graph.nodes?.()?.length || 0;
|
|
179
|
-
const m = graph.edges?.()?.length || 0;
|
|
180
|
-
const density = n > 1 ? (2 * m) / (n * (n - 1)) : 0;
|
|
181
|
-
return {
|
|
182
|
-
k: Math.sqrt(1 / n), // Fruchterman-Reingold optimal distance
|
|
183
|
-
iterations: n < 50 ? 500 : n < 200 ? 300 : 200,
|
|
184
|
-
gravity: density < 0.1 ? 0.5 : density < 0.3 ? 1.0 : 2.0,
|
|
185
|
-
scalingRatio: n < 50 ? 2.0 : n < 200 ? 5.0 : 10.0
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Layout Quality Metrics
|
|
190
|
-
* ======================
|
|
191
|
-
*/
|
|
192
|
-
/**
|
|
193
|
-
* Calculate simple layout quality metrics.
|
|
194
|
-
*
|
|
195
|
-
* @param graph - Input graph
|
|
196
|
-
* @param positions - Layout positions
|
|
197
|
-
* @returns Quality metrics
|
|
198
|
-
*/
|
|
199
|
-
export function layoutQuality(graph, positions) {
|
|
200
|
-
const nodes = graph.nodes?.() || [];
|
|
201
|
-
const edges = graph.edges?.() || [];
|
|
202
|
-
// Edge lengths
|
|
203
|
-
const edgeLengths = edges.map(([u, v]) => {
|
|
204
|
-
const p1 = positions[u];
|
|
205
|
-
const p2 = positions[v];
|
|
206
|
-
return Math.sqrt(p1.reduce((sum, val, i) => sum + (val - p2[i]) ** 2, 0));
|
|
207
|
-
});
|
|
208
|
-
const avgEdgeLength = edgeLengths.reduce((a, b) => a + b, 0) / edgeLengths.length || 0;
|
|
209
|
-
const variance = edgeLengths.reduce((sum, len) => sum + (len - avgEdgeLength) ** 2, 0) / edgeLengths.length || 0;
|
|
210
|
-
const edgeLengthStdDev = Math.sqrt(variance);
|
|
211
|
-
// Minimum node distance
|
|
212
|
-
let minNodeDistance = Infinity;
|
|
213
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
214
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
215
|
-
const p1 = positions[nodes[i]];
|
|
216
|
-
const p2 = positions[nodes[j]];
|
|
217
|
-
const dist = Math.sqrt(p1.reduce((sum, val, k) => sum + (val - p2[k]) ** 2, 0));
|
|
218
|
-
minNodeDistance = Math.min(minNodeDistance, dist);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// Bounding box and aspect ratio
|
|
222
|
-
let minX = Infinity, maxX = -Infinity;
|
|
223
|
-
let minY = Infinity, maxY = -Infinity;
|
|
224
|
-
nodes.forEach(node => {
|
|
225
|
-
const [x, y] = positions[node];
|
|
226
|
-
minX = Math.min(minX, x);
|
|
227
|
-
maxX = Math.max(maxX, x);
|
|
228
|
-
minY = Math.min(minY, y);
|
|
229
|
-
maxY = Math.max(maxY, y);
|
|
230
|
-
});
|
|
231
|
-
const width = maxX - minX || 1;
|
|
232
|
-
const height = maxY - minY || 1;
|
|
233
|
-
const aspectRatio = Math.max(width, height) / Math.min(width, height);
|
|
234
|
-
return {
|
|
235
|
-
avgEdgeLength,
|
|
236
|
-
edgeLengthStdDev,
|
|
237
|
-
minNodeDistance: minNodeDistance === Infinity ? 0 : minNodeDistance,
|
|
238
|
-
aspectRatio
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Layout Utilities
|
|
243
|
-
* ================
|
|
244
|
-
*/
|
|
245
|
-
/**
|
|
246
|
-
* Combine multiple layouts with weights.
|
|
247
|
-
*
|
|
248
|
-
* @param layouts - Array of position maps
|
|
249
|
-
* @param weights - Weight for each layout (default: equal)
|
|
250
|
-
* @returns Combined positions
|
|
251
|
-
*/
|
|
252
|
-
export function combineLayouts(layouts, weights) {
|
|
253
|
-
if (layouts.length === 0)
|
|
254
|
-
return {};
|
|
255
|
-
const w = weights || new Array(layouts.length).fill(1 / layouts.length);
|
|
256
|
-
const result = {};
|
|
257
|
-
const nodes = Object.keys(layouts[0]);
|
|
258
|
-
nodes.forEach(node => {
|
|
259
|
-
const dim = layouts[0][node].length;
|
|
260
|
-
result[node] = new Array(dim).fill(0);
|
|
261
|
-
layouts.forEach((layout, i) => {
|
|
262
|
-
if (node in layout) {
|
|
263
|
-
for (let d = 0; d < dim; d++) {
|
|
264
|
-
result[node][d] += layout[node][d] * w[i];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Create smooth animation frames between layouts.
|
|
273
|
-
*
|
|
274
|
-
* @param from - Starting positions
|
|
275
|
-
* @param to - Ending positions
|
|
276
|
-
* @param steps - Number of frames
|
|
277
|
-
* @returns Array of position maps
|
|
278
|
-
*/
|
|
279
|
-
export function interpolateLayouts(from, to, steps = 10) {
|
|
280
|
-
const frames = [];
|
|
281
|
-
const nodes = Object.keys(from);
|
|
282
|
-
for (let i = 0; i <= steps; i++) {
|
|
283
|
-
const t = i / steps;
|
|
284
|
-
const frame = {};
|
|
285
|
-
nodes.forEach(node => {
|
|
286
|
-
if (node in to) {
|
|
287
|
-
frame[node] = from[node].map((val, d) => val + t * (to[node][d] - val));
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
frames.push(frame);
|
|
291
|
-
}
|
|
292
|
-
return frames;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Internal Helper Functions
|
|
296
|
-
* =========================
|
|
297
|
-
*/
|
|
298
|
-
function groupByDegree(nodes, edges, numGroups) {
|
|
299
|
-
// Calculate degrees
|
|
300
|
-
const degrees = new Map();
|
|
301
|
-
nodes.forEach(node => degrees.set(node, 0));
|
|
302
|
-
edges.forEach(([u, v]) => {
|
|
303
|
-
degrees.set(u, degrees.get(u) + 1);
|
|
304
|
-
degrees.set(v, degrees.get(v) + 1);
|
|
305
|
-
});
|
|
306
|
-
// Sort by degree
|
|
307
|
-
const sorted = nodes.slice().sort((a, b) => degrees.get(b) - degrees.get(a));
|
|
308
|
-
// Distribute into groups
|
|
309
|
-
const groups = new Array(numGroups).fill(null).map(() => []);
|
|
310
|
-
const nodesPerGroup = Math.ceil(nodes.length / numGroups);
|
|
311
|
-
sorted.forEach((node, i) => {
|
|
312
|
-
const groupIdx = Math.floor(i / nodesPerGroup);
|
|
313
|
-
if (groupIdx < numGroups) {
|
|
314
|
-
groups[groupIdx].push(node);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
return groups.filter(g => g.length > 0);
|
|
318
|
-
}
|
|
319
|
-
function groupByDistance(nodes, edges, root) {
|
|
320
|
-
if (nodes.length === 0)
|
|
321
|
-
return [];
|
|
322
|
-
const start = root || nodes[0];
|
|
323
|
-
const adj = new Map();
|
|
324
|
-
nodes.forEach(node => adj.set(node, []));
|
|
325
|
-
edges.forEach(([u, v]) => {
|
|
326
|
-
adj.get(u).push(v);
|
|
327
|
-
adj.get(v).push(u);
|
|
328
|
-
});
|
|
329
|
-
// BFS layers
|
|
330
|
-
const visited = new Set();
|
|
331
|
-
const layers = [];
|
|
332
|
-
let currentLayer = [start];
|
|
333
|
-
visited.add(start);
|
|
334
|
-
while (currentLayer.length > 0) {
|
|
335
|
-
layers.push(currentLayer.slice());
|
|
336
|
-
const nextLayer = [];
|
|
337
|
-
currentLayer.forEach(node => {
|
|
338
|
-
adj.get(node).forEach(neighbor => {
|
|
339
|
-
if (!visited.has(neighbor)) {
|
|
340
|
-
visited.add(neighbor);
|
|
341
|
-
nextLayer.push(neighbor);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
currentLayer = nextLayer;
|
|
346
|
-
}
|
|
347
|
-
// Add disconnected nodes
|
|
348
|
-
const unvisited = nodes.filter(n => !visited.has(n));
|
|
349
|
-
if (unvisited.length > 0) {
|
|
350
|
-
layers.push(unvisited);
|
|
351
|
-
}
|
|
352
|
-
return layers;
|
|
353
|
-
}
|
|
354
|
-
function groupByKCore(nodes, edges) {
|
|
355
|
-
// Build adjacency
|
|
356
|
-
const adj = new Map();
|
|
357
|
-
const degree = new Map();
|
|
358
|
-
nodes.forEach(node => {
|
|
359
|
-
adj.set(node, new Set());
|
|
360
|
-
degree.set(node, 0);
|
|
361
|
-
});
|
|
362
|
-
edges.forEach(([u, v]) => {
|
|
363
|
-
adj.get(u).add(v);
|
|
364
|
-
adj.get(v).add(u);
|
|
365
|
-
degree.set(u, degree.get(u) + 1);
|
|
366
|
-
degree.set(v, degree.get(v) + 1);
|
|
367
|
-
});
|
|
368
|
-
// k-core decomposition
|
|
369
|
-
const coreNumber = new Map();
|
|
370
|
-
const remaining = new Set(nodes);
|
|
371
|
-
while (remaining.size > 0) {
|
|
372
|
-
// Find minimum degree
|
|
373
|
-
let minDegree = Infinity;
|
|
374
|
-
remaining.forEach(node => {
|
|
375
|
-
minDegree = Math.min(minDegree, degree.get(node));
|
|
376
|
-
});
|
|
377
|
-
// Remove nodes with degree <= minDegree
|
|
378
|
-
const toRemove = [];
|
|
379
|
-
remaining.forEach(node => {
|
|
380
|
-
if (degree.get(node) <= minDegree) {
|
|
381
|
-
toRemove.push(node);
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
while (toRemove.length > 0) {
|
|
385
|
-
const node = toRemove.shift();
|
|
386
|
-
coreNumber.set(node, minDegree);
|
|
387
|
-
remaining.delete(node);
|
|
388
|
-
adj.get(node).forEach(neighbor => {
|
|
389
|
-
if (remaining.has(neighbor)) {
|
|
390
|
-
degree.set(neighbor, degree.get(neighbor) - 1);
|
|
391
|
-
if (degree.get(neighbor) <= minDegree && !toRemove.includes(neighbor)) {
|
|
392
|
-
toRemove.push(neighbor);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
// Group by core number
|
|
399
|
-
const groups = new Map();
|
|
400
|
-
nodes.forEach(node => {
|
|
401
|
-
const core = coreNumber.get(node);
|
|
402
|
-
if (!groups.has(core))
|
|
403
|
-
groups.set(core, []);
|
|
404
|
-
groups.get(core).push(node);
|
|
405
|
-
});
|
|
406
|
-
// Return sorted by core number (highest first)
|
|
407
|
-
return Array.from(groups.entries())
|
|
408
|
-
.sort((a, b) => b[0] - a[0])
|
|
409
|
-
.map(([, nodeList]) => nodeList);
|
|
410
|
-
}
|
|
411
|
-
function groupByCommunity(nodes, edges, targetGroups) {
|
|
412
|
-
// Fast community detection using label propagation
|
|
413
|
-
const communities = new Map();
|
|
414
|
-
const adj = new Map();
|
|
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
|
|
423
|
-
edges.forEach(([u, v]) => {
|
|
424
|
-
adj.get(u).add(v);
|
|
425
|
-
adj.get(v).add(u);
|
|
426
|
-
});
|
|
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);
|
|
445
|
-
});
|
|
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;
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
if (bestComm !== communities.get(node)) {
|
|
458
|
-
communities.set(node, bestComm);
|
|
459
|
-
changed = true;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (!changed) break;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Consolidate communities
|
|
467
|
-
const groups = new Map();
|
|
468
|
-
communities.forEach((comm, node) => {
|
|
469
|
-
if (!groups.has(comm)) groups.set(comm, []);
|
|
470
|
-
groups.get(comm).push(node);
|
|
471
|
-
});
|
|
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;
|
|
485
|
-
}
|
|
486
|
-
//# sourceMappingURL=layout-helpers.js.map
|
|
1
|
+
// Helper file to make functions available globally for button onclick handlers
|
|
2
|
+
import { randomLayout, circularLayout, shellLayout, springLayout, fruchtermanReingoldLayout,
|
|
3
|
+
spectralLayout, spiralLayout, bipartiteLayout, multipartiteLayout, bfsLayout,
|
|
4
|
+
planarLayout, kamadaKawaiLayout, forceatlas2Layout, arfLayout, rescaleLayout,
|
|
5
|
+
rescaleLayoutDict } from './layout.js';
|
|
6
|
+
|
|
7
|
+
// Make layout functions available globally
|
|
8
|
+
window.randomLayout = randomLayout;
|
|
9
|
+
window.circularLayout = circularLayout;
|
|
10
|
+
window.shellLayout = shellLayout;
|
|
11
|
+
window.springLayout = springLayout;
|
|
12
|
+
window.fruchtermanReingoldLayout = fruchtermanReingoldLayout;
|
|
13
|
+
window.spectralLayout = spectralLayout;
|
|
14
|
+
window.spiralLayout = spiralLayout;
|
|
15
|
+
window.bipartiteLayout = bipartiteLayout;
|
|
16
|
+
window.multipartiteLayout = multipartiteLayout;
|
|
17
|
+
window.bfsLayout = bfsLayout;
|
|
18
|
+
window.planarLayout = planarLayout;
|
|
19
|
+
window.kamadaKawaiLayout = kamadaKawaiLayout;
|
|
20
|
+
window.forceatlas2Layout = forceatlas2Layout;
|
|
21
|
+
window.arfLayout = arfLayout;
|
|
22
|
+
window.rescaleLayout = rescaleLayout;
|
|
23
|
+
window.rescaleLayoutDict = rescaleLayoutDict;
|