@intact-ebi/cytoscape-lumina 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -12
- package/README.md +150 -104
- package/dist/index.d.ts +2 -115
- package/dist/index.js +3 -2764
- package/dist/lumina-layout.d.ts +155 -0
- package/dist/lumina-layout.js +2498 -0
- package/package.json +50 -26
package/LICENSE
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
Copyright 2026 EMBL-EBI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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,169 @@
|
|
|
1
|
-
# LUMINA
|
|
1
|
+
# LUMINA
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install cytoscape @intact-ebi/cytoscape-lumina
|
|
9
34
|
```
|
|
10
35
|
|
|
11
|
-
|
|
36
|
+
`cytoscape` (^3) is a peer dependency.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
12
39
|
|
|
13
|
-
```
|
|
40
|
+
```ts
|
|
14
41
|
import cytoscape from 'cytoscape';
|
|
15
|
-
import
|
|
42
|
+
import registerLumina from '@intact-ebi/cytoscape-lumina';
|
|
16
43
|
|
|
17
|
-
|
|
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: '
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
Internally these map to upper-case constants (e.g. `idealLength` → `IDEAL_LENGTH`); `DEFAULT_PARAMS` and `resolveParams` are exported if you need them.
|
|
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
|
+
| `maxCycleLength` | `30` | Reserved. Currently **not enforced** by the cycle search. |
|
|
117
|
+
| `minChainLength` | `2` | Minimum number of nodes in a chain. |
|
|
118
|
+
| `minParallelNeighbors` | `2` | Minimum shared neighbours for nodes to count as a parallel group. |
|
|
119
|
+
|
|
120
|
+
### Substructure spacing
|
|
121
|
+
|
|
122
|
+
| Option | Default | Description |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `cycleNodeSpacing` | `100` | Target spacing between neighbouring nodes on a cycle (also sizes chain rings and virtual node radii). |
|
|
125
|
+
| `starRingSpacing` | `100` | Distance between concentric rings of a star. |
|
|
126
|
+
| `starBaseNodesPerRing` | `6` | Nodes in the first star ring; each further ring holds this many more. |
|
|
127
|
+
| `chainMinRadius` | `150` | Minimum radius of a chain ring. |
|
|
128
|
+
| `leafNodeDistance` | `200` | Distance of non-chain leaves from their parent (only used when `substructureLayout` is `false`). |
|
|
129
|
+
|
|
130
|
+
### Flags
|
|
131
|
+
|
|
132
|
+
| Option | Default | Description |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `randomizeInitialPositions` | `false` | Randomize node positions inside the bounding box before the layout starts. |
|
|
135
|
+
| `spreadVNodes` | `true` | After solving, scatter real nodes slightly around their virtual node centre. |
|
|
136
|
+
| `substructureLayout` | `false` | When `false` (default), each substructure is arranged internally (circle, rings, grid, ...). When `true`, that step is skipped. |
|
|
137
|
+
| `stepByStep` | `false` | Record a snapshot after each stage (see below). |
|
|
138
|
+
|
|
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
|
+
## Step-by-step debugging
|
|
144
|
+
|
|
145
|
+
Set `stepByStep: true`, keep a reference to the layout, and run it:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const layout = cy.layout({ name: 'lumina', stepByStep: true });
|
|
149
|
+
layout.run();
|
|
150
|
+
|
|
151
|
+
layout.listSteps(); // print and return all captured steps
|
|
152
|
+
layout.goToStep(3); // jump to a step
|
|
153
|
+
layout.nextStep();
|
|
154
|
+
layout.prevStep();
|
|
155
|
+
layout.clearVirtualNodes();
|
|
115
156
|
```
|
|
116
157
|
|
|
117
|
-
|
|
158
|
+
Each step restores real-node positions and overlays the virtual nodes (translucent circles) and virtual edges (dashed lines) at that stage.
|
|
159
|
+
|
|
160
|
+
## Which algorithm should I use?
|
|
118
161
|
|
|
119
|
-
|
|
162
|
+
- **`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).
|
|
163
|
+
- **`force`** – classic spring-and-repulsion with annealing, plus an optional angular force for evenly fanned-out leaves. Tune with `repulsion`, `springK` and `useAngularForce`.
|
|
120
164
|
|
|
121
|
-
##
|
|
165
|
+
## Notes
|
|
122
166
|
|
|
123
|
-
|
|
167
|
+
- Nodes with 8 or more neighbours are skipped during cycle search, for performance.
|
|
168
|
+
- Self-loops and duplicate edges are ignored by the layout.
|
|
169
|
+
- 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
|
-
|
|
2
|
-
export
|
|
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';
|