@walterra/pi-graphviz 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @walterra/pi-graphviz
|
|
2
|
+
|
|
3
|
+
Graphviz DOT diagram extension for [pi coding agent](https://github.com/badlogic/pi-mono) - render diagrams as inline terminal images.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install @walterra/pi-graphviz
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your pi config manually:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @walterra/pi-graphviz
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then in your pi config, add the extension path.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **DOT Language**: Full support for Graphviz DOT syntax
|
|
22
|
+
- **Auto Install**: Graphviz auto-installed via brew (macOS) or apt/dnf (Linux)
|
|
23
|
+
- **Multiple Engines**: dot, neato, fdp, circo, twopi layout engines
|
|
24
|
+
- **Inline Display**: Diagrams render directly in terminals supporting inline images
|
|
25
|
+
- **SVG/PNG Output**: Save diagrams in either format
|
|
26
|
+
|
|
27
|
+
## Tool: `graphviz_chart`
|
|
28
|
+
|
|
29
|
+
Renders a Graphviz DOT specification as a PNG image.
|
|
30
|
+
|
|
31
|
+
### Parameters
|
|
32
|
+
|
|
33
|
+
| Parameter | Type | Required | Description |
|
|
34
|
+
|-----------|------|----------|-------------|
|
|
35
|
+
| `dot` | string | ✅ | Graphviz DOT specification |
|
|
36
|
+
| `engine` | string | | Layout engine: dot (default), neato, fdp, circo, twopi |
|
|
37
|
+
| `width` | number | | Output width in pixels |
|
|
38
|
+
| `height` | number | | Output height in pixels |
|
|
39
|
+
| `save_path` | string | | Optional file path (.png or .svg) |
|
|
40
|
+
|
|
41
|
+
### Example
|
|
42
|
+
|
|
43
|
+
```dot
|
|
44
|
+
digraph G {
|
|
45
|
+
rankdir=LR;
|
|
46
|
+
node [shape=box style="rounded,filled" fillcolor=lightblue];
|
|
47
|
+
|
|
48
|
+
A [label="Start"];
|
|
49
|
+
B [label="Process"];
|
|
50
|
+
C [label="End" fillcolor=lightgreen];
|
|
51
|
+
|
|
52
|
+
A -> B [label="step 1"];
|
|
53
|
+
B -> C [label="step 2"];
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Use Cases
|
|
58
|
+
|
|
59
|
+
- Architecture diagrams
|
|
60
|
+
- Flowcharts
|
|
61
|
+
- State machines
|
|
62
|
+
- Dependency graphs
|
|
63
|
+
- ER diagrams
|
|
64
|
+
- Network topologies
|
|
65
|
+
|
|
66
|
+
## Reference Documentation
|
|
67
|
+
|
|
68
|
+
See [graphviz-reference.md](./extensions/graphviz-chart/graphviz-reference.md) for comprehensive documentation on:
|
|
69
|
+
|
|
70
|
+
- DOT language syntax
|
|
71
|
+
- All node shapes and edge styles
|
|
72
|
+
- Clusters and subgraphs
|
|
73
|
+
- Layout engines
|
|
74
|
+
- Professional theming
|
|
75
|
+
- Common patterns
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
# Graphviz DOT Language Reference
|
|
2
|
+
|
|
3
|
+
Graphviz is a powerful tool for creating diagrams and visual representations of graphs using the DOT language. This reference covers the essential syntax, attributes, and patterns for creating effective diagrams.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Graphviz is **auto-installed** when first using this extension:
|
|
8
|
+
|
|
9
|
+
- **macOS**: via Homebrew (`brew install graphviz`)
|
|
10
|
+
- **Linux**: via apt or dnf (`sudo apt install -y graphviz` / `sudo dnf install -y graphviz`)
|
|
11
|
+
|
|
12
|
+
If auto-install fails (e.g., missing package manager, permissions), manual installation is required:
|
|
13
|
+
|
|
14
|
+
- **macOS**: `brew install graphviz`
|
|
15
|
+
- **Ubuntu/Debian**: `sudo apt install graphviz`
|
|
16
|
+
- **Fedora/RHEL**: `sudo dnf install graphviz`
|
|
17
|
+
- **Windows**: Download from https://graphviz.org/download/
|
|
18
|
+
|
|
19
|
+
Verify installation: `dot -V`
|
|
20
|
+
|
|
21
|
+
If Graphviz cannot be installed, the tool returns an error with instructions. Do NOT fall back to ASCII art.
|
|
22
|
+
|
|
23
|
+
## Basic Structure
|
|
24
|
+
|
|
25
|
+
### Graph Types
|
|
26
|
+
|
|
27
|
+
```dot
|
|
28
|
+
// Undirected graph
|
|
29
|
+
graph G {
|
|
30
|
+
A -- B;
|
|
31
|
+
B -- C;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Directed graph (digraph)
|
|
35
|
+
digraph G {
|
|
36
|
+
A -> B;
|
|
37
|
+
B -> C;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Strict graph (no multi-edges)
|
|
41
|
+
strict digraph G {
|
|
42
|
+
A -> B;
|
|
43
|
+
A -> B; // ignored, already exists
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Comments
|
|
48
|
+
|
|
49
|
+
```dot
|
|
50
|
+
// Single line comment
|
|
51
|
+
/* Multi-line
|
|
52
|
+
comment */
|
|
53
|
+
# Preprocessor-style comment (line must start with #)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Nodes
|
|
57
|
+
|
|
58
|
+
### Basic Node Declaration
|
|
59
|
+
|
|
60
|
+
```dot
|
|
61
|
+
digraph G {
|
|
62
|
+
// Implicit node creation
|
|
63
|
+
A -> B;
|
|
64
|
+
|
|
65
|
+
// Explicit node with label
|
|
66
|
+
C [label="Node C"];
|
|
67
|
+
|
|
68
|
+
// Node with attributes
|
|
69
|
+
D [label="Database" shape=cylinder color=blue];
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Node Shapes
|
|
74
|
+
|
|
75
|
+
**Common shapes:**
|
|
76
|
+
|
|
77
|
+
- `box`, `rect`, `rectangle` - Rectangular
|
|
78
|
+
- `ellipse`, `oval` - Elliptical (default)
|
|
79
|
+
- `circle`, `doublecircle` - Circular
|
|
80
|
+
- `diamond` - Diamond
|
|
81
|
+
- `plaintext`, `plain`, `none` - No border
|
|
82
|
+
- `point` - Small circle
|
|
83
|
+
- `triangle`, `invtriangle` - Triangular
|
|
84
|
+
- `house`, `invhouse` - House-shaped
|
|
85
|
+
- `pentagon`, `hexagon`, `septagon`, `octagon` - Polygons
|
|
86
|
+
- `cylinder` - Database/storage
|
|
87
|
+
- `note` - Document note
|
|
88
|
+
- `folder`, `tab` - File system
|
|
89
|
+
- `box3d` - 3D box
|
|
90
|
+
- `component` - UML component
|
|
91
|
+
- `star` - Star shape
|
|
92
|
+
- `record` - Record with fields (use HTML labels instead)
|
|
93
|
+
|
|
94
|
+
**Example:**
|
|
95
|
+
|
|
96
|
+
```dot
|
|
97
|
+
digraph shapes {
|
|
98
|
+
database [shape=cylinder label="PostgreSQL"];
|
|
99
|
+
service [shape=box label="API Service"];
|
|
100
|
+
queue [shape=parallelogram label="Message Queue"];
|
|
101
|
+
decision [shape=diamond label="Is Valid?"];
|
|
102
|
+
|
|
103
|
+
service -> database;
|
|
104
|
+
service -> queue;
|
|
105
|
+
queue -> decision;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Node Styles
|
|
110
|
+
|
|
111
|
+
```dot
|
|
112
|
+
node [style=filled]; // Filled background
|
|
113
|
+
node [style=dashed]; // Dashed border
|
|
114
|
+
node [style=dotted]; // Dotted border
|
|
115
|
+
node [style=bold]; // Bold border
|
|
116
|
+
node [style=rounded]; // Rounded corners (for box)
|
|
117
|
+
node [style="filled,rounded"]; // Multiple styles
|
|
118
|
+
node [style=invis]; // Invisible
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Edges
|
|
122
|
+
|
|
123
|
+
### Edge Types
|
|
124
|
+
|
|
125
|
+
```dot
|
|
126
|
+
// Directed edge
|
|
127
|
+
A -> B;
|
|
128
|
+
|
|
129
|
+
// Undirected edge (in graph, not digraph)
|
|
130
|
+
A -- B;
|
|
131
|
+
|
|
132
|
+
// Chain of edges
|
|
133
|
+
A -> B -> C -> D;
|
|
134
|
+
|
|
135
|
+
// Multiple targets
|
|
136
|
+
A -> {B C D};
|
|
137
|
+
|
|
138
|
+
// Edge with label
|
|
139
|
+
A -> B [label="sends data"];
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Edge Attributes
|
|
143
|
+
|
|
144
|
+
```dot
|
|
145
|
+
digraph G {
|
|
146
|
+
// Labeled edge
|
|
147
|
+
A -> B [label="HTTP"];
|
|
148
|
+
|
|
149
|
+
// Colored edge
|
|
150
|
+
A -> C [color=red];
|
|
151
|
+
|
|
152
|
+
// Styled edge
|
|
153
|
+
A -> D [style=dashed];
|
|
154
|
+
A -> E [style=dotted];
|
|
155
|
+
A -> F [style=bold];
|
|
156
|
+
|
|
157
|
+
// Weighted edge (affects layout)
|
|
158
|
+
A -> G [weight=2];
|
|
159
|
+
|
|
160
|
+
// Constraint (if false, doesn't affect ranking)
|
|
161
|
+
A -> H [constraint=false];
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Arrow Types
|
|
166
|
+
|
|
167
|
+
```dot
|
|
168
|
+
digraph arrows {
|
|
169
|
+
// Arrow head styles
|
|
170
|
+
A -> B [arrowhead=normal]; // Default
|
|
171
|
+
A -> C [arrowhead=dot];
|
|
172
|
+
A -> D [arrowhead=odot]; // Open dot
|
|
173
|
+
A -> E [arrowhead=diamond];
|
|
174
|
+
A -> F [arrowhead=box];
|
|
175
|
+
A -> G [arrowhead=vee];
|
|
176
|
+
A -> H [arrowhead=none];
|
|
177
|
+
|
|
178
|
+
// Both ends (for digraph)
|
|
179
|
+
I -> J [dir=both arrowhead=normal arrowtail=dot];
|
|
180
|
+
|
|
181
|
+
// Reverse direction
|
|
182
|
+
K -> L [dir=back];
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Arrow modifiers:**
|
|
187
|
+
|
|
188
|
+
- `o` prefix: open (hollow) - `odot`, `odiamond`
|
|
189
|
+
- `l`/`r` prefix: left/right half - `lnormal`, `rnormal`
|
|
190
|
+
- Combinations: `obox`, `olnormal`
|
|
191
|
+
|
|
192
|
+
## Attributes
|
|
193
|
+
|
|
194
|
+
### Graph Attributes
|
|
195
|
+
|
|
196
|
+
```dot
|
|
197
|
+
digraph G {
|
|
198
|
+
// Layout direction
|
|
199
|
+
rankdir=LR; // Left to Right
|
|
200
|
+
rankdir=TB; // Top to Bottom (default)
|
|
201
|
+
rankdir=BT; // Bottom to Top
|
|
202
|
+
rankdir=RL; // Right to Left
|
|
203
|
+
|
|
204
|
+
// Spacing
|
|
205
|
+
ranksep=0.5; // Rank separation (inches)
|
|
206
|
+
nodesep=0.5; // Node separation (inches)
|
|
207
|
+
|
|
208
|
+
// Background
|
|
209
|
+
bgcolor=lightgrey;
|
|
210
|
+
|
|
211
|
+
// Size constraints
|
|
212
|
+
size="10,10"; // Max size in inches
|
|
213
|
+
ratio=fill; // Fill the size
|
|
214
|
+
|
|
215
|
+
// Font defaults
|
|
216
|
+
fontname="Helvetica";
|
|
217
|
+
fontsize=12;
|
|
218
|
+
|
|
219
|
+
// Label (graph title)
|
|
220
|
+
label="System Architecture";
|
|
221
|
+
labelloc=t; // Top (t), bottom (b)
|
|
222
|
+
labeljust=l; // Left (l), center (c), right (r)
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Node Attributes
|
|
227
|
+
|
|
228
|
+
| Attribute | Description | Example |
|
|
229
|
+
| ----------- | ---------------------------------- | --------------------- |
|
|
230
|
+
| `label` | Display text | `label="Web Server"` |
|
|
231
|
+
| `shape` | Node shape | `shape=box` |
|
|
232
|
+
| `color` | Border color | `color=red` |
|
|
233
|
+
| `fillcolor` | Fill color (requires style=filled) | `fillcolor=lightblue` |
|
|
234
|
+
| `style` | Visual style | `style=filled` |
|
|
235
|
+
| `fontname` | Font family | `fontname="Arial"` |
|
|
236
|
+
| `fontsize` | Font size in points | `fontsize=14` |
|
|
237
|
+
| `fontcolor` | Text color | `fontcolor=white` |
|
|
238
|
+
| `width` | Minimum width (inches) | `width=2` |
|
|
239
|
+
| `height` | Minimum height (inches) | `height=1` |
|
|
240
|
+
| `penwidth` | Border thickness | `penwidth=2.0` |
|
|
241
|
+
| `tooltip` | Hover text (SVG) | `tooltip="Click me"` |
|
|
242
|
+
| `URL` | Clickable link (SVG) | `URL="https://..."` |
|
|
243
|
+
|
|
244
|
+
### Edge Attributes
|
|
245
|
+
|
|
246
|
+
| Attribute | Description | Example |
|
|
247
|
+
| ------------ | -------------------------------------------- | ------------------ |
|
|
248
|
+
| `label` | Edge label | `label="HTTP"` |
|
|
249
|
+
| `color` | Edge color | `color=blue` |
|
|
250
|
+
| `style` | Line style | `style=dashed` |
|
|
251
|
+
| `arrowhead` | Head arrow style | `arrowhead=vee` |
|
|
252
|
+
| `arrowtail` | Tail arrow style | `arrowtail=dot` |
|
|
253
|
+
| `dir` | Arrow direction | `dir=both` |
|
|
254
|
+
| `penwidth` | Line thickness | `penwidth=2.0` |
|
|
255
|
+
| `weight` | Layout weight (higher = shorter, straighter) | `weight=2` |
|
|
256
|
+
| `constraint` | Affects ranking | `constraint=false` |
|
|
257
|
+
| `minlen` | Minimum edge length (ranks) | `minlen=2` |
|
|
258
|
+
| `headlabel` | Label at head | `headlabel="1"` |
|
|
259
|
+
| `taillabel` | Label at tail | `taillabel="*"` |
|
|
260
|
+
|
|
261
|
+
## Colors
|
|
262
|
+
|
|
263
|
+
### Named Colors (X11 Scheme)
|
|
264
|
+
|
|
265
|
+
Common colors: `red`, `blue`, `green`, `yellow`, `orange`, `purple`, `pink`, `brown`, `black`, `white`, `gray`, `grey`
|
|
266
|
+
|
|
267
|
+
Light variants: `lightblue`, `lightgreen`, `lightyellow`, `lightgray`
|
|
268
|
+
Dark variants: `darkblue`, `darkgreen`, `darkred`, `darkgray`
|
|
269
|
+
|
|
270
|
+
### Hex Colors
|
|
271
|
+
|
|
272
|
+
```dot
|
|
273
|
+
node [fillcolor="#3b82f6"]; // Blue
|
|
274
|
+
node [color="#ef4444"]; // Red
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### RGB/RGBA
|
|
278
|
+
|
|
279
|
+
```dot
|
|
280
|
+
node [fillcolor="0.5 0.8 0.2"]; // HSB format
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Subgraphs and Clusters
|
|
284
|
+
|
|
285
|
+
### Basic Subgraph
|
|
286
|
+
|
|
287
|
+
```dot
|
|
288
|
+
digraph G {
|
|
289
|
+
subgraph {
|
|
290
|
+
A; B; C;
|
|
291
|
+
}
|
|
292
|
+
D -> A;
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Clusters (Named subgraphs starting with "cluster")
|
|
297
|
+
|
|
298
|
+
```dot
|
|
299
|
+
digraph G {
|
|
300
|
+
subgraph cluster_frontend {
|
|
301
|
+
label="Frontend";
|
|
302
|
+
style=filled;
|
|
303
|
+
color=lightblue;
|
|
304
|
+
|
|
305
|
+
web [label="Web App"];
|
|
306
|
+
mobile [label="Mobile App"];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
subgraph cluster_backend {
|
|
310
|
+
label="Backend";
|
|
311
|
+
style=filled;
|
|
312
|
+
color=lightgreen;
|
|
313
|
+
|
|
314
|
+
api [label="API Server"];
|
|
315
|
+
worker [label="Worker"];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
subgraph cluster_data {
|
|
319
|
+
label="Data Layer";
|
|
320
|
+
style=filled;
|
|
321
|
+
color=lightyellow;
|
|
322
|
+
|
|
323
|
+
db [label="Database" shape=cylinder];
|
|
324
|
+
cache [label="Cache" shape=cylinder];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
web -> api;
|
|
328
|
+
mobile -> api;
|
|
329
|
+
api -> db;
|
|
330
|
+
api -> cache;
|
|
331
|
+
worker -> db;
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Rank Constraints
|
|
336
|
+
|
|
337
|
+
```dot
|
|
338
|
+
digraph G {
|
|
339
|
+
// Force nodes to same rank (horizontal alignment in TB)
|
|
340
|
+
{rank=same; A; B; C;}
|
|
341
|
+
|
|
342
|
+
// Source rank (top in TB)
|
|
343
|
+
{rank=source; Start;}
|
|
344
|
+
|
|
345
|
+
// Sink rank (bottom in TB)
|
|
346
|
+
{rank=sink; End;}
|
|
347
|
+
|
|
348
|
+
// Min/max rank
|
|
349
|
+
{rank=min; First;}
|
|
350
|
+
{rank=max; Last;}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Common Patterns
|
|
355
|
+
|
|
356
|
+
### Flowchart
|
|
357
|
+
|
|
358
|
+
```dot
|
|
359
|
+
digraph flowchart {
|
|
360
|
+
rankdir=TB;
|
|
361
|
+
node [shape=box style="rounded,filled" fillcolor=lightblue];
|
|
362
|
+
|
|
363
|
+
start [shape=ellipse label="Start" fillcolor=lightgreen];
|
|
364
|
+
end [shape=ellipse label="End" fillcolor=lightcoral];
|
|
365
|
+
decision [shape=diamond label="Valid?" fillcolor=lightyellow];
|
|
366
|
+
|
|
367
|
+
start -> process1 [label="Begin"];
|
|
368
|
+
process1 [label="Process Data"];
|
|
369
|
+
process1 -> decision;
|
|
370
|
+
decision -> process2 [label="Yes"];
|
|
371
|
+
decision -> error [label="No"];
|
|
372
|
+
process2 [label="Save Results"];
|
|
373
|
+
error [label="Handle Error" fillcolor=lightcoral];
|
|
374
|
+
process2 -> end;
|
|
375
|
+
error -> end;
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Architecture Diagram
|
|
380
|
+
|
|
381
|
+
```dot
|
|
382
|
+
digraph architecture {
|
|
383
|
+
rankdir=LR;
|
|
384
|
+
node [shape=box style=filled];
|
|
385
|
+
|
|
386
|
+
// External
|
|
387
|
+
user [label="User" shape=ellipse fillcolor=lightgray];
|
|
388
|
+
|
|
389
|
+
// Load Balancer
|
|
390
|
+
lb [label="Load Balancer" fillcolor=lightyellow];
|
|
391
|
+
|
|
392
|
+
// Application tier
|
|
393
|
+
subgraph cluster_app {
|
|
394
|
+
label="Application Tier";
|
|
395
|
+
color=lightblue;
|
|
396
|
+
style=filled;
|
|
397
|
+
|
|
398
|
+
app1 [label="App Server 1"];
|
|
399
|
+
app2 [label="App Server 2"];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Data tier
|
|
403
|
+
subgraph cluster_data {
|
|
404
|
+
label="Data Tier";
|
|
405
|
+
color=lightgreen;
|
|
406
|
+
style=filled;
|
|
407
|
+
|
|
408
|
+
db [label="Primary DB" shape=cylinder];
|
|
409
|
+
replica [label="Replica DB" shape=cylinder];
|
|
410
|
+
cache [label="Redis Cache" shape=cylinder];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
user -> lb;
|
|
414
|
+
lb -> app1;
|
|
415
|
+
lb -> app2;
|
|
416
|
+
app1 -> db;
|
|
417
|
+
app2 -> db;
|
|
418
|
+
app1 -> cache;
|
|
419
|
+
app2 -> cache;
|
|
420
|
+
db -> replica [style=dashed label="replication"];
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Sequence-like Diagram
|
|
425
|
+
|
|
426
|
+
```dot
|
|
427
|
+
digraph sequence {
|
|
428
|
+
rankdir=LR;
|
|
429
|
+
node [shape=box];
|
|
430
|
+
|
|
431
|
+
// Actors
|
|
432
|
+
{rank=same; client; server; db;}
|
|
433
|
+
|
|
434
|
+
client [label="Client"];
|
|
435
|
+
server [label="Server"];
|
|
436
|
+
db [label="Database" shape=cylinder];
|
|
437
|
+
|
|
438
|
+
// Messages (use invisible nodes for spacing)
|
|
439
|
+
client -> server [label="1. Request"];
|
|
440
|
+
server -> db [label="2. Query"];
|
|
441
|
+
db -> server [label="3. Results" style=dashed];
|
|
442
|
+
server -> client [label="4. Response" style=dashed];
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### State Machine
|
|
447
|
+
|
|
448
|
+
```dot
|
|
449
|
+
digraph state_machine {
|
|
450
|
+
rankdir=LR;
|
|
451
|
+
node [shape=circle];
|
|
452
|
+
|
|
453
|
+
// Start state
|
|
454
|
+
start [shape=point width=0.2];
|
|
455
|
+
|
|
456
|
+
// End state
|
|
457
|
+
end [shape=doublecircle];
|
|
458
|
+
|
|
459
|
+
// States
|
|
460
|
+
idle [label="Idle"];
|
|
461
|
+
running [label="Running"];
|
|
462
|
+
paused [label="Paused"];
|
|
463
|
+
|
|
464
|
+
start -> idle;
|
|
465
|
+
idle -> running [label="start"];
|
|
466
|
+
running -> paused [label="pause"];
|
|
467
|
+
paused -> running [label="resume"];
|
|
468
|
+
running -> idle [label="stop"];
|
|
469
|
+
paused -> idle [label="stop"];
|
|
470
|
+
running -> end [label="complete"];
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Dependency Graph
|
|
475
|
+
|
|
476
|
+
```dot
|
|
477
|
+
digraph dependencies {
|
|
478
|
+
rankdir=BT; // Bottom to top for dependencies
|
|
479
|
+
node [shape=box style="rounded,filled" fillcolor=lightblue];
|
|
480
|
+
|
|
481
|
+
app [label="Application" fillcolor=lightgreen];
|
|
482
|
+
|
|
483
|
+
// Direct dependencies
|
|
484
|
+
lodash [label="lodash"];
|
|
485
|
+
express [label="express"];
|
|
486
|
+
pg [label="pg"];
|
|
487
|
+
|
|
488
|
+
// Transitive dependencies
|
|
489
|
+
body_parser [label="body-parser" fillcolor=lightyellow];
|
|
490
|
+
qs [label="qs" fillcolor=lightyellow];
|
|
491
|
+
|
|
492
|
+
app -> lodash;
|
|
493
|
+
app -> express;
|
|
494
|
+
app -> pg;
|
|
495
|
+
express -> body_parser;
|
|
496
|
+
body_parser -> qs;
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### ER Diagram (using HTML labels)
|
|
501
|
+
|
|
502
|
+
```dot
|
|
503
|
+
digraph er {
|
|
504
|
+
rankdir=LR;
|
|
505
|
+
node [shape=none margin=0];
|
|
506
|
+
|
|
507
|
+
users [label=<
|
|
508
|
+
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="4">
|
|
509
|
+
<TR><TD BGCOLOR="lightblue"><B>users</B></TD></TR>
|
|
510
|
+
<TR><TD ALIGN="LEFT">id: INT PK</TD></TR>
|
|
511
|
+
<TR><TD ALIGN="LEFT">name: VARCHAR</TD></TR>
|
|
512
|
+
<TR><TD ALIGN="LEFT">email: VARCHAR</TD></TR>
|
|
513
|
+
</TABLE>
|
|
514
|
+
>];
|
|
515
|
+
|
|
516
|
+
orders [label=<
|
|
517
|
+
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="4">
|
|
518
|
+
<TR><TD BGCOLOR="lightgreen"><B>orders</B></TD></TR>
|
|
519
|
+
<TR><TD ALIGN="LEFT">id: INT PK</TD></TR>
|
|
520
|
+
<TR><TD ALIGN="LEFT">user_id: INT FK</TD></TR>
|
|
521
|
+
<TR><TD ALIGN="LEFT">total: DECIMAL</TD></TR>
|
|
522
|
+
</TABLE>
|
|
523
|
+
>];
|
|
524
|
+
|
|
525
|
+
users -> orders [label="1:N" arrowhead=crow arrowtail=none];
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## Layout Engines
|
|
530
|
+
|
|
531
|
+
Graphviz includes several layout engines:
|
|
532
|
+
|
|
533
|
+
| Engine | Best For | Direction |
|
|
534
|
+
| ----------- | ------------------------------- | --------- |
|
|
535
|
+
| `dot` | Hierarchical graphs, DAGs | TB/LR |
|
|
536
|
+
| `neato` | Undirected graphs, spring model | Any |
|
|
537
|
+
| `fdp` | Large undirected graphs | Any |
|
|
538
|
+
| `sfdp` | Very large graphs | Any |
|
|
539
|
+
| `twopi` | Radial layouts | Radial |
|
|
540
|
+
| `circo` | Circular layouts | Circular |
|
|
541
|
+
| `osage` | Clustered graphs | - |
|
|
542
|
+
| `patchwork` | Treemaps | - |
|
|
543
|
+
|
|
544
|
+
Specify with command line: `dot -Kneato -Tpng input.dot -o output.png`
|
|
545
|
+
|
|
546
|
+
## Best Practices
|
|
547
|
+
|
|
548
|
+
1. **Use meaningful IDs**: `web_server` not `n1`
|
|
549
|
+
2. **Group related nodes**: Use clusters for visual organization
|
|
550
|
+
3. **Control direction**: `rankdir=LR` for wide diagrams, `TB` for tall
|
|
551
|
+
4. **Use consistent styling**: Set defaults with `node [...]` and `edge [...]`
|
|
552
|
+
5. **Label edges**: Make relationships clear
|
|
553
|
+
6. **Use colors purposefully**: Highlight important nodes/paths
|
|
554
|
+
7. **Keep it simple**: Don't overcrowd; split complex diagrams
|
|
555
|
+
8. **Use constraint=false**: For edges that shouldn't affect layout
|
|
556
|
+
9. **Adjust spacing**: Use `ranksep` and `nodesep` for readability
|
|
557
|
+
10. **Test with different engines**: `dot` vs `neato` can give very different results
|
|
558
|
+
|
|
559
|
+
## Critical Rendering Issues
|
|
560
|
+
|
|
561
|
+
### HTML TABLE Labels Break Kerning
|
|
562
|
+
|
|
563
|
+
**Problem:** Using `<TABLE>` in HTML labels causes Pango to render each cell separately, destroying letter spacing (kerning).
|
|
564
|
+
|
|
565
|
+
**Symptom:** Text like "eddo-telegram-bot" has uneven letter spacing.
|
|
566
|
+
|
|
567
|
+
```dot
|
|
568
|
+
// ❌ BAD - TABLE breaks kerning
|
|
569
|
+
node [label=<<TABLE><TR><TD><FONT>Service Name</FONT></TD></TR></TABLE>>]
|
|
570
|
+
|
|
571
|
+
// ✅ GOOD - Simple HTML with BR preserves kerning
|
|
572
|
+
node [label=<Service Name<BR/><FONT POINT-SIZE="10" COLOR="#737373">subtitle</FONT>>]
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Rule:** Only use `<TABLE>` when you need multi-column alignment. For simple multi-line labels, use `<BR/>`.
|
|
576
|
+
|
|
577
|
+
### DPI for Crisp Output
|
|
578
|
+
|
|
579
|
+
**Problem:** Default 96 DPI produces fuzzy text on modern displays.
|
|
580
|
+
|
|
581
|
+
**Solution:** Set high DPI in graph attributes:
|
|
582
|
+
|
|
583
|
+
```dot
|
|
584
|
+
digraph G {
|
|
585
|
+
dpi=192; // 2x for retina displays
|
|
586
|
+
// or dpi=300 for print quality
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Note:** Higher DPI increases file size proportionally.
|
|
591
|
+
|
|
592
|
+
### Font Selection and Rendering
|
|
593
|
+
|
|
594
|
+
**Problem:** Graphviz uses fontconfig which matches font names loosely. "Helvetica Neue" might fall back to something unexpected.
|
|
595
|
+
|
|
596
|
+
**Solution:** Use widely-available fonts:
|
|
597
|
+
|
|
598
|
+
```dot
|
|
599
|
+
graph [fontname="Arial" fontsize=14];
|
|
600
|
+
node [fontname="Arial" fontsize=12];
|
|
601
|
+
edge [fontname="Arial" fontsize=10];
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Safe fonts:** Arial, Helvetica, Times, Courier, Georgia, Verdana
|
|
605
|
+
|
|
606
|
+
### Edge Labels with Ortho Splines
|
|
607
|
+
|
|
608
|
+
**Problem:** Edge labels may not appear when using `splines=ortho`.
|
|
609
|
+
|
|
610
|
+
**Solution:** Use `splines=true` (curved) or `splines=line` (straight) when labels are important.
|
|
611
|
+
|
|
612
|
+
## Visual Design Principles
|
|
613
|
+
|
|
614
|
+
### Minimalist (Data-Ink Ratio)
|
|
615
|
+
|
|
616
|
+
Maximize data, minimize non-data ink:
|
|
617
|
+
|
|
618
|
+
- Remove decorative borders if they don't encode information
|
|
619
|
+
- Use line weight to encode traffic/volume
|
|
620
|
+
- Avoid chartjunk (3D effects, gradients, unnecessary fills)
|
|
621
|
+
- Every pixel should convey data
|
|
622
|
+
|
|
623
|
+
**When to apply:** Technical documentation, data-dense dashboards, publications.
|
|
624
|
+
|
|
625
|
+
**Limitation:** Can be too minimal for audiences who need visual anchors.
|
|
626
|
+
|
|
627
|
+
### Communicative (Functional Clarity)
|
|
628
|
+
|
|
629
|
+
Emphasize communication over minimalism:
|
|
630
|
+
|
|
631
|
+
- **Functional decoration is not chartjunk** — borders and shapes aid parsing
|
|
632
|
+
- **Redundant encoding for critical data** — error states need color + icon + size
|
|
633
|
+
- **Know your audience** — label for the least technical viewer
|
|
634
|
+
- **Guide the eye** — create visual hierarchy that leads to the insight
|
|
635
|
+
|
|
636
|
+
**When to apply:** Operational dashboards, presentations to mixed audiences, alerting systems.
|
|
637
|
+
|
|
638
|
+
### Modern SaaS (Clean Professional)
|
|
639
|
+
|
|
640
|
+
Clean, professional design for modern tooling:
|
|
641
|
+
|
|
642
|
+
- **White space is design** — let elements breathe
|
|
643
|
+
- **One accent color** — gray scale for structure, color for emphasis
|
|
644
|
+
- **No gradients, no shadows** — flat, honest design
|
|
645
|
+
- **Typography carries hierarchy** — weight and size, not decoration
|
|
646
|
+
- **Thin lines, subtle borders** — 1px borders, never black
|
|
647
|
+
|
|
648
|
+
**When to apply:** Product documentation, engineering blogs, modern tooling.
|
|
649
|
+
|
|
650
|
+
### Choosing an Approach
|
|
651
|
+
|
|
652
|
+
| Audience | Approach | Style |
|
|
653
|
+
| ---------------------- | ------------- | ------------------------- |
|
|
654
|
+
| Engineers reading docs | Minimalist | Minimal, data-dense |
|
|
655
|
+
| Ops team monitoring | Communicative | Clear, redundant encoding |
|
|
656
|
+
| Product/stakeholders | Modern SaaS | Clean, professional |
|
|
657
|
+
| Print/publication | Minimalist | High information density |
|
|
658
|
+
|
|
659
|
+
## Professional Theming
|
|
660
|
+
|
|
661
|
+
### Light Theme (SaaS Style)
|
|
662
|
+
|
|
663
|
+
```dot
|
|
664
|
+
digraph G {
|
|
665
|
+
bgcolor="white";
|
|
666
|
+
dpi=192;
|
|
667
|
+
pad=0.5;
|
|
668
|
+
|
|
669
|
+
graph [fontname="Arial" fontsize=14 fontcolor="#404040"];
|
|
670
|
+
node [fontname="Arial" fontsize=12 fontcolor="#171717"];
|
|
671
|
+
edge [fontname="Arial" fontsize=9 fontcolor="#525252" color="#a3a3a3"];
|
|
672
|
+
|
|
673
|
+
// Entry points (ellipse)
|
|
674
|
+
users [label="Users" shape=ellipse style=filled fillcolor="#f5f5f5" color="#d4d4d4"];
|
|
675
|
+
|
|
676
|
+
// Services (rounded box)
|
|
677
|
+
api [label=<api-server<BR/><FONT POINT-SIZE="10" COLOR="#525252">572K traces</FONT>>
|
|
678
|
+
shape=box style="rounded" color="#d4d4d4"];
|
|
679
|
+
|
|
680
|
+
// Error state (red accent)
|
|
681
|
+
problem [label=<problem-service<BR/><FONT POINT-SIZE="10" COLOR="#dc2626">8% errors</FONT>>
|
|
682
|
+
shape=box style="rounded,filled" fillcolor="#fef2f2" color="#dc2626" penwidth=2];
|
|
683
|
+
|
|
684
|
+
// Data layer (cylinder)
|
|
685
|
+
db [label="Database" shape=cylinder style=filled fillcolor="#fafafa" color="#d4d4d4"];
|
|
686
|
+
|
|
687
|
+
// External (dashed)
|
|
688
|
+
external [label="External APIs" shape=box style="dashed,rounded" color="#a3a3a3"];
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
**Light palette:**
|
|
693
|
+
| Element | Color |
|
|
694
|
+
|---------|-------|
|
|
695
|
+
| Background | `#ffffff` |
|
|
696
|
+
| Node fill | `#f5f5f5` / `#fafafa` |
|
|
697
|
+
| Node border | `#d4d4d4` |
|
|
698
|
+
| Primary text | `#171717` |
|
|
699
|
+
| Secondary text | `#525252` |
|
|
700
|
+
| Muted text | `#737373` |
|
|
701
|
+
| Edges | `#a3a3a3` |
|
|
702
|
+
| Error fill | `#fef2f2` |
|
|
703
|
+
| Error border | `#dc2626` |
|
|
704
|
+
|
|
705
|
+
### Dark Theme
|
|
706
|
+
|
|
707
|
+
```dot
|
|
708
|
+
digraph G {
|
|
709
|
+
bgcolor="#0a0a0a";
|
|
710
|
+
dpi=192;
|
|
711
|
+
pad=0.5;
|
|
712
|
+
|
|
713
|
+
graph [fontname="Arial" fontsize=14 fontcolor="#a3a3a3"];
|
|
714
|
+
node [fontname="Arial" fontsize=12 fontcolor="#e5e5e5"];
|
|
715
|
+
edge [fontname="Arial" fontsize=9 fontcolor="#737373" color="#525252"];
|
|
716
|
+
|
|
717
|
+
// Entry points
|
|
718
|
+
users [label="Users" shape=ellipse style=filled fillcolor="#1c1c1c" color="#404040" fontcolor="#a3a3a3"];
|
|
719
|
+
|
|
720
|
+
// Services
|
|
721
|
+
api [label=<api-server<BR/><FONT POINT-SIZE="10" COLOR="#a3a3a3">572K traces</FONT>>
|
|
722
|
+
shape=box style="rounded,filled" fillcolor="#171717" color="#404040"];
|
|
723
|
+
|
|
724
|
+
// Error state (red glow on dark)
|
|
725
|
+
problem [label=<problem-service<BR/><FONT POINT-SIZE="10" COLOR="#f87171">8% errors</FONT>>
|
|
726
|
+
shape=box style="rounded,filled" fillcolor="#1f1315" color="#dc2626" penwidth=2 fontcolor="#fecaca"];
|
|
727
|
+
|
|
728
|
+
// Data layer
|
|
729
|
+
db [label="Database" shape=cylinder style=filled fillcolor="#1c1c1c" color="#404040"];
|
|
730
|
+
|
|
731
|
+
// External
|
|
732
|
+
external [label="External APIs" shape=box style="dashed,rounded,filled" fillcolor="#0a0a0a" color="#525252"];
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Dark palette:**
|
|
737
|
+
| Element | Color |
|
|
738
|
+
|---------|-------|
|
|
739
|
+
| Background | `#0a0a0a` |
|
|
740
|
+
| Node fill | `#171717` / `#1c1c1c` |
|
|
741
|
+
| Node border | `#404040` |
|
|
742
|
+
| Primary text | `#e5e5e5` |
|
|
743
|
+
| Secondary text | `#a3a3a3` |
|
|
744
|
+
| Muted text | `#737373` |
|
|
745
|
+
| Edges | `#525252` |
|
|
746
|
+
| Error fill | `#1f1315` |
|
|
747
|
+
| Error border | `#dc2626` |
|
|
748
|
+
| Error text | `#f87171` (lighter for contrast) |
|
|
749
|
+
|
|
750
|
+
## Edge Label Strategy
|
|
751
|
+
|
|
752
|
+
### When to Label Edges
|
|
753
|
+
|
|
754
|
+
Apply the principle: **Label only what deviates from expectation.**
|
|
755
|
+
|
|
756
|
+
| Label Type | Keep? | Reason |
|
|
757
|
+
| ----------- | ----- | -------------------------------- |
|
|
758
|
+
| HTTP/REST | ❌ | Assumed between web services |
|
|
759
|
+
| CRUD/SQL | ❌ | Assumed for database connections |
|
|
760
|
+
| IPC | ✅ | Surprising — not HTTP |
|
|
761
|
+
| webhook | ✅ | Notable — async external |
|
|
762
|
+
| oauth | ✅ | Specific authentication flow |
|
|
763
|
+
| gRPC | ✅ | Different from default HTTP |
|
|
764
|
+
| async/queue | ✅ | Different execution model |
|
|
765
|
+
|
|
766
|
+
### Edge Label Styling
|
|
767
|
+
|
|
768
|
+
Keep labels subordinate to nodes:
|
|
769
|
+
|
|
770
|
+
```dot
|
|
771
|
+
edge [fontname="Arial" fontsize=9 fontcolor="#737373"];
|
|
772
|
+
|
|
773
|
+
A -> B [label="http"]; // Only if audience needs it
|
|
774
|
+
A -> C [label="ipc"]; // Notable - different protocol
|
|
775
|
+
A -> D [label="webhook"]; // Notable - async pattern
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
## Complete Architecture Example
|
|
779
|
+
|
|
780
|
+
````dot
|
|
781
|
+
digraph EdDoArchitecture {
|
|
782
|
+
rankdir=TB;
|
|
783
|
+
splines=true;
|
|
784
|
+
nodesep=0.9;
|
|
785
|
+
ranksep=0.85;
|
|
786
|
+
bgcolor="white";
|
|
787
|
+
pad=0.5;
|
|
788
|
+
dpi=192;
|
|
789
|
+
|
|
790
|
+
// Typography
|
|
791
|
+
graph [fontname="Arial" fontsize=14 fontcolor="#404040"];
|
|
792
|
+
node [fontname="Arial" fontsize=12 fontcolor="#171717"];
|
|
793
|
+
edge [fontname="Arial" fontsize=9 fontcolor="#525252" color="#a3a3a3" arrowsize=0.6 arrowhead=vee];
|
|
794
|
+
|
|
795
|
+
// Title with insight (communicative: guide the eye)
|
|
796
|
+
labelloc=t;
|
|
797
|
+
label=<Service Architecture<BR/><FONT POINT-SIZE="11" COLOR="#737373">telegram-bot error rate needs attention</FONT>>;
|
|
798
|
+
|
|
799
|
+
// Entry points (distinct shape)
|
|
800
|
+
web_users [label="Web Users" shape=ellipse style=filled fillcolor="#f5f5f5" color="#d4d4d4" fontsize=11];
|
|
801
|
+
telegram_users [label="Telegram Users" shape=ellipse style=filled fillcolor="#f5f5f5" color="#d4d4d4" fontsize=11];
|
|
802
|
+
|
|
803
|
+
// Services - size encodes importance (minimalist: data in the ink)
|
|
804
|
+
web_client [
|
|
805
|
+
label=<web-client<BR/><FONT POINT-SIZE="10" COLOR="#525252">11K traces</FONT>>
|
|
806
|
+
shape=box style="rounded" color="#d4d4d4" penwidth=1
|
|
807
|
+
];
|
|
808
|
+
|
|
809
|
+
// Error state: redundant encoding (communicative)
|
|
810
|
+
telegram_bot [
|
|
811
|
+
label=<telegram-bot<BR/><FONT POINT-SIZE="11" COLOR="#dc2626"><B>97K · 8% errors</B></FONT>>
|
|
812
|
+
shape=box style="rounded,filled" fillcolor="#fef2f2" color="#dc2626" penwidth=2
|
|
813
|
+
fontsize=13
|
|
814
|
+
];
|
|
815
|
+
|
|
816
|
+
web_api [
|
|
817
|
+
label=<web-api<BR/><FONT POINT-SIZE="10" COLOR="#525252">572K traces</FONT>>
|
|
818
|
+
shape=box style="rounded" color="#d4d4d4" penwidth=1
|
|
819
|
+
fontsize=14
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
mcp_server [
|
|
823
|
+
label=<mcp-server<BR/><FONT POINT-SIZE="10" COLOR="#525252">32K traces</FONT>>
|
|
824
|
+
shape=box style="rounded" color="#d4d4d4" penwidth=1
|
|
825
|
+
];
|
|
826
|
+
|
|
827
|
+
// Data layer (distinct shape)
|
|
828
|
+
couchdb [
|
|
829
|
+
label=<CouchDB<BR/><FONT POINT-SIZE="10" COLOR="#525252">225K queries</FONT>>
|
|
830
|
+
shape=cylinder style=filled fillcolor="#fafafa" color="#d4d4d4"
|
|
831
|
+
];
|
|
832
|
+
|
|
833
|
+
// External (clearly outside system boundary)
|
|
834
|
+
external [
|
|
835
|
+
label=<External APIs<BR/><FONT POINT-SIZE="9" COLOR="#737373">Telegram · Google · GitHub</FONT>>
|
|
836
|
+
shape=box style="dashed,rounded" color="#a3a3a3"
|
|
837
|
+
];
|
|
838
|
+
|
|
839
|
+
// Layout control
|
|
840
|
+
{rank=same; web_users; telegram_users;}
|
|
841
|
+
{rank=same; web_client; telegram_bot;}
|
|
842
|
+
{rank=same; web_api; mcp_server;}
|
|
843
|
+
|
|
844
|
+
// Edges - weight encodes traffic (minimalist)
|
|
845
|
+
web_users -> web_client [penwidth=1];
|
|
846
|
+
telegram_users -> telegram_bot [penwidth=1];
|
|
847
|
+
|
|
848
|
+
// Label only what's notable (not "http" - assumed)
|
|
849
|
+
web_client -> web_api [penwidth=1.5 label="http"];
|
|
850
|
+
telegram_bot -> web_api [penwidth=2 label="http"];
|
|
851
|
+
|
|
852
|
+
// IPC is notable
|
|
853
|
+
web_api -> mcp_server [penwidth=1 dir=both arrowsize=0.45 constraint=false label="ipc"];
|
|
854
|
+
|
|
855
|
+
// Database (no labels - CRUD assumed)
|
|
856
|
+
web_api -> couchdb [penwidth=2.5];
|
|
857
|
+
telegram_bot -> couchdb [penwidth=1.2];
|
|
858
|
+
mcp_server -> couchdb [penwidth=1];
|
|
859
|
+
|
|
860
|
+
// External (dashed = crosses boundary)
|
|
861
|
+
telegram_bot -> external [penwidth=0.75 color="#b5b5b5" style=dashed constraint=false label="webhook"];
|
|
862
|
+
web_api -> external [penwidth=0.75 color="#b5b5b5" style=dashed constraint=false label="oauth"];
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
## Output Formats
|
|
866
|
+
|
|
867
|
+
Common formats: `png`, `svg`, `pdf`, `ps`, `json`, `dot` (canonical)
|
|
868
|
+
|
|
869
|
+
```bash
|
|
870
|
+
# PNG output
|
|
871
|
+
dot -Tpng input.dot -o output.png
|
|
872
|
+
|
|
873
|
+
# SVG output (best for web, scalable)
|
|
874
|
+
dot -Tsvg input.dot -o output.svg
|
|
875
|
+
|
|
876
|
+
# PDF output
|
|
877
|
+
dot -Tpdf input.dot -o output.pdf
|
|
878
|
+
````
|
|
879
|
+
|
|
880
|
+
## Quick Reference: Quality Checklist
|
|
881
|
+
|
|
882
|
+
Before finalizing any diagram, verify:
|
|
883
|
+
|
|
884
|
+
- [ ] **DPI set to 192+** for crisp text
|
|
885
|
+
- [ ] **Font explicitly set** to Arial or Helvetica
|
|
886
|
+
- [ ] **No TABLE in HTML labels** (use BR instead for kerning)
|
|
887
|
+
- [ ] **Font sizes 10pt+** for readability
|
|
888
|
+
- [ ] **Edge labels only for surprising protocols** (skip HTTP, CRUD)
|
|
889
|
+
- [ ] **Error states use redundant encoding** (color + fill + size)
|
|
890
|
+
- [ ] **Shapes distinguish categories** (ellipse=users, box=services, cylinder=data)
|
|
891
|
+
- [ ] **splines=true or splines=line** if edge labels needed
|
|
892
|
+
- [ ] **Accessibility:** contrast ratio 4.5:1+ for text
|
|
893
|
+
|
|
894
|
+
## References
|
|
895
|
+
|
|
896
|
+
- [DOT Language Specification](https://graphviz.org/doc/info/lang.html)
|
|
897
|
+
- [Node Shapes](https://graphviz.org/doc/info/shapes.html)
|
|
898
|
+
- [Attributes Reference](https://graphviz.org/doc/info/attrs.html)
|
|
899
|
+
- [Color Names](https://graphviz.org/doc/info/colors.html)
|
|
900
|
+
- [Arrow Shapes](https://graphviz.org/doc/info/arrows.html)
|
|
901
|
+
- [Gallery](https://graphviz.org/gallery/)
|
|
902
|
+
|
|
903
|
+
### Visual Design References
|
|
904
|
+
|
|
905
|
+
- _The Visual Display of Quantitative Information_ (data-ink ratio, minimalist approach)
|
|
906
|
+
- _The Functional Art_, _How Charts Lie_ (clarity over minimalism, communicative approach)
|
|
907
|
+
- [Observable](https://observablehq.com/) (modern SaaS aesthetics, D3.js patterns)
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graphviz Chart Extension
|
|
3
|
+
*
|
|
4
|
+
* Renders Graphviz DOT specifications as PNG or SVG images in terminals that support
|
|
5
|
+
* inline images (Ghostty, Kitty, iTerm2, WezTerm).
|
|
6
|
+
*
|
|
7
|
+
* Use cases:
|
|
8
|
+
* - Architecture diagrams
|
|
9
|
+
* - Flowcharts
|
|
10
|
+
* - State machines
|
|
11
|
+
* - Dependency graphs
|
|
12
|
+
* - ER diagrams
|
|
13
|
+
* - Network topologies
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
17
|
+
import { Type } from '@sinclair/typebox';
|
|
18
|
+
import { execSync } from 'node:child_process';
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
// Compute reference path using ESM import.meta.url
|
|
25
|
+
const GRAPHVIZ_REFERENCE_PATH = join(
|
|
26
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
27
|
+
'graphviz-reference.md',
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export default function (pi: ExtensionAPI) {
|
|
31
|
+
pi.registerTool({
|
|
32
|
+
name: 'graphviz_chart',
|
|
33
|
+
label: 'Graphviz Chart',
|
|
34
|
+
description: `Render a Graphviz DOT specification as a PNG image.
|
|
35
|
+
|
|
36
|
+
Graphviz will be auto-installed if not present (via brew on macOS, apt/dnf on Linux).
|
|
37
|
+
If auto-install fails, the tool returns installation instructions - do NOT fall back to ASCII art.
|
|
38
|
+
|
|
39
|
+
IMPORTANT: Before using this tool, read the complete reference documentation at:
|
|
40
|
+
${GRAPHVIZ_REFERENCE_PATH}
|
|
41
|
+
|
|
42
|
+
The reference contains critical information about:
|
|
43
|
+
- DOT language syntax for graphs, nodes, and edges
|
|
44
|
+
- All node shapes (box, cylinder, diamond, ellipse, etc.)
|
|
45
|
+
- Edge styles and arrow types
|
|
46
|
+
- Clusters and subgraphs
|
|
47
|
+
- Layout engines (dot, neato, fdp, circo, twopi)
|
|
48
|
+
- Professional theming (light/dark themes, SaaS aesthetics)
|
|
49
|
+
- Common patterns (flowcharts, architecture diagrams, state machines)
|
|
50
|
+
|
|
51
|
+
Pass a complete DOT graph definition. Supports:
|
|
52
|
+
- Graph types: graph (undirected), digraph (directed), strict
|
|
53
|
+
- Node shapes: box, ellipse, circle, diamond, cylinder, record, etc.
|
|
54
|
+
- Edge styles: solid, dashed, dotted, bold
|
|
55
|
+
- Arrows: normal, dot, diamond, box, vee, none, etc.
|
|
56
|
+
- Clusters: subgraph cluster_name { ... }
|
|
57
|
+
- Attributes: color, fillcolor, style, label, fontname, etc.
|
|
58
|
+
- Layout engines: dot (default), neato, fdp, circo, twopi
|
|
59
|
+
|
|
60
|
+
Example DOT syntax:
|
|
61
|
+
\`\`\`
|
|
62
|
+
digraph G {
|
|
63
|
+
rankdir=LR;
|
|
64
|
+
node [shape=box style="rounded,filled" fillcolor=lightblue];
|
|
65
|
+
|
|
66
|
+
A [label="Start"];
|
|
67
|
+
B [label="Process"];
|
|
68
|
+
C [label="End" fillcolor=lightgreen];
|
|
69
|
+
|
|
70
|
+
A -> B [label="step 1"];
|
|
71
|
+
B -> C [label="step 2"];
|
|
72
|
+
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
Reference: https://graphviz.org/doc/info/lang.html`,
|
|
76
|
+
parameters: Type.Object({
|
|
77
|
+
dot: Type.String({
|
|
78
|
+
description: 'Graphviz DOT specification (complete graph definition)',
|
|
79
|
+
}),
|
|
80
|
+
engine: Type.Optional(
|
|
81
|
+
Type.String({
|
|
82
|
+
description:
|
|
83
|
+
'Layout engine: dot (hierarchical, default), neato (spring), fdp (force-directed), circo (circular), twopi (radial)',
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
width: Type.Optional(
|
|
87
|
+
Type.Number({
|
|
88
|
+
description: 'Output width in pixels (default: auto based on graph)',
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
91
|
+
height: Type.Optional(
|
|
92
|
+
Type.Number({
|
|
93
|
+
description: 'Output height in pixels (default: auto based on graph)',
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
save_path: Type.Optional(
|
|
97
|
+
Type.String({
|
|
98
|
+
description:
|
|
99
|
+
'Optional file path to save the chart. Format determined by extension: .svg for SVG, .png for PNG (default)',
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
async execute(_toolCallId, params, _onUpdate, _ctx, signal) {
|
|
105
|
+
const {
|
|
106
|
+
dot,
|
|
107
|
+
engine = 'dot',
|
|
108
|
+
width,
|
|
109
|
+
height,
|
|
110
|
+
save_path,
|
|
111
|
+
} = params as {
|
|
112
|
+
dot: string;
|
|
113
|
+
engine?: string;
|
|
114
|
+
width?: number;
|
|
115
|
+
height?: number;
|
|
116
|
+
save_path?: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (signal?.aborted) {
|
|
120
|
+
return { content: [{ type: 'text', text: 'Cancelled' }], details: {} };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate engine
|
|
124
|
+
const validEngines = ['dot', 'neato', 'fdp', 'sfdp', 'circo', 'twopi', 'osage', 'patchwork'];
|
|
125
|
+
if (!validEngines.includes(engine)) {
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: `Invalid engine "${engine}". Valid engines: ${validEngines.join(', ')}`,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
details: { error: 'Invalid engine' },
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Determine output format from save_path extension
|
|
139
|
+
const isSvgOutput = save_path?.toLowerCase().endsWith('.svg') ?? false;
|
|
140
|
+
const outputFormat = isSvgOutput ? 'svg' : 'png';
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Check if graphviz is installed, auto-install if not
|
|
144
|
+
try {
|
|
145
|
+
execSync('which dot', { encoding: 'utf-8' });
|
|
146
|
+
} catch {
|
|
147
|
+
// Try to auto-install graphviz
|
|
148
|
+
const platform = process.platform;
|
|
149
|
+
let installCmd: string | null = null;
|
|
150
|
+
|
|
151
|
+
if (platform === 'darwin') {
|
|
152
|
+
// macOS - check if brew is available
|
|
153
|
+
try {
|
|
154
|
+
execSync('which brew', { encoding: 'utf-8' });
|
|
155
|
+
installCmd = 'brew install graphviz';
|
|
156
|
+
} catch {
|
|
157
|
+
// brew not available
|
|
158
|
+
}
|
|
159
|
+
} else if (platform === 'linux') {
|
|
160
|
+
// Linux - try apt first, then dnf
|
|
161
|
+
try {
|
|
162
|
+
execSync('which apt', { encoding: 'utf-8' });
|
|
163
|
+
installCmd = 'sudo apt install -y graphviz';
|
|
164
|
+
} catch {
|
|
165
|
+
try {
|
|
166
|
+
execSync('which dnf', { encoding: 'utf-8' });
|
|
167
|
+
installCmd = 'sudo dnf install -y graphviz';
|
|
168
|
+
} catch {
|
|
169
|
+
// no supported package manager
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (installCmd) {
|
|
175
|
+
try {
|
|
176
|
+
execSync(installCmd, { encoding: 'utf-8', stdio: 'inherit' });
|
|
177
|
+
// Verify installation succeeded
|
|
178
|
+
execSync('which dot', { encoding: 'utf-8' });
|
|
179
|
+
} catch {
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: 'text',
|
|
184
|
+
text: `Failed to auto-install Graphviz. Please install manually:\n- macOS: brew install graphviz\n- Ubuntu/Debian: sudo apt install graphviz\n- Fedora/RHEL: sudo dnf install graphviz`,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
details: { error: 'Graphviz installation failed' },
|
|
188
|
+
isError: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: 'text',
|
|
196
|
+
text: `Graphviz not found and auto-install not supported on this platform. Please install manually:\n- macOS: brew install graphviz\n- Ubuntu/Debian: sudo apt install graphviz\n- Fedora/RHEL: sudo dnf install graphviz\n- Windows: https://graphviz.org/download/`,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
details: { error: 'Graphviz not installed' },
|
|
200
|
+
isError: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const tmpDot = join(tmpdir(), `graphviz-${Date.now()}.dot`);
|
|
206
|
+
const tmpOutput = join(tmpdir(), `graphviz-${Date.now()}.${outputFormat}`);
|
|
207
|
+
// Always generate PNG for terminal display
|
|
208
|
+
const tmpPng = isSvgOutput
|
|
209
|
+
? join(tmpdir(), `graphviz-${Date.now()}-display.png`)
|
|
210
|
+
: tmpOutput;
|
|
211
|
+
|
|
212
|
+
// Write DOT file
|
|
213
|
+
writeFileSync(tmpDot, dot);
|
|
214
|
+
|
|
215
|
+
// Build graphviz command for the requested output format
|
|
216
|
+
let cmd = `${engine} -T${outputFormat}`;
|
|
217
|
+
|
|
218
|
+
// Add size constraints if specified (applies to both formats)
|
|
219
|
+
if (width || height) {
|
|
220
|
+
// Graphviz uses inches, convert from pixels (assuming 96 DPI)
|
|
221
|
+
const dpi = 96;
|
|
222
|
+
if (width && height) {
|
|
223
|
+
cmd += ` -Gsize="${width / dpi},${height / dpi}!"`;
|
|
224
|
+
} else if (width) {
|
|
225
|
+
cmd += ` -Gsize="${width / dpi},1000!" -Gratio=compress`;
|
|
226
|
+
} else if (height) {
|
|
227
|
+
cmd += ` -Gsize="1000,${height / dpi}!" -Gratio=compress`;
|
|
228
|
+
}
|
|
229
|
+
if (outputFormat === 'png') {
|
|
230
|
+
cmd += ` -Gdpi=${dpi}`;
|
|
231
|
+
}
|
|
232
|
+
} else if (outputFormat === 'png') {
|
|
233
|
+
// Default: higher DPI for better quality (PNG only)
|
|
234
|
+
cmd += ` -Gdpi=150`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
cmd += ` "${tmpDot}" -o "${tmpOutput}"`;
|
|
238
|
+
|
|
239
|
+
// Execute graphviz for main output
|
|
240
|
+
try {
|
|
241
|
+
execSync(cmd, {
|
|
242
|
+
encoding: 'utf-8',
|
|
243
|
+
timeout: 30000,
|
|
244
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
245
|
+
});
|
|
246
|
+
} catch (execError: any) {
|
|
247
|
+
const errorMsg = execError.stderr || execError.message;
|
|
248
|
+
// Clean up
|
|
249
|
+
try {
|
|
250
|
+
unlinkSync(tmpDot);
|
|
251
|
+
} catch {}
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: 'text', text: `Graphviz error: ${errorMsg}` }],
|
|
254
|
+
details: { error: errorMsg },
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if output was created
|
|
260
|
+
if (!existsSync(tmpOutput)) {
|
|
261
|
+
try {
|
|
262
|
+
unlinkSync(tmpDot);
|
|
263
|
+
} catch {}
|
|
264
|
+
return {
|
|
265
|
+
content: [
|
|
266
|
+
{ type: 'text', text: 'Graphviz produced no output. Check your DOT syntax.' },
|
|
267
|
+
],
|
|
268
|
+
details: { error: 'No output' },
|
|
269
|
+
isError: true,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// If SVG output, also generate PNG for terminal display
|
|
274
|
+
if (isSvgOutput) {
|
|
275
|
+
let pngCmd = `${engine} -Tpng`;
|
|
276
|
+
if (width || height) {
|
|
277
|
+
const dpi = 96;
|
|
278
|
+
if (width && height) {
|
|
279
|
+
pngCmd += ` -Gsize="${width / dpi},${height / dpi}!"`;
|
|
280
|
+
} else if (width) {
|
|
281
|
+
pngCmd += ` -Gsize="${width / dpi},1000!" -Gratio=compress`;
|
|
282
|
+
} else if (height) {
|
|
283
|
+
pngCmd += ` -Gsize="1000,${height / dpi}!" -Gratio=compress`;
|
|
284
|
+
}
|
|
285
|
+
pngCmd += ` -Gdpi=${dpi}`;
|
|
286
|
+
} else {
|
|
287
|
+
pngCmd += ` -Gdpi=150`;
|
|
288
|
+
}
|
|
289
|
+
pngCmd += ` "${tmpDot}" -o "${tmpPng}"`;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
execSync(pngCmd, {
|
|
293
|
+
encoding: 'utf-8',
|
|
294
|
+
timeout: 30000,
|
|
295
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
296
|
+
});
|
|
297
|
+
} catch {
|
|
298
|
+
// If PNG generation fails, we can still save SVG but won't display
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Read the PNG file as base64 for terminal display
|
|
303
|
+
let base64Data: string | undefined;
|
|
304
|
+
if (existsSync(tmpPng)) {
|
|
305
|
+
const pngBuffer = readFileSync(tmpPng);
|
|
306
|
+
base64Data = pngBuffer.toString('base64');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// If save_path provided, copy the output to that location
|
|
310
|
+
let savedPath: string | undefined;
|
|
311
|
+
if (save_path) {
|
|
312
|
+
try {
|
|
313
|
+
// Ensure directory exists
|
|
314
|
+
mkdirSync(dirname(save_path), { recursive: true });
|
|
315
|
+
const outputBuffer = readFileSync(tmpOutput);
|
|
316
|
+
writeFileSync(save_path, outputBuffer);
|
|
317
|
+
savedPath = save_path;
|
|
318
|
+
} catch (saveErr: any) {
|
|
319
|
+
// Don't fail the whole operation, just note the error
|
|
320
|
+
console.error(`Failed to save to ${save_path}: ${saveErr.message}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Clean up temp files
|
|
325
|
+
try {
|
|
326
|
+
unlinkSync(tmpDot);
|
|
327
|
+
} catch {}
|
|
328
|
+
try {
|
|
329
|
+
unlinkSync(tmpOutput);
|
|
330
|
+
} catch {}
|
|
331
|
+
if (isSvgOutput && tmpPng !== tmpOutput) {
|
|
332
|
+
try {
|
|
333
|
+
unlinkSync(tmpPng);
|
|
334
|
+
} catch {}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Count nodes and edges (rough estimate)
|
|
338
|
+
const nodeCount =
|
|
339
|
+
(dot.match(/\w+\s*\[/g) || []).length + (dot.match(/^\s*\w+\s*;/gm) || []).length;
|
|
340
|
+
const edgeCount = (dot.match(/->/g) || []).length + (dot.match(/--/g) || []).length;
|
|
341
|
+
|
|
342
|
+
const formatInfo = isSvgOutput ? ` as ${outputFormat.toUpperCase()}` : '';
|
|
343
|
+
const textMsg = savedPath
|
|
344
|
+
? `Rendered Graphviz chart (${engine} engine, ~${nodeCount} nodes, ~${edgeCount} edges) - saved${formatInfo} to ${savedPath}`
|
|
345
|
+
: `Rendered Graphviz chart (${engine} engine, ~${nodeCount} nodes, ~${edgeCount} edges)`;
|
|
346
|
+
|
|
347
|
+
const content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> =
|
|
348
|
+
[];
|
|
349
|
+
|
|
350
|
+
// Add image for terminal display if available
|
|
351
|
+
if (base64Data) {
|
|
352
|
+
content.push({ type: 'image', data: base64Data, mimeType: 'image/png' });
|
|
353
|
+
}
|
|
354
|
+
content.push({ type: 'text', text: textMsg });
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
content,
|
|
358
|
+
details: { engine, nodeCount, edgeCount, savedPath, format: outputFormat },
|
|
359
|
+
};
|
|
360
|
+
} catch (error: any) {
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: 'text', text: `Error rendering chart: ${error.message}` }],
|
|
363
|
+
details: { error: error.message },
|
|
364
|
+
isError: true,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@walterra/pi-graphviz",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Graphviz chart extension for pi coding agent - render DOT diagrams as inline images",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"extensions",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"pi": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./extensions/graphviz-chart/index.ts"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/walterra/agent-tools.git",
|
|
18
|
+
"directory": "packages/pi-graphviz"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"pi",
|
|
22
|
+
"pi-coding-agent",
|
|
23
|
+
"extension",
|
|
24
|
+
"graphviz",
|
|
25
|
+
"dot",
|
|
26
|
+
"diagrams",
|
|
27
|
+
"flowcharts",
|
|
28
|
+
"graphs"
|
|
29
|
+
],
|
|
30
|
+
"author": "walterra",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@sinclair/typebox": "^0.32.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|