(追記:2014-3-3) Gvizについてのまとめ頁を作りました。

Gvizの目次 - Rubyの世界からGraphvizの世界にこんにちは!


このブログとかでたまに無向だとか有向だとかのチャートの方じゃないグラフが書きたいと思うことがあるよ。でまさかこのご時世で今更VISIOとかあり得ないからGraphvizのdotファイルを書くことになるんだけどDOT言語は制御構造を持ってないから結局より高級な言語によるインタフェースが必要になるよ。でRubyの出番ってことになるんだけどGithubで”graphviz ruby“で検索すると1200件以上ものリポジトリがヒットするんだよ。でこの中から適当なものを選んで使えばいいってことなんだろうけどさすがにこれだけあるとどれを選んでいいか全然わからないから結局The Ruby Toolbox当たりで”graphviz“にヒットする20件くらいの中からDownload数の多いものを1つ2つ試してみるって結末になるよ。でそれらのインタフェースが今ひとつシックリ来ない要はRubyっぽくないとかおまえ内部DSLやり過ぎ!ってのに当たるともう面倒臭いからオレが適当にでっち上げたほうがいいかもって結論に至るんだけど。で一般に車輪の再発明は悪ってことになってるんだけどどうやらRuby界隈では奨励されているみたいな空気があるんだよね最近1

そんなわけで…

GvizというRubyのGraphvizインタフェースを作りましたので、ここで紹介させて下さい^ ^;

使い方

まずはgem install gvizとしまして、次のようなコードを書きます。

require "gviz"

gv = Gviz.new
gv.graph do
  route :main => [:init, :parse, :cleanup, :printf]
  route :init => :make, :parse => :execute
  route :execute => [:make, :compare, :printf]
end

gv.save(:sample1, :png)

まずはGviz.newでgvizオブジェクトを生成して、graphのブロックの中でrouteメソッドでルート、つまりノードとエッジの繋がりをハッシュ形式で記述します。シンボルだけを渡すと単独のノードを生成します。saveメソッドは与えられたファイル名でdotファイルを出力します。更に上のようにフォーマットを指定すると、dotファイルに加えてその形式のファイルも出力します。saveせずにputs gvとすれば標準出力にdotのコードが出力されるので、ruby sample.rb > sample1.dotなどとしてもいいです。

出力グラフは次のようになります。

G main main init init main->init parse parse main->parse cleanup cleanup main->cleanup printf printf main->printf make make init->make execute execute parse->execute execute->printf execute->make compare compare execute->compare

対応出力フォーマットは以下を見て下さい。

Output Formats | Graphviz - Graph Visualization Software

この頁のソースを見れば分かりますが、上のグラフはSVG(Scalable Vector Graphics)で出力したものを張り付けています2

グラフ、ノード、エッジの属性変更

次に先のグラフに対して色などの属性を変更してみます。

require "gviz"

gv = Gviz.new
gv.graph do
  route :main => [:init, :parse, :cleanup, :printf]
  route :init => :make, :parse => :execute
  route :execute => [:make, :compare, :printf]

  #        ---- 追加 ----
  nodes(colorscheme:'piyg8', style:'filled')
  nodeset.each.with_index(1) { |nd, i| node(nd.id, fillcolor:i) }
  edges(arrowhead:'onormal', style:'bold', color:'magenta4')
  edge(:main_printf, arrowtail:'diamond', dir:'both', color:'#3355FF')
  global(bgcolor:'powderblue')
end

gv.save(:sample2, :png)

nodesメソッドは全ノードに対する属性をセットします。ここではpiyg8というカラーセットを指定しています。nodesetは全ノードのオブジェクトを呼び出します。nodeメソッドは個別ノードの属性をセットします。ここではfillcolorでpiyg8カラーセットの色番号1〜8を各ノードに順にセットしています。

edgesメソッドは全エッジに対する属性をセットします。ここではarrowhead, style, colorの属性を変更しています。edgeメソッドは個別エッジの属性をセットします。対象エッジの指定はその両端ノードのidを_で連結したもので行います。globalはグラフ全体の属性をセットします。ここでは背景色を変えています。

出力は次のようになります。

sample2 noshadow

色見本は以下にあります。

Color Names | Graphviz - Graph Visualization Software

グラフ、ノードおよびエッジの変更可能な属性については、以下を見て下さい。

