Creating Graphviz graphs from Ruby arrays 2009-01-31


As part of my compiler project I wanted a way to visualize the programs, and since the syntax tree (so far at least) is represented with plain Ruby arrays I decided to throw together a script to use Graphviz to generate some graphs. I've written about using Graphviz previously here

The code does not make any assumptions tied to my compiler, but it's NOT an attempt at visualizing arbitrary object structures. You need to pass it an Array object, which can contain other arrays or objects that responds to #to_s. NOTE: The code makes NO attempt to deal with structures that have loops - you'll run out of stack space soon enough if you try that. Feel free to post your fixes to do that in the comments (easy enough - just need to keep track of visited objects).

An example example. Given this:


        [:defun, :parse_quoted, [:c],
          [:while, [:and, [:ne, [:assign, :c, [:getchar]], -1], [:ne, :c, 34]], [:do,
            [:putchar, :c]
           ]
          ]
        ]

I generate this image (the gradients and shadows are thanks to my previously described XSL transform to pretty up the Graphviz SVG output):

The code is quite straightforward, though I'm not quite happy with the amount of monkey-patching. There were two easy choices: monkey-patching or lots of #is_a? calls, which made it horribly messy. I wouldn't advocate including this into a larger app without cleaning it up first, but as a quick hack it works well.


    module ToDot
     def self.escape str
      str.gsub(/([<>{} |\])/) { "\""+$1 }
     end
    end
    
    class String
     def to_dot_label; '\"'+ToDot::escape(self)+'\"'; end
    end
    
    class Array
     def to_dot_label; "..."; end
    
     def to_dot_edge src, shorten
      " #{src}" + (shorten ? "" : ":#{object_id}") + " -> #{object_id};\n"
     end
    
     def to_dot_subgraph
      return "" if nil
      ary = self[0].is_a?(Array)
      shorten = !ary && self[1..-1].detect{|o| !o.is_a?(Array)} == nil
      s = " #{object_id} [label=\""
      if shorten
       s += self[0].to_dot_label + "\", shape=rect];\n"
      else
       s += collect { |o| "<#{o.object_id}> " + o.to_dot_label }.join("|")
       s += "\"];\n"
      end
      s += collect {|o| o.to_dot_edge(object_id,shorten) }.join
      s += collect {|o| o.to_dot_subgraph }.join
      s
     end
    end
    
    
    class Object
     def to_dot_subgraph; end
     def to_dot_edge src, shorten; end
     def to_dot_label; ToDot::escape(to_s); end
    
     def to_dot
      s = "digraph G {\n"
      s += " node [shape=record style=filled fillcolor=lightblue "
      s += "fontname=Verdana height=0.05 fontsize=10.0 ];\n"
      s += to_a.to_dot_subgraph
      s += "}\n"
     end
    end
    

As for how to use it:


    require 'arytodot'
    
    puts someArray.to_dot

Then pipe the output to "dot -Tsvg >output file" and use your favorite XSL processor to render an image from it. I used "rsvg file.svg file.png". If you want to use my XSL transform to pretty it up, follow the instructions in the article linked to above.

Here's the full "parser" example from my compiler series (click for the full size version):


blog comments powered by Disqus