@intact-ebi/cytoscape-lumina 0.1.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/LICENSE CHANGED
@@ -1,15 +1,7 @@
1
- ISC License
1
+ Copyright 2026 EMBL-EBI
2
2
 
3
- Copyright (c) 2026 EMBL-EBI
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
4
 
5
- Permission to use, copy, modify, and/or distribute this software for any
6
- purpose with or without fee is hereby granted, provided that the above
7
- copyright notice and this permission notice appear in all copies.
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
6
 
9
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
- PERFORMANCE OF THIS SOFTWARE.
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,123 +1,164 @@
1
- # LUMINA:
1
+ # LUMINA
2
2
 
3
- ## Usage
3
+ **LUMINA** is a [Cytoscape.js](https://js.cytoscape.org/) layout extension that is **structure-aware**: instead of treating every node the same, it detects common substructures in your graph, collapses each one into a *virtual node*, lays out the virtual nodes with either a force-directed or a stress-majorization solver, and then expands every substructure back into a clean, purpose-built arrangement.
4
+
5
+ Written in TypeScript. Registered under the layout name `lumina` (the legacy name `substructure-layout` is still registered as an alias).
6
+
7
+ ## Features
8
+
9
+ - **Substructure detection**
10
+ - **Cycle** – laid out as a perfect circle
11
+ - **Chain** – a path hanging off a leaf, laid out as a ring with its leaf end in the middle
12
+ - **Star** – a hub with leaf neighbours, laid out in concentric rings
13
+ - **Parallel** – nodes sharing the same neighbours, laid out as an aligned grid between their shared endpoints
14
+ - Leaves that don't belong to a chain are placed on a circle around their parent
15
+ - **Two interchangeable solvers** for the virtual-node graph: `force` and `stress`
16
+ - **Multiple connected components** are each laid out independently, rotated to their minimum bounding box, and packed into rows that match the canvas aspect ratio
17
+ - **Compound parents** (`.substructure-group`) are generated for each detected substructure
18
+ - **Step-by-step mode** to inspect and replay every stage of the pipeline
19
+
20
+ ## How it works
4
21
 
5
- ### Installation
22
+ 1. Split the graph into connected components.
23
+ 2. Detect cycles, chains, stars and parallel groups.
24
+ 3. Create one virtual node per substructure (single nodes become their own virtual node) and virtual edges between them.
25
+ 4. Compute a radius for each virtual node from its type and size.
26
+ 5. Run the selected solver (`force` or `stress`) on the virtual graph, then an anti-collision pass.
27
+ 6. Expand each virtual node into its real nodes using the substructure-specific arrangement.
28
+ 7. Rotate each component to its minimum-area bounding box and pack all components onto the canvas.
6
29
 
7
- ```zsh
8
- npm install @intact-portal/sub-structure-cyto-layout cytoscape
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install cytoscape @intact-ebi/cytoscape-lumina
9
34
  ```
10
35
 
11
- ### Minimal import
36
+ `cytoscape` (^3) is a peer dependency.
37
+
38
+ ## Usage
12
39
 
13
- ```js
40
+ ```ts
14
41
  import cytoscape from 'cytoscape';
15
- import registerSubstructureLayout from '@intact-portal/sub-structure-cyto-layout';
42
+ import registerLumina from '@intact-ebi/cytoscape-lumina';
16
43
 
17
- registerSubstructureLayout(cytoscape);
44
+ registerLumina(cytoscape);
18
45
 
19
- const cy = cytoscape({
20
- container: document.getElementById('cy'),
21
- elements
22
- });
46
+ const cy = cytoscape({ container: document.getElementById('cy'), elements: [/* ... */] });
23
47
 
24
48
  cy.layout({
25
- name: 'substructure-layout'
49
+ name: 'lumina',
50
+ layoutAlgorithm: 'stress', // 'stress' (default) or 'force'
51
+
52
+ // shared by both algorithms
53
+ idealLength: 100,
54
+ iterations: 600,
55
+
56
+ // used only when layoutAlgorithm is 'force'
57
+ force: { repulsion: 300, springK: 0.15, useAngularForce: true },
58
+
59
+ // used only when layoutAlgorithm is 'stress'
60
+ stress: { weightExponent: 2 }
26
61
  }).run();
27
62
  ```
28
63
 
29
- ### Demo project
30
-
31
- ```js
32
- <!doctype html>
33
- <html lang="en">
34
- <head>
35
- <meta charset="utf-8">
36
- <meta name="viewport" content="width=device-width, initial-scale=1">
37
- <title>Cytoscape LUMINA demo</title>
38
- <style>
39
- html,
40
- body {
41
- height: 100%;
42
- margin: 0;
43
- font-family: system-ui, sans-serif;
44
- }
45
-
46
- body {
47
- display: grid;
48
- grid-template-rows: auto 1fr;
49
- }
50
- </style>
51
-
52
- <script type="importmap">
53
- {
54
- "imports": {
55
- "cytoscape": "./node_modules/cytoscape/dist/cytoscape.esm.min.mjs",
56
- "@intact-ebi/cytoscape-lumina": "./node_modules/@intact-ebi/cytoscape-lumina/dist/index.js"
57
- }
58
- }
59
- </script>
60
- </head>
61
- <body>
62
- <header>
63
- <h1>LUMINA layout demo</h1>
64
- </header>
65
- <main id="cy" aria-label="Interactive network graph"></main>
66
-
67
- <script type="module">
68
- import cytoscape from 'cytoscape';
69
- import registerSubstructureLayout from '@intact-ebi/cytoscape-lumina';
70
-
71
- registerSubstructureLayout(cytoscape);
72
-
73
- const elements = [
74
- { data: { id: 'a' } },
75
- { data: { id: 'b' } },
76
- { data: { id: 'c' } },
77
- { data: { id: 'd' } },
78
- { data: { id: 'hub' } },
79
- { data: { id: 'leaf-1' } },
80
- { data: { id: 'leaf-2' } },
81
- { data: { id: 'leaf-3' } },
82
- { data: { id: 'chain-1' } },
83
- { data: { id: 'chain-2' } },
84
- { data: { id: 'ab', source: 'a', target: 'b' } },
85
- { data: { id: 'bc', source: 'b', target: 'c' } },
86
- { data: { id: 'cd', source: 'c', target: 'd' } },
87
- { data: { id: 'da', source: 'd', target: 'a' } },
88
- { data: { id: 'c-hub', source: 'c', target: 'hub' } },
89
- { data: { id: 'hub-leaf-1', source: 'hub', target: 'leaf-1' } },
90
- { data: { id: 'hub-leaf-2', source: 'hub', target: 'leaf-2' } },
91
- { data: { id: 'hub-leaf-3', source: 'hub', target: 'leaf-3' } },
92
- { data: { id: 'd-chain-1', source: 'd', target: 'chain-1' } },
93
- { data: { id: 'chain-1-chain-2', source: 'chain-1', target: 'chain-2' } }
94
- ];
95
-
96
- const cy = cytoscape({
97
- container: document.getElementById('cy'),
98
- elements,
99
- style: [
100
- {
101
- selector: 'node',
102
- style: {
103
- 'label': 'data(id)',
104
- }
105
- },
106
- ]
107
- });
108
-
109
- cy.layout({
110
- name: 'substructure-layout',
111
- }).run();
112
- </script>
113
- </body>
114
- </html>
64
+ Registration is idempotent, so it is safe to call more than once (e.g. with hot reload).
65
+
66
+ ## Options
67
+
68
+ Options are split by who uses them:
69
+
70
+ - **Shared** options are passed at the top level and apply to both algorithms.
71
+ - **Force-only** options go under `force: { ... }` and are ignored when `layoutAlgorithm` is `'stress'`.
72
+ - **Stress-only** options go under `stress: { ... }` and are ignored when `layoutAlgorithm` is `'force'`.
73
+
74
+ `DEFAULT_PARAMS` and `resolveParams` are exported for consumers that need the complete resolved configuration.
75
+
76
+ ### Algorithm
77
+
78
+ | Option | Default | Description |
79
+ | --- | --- | --- |
80
+ | `layoutAlgorithm` | `'stress'` | `'force'` or `'stress'`. |
81
+
82
+ ### Shared solver options (both algorithms)
83
+
84
+ | Option | Default | Description |
85
+ | --- | --- | --- |
86
+ | `idealLength` | `100` | Ideal surface-to-surface gap between connected virtual nodes. Spring rest length for `force`, base of the target distance for `stress`; also the radius of the initial radial placement of leaf virtual nodes in both. |
87
+ | `iterations` | `600` | Maximum iterations of the main solver loop. |
88
+ | `convergenceThreshold` | `0.5` | Stop early once the largest movement of any virtual node is below this many pixels. |
89
+ | `collisionPadding` | `idealLength` | Extra gap enforced between virtual nodes in the anti-collision pass. |
90
+ | `maxOverlapIterations` | `300` | Safety cap for the anti-collision loop (stress). |
91
+
92
+ ### Force-only options (`force: { ... }`)
93
+
94
+ | Option | Default | Description |
95
+ | --- | --- | --- |
96
+ | `repulsion` | `300` | Repulsion strength between virtual nodes. |
97
+ | `springK` | `0.15` | Spring stiffness of virtual edges. |
98
+ | `overlapRepulsionFactor` | `5` | Overlapping virtual nodes repel this many times harder. |
99
+ | `coolingExponent` | `2` | Annealing: `cooling = (1 − iter / iterations) ^ coolingExponent`. |
100
+ | `useAngularForce` | `false` | Spread leaf virtual nodes evenly in angle around their hub. |
101
+ | `angularStrength` | `0.2` | Strength of the angular force (needs `useAngularForce`). |
102
+ | `angularMaxForce` | `2.0` | Per-step cap on the angular force (needs `useAngularForce`). |
103
+
104
+ ### Stress-only options (`stress: { ... }`)
105
+
106
+ | Option | Default | Description |
107
+ | --- | --- | --- |
108
+ | `weightExponent` | `2` | Stress weights are `w_ij = 1 / d_ij ^ weightExponent`; `2` is classic stress majorization. |
109
+
110
+ ### Structure detection
111
+
112
+ | Option | Default | Description |
113
+ | --- | --- | --- |
114
+ | `minStarLeaves` | `3` | Minimum leaf neighbours for a node to become a star centre. |
115
+ | `minCycleLength` | `3` | Minimum number of nodes in a detected cycle. |
116
+ | `minChainLength` | `2` | Minimum number of nodes in a chain. |
117
+ | `minParallelNeighbors` | `2` | Minimum shared neighbours for nodes to count as a parallel group. |
118
+
119
+ ### Substructure spacing
120
+
121
+ | Option | Default | Description |
122
+ | --- | --- | --- |
123
+ | `cycleNodeSpacing` | `100` | Target spacing between neighbouring nodes on a cycle (also sizes chain rings and virtual node radii). |
124
+ | `starRingSpacing` | `100` | Distance between concentric rings of a star. |
125
+ | `starBaseNodesPerRing` | `6` | Nodes in the first star ring; each further ring holds this many more. |
126
+ | `chainMinRadius` | `150` | Minimum radius of a chain ring. |
127
+ | `leafNodeDistance` | `200` | Distance of non-chain leaves from their parent (only used when `substructureLayout` is `false`). |
128
+
129
+ ### Flags
130
+
131
+ | Option | Default | Description |
132
+ | --- | --- | --- |
133
+ | `randomizeInitialPositions` | `false` | Randomize node positions inside the bounding box before the layout starts. |
134
+ | `spreadVNodes` | `true` | After solving, scatter real nodes slightly around their virtual node centre. |
135
+ | `substructureLayout` | `false` | When `false` (default), each substructure is arranged internally (circle, rings, grid, ...). When `true`, that step is skipped. |
136
+ | `stepByStep` | `false` | Record a snapshot after each stage (see below). |
137
+
138
+ ## Step-by-step debugging
139
+
140
+ Set `stepByStep: true`, keep a reference to the layout, and run it:
141
+
142
+ ```ts
143
+ const layout = cy.layout({ name: 'lumina', stepByStep: true });
144
+ layout.run();
145
+
146
+ layout.listSteps(); // print and return all captured steps
147
+ layout.goToStep(3); // jump to a step
148
+ layout.nextStep();
149
+ layout.prevStep();
150
+ layout.clearVirtualNodes();
115
151
  ```
116
152
 
117
- ## Configuration
153
+ Each step restores real-node positions and overlays the virtual nodes (translucent circles) and virtual edges (dashed lines) at that stage.
154
+
155
+ ## Which algorithm should I use?
118
156
 
119
- TODO
157
+ - **`stress`** (default) – deterministic-looking, globally consistent distances, good for most graphs. Tune with `idealLength` and `weightExponent`. Cost grows with the number of virtual nodes (all-pairs shortest paths).
158
+ - **`force`** – classic spring-and-repulsion with annealing, plus an optional angular force for evenly fanned-out leaves. Tune with `repulsion`, `springK` and `useAngularForce`.
120
159
 
121
- ## Development
160
+ ## Notes
122
161
 
123
- TODO
162
+ - Nodes with 8 or more neighbours are skipped during cycle search, for performance.
163
+ - Self-loops and duplicate edges are ignored by the layout.
164
+ - Generated compound parents have the class `substructure-group` and ids of the form `<componentIndex>_<groupId>`; they are removed and regenerated on each run.
package/dist/index.d.ts CHANGED
@@ -1,115 +1,2 @@
1
- import cytoscape from 'cytoscape';
2
- export type LayoutAlgorithm = 'force' | 'stress';
3
- export interface LayoutParameters {
4
- LAYOUT_ALGORITHM: LayoutAlgorithm;
5
- IDEAL_LENGTH: number;
6
- REPULSION: number;
7
- SPRING_K: number;
8
- ITERATIONS: number;
9
- ANGULAR_STRENGTH: number;
10
- CENTER_GRAVITY: number;
11
- USE_ANGULAR_FORCE: boolean;
12
- RANDOMIZE_INITIAL_POSITIONS: boolean;
13
- MIN_STAR_LEAVES: number;
14
- MIN_CYCLE_LENGTH: number;
15
- MAX_CYCLE_LENGTH: number;
16
- MIN_CHAIN_LENGTH: number;
17
- MIN_PARALLEL_NEIGHBORS: number;
18
- CYCLE_NODE_SPACING: number;
19
- STAR_RING_SPACING: number;
20
- STAR_BASE_NODES_PER_RING: number;
21
- CHAIN_MIN_RADIUS: number;
22
- PARALLEL_GAP: number;
23
- LEAF_NODE_DISTANCE: number;
24
- VNODE_RADIUS_MULTIPLIER: number;
25
- VNODE_REPULSION: number;
26
- VNODE_SPRING_K: number;
27
- VNODE_ITERATIONS: number;
28
- VNODE_ANGULAR_STRENGTH: number;
29
- SPREAD_V_NODES: boolean;
30
- SUBSTRUCTURE_LAYOUT: boolean;
31
- ENABLE_INITIAL_FORCE_LAYOUT: boolean;
32
- STEP_BY_STEP: boolean;
33
- SHOW_ALL_EDGES: boolean;
34
- }
35
- export declare const DEFAULT_PARAMS: LayoutParameters;
36
- export interface LayoutStep {
37
- stepNumber: number;
38
- stepName: string;
39
- description: string;
40
- nodePositions: {
41
- [nodeId: string]: {
42
- x: number;
43
- y: number;
44
- };
45
- };
46
- virtualNodes?: VNode[];
47
- virtualEdges?: VEdge[];
48
- metadata?: any;
49
- }
50
- export type SubstructureType = 'Normal' | 'Cycle' | 'Chain' | 'Star-Center' | 'Star-Member' | 'Parallel' | 'LeafButNotChain';
51
- type Node = cytoscape.NodeSingular;
52
- declare class VNode {
53
- id: string;
54
- type: any;
55
- center_x: number;
56
- center_y: number;
57
- radius: number;
58
- rotate_angle: number;
59
- nodes: Node[];
60
- neighbors: VNode[];
61
- _permanentOrder?: string[];
62
- constructor(id: string, x: number, y: number, radius: number);
63
- get position(): {
64
- x: number;
65
- y: number;
66
- };
67
- setPosition(x: number, y: number): void;
68
- addNode(node: Node): void;
69
- setPermanentOrder(order: string[]): void;
70
- getPermanentOrder(): string[] | undefined;
71
- toSnapshot(): {
72
- id: string;
73
- type: any;
74
- center_x: number;
75
- center_y: number;
76
- radius: number;
77
- rotate_angle: number;
78
- nodeIds: cytoscape.NodeSingular[];
79
- };
80
- }
81
- declare class VEdge {
82
- source: VNode;
83
- target: VNode;
84
- weight: number;
85
- constructor(source: VNode, target: VNode, weight?: number);
86
- toSnapshot(): {
87
- source: string;
88
- target: string;
89
- weight: number;
90
- };
91
- }
92
- /**
93
- * Register this layout as a Cytoscape.js layout extension.
94
- *
95
- * Usage:
96
- * import cytoscape from 'cytoscape';
97
- * import substructureLayout from './cytoscape-substructure-layout';
98
- *
99
- * substructureLayout(cytoscape);
100
- *
101
- * cy.layout({
102
- * name: 'substructure-layout',
103
- * layoutAlgorithm: 'force',
104
- * idealLength: 100,
105
- * repulsion: 10000,
106
- * springK: 0.15,
107
- * iterations: 400
108
- * }).run();
109
- */
110
- export default function register(cytoscapeInstance: any): void;
111
- export { SubstructureLayout };
112
- /**
113
- * Enhanced force-directed layout with structure awareness
114
- */
115
- declare function SubstructureLayout(this: any, options: any): void;
1
+ export * from './lumina-layout.js';
2
+ export { default } from './lumina-layout.js';