in the mythosil

Rhythm & Biology.

CからGraphvizを使う

perlだとGraphvizモジュールがあったり、C++ならBoost::Graphがあって、情報も調べればすぐに出てくるのですが、Cから使う場合に関しては公式ドキュメント以外にあまり見かけない気がします。そこで今回、Cからgraphvizのライブラリを使う方法に関して簡単にまとめてみました。

サンプルコード

まず先に簡単なサンプルコードを見てみます。

#include <gvc.h>

int main() {
  GVC_t* gvc = gvContext();

  /* create graph (type: digraph) */
  Agraph_t* G = agopen("sample", AGDIGRAPH);

  /* create node (n1, n2) */
  Agnode_t* n1 = agnode(G, "n1");
  Agnode_t* n2 = agnode(G, "n2");

  /* create edge (n1 -> n2) */
  Agedge_t* e = agedge(G, n1, n2);

  /* layout (using dot algorithm) and export as png */
  gvLayout(gvc, G, "dot");
  gvRenderFilename(gvc, G, "png", "sample.png");
  gvFreeLayout(gvc, G);

  agclose(G);
  gvFreeContext(gvc);
  return 0;
}

このコードをコンパイル・実行します。今回はmacportsgraphvizをインストールしていると仮定しているので、オプションを適宜自分の環境に合わせてください。

% gcc sample.c -I/opt/local/include/graphviz -L/opt/local/lib -lgvc -lgraph -lcdt
% ./a.out
% open sample.png

macportsでインストールしている場合にはpkg-config用の設定ファイルも同時にインストールされているので、以下のようにしてコンパイルすることもできます。

% gcc sample.c `pkg-config libgvc --cflags --libs`

解説

それでは、順番にコードの解説をしていきます。
まず、gvcのセットアップを行います。

GVC_t* gvc = gvContext();

これはレイアウト情報などを保持しておくものです。詳しい機能は今回は説明しませんが、アプリケーション内で一度だけgvContext()を呼んでおいて、あとはずっと使い回して問題ありません。
次に、グラフを作成します。

Agraph_t* G = agopen("sample", AGDIGRAPH);

今回はsampleという名前の有向グラフ(digraph)を作成しています。グラフの種類に関しては、

  • AGRAPH: non-strict, undirected graph
  • AGRAPHSTRICT: strict, undirected graph
  • AGDIGRAPH: non-strict, directed graph
  • AGDIGRAPHSTRICT: strict, directed graph

が用意されています。
以降、このグラフに対してノードやエッジを配置していきます。
まず、ノードの作成です。

Agnode_t* n1 = agnode(G, "n1");

グラフ(G)に対し、n1という名前のノードを配置しています。サンプルコードでは同様にしてn2という名前のノードも配置しています。
次にエッジを作成します。

Agedge_t* e = agedge(G, n1, n2);

グラフ(G)に対し、ノードn1からノードn2に向かうエッジを配置しています。
グラフに対しノードとエッジを配置しましたが、この段階ではまだレイアウトされていません。そこで、続いてグラフのレイアウトを作成します。

gvLayout(gvc, G, "dot");

グラフ(G)をdotアルゴリズムでレイアウトしています。他にもneatoアルゴリズムなどが用意されています。
レイアウトが完了したら、作成されたグラフを画像として出力します。

gvRenderFilename(gvc, G, "png", "sample.png");

グラフ(G)をsample.pngというファイル名で、pngフォーマットで出力しています。出力方法は、今回のようにgvRenderFilenameでファイル名指定のものと、gvRenderでファイルポインタを指定のものがあります。

gvRender(gvc, G, "dot", stdout);

とすると、dotフォーマットで標準出力に出力します。
グラフの生成、出力が終了したので、後処理をします。

gvFreeLayout(gvc, G);
agclose(G);
gvFreeContext(gvc);

上から順番に、レイアウト情報、グラフ、gvcコンテキストの削除をしています。gvcコンテキストに関しては、先ほど書いた通りアプリケーション内でずっと使い回せるので、毎回削除する必要はなく、アプリケーション終了時に削除すれば大丈夫です。

サンプルコードの拡張

以上が簡単なグラフの作成方法でしたが、少し拡張してノードの属性を変更してみます。
dotファイルで記述すると、

digraph sample {
  n1 -> n2;
  n2 [shape=diamond];
}

のようになります。これはノードn2の形をdiamondに変更しています(属性を指定しない場合のデフォルトはellipse)。
この拡張を先ほどのサンプルコードにも適用してみます。

#include <gvc.h>

int main() {
  GVC_t* gvc = gvContext();

  Agraph_t* G = agopen("sample", AGDIGRAPH);

  Agnode_t* n1 = agnode(G, "n1");
  Agnode_t* n2 = agnode(G, "n2");

  Agedge_t* e = agedge(G, n1, n2);

  /* change attribute */
  Agsym_t* sym = agnodeattr(G, "shape", "ellipse");
  agset(n2, "shape", "diamond");

  gvLayout(gvc, G, "dot");
  gvRenderFilename(gvc, G, "png", "sample.png");
  gvFreeLayout(gvc, G);

  agclose(G);
  gvFreeContext(gvc);
  return 0;
}

それでは追加したコードの解説をします。
属性の変更の際には、まずデフォルト値の設定が必要になります。

Agsym_t* sym = agnodeattr(G, "shape", "ellipse");

ノード全てのshape属性のデフォルト値をellipseに設定しています。
そして、特定のノード(n2)のshape属性を変更します。

agset(n2, "shape", "diamond");

今回はノードn2のshape属性をdiamondに変更しています。
ノードのshape属性の変更の仕方は以上になりますが、同様にしてshape以外の属性値も変更できます。注意すべき点は、先ほど書いた通り"まずデフォルト値の設定が必要になる"ということです。
また、ノードだけでなくグラフやエッジの属性も変更できます。それぞれ、

agraphattr(G, "属性名", "デフォルト値");
agedgeattr(G, "属性名", "デフォルト値");

でデフォルト値を設定し、ノードの時と同様にしてagsetで属性の変更を行います。

まとめ

ドキュメントがあまり充実していないため最初は戸惑いましたが、分かってしまえばそれほど難しいものではありませんでした。
ただ残念な点は、画像データをそのまま内部で利用することができず、一度ファイルとして書き出す必要があるところです。もしビットマップのバイナリデータを取り出す方法があったら教えていただきたいです。

属性の変更に関しては他にパフォーマンスのいい書き方(データ構造に直接アクセスする方法)があるらしいので、後日改めて調べて書きたいと思います。