@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.
@@ -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
- * Layout Helper Functions
3
- * =======================
4
- *
5
- * Essential utilities for graph layouts
6
- */
7
- /**
8
- * Universal Node Grouping
9
- * =======================
10
- * Groups nodes for shell, multipartite, or custom layouts
11
- */
12
- /**
13
- * Group nodes by various centrality measures.
14
- * Works for shell layout, multipartite layout, or any grouping need.
15
- *
16
- * @param graph - Input graph
17
- * @param method - Grouping method
18
- * @param numGroups - Number of groups to create
19
- * @returns Array of node arrays (groups)
20
- *
21
- * @example
22
- * // For shell layout
23
- * const shells = groupNodes(graph, 'degree', 3);
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;