Graph Display
The graph display utility renders directed acyclic graph (DAG) data as formatted text in the terminal. Unlike the Tree Display where each node has one parent, a DAG allows nodes to have multiple parents (fan-in). Shared dependencies are shown once rather than duplicated, making it ideal for visualizing build pipelines, dependency graphs, and task workflows.
Quick Start
import org.aesh.util.graph.Graph;
import org.aesh.util.graph.GraphNode;
GraphNode shared = GraphNode.of("C");
GraphNode root = GraphNode.of("Root")
.child(GraphNode.of("A").child(shared))
.child(GraphNode.of("B").child(shared));
String output = Graph.render(root);
invocation.println(output);Output (diamond pattern — C is shared between A and B):
Root
┌─┴┐
A B
└┬─┘
CGraphNode API
GraphNode provides a fluent API for building graph structures. The DAG semantics come from sharing the same node instance across multiple parents:
GraphNode shared = GraphNode.of("shared");
GraphNode root = GraphNode.of("root")
.child(GraphNode.of("left").child(shared))
.child(GraphNode.of("right").child(shared));| Method | Returns | Description |
|---|---|---|
GraphNode.of(label) | GraphNode | Creates a new node |
child(GraphNode) | this | Adds an existing node as child |
child(String) | this | Creates and adds a leaf node |
label() | String | Returns the node’s label |
children() | List | Returns unmodifiable children list |
Static API
For quick rendering of GraphNode graphs:
// Default UNICODE style
String output = Graph.render(root);
// Custom style
String output = Graph.render(root, GraphStyle.ASCII);
// With max width (wraps wide layers)
String output = Graph.render(root, 25);
// Custom style + max width
String output = Graph.render(root, GraphStyle.ASCII, 25);Builder API
For rendering existing typed DAGs without converting to GraphNode:
import org.aesh.util.graph.Graph;
import org.aesh.util.graph.GraphStyle;
String output = Graph.<Task>builder()
.label(Task::getName)
.children(Task::getDependencies)
.style(GraphStyle.UNICODE)
.maxWidth(40)
.build()
.render(rootTask);| Method | Default | Description |
|---|---|---|
label(Function<T, String>) | required | Extracts display text from each node |
children(Function<T, List<T>>) | required | Extracts children (null/empty = leaf) |
style(GraphStyle) | UNICODE | Visual style for connectors |
maxWidth(int) | 0 | Max output width (0 = no limit) |
Calling build() throws IllegalStateException if label or children are not set.
Graph Styles
Three predefined styles are available via GraphStyle:
UNICODE (default):
Root
┌─┴┐
A B
└┬─┘
CASCII:
Root
+-++
A B
++-+
CROUNDED:
Root
╭─┴╮
A B
╰┬─╯
CEach style defines eleven box-drawing characters used for edge routing:
| Character | UNICODE | ASCII | ROUNDED | Purpose |
|---|---|---|---|---|
| horizontal | ─ | - | ─ | Horizontal edge |
| vertical | │ | | | │ | Vertical edge |
| downTee | ┬ | + | ┬ | Parent splits down |
| upTee | ┴ | + | ┴ | Child joins up |
| cross | ┼ | + | ┼ | Vertical crosses horizontal |
| topLeft | ┌ | + | ╭ | Corner: down+right |
| topRight | ┐ | + | ╮ | Corner: down+left |
| bottomLeft | └ | + | ╰ | Corner: up+right |
| bottomRight | ┘ | + | ╯ | Corner: up+left |
| rightTee | ├ | + | ├ | T-junction: up+down+right |
| leftTee | ┤ | + | ┤ | T-junction: up+down+left |
Common Patterns
Fan-out (one parent, multiple children)
GraphNode root = GraphNode.of("Root")
.child("A")
.child("B")
.child("C"); Root
┌──┼──┐
A B CDiamond (shared dependency)
GraphNode shared = GraphNode.of("C");
GraphNode root = GraphNode.of("Root")
.child(GraphNode.of("A").child(shared))
.child(GraphNode.of("B").child(shared));Root
┌─┴┐
A B
└┬─┘
CComplex DAG (multiple shared nodes)
GraphNode d = GraphNode.of("D");
GraphNode a = GraphNode.of("A").child("C").child(d);
GraphNode b = GraphNode.of("B").child(d).child("E");
GraphNode root = GraphNode.of("Root").child(a).child(b); Root
┌─┴┐
A B
┌┴─┐│
│ ├┴─┐
C D EHere D is shared between A and B — it appears once with edges from both parents. Each parent’s edges are drawn on separate routing rows to avoid visual ambiguity.
Width Limiting
When a node has many children, the graph can grow very wide. The maxWidth parameter constrains the output by wrapping wide layers onto multiple rows. Children that don’t fit are moved to additional rows, with vertical connectors routing edges through the intermediate layers automatically.
GraphNode root = GraphNode.of("Pipeline")
.child("Compile")
.child("Test")
.child("Lint")
.child("Package")
.child("Deploy")
.child("Notify");
// Without maxWidth — all children on one wide row:
Graph.render(root); Pipeline
┌───────┬─────┬────┴─┬────────┬───────┐
Compile Test Lint Package Deploy Notify// With maxWidth=25 — children wrap to fit:
Graph.render(root, 25); Pipeline
┌─────┬──┬─┴──┬───┬────┐
Compile │ │ Test │ Lint
┌───┘ │ │
│ └─┐ │
│ │ └┐
Package Deploy NotifyA maxWidth of 0 (the default) means no limit. The wrapping applies to the node label layers; routing lines between layers may extend slightly beyond maxWidth when connecting nodes across split rows.
Using in Commands
Here is a complete command example that displays a build dependency graph:
@CommandDefinition(name = "deps", description = "Display dependency graph")
public class DepsCommand implements Command<CommandInvocation> {
@Option(name = "style", shortName = 's', defaultValue = {"UNICODE"},
description = "Graph style: ASCII, UNICODE, ROUNDED")
private GraphStyle style;
@Override
public CommandResult execute(CommandInvocation invocation) {
// Build a dependency graph
GraphNode test = GraphNode.of("test");
GraphNode compile = GraphNode.of("compile");
GraphNode lint = GraphNode.of("lint");
GraphNode validate = GraphNode.of("validate").child(compile).child(lint);
GraphNode root = GraphNode.of("build")
.child(validate)
.child(test);
String output = Graph.render(root, style);
invocation.println(output);
return CommandResult.SUCCESS;
}
}Cycle Detection
Graphs must be acyclic. If the graph contains a cycle, render() throws an IllegalArgumentException:
GraphNode a = GraphNode.of("A");
GraphNode b = GraphNode.of("B").child(a);
a.child(b); // creates A → B → A cycle
Graph.render(a); // throws IllegalArgumentException: "Graph contains a cycle"Edge Cases
| Scenario | Behavior |
|---|---|
| Root with no children | Only the root label is printed |
children() returns null | Treated as a leaf node (no crash) |
children() returns empty list | Treated as a leaf node |
| Cyclic graph | Throws IllegalArgumentException |
| Shared child node | Rendered once with edges from all parents |
Tree vs Graph
| Feature | Tree | Graph |
|---|---|---|
| Structure | Each node has one parent | Nodes can have many parents |
| Rendering | Vertical with indentation | Layered horizontal layout |
| Shared nodes | Duplicated in output | Shown once with fan-in |
| Cycle detection | N/A (tree structure) | Throws on cycles |
| Use case | File trees, hierarchies | Dependencies, pipelines |
Use Tree for simple hierarchies (file systems, org charts). Use Graph when nodes can be shared across multiple parents (build steps, dependency resolution).
API Reference
GraphNode
| Method | Returns | Description |
|---|---|---|
of(String) | GraphNode | Factory method |
child(GraphNode) | GraphNode | Add child node, returns this |
child(String) | GraphNode | Add leaf child, returns this |
label() | String | Get label |
children() | List | Unmodifiable children list |
Graph (static)
| Method | Returns | Description |
|---|---|---|
render(GraphNode) | String | Render with UNICODE style |
render(GraphNode, int) | String | Render with UNICODE style + max width |
render(GraphNode, GraphStyle) | String | Render with specified style |
render(GraphNode, GraphStyle, int) | String | Render with specified style + max width |
builder() | Builder | Create a generic builder |
GraphStyle
| Constant | Description |
|---|---|
ASCII | Uses +, -, | characters |
UNICODE | Uses box-drawing characters (default) |
ROUNDED | Uses rounded corners (╭, ╮, ╰, ╯) |
See Also
- Tree Display — Hierarchical tree rendering
- Table Display — Tabular data rendering
- Progress Bar — Progress feedback for long-running operations