Skip to main content

Edges & Connections

This guide covers edge routing, markers, labels, connection ports, self-loops, and dangling edges.

Basic edges

Connect two nodes with .edge(fromId, toId). By default, edges are straight lines that stop at the node boundary.

Edge routing

Three routing modes: straight (default), curved, and orthogonal. Add waypoints with .via(x, y).

MethodEffect
.straight()Straight line (default)
.curved()Smooth bezier curve
.orthogonal()Right-angle elbows
.routing(mode)Programmatic: 'straight', 'curved', or 'orthogonal'
.via(x, y)Add a waypoint (chainable)

Waypoint-aware endpoint anchoring

When an edge has waypoints, the boundary anchor on each node is computed toward the nearest waypoint rather than toward the other node's center. Specifically:

  • The source anchor aims toward the first waypoint.
  • The target anchor aims toward the last waypoint.

This is essential for edge bundling: multiple edges that share a convergence waypoint will anchor at the exact same perimeter point on the target node, producing a clean fan-in without crossing artifacts.

When no waypoints are present, endpoint anchoring falls back to the default center-to-center boundary intersection.

tip

Combine waypoints with .curved() routing for smooth bundled edges, or with .straight() for sharp polyline fans.

Edge labels

Place labels at 'start', 'mid', or 'end' positions. Chain multiple .label() calls for multi-label edges (useful for ER diagrams):

Edge styling

Style edges with .stroke(), .fill(), .opacity(), .dashed(), .dotted(), and .dash():

Edge markers (arrowheads)

VizCraft provides 10 marker types. Use .markerEnd(type), .markerStart(type), or the .arrow() shorthand:

Available marker types: none, arrow, arrowOpen, diamond, diamondOpen, circle, circleOpen, square, bar, halfArrow

tip

Marker colors automatically match the edge's stroke color.

Connection ports

Ports let you control exactly where edges attach on a node.

Explicit ports

Define ports with .port(id, offset) on nodes, then target them with .fromPort(id) / .toPort(id) on edges:

Default ports

Every shape has built-in ports you can reference without explicit .port() calls:

// Rectangle default ports: 'top', 'right', 'bottom', 'left'
builder.edge('A', 'B').fromPort('right').toPort('left').arrow();

Equidistant ports

For evenly distributed ports, use getEquidistantPorts:

import { getEquidistantPorts, toNodePorts } from 'vizcraft';

const ports = toNodePorts(
getEquidistantPorts({ kind: 'rect', w: 120, h: 60 }, 8)
);
const node = builder.node('a').at(100, 100).rect(120, 60);
for (const p of ports) node.port(p.id, p.offset, p.direction);

Perimeter anchors

Pin edges to a specific angle with .fromAngle(deg) / .toAngle(deg) (0° = right, 90° = down):

Self-loop edges

Edges from a node to itself render as bezier loops. Control them with .loopSide() and .loopSize():

Dangling edges

A dangling edge has one endpoint at a free coordinate (not attached to a node) — useful for interactive editors showing "in-progress" connections:

MethodEffect
.danglingEdge(id)Start a dangling edge (returns EdgeBuilder)
.from(nodeId)Attach source to a node
.to(nodeId)Attach target to a node
.fromAt(pos)Free source coordinate
.toAt(pos)Free target coordinate

Custom edge path resolver

Override how edge paths are computed with builder.setEdgePathResolver():

builder.setEdgePathResolver((edge, scene, defaultResolver) => {
if (!edge.meta?.customRouting) return defaultResolver(edge, scene);

const from = scene.nodes.find((n) => n.id === edge.from);
const to = scene.nodes.find((n) => n.id === edge.to);
if (!from || !to) return defaultResolver(edge, scene);

const cx = (from.pos.x + to.pos.x) / 2;
const cy = Math.min(from.pos.y, to.pos.y) - 80;
return `M ${from.pos.x} ${from.pos.y} Q ${cx} ${cy} ${to.pos.x} ${to.pos.y}`;
});

Resolve edge geometry

Use resolveEdgeGeometry(scene, edgeId) to get the full rendered geometry of an edge in a single call. It handles node lookup, self-loop detection, port/angle/boundary anchors, waypoints, and routing — no need to orchestrate multiple helpers manually.

import { resolveEdgeGeometry } from 'vizcraft';

const geo = resolveEdgeGeometry(scene, 'edge-1');
if (!geo) return; // edge not found or unresolvable

// SVG path
overlayPath.setAttribute('d', geo.d);

// Label positions
positionToolbar(geo.mid); // midpoint
positionSourceLabel(geo.startLabel); // ~15% along path
positionTargetLabel(geo.endLabel); // ~85% along path

// True boundary anchor points (where the edge exits/enters each node)
drawHandle(geo.startAnchor);
drawHandle(geo.endAnchor);

// Waypoints and self-loop flag
geo.waypoints.forEach(drawWaypointDot);
if (geo.isSelfLoop) {
/* handle loop-specific overlays */
}

Returns null when:

  • The edge id is not found in the scene.
  • A referenced node id does not exist.
  • Both endpoints are missing (no from/to and no fromAt/toAt).

The return type is ResolvedEdgeGeometry, which extends EdgePathResult.

For batch processing, use the lower-level resolveEdgeGeometryFromData(edge, nodesById) to avoid rebuilding the node map on each call:

import { resolveEdgeGeometryFromData } from 'vizcraft';

const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));

for (const edge of scene.edges) {
const geo = resolveEdgeGeometryFromData(edge, nodesById);
if (!geo) continue;
// use geo.d, geo.mid, etc.
}

Found a problem? Open an issue on GitHub.