10. Layer 7 — Layout
Contents of DISL 0.8
Automatic layout arranges nodes and routes edges. DISL names layout algorithms, passes options, and declares when layout runs and how it interacts with user placement and bound coordinates. It does not define the algorithms it names, with three exceptions (DISL 0.3): rowPacked, rows and tidyTree are standard algorithms, defined in 10.1 to 10.3, whose results are normative so that every runtime draws the same diagram; each takes its options in a LayoutConfig property of its own name. (DISL 0.4) force with tiers (10.4) is not fully defined, but its result MUST have the properties 10.4 lists.
{
"layout": {
"algorithms": {
"flow": {
"algorithm": "layered",
"direction": "right",
"spacing": { "node": 40, "layer": 80, "edge": 12 },
"edgeRouting": "orthogonal",
"options": { "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP" },
"doc": "Left-to-right flow; transitions become orthogonal."
}
},
"default": "flow",
"trigger": "manual",
"respect": "pinned",
"animate": { "durationMs": 300 }
}
}
| Property | Type | Description |
|---|---|---|
algorithms |
map → LayoutConfig | Named configurations. |
default |
name | Used by the "Arrange" command and trigger. |
containers |
map TypeRef → name or LayoutConfig | Layout inside containers of a type (in addition to ContainerSpec's simple layouts). |
trigger |
"manual", "onCreate", "onChange", "onLoadIfMissing", "always" |
When layout runs automatically. onLoadIfMissing: when a DID definition lacks view data (for example generated by a program). always: positions are never user-controlled (pure layout-driven diagrams). Default "manual". |
respect |
"none", "pinned", "all" |
Which user positions survive automatic layout: none, only pinned elements (users can pin), or all existing (incremental layout places new elements only). Default "pinned". |
incremental |
bool | Prefer stability: minimise movement of existing elements. |
animate |
{durationMs, easing} or false |
|
doc |
Doc |
LayoutConfig
| Property | Type | Description |
|---|---|---|
algorithm |
string | "none", "layered" (Sugiyama), "tree", "mrtree", "radial", "force", "stress", "orthogonal", "box", "grid", "circular", "rectpacking", "lanes" (assign nodes to ordinal bands, then layer within bands), "sequence" (sequence diagrams), "elk:<id>" (any ELK algorithm id), "plugin:<name>"; and the standard algorithms "rowPacked" (10.1), "rows" (10.2) and "tidyTree" (10.3), DISL 0.3. |
direction |
"right", "down", "left", "up" |
Main flow direction. |
spacing |
{node, layer, edge, component, port} |
Canvas units. |
edgeRouting |
Routing | Routing applied to edges with routing: "layout" or when overrideRouting is true. |
overrideRouting |
bool | Layout replaces user bendpoints. |
options |
map string → JSON | Pass-through options for the algorithm (ELK option ids recommended for interoperability). Unknown options are ignored. |
scope |
"all", "selection", "component" |
Default scope when invoked. |
includeViewOnly |
bool | Whether notes and annotations take part. |
rowPacked, rows, tidyTree |
RowPacked, RowsLayout, TidyTree | (DISL 0.3) The options of the standard algorithm of that name (10.1 to 10.3); given only with that algorithm. |
force |
ForceLayout | (DISL 0.4) The options of force (10.4); given only with that algorithm. |
doc |
Doc |
Interaction with coordinate systems. Layout only assigns coordinates whose PlacementSource is free or layout. Bound coordinates (a task's start date) are never changed by layout; algorithms receive them as fixed constraints. For example, "lanes" on a schedule keeps x (time) fixed and only packs y within bands. Layout results are snapped with the rules of the coordinate system unless options["dedl.snap"] is false.
The one exception is an algorithm that declares the attributes it writes, as rows does with writes (10.2): it changes those attributes, and only those, as a model edit in one transaction and one undo step, with constraints and hooks as for any edit. Every other bound coordinate stays fixed.
rowPacked keeps every element on its row and packs each row along the main axis (direction, default "right"; the rows run across it). It suits a variant that shortens what a true-scale view draws, such as a hype cycle's compact view. RowPacked ($defs/RowPacked), the LayoutConfig property rowPacked:
| Property | Type | Default | Description |
|---|---|---|---|
keepRows |
true |
true |
States that rows are kept; it cannot be switched off. |
order |
"start", "document" |
required | The order within a row. start: by the element's start along the packed axis as its base placement gives it (in a viewpoint variant, the placement of the viewpoint it varies, 3.5), ties in model order. document: model order. |
gap |
number | required | The least distance between two elements on a row, in canvas units. |
followConnections |
TypeRef | – | A relation type whose targets start after their sources (below). |
targetAfter |
"sourceStart", "sourceMiddle", "sourceEnd" |
"sourceEnd" |
The point of the source a target starts no earlier than. |
rowsCovered |
Expression → int | 1 |
The number of rows an element covers from its own row down; context element. |
doc |
Doc |
rowPackedMUST NOT change an element's row coordinate.- Within a row it places elements in the order
ordergives, each at the smallest main-axis position at which it is at leastgapafter the previous element on every row it covers. An element that covers several rows (rowsCovered) occupies each of them. - With
followConnections, the target of a relation of that type MUST start no earlier than the source'stargetAfterpoint; where that is impossible because of a cycle, the cycle is broken at the element earliest inorder. - LayoutConfig
scope: "all"lays out the whole model, not only what is in the viewport.
"compactRows": { "algorithm": "rowPacked", "scope": "all",
"rowPacked": { "keepRows": true, "order": "start", "gap": 4, "followConnections": "Influence",
"targetAfter": "sourceMiddle", "rowsCovered": "int(math.ceil(self.height / 56.0))" } }
In a hype cycle's compact view, Steam engine on row 0 influences Railways on row 1, so Railways starts after the middle of Steam engine's 96-unit banner even if row 1 is otherwise empty.
rows assigns rows and nothing else, for diagrams whose other axis is data, such as a hype cycle's time. It is invoked through a layout action (9.4), typically from an Arrange operation, rather than run automatically. RowsLayout ($defs/RowsLayout), the LayoutConfig property rows:
| Property | Type | Default | Description |
|---|---|---|---|
writes |
attribute name[] | required | The attributes the algorithm writes, the row first. |
extent |
map TypeRef → {from, to, rows} |
required | Per type, the extent an element occupies on its row: from and to along the main axis and rows, the rows it covers from its own row down (default 1). GeomExpr in the layoutExtent context (12.3). Elements of types not listed do not take part. |
clearance |
number | 0 |
The least distance between two extents on one row, in canvas units. |
affinity |
TypeRef | – | A relation type that draws linked elements to nearby rows. |
ties |
"lower", "upper" |
"lower" |
Between two free rows equally near the wanted one: the smaller row number (lower) or the larger. |
swapPasses |
int | 0 |
The most passes of neighbouring-row swaps (below). |
doc |
Doc |
What takes part. An element takes part when it has a drawable extent: its type is in extent and its extent evaluates, which it does not for an element missing a value it is computed from (a trend without a span, a trigger without a date). An element with no id, or with an id an element earlier in model order already has, does not take part either. An element that does not take part keeps its row.
The assignment. The algorithm takes the elements in the order of their extents' from, then to, then model order. It puts each on a free top row: one from which every row its extent covers keeps clearance from every extent already there, among the rows already in use. With affinity, that is the free top row nearest the average row of its already-placed linked elements, ties deciding between two equally near; otherwise, and for an element with no placed link, the first free top row. When no row in use is free, it takes the lowest top row from which its rows are free, counting the rows beyond the last in use as free, so that rows are opened below. With swapPasses, it then swaps whole neighbouring rows, from the top down, keeping a swap only when it strictly shortens the total row distance of the affinity links, for at most that many passes or until a pass keeps none. A row covered by an element that covers more than one row is pinned: a swap MUST NOT move it, because that would tear the element apart.
rowsMUST change only the attributes inwrites, and MUST assign rows as the assignment above describes.- When every extent covers one row, the result uses the fewest rows any assignment of the extents could use, because a row is opened only where that many extents meet. With an extent that covers several rows it need not, and a runtime MUST NOT use fewer rows than the assignment gives.
- It MUST be deterministic: it reads no row, so running it on its own result changes nothing.
- The changed values MUST be written in one transaction and one undo step, and only where they change.
- While no element in its scope has a drawable extent, the
layoutaction is refused withrefusals.nothingDrawn; otherwise, when it would change nothing, withrefusals.unchanged(9.4).
"arrangeRows": { "algorithm": "rows",
"rows": { "writes": ["row"], "clearance": 16, "affinity": "Influence", "ties": "lower", "swapPasses": 64,
"extent": { "Trend": { "from": "x - 8.0 - textWidth(self.name, 12.0)", "to": "x2" },
"Trigger": { "from": "x - 8.0 - textWidth(self.name + ' · ' + formatWhen(self.date, diagram.unit, false), 12.0)",
"to": "x2" },
"Note": { "from": "x", "to": "x + width", "rows": "int(math.ceil(height / 56.0))" } } } }
"arrange": { "label": "Arrange diagram", "for": "diagram",
"actions": [ { "layout": { "algorithm": "arrangeRows",
"refusals": { "nothingDrawn": "There is nothing to arrange until this graph has a trend.",
"unchanged": "This graph is already arranged." } } } ] }
Three trends from 1800 to 1850, 1840 to 1900 and 1860 to 1900 on rows 0, 1 and 2 are arranged onto rows 0, 1 and 0. A trend's extent starts at its name, 8 units before its banner, and a trigger's at its label, its name and date, 8 units before its circle; x is the left edge of the element's bounds. A note two rows tall pins both its rows, so the rows it spans are never swapped apart. A graph whose only trend has no span has nothing drawable and is refused with "There is nothing to arrange until this graph has a trend.".
tidyTree lays out containment as a tidy tree (Reingold and Tilford): each element's children in model order along the cross axis, each subtree packed as close as its outline allows. direction gives the main axis (default "down", the root at the top), and spacing.node, spacing.layer and spacing.component the distances below. TidyTree ($defs/TidyTree), the LayoutConfig property tidyTree:
| Property | Type | Default | Description |
|---|---|---|---|
order |
"children" |
"children" |
Each element's children in their model order. |
parentAlign |
"firstLastCentre", "childrenCentre" |
"firstLastCentre" |
A parent centred over its first and last child, or over all its children's extent. |
origin |
"leftmostAtZero", "rootAtZero" |
"leftmostAtZero" |
The whole is shifted so its leftmost node, or its first root, is at cross-axis 0. |
rows |
{stored: "none" | "y", minGap: number} |
{stored: "none"} |
Main-axis positions stored per row (below). |
drag |
{carry: "subtree" | "node", across: "reorder" | "none", down: "row" | "none"} |
{carry: "node", across: "none", down: "none"} |
What dragging an element does (below). |
doc |
Doc |
tidyTreeMUST place each element's children in their model order along the cross axis, MUST pack each subtree so that no node is closer thanspacing.nodeto a node of a neighbouring subtree at the same depth, measured between their facing edges, and MUST place each parent centred over its first and last child (firstLastCentre) or over all its children's extent (childrenCentre). Top-level elements MUST be packed the same way,spacing.componentapart.- Distances on the main axis are gaps between edges, not between positions: children MUST lie with their near edge
spacing.layerbeyond their parent's far edge (withdirection: "down", their topspacing.layerbelow the parent's bottom). The top-level elements MUST share one row, at main-axis position 0, so every depth is one row. - With
rows.stored: "y", the top-level elements MUST share one position on the main axis, the first stored for any of them in model order, else 0; and all children of one parent MUST share one position on the main axis: the first stored for any of them in model order, else with their near edgespacing.layerbeyond the parent's far edge as the parent is drawn, and never with it closer to that edge thanrows.minGap. A row's position is the one everything beneath it hangs from, so a moved row carries the rows beneath that store none. The cross-axis position MUST always be the computed one. The stored positions are view data (11.6), or a registration's layout when the model lives in a format binding. - With
drag.across: "reorder", dropping a dragged element past a sibling's centre MUST reorder it among its siblings in the model; withdrag.down: "row", the dropped main-axis position MUST be stored for every element of the row and beneath it; withdrag.carry: "subtree", everything beneath the dragged element MUST move with it while dragged. A drag MUST be one transaction and one undo step.
"layout": { "algorithms": { "tree": { "algorithm": "tidyTree", "direction": "down",
"spacing": { "node": 28, "layer": 56, "component": 56 },
"tidyTree": { "order": "children", "parentAlign": "firstLastCentre", "origin": "leftmostAtZero",
"rows": { "stored": "y", "minGap": 16 },
"drag": { "carry": "subtree", "across": "reorder", "down": "row" } } } },
"default": "tree", "trigger": "always", "respect": "pinned" }
An agent behavior model draws its tree top-down, its nodes 200 by 60: neighbouring subtrees 28 apart, each row of children 56 below its parent's bottom edge, and several roots side by side on one row, 56 apart. A node dragged past its sibling's centre swaps places with it in the Markdown, and a row dragged down keeps its new height for itself and everything beneath it; a row dragged up stops with its top 16 below its parent's bottom.
force pulls related elements together and pushes all elements apart. With tiers it also radiates by element type, from the centre outwards, as a picture of projects, then their work, then who does it and where, reads. ForceLayout ($defs/ForceLayout), the LayoutConfig property force:
| Property | Type | Default | Description |
|---|---|---|---|
tiers |
(TypeRef or TypeRef[])[] | – | Node types from the centre outwards: the first entry is the centre, each next one a ring further out. An entry that is a list puts several types on one ring. A node type in no entry is placed by forces alone. |
gap |
number | 24 | The least distance, in canvas units, between the bounds of any two nodes the layout places. |
doc |
Doc |
The algorithm itself is not defined here: a specification that uses tiers SHOULD describe a reference algorithm, with its numbers, where a second host can read it (its companion document), so that hosts draw close pictures. Whatever algorithm a runtime uses, its result MUST have these properties:
- Tiers. For two nodes of different tiers, both placed by the layout, the one of the inner tier is no further from the centre than the one of the outer tier, measured from the centre of their bounds to the centroid of the first tier's nodes (or the origin when the first tier is empty). A node without relations is still placed on its tier's ring.
- No overlap. No two nodes' bounds, each grown by
gap / 2, intersect, whatever their sizes; this MUST hold again after a node's size changes (a group expanded, 6.9), by running the layout again. - Pinned nodes stay. Under
respect: "pinned", a pinned node keeps its stored position and acts on the others as a fixed point; the layout MUST NOT move it, and property 2 is met by moving the others. - The same input, the same picture. Opened from the same model and view data, the result MUST be the same on every run and every machine: the start positions follow from the tiers and the model order, the number of iterations is fixed, and nothing is random.
- Little movement. When the model changes while the view is open, the layout MUST start from the positions on screen rather than from the computed start, so that nodes the change does not touch move as little as the forces allow. The result after a run of live changes may therefore differ from the result the same model gives when opened (property 4 is about opening).
- Positions are not data. The positions the layout computes are not view data: they MUST NOT be written, unless the user pins a node (7.3).
A drag pins. Under respect: "pinned" with a trigger other than manual, dragging a node the layout placed MUST pin it where it was dropped, as one edit with the move; dropping a new node from the toolbox pins it at the drop point. A node added by any other means (an edit of the file, an operation) is not pinned.
"layout": {
"algorithms": { "rings": { "algorithm": "force", "force": { "tiers": ["Board", "Item", ["Person", "Team"]], "gap": 32 } } },
"default": "rings", "trigger": "onChange", "respect": "pinned", "animate": { "durationMs": 250 }
}
Boards sit in the middle, their items around them and the people and teams who work on them outermost; a person dragged into a corner stays there and the others arrange themselves around.
Illustrated by
- Causal loop diagram: 3 items in this layer
- Entity–relationship diagram: 2 items in this layer
- Gartner hype cycle graph: 4 items in this layer
- State machine: 4 items in this layer
- Project timeline: 3 items in this layer
- Work board: 5 items in this layer
Referenced from
- 3. Specification structure, layers and viewpoints
- 5. Layer 2 — Coordinate systems, placement and snapping
- 6. Layer 3 — Notation (visual definition)
- 7. Layer 4 — Toolbox, context tools and forms
- 9. Layer 6 — Behavior
- 12. The CEL environment
- 17. Complete examples
- Appendix B — Built-in catalogues
- Appendix D — Design rationale and open questions