@intact-ebi/cytoscape-lumina 1.0.0 → 1.0.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.
- package/README.md +1 -6
- package/dist/lumina-layout.d.ts +61 -69
- package/dist/lumina-layout.js +121 -209
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Options are split by who uses them:
|
|
|
71
71
|
- **Force-only** options go under `force: { ... }` and are ignored when `layoutAlgorithm` is `'stress'`.
|
|
72
72
|
- **Stress-only** options go under `stress: { ... }` and are ignored when `layoutAlgorithm` is `'force'`.
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
`DEFAULT_PARAMS` and `resolveParams` are exported for consumers that need the complete resolved configuration.
|
|
75
75
|
|
|
76
76
|
### Algorithm
|
|
77
77
|
|
|
@@ -113,7 +113,6 @@ Internally these map to upper-case constants (e.g. `idealLength` → `IDEAL_LENG
|
|
|
113
113
|
| --- | --- | --- |
|
|
114
114
|
| `minStarLeaves` | `3` | Minimum leaf neighbours for a node to become a star centre. |
|
|
115
115
|
| `minCycleLength` | `3` | Minimum number of nodes in a detected cycle. |
|
|
116
|
-
| `maxCycleLength` | `30` | Reserved. Currently **not enforced** by the cycle search. |
|
|
117
116
|
| `minChainLength` | `2` | Minimum number of nodes in a chain. |
|
|
118
117
|
| `minParallelNeighbors` | `2` | Minimum shared neighbours for nodes to count as a parallel group. |
|
|
119
118
|
|
|
@@ -136,10 +135,6 @@ Internally these map to upper-case constants (e.g. `idealLength` → `IDEAL_LENG
|
|
|
136
135
|
| `substructureLayout` | `false` | When `false` (default), each substructure is arranged internally (circle, rings, grid, ...). When `true`, that step is skipped. |
|
|
137
136
|
| `stepByStep` | `false` | Record a snapshot after each stage (see below). |
|
|
138
137
|
|
|
139
|
-
### Backward compatibility
|
|
140
|
-
|
|
141
|
-
The old flat force options (`repulsion`, `springK`, `useAngularForce`, `angularStrength`) and the legacy upper-case `params: { ... }` object are still accepted. Nested `force` / `stress` objects take priority.
|
|
142
|
-
|
|
143
138
|
## Step-by-step debugging
|
|
144
139
|
|
|
145
140
|
Set `stepByStep: true`, keep a reference to the layout, and run it:
|
package/dist/lumina-layout.d.ts
CHANGED
|
@@ -1,78 +1,66 @@
|
|
|
1
1
|
import cytoscape from 'cytoscape';
|
|
2
2
|
export type LayoutAlgorithm = 'force' | 'stress';
|
|
3
|
-
/**
|
|
4
|
-
export interface
|
|
5
|
-
|
|
3
|
+
/** Options used only by the force-directed solver. */
|
|
4
|
+
export interface LuminaForceOptions {
|
|
5
|
+
/** Node-node repulsion strength. */
|
|
6
|
+
repulsion?: number;
|
|
7
|
+
/** Spring stiffness of edges. */
|
|
8
|
+
springK?: number;
|
|
9
|
+
/** Multiplier applied to repulsion for overlapping virtual nodes. */
|
|
10
|
+
overlapRepulsionFactor?: number;
|
|
11
|
+
/** Exponent used by the simulated-annealing cooling function. */
|
|
12
|
+
coolingExponent?: number;
|
|
13
|
+
/** Whether to spread leaf virtual nodes evenly around their hub. */
|
|
14
|
+
useAngularForce?: boolean;
|
|
15
|
+
angularStrength?: number;
|
|
16
|
+
angularMaxForce?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Options used only by the stress-majorization solver. */
|
|
19
|
+
export interface LuminaStressOptions {
|
|
20
|
+
/** Exponent in the stress weight formula: w = 1 / distance ^ exponent. */
|
|
21
|
+
weightExponent?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Public LUMINA options, excluding Cytoscape's required layout name. */
|
|
24
|
+
export interface LuminaOptions {
|
|
25
|
+
layoutAlgorithm?: LayoutAlgorithm;
|
|
6
26
|
/** Ideal surface-to-surface gap between connected virtual nodes
|
|
7
27
|
* (force: spring rest length; stress: base of the target distance;
|
|
8
|
-
* both: radius of the initial radial placement of leaf
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
SPREAD_V_NODES: boolean;
|
|
31
|
-
SUBSTRUCTURE_LAYOUT: boolean;
|
|
32
|
-
STEP_BY_STEP: boolean;
|
|
28
|
+
* both: radius of the initial radial placement of leaf virtual nodes). */
|
|
29
|
+
idealLength?: number;
|
|
30
|
+
iterations?: number;
|
|
31
|
+
convergenceThreshold?: number;
|
|
32
|
+
/** Extra gap in the anti-collision pass. Null uses `idealLength`. */
|
|
33
|
+
collisionPadding?: number | null;
|
|
34
|
+
maxOverlapIterations?: number;
|
|
35
|
+
minStarLeaves?: number;
|
|
36
|
+
minCycleLength?: number;
|
|
37
|
+
minChainLength?: number;
|
|
38
|
+
minParallelNeighbors?: number;
|
|
39
|
+
cycleNodeSpacing?: number;
|
|
40
|
+
starRingSpacing?: number;
|
|
41
|
+
starBaseNodesPerRing?: number;
|
|
42
|
+
chainMinRadius?: number;
|
|
43
|
+
leafNodeDistance?: number;
|
|
44
|
+
randomizeInitialPositions?: boolean;
|
|
45
|
+
spreadVNodes?: boolean;
|
|
46
|
+
substructureLayout?: boolean;
|
|
47
|
+
stepByStep?: boolean;
|
|
48
|
+
force?: LuminaForceOptions;
|
|
49
|
+
stress?: LuminaStressOptions;
|
|
33
50
|
}
|
|
34
|
-
/**
|
|
35
|
-
export interface
|
|
36
|
-
|
|
37
|
-
REPULSION: number;
|
|
38
|
-
/** Spring stiffness of edges */
|
|
39
|
-
SPRING_K: number;
|
|
40
|
-
/** Overlapping vnodes repel REPULSION * this factor harder */
|
|
41
|
-
OVERLAP_REPULSION_FACTOR: number;
|
|
42
|
-
/** Simulated-annealing cooling: cooling = (1 - iter/ITERATIONS) ^ this */
|
|
43
|
-
COOLING_EXPONENT: number;
|
|
44
|
-
/** Angular force that spreads leaf vnodes evenly around a hub */
|
|
45
|
-
USE_ANGULAR_FORCE: boolean;
|
|
46
|
-
ANGULAR_STRENGTH: number;
|
|
47
|
-
ANGULAR_MAX_FORCE: number;
|
|
51
|
+
/** Options passed to `cy.layout()` for the LUMINA layout. */
|
|
52
|
+
export interface LuminaLayoutOptions extends LuminaOptions, cytoscape.BaseLayoutOptions {
|
|
53
|
+
name: 'lumina' | 'substructure-layout';
|
|
48
54
|
}
|
|
49
|
-
/**
|
|
50
|
-
export interface
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
/** Fully resolved options used internally by the layout. */
|
|
56
|
+
export interface LuminaParameters extends Required<Omit<LuminaOptions, 'collisionPadding' | 'force' | 'stress'>> {
|
|
57
|
+
collisionPadding: number;
|
|
58
|
+
force: Required<LuminaForceOptions>;
|
|
59
|
+
stress: Required<LuminaStressOptions>;
|
|
53
60
|
}
|
|
54
|
-
export
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
export declare const DEFAULT_PARAMS: LayoutParameters;
|
|
59
|
-
/**
|
|
60
|
-
* Build the final parameters from Cytoscape layout options.
|
|
61
|
-
*
|
|
62
|
-
* cy.layout({
|
|
63
|
-
* name: 'lumina',
|
|
64
|
-
* layoutAlgorithm: 'force',
|
|
65
|
-
* idealLength: 100, // shared
|
|
66
|
-
* iterations: 600, // shared
|
|
67
|
-
* force: { repulsion: 300, springK: 0.15, useAngularForce: true },
|
|
68
|
-
* stress: { weightExponent: 2 }
|
|
69
|
-
* })
|
|
70
|
-
*
|
|
71
|
-
* Backward compatibility: the old flat force options (repulsion, springK,
|
|
72
|
-
* angularStrength, useAngularForce) and the legacy `params` object (flat,
|
|
73
|
-
* upper-case) are still accepted. Nested `force` / `stress` take priority.
|
|
74
|
-
*/
|
|
75
|
-
export declare function resolveParams(options: any): LayoutParameters;
|
|
61
|
+
export declare const DEFAULT_PARAMS: LuminaParameters;
|
|
62
|
+
/** Resolve optional public settings into the complete internal configuration. */
|
|
63
|
+
export declare function resolveParams(options?: LuminaOptions): LuminaParameters;
|
|
76
64
|
export interface LayoutStep {
|
|
77
65
|
stepNumber: number;
|
|
78
66
|
stepName: string;
|
|
@@ -152,4 +140,8 @@ export { LuminaLayout, LuminaLayout as SubstructureLayout };
|
|
|
152
140
|
/**
|
|
153
141
|
* Enhanced force-directed layout with structure awareness
|
|
154
142
|
*/
|
|
155
|
-
declare function LuminaLayout(this: any, options:
|
|
143
|
+
declare function LuminaLayout(this: any, options: LuminaLayoutOptions & {
|
|
144
|
+
cy?: cytoscape.Core;
|
|
145
|
+
eles?: cytoscape.CollectionReturnValue;
|
|
146
|
+
boundingBox?: cytoscape.BoundingBox12 | cytoscape.BoundingBoxWH;
|
|
147
|
+
}): void;
|
package/dist/lumina-layout.js
CHANGED
|
@@ -1,143 +1,60 @@
|
|
|
1
1
|
export const DEFAULT_PARAMS = {
|
|
2
|
-
|
|
2
|
+
layoutAlgorithm: 'stress',
|
|
3
3
|
// shared solver
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// structure
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
MIN_PARALLEL_NEIGHBORS: 2,
|
|
4
|
+
idealLength: 100,
|
|
5
|
+
iterations: 600,
|
|
6
|
+
convergenceThreshold: 0.5,
|
|
7
|
+
collisionPadding: 100,
|
|
8
|
+
maxOverlapIterations: 300,
|
|
9
|
+
// structure deteciton
|
|
10
|
+
minStarLeaves: 3,
|
|
11
|
+
minCycleLength: 3,
|
|
12
|
+
minChainLength: 2,
|
|
13
|
+
minParallelNeighbors: 2,
|
|
15
14
|
// substructure spacing
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
STEP_BY_STEP: false,
|
|
15
|
+
cycleNodeSpacing: 100,
|
|
16
|
+
starRingSpacing: 100,
|
|
17
|
+
starBaseNodesPerRing: 6,
|
|
18
|
+
chainMinRadius: 150,
|
|
19
|
+
leafNodeDistance: 200,
|
|
20
|
+
randomizeInitialPositions: false,
|
|
21
|
+
spreadVNodes: true,
|
|
22
|
+
substructureLayout: false,
|
|
23
|
+
stepByStep: false,
|
|
26
24
|
force: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
repulsion: 300,
|
|
26
|
+
springK: 0.15,
|
|
27
|
+
overlapRepulsionFactor: 5,
|
|
28
|
+
coolingExponent: 2,
|
|
29
|
+
useAngularForce: false,
|
|
30
|
+
angularStrength: 0.2,
|
|
31
|
+
angularMaxForce: 2.0
|
|
34
32
|
},
|
|
35
33
|
stress: {
|
|
36
|
-
|
|
34
|
+
weightExponent: 2
|
|
37
35
|
}
|
|
38
36
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
iterations: 'ITERATIONS',
|
|
46
|
-
convergenceThreshold: 'CONVERGENCE_THRESHOLD',
|
|
47
|
-
collisionPadding: 'COLLISION_PADDING',
|
|
48
|
-
maxOverlapIterations: 'MAX_OVERLAP_ITERATIONS',
|
|
49
|
-
minStarLeaves: 'MIN_STAR_LEAVES',
|
|
50
|
-
minCycleLength: 'MIN_CYCLE_LENGTH',
|
|
51
|
-
maxCycleLength: 'MAX_CYCLE_LENGTH',
|
|
52
|
-
minChainLength: 'MIN_CHAIN_LENGTH',
|
|
53
|
-
minParallelNeighbors: 'MIN_PARALLEL_NEIGHBORS',
|
|
54
|
-
cycleNodeSpacing: 'CYCLE_NODE_SPACING',
|
|
55
|
-
starRingSpacing: 'STAR_RING_SPACING',
|
|
56
|
-
starBaseNodesPerRing: 'STAR_BASE_NODES_PER_RING',
|
|
57
|
-
chainMinRadius: 'CHAIN_MIN_RADIUS',
|
|
58
|
-
leafNodeDistance: 'LEAF_NODE_DISTANCE',
|
|
59
|
-
randomizeInitialPositions: 'RANDOMIZE_INITIAL_POSITIONS',
|
|
60
|
-
spreadVNodes: 'SPREAD_V_NODES',
|
|
61
|
-
substructureLayout: 'SUBSTRUCTURE_LAYOUT',
|
|
62
|
-
stepByStep: 'STEP_BY_STEP'
|
|
63
|
-
};
|
|
64
|
-
const FORCE_KEYS = {
|
|
65
|
-
repulsion: 'REPULSION',
|
|
66
|
-
springK: 'SPRING_K',
|
|
67
|
-
overlapRepulsionFactor: 'OVERLAP_REPULSION_FACTOR',
|
|
68
|
-
coolingExponent: 'COOLING_EXPONENT',
|
|
69
|
-
useAngularForce: 'USE_ANGULAR_FORCE',
|
|
70
|
-
angularStrength: 'ANGULAR_STRENGTH',
|
|
71
|
-
angularMaxForce: 'ANGULAR_MAX_FORCE'
|
|
72
|
-
};
|
|
73
|
-
const STRESS_KEYS = {
|
|
74
|
-
weightExponent: 'WEIGHT_EXPONENT'
|
|
75
|
-
};
|
|
76
|
-
function pick(src, map) {
|
|
77
|
-
const out = {};
|
|
78
|
-
if (!src)
|
|
79
|
-
return out;
|
|
80
|
-
for (const pub of Object.keys(map)) {
|
|
81
|
-
if (src[pub] !== undefined)
|
|
82
|
-
out[map[pub]] = src[pub];
|
|
37
|
+
function applyDefaults(defaults, overrides) {
|
|
38
|
+
const resolved = { ...defaults };
|
|
39
|
+
for (const key of Object.keys(defaults)) {
|
|
40
|
+
if (overrides?.[key] !== undefined) {
|
|
41
|
+
resolved[key] = overrides[key];
|
|
42
|
+
}
|
|
83
43
|
}
|
|
84
|
-
return
|
|
44
|
+
return resolved;
|
|
85
45
|
}
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
* Backward compatibility: the old flat force options (repulsion, springK,
|
|
99
|
-
* angularStrength, useAngularForce) and the legacy `params` object (flat,
|
|
100
|
-
* upper-case) are still accepted. Nested `force` / `stress` take priority.
|
|
101
|
-
*/
|
|
102
|
-
export function resolveParams(options) {
|
|
103
|
-
options = options || {};
|
|
104
|
-
const legacy = options.params || {};
|
|
105
|
-
// flat legacy force options at top level
|
|
106
|
-
const legacyForce = pick(options, FORCE_KEYS);
|
|
107
|
-
// legacy upper-case flat `params`: route each key to the right group
|
|
108
|
-
const legacyCommon = {};
|
|
109
|
-
const legacyForceUpper = {};
|
|
110
|
-
const legacyStressUpper = {};
|
|
111
|
-
Object.keys(legacy).forEach((k) => {
|
|
112
|
-
if (k in DEFAULT_PARAMS.force)
|
|
113
|
-
legacyForceUpper[k] = legacy[k];
|
|
114
|
-
else if (k in DEFAULT_PARAMS.stress)
|
|
115
|
-
legacyStressUpper[k] = legacy[k];
|
|
116
|
-
else if (k !== 'force' && k !== 'stress')
|
|
117
|
-
legacyCommon[k] = legacy[k];
|
|
118
|
-
});
|
|
119
|
-
const params = {
|
|
120
|
-
...DEFAULT_PARAMS,
|
|
121
|
-
...legacyCommon,
|
|
122
|
-
...pick(options, COMMON_KEYS),
|
|
123
|
-
force: {
|
|
124
|
-
...DEFAULT_PARAMS.force,
|
|
125
|
-
...legacyForceUpper,
|
|
126
|
-
...(legacy.force || {}),
|
|
127
|
-
...legacyForce,
|
|
128
|
-
...pick(options.force, FORCE_KEYS)
|
|
129
|
-
},
|
|
130
|
-
stress: {
|
|
131
|
-
...DEFAULT_PARAMS.stress,
|
|
132
|
-
...legacyStressUpper,
|
|
133
|
-
...(legacy.stress || {}),
|
|
134
|
-
...pick(options.stress, STRESS_KEYS)
|
|
135
|
-
}
|
|
46
|
+
/** Resolve optional public settings into the complete internal configuration. */
|
|
47
|
+
export function resolveParams(options = {}) {
|
|
48
|
+
const { collisionPadding: _defaultCollisionPadding, force: defaultForce, stress: defaultStress, ...commonDefaults } = DEFAULT_PARAMS;
|
|
49
|
+
const common = applyDefaults(commonDefaults, options);
|
|
50
|
+
const idealLength = options.idealLength ?? common.idealLength;
|
|
51
|
+
return {
|
|
52
|
+
...common,
|
|
53
|
+
idealLength,
|
|
54
|
+
collisionPadding: options.collisionPadding ?? idealLength,
|
|
55
|
+
force: applyDefaults(defaultForce, options.force),
|
|
56
|
+
stress: applyDefaults(defaultStress, options.stress)
|
|
136
57
|
};
|
|
137
|
-
if (params.COLLISION_PADDING == null) {
|
|
138
|
-
params.COLLISION_PADDING = params.IDEAL_LENGTH;
|
|
139
|
-
}
|
|
140
|
-
return params;
|
|
141
58
|
}
|
|
142
59
|
class VNode {
|
|
143
60
|
constructor(id, x, y, radius) {
|
|
@@ -241,15 +158,12 @@ export { LuminaLayout, LuminaLayout as SubstructureLayout };
|
|
|
241
158
|
function LuminaLayout(options) {
|
|
242
159
|
this.options = options || {};
|
|
243
160
|
this.cy = this.options.cy;
|
|
244
|
-
this.eles = this.options.eles || this.cy.elements();
|
|
245
|
-
this.boundingBox = this.options.boundingBox;
|
|
246
|
-
this.stopped = false;
|
|
247
161
|
if (!this.cy) {
|
|
248
162
|
throw new Error('cytoscape-lumina: options.cy is required');
|
|
249
163
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
164
|
+
this.eles = this.options.eles || this.cy.elements();
|
|
165
|
+
this.boundingBox = this.options.boundingBox;
|
|
166
|
+
this.stopped = false;
|
|
253
167
|
this.params = resolveParams(this.options);
|
|
254
168
|
// Instance-specific arrays instead of global
|
|
255
169
|
this.vnodes = [];
|
|
@@ -262,7 +176,7 @@ function LuminaLayout(options) {
|
|
|
262
176
|
* Capture a snapshot of the current layout state
|
|
263
177
|
*/
|
|
264
178
|
LuminaLayout.prototype.captureStep = function (stepName, description, metadata) {
|
|
265
|
-
if (!this.params.
|
|
179
|
+
if (!this.params.stepByStep)
|
|
266
180
|
return;
|
|
267
181
|
const nodes = this.cy.nodes();
|
|
268
182
|
const nodePositions = {};
|
|
@@ -403,7 +317,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
403
317
|
for (const v of neighbors) {
|
|
404
318
|
const vId = v.id();
|
|
405
319
|
// 1. Found a cycle back to our specific START node
|
|
406
|
-
if (vId === startId && path.length >= params.
|
|
320
|
+
if (vId === startId && path.length >= params.minCycleLength) {
|
|
407
321
|
const cycle = [...path];
|
|
408
322
|
const sortedKey = [...cycle].sort().join(',');
|
|
409
323
|
if (!seenCycles.has(sortedKey)) {
|
|
@@ -521,7 +435,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
521
435
|
}
|
|
522
436
|
}
|
|
523
437
|
// 4. Save the chain that was found
|
|
524
|
-
if (currentChainNodes.length >= params.
|
|
438
|
+
if (currentChainNodes.length >= params.minChainLength) {
|
|
525
439
|
nodeId = 0;
|
|
526
440
|
currentChainNodes.forEach((node) => {
|
|
527
441
|
node.addClass('substructure-chain');
|
|
@@ -566,7 +480,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
566
480
|
const neighborCount = neighbors.length;
|
|
567
481
|
// Check whether the neighbors consist only of distinct neighbor nodes
|
|
568
482
|
const leafNeighbors = neighbors.filter((n) => n.neighborhood().nodes().length === 1);
|
|
569
|
-
if (leafNeighbors.length >= params.
|
|
483
|
+
if (leafNeighbors.length >= params.minStarLeaves && neighborCount >= params.minStarLeaves) {
|
|
570
484
|
if (node.data('structType') !== 'Cycle' && node.data('structType') !== 'Chain') {
|
|
571
485
|
const groupId = 'Star_' + starIndex;
|
|
572
486
|
node.addClass(['substructure-star', 'substructure-star-center']);
|
|
@@ -608,7 +522,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
|
|
|
608
522
|
const v = nodes[j];
|
|
609
523
|
if (v.data('structType') === 'Normal') {
|
|
610
524
|
const v1 = v.neighborhood().nodes();
|
|
611
|
-
if (areNodesEqual(v1, u1) && v1.length >= params.
|
|
525
|
+
if (areNodesEqual(v1, u1) && v1.length >= params.minParallelNeighbors && u1.length >= params.minParallelNeighbors) {
|
|
612
526
|
if (nodeVecParallel.length === 0) {
|
|
613
527
|
nodeVecParallel.push(u.id());
|
|
614
528
|
}
|
|
@@ -691,7 +605,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
691
605
|
return true;
|
|
692
606
|
});
|
|
693
607
|
// 2. NOW define your collections to get the updated state
|
|
694
|
-
if (params.
|
|
608
|
+
if (params.randomizeInitialPositions) {
|
|
695
609
|
nodes.forEach((node) => {
|
|
696
610
|
node.position({
|
|
697
611
|
x: bb.x1 + Math.random() * width, y: bb.y1 + Math.random() * height
|
|
@@ -905,8 +819,8 @@ LuminaLayout.prototype.run = function () {
|
|
|
905
819
|
this.vnodes.forEach((v1) => {
|
|
906
820
|
if (v1.type == 'Star') {
|
|
907
821
|
const allMemberNode = v1.nodes.filter((node) => node.data('structType') !== 'Star-Center');
|
|
908
|
-
const ringSpacing = params.
|
|
909
|
-
const baseNodesInFirstRing = params.
|
|
822
|
+
const ringSpacing = params.starRingSpacing;
|
|
823
|
+
const baseNodesInFirstRing = params.starBaseNodesPerRing;
|
|
910
824
|
// 1. Pre-calculate how many nodes go into each ring
|
|
911
825
|
const rings = [];
|
|
912
826
|
let tempNodes = [...allMemberNode];
|
|
@@ -921,7 +835,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
921
835
|
}
|
|
922
836
|
else if (v1.type == 'Cycle') {
|
|
923
837
|
const count = v1.nodes.length;
|
|
924
|
-
const k = params.
|
|
838
|
+
const k = params.cycleNodeSpacing;
|
|
925
839
|
v1.radius = (count * k) / (2 * Math.PI);
|
|
926
840
|
}
|
|
927
841
|
else if (v1.type == 'Parallel') {
|
|
@@ -949,8 +863,8 @@ LuminaLayout.prototype.run = function () {
|
|
|
949
863
|
}
|
|
950
864
|
else if (v1.type == 'Chain') {
|
|
951
865
|
const count = v1.nodes.length;
|
|
952
|
-
const miniMumRadius = params.
|
|
953
|
-
v1.radius = Math.max((count * params.
|
|
866
|
+
const miniMumRadius = params.chainMinRadius;
|
|
867
|
+
v1.radius = Math.max((count * params.cycleNodeSpacing) / (2 * Math.PI), miniMumRadius);
|
|
954
868
|
}
|
|
955
869
|
else {
|
|
956
870
|
v1.radius = 10;
|
|
@@ -959,21 +873,20 @@ LuminaLayout.prototype.run = function () {
|
|
|
959
873
|
}
|
|
960
874
|
this.captureStep('Virtual Nodes Positioned', 'Virtual node centers and radii calculated', null);
|
|
961
875
|
//******************** virtual node force layout ************************
|
|
962
|
-
//
|
|
963
|
-
|
|
964
|
-
if (params.LAYOUT_ALGORITHM === 'force') {
|
|
876
|
+
// Shared settings are on params; solver-specific settings are nested.
|
|
877
|
+
if (params.layoutAlgorithm === 'force') {
|
|
965
878
|
const fp = params.force;
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
const
|
|
969
|
-
const
|
|
879
|
+
const idealLength = params.idealLength;
|
|
880
|
+
const repulsion = fp.repulsion;
|
|
881
|
+
const springK = fp.springK;
|
|
882
|
+
const iterations = params.iterations;
|
|
970
883
|
var colisionFlag = true;
|
|
971
884
|
let iter = 0;
|
|
972
885
|
var maxAttractMove = 10e10;
|
|
973
886
|
var maxRepulsetMove = 10e10;
|
|
974
887
|
var numOfCollision = 0;
|
|
975
888
|
// Only consider the whole graph truly stationary when the maximum movement of every node is below this threshold
|
|
976
|
-
const
|
|
889
|
+
const convergenceThreshold = params.convergenceThreshold;
|
|
977
890
|
// Build the adjacency list in advance so it can be reused seamlessly for both "initial ordering" and the "later angular force"
|
|
978
891
|
const adj = new Map();
|
|
979
892
|
this.vedges.forEach((e) => {
|
|
@@ -1000,13 +913,13 @@ LuminaLayout.prototype.run = function () {
|
|
|
1000
913
|
// Also give them an initial star-shaped radial geometric distribution that is guaranteed not to cross
|
|
1001
914
|
leafNeighbors.forEach((nb, index) => {
|
|
1002
915
|
const initAngle = (Math.PI * 2 / leafNeighbors.length) * index;
|
|
1003
|
-
nb.center_x = node.center_x + Math.cos(initAngle) *
|
|
1004
|
-
nb.center_y = node.center_y + Math.sin(initAngle) *
|
|
916
|
+
nb.center_x = node.center_x + Math.cos(initAngle) * idealLength;
|
|
917
|
+
nb.center_y = node.center_y + Math.sin(initAngle) * idealLength;
|
|
1005
918
|
});
|
|
1006
919
|
}
|
|
1007
920
|
}
|
|
1008
921
|
});
|
|
1009
|
-
while (iter <
|
|
922
|
+
while (iter < iterations) {
|
|
1010
923
|
if (this.stopped) {
|
|
1011
924
|
break;
|
|
1012
925
|
}
|
|
@@ -1039,7 +952,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1039
952
|
}
|
|
1040
953
|
}
|
|
1041
954
|
// Calculate the current global cooling factor (the core simulated-annealing control)
|
|
1042
|
-
const cooling = Math.pow(1 - iter /
|
|
955
|
+
const cooling = Math.pow(1 - iter / iterations, fp.coolingExponent);
|
|
1043
956
|
/* ---------- A. Attraction (Spring) ---------- */
|
|
1044
957
|
if (1) {
|
|
1045
958
|
this.vedges.forEach((e) => {
|
|
@@ -1051,9 +964,9 @@ LuminaLayout.prototype.run = function () {
|
|
|
1051
964
|
const dy = t.center_y - s.center_y;
|
|
1052
965
|
const centerDist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
|
|
1053
966
|
const surfaceDist = centerDist - (s.radius + t.radius);
|
|
1054
|
-
const delta = Math.max(0, surfaceDist -
|
|
967
|
+
const delta = Math.max(0, surfaceDist - idealLength);
|
|
1055
968
|
// Apply the cooling factor to the attractive force
|
|
1056
|
-
let force =
|
|
969
|
+
let force = springK * delta * cooling;
|
|
1057
970
|
const fx = (force * dx) / centerDist;
|
|
1058
971
|
const fy = (force * dy) / centerDist;
|
|
1059
972
|
s.center_x += fx;
|
|
@@ -1065,7 +978,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1065
978
|
}
|
|
1066
979
|
/* ---------- B. Repulsion ---------- */
|
|
1067
980
|
if (1) {
|
|
1068
|
-
const
|
|
981
|
+
const minGap = 0.1;
|
|
1069
982
|
for (let i = 0; i < this.vnodes.length; i++) {
|
|
1070
983
|
for (let j = i + 1; j < this.vnodes.length; j++) {
|
|
1071
984
|
const n1 = this.vnodes[i];
|
|
@@ -1081,15 +994,15 @@ LuminaLayout.prototype.run = function () {
|
|
|
1081
994
|
let force = 0;
|
|
1082
995
|
if (centerDist < minDistance) {
|
|
1083
996
|
const overlap = minDistance - centerDist;
|
|
1084
|
-
force = (
|
|
997
|
+
force = (repulsion * fp.overlapRepulsionFactor) * (overlap / (centerDist + minGap));
|
|
1085
998
|
}
|
|
1086
999
|
else {
|
|
1087
1000
|
const gap = centerDist - minDistance;
|
|
1088
|
-
force =
|
|
1001
|
+
force = repulsion / (gap * gap + 20);
|
|
1089
1002
|
}
|
|
1090
1003
|
// Apply the cooling factor to the repulsive force
|
|
1091
1004
|
force *= cooling;
|
|
1092
|
-
const maxForceLimit =
|
|
1005
|
+
const maxForceLimit = repulsion * 2 * cooling;
|
|
1093
1006
|
if (force > maxForceLimit)
|
|
1094
1007
|
force = maxForceLimit;
|
|
1095
1008
|
const fx = (force * dx) / centerDist;
|
|
@@ -1103,10 +1016,10 @@ LuminaLayout.prototype.run = function () {
|
|
|
1103
1016
|
}
|
|
1104
1017
|
}
|
|
1105
1018
|
/* ---------- C. Angular Repulsion ---------- */
|
|
1106
|
-
if (fp.
|
|
1019
|
+
if (fp.useAngularForce) {
|
|
1107
1020
|
const norm = (a) => Math.atan2(Math.sin(a), Math.cos(a));
|
|
1108
|
-
const
|
|
1109
|
-
const
|
|
1021
|
+
const angularStrength = fp.angularStrength;
|
|
1022
|
+
const maxForce = fp.angularMaxForce;
|
|
1110
1023
|
this.vnodes.forEach((node) => {
|
|
1111
1024
|
if (!node._permanentOrder || node._permanentOrder.length < 2) {
|
|
1112
1025
|
return;
|
|
@@ -1139,8 +1052,8 @@ LuminaLayout.prototype.run = function () {
|
|
|
1139
1052
|
continue;
|
|
1140
1053
|
}
|
|
1141
1054
|
const gapError = idealGap - gap;
|
|
1142
|
-
let force = gapError *
|
|
1143
|
-
force = Math.min(force,
|
|
1055
|
+
let force = gapError * angularStrength;
|
|
1056
|
+
force = Math.min(force, maxForce);
|
|
1144
1057
|
// Tangential direction of the left item
|
|
1145
1058
|
const ltx = -left.dy / left.dist;
|
|
1146
1059
|
const lty = left.dx / left.dist;
|
|
@@ -1160,7 +1073,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1160
1073
|
}
|
|
1161
1074
|
// [Exit criterion] Only allow an early safe exit when all movement energy in the graph has completely settled and become extremely small
|
|
1162
1075
|
const totalMaxMovement = Math.max(maxAttractMove, maxRepulsetMove, maxAngularMove);
|
|
1163
|
-
if (totalMaxMovement <
|
|
1076
|
+
if (totalMaxMovement < convergenceThreshold && iter > 10) {
|
|
1164
1077
|
break;
|
|
1165
1078
|
}
|
|
1166
1079
|
}
|
|
@@ -1174,7 +1087,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1174
1087
|
if (1) {
|
|
1175
1088
|
colisionFlag = true;
|
|
1176
1089
|
numOfCollision = 0;
|
|
1177
|
-
const padding = params.
|
|
1090
|
+
const padding = params.collisionPadding;
|
|
1178
1091
|
let iter = 0;
|
|
1179
1092
|
while (colisionFlag) {
|
|
1180
1093
|
if (this.stopped) {
|
|
@@ -1221,9 +1134,9 @@ LuminaLayout.prototype.run = function () {
|
|
|
1221
1134
|
console.warn("avoid dead loop");
|
|
1222
1135
|
break;
|
|
1223
1136
|
}
|
|
1224
|
-
// this.captureStep('Anti-collision', 'Eliminate all collisions ' + iter, {
|
|
1137
|
+
// this.captureStep('Anti-collision', 'Eliminate all collisions ' + iter, {iterations});
|
|
1225
1138
|
}
|
|
1226
|
-
this.captureStep('Anti-collision', 'Eliminate all collisions', { iterations
|
|
1139
|
+
this.captureStep('Anti-collision', 'Eliminate all collisions', { iterations });
|
|
1227
1140
|
if (1) {
|
|
1228
1141
|
numOfCollision = 0;
|
|
1229
1142
|
for (let i = 0; i < this.vnodes.length; i++) {
|
|
@@ -1233,26 +1146,25 @@ LuminaLayout.prototype.run = function () {
|
|
|
1233
1146
|
let dx = n1.center_x - n2.center_x;
|
|
1234
1147
|
let dy = n1.center_y - n2.center_y;
|
|
1235
1148
|
const centerDist = Math.sqrt(dx * dx + dy * dy);
|
|
1236
|
-
if (centerDist < (
|
|
1149
|
+
if (centerDist < (idealLength + n1.radius + n2.radius)) {
|
|
1237
1150
|
numOfCollision++;
|
|
1238
1151
|
}
|
|
1239
1152
|
}
|
|
1240
1153
|
}
|
|
1241
1154
|
}
|
|
1242
1155
|
}
|
|
1243
|
-
this.captureStep('Virtual Node Layout', 'Force-directed layout applied to virtual nodes', { iterations
|
|
1156
|
+
this.captureStep('Virtual Node Layout', 'Force-directed layout applied to virtual nodes', { iterations });
|
|
1244
1157
|
}
|
|
1245
1158
|
//******************** stress force layout ************************
|
|
1246
1159
|
// Stress Majorization algorithm
|
|
1247
1160
|
// All-Pairs shortest-path calculation and Guttman Transform (weighted Laplacian updates),
|
|
1248
1161
|
// while retaining the polar-coordinate initialization for leaf nodes and the Anti-Collision post-processing in the original code
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
if (params.LAYOUT_ALGORITHM === 'stress') {
|
|
1162
|
+
// Shared settings are on params; solver-specific settings are nested.
|
|
1163
|
+
if (params.layoutAlgorithm === 'stress') {
|
|
1252
1164
|
const sp = params.stress;
|
|
1253
|
-
const
|
|
1254
|
-
const
|
|
1255
|
-
const
|
|
1165
|
+
const idealLength = params.idealLength;
|
|
1166
|
+
const iterations = params.iterations;
|
|
1167
|
+
const convergenceThreshold = params.convergenceThreshold;
|
|
1256
1168
|
const numNodes = this.vnodes.length;
|
|
1257
1169
|
if (numNodes === 0)
|
|
1258
1170
|
return;
|
|
@@ -1285,7 +1197,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1285
1197
|
const u = nodeIndexMap.get(e.source.id);
|
|
1286
1198
|
const v = nodeIndexMap.get(e.target.id);
|
|
1287
1199
|
if (u !== undefined && v !== undefined) {
|
|
1288
|
-
const idealDist =
|
|
1200
|
+
const idealDist = idealLength + e.source.radius + e.target.radius;
|
|
1289
1201
|
distMatrix[u][v] = Math.min(distMatrix[u][v], idealDist);
|
|
1290
1202
|
distMatrix[v][u] = Math.min(distMatrix[v][u], idealDist);
|
|
1291
1203
|
}
|
|
@@ -1300,14 +1212,14 @@ LuminaLayout.prototype.run = function () {
|
|
|
1300
1212
|
}
|
|
1301
1213
|
}
|
|
1302
1214
|
}
|
|
1303
|
-
// Calculate the weight matrix W_ij = 1 / (d_ij ^
|
|
1215
|
+
// Calculate the weight matrix W_ij = 1 / (d_ij ^ weightExponent)
|
|
1304
1216
|
for (let i = 0; i < numNodes; i++) {
|
|
1305
1217
|
for (let j = 0; j < numNodes; j++) {
|
|
1306
1218
|
if (i !== j && distMatrix[i][j] !== Infinity) {
|
|
1307
1219
|
// Account for the actual node radius to prevent excessive crowding
|
|
1308
1220
|
const minR = this.vnodes[i].radius + this.vnodes[j].radius;
|
|
1309
1221
|
const d = Math.max(distMatrix[i][j], minR);
|
|
1310
|
-
weightMatrix[i][j] = 1 / Math.pow(d, sp.
|
|
1222
|
+
weightMatrix[i][j] = 1 / Math.pow(d, sp.weightExponent);
|
|
1311
1223
|
}
|
|
1312
1224
|
}
|
|
1313
1225
|
}
|
|
@@ -1326,8 +1238,8 @@ LuminaLayout.prototype.run = function () {
|
|
|
1326
1238
|
node._permanentOrder = leafNeighbors.map((a) => a.id);
|
|
1327
1239
|
leafNeighbors.forEach((nb, index) => {
|
|
1328
1240
|
const initAngle = (Math.PI * 2 / leafNeighbors.length) * index;
|
|
1329
|
-
nb.center_x = node.center_x + Math.cos(initAngle) *
|
|
1330
|
-
nb.center_y = node.center_y + Math.sin(initAngle) *
|
|
1241
|
+
nb.center_x = node.center_x + Math.cos(initAngle) * idealLength;
|
|
1242
|
+
nb.center_y = node.center_y + Math.sin(initAngle) * idealLength;
|
|
1331
1243
|
});
|
|
1332
1244
|
}
|
|
1333
1245
|
}
|
|
@@ -1336,7 +1248,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1336
1248
|
// [Main loop] Stress Majorization iteration (Guttman Transform)
|
|
1337
1249
|
// -------------------------------------------------------------
|
|
1338
1250
|
let iter = 0;
|
|
1339
|
-
while (iter <
|
|
1251
|
+
while (iter < iterations) {
|
|
1340
1252
|
if (this.stopped) {
|
|
1341
1253
|
break;
|
|
1342
1254
|
}
|
|
@@ -1390,7 +1302,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1390
1302
|
vi.center_y = nextY[i];
|
|
1391
1303
|
}
|
|
1392
1304
|
// Check the convergence condition and exit early
|
|
1393
|
-
if (maxStressMove <
|
|
1305
|
+
if (maxStressMove < convergenceThreshold && iter > 10) {
|
|
1394
1306
|
break;
|
|
1395
1307
|
}
|
|
1396
1308
|
}
|
|
@@ -1407,7 +1319,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1407
1319
|
if (1) {
|
|
1408
1320
|
let colisionFlag = true;
|
|
1409
1321
|
let numOfCollision = 0;
|
|
1410
|
-
const padding = params.
|
|
1322
|
+
const padding = params.collisionPadding;
|
|
1411
1323
|
let overlapIter = 0;
|
|
1412
1324
|
while (colisionFlag) {
|
|
1413
1325
|
if (this.stopped) {
|
|
@@ -1441,18 +1353,18 @@ LuminaLayout.prototype.run = function () {
|
|
|
1441
1353
|
}
|
|
1442
1354
|
}
|
|
1443
1355
|
}
|
|
1444
|
-
if (numOfCollision > 10000 || overlapIter > params.
|
|
1356
|
+
if (numOfCollision > 10000 || overlapIter > params.maxOverlapIterations) {
|
|
1445
1357
|
console.warn("Avoid dead loop in overlap removal");
|
|
1446
1358
|
break;
|
|
1447
1359
|
}
|
|
1448
1360
|
}
|
|
1449
|
-
this.captureStep('Anti-collision', 'Eliminate all collisions', { iterations
|
|
1361
|
+
this.captureStep('Anti-collision', 'Eliminate all collisions', { iterations });
|
|
1450
1362
|
}
|
|
1451
|
-
this.captureStep('Virtual Node Layout', 'Stress Majorization layout applied to virtual nodes', { iterations
|
|
1363
|
+
this.captureStep('Virtual Node Layout', 'Stress Majorization layout applied to virtual nodes', { iterations });
|
|
1452
1364
|
}
|
|
1453
1365
|
/////////////////////////////////////////////////////////////////////////////
|
|
1454
1366
|
if (1) {
|
|
1455
|
-
if (params.
|
|
1367
|
+
if (params.spreadVNodes) {
|
|
1456
1368
|
this.vnodes.forEach((v) => {
|
|
1457
1369
|
v.nodes.forEach((n) => {
|
|
1458
1370
|
n.position().x = v.center_x + Math.random() * 5;
|
|
@@ -1463,15 +1375,15 @@ LuminaLayout.prototype.run = function () {
|
|
|
1463
1375
|
}
|
|
1464
1376
|
}
|
|
1465
1377
|
///////////////////// Delete the old layout so that only virtual-node VNode layout remains ////////////////
|
|
1466
|
-
if (!params.
|
|
1467
|
-
const
|
|
1378
|
+
if (!params.substructureLayout) {
|
|
1379
|
+
const idealLength = params.leafNodeDistance;
|
|
1468
1380
|
this.vnodes.forEach((v) => {
|
|
1469
1381
|
if (v.type == 'Star') {
|
|
1470
1382
|
// if(targetNode){ // star is an independent star, not attached to a Cycle
|
|
1471
1383
|
if (1) {
|
|
1472
1384
|
const allMemberNode = v.nodes.filter((node) => node.data('structType') !== 'Star-Center');
|
|
1473
|
-
const ringSpacing = params.
|
|
1474
|
-
const baseNodesInFirstRing = params.
|
|
1385
|
+
const ringSpacing = params.starRingSpacing;
|
|
1386
|
+
const baseNodesInFirstRing = params.starBaseNodesPerRing;
|
|
1475
1387
|
// 1. Pre-calculate how many nodes go into each ring
|
|
1476
1388
|
const rings = [];
|
|
1477
1389
|
let tempNodes = [...allMemberNode];
|
|
@@ -1540,10 +1452,10 @@ LuminaLayout.prototype.run = function () {
|
|
|
1540
1452
|
cx /= count;
|
|
1541
1453
|
cy /= count;
|
|
1542
1454
|
// 2. Special handling: exactly 3 nodes form a strict equilateral triangle
|
|
1543
|
-
// (do not use
|
|
1455
|
+
// (do not use chainMinRadius, so the triangle is not artificially enlarged)
|
|
1544
1456
|
if (count === 3) {
|
|
1545
1457
|
// CYCLE_NODE_SPACING represents the desired node spacing (triangle side length)
|
|
1546
|
-
const sideLength = params.
|
|
1458
|
+
const sideLength = params.cycleNodeSpacing;
|
|
1547
1459
|
// Circumradius of the equilateral triangle: radius = side / sqrt(3)
|
|
1548
1460
|
const radius = sideLength / Math.sqrt(3);
|
|
1549
1461
|
// 3. Search for the best rotation angle (0°, 10°, ... 350°),
|
|
@@ -1580,8 +1492,8 @@ LuminaLayout.prototype.run = function () {
|
|
|
1580
1492
|
// 3. count > 3
|
|
1581
1493
|
// gNodes[0] -> center node, gNodes[1...] -> circumference nodes
|
|
1582
1494
|
// 4. Calculate the standard radius based on the number of nodes
|
|
1583
|
-
const miniMumRadius = params.
|
|
1584
|
-
const radius = Math.max((count * params.
|
|
1495
|
+
const miniMumRadius = params.chainMinRadius;
|
|
1496
|
+
const radius = Math.max((count * params.cycleNodeSpacing) / (2 * Math.PI), miniMumRadius);
|
|
1585
1497
|
// 5. Circumference nodes
|
|
1586
1498
|
const sorted = gNodes.slice(1);
|
|
1587
1499
|
// 6. Search for the best rotation angle
|
|
@@ -1648,7 +1560,7 @@ LuminaLayout.prototype.run = function () {
|
|
|
1648
1560
|
}
|
|
1649
1561
|
// 2. Calculate the standard radius based on the number of nodes (to keep node spacing close to k)
|
|
1650
1562
|
const count = v.nodes.length;
|
|
1651
|
-
const k = params.
|
|
1563
|
+
const k = params.cycleNodeSpacing;
|
|
1652
1564
|
const radius = (count * k) / (2 * Math.PI);
|
|
1653
1565
|
// 3. Sort to prevent nodes from flickering around the circumference
|
|
1654
1566
|
const sorted = v.nodes;
|
|
@@ -1713,14 +1625,14 @@ LuminaLayout.prototype.run = function () {
|
|
|
1713
1625
|
let bestRotate = 0; //Find the best rotation angle
|
|
1714
1626
|
let aroundNodesVec = [];
|
|
1715
1627
|
nodes.forEach((nd, i) => {
|
|
1716
|
-
if (((nd.position().x - fatherPos.x) * (nd.position().x - fatherPos.x) + (nd.position().y - fatherPos.y) * (nd.position().y - fatherPos.y)) < 2 *
|
|
1628
|
+
if (((nd.position().x - fatherPos.x) * (nd.position().x - fatherPos.x) + (nd.position().y - fatherPos.y) * (nd.position().y - fatherPos.y)) < 2 * idealLength * idealLength) {
|
|
1717
1629
|
aroundNodesVec.push(nd);
|
|
1718
1630
|
}
|
|
1719
1631
|
});
|
|
1720
1632
|
for (let rotate = 0; rotate <= 360; rotate = rotate + 10) {
|
|
1721
1633
|
n.position({
|
|
1722
|
-
x: fatherPos.x + Math.cos(rotate * 3.14 / 180) *
|
|
1723
|
-
y: fatherPos.y + Math.sin(rotate * 3.14 / 180) *
|
|
1634
|
+
x: fatherPos.x + Math.cos(rotate * 3.14 / 180) * idealLength,
|
|
1635
|
+
y: fatherPos.y + Math.sin(rotate * 3.14 / 180) * idealLength
|
|
1724
1636
|
});
|
|
1725
1637
|
let totalLength = 0;
|
|
1726
1638
|
aroundNodesVec.forEach((nd, i) => {
|
|
@@ -1736,8 +1648,8 @@ LuminaLayout.prototype.run = function () {
|
|
|
1736
1648
|
}
|
|
1737
1649
|
//
|
|
1738
1650
|
n.position({
|
|
1739
|
-
x: fatherPos.x + Math.cos(bestRotate * 3.14 / 180) *
|
|
1740
|
-
y: fatherPos.y + Math.sin(bestRotate * 3.14 / 180) *
|
|
1651
|
+
x: fatherPos.x + Math.cos(bestRotate * 3.14 / 180) * idealLength,
|
|
1652
|
+
y: fatherPos.y + Math.sin(bestRotate * 3.14 / 180) * idealLength
|
|
1741
1653
|
});
|
|
1742
1654
|
}
|
|
1743
1655
|
});
|
|
@@ -2357,7 +2269,7 @@ LuminaLayout.prototype.packNetworks = function (networks) {
|
|
|
2357
2269
|
* Navigate to a specific step in step-by-step mode
|
|
2358
2270
|
*/
|
|
2359
2271
|
LuminaLayout.prototype.goToStep = function (stepIndex) {
|
|
2360
|
-
if (!this.params.
|
|
2272
|
+
if (!this.params.stepByStep || this.steps.length === 0) {
|
|
2361
2273
|
console.warn('Step-by-step mode is not enabled or no steps have been captured');
|
|
2362
2274
|
return this;
|
|
2363
2275
|
}
|
|
@@ -2409,7 +2321,7 @@ LuminaLayout.prototype.prevStep = function () {
|
|
|
2409
2321
|
* Get information about all steps
|
|
2410
2322
|
*/
|
|
2411
2323
|
LuminaLayout.prototype.listSteps = function () {
|
|
2412
|
-
if (!this.params.
|
|
2324
|
+
if (!this.params.stepByStep || this.steps.length === 0) {
|
|
2413
2325
|
console.log('No steps available');
|
|
2414
2326
|
return [];
|
|
2415
2327
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intact-ebi/cytoscape-lumina",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -66,4 +66,4 @@
|
|
|
66
66
|
"allowScripts": {
|
|
67
67
|
"fsevents@2.3.3": true
|
|
68
68
|
}
|
|
69
|
-
}
|
|
69
|
+
}
|