Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ __tmp__*

# Kiro
.kiro

# Codex
.codex
.agents
AGENTS.md
51 changes: 50 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# ng-diagram
# ngDiagram

[![npm version](https://badge.fury.io/js/ng-diagram.svg)](https://badge.fury.io/js/ng-diagram)
[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202-blue)](https://opensource.org/license/apache-2-0)

A robust Angular library for building interactive diagrams, node-based editors, and visual programming interfaces. Designed with Angular and TypeScript, it offers a complete toolkit to create sophisticated, customizable, and high-performance diagramming applications.

Unlike generic diagramming libraries, **ng-diagram** is Angular-first - built on Angular signals and templates for seamless integration and performance.
Unlike generic diagramming libraries, **ngDiagram** is Angular-first - built on Angular signals and templates for seamless integration and performance.

![Interactive ng-diagram editor: custom Angular components as nodes — including a live chart — with groups, edge drawing, rotation, and zooming](https://raw.githubusercontent.com/synergycodes/ng-diagram/main/.github/assets/ngdiagram-hero.gif)
![Interactive ngDiagram editor: custom Angular components as nodes — including a live chart — with groups, edge drawing, rotation, and zooming](https://raw.githubusercontent.com/synergycodes/ng-diagram/main/.github/assets/ngdiagram-hero.gif)

## ✨ Features

Expand All @@ -27,7 +27,7 @@ Unlike generic diagramming libraries, **ng-diagram** is Angular-first - built on

## 📚 What You Can Build

With ng-diagram, you can create:
With ngDiagram, you can create:

- **Flow Diagrams**: Process flows, decision trees, and workflow visualizations
- **Node-Based Editors**: Visual programming interfaces and data flow editors
Expand All @@ -40,11 +40,21 @@ See our [Templates](#-templates) for production-ready examples you can fork and

## 🎮 Try the Demo

See ng-diagram in action: **[Live Demo](https://synergycodes.github.io/ng-diagram-demo/)** | **[Source Code](https://github.com/synergycodes/ng-diagram-demo)**
See ngDiagram in action: **[Live Demo](https://synergycodes.github.io/ng-diagram-demo/)** | **[Source Code](https://github.com/synergycodes/ng-diagram-demo)**

## 🎓 Video Course

Learn ngDiagram by building an Angular workflow builder from scratch — a step-by-step video series by the team:

1. **[Angular workflow builder from scratch | ngDiagram Ep. 1: Project setup](https://youtu.be/Ck_lPtyKE_Q)**
2. **[Angular signals, zero glue code | ngDiagram Ep. 2: Properties panel](https://youtu.be/8CdH9Rk99KE)**
3. **[Custom nodes in an Angular workflow builder | ngDiagram Ep. 3: Node types](https://youtu.be/UzK0h9wcyEo)**

The series is ongoing — new episodes are in production. Follow the [Synergy Codes YouTube channel](https://www.youtube.com/@synergycodes) to catch the next one.

## 🧩 Templates

Production-ready starter kits built with ng-diagram. Fork, customize, ship.
Production-ready starter kits built with ngDiagram. Fork, customize, ship.

| Template | Description | Demo | Source |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------ |
Expand Down Expand Up @@ -175,7 +185,7 @@ The library ships **components** for the diagram canvas, backgrounds, ports, pal

## 👩‍💻 About the Creators

ng-diagram is built and maintained by [**Synergy Codes**](https://www.synergycodes.com) - a team of developers who’ve spent **over a decade designing and delivering diagramming solutions** for clients worldwide.
ngDiagram is built and maintained by [**Synergy Codes**](https://www.synergycodes.com) - a team of developers who’ve spent **over a decade designing and delivering diagramming solutions** for clients worldwide.

We are continuously distilling everything we know about building interactive diagrams, editors, and visual tools into this library. Our goal is simple: to empower Angular developers to create diagramming applications faster, easier, and with confidence.

Expand All @@ -197,7 +207,7 @@ The documentation includes:

## 🤖 MCP Server

Use AI assistants like Claude, Cursor, or Windsurf to search ng-diagram docs and API directly from your editor. No browser needed.
Use AI assistants like Claude, Cursor, or Windsurf to search ngDiagram docs and API directly from your editor. No browser needed.

```json
{
Expand Down Expand Up @@ -227,7 +237,7 @@ This project is licensed under the Apache 2.0 License - see the [LICENSE](https:
- **Documentation**: [https://www.ngdiagram.dev/docs](https://www.ngdiagram.dev/docs)
- **GitHub**: [https://github.com/synergycodes/ng-diagram](https://github.com/synergycodes/ng-diagram)
- **NPM**: [https://www.npmjs.com/package/ng-diagram](https://www.npmjs.com/package/ng-diagram)
- **MCP Server**: [@ng-diagram/mcp](https://www.npmjs.com/package/@ng-diagram/mcp) - let AI assistants search ng-diagram docs and API
- **MCP Server**: [@ng-diagram/mcp](https://www.npmjs.com/package/@ng-diagram/mcp) - let AI assistants search ngDiagram docs and API
- **Website**: [https://www.ngdiagram.dev](https://www.ngdiagram.dev)

## 🆘 Support
Expand Down
12 changes: 10 additions & 2 deletions apps/angular-demo/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<div class="diagram-container">
<div class="diagram-container" [class.reveal-hidden-ghosts]="revealHiddenGhosts()">
<app-palette [model]="paletteModel" />
<ng-diagram
[nodeTemplateMap]="nodeTemplateMap"
[edgeTemplateMap]="edgeTemplateMap"
Expand All @@ -13,6 +14,8 @@
(nodeRotateEnded)="onNodeRotateEnded($event)"
(edgeDrawn)="onEdgeDrawn($event)"
(edgeDrawEnded)="onEdgeDrawEnded($event)"
(edgeRelinkStarted)="onEdgeRelinkStarted($event)"
(edgeRelinkEnded)="onEdgeRelinkEnded($event)"
(clipboardPasted)="onClipboardPasted($event)"
(nodeResized)="onNodeResized($event)"
(nodeResizeStarted)="onNodeResizeStarted($event)"
Expand All @@ -31,17 +34,22 @@
<app-measurement-tests (exit)="exitMeasurementTest()" />
} @else if (awaitableTestMode()) {
<app-awaitable-tests (exit)="exitAwaitableTest()" />
} @else if (hiddenElementsDemoMode()) {
<app-hidden-elements-toolbar (exit)="exitHiddenElementsDemo()" (revealGhosts)="revealHiddenGhosts.set($event)" />
} @else if (relinkingTestMode()) {
<app-relinking-toolbar (exit)="exitRelinkingTest()" (resetScene)="resetRelinkingScene()" />
} @else {
<app-toolbar
(enterBatchTest)="enterBatchTest()"
(measurementTestEnter)="enterMeasurementTest()"
(awaitableTestEnter)="enterAwaitableTest()"
(hiddenElementsDemoEnter)="enterHiddenElementsDemo()"
(relinkingTestEnter)="enterRelinkingTest()"
(testVirtualizationClick)="enableVirtualizationTest()"
(reinitializeModelClick)="onReinitializeModel()"
(saveModelClick)="onSaveModel()"
(loadModelClick)="onLoadModel()"
(simulateModelDownloadClick)="onSimulateModelDownload()"
/>
}
<app-palette [model]="paletteModel" />
</div>
104 changes: 100 additions & 4 deletions apps/angular-demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
DiagramInitEvent,
EdgeDrawEndedEvent,
EdgeDrawnEvent,
EdgeRelinkEndedEvent,
EdgeRelinkStartedEvent,
GroupMembershipChangedEvent,
initializeModel,
MinimapNodeStyle,
Expand All @@ -17,6 +19,7 @@ import {
NgDiagramModelService,
NgDiagramNodeTemplateMap,
NgDiagramPaletteItem,
NgDiagramService,
NodeDragEndedEvent,
NodeDragStartedEvent,
NodeResizedEvent,
Expand All @@ -40,18 +43,23 @@ import { defaultModel } from './data/default-model';
import { downloadedModel } from './data/downloaded-model';
import { generateDynamicPortsTestModel } from './data/dynamic-ports-test-model';
import { generateModel } from './data/generate-model';
import { createHiddenElementsModel } from './data/hidden-elements-model';
import { nodeTemplateMap } from './data/node-template';
import { paletteModel } from './data/palette-model';
import { createRelinkingModel } from './data/relinking-model';
import { virtualizationConfigOverrides, virtualizationTestConfig } from './data/virtualization-test.config';
import { ButtonEdgeComponent } from './edge-template/button-edge/button-edge.component';
import { CustomPolylineEdgeComponent } from './edge-template/custom-polyline-edge/custom-polyline-edge.component';
import { DashedEdgeComponent } from './edge-template/dashed-edge/dashed-edge.component';
import { DefaultLabelledEdgeComponent } from './edge-template/default-labelled-edge/default-labelled-edge.component';
import { HiddenLabelEdgeComponent } from './edge-template/hidden-label-edge/hidden-label-edge.component';
import { LabelledEdgeComponent } from './edge-template/labelled-edge/labelled-edge.component';
import { MeasurementTestsComponent } from './measurement-tests/measurement-tests.component';
import { ImageMinimapNodeComponent } from './minimap-node-template/image-minimap-node/image-minimap-node.component';
import { PaletteComponent } from './palette/palette.component';
import { BatchTestToolbarComponent } from './toolbar/batch-test-toolbar.component';
import { HiddenElementsToolbarComponent } from './toolbar/hidden-elements-toolbar.component';
import { RelinkingToolbarComponent } from './toolbar/relinking-toolbar.component';
import { ToolbarComponent } from './toolbar/toolbar.component';

const LOCAL_STORAGE_KEY = 'ng-diagram-demo';
Expand All @@ -63,6 +71,8 @@ const LOCAL_STORAGE_KEY = 'ng-diagram-demo';
imports: [
ToolbarComponent,
BatchTestToolbarComponent,
HiddenElementsToolbarComponent,
RelinkingToolbarComponent,
MeasurementTestsComponent,
AwaitableTestsComponent,
PaletteComponent,
Expand All @@ -76,6 +86,7 @@ const LOCAL_STORAGE_KEY = 'ng-diagram-demo';
export class AppComponent {
private readonly injector = inject(Injector);
private readonly modelService = inject(NgDiagramModelService);
private readonly ngDiagramService = inject(NgDiagramService);

paletteModel: NgDiagramPaletteItem[] = paletteModel;
nodeTemplateMap: NgDiagramNodeTemplateMap = nodeTemplateMap;
Expand All @@ -85,16 +96,17 @@ export class AppComponent {
['labelled-edge', LabelledEdgeComponent],
['dashed-edge', DashedEdgeComponent],
['default-labelled-edge', DefaultLabelledEdgeComponent],
['hidden-label-edge', HiddenLabelEdgeComponent],
]);

minimapNodeTemplateMap = new NgDiagramMinimapNodeTemplateMap([['image', ImageMinimapNodeComponent]]);

config: NgDiagramConfig = {
zoom: {
max: 2,
// Left at the library default (10x) so high-zoom rendering is reachable here.
zoomToFit: {
onInit: true,
padding: [50, 50, 100, 350],
padding: [50, 50, 100, 50],
},
},
resize: {
Expand All @@ -119,6 +131,17 @@ export class AppComponent {
},
linking: {
selectNodeOnPortPress: false,
// Every selected edge shows grabbable endpoint handles unless its own
// `relinkable` says otherwise — drag one to reconnect it to another
// port or drop it on empty canvas to detach it.
defaultRelinkable: true,
},
// Dangling edges: a link drawn onto empty canvas is kept (with a free
// endpoint), and deleting a node detaches its edges instead of deleting
// them. Delete an edge explicitly by selecting it.
danglingEdges: {
enabled: true,
detachOnNodeDelete: true,
},
shortcuts: configureShortcuts([
{
Expand Down Expand Up @@ -169,6 +192,60 @@ export class AppComponent {

private savedModelData: Partial<{ nodes: Node[]; edges: Edge[] }> | null = null;

hiddenElementsDemoMode = signal(false);

/** Hidden-elements demo: reveals hidden elements as translucent ghosts. */
revealHiddenGhosts = signal(false);

enterHiddenElementsDemo(): void {
this.savedModelData = this.modelData();
this.hiddenElementsDemoMode.set(true);
this.modelData.set(createHiddenElementsModel());
}

exitHiddenElementsDemo(): void {
this.hiddenElementsDemoMode.set(false);
this.revealHiddenGhosts.set(false);
if (this.savedModelData) {
this.modelData.set(this.savedModelData);
this.savedModelData = null;
}
}

relinkingTestMode = signal(false);

enterRelinkingTest(): void {
this.savedModelData = this.modelData();
this.relinkingTestMode.set(true);
this.modelData.set(createRelinkingModel());
}

exitRelinkingTest(): void {
this.relinkingTestMode.set(false);
// The panel writes its own linking/dangling rules into the live config.
// A model swap rebuilds the diagram core from the `[config]` input, which
// already carries these values; this call restores them even when there is
// no saved model to swap back to.
this.ngDiagramService.updateConfig({
linking: { defaultRelinkable: true, validateConnection: () => true },
danglingEdges: {
enabled: true,
detachOnNodeDelete: true,
shouldKeepOnDrop: () => true,
shouldDetachOnNodeDelete: () => true,
},
});
if (this.savedModelData) {
this.modelData.set(this.savedModelData);
this.savedModelData = null;
}
}

/** Rebuilds the relinking scene from scratch, keeping the mode open. */
resetRelinkingScene(): void {
this.modelData.set(createRelinkingModel());
}

enterBatchTest(): void {
this.savedModelData = this.modelData();
this.batchTestMode.set(true);
Expand Down Expand Up @@ -249,18 +326,37 @@ export class AppComponent {
});
}

onEdgeRelinkStarted(event: EdgeRelinkStartedEvent): void {
console.log('Edge Relink Started:', { edge: event.edge.id, end: event.end });
}

onEdgeRelinkEnded(event: EdgeRelinkEndedEvent): void {
console.log('Edge Relink Ended:', {
edge: event.edge.id,
end: event.end,
success: event.success,
previousNode: event.previousNode?.id,
previousPort: event.previousPort,
target: event.target?.id,
targetPort: event.targetPort,
reason: event.reason,
dropPosition: event.dropPosition,
});
}

onEdgeDrawEnded(event: EdgeDrawEndedEvent): void {
if (event.success) {
console.log('Edge Draw Ended (success):', {
edge: event.edge!.id,
source: event.source.id,
// source is undefined for draws started from empty canvas
source: event.source?.id,
target: event.target?.id,
sourcePort: event.sourcePort,
targetPort: event.targetPort,
});
} else {
console.log('Edge Draw Ended (cancelled):', {
source: event.source.id,
source: event.source?.id,
sourcePort: event.sourcePort,
reason: event.reason,
dropPosition: event.dropPosition,
Expand Down
Loading
Loading