@ngx-km/graph 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 +43 -0
- package/fesm2022/ngx-km-graph.mjs +564 -0
- package/fesm2022/ngx-km-graph.mjs.map +1 -0
- package/index.d.ts +280 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @ngx-km/graph
|
|
2
|
+
|
|
3
|
+
Main graph visualization library combining all sub-libraries into a unified component.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Unified graph component integrating grid, layout, path-finding, and path-drawing
|
|
8
|
+
- Custom Angular components as nodes
|
|
9
|
+
- Type-safe node and relationship definitions
|
|
10
|
+
- Automatic layout with ELK.js
|
|
11
|
+
- Intelligent path routing between nodes
|
|
12
|
+
- Custom path pill components
|
|
13
|
+
- Reactive input handling with position preservation
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @ngx-km/graph
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { GraphComponent } from '@ngx-km/graph';
|
|
25
|
+
|
|
26
|
+
@Component({
|
|
27
|
+
imports: [GraphComponent],
|
|
28
|
+
template: `
|
|
29
|
+
<ngx-graph
|
|
30
|
+
[nodes]="nodes"
|
|
31
|
+
[relationships]="relationships"
|
|
32
|
+
[nodeComponent]="MyNodeComponent">
|
|
33
|
+
</ngx-graph>
|
|
34
|
+
`
|
|
35
|
+
})
|
|
36
|
+
export class MyComponent {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Running unit tests
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
nx test ngx-graph
|
|
43
|
+
```
|
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Injector, viewChild, input, output, signal, computed, effect, Component } from '@angular/core';
|
|
3
|
+
import { NgComponentOutlet } from '@angular/common';
|
|
4
|
+
import { GridComponent } from '@ngx-km/grid';
|
|
5
|
+
import { LayoutService } from '@ngx-km/layout';
|
|
6
|
+
import { PathFindingService } from '@ngx-km/path-finding';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Injection token for accessing node data in dynamically rendered node components.
|
|
10
|
+
* The data can be either a plain value or a Signal for reactive updates.
|
|
11
|
+
*/
|
|
12
|
+
const GRAPH_NODE_DATA = new InjectionToken('GRAPH_NODE_DATA');
|
|
13
|
+
/**
|
|
14
|
+
* Injection token for accessing relationship data in dynamically rendered pill components.
|
|
15
|
+
* The data can be either a plain value or a Signal for reactive updates.
|
|
16
|
+
*/
|
|
17
|
+
const GRAPH_RELATIONSHIP_DATA = new InjectionToken('GRAPH_RELATIONSHIP_DATA');
|
|
18
|
+
/**
|
|
19
|
+
* Default path configuration
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_GRAPH_PATH_CONFIG = {
|
|
22
|
+
pathType: 'orthogonal',
|
|
23
|
+
strokeColor: '#6366f1',
|
|
24
|
+
strokeWidth: 2,
|
|
25
|
+
strokePattern: 'solid',
|
|
26
|
+
strokeOpacity: 1,
|
|
27
|
+
cornerRadius: 0,
|
|
28
|
+
arrowStyle: 'filled',
|
|
29
|
+
arrowSize: 10,
|
|
30
|
+
obstaclePadding: 20,
|
|
31
|
+
anchorSpacing: 15,
|
|
32
|
+
spreadAnchors: true,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Default layout configuration
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_GRAPH_LAYOUT_CONFIG = {
|
|
38
|
+
algorithm: 'layered',
|
|
39
|
+
direction: 'DOWN',
|
|
40
|
+
nodeSpacing: 50,
|
|
41
|
+
layerSpacing: 100,
|
|
42
|
+
autoLayout: true,
|
|
43
|
+
preservePositions: true,
|
|
44
|
+
fitPadding: 40,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Default graph configuration
|
|
48
|
+
*/
|
|
49
|
+
const DEFAULT_GRAPH_CONFIG = {
|
|
50
|
+
grid: {
|
|
51
|
+
dimensionMode: 'full',
|
|
52
|
+
backgroundMode: 'dots',
|
|
53
|
+
cellSize: 20,
|
|
54
|
+
panEnabled: true,
|
|
55
|
+
zoomEnabled: true,
|
|
56
|
+
dragEnabled: true,
|
|
57
|
+
},
|
|
58
|
+
layout: DEFAULT_GRAPH_LAYOUT_CONFIG,
|
|
59
|
+
paths: DEFAULT_GRAPH_PATH_CONFIG,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
class GraphComponent {
|
|
63
|
+
layoutService = inject(LayoutService);
|
|
64
|
+
pathFindingService = inject(PathFindingService);
|
|
65
|
+
injector = inject(Injector);
|
|
66
|
+
gridRef = viewChild('gridRef', ...(ngDevMode ? [{ debugName: "gridRef" }] : []));
|
|
67
|
+
// Inputs
|
|
68
|
+
nodes = input([], ...(ngDevMode ? [{ debugName: "nodes" }] : []));
|
|
69
|
+
relationships = input([], ...(ngDevMode ? [{ debugName: "relationships" }] : []));
|
|
70
|
+
config = input(DEFAULT_GRAPH_CONFIG, ...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
71
|
+
// Outputs
|
|
72
|
+
nodePositionChange = output();
|
|
73
|
+
viewportChange = output();
|
|
74
|
+
// Internal state - single source of truth for positions
|
|
75
|
+
nodePositions = signal(new Map(), ...(ngDevMode ? [{ debugName: "nodePositions" }] : []));
|
|
76
|
+
nodeDimensions = signal(new Map(), ...(ngDevMode ? [{ debugName: "nodeDimensions" }] : []));
|
|
77
|
+
viewport = signal({ x: 0, y: 0, zoom: 1 }, ...(ngDevMode ? [{ debugName: "viewport" }] : []));
|
|
78
|
+
calculatedPaths = signal([], ...(ngDevMode ? [{ debugName: "calculatedPaths" }] : []));
|
|
79
|
+
/** Whether initial layout has completed - used to prevent flash of unstyled nodes */
|
|
80
|
+
layoutReady = signal(false, ...(ngDevMode ? [{ debugName: "layoutReady" }] : []));
|
|
81
|
+
// Computed configs
|
|
82
|
+
gridConfig = computed(() => ({
|
|
83
|
+
dimensionMode: 'full',
|
|
84
|
+
backgroundMode: 'dots',
|
|
85
|
+
cellSize: 20,
|
|
86
|
+
panEnabled: true,
|
|
87
|
+
zoomEnabled: true,
|
|
88
|
+
dragEnabled: true,
|
|
89
|
+
...this.config().grid,
|
|
90
|
+
}), ...(ngDevMode ? [{ debugName: "gridConfig" }] : []));
|
|
91
|
+
layoutConfig = computed(() => ({
|
|
92
|
+
...DEFAULT_GRAPH_LAYOUT_CONFIG,
|
|
93
|
+
...this.config().layout,
|
|
94
|
+
}), ...(ngDevMode ? [{ debugName: "layoutConfig" }] : []));
|
|
95
|
+
pathConfig = computed(() => ({
|
|
96
|
+
...DEFAULT_GRAPH_PATH_CONFIG,
|
|
97
|
+
...this.config().paths,
|
|
98
|
+
}), ...(ngDevMode ? [{ debugName: "pathConfig" }] : []));
|
|
99
|
+
/** Pill component to render at path midpoints */
|
|
100
|
+
pillComponent = computed(() => this.config().pillComponent, ...(ngDevMode ? [{ debugName: "pillComponent" }] : []));
|
|
101
|
+
// Grid elements - combines input nodes with their positions
|
|
102
|
+
gridElements = computed(() => {
|
|
103
|
+
const inputNodes = this.nodes();
|
|
104
|
+
const positions = this.nodePositions();
|
|
105
|
+
return inputNodes.map((node, index) => {
|
|
106
|
+
const pos = positions.get(node.id);
|
|
107
|
+
// Default grid position if not set
|
|
108
|
+
const cols = Math.ceil(Math.sqrt(inputNodes.length));
|
|
109
|
+
const defaultX = (index % cols) * 200;
|
|
110
|
+
const defaultY = Math.floor(index / cols) * 150;
|
|
111
|
+
return {
|
|
112
|
+
id: node.id,
|
|
113
|
+
component: node.component,
|
|
114
|
+
data: node.data,
|
|
115
|
+
x: pos?.x ?? node.x ?? defaultX,
|
|
116
|
+
y: pos?.y ?? node.y ?? defaultY,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}, ...(ngDevMode ? [{ debugName: "gridElements" }] : []));
|
|
120
|
+
pathsTransform = computed(() => {
|
|
121
|
+
const vp = this.viewport();
|
|
122
|
+
return `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`;
|
|
123
|
+
}, ...(ngDevMode ? [{ debugName: "pathsTransform" }] : []));
|
|
124
|
+
constructor() {
|
|
125
|
+
// Run initial layout when nodes change
|
|
126
|
+
effect(() => {
|
|
127
|
+
const inputNodes = this.nodes();
|
|
128
|
+
const layoutCfg = this.layoutConfig();
|
|
129
|
+
if (inputNodes.length === 0) {
|
|
130
|
+
// No nodes - mark as ready immediately
|
|
131
|
+
this.layoutReady.set(true);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!layoutCfg.autoLayout) {
|
|
135
|
+
// Auto-layout disabled - show nodes at their provided positions immediately
|
|
136
|
+
this.layoutReady.set(true);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Check if we have positions for all nodes
|
|
140
|
+
const positions = this.nodePositions();
|
|
141
|
+
const needsLayout = inputNodes.some(n => !positions.has(n.id));
|
|
142
|
+
if (needsLayout) {
|
|
143
|
+
// Reset layoutReady when new nodes need positioning
|
|
144
|
+
this.layoutReady.set(false);
|
|
145
|
+
this.runLayout();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// Recalculate paths when relationships or positions change
|
|
149
|
+
effect(() => {
|
|
150
|
+
const relationships = this.relationships();
|
|
151
|
+
const positions = this.nodePositions();
|
|
152
|
+
const dimensions = this.nodeDimensions();
|
|
153
|
+
if (relationships.length > 0 && positions.size > 0) {
|
|
154
|
+
this.calculatePaths();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Event handlers
|
|
159
|
+
onViewportChange(viewport) {
|
|
160
|
+
this.viewport.set(viewport);
|
|
161
|
+
this.viewportChange.emit(viewport);
|
|
162
|
+
}
|
|
163
|
+
onElementsRendered(elements) {
|
|
164
|
+
const dims = new Map(this.nodeDimensions());
|
|
165
|
+
let changed = false;
|
|
166
|
+
elements.forEach(el => {
|
|
167
|
+
const existing = dims.get(el.id);
|
|
168
|
+
if (!existing || existing.width !== el.width || existing.height !== el.height) {
|
|
169
|
+
dims.set(el.id, { width: el.width, height: el.height });
|
|
170
|
+
changed = true;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (changed) {
|
|
174
|
+
this.nodeDimensions.set(dims);
|
|
175
|
+
this.calculatePaths();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
onElementPositionChange(change) {
|
|
179
|
+
// Update position
|
|
180
|
+
const positions = new Map(this.nodePositions());
|
|
181
|
+
positions.set(change.id, { x: change.x, y: change.y });
|
|
182
|
+
this.nodePositions.set(positions);
|
|
183
|
+
// Emit event
|
|
184
|
+
this.nodePositionChange.emit({
|
|
185
|
+
nodeId: change.id,
|
|
186
|
+
x: change.x,
|
|
187
|
+
y: change.y,
|
|
188
|
+
previousX: change.previousX,
|
|
189
|
+
previousY: change.previousY,
|
|
190
|
+
});
|
|
191
|
+
// Recalculate paths
|
|
192
|
+
this.calculatePaths();
|
|
193
|
+
}
|
|
194
|
+
// Public method to force re-layout
|
|
195
|
+
recalculateLayout() {
|
|
196
|
+
this.runLayout();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Fit all graph content to view
|
|
200
|
+
* Centers content in the viewport, optionally zooming out to fit everything
|
|
201
|
+
* @param fitZoom If true, zooms out if needed to fit all content (never zooms in beyond 1.0)
|
|
202
|
+
* @param padding Padding around content in pixels (default: 40)
|
|
203
|
+
*/
|
|
204
|
+
fitToView(fitZoom = false, padding = 40) {
|
|
205
|
+
this.gridRef()?.fitToView(fitZoom, padding);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if a node is fully visible in the viewport
|
|
209
|
+
* @param nodeId The node ID to check
|
|
210
|
+
* @param padding Padding from viewport edges (default: 0)
|
|
211
|
+
* @returns true if node is fully visible, false otherwise
|
|
212
|
+
*/
|
|
213
|
+
isNodeVisible(nodeId, padding = 0) {
|
|
214
|
+
return this.gridRef()?.isElementVisible(nodeId, padding) ?? false;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Center viewport on a specific node (no zoom change)
|
|
218
|
+
* @param nodeId The node ID to center on
|
|
219
|
+
*/
|
|
220
|
+
centerOnNode(nodeId) {
|
|
221
|
+
this.gridRef()?.centerOnElement(nodeId);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Scroll node into view with minimal pan
|
|
225
|
+
* Only pans if node is not fully visible
|
|
226
|
+
* @param nodeId The node ID to scroll into view
|
|
227
|
+
* @param padding Padding from viewport edges (default: 40)
|
|
228
|
+
*/
|
|
229
|
+
scrollToNode(nodeId, padding = 40) {
|
|
230
|
+
this.gridRef()?.scrollToElement(nodeId, padding);
|
|
231
|
+
}
|
|
232
|
+
// Private methods
|
|
233
|
+
async runLayout() {
|
|
234
|
+
const inputNodes = this.nodes();
|
|
235
|
+
const relationships = this.relationships();
|
|
236
|
+
const layoutCfg = this.layoutConfig();
|
|
237
|
+
const dimensions = this.nodeDimensions();
|
|
238
|
+
if (inputNodes.length === 0)
|
|
239
|
+
return;
|
|
240
|
+
const layoutNodes = inputNodes.map(node => {
|
|
241
|
+
const dim = dimensions.get(node.id);
|
|
242
|
+
return {
|
|
243
|
+
id: node.id,
|
|
244
|
+
width: dim?.width ?? 150,
|
|
245
|
+
height: dim?.height ?? 80,
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
const layoutEdges = relationships.map(rel => ({
|
|
249
|
+
id: rel.id,
|
|
250
|
+
sourceId: rel.sourceId,
|
|
251
|
+
targetId: rel.targetId,
|
|
252
|
+
}));
|
|
253
|
+
try {
|
|
254
|
+
const result = await this.layoutService.calculateLayout({
|
|
255
|
+
nodes: layoutNodes,
|
|
256
|
+
edges: layoutEdges,
|
|
257
|
+
options: {
|
|
258
|
+
algorithm: layoutCfg.algorithm,
|
|
259
|
+
direction: layoutCfg.direction,
|
|
260
|
+
nodeSpacing: layoutCfg.nodeSpacing,
|
|
261
|
+
layerSpacing: layoutCfg.layerSpacing,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
// Update all positions from layout result
|
|
265
|
+
const positions = new Map();
|
|
266
|
+
result.nodes.forEach(n => {
|
|
267
|
+
positions.set(n.id, { x: n.x, y: n.y });
|
|
268
|
+
});
|
|
269
|
+
this.nodePositions.set(positions);
|
|
270
|
+
// Recalculate paths
|
|
271
|
+
this.calculatePaths();
|
|
272
|
+
// Fit to view and then show nodes (use requestAnimationFrame to ensure DOM is updated)
|
|
273
|
+
requestAnimationFrame(() => {
|
|
274
|
+
this.fitToView(true, layoutCfg.fitPadding ?? 40);
|
|
275
|
+
// Mark layout as ready after fit (shows nodes and paths)
|
|
276
|
+
this.layoutReady.set(true);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error('Layout calculation failed:', error);
|
|
281
|
+
// Still mark as ready on error so user can see something
|
|
282
|
+
this.layoutReady.set(true);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
calculatePaths() {
|
|
286
|
+
const inputNodes = this.nodes();
|
|
287
|
+
const relationships = this.relationships();
|
|
288
|
+
const positions = this.nodePositions();
|
|
289
|
+
const dimensions = this.nodeDimensions();
|
|
290
|
+
const pathCfg = this.pathConfig();
|
|
291
|
+
if (inputNodes.length === 0 || relationships.length === 0) {
|
|
292
|
+
this.calculatedPaths.set([]);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const pfNodes = inputNodes.map(node => {
|
|
296
|
+
const pos = positions.get(node.id);
|
|
297
|
+
const dim = dimensions.get(node.id);
|
|
298
|
+
return {
|
|
299
|
+
id: node.id,
|
|
300
|
+
x: pos?.x ?? 0,
|
|
301
|
+
y: pos?.y ?? 0,
|
|
302
|
+
width: dim?.width ?? 150,
|
|
303
|
+
height: dim?.height ?? 80,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
const pfRelationships = relationships.map(rel => ({
|
|
307
|
+
id: rel.id,
|
|
308
|
+
sourceId: rel.sourceId,
|
|
309
|
+
targetId: rel.targetId,
|
|
310
|
+
sourceAnchor: rel.sourceAnchorSide,
|
|
311
|
+
targetAnchor: rel.targetAnchorSide,
|
|
312
|
+
}));
|
|
313
|
+
const input = {
|
|
314
|
+
nodes: pfNodes,
|
|
315
|
+
relationships: pfRelationships,
|
|
316
|
+
options: {
|
|
317
|
+
pathType: pathCfg.pathType,
|
|
318
|
+
obstaclePadding: pathCfg.obstaclePadding,
|
|
319
|
+
anchorSpacing: pathCfg.anchorSpacing,
|
|
320
|
+
spreadAnchors: pathCfg.spreadAnchors,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
const result = this.pathFindingService.calculatePaths(input);
|
|
324
|
+
this.calculatedPaths.set(result.paths);
|
|
325
|
+
}
|
|
326
|
+
// Path rendering helpers
|
|
327
|
+
getArrowPoints() {
|
|
328
|
+
const size = this.pathConfig().arrowSize;
|
|
329
|
+
return `0,0 ${size},${size / 2} 0,${size}`;
|
|
330
|
+
}
|
|
331
|
+
getStrokeDasharray() {
|
|
332
|
+
const pattern = this.pathConfig().strokePattern;
|
|
333
|
+
switch (pattern) {
|
|
334
|
+
case 'dashed': return '8,4';
|
|
335
|
+
case 'dotted': return '2,4';
|
|
336
|
+
default: return '';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
getPathD(path) {
|
|
340
|
+
const waypoints = path.waypoints;
|
|
341
|
+
if (!waypoints || waypoints.length === 0)
|
|
342
|
+
return '';
|
|
343
|
+
const cfg = this.pathConfig();
|
|
344
|
+
if (cfg.pathType === 'bezier') {
|
|
345
|
+
return this.generateBezierPath(waypoints);
|
|
346
|
+
}
|
|
347
|
+
else if (cfg.pathType === 'straight') {
|
|
348
|
+
return this.generateStraightPath(waypoints);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
return this.generateOrthogonalPath(waypoints, cfg.cornerRadius);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
generateOrthogonalPath(waypoints, cornerRadius) {
|
|
355
|
+
const points = waypoints.filter(w => !w.isControlPoint);
|
|
356
|
+
if (points.length === 0)
|
|
357
|
+
return '';
|
|
358
|
+
if (cornerRadius === 0 || points.length < 3) {
|
|
359
|
+
return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
|
|
360
|
+
}
|
|
361
|
+
let path = `M ${points[0].x} ${points[0].y}`;
|
|
362
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
363
|
+
const prev = points[i - 1];
|
|
364
|
+
const curr = points[i];
|
|
365
|
+
const next = points[i + 1];
|
|
366
|
+
const dx1 = curr.x - prev.x;
|
|
367
|
+
const dy1 = curr.y - prev.y;
|
|
368
|
+
const dx2 = next.x - curr.x;
|
|
369
|
+
const dy2 = next.y - curr.y;
|
|
370
|
+
const dist1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
|
|
371
|
+
const dist2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
|
372
|
+
if (dist1 === 0 || dist2 === 0) {
|
|
373
|
+
path += ` L ${curr.x} ${curr.y}`;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const r = Math.min(cornerRadius, dist1 / 2, dist2 / 2);
|
|
377
|
+
const startX = curr.x - (dx1 / dist1) * r;
|
|
378
|
+
const startY = curr.y - (dy1 / dist1) * r;
|
|
379
|
+
const endX = curr.x + (dx2 / dist2) * r;
|
|
380
|
+
const endY = curr.y + (dy2 / dist2) * r;
|
|
381
|
+
path += ` L ${startX} ${startY}`;
|
|
382
|
+
path += ` Q ${curr.x} ${curr.y}, ${endX} ${endY}`;
|
|
383
|
+
}
|
|
384
|
+
const last = points[points.length - 1];
|
|
385
|
+
path += ` L ${last.x} ${last.y}`;
|
|
386
|
+
return path;
|
|
387
|
+
}
|
|
388
|
+
generateBezierPath(waypoints) {
|
|
389
|
+
if (waypoints.length < 2)
|
|
390
|
+
return '';
|
|
391
|
+
if (waypoints.length === 4) {
|
|
392
|
+
const [start, cp1, cp2, end] = waypoints;
|
|
393
|
+
return `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`;
|
|
394
|
+
}
|
|
395
|
+
if (waypoints.length === 3) {
|
|
396
|
+
const [start, cp, end] = waypoints;
|
|
397
|
+
return `M ${start.x} ${start.y} Q ${cp.x} ${cp.y}, ${end.x} ${end.y}`;
|
|
398
|
+
}
|
|
399
|
+
return this.generateStraightPath(waypoints);
|
|
400
|
+
}
|
|
401
|
+
generateStraightPath(waypoints) {
|
|
402
|
+
if (waypoints.length < 2)
|
|
403
|
+
return '';
|
|
404
|
+
const start = waypoints[0];
|
|
405
|
+
const end = waypoints[waypoints.length - 1];
|
|
406
|
+
return `M ${start.x} ${start.y} L ${end.x} ${end.y}`;
|
|
407
|
+
}
|
|
408
|
+
// Pill rendering helpers
|
|
409
|
+
/** Create an injector for a pill component, passing the relationship data */
|
|
410
|
+
createPillInjector(relationshipId) {
|
|
411
|
+
const relationship = this.relationships().find(r => r.id === relationshipId);
|
|
412
|
+
return Injector.create({
|
|
413
|
+
providers: [
|
|
414
|
+
{ provide: GRAPH_RELATIONSHIP_DATA, useValue: relationship?.data },
|
|
415
|
+
],
|
|
416
|
+
parent: this.injector,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
420
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: GraphComponent, isStandalone: true, selector: "ngx-graph", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, relationships: { classPropertyName: "relationships", publicName: "relationships", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodePositionChange: "nodePositionChange", viewportChange: "viewportChange" }, viewQueries: [{ propertyName: "gridRef", first: true, predicate: ["gridRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
421
|
+
<div class="graph-wrapper" [class.layout-ready]="layoutReady()">
|
|
422
|
+
<ngx-grid
|
|
423
|
+
#gridRef
|
|
424
|
+
[config]="gridConfig()"
|
|
425
|
+
[elements]="gridElements()"
|
|
426
|
+
(viewportChange)="onViewportChange($event)"
|
|
427
|
+
(elementsRendered)="onElementsRendered($event)"
|
|
428
|
+
(elementPositionChange)="onElementPositionChange($event)"
|
|
429
|
+
/>
|
|
430
|
+
<!-- SVG layer for paths -->
|
|
431
|
+
<svg
|
|
432
|
+
class="paths-layer"
|
|
433
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
434
|
+
[style.transform]="pathsTransform()"
|
|
435
|
+
>
|
|
436
|
+
<defs>
|
|
437
|
+
@for (path of calculatedPaths(); track path.relationshipId) {
|
|
438
|
+
<marker
|
|
439
|
+
[attr.id]="'arrow-' + path.relationshipId"
|
|
440
|
+
[attr.markerWidth]="pathConfig().arrowSize"
|
|
441
|
+
[attr.markerHeight]="pathConfig().arrowSize"
|
|
442
|
+
[attr.refX]="pathConfig().arrowSize"
|
|
443
|
+
[attr.refY]="pathConfig().arrowSize / 2"
|
|
444
|
+
orient="auto-start-reverse"
|
|
445
|
+
markerUnits="userSpaceOnUse"
|
|
446
|
+
>
|
|
447
|
+
<polygon
|
|
448
|
+
[attr.points]="getArrowPoints()"
|
|
449
|
+
[attr.fill]="pathConfig().strokeColor"
|
|
450
|
+
/>
|
|
451
|
+
</marker>
|
|
452
|
+
}
|
|
453
|
+
</defs>
|
|
454
|
+
|
|
455
|
+
@for (path of calculatedPaths(); track path.relationshipId) {
|
|
456
|
+
<path
|
|
457
|
+
[attr.d]="getPathD(path)"
|
|
458
|
+
fill="none"
|
|
459
|
+
[attr.stroke]="pathConfig().strokeColor"
|
|
460
|
+
[attr.stroke-width]="pathConfig().strokeWidth"
|
|
461
|
+
[attr.stroke-dasharray]="getStrokeDasharray()"
|
|
462
|
+
[attr.marker-end]="'url(#arrow-' + path.relationshipId + ')'"
|
|
463
|
+
stroke-linecap="round"
|
|
464
|
+
stroke-linejoin="round"
|
|
465
|
+
/>
|
|
466
|
+
}
|
|
467
|
+
</svg>
|
|
468
|
+
<!-- Pills layer - rendered at path midpoints -->
|
|
469
|
+
@if (pillComponent()) {
|
|
470
|
+
<div class="pills-layer" [style.transform]="pathsTransform()">
|
|
471
|
+
@for (path of calculatedPaths(); track path.relationshipId) {
|
|
472
|
+
<div
|
|
473
|
+
class="pill-wrapper"
|
|
474
|
+
[style.left.px]="path.midpoint.x"
|
|
475
|
+
[style.top.px]="path.midpoint.y"
|
|
476
|
+
>
|
|
477
|
+
<ng-container
|
|
478
|
+
*ngComponentOutlet="pillComponent()!; injector: createPillInjector(path.relationshipId)"
|
|
479
|
+
/>
|
|
480
|
+
</div>
|
|
481
|
+
}
|
|
482
|
+
</div>
|
|
483
|
+
}
|
|
484
|
+
</div>
|
|
485
|
+
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}.graph-wrapper{position:relative;width:100%;height:100%}.graph-wrapper:not(.layout-ready) ::ng-deep .grid-viewport,.graph-wrapper:not(.layout-ready) .paths-layer,.graph-wrapper:not(.layout-ready) .pills-layer{opacity:0}.paths-layer,.pills-layer{position:absolute;top:0;left:0;width:100%;height:100%;overflow:visible;pointer-events:none;transform-origin:0 0}.pill-wrapper{position:absolute;transform:translate(-50%,-50%);pointer-events:auto}\n"], dependencies: [{ kind: "component", type: GridComponent, selector: "ngx-grid", inputs: ["config", "elements"], outputs: ["viewportChange", "elementsRendered", "elementPositionChange"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }] });
|
|
486
|
+
}
|
|
487
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GraphComponent, decorators: [{
|
|
488
|
+
type: Component,
|
|
489
|
+
args: [{ selector: 'ngx-graph', standalone: true, imports: [GridComponent, NgComponentOutlet], template: `
|
|
490
|
+
<div class="graph-wrapper" [class.layout-ready]="layoutReady()">
|
|
491
|
+
<ngx-grid
|
|
492
|
+
#gridRef
|
|
493
|
+
[config]="gridConfig()"
|
|
494
|
+
[elements]="gridElements()"
|
|
495
|
+
(viewportChange)="onViewportChange($event)"
|
|
496
|
+
(elementsRendered)="onElementsRendered($event)"
|
|
497
|
+
(elementPositionChange)="onElementPositionChange($event)"
|
|
498
|
+
/>
|
|
499
|
+
<!-- SVG layer for paths -->
|
|
500
|
+
<svg
|
|
501
|
+
class="paths-layer"
|
|
502
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
503
|
+
[style.transform]="pathsTransform()"
|
|
504
|
+
>
|
|
505
|
+
<defs>
|
|
506
|
+
@for (path of calculatedPaths(); track path.relationshipId) {
|
|
507
|
+
<marker
|
|
508
|
+
[attr.id]="'arrow-' + path.relationshipId"
|
|
509
|
+
[attr.markerWidth]="pathConfig().arrowSize"
|
|
510
|
+
[attr.markerHeight]="pathConfig().arrowSize"
|
|
511
|
+
[attr.refX]="pathConfig().arrowSize"
|
|
512
|
+
[attr.refY]="pathConfig().arrowSize / 2"
|
|
513
|
+
orient="auto-start-reverse"
|
|
514
|
+
markerUnits="userSpaceOnUse"
|
|
515
|
+
>
|
|
516
|
+
<polygon
|
|
517
|
+
[attr.points]="getArrowPoints()"
|
|
518
|
+
[attr.fill]="pathConfig().strokeColor"
|
|
519
|
+
/>
|
|
520
|
+
</marker>
|
|
521
|
+
}
|
|
522
|
+
</defs>
|
|
523
|
+
|
|
524
|
+
@for (path of calculatedPaths(); track path.relationshipId) {
|
|
525
|
+
<path
|
|
526
|
+
[attr.d]="getPathD(path)"
|
|
527
|
+
fill="none"
|
|
528
|
+
[attr.stroke]="pathConfig().strokeColor"
|
|
529
|
+
[attr.stroke-width]="pathConfig().strokeWidth"
|
|
530
|
+
[attr.stroke-dasharray]="getStrokeDasharray()"
|
|
531
|
+
[attr.marker-end]="'url(#arrow-' + path.relationshipId + ')'"
|
|
532
|
+
stroke-linecap="round"
|
|
533
|
+
stroke-linejoin="round"
|
|
534
|
+
/>
|
|
535
|
+
}
|
|
536
|
+
</svg>
|
|
537
|
+
<!-- Pills layer - rendered at path midpoints -->
|
|
538
|
+
@if (pillComponent()) {
|
|
539
|
+
<div class="pills-layer" [style.transform]="pathsTransform()">
|
|
540
|
+
@for (path of calculatedPaths(); track path.relationshipId) {
|
|
541
|
+
<div
|
|
542
|
+
class="pill-wrapper"
|
|
543
|
+
[style.left.px]="path.midpoint.x"
|
|
544
|
+
[style.top.px]="path.midpoint.y"
|
|
545
|
+
>
|
|
546
|
+
<ng-container
|
|
547
|
+
*ngComponentOutlet="pillComponent()!; injector: createPillInjector(path.relationshipId)"
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
}
|
|
551
|
+
</div>
|
|
552
|
+
}
|
|
553
|
+
</div>
|
|
554
|
+
`, styles: [":host{display:block;width:100%;height:100%}.graph-wrapper{position:relative;width:100%;height:100%}.graph-wrapper:not(.layout-ready) ::ng-deep .grid-viewport,.graph-wrapper:not(.layout-ready) .paths-layer,.graph-wrapper:not(.layout-ready) .pills-layer{opacity:0}.paths-layer,.pills-layer{position:absolute;top:0;left:0;width:100%;height:100%;overflow:visible;pointer-events:none;transform-origin:0 0}.pill-wrapper{position:absolute;transform:translate(-50%,-50%);pointer-events:auto}\n"] }]
|
|
555
|
+
}], ctorParameters: () => [], propDecorators: { gridRef: [{ type: i0.ViewChild, args: ['gridRef', { isSignal: true }] }], nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], relationships: [{ type: i0.Input, args: [{ isSignal: true, alias: "relationships", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], nodePositionChange: [{ type: i0.Output, args: ["nodePositionChange"] }], viewportChange: [{ type: i0.Output, args: ["viewportChange"] }] } });
|
|
556
|
+
|
|
557
|
+
// Models - Values
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Generated bundle index. Do not edit.
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
export { DEFAULT_GRAPH_CONFIG, DEFAULT_GRAPH_LAYOUT_CONFIG, DEFAULT_GRAPH_PATH_CONFIG, GRAPH_NODE_DATA, GRAPH_RELATIONSHIP_DATA, GraphComponent };
|
|
564
|
+
//# sourceMappingURL=ngx-km-graph.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngx-km-graph.mjs","sources":["../../../../libs/ngx-km-graph/src/lib/models/graph.models.ts","../../../../libs/ngx-km-graph/src/lib/components/graph.component.ts","../../../../libs/ngx-km-graph/src/index.ts","../../../../libs/ngx-km-graph/src/ngx-km-graph.ts"],"sourcesContent":["import { Type, InjectionToken, Signal } from '@angular/core';\nimport type { GridConfig } from '@ngx-km/grid';\nimport type { LayoutAlgorithm, LayoutDirection } from '@ngx-km/layout';\nimport type { PathType } from '@ngx-km/path-finding';\nimport type { StrokePattern, ArrowStyle, ArrowPosition } from '@ngx-km/path-drawing';\n\n/**\n * Injection token for accessing node data in dynamically rendered node components.\n * The data can be either a plain value or a Signal for reactive updates.\n */\nexport const GRAPH_NODE_DATA = new InjectionToken<unknown>('GRAPH_NODE_DATA');\n\n/**\n * Injection token for accessing relationship data in dynamically rendered pill components.\n * The data can be either a plain value or a Signal for reactive updates.\n */\nexport const GRAPH_RELATIONSHIP_DATA = new InjectionToken<unknown>('GRAPH_RELATIONSHIP_DATA');\n\n/**\n * Type alias for data that can be either a plain value or a Signal.\n * Use Signal<T> when you need the component to reactively update based on data changes.\n */\nexport type ReactiveData<T> = T | Signal<T>;\n\n// ============================================================================\n// Node Definitions\n// ============================================================================\n\n/**\n * Definition for a node in the graph\n * @template T Type of the data passed to the node component\n */\nexport interface GraphNode<T = unknown> {\n /** Unique identifier for this node */\n id: string;\n /** Angular component class to render for this node */\n component: Type<unknown>;\n /**\n * Data to pass to the node component via GRAPH_NODE_DATA injection token.\n * Can be a plain value or a Signal for reactive updates.\n * When using Signal<T>, the component can reactively respond to data changes.\n */\n data?: ReactiveData<T>;\n /** Optional initial X position (used when preservePositions is true) */\n x?: number;\n /** Optional initial Y position (used when preservePositions is true) */\n y?: number;\n}\n\n/**\n * Internal node representation with resolved position\n */\nexport interface GraphNodeInternal<T = unknown> extends GraphNode<T> {\n /** Resolved X position in world space */\n x: number;\n /** Resolved Y position in world space */\n y: number;\n /** Rendered width (tracked after render) */\n width?: number;\n /** Rendered height (tracked after render) */\n height?: number;\n}\n\n// ============================================================================\n// Relationship Definitions\n// ============================================================================\n\n/**\n * Type of relationship (affects path drawing)\n * - 'one-way': Arrow on target end only\n * - 'two-way': Arrows on both ends\n */\nexport type RelationshipType = 'one-way' | 'two-way';\n\n/**\n * Definition for a relationship (edge) between nodes\n * @template T Type of the data passed to the pill component\n */\nexport interface GraphRelationship<T = unknown> {\n /** Unique identifier for this relationship */\n id: string;\n /** ID of the source node */\n sourceId: string;\n /** ID of the target node */\n targetId: string;\n /** Type of relationship (default: 'one-way') */\n type?: RelationshipType;\n /**\n * Data to pass to the pill component via GRAPH_RELATIONSHIP_DATA injection token.\n * Can be a plain value or a Signal for reactive updates.\n * When using Signal<T>, the component can reactively respond to data changes.\n */\n data?: ReactiveData<T>;\n /** Optional: Force specific source anchor side */\n sourceAnchorSide?: 'top' | 'right' | 'bottom' | 'left';\n /** Optional: Force specific target anchor side */\n targetAnchorSide?: 'top' | 'right' | 'bottom' | 'left';\n}\n\n// ============================================================================\n// Path Configuration\n// ============================================================================\n\n/**\n * Configuration for path rendering\n */\nexport interface GraphPathConfig {\n /** Path routing type (default: 'orthogonal') */\n pathType?: PathType;\n /** Stroke color for paths (default: '#6366f1') */\n strokeColor?: string;\n /** Stroke width in pixels (default: 2) */\n strokeWidth?: number;\n /** Stroke pattern (default: 'solid') */\n strokePattern?: StrokePattern;\n /** Stroke opacity 0-1 (default: 1) */\n strokeOpacity?: number;\n /** Corner radius for orthogonal paths (default: 0) */\n cornerRadius?: number;\n /** Arrow head style (default: 'filled') */\n arrowStyle?: ArrowStyle;\n /** Arrow head size in pixels (default: 10) */\n arrowSize?: number;\n /** Padding around nodes for obstacle avoidance (default: 20) */\n obstaclePadding?: number;\n /** Spacing between connection points on same side (default: 15) */\n anchorSpacing?: number;\n /** Whether to spread multiple anchors on same side (default: true) */\n spreadAnchors?: boolean;\n}\n\n/**\n * Default path configuration\n */\nexport const DEFAULT_GRAPH_PATH_CONFIG: Required<GraphPathConfig> = {\n pathType: 'orthogonal',\n strokeColor: '#6366f1',\n strokeWidth: 2,\n strokePattern: 'solid',\n strokeOpacity: 1,\n cornerRadius: 0,\n arrowStyle: 'filled',\n arrowSize: 10,\n obstaclePadding: 20,\n anchorSpacing: 15,\n spreadAnchors: true,\n};\n\n// ============================================================================\n// Layout Configuration\n// ============================================================================\n\n/**\n * Configuration for automatic layout\n */\nexport interface GraphLayoutConfig {\n /** Layout algorithm to use (default: 'layered') */\n algorithm?: LayoutAlgorithm;\n /** Layout direction (default: 'DOWN') */\n direction?: LayoutDirection;\n /** Spacing between nodes in pixels (default: 50) */\n nodeSpacing?: number;\n /** Spacing between layers in pixels (default: 100) */\n layerSpacing?: number;\n /** Whether to run layout automatically on init (default: true) */\n autoLayout?: boolean;\n /** Whether to preserve manually moved node positions on data update (default: true) */\n preservePositions?: boolean;\n /** Padding around content when fitting to view after layout (default: 40) */\n fitPadding?: number;\n}\n\n/**\n * Default layout configuration\n */\nexport const DEFAULT_GRAPH_LAYOUT_CONFIG: Required<GraphLayoutConfig> = {\n algorithm: 'layered',\n direction: 'DOWN',\n nodeSpacing: 50,\n layerSpacing: 100,\n autoLayout: true,\n preservePositions: true,\n fitPadding: 40,\n};\n\n// ============================================================================\n// Main Graph Configuration\n// ============================================================================\n\n/**\n * Main configuration for the graph component\n * Combines grid, layout, and path configurations\n */\nexport interface GraphConfig {\n /** Grid configuration (pan, zoom, background, etc.) */\n grid?: Partial<GridConfig>;\n /** Layout configuration (algorithm, spacing, etc.) */\n layout?: GraphLayoutConfig;\n /** Path rendering configuration (colors, arrows, etc.) */\n paths?: GraphPathConfig;\n /** Component to render at path midpoints (optional) */\n pillComponent?: Type<unknown>;\n}\n\n/**\n * Default graph configuration\n */\nexport const DEFAULT_GRAPH_CONFIG: GraphConfig = {\n grid: {\n dimensionMode: 'full',\n backgroundMode: 'dots',\n cellSize: 20,\n panEnabled: true,\n zoomEnabled: true,\n dragEnabled: true,\n },\n layout: DEFAULT_GRAPH_LAYOUT_CONFIG,\n paths: DEFAULT_GRAPH_PATH_CONFIG,\n};\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * Event emitted when a node is selected\n */\nexport interface NodeSelectEvent<T = unknown> {\n /** The selected node */\n node: GraphNode<T>;\n}\n\n/**\n * Event emitted when a node position changes\n */\nexport interface NodePositionChangeEvent {\n /** Node ID */\n nodeId: string;\n /** New X position */\n x: number;\n /** New Y position */\n y: number;\n /** Previous X position */\n previousX: number;\n /** Previous Y position */\n previousY: number;\n}\n\n/**\n * Event emitted when a relationship is selected\n */\nexport interface RelationshipSelectEvent<T = unknown> {\n /** The selected relationship */\n relationship: GraphRelationship<T>;\n}\n","import {\n Component,\n input,\n output,\n computed,\n signal,\n effect,\n inject,\n Injector,\n viewChild,\n} from '@angular/core';\nimport { NgComponentOutlet } from '@angular/common';\nimport {\n GridComponent,\n GridConfig,\n GridElement,\n RenderedElement,\n ElementPositionChange,\n ViewportState,\n} from '@ngx-km/grid';\nimport { LayoutService, LayoutNode, LayoutEdge } from '@ngx-km/layout';\nimport {\n PathFindingService,\n PathNode,\n PathRelationship,\n CalculatedPath,\n PathFindingInput,\n} from '@ngx-km/path-finding';\nimport {\n GraphConfig,\n GraphNode,\n GraphRelationship,\n NodePositionChangeEvent,\n DEFAULT_GRAPH_CONFIG,\n DEFAULT_GRAPH_LAYOUT_CONFIG,\n DEFAULT_GRAPH_PATH_CONFIG,\n GRAPH_RELATIONSHIP_DATA,\n} from '../models/graph.models';\n\n/** Internal node with position */\ninterface InternalNode<T> {\n id: string;\n component: GraphNode<T>['component'];\n data: T | undefined;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n@Component({\n selector: 'ngx-graph',\n standalone: true,\n imports: [GridComponent, NgComponentOutlet],\n template: `\n <div class=\"graph-wrapper\" [class.layout-ready]=\"layoutReady()\">\n <ngx-grid\n #gridRef\n [config]=\"gridConfig()\"\n [elements]=\"gridElements()\"\n (viewportChange)=\"onViewportChange($event)\"\n (elementsRendered)=\"onElementsRendered($event)\"\n (elementPositionChange)=\"onElementPositionChange($event)\"\n />\n <!-- SVG layer for paths -->\n <svg\n class=\"paths-layer\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [style.transform]=\"pathsTransform()\"\n >\n <defs>\n @for (path of calculatedPaths(); track path.relationshipId) {\n <marker\n [attr.id]=\"'arrow-' + path.relationshipId\"\n [attr.markerWidth]=\"pathConfig().arrowSize\"\n [attr.markerHeight]=\"pathConfig().arrowSize\"\n [attr.refX]=\"pathConfig().arrowSize\"\n [attr.refY]=\"pathConfig().arrowSize / 2\"\n orient=\"auto-start-reverse\"\n markerUnits=\"userSpaceOnUse\"\n >\n <polygon\n [attr.points]=\"getArrowPoints()\"\n [attr.fill]=\"pathConfig().strokeColor\"\n />\n </marker>\n }\n </defs>\n\n @for (path of calculatedPaths(); track path.relationshipId) {\n <path\n [attr.d]=\"getPathD(path)\"\n fill=\"none\"\n [attr.stroke]=\"pathConfig().strokeColor\"\n [attr.stroke-width]=\"pathConfig().strokeWidth\"\n [attr.stroke-dasharray]=\"getStrokeDasharray()\"\n [attr.marker-end]=\"'url(#arrow-' + path.relationshipId + ')'\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n }\n </svg>\n <!-- Pills layer - rendered at path midpoints -->\n @if (pillComponent()) {\n <div class=\"pills-layer\" [style.transform]=\"pathsTransform()\">\n @for (path of calculatedPaths(); track path.relationshipId) {\n <div\n class=\"pill-wrapper\"\n [style.left.px]=\"path.midpoint.x\"\n [style.top.px]=\"path.midpoint.y\"\n >\n <ng-container\n *ngComponentOutlet=\"pillComponent()!; injector: createPillInjector(path.relationshipId)\"\n />\n </div>\n }\n </div>\n }\n </div>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n .graph-wrapper {\n position: relative;\n width: 100%;\n height: 100%;\n }\n /* Hide nodes and paths until layout is ready to prevent flash */\n .graph-wrapper:not(.layout-ready) ::ng-deep .grid-viewport,\n .graph-wrapper:not(.layout-ready) .paths-layer,\n .graph-wrapper:not(.layout-ready) .pills-layer {\n opacity: 0;\n }\n .paths-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n overflow: visible;\n pointer-events: none;\n transform-origin: 0 0;\n }\n .pills-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n overflow: visible;\n pointer-events: none;\n transform-origin: 0 0;\n }\n .pill-wrapper {\n position: absolute;\n transform: translate(-50%, -50%);\n pointer-events: auto;\n }\n `],\n})\nexport class GraphComponent<TNode = unknown, TRelationship = unknown> {\n private readonly layoutService = inject(LayoutService);\n private readonly pathFindingService = inject(PathFindingService);\n private readonly injector = inject(Injector);\n private readonly gridRef = viewChild<GridComponent>('gridRef');\n\n // Inputs\n nodes = input<GraphNode<TNode>[]>([]);\n relationships = input<GraphRelationship<TRelationship>[]>([]);\n config = input<GraphConfig>(DEFAULT_GRAPH_CONFIG);\n\n // Outputs\n nodePositionChange = output<NodePositionChangeEvent>();\n viewportChange = output<ViewportState>();\n\n // Internal state - single source of truth for positions\n private nodePositions = signal<Map<string, { x: number; y: number }>>(new Map());\n private nodeDimensions = signal<Map<string, { width: number; height: number }>>(new Map());\n private viewport = signal<ViewportState>({ x: 0, y: 0, zoom: 1 });\n calculatedPaths = signal<CalculatedPath[]>([]);\n\n /** Whether initial layout has completed - used to prevent flash of unstyled nodes */\n layoutReady = signal<boolean>(false);\n\n // Computed configs\n gridConfig = computed((): GridConfig => ({\n dimensionMode: 'full',\n backgroundMode: 'dots',\n cellSize: 20,\n panEnabled: true,\n zoomEnabled: true,\n dragEnabled: true,\n ...this.config().grid,\n }));\n\n layoutConfig = computed(() => ({\n ...DEFAULT_GRAPH_LAYOUT_CONFIG,\n ...this.config().layout,\n }));\n\n pathConfig = computed(() => ({\n ...DEFAULT_GRAPH_PATH_CONFIG,\n ...this.config().paths,\n }));\n\n /** Pill component to render at path midpoints */\n pillComponent = computed(() => this.config().pillComponent);\n\n // Grid elements - combines input nodes with their positions\n gridElements = computed((): GridElement<TNode>[] => {\n const inputNodes = this.nodes();\n const positions = this.nodePositions();\n\n return inputNodes.map((node, index) => {\n const pos = positions.get(node.id);\n // Default grid position if not set\n const cols = Math.ceil(Math.sqrt(inputNodes.length));\n const defaultX = (index % cols) * 200;\n const defaultY = Math.floor(index / cols) * 150;\n\n return {\n id: node.id,\n component: node.component,\n data: node.data,\n x: pos?.x ?? node.x ?? defaultX,\n y: pos?.y ?? node.y ?? defaultY,\n };\n });\n });\n\n pathsTransform = computed(() => {\n const vp = this.viewport();\n return `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`;\n });\n\n constructor() {\n // Run initial layout when nodes change\n effect(() => {\n const inputNodes = this.nodes();\n const layoutCfg = this.layoutConfig();\n\n if (inputNodes.length === 0) {\n // No nodes - mark as ready immediately\n this.layoutReady.set(true);\n return;\n }\n\n if (!layoutCfg.autoLayout) {\n // Auto-layout disabled - show nodes at their provided positions immediately\n this.layoutReady.set(true);\n return;\n }\n\n // Check if we have positions for all nodes\n const positions = this.nodePositions();\n const needsLayout = inputNodes.some(n => !positions.has(n.id));\n\n if (needsLayout) {\n // Reset layoutReady when new nodes need positioning\n this.layoutReady.set(false);\n this.runLayout();\n }\n });\n\n // Recalculate paths when relationships or positions change\n effect(() => {\n const relationships = this.relationships();\n const positions = this.nodePositions();\n const dimensions = this.nodeDimensions();\n\n if (relationships.length > 0 && positions.size > 0) {\n this.calculatePaths();\n }\n });\n }\n\n // Event handlers\n onViewportChange(viewport: ViewportState): void {\n this.viewport.set(viewport);\n this.viewportChange.emit(viewport);\n }\n\n onElementsRendered(elements: RenderedElement[]): void {\n const dims = new Map(this.nodeDimensions());\n let changed = false;\n\n elements.forEach(el => {\n const existing = dims.get(el.id);\n if (!existing || existing.width !== el.width || existing.height !== el.height) {\n dims.set(el.id, { width: el.width, height: el.height });\n changed = true;\n }\n });\n\n if (changed) {\n this.nodeDimensions.set(dims);\n this.calculatePaths();\n }\n }\n\n onElementPositionChange(change: ElementPositionChange): void {\n // Update position\n const positions = new Map(this.nodePositions());\n positions.set(change.id, { x: change.x, y: change.y });\n this.nodePositions.set(positions);\n\n // Emit event\n this.nodePositionChange.emit({\n nodeId: change.id,\n x: change.x,\n y: change.y,\n previousX: change.previousX,\n previousY: change.previousY,\n });\n\n // Recalculate paths\n this.calculatePaths();\n }\n\n // Public method to force re-layout\n recalculateLayout(): void {\n this.runLayout();\n }\n\n /**\n * Fit all graph content to view\n * Centers content in the viewport, optionally zooming out to fit everything\n * @param fitZoom If true, zooms out if needed to fit all content (never zooms in beyond 1.0)\n * @param padding Padding around content in pixels (default: 40)\n */\n fitToView(fitZoom = false, padding = 40): void {\n this.gridRef()?.fitToView(fitZoom, padding);\n }\n\n /**\n * Check if a node is fully visible in the viewport\n * @param nodeId The node ID to check\n * @param padding Padding from viewport edges (default: 0)\n * @returns true if node is fully visible, false otherwise\n */\n isNodeVisible(nodeId: string, padding = 0): boolean {\n return this.gridRef()?.isElementVisible(nodeId, padding) ?? false;\n }\n\n /**\n * Center viewport on a specific node (no zoom change)\n * @param nodeId The node ID to center on\n */\n centerOnNode(nodeId: string): void {\n this.gridRef()?.centerOnElement(nodeId);\n }\n\n /**\n * Scroll node into view with minimal pan\n * Only pans if node is not fully visible\n * @param nodeId The node ID to scroll into view\n * @param padding Padding from viewport edges (default: 40)\n */\n scrollToNode(nodeId: string, padding = 40): void {\n this.gridRef()?.scrollToElement(nodeId, padding);\n }\n\n // Private methods\n private async runLayout(): Promise<void> {\n const inputNodes = this.nodes();\n const relationships = this.relationships();\n const layoutCfg = this.layoutConfig();\n const dimensions = this.nodeDimensions();\n\n if (inputNodes.length === 0) return;\n\n const layoutNodes: LayoutNode[] = inputNodes.map(node => {\n const dim = dimensions.get(node.id);\n return {\n id: node.id,\n width: dim?.width ?? 150,\n height: dim?.height ?? 80,\n };\n });\n\n const layoutEdges: LayoutEdge[] = relationships.map(rel => ({\n id: rel.id,\n sourceId: rel.sourceId,\n targetId: rel.targetId,\n }));\n\n try {\n const result = await this.layoutService.calculateLayout({\n nodes: layoutNodes,\n edges: layoutEdges,\n options: {\n algorithm: layoutCfg.algorithm,\n direction: layoutCfg.direction,\n nodeSpacing: layoutCfg.nodeSpacing,\n layerSpacing: layoutCfg.layerSpacing,\n },\n });\n\n // Update all positions from layout result\n const positions = new Map<string, { x: number; y: number }>();\n result.nodes.forEach(n => {\n positions.set(n.id, { x: n.x, y: n.y });\n });\n this.nodePositions.set(positions);\n\n // Recalculate paths\n this.calculatePaths();\n\n // Fit to view and then show nodes (use requestAnimationFrame to ensure DOM is updated)\n requestAnimationFrame(() => {\n this.fitToView(true, layoutCfg.fitPadding ?? 40);\n // Mark layout as ready after fit (shows nodes and paths)\n this.layoutReady.set(true);\n });\n } catch (error) {\n console.error('Layout calculation failed:', error);\n // Still mark as ready on error so user can see something\n this.layoutReady.set(true);\n }\n }\n\n private calculatePaths(): void {\n const inputNodes = this.nodes();\n const relationships = this.relationships();\n const positions = this.nodePositions();\n const dimensions = this.nodeDimensions();\n const pathCfg = this.pathConfig();\n\n if (inputNodes.length === 0 || relationships.length === 0) {\n this.calculatedPaths.set([]);\n return;\n }\n\n const pfNodes: PathNode[] = inputNodes.map(node => {\n const pos = positions.get(node.id);\n const dim = dimensions.get(node.id);\n return {\n id: node.id,\n x: pos?.x ?? 0,\n y: pos?.y ?? 0,\n width: dim?.width ?? 150,\n height: dim?.height ?? 80,\n };\n });\n\n const pfRelationships: PathRelationship[] = relationships.map(rel => ({\n id: rel.id,\n sourceId: rel.sourceId,\n targetId: rel.targetId,\n sourceAnchor: rel.sourceAnchorSide,\n targetAnchor: rel.targetAnchorSide,\n }));\n\n const input: PathFindingInput = {\n nodes: pfNodes,\n relationships: pfRelationships,\n options: {\n pathType: pathCfg.pathType,\n obstaclePadding: pathCfg.obstaclePadding,\n anchorSpacing: pathCfg.anchorSpacing,\n spreadAnchors: pathCfg.spreadAnchors,\n },\n };\n\n const result = this.pathFindingService.calculatePaths(input);\n this.calculatedPaths.set(result.paths);\n }\n\n // Path rendering helpers\n getArrowPoints(): string {\n const size = this.pathConfig().arrowSize;\n return `0,0 ${size},${size / 2} 0,${size}`;\n }\n\n getStrokeDasharray(): string {\n const pattern = this.pathConfig().strokePattern;\n switch (pattern) {\n case 'dashed': return '8,4';\n case 'dotted': return '2,4';\n default: return '';\n }\n }\n\n getPathD(path: CalculatedPath): string {\n const waypoints = path.waypoints;\n if (!waypoints || waypoints.length === 0) return '';\n\n const cfg = this.pathConfig();\n\n if (cfg.pathType === 'bezier') {\n return this.generateBezierPath(waypoints);\n } else if (cfg.pathType === 'straight') {\n return this.generateStraightPath(waypoints);\n } else {\n return this.generateOrthogonalPath(waypoints, cfg.cornerRadius);\n }\n }\n\n private generateOrthogonalPath(\n waypoints: { x: number; y: number; isControlPoint?: boolean }[],\n cornerRadius: number\n ): string {\n const points = waypoints.filter(w => !w.isControlPoint);\n if (points.length === 0) return '';\n\n if (cornerRadius === 0 || points.length < 3) {\n return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');\n }\n\n let path = `M ${points[0].x} ${points[0].y}`;\n\n for (let i = 1; i < points.length - 1; i++) {\n const prev = points[i - 1];\n const curr = points[i];\n const next = points[i + 1];\n\n const dx1 = curr.x - prev.x;\n const dy1 = curr.y - prev.y;\n const dx2 = next.x - curr.x;\n const dy2 = next.y - curr.y;\n\n const dist1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);\n const dist2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);\n\n if (dist1 === 0 || dist2 === 0) {\n path += ` L ${curr.x} ${curr.y}`;\n continue;\n }\n\n const r = Math.min(cornerRadius, dist1 / 2, dist2 / 2);\n\n const startX = curr.x - (dx1 / dist1) * r;\n const startY = curr.y - (dy1 / dist1) * r;\n const endX = curr.x + (dx2 / dist2) * r;\n const endY = curr.y + (dy2 / dist2) * r;\n\n path += ` L ${startX} ${startY}`;\n path += ` Q ${curr.x} ${curr.y}, ${endX} ${endY}`;\n }\n\n const last = points[points.length - 1];\n path += ` L ${last.x} ${last.y}`;\n\n return path;\n }\n\n private generateBezierPath(waypoints: { x: number; y: number }[]): string {\n if (waypoints.length < 2) return '';\n if (waypoints.length === 4) {\n const [start, cp1, cp2, end] = waypoints;\n return `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`;\n }\n if (waypoints.length === 3) {\n const [start, cp, end] = waypoints;\n return `M ${start.x} ${start.y} Q ${cp.x} ${cp.y}, ${end.x} ${end.y}`;\n }\n return this.generateStraightPath(waypoints);\n }\n\n private generateStraightPath(waypoints: { x: number; y: number }[]): string {\n if (waypoints.length < 2) return '';\n const start = waypoints[0];\n const end = waypoints[waypoints.length - 1];\n return `M ${start.x} ${start.y} L ${end.x} ${end.y}`;\n }\n\n // Pill rendering helpers\n\n /** Create an injector for a pill component, passing the relationship data */\n createPillInjector(relationshipId: string): Injector {\n const relationship = this.relationships().find(r => r.id === relationshipId);\n return Injector.create({\n providers: [\n { provide: GRAPH_RELATIONSHIP_DATA, useValue: relationship?.data },\n ],\n parent: this.injector,\n });\n }\n}\n","// Models - Types\nexport type {\n GraphNode,\n GraphNodeInternal,\n GraphRelationship,\n RelationshipType,\n ReactiveData,\n GraphPathConfig,\n GraphLayoutConfig,\n GraphConfig,\n NodeSelectEvent,\n NodePositionChangeEvent,\n RelationshipSelectEvent,\n} from './lib/models/graph.models';\n\n// Models - Values\nexport {\n GRAPH_NODE_DATA,\n GRAPH_RELATIONSHIP_DATA,\n DEFAULT_GRAPH_PATH_CONFIG,\n DEFAULT_GRAPH_LAYOUT_CONFIG,\n DEFAULT_GRAPH_CONFIG,\n} from './lib/models/graph.models';\n\n// Components\nexport { GraphComponent } from './lib/components/graph.component';\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,280 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { Type, Signal, InjectionToken, Injector } from '@angular/core';
|
|
3
|
+
import { GridConfig, ViewportState, GridElement, RenderedElement, ElementPositionChange } from '@ngx-km/grid';
|
|
4
|
+
import * as _ngx_km_layout from '@ngx-km/layout';
|
|
5
|
+
import { LayoutAlgorithm, LayoutDirection } from '@ngx-km/layout';
|
|
6
|
+
import * as _ngx_km_path_finding from '@ngx-km/path-finding';
|
|
7
|
+
import { PathType, CalculatedPath } from '@ngx-km/path-finding';
|
|
8
|
+
import * as _ngx_km_path_drawing from '@ngx-km/path-drawing';
|
|
9
|
+
import { StrokePattern, ArrowStyle } from '@ngx-km/path-drawing';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Injection token for accessing node data in dynamically rendered node components.
|
|
13
|
+
* The data can be either a plain value or a Signal for reactive updates.
|
|
14
|
+
*/
|
|
15
|
+
declare const GRAPH_NODE_DATA: InjectionToken<unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Injection token for accessing relationship data in dynamically rendered pill components.
|
|
18
|
+
* The data can be either a plain value or a Signal for reactive updates.
|
|
19
|
+
*/
|
|
20
|
+
declare const GRAPH_RELATIONSHIP_DATA: InjectionToken<unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Type alias for data that can be either a plain value or a Signal.
|
|
23
|
+
* Use Signal<T> when you need the component to reactively update based on data changes.
|
|
24
|
+
*/
|
|
25
|
+
type ReactiveData<T> = T | Signal<T>;
|
|
26
|
+
/**
|
|
27
|
+
* Definition for a node in the graph
|
|
28
|
+
* @template T Type of the data passed to the node component
|
|
29
|
+
*/
|
|
30
|
+
interface GraphNode<T = unknown> {
|
|
31
|
+
/** Unique identifier for this node */
|
|
32
|
+
id: string;
|
|
33
|
+
/** Angular component class to render for this node */
|
|
34
|
+
component: Type<unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* Data to pass to the node component via GRAPH_NODE_DATA injection token.
|
|
37
|
+
* Can be a plain value or a Signal for reactive updates.
|
|
38
|
+
* When using Signal<T>, the component can reactively respond to data changes.
|
|
39
|
+
*/
|
|
40
|
+
data?: ReactiveData<T>;
|
|
41
|
+
/** Optional initial X position (used when preservePositions is true) */
|
|
42
|
+
x?: number;
|
|
43
|
+
/** Optional initial Y position (used when preservePositions is true) */
|
|
44
|
+
y?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Internal node representation with resolved position
|
|
48
|
+
*/
|
|
49
|
+
interface GraphNodeInternal<T = unknown> extends GraphNode<T> {
|
|
50
|
+
/** Resolved X position in world space */
|
|
51
|
+
x: number;
|
|
52
|
+
/** Resolved Y position in world space */
|
|
53
|
+
y: number;
|
|
54
|
+
/** Rendered width (tracked after render) */
|
|
55
|
+
width?: number;
|
|
56
|
+
/** Rendered height (tracked after render) */
|
|
57
|
+
height?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Type of relationship (affects path drawing)
|
|
61
|
+
* - 'one-way': Arrow on target end only
|
|
62
|
+
* - 'two-way': Arrows on both ends
|
|
63
|
+
*/
|
|
64
|
+
type RelationshipType = 'one-way' | 'two-way';
|
|
65
|
+
/**
|
|
66
|
+
* Definition for a relationship (edge) between nodes
|
|
67
|
+
* @template T Type of the data passed to the pill component
|
|
68
|
+
*/
|
|
69
|
+
interface GraphRelationship<T = unknown> {
|
|
70
|
+
/** Unique identifier for this relationship */
|
|
71
|
+
id: string;
|
|
72
|
+
/** ID of the source node */
|
|
73
|
+
sourceId: string;
|
|
74
|
+
/** ID of the target node */
|
|
75
|
+
targetId: string;
|
|
76
|
+
/** Type of relationship (default: 'one-way') */
|
|
77
|
+
type?: RelationshipType;
|
|
78
|
+
/**
|
|
79
|
+
* Data to pass to the pill component via GRAPH_RELATIONSHIP_DATA injection token.
|
|
80
|
+
* Can be a plain value or a Signal for reactive updates.
|
|
81
|
+
* When using Signal<T>, the component can reactively respond to data changes.
|
|
82
|
+
*/
|
|
83
|
+
data?: ReactiveData<T>;
|
|
84
|
+
/** Optional: Force specific source anchor side */
|
|
85
|
+
sourceAnchorSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
86
|
+
/** Optional: Force specific target anchor side */
|
|
87
|
+
targetAnchorSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Configuration for path rendering
|
|
91
|
+
*/
|
|
92
|
+
interface GraphPathConfig {
|
|
93
|
+
/** Path routing type (default: 'orthogonal') */
|
|
94
|
+
pathType?: PathType;
|
|
95
|
+
/** Stroke color for paths (default: '#6366f1') */
|
|
96
|
+
strokeColor?: string;
|
|
97
|
+
/** Stroke width in pixels (default: 2) */
|
|
98
|
+
strokeWidth?: number;
|
|
99
|
+
/** Stroke pattern (default: 'solid') */
|
|
100
|
+
strokePattern?: StrokePattern;
|
|
101
|
+
/** Stroke opacity 0-1 (default: 1) */
|
|
102
|
+
strokeOpacity?: number;
|
|
103
|
+
/** Corner radius for orthogonal paths (default: 0) */
|
|
104
|
+
cornerRadius?: number;
|
|
105
|
+
/** Arrow head style (default: 'filled') */
|
|
106
|
+
arrowStyle?: ArrowStyle;
|
|
107
|
+
/** Arrow head size in pixels (default: 10) */
|
|
108
|
+
arrowSize?: number;
|
|
109
|
+
/** Padding around nodes for obstacle avoidance (default: 20) */
|
|
110
|
+
obstaclePadding?: number;
|
|
111
|
+
/** Spacing between connection points on same side (default: 15) */
|
|
112
|
+
anchorSpacing?: number;
|
|
113
|
+
/** Whether to spread multiple anchors on same side (default: true) */
|
|
114
|
+
spreadAnchors?: boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Default path configuration
|
|
118
|
+
*/
|
|
119
|
+
declare const DEFAULT_GRAPH_PATH_CONFIG: Required<GraphPathConfig>;
|
|
120
|
+
/**
|
|
121
|
+
* Configuration for automatic layout
|
|
122
|
+
*/
|
|
123
|
+
interface GraphLayoutConfig {
|
|
124
|
+
/** Layout algorithm to use (default: 'layered') */
|
|
125
|
+
algorithm?: LayoutAlgorithm;
|
|
126
|
+
/** Layout direction (default: 'DOWN') */
|
|
127
|
+
direction?: LayoutDirection;
|
|
128
|
+
/** Spacing between nodes in pixels (default: 50) */
|
|
129
|
+
nodeSpacing?: number;
|
|
130
|
+
/** Spacing between layers in pixels (default: 100) */
|
|
131
|
+
layerSpacing?: number;
|
|
132
|
+
/** Whether to run layout automatically on init (default: true) */
|
|
133
|
+
autoLayout?: boolean;
|
|
134
|
+
/** Whether to preserve manually moved node positions on data update (default: true) */
|
|
135
|
+
preservePositions?: boolean;
|
|
136
|
+
/** Padding around content when fitting to view after layout (default: 40) */
|
|
137
|
+
fitPadding?: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Default layout configuration
|
|
141
|
+
*/
|
|
142
|
+
declare const DEFAULT_GRAPH_LAYOUT_CONFIG: Required<GraphLayoutConfig>;
|
|
143
|
+
/**
|
|
144
|
+
* Main configuration for the graph component
|
|
145
|
+
* Combines grid, layout, and path configurations
|
|
146
|
+
*/
|
|
147
|
+
interface GraphConfig {
|
|
148
|
+
/** Grid configuration (pan, zoom, background, etc.) */
|
|
149
|
+
grid?: Partial<GridConfig>;
|
|
150
|
+
/** Layout configuration (algorithm, spacing, etc.) */
|
|
151
|
+
layout?: GraphLayoutConfig;
|
|
152
|
+
/** Path rendering configuration (colors, arrows, etc.) */
|
|
153
|
+
paths?: GraphPathConfig;
|
|
154
|
+
/** Component to render at path midpoints (optional) */
|
|
155
|
+
pillComponent?: Type<unknown>;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Default graph configuration
|
|
159
|
+
*/
|
|
160
|
+
declare const DEFAULT_GRAPH_CONFIG: GraphConfig;
|
|
161
|
+
/**
|
|
162
|
+
* Event emitted when a node is selected
|
|
163
|
+
*/
|
|
164
|
+
interface NodeSelectEvent<T = unknown> {
|
|
165
|
+
/** The selected node */
|
|
166
|
+
node: GraphNode<T>;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Event emitted when a node position changes
|
|
170
|
+
*/
|
|
171
|
+
interface NodePositionChangeEvent {
|
|
172
|
+
/** Node ID */
|
|
173
|
+
nodeId: string;
|
|
174
|
+
/** New X position */
|
|
175
|
+
x: number;
|
|
176
|
+
/** New Y position */
|
|
177
|
+
y: number;
|
|
178
|
+
/** Previous X position */
|
|
179
|
+
previousX: number;
|
|
180
|
+
/** Previous Y position */
|
|
181
|
+
previousY: number;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Event emitted when a relationship is selected
|
|
185
|
+
*/
|
|
186
|
+
interface RelationshipSelectEvent<T = unknown> {
|
|
187
|
+
/** The selected relationship */
|
|
188
|
+
relationship: GraphRelationship<T>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
declare class GraphComponent<TNode = unknown, TRelationship = unknown> {
|
|
192
|
+
private readonly layoutService;
|
|
193
|
+
private readonly pathFindingService;
|
|
194
|
+
private readonly injector;
|
|
195
|
+
private readonly gridRef;
|
|
196
|
+
nodes: _angular_core.InputSignal<GraphNode<TNode>[]>;
|
|
197
|
+
relationships: _angular_core.InputSignal<GraphRelationship<TRelationship>[]>;
|
|
198
|
+
config: _angular_core.InputSignal<GraphConfig>;
|
|
199
|
+
nodePositionChange: _angular_core.OutputEmitterRef<NodePositionChangeEvent>;
|
|
200
|
+
viewportChange: _angular_core.OutputEmitterRef<ViewportState>;
|
|
201
|
+
private nodePositions;
|
|
202
|
+
private nodeDimensions;
|
|
203
|
+
private viewport;
|
|
204
|
+
calculatedPaths: _angular_core.WritableSignal<CalculatedPath[]>;
|
|
205
|
+
/** Whether initial layout has completed - used to prevent flash of unstyled nodes */
|
|
206
|
+
layoutReady: _angular_core.WritableSignal<boolean>;
|
|
207
|
+
gridConfig: _angular_core.Signal<GridConfig>;
|
|
208
|
+
layoutConfig: _angular_core.Signal<{
|
|
209
|
+
algorithm: _ngx_km_layout.LayoutAlgorithm;
|
|
210
|
+
direction: _ngx_km_layout.LayoutDirection;
|
|
211
|
+
nodeSpacing: number;
|
|
212
|
+
layerSpacing: number;
|
|
213
|
+
autoLayout: boolean;
|
|
214
|
+
preservePositions: boolean;
|
|
215
|
+
fitPadding: number;
|
|
216
|
+
}>;
|
|
217
|
+
pathConfig: _angular_core.Signal<{
|
|
218
|
+
pathType: _ngx_km_path_finding.PathType;
|
|
219
|
+
strokeColor: string;
|
|
220
|
+
strokeWidth: number;
|
|
221
|
+
strokePattern: _ngx_km_path_drawing.StrokePattern;
|
|
222
|
+
strokeOpacity: number;
|
|
223
|
+
cornerRadius: number;
|
|
224
|
+
arrowStyle: _ngx_km_path_drawing.ArrowStyle;
|
|
225
|
+
arrowSize: number;
|
|
226
|
+
obstaclePadding: number;
|
|
227
|
+
anchorSpacing: number;
|
|
228
|
+
spreadAnchors: boolean;
|
|
229
|
+
}>;
|
|
230
|
+
/** Pill component to render at path midpoints */
|
|
231
|
+
pillComponent: _angular_core.Signal<_angular_core.Type<unknown> | undefined>;
|
|
232
|
+
gridElements: _angular_core.Signal<GridElement<TNode>[]>;
|
|
233
|
+
pathsTransform: _angular_core.Signal<string>;
|
|
234
|
+
constructor();
|
|
235
|
+
onViewportChange(viewport: ViewportState): void;
|
|
236
|
+
onElementsRendered(elements: RenderedElement[]): void;
|
|
237
|
+
onElementPositionChange(change: ElementPositionChange): void;
|
|
238
|
+
recalculateLayout(): void;
|
|
239
|
+
/**
|
|
240
|
+
* Fit all graph content to view
|
|
241
|
+
* Centers content in the viewport, optionally zooming out to fit everything
|
|
242
|
+
* @param fitZoom If true, zooms out if needed to fit all content (never zooms in beyond 1.0)
|
|
243
|
+
* @param padding Padding around content in pixels (default: 40)
|
|
244
|
+
*/
|
|
245
|
+
fitToView(fitZoom?: boolean, padding?: number): void;
|
|
246
|
+
/**
|
|
247
|
+
* Check if a node is fully visible in the viewport
|
|
248
|
+
* @param nodeId The node ID to check
|
|
249
|
+
* @param padding Padding from viewport edges (default: 0)
|
|
250
|
+
* @returns true if node is fully visible, false otherwise
|
|
251
|
+
*/
|
|
252
|
+
isNodeVisible(nodeId: string, padding?: number): boolean;
|
|
253
|
+
/**
|
|
254
|
+
* Center viewport on a specific node (no zoom change)
|
|
255
|
+
* @param nodeId The node ID to center on
|
|
256
|
+
*/
|
|
257
|
+
centerOnNode(nodeId: string): void;
|
|
258
|
+
/**
|
|
259
|
+
* Scroll node into view with minimal pan
|
|
260
|
+
* Only pans if node is not fully visible
|
|
261
|
+
* @param nodeId The node ID to scroll into view
|
|
262
|
+
* @param padding Padding from viewport edges (default: 40)
|
|
263
|
+
*/
|
|
264
|
+
scrollToNode(nodeId: string, padding?: number): void;
|
|
265
|
+
private runLayout;
|
|
266
|
+
private calculatePaths;
|
|
267
|
+
getArrowPoints(): string;
|
|
268
|
+
getStrokeDasharray(): string;
|
|
269
|
+
getPathD(path: CalculatedPath): string;
|
|
270
|
+
private generateOrthogonalPath;
|
|
271
|
+
private generateBezierPath;
|
|
272
|
+
private generateStraightPath;
|
|
273
|
+
/** Create an injector for a pill component, passing the relationship data */
|
|
274
|
+
createPillInjector(relationshipId: string): Injector;
|
|
275
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<GraphComponent<any, any>, never>;
|
|
276
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<GraphComponent<any, any>, "ngx-graph", never, { "nodes": { "alias": "nodes"; "required": false; "isSignal": true; }; "relationships": { "alias": "relationships"; "required": false; "isSignal": true; }; "config": { "alias": "config"; "required": false; "isSignal": true; }; }, { "nodePositionChange": "nodePositionChange"; "viewportChange": "viewportChange"; }, never, never, true, never>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { DEFAULT_GRAPH_CONFIG, DEFAULT_GRAPH_LAYOUT_CONFIG, DEFAULT_GRAPH_PATH_CONFIG, GRAPH_NODE_DATA, GRAPH_RELATIONSHIP_DATA, GraphComponent };
|
|
280
|
+
export type { GraphConfig, GraphLayoutConfig, GraphNode, GraphNodeInternal, GraphPathConfig, GraphRelationship, NodePositionChangeEvent, NodeSelectEvent, ReactiveData, RelationshipSelectEvent, RelationshipType };
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ngx-km/graph",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"@ngx-km/grid": "^0.0.1",
|
|
6
|
+
"@ngx-km/layout": "^0.0.1",
|
|
7
|
+
"@ngx-km/path-finding": "^0.0.1",
|
|
8
|
+
"@ngx-km/path-drawing": "^0.0.1",
|
|
9
|
+
"tslib": "^2.3.0"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@angular/common": ">=19.0.0",
|
|
13
|
+
"@angular/core": ">=19.0.0"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"module": "fesm2022/ngx-km-graph.mjs",
|
|
17
|
+
"typings": "index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
"./package.json": {
|
|
20
|
+
"default": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./index.d.ts",
|
|
24
|
+
"default": "./fesm2022/ngx-km-graph.mjs"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|