attrs | Graphviz - Graph Visualization Software

またエッジの矢印の種類については以下を。

Arrow Shapes | Graphviz - Graph Visualization Software

エッジポート、ノード配置、サブグラフ

更に次の変更を加えます。

  1. ノードに対するエッジの接続先(ポート)を変更
  2. ノードの配置を変更
  3. 一部のノードをサブグラフ化
require "gviz"

gv = Gviz.new
gv.graph do
  route :main => [:init, :parse, :cleanup, :printf]
  route :init => :make, :parse => :execute
  route :execute => [:make, :compare, :printf]

  nodes colorscheme:'piyg8', style:'filled'
  nodeset.each.with_index(1) { |nd, i| node nd.id, fillcolor:i }
  edges arrowhead:'onormal', style:'bold', color:'magenta4'
  edge :main_printf, arrowtail:'diamond', dir:'both', color:'#3355FF'
  global bgcolor:'powderblue'

  #        ---- 追加 ----
  node :execute, shape:'Mrecord', label:'{<x>execute | {a | b | c}}'
  node :printf, shape:'Mrecord', label:'{printf |<y> format}'
  edge 'execute:x_printf:y'
  rank :same, :cleanup, :execute
  subgraph do
    global label:'SUB'
    node :init
    node :make
  end
end

gv.save(:sample3, :png)

nodeメソッドで:executeと:printfのshapeをMrecordにし、label属性をセットするときポート情報を埋め込みます(<x>, <y>)。edgeメソッドでエッジのidを指定するとき文字列を使い、各ノードidの後ろに:を挟んでポートidを付けます。

rankメソッドではそのランクの種類(ここでは:same)を指定し、対象ノードをリストアップします。サブグラフを作るときはsubgraphメソッドのブロック内で指定します。

出力は次のようになります。

sample3 noshadow

ノードの形については次を参考にして下さい。

Node Shapes | Graphviz - Graph Visualization Software

日本地図を書く

別の例をやってみます。Gvizで日本地図に挑戦してみます。まずは都道府県 - Wikipediaを参考に、次のようなCSVファイル(pref.csv)を用意します。データは都道府県コード、地方区分、名称、隣接県のコードの順に並んでいます。

このファイルをRubyに読み込んでGvizで都道府県地図を作ります。まずはCSVを読み込んで処理しやすいように加工します。

# encoding: UTF-8
require "gviz"
require "csv"

header, *data = CSV.read('pref.csv')
data = data.map { |id, region, name, *link| [id.intern, region.to_i, name, link.map(&:intern)] }

