1. Technology
You can opt-out at any time. Please refer to our privacy policy for contact information.

Making Digraphs in Ruby

By

Making Digraphs in Ruby
Making Digraphs in Ruby

Digraphs, or "directed graphs," are graphs consisting of nodes with arrows pointing between them. Think of them like flowcharts, each node represents a step in a process and each arrow between them (or "edge") represents a user following that path. Generating digraph images is handled by Graphviz, and interfacing with Graphviz is handled by the graph gem.

First, you'll need to install the graph gem. This is a pure-Ruby gem and doesn't require any compilation. It more or less just generates "dot dot" (named with the .dot extension) files, which are plain text so it's not doing all that much work. The gem itself is more of a domain specific language for generating dot dot files using Ruby code.


$ gem install graph

But there's an external dependency, Graphviz itself. Why did we install the graph gem first? The graph gem comes with various "port" files for installing Graphviz using Homebrew, Macports, FreeBSD ports, etc. You may need these for this next step.

Now install Graphviz itself. I'm using OS X Lion and Homebrew, so the command I use for this is as listed below. Refer to your system documentation if you need help installing Graphviz.


$ brew install graphviz

Generating Graphs

Now to the issue at hand: generating graphs. You'd think that you'll be defining a whole lot of nodes, what they look like, where they are on the image and the connections between them, but you'd be wrong. Graphviz and graph make it even simpler that that. All you really need to do is define the connections between the nodes, that's it. Everything else is handled behind the scenes. So let's just make a graph and see what we get.


#!/usr/bin/env ruby
require 'graph'

digraph do
  edge "C/C++", "Java"
  edge "Java", "Groovy"
  edge "Java", "Clojure"
  edge "Java", "JRuby"

  edge "C/C++", "Perl"
  edge "Perl", "PHP"
  edge "Perl", "Ruby"
  edge "Ruby", "Rubinius"
  edge "Ruby", "MacRuby"
  edge "Ruby", "JRuby"

  save 'languages', 'png'
end

In this example we made a family tree of programming languages (and Ruby interpreters). It's far from complete but tries to include Ruby and its interpreters. The first thing to notice is require 'graph', nothing surprising there. Next is the digraph method. It doesn't need any parameters except a block, and inside this block you have access to other methods like the edge and save methods used here.

The edge method is the most important method here, it defines a path from one node to another. If you'll notice, you don't actually define the nodes anywhere. You don't need to, it just infers that the nodes exist when you create edges between them. There is a way to refer to nodes individually though, and you'll see that below. For now, just make the edges. In this example, we're saying that C and C++ lead to the development of Java, which then led to Clojure, Groovy and JRuby. While it's difficult to see the relationships here, it will be clear once you see the image it generates.

Call the save method when you're done with your edge nodes . This method takes a file basename, in this case languages and a file type, here I used png. After your script has run open up the new languages.png file and take a look. That's quite a good looking graph for almost no work, no?

Adding Color

Among several things you can do to organize and accentuate graphs is to add color. In this graph, we're really only interested in Ruby. So let's color the Ruby-related nodes red. To do this, we're going to add another small section after our edge calls.

First off, we add a label using the label method. This label doesn't define a node, it's just a title for the graph that will appear at the bottom of the image.

We're going to color all of the nodes light blue, and the Ruby nodes red (what other color could there be for Ruby?). We're also going to make the nodes filled instead of outlined. This is done by "pushing" attributes onto node_attribs, the default node attributes. Whenever a new node is created when the dot dot file is generated, it will copy from these attributes. The two attributes we want to push are lightblue and filled so unless we specify attributes for nodes individually, they'll be light blue filled nodes, a more attractive default than black and white outlines.

Behind the scenes how this works is rather simple. Calling the node_attribs method returns @node_attribs from the Graph instance. Normally this is an empty array, and we're just appending new attributes to it. The attribute names themselves (things like filled and lightblue) are methods that return Attribute objects. These Attribute objects exist so they can be converted to strings (with to_s) and be put directly into the dot dot output. They're also defined programatically from arrays defined the source, conveniently located at the top of the graph.rb file. If you ever need a list of colors, shapes or arrow styles, see the constants LIGHT_COLORS, COLORS, SHAPES, etc.

On to Ruby's nodes. We're going to use a shade of red called tomato, a light red that goes with the light blue. In this case we're going to do just the opposite. We're going to push the nodes (found by using the node method with the text of the node) onto the color attribute. Though pushing a node onto an attribute rather than pushing an attribute onto a list of attributes, it does the same thing. The method Attribute#<< reads as the following:


