Prettier Graphviz Diagrams
10 Nov 2022Graphviz is an incredible tool. It allows you to visualize complex graphs by writing a simple declarative domain-specific language that’s equally easy to write by hand or generate programatically. Unfortunately, Graphviz’s defaults leave something to be desired. Without any attention to styling, the an example graph might look something like this:
Compare that to the same graph rendered using a newer tool, Mermaid.js:
To my eyes, Mermaid makes more visually pleasing results without any need to tweak the defaults. I use it for most simple diagrams that I need to make, but sometimes I really do need some of the additional flexibility that Graphviz provides. When I want to use Graphviz and have my results look not-terrible, here are the most important tips that I use.
Layout direction
I find that left-to-right graphs usually look better than top-down graphs. To achieve that, just set rankdir=LR
attribute on your graph:
Node shape
Graphviz’s default node shape (oval
) is ugly. There are lots of alternatives. box
is a sensible default. You can set it as the default for all nodes in your graph with node [shape=box]
.
Ports
By default, Graphviz draw edges by connecting the centers of each node pair and then clipping to the node boundary. I find graphs often easier to read if I force edges to originate from and arrive at specific ports using the headport
and tailport
edge attributes. For a left-to-right graph, you can set reasonable defaults for all edges in your graph by adding edge [headport=w, tailport=e]
:
Note that there’s a shorthand for setting these attributes too. Instead of saying:
a -> b [headport=w, tailport=e]
You can equivalently say:
a:e -> b:w
Fonts
Everyone’s got a favorite. For technical diagrams, sans serif fonts look better to me. You can set a default font for node labels using the fontname node attribute. Set the default for all nodes via node [fontname=<your-favorite-font>]
:
The fontname
attribute can be applied to edges (for edge label text) and graphs (for subgraph labels) as well.
Colors
Graphviz recognizes lots of built-in color names. If you just want colors that look OK together, use one of the Brewer color schemes. You can set the style=filled
and colorscheme
default node attributes, and then assign individual nodes colors with color=N
, where N
is just a numeric index into the color scheme of your choice:
Ranksep
The default layout can get a little squishy, especially with high edge densities. Set ranksep=0.8
(or higher, to taste) to push nodes of different ranks a bit further apart:
Dealing with backwards edges
Sometimes adding an edge that points ‘backwards’ in your graph will really mess up the layout:
There are several ways to fix this, but I usually start by giving the backwards edge weight=0
and assigning edge ports:
Grouping
Sometimes it’s nice to emphasize one particular flow within your graph. One way to do that is by ensuring that all of the nodes within that flow are co-linear with each other. The group
node attribute tells Graphviz you want that, although sometimes it’ll come up with, um … creative ways of satisfying your request:
One way to convince Graphviz into laying things out in the way you wanted is to include ‘extra’ edges in your graph, and force the nodes connected by those edges to have the same rank by putting them into a subgraph with rank=same
:
If you don’t want those edges to render, you can then hide them by setting style=invis
on them:
Graphviz sources for these examples
Here’s the Graphviz code used to produce the final diagram in this sequence, which demonstrates all of these techniques combined together:
digraph {
rankdir=LR
node [shape=box]
edge [headport=w, tailport=e]
node [fontname="Courier New"]
node [style=filled colorscheme=dark26]
ranksep=0.8
client [label="Client", color=1, group=main]
lb [label="Load balancer", color=2, group=main]
backend1 [label="Backend", color=3]
backend2 [label="Backend", color=3, group=main]
backend3 [label="Backend", color=3]
db [label="DB", color=4]
cache [label="Cache", color=5, group=main]
streamer [label="Streamer", color=6, group=main]
subgraph f {
rank=same
edge [style=invis, headport=s, tailport=n]
backend1 -> backend2 -> backend3
}
client -> lb
lb -> {backend1, backend2, backend3}
{backend1, backend2, backend3} -> db
{backend1, backend2, backend3} -> cache
cache -> streamer [weight=1]
streamer:n -> client:n [weight=0]
}
Acknowledgements
Thanks to @johanneshoff
for pointing out a bug in one of the examples in this post!