Your layout is not sparse because of spacing — your graph is not a DAG

Because layered layout assumes a roughly acyclic graph and yours almost certainly is not. Spacing was our first suspect too, and it was innocent. The map Backthread draws of a system that agents keep rewriting — the picture a team reads to see where each change landed and who understands that area — spent weeks rendering at 8.1 percent fill.

What we were looking at

The top level of our own demo repository is 38 boxes: the containers of the system, with the dependencies between them as edges. Rendered with a layered engine at default settings, those 38 boxes came out spread across 28 layers, occupying 8.1 percent of a canvas measuring 14,277 by 15,992 pixels. Nearly 92 percent of the drawing was white space, and the only zoom level at which the whole thing fit on screen was one where no label could be read.

This is a common complaint and the common answers are about aesthetics: reduce the node padding, shrink the font, tighten the spacing constants. We tried the measurement first.

The two suspects we cleared

Node size. The boxes were not oversized relative to the canvas. At 8.1 percent fill, you could have doubled every box and still had three quarters of the drawing empty. Node size cannot explain a gap of that magnitude; it can only explain a few percent either way.

Spacing. This is the suspect everyone names, and it is the easiest to test: measure the gaps that actually came out and compare them to the configuration. The median gap between adjacent boxes was exactly 120 pixels, which is exactly what we had asked for. The engine was not inflating anything. It was doing what it was told, in a structure that made the result absurd.

Clearing those two first is the part worth copying. Both are plausible, both are cheap to change, and changing either one would have produced a small improvement that felt like progress and ended the investigation.

What was actually wrong

A layered engine runs in phases, and the first one makes the graph acyclic by reversing edges, because layer assignment requires a directed acyclic graph as input. That phase is a repair job. It is designed for a graph with a few back edges in an otherwise forward-flowing structure.

Ours was not that. Of 261 container pairs with any dependency between them, 166 were reciprocal — A depends on B and B depends on A. That is 64 percent of the relationships pointing both ways. It is also entirely normal for application code: a module and its helpers, a service and its client, a model and the repository that loads it all legitimately reference each other.

Hand that to cycle breaking and the repair becomes the dominant operation. Every reversed edge creates a constraint that pushes nodes apart in the layering, and with two thirds of the graph reciprocal the engine has no choice but to keep adding layers to satisfy them. Twenty-eight layers for 38 boxes is not a bug in the implementation. It is the correct output for an input the algorithm was never meant to receive.

The giveaway, in hindsight: layer count close to node count. If your layered drawing has nearly as many layers as it has boxes, you do not have a spacing problem.

What fixed it

Not a setting on the layered engine. We tried that too, properly, before giving up on it.

What we changedCanvas fillBoxes that moved after adding one edge
Layered engine, default configuration8.1%37 of 38
Layered engine, spacing and sizing tuned8.6%not re-measured
Rectangle packing at the top level50.0%0 of 38

Tuning bought half a percentage point. Switching the top level from layered layout to packing — treating the containers as boxes to arrange rather than as a hierarchy to rank — took fill to 50.0 percent and made the whole map legible at 3.3 times the previous zoom.

The reasoning behind the switch is that the top level of a system is not a hierarchy. Layered layout earns its complexity when direction means something: a build pipeline, a call graph with a clear entry point, a state machine. For a set of containers that all talk to each other, rank is a fiction the algorithm has to invent, and inventing it is what costs the space. Packing drops the fiction and arranges by size instead.

The side effect we were not aiming at

The same change fixed a defect we had been treating as unrelated. Perturbing the graph by one edge and re-laying it out moved 37 of 38 boxes under the layered engine. Under packing it moved 0 of 38.

That is the same instability we wrote about when a diagram reshuffles completely after a small commit — there the fix was laying out a canonical superset once and rendering each version as a subset of it. This result says something narrower and more useful: part of the churn people blame on regeneration is actually cycle breaking re-solving a graph it cannot satisfy. Remove the ranking requirement and a chunk of the movement disappears on its own, before any canonical-layout work.

What these numbers do not show

  • One graph, one shape, one level. Thirty-eight containers from one repository at the top level. A codebase with genuinely layered architecture behaves differently here; measure the reciprocal-pair share on yours rather than comparing fill percentages against ours.
  • Packing is not better, it is differently wrong. It arranges by size and preserves order, which means it carries no information about direction at all. Inside a container, where dependencies do flow one way, we still use layered layout and it still earns its place.
  • We did not isolate cycle breaking directly. The evidence is a correlation between a reciprocal-heavy input and a layer count near the node count, plus a fix that removes the ranking step entirely. We did not instrument how many edges the phase reversed, which would have been the cleaner experiment.
  • The tuned row is incomplete. We stopped measuring the tuned configuration once fill came in at 8.6 percent, so the stability column has no entry. It was not worth another run.

If you are debugging this now

  1. Count your layers against your nodes. Close together means the structure is fighting the algorithm, not that the spacing is wrong.
  2. Count reciprocal pairs. Pairs with edges both ways, over pairs with any edge. Above roughly half, a layered engine is solving the wrong problem for you.
  3. Measure one gap before you change a constant. If the median gap equals your configuration, spacing is exonerated and you have saved yourself a day.
  4. Try packing at the outermost level only. Keep layered where direction is real. The mixed answer beat both pure ones for us.

We publish the ones we got wrong because the record of what we changed and why is the same artefact we keep for a system a team has to go on understanding, and this investigation is in ours with the exonerated suspects still attached.

Connect one repo and the top-level map of your own system arrives the same day, along with the reciprocal-pair count that tells you how layered your architecture actually is; the trial runs fourteen days.

In short

Measure the gap before you touch the spacing constant
Our median gap came out at exactly the 120 pixels we had configured, which cleared spacing as a cause in one measurement. Node size was cleared the same way: at 8.1 percent fill, doubling every box would still have left three quarters of the canvas empty.
Reciprocal dependencies, not spacing, are what smear a layered drawing
166 of 261 container pairs pointed both ways. Cycle breaking has to reverse edges to produce the acyclic graph that layer assignment needs, and at that density the repair dominates, which is how 38 boxes ended up across 28 layers.
Packing beat tuning by two orders of magnitude of effect
Tuning the layered engine moved canvas fill from 8.1 to 8.6 percent. Replacing it with rectangle packing at the top level moved it to 50.0 percent and made the map legible at 3.3 times the zoom.
The same change removed the per-commit churn
Adding one edge moved 37 of 38 boxes under the layered engine and 0 of 38 under packing. Some of the instability blamed on regenerating diagrams is cycle breaking re-solving an input it cannot satisfy.

Sources

  1. Layered (overview) — Eclipse Layout Kernel documentation on the five phases, cycle breaking first
  2. ELK Rectangle Packing — the algorithm reference for packing unconnected boxes
  3. Methods for Visual Understanding of Hierarchical System Structures — Sugiyama, Tagawa and Toda, IEEE TSMC, 1981
  4. Hierarchical Drawing Algorithms — Healy and Nikolov, Handbook of Graph Drawing and Visualization

Backthread shows how much of what your agents built your team really understands. See how it works