Absurdist Arcana Ben Weintraub's blog

Prettier Graphviz Diagrams

Graphviz 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:

client Client lb Load balancer client->lb backend1 Backend lb->backend1 backend2 Backend lb->backend2 backend3 Backend lb->backend3 db DB backend1->db cache Cache backend1->cache backend2->db backend2->cache backend3->db backend3->cache

Compare that to the same graph rendered using a newer tool, Mermaid.js:

Command Not Found: mmdc

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:

client Client lb Load balancer client->lb backend1 Backend lb->backend1 backend2 Backend lb->backend2 backend3 Backend lb->backend3 db DB backend1->db cache Cache backend1->cache backend2->db backend2->cache backend3->db backend3->cache

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].

client Client lb Load balancer client->lb backend1 Backend lb->backend1 backend2 Backend lb->backend2 backend3 Backend lb->backend3 db DB backend1->db cache Cache backend1->cache backend2->db backend2->cache backend3->db backend3->cache

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]:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w

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>]:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w

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:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w

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:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w

Dealing with backwards edges

Sometimes adding an edge that points ‘backwards’ in your graph will really mess up the layout:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w streamer Streamer cache:e->streamer:w streamer:e->client:w

There are several ways to fix this, but I usually start by giving the backwards edge weight=0 and assigning edge ports:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w streamer Streamer cache:e->streamer:w streamer:n->client:n

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:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w streamer Streamer cache:e->streamer:w streamer:n->client:n

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:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w backend1:s->backend2:n db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:s->backend3:n backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w streamer Streamer cache:e->streamer:w streamer:n->client:n

If you don’t want those edges to render, you can then hide them by setting style=invis on them:

client Client lb Load balancer client:e->lb:w backend1 Backend lb:e->backend1:w backend2 Backend lb:e->backend2:w backend3 Backend lb:e->backend3:w db DB backend1:e->db:w cache Cache backend1:e->cache:w backend2:e->db:w backend2:e->cache:w backend3:e->db:w backend3:e->cache:w streamer Streamer cache:e->streamer:w streamer:n->client:n

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!