clHierarchicalLayout function
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;
}