https://github.com/fidesit/graph-editor
Configuration-driven visual graph editor for Angular 19+
https://github.com/fidesit/graph-editor
Last synced: 21 days ago
JSON representation
Configuration-driven visual graph editor for Angular 19+
- Host: GitHub
- URL: https://github.com/fidesit/graph-editor
- Owner: fidesit
- License: mit
- Created: 2026-02-28T02:26:58.000Z (29 days ago)
- Default Branch: main
- Last Pushed: 2026-02-28T07:24:41.000Z (28 days ago)
- Last Synced: 2026-02-28T08:32:00.495Z (28 days ago)
- Language: TypeScript
- Size: 295 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
- fucking-awesome-angular - @utisha/graph-editor - Configuration-driven visual graph editor with SVG canvas, theming, undo/redo, custom node rendering, and dagre auto-layout. (Third Party Components / Charts)
- awesome-angular - @utisha/graph-editor - Configuration-driven visual graph editor with SVG canvas, theming, undo/redo, custom node rendering, and dagre auto-layout. (Third Party Components / Charts)
README
# @utisha/graph-editor
[](https://www.npmjs.com/package/@utisha/graph-editor)
[](https://github.com/fidesit/graph-editor/actions/workflows/ci.yml)
[](https://fidesit.github.io/graph-editor)
[](https://opensource.org/licenses/MIT)
[](https://stackblitz.com/github/fidesit/graph-editor)
Configuration-driven visual graph editor for Angular 19+.
**[Live Demo](https://fidesit.github.io/graph-editor)** | **[Try on StackBlitz](https://stackblitz.com/github/fidesit/graph-editor)**


## Features
- âī¸ **Configuration-driven** â No hardcoded domain logic
- đ¯ **Type-safe** â Full TypeScript support with strict mode
- đ **Themeable** â Full theme config (canvas, nodes, edges, ports, selection, fonts, toolbar) + CSS custom properties
- â¨ī¸ **Keyboard shortcuts** â Delete, arrow keys, escape, undo/redo
- đĻ **Lightweight** â Only Angular + dagre dependencies
- đ **Framework-agnostic data** â Works with any backend/state management
- đŧī¸ **Custom node images** â Use images instead of emoji icons
- đ¨ **Custom SVG icons** â Define your own icon sets with `iconSvg` property
- âŦ **Multi-selection** â Box select (Shift+drag) or Ctrl+Click to select multiple items
- âŠī¸ **Undo/Redo** â Full history with Ctrl+Z / Ctrl+Y
- đ˛ **Node resize** â Drag corner handle to resize nodes (Hand tool)
- đ **Text wrapping** â Labels wrap and auto-size to fit within node bounds
- đ ī¸ **Built-in toolbar** â Tools, zoom controls, layout actions in top bar; node palette on left
- đ§Š **Custom rendering** â ng-template injection for nodes (HTML or SVG) and edges, plus `ngComponentOutlet` support
- đ **Edge path strategies** â Straight, bezier, and step routing algorithms
- đ **Copy/Paste/Cut** â Ctrl+C/V/X to duplicate or cut selected nodes with their internal edges
- đ **Snap alignment guides** â Visual guide lines when dragging near other nodes' edges or center
- đ **Drag-to-connect** â Select node to reveal ports, drag from port to create edges
- đĩ **Multiple anchor points** â Dynamic port density per side with configurable spacing
- đĄī¸ **Lifecycle hooks** â `beforeNodeAdd`, `beforeNodeRemove`, `beforeEdgeAdd`, `beforeEdgeRemove`, `canConnect` guards that can cancel operations
## Installation
```bash
npm install @utisha/graph-editor
```
## Quick Start
### 1. Import the component
```typescript
import { Component, signal } from '@angular/core';
import { GraphEditorComponent, Graph, GraphEditorConfig } from '@utisha/graph-editor';
@Component({
selector: 'app-my-editor',
standalone: true,
imports: [GraphEditorComponent],
template: `
`
})
export class MyEditorComponent {
// See configuration below
}
```
### 2. Configure the editor
```typescript
editorConfig: GraphEditorConfig = {
nodes: {
types: [
{
type: 'process',
label: 'Process',
icon: 'âī¸',
component: null, // Uses default rendering, or provide your own component
defaultData: { name: 'New Process' },
size: { width: 180, height: 80 }
},
{
type: 'decision',
label: 'Decision',
icon: 'đ',
component: null,
defaultData: { name: 'Decision' },
size: { width: 180, height: 80 }
}
]
},
edges: {
component: null, // Uses default rendering
style: {
stroke: '#94a3b8',
strokeWidth: 2,
markerEnd: 'arrow'
}
},
canvas: {
grid: {
enabled: true,
size: 20,
snap: true
},
zoom: {
enabled: true,
min: 0.25,
max: 2.0,
wheelEnabled: true
},
pan: {
enabled: true
}
},
palette: {
enabled: true,
position: 'left'
}
};
```
### 3. Initialize your graph
```typescript
currentGraph = signal({
nodes: [
{ id: '1', type: 'process', data: { name: 'Start' }, position: { x: 100, y: 100 } },
{ id: '2', type: 'decision', data: { name: 'Check' }, position: { x: 300, y: 100 } }
],
edges: [
{ id: 'e1', source: '1', target: '2' }
]
});
onGraphChange(graph: Graph): void {
this.currentGraph.set(graph);
// Save to backend, update state, etc.
}
```
## Configuration
### GraphEditorConfig
| Property | Type | Description |
|----------|------|-------------|
| `nodes` | `NodesConfig` | Node type definitions + icon position |
| `edges` | `EdgesConfig` | Edge configuration |
| `canvas` | `CanvasConfig` | Canvas behavior (grid, zoom, pan) |
| `validation` | `ValidationConfig` | Validation rules |
| `palette` | `PaletteConfig` | Node palette configuration |
| `layout` | `LayoutConfig` | Layout algorithm (dagre, force, tree) |
| `theme` | `ThemeConfig` | Visual theme (shadows, CSS variables) |
| `toolbar` | `ToolbarConfig` | Top toolbar visibility and button selection |
| `hooks` | `LifecycleHooks` | Lifecycle guards to intercept/cancel user actions |
### Node Type Definition
```typescript
interface NodeTypeDefinition {
type: string; // Unique identifier
label?: string; // Display name in palette
icon?: string; // Fallback icon (emoji or text)
iconSvg?: SvgIconDefinition; // Professional SVG icon (preferred)
component: Type; // Angular component to render
defaultData: Record;
size?: { width: number; height: number };
ports?: PortConfig; // Connection ports
constraints?: NodeConstraints;
}
```
### Custom Node Images
Nodes can display custom images instead of emoji icons. Set `imageUrl` in `defaultData` or per-instance in `node.data['imageUrl']`:
```typescript
// In node type definition (applies to all nodes of this type)
{
type: 'agent',
label: 'AI Agent',
icon: 'đ¤', // Fallback if imageUrl fails to load
component: null,
defaultData: {
name: 'Agent',
imageUrl: '/assets/icons/agent.svg' // Custom image URL
}
}
// Or per-instance (overrides type default)
const node: GraphNode = {
id: '1',
type: 'agent',
data: {
name: 'Custom Agent',
imageUrl: 'https://example.com/custom-icon.png' // Instance-specific
},
position: { x: 100, y: 100 }
};
```
Supported formats: SVG, PNG, JPG, data URLs, or any valid image URL.
### Custom SVG Icons
Define your own SVG icons using the `SvgIconDefinition` interface. This allows you to use professional vector icons that match your design system:
```typescript
import { SvgIconDefinition, NodeTypeDefinition } from '@utisha/graph-editor';
// Define your icon set
const MY_ICONS: Record = {
process: {
viewBox: '0 0 24 24',
fill: 'none',
stroke: '#6366f1', // Your brand color
strokeWidth: 1.75,
path: `M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z
M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06...`
},
decision: {
viewBox: '0 0 24 24',
fill: 'none',
stroke: '#8b5cf6',
strokeWidth: 1.75,
path: `M12 3L21 12L12 21L3 12L12 3Z
M12 8v4
M12 16h.01`
},
start: {
viewBox: '0 0 24 24',
fill: 'none',
stroke: '#22c55e', // Semantic: green for start
strokeWidth: 1.75,
path: `M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2...
M10 8l6 4-6 4V8Z`
}
};
// Use in node types
const nodeTypes: NodeTypeDefinition[] = [
{ type: 'process', label: 'Process', iconSvg: MY_ICONS.process, component: null, defaultData: { name: 'Process' } },
{ type: 'decision', label: 'Decision', iconSvg: MY_ICONS.decision, component: null, defaultData: { name: 'Decision' } },
{ type: 'start', label: 'Start', iconSvg: MY_ICONS.start, component: null, defaultData: { name: 'Start' } },
];
```
**Icon priority:** `node.data['imageUrl']` â `nodeType.iconSvg` â `nodeType.defaultData['imageUrl']` â `nodeType.icon` (emoji fallback)
### Canvas Configuration
```typescript
interface CanvasConfig {
grid?: {
enabled: boolean;
size: number; // Grid cell size in pixels
snap: boolean; // Snap nodes to grid
color?: string;
};
zoom?: {
enabled: boolean;
min: number; // Minimum zoom level
max: number; // Maximum zoom level
step: number; // Zoom increment
wheelEnabled: boolean;
};
pan?: {
enabled: boolean;
};
}
```
## API
### Inputs
| Input | Type | Description |
|-------|------|-------------|
| `config` | `GraphEditorConfig` | Editor configuration (required) |
| `graph` | `Graph` | Current graph data |
| `readonly` | `boolean` | Disable editing |
| `visualizationMode` | `boolean` | Display only mode |
### Outputs
| Output | Type | Description |
|--------|------|-------------|
| `graphChange` | `EventEmitter` | Emitted on any graph mutation |
| `nodeClick` | `EventEmitter` | Node clicked |
| `nodeDoubleClick` | `EventEmitter` | Node double-clicked |
| `edgeClick` | `EventEmitter` | Edge clicked |
| `edgeDoubleClick` | `EventEmitter` | Edge double-clicked |
| `selectionChange` | `EventEmitter` | Selection changed |
| `validationChange` | `EventEmitter` | Validation state changed |
| `contextMenu` | `EventEmitter` | Right-click on canvas/node/edge |
### Methods
```typescript
// Node operations
addNode(type: string, position?: Position): GraphNode;
removeNode(nodeId: string): void;
updateNode(nodeId: string, updates: Partial): void;
// Selection
selectNode(nodeId: string | null): void;
selectEdge(edgeId: string | null): void;
clearSelection(): void;
getSelection(): SelectionState;
// Layout
applyLayout(algorithm?: 'dagre-tb' | 'dagre-lr' | 'force' | 'tree'): Promise;
fitToScreen(padding?: number): void;
zoomTo(level: number): void;
// Validation
validate(): ValidationResult;
```
## Theming
Customize the editor appearance using CSS custom properties:
```css
:root {
--graph-editor-canvas-bg: #f8f9fa;
--graph-editor-grid-color: #e0e0e0;
--graph-editor-node-bg: #ffffff;
--graph-editor-node-border: #cbd5e0;
--graph-editor-node-selected: #3b82f6;
--graph-editor-edge-stroke: #94a3b8;
--graph-editor-edge-selected: #3b82f6;
}
```
## Validation
Add custom validation rules:
```typescript
const config: GraphEditorConfig = {
// ...
validation: {
validateOnChange: true,
validators: [
{
id: 'no-orphans',
message: 'All nodes must be connected',
validator: (graph) => {
const orphans = findOrphanNodes(graph);
return orphans.map(node => ({
rule: 'no-orphans',
message: `Node "${node.data.name}" is not connected`,
nodeId: node.id,
severity: 'warning'
}));
}
}
]
}
};
```
## Lifecycle Hooks
Lifecycle hooks let you intercept and cancel user-initiated graph mutations.
Configure them via the `hooks` property on `GraphEditorConfig`.
> **Note:** Hooks only apply to user-initiated actions (palette add, keyboard delete/cut, drag-to-connect).
> Programmatic API calls (`addNode()`, `removeNode()`, `removeEdge()`) do **not** trigger hooks.
### Hook Types
| Hook | Sync/Async | When |
|------|-----------|------|
| `canConnect` | **Sync** | Every mousemove during drag-to-connect and edge reconnection |
| `beforeNodeAdd` | Async | Before a node is added via the palette |
| `beforeNodeRemove` | Async | Before nodes are deleted (Delete/Backspace/Cut) |
| `beforeEdgeAdd` | Async | Before an edge is created via drag-to-connect |
| `beforeEdgeRemove` | Async | Before edges are deleted (Delete/Backspace/Cut) |
All async hooks accept a return type of `boolean | Promise`. Returning (or resolving) `false` cancels the operation. If a hook throws an error, the operation is also cancelled.
### Example
```typescript
import { GraphEditorConfig, LifecycleHooks } from '@utisha/graph-editor';
const hooks: LifecycleHooks = {
// Sync â called on every mousemove, must return immediately
canConnect: (source, target, graph) => {
// No self-loops
if (source.nodeId === target.nodeId) return false;
// No duplicate edges
return !graph.edges.some(
e => e.source === source.nodeId && e.target === target.nodeId
);
},
// Async â can show confirmation dialogs or call a server
beforeNodeRemove: async (nodes, graph) => {
const critical = nodes.filter(n => n.type === 'start');
if (critical.length > 0) {
return confirm('Delete the Start node?');
}
return true;
},
beforeNodeAdd: (type, graph) => {
// Only one Start node allowed
if (type === 'start' && graph.nodes.some(n => n.type === 'start')) {
return false;
}
return true;
},
beforeEdgeAdd: (edge, graph) => true,
beforeEdgeRemove: (edges, graph) => true,
};
const config: GraphEditorConfig = {
// ...nodes, edges, canvas, etc.
hooks
};
```
### Demo
The [live demo](https://fidesit.github.io/graph-editor) includes a **Guards** toggle
that enables workflow-level lifecycle hooks with toast notifications:
- `canConnect` â prevents self-loops, duplicate edges, incoming to Start, outgoing from End
- `beforeNodeAdd` â enforces max 1 Start and 1 End node (with toast notification)
- `beforeNodeRemove` â shows `confirm()` dialog when deleting Start/End nodes
- `beforeEdgeAdd` â limits non-Decision nodes to 2 outgoing edges (with toast)
Toggle it off to compare unrestricted editing.
## Development
```bash
# Clone the repository
git clone https://github.com/fidesit/graph-editor.git
cd graph-editor
# Install dependencies
npm install
# Build the library
npm run build
# Run the demo app
npm run start
# Run tests
npm test
```
## Roadmap
### Completed
- [x] ~~Custom node components via `foreignObject`~~ â Template injection + `ngComponentOutlet`
- [x] ~~Context menus~~ â Event emits on right-click (see demo for example UI)
- [x] ~~Multi-select~~ â Box selection (Shift+drag) and Ctrl+Click toggle
- [x] ~~Keyboard shortcuts~~ â Delete, arrows, Escape, Undo/Redo
- [x] ~~Undo/Redo~~ â Ctrl+Z / Ctrl+Y with full history
- [x] ~~Custom node images~~ â Use `imageUrl` in node data
- [x] ~~Custom SVG icons~~ â Define icons with `iconSvg` property
- [x] ~~Node resize~~ â Drag corner handle with Hand tool
- [x] ~~Text wrapping~~ â Labels wrap and auto-size within node bounds
- [x] ~~Comprehensive theming~~ â Full ThemeConfig with 7 sub-interfaces + CSS custom properties bridge
### Interaction & Editing
- [x] ~~Copy/paste~~ â Ctrl+C/V/X to duplicate or cut nodes with their internal edges
- [x] ~~Snap guides~~ â Alignment lines when dragging near another node's edge/center (also during resize)
- [x] ~~Drag-to-connect~~ â Select node to reveal ports, drag from port to connect (replaces line tool)
- [ ] Edge labels â Clickable, editable text on edges (conditions, weights, transition names)
- [x] ~~Edge waypoints~~ â Ctrl+click to add draggable waypoints on edges for manual routing bends
- [ ] Group/collapse â Select multiple nodes and group into a collapsible container node
### Navigation & Visualization
- [ ] Minimap â Small overview panel with viewport indicator
- [ ] Search/filter â Ctrl+F to find nodes by label/type, dim non-matching ones
- [ ] Heatmap overlay â Color nodes by a numeric metric using `overlayData`
- [ ] Edge animation â Animated dashes flowing along edges to show data direction/activity
- [ ] Zoom to selection â Double-click a node to zoom and center on it
### Data & Export
- [ ] Export as image â SVG/PNG export of the current canvas
- [ ] Import/export JSON â Toolbar button or API to serialize/deserialize the graph
- [ ] Clipboard integration â Paste graph JSON from clipboard to import subgraphs
### Validation & Feedback
- [ ] Port-based connections with type checking â Color-coded ports that only accept compatible connections
- [ ] Inline validation badges â Show error/warning icons directly on offending nodes
- [ ] Max connections indicator â Show remaining connection slots on ports
### Developer Experience
- [x] ~~Event hooks~~ â `beforeNodeRemove`, `beforeEdgeAdd`, `canConnect` lifecycle guards with async support
- [ ] Custom toolbar items â Inject custom buttons/components into the toolbar via template projection
- [ ] Readonly per-node â Lock individual nodes from editing while others remain editable
- [ ] Touch/mobile support â Pinch-to-zoom, touch drag, long-press for context menu
- [ ] Accessibility improvements
### Layout
- [x] ~~Multiple layout algorithms~~ â Hierarchical (dagre TB/LR), force-directed, and tree layouts with dropdown switcher
- [ ] Incremental layout â Re-layout only the neighborhood of a changed node
- [ ] Swim lanes â Horizontal/vertical partitions that nodes snap into
## Contributing
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
## License
[MIT](LICENSE) Š Utisha / Fides IT