# Laying a graph out — ranks, routed edges and a run's path on the flow canvas

> How the flow canvas draws a directed graph by itself: nodes in ranks from the starts to the ends, the main path on one straight row, every edge routed on a line of its own with an arrowhead, returns and loops drawn as what they are, the whole picture fitted to its box and turned top-to-bottom on a phone — and a run's path drawn over it.

<!-- id: ui-flow-layout · area: ui · stability: preview · html: https://osysharp.com/reference/ui/flow-layout/ -->

## Summary        {#summary}

Give [`FlowCanvas`](https://osysharp.com/reference/ui/flow-kit/) nodes and edges with no positions and it places them itself. With
`autoLayout: always` it goes further and draws a **picture**: it also routes every edge, keeps the picture fitted to
its box, and — given where a run has been — draws the run's path over it.

```osy syntax
FlowCanvas(
  nodes: Fulfilment.Graph.States, edges: Fulfilment.Graph.Transitions,
  sourceField: e => e.From, targetField: e => e.To, labelField: n => n.Label,
  readOnly: true, autoLayout: always,
  visited: ["Placed", "Packed"], highlighted: ["Shipped"]
) { s => Text(s.Label); }
```

The same graph is always the same picture: nothing in the layout is random, and every tie is broken by the order in
which you listed the nodes and edges.

## Signature      {#signature}

```osy syntax
FlowCanvas(
  …,
  autoLayout: always,           // initial | always | none — `always` is a picture: positions AND routes are the canvas's
  layoutDirection: auto,        // auto | right | down | left | up
  initialField: n => n.IsInitial,    // which field of a node row says it is a START
  terminalField: n => n.IsTerminal,  // which field says it is an END — ends share the last rank
  mainPath: ["Placed", "Packed", "Shipped", "Delivered"],   // the path kept on one straight row (omit: worked out)
  nodeSizing: equal,            // equal | natural — in a picture, every node the size of the largest, or its own
  highlighted: ["Shipped"],     // where a run IS
  visited: ["Placed", "Packed"],// where it has BEEN, in order
  path: [ … ],                  // the exact way it went, repeats included, when you know it
  failed: [ … ]                 // where it FAILED
)
```

## Description    {#description}

### What the layout does, step by step   {#steps}

1. **Cycles.** It walks the graph from the starts, edges in the order you listed them. An edge that leads back to a
   node the walk is still inside closes a cycle: it is a **return**. Returns are set aside for ranking and drawn
   against the flow, dashed.
2. **Ranks.** Every node gets the lowest rank that still puts it after everything that leads to it, so no edge that is
   not a return ever points backwards. **Ends share the last rank.** A start that nothing leads to sits one rank before
   what it feeds.
3. **The main path** is kept on one straight row — see [below](#main-path).
4. **Order within a rank.** An edge that spans several ranks gets a waypoint in each rank it passes. Ranks are then
   reordered by where each node's neighbours are, sweeping down and up a fixed number of times from two starting
   orders, keeping the order with the fewest crossings and finishing with swaps of neighbours.
5. **Positions.** Across the flow, nodes are placed by priority — the main path first, exactly straight; then the
   waypoints, so a long edge runs straight; then the nodes with the most neighbours — each as near the middle of its
   neighbours as the ones already placed allow. A place is always chosen inside the room that is left, so two nodes
   cannot overlap.
6. **Ports.** Every edge leaves and enters a node at a point of its own, in the order of where the other ends are, so
   lines do not cross as they leave a node.
7. **Routes.** Between two ranks every bending edge gets its own track. Tracks are ordered so that edges cross as
   little as they can and never lie on one another; corners are rounded and each edge ends in an arrowhead.

`autoLayout: initial` and the **Tidy** button use steps 1–5 to place the nodes of an editor (and report each move as a
`nodeMoved` event); edges there keep running from port to port, because the person will move the nodes next.
`autoLayout: always` uses all seven.

### What a picture promises   {#promises}

For any graph, in any direction:

- no two nodes overlap;
- no edge passes through a node, and no two edges lie on one another — where two lines meet, they cross;
- every edge that is not a return leads to a later rank;
- every end is in the last rank;
- the main path is one straight line;
- an edge from a node to itself is a loop beside the node; two edges between the same two nodes are two lines;
- an edge's label sits on its own edge, beside the node the edge leaves, and never on a node.

These are what the kit's own tests assert, and what [the probe](#testing) lets yours assert.

### Starts, ends and the main path   {#main-path}

The layout begins at the nodes marked as **starts** and puts every node marked as an **end** in the last rank. It reads
both off the node rows: `initialField` (default `n => n.IsInitial`) and `terminalField` (default `n => n.IsTerminal`)
select two `bool` members. A [workflow's states](https://osysharp.com/reference/workflow/graph/) have both, so a workflow's picture needs neither prop. Rows
without those fields are simply neither: the layout then starts from the nodes nothing leads to.

The **main path** is the row the eye follows. Name it — `mainPath: ["Placed", "Packed", "Shipped", "Delivered"]`, node
keys in order — or let the canvas work it out: from the first start to the **first end you listed**, by the longest way
there. A declaration usually names the outcome it is about first, so that is the row most pictures want.

### Which way it flows, and how it fits   {#direction}

`layoutDirection: auto` (the default) lays the graph out along the page's reading direction — left to right, or right
to left on a right-to-left page — **unless it would be drawn clearly larger flowing down**, which is what a phone's
narrow box comes to. It is decided by the two pictures themselves, so a wide box that happens to be short still gets
the layout that fits it. `right`, `down`, `left` and `up` say it outright.

A picture is **fitted to its box**: routes, loops and labels included, centred, and re-fitted when the box changes —
until the person pans or zooms, after which the view is theirs (the fit button gives it back). It is never enlarged
past its own size, so three nodes in a wide box stay the size you drew them. The zoom / fit toolbar takes whichever
corner the picture leaves empty.

### How big is each node?   {#sizes}

A node is as wide as what your template wants to be, so give its outer box a `maxW:` and a long name wraps inside it
rather than being cut. In a picture, `nodeSizing: equal` (the default) then holds every node to the size of the
largest — the same width and height when the picture flows sideways; the same height, each its own width, when it
flows down, because width is what a phone has least of. `nodeSizing: natural` leaves every node its own size.

### Edges — routes, returns, loops and labels   {#edges}

In a picture, edges are **routed**: horizontal and vertical pieces with rounded corners, each edge on its own track
between two ranks. (`edgeShape` is for an editor, where edges run from port to port.) Every edge, in either mode, ends
in an **arrowhead**.

| | drawn as |
|---|---|
| an edge that closes a cycle (a retry, a way back) | a **return**: dashed, running against the flow, its arrow at the node it returns to |
| an edge from a node to itself | a small **loop** beside the node |
| two edges between the same two nodes | two lines, side by side |
| `edgeLabelField` | the label on the edge's first straight run, beside the node it leaves |

### A run on the picture   {#run}

| prop | means | drawn as |
|---|---|---|
| `highlighted` | where the run **is** | a thick ring that breathes (still, for someone who prefers reduced motion) |
| `visited` | where it has **been**, in order | an accent outline and a tick |
| `failed` | where it **failed** | a broken outline in the danger colour and a `!` |
| *(everything else)* | not reached | drawn quietly |

The edges between one visited node and the next are drawn as **taken** — heavier, in the accent colour — and the one
that brought the run to where it is now flows. Only those: an edge between two places the run has been, which the run
did not take, stays quiet. When a run passed through a node more than once, `path` gives the exact way — every node key
in order, repeats included — and decides which edges are taken.

Every state has a mark that does not depend on colour: weight for a taken edge, dashes for a return, a ring, a tick, a
broken outline. All colours are the app's theme tokens ([theme tokens](https://osysharp.com/reference/ui/theming/)), so the picture follows light and dark.

### What does a screen reader get?   {#accessibility}

Beside the canvas, for a screen reader, is the same picture as text: how many nodes and edges and which way it runs,
then every node in the order it is drawn — whether it is a start or an end, where the run stands with it, and where
its edges lead (*"Assessing risk (visited): to Assessed by hand (taken); to Settling terms; to Withdrawn."*). It is
kept up to date as the run moves. Nodes and edges are also reachable one by one from the keyboard
([[ui-flow-kit#keyboard]]).

### Asserting a layout in a test   {#testing}

The canvas publishes what it counted about its last layout as [probe](https://osysharp.com/reference/ui/control-probe/) fields:

| field | type | what it is |
|---|---|---|
| `direction` | `string` | `right`, `down`, `left` or `up` |
| `ranks` | `int` | how many ranks |
| `crossings` | `int` | places where two routed edges cross |
| `overlaps` | `int` | places where two routed edges lie on one another |
| `throughNodes` | `int` | routed edges that pass through a node |
| `nodeOverlaps` | `int` | pairs of nodes that overlap |
| `labelsOnNodes` | `int` | edge labels lying on a node |
| `backward` | `int` | edges that are not returns and yet do not lead to a later rank |
| `returns` | `int` | edges drawn as returns |
| `endsNotLast` | `int` | ends that are not in the last rank |
| `mainStraight` | `bool` | the main path lies on one straight line |
| `mainPath` | `string` | the main path's node keys, joined by `" > "` |
| `stable` | `bool` | laid out once more just now, it came out the same |
| `layout` | `string` | a fingerprint of every position and route |
| `layoutMs` | `double` | how long the layout took to work out |
| `taken` | `int` | edges drawn as taken by the run |
| `outline` | `string` | the picture in words |

```osy syntax
Ui.Visit("/orders/A-1001");
Assert.Probe("FlowCanvas", "crossings", 0);
Assert.Probe("FlowCanvas", "mainPath", "Placed > Packed > Shipped > Delivered");
Assert.Probe("FlowCanvas", "edgesTaken", 2);
```

A test that runs without a browser has nothing to measure, so there the canvas lays a picture out at a nominal node
size: the ranks, the order and the routes are the real ones, and every count above is real. `osy test --pixels` runs
the same test at the sizes the nodes really have.

### How long it takes   {#cost}

A graph of twenty nodes and thirty-five edges is laid out — in both directions, to choose between them — in about ten
milliseconds in a browser. The layout is a chunk of its own (about 15 KB), downloaded the first time a canvas lays
itself out; a canvas that only draws stored positions never fetches it.

## Examples       {#examples}

```osy title="a picture of a process, with a run on it — plain values in, nothing stored" test app=ui-flow-layout
// ⚠ This example DECLARES the control inline, because a documentation example compiles on its own with no manifest
// to carry a `use`. In your app you write `use Osysharp.Diagrams@0;` and `using Osysharp.Diagrams;` instead and delete
// this block — the declaration arrives with the kit. What follows is `kits/diagrams/flow.osy`, trimmed to what this
// page uses.
public class FlowPort { public string Id; public string Kind; public string Side; }
public class FlowNodeType { public string Name; public FlowPort[] Ports; }

control FlowCanvas<TNode, TEdge> {
  contractVersion "1.1"
  participation headless
  sizing fill
  props {
    TNode[] nodes;
    TEdge[] edges;
    FlowNodeType[] nodeTypes;
    Func<TEdge, object> sourceField = e => e.Source;
    Func<TEdge, object> targetField = e => e.Target;
    Func<TNode, string>? labelField;
    Func<TEdge, string>? edgeLabelField;
    bool readOnly = false;
    [Values(initial, always, none)] string autoLayout = "initial";
    [Values(auto, right, down, left, up)] string layoutDirection = "auto";
    Func<TNode, bool> initialField = n => n.IsInitial;
    Func<TNode, bool> terminalField = n => n.IsTerminal;
    string[] mainPath = [];
    string[] highlighted = [];
    string[] visited = [];
    string[] failed = [];
  }
  slots from nodeTypes.Name
  chunks { XY; Layout; }
  probe {
    int crossings;
    string mainPath;
    int edgesTaken;
  }
}

// A stage of a repair, and a way from one stage to another — plain values, no table behind them.
public class Stage {
  public string Id;
  public string Label;
  public bool IsInitial;
  public bool IsTerminal;
}

public class Move {
  public string Id;
  public string From;
  public string To;
  public string When;
}

Stage[] Stages() {
  return [
    new Stage { Id = "Booked",   Label = "Booked in",          IsInitial = true,  IsTerminal = false },
    new Stage { Id = "Looking",  Label = "Finding the fault",  IsInitial = false, IsTerminal = false },
    new Stage { Id = "Parts",    Label = "Waiting for a part", IsInitial = false, IsTerminal = false },
    new Stage { Id = "Mending",  Label = "Mending",            IsInitial = false, IsTerminal = false },
    new Stage { Id = "Returned", Label = "Returned, working",  IsInitial = false, IsTerminal = true },
    new Stage { Id = "Scrapped", Label = "Beyond repair",      IsInitial = false, IsTerminal = true }
  ];
}

Move[] Moves() {
  return [
    new Move { Id = "m1", From = "Booked",  To = "Looking",  When = "Opened" },
    new Move { Id = "m2", From = "Looking", To = "Mending",  When = "Found" },
    new Move { Id = "m3", From = "Looking", To = "Parts",    When = "Part ordered" },
    new Move { Id = "m4", From = "Parts",   To = "Mending",  When = "Part arrived" },
    new Move { Id = "m5", From = "Mending", To = "Returned", When = "Tested" },
    new Move { Id = "m6", From = "Mending", To = "Looking",  When = "Still faulty" },   // a way back: drawn as a return
    new Move { Id = "m7", From = "Looking", To = "Scrapped", When = "Written off" }
  ];
}

[Route("/repair")] [AllowAnonymous]
component Repair() {
  render {
    Box(h: "360px") {
      FlowCanvas(
        nodes: Stages(), edges: Moves(),
        nodeTypes: [ new FlowNodeType { Name = "Stage", Ports = [
          new FlowPort { Id = "in", Kind = "target", Side = "left" },
          new FlowPort { Id = "out", Kind = "source", Side = "right" } ] } ],
        sourceField: e => e.From, targetField: e => e.To, labelField: n => n.Label, edgeLabelField: e => e.When,
        readOnly: true,
        autoLayout: always,                          // a picture: the canvas places every node and routes every edge
        visited: ["Booked", "Looking", "Parts"],     // where this repair has been, in order
        highlighted: ["Mending"]                     // where it is
      ) { s =>
        Stack(justify: Justify.Center, px: 3, py: 2, minW: "104px", maxW: "152px", bg: Colors.Surface, borderW: 1,
              border: Colors.Border, rounded: s.IsTerminal ? Radius.Pill : Radius.Md) {
          Text(s.Label, fontSize: FontSize.Label, fontWeight: FontWeight.Semibold, textAlign: TextAlign.Center);
        }
      }
    }
  }
}
```

That page draws six stages in four ranks — *Booked in → Finding the fault → Waiting for a part → Mending* on one row,
the two ends in the last rank — with *Still faulty* dashed and running back, the three moves the repair made drawn
heavy, and *Mending* ringed.

## See also       {#see-also}
- [The flow kit — a node-and-edge canvas whose nodes are your own Osy#](https://osysharp.com/reference/ui/flow-kit/) — the canvas itself: nodes as your own Osy#, editing, selection, the keyboard
- [Graph — every state and every transition of a workflow](https://osysharp.com/reference/workflow/graph/) — `<Wf>.Graph`, a workflow's states and transitions, ready to draw
- [probe — what a control says about itself](https://osysharp.com/reference/ui/control-probe/) — asserting what a control publishes about itself
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens the picture is painted with
- [accessibility](https://osysharp.com/reference/ui/accessibility/) — naming and describing what is on a page
