npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/graph

Quick 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