---
url: https://adecore.dev/diagram/layout.md
---
# Layout

`layoutOf(content)` places every node, group and edge in whole world units. It reads `meta`, `nodes`, `groups` and `edges`, so a document and its content both work. The result depends on nothing but the content: the same file gives the same layout on every machine.

```ts
import { layoutOf } from '@adecore/diagram';

const layout = layoutOf(diagram);
// layout.nodes:  { id, x, y, w, h, layer, pinned, label: string[], sub: string[] }[]
// layout.groups: { id, x, y, w, h, labelBox }[]
// layout.edges:  { index, from, to, points, label: { x, y, w, h, lines } | null }[]
// layout.bounds: { x, y, w, h }
```

`nodes` and `edges` come in file order, `groups` too, but a group that wraps no node has no box and is left out. `EdgeRoute.index` is the position of the edge in the file, and `points` run from the source to the target, with the arrow head at the last one. `bounds` covers the boxes, the group labels, the corners of the edges and the edge labels, and is all zero for an empty diagram. The types are `DiagramLayout`, `NodeBox`, `GroupBox`, `EdgeRoute` and `EdgeLabelBox`.

## Layers

A diagram runs in columns along its direction. `layersOf(nodes, edges)` gives the column of every node: the longest path to it from a node that nothing points at, counting from 0. A node without edges is in column 0.

A cycle has no longest path, so it is broken first. A walk through the graph in file order leaves out every edge that points back at a node it is still inside of, and ranks what is left. Those edges are still drawn, against the flow. Which edge of a cycle is left out depends on the order of the file, so moving a node in the file can change the layout.

## Order and placement

Inside a column, nodes are sorted to cross fewer edges: twelve sweeps move each node to the average position of its neighbors, and the best order any sweep reached is kept. A tie keeps the order the nodes already had, which is file order at the start. The members of a group stay together, and groups keep the same order in every column. Then each node moves towards its neighbors as far as the nodes beside it allow, so a chain lines up straight.

It is a heuristic, not an optimum. Dense graphs can still cross edges, and there are no options for spacing or the number of sweeps.

A `down` diagram is laid out as a `right` one and mirrored across the diagonal. The boxes keep their size and their text stays upright.

## Sizes

`sizeOfNode(node)` returns a `NodeSize`, the box and the wrapped lines of a node:

* The label is `LABEL_SIZE` (14) and bold, on lines `LABEL_LINE` (18) apart. `sub` is `SUB_SIZE` (12), on lines `SUB_LINE` (16) apart.
* Text wraps to `NODE_MAX_WIDTH` (280) minus the padding. A box is at least 120 wide.
* A `diamond` wraps narrower and is twice the size of its text, so the text fits between its corners. It can end up wider than `NODE_MAX_WIDTH`.
* A `pill` widens its padding for its round ends, and a `cylinder` is `CYLINDER_LID` (10) taller for its lid.

`sizeOfEdgeLabel(text)` does the same for an edge label: `SUB_SIZE`, wrapped to `EDGE_LABEL_MAX_WIDTH` (160), with `EDGE_LABEL_PADDING` (`{ x: 4, y: 2 }`) around it.

There is no font engine in the package, so text is estimated, never measured. `estimateTextWidth(text, size, bold)` adds up a width per kind of character, measured once for common system fonts and leaning towards too wide. `wrapText(text, size, bold, maxWidth)` wraps at spaces and reads a line break as a space. A font the estimate does not fit can run past its box, because exporting in another font does not change the layout.

## Groups

A group is a box around its members, `GROUP_PADDING` (20) away from them, with its label in a band of `GROUP_LABEL_BAND` (24) along the top, `GROUP_LABEL_INSET` (12) in from the side. Groups do not nest. Nodes outside a group stay on the same side of it from column to column, so an edge between two of them does not have to cross it.

## Edges

Edges run in right angles through the space between two columns, and each gets a track of its own where it would otherwise run on top of another. An edge that skips columns passes through every column in between. Where several edges leave one side of a box, they spread along it in the order of their other ends, and they end on the outline of the shape, not on its box. A label sits in that space beside its edge, and the space grows wider when labels need the room.

An edge from a node to itself is a small loop on the side of its box.

## Dragged nodes

A node with a `pos` stays at that position, rounded to whole units, and gets `pinned: true`. The app writes `pos` when a person drags a node, and removes it to hand the node back to the layout.

```ts
const layout = layoutOf({
    meta: { title: '', direction: 'down' },
    nodes: [
        { id: 'draft', label: 'Draft' },
        { id: 'review', label: 'Review', pos: [300.4, 200.6] }
    ],
    groups: [],
    edges: [{ from: 'draft', to: 'review', label: 'submit' }]
});
// review: { x: 300, y: 201, pinned: true, layer: 1, ... }
```

A pinned node keeps its `layer` number but takes no room in the layout, so the other nodes do not move out of its way. Its edges, and edges between two nodes in the same column, take a simpler route: out of one box, a step halfway, into the other. A group with a pinned member stretches to include it, wherever it is. Overlap is the app's to prevent.
