The final1 piece of the Graphvix spec to look at is the ability to rank nodes and subgraphs in order to enforce vertical or horizontal hierarchies in a graph. The functionality to make this happen is actually already in Graphvix, but since it can be such an important piece of structuring a graph, I wanted to call a bit of extra attention to it.

#### This post is part of a series:

As an example, let’s draw a very simple family tree, with years of birth, of my immediate family. To do this, we need to make sure a timeline of years is ordered correctly along the left side, and that each family member is aligned with their birth year. Let’s begin by creating the vertices:

g = Graph.new
{g, michael} = Graph.add_vertex(g, "Michael") # myself
{g, lauren} = Graph.add_vertex(g, "Lauren") # wife
{g, rachel} = Graph.add_vertex(g, "Rachel") # sister
{g, corey} = Graph.add_vertex(g, "Corey") # brother-in-law
{g, howard} = Graph.add_vertex(g, "Howard") # father
{g, jessi} = Graph.add_vertex(g, "Jessi") # mother
{g, h_and_j} = Graph.add_vertex(g, "Howard + Jessi")
{g, l_and_m} = Graph.add_vertex(g, "Lauren + Michael")
{g, r_and_c} = Graph.add_vertex(g, "Rachel + Corey")

Then we create the edges necessary to order the timeline

{g, _} = Graph.add_edge(g, y53, y56)
{g, _} = Graph.add_edge(g, y56, y78)
{g, _} = Graph.add_edge(g, y78, y79)
{g, _} = Graph.add_edge(g, y79, y85)
{g, _} = Graph.add_edge(g, y85, y89)
{g, _} = Graph.add_edge(g, y89, y2015)
{g, _} = Graph.add_edge(g, y2015, y2018)

Now we create a set of subgraphs, each containing a year vertex and the vertex or vertices of births or marriages that occurred in that year. We give each subgraph a property of rank=same to print all the vertices in the subgraph at the same rank:

{g, _} = Graph.add_subgraph(g, [y53, howard], rank: "same")
{g, _} = Graph.add_subgraph(g, [y56, jessi], rank: "same")
{g, _} = Graph.add_subgraph(g, [y78, lauren], rank: "same")
{g, _} = Graph.add_subgraph(g, [y79, h_and_j], rank: "same")
{g, _} = Graph.add_subgraph(g, [y85, michael], rank: "same")
{g, _} = Graph.add_subgraph(g, [y89, rachel, corey], rank: "same")
{g, _} = Graph.add_subgraph(g, [y2015, r_and_c], rank: "same")
{g, _} = Graph.add_subgraph(g, [y2018, l_and_m], rank: "same")

And, finally, we add edges to show relationships. Blue represents a partner in a marriage, and black represents a parent -> child relationship.

{g, _} = Graph.add_edge(g, howard, h_and_j, color: "blue")
{g, _} = Graph.add_edge(g, jessi, h_and_j, color: "blue")
{g, _} = Graph.add_edge(g, h_and_j, michael)
{g, _} = Graph.add_edge(g, h_and_j, rachel)
{g, _} = Graph.add_edge(g, rachel, r_and_c, color: "blue")
{g, _} = Graph.add_edge(g, corey, r_and_c, color: "blue")
{g, _} = Graph.add_edge(g, lauren, l_and_m, color: "blue")
{g, _} = Graph.add_edge(g, michael, l_and_m, color: "blue")

Let’s take a look at the resulting graph:

Not the prettiest graph in the world, but it does what we set out to have it do. For the most part, that’s all there really is to ranks in DOT. But, in addition to the rank of same, we can pass in min, max, source or sink. Using our example above, we can see what effect these ranks have.

If a subgraph’s rank is set to min, every vertex in the subgraph will be on at least as low* a rank as any other vertex in the graph.

*In this instance, low is relative to the rank direction of the graph. The default is top-to-bottom, so a lower rank is towards the top of the graph.

For example, if I wanted my own name to be at least as high on the graph as anything else, I could change the line to read

{g, _} = Graph.add_subgraph(g, [y85, michael], rank: "min”)

And the resulting graph looks like

If I wanted my name to be at a lower rank than any others, I would use source instead of min, resulting in the graph below:

max and sink, work analogously to min and source respectively, but in the opposite direction, ensuring a rank higher than (or equal to, in the case of max) any other vertex in the graph.

### rankdir

As I said above, the default direction for graph ranking is top-to-bottom. This can be set explicitly by adding a global property of rankdir=TB to the graph2

Graph.set_graph_property(g, :rankdir, "TB")

This results in a graph that looks identical to our first graph. As you could probably guess, there are four values for rankdir: TB, BT, LR and RL. These behave as you might expect. For example, here is what our family tree looks like with rankdir=LR

And, if we were to set rank=source for my name and birth year, we see, again that the subgraph is shifted to the lowest possible rank, relative to the new rankdir:

And with that, we’ve reached the end of my series on Graphvix. We started with an overly complex library using GenServers to hew too close to a traditional object oriented approach to the problem, and have ended with a library that not only has more functionality than our original, but also works much more in the style and spirit of Elixir. Thank you for joining me on this journey.

This is far from a complete survey of the DOT language syntax. There are properties for graphs, subgraphs, nodes and edges that I have not touched on in this series. However, they can be added, just as the properties we have looked at, using the API we have written. A more complete overview of the DOT language, which I used as a resource while writing Graphvix and this blog series, can be found as a PDF here.

1. Not actually the final post. There will be a postscript on HTML-style records following this.

2. This function, and the associated functions to display these properties, need to be created at this point, an oversight on my part earlier in the implementation. However, as they have much in common with other functions which already exist, there is little need to describe them in any detail. The Graphvix code containing these additions can be found tagged here