PlantUML rendering engine internals: 7 Graphviz/dot tuning knobs

puml.online

Most PlantUML diagrams (component, state, object, class) are laid out by Graphviz’s dot algorithm underneath. Understanding how dot works explains many “why does the diagram auto-arrange like that?” mysteries, and how to make it look better.

What the dot algorithm is

When PlantUML renders component / class / state, its parser first converts .puml to Graphviz’s dot language, then the dot engine calculates the layout. The final SVG comes from dot’s positions plus PlantUML’s own style sheet.

This means: you can leverage all of dot’s features to influence PlantUML diagrams. PlantUML wraps them as skinparam, !pragma, etc., but understanding the layer below helps diagnose weird layouts.

Knob 1: rankdir changes direction

1
2
3
4
5
6
@startuml
left to right direction
component A
component B
A --> B
@enduml

Underlying dot:

1
2
3
4
digraph {
rankdir=LR
A -> B
}

rankdir accepts: LR (left-to-right), TB (default, top-to-bottom), BT, RL.

When to use left to right:

  • Sequence diagram too wide horizontally → switch to top to bottom (default)
  • Deployment diagram has many zones → left to right lines them up

Knob 2: nodesep / ranksep control spacing

1
2
3
4
5
6
7
8
9
@startuml
skinparam nodesep 30
skinparam ranksep 50
component A
component B
component C
A --> B
B --> C
@enduml

nodesep: spacing between nodes on the same rank (same row/column). Default 40 (px). Nodes too cramped → increase.

ranksep: spacing between different ranks (rows/columns). Default 60. Hierarchy unclear → increase.

Knob 3: invisible nodes for layout control

1
2
3
4
5
6
7
8
9
10
11
12
@startuml
component A
component B
component C
A --> B
A --> C

' invisible node forces B and C onto the same rank
A --> (hub)
(hub) --> B
(hub) --> C
@enduml

(hub) is an anonymous node — dot treats it as a real node for rank calculation. It pulls multiple targets onto the same rank.

Field usage: in an architecture diagram where multiple services all connect to one DB, the DB naturally drops down a level — add invisible nodes to center the DB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
component "auth-svc" as auth
component "order-svc" as order
component "pay-svc" as pay

database "PostgreSQL" as pg

auth --> (auth_pg) : SQL
order --> (order_pg) : SQL
pay --> (pay_pg) : SQL

(auth_pg) --> pg
(order_pg) --> pg
(pay_pg) --> pg
@enduml

Knob 4: hidden edges for alignment

1
2
3
4
5
6
7
@startuml
component A
component B
component C
A --> B
A -[hidden]- C
@enduml

-[hidden]- is dot’s special edge type — used for layout only, doesn’t render. A and C get pulled to the same rank.

Field usage: two services with no direct relationship but need horizontal alignment:

1
2
3
4
5
6
7
8
component "Web App" as web
component "API Gateway" as gw
component "Admin Tool" as admin

web --> gw
admin --> gw
web -[hidden]- admin
@enduml

Web App and Admin Tool align horizontally.

Knob 5: subgraph grouping (package / node)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@startuml
package "Frontend" {
component "Web" as web
component "Mobile" as mob
}

package "Backend" {
component "API" as api
component "Worker" as wkr
}

web --> api
mob --> api
api --> wkr
@enduml

dot treats package as a subgraphautomatically draws a bounding box and packs the nodes inside.

Note: subgraphs don’t affect edge direction, only visual grouping.

Knob 6: nested cluster

1
2
3
4
5
6
7
8
9
10
11
12
13
@startuml
node "VPC" {
node "Public Subnet" {
component "ALB" as alb
}
node "Private Subnet" {
component "App" as app
database "DB" as db
}
}
alb --> app
app --> db
@enduml

Nested node produces nested clusters — classic AWS architecture diagram pattern.

Knob 7: edge weight

1
2
3
4
5
6
7
8
9
10
@startuml
component A
component B
component C
component D

A --> B : "1 (default)"
A --> C : "weight 10"
A --> D : "weight 5"
@enduml

