Layout levers: what each one would save
Written 2026-11-09 against tip da244717ddf3 (end of stage 7). Every number below was measured, not
estimated, over the 103 component, package and state goldens with the metrics of core::metrics
(crossings, overlaps, bends), the same counts the quality test prints. The baseline is
5 crossings, 0 overlaps, 341 bends (component 5/0/148, package 0/0/34, state 0/0/159).
The project owner observed three things in the rendered diagrams:
- (a) edge labels force bends that are not needed;
- (b) container and composite names sometimes block lines that could run straight if the name moved;
- (c) some boxes could be wider or taller, so that a connector runs straight instead of jogging.
Each was tested as a counterfactual. The experiment code was a thread-local switch set read by the three adapters, plus an analysis test that ran every variant and wrote the SVGs for inspection. It was not committed: no layout behaviour and no golden file changed.
Summary
| Lever | Upper bound | Realistic variant | Crossings | Area cost | Effort | Risk |
|---|---|---|---|---|---|---|
| Box growth, up to 4 grid steps | — | −128 bends | 0 | +3.1% of box area, no growth of the diagram | Medium (2–3 days) | Low to medium: uneven box sizes |
| Box growth, up to 2 grid steps | — | −90 bends | 0 | +1.6% of box area | Medium | Low |
| Box growth, 1 grid step | — | −56 bends | 0 | +0.9% of box area | Medium | Low |
| Corner inset for ends (attachment) | −22 bends, −1 crossing, but 3 merged ends | −12 bends | 0 | none | Very small (hours) | Low |
| Titles (names and tabs) | −12 bends | −10 bends | 0 | none | Small | Low (tabs), medium (state names) |
| Labels not forcing bends (state) | −59 bends, +3 crossings | −26 bends | 0 | none | Large (3–5 days) | Medium to high |
| Labels, no reservation at all | −69 bends, +8 crossings | not usable | +8 | −20% diagram area | — | High |
| All realistic levers but labels | 3 crossings, 0 overlaps, 207 bends | −2 | as box growth | |||
| All realistic levers | 3 crossings, 0 overlaps, 185 bends | −2 | as box growth |
“Upper bound” removes the cause outright, which is not always acceptable to draw. “Realistic” is a variant that keeps every rule in the principles: no label touches ink or reads ambiguously, no arrow pierces a name, no two connectors merge. Each realistic variant was also checked per diagram: where it made any count worse, the diagram kept its baseline. Only the label variant needed this. In three state diagrams it is worse on its own, and with the check the label lever saves 26 bends rather than 11.
Recommendation, in order: box growth, then the corner inset, then arrows ending on package tabs, then the label hybrid. Moving state names aside is small but worth only 4 bends. Details are in Ranking.
Per lever and diagram kind
Crossings, overlaps and bends, summed over each kind (65 component, 16 package and 22 state goldens).
| Variant | Component | Package | State | Total |
|---|---|---|---|---|
| Baseline | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 341 |
| Labels free: no waypoint, no label obstacles | 5 / 0 / 148 | 0 / 0 / 34 | 3 / 0 / 100 | 8 / 0 / 282 |
| Labels, no reservation in the layout | 5 / 0 / 148 | 0 / 0 / 34 | 8 / 0 / 90 | 13 / 0 / 272 |
| Labels hybrid, raw | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 1 / 148 | 5 / 1 / 330 |
| Labels hybrid, never worse per diagram | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 133 | 5 / 0 / 315 |
| Names ignored as obstacles only | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 341 |
| Names and tabs ignored everywhere | 5 / 0 / 146 | 0 / 0 / 28 | 0 / 0 / 155 | 5 / 0 / 329 |
| Titles realistic: tab ends, state names aside | 5 / 0 / 148 | 0 / 0 / 28 | 0 / 0 / 155 | 5 / 0 / 331 |
| Exact state header obstacles | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 341 |
| Slides: corner inset 8, ends 8 apart | 4 / 0 / 136 | 0 / 0 / 34 | 0 / 0 / 149 | 4 / 0 / 319 |
| Slides: ends 8 apart only | 4 / 0 / 140 | 0 / 0 / 34 | 0 / 0 / 155 | 4 / 0 / 329 |
| Slides: corner inset 8 (12 on rounded states) | 5 / 0 / 136 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 329 |
| Box growth N = 1 | 5 / 0 / 102 | 0 / 0 / 32 | 0 / 0 / 151 | 5 / 0 / 285 |
| Box growth N = 2 | 5 / 0 / 94 | 0 / 0 / 32 | 0 / 0 / 125 | 5 / 0 / 251 |
| Box growth N = 4 | 5 / 0 / 82 | 0 / 0 / 20 | 0 / 0 / 111 | 5 / 0 / 213 |
| Titles + corner inset + growth N = 2 | 3 / 0 / 90 | 0 / 0 / 26 | 0 / 0 / 119 | 3 / 0 / 235 |
| Titles + corner inset + growth N = 4 | 3 / 0 / 78 | 0 / 0 / 20 | 0 / 0 / 109 | 3 / 0 / 207 |
| All realistic, labels included | 3 / 0 / 78 | 0 / 0 / 20 | 0 / 0 / 92 | 3 / 0 / 190 |
| All realistic, never worse per diagram | 3 / 0 / 78 | 0 / 0 / 20 | 0 / 0 / 87 | 3 / 0 / 185 |
Labels
Component and package diagrams already route first and place labels afterwards (labels::Placer),
so labels never cause a bend there: every label lever leaves them unchanged. In state diagrams a label is
a dummy in the layered layout (layered::EdgeLabel), the line is routed in two halves through the
label’s anchor, and every label is an obstacle for every other line.
Upper bound. Keeping the layout, which still reserves the room, but routing with no waypoint and no
label obstacles saves 59 of 159 state bends, and adds 3 crossings. Freed back edges bundle into one
corridor and cross (TCP connection: the rcv ACK and timeout 2MSL returns). Dropping the reservation
as well saves 69 bends, makes the state diagrams 20% smaller, and adds 8 crossings.
Realistic variant: route, then place. After free routing, each label was placed beside its own line. The candidates were every segment, at five positions along it, on both sides, 8 or 20 px off the line. The score counted hits on boxes, headers, other labels and lines, and preferred long segments, the middle, the flowing side and the near offset. Of the 109 state transition labels:
| Placement | Collide with something | Far from their line (over 12 px) | Ambiguous (another line at least as near) |
|---|---|---|---|
| Baseline, reserved spots | 0 | 0 | 3 |
| Free routing, placed after | 9 | 0 | 22 |
| No reservation, placed by the existing placer | 16 | 0 | 11 |
So one label in twelve collides and one in five sits as close to a foreign line as to its own. The
hybrid pins every label that collides, sits far or reads ambiguously, and every line that gains a
crossing. A pinned label gets its waypoint and obstacle back, and the routing repeats until stable.
It pinned 32 of 109 labels and saved 11 bends. It is worse than the baseline in three diagrams
(language-labels and stress-right-regions gain bends, one diagram gains an overlap). Keeping the
baseline in those raises the saving to 26 bends with no new crossing or overlap.
Where the bends come from: most are short transitions between neighbouring ranks, whose label dummy sits
at a cross position chosen for label room, not for the line. In Elevator controller, near target floor,
speed zero, levelled at floor and fully open each jog only to pass their anchor.
Titles
Composite names (component, state) and package tabs are routing obstacles, and they also narrow the
span where ends may attach. A package’s top side starts right of its tab. A child’s top side under its
parent’s tab starts clear of it (tab_clear, under_tab). A package used as a component container
takes ends only on the right half of its top. An entry point slides along the border off the state’s
centred name (clear_of_name).
- As obstacles alone, names cost nothing. Ignoring every name and header obstacle changed no line in any golden file. A more exact state header obstacle, with the name centred and the activities where they are drawn, also changed nothing.
- Through attachment spans, they cost 12 bends (component 2, package 6, state 4). Ignoring them
outright is not drawable: arrows then pierce package tabs and the entry line runs through
Login. - Realistic. Package arrows that arrive from above may end on top of the tab (UML 12.2.4 makes the
tab part of the package symbol): −6 bends in
language-relationsandlanguage-cycle. A composite state’s name moves left or right when an entry point wants the middle: 2 of the 11 composite state names moved, −4 bends. Moving component composite names helped nowhere, and 9.2.4.1 asks for them centred. Across 25 titled composites and 20 titled packages, a name moved twice.
Box size
For each edge with bends, one end box grew by 1 to N grid steps (8 px) on the side toward the other end, on either axis. The smallest growth that reduced that edge’s bends was kept, when the diagram’s totals got no worse and no two connectors merged. Growth was allowed only for plain leaf boxes, kept 16 px from every other box, stayed inside the parent’s padding and header, and stayed out of reserved labels, self-transition loops and interface stubs. The layout itself did not change, so the diagram’s bounds are unchanged.
| N | Bends saved | Boxes grown | Area added | Share of box area |
|---|---|---|---|---|
| 1 | 56 | 20 | 27,520 px² | 0.9% |
| 2 | 90 | 34 | 50,816 px² | 1.6% |
| 4 | 128 | 51 | 97,408 px² | 3.1% |
By kind at N = 4: component −66 (2.5% of box area), package −14 (3.0%), state −48 (5.5%). The images
read better: Event-driven checkout loses all four jogs once Message Bus, Payments and Inventory
gain 8–24 px. The cost is unevenness: boxes in one rank no longer share a height, and a box can grow on
one side only, so its name is off the line of its neighbours’ names. At N = 2 this is hard to notice.
Attachment along the side. Stage 7 already slides ends between their neighbours. What remains is
the 16 px corner inset and the 16 px gap between ends. An 8 px inset and an 8 px gap save 22 bends and
one crossing, but two ends then reach the same point. Hexagonal service’s “fixed” crossing is two
connectors leaving Use Cases together and splitting, which the metric excuses because they share an
end. The gap alone causes this, and the inset alone does not: an 8 px inset on square boxes saves 12
component bends with no merged end. On rounded state boxes (corner radius 10) an 8 px inset puts the
arrowhead on the curve. At 12 px it saves nothing.
Every bend by cause
Each of the 341 bends counted once, by its first cause in this order: label, title, attachment, box size, crowding by other lines, then geometry. A cause is credited only with bends its own counterfactual removed for that edge. Crowding was tested by routing each bent edge alone in the same geometry. The geometric tests take the two end boxes: 0 bends are needed if their attach ranges overlap across the gap, 1 if they are apart on both axes, and 2 otherwise. Bends beyond that minimum are a side choice when a clear L exists, or in the way when another box or label blocks the straight corridor. Bends within the minimum are necessary when the end shares its side with an edge to another node (a fan), and otherwise placement.
| Cause | Component | Package | State | Total |
|---|---|---|---|---|
| Label (reservation, waypoint, obstacle) | 0 | 0 | 62 | 62 |
| Title (spans narrowed by tabs and names) | 2 | 6 | 4 | 12 |
| Attachment: corner inset and gap | 12 | 0 | 2 | 14 |
| Attachment: port policy (inner straight, outer jogs) | 32 | 0 | 0 | 32 |
| Box size | 58 | 8 | 15 | 81 |
| Crowding by other lines | 0 | 0 | 0 | 0 |
| Side choice: a Z where a clear L fits | 11 | 6 | 11 | 28 |
| Rank or ordering: a box or label in the way | 12 | 2 | 22 | 36 |
| Rank or ordering: ends misaligned with no competing edge | 0 | 1 | 9 | 10 |
| Necessary: fan-in or fan-out on one side | 17 | 11 | 28 | 56 |
| Defect or limit | 4 | 0 | 6 | 10 |
| Total | 148 | 34 | 159 | 341 |
Box growth alone removes 128 bends, but only 81 are credited to it, because labels and attachment come first in the order. No bend is caused by other lines: routing each bent edge alone gave the same bends, so the router’s sharing and crossing penalties are not what bends lines.
The 10 defect bends are two patterns. In component diagrams (Compiler pipeline twice), the typed AST
line jogs between containers: each container is laid out on its own, so Type Checker and IR Builder
do not line up across them. This is the recursive arrange limit noted in the aesthetics report. In
state diagrams, a reverse pair between a state and a composite’s border (Off and Playing, Idle and
Operating) has 28 px of common span, too little for two ends at the 24 px slot spacing, so both lines
jog.
The five crossings
| Diagram | Lines | Cause |
|---|---|---|
| component/introduction-01 (Order Platform) | Billing to Event Bus crosses the SQL dependency from Order Service to Postgres | Rank or ordering. Event Bus sits above Postgres inside Infrastructure, but its clients lie above and below the SQL line. The container orders its children without seeing outside lines (recursive layout). |
| component/example-orders (Order Platform) | the same two lines | The same diagram as above. |
| component/example-simple (Compiler Pipeline) | invoke from Driver to Code Generator crosses SSA from Optimizer | Attachment order. The slot of invoke on Code Generator is left of SSA’s, because Driver is to the left, but the route has to come down the right of Front End. Slots are ordered by the peer’s position before routing. |
| component/gallery-tools-01 (Compiler pipeline) | the same two lines | The same diagram as above. |
| component/gallery-services-01 (Hexagonal service) | Use Cases to the repo port crosses Use Cases to Domain Model | Attachment order on Use Cases’ right side, made necessary by ranking. Domain Model sits between Use Cases and Core’s right-hand ports, so the delegation has to climb over it, yet its slot is below the other line’s. |
Box growth together with the corner inset removes the two Compiler pipeline crossings: Driver grows
8 px and invoke then leaves on the right. Nothing realistic removed the other three. Fixing them
needs ordering across containers (Forster, Sander) or slot order taken from the route.
Ranking
Bends and crossings saved, weighed against effort and risk.
- Grow end boxes to straighten connectors (−90 bends at N = 2, −128 at N = 4, no crossings, +1.6% or +3.1% box area).
Sketch: a pure stage in
coreafter layout and slot settling. For each edge whose two end ranges do not overlap, and whose corridor is clear, it computes the growth that makes them overlap by at least one slot. It grows a leaf when the room around it allows (16 px clearance, parent padding, keep-out rectangles from the adapter), then routes once. A second round catches edges that one growth unblocked. Adapters pass which nodes may grow and their keep-out areas, as they pass obstacles now. Rendering needs nothing, since names centre in the final rect. Effort: 2–3 days with tests and re-blessed goldens. Risk: uneven sizes in a rank. Start at N = 2, and consider growing all boxes of a rank together. - Corner inset of 8 px on square boxes (−12 bends). Sketch: pass 8 instead of 16 to the slide ranges of component and package boxes, and keep 16 on rounded state boxes and the 16 px gap between ends. Effort: hours. Risk: low. Do not shrink the gap: that merges ends.
- Arrows may end on a package tab (−6 bends). Sketch: in package routing, a top slot inside the tab’s span gets the tab’s top edge as its point, and the top side’s span starts at the left corner. Effort: half a day. Risk: low. UML 12.2.4 draws the tab as part of the symbol.
- Label hybrid in state diagrams (−26 bends). Sketch: route without waypoints, place labels beside their own lines with a quality check (no collision, not far, not ambiguous), pin the failures back to their reserved spot and re-route, and keep the reserved drawing for a diagram where this is worse. This needs a label-quality measure in the quality test first, so that a regression shows. Effort: 3–5 days. Risk: medium to high. Ambiguous labels are worse than a jog (see the principles), and freed back edges cross.
- State names give way to entry points (−4 bends). Sketch: when an entry point wants the middle of a composite’s top, put the name left (or in a UML name tab, 14.2.4.4) instead of sliding the entry point off it. Effort: a day. Risk: medium, because names in one diagram are then aligned differently.
Not recommended: dropping label reservation (+8 crossings, 16 collisions); shrinking the gap between ends (merged connectors); moving component composite names (no gain, and 9.2.4.1 asks for them centred).
Expected totals. Levers 1 to 3 and 5 at N = 4: 3 crossings, 0 overlaps, 207 bends, down from 5/0/341 (−39% bends), measured together. At N = 2: 3/0/235. Adding the label hybrid: 3/0/185.
Further levers found on the way, not measured as realistic variants: choosing an L over a Z when the L is clear (28 bends), letting an outer connector take the straight line at a port when the delegation inside has room (32 bends), slot order taken from the route’s direction of arrival (3 of 5 crossings), and crossing reduction across containers (2 crossings, 4 bends).
Images checked
Rasterized with resvg at 1.5× and inspected:
- Labels: Elevator controller and TCP connection, baseline, free placement and hybrid. Free
placement straightens
Moving→Stopping→LevelledandClosed→Opening→Open, but putsnear target flooron thenext callline and lets the returns toIdleandClosedcross. The hybrid keeps the straight lines, but in TCPrcv ACKfloats between two parallel returns. - Titles: Relations (package), baseline, names ignored and tab ends. Ignoring names lets arrows
pierce the tabs. Ending on the tab reads correctly. Entry and exit points: ignoring the name runs the
entry line through
Login, which is why the realistic variant moves the name instead. - Box size: Event-driven checkout and Connectors (component), Elevator controller (all levers). Every saved bend reads better. Uneven heights show at N = 4 in Connectors.
- Attachment: Long labels (state) at an 8 px inset puts the arrowhead into
B’s rounded corner. Hexagonal service at an 8 px gap merges two connectors. - Crossings: Order Platform, Compiler Pipeline and Hexagonal service, baseline and all levers.
Method notes
- Metrics are those of
core::metricsover the router’s polylines. A merged pair of ends, two different connectors leaving one point along one line, was counted separately, becauseoverlapsexcuses lines that share an end. The baseline has two merged pairs, both intentional fan-ins to one port. - Growth was greedy in edge order and verified by re-routing the whole diagram per trial. A real implementation should predict growth geometrically and route once or twice.
- The label placer of the realistic variant was written for the experiment. The existing
Placerallows only single-line labels and five positions. Its collision and ambiguity counts are therefore a fair estimate, not a lower bound.