Custom Edge
This example demonstrates how to create custom edges in ngDiagram with unique visual styles and interactive elements. It showcases three different edge types:
-
Default Edge:
- a standard edge parametrized through model data
- enhanced with a custom circular arrowhead marker defined in SVG
-
Labeled Edge:
- a custom edge with adjusted stroke width dynamically changing on selection
- features interactive button label positioned at its midpoint
-
Sinusoid Edge:
- a custom wave-shaped edge with adjusted stroke color
- renders a sinusoidal curve between nodes
The example illustrates key concepts including:
- Creating custom edge components that implement
NgDiagramEdgeTemplate
- Adjusting parameters of
NgDiagramBaseEdgeComponent
in custom edges - Registering edge types using the
edgeTemplateMap
- Defining custom SVG markers for arrowheads
- Adding interactive elements with
BaseEdgeLabelComponent
- Generating complex path calculations for non-standard edge shapes
import { Component } from '@angular/core';
@Component({ selector: 'circle-arrowhead', template: ` <svg height="0" width="0"> <defs> <marker id="circle-arrowhead" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto" > <circle cx="5" cy="5" r="4" fill="red" /> </marker> </defs> </svg> `,})export class CircleArrowheadComponent {}
import '@angular/compiler';
import { Component } from '@angular/core';import { initializeModel, NgDiagramComponent, NgDiagramEdgeTemplateMap, provideNgDiagram,} from 'ng-diagram';import { CircleArrowheadComponent } from './circle-arrowhead.component';import { LabeledEdgeComponent } from './labeled-edge.component';import { SinusoidEdgeComponent } from './sinusoid-edge.component';
@Component({ imports: [NgDiagramComponent, CircleArrowheadComponent], providers: [provideNgDiagram()], template: ` <ng-diagram [model]="model" [edgeTemplateMap]="edgeTemplateMap" /> <circle-arrowhead /> `, styleUrls: ['diagram.scss', 'edges.scss'],})export class Diagram { edgeTemplateMap = new NgDiagramEdgeTemplateMap([ ['labeled', LabeledEdgeComponent], ['sinusoid', SinusoidEdgeComponent], ]);
model = initializeModel({ metadata: { viewport: { x: -30, y: -30, scale: 0.7 }, }, nodes: [ { id: '1', position: { x: 200, y: 100 }, data: { label: 'Node 1' }, }, { id: '2', position: { x: 700, y: 200 }, data: { label: 'Node 2' }, }, { id: '3', position: { x: 200, y: 300 }, data: { label: 'Node 3' }, }, { id: '4', position: { x: 700, y: 400 }, data: { label: 'Node 4' }, }, ], edges: [ { id: '1', source: '1', sourcePort: 'port-right', targetPort: 'port-left', target: '2', data: {}, sourceArrowhead: 'circle-arrowhead', }, { id: '2', source: '2', sourcePort: 'port-left', targetPort: 'port-right', target: '3', type: 'labeled', data: {}, }, { id: '3', source: '3', sourcePort: 'port-right', targetPort: 'port-left', target: '4', type: 'sinusoid', data: {}, }, ], });}
import { Component, computed, input } from '@angular/core';import { BaseEdgeLabelComponent, NgDiagramBaseEdgeComponent, type Edge, type NgDiagramEdgeTemplate,} from 'ng-diagram';
const STROKE_WIDTH_DEFAULT = 2;const STROKE_WIDTH_SELECTED = 4;
@Component({ selector: 'labeled-edge', template: `<ng-diagram-base-edge [edge]="customEdge()" stroke="orange" [strokeWidth]="strokeWidth()" > <ng-diagram-base-edge-label id="test-label" positionOnEdge="0.5"> <button style="white-space: nowrap; padding: 4px 8px;" (click)="onButtonClick()" > Click Me </button> </ng-diagram-base-edge-label> </ng-diagram-base-edge> `, imports: [NgDiagramBaseEdgeComponent, BaseEdgeLabelComponent],})export class LabeledEdgeComponent implements NgDiagramEdgeTemplate { edge = input.required<Edge>(); selected = computed(() => this.edge().selected); strokeWidth = computed(() => this.selected() ? STROKE_WIDTH_SELECTED : STROKE_WIDTH_DEFAULT );
onButtonClick() { const edge = this.edge(); alert(`Edge ID: ${edge.id}`); }
customEdge = computed(() => { const edge = this.edge(); const { sourcePosition, targetPosition } = edge;
if (!sourcePosition || !targetPosition) { return edge; }
// Create custom points for the edge path const points = [ { x: sourcePosition.x, y: sourcePosition.y }, { x: targetPosition.x, y: targetPosition.y }, ];
return { ...edge, points, routing: 'polyline', routingMode: 'manual' as const, }; });}
import { Component, computed, input } from '@angular/core';import { NgDiagramBaseEdgeComponent, type Edge, type NgDiagramEdgeTemplate, type Point,} from 'ng-diagram';
@Component({ selector: 'sinusoid-edge', template: `<ng-diagram-base-edge [edge]="customEdge()" stroke="rebeccapurple" targetArrowhead="ng-diagram-arrow" />`, imports: [NgDiagramBaseEdgeComponent],})export class SinusoidEdgeComponent implements NgDiagramEdgeTemplate { edge = input.required<Edge>();
customEdge = computed(() => { const edge = this.edge(); const { sourcePosition, targetPosition } = edge;
if (!sourcePosition || !targetPosition) { return edge; }
const points = this.generateSinusoidPoints(sourcePosition, targetPosition);
return { ...edge, points, routing: 'polyline', routingMode: 'manual' as const, }; });
private generateSinusoidPoints(sourcePosition: Point, targetPosition: Point) { const startX = sourcePosition.x; const startY = sourcePosition.y; const endX = targetPosition.x; const endY = targetPosition.y;
// Calculate distance and angle between points const distance = Math.sqrt( Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2) ); const angle = Math.atan2(endY - startY, endX - startX);
// Sinusoidal wave parameters const amplitude = 20; const frequency = Math.max(2, distance / 100); const segments = Math.max(20, Math.floor(distance / 5));
const points = [{ x: startX, y: startY }];
// Generate sinusoidal curve points for (let i = 1; i < segments; i++) { const t = i / segments;
// Base position along the straight line const baseX = startX + (endX - startX) * t; const baseY = startY + (endY - startY) * t;
// Calculate perpendicular offset using sine wave // Fade out amplitude as we approach the end to ensure smooth connection const fadeFactor = i < segments - 5 ? 1 : (segments - i) / 5; const sineOffset = Math.sin(t * 2 * Math.PI * frequency) * amplitude * fadeFactor; const perpAngle = angle + Math.PI / 2;
const x = baseX + Math.cos(perpAngle) * sineOffset; const y = baseY + Math.sin(perpAngle) * sineOffset;
points.push({ x, y }); }
// Always end exactly at the target position points.push({ x: endX, y: endY });
return points; }}
:host { flex: 1; display: flex; height: 100%;}
:host { --ngd-default-edge-stroke: #cccccc; --ngd-default-edge-stroke-hover: gray; --ngd-default-edge-stroke-selected: blue;}