Layout
A flow diagram is arranged in one of three modes. You can mix written positions with automatic
arrangement, add constraints that line boxes up, name ports on boxes and choose how connectors are
drawn. The same rules apply in the command line, the editor and every export. Free diagrams use the
same modes with layout manual as their default, and place group children inside their group
(free.md). Sequence diagrams have their own layout, from the order of their events
(sequence.md); C4 views use the flow layout (c4.md). When the engine
arranges a diagram, it leaves room for relation labels between the boxes.
diagram checkout: flow { direction LR layout assisted ...}| Mode | Positions |
|---|---|
auto (default) |
The layout engine arranges every box. place statements and route waypoints are ignored. |
assisted |
Placed boxes stay exactly where they are; the engine arranges the others around them. |
manual |
Every box needs a place; a missing one is layout.missing-placement. |
auto:- Every box, note and tag is arranged. Groups fit their children, and a group’s own
placeis ignored too. A group’s writtensizeis the least size the engine gives it, its children centred inside, so a container drawn larger than its content keeps its size. - A group at the top level with
placeandsizeand nothing inside, a band, stays where it is written, out of the engine’s way, as inassisted; itsplaceis not reported. A band inside a group the engine arranges has no place to keep and islayout.missing-placement. - The first ignored
place, orroutewith waypoints, is reported once aslayout.placement-ignored, with the hint to uselayout assisted. size,label, the ports ofroutestatements, constraints and styles apply as usual.- In free diagrams, a list is arranged as one box and keeps its children in its order, and lines and pens keep their written points (free.md).
- Connectors follow the engine’s orthogonal routes.
- Every box, note and tag is arranged. Groups fit their children, and a group’s own
assisted:- The engine arranges the whole graph.
- Each unplaced box keeps its offset from the placed box nearest to it in that arrangement.
- A box that would then overlap one already positioned moves away from that placed box, along the layout direction, until it is clear.
- A group with both
placeandsizeis neither arranged nor in the way of the arranged boxes, so an untitled group on a back layer is a band behind them (free.md). - Connectors use the anchor rules of svg-export.md.
manual: positions are only what is written.
direction (LR, RL, TB, BT) sets the flow of the arrangement. Primary relations (->)
weigh more than secondary ones (-->) when the engine straightens edges.
The engine
Section titled “The engine”The command line, the editor and every export use ELK’s layered algorithm (elkjs 0.12.0, in
@meltemi/diagrams-layout-elk; see decision 0015). The arrangement is deterministic:
- the same source gives the same picture;
- boxes keep their source order within each layer;
- adding an unrelated box does not reorder the existing ones.
Hosts pass an engine to layoutDiagram(compilation, diagram, { engine }). Without one, auto and
assisted diagrams render only when every box is placed, and report layout.mode-unsupported
otherwise. An engine is any object with a synchronous arrange(request):
- the request lists boxes with their sizes, parent groups, ports and, in
assisted, the fixed centres; groups with their paddings, whose top is the title band of a titled group andgroupPaddingfor an untitled one; and relations with their ports and kind; - the engine returns every box’s centre and every relation’s points.
An engine that throws is reported as layout.engine-failed.
Writing positions down
Section titled “Writing positions down”meltemi-diagrams layout <file.meltemi> runs the engine and writes what it would draw:
- without options: a
placefor every unplaced box, around the placed ones, aslayout assisteddraws them; --all: arranges everything anew, rewrites everyplace(and theplaceandsizeof placed groups) and hands connection points back to the engine: compass sides and points go and named ports stay. The bends of the connectors the engine draws are written as waypoints, ends free (route store from api to db via [(860, 120)]), naming an anonymous relation<from>_<to>first; a route left with nothing is removed. The connectors exported after it have the bends they had before it, each within 1 px.
In free diagrams both also write a place and a size for every arranged group, and the places
of group children relative to the group’s interior. A list (layout column|row in a group block)
is arranged as one box: it gets a place and keeps its derived size, and nothing is written for
the children it places (free.md).
Both turn layout auto into layout assisted, so the file keeps the picture. Other options:
| Option | Effect |
|---|---|
--diagram <id> |
arranges one diagram |
--check |
writes nothing and exits 1 when something would change |
--stdout |
prints the result instead of writing it |
Dragging a box in the editor writes its place and also turns layout auto into layout assisted. Editors call planEdit() with the arrange operation, which is the same edit the
command makes.
Named ports
Section titled “Named ports”A box can declare ports in its block. Routes then connect to them by name:
bus["Event bus"] { port in west port out east 30%}placement { route publish from api.east to bus.in}port <name> <side> [<n>%]puts a port onnorth,south,eastorwest. The percentage runs along the side, left to right or top to bottom, and is 50 % by default.- Port names are identifiers other than the compass words and
auto. A duplicate isref.duplicate-id. - A route that names a port the box does not declare is
placement.unknown-port, with the box’s ports in the hint. - A named port is a point: connectors start or end exactly there and leave along the side’s normal. Compass ports only choose the side; the anchor rules choose the point.
- A percentage after a compass side pins a route’s end to that point of the side, read as in
port:route publish from api.east 50% to bus.inleaves the middle ofapi’s east side. The end then anchors like a named port, in every layout mode, and bends do not move it. Afterauto, a bare element or a named port, or outside 0 to 100, the percentage isprop.invalid-value. - On a box drawn with a shape other than a rectangle (a decision’s diamond, a data store’s
cylinder, a
shape hexagonnode), a port or a pinned point is drawn where the side’s normal through it meets the outline; the text keeps the side and the percentage (free.md).
Constraints
Section titled “Constraints”Constraints go in placement blocks and hold in every mode:
| Statement | Meaning |
|---|---|
align a, b, c top |
equal top edges; also bottom, left, right |
align a, b, c middle |
equal vertical centres; center gives equal horizontal centres |
row a, b, c |
the same as align a, b, c middle |
column a, b, c |
the same as align a, b, c center |
order a, b, c [horizontal|vertical] |
strictly increasing centres in the written order; axis from direction |
distance a, b 80 |
at least 80 px between the two boxes, edge to edge |
- Who moves. Placed boxes never move. Free boxes are moved until every constraint holds:
- aligned boxes take the first placed member as reference, or else the mean of the free ones;
- ordered boxes are pushed past their predecessor plus 40 px;
- a box closer than
distanceis pushed away.
- Conflicts. What still fails after 20 rounds is
layout.constraint-conflict(error) on the statement, naming the boxes and how far off they are. The picture is still drawn. manual. Every box is placed, so constraints only check the positions.- Model errors. Constraints apply to boxes, notes and tags, not groups. The model reports:
- an unknown box (
ref.unknown-element); - a box listed twice;
- an
align,roworcolumnwith a single box; - a
distancewithout exactly two boxes and a positive gap.
- an unknown box (
Connectors
Section titled “Connectors”routing orthogonal // diagram defaultstyle notify { routing bezier }routing is a diagram property and a relation style property (also through classes and rule relation):
| Value | Connector |
|---|---|
auto (default) |
the anchor rules of svg-export.md, or the engine’s routes in auto mode |
orthogonal |
horizontal and vertical segments through the anchors and waypoints, leaving and entering square to the sides |
polyline |
straight segments between the anchors and waypoints |
bezier |
a smooth curve through the anchors and waypoints, leaving and entering square to the sides |
The engine’s routes are orthogonal, so in auto mode polyline and bezier connectors join their
anchors directly.
Rounded corners. corners rounded (a relation style, or the theme token for every relation)
replaces each bend of an auto, orthogonal or polyline connector by a curve that starts 10 px
from the bend, or half the shorter segment when that is less. The built-in themes keep sharp
corners, but for the sketch.* pair, which rounds them; bezier connectors are smooth already.
Lines without heads. A relation written -- or --- is routed exactly as an arrow: the anchor
rules, the lanes, the detours and the gaps under group titles apply, and the line reaches both
boxes, since there is no head to stop at.
Free lines. A line of a free diagram (free.md) is a connector whose anchors
and waypoints are its written points, moved by its parent’s interior origin. It is polyline by
default, and for auto too, since it has no box to anchor to: straight segments through its
points. routing orthogonal puts one elbow between two consecutive points that differ on both
axes, the first segment along the axis of the longer step (from (0, 0) to (200, 100) goes
through (200, 0)); routing bezier curves through every point; corners rounded rounds its
bends. Its heads are arrowtail and arrowhead, none by default, cut from the line as a
relation’s are. A line takes no lane, no detour and no gap under a group title, and the
composition checks between boxes leave it out: its points are where you wrote them. A line whose
points coincide is drawn as a dot of its width and reported as layout.degenerate-line
(warning).
Pen strokes. A pen (free.md) is drawn beside the lines, but it is
not routed: its written points, moved by its parent’s interior origin, are joined by one smooth
path through the midpoints of consecutive points, whatever the routing. It takes no lane, no
detour and no gap, no engine moves it, and the composition checks leave it out. One point, or
points that coincide, draw a dot, without a warning. Its heads are arrowtail and arrowhead,
cut along its end segments as a line’s are.
Lists. A group that writes layout column or layout row places its own children in source
order (free.md), before any connector is drawn: a connector between two cards of
a list ends where the list puts them. The engine sees a list as one box of its derived size in
every mode, with the relations to what it holds ending on it, and never arranges inside it.
Turned elements. A connector meets an element turned by rotation on its turned outline. A
free end is routed to the turned drawing’s bounding box, like any box, and slides from that box’s
side onto the outline. A named port or a pinned point (api.north 30%) is found on the unturned
element and turns with it: api.north of a node turned by 90 degrees faces east, and the connector
leaves it eastwards. In layout auto the engine arranges the turned drawing’s bounding box, and the
element’s box is centred on it.
Around other boxes. In every mode, an auto or orthogonal connector with no waypoints and
no parallel lane whose route — the engine’s or the anchor rules’ — would run through another box
goes around it. With both ends free it first takes the L- or Z-shaped route between the middles of
two sides that crosses the fewest boxes, then the shortest one. When that still crosses a box, or
an end is pinned (api.south, api.south 30% or a named port), it takes the shortest orthogonal
detour that leaves and enters where the connector did, square to those sides, with as few bends
as it can. A connector nothing stands in the way of keeps its route. A box or group that holds one
of the connector’s ends (a group around it, a box it is stacked inside) is not in the way, nor is
a box on another layer than the relation, so connectors cross containers and bands straight.
Labels the layout places. A relation’s label without a label statement sits in the middle
of its connector when that spot is clear; otherwise it slides along the connector, out from the
middle, to the first spot where it covers no box, no group title, no other label and no other
connector. label <relation> at <n>% places it by hand.
Group titles. A straight connector that crosses a group’s title (and its description) is drawn with a gap there, so it passes under the title instead of over it. Untitled groups have no title band, so they cut no gap and labels may sit at their top.
Lanes. Free ends of connectors drawn by the anchor rules (no waypoints, no named port, no point of a side) that meet the same side of a box less than a lane apart are spread a lane apart: 16 px, or a label’s height plus 4 px on a side holding labelled relations. The ends of a run are ordered along the side by where each connector’s other end is, so they do not cross, and centred on the middle of their anchors. A straight connector moved at one end moves at the other too, so it stays straight. Relations between the same two boxes, in either direction, are such ends at both boxes, so they run in parallel lanes 16 px apart.
Composition checks
Section titled “Composition checks”After every layout the scene is checked. Each finding is a warning on the later element’s statement:
| Code | Finding |
|---|---|
layout.overlap |
two boxes of a layer intersect, unless the later one lies strictly inside the earlier one; a box crosses the edge of a group, or lies over a group not its own |
layout.label-under-connector |
a relation label lies on another relation’s connector |
layout.container-margin |
a box in a placed and sized group of its layer is within half the group padding of its edge, or in its title band (an untitled group has none) |
layout.connector-overlap |
two connectors run along each other for more than 24 px |
layout.connector-crossing |
a connector runs through a box of its layer that it does not join and that does not hold one of its ends; a shaped box by its outline |
layout.label-cut |
a text goes beyond its shape’s text area, its label’s background or the picture |
layout.label-overlap |
a text overlaps another text, the box of an element it does not belong to, or another connector |
layout.connector-overlap also keeps the primary flow (->) apart from secondary dependencies
(-->); ELK already routes primary relations straighter. Notes and tags are annotations that you
place on boxes on purpose, so their boxes are not checked; their text still must not cover other
text. In free diagrams, texts, icons and images are annotations too, and lines and pens are left
out of the checks: a stroke may cross boxes and labels. A sticky is checked as a box.
- Stacks. A box declared after a box of its layer that holds it, strictly inside it, is a deliberate stack, drawn over it, and is not reported; a partial overlap is, and so are two boxes that coincide. A stack declared inner first is reported on the outer box, naming both, with a hint to declare the outer box first. Moving an inner box to the back therefore turns a clean stack into a finding.
- Layers. Boxes and groups on different layers are not checked against each other, and a connector is not checked against boxes on another layer, so a band on a back layer may lie under boxes and connectors.
- Containers. A group or box that holds one of a connector’s ends is not crossed by it, and a
container’s own text may lie over the boxes it holds; two texts drawn at the same point are still
layout.label-overlap, so stacked boxes need empty or separate labels. - Shapes. A connector that passes through a diamond’s empty corner does not cross it:
layout.connector-crossingfollows the outline of shaped boxes, andlayout.label-cutmeasures their text against the shape’s text area.layout.overlapcompares the rectangles.
The label checks (label-cut, label-overlap) look at the drawing itself: every text, measured
with the theme’s fonts, against its own shape’s text area, the other texts, the boxes and the drawn
connectors, in every profile. A text already named by another finding (an overflowing size,
overlapping boxes, a label on a connector, a connector through a box) is not reported again. The
acceptance corpus keeps every profile at zero (docs/quality.md).
meltemi-diagrams validate and export print the findings, as the editor shows them.