Layout Integration Example
This example demonstrates how to integrate external layout libraries and use built-in layout features in ngDiagram.
import { type Edge, type Node } from 'ng-diagram';import { NodeTemplateType } from './types';
export const diagramModel: { nodes: Node[]; edges: Edge[] } = { nodes: [ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Root' }, type: NodeTemplateType.CustomNodeType, }, { id: '2', position: { x: 567, y: 122 }, data: { label: 'B' }, type: NodeTemplateType.CustomNodeType, }, { id: '3', position: { x: 320, y: 301 }, data: { label: 'A' }, type: NodeTemplateType.CustomNodeType, }, { id: '4', position: { x: 611, y: 487 }, data: { label: 'B 2' }, type: NodeTemplateType.CustomNodeType, }, { id: '5', position: { x: 97, y: 178 }, data: { label: 'A 1' }, type: NodeTemplateType.CustomNodeType, }, { id: '6', position: { x: 235, y: 85 }, data: { label: 'A 2' }, type: NodeTemplateType.CustomNodeType, }, { id: '7', position: { x: 528, y: 320 }, data: { label: 'B 2' }, type: NodeTemplateType.CustomNodeType, }, { id: '8', position: { x: 165, y: 387 }, data: { label: 'A 1.1' }, type: NodeTemplateType.CustomNodeType, }, ], edges: [ { id: 'e1-2', source: '1', sourcePort: 'port-right', targetPort: 'port-left', target: '2', data: {}, }, { id: 'e1-3', source: '1', sourcePort: 'port-bottom', targetPort: 'port-left', target: '3', data: {}, }, { id: 'e2-4', source: '2', sourcePort: 'port-right', targetPort: 'port-left', target: '4', data: {}, }, { id: 'e3-5', source: '3', sourcePort: 'port-bottom', targetPort: 'port-left', target: '5', data: {}, }, { id: 'e3-6', source: '3', sourcePort: 'port-right', targetPort: 'port-left', target: '6', data: {}, }, { id: 'e4-7', source: '4', sourcePort: 'port-right', targetPort: 'port-left', target: '7', data: {}, }, { id: 'e5-8', source: '5', sourcePort: 'port-right', targetPort: 'port-left', target: '8', data: {}, }, ],};import '@angular/compiler';
import { Component, inject } from '@angular/core';import { initializeModel, NgDiagramBackgroundComponent, NgDiagramComponent, NgDiagramModelService, NgDiagramNodeTemplateMap, provideNgDiagram, type NgDiagramConfig, type SelectionMovedEvent,} from 'ng-diagram';import { diagramModel } from './data';import { LayoutButtonsComponent } from './layout-buttons/layout-buttons.component';import { NodeComponent } from './node/node.component';import { NodeTemplateType } from './types';
@Component({ imports: [ NgDiagramComponent, NgDiagramBackgroundComponent, LayoutButtonsComponent, ], template: ` <div class="not-content diagram"> <ng-diagram [model]="model" [config]="config" (selectionMoved)="onSelectionMoved($event)" [nodeTemplateMap]="nodeTemplateMap" > <ng-diagram-background /> </ng-diagram> <layout-buttons /> </div> `, styleUrl: './diagram.component.scss', providers: [provideNgDiagram()],})export class DiagramComponent { nodeTemplateMap = new NgDiagramNodeTemplateMap([ [NodeTemplateType.CustomNodeType, NodeComponent], ]); private modelService = inject(NgDiagramModelService);
model = initializeModel({ nodes: diagramModel.nodes, edges: diagramModel.edges, });
config: NgDiagramConfig = { zoom: { zoomToFit: { onInit: true, padding: 80, }, }, };
// When user manually moves nodes, edges in manual routing mode should be // reset to auto so they follow the node movement and re-route appropriately onSelectionMoved(event: SelectionMovedEvent): void { const movedNodeIds = new Set(event.nodes.map((n) => n.id)); const edges = this.modelService.edges();
const edgesToUpdate = edges .filter( (edge) => (movedNodeIds.has(edge.source) || movedNodeIds.has(edge.target)) && edge.routingMode === 'manual' ) .map((edge) => ({ id: edge.id, routingMode: 'auto' as const, }));
if (edgesToUpdate.length > 0) { this.modelService.updateEdges(edgesToUpdate); } }}import { Component, computed, inject } from '@angular/core';import { NgDiagramModelService, NgDiagramService, NgDiagramViewportService,} from 'ng-diagram';import { performLayout } from '../perform-layout';
@Component({ imports: [], selector: 'layout-buttons', template: ` <button class="btn" (click)="onCustomLayout()"> Perform custom layout </button> `, styleUrls: ['./layout-buttons.component.scss'],})export class LayoutButtonsComponent { diagramService = inject(NgDiagramService); modelService = inject(NgDiagramModelService); viewportService = inject(NgDiagramViewportService);
nodes = computed(() => this.modelService.getModel().getNodes()); edges = computed(() => this.modelService.getModel().getEdges());
async onCustomLayout() { const { nodes: finalNodes, edges: finalEdges } = await performLayout( this.nodes(), this.edges() );
if (finalNodes.length > 0) { this.modelService.updateNodes(finalNodes); }
if (finalEdges.length > 0) { this.modelService.updateEdges(finalEdges); }
setTimeout(() => { this.viewportService.zoomToFit(); }, 1); }}import { Component, input } from '@angular/core';import { NgDiagramBaseNodeTemplateComponent, NgDiagramPortComponent, type NgDiagramNodeTemplate, type Node,} from 'ng-diagram';
@Component({ imports: [NgDiagramPortComponent, NgDiagramBaseNodeTemplateComponent], templateUrl: './node.component.html', styleUrls: ['./node.component.scss'], host: { '[class.ng-diagram-port-hoverable-over-node]': 'true', },})export class NodeComponent implements NgDiagramNodeTemplate { node = input.required<Node>();}import ELK, { type ElkExtendedEdge, type ElkNode } from 'elkjs';import { type Edge, type Node } from 'ng-diagram';
const elk = new ELK();const layoutOptions = { 'elk.algorithm': 'layered', 'elk.direction': 'RIGHT', 'elk.layered.nodePlacement.strategy': 'SIMPLE', 'elk.layered.cycleBreaking.strategy': 'DEPTH_FIRST', 'elk.layered.spacing.edgeNodeBetweenLayers': '30', 'spacing.edgeNode': '70', 'spacing.nodeNode': '50', 'layered.spacing.edgeNodeBetweenLayers': '50', 'layered.spacing.nodeNodeBetweenLayers': '50', 'edgeLabels.sideSelection': 'ALWAYS_UP', 'layering.strategy': 'NETWORK_SIMPLEX', 'nodePlacement.strategy': 'BRANDES_KOEPF',};
export async function performLayout(nodes: Node[], edges: Edge[]) { const nodesToLayout = nodes.map( ({ id: nodeId, size, measuredPorts }): ElkNode => ({ id: nodeId, ...size, layoutOptions: { portConstraints: 'FIXED_POS', }, ports: measuredPorts?.map( ({ id: portId, position, size: portSize }: any) => ({ id: `${nodeId}:${portId}`, ...portSize, ...position, }) ), }) );
const graph: ElkNode = { id: 'root', layoutOptions, children: nodesToLayout, edges: edges.map(({ id, source, target, sourcePort, targetPort }) => { const sourceWithPort = sourcePort ? `${source}:${sourcePort}` : source; const targetWithPort = targetPort ? `${target}:${targetPort}` : target;
return { id, sources: [sourceWithPort], targets: [targetWithPort], }; }), };
const { children: laidOutNodes, edges: laidOutEdges } = await elk.layout(graph);
const updatedNodes: Node[] = nodes.map((node) => { const { position: { x: baseX, y: baseY }, } = node;
const { x = baseX, y = baseY } = laidOutNodes?.find( ({ id }: any) => id === node.id ) ?? { x: baseX, y: baseY, };
return { ...node, position: { x, y }, }; });
const updatedEdges: Edge[] = edges.map((edge) => { const elkEdge = laidOutEdges?.find(({ id }: any) => id === edge.id); if (!elkEdge) { return edge; }
const points = getLayoutPoints(elkEdge);
return { ...edge, // Set routing mode to manual to disable automatic routing from ngDiagram // and use the exact points calculated by ELK layout engine routingMode: 'manual', points, }; });
return { nodes: updatedNodes, edges: updatedEdges };}
function getLayoutPoints(elkEdge: ElkExtendedEdge) { if (!elkEdge.sections?.length) return [];
const section = elkEdge.sections[0];
return [section.startPoint, ...(section.bendPoints ?? []), section.endPoint];}export enum NodeTemplateType { CustomNodeType = 'customNodeType',}<ng-diagram-base-node-template [node]="node"> <span class="node-label">{{ node().data.label }}</span></ng-diagram-base-node-template><ng-diagram-port id="port-bottom" type="both" side="bottom" /><ng-diagram-port id="port-top" type="both" side="top" />.diagram { position: relative; display: flex; height: var(--ng-diagram-height); border: var(--ng-diagram-border); margin-top: 0;}:host { position: absolute; top: 1rem; right: 1rem; z-index: 1; display: flex; padding: 1rem; background-color: var(--ngd-node-bg-primary-default); border: var(--ng-diagram-border);
button { margin-top: 0; }}:host { .node-label { width: 200px; height: 32px; display: flex; align-items: center; justify-content: center; }}Additional Explanation
Section titled “Additional Explanation”Key Concepts
Section titled “Key Concepts”- External Layout Libraries: Integrate solutions like ELK.js for advanced layouts. ELK.js assigns
positionto nodes and generates edge paths automatically. - User Interaction Detection: Use the
selectionMovedevent to detect manual node movement. - Edge Routing Modes:
- Batch Updates: Use
updateNodesandupdateEdgesfor efficient diagram updates.