def << thing 
  thing.attributes << self 
  self 
end 

In other words, doing tomato << node("Ruby") and node("Ruby").attributes << tomato does exactly the same thing, one just requires less typing.

And now that you've made the Ruby-related nodes red, you have a much more attractive graph. Digraphs can get out of hand quickly, coloring your graphs effectively is the difference between seeing what you need right away and staring at a plate of spaghetti and meatballs.


#!/usr/bin/env ruby
require 'graph'

digraph do
  edge "C/C++", "Java"
  edge "Java", "Groovy"
  edge "Java", "Clojure"
  edge "Java", "JRuby"

  edge "C/C++", "Perl"
  edge "Perl", "PHP"
  edge "Perl", "Ruby"
  edge "Ruby", "Rubinius"
  edge "Ruby", "MacRuby"
  edge "Ruby", "JRuby"

  node_attribs << lightblue << filled
  %w{ Ruby JRuby MacRuby Rubinius }.each do|ruby|
    tomato << node(ruby)
  end

  save 'languages', 'png'
end

Edge Attributes

Much like the default node attribute array node_attribs, there's also a default edge attribute array edge_attribs. To this, you can push color attributes and, more importantly, arrow types. Arrow types are important, especially if there is more than one type of relationship in the graph.

We'll be using the vee arrow type, which is a more stylized arrow type. There are also edges with no arrows, boxes, diamonds, etc. You can see a list of the arrow types in the ARROWS constant. We'll also be coloring the arrows with gray (not grey, it must be spelled with an a). So add the following line near your node_attribs line.


edge_attribs << vee << gray

Node Shapes

By default, the nodes are an oval shape. If this isn't what you want, you can change it by defining a shape to the node_attribs array. There are a plethora of shapes available in the SHAPES constant array and here is a visual catalog of them for you to pick and choose. We'll make our nodes egg shaped, so our node_attribs line looks like this:


node_attribs << lightblue << filled << egg

Alternatively, because the box shape is such a common option a boxes method is also provided. All it does is node_attribs << shape("box"), which would be the same as node_attribs << box, it's just easier to type and read.

Clusters

If you want to highlight a certain group of nodes, you can put them in a cluster. Clusters are portions of the graph with a box drawn around them. To make them, call the method cluster from within a graph definition and pass it a block. This block technically makes a subgraph, which inherits all the default node and edge attributes from its parent graph. Be aware that it even inherits the graph label.

Let's put all the Java-related languages in a cluster. So all we do is call cluster, start a block, change the label and move all the Java-related stuff in there.


cluster("Java") do
  label "Java"

  edge "Java", "Groovy"
  edge "Java", "Clojure"
  edge "Java", "JRuby"
end

Prettier Output

The graphs are looking pretty good now, but not great. Look at the graphs closely and notice the lines and curves. They're horribly aliased and just don't look good. They're functional but we can make them prettier. Now, instead of outputting a PNG directly we're going to output SVG. SVG stands for Scalable Vector Graphics and is an XML format for defining vector graphics. A number of high-quality renderers exist for it and modern web browsers support SVG natively.

The only real change is the change to the save line. Instead of png, svg is used. You can either keep the SVG as it is, as many web browsers support SVG natively, or render the SVG to an image. I'm rendering the SVG to PNGs (that's what we started with, right?) with a tool called svg2png that I got with the OS X Brew command brew install svg2png. It uses Cairo, is fast and produces high quality output. All the lines and circles look much smoother, and the image is overall just better to look at.


save 'languages', 'svg' 
`svg2png languages.svg languages.png` 

Learning More

There isn't much documentation for the graph gem out there. The official documentation is pretty thin itself, and there isn't much example code out there. However, most of what you need can be done with what I just described. And there are a few examples that came with graph that can be found by changing directory to the graph gem directory after installing it.


$ cd $GEM_HOME/gems/graph-*

In this directory you'll find the lib/graph.rb file, this is the source code for the gem itself. With a bit of digging you can grok how this all works. The main classes at play here are Graph, Node, Edge and Attribute. You'll also find a number of constant arrays defining the colors, edge types, etc. The file is about 500 lines of code, but it's not difficult to understand.

In addition to the graph.rb source code, there's also a number of examples in the gallery/ directory. Each of these generate a graph and reading through them can show you a few tricks.

  1. About.com
  2. Technology
  3. Ruby
  4. Tutorials
  5. Making Digraphs in Ruby

©2014 About.com. All rights reserved.