2005年05月24日

グラフが簡単に描ける―Graphvizの使い方

Graphvizはグラフを視覚化して様々なフォーマットに出力してくれるツールである。 テキストファイルにノードとエッジの関係を書いておけば、見やすいように自動でノードを配置してエッジを描いてくれる。出力形式もPostScript、SVG、PNGなど各種ある。GUIで図形を置きながら位置を揃えたりすることに気を遣うよりはずっと楽である。

本稿はそのGraphvizに与えるグラフの書き方(dot形式と呼ぶ)を簡単に説明する。 例としてconfig(8)のときに作成した図を用いる。

1. ノード間の関係を記述する

まず、グラフを構成するトポロジー、つまりどのようなノードとエッジがあるかだけを集中して書いてしまう。これだけで一応グラフは描けるので、最低限のレベルはクリアである。

グラフは、種類を示すキーワード(有向グラフ(digraph)または無向グラフ(graph))に続けて { ... } で囲んで記述する。

ノードにはラベルとして表示させたい名前をそのまま書けばよい。但しノードの名前に空白や記号などを含む場合は、"files.i386"のようにクォーテーションで囲む。

ノードとノードの間をエッジで結ぶにはNodeA -> NodeBのように書く。NodeA -> NodeB -> NodeCのようにいくつも繋げて書いてもよい。同じノードから複数のノードにそれぞれエッジを結ぶ場合、a->b; a->c; a-> d;と書く代わりにa -> { b c d }のようにひとまとめにできる。

なお、無向グラフでは->の代わりに--を使う。

digraph {
	MYKERNEL -> yyparse -> {fntab dtab opt mkopt};
	{files "files.i386" fntab} -> read_files -> ftab;
	{"Makefile.i386" mkopt ftab} -> makefile -> Makefile;
	{options "options.i386"} -> otab;
	{opt otab} -> f_options -> "opt_*.h";
	MYKERNEL -> configfile -> "config.c";
	"MYKERNEL.hints" -> makefile -> "hints.c";
	"MYKERNEL.env" -> makefile -> "env.c";
	{dtab ftab} -> headers -> "*.h";

	f_options [ label="options" ];
}

ノードのlabel属性を指定すると、ノードの名前と違う文字列を表示させることができる。複数の異なるノードに同じラベルをつけるには、ノードの名前は別々にしておいて、label属性に同じラベル文字列を指定すればよい。ノードに属性を与えるにはノードに続けて[ 属性= ]と書く。例ではf_optionsというノードにoptionsというラベルを与えている。optionsというノードは別にあるが、それとこれとは別個に表示される。

本例では使用していないが、エッジにもラベルを表示させることができる。 ノードの場合と同様に、エッジのlabel属性を指定すればよい。エッジの属性はNodeA -> NodeB [属性= ]のように記述する。

dot形式のファイルを元にして実際にグラフを描画させるにはdotコマンドを使い、オプションで出力形式や出力ファイル名を指定する。

% dot -Tpng -o config.png config.dot
drawn graph (1)

2. ノードのスタイルを指定する

既にグラフは出来た。ここからはグラフを見易くするための装飾のみである。

まずは、ノードの種類ごとに形を変えてみる。例の図においてノードが表すものにはファイル、変数、関数がある。このうちファイルを四角、変数は楕円、関数は平行四辺形にしよう。

ノードの形はshape属性で指定する。

ノード一つ一つに属性を指定してもいいが、面倒だ。ノードのデフォルト属性はnode [ 属性= ]という文で変更できる。この文はそれ以後に新しく出現したノードにのみ効果がある。そこで、デフォルト属性を変えながら種類ごとにノードをまとめて宣言するのがよい。

digraph config {
	// files
	node [shape = box]
	MYKERNEL
	"MYKERNEL.env" "MYKERNEL.hints"
	files "files.i386" "Makefile.i386" options "options.i386"
	Makefile "opt_*.h" "hints.c" "env.c" "config.c" "*.h"

	// variables
	node [shape = ellipse]
	fntab dtab opt mkopt otab ftab

	// functions
	node [shape = parallelogram]
	yyparse
	f_options [ label="options" ]
	makefile headers configfile read_files

	// 以下は既に作成したエッジの記述 (省略)
	...
}
drawn graph (2)

3. グラフの展開方向を指定し、ノードの位置を揃える

わかりやすくなったが、まだ今一つ、直観的ではない。特にyyparseの生成する4つの変数がばらばらになってしまっているのが気にくわない。そこでグラフを左から右に流れるようにし、更にノードの位置を揃えよう。特にyyparseの生成する4つの変数はどれも同じレベルに表示させたい。ついでに入力ファイル、出力ファイルの位置をそれぞれ揃える。

グラフの方向にはグラフのrankdir属性を指定する。グラフの属性は、グラフ内の適当なところに属性 = と書けばよい。

ノードの位置を揃えるには、{ ... }でノードをグループ化し、その括弧の中にrank=sameと記述する。

digraph {
	rankdir = LR;

	// files
	node [shape = box]
	{
		rank=same
		MYKERNEL "MYKERNEL.env" "MYKERNEL.hints"
		files "files.i386" "Makefile.i386" options "options.i386"
	}
	{
		rank=same
		Makefile "opt_*.h" "hints.c" "env.c" "config.c" "*.h"
	}
	
	// variables
	node [shape = ellipse]
	{
		rank=same
		fntab dtab opt mkopt
	}
	otab ftab

	// 以下略
}

これで概ね出来上がりである。説明しなかった属性のリストなど詳細はdot(3)のマニュアル等を参照されたい。

drawn graph (3)

4. 表示の工夫のためにグラフに手を加える

更に一工夫、入力ファイルをユーザが記述するものとそうでないものに分けてみた。しかし具合の悪いことに、後者のファイルがyyparseの生成する4つの変数の間に割り込んで表示されるようになってしまった。

そこでダミーの見えないエッジを使い両者のランクを強引に分けることとした。エッジはstyle=invisという属性を指定すると描画されない。エッジの属性はNodeA -> NodeB [属性= ]のように記述する。

digraph {
	...
	// files
	node [shape = box]
	{
		rank=same
		MYKERNEL "MYKERNEL.env" "MYKERNEL.hints"
	}
	{
		rank=same
		files "files.i386" "Makefile.i386" options "options.i386"
	}
	{
		rank=same
		Makefile "opt_*.h" "hints.c" "env.c" "config.c" "*.h"
	}

	...

	// functions
	node [shape = parallelogram]
	yyparse read_files
	{
		rank=same;
		makefile headers configfile f_options [ label="options" ]
	}

	...

	// 表示位置を違えるためのダミーのエッジ
	fntab -> files [ style=invis ]
}

ついでに、出力を担当する4つの関数の位置も揃えた。

drawn graph (4)
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/3858671
※言及リンクのないトラックバックは受信されません。

この記事へのトラックバック
この記事へのコメント
コメントを書く
お名前: [必須入力]

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。