@ngx-km/layout 0.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 +186 -0
- package/fesm2022/ngx-km-layout.mjs +429 -0
- package/fesm2022/ngx-km-layout.mjs.map +1 -0
- package/index.d.ts +305 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @ngx-km/layout
|
|
2
|
+
|
|
3
|
+
Layout calculation library for Angular graph visualization. Uses an abstracted engine architecture (default: ELK.js) that can be swapped for alternative layout engines.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Pluggable layout engine architecture
|
|
8
|
+
- ELK.js implementation included (default)
|
|
9
|
+
- Multiple layout algorithms: layered, force, stress, radial, tree
|
|
10
|
+
- Configurable spacing, direction, and padding
|
|
11
|
+
- Incremental layout support (add/remove nodes while preserving positions)
|
|
12
|
+
- Returns node coordinates only (no rendering)
|
|
13
|
+
- Validation of input graphs
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @ngx-km/layout elkjs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { Component, inject } from '@angular/core';
|
|
25
|
+
import { LayoutService, LayoutNode, LayoutEdge } from '@ngx-km/layout';
|
|
26
|
+
|
|
27
|
+
@Component({
|
|
28
|
+
providers: [LayoutService],
|
|
29
|
+
// ...
|
|
30
|
+
})
|
|
31
|
+
export class MyComponent {
|
|
32
|
+
private layoutService = inject(LayoutService);
|
|
33
|
+
|
|
34
|
+
async calculateLayout() {
|
|
35
|
+
const nodes: LayoutNode[] = [
|
|
36
|
+
{ id: 'a', width: 100, height: 50 },
|
|
37
|
+
{ id: 'b', width: 100, height: 50 },
|
|
38
|
+
{ id: 'c', width: 100, height: 50 },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const edges: LayoutEdge[] = [
|
|
42
|
+
{ id: 'e1', sourceId: 'a', targetId: 'b' },
|
|
43
|
+
{ id: 'e2', sourceId: 'a', targetId: 'c' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const result = await this.layoutService.layout(nodes, edges, {
|
|
47
|
+
algorithm: 'layered',
|
|
48
|
+
direction: 'DOWN',
|
|
49
|
+
nodeSpacing: 50,
|
|
50
|
+
layerSpacing: 100,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// result.nodes contains positioned nodes with x, y coordinates
|
|
54
|
+
console.log(result.nodes);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Incremental Layout
|
|
60
|
+
|
|
61
|
+
The library supports incremental layout operations for dynamic graphs:
|
|
62
|
+
|
|
63
|
+
### Adding Nodes
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// Start with initial layout
|
|
67
|
+
const initialResult = await layoutService.layout(nodes, edges);
|
|
68
|
+
|
|
69
|
+
// Add new nodes while preserving existing positions
|
|
70
|
+
const newNodes = [{ id: 'd', width: 100, height: 50 }];
|
|
71
|
+
const allEdges = [...edges, { id: 'e3', sourceId: 'c', targetId: 'd' }];
|
|
72
|
+
|
|
73
|
+
const updatedResult = await layoutService.addNodesToLayout(
|
|
74
|
+
initialResult,
|
|
75
|
+
newNodes,
|
|
76
|
+
allEdges
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Removing Nodes
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Remove nodes (connected edges are automatically filtered)
|
|
84
|
+
const result = await layoutService.removeNodesFromLayout(
|
|
85
|
+
previousResult,
|
|
86
|
+
['nodeIdToRemove'],
|
|
87
|
+
allEdges,
|
|
88
|
+
options,
|
|
89
|
+
true // preservePositions: remaining nodes keep their positions
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Recalculating with Position Preservation
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Manually specify which positions to preserve
|
|
97
|
+
const existingPositions = new Map<string, { x: number; y: number }>();
|
|
98
|
+
existingPositions.set('a', { x: 100, y: 50 });
|
|
99
|
+
existingPositions.set('b', { x: 200, y: 50 });
|
|
100
|
+
|
|
101
|
+
const result = await layoutService.recalculateLayout(
|
|
102
|
+
allNodes,
|
|
103
|
+
allEdges,
|
|
104
|
+
existingPositions,
|
|
105
|
+
options
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Layout Options
|
|
110
|
+
|
|
111
|
+
| Option | Type | Default | Description |
|
|
112
|
+
|--------|------|---------|-------------|
|
|
113
|
+
| `algorithm` | `'layered' \| 'force' \| 'stress' \| 'radial' \| 'tree'` | `'layered'` | Layout algorithm |
|
|
114
|
+
| `direction` | `'DOWN' \| 'UP' \| 'RIGHT' \| 'LEFT'` | `'DOWN'` | Direction for hierarchical layouts |
|
|
115
|
+
| `nodeSpacing` | `number` | `50` | Horizontal spacing between nodes |
|
|
116
|
+
| `layerSpacing` | `number` | `50` | Vertical spacing between layers |
|
|
117
|
+
| `padding` | `number` | `20` | Padding around the graph |
|
|
118
|
+
| `considerEdges` | `boolean` | `true` | Whether edges affect layout |
|
|
119
|
+
|
|
120
|
+
## Edge Types
|
|
121
|
+
|
|
122
|
+
Edges can be directed (default) or bidirectional:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const edges: LayoutEdge[] = [
|
|
126
|
+
{ id: 'e1', sourceId: 'a', targetId: 'b' }, // directed (default)
|
|
127
|
+
{ id: 'e2', sourceId: 'b', targetId: 'c', type: 'directed' }, // explicitly directed
|
|
128
|
+
{ id: 'e3', sourceId: 'c', targetId: 'd', type: 'bidirectional' }, // two-way
|
|
129
|
+
];
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Custom Layout Engine
|
|
133
|
+
|
|
134
|
+
You can provide a custom layout engine by implementing the `LayoutEngine` interface:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { LAYOUT_ENGINE, LayoutEngine, LayoutInput, LayoutResult } from '@ngx-km/layout';
|
|
138
|
+
|
|
139
|
+
class CustomLayoutEngine implements LayoutEngine {
|
|
140
|
+
readonly name = 'Custom';
|
|
141
|
+
|
|
142
|
+
async calculateLayout(input: LayoutInput): Promise<LayoutResult> {
|
|
143
|
+
// Your custom layout logic
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
dispose?(): void {
|
|
147
|
+
// Optional cleanup
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Provide in component or module
|
|
152
|
+
providers: [
|
|
153
|
+
LayoutService,
|
|
154
|
+
{ provide: LAYOUT_ENGINE, useClass: CustomLayoutEngine }
|
|
155
|
+
]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## API Reference
|
|
159
|
+
|
|
160
|
+
### LayoutService Methods
|
|
161
|
+
|
|
162
|
+
| Method | Description |
|
|
163
|
+
|--------|-------------|
|
|
164
|
+
| `calculateLayout(input)` | Calculate layout from LayoutInput object |
|
|
165
|
+
| `layout(nodes, edges, options?)` | Convenience method with separate parameters |
|
|
166
|
+
| `calculateLayoutMap(input)` | Returns Map of node ID to position |
|
|
167
|
+
| `recalculateLayout(nodes, edges, existingPositions, options?)` | Recalculate with position preservation |
|
|
168
|
+
| `addNodesToLayout(previousResult, newNodes, allEdges, options?)` | Add nodes incrementally |
|
|
169
|
+
| `removeNodesFromLayout(previousResult, nodeIds, allEdges, options?, preservePositions?)` | Remove nodes |
|
|
170
|
+
|
|
171
|
+
### Types
|
|
172
|
+
|
|
173
|
+
| Type | Description |
|
|
174
|
+
|------|-------------|
|
|
175
|
+
| `LayoutNode` | Input node with id, width, height, optional fixedX/fixedY |
|
|
176
|
+
| `LayoutEdge` | Edge with id, sourceId, targetId, optional type |
|
|
177
|
+
| `LayoutOptions` | Configuration for layout algorithm |
|
|
178
|
+
| `LayoutResult` | Output with positioned nodes and graph dimensions |
|
|
179
|
+
| `LayoutNodeResult` | Positioned node with x, y coordinates |
|
|
180
|
+
| `LayoutEngine` | Interface for custom layout engines |
|
|
181
|
+
|
|
182
|
+
## Running unit tests
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
nx test ngx-layout
|
|
186
|
+
```
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { InjectionToken, inject, Injectable } from '@angular/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Layout Library Models
|
|
7
|
+
*
|
|
8
|
+
* These interfaces are engine-agnostic and define the contract
|
|
9
|
+
* for any layout engine implementation (ELK, dagre, etc.)
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Default layout options
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_LAYOUT_OPTIONS = {
|
|
15
|
+
algorithm: 'layered',
|
|
16
|
+
direction: 'DOWN',
|
|
17
|
+
nodeSpacing: 50,
|
|
18
|
+
layerSpacing: 50,
|
|
19
|
+
padding: 20,
|
|
20
|
+
considerEdges: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* ELK algorithm identifiers
|
|
25
|
+
* Maps our abstract algorithm types to ELK-specific algorithm IDs
|
|
26
|
+
*/
|
|
27
|
+
const ELK_ALGORITHMS = {
|
|
28
|
+
layered: 'layered',
|
|
29
|
+
force: 'force',
|
|
30
|
+
stress: 'stress',
|
|
31
|
+
radial: 'radial',
|
|
32
|
+
tree: 'mrtree',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* ELK direction values
|
|
36
|
+
* Maps our direction type to ELK-specific direction values
|
|
37
|
+
*/
|
|
38
|
+
const ELK_DIRECTIONS = {
|
|
39
|
+
DOWN: 'DOWN',
|
|
40
|
+
UP: 'UP',
|
|
41
|
+
RIGHT: 'RIGHT',
|
|
42
|
+
LEFT: 'LEFT',
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* ELK.js layout engine implementation
|
|
46
|
+
*
|
|
47
|
+
* Uses ELK.js (Eclipse Layout Kernel) for graph layout calculation.
|
|
48
|
+
* This is a high-quality layout library that supports multiple algorithms.
|
|
49
|
+
*/
|
|
50
|
+
class ElkLayoutEngine {
|
|
51
|
+
name = 'ELK';
|
|
52
|
+
elk;
|
|
53
|
+
constructor() {
|
|
54
|
+
this.elk = new ELK();
|
|
55
|
+
}
|
|
56
|
+
async calculateLayout(input) {
|
|
57
|
+
const options = { ...DEFAULT_LAYOUT_OPTIONS, ...input.options };
|
|
58
|
+
// Build ELK graph structure
|
|
59
|
+
const elkGraph = this.buildElkGraph(input, options);
|
|
60
|
+
// Calculate layout
|
|
61
|
+
const layoutedGraph = await this.elk.layout(elkGraph);
|
|
62
|
+
// Extract results
|
|
63
|
+
return this.extractResults(layoutedGraph);
|
|
64
|
+
}
|
|
65
|
+
dispose() {
|
|
66
|
+
// ELK doesn't require explicit cleanup, but we provide
|
|
67
|
+
// this method for consistency with the interface
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Convert our abstract input to ELK-specific graph structure
|
|
71
|
+
*/
|
|
72
|
+
buildElkGraph(input, options) {
|
|
73
|
+
const elkOptions = this.buildElkOptions(options);
|
|
74
|
+
// Build nodes
|
|
75
|
+
const children = input.nodes.map((node) => {
|
|
76
|
+
const elkNode = {
|
|
77
|
+
id: node.id,
|
|
78
|
+
width: node.width,
|
|
79
|
+
height: node.height,
|
|
80
|
+
};
|
|
81
|
+
// Handle fixed positions
|
|
82
|
+
if (node.fixedX !== undefined && node.fixedY !== undefined) {
|
|
83
|
+
elkNode.x = node.fixedX;
|
|
84
|
+
elkNode.y = node.fixedY;
|
|
85
|
+
// Mark as fixed in ELK
|
|
86
|
+
elkNode.layoutOptions = {
|
|
87
|
+
'elk.position': `(${node.fixedX}, ${node.fixedY})`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return elkNode;
|
|
91
|
+
});
|
|
92
|
+
// Build edges (handling bidirectional edges)
|
|
93
|
+
const edges = options.considerEdges
|
|
94
|
+
? this.buildElkEdges(input.edges)
|
|
95
|
+
: [];
|
|
96
|
+
return {
|
|
97
|
+
id: 'root',
|
|
98
|
+
layoutOptions: elkOptions,
|
|
99
|
+
children,
|
|
100
|
+
edges,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Convert layout edges to ELK edges, expanding bidirectional edges
|
|
105
|
+
*/
|
|
106
|
+
buildElkEdges(edges) {
|
|
107
|
+
const elkEdges = [];
|
|
108
|
+
for (const edge of edges) {
|
|
109
|
+
// Add the primary edge (source → target)
|
|
110
|
+
elkEdges.push({
|
|
111
|
+
id: edge.id,
|
|
112
|
+
sources: [edge.sourceId],
|
|
113
|
+
targets: [edge.targetId],
|
|
114
|
+
});
|
|
115
|
+
// For bidirectional edges, add reverse edge (target → source)
|
|
116
|
+
if (edge.type === 'bidirectional') {
|
|
117
|
+
elkEdges.push({
|
|
118
|
+
id: `${edge.id}_reverse`,
|
|
119
|
+
sources: [edge.targetId],
|
|
120
|
+
targets: [edge.sourceId],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return elkEdges;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Convert our abstract options to ELK-specific layout options
|
|
128
|
+
*/
|
|
129
|
+
buildElkOptions(options) {
|
|
130
|
+
const elkOptions = {
|
|
131
|
+
'elk.algorithm': ELK_ALGORITHMS[options.algorithm],
|
|
132
|
+
'elk.spacing.nodeNode': String(options.nodeSpacing),
|
|
133
|
+
'elk.padding': `[top=${options.padding}, left=${options.padding}, bottom=${options.padding}, right=${options.padding}]`,
|
|
134
|
+
};
|
|
135
|
+
// Algorithm-specific options
|
|
136
|
+
switch (options.algorithm) {
|
|
137
|
+
case 'layered':
|
|
138
|
+
elkOptions['elk.direction'] = ELK_DIRECTIONS[options.direction];
|
|
139
|
+
elkOptions['elk.layered.spacing.nodeNodeBetweenLayers'] = String(options.layerSpacing);
|
|
140
|
+
// Improve edge routing
|
|
141
|
+
elkOptions['elk.layered.spacing.edgeNodeBetweenLayers'] = String(options.layerSpacing / 2);
|
|
142
|
+
break;
|
|
143
|
+
case 'tree':
|
|
144
|
+
elkOptions['elk.direction'] = ELK_DIRECTIONS[options.direction];
|
|
145
|
+
elkOptions['elk.mrtree.weighting'] = 'CONSTRAINT';
|
|
146
|
+
break;
|
|
147
|
+
case 'force':
|
|
148
|
+
// Force-directed doesn't use direction
|
|
149
|
+
elkOptions['elk.force.iterations'] = '300';
|
|
150
|
+
break;
|
|
151
|
+
case 'stress':
|
|
152
|
+
// Stress-based layout options
|
|
153
|
+
elkOptions['elk.stress.desiredEdgeLength'] = String(options.nodeSpacing * 2);
|
|
154
|
+
break;
|
|
155
|
+
case 'radial':
|
|
156
|
+
// Radial layout options
|
|
157
|
+
elkOptions['elk.radial.radius'] = String(options.layerSpacing);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
return elkOptions;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Extract layout results from ELK's output
|
|
164
|
+
*/
|
|
165
|
+
extractResults(elkGraph) {
|
|
166
|
+
const nodes = (elkGraph.children || []).map((child) => ({
|
|
167
|
+
id: child.id,
|
|
168
|
+
x: child.x ?? 0,
|
|
169
|
+
y: child.y ?? 0,
|
|
170
|
+
width: child.width ?? 0,
|
|
171
|
+
height: child.height ?? 0,
|
|
172
|
+
}));
|
|
173
|
+
return {
|
|
174
|
+
nodes,
|
|
175
|
+
width: elkGraph.width ?? 0,
|
|
176
|
+
height: elkGraph.height ?? 0,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Injection token for providing a custom layout engine
|
|
183
|
+
* Use this to swap the default ELK engine with a different implementation
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* providers: [
|
|
188
|
+
* { provide: LAYOUT_ENGINE, useClass: CustomLayoutEngine }
|
|
189
|
+
* ]
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
const LAYOUT_ENGINE = new InjectionToken('LAYOUT_ENGINE');
|
|
193
|
+
/**
|
|
194
|
+
* Factory function to create the default layout engine
|
|
195
|
+
*/
|
|
196
|
+
function defaultLayoutEngineFactory() {
|
|
197
|
+
return new ElkLayoutEngine();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Layout Service
|
|
201
|
+
*
|
|
202
|
+
* Provides graph layout calculation using a pluggable layout engine.
|
|
203
|
+
* By default uses ELK.js, but can be configured to use any engine
|
|
204
|
+
* that implements the LayoutEngine interface.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* // Basic usage
|
|
209
|
+
* const result = await layoutService.calculateLayout({
|
|
210
|
+
* nodes: [
|
|
211
|
+
* { id: 'a', width: 100, height: 50 },
|
|
212
|
+
* { id: 'b', width: 100, height: 50 },
|
|
213
|
+
* ],
|
|
214
|
+
* edges: [
|
|
215
|
+
* { id: 'e1', sourceId: 'a', targetId: 'b' }
|
|
216
|
+
* ],
|
|
217
|
+
* options: { algorithm: 'layered', direction: 'DOWN' }
|
|
218
|
+
* });
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
class LayoutService {
|
|
222
|
+
engine;
|
|
223
|
+
constructor() {
|
|
224
|
+
// Try to inject a custom engine, fall back to default ELK engine
|
|
225
|
+
const injectedEngine = inject(LAYOUT_ENGINE, { optional: true });
|
|
226
|
+
this.engine = injectedEngine ?? defaultLayoutEngineFactory();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get the name of the current layout engine
|
|
230
|
+
*/
|
|
231
|
+
get engineName() {
|
|
232
|
+
return this.engine.name;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Calculate layout positions for nodes based on their relationships
|
|
236
|
+
*
|
|
237
|
+
* @param input Layout input containing nodes, edges, and options
|
|
238
|
+
* @returns Promise resolving to layout result with positioned nodes
|
|
239
|
+
*/
|
|
240
|
+
async calculateLayout(input) {
|
|
241
|
+
// Validate input
|
|
242
|
+
this.validateInput(input);
|
|
243
|
+
return this.engine.calculateLayout(input);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Convenience method to calculate layout from separate parameters
|
|
247
|
+
*
|
|
248
|
+
* @param nodes Nodes to layout
|
|
249
|
+
* @param edges Edges/relationships between nodes
|
|
250
|
+
* @param options Layout options
|
|
251
|
+
* @returns Promise resolving to layout result
|
|
252
|
+
*/
|
|
253
|
+
async layout(nodes, edges, options) {
|
|
254
|
+
return this.calculateLayout({ nodes, edges, options });
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Calculate layout and return a map of node positions by ID
|
|
258
|
+
* Useful for quick position lookups
|
|
259
|
+
*
|
|
260
|
+
* @param input Layout input
|
|
261
|
+
* @returns Promise resolving to a Map of node ID to position
|
|
262
|
+
*/
|
|
263
|
+
async calculateLayoutMap(input) {
|
|
264
|
+
const result = await this.calculateLayout(input);
|
|
265
|
+
const positionMap = new Map();
|
|
266
|
+
for (const node of result.nodes) {
|
|
267
|
+
positionMap.set(node.id, { x: node.x, y: node.y });
|
|
268
|
+
}
|
|
269
|
+
return positionMap;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Recalculate layout while preserving positions of existing nodes
|
|
273
|
+
*
|
|
274
|
+
* This is useful when adding new nodes to an existing layout.
|
|
275
|
+
* Existing nodes will keep their positions (as fixed points),
|
|
276
|
+
* and only new nodes will be positioned by the layout algorithm.
|
|
277
|
+
*
|
|
278
|
+
* @param nodes All nodes (existing + new)
|
|
279
|
+
* @param edges All edges
|
|
280
|
+
* @param existingPositions Map of existing node IDs to their positions
|
|
281
|
+
* @param options Layout options
|
|
282
|
+
* @returns Promise resolving to layout result
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* // Add a new node to existing layout
|
|
287
|
+
* const newNodes = [...existingNodes, { id: 'new', width: 100, height: 50 }];
|
|
288
|
+
* const result = await layoutService.recalculateLayout(
|
|
289
|
+
* newNodes,
|
|
290
|
+
* edges,
|
|
291
|
+
* existingPositions, // Map from previous layout
|
|
292
|
+
* options
|
|
293
|
+
* );
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
async recalculateLayout(nodes, edges, existingPositions, options) {
|
|
297
|
+
// Apply fixed positions to existing nodes
|
|
298
|
+
const nodesWithFixedPositions = nodes.map((node) => {
|
|
299
|
+
const existingPos = existingPositions.get(node.id);
|
|
300
|
+
if (existingPos) {
|
|
301
|
+
return {
|
|
302
|
+
...node,
|
|
303
|
+
fixedX: existingPos.x,
|
|
304
|
+
fixedY: existingPos.y,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return node;
|
|
308
|
+
});
|
|
309
|
+
return this.calculateLayout({
|
|
310
|
+
nodes: nodesWithFixedPositions,
|
|
311
|
+
edges,
|
|
312
|
+
options,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Calculate incremental layout for new nodes only
|
|
317
|
+
*
|
|
318
|
+
* Takes a previous layout result and adds new nodes to it.
|
|
319
|
+
* Existing nodes keep their positions, new nodes are positioned.
|
|
320
|
+
*
|
|
321
|
+
* @param previousResult Previous layout result
|
|
322
|
+
* @param newNodes New nodes to add
|
|
323
|
+
* @param allEdges All edges (including edges for new nodes)
|
|
324
|
+
* @param options Layout options
|
|
325
|
+
* @returns Promise resolving to updated layout result
|
|
326
|
+
*/
|
|
327
|
+
async addNodesToLayout(previousResult, newNodes, allEdges, options) {
|
|
328
|
+
// Build position map from previous result
|
|
329
|
+
const existingPositions = new Map();
|
|
330
|
+
for (const node of previousResult.nodes) {
|
|
331
|
+
existingPositions.set(node.id, { x: node.x, y: node.y });
|
|
332
|
+
}
|
|
333
|
+
// Combine existing nodes (with dimensions) and new nodes
|
|
334
|
+
const existingNodes = previousResult.nodes.map((n) => ({
|
|
335
|
+
id: n.id,
|
|
336
|
+
width: n.width,
|
|
337
|
+
height: n.height,
|
|
338
|
+
}));
|
|
339
|
+
const allNodes = [...existingNodes, ...newNodes];
|
|
340
|
+
return this.recalculateLayout(allNodes, allEdges, existingPositions, options);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Remove nodes from layout and recalculate
|
|
344
|
+
*
|
|
345
|
+
* Removes specified nodes and their connected edges,
|
|
346
|
+
* then recalculates layout for remaining nodes.
|
|
347
|
+
*
|
|
348
|
+
* @param previousResult Previous layout result
|
|
349
|
+
* @param nodeIdsToRemove IDs of nodes to remove
|
|
350
|
+
* @param allEdges All edges (will be filtered to remove orphaned edges)
|
|
351
|
+
* @param options Layout options
|
|
352
|
+
* @param preservePositions If true, remaining nodes keep their positions
|
|
353
|
+
* @returns Promise resolving to updated layout result
|
|
354
|
+
*/
|
|
355
|
+
async removeNodesFromLayout(previousResult, nodeIdsToRemove, allEdges, options, preservePositions = true) {
|
|
356
|
+
const removeSet = new Set(nodeIdsToRemove);
|
|
357
|
+
// Filter out removed nodes
|
|
358
|
+
const remainingNodes = previousResult.nodes
|
|
359
|
+
.filter((n) => !removeSet.has(n.id))
|
|
360
|
+
.map((n) => ({
|
|
361
|
+
id: n.id,
|
|
362
|
+
width: n.width,
|
|
363
|
+
height: n.height,
|
|
364
|
+
}));
|
|
365
|
+
// Filter out edges connected to removed nodes
|
|
366
|
+
const remainingEdges = allEdges.filter((e) => !removeSet.has(e.sourceId) && !removeSet.has(e.targetId));
|
|
367
|
+
if (remainingNodes.length === 0) {
|
|
368
|
+
return { nodes: [], width: 0, height: 0 };
|
|
369
|
+
}
|
|
370
|
+
if (preservePositions) {
|
|
371
|
+
const existingPositions = new Map();
|
|
372
|
+
for (const node of previousResult.nodes) {
|
|
373
|
+
if (!removeSet.has(node.id)) {
|
|
374
|
+
existingPositions.set(node.id, { x: node.x, y: node.y });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return this.recalculateLayout(remainingNodes, remainingEdges, existingPositions, options);
|
|
378
|
+
}
|
|
379
|
+
return this.calculateLayout({
|
|
380
|
+
nodes: remainingNodes,
|
|
381
|
+
edges: remainingEdges,
|
|
382
|
+
options,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
ngOnDestroy() {
|
|
386
|
+
this.engine.dispose?.();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Validate layout input
|
|
390
|
+
*/
|
|
391
|
+
validateInput(input) {
|
|
392
|
+
if (!input.nodes || input.nodes.length === 0) {
|
|
393
|
+
throw new Error('LayoutService: At least one node is required');
|
|
394
|
+
}
|
|
395
|
+
// Check for duplicate node IDs
|
|
396
|
+
const nodeIds = new Set();
|
|
397
|
+
for (const node of input.nodes) {
|
|
398
|
+
if (nodeIds.has(node.id)) {
|
|
399
|
+
throw new Error(`LayoutService: Duplicate node ID "${node.id}"`);
|
|
400
|
+
}
|
|
401
|
+
nodeIds.add(node.id);
|
|
402
|
+
}
|
|
403
|
+
// Validate edges reference existing nodes
|
|
404
|
+
if (input.edges) {
|
|
405
|
+
for (const edge of input.edges) {
|
|
406
|
+
if (!nodeIds.has(edge.sourceId)) {
|
|
407
|
+
throw new Error(`LayoutService: Edge "${edge.id}" references unknown source node "${edge.sourceId}"`);
|
|
408
|
+
}
|
|
409
|
+
if (!nodeIds.has(edge.targetId)) {
|
|
410
|
+
throw new Error(`LayoutService: Edge "${edge.id}" references unknown target node "${edge.targetId}"`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
416
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LayoutService });
|
|
417
|
+
}
|
|
418
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LayoutService, decorators: [{
|
|
419
|
+
type: Injectable
|
|
420
|
+
}], ctorParameters: () => [] });
|
|
421
|
+
|
|
422
|
+
// Models - Values
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Generated bundle index. Do not edit.
|
|
426
|
+
*/
|
|
427
|
+
|
|
428
|
+
export { DEFAULT_LAYOUT_OPTIONS, ElkLayoutEngine, LAYOUT_ENGINE, LayoutService, defaultLayoutEngineFactory };
|
|
429
|
+
//# sourceMappingURL=ngx-km-layout.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngx-km-layout.mjs","sources":["../../../../libs/ngx-km-layout/src/lib/models/layout.models.ts","../../../../libs/ngx-km-layout/src/lib/engines/elk-layout-engine.ts","../../../../libs/ngx-km-layout/src/lib/services/layout.service.ts","../../../../libs/ngx-km-layout/src/index.ts","../../../../libs/ngx-km-layout/src/ngx-km-layout.ts"],"sourcesContent":["/**\n * Layout Library Models\n *\n * These interfaces are engine-agnostic and define the contract\n * for any layout engine implementation (ELK, dagre, etc.)\n */\n\n/**\n * Direction for hierarchical/layered layouts\n */\nexport type LayoutDirection = 'DOWN' | 'UP' | 'RIGHT' | 'LEFT';\n\n/**\n * Available layout algorithm types\n * These are abstract algorithm categories that map to specific engine implementations\n */\nexport type LayoutAlgorithm =\n | 'layered' // Hierarchical/layered layout (Sugiyama-style)\n | 'force' // Force-directed layout\n | 'stress' // Stress-minimization layout\n | 'radial' // Radial tree layout\n | 'tree'; // Simple tree layout\n\n/**\n * Input node definition for layout calculation\n */\nexport interface LayoutNode {\n /** Unique identifier for the node */\n id: string;\n /** Width of the node in pixels */\n width: number;\n /** Height of the node in pixels */\n height: number;\n /** Optional: Fixed X position (node won't be moved if set) */\n fixedX?: number;\n /** Optional: Fixed Y position (node won't be moved if set) */\n fixedY?: number;\n}\n\n/**\n * Edge type for layout calculation\n * - 'directed': One-way relationship (source → target)\n * - 'bidirectional': Two-way relationship (source ↔ target)\n */\nexport type EdgeType = 'directed' | 'bidirectional';\n\n/**\n * Edge/relationship between nodes for layout calculation\n */\nexport interface LayoutEdge {\n /** Unique identifier for the edge */\n id: string;\n /** Source node ID */\n sourceId: string;\n /** Target node ID */\n targetId: string;\n /** Edge type (default: 'directed') */\n type?: EdgeType;\n}\n\n/**\n * Configuration options for layout calculation\n */\nexport interface LayoutOptions {\n /** Layout algorithm to use (default: 'layered') */\n algorithm?: LayoutAlgorithm;\n\n /** Direction for hierarchical layouts (default: 'DOWN') */\n direction?: LayoutDirection;\n\n /** Horizontal spacing between nodes in pixels (default: 50) */\n nodeSpacing?: number;\n\n /** Vertical spacing between layers in pixels (default: 50) */\n layerSpacing?: number;\n\n /** Padding around the entire graph in pixels (default: 20) */\n padding?: number;\n\n /** Whether edges should be considered for layout (default: true) */\n considerEdges?: boolean;\n}\n\n/**\n * Result of layout calculation for a single node\n */\nexport interface LayoutNodeResult {\n /** Node ID */\n id: string;\n /** Calculated X position (top-left corner) */\n x: number;\n /** Calculated Y position (top-left corner) */\n y: number;\n /** Node width (same as input) */\n width: number;\n /** Node height (same as input) */\n height: number;\n}\n\n/**\n * Complete result of layout calculation\n */\nexport interface LayoutResult {\n /** Positioned nodes */\n nodes: LayoutNodeResult[];\n /** Total width of the laid out graph */\n width: number;\n /** Total height of the laid out graph */\n height: number;\n}\n\n/**\n * Input data for layout calculation\n */\nexport interface LayoutInput {\n /** Nodes to layout */\n nodes: LayoutNode[];\n /** Edges/relationships between nodes */\n edges: LayoutEdge[];\n /** Layout options */\n options?: LayoutOptions;\n}\n\n/**\n * Default layout options\n */\nexport const DEFAULT_LAYOUT_OPTIONS: Required<LayoutOptions> = {\n algorithm: 'layered',\n direction: 'DOWN',\n nodeSpacing: 50,\n layerSpacing: 50,\n padding: 20,\n considerEdges: true,\n};\n","import ELK, { ElkNode, ElkExtendedEdge, LayoutOptions as ElkLayoutOptions } from 'elkjs/lib/elk.bundled.js';\nimport { LayoutEngine } from './layout-engine';\nimport {\n LayoutInput,\n LayoutResult,\n LayoutNodeResult,\n LayoutOptions,\n LayoutAlgorithm,\n LayoutDirection,\n LayoutEdge,\n DEFAULT_LAYOUT_OPTIONS,\n} from '../models/layout.models';\n\n/**\n * ELK algorithm identifiers\n * Maps our abstract algorithm types to ELK-specific algorithm IDs\n */\nconst ELK_ALGORITHMS: Record<LayoutAlgorithm, string> = {\n layered: 'layered',\n force: 'force',\n stress: 'stress',\n radial: 'radial',\n tree: 'mrtree',\n};\n\n/**\n * ELK direction values\n * Maps our direction type to ELK-specific direction values\n */\nconst ELK_DIRECTIONS: Record<LayoutDirection, string> = {\n DOWN: 'DOWN',\n UP: 'UP',\n RIGHT: 'RIGHT',\n LEFT: 'LEFT',\n};\n\n/**\n * ELK.js layout engine implementation\n *\n * Uses ELK.js (Eclipse Layout Kernel) for graph layout calculation.\n * This is a high-quality layout library that supports multiple algorithms.\n */\nexport class ElkLayoutEngine implements LayoutEngine {\n readonly name = 'ELK';\n\n private elk: InstanceType<typeof ELK>;\n\n constructor() {\n this.elk = new ELK();\n }\n\n async calculateLayout(input: LayoutInput): Promise<LayoutResult> {\n const options = { ...DEFAULT_LAYOUT_OPTIONS, ...input.options };\n\n // Build ELK graph structure\n const elkGraph = this.buildElkGraph(input, options);\n\n // Calculate layout\n const layoutedGraph = await this.elk.layout(elkGraph);\n\n // Extract results\n return this.extractResults(layoutedGraph);\n }\n\n dispose(): void {\n // ELK doesn't require explicit cleanup, but we provide\n // this method for consistency with the interface\n }\n\n /**\n * Convert our abstract input to ELK-specific graph structure\n */\n private buildElkGraph(\n input: LayoutInput,\n options: Required<LayoutOptions>\n ): ElkNode {\n const elkOptions = this.buildElkOptions(options);\n\n // Build nodes\n const children: ElkNode[] = input.nodes.map((node) => {\n const elkNode: ElkNode = {\n id: node.id,\n width: node.width,\n height: node.height,\n };\n\n // Handle fixed positions\n if (node.fixedX !== undefined && node.fixedY !== undefined) {\n elkNode.x = node.fixedX;\n elkNode.y = node.fixedY;\n // Mark as fixed in ELK\n elkNode.layoutOptions = {\n 'elk.position': `(${node.fixedX}, ${node.fixedY})`,\n };\n }\n\n return elkNode;\n });\n\n // Build edges (handling bidirectional edges)\n const edges: ElkExtendedEdge[] = options.considerEdges\n ? this.buildElkEdges(input.edges)\n : [];\n\n return {\n id: 'root',\n layoutOptions: elkOptions,\n children,\n edges,\n };\n }\n\n /**\n * Convert layout edges to ELK edges, expanding bidirectional edges\n */\n private buildElkEdges(edges: LayoutEdge[]): ElkExtendedEdge[] {\n const elkEdges: ElkExtendedEdge[] = [];\n\n for (const edge of edges) {\n // Add the primary edge (source → target)\n elkEdges.push({\n id: edge.id,\n sources: [edge.sourceId],\n targets: [edge.targetId],\n });\n\n // For bidirectional edges, add reverse edge (target → source)\n if (edge.type === 'bidirectional') {\n elkEdges.push({\n id: `${edge.id}_reverse`,\n sources: [edge.targetId],\n targets: [edge.sourceId],\n });\n }\n }\n\n return elkEdges;\n }\n\n /**\n * Convert our abstract options to ELK-specific layout options\n */\n private buildElkOptions(options: Required<LayoutOptions>): ElkLayoutOptions {\n const elkOptions: ElkLayoutOptions = {\n 'elk.algorithm': ELK_ALGORITHMS[options.algorithm],\n 'elk.spacing.nodeNode': String(options.nodeSpacing),\n 'elk.padding': `[top=${options.padding}, left=${options.padding}, bottom=${options.padding}, right=${options.padding}]`,\n };\n\n // Algorithm-specific options\n switch (options.algorithm) {\n case 'layered':\n elkOptions['elk.direction'] = ELK_DIRECTIONS[options.direction];\n elkOptions['elk.layered.spacing.nodeNodeBetweenLayers'] = String(\n options.layerSpacing\n );\n // Improve edge routing\n elkOptions['elk.layered.spacing.edgeNodeBetweenLayers'] = String(\n options.layerSpacing / 2\n );\n break;\n\n case 'tree':\n elkOptions['elk.direction'] = ELK_DIRECTIONS[options.direction];\n elkOptions['elk.mrtree.weighting'] = 'CONSTRAINT';\n break;\n\n case 'force':\n // Force-directed doesn't use direction\n elkOptions['elk.force.iterations'] = '300';\n break;\n\n case 'stress':\n // Stress-based layout options\n elkOptions['elk.stress.desiredEdgeLength'] = String(\n options.nodeSpacing * 2\n );\n break;\n\n case 'radial':\n // Radial layout options\n elkOptions['elk.radial.radius'] = String(options.layerSpacing);\n break;\n }\n\n return elkOptions;\n }\n\n /**\n * Extract layout results from ELK's output\n */\n private extractResults(elkGraph: ElkNode): LayoutResult {\n const nodes: LayoutNodeResult[] = (elkGraph.children || []).map((child) => ({\n id: child.id,\n x: child.x ?? 0,\n y: child.y ?? 0,\n width: child.width ?? 0,\n height: child.height ?? 0,\n }));\n\n return {\n nodes,\n width: elkGraph.width ?? 0,\n height: elkGraph.height ?? 0,\n };\n }\n}\n","import { Injectable, InjectionToken, inject, OnDestroy } from '@angular/core';\nimport { LayoutEngine } from '../engines/layout-engine';\nimport { ElkLayoutEngine } from '../engines/elk-layout-engine';\nimport {\n LayoutInput,\n LayoutResult,\n LayoutNode,\n LayoutNodeResult,\n LayoutEdge,\n LayoutOptions,\n} from '../models/layout.models';\n\n/**\n * Injection token for providing a custom layout engine\n * Use this to swap the default ELK engine with a different implementation\n *\n * @example\n * ```typescript\n * providers: [\n * { provide: LAYOUT_ENGINE, useClass: CustomLayoutEngine }\n * ]\n * ```\n */\nexport const LAYOUT_ENGINE = new InjectionToken<LayoutEngine>('LAYOUT_ENGINE');\n\n/**\n * Factory function to create the default layout engine\n */\nexport function defaultLayoutEngineFactory(): LayoutEngine {\n return new ElkLayoutEngine();\n}\n\n/**\n * Layout Service\n *\n * Provides graph layout calculation using a pluggable layout engine.\n * By default uses ELK.js, but can be configured to use any engine\n * that implements the LayoutEngine interface.\n *\n * @example\n * ```typescript\n * // Basic usage\n * const result = await layoutService.calculateLayout({\n * nodes: [\n * { id: 'a', width: 100, height: 50 },\n * { id: 'b', width: 100, height: 50 },\n * ],\n * edges: [\n * { id: 'e1', sourceId: 'a', targetId: 'b' }\n * ],\n * options: { algorithm: 'layered', direction: 'DOWN' }\n * });\n * ```\n */\n@Injectable()\nexport class LayoutService implements OnDestroy {\n private engine: LayoutEngine;\n\n constructor() {\n // Try to inject a custom engine, fall back to default ELK engine\n const injectedEngine = inject(LAYOUT_ENGINE, { optional: true });\n this.engine = injectedEngine ?? defaultLayoutEngineFactory();\n }\n\n /**\n * Get the name of the current layout engine\n */\n get engineName(): string {\n return this.engine.name;\n }\n\n /**\n * Calculate layout positions for nodes based on their relationships\n *\n * @param input Layout input containing nodes, edges, and options\n * @returns Promise resolving to layout result with positioned nodes\n */\n async calculateLayout(input: LayoutInput): Promise<LayoutResult> {\n // Validate input\n this.validateInput(input);\n\n return this.engine.calculateLayout(input);\n }\n\n /**\n * Convenience method to calculate layout from separate parameters\n *\n * @param nodes Nodes to layout\n * @param edges Edges/relationships between nodes\n * @param options Layout options\n * @returns Promise resolving to layout result\n */\n async layout(\n nodes: LayoutNode[],\n edges: LayoutEdge[],\n options?: LayoutOptions\n ): Promise<LayoutResult> {\n return this.calculateLayout({ nodes, edges, options });\n }\n\n /**\n * Calculate layout and return a map of node positions by ID\n * Useful for quick position lookups\n *\n * @param input Layout input\n * @returns Promise resolving to a Map of node ID to position\n */\n async calculateLayoutMap(\n input: LayoutInput\n ): Promise<Map<string, { x: number; y: number }>> {\n const result = await this.calculateLayout(input);\n\n const positionMap = new Map<string, { x: number; y: number }>();\n for (const node of result.nodes) {\n positionMap.set(node.id, { x: node.x, y: node.y });\n }\n\n return positionMap;\n }\n\n /**\n * Recalculate layout while preserving positions of existing nodes\n *\n * This is useful when adding new nodes to an existing layout.\n * Existing nodes will keep their positions (as fixed points),\n * and only new nodes will be positioned by the layout algorithm.\n *\n * @param nodes All nodes (existing + new)\n * @param edges All edges\n * @param existingPositions Map of existing node IDs to their positions\n * @param options Layout options\n * @returns Promise resolving to layout result\n *\n * @example\n * ```typescript\n * // Add a new node to existing layout\n * const newNodes = [...existingNodes, { id: 'new', width: 100, height: 50 }];\n * const result = await layoutService.recalculateLayout(\n * newNodes,\n * edges,\n * existingPositions, // Map from previous layout\n * options\n * );\n * ```\n */\n async recalculateLayout(\n nodes: LayoutNode[],\n edges: LayoutEdge[],\n existingPositions: Map<string, { x: number; y: number }>,\n options?: LayoutOptions\n ): Promise<LayoutResult> {\n // Apply fixed positions to existing nodes\n const nodesWithFixedPositions = nodes.map((node) => {\n const existingPos = existingPositions.get(node.id);\n if (existingPos) {\n return {\n ...node,\n fixedX: existingPos.x,\n fixedY: existingPos.y,\n };\n }\n return node;\n });\n\n return this.calculateLayout({\n nodes: nodesWithFixedPositions,\n edges,\n options,\n });\n }\n\n /**\n * Calculate incremental layout for new nodes only\n *\n * Takes a previous layout result and adds new nodes to it.\n * Existing nodes keep their positions, new nodes are positioned.\n *\n * @param previousResult Previous layout result\n * @param newNodes New nodes to add\n * @param allEdges All edges (including edges for new nodes)\n * @param options Layout options\n * @returns Promise resolving to updated layout result\n */\n async addNodesToLayout(\n previousResult: LayoutResult,\n newNodes: LayoutNode[],\n allEdges: LayoutEdge[],\n options?: LayoutOptions\n ): Promise<LayoutResult> {\n // Build position map from previous result\n const existingPositions = new Map<string, { x: number; y: number }>();\n for (const node of previousResult.nodes) {\n existingPositions.set(node.id, { x: node.x, y: node.y });\n }\n\n // Combine existing nodes (with dimensions) and new nodes\n const existingNodes: LayoutNode[] = previousResult.nodes.map((n) => ({\n id: n.id,\n width: n.width,\n height: n.height,\n }));\n\n const allNodes = [...existingNodes, ...newNodes];\n\n return this.recalculateLayout(allNodes, allEdges, existingPositions, options);\n }\n\n /**\n * Remove nodes from layout and recalculate\n *\n * Removes specified nodes and their connected edges,\n * then recalculates layout for remaining nodes.\n *\n * @param previousResult Previous layout result\n * @param nodeIdsToRemove IDs of nodes to remove\n * @param allEdges All edges (will be filtered to remove orphaned edges)\n * @param options Layout options\n * @param preservePositions If true, remaining nodes keep their positions\n * @returns Promise resolving to updated layout result\n */\n async removeNodesFromLayout(\n previousResult: LayoutResult,\n nodeIdsToRemove: string[],\n allEdges: LayoutEdge[],\n options?: LayoutOptions,\n preservePositions = true\n ): Promise<LayoutResult> {\n const removeSet = new Set(nodeIdsToRemove);\n\n // Filter out removed nodes\n const remainingNodes: LayoutNode[] = previousResult.nodes\n .filter((n) => !removeSet.has(n.id))\n .map((n) => ({\n id: n.id,\n width: n.width,\n height: n.height,\n }));\n\n // Filter out edges connected to removed nodes\n const remainingEdges = allEdges.filter(\n (e) => !removeSet.has(e.sourceId) && !removeSet.has(e.targetId)\n );\n\n if (remainingNodes.length === 0) {\n return { nodes: [], width: 0, height: 0 };\n }\n\n if (preservePositions) {\n const existingPositions = new Map<string, { x: number; y: number }>();\n for (const node of previousResult.nodes) {\n if (!removeSet.has(node.id)) {\n existingPositions.set(node.id, { x: node.x, y: node.y });\n }\n }\n return this.recalculateLayout(\n remainingNodes,\n remainingEdges,\n existingPositions,\n options\n );\n }\n\n return this.calculateLayout({\n nodes: remainingNodes,\n edges: remainingEdges,\n options,\n });\n }\n\n ngOnDestroy(): void {\n this.engine.dispose?.();\n }\n\n /**\n * Validate layout input\n */\n private validateInput(input: LayoutInput): void {\n if (!input.nodes || input.nodes.length === 0) {\n throw new Error('LayoutService: At least one node is required');\n }\n\n // Check for duplicate node IDs\n const nodeIds = new Set<string>();\n for (const node of input.nodes) {\n if (nodeIds.has(node.id)) {\n throw new Error(`LayoutService: Duplicate node ID \"${node.id}\"`);\n }\n nodeIds.add(node.id);\n }\n\n // Validate edges reference existing nodes\n if (input.edges) {\n for (const edge of input.edges) {\n if (!nodeIds.has(edge.sourceId)) {\n throw new Error(\n `LayoutService: Edge \"${edge.id}\" references unknown source node \"${edge.sourceId}\"`\n );\n }\n if (!nodeIds.has(edge.targetId)) {\n throw new Error(\n `LayoutService: Edge \"${edge.id}\" references unknown target node \"${edge.targetId}\"`\n );\n }\n }\n }\n }\n}\n","// Models - Types\nexport type {\n LayoutDirection,\n LayoutAlgorithm,\n EdgeType,\n LayoutNode,\n LayoutEdge,\n LayoutOptions,\n LayoutNodeResult,\n LayoutResult,\n LayoutInput,\n} from './lib/models/layout.models';\n\n// Models - Values\nexport { DEFAULT_LAYOUT_OPTIONS } from './lib/models/layout.models';\n\n// Engine abstraction (for custom engine implementations)\nexport type { LayoutEngine } from './lib/engines/layout-engine';\nexport { ElkLayoutEngine } from './lib/engines/elk-layout-engine';\n\n// Service\nexport {\n LayoutService,\n LAYOUT_ENGINE,\n defaultLayoutEngineFactory,\n} from './lib/services/layout.service';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { OnDestroy, InjectionToken } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Layout Library Models
|
|
6
|
+
*
|
|
7
|
+
* These interfaces are engine-agnostic and define the contract
|
|
8
|
+
* for any layout engine implementation (ELK, dagre, etc.)
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Direction for hierarchical/layered layouts
|
|
12
|
+
*/
|
|
13
|
+
type LayoutDirection = 'DOWN' | 'UP' | 'RIGHT' | 'LEFT';
|
|
14
|
+
/**
|
|
15
|
+
* Available layout algorithm types
|
|
16
|
+
* These are abstract algorithm categories that map to specific engine implementations
|
|
17
|
+
*/
|
|
18
|
+
type LayoutAlgorithm = 'layered' | 'force' | 'stress' | 'radial' | 'tree';
|
|
19
|
+
/**
|
|
20
|
+
* Input node definition for layout calculation
|
|
21
|
+
*/
|
|
22
|
+
interface LayoutNode {
|
|
23
|
+
/** Unique identifier for the node */
|
|
24
|
+
id: string;
|
|
25
|
+
/** Width of the node in pixels */
|
|
26
|
+
width: number;
|
|
27
|
+
/** Height of the node in pixels */
|
|
28
|
+
height: number;
|
|
29
|
+
/** Optional: Fixed X position (node won't be moved if set) */
|
|
30
|
+
fixedX?: number;
|
|
31
|
+
/** Optional: Fixed Y position (node won't be moved if set) */
|
|
32
|
+
fixedY?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Edge type for layout calculation
|
|
36
|
+
* - 'directed': One-way relationship (source → target)
|
|
37
|
+
* - 'bidirectional': Two-way relationship (source ↔ target)
|
|
38
|
+
*/
|
|
39
|
+
type EdgeType = 'directed' | 'bidirectional';
|
|
40
|
+
/**
|
|
41
|
+
* Edge/relationship between nodes for layout calculation
|
|
42
|
+
*/
|
|
43
|
+
interface LayoutEdge {
|
|
44
|
+
/** Unique identifier for the edge */
|
|
45
|
+
id: string;
|
|
46
|
+
/** Source node ID */
|
|
47
|
+
sourceId: string;
|
|
48
|
+
/** Target node ID */
|
|
49
|
+
targetId: string;
|
|
50
|
+
/** Edge type (default: 'directed') */
|
|
51
|
+
type?: EdgeType;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Configuration options for layout calculation
|
|
55
|
+
*/
|
|
56
|
+
interface LayoutOptions {
|
|
57
|
+
/** Layout algorithm to use (default: 'layered') */
|
|
58
|
+
algorithm?: LayoutAlgorithm;
|
|
59
|
+
/** Direction for hierarchical layouts (default: 'DOWN') */
|
|
60
|
+
direction?: LayoutDirection;
|
|
61
|
+
/** Horizontal spacing between nodes in pixels (default: 50) */
|
|
62
|
+
nodeSpacing?: number;
|
|
63
|
+
/** Vertical spacing between layers in pixels (default: 50) */
|
|
64
|
+
layerSpacing?: number;
|
|
65
|
+
/** Padding around the entire graph in pixels (default: 20) */
|
|
66
|
+
padding?: number;
|
|
67
|
+
/** Whether edges should be considered for layout (default: true) */
|
|
68
|
+
considerEdges?: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Result of layout calculation for a single node
|
|
72
|
+
*/
|
|
73
|
+
interface LayoutNodeResult {
|
|
74
|
+
/** Node ID */
|
|
75
|
+
id: string;
|
|
76
|
+
/** Calculated X position (top-left corner) */
|
|
77
|
+
x: number;
|
|
78
|
+
/** Calculated Y position (top-left corner) */
|
|
79
|
+
y: number;
|
|
80
|
+
/** Node width (same as input) */
|
|
81
|
+
width: number;
|
|
82
|
+
/** Node height (same as input) */
|
|
83
|
+
height: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Complete result of layout calculation
|
|
87
|
+
*/
|
|
88
|
+
interface LayoutResult {
|
|
89
|
+
/** Positioned nodes */
|
|
90
|
+
nodes: LayoutNodeResult[];
|
|
91
|
+
/** Total width of the laid out graph */
|
|
92
|
+
width: number;
|
|
93
|
+
/** Total height of the laid out graph */
|
|
94
|
+
height: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Input data for layout calculation
|
|
98
|
+
*/
|
|
99
|
+
interface LayoutInput {
|
|
100
|
+
/** Nodes to layout */
|
|
101
|
+
nodes: LayoutNode[];
|
|
102
|
+
/** Edges/relationships between nodes */
|
|
103
|
+
edges: LayoutEdge[];
|
|
104
|
+
/** Layout options */
|
|
105
|
+
options?: LayoutOptions;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Default layout options
|
|
109
|
+
*/
|
|
110
|
+
declare const DEFAULT_LAYOUT_OPTIONS: Required<LayoutOptions>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Abstract layout engine interface
|
|
114
|
+
*
|
|
115
|
+
* Implement this interface to create a new layout engine.
|
|
116
|
+
* The layout service uses this abstraction to allow swapping
|
|
117
|
+
* different layout engines (ELK, dagre, etc.) without changing
|
|
118
|
+
* the consumer API.
|
|
119
|
+
*/
|
|
120
|
+
interface LayoutEngine {
|
|
121
|
+
/**
|
|
122
|
+
* Name of the layout engine (for debugging/logging)
|
|
123
|
+
*/
|
|
124
|
+
readonly name: string;
|
|
125
|
+
/**
|
|
126
|
+
* Calculate layout positions for the given input
|
|
127
|
+
* @param input Nodes, edges, and options for layout calculation
|
|
128
|
+
* @returns Promise resolving to the layout result with node positions
|
|
129
|
+
*/
|
|
130
|
+
calculateLayout(input: LayoutInput): Promise<LayoutResult>;
|
|
131
|
+
/**
|
|
132
|
+
* Optional: Clean up any resources used by the engine
|
|
133
|
+
*/
|
|
134
|
+
dispose?(): void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* ELK.js layout engine implementation
|
|
139
|
+
*
|
|
140
|
+
* Uses ELK.js (Eclipse Layout Kernel) for graph layout calculation.
|
|
141
|
+
* This is a high-quality layout library that supports multiple algorithms.
|
|
142
|
+
*/
|
|
143
|
+
declare class ElkLayoutEngine implements LayoutEngine {
|
|
144
|
+
readonly name = "ELK";
|
|
145
|
+
private elk;
|
|
146
|
+
constructor();
|
|
147
|
+
calculateLayout(input: LayoutInput): Promise<LayoutResult>;
|
|
148
|
+
dispose(): void;
|
|
149
|
+
/**
|
|
150
|
+
* Convert our abstract input to ELK-specific graph structure
|
|
151
|
+
*/
|
|
152
|
+
private buildElkGraph;
|
|
153
|
+
/**
|
|
154
|
+
* Convert layout edges to ELK edges, expanding bidirectional edges
|
|
155
|
+
*/
|
|
156
|
+
private buildElkEdges;
|
|
157
|
+
/**
|
|
158
|
+
* Convert our abstract options to ELK-specific layout options
|
|
159
|
+
*/
|
|
160
|
+
private buildElkOptions;
|
|
161
|
+
/**
|
|
162
|
+
* Extract layout results from ELK's output
|
|
163
|
+
*/
|
|
164
|
+
private extractResults;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Injection token for providing a custom layout engine
|
|
169
|
+
* Use this to swap the default ELK engine with a different implementation
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* providers: [
|
|
174
|
+
* { provide: LAYOUT_ENGINE, useClass: CustomLayoutEngine }
|
|
175
|
+
* ]
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
declare const LAYOUT_ENGINE: InjectionToken<LayoutEngine>;
|
|
179
|
+
/**
|
|
180
|
+
* Factory function to create the default layout engine
|
|
181
|
+
*/
|
|
182
|
+
declare function defaultLayoutEngineFactory(): LayoutEngine;
|
|
183
|
+
/**
|
|
184
|
+
* Layout Service
|
|
185
|
+
*
|
|
186
|
+
* Provides graph layout calculation using a pluggable layout engine.
|
|
187
|
+
* By default uses ELK.js, but can be configured to use any engine
|
|
188
|
+
* that implements the LayoutEngine interface.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* // Basic usage
|
|
193
|
+
* const result = await layoutService.calculateLayout({
|
|
194
|
+
* nodes: [
|
|
195
|
+
* { id: 'a', width: 100, height: 50 },
|
|
196
|
+
* { id: 'b', width: 100, height: 50 },
|
|
197
|
+
* ],
|
|
198
|
+
* edges: [
|
|
199
|
+
* { id: 'e1', sourceId: 'a', targetId: 'b' }
|
|
200
|
+
* ],
|
|
201
|
+
* options: { algorithm: 'layered', direction: 'DOWN' }
|
|
202
|
+
* });
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
declare class LayoutService implements OnDestroy {
|
|
206
|
+
private engine;
|
|
207
|
+
constructor();
|
|
208
|
+
/**
|
|
209
|
+
* Get the name of the current layout engine
|
|
210
|
+
*/
|
|
211
|
+
get engineName(): string;
|
|
212
|
+
/**
|
|
213
|
+
* Calculate layout positions for nodes based on their relationships
|
|
214
|
+
*
|
|
215
|
+
* @param input Layout input containing nodes, edges, and options
|
|
216
|
+
* @returns Promise resolving to layout result with positioned nodes
|
|
217
|
+
*/
|
|
218
|
+
calculateLayout(input: LayoutInput): Promise<LayoutResult>;
|
|
219
|
+
/**
|
|
220
|
+
* Convenience method to calculate layout from separate parameters
|
|
221
|
+
*
|
|
222
|
+
* @param nodes Nodes to layout
|
|
223
|
+
* @param edges Edges/relationships between nodes
|
|
224
|
+
* @param options Layout options
|
|
225
|
+
* @returns Promise resolving to layout result
|
|
226
|
+
*/
|
|
227
|
+
layout(nodes: LayoutNode[], edges: LayoutEdge[], options?: LayoutOptions): Promise<LayoutResult>;
|
|
228
|
+
/**
|
|
229
|
+
* Calculate layout and return a map of node positions by ID
|
|
230
|
+
* Useful for quick position lookups
|
|
231
|
+
*
|
|
232
|
+
* @param input Layout input
|
|
233
|
+
* @returns Promise resolving to a Map of node ID to position
|
|
234
|
+
*/
|
|
235
|
+
calculateLayoutMap(input: LayoutInput): Promise<Map<string, {
|
|
236
|
+
x: number;
|
|
237
|
+
y: number;
|
|
238
|
+
}>>;
|
|
239
|
+
/**
|
|
240
|
+
* Recalculate layout while preserving positions of existing nodes
|
|
241
|
+
*
|
|
242
|
+
* This is useful when adding new nodes to an existing layout.
|
|
243
|
+
* Existing nodes will keep their positions (as fixed points),
|
|
244
|
+
* and only new nodes will be positioned by the layout algorithm.
|
|
245
|
+
*
|
|
246
|
+
* @param nodes All nodes (existing + new)
|
|
247
|
+
* @param edges All edges
|
|
248
|
+
* @param existingPositions Map of existing node IDs to their positions
|
|
249
|
+
* @param options Layout options
|
|
250
|
+
* @returns Promise resolving to layout result
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* // Add a new node to existing layout
|
|
255
|
+
* const newNodes = [...existingNodes, { id: 'new', width: 100, height: 50 }];
|
|
256
|
+
* const result = await layoutService.recalculateLayout(
|
|
257
|
+
* newNodes,
|
|
258
|
+
* edges,
|
|
259
|
+
* existingPositions, // Map from previous layout
|
|
260
|
+
* options
|
|
261
|
+
* );
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
recalculateLayout(nodes: LayoutNode[], edges: LayoutEdge[], existingPositions: Map<string, {
|
|
265
|
+
x: number;
|
|
266
|
+
y: number;
|
|
267
|
+
}>, options?: LayoutOptions): Promise<LayoutResult>;
|
|
268
|
+
/**
|
|
269
|
+
* Calculate incremental layout for new nodes only
|
|
270
|
+
*
|
|
271
|
+
* Takes a previous layout result and adds new nodes to it.
|
|
272
|
+
* Existing nodes keep their positions, new nodes are positioned.
|
|
273
|
+
*
|
|
274
|
+
* @param previousResult Previous layout result
|
|
275
|
+
* @param newNodes New nodes to add
|
|
276
|
+
* @param allEdges All edges (including edges for new nodes)
|
|
277
|
+
* @param options Layout options
|
|
278
|
+
* @returns Promise resolving to updated layout result
|
|
279
|
+
*/
|
|
280
|
+
addNodesToLayout(previousResult: LayoutResult, newNodes: LayoutNode[], allEdges: LayoutEdge[], options?: LayoutOptions): Promise<LayoutResult>;
|
|
281
|
+
/**
|
|
282
|
+
* Remove nodes from layout and recalculate
|
|
283
|
+
*
|
|
284
|
+
* Removes specified nodes and their connected edges,
|
|
285
|
+
* then recalculates layout for remaining nodes.
|
|
286
|
+
*
|
|
287
|
+
* @param previousResult Previous layout result
|
|
288
|
+
* @param nodeIdsToRemove IDs of nodes to remove
|
|
289
|
+
* @param allEdges All edges (will be filtered to remove orphaned edges)
|
|
290
|
+
* @param options Layout options
|
|
291
|
+
* @param preservePositions If true, remaining nodes keep their positions
|
|
292
|
+
* @returns Promise resolving to updated layout result
|
|
293
|
+
*/
|
|
294
|
+
removeNodesFromLayout(previousResult: LayoutResult, nodeIdsToRemove: string[], allEdges: LayoutEdge[], options?: LayoutOptions, preservePositions?: boolean): Promise<LayoutResult>;
|
|
295
|
+
ngOnDestroy(): void;
|
|
296
|
+
/**
|
|
297
|
+
* Validate layout input
|
|
298
|
+
*/
|
|
299
|
+
private validateInput;
|
|
300
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<LayoutService, never>;
|
|
301
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<LayoutService>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export { DEFAULT_LAYOUT_OPTIONS, ElkLayoutEngine, LAYOUT_ENGINE, LayoutService, defaultLayoutEngineFactory };
|
|
305
|
+
export type { EdgeType, LayoutAlgorithm, LayoutDirection, LayoutEdge, LayoutEngine, LayoutInput, LayoutNode, LayoutNodeResult, LayoutOptions, LayoutResult };
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ngx-km/layout",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@angular/common": ">=19.0.0",
|
|
6
|
+
"@angular/core": ">=19.0.0"
|
|
7
|
+
},
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"module": "fesm2022/ngx-km-layout.mjs",
|
|
10
|
+
"typings": "index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
"./package.json": {
|
|
13
|
+
"default": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"default": "./fesm2022/ngx-km-layout.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"tslib": "^2.3.0"
|
|
22
|
+
}
|
|
23
|
+
}
|