Configuration-driven visual graph editor for Angular 19+.
- ⚙️ 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
iconSvgproperty - ⬜ 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
ngComponentOutletsupport - 🔀 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
npm install @utisha/graph-editorimport { Component, signal } from '@angular/core';
import { GraphEditorComponent, Graph, GraphEditorConfig } from '@utisha/graph-editor';
@Component({
selector: 'app-my-editor',
standalone: true,
imports: [GraphEditorComponent],
template: `
<graph-editor
[config]="editorConfig"
[graph]="currentGraph()"
(graphChange)="onGraphChange($event)"
/>
`
})
export class MyEditorComponent {
// See configuration below
}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'
}
};currentGraph = signal<Graph>({
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.
}| 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 |
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<any>; // Angular component to render
defaultData: Record<string, any>;
size?: { width: number; height: number };
ports?: PortConfig; // Connection ports
constraints?: NodeConstraints;
}Nodes can display custom images instead of emoji icons. Set imageUrl in defaultData or per-instance in node.data['imageUrl']:
// 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.
Define your own SVG icons using the SvgIconDefinition interface. This allows you to use professional vector icons that match your design system:
import { SvgIconDefinition, NodeTypeDefinition } from '@utisha/graph-editor';
// Define your icon set
const MY_ICONS: Record<string, SvgIconDefinition> = {
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)
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;
};
}| Input | Type | Description |
|---|---|---|
config |
GraphEditorConfig |
Editor configuration (required) |
graph |
Graph |
Current graph data |
readonly |
boolean |
Disable editing |
visualizationMode |
boolean |
Display only mode |
| Output | Type | Description |
|---|---|---|
graphChange |
EventEmitter<Graph> |
Emitted on any graph mutation |
nodeClick |
EventEmitter<GraphNode> |
Node clicked |
nodeDoubleClick |
EventEmitter<GraphNode> |
Node double-clicked |
edgeClick |
EventEmitter<GraphEdge> |
Edge clicked |
edgeDoubleClick |
EventEmitter<GraphEdge> |
Edge double-clicked |
selectionChange |
EventEmitter<SelectionState> |
Selection changed |
validationChange |
EventEmitter<ValidationResult> |
Validation state changed |
contextMenu |
EventEmitter<ContextMenuEvent> |
Right-click on canvas/node/edge |
// Node operations
addNode(type: string, position?: Position): GraphNode;
removeNode(nodeId: string): void;
updateNode(nodeId: string, updates: Partial<GraphNode>): 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<void>;
fitToScreen(padding?: number): void;
zoomTo(level: number): void;
// Validation
validate(): ValidationResult;Customize the editor appearance using CSS custom properties:
: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;
}Add custom validation rules:
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'
}));
}
}
]
}
};# 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-
Custom node components via— Template injection +foreignObjectngComponentOutlet -
Context menus— Event emits on right-click (see demo for example UI) -
Multi-select— Box selection (Shift+drag) and Ctrl+Click toggle -
Keyboard shortcuts— Delete, arrows, Escape, Undo/Redo -
Undo/Redo— Ctrl+Z / Ctrl+Y with full history -
Custom node images— UseimageUrlin node data -
Custom SVG icons— Define icons withiconSvgproperty -
Node resize— Drag corner handle with Hand tool -
Text wrapping— Labels wrap and auto-size within node bounds -
Comprehensive theming— Full ThemeConfig with 7 sub-interfaces + CSS custom properties bridge
-
Copy/paste— Ctrl+C/V/X to duplicate or cut nodes with their internal edges -
Snap guides— Alignment lines when dragging near another node's edge/center (also during resize) -
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)
- Edge waypoints — Draggable midpoints on edges to create manual routing bends
- Group/collapse — Select multiple nodes and group into a collapsible container node
- 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
- 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
- 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
- Event hooks —
beforeNodeRemove,beforeEdgeAddetc. that can cancel operations - 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
-
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
Contributions are welcome! Please read our Contributing Guide for details.
MIT © Utisha / Fides IT

