Skip to content

Measuring complexity

A diagram stops helping once a reader cannot hold it in mind. The complexity rules put a number on that point, so a review can say "split this" without an argument about taste.

Two measures

Node count is the number of nodes mermaid's parser reads from the diagram. What counts as a node depends on the diagram type, as What counts as a node lists.

Visual complexity score adds what nodes alone miss:

score = (nodes + 0.5 × edges + 0.25 × rows + 3 × subgraphs) × (1 + 0.1 × nesting depth)

Edges add clutter, each subgraph adds a boundary to track, and each level of nesting makes all of it harder to follow. A row is a line of text inside a node, such as an entity's attribute, and only an erDiagram or a classDiagram has rows. For a mindmap, treemap, treeView or ishikawa, the nesting depth in the score is the tree's depth below its root. The four weights are options: edgeWeight, rowWeight, subgraphWeight and depthWeight.

The score, one idea at a time

The steps below build the score from a single idea upwards. Each step shows a small diagram's source, then the structure mermaid's parser returned for it. That graph is not drawn from the source text. The site build runs the linter's own parse engine over each example, and draws the nodes, edges and subgraphs it read.

A subgraph is drawn as a dashed box around its members. The worked score under each graph is also computed in that build, with the default weights.

Nodes

Start with nodes and nothing else. Each node is one thing the reader must find and name, so each adds one point. Six unconnected nodes score 6.

Mermaid source

flowchart LR
  A[Order]
  B[Payment]
  C[Stock]
  D[Packing]
  E[Shipping]
  F[Receipt]

The diagram as drawn

flowchart LR
  A[Order]
  B[Payment]
  C[Stock]
  D[Packing]
  E[Shipping]
  F[Receipt]

What the parser returned: 6 nodes, 0 edges, 0 subgraphs

depth = max(subgraph nesting 0, tree depth 0) = 0

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (6 + 0.5 × 0 + 3 × 0) × (1 + 0.1 × 0)
      = (6 + 0 + 0) × 1
      = 6 × 1
      = 6

Edges

Join the same six nodes with five edges. An edge is a line to trace, but it is lighter than a node, so each adds half a point. The diagram now scores 8.5.

Mermaid source

flowchart LR
  A[Order]
  B[Payment]
  C[Stock]
  D[Packing]
  E[Shipping]
  F[Receipt]
  A --> B
  A --> C
  B --> F
  C --> D
  D --> E

The diagram as drawn

flowchart LR
  A[Order]
  B[Payment]
  C[Stock]
  D[Packing]
  E[Shipping]
  F[Receipt]
  A --> B
  A --> C
  B --> F
  C --> D
  D --> E

What the parser returned: 6 nodes, 5 edges, 0 subgraphs

depth = max(subgraph nesting 0, tree depth 0) = 0

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (6 + 0.5 × 5 + 3 × 0) × (1 + 0.1 × 0)
      = (6 + 2.5 + 0) × 1
      = 8.5 × 1
      = 8.5

Subgraphs

Group five of the nodes into two subgraphs, Billing and Warehouse. A subgraph is a boundary the reader must track, so each adds three points. Grouping also creates one level of nesting, which scales the whole sum by 1.1. The diagram now scores 15.95.

Mermaid source

flowchart LR
  A[Order]
  subgraph billing [Billing]
    B[Payment]
    F[Receipt]
  end
  subgraph warehouse [Warehouse]
    C[Stock]
    D[Packing]
    E[Shipping]
  end
  A --> B
  A --> C
  B --> F
  C --> D
  D --> E

The diagram as drawn

flowchart LR
  A[Order]
  subgraph billing [Billing]
    B[Payment]
    F[Receipt]
  end
  subgraph warehouse [Warehouse]
    C[Stock]
    D[Packing]
    E[Shipping]
  end
  A --> B
  A --> C
  B --> F
  C --> D
  D --> E

What the parser returned: 6 nodes, 5 edges, 2 subgraphs

depth = max(subgraph nesting 1, tree depth 0) = 1

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (6 + 0.5 × 5 + 3 × 2) × (1 + 0.1 × 1)
      = (6 + 2.5 + 6) × 1.1
      = 14.5 × 1.1
      = 15.95

Nesting depth

Move Packing and Shipping into a Dispatch subgraph inside Warehouse. The extra subgraph adds three points, and the second level of nesting raises the scale to 1.2. Nesting multiplies the score rather than adding to it, because a deeper diagram makes every element harder to place. The diagram now scores 21.

Mermaid source

flowchart LR
  A[Order]
  subgraph billing [Billing]
    B[Payment]
    F[Receipt]
  end
  subgraph warehouse [Warehouse]
    C[Stock]
    subgraph dispatch [Dispatch]
      D[Packing]
      E[Shipping]
    end
  end
  A --> B
  A --> C
  B --> F
  C --> D
  D --> E

The diagram as drawn

flowchart LR
  A[Order]
  subgraph billing [Billing]
    B[Payment]
    F[Receipt]
  end
  subgraph warehouse [Warehouse]
    C[Stock]
    subgraph dispatch [Dispatch]
      D[Packing]
      E[Shipping]
    end
  end
  A --> B
  A --> C
  B --> F
  C --> D
  D --> E

What the parser returned: 6 nodes, 5 edges, 3 subgraphs

depth = max(subgraph nesting 2, tree depth 0) = 2

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (6 + 0.5 × 5 + 3 × 3) × (1 + 0.1 × 2)
      = (6 + 2.5 + 9) × 1.2
      = 17.5 × 1.2
      = 21

At three levels, nesting also fires its own rule, as Each measure is a ladder explains.

Tree depth

A mindmap draws no subgraphs, but a deep tree is as hard to follow as deep nesting. For a tree-shaped type, the depth in the score is how far the tree reaches below its root. Every entry is a node, and every link from parent to child is an edge. This mindmap reaches three levels below its root, so it scores 13.

Mermaid source

mindmap
  root((Release))
    Build
      Compile
      Test
    Ship
      Package
        Sign

The diagram as drawn

mindmap
  root((Release))
    Build
      Compile
      Test
    Ship
      Package
        Sign

What the parser returned: 7 nodes, 6 edges, 0 subgraphs

depth = max(subgraph nesting 0, tree depth 3) = 3

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (7 + 0.5 × 6 + 3 × 0) × (1 + 0.1 × 3)
      = (7 + 3 + 0) × 1.3
      = 10 × 1.3
      = 13

The full score

A real diagram combines all of these. This shop architecture has more nodes, more edges and five subgraphs, nested two levels deep. It scores 45.

Mermaid source

flowchart TD
  CUST[Customer]
  subgraph web [Web]
    UI[Storefront]
    API[API gateway]
  end
  subgraph services [Services]
    subgraph orders [Orders]
      OS[Order service]
      OQ[Order queue]
    end
    subgraph billing [Billing]
      PS[Payment service]
      INV[Invoices]
    end
    subgraph stock [Stock]
      SS[Stock service]
      WH[Warehouse feed]
    end
  end
  DB[(Orders DB)]
  PDB[(Payments DB)]
  SDB[(Stock DB)]
  MAIL[Email]
  PSP[Card processor]
  LOG[Audit log]
  CUST --> UI
  UI --> API
  API --> OS
  OS --> OQ
  OQ --> PS
  OQ --> SS
  OS --> DB
  PS --> PDB
  PS --> PSP
  PS --> INV
  INV --> MAIL
  SS --> SDB
  WH --> SS
  OS --> LOG
  PS --> LOG

The diagram as drawn

flowchart TD
  CUST[Customer]
  subgraph web [Web]
    UI[Storefront]
    API[API gateway]
  end
  subgraph services [Services]
    subgraph orders [Orders]
      OS[Order service]
      OQ[Order queue]
    end
    subgraph billing [Billing]
      PS[Payment service]
      INV[Invoices]
    end
    subgraph stock [Stock]
      SS[Stock service]
      WH[Warehouse feed]
    end
  end
  DB[(Orders DB)]
  PDB[(Payments DB)]
  SDB[(Stock DB)]
  MAIL[Email]
  PSP[Card processor]
  LOG[Audit log]
  CUST --> UI
  UI --> API
  API --> OS
  OS --> OQ
  OQ --> PS
  OQ --> SS
  OS --> DB
  PS --> PDB
  PS --> PSP
  PS --> INV
  INV --> MAIL
  SS --> SDB
  WH --> SS
  OS --> LOG
  PS --> LOG

What the parser returned: 15 nodes, 15 edges, 5 subgraphs

depth = max(subgraph nesting 2, tree depth 0) = 2

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (15 + 0.5 × 15 + 3 × 5) × (1 + 0.1 × 2)
      = (15 + 7.5 + 15) × 1.2
      = 37.5 × 1.2
      = 45

The score on its own says nothing about whether 45 is too much. That depends on the audience, which is what a preset sets.

Presets set the thresholds

A preset is a named set of thresholds for one density of diagram. The default is high-density, which follows Huang et al. 2020. That study found that around 50 nodes is where a network diagram becomes hard to read, and that 100 is the limit.

Option low-density medium-density high-density
nodeIdeal 8 12 20
nodeAcceptable 12 20 35
nodeComplex 20 35 50
nodeHardLimit 35 60 100
vcsIdeal 15 25 35
vcsAcceptable 25 40 60
vcsComplex 40 70 100
vcsCritical 60 100 150
nodeTarget 10 15 25
vcsTarget 20 30 40

Pick low-density or medium-density for audiences that read diagrams quickly, such as slides or onboarding docs.

Each measure is a ladder

The thresholds form bands, and a diagram lands in exactly one band per measure. Only the rule for that band fires, so one diagram never gets three findings for the same problem.

Measure Band Rule
Nodes above nodeAcceptable, up to nodeComplex complexity-node-count-acceptable-exceeded
Nodes above nodeComplex, up to nodeHardLimit complexity-node-count-cognitive-limit
Nodes above nodeHardLimit complexity-node-count-hard-limit
Score above vcsAcceptable, up to vcsComplex complexity-visual-score-acceptable-exceeded
Score above vcsComplex complexity-visual-score-critical

nodeComplex, nodeHardLimit and vcsComplex each bound two neighbouring bands, so two rules read each of them. nodeAcceptable and vcsAcceptable bound only the lowest band, so one rule reads each.

The shop architecture from The full score lands in a different band under each preset. Each row below is a real run of the complexity rules over that diagram.

Preset Nodes nodeAcceptable / nodeComplex / nodeHardLimit Score vcsAcceptable / vcsComplex Rules that fire
low-density 15 12 / 20 / 35 45 25 / 40 complexity-node-count-acceptable-exceeded
complexity-visual-score-critical
medium-density 15 20 / 35 / 60 45 40 / 70 complexity-visual-score-acceptable-exceeded
high-density 15 35 / 50 / 100 45 60 / 100 None

Nesting has its own rule: subgraphs nested three or more levels deep fire complexity-subgraph-nesting-too-deep. That limit is fixed at three, and no preset or option changes it. The rule reads only real subgraphs, such as a flowchart subgraph or a composite state. A deep mindmap, treemap, treeView or ishikawa draws no subgraphs, so the rule never fires on it. Its depth counts only toward the score.

Some threshold options have no effect yet

The presets also define nodeIdeal, vcsIdeal and vcsCritical, and the config accepts them, but no rule reads them today. complexity-visual-score-critical fires above vcsComplex, the complex threshold, and its message names that threshold.

Because each rule covers one band, set the preset for the whole ladder at once with --preset. Setting preset or a threshold on one complexity rule in the config moves only that rule's band. That can leave a gap between bands, or an overlap where two rules report the same diagram. Rule settings shows a threshold set on both rules that read it.

What counts as a node

Each diagram type has its own unit, read from mermaid's parser result. src/parse/stats.ts holds one adapter per type, and it is the authority when this table and the code disagree.

Diagram type A node is A subgraph is
flowchart, flowchart-elk, graph A vertex, but not a subgraph's id used as an edge's end, since that edge runs to the subgraph A subgraph
classDiagram A class, whose attributes and methods are rows A namespace, and one inside another adds nesting
sequenceDiagram A participant or actor A box, and a block such as loop, alt or par
stateDiagram A state, including a composite state, a fork and a choice A composite state, but not a concurrency region inside it
erDiagram An entity, whose attributes are rows None
block A leaf block A block that holds others
C4 diagrams A shape, such as a person or system A boundary
requirementDiagram A requirement or an element None
gantt A task A section
journey A task A section
timeline A period, and each event under it A section
kanban A task A column
mindmap, ishikawa Every entry in the tree None
quadrantChart A plotted point, and a quadrant that has a label None
xychart An x position: a category, or a point of the longest series None
sankey A node None
venn A set None
pie A slice None
packet A block None
gitGraph A commit A branch
radar An axis or a curve None
treemap, treeView Every entry in the tree None
architecture-beta A service A group
wardley An anchor or a component None
info None None

A note is never counted, in any diagram type.

Sequence, entity and state diagrams

These three types draw something other than boxes joined by lines, so each counts its own parts. As in the steps above, each graph is what the parser returned, and each score is computed by the site build.

Sequence diagrams

A participant is a node, and each message from one participant to another is an edge. A box around participants is a subgraph, and so is each block, such as loop, alt, opt or par. A block inside another block adds a level of nesting. An else divides its block rather than opening a new one.

A note, an activation and autonumber add nothing. Neither does a rect, which tints the background behind its messages rather than drawing a boundary. This checkout has three participants, six messages, a box and an alt block, so it scores 13.2.

Mermaid source

sequenceDiagram
  actor C as Customer
  box Shop
    participant W as Web
    participant P as Payments
  end
  C->>W: Place order
  activate W
  W->>P: Charge card
  alt approved
    P-->>W: Receipt
    W-->>C: Confirmation
  else declined
    P-->>W: Declined
    W-->>C: Try again
  end
  deactivate W
  Note over C,W: Notes are not counted

The diagram as drawn

sequenceDiagram
  actor C as Customer
  box Shop
    participant W as Web
    participant P as Payments
  end
  C->>W: Place order
  activate W
  W->>P: Charge card
  alt approved
    P-->>W: Receipt
    W-->>C: Confirmation
  else declined
    P-->>W: Declined
    W-->>C: Try again
  end
  deactivate W
  Note over C,W: Notes are not counted

What the parser returned: 3 nodes, 6 edges, 2 subgraphs

depth = max(subgraph nesting 1, tree depth 0) = 1

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (3 + 0.5 × 6 + 3 × 2) × (1 + 0.1 × 1)
      = (3 + 3 + 6) × 1.1
      = 12 × 1.1
      = 13.2

A block frames messages rather than participants, so the graph draws it as an empty boundary.

Entity relationship diagrams

An entity is a node, and a relationship is an edge. Each attribute is a row, which adds a quarter of a point. A row is text to read, but it sits inside a box the reader has already found, so it weighs less than an edge. These four entities, three relationships and five attributes score 6.75.

Mermaid source

erDiagram
  CUSTOMER ||--o{ ORDER : places
  ORDER ||--|{ LINE_ITEM : contains
  PRODUCT ||--o{ LINE_ITEM : "ordered in"
  CUSTOMER {
    string id PK
    string name
    string email
  }
  ORDER {
    string id PK
    date placed
  }

The diagram as drawn

erDiagram
  CUSTOMER ||--o{ ORDER : places
  ORDER ||--|{ LINE_ITEM : contains
  PRODUCT ||--o{ LINE_ITEM : "ordered in"
  CUSTOMER {
    string id PK
    string name
    string email
  }
  ORDER {
    string id PK
    date placed
  }

What the parser returned: 4 nodes, 3 edges, 0 subgraphs, 5 rows

depth = max(subgraph nesting 0, tree depth 0) = 0

score = (nodes + 0.5 × edges + 0.25 × rows + 3 × subgraphs) × (1 + 0.1 × depth)
      = (4 + 0.5 × 3 + 0.25 × 5 + 3 × 0) × (1 + 0.1 × 0)
      = (4 + 1.5 + 1.25 + 0) × 1
      = 6.75 × 1
      = 6.75

State diagrams

A state is a node, and a transition between two states is an edge. A composite state is a node and also a subgraph around the states inside it. A -- line splits a composite state into concurrency regions, drawn as dashed-off areas of its box. A region is not a state or a subgraph, and the states in it count inside the composite.

A start or end marker, [*], only marks where a path begins or ends. It is not a state, and its transition is not an edge. A fork and a choice are nodes, because each is a point where the path divides. This order flow has five states, three transitions and one composite state, so it scores 10.45.

Mermaid source

stateDiagram-v2
  [*] --> Cart
  Cart --> Checkout
  state Checkout {
    [*] --> Address
    Address --> Payment
    Payment --> [*]
  }
  Checkout --> Shipped
  Shipped --> [*]
  note right of Cart : Notes are not counted

The diagram as drawn

stateDiagram-v2
  [*] --> Cart
  Cart --> Checkout
  state Checkout {
    [*] --> Address
    Address --> Payment
    Payment --> [*]
  }
  Checkout --> Shipped
  Shipped --> [*]
  note right of Cart : Notes are not counted

What the parser returned: 5 nodes, 3 edges, 1 subgraphs

depth = max(subgraph nesting 1, tree depth 0) = 1

score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
      = (5 + 0.5 × 3 + 3 × 1) × (1 + 0.1 × 1)
      = (5 + 1.5 + 3) × 1.1
      = 9.5 × 1.1
      = 10.45

Why complexity is never fixed automatically

Splitting a diagram changes what it says. A finding names the existing subgraphs to split along and a target size, and a human decides where the seams go. When a split plan meets its contract, the finding also carries the plan: an overview and one detail diagram per group. The plan is still a recommendation, as Summarising a diagram explains.

Diagrams that cannot be measured

A diagram the parser cannot read has no trustworthy count. It is reported as parse-diagram-unparseable, and no complexity rule is evaluated against it.