Layout Integration
To perform layouts on in ngDiagram, integrate with external layout libraries to automatically position and arrange diagram elements.
The implementation below shows:
- How to use built-in layout using
NgDiagramService
- How to use external layout libraries like ELK.js
- How to use the
selectionMoved
event to detect when nodes are moved by the user - How to manage edge routing modes (
manual
vsauto
):- Setting edges to
manual
mode to preserve custom layout points from ELK - Resetting edges to
auto
mode when connected nodes are moved manually
- Setting edges to
- How to use batch update methods (
updateNodes
andupdateEdges
) for better performance
Implementation Example
Section titled “Implementation Example”import { type Edge, type Node } from 'ng-diagram';
export const diagramModel: { nodes: Node[]; edges: Edge[] } = { nodes: [ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Root' } }, { id: '2', position: { x: 567, y: 122 }, data: { label: 'B' } }, { id: '3', position: { x: 320, y: 301 }, data: { label: 'A' } }, { id: '4', position: { x: 412, y: 254 }, data: { label: 'B 1' } }, { id: '5', position: { x: 611, y: 487 }, data: { label: 'B 2' } }, { id: '6', position: { x: 97, y: 178 }, data: { label: 'A 1' } }, { id: '7', position: { x: 235, y: 85 }, data: { label: 'A 2' } }, { id: '8', position: { x: 528, y: 320 }, data: { label: 'B 1' } }, { id: '9', position: { x: 378, y: 465 }, data: { label: 'B 2' } }, { id: '10', position: { x: 165, y: 387 }, data: { label: 'A 1.1' } }, ], edges: [ { id: 'e1-2', source: '1', sourcePort: 'port-right', targetPort: 'port-left', target: '2', data: {}, }, { id: 'e1-3', source: '1', sourcePort: 'port-right', targetPort: 'port-left', target: '3', data: {}, }, { id: 'e2-4', source: '2', sourcePort: 'port-right', targetPort: 'port-left', target: '4', data: {}, }, { id: 'e2-5', source: '2', sourcePort: 'port-right', targetPort: 'port-left', target: '5', data: {}, }, { id: 'e3-6', source: '3', sourcePort: 'port-right', targetPort: 'port-left', target: '6', data: {}, }, { id: 'e3-7', source: '3', sourcePort: 'port-right', targetPort: 'port-left', target: '7', data: {}, }, { id: 'e5-8', source: '5', sourcePort: 'port-right', targetPort: 'port-left', target: '8', data: {}, }, { id: 'e5-9', source: '5', sourcePort: 'port-right', targetPort: 'port-left', target: '9', data: {}, }, { id: 'e6-10', source: '6', sourcePort: 'port-right', targetPort: 'port-left', target: '10', data: {}, }, ],};
import '@angular/compiler';
import { Component, inject } from '@angular/core';import { initializeModel, NgDiagramComponent, NgDiagramModelService, type NgDiagramConfig, type SelectionMovedEvent,} from 'ng-diagram';import { diagramModel } from './data';import { LayoutButtonsComponent } from './layout-buttons.component';
@Component({ selector: 'diagram-component', imports: [NgDiagramComponent, LayoutButtonsComponent], template: ` <ng-diagram [model]="model" [config]="config" (selectionMoved)="onSelectionMoved($event)" /> <layout-buttons /> `, styles: ` :host { flex: 1; position: relative; display: flex; height: 100%;
.coordinates { display: flex; } } `,})export class DiagramComponent { private modelService = inject(NgDiagramModelService);
model = initializeModel({ metadata: { viewport: { x: 100, y: 80, scale: 0.5 }, }, nodes: diagramModel.nodes, edges: diagramModel.edges, });
config: NgDiagramConfig = { edgeRouting: { defaultRouting: 'orthogonal' }, };
// 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 } from 'ng-diagram';import { performLayout } from './perform-layout';
@Component({ imports: [], selector: 'layout-buttons', template: ` <button (click)="onCustomLayout()">Perform custom layout</button> `, styles: ` :host { position: absolute; width: 100%; height: 2rem; gap: 1rem; bottom: 1rem; left: 0;
display: flex; justify-content: center; align-items: center; } `,})export class LayoutButtonsComponent { diagramService = inject(NgDiagramService); modelService = inject(NgDiagramModelService);
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); } }}
import '@angular/compiler';import { Component } from '@angular/core';import { provideNgDiagram } from 'ng-diagram';import { DiagramComponent } from './diagram.component';
@Component({ selector: 'layout-integration-wrapper', imports: [DiagramComponent], providers: [provideNgDiagram()], template: ` <diagram-component /> `, styles: [ ` :host { flex: 1; display: flex; position: relative; height: 100%; } `, ],})export class LayoutIntegrationWrapperComponent {}
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': '50', 'spacing.nodeNode': '50', 'layered.spacing.edgeNodeBetweenLayers': '50', 'layered.spacing.nodeNodeBetweenLayers': '50', 'edgeLabels.sideSelection': 'ALWAYS_UP', 'layering.strategy': 'COFFMAN_GRAHAM', '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];}