dot言語(graphviz)の拡張についてのメモ
以前の記事の続きというかなんと言うか. ほぼ/dev/null
と化しているこのブログながら, この記事は何故か結構なアクセス数を持っていたりする. みんな困ってるのかな.
上の記事でしたかったことは, 要するに, 後から辺を追加するときに, レイアウトが崩れるのを防ぎたいということ. で, そのためにダミーノードを追加して対処ということを考えた. つまり, 想定している用途は, 一種のコマ送り的なものであるといえる.
そのような限定的な用途であるならば, dot言語に時系列情報を追加することで, うまいことdotに配置を知らせられるのではないかと思った(ダミーノード, ダミーエッジを追加することで).
確かめるべく適当に実装してみた. まず, 入力は以下のようなdot言語のファイル.
digraph {
A
B
C [color=$|if(t<1){print "black"}else{print "red"}|]
A -> B [style=$|if(t<1){print "invis"}else{print "solid"}|]
B -> C [style=$|if(t<2){print "invis"}else{print "solid"}|]
}
ここで, $||
に囲まれた場所がawkスクリプトとして解釈され, t
は時間を表す変数となる.したがって, pngにして出力すると以下のように うまいこと最初(t=0)から, A, B, Cの位置情報が整って表示される.
t |
画像 |
---|---|
0 | |
1 | |
2 |
ちなみに, これを3つの独立したdotグラフとして書くと, 以下のような画像になるので, 違いは歴然.
t |
画像 |
---|---|
0 | |
1 | |
2 |
最後に, サンプルの変換に用いたscala scriptはこちら. 正規表現とsys.processが組み合わさってすごいことになっている.
import scala.sys.process._
val fileName = args(0)
val time = args(1)
val command = "([^\\$]*)\\$\\|([^\\|]*)\\|(.*)".r
println(
(for (line <- s"cat ${fileName}".lineStream_!) yield {
line match {
case command(prefix, command, suffix) =>
val result = Seq("awk", s"BEGIN{t=${time};${command}}").!!.replace("\n","")
s"${prefix}${result}${suffix}"
case l => l
}
}).mkString("\n")
)
実用するとしたら
埋め込むスクリプトについては色々考慮の余地がある. 読みやすさを考えると,
- dotの記法に埋め込まれている(
foo=${script}
みたいな記法が良いと思う) - 記法が単純である(awkみたいにいちいちprintを書かなければいけないのは論外だろうと思う).
- 基本的に場合分けであるため, 複数行にわたるスクリプト埋め込み
- よく使うスクリプトをまとめた関数(?)のようなものの定義
といった要請があると思う.
まず, 1と3についてであるが, おそらく""
で囲ってやり, それをdotのパーサー(例えば, これとか)でパースすることで達成できると思う.
次に, 2であるが, 要するに関数型っぽく, 条件分岐が見やすいDSLが欲しいということ. 自分としては以下のように書けるといいかなと思う.
"time" : [0, 1], "state" : "abc(.*)" :: "inviz"
// 0 <= time <= 1 かつ stateが"abc(.*)"にマッチする場合はinviz
otherwise :: "solid"
これの利点は, 条件分岐がJSONでかけるということ. したがって, 各行を::
で分割して, 前者を{}
で囲ってJSONでパースすればすぐ条件にできるので処理が簡単.
本当を言うと, [0, 1]は, (0: 1)とか, [0: 1)とか, [: 1]とかで書きたい. また, orの条件を書くのが若干面倒というのはあるけれども, andとorを両方入れると優先順位を考えないと解釈できなくなるので面倒かなと思っている.
最後に, 4はなんか適当にマクロっぽいの準備すればできるだろうと思う.