Skip to content
D
Documentation

Route and label edges

how-to
6 min readUpdated

Use routing, labels and endpoint markers when a diagram needs to distinguish a connection from a crossing, or several relationships between the same nodes. The example below draws a right-angle detour around an obstacle, two crossing wires with a jump-over, three separate parallel lanes, and a self-loop outside its node.

An edge stores intent; the renderer turns that intent into geometry as nodes move. Describe connections with EdgeSpec rather than calculating SVG paths yourself.

1. Describe the routes and their labels

Create edge-data.ts in your browser application. The NodeSpec arrays provide actual endpoints and an obstacle. Every edge has a stable id so you can identify the corresponding live link later.

The first edge pins its endpoints halfway down the right and left sides. Its router chooses the route; its connector rounds the bends. The label sits above the line, the source carries a circle, and the target carries an arrow.

ts
import type { EdgeSpec, NodeSpec, SVGRendererConfig } from '@grafloria/renderer';

export const nodes: NodeSpec[] = [
  { id: 'a', label: 'A', position: { x: 40, y: 100 }, size: { width: 110, height: 60 } },
  { id: 'b', label: 'B', position: { x: 750, y: 100 }, size: { width: 110, height: 60 } },
  { id: 'wall', label: 'Obstacle', position: { x: 400, y: 60 }, size: { width: 120, height: 140 } },
  { id: 'c', label: 'C', position: { x: 40, y: 270 }, size: { width: 110, height: 44 } },
  { id: 'd', label: 'D', position: { x: 750, y: 270 }, size: { width: 110, height: 44 } },
  { id: 'e', label: 'E', position: { x: 40, y: 430 }, size: { width: 110, height: 44 } },
  { id: 'f', label: 'F', position: { x: 750, y: 430 }, size: { width: 110, height: 44 } },
  { id: 'g', label: 'G', position: { x: 40, y: 560 }, size: { width: 110, height: 60 } },
  { id: 'h', label: 'H', position: { x: 480, y: 560 }, size: { width: 110, height: 60 } },
  { id: 'self', label: 'Self', position: { x: 750, y: 560 }, size: { width: 110, height: 60 } },
];

export const edges: EdgeSpec[] = [
  {
    id: 'detour', source: 'a', target: 'b',
    sourceHandle: 'right@50%', targetHandle: 'left@50%',
    type: 'orthogonal', router: 'orthogonal', connector: 'rounded',
    label: 'depends on', labelPlacement: 'above',
    labelStyle: { color: '#15803d', fontSize: 12 },
    style: {
      stroke: '#15803d', strokeWidth: 2,
      arrowTail: { type: 'circle', size: 6, filled: true },
      arrowHead: { type: 'arrow', size: 10, filled: true },
    },
  },
  {
    id: 'cf', source: 'c', target: 'f', type: 'direct',
    sourceHandle: 'right', targetHandle: 'left',
    style: { jumpPoints: { enabled: true, size: 10, detectMode: 'all' } },
  },
  {
    id: 'ed', source: 'e', target: 'd', type: 'direct',
    sourceHandle: 'right', targetHandle: 'left',
    style: { jumpPoints: { enabled: true, size: 10, detectMode: 'all' } },
  },
  { id: 'p1', source: 'g', target: 'h', type: 'direct', label: 'request' },
  { id: 'p2', source: 'g', target: 'h', type: 'direct', label: 'response' },
  { id: 'p3', source: 'g', target: 'h', type: 'direct', label: 'audit' },
  {
    id: 'loop', source: 'self', target: 'self', label: 'retry',
    style: { selfLoop: { side: 'top', size: 40 } },
  },
];

export const rendererConfig: Partial<SVGRendererConfig> = {
  parallelLinks: true,
  parallelSpacing: 24,
  jumpOwnership: 'single',
};

SVGRendererConfig controls the whole diagram's lane spacing and crossing ownership. jumpOwnership: 'single' gives each crossing one hop even though both crossing edges enable jump points.

2. Mount the diagram

Choose your framework's tab and place its file beside edge-data.ts. Install that tab's packages in your application:

bash
# JavaScript
npm install @grafloria/element @grafloria/engine @grafloria/renderer

# Angular
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element

# Qwik
npm install @grafloria/qwik @grafloria/engine @grafloria/renderer @builder.io/qwik @grafloria/element

# React
npm install @grafloria/react @grafloria/engine @grafloria/renderer react react-dom @grafloria/element

# Vue
npm install @grafloria/vue @grafloria/engine @grafloria/renderer vue @grafloria/element

For JavaScript, render mounts the spec and returns a live DiagramInstance. The JavaScript tab uses TypeScript in main.ts and creates its container in the browser.

For Angular, render DiagramCanvasComponent in app.component.ts; its two-way bindings return node and edge edits to your arrays. For Qwik, React and Vue, render the binding's component: GrafloriaFlow, GrafloriaFlow, or GrafloriaFlow, respectively. These tabs seed an uncontrolled instance with defaultNodes and defaultEdges.

ts
import { render } from '@grafloria/element';
import { nodes, edges, rendererConfig } from './edge-data';

const container = document.createElement('div');
container.style.height = '700px';
document.body.append(container);

