@intact-ebi/cytoscape-lumina 1.0.1 → 1.1.0
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/README.md +3 -1
- package/dist/lumina-layout.js +139 -141
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -107,6 +107,8 @@ Options are split by who uses them:
|
|
|
107
107
|
| --- | --- | --- |
|
|
108
108
|
| `weightExponent` | `2` | Stress weights are `w_ij = 1 / d_ij ^ weightExponent`; `2` is classic stress majorization. |
|
|
109
109
|
|
|
110
|
+
Stress shortest paths use weighted Dijkstra. Computed edge lengths (`idealLength` plus both virtual-node radii) must be finite and non-negative; invalid lengths throw a `RangeError`.
|
|
111
|
+
|
|
110
112
|
### Structure detection
|
|
111
113
|
|
|
112
114
|
| Option | Default | Description |
|
|
@@ -143,7 +145,7 @@ Set `stepByStep: true`, keep a reference to the layout, and run it:
|
|
|
143
145
|
const layout = cy.layout({ name: 'lumina', stepByStep: true });
|
|
144
146
|
layout.run();
|
|
145
147
|
|
|
146
|
-
layout.listSteps();
|
|
148
|
+
const steps = layout.listSteps(); // return all captured steps
|
|
147
149
|
layout.goToStep(3); // jump to a step
|
|
148
150
|
layout.nextStep();
|
|
149
151
|
layout.prevStep();
|
package/dist/lumina-layout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import FastPriorityQueue from 'fastpriorityqueue';
|
|
1
2
|
export const DEFAULT_PARAMS = {
|
|
2
3
|
layoutAlgorithm: 'stress',
|
|
3
4
|
// shared solver
|
|
@@ -110,6 +111,52 @@ class VEdge {
|
|
|
110
111
|
};
|
|
111
112
|
}
|
|
112
113
|
}
|
|
114
|
+
/** Dense stress distances from repeated weighted Dijkstra. */
|
|
115
|
+
function computeStressDistances(nodes, edges, idealLength) {
|
|
116
|
+
const numNodes = nodes.length;
|
|
117
|
+
const nodeIndexMap = new Map();
|
|
118
|
+
nodes.forEach((node, index) => nodeIndexMap.set(node.id, index));
|
|
119
|
+
const adjacency = Array.from({ length: numNodes }, () => []);
|
|
120
|
+
edges.forEach(edge => {
|
|
121
|
+
if (!edge.source || !edge.target) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const source = nodeIndexMap.get(edge.source.id);
|
|
125
|
+
const target = nodeIndexMap.get(edge.target.id);
|
|
126
|
+
if (source === undefined || target === undefined) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const distance = idealLength + edge.source.radius + edge.target.radius;
|
|
130
|
+
if (distance < 0 || !Number.isFinite(distance)) {
|
|
131
|
+
throw new RangeError('cytoscape-lumina: stress edge lengths must be finite and non-negative');
|
|
132
|
+
}
|
|
133
|
+
adjacency[source].push({ node: target, distance });
|
|
134
|
+
adjacency[target].push({ node: source, distance });
|
|
135
|
+
});
|
|
136
|
+
const distances = Array.from({ length: numNodes }, (_, index) => {
|
|
137
|
+
const row = new Array(numNodes).fill(Infinity);
|
|
138
|
+
row[index] = 0;
|
|
139
|
+
return row;
|
|
140
|
+
});
|
|
141
|
+
for (let source = 0; source < numNodes; source++) {
|
|
142
|
+
const row = distances[source];
|
|
143
|
+
const heap = new FastPriorityQueue((a, b) => a.distance < b.distance || (a.distance === b.distance && a.node < b.node));
|
|
144
|
+
heap.add({ node: source, distance: 0 });
|
|
145
|
+
let current;
|
|
146
|
+
while ((current = heap.poll()) !== undefined) {
|
|
147
|
+
if (current.distance !== row[current.node])
|
|
148
|
+
continue;
|
|
149
|
+
for (const neighbor of adjacency[current.node]) {
|
|
150
|
+
const candidate = current.distance + neighbor.distance;
|
|
151
|
+
if (candidate < row[neighbor.node]) {
|
|
152
|
+
row[neighbor.node] = candidate;
|
|
153
|
+
heap.add({ node: neighbor.node, distance: candidate });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return distances;
|
|
159
|
+
}
|
|
113
160
|
/**
|
|
114
161
|
* Register this layout as a Cytoscape.js layout extension.
|
|
115
162
|
*
|
|
@@ -203,18 +250,8 @@ LuminaLayout.prototype.captureStep = function (stepName, description, metadata)
|
|
|
203
250
|
};
|
|
204
251
|
this.steps.push(step);
|
|
205
252
|
this.currentStepIndex = this.steps.length - 1;
|
|
206
|
-
console.
|
|
253
|
+
console.debug(`[Step ${step.stepNumber}] ${stepName}: ${description}`);
|
|
207
254
|
};
|
|
208
|
-
/////////////// Check whether two node arrays are identical, ignoring order
|
|
209
|
-
function areNodesEqual(arr1, arr2) {
|
|
210
|
-
// 1. Different lengths mean they are definitely different
|
|
211
|
-
if (arr1.length !== arr2.length)
|
|
212
|
-
return false;
|
|
213
|
-
// 2. Extract all IDs and put them into a Set
|
|
214
|
-
const ids1 = new Set(arr1.map((node) => node.id()));
|
|
215
|
-
// 3. Check whether every ID in arr2 exists in the Set
|
|
216
|
-
return arr2.every(node => ids1.has(node.id()));
|
|
217
|
-
}
|
|
218
255
|
////////////////sum length of all edges ////////////////
|
|
219
256
|
function totalEdgeLength(edges) {
|
|
220
257
|
let total = 0;
|
|
@@ -293,6 +330,17 @@ function layoutRectangular(nodes, center, dirVector, rowSpacing = 60, colSpacing
|
|
|
293
330
|
}
|
|
294
331
|
LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
295
332
|
const params = this.params;
|
|
333
|
+
// Topology stays fixed during detection; structure markers are filtered dynamically.
|
|
334
|
+
const neighborhoodCache = new Map();
|
|
335
|
+
const neighborsOf = (node) => {
|
|
336
|
+
const id = node.id();
|
|
337
|
+
let neighbors = neighborhoodCache.get(id);
|
|
338
|
+
if (neighbors === undefined) {
|
|
339
|
+
neighbors = node.neighborhood().nodes();
|
|
340
|
+
neighborhoodCache.set(id, neighbors);
|
|
341
|
+
}
|
|
342
|
+
return neighbors;
|
|
343
|
+
};
|
|
296
344
|
// 0. Reset all markers
|
|
297
345
|
nodes.data('structType', 'Normal');
|
|
298
346
|
// nodes.data('structColor', '#999999');
|
|
@@ -304,7 +352,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
304
352
|
///////////////////////////// 2. detecting Cycle - by DFS algorithm /////////////////
|
|
305
353
|
const allCycles = []; // each sub-array represents an independent cycle
|
|
306
354
|
if (1) {
|
|
307
|
-
const normalNodes = nodes.toArray().filter((n) => n.data('structType') === 'Normal' && n
|
|
355
|
+
const normalNodes = nodes.toArray().filter((n) => n.data('structType') === 'Normal' && neighborsOf(n).length >= 2);
|
|
308
356
|
const seenCycles = new Set();
|
|
309
357
|
// To avoid duplicate cycles (like A-B-C and B-C-A), we sort and stringify for a check
|
|
310
358
|
normalNodes.forEach((startNode, startIndex) => {
|
|
@@ -312,7 +360,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
312
360
|
// This is a massive optimization to prevent finding the same cycle N times
|
|
313
361
|
const startId = startNode.id();
|
|
314
362
|
const findCycles = (u, parent, path) => {
|
|
315
|
-
const neighbors = u
|
|
363
|
+
const neighbors = neighborsOf(u).toArray().filter((n) => n.data('structType') === 'Normal');
|
|
316
364
|
if (neighbors.length < 8) { // If a node has too many neighbors, it is unlikely to be part of a cycle; this mainly improves efficiency and prevents the search from getting stuck here
|
|
317
365
|
for (const v of neighbors) {
|
|
318
366
|
const vId = v.id();
|
|
@@ -393,11 +441,10 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
393
441
|
});
|
|
394
442
|
circleIndex++;
|
|
395
443
|
});
|
|
396
|
-
console.log("num of cycles: ", filteredCycles.length);
|
|
397
444
|
const chains = [];
|
|
398
445
|
const processedNodeIds = new Set(); // avoid processing nodes more than once
|
|
399
446
|
// 2. Find all leaf nodes (Normal type with degree 1)
|
|
400
|
-
const leafNodes = nodes.filter((n) => n.data('structType') === 'Normal' && n
|
|
447
|
+
const leafNodes = nodes.filter((n) => n.data('structType') === 'Normal' && neighborsOf(n).length === 1);
|
|
401
448
|
let chainId = 0;
|
|
402
449
|
leafNodes.forEach((leaf) => {
|
|
403
450
|
if (processedNodeIds.has(leaf.id()))
|
|
@@ -410,8 +457,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
410
457
|
currentChainNodes.push(currentNode);
|
|
411
458
|
processedNodeIds.add(currentNode.id());
|
|
412
459
|
// Find the next neighbor
|
|
413
|
-
const neighbors = currentNode
|
|
414
|
-
.neighborhood('node')
|
|
460
|
+
const neighbors = neighborsOf(currentNode)
|
|
415
461
|
.filter((n) => n.id() !== currentNode.id() &&
|
|
416
462
|
n.data('structType') === 'Normal' &&
|
|
417
463
|
!processedNodeIds.has(n.id()));
|
|
@@ -420,8 +466,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
420
466
|
// 2. The neighbor must not have a high degree (if degree > 2, a branching point has been reached and the chain ends)
|
|
421
467
|
if (neighbors.length === 1) {
|
|
422
468
|
const nextNode = neighbors[0];
|
|
423
|
-
const nextNodeNeighborCount = nextNode
|
|
424
|
-
.neighborhood('node')
|
|
469
|
+
const nextNodeNeighborCount = neighborsOf(nextNode)
|
|
425
470
|
.filter((n) => n.id() !== nextNode.id())
|
|
426
471
|
.length;
|
|
427
472
|
if (nextNodeNeighborCount > 2) {
|
|
@@ -475,11 +520,11 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
475
520
|
///////////////////////////////// Star structure ///////////
|
|
476
521
|
let starIndex = 0;
|
|
477
522
|
nodes.forEach((node) => {
|
|
478
|
-
const neighbors = node
|
|
523
|
+
const neighbors = neighborsOf(node);
|
|
479
524
|
// Number of distinct neighbors
|
|
480
525
|
const neighborCount = neighbors.length;
|
|
481
526
|
// Check whether the neighbors consist only of distinct neighbor nodes
|
|
482
|
-
const leafNeighbors = neighbors.filter((n) => n
|
|
527
|
+
const leafNeighbors = neighbors.filter((n) => neighborsOf(n).length === 1);
|
|
483
528
|
if (leafNeighbors.length >= params.minStarLeaves && neighborCount >= params.minStarLeaves) {
|
|
484
529
|
if (node.data('structType') !== 'Cycle' && node.data('structType') !== 'Chain') {
|
|
485
530
|
const groupId = 'Star_' + starIndex;
|
|
@@ -508,56 +553,57 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
508
553
|
}
|
|
509
554
|
});
|
|
510
555
|
/////////////////////////////// Find parallel/diamond structures ///////////////////////////////////
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const u1 = u.neighborhood().nodes();
|
|
518
|
-
if (u.data('structType') === 'Normal') {
|
|
519
|
-
for (let j = 0; j < nodes.length; j++) {
|
|
520
|
-
if (i === j)
|
|
521
|
-
continue;
|
|
522
|
-
const v = nodes[j];
|
|
523
|
-
if (v.data('structType') === 'Normal') {
|
|
524
|
-
const v1 = v.neighborhood().nodes();
|
|
525
|
-
if (areNodesEqual(v1, u1) && v1.length >= params.minParallelNeighbors && u1.length >= params.minParallelNeighbors) {
|
|
526
|
-
if (nodeVecParallel.length === 0) {
|
|
527
|
-
nodeVecParallel.push(u.id());
|
|
528
|
-
}
|
|
529
|
-
nodeVecParallel.push(v.id());
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
556
|
+
const nodesById = new Map();
|
|
557
|
+
const parallelGroups = new Map();
|
|
558
|
+
nodes.forEach((node) => {
|
|
559
|
+
nodesById.set(node.id(), node);
|
|
560
|
+
if (node.data("structType") !== "Normal") {
|
|
561
|
+
return;
|
|
533
562
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
});
|
|
550
|
-
u1.forEach((v1) => {
|
|
551
|
-
nodes.forEach((node) => {
|
|
552
|
-
if (node.id() == v1.id()) {
|
|
553
|
-
node.data('parallelGroupIdVec', [...node.data('parallelGroupIdVec'), 'Parallel' + parallelId]);
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
parallelId++;
|
|
563
|
+
const neighbors = neighborsOf(node);
|
|
564
|
+
if (!(neighbors.length >= params.minParallelNeighbors)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Encode a sorted copy so IDs cannot collide and cached neighbor order stays intact.
|
|
568
|
+
const signature = JSON.stringify(neighbors.map((neighbor) => neighbor.id()).sort());
|
|
569
|
+
const group = parallelGroups.get(signature);
|
|
570
|
+
if (group) {
|
|
571
|
+
group.push(node);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
parallelGroups.set(signature, [node]);
|
|
558
575
|
}
|
|
576
|
+
});
|
|
577
|
+
// Map insertion order preserves group numbering by the first eligible member.
|
|
578
|
+
let parallelId = 0;
|
|
579
|
+
for (const group of parallelGroups.values()) {
|
|
580
|
+
if (group.length < 2)
|
|
581
|
+
continue;
|
|
582
|
+
const groupId = "Parallel" + parallelId;
|
|
583
|
+
group.forEach((node) => {
|
|
584
|
+
node.addClass("substructure-parallel");
|
|
585
|
+
node.data("structType", "Parallel");
|
|
586
|
+
node.data("groupId", groupId);
|
|
587
|
+
node.data("structs", {
|
|
588
|
+
...node.data("structs"),
|
|
589
|
+
Parallel: {
|
|
590
|
+
color: "#50C878",
|
|
591
|
+
groupId: `Parallel_${parallelId}`,
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
neighborsOf(group[0]).forEach((neighbor) => {
|
|
596
|
+
// Neighborhoods may include nodes outside the supplied collection.
|
|
597
|
+
const node = nodesById.get(neighbor.id());
|
|
598
|
+
if (node) {
|
|
599
|
+
node.data("parallelGroupIdVec", [
|
|
600
|
+
...node.data("parallelGroupIdVec"),
|
|
601
|
+
groupId,
|
|
602
|
+
]);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
parallelId++;
|
|
559
606
|
}
|
|
560
|
-
console.log("num of chains: ", chains.length);
|
|
561
607
|
};
|
|
562
608
|
LuminaLayout.prototype.run = function () {
|
|
563
609
|
this.stopped = false;
|
|
@@ -583,7 +629,6 @@ LuminaLayout.prototype.run = function () {
|
|
|
583
629
|
components.forEach((component, index) => {
|
|
584
630
|
this.vnodes = [];
|
|
585
631
|
this.vedges = [];
|
|
586
|
-
console.log(`independent net: ${index + 1}`);
|
|
587
632
|
// 1. get user defined boundingBox,if not then default cy container
|
|
588
633
|
const bb = this.boundingBox || this.cy.extent();
|
|
589
634
|
const width = bb.x2 - bb.x1;
|
|
@@ -612,8 +657,6 @@ LuminaLayout.prototype.run = function () {
|
|
|
612
657
|
});
|
|
613
658
|
});
|
|
614
659
|
}
|
|
615
|
-
console.log("num of nodes: " + nodes.length);
|
|
616
|
-
console.log("num of edges: " + edgeSet.length);
|
|
617
660
|
// Capture initial state
|
|
618
661
|
this.captureStep('Initial', 'Initial node positions before any layout', {
|
|
619
662
|
nodeCount: nodes.length, edgeCount: edgeSet.length
|
|
@@ -1089,6 +1132,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1089
1132
|
numOfCollision = 0;
|
|
1090
1133
|
const padding = params.collisionPadding;
|
|
1091
1134
|
let iter = 0;
|
|
1135
|
+
let overlapLimitReached = false;
|
|
1092
1136
|
while (colisionFlag) {
|
|
1093
1137
|
if (this.stopped) {
|
|
1094
1138
|
break;
|
|
@@ -1131,12 +1175,17 @@ LuminaLayout.prototype.run = function () {
|
|
|
1131
1175
|
}
|
|
1132
1176
|
// [Optimization 3] Safety valve: exit directly if the iteration count becomes too large (typically when nodes are extremely dense)
|
|
1133
1177
|
if (numOfCollision > 10000) {
|
|
1134
|
-
|
|
1178
|
+
overlapLimitReached = true;
|
|
1135
1179
|
break;
|
|
1136
1180
|
}
|
|
1137
|
-
// this.captureStep('Anti-collision', 'Eliminate all collisions ' + iter, {iterations});
|
|
1138
1181
|
}
|
|
1139
|
-
this.captureStep(
|
|
1182
|
+
this.captureStep("Anti-collision", overlapLimitReached
|
|
1183
|
+
? "Overlap removal stopped at safety limit"
|
|
1184
|
+
: "Eliminate all collisions", {
|
|
1185
|
+
iterations,
|
|
1186
|
+
overlapIterations: iter,
|
|
1187
|
+
overlapLimitReached,
|
|
1188
|
+
});
|
|
1140
1189
|
if (1) {
|
|
1141
1190
|
numOfCollision = 0;
|
|
1142
1191
|
for (let i = 0; i < this.vnodes.length; i++) {
|
|
@@ -1169,8 +1218,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1169
1218
|
if (numNodes === 0)
|
|
1170
1219
|
return;
|
|
1171
1220
|
// Index mapping: ID -> array index, for matrix operations
|
|
1172
|
-
|
|
1173
|
-
this.vnodes.forEach((v, i) => nodeIndexMap.set(v.id, i));
|
|
1221
|
+
// The distance helper builds the index alongside its weighted adjacency.
|
|
1174
1222
|
// Extract the adjacency list for reuse during initialization
|
|
1175
1223
|
const adj = new Map();
|
|
1176
1224
|
this.vedges.forEach((e) => {
|
|
@@ -1184,34 +1232,10 @@ LuminaLayout.prototype.run = function () {
|
|
|
1184
1232
|
adj.get(e.target.id).push(e.source);
|
|
1185
1233
|
});
|
|
1186
1234
|
// -------------------------------------------------------------
|
|
1187
|
-
// [Preprocessing 1] Calculate
|
|
1235
|
+
// [Preprocessing 1] Calculate all-pairs shortest paths using weighted Dijkstra
|
|
1188
1236
|
// -------------------------------------------------------------
|
|
1189
|
-
const distMatrix =
|
|
1237
|
+
const distMatrix = computeStressDistances(this.vnodes, this.vedges, idealLength);
|
|
1190
1238
|
const weightMatrix = Array.from({ length: numNodes }, () => new Array(numNodes).fill(0));
|
|
1191
|
-
for (let i = 0; i < numNodes; i++)
|
|
1192
|
-
distMatrix[i][i] = 0;
|
|
1193
|
-
// Assign the ideal distance baseline based on real edges
|
|
1194
|
-
this.vedges.forEach((e) => {
|
|
1195
|
-
if (!e.source || !e.target)
|
|
1196
|
-
return;
|
|
1197
|
-
const u = nodeIndexMap.get(e.source.id);
|
|
1198
|
-
const v = nodeIndexMap.get(e.target.id);
|
|
1199
|
-
if (u !== undefined && v !== undefined) {
|
|
1200
|
-
const idealDist = idealLength + e.source.radius + e.target.radius;
|
|
1201
|
-
distMatrix[u][v] = Math.min(distMatrix[u][v], idealDist);
|
|
1202
|
-
distMatrix[v][u] = Math.min(distMatrix[v][u], idealDist);
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
// Use Floyd-Warshall to compute shortest paths between all node pairs
|
|
1206
|
-
for (let k = 0; k < numNodes; k++) {
|
|
1207
|
-
for (let i = 0; i < numNodes; i++) {
|
|
1208
|
-
for (let j = 0; j < numNodes; j++) {
|
|
1209
|
-
if (distMatrix[i][k] + distMatrix[k][j] < distMatrix[i][j]) {
|
|
1210
|
-
distMatrix[i][j] = distMatrix[i][k] + distMatrix[k][j];
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
1239
|
// Calculate the weight matrix W_ij = 1 / (d_ij ^ weightExponent)
|
|
1216
1240
|
for (let i = 0; i < numNodes; i++) {
|
|
1217
1241
|
for (let j = 0; j < numNodes; j++) {
|
|
@@ -1321,6 +1345,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1321
1345
|
let numOfCollision = 0;
|
|
1322
1346
|
const padding = params.collisionPadding;
|
|
1323
1347
|
let overlapIter = 0;
|
|
1348
|
+
let overlapLimitReached = false;
|
|
1324
1349
|
while (colisionFlag) {
|
|
1325
1350
|
if (this.stopped) {
|
|
1326
1351
|
break;
|
|
@@ -1354,11 +1379,17 @@ LuminaLayout.prototype.run = function () {
|
|
|
1354
1379
|
}
|
|
1355
1380
|
}
|
|
1356
1381
|
if (numOfCollision > 10000 || overlapIter > params.maxOverlapIterations) {
|
|
1357
|
-
|
|
1382
|
+
overlapLimitReached = true;
|
|
1358
1383
|
break;
|
|
1359
1384
|
}
|
|
1360
1385
|
}
|
|
1361
|
-
this.captureStep(
|
|
1386
|
+
this.captureStep("Anti-collision", overlapLimitReached
|
|
1387
|
+
? "Overlap removal stopped at safety limit"
|
|
1388
|
+
: "Eliminate all collisions", {
|
|
1389
|
+
iterations,
|
|
1390
|
+
overlapIterations: overlapIter,
|
|
1391
|
+
overlapLimitReached,
|
|
1392
|
+
});
|
|
1362
1393
|
}
|
|
1363
1394
|
this.captureStep('Virtual Node Layout', 'Stress Majorization layout applied to virtual nodes', { iterations });
|
|
1364
1395
|
}
|
|
@@ -1670,34 +1701,6 @@ LuminaLayout.prototype.run = function () {
|
|
|
1670
1701
|
nodes[2].position().y = 90;
|
|
1671
1702
|
}
|
|
1672
1703
|
rotateNetworkToMinimumBoundingBox(nodes);
|
|
1673
|
-
if (1) {
|
|
1674
|
-
let maxDis = 0;
|
|
1675
|
-
let maxS = 0;
|
|
1676
|
-
let maxT = 0;
|
|
1677
|
-
let minDist = 10e10;
|
|
1678
|
-
let minS = 0;
|
|
1679
|
-
let minT = 0;
|
|
1680
|
-
let avgDis = 0;
|
|
1681
|
-
edgeSet.forEach((e) => {
|
|
1682
|
-
const s = e.source();
|
|
1683
|
-
const t = e.target();
|
|
1684
|
-
const distance = Math.sqrt(Math.pow(s.position().x - t.position().x, 2) + Math.pow(s.position().y - t.position().y, 2));
|
|
1685
|
-
if (maxDis < distance) {
|
|
1686
|
-
maxDis = distance;
|
|
1687
|
-
maxS = s.id();
|
|
1688
|
-
maxT = t.id();
|
|
1689
|
-
}
|
|
1690
|
-
if (minDist > distance) {
|
|
1691
|
-
minDist = distance;
|
|
1692
|
-
minS = s.id();
|
|
1693
|
-
minT = t.id();
|
|
1694
|
-
}
|
|
1695
|
-
avgDis += distance;
|
|
1696
|
-
});
|
|
1697
|
-
console.log("maxDis:", maxDis, " ", maxS, "->", maxT);
|
|
1698
|
-
console.log("minDist:", minDist, " ", minS, "->", minT);
|
|
1699
|
-
console.log("avgDis:", avgDis / edgeSet.length);
|
|
1700
|
-
}
|
|
1701
1704
|
const bb2 = nodes.boundingBox();
|
|
1702
1705
|
networkInfos.push({
|
|
1703
1706
|
component: component,
|
|
@@ -2289,7 +2292,7 @@ LuminaLayout.prototype.goToStep = function (stepIndex) {
|
|
|
2289
2292
|
});
|
|
2290
2293
|
// Visualize virtual nodes if they exist at this step
|
|
2291
2294
|
this.visualizeVirtualNodes(step);
|
|
2292
|
-
console.
|
|
2295
|
+
console.debug(`[Step ${step.stepNumber}/${this.steps.length - 1}] ${step.stepName}: ${step.description}`);
|
|
2293
2296
|
this.cy.fit(null, 50);
|
|
2294
2297
|
return this;
|
|
2295
2298
|
};
|
|
@@ -2301,7 +2304,7 @@ LuminaLayout.prototype.nextStep = function () {
|
|
|
2301
2304
|
return this.goToStep(this.currentStepIndex + 1);
|
|
2302
2305
|
}
|
|
2303
2306
|
else {
|
|
2304
|
-
console.
|
|
2307
|
+
console.debug("Already at the last step");
|
|
2305
2308
|
return this;
|
|
2306
2309
|
}
|
|
2307
2310
|
};
|
|
@@ -2313,7 +2316,7 @@ LuminaLayout.prototype.prevStep = function () {
|
|
|
2313
2316
|
return this.goToStep(this.currentStepIndex - 1);
|
|
2314
2317
|
}
|
|
2315
2318
|
else {
|
|
2316
|
-
console.
|
|
2319
|
+
console.debug("Already at the first step");
|
|
2317
2320
|
return this;
|
|
2318
2321
|
}
|
|
2319
2322
|
};
|
|
@@ -2322,14 +2325,9 @@ LuminaLayout.prototype.prevStep = function () {
|
|
|
2322
2325
|
*/
|
|
2323
2326
|
LuminaLayout.prototype.listSteps = function () {
|
|
2324
2327
|
if (!this.params.stepByStep || this.steps.length === 0) {
|
|
2325
|
-
console.
|
|
2328
|
+
console.debug("No steps available");
|
|
2326
2329
|
return [];
|
|
2327
2330
|
}
|
|
2328
|
-
console.log(`Total steps: ${this.steps.length}`);
|
|
2329
|
-
this.steps.forEach((step, index) => {
|
|
2330
|
-
const current = index === this.currentStepIndex ? ' ← CURRENT' : '';
|
|
2331
|
-
console.log(` [${index}] ${step.stepName}: ${step.description}${current}`);
|
|
2332
|
-
});
|
|
2333
2331
|
return this.steps.map((s) => ({
|
|
2334
2332
|
stepNumber: s.stepNumber,
|
|
2335
2333
|
stepName: s.stepName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intact-ebi/cytoscape-lumina",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "LUMINA: a structure-aware Cytoscape.js layout that detects cycles, chains, stars and parallel groups and lays them out with force-directed or stress-majorization solvers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"registry": "https://registry.npmjs.org/"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "
|
|
30
|
-
"
|
|
29
|
+
"build": "npm run build:lib && npm run build:browser && cp demo/favicon.ico dist/favicon.ico && cp demo/index.html dist/index.html && cp node_modules/cytoscape/dist/cytoscape.min.js dist/ && if [ -d demo/data ]; then cp -r demo/data dist/data; fi && touch dist/.nojekyll",
|
|
30
|
+
"build:browser": "esbuild src/index.ts --bundle --format=esm --platform=browser --target=es2020 --external:cytoscape --outfile=dist/lumina-browser.js",
|
|
31
|
+
"dev": "npm run build && concurrently \"tsc --watch\" \"npm run build:browser -- --watch\" \"node watch.cjs\" \"http-server dist -p 8080 -c-1\"",
|
|
31
32
|
"deploy": "./deploy.sh",
|
|
32
33
|
"build:lib": "tsc",
|
|
33
34
|
"local-publish": "npm run build:lib && yalc push",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"concurrently": "^9.2.4",
|
|
39
40
|
"cytoscape": "^3.33.1",
|
|
41
|
+
"esbuild": "^0.28.2",
|
|
40
42
|
"gh-pages": "^6.3.0",
|
|
41
43
|
"http-server": "^14.1.1",
|
|
42
44
|
"typescript": "^5.9.3",
|
|
@@ -65,5 +67,8 @@
|
|
|
65
67
|
},
|
|
66
68
|
"allowScripts": {
|
|
67
69
|
"fsevents@2.3.3": true
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"fastpriorityqueue": "^0.8.0"
|
|
68
73
|
}
|
|
69
|
-
}
|
|
74
|
+
}
|