# Route and label edges

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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec#edgespec) rather than calculating SVG paths yourself.

## 1. Describe the routes and their labels

Create `edge-data.ts` in your browser application. The [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec#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 title="edge-data.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-types-svgrendererconfig#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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#render) mounts the spec and returns a live [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance). The JavaScript tab uses TypeScript in `main.ts` and creates its container in the browser.

For Angular, render [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent#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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflow), [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriaflow), or [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue#grafloriaflow), respectively. These tabs seed an uncontrolled instance with `defaultNodes` and `defaultEdges`.

:::code-group
```ts title="JavaScript"
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();
```
```ts title="Angular"
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { nodes, edges, rendererConfig } from './edge-data';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      [rendererConfig]="rendererConfig"
      style="display:block; height:700px" />
  `,
})
export class AppComponent implements AfterViewInit {
  nodes: NodeSpec[] = nodes;
  edges: EdgeSpec[] = edges;
  rendererConfig = rendererConfig;
  @ViewChild(DiagramCanvasComponent) canvas?: DiagramCanvasComponent;

  ngAfterViewInit(): void {
    requestAnimationFrame(() => this.canvas?.fitToContent());
  }
}
```
```tsx title="Qwik"
import { component$ } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import { nodes, edges, rendererConfig } from './edge-data';

export default component$(() => (
  <div style={{ height: '700px' }}>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
      rendererConfig={rendererConfig} fitView />
  </div>
));
```
```tsx title="React"
import { GrafloriaFlow } from '@grafloria/react';
import { nodes, edges, rendererConfig } from './edge-data';

export default function App() {
  return (
    <div style={{ height: '700px' }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
        rendererConfig={rendererConfig} fitView />
    </div>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import { nodes, edges, rendererConfig } from './edge-data';
</script>

<template>
  <div style="height:700px">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges"
      :renderer-config="rendererConfig" fit-view />
  </div>
</template>
```
:::

![JavaScript: the green A → B route passes below Obstacle, with a crossing hop, three labeled G → H lanes and a retry loop above Self.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/f759742d29597fc92d6a48fd8d8227af.png)

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.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/6cf4585d3939d76c009470f5a1a193d7.png)

Qwik renders the seeded edge specs.

![Qwik: the obstacle detour, crossing hop, request, response and audit lanes, and retry loop.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/05f8b7b9c57bb5267daab051038bb49d.png)

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.

| `type` | Router when omitted | Connector when omitted and no explicit router is set |
| --- | --- | --- |
| `direct` | `straight` | `straight` |
| `smooth` | `straight` | `smooth` |
| `orthogonal` | `orthogonal` | `rounded` |
| `bezier` | `straight` | `bezier` |

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.

| Router | What you get |
| --- | --- |
| `straight` | A direct route between endpoints. |
| `orthogonal` | Right-angle segments respecting port exit directions; the sample detours around the obstacle. |
| `manhattan` | Grid-based right-angle routing with turn minimization. |
| `avoid` | Obstacle-avoiding routing through the built-in A* router. |
| `elk` | The 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `sourceHandle`, `targetHandle` | `string` | Port-facing when neither is named | Pin 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. |
| `waypoints` | Point array | No supplied bends | Supply 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. |
| `labelStyle` | Label style | No override | Set the label's own color, font size, weight, family or background. |
| `style.arrowHead` | Arrow style | Filled arrow, size `10` | Choose the target marker. Supply `type`, `size` and `filled`. |
| `style.arrowTail` | Arrow style | No source marker | Choose the source marker with the same fields. |
| `parallelLinks` | `boolean` | `true` | Fan links between the same unordered pair of nodes into separate lanes, including reverse-direction links. |
| `parallelSpacing` | `number` | `16` | Set 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.enabled` | `boolean` | Not enabled without configuration | Enable crossing decorations on that edge. |
| `style.jumpPoints.size` | `number` | `10` | Set the jump size in pixels. |
| `style.selfLoop.size` | `number` | `40` | Set 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-linkmodel#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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/commands-and-history), rather than treating setup model mutations as commands.

## Live demos and related tasks

- [Edge routing](https://grafloria.com/demos/edges/edge-routing.html): drag or remove the obstacle and inspect the detour.
- [Edge labels](https://grafloria.com/demos/edges/edge-labels.html): drag a label along its edge, then move an endpoint.
- [Parallel links and self-loops](https://grafloria.com/demos/edges/parallel-links-and-self-loops.html): inspect separate lanes and the outside loop.
- [Jump-overs](https://grafloria.com/demos/edges/jump-overs.html): move the crossing and watch its hop follow.

See [Validate port connections](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/validate-port-connections) for allowed connections, [Configure editing gestures](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/configure-editing-gestures) for interaction settings, and [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents) for preserving the live document.
