Mac で Graphviz の階層をインデント付きアウトラインのように表示するには、各ノードに行とインデントの座標を割り当て、neato -n2 でレンダリングします。これにより、親子間のエッジを保ちながら、読み進める順序を制御できます。

この手順では、6 ノードの例を作成して PDF に出力します。目標は明確です。1 行に 1 ノードを配置し、子孫を親のすぐ下に置き、同じ深さのノードは同じインデントにそろえます。Graphviz では、-n コマンドラインオプションを使った固定位置のレンダリングが説明されています。

通常の Graphviz のツリーがアウトラインのように見えないのはなぜですか?

階層が表すのは関係性ですが、インデント付きアウトラインでは読み進める順序も指定します。この例では、ルートに 2 つの分岐があり、1 つ目の分岐の子孫を 2 つ目の分岐より先に表示する必要があります。目的の順序は次のとおりです。

Node R
    Node A
        Node C
        Node D
    Node B
        Node E

グラフの関係は、ルートから A、ルートから B、A から C、A から D、B から E です。B が次の行にあるからといって、D から B への関係があるわけではありません。DOT では -> 演算子で有向エッジを表します。ノードの宣言と表示属性は、言語内で別の要素です。公式の DOT 言語リファレンスを参照してください。

Graphviz の dot エンジンは、階層図やレイヤー図を生成します。エッジの方向をそろえながら、交差やエッジの長さを減らそうとします。この目的は分岐図には適していますが、上に示した 1 行 1 ノードのアウトラインを定義するものではありません。dot レイアウトのドキュメントを参照してください。

よく使われる属性はいくつかありますが、それぞれ問題の一部にしか対応しません。

設定 ドキュメントに記載された用途 未解決の点
rankdir=LR レイアウトの方向を左から右に変更する 縦方向のアウトラインの順序は指定しない
rank=same サブグラフのノードを同じランクに配置する すべてのノードに個別のアウトライン行を割り当てない
ordering=out 出ていくエッジを入力順に左から右へ配置するよう制約する 深さに基づくインデントと行の座標は定義しない

これらの制約は、rankdir、rank、orderingの公式リファレンスに記載されています。この記事のように行を正確に配置する場合は、これらの属性を組み合わせるより、座標を明示するほうが直接的です。

Mac で Graphviz を準備する方法

Terminal を開きます。すでに Homebrew を使っている場合は、次のコマンドで Graphviz をインストールします。公式の Graphviz ダウンロードページに、この Mac 向けインストール方法が記載されています。

brew install graphviz

代わりに MacPorts を使う場合は、同じダウンロードページに sudo port install graphviz と記載されています。すでに使っているパッケージマネージャーを選んでください。どちらもインストールされていない場合は、この手順に戻る前に、ページにある該当のインストールリンクに従ってください。

レンダラーが使えることを確認します。

neato -V

-V オプションは Graphviz のバージョン情報を表示します。コンピューター間で結果を比較する際は、表示されたバージョンを記録してください。この記事に特定のバージョン番号は記載されていません。Graphviz の コマンドラインリファレンスにこのオプションの説明があります。

インデント付き階層を作成する方法

次のシェルブロックを実行すると、新しい作業フォルダーが作成され、その中に DOT ファイルが書き込まれます。Terminal で実行してください。完了するとフォルダーのパスが表示されます。引用符で囲んだ heredoc の区切り文字により、DOT のテキストは記述どおりに保持されます。

outline_dir=$(mktemp -d "$HOME/graphviz-outline.XXXXXX")
if [ -n "$outline_dir" ] && cd "$outline_dir"; then
  cat > hierarchy.dot <<'DOT'
digraph Hierarchy {
  graph [splines=ortho, bgcolor=white];
  node [
    shape=box,
    width=1.2,
    height=0.35,
    fixedsize=true,
    fontname="Courier",
    fontsize=12
  ];

  r [label="Node R", pos="100,300"];
  a [label="Node A", pos="136,252"];
  c [label="Node C", pos="172,204"];
  d [label="Node D", pos="172,156"];
  b [label="Node B", pos="136,108"];
  e [label="Node E", pos="172,60"];

  r -> a;
  r -> b;
  a -> c;
  a -> d;
  b -> e;
}
DOT
  pwd
fi

ノードの位置でアウトラインの配置を表します。行が 1 つ進むごとに 48 ポイント下へ移動し、深さが 1 増えるごとに 36 ポイント右へ移動します。この数値は間隔の例であり、Graphviz のデフォルト値ではありません。pos のドキュメントによると、ノード位置はノードの中心を指します。また、-n と併用した場合、neato は位置をポイント単位として解釈します。

ノード 深さ 0 から数えた行 中心位置
Node R 0 0 100,300
Node A 1 1 136,252
Node C 2 2 172,204
Node D 2 3 172,156
Node B 1 4 136,108
Node E 2 5 172,60

