GraphvizRでオブジェクトをグラフ化してみる

jitte2007-11-24


びっずびずにしてやんよ!

とりあえずArrayとHash、それからインスタンス変数のあるクラスに対応。あとはinspectでそれなりに。中身が長いと横に伸びすぎて大変なことに(アンパンマン風)なるのは気にしない方向でひとつ。

vizviz.rb

require 'rubygems'
require 'graphviz_r'
 
class Object
  def vis_node(g)
    if vis_elements.empty?
      g[vis_id, [:label => inspect]]
    else
      vis_node_instance(g)
    end
  end

  def vis_elements
    instance_variables
  end

  def vis_element(e)
    instance_eval(e)
  end
  
  def vis_each
    vis_elements.sort.each do |e|
      yield e
    end
  end

  def vis_node_instance(g)
    g[vis_id, [:shape => :record, :label => vis_label]]
    vis_each do |e|
      vis_element(e).vis_node(g)
    end
  end

  def vis_label
    a = [self.class.inspect]
    vis_each do |e|
      a << "<#{vis_port(e)}>#{e}"
    end
    a.join '|'
  end
  
  def vis_edge(g)
    unless vis_elements.empty?
      vis_each do |e|
        v = vis_element(e)
        g[vis_id, vis_port(e)] >> g[v.vis_id]
        v.vis_edge(g)
      end
    end
  end
  
  def vis_id
    "id#{__id__}".to_sym
  end

  def vis_port(e)
    vis_element(e).vis_id
  end
end

class Hash
  def vis_elements
    keys
  end

  def vis_element(e)
    self[e]
  end
end

class Array
  def vis_elements
    self
  end

  def vis_element(e)
    self[e]
  end

  def vis_each
    (0...size).each do |e|
      yield e
    end
  end
end

def vv(obj)
  gvr = GraphvizR.new 'vv'
  gvr.graph[:rankdir => 'LR']
  obj.vis_node(gvr)
  obj.vis_edge(gvr)
  puts gvr.to_dot
  gvr.output
end


if __FILE__ == $0
  vv(:a => %w(x y z),
     :b => GraphvizR.new('sample'),
     :c => nil
    )
end

それにしても一番手間取ったのは、エッジを引くときの表記に妙な制約があったこと。

gvr = GraphvizR.new 'vv'
gvr.node1 >> gvr.node2
gvr.node1 >> gvr.node3
gvr.node1 >> gvr.node4

これはOKだけど、

gvr = GraphvizR.new 'vv'
node = gvr.node1
node >> gvr.node2
node >> gvr.node3
node >> gvr.node4

こっちはNG。なぜかというと・・・

class GraphvizR
  class Edge
    def initialize(from, to, parent, arrow='->')
      @attributes = {}
      @nodes = [from, to]
      @arrow = arrow
      @parent = parent
      (from.is_a?(Array) ? from : [from]).size.times do
        @parent.statements.pop # ←これ
      end
      (to.is_a?(Array) ? to : [to]).size.times do
        @parent.statements.pop # ←これ
      end
      @parent.statements << self
    end
end

statementsというのがGraphvizステートメントをためておくバッファで、これをpopしているから、直前のノード定義を削除するようになっているようだ。つまり、

  • まず、両端ノードを生成する。
  • その後、エッジを定義する。
  • このとき、ノードは再生成する。

という前提を守る必要があるらしい。あるいは、どうやらサブグラフを使えばよかったかもしれない(別のGraphvizRインスタンスが生成される)が、もうコード書いちゃったからいいや。