@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 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
- Internally these map to upper-case constants (e.g. `idealLength` → `IDEAL_LENGTH`); `DEFAULT_PARAMS` and `resolveParams` are exported if you need them.
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:
@@ -1,78 +1,66 @@
1
1
  import cytoscape from 'cytoscape';
2
2
  export type LayoutAlgorithm = 'force' | 'stress';
3
- /** Parameters shared by every algorithm */
4
- export interface CommonParams {
5
- LAYOUT_ALGORITHM: LayoutAlgorithm;
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 vnodes) */
9
- IDEAL_LENGTH: number;
10
- /** Max iterations of the main solver loop */
11
- ITERATIONS: number;
12
- /** Stop early when the max movement of any vnode is below this (px) */
13
- CONVERGENCE_THRESHOLD: number;
14
- /** Extra gap enforced between vnodes in the anti-collision pass.
15
- * null => use IDEAL_LENGTH */
16
- COLLISION_PADDING: number | null;
17
- /** Safety cap for the anti-collision loop */
18
- MAX_OVERLAP_ITERATIONS: number;
19
- MIN_STAR_LEAVES: number;
20
- MIN_CYCLE_LENGTH: number;
21
- MAX_CYCLE_LENGTH: number;
22
- MIN_CHAIN_LENGTH: number;
23
- MIN_PARALLEL_NEIGHBORS: number;
24
- CYCLE_NODE_SPACING: number;
25
- STAR_RING_SPACING: number;
26
- STAR_BASE_NODES_PER_RING: number;
27
- CHAIN_MIN_RADIUS: number;
28
- LEAF_NODE_DISTANCE: number;
29
- RANDOMIZE_INITIAL_POSITIONS: boolean;
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
- /** Only for the 'force' algorithm */
35
- export interface ForceParams {
36
- /** Node-node repulsion strength */
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
- /** Only for the 'stress' algorithm */
50
- export interface StressParams {
51
- /** weight w_ij = 1 / d_ij ^ WEIGHT_EXPONENT (2 = classic stress majorization) */
52
- WEIGHT_EXPONENT: number;
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 interface LayoutParameters extends CommonParams {
55
- force: ForceParams;
56
- stress: StressParams;
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: any): void;
143
+ declare function LuminaLayout(this: any, options: LuminaLayoutOptions & {
144
+ cy?: cytoscape.Core;
145
+ eles?: cytoscape.CollectionReturnValue;
146
+ boundingBox?: cytoscape.BoundingBox12 | cytoscape.BoundingBoxWH;
147
+ }): void;
@@ -1,143 +1,60 @@
1
1
  export const DEFAULT_PARAMS = {
2
- LAYOUT_ALGORITHM: 'stress',
2
+ layoutAlgorithm: 'stress',
3
3
  // shared solver
4
- IDEAL_LENGTH: 100,
5
- ITERATIONS: 600,
6
- CONVERGENCE_THRESHOLD: 0.5,
7
- COLLISION_PADDING: null,
8
- MAX_OVERLAP_ITERATIONS: 300,
9
- // structure detection
10
- MIN_STAR_LEAVES: 3,
11
- MIN_CYCLE_LENGTH: 3,
12
- MAX_CYCLE_LENGTH: 30,
13
- MIN_CHAIN_LENGTH: 2,
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
- CYCLE_NODE_SPACING: 100,
17
- STAR_RING_SPACING: 100,
18
- STAR_BASE_NODES_PER_RING: 6,
19
- CHAIN_MIN_RADIUS: 150,
20
- LEAF_NODE_DISTANCE: 200,
21
- // flags
22
- RANDOMIZE_INITIAL_POSITIONS: false,
23
- SPREAD_V_NODES: true,
24
- SUBSTRUCTURE_LAYOUT: false,
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
- REPULSION: 300,
28
- SPRING_K: 0.15,
29
- OVERLAP_REPULSION_FACTOR: 5,
30
- COOLING_EXPONENT: 2,
31
- USE_ANGULAR_FORCE: false,
32
- ANGULAR_STRENGTH: 0.2,
33
- ANGULAR_MAX_FORCE: 2.0
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
- WEIGHT_EXPONENT: 2
34
+ weightExponent: 2
37
35
  }
38
36
  };
39
- // ----------------------------------------------------------------------------
40
- // Public (Cytoscape camelCase) option names -> internal names
41
- // ----------------------------------------------------------------------------
42
- const COMMON_KEYS = {
43
- layoutAlgorithm: 'LAYOUT_ALGORITHM',
44
- idealLength: 'IDEAL_LENGTH',
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 out;
44
+ return resolved;
85
45
  }
86
- /**
87
- * Build the final parameters from Cytoscape layout options.
88
- *
89
- * cy.layout({
90
- * name: 'lumina',
91
- * layoutAlgorithm: 'force',
92
- * idealLength: 100, // shared
93
- * iterations: 600, // shared
94
- * force: { repulsion: 300, springK: 0.15, useAngularForce: true },
95
- * stress: { weightExponent: 2 }
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
- // Public Cytoscape.js options use camelCase; shared options are flat,
251
- // algorithm-specific ones live under `force: {...}` / `stress: {...}`.
252
- // See the parameter section at the top of this file. The legacy `params` object is still accepted.
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.STEP_BY_STEP)
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.MIN_CYCLE_LENGTH) {
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.MIN_CHAIN_LENGTH) {
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.MIN_STAR_LEAVES && neighborCount >= params.MIN_STAR_LEAVES) {
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.MIN_PARALLEL_NEIGHBORS && u1.length >= params.MIN_PARALLEL_NEIGHBORS) {
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.RANDOMIZE_INITIAL_POSITIONS) {
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.STAR_RING_SPACING;
909
- const baseNodesInFirstRing = params.STAR_BASE_NODES_PER_RING;
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.CYCLE_NODE_SPACING;
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.CHAIN_MIN_RADIUS;
953
- v1.radius = Math.max((count * params.CYCLE_NODE_SPACING) / (2 * Math.PI), miniMumRadius);
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
- // Uses: shared params (IDEAL_LENGTH, ITERATIONS, CONVERGENCE_THRESHOLD,
963
- // COLLISION_PADDING) + force-only params (params.force.*)
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 IDEAL_LENGTH = params.IDEAL_LENGTH;
967
- const REPULSION = fp.REPULSION;
968
- const SPRING_K = fp.SPRING_K;
969
- const ITERATIONS = params.ITERATIONS;
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 ENERGY_THRESHOLD = params.CONVERGENCE_THRESHOLD;
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) * IDEAL_LENGTH;
1004
- nb.center_y = node.center_y + Math.sin(initAngle) * IDEAL_LENGTH;
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 < ITERATIONS) {
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 / ITERATIONS, fp.COOLING_EXPONENT);
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 - IDEAL_LENGTH);
967
+ const delta = Math.max(0, surfaceDist - idealLength);
1055
968
  // Apply the cooling factor to the attractive force
1056
- let force = SPRING_K * delta * cooling;
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 MIN_GAP = 0.1;
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 = (REPULSION * fp.OVERLAP_REPULSION_FACTOR) * (overlap / (centerDist + MIN_GAP));
997
+ force = (repulsion * fp.overlapRepulsionFactor) * (overlap / (centerDist + minGap));
1085
998
  }
1086
999
  else {
1087
1000
  const gap = centerDist - minDistance;
1088
- force = REPULSION / (gap * gap + 20);
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 = REPULSION * 2 * cooling;
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.USE_ANGULAR_FORCE) {
1019
+ if (fp.useAngularForce) {
1107
1020
  const norm = (a) => Math.atan2(Math.sin(a), Math.cos(a));
1108
- const ANGULAR_STRENGTH = fp.ANGULAR_STRENGTH;
1109
- const MAX_FORCE = fp.ANGULAR_MAX_FORCE;
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 * ANGULAR_STRENGTH;
1143
- force = Math.min(force, MAX_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 < ENERGY_THRESHOLD && iter > 10) {
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.COLLISION_PADDING;
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, { iterations: ITERATIONS });
1137
+ // this.captureStep('Anti-collision', 'Eliminate all collisions ' + iter, {iterations});
1225
1138
  }
1226
- this.captureStep('Anti-collision', 'Eliminate all collisions', { iterations: 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 < (IDEAL_LENGTH + n1.radius + n2.radius)) {
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: 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
- // Uses: shared params (IDEAL_LENGTH, ITERATIONS, CONVERGENCE_THRESHOLD,
1250
- // COLLISION_PADDING, MAX_OVERLAP_ITERATIONS) + stress-only params (params.stress.*)
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 IDEAL_LENGTH = params.IDEAL_LENGTH;
1254
- const ITERATIONS = params.ITERATIONS;
1255
- const ENERGY_THRESHOLD = params.CONVERGENCE_THRESHOLD;
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 = IDEAL_LENGTH + e.source.radius + e.target.radius;
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 ^ WEIGHT_EXPONENT)
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.WEIGHT_EXPONENT);
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) * IDEAL_LENGTH;
1330
- nb.center_y = node.center_y + Math.sin(initAngle) * IDEAL_LENGTH;
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 < ITERATIONS) {
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 < ENERGY_THRESHOLD && iter > 10) {
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.COLLISION_PADDING;
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.MAX_OVERLAP_ITERATIONS) {
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: 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: 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.SPREAD_V_NODES) {
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.SUBSTRUCTURE_LAYOUT) {
1467
- const IDEAL_LENGTH = params.LEAF_NODE_DISTANCE;
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.STAR_RING_SPACING;
1474
- const baseNodesInFirstRing = params.STAR_BASE_NODES_PER_RING;
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 CHAIN_MIN_RADIUS, so the triangle is not artificially enlarged)
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.CYCLE_NODE_SPACING;
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.CHAIN_MIN_RADIUS;
1584
- const radius = Math.max((count * params.CYCLE_NODE_SPACING) / (2 * Math.PI), miniMumRadius);
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.CYCLE_NODE_SPACING;
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 * IDEAL_LENGTH * IDEAL_LENGTH) {
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) * IDEAL_LENGTH,
1723
- y: fatherPos.y + Math.sin(rotate * 3.14 / 180) * IDEAL_LENGTH
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) * IDEAL_LENGTH,
1740
- y: fatherPos.y + Math.sin(bestRotate * 3.14 / 180) * IDEAL_LENGTH
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.STEP_BY_STEP || this.steps.length === 0) {
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.STEP_BY_STEP || this.steps.length === 0) {
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.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
+ }