In dot, edges have weights — higher weight edges are prioritized for straight alignment within the same rank, lower weight edges detour. weight 10 forces A and C onto the same rank, aligned.

Field usage: critical path goes straight, secondary path detours:

1
2
3
4
5
6
7
8
9
component "User" as user
component "API" as api
component "Cache" as cache
component "DB" as db

user --> api : "weight 10"
api --> db : "weight 10"
api --> cache : "weight 1"
@enduml

User → API → DB is the straight main path; API → Cache is the side path.

How to debug weird layouts

Method 1: export the dot file

PlantUML CLI:

1
2
plantuml -tlog diagram.puml
# INFO outputs the actual generated dot code

Even more detailed:

1
2
3
plantuml -tdot diagram.puml
# outputs a .dot file; render it directly with Graphviz
dot -Tsvg diagram.dot -o diagram-from-dot.svg

Compare plantuml output and dot output — if dot’s own layout is odd, the problem is in dot, not PlantUML.

Method 2: -stdlib to simplify

1
plantuml -tsvg -Sstdlib=true diagram.puml

-Sstdlib=true disables PlantUML’s stdlib themes, showing what default dot layout looks like.

Method 3: !pragma layout directive

1
2
3
4
@startuml
!pragma layout elk
' or !pragma layout neato
@enduml

PlantUML 1.2023+ supports elk and neato engines. elk (Eclipse Layout Kernel) handles complex diagrams better.

Edge label position: PlantUML-only

1
2
3
4
5
@startuml
A --> B : above
A --> B : <<on right>> on right
A --> B : <<on bottom>> below
@enduml

The <<...>> syntax controls edge label placement — PlantUML implements this itself, dot doesn’t.

Common:

  • <<on left>> <<on right>>: left or right side
  • <<top>> <<bottom>>: above or below
  • <<start>> <<end>>: along edge start or end

Where dot struggles

1. Too many nodes (>100)

dot’s complexity is O(V log V + E) — 200+ nodes make layout visibly slow, results often chaotic.

Fix:

  • Use elk engine: !pragma layout elk
  • Split into multiple diagrams
  • Use cluster nesting to reduce nodes at the same layout level

2. Heavy edge crossings

dot minimizes crossings but can’t guarantee global optimum.

Fix:

  • Tune positions (invisible nodes / hidden edges)
  • Try different rankdir and see which has fewer crossings
  • Split the diagram

3. Giant sequence diagrams (50+ lifelines)

dot performs poorly on sequence diagrams — sequence diagrams use PlantUML’s built-in algorithm, not dot.

Field example: turning a messy diagram tidy

1
2
3
4
5
6
7
8
9
10
11
@startuml
' original — A in center, 8 surrounding nodes, crammed
A --> B
A --> C
A --> D
A --> E
A --> F
A --> G
A --> H
A --> I
@enduml

After tuning:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@startuml
skinparam nodesep 30
skinparam ranksep 40

' 4 in one row, 4 in another
A --> B
A --> C
A --> D
A --> E

A --> F
A --> G
A --> H
A --> I

' force B-E in one row, F-I in another
B -[hidden]- C
C -[hidden]- D
D -[hidden]- E
F -[hidden]- G
G -[hidden]- H
H -[hidden]- I
@enduml

Two clearly separated ranks, A centered, nodes evenly distributed.

Summary

Scenario Knob
Wrong direction left to right direction
Nodes too cramped skinparam nodesep / ranksep
Multiple targets need alignment invisible node (hub)
Two unrelated components need alignment -[hidden]-
Group layout package node
Main path should be straight weight 10
Debug weird diagram -tdot export dot file

The core idea for tuning PlantUML layout: dot is the underlying engine, PlantUML is its DSL. Use skinparam for skin, use dot concepts (hidden, invisible nodes, weight) for layout.

  • Title: PlantUML rendering engine internals: 7 Graphviz/dot tuning knobs
  • Author: puml.online
  • Created at : 2026-07-30 17:05:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-graphviz-internals-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.