この配置では、座標の規則は x = 100 + 36 × depth と y = 300 − 48 × row です。Node D から Node B への移行に注目してください。行は進みますが、深さは浅くなります。この移行があるため、図は一本の連鎖ではなくアウトラインとして読めます。

すべてのボックスは同じ寸法なので、中心座標が同じ列にそろえば左端もそろいます。長さをそろえた例示用ラベルにより、テキストの配置を比較しやすくしています。fixedsize=true を指定すると、Graphviz は内容に合わせてボックスを拡大せず、指定したノード寸法を使用します。ラベルが大きすぎると警告が出ます。fixedsize のリファレンスを参照してください。

結果をレンダリングして確認する方法

同じ Terminal セッションで、作業フォルダー内から次を実行します。

neato -n2 -Tpdf hierarchy.dot -o hierarchy.pdf

ここでは -n2 によって、重なりを調整せずに指定したノード配置を保持し、個別の位置が設定されていないエッジの経路を計算します。そのため、すべてのノードに pos の値が必要です。-Tpdf と -o オプションで、出力形式を PDF にし、保存先を指定します。これらの動作はコマンドラインリファレンスに記載されています。

Finder で作業フォルダーを開き、hierarchy.pdf を開きます。PDF 出力には、Graphviz のPDF 出力ページに記載されている Cairo レンダラーを含むビルドが必要です。この例はドキュメントと照合済みですが、調査環境では Graphviz を利用できなかったため、ここではレンダリングを実行して確認していません。

次の 3 点をそれぞれ確認してください。

  1. 上から順に読むと、R、A、C、D、B、E が連続する行に並んでいること。
  2. インデントを比べると、A と B がそろい、C、D、E が 1 段右にそろっていること。
  3. 関係をたどると、R は A と B に、A は C と D に、B は E に接続していること。

最初の 2 点では座標の配置を確認します。3 点目では実際のグラフを確認します。これらを分けて確認すると、整然として見えても親子間のエッジが誤っている図を見つけやすくなります。

コネクターはファイルブラウザーのツリーとまったく同じ見た目になりますか?

ノードの行とインデントは明示的に指定しますが、コネクターの経路は引き続き自動で計算されます。この例では splines=ortho を指定し、エッジを軸に平行な線分で経路指定します。Graphviz のドキュメントによると、この経路指定はポートに対応していないため、コンパスポートを追加しても、特定の曲がり方を強制する方法としては記載されていません。splines のリファレンスを参照してください。

そのため、この方法では、コネクターを自動で直角に経路指定したインデント付き階層を作れます。一方で、共有する縦の幹と、正確に配置された分岐点までは指定しません。このコネクターの形状が不可欠な場合は、エッジの経路を明示的に指定する追加の構成が必要です。splines=ortho だけでは、その要件を表せません。

コネクターを表示せずにすっきりしたアウトラインにするには、グラフの設定を splines=none に変更して、もう一度レンダリングします。DOT ファイルには親子間のエッジが残りますが、レンダラーはその線を表示しません。これは、同じ splines のリファレンスに記載されているレンダリングオプションです。

意味を変えずに例を調整する方法

まず、エッジの一覧とは別に、意図する読み順を書き出します。親から始めて、最初の子とその子孫を見てから、次の子へ移ります。その順序の各ノードに 1 行ずつ割り当てます。次に、深さに基づいてインデントを設定します。

たとえば、Node D の後に Node A の子を追加するには、読み順でそのノードを Node B より前に挿入します。深さを 2 にし、後続の行を下へ移動して、A から新しいノードへのエッジを追加します。行番号を変えただけで、既存の親子関係が変わることはありません。

編集後のバージョンが意図どおりに見えない場合は、次の順序で原因を調べてください。

  • 図が別の配置に広がる: コマンドに neato -n2 が引き続き指定されているか確認します。この例は通常の自動レイアウトではなく、指定した位置に依存します。
  • レンダリング時に位置がないと表示される: エッジの記述にしか登場しない ID を含め、すべてのノード ID を確認します。この方法では、各ノードに位置を指定した宣言が必要です。
  • ラベルがボックスからはみ出す: ボックスの共通サイズを大きくするか、ラベルを短くします。固定サイズのボックスは自動では拡大しません。
  • ボックスを大きくした後に行が重なる: 行間を広げます。このレンダリングモードは位置を保持し、重なりを修正しません。
  • 分岐が誤ったノードにつながる: エッジの記述を修正します。ボックスを移動しても、誤った関係は直りません。

位置の欠落と重なりの条件は、Graphviz がドキュメントで説明している -n2 の動作に基づいています。ラベルの条件は、fixedsize の動作に基づいています。手動で位置を指定するこの例では、間隔を広げることが相応の設計上の調整です。

階層の関係はエッジの一覧で管理し、表示方法は座標の表で管理します。Mac のデスクトップで小さな図を作る場合、この分け方なら簡単に保守できます。階層が変わったらエッジを変更し、読み順が変わったら行を変更し、ラベルにもっと余白が必要なら間隔を変更します。

出典