Skip to content

Commit 6506b28

Browse files
committed
Added documentation for the Graph Display (DAG visualization) utility
1 parent 5c1f43d commit 6506b28

File tree

2 files changed

+287
-0
lines changed

2 files changed

+287
-0
lines changed

content/docs/aesh/graph.md

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
---
2+
date: '2026-03-09T16:00:00+01:00'
3+
draft: false
4+
title: 'Graph Display'
5+
weight: 22
6+
---
7+
8+
The graph display utility renders directed acyclic graph (DAG) data as formatted text in the terminal. Unlike the [Tree Display]({{< relref "tree" >}}) 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.
9+
10+
## Quick Start
11+
12+
```java
13+
import org.aesh.util.graph.Graph;
14+
import org.aesh.util.graph.GraphNode;
15+
16+
GraphNode shared = GraphNode.of("C");
17+
GraphNode root = GraphNode.of("Root")
18+
.child(GraphNode.of("A").child(shared))
19+
.child(GraphNode.of("B").child(shared));
20+
21+
String output = Graph.render(root);
22+
invocation.println(output);
23+
```
24+
25+
Output (diamond pattern — C is shared between A and B):
26+
27+
```
28+
Root
29+
┌─┴┐
30+
A B
31+
└┬─┘
32+
C
33+
```
34+
35+
## GraphNode API
36+
37+
`GraphNode` provides a fluent API for building graph structures. The DAG semantics come from sharing the same node instance across multiple parents:
38+
39+
```java
40+
GraphNode shared = GraphNode.of("shared");
41+
GraphNode root = GraphNode.of("root")
42+
.child(GraphNode.of("left").child(shared))
43+
.child(GraphNode.of("right").child(shared));
44+
```
45+
46+
| Method | Returns | Description |
47+
|----------------------|-------------|------------------------------------|
48+
| `GraphNode.of(label)` | `GraphNode` | Creates a new node |
49+
| `child(GraphNode)` | `this` | Adds an existing node as child |
50+
| `child(String)` | `this` | Creates and adds a leaf node |
51+
| `label()` | `String` | Returns the node's label |
52+
| `children()` | `List` | Returns unmodifiable children list |
53+
54+
## Static API
55+
56+
For quick rendering of `GraphNode` graphs:
57+
58+
```java
59+
// Default UNICODE style
60+
String output = Graph.render(root);
61+
62+
// Custom style
63+
String output = Graph.render(root, GraphStyle.ASCII);
64+
```
65+
66+
## Builder API
67+
68+
For rendering existing typed DAGs without converting to `GraphNode`:
69+
70+
```java
71+
import org.aesh.util.graph.Graph;
72+
import org.aesh.util.graph.GraphStyle;
73+
74+
String output = Graph.<Task>builder()
75+
.label(Task::getName)
76+
.children(Task::getDependencies)
77+
.style(GraphStyle.UNICODE)
78+
.build()
79+
.render(rootTask);
80+
```
81+
82+
| Method | Default | Description |
83+
|----------------------------------|------------|---------------------------------------|
84+
| `label(Function<T, String>)` | *required* | Extracts display text from each node |
85+
| `children(Function<T, List<T>>)` | *required* | Extracts children (null/empty = leaf) |
86+
| `style(GraphStyle)` | `UNICODE` | Visual style for connectors |
87+
88+
Calling `build()` throws `IllegalStateException` if `label` or `children` are not set.
89+
90+
## Graph Styles
91+
92+
Three predefined styles are available via `GraphStyle`:
93+
94+
**UNICODE** (default):
95+
```
96+
Root
97+
┌─┴┐
98+
A B
99+
└┬─┘
100+
C
101+
```
102+
103+
**ASCII**:
104+
```
105+
Root
106+
+-++
107+
A B
108+
++-+
109+
C
110+
```
111+
112+
**ROUNDED**:
113+
```
114+
Root
115+
╭─┴╮
116+
A B
117+
╰┬─╯
118+
C
119+
```
120+
121+
Each style defines eleven box-drawing characters used for edge routing:
122+
123+
| Character | UNICODE | ASCII | ROUNDED | Purpose |
124+
|---------------|---------|-------|---------|-----------------------------|
125+
| horizontal | `` | `-` | `` | Horizontal edge |
126+
| vertical | `` | `\|` | `` | Vertical edge |
127+
| downTee | `` | `+` | `` | Parent splits down |
128+
| upTee | `` | `+` | `` | Child joins up |
129+
| cross | `` | `+` | `` | Vertical crosses horizontal |
130+
| topLeft | `` | `+` | `` | Corner: down+right |
131+
| topRight | `` | `+` | `` | Corner: down+left |
132+
| bottomLeft | `` | `+` | `` | Corner: up+right |
133+
| bottomRight | `` | `+` | `` | Corner: up+left |
134+
| rightTee | `` | `+` | `` | T-junction: up+down+right |
135+
| leftTee | `` | `+` | `` | T-junction: up+down+left |
136+
137+
## Common Patterns
138+
139+
### Fan-out (one parent, multiple children)
140+
141+
```java
142+
GraphNode root = GraphNode.of("Root")
143+
.child("A")
144+
.child("B")
145+
.child("C");
146+
```
147+
148+
```
149+
Root
150+
┌──┼──┐
151+
A B C
152+
```
153+
154+
### Diamond (shared dependency)
155+
156+
```java
157+
GraphNode shared = GraphNode.of("C");
158+
GraphNode root = GraphNode.of("Root")
159+
.child(GraphNode.of("A").child(shared))
160+
.child(GraphNode.of("B").child(shared));
161+
```
162+
163+
```
164+
Root
165+
┌─┴┐
166+
A B
167+
└┬─┘
168+
C
169+
```
170+
171+
### Complex DAG (multiple shared nodes)
172+
173+
```java
174+
GraphNode d = GraphNode.of("D");
175+
GraphNode a = GraphNode.of("A").child("C").child(d);
176+
GraphNode b = GraphNode.of("B").child(d).child("E");
177+
GraphNode root = GraphNode.of("Root").child(a).child(b);
178+
```
179+
180+
```
181+
Root
182+
┌─┴┐
183+
A B
184+
┌┴─┬┴─┐
185+
C D E
186+
```
187+
188+
Here D is shared between A and B — it appears once with edges from both parents.
189+
190+
## Using in Commands
191+
192+
Here is a complete command example that displays a build dependency graph:
193+
194+
```java
195+
@CommandDefinition(name = "deps", description = "Display dependency graph")
196+
public class DepsCommand implements Command<CommandInvocation> {
197+
198+
@Option(name = "style", shortName = 's', defaultValue = {"UNICODE"},
199+
description = "Graph style: ASCII, UNICODE, ROUNDED")
200+
private GraphStyle style;
201+
202+
@Override
203+
public CommandResult execute(CommandInvocation invocation) {
204+
// Build a dependency graph
205+
GraphNode test = GraphNode.of("test");
206+
GraphNode compile = GraphNode.of("compile");
207+
GraphNode lint = GraphNode.of("lint");
208+
GraphNode validate = GraphNode.of("validate").child(compile).child(lint);
209+
GraphNode root = GraphNode.of("build")
210+
.child(validate)
211+
.child(test);
212+
213+
String output = Graph.render(root, style);
214+
invocation.println(output);
215+
return CommandResult.SUCCESS;
216+
}
217+
}
218+
```
219+
220+
## Cycle Detection
221+
222+
Graphs must be acyclic. If the graph contains a cycle, `render()` throws an `IllegalArgumentException`:
223+
224+
```java
225+
GraphNode a = GraphNode.of("A");
226+
GraphNode b = GraphNode.of("B").child(a);
227+
a.child(b); // creates A → B → A cycle
228+
229+
Graph.render(a); // throws IllegalArgumentException: "Graph contains a cycle"
230+
```
231+
232+
## Edge Cases
233+
234+
| Scenario | Behavior |
235+
|---------------------------------|------------------------------------------------|
236+
| Root with no children | Only the root label is printed |
237+
| `children()` returns `null` | Treated as a leaf node (no crash) |
238+
| `children()` returns empty list | Treated as a leaf node |
239+
| Cyclic graph | Throws `IllegalArgumentException` |
240+
| Shared child node | Rendered once with edges from all parents |
241+
242+
## Tree vs Graph
243+
244+
| Feature | Tree | Graph |
245+
|--------------------|---------------------------|-----------------------------|
246+
| Structure | Each node has one parent | Nodes can have many parents |
247+
| Rendering | Vertical with indentation | Layered horizontal layout |
248+
| Shared nodes | Duplicated in output | Shown once with fan-in |
249+
| Cycle detection | N/A (tree structure) | Throws on cycles |
250+
| Use case | File trees, hierarchies | Dependencies, pipelines |
251+
252+
Use `Tree` for simple hierarchies (file systems, org charts). Use `Graph` when nodes can be shared across multiple parents (build steps, dependency resolution).
253+
254+
## API Reference
255+
256+
### GraphNode
257+
258+
| Method | Returns | Description |
259+
|----------------------|-------------|--------------------------------|
260+
| `of(String)` | `GraphNode` | Factory method |
261+
| `child(GraphNode)` | `GraphNode` | Add child node, returns this |
262+
| `child(String)` | `GraphNode` | Add leaf child, returns this |
263+
| `label()` | `String` | Get label |
264+
| `children()` | `List` | Unmodifiable children list |
265+
266+
### Graph (static)
267+
268+
| Method | Returns | Description |
269+
|---------------------------------|------------|------------------------------|
270+
| `render(GraphNode)` | `String` | Render with UNICODE style |
271+
| `render(GraphNode, GraphStyle)` | `String` | Render with specified style |
272+
| `builder()` | `Builder` | Create a generic builder |
273+
274+
### GraphStyle
275+
276+
| Constant | Description |
277+
|-----------|-----------------------------------------|
278+
| `ASCII` | Uses `+`, `-`, `\|` characters |
279+
| `UNICODE` | Uses box-drawing characters (default) |
280+
| `ROUNDED` | Uses rounded corners (``, ``, ``, ``) |
281+
282+
## See Also
283+
284+
- [Tree Display]({{< relref "tree" >}}) — Hierarchical tree rendering
285+
- [Table Display]({{< relref "table" >}}) — Tabular data rendering
286+
- [Progress Bar]({{< relref "progress-bar" >}}) — Progress feedback for long-running operations

content/docs/aesh/tree.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,5 +246,6 @@ public class TreeCommand implements Command<CommandInvocation> {
246246

247247
## See Also
248248

249+
- [Graph Display]({{< relref "graph" >}}) — DAG rendering for shared dependencies
249250
- [Table Display]({{< relref "table" >}}) — Tabular data rendering
250251
- [Progress Bar]({{< relref "progress-bar" >}}) — Progress feedback for long-running operations

0 commit comments

Comments
 (0)