const instance = render({ nodes, edges }, container, {
  renderer: rendererConfig,
});
instance.fitView();
JavaScript: the green A → B route passes below Obstacle, with a crossing hop, three labeled G → H lanes and a retry loop above Self.

Angular draws the same connections in its canvas.

Angular: a labeled green detour, a hop at the diagonal crossing, three parallel lanes and the Self retry loop.

Qwik renders the seeded edge specs.

Qwik: the obstacle detour, crossing hop, request, response and audit lanes, and retry loop.

React renders the same edge geometry.

Vue renders the routes and labels from the shared data.

Follow A → B around the obstacle, then inspect the middle crossing: the hop distinguishes two unrelated wires from a junction. At the bottom, G → H has three individually selectable lanes, and Self → Self leaves the node body before returning. Drag the obstacle or an endpoint node to see the routes update.

Choose the geometry

type is a shorthand for the line's shape. Explicit router and connector fields let you choose its path and its drawing independently.

typeRouter when omittedConnector when omitted and no explicit router is set
directstraightstraight
smoothstraightsmooth
orthogonalorthogonalrounded
bezierstraightbezier

An explicit orthogonal, manhattan, avoid or elk router implies a rounded connector unless you name a connector yourself. Use straight for sharp polyline bends, rounded for rounded corners, or smooth / bezier for curved drawing.

RouterWhat you get
straightA direct route between endpoints.
orthogonalRight-angle segments respecting port exit directions; the sample detours around the obstacle.
manhattanGrid-based right-angle routing with turn minimization.
avoidObstacle-avoiding routing through the built-in A* router.
elkThe synchronous renderer currently substitutes orthogonal.

Known issue: router: 'elk' does not produce ELK routing through the mounted renderer: ELK routing is async-only, and this rendering path substitutes orthogonal. Until it is fixed, request router: 'orthogonal' explicitly, as the sample does; use router: 'avoid' when you want the built-in A* obstacle router.

For node placement by ELK, see Lay out a diagram.

Choose attachments, labels and markers

Use handles when a connection must stay at a particular port or position on a box. Omit both handles to let the attachment follow the real port on the side facing its partner. For true perimeter floating, set the edge's metadata to { connectionPoint: 'smart' }; this permits attachment along the outline rather than only at a port.

OptionTypeDefaultWhat it does
sourceHandle, targetHandlestringPort-facing when neither is namedPin to a port id, side name, or point along a side. right@36 is 36 px down the right side; bottom@138 is 138 px from its left end; left@50% is halfway down.
waypointsPoint arrayNo supplied bendsSupply interior bends in world coordinates; endpoints stay attached to ports.
labelPlacement'on' | 'above' | 'below''on'Draw a label chip on the line, or text off the line without a box unless its style requests a background.
labelStyleLabel styleNo overrideSet the label's own color, font size, weight, family or background.
style.arrowHeadArrow styleFilled arrow, size 10Choose the target marker. Supply type, size and filled.
style.arrowTailArrow styleNo source markerChoose the source marker with the same fields.
parallelLinksbooleantrueFan links between the same unordered pair of nodes into separate lanes, including reverse-direction links.
parallelSpacingnumber16Set adjacent parallel-lane spacing in pixels.
jumpOwnership'both' | 'single''both'Choose whether both jump-enabled links or one link draws a crossing hop.
style.jumpPoints.enabledbooleanNot enabled without configurationEnable crossing decorations on that edge.
style.jumpPoints.sizenumber10Set the jump size in pixels.
style.selfLoop.sizenumber40Set how far a self-loop bulges outside its node.
style.selfLoop.side'auto' | 'top' | 'right' | 'bottom' | 'left''auto'Choose the loop's side; automatic uses its source port's side.

Endpoint markers include arrow, circle, square and diamond, plus ER markers such as crow-foot and zero-or-many, and UML markers such as generalization and hollow-diamond. Set type: 'none' to suppress an endpoint marker. Use the shipped shapes before registering your own geometry.

For more than one label on a link, obtain its live LinkModel through instance.getModel().getLink(id). addLabel({ text: 'condition', position: 0.25 }) adds text a quarter of the way along its path; call instance.renderNow() after setup mutations to repaint. A fractional label position follows the route instead of remaining at an absolute canvas coordinate.

Pitfalls

  • A connection-point strategy that accepts the edge owns both endpoints before per-end metadata.sourceAnchor / metadata.targetAnchor are considered. Do not combine a floating strategy with per-end anchors expecting the anchors to take precedence.
  • above and below are relative to the run's direction: on a vertical run, above places text to its left.
  • Changing a live link's router through setRouter() clears its cached points and manual-waypoint flag. Changing setConnector() leaves routed points intact. Choose the router before adding bends you need to keep.
  • Jump points do not replace a two-point smooth or bezier curve with a chord-based hop. Use direct or right-angle crossing wires, as above, when you need jump-overs.
  • For user-facing edits that belong in undo history, follow Commands and history, rather than treating setup model mutations as commands.

See Validate port connections for allowed connections, Configure editing gestures for interaction settings, and Save and restore documents for preserving the live document.

Was this page helpful?

Route and label edges — Grafloria