Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

LeverUpper boundRealistic variantCrossingsArea costEffortRisk
Box growth, up to 4 grid steps—−128 bends0+3.1% of box area, no growth of the diagramMedium (2–3 days)Low to medium: uneven box sizes
Box growth, up to 2 grid steps—−90 bends0+1.6% of box areaMediumLow
Box growth, 1 grid step—−56 bends0+0.9% of box areaMediumLow
Corner inset for ends (attachment)−22 bends, −1 crossing, but 3 merged ends−12 bends0noneVery small (hours)Low
Titles (names and tabs)−12 bends−10 bends0noneSmallLow (tabs), medium (state names)
Labels not forcing bends (state)−59 bends, +3 crossings−26 bends0noneLarge (3–5 days)Medium to high
Labels, no reservation at all−69 bends, +8 crossingsnot usable+8−20% diagram area—High
All realistic levers but labels3 crossings, 0 overlaps, 207 bends−2as box growth
All realistic levers3 crossings, 0 overlaps, 185 bends−2as 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).

VariantComponentPackageStateTotal
Baseline5 / 0 / 1480 / 0 / 340 / 0 / 1595 / 0 / 341
Labels free: no waypoint, no label obstacles5 / 0 / 1480 / 0 / 343 / 0 / 1008 / 0 / 282
Labels, no reservation in the layout5 / 0 / 1480 / 0 / 348 / 0 / 9013 / 0 / 272
Labels hybrid, raw5 / 0 / 1480 / 0 / 340 / 1 / 1485 / 1 / 330
Labels hybrid, never worse per diagram5 / 0 / 1480 / 0 / 340 / 0 / 1335 / 0 / 315
Names ignored as obstacles only5 / 0 / 1480 / 0 / 340 / 0 / 1595 / 0 / 341
Names and tabs ignored everywhere5 / 0 / 1460 / 0 / 280 / 0 / 1555 / 0 / 329
Titles realistic: tab ends, state names aside5 / 0 / 1480 / 0 / 280 / 0 / 1555 / 0 / 331
Exact state header obstacles5 / 0 / 1480 / 0 / 340 / 0 / 1595 / 0 / 341
Slides: corner inset 8, ends 8 apart4 / 0 / 1360 / 0 / 340 / 0 / 1494 / 0 / 319
Slides: ends 8 apart only4 / 0 / 1400 / 0 / 340 / 0 / 1554 / 0 / 329
Slides: corner inset 8 (12 on rounded states)5 / 0 / 1360 / 0 / 340 / 0 / 1595 / 0 / 329
Box growth N = 15 / 0 / 1020 / 0 / 320 / 0 / 1515 / 0 / 285
Box growth N = 25 / 0 / 940 / 0 / 320 / 0 / 1255 / 0 / 251
Box growth N = 45 / 0 / 820 / 0 / 200 / 0 / 1115 / 0 / 213
Titles + corner inset + growth N = 23 / 0 / 900 / 0 / 260 / 0 / 1193 / 0 / 235
Titles + corner inset + growth N = 43 / 0 / 780 / 0 / 200 / 0 / 1093 / 0 / 207
All realistic, labels included3 / 0 / 780 / 0 / 200 / 0 / 923 / 0 / 190
All realistic, never worse per diagram3 / 0 / 780 / 0 / 200 / 0 / 873 / 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:

PlacementCollide with somethingFar from their line (over 12 px)Ambiguous (another line at least as near)
Baseline, reserved spots003
Free routing, placed after9022
No reservation, placed by the existing placer16011

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-relations and language-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.

NBends savedBoxes grownArea addedShare of box area
1562027,520 px²0.9%
2903450,816 px²1.6%
41285197,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.

CauseComponentPackageStateTotal
Label (reservation, waypoint, obstacle)006262
Title (spans narrowed by tabs and names)26412
Attachment: corner inset and gap120214
Attachment: port policy (inner straight, outer jogs)320032
Box size5881581
Crowding by other lines0000
Side choice: a Z where a clear L fits1161128
Rank or ordering: a box or label in the way1222236
Rank or ordering: ends misaligned with no competing edge01910
Necessary: fan-in or fan-out on one side17112856
Defect or limit40610
Total14834159341

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

DiagramLinesCause
component/introduction-01 (Order Platform)Billing to Event Bus crosses the SQL dependency from Order Service to PostgresRank 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 linesThe same diagram as above.
component/example-simple (Compiler Pipeline)invoke from Driver to Code Generator crosses SSA from OptimizerAttachment 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 linesThe same diagram as above.
component/gallery-services-01 (Hexagonal service)Use Cases to the repo port crosses Use Cases to Domain ModelAttachment 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.

  1. 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 core after 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 → Levelled and Closed → Opening → Open, but puts near target floor on the next call line and lets the returns to Idle and Closed cross. The hybrid keeps the straight lines, but in TCP rcv ACK floats 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::metrics over the router’s polylines. A merged pair of ends, two different connectors leaving one point along one line, was counted separately, because overlaps excuses 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 Placer allows only single-line labels and five positions. Its collision and ambiguity counts are therefore a fair estimate, not a lower bound.