npm package: @ngx-km/graph
Description: Angular 19+ library for creating interactive graph visualizations with custom node components.
Latest version: 0.0.4

# @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

```bash
npm install @ngx-km/graph
```

## Quick Start

```typescript
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:

```typescript
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

```typescript
// 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:

```typescript
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:

```typescript
// 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

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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

```typescript
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:

```typescript
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:

```typescript
const config: GraphConfig = {
  pillComponent: EdgePillComponent,
};
```

## Reactive Data Updates

Both node and relationship data support Angular Signals for reactive updates:

```typescript
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

```typescript
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