clHierarchicalLayout function

Map<String, Offset> clHierarchicalLayout(
  1. List<CLGraphNode> nodes,
  2. List<CLGraphEdge> edges
)

Layout gerarchico L→R deterministico: la colonna è la profondità nella gerarchia di contenimento; la y di un nodo è la media delle y dei suoi figli (foglie impilate in ordine stabile). Gli archi link sono ignorati.

Implementation

Map<String, Offset> clHierarchicalLayout(List<CLGraphNode> nodes, List<CLGraphEdge> edges) {
  final children = <String, List<String>>{};
  final hasParent = <String>{};
  final byId = {for (final n in nodes) n.id: n};
  for (final e in edges) {
    if (e.kind != CLGraphEdgeKind.containment) continue;
    if (!byId.containsKey(e.fromNodeId) || !byId.containsKey(e.toNodeId)) continue;
    (children[e.fromNodeId] ??= []).add(e.toNodeId);
    hasParent.add(e.toNodeId);
  }

  final positions = <String, Offset>{};
  final placed = <String>{};
  var rowCursor = 0;

  double place(String id, int depth) {
    if (placed.contains(id)) return positions[id]?.dy ?? rowCursor * _rowHeight;
    placed.add(id);
    final kids = children[id] ?? const [];
    final double y;
    if (kids.isEmpty) {
      y = rowCursor * _rowHeight;
      rowCursor++;
    } else {
      final ys = [for (final k in kids) place(k, depth + 1)];
      y = ys.reduce((a, b) => a + b) / ys.length;
    }
    positions[id] = Offset(depth * _colWidth, y);
    return y;
  }

  // root = nodi senza padre di contenimento, in ordine stabile della lista.
  for (final n in nodes) {
    if (!hasParent.contains(n.id)) place(n.id, 0);
  }
  // eventuali nodi non raggiunti (dati incoerenti): impilali a destra.
  for (final n in nodes) {
    if (!positions.containsKey(n.id)) {
      positions[n.id] = Offset(_colWidth, rowCursor * _rowHeight);
      rowCursor++;
    }
  }
  return positions;
}