@ngx-km/graph 0.0.1 → 0.0.2
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 +333 -17
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# @ngx-km/graph
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Angular 19+ library for creating interactive graph visualizations with custom node components.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- Custom
|
|
13
|
-
-
|
|
7
|
+
- Use any Angular component as a node
|
|
8
|
+
- Automatic graph layout (hierarchical, force-directed, stress)
|
|
9
|
+
- Obstacle-aware path routing between nodes
|
|
10
|
+
- Infinite canvas with pan and zoom
|
|
11
|
+
- Draggable nodes with position tracking
|
|
12
|
+
- Custom labels on paths (pill components)
|
|
13
|
+
- Fully type-safe API
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -18,26 +18,342 @@ Main graph visualization library combining all sub-libraries into a unified comp
|
|
|
18
18
|
npm install @ngx-km/graph
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Quick Start
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
import {
|
|
24
|
+
import { Component } from '@angular/core';
|
|
25
|
+
import { GraphComponent, GraphNode, GraphRelationship } from '@ngx-km/graph';
|
|
26
|
+
import { MyNodeComponent } from './my-node.component';
|
|
25
27
|
|
|
26
28
|
@Component({
|
|
29
|
+
selector: 'app-root',
|
|
30
|
+
standalone: true,
|
|
27
31
|
imports: [GraphComponent],
|
|
28
32
|
template: `
|
|
29
33
|
<ngx-graph
|
|
30
34
|
[nodes]="nodes"
|
|
31
35
|
[relationships]="relationships"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
36
|
+
/>
|
|
37
|
+
`,
|
|
38
|
+
styles: [`
|
|
39
|
+
:host { display: block; width: 100%; height: 100vh; }
|
|
40
|
+
`]
|
|
35
41
|
})
|
|
36
|
-
export class
|
|
42
|
+
export class AppComponent {
|
|
43
|
+
nodes: GraphNode<MyNodeData>[] = [
|
|
44
|
+
{ id: '1', component: MyNodeComponent, data: { label: 'Node 1' } },
|
|
45
|
+
{ id: '2', component: MyNodeComponent, data: { label: 'Node 2' } },
|
|
46
|
+
{ id: '3', component: MyNodeComponent, data: { label: 'Node 3' } },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
relationships: GraphRelationship[] = [
|
|
50
|
+
{ id: 'e1', sourceId: '1', targetId: '2' },
|
|
51
|
+
{ id: 'e2', sourceId: '2', targetId: '3' },
|
|
52
|
+
];
|
|
53
|
+
}
|
|
37
54
|
```
|
|
38
55
|
|
|
39
|
-
##
|
|
56
|
+
## Creating Node Components
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
Node components receive data via the `GRAPH_NODE_DATA` injection token:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { Component, inject } from '@angular/core';
|
|
62
|
+
import { GRAPH_NODE_DATA } from '@ngx-km/graph';
|
|
63
|
+
|
|
64
|
+
interface MyNodeData {
|
|
65
|
+
label: string;
|
|
66
|
+
status?: 'active' | 'inactive';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Component({
|
|
70
|
+
selector: 'app-my-node',
|
|
71
|
+
standalone: true,
|
|
72
|
+
template: `
|
|
73
|
+
<div class="node" [class.active]="data?.status === 'active'">
|
|
74
|
+
{{ data?.label }}
|
|
75
|
+
</div>
|
|
76
|
+
`,
|
|
77
|
+
styles: [`
|
|
78
|
+
.node {
|
|
79
|
+
padding: 16px 24px;
|
|
80
|
+
background: white;
|
|
81
|
+
border: 2px solid #e5e7eb;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
}
|
|
85
|
+
.node.active {
|
|
86
|
+
border-color: #10b981;
|
|
87
|
+
background: #ecfdf5;
|
|
88
|
+
}
|
|
89
|
+
`]
|
|
90
|
+
})
|
|
91
|
+
export class MyNodeComponent {
|
|
92
|
+
data = inject<MyNodeData>(GRAPH_NODE_DATA, { optional: true });
|
|
93
|
+
}
|
|
43
94
|
```
|
|
95
|
+
|
|
96
|
+
## API Reference
|
|
97
|
+
|
|
98
|
+
### Inputs
|
|
99
|
+
|
|
100
|
+
| Input | Type | Description |
|
|
101
|
+
|-------|------|-------------|
|
|
102
|
+
| `nodes` | `GraphNode<T>[]` | Array of node definitions |
|
|
103
|
+
| `relationships` | `GraphRelationship<T>[]` | Array of relationship definitions |
|
|
104
|
+
| `config` | `GraphConfig` | Configuration object |
|
|
105
|
+
|
|
106
|
+
### Outputs
|
|
107
|
+
|
|
108
|
+
| Output | Type | Description |
|
|
109
|
+
|--------|------|-------------|
|
|
110
|
+
| `nodePositionChange` | `NodePositionChangeEvent` | Emitted when a node is dragged |
|
|
111
|
+
| `viewportChange` | `ViewportState` | Emitted on pan/zoom |
|
|
112
|
+
|
|
113
|
+
### Public Methods
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Get a reference to the graph component
|
|
117
|
+
@ViewChild(GraphComponent) graph!: GraphComponent;
|
|
118
|
+
|
|
119
|
+
// Fit all content to viewport
|
|
120
|
+
this.graph.fitToView(fitZoom?: boolean, padding?: number);
|
|
121
|
+
|
|
122
|
+
// Center viewport on a specific node
|
|
123
|
+
this.graph.centerOnNode(nodeId: string);
|
|
124
|
+
|
|
125
|
+
// Scroll node into view with minimal pan
|
|
126
|
+
this.graph.scrollToNode(nodeId: string, padding?: number);
|
|
127
|
+
|
|
128
|
+
// Check if node is visible in viewport
|
|
129
|
+
this.graph.isNodeVisible(nodeId: string, padding?: number): boolean;
|
|
130
|
+
|
|
131
|
+
// Force recalculate layout
|
|
132
|
+
this.graph.recalculateLayout();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Interfaces
|
|
136
|
+
|
|
137
|
+
### GraphNode
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
interface GraphNode<T = unknown> {
|
|
141
|
+
id: string; // Unique identifier
|
|
142
|
+
component: Type<unknown>; // Angular component to render
|
|
143
|
+
data?: T; // Data passed to component
|
|
144
|
+
x?: number; // Initial X position (optional)
|
|
145
|
+
y?: number; // Initial Y position (optional)
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### GraphRelationship
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
interface GraphRelationship<T = unknown> {
|
|
153
|
+
id: string; // Unique identifier
|
|
154
|
+
sourceId: string; // Source node ID
|
|
155
|
+
targetId: string; // Target node ID
|
|
156
|
+
type?: 'one-way' | 'two-way'; // Arrow direction
|
|
157
|
+
data?: T; // Data passed to pill component
|
|
158
|
+
sourceAnchorSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
159
|
+
targetAnchorSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
### Full Configuration Example
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { GraphConfig } from '@ngx-km/graph';
|
|
169
|
+
|
|
170
|
+
const config: GraphConfig = {
|
|
171
|
+
// Grid settings
|
|
172
|
+
grid: {
|
|
173
|
+
backgroundMode: 'dots', // 'dots' | 'lines' | 'none'
|
|
174
|
+
cellSize: 20,
|
|
175
|
+
panEnabled: true,
|
|
176
|
+
zoomEnabled: true,
|
|
177
|
+
dragEnabled: true,
|
|
178
|
+
minZoom: 0.1,
|
|
179
|
+
maxZoom: 3,
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Layout settings
|
|
183
|
+
layout: {
|
|
184
|
+
algorithm: 'layered', // 'layered' | 'force' | 'stress'
|
|
185
|
+
direction: 'DOWN', // 'DOWN' | 'UP' | 'LEFT' | 'RIGHT'
|
|
186
|
+
nodeSpacing: 50,
|
|
187
|
+
layerSpacing: 100,
|
|
188
|
+
autoLayout: true,
|
|
189
|
+
preservePositions: true,
|
|
190
|
+
fitPadding: 40,
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// Path settings
|
|
194
|
+
paths: {
|
|
195
|
+
pathType: 'orthogonal', // 'orthogonal' | 'bezier' | 'straight'
|
|
196
|
+
strokeColor: '#6366f1',
|
|
197
|
+
strokeWidth: 2,
|
|
198
|
+
strokePattern: 'solid', // 'solid' | 'dashed' | 'dotted'
|
|
199
|
+
cornerRadius: 8,
|
|
200
|
+
arrowSize: 10,
|
|
201
|
+
obstaclePadding: 20,
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Optional: Component for path labels
|
|
205
|
+
pillComponent: MyPillComponent,
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Layout Algorithms
|
|
210
|
+
|
|
211
|
+
| Algorithm | Best For |
|
|
212
|
+
|-----------|----------|
|
|
213
|
+
| `layered` | Hierarchical structures, flowcharts, org charts |
|
|
214
|
+
| `force` | Networks, social graphs, unstructured data |
|
|
215
|
+
| `stress` | General purpose, good edge length consistency |
|
|
216
|
+
|
|
217
|
+
### Path Types
|
|
218
|
+
|
|
219
|
+
| Type | Description |
|
|
220
|
+
|------|-------------|
|
|
221
|
+
| `orthogonal` | Right-angle paths that avoid obstacles |
|
|
222
|
+
| `bezier` | Smooth curved paths |
|
|
223
|
+
| `straight` | Direct lines between nodes |
|
|
224
|
+
|
|
225
|
+
## Pill Components
|
|
226
|
+
|
|
227
|
+
Add labels or controls to paths using pill components:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { Component, inject } from '@angular/core';
|
|
231
|
+
import { GRAPH_RELATIONSHIP_DATA } from '@ngx-km/graph';
|
|
232
|
+
|
|
233
|
+
interface EdgeData {
|
|
234
|
+
label: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@Component({
|
|
238
|
+
selector: 'app-edge-pill',
|
|
239
|
+
standalone: true,
|
|
240
|
+
template: `
|
|
241
|
+
<div class="pill">{{ data?.label }}</div>
|
|
242
|
+
`,
|
|
243
|
+
styles: [`
|
|
244
|
+
.pill {
|
|
245
|
+
padding: 4px 12px;
|
|
246
|
+
background: #6366f1;
|
|
247
|
+
color: white;
|
|
248
|
+
border-radius: 12px;
|
|
249
|
+
font-size: 12px;
|
|
250
|
+
}
|
|
251
|
+
`]
|
|
252
|
+
})
|
|
253
|
+
export class EdgePillComponent {
|
|
254
|
+
data = inject<EdgeData>(GRAPH_RELATIONSHIP_DATA, { optional: true });
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Use it in config:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const config: GraphConfig = {
|
|
262
|
+
pillComponent: EdgePillComponent,
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Reactive Data Updates
|
|
267
|
+
|
|
268
|
+
Both node and relationship data support Angular Signals for reactive updates:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { signal } from '@angular/core';
|
|
272
|
+
|
|
273
|
+
// Data as signals - component updates automatically
|
|
274
|
+
nodes: GraphNode[] = [
|
|
275
|
+
{
|
|
276
|
+
id: '1',
|
|
277
|
+
component: MyNodeComponent,
|
|
278
|
+
data: signal({ label: 'Node 1', count: 0 }),
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
// Update data reactively
|
|
283
|
+
updateNode() {
|
|
284
|
+
const nodeData = this.nodes[0].data as WritableSignal<MyNodeData>;
|
|
285
|
+
nodeData.update(d => ({ ...d, count: d.count + 1 }));
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Complete Example
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { Component, signal } from '@angular/core';
|
|
293
|
+
import {
|
|
294
|
+
GraphComponent,
|
|
295
|
+
GraphNode,
|
|
296
|
+
GraphRelationship,
|
|
297
|
+
GraphConfig,
|
|
298
|
+
} from '@ngx-km/graph';
|
|
299
|
+
import { MyNodeComponent } from './my-node.component';
|
|
300
|
+
import { EdgePillComponent } from './edge-pill.component';
|
|
301
|
+
|
|
302
|
+
@Component({
|
|
303
|
+
selector: 'app-workflow',
|
|
304
|
+
standalone: true,
|
|
305
|
+
imports: [GraphComponent],
|
|
306
|
+
template: `
|
|
307
|
+
<ngx-graph
|
|
308
|
+
#graph
|
|
309
|
+
[nodes]="nodes"
|
|
310
|
+
[relationships]="relationships"
|
|
311
|
+
[config]="config"
|
|
312
|
+
(nodePositionChange)="onNodeMoved($event)"
|
|
313
|
+
/>
|
|
314
|
+
<button (click)="graph.fitToView(true)">Fit to View</button>
|
|
315
|
+
`,
|
|
316
|
+
})
|
|
317
|
+
export class WorkflowComponent {
|
|
318
|
+
nodes: GraphNode[] = [
|
|
319
|
+
{ id: 'start', component: MyNodeComponent, data: { label: 'Start', type: 'start' } },
|
|
320
|
+
{ id: 'process', component: MyNodeComponent, data: { label: 'Process', type: 'task' } },
|
|
321
|
+
{ id: 'decision', component: MyNodeComponent, data: { label: 'Approve?', type: 'decision' } },
|
|
322
|
+
{ id: 'end', component: MyNodeComponent, data: { label: 'End', type: 'end' } },
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
relationships: GraphRelationship[] = [
|
|
326
|
+
{ id: 'e1', sourceId: 'start', targetId: 'process' },
|
|
327
|
+
{ id: 'e2', sourceId: 'process', targetId: 'decision' },
|
|
328
|
+
{ id: 'e3', sourceId: 'decision', targetId: 'end', data: { label: 'Yes' } },
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
config: GraphConfig = {
|
|
332
|
+
layout: {
|
|
333
|
+
algorithm: 'layered',
|
|
334
|
+
direction: 'DOWN',
|
|
335
|
+
nodeSpacing: 60,
|
|
336
|
+
layerSpacing: 120,
|
|
337
|
+
},
|
|
338
|
+
paths: {
|
|
339
|
+
pathType: 'orthogonal',
|
|
340
|
+
strokeColor: '#6366f1',
|
|
341
|
+
cornerRadius: 8,
|
|
342
|
+
},
|
|
343
|
+
pillComponent: EdgePillComponent,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
onNodeMoved(event: NodePositionChangeEvent) {
|
|
347
|
+
console.log(`Node ${event.nodeId} moved to (${event.x}, ${event.y})`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Requirements
|
|
353
|
+
|
|
354
|
+
- Angular 19+
|
|
355
|
+
- TypeScript 5.4+
|
|
356
|
+
|
|
357
|
+
## License
|
|
358
|
+
|
|
359
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngx-km/graph",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"dependencies": {
|
|
5
|
-
"@ngx-km/grid": "^0.0.
|
|
6
|
-
"@ngx-km/layout": "^0.0.
|
|
7
|
-
"@ngx-km/path-finding": "^0.0.
|
|
8
|
-
"@ngx-km/path-drawing": "^0.0.
|
|
5
|
+
"@ngx-km/grid": "^0.0.2",
|
|
6
|
+
"@ngx-km/layout": "^0.0.2",
|
|
7
|
+
"@ngx-km/path-finding": "^0.0.2",
|
|
8
|
+
"@ngx-km/path-drawing": "^0.0.2",
|
|
9
9
|
"tslib": "^2.3.0"
|
|
10
10
|
},
|
|
11
11
|
"peerDependencies": {
|