data # => [[:"01", 1, "北海道", [:"02"]], [:"02", 2, "青森県", [:"01", :"03", :"05"]], [:"03", 2, "岩手県", [:"02", :"05", :"04"]], [:"04", 2, "宮城県", [:"03", :"05", :"06", :"07"]], [:"05", 2, "秋田県", [:"02", :"03", :"04", :"06"]], [:"06", 2, "山形県", [:"05", :"04", :"07", :"15"]], [:"07", 2, "福島県", [:"04", :"06", :"15", :"10", :"09", :"08"]], [:"08", 3, "茨城県", [:"07", :"09", :"11", :"12"]], [:"09", 3, "栃木県", [:"07", :"10", :"08", :"11"]], [:"10", 3, "群馬県", [:"07", :"09", :"11", :"20", :"15"]], [:"11", 3, "埼玉県", [:"10", :"09", :"08", :"12", :"13", :"19", :"20"]], [:"12", 3, "千葉県", [:"13", :"11", :"08"]], [:"13", 3, "東京都", [:"11", :"12", :"14", :"19"]], [:"14", 3, "神奈川県", [:"13", :"22", :"19"]], [:"15", 4, "新潟県", [:"06", :"07", :"10", :"20", :"16"]], [:"16", 4, "富山県", [:"15", :"20", :"21", :"17"]], [:"17", 4, "石川県", [:"16", :"21", :"18"]], [:"18", 4, "福井県", [:"17", :"21", :"25", :"26"]], [:"19", 4, "山梨県", [:"20", :"11", :"13", :"14", :"22"]], [:"20", 4, "長野県", [:"15", :"10", :"11", :"19", :"22", :"23", :"21", :"16"]], [:"21", 4, "岐阜県", [:"16", :"20", :"23", :"24", :"25", :"18", :"17"]], [:"22", 4, "静岡県", [:"23", :"20", :"19", :"14"]], [:"23", 4, "愛知県", [:"24", :"21", :"20", :"22"]], [:"24", 5, "三重県", [:"23", :"21", :"25", :"26", :"29", :"30"]], [:"25", 5, "滋賀県", [:"18", :"21", :"24", :"26"]], [:"26", 5, "京都府", [:"18", :"25", :"24", :"29", :"27", :"28"]], [:"27", 5, "大阪府", [:"26", :"29", :"30", :"28"]], [:"28", 5, "兵庫県", [:"26", :"27", :"33", :"31"]], [:"29", 5, "奈良県", [:"26", :"24", :"30", :"27"]], [:"30", 5, "和歌山県", [:"27", :"29", :"24"]], [:"31", 6, "鳥取県", [:"28", :"33", :"34", :"32"]], [:"32", 6, "島根県", [:"31", :"33", :"34", :"35"]], [:"33", 6, "岡山県", [:"28", :"31", :"32", :"34", :"37"]], [:"34", 6, "広島県", [:"33", :"31", :"32", :"35"]], [:"35", 6, "山口県", [:"32", :"34", :"40"]], [:"36", 7, "徳島県", [:"37", :"38", :"39"]], [:"37", 7, "香川県", [:"36", :"38", :"33"]], [:"38", 7, "愛媛県", [:"37", :"36", :"39"]], [:"39", 7, "高知県", [:"38", :"36"]], [:"40", 8, "福岡県", [:"35", :"44", :"43", :"41"]], [:"41", 8, "佐賀県", [:"40", :"42"]], [:"42", 8, "長崎県", [:"41"]], [:"43", 8, "熊本県", [:"40", :"44", :"45", :"46"]], [:"44", 8, "大分県", [:"40", :"43", :"45"]], [:"45", 8, "宮崎県", [:"44", :"43", :"46"]], [:"46", 8, "鹿児島県", [:"43", :"45", :"47"]], [:"47", 9, "沖縄県", [:"46"]]]

次にGVizでこれらを処理します。各都道府県をノードとし隣接県情報を使ってノードをつなぎます。

# encoding: UTF-8
require "gviz"
require "csv"

header, *data = CSV.read('pref.csv')
data = data.map { |id, region, name, *link| [id.intern, region.to_i, name, link.map(&:intern)] }

gv = Gviz.new(:Pref)

gv.graph do
  data.each do |id, reg, name, link|
    route id => link
    node id, label: name
  end
end

gv.save(:pref, :png)

結果を見てみましょう。

pref1 noshadow

ややっ、これはヒドイ。

graphのレイアウトをneatoに変えてみます。

gv = Gviz.new(:Pref)
gv.graph do
  global layout:'neato'
  data.each do |id, reg, name, link|
    route id => link
    node id, label: name
  end
end

gv.save(:pref, :png)

さあどうでしょうか。

pref2 noshadow

南北が逆で見慣れないですが大分良くなりました。

ノードの重なりが気になるので重なり(overlap)を無くします。

gv = Gviz.new(:Pref)
gv.graph do
  global layout:'neato', overlap:false
  data.each do |id, reg, name, link|
    route id => link
    node id, label: name
  end
end

gv.save(:pref, :png)

pref3 noshadow

何となく日本地図に見えてきましたか?

形はこれで良しとして(マジか)、色を付けて仕上げます:)

gv = Gviz.new(:Pref)
gv.graph do
  global layout:'neato', overlap:false, label:'日本地図'
  nodes colorscheme:'set310'
  edges arrowhead:'none'
  data.each do |id, reg, name, link|
    route id => link
    node id, label: name, style:'filled', fillcolor:reg
  end
end

gv.save(:pref, :png)

日本地図の完成です!

pref4 noshadow

いいですね!

というわけで…

あなたもGvizで何かグラフを作ってみませんか?


Github Repo: melborne/Gviz


グラフ理論入門 by R.J. ウィルソン


  1. コメント行があるとうまくいかなかったのでコメント行を削除しています。
  2. rubysapporo: Keynote Yukihiro "Matz" Matsumoto "One size does not fit all" http://www.ustream.tv/recorded/25417206


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS