Root nodes in fan-in groups are placed without target-aware grouping, causing edge overlap

Author: ktrysmtCreated Mar 18, 2026Updated Aug 29, 2026

Bug

When a graph has multiple fan-in groups feeding into different target nodes, all root nodes are placed in a flat horizontal sequence regardless of which target they connect to. This causes edge paths from different fan-in groups to overlap, making the diagram hard to read.

Reproduction

mermaid
graph TD
    A1 --> A
    A2 --> A
    B1 --> B
    B2 --> B
    A --> C
    B --> C
typescript
import { renderMermaidASCII } from 'beautiful-mermaid';

const code = `graph TD
    A1 --> A
    A2 --> A
    B1 --> B
    B2 --> B
    A --> C
    B --> C`;

console.log(renderMermaidASCII(code));

Current output (conceptual)

A1   A2   B1   B2      <-- all roots on one row, no grouping
 └────┼────┴────┘       <-- paths overlap, can't tell A-group from B-group
      |    |
      A    B

Expected output (conceptual)

A1   A2        B1   B2
 └────┘         └────┘
      |              |
      A              B

Root nodes feeding into the same target should be placed contiguously, with fan-in targets aligned near their respective root groups.

Analysis

The root cause is in createMapping in src/ascii/grid.ts (around lines 448-455). Root nodes are placed sequentially with a fixed +4 increment, without considering which downstream target each root feeds into:

typescript
for (const node of externalRootNodes) {
  const requested: GridCoord = dir === 'LR'
    ? { x: 0, y: highestPositionPerLevel[0]\! }
    : { x: highestPositionPerLevel[0]\!, y: 0 }
  reserveSpotInGrid(graph, graph.nodes[node.index]\!, requested)
  highestPositionPerLevel[0] = highestPositionPerLevel[0]\! + 4
}

For the example above, A1, A2, B1, B2 all get placed at x = 0, 4, 8, 12 regardless of A being at one position and B at another.

Additionally, the child placement loop places children at the next available slot on their level (highestPositionPerLevel[childLevel]), ignoring the parent's perpendicular position. This means fan-in targets like B end up far from their root group (e.g. B1 at x=8, B2 at x=12, but B at x=4).

This also contributes to the pathfinding cost described in #64 — when roots are poorly positioned relative to their targets, edge routing becomes unnecessarily complex.

Suggested fix

Two changes in src/ascii/grid.ts:

  1. Group roots by immediate downstream target before placement. Roots feeding into the same target are placed contiguously, so their edge spans don't interleave with other groups.

  2. Align fan-in targets with their parents. When placing a child that has multiple incoming edges (in-degree > 1), use Math.max(levelTracker, parentPerpendicular) instead of just the level tracker. This keeps fan-in targets near their root groups rather than at the next available slot.

Edge cases to consider

  • A root node with edges to multiple targets should only appear once (group by first child as a heuristic)
  • Single fan-in group: no grouping effect needed, behavior unchanged from current
  • LR direction: the same issue exists vertically; the fix should apply symmetrically

Environment

  • beautiful-mermaid 1.1.3

Source: lukilabs/beautiful-mermaid