@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 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
+ }