@ngx-km/graph
v0.0.4
Published
Angular 19+ library for creating interactive graph visualizations with custom node components.
Downloads
325
Readme
@ngx-km/graph
Angular 19+ library for creating interactive graph visualizations with custom node components.
Features
- Use any Angular component as a node
- Automatic graph layout (hierarchical, force-directed, stress)
- Obstacle-aware path routing between nodes
- Infinite canvas with pan and zoom
- Animated viewport transitions with configurable easing
- Draggable nodes with position tracking
- Custom labels on paths (pill components)
- Fully type-safe API
Installation
npm install @ngx-km/graphQuick Start
import { Component } from '@angular/core';
import { GraphComponent, GraphNode, GraphRelationship } from '@ngx-km/graph';
import { MyNodeComponent } from './my-node.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [GraphComponent],
template: `
<ngx-graph
[nodes]="nodes"
[relationships]="relationships"
/>
`,
styles: [`
:host { display: block; width: 100%; height: 100vh; }
`]
})
export class AppComponent {
nodes: GraphNode<MyNodeData>[] = [
{ id: '1', component: MyNodeComponent, data: { label: 'Node 1' } },
{ id: '2', component: MyNodeComponent, data: { label: 'Node 2' } },
{ id: '3', component: MyNodeComponent, data: { label: 'Node 3' } },
];
relationships: GraphRelationship[] = [
{ id: 'e1', sourceId: '1', targetId: '2' },
{ id: 'e2', sourceId: '2', targetId: '3' },
];
}Creating Node Components
Node components receive data via the GRAPH_NODE_DATA injection token:
import { Component, inject } from '@angular/core';
import { GRAPH_NODE_DATA } from '@ngx-km/graph';
interface MyNodeData {
label: string;
status?: 'active' | 'inactive';
}
@Component({
selector: 'app-my-node',
standalone: true,
template: `
<div class="node" [class.active]="data?.status === 'active'">
{{ data?.label }}
</div>
`,
styles: [`
.node {
padding: 16px 24px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-weight: 500;
}
.node.active {
border-color: #10b981;
background: #ecfdf5;
}
`]
})
export class MyNodeComponent {
data = inject<MyNodeData>(GRAPH_NODE_DATA, { optional: true });
}API Reference
Inputs
| Input | Type | Description |
|-------|------|-------------|
| nodes | GraphNode<T>[] | Array of node definitions |
| relationships | GraphRelationship<T>[] | Array of relationship definitions |
| config | GraphConfig | Configuration object |
Outputs
| Output | Type | Description |
|--------|------|-------------|
| nodePositionChange | NodePositionChangeEvent | Emitted when a node is dragged |
| viewportChange | ViewportState | Emitted on pan/zoom |
Public Methods
// Get a reference to the graph component
@ViewChild(GraphComponent) graph!: GraphComponent;
// Fit all content to viewport
this.graph.fitToView(fitZoom?: boolean, padding?: number, options?: ViewportAnimationOptions);
// Center viewport on a specific node
this.graph.centerOnNode(nodeId: string, options?: ViewportAnimationOptions);
// Scroll node into view with minimal pan
this.graph.scrollToNode(nodeId: string, padding?: number, options?: ViewportAnimationOptions);
// Check if node is visible in viewport
this.graph.isNodeVisible(nodeId: string, padding?: number): boolean;
// Force recalculate layout
this.graph.recalculateLayout();Animated Viewport Transitions
All viewport methods support smooth animated transitions:
import { ViewportAnimationOptions } from '@ngx-km/graph';
// Animate with default settings (300ms, ease-out)
this.graph.centerOnNode('node-1', { animated: true });
// Custom duration and easing
this.graph.fitToView(true, 40, {
animated: true,
duration: 500,
easing: 'ease-in-out',
});
// Instant (no animation)
this.graph.scrollToNode('node-2', 40, { animated: false });ViewportAnimationOptions
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| animated | boolean | false | Enable animated transition |
| duration | number | 300 | Animation duration in milliseconds |
| easing | EasingName \| EasingFunction | 'ease-out' | Easing function |
Built-in Easing Functions
| Name | Description |
|------|-------------|
| 'linear' | Constant speed |
| 'ease-out' | Fast start, slow end (default) |
| 'ease-in' | Slow start, fast end |
| 'ease-in-out' | Slow start and end |
Custom Easing Functions
You can provide a custom easing function:
// Custom bounce easing
this.graph.centerOnNode('node-1', {
animated: true,
duration: 600,
easing: (t: number) => {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) return n1 * t * t;
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
return n1 * (t -= 2.625 / d1) * t + 0.984375;
},
});Note: Animations are automatically cancelled when the user starts panning or zooming.
Interfaces
GraphNode
interface GraphNode<T = unknown> {
id: string; // Unique identifier
component: Type<unknown>; // Angular component to render
data?: T; // Data passed to component
x?: number; // Initial X position (optional)
y?: number; // Initial Y position (optional)
}GraphRelationship
interface GraphRelationship<T = unknown> {
id: string; // Unique identifier
sourceId: string; // Source node ID
targetId: string; // Target node ID
type?: 'one-way' | 'two-way'; // Arrow direction (default: 'one-way')
data?: T; // Data passed to pill component
sourceAnchorSide?: 'top' | 'right' | 'bottom' | 'left';
targetAnchorSide?: 'top' | 'right' | 'bottom' | 'left';
}Relationship Types
Relationships support two arrow modes:
| Type | Description | Visual |
|------|-------------|--------|
| 'one-way' | Arrow on target end only (default) | A ───→ B |
| 'two-way' | Arrows on both ends (bidirectional) | A ←──→ B |
One-Way Relationships (Default)
One-way relationships show a single arrow pointing from source to target. This is the default when type is omitted:
const relationships: GraphRelationship[] = [
// Explicit one-way
{ id: 'e1', sourceId: 'A', targetId: 'B', type: 'one-way' },
// Implicit one-way (type defaults to 'one-way')
{ id: 'e2', sourceId: 'B', targetId: 'C' },
];Two-Way Relationships (Bidirectional)
Two-way relationships show arrows on both ends, indicating a bidirectional connection:
const relationships: GraphRelationship[] = [
// Bidirectional relationship
{ id: 'e1', sourceId: 'A', targetId: 'B', type: 'two-way' },
// Mixed: some one-way, some two-way
{ id: 'e2', sourceId: 'B', targetId: 'C', type: 'one-way' },
{ id: 'e3', sourceId: 'C', targetId: 'D', type: 'two-way' },
];Complete Example with Mixed Relationship Types
import { Component } from '@angular/core';
import { GraphComponent, GraphNode, GraphRelationship } from '@ngx-km/graph';
import { NodeComponent } from './node.component';
@Component({
selector: 'app-network',
standalone: true,
imports: [GraphComponent],
template: `
<ngx-graph [nodes]="nodes" [relationships]="relationships" />
`,
})
export class NetworkComponent {
nodes: GraphNode[] = [
{ id: 'server', component: NodeComponent, data: { label: 'Server' } },
{ id: 'client1', component: NodeComponent, data: { label: 'Client 1' } },
{ id: 'client2', component: NodeComponent, data: { label: 'Client 2' } },
{ id: 'database', component: NodeComponent, data: { label: 'Database' } },
];
relationships: GraphRelationship[] = [
// Bidirectional: Server communicates both ways with clients
{ id: 'r1', sourceId: 'server', targetId: 'client1', type: 'two-way' },
{ id: 'r2', sourceId: 'server', targetId: 'client2', type: 'two-way' },
// One-way: Server writes to database
{ id: 'r3', sourceId: 'server', targetId: 'database', type: 'one-way' },
];
}Configuration
Full Configuration Example
import { GraphConfig } from '@ngx-km/graph';
const config: GraphConfig = {
// Grid settings
grid: {
backgroundMode: 'dots', // 'dots' | 'lines' | 'none'
cellSize: 20,
panEnabled: true,
zoomEnabled: true,
dragEnabled: true,
minZoom: 0.1,
maxZoom: 3,
},
// Layout settings
layout: {
algorithm: 'layered', // 'layered' | 'force' | 'stress'
direction: 'DOWN', // 'DOWN' | 'UP' | 'LEFT' | 'RIGHT'
nodeSpacing: 50,
layerSpacing: 100,
autoLayout: true,
preservePositions: true,
fitPadding: 40,
},
// Path settings
paths: {
pathType: 'orthogonal', // 'orthogonal' | 'bezier' | 'straight'
strokeColor: '#6366f1',
strokeWidth: 2,
strokePattern: 'solid', // 'solid' | 'dashed' | 'dotted'
cornerRadius: 8,
arrowSize: 10,
obstaclePadding: 20,
},
// Optional: Component for path labels
pillComponent: MyPillComponent,
};Layout Algorithms
| Algorithm | Best For |
|-----------|----------|
| layered | Hierarchical structures, flowcharts, org charts |
| force | Networks, social graphs, unstructured data |
| stress | General purpose, good edge length consistency |
| radial | Radial tree structures, centered hierarchies |
| tree | Simple tree structures, file systems |
Path Types
| Type | Description |
|------|-------------|
| orthogonal | Right-angle paths that avoid obstacles |
| bezier | Smooth curved paths |
| straight | Direct lines between nodes |
Pill Components
Add labels or controls to paths using pill components:
import { Component, inject } from '@angular/core';
import { GRAPH_RELATIONSHIP_DATA } from '@ngx-km/graph';
interface EdgeData {
label: string;
}
@Component({
selector: 'app-edge-pill',
standalone: true,
template: `
<div class="pill">{{ data?.label }}</div>
`,
styles: [`
.pill {
padding: 4px 12px;
background: #6366f1;
color: white;
border-radius: 12px;
font-size: 12px;
}
`]
})
export class EdgePillComponent {
data = inject<EdgeData>(GRAPH_RELATIONSHIP_DATA, { optional: true });
}Use it in config:
const config: GraphConfig = {
pillComponent: EdgePillComponent,
};Reactive Data Updates
Both node and relationship data support Angular Signals for reactive updates:
import { signal } from '@angular/core';
// Data as signals - component updates automatically
nodes: GraphNode[] = [
{
id: '1',
component: MyNodeComponent,
data: signal({ label: 'Node 1', count: 0 }),
},
];
// Update data reactively
updateNode() {
const nodeData = this.nodes[0].data as WritableSignal<MyNodeData>;
nodeData.update(d => ({ ...d, count: d.count + 1 }));
}Complete Example
import { Component, signal } from '@angular/core';
import {
GraphComponent,
GraphNode,
GraphRelationship,
GraphConfig,
} from '@ngx-km/graph';
import { MyNodeComponent } from './my-node.component';
import { EdgePillComponent } from './edge-pill.component';
@Component({
selector: 'app-workflow',
standalone: true,
imports: [GraphComponent],
template: `
<ngx-graph
#graph
[nodes]="nodes"
[relationships]="relationships"
[config]="config"
(nodePositionChange)="onNodeMoved($event)"
/>
<button (click)="graph.fitToView(true)">Fit to View</button>
`,
})
export class WorkflowComponent {
nodes: GraphNode[] = [
{ id: 'start', component: MyNodeComponent, data: { label: 'Start', type: 'start' } },
{ id: 'process', component: MyNodeComponent, data: { label: 'Process', type: 'task' } },
{ id: 'decision', component: MyNodeComponent, data: { label: 'Approve?', type: 'decision' } },
{ id: 'end', component: MyNodeComponent, data: { label: 'End', type: 'end' } },
];
relationships: GraphRelationship[] = [
{ id: 'e1', sourceId: 'start', targetId: 'process' },
{ id: 'e2', sourceId: 'process', targetId: 'decision' },
{ id: 'e3', sourceId: 'decision', targetId: 'end', data: { label: 'Yes' } },
];
config: GraphConfig = {
layout: {
algorithm: 'layered',
direction: 'DOWN',
nodeSpacing: 60,
layerSpacing: 120,
},
paths: {
pathType: 'orthogonal',
strokeColor: '#6366f1',
cornerRadius: 8,
},
pillComponent: EdgePillComponent,
};
onNodeMoved(event: NodePositionChangeEvent) {
console.log(`Node ${event.nodeId} moved to (${event.x}, ${event.y})`);
}
}Requirements
- Angular 19+
- TypeScript 5.4+
License
MIT
