こんにちは。キャスレーコンサルティングSI(システム・インテグレーション)部の佐藤です。
今回は、「dot言語でFizzBuzzを可視化する」というテーマになります。
FizzBuzzの結果を、グラフ理論でよく使用されるdot言語で表し、
それをGraphvizを元に作成されたviz.jsを用いて可視化します。
基本的に、今回はdot言語の入門編であるため、
可視化するにあたっては、「こうしてみたらどうなるだろう」をコードに移してみる形をとっています。
使用するのは、以下の通りです。
- Ubuntu 14.04 LTS
- Electron
- jquery
- viz.js
開発環境の準備
dot言語について説明する前に、今回の開発環境を整えます。
今回使用するviz.jsは、npmからインストールした方が手軽なため、
node.jsを内蔵するElectronで開発を行うことにします。
Electronについては、以前「ElectronとAngular2でHello World」の記事で触れていますので、
Electron自体の導入及び、プロジェクトの作成方法については、そちらの記事を参照していただければと思います。
Electronの諸設定
まずは、Electronに「hello_dots」というプロジェクトを作成します。
index.html、main.js、package.jsonを用意し、一度起動します。今回の初期状態は以下になります。
前回とは少しだけ変えて、アプリケーションの画面を大きめにとっています。
viz.js導入の諸設定
次に、今回FizzBuzzを可視化するにあたって必要なviz.jsをインストールします。
また、他にも今回の実装上必要になるjQueryと、JavaScriptのデバッグ用にdevtronを導入することにします。
viz.jsは、dot言語で記述された構造からグラフを作成するためのパッケージです。
元々は、Graphvizというアプリケーションで、それをJavaScriptにしたものがviz.jsになります。
devtronは導入すると、Chromeのデベロッパーツールが使えるようになります。
JavaScriptのエラー箇所を表示したり、コンソールにログを出したりすることができ、
開発上とても重要なので導入します。
まずは、先ほど開発環境として作成したディレクトリで、以下の通りにパッケージをインストールします。
% npm install viz.js % npm install jquery % npm install devtron
インストールが完了したら、package.jsonとmain.jsに設定を書き加えます。
[package.json]
{ "name": "hello_dots", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "viz.js": "^1.8.0", "jquery": "^3.2.1" }, "devDependencies": { "devtron": "^1.4.0", "viz.js": "^1.8.0" } }
[main.js]
'use strict'; var electron = require('electron'); var app = electron.app; var BrowserWindow = electron.BrowserWindow; var mainWindow = null; // 画面がすべて閉じたらアプリを終了する app.on('window-all-closed', function() { if(process.platform != 'darwin') app.quit(); }); app.on('ready', function() { // ブラウザ(Chromium)の起動, 800 * 1200 mainWindow = new BrowserWindow({width:1200, height:800}); // 最初に読み込む画面のパスを指定 mainWindow.loadURL('file://' + __dirname + '/index.html'); mainWindow.on('closed', function() { mainWindow = null; }); // デベロッパーツールの起動 mainWindow.webContents.openDevTools(); });
再びアプリケーションを起動し、問題なく表示されることを確認してください。
デバッグ用にdevtronを導入したことで、Chromeのデベロッパーツールが立ち上がっているはずです。
今後、ローカルでHTMLやJavaScriptに修正を加えた場合、
デベロッパーツール上でF5を押して更新することで、
いちいちElectronを立ち上げ直すことなくスクリプトを読み込み直すことができます。
1. dot言語について
1-1. dot言語とは
dot言語とは、データ記述言語の一つです。
データの構造を、頂点と頂点間を結ぶ辺のみで構成されるグラフとして、記述することに長けています。
よくグラフ理論という分野でグラフを描画する際に使用されるツールなので、
自動生成された画像には、何となくどこかで見たことがある絵面をしています。
複雑なグラフであっても、大体の場合は小さなグラフがいくつか連結しているだけだったりするので、そういった構造の把握にも使えたりします。
しかし、個人的に一番重要なことは、データ構造をテキストで書くと、グラフが簡単に描けるということです。
なによりテキストベースなので、メンテナンスが簡単です。
1-2. dot言語を使うと何ができるのか
複雑な図形を、そこそこ簡単に描くことができます。
例えば、以下のような図形を描きたかったとします。
例えば、Excelでこの図を描くならば、四角と丸と直線を描いておき、
それをコピー&ペーストしてそれぞれの個数を合わせたりすると思います。
そして、最後に文字と配置を整えていくかと思います。
文字入れは、テキストボックスを選択したり、絶妙にずれる位置関係を手直ししたり、全体のバランスを整えたり。
作り自体は簡単に見えるのに、実際に手を動かすとなると結構手間だと思います。
これをdot言語で書くと以下のようになります。
digraph G { node1[shape="box"]; node2; node3; node4[shape="box"]; node1->node2[label="edge1"]; node1->node3[label="edge2"]; node2->node4[label="edge3"]; node3->node4[label="edge4"]; }
いかがでしょうか。
テキストベースかつ構造だけで記すと、この程度で収まるものなんです。
書き方もとても簡単です。
- まず頂点(node)を宣言する。
- 次に頂点間を結ぶ辺(edge)を列挙する。
これだけです。
あとは、頂点の形を変えたければshapeで好きな形を宣言し、辺に名前をつけたければlabelで名前を付けるだけです。
書き出しのdigraph Gというのは、「有向グラフG」を意味しています。
グラフ内部で使われる、辺に向きのあるグラフ(=有向グラフ)という宣言です。
有向グラフにしていても向きのない辺を描くことはできますので
基本的にはこの部分はおまじないだと思っていただいて構いません。
より詳しく知りたい方は、以下のサイトをご参照ください。
1-3. viz.jsで可視化してみる
そもそもの話になりますが、dot言語はあくまでデータ構造を記すための言語なので、それ単体では可視化できません。
そのため、dot言語で書き記したあとにそれを画像に変換する必要があります。
そこで、graphvizというコンパイラが使われます。
今回使用するviz.jsとは、このgraphvizをJavaScriptで実装したものになります。
実際にElectron上で使用してみましょう。
[sample.js]
// viz.jsを読み込む const Viz = require('viz.js'); // jQueryの $ が使用できるよう宣言をする。 window.jQuery = window.$ = require('jquery'); $(function(){ // 読み込むグラフをdot言語で記載する。 var dot_str = 'digraph G {' + 'node1[shape="box"];' + 'node2;' + 'node3;' + 'node4[shape="box"];' + 'node1->node2[label="edge1"];' + 'node1->node3[label="edge2"];' + 'node2->node4[label="edge3"];' + 'node3->node4[label="edge4"];' + '}'; // dot言語の内容をvizで読み込む。 $("#dotBox").append(Viz(dot_str)); });
[index.html]
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Hello, Dots!</title> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2FjavaScript%22%20src%3D%22sample.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /> </head> <body> <h1>DOT言語、はじめました。</h1> <div id="dotBox"></div> </body> </html>
少し意図していた形とはずれましたが、おおよそ求めている形の作図ができました。
注意していただきたいのは、Electronの仕様上、JavaScriptの先頭でrequireを宣言する必要があることです。
また、今回jQueryを使用していますが、Electronでは”$”が内部で使用されているため、
そのままでは使用することができません。
使用するJavaScript内で、改めて宣言することで使用可能になります。
2. FizzBuzzを可視化してみる
2-1. まずFizzBuzzを用意する
まずは、今回使用するFizzBuzzを用意します。
以下のfizzBuzz.jsを、HTMLのsample.jsの直後に読み込んでください。
[fizzBuzz.js]
// 初期値 var MAX = 20; var FIZZ = 3; var BUZZ = 5; // 変数宣言 var fizzNum = 0; var buzzNum = 0; var max = 0; var array; /** * 初期化処理 */ function fizzbuzzInit(){ fizzNum = $("#fizzNum").val(); buzzNum = $("#buzzNum").val(); max = $("#max").val(); // 最大値が空欄ならば初期値を入れる if(max==null || max==""){ max=MAX; } if(fizzNum==null || fizzNum=="" ){ fizzNum = FIZZ; } if(buzzNum ==null || buzzNum ==""){ buzzNum = BUZZ; } // 配列初期化 array = new Array(max); // fizzbuzz算出 fizzbuzz(); // コンソールで確認してみる。 console.log(array); } /** * fizzbuzz計算 */ function fizzbuzz(){ var tmp; var n; var i; for(i=0;i<max;i++){ n = i+1; if(n%fizzNum==0 && n%buzzNum==0){ // fizzbuzz tmp = "fizz buzz"; } else if(n%fizzNum==0){ // fizz tmp = "fizz"; } else if(n%buzzNum==0){ // buzz tmp = "buzz"; } else { tmp = n; } array[i] = tmp; } }
結果をコンソールに書きだすようにしたので、実際に動かしてみましょう。
1から20までの数字で、FizzBuzzの結果を見てみます。
ちなみに、1から100までの結果とすると以下のようになります。
2-2. FizzBuzzを元にグラフを作ってみる
まずは、FizzBuzzの結果を頂点にして全列挙してみます。
[fizzBuzz.js]
/** * 初期化処理 */ function fizzbuzzInit(){ fizzNum = $("#fizzNum").val(); buzzNum = $("#buzzNum").val(); max = $("#max").val(); // 最大値が空欄ならば初期値を入れる if(max==null || max==""){ max=MAX; } if(fizzNum==null || fizzNum=="" ){ fizzNum = FIZZ; } if(buzzNum ==null || buzzNum ==""){ buzzNum = BUZZ; } // 配列初期化 array = new Array(max); // fizzbuzz算出 fizzbuzz(); // コンソールで確認してみる。 // console.log(array); // 可視化してみる。 printGraph(); } /** * グラフ可視化メソッド */ function printGraph(){ // 表示エリアクリア $("#fizzBuzz").empty(); var i; var pretext = 'digraph G{' + 'node [width=0.5];'; var nodes = ''; var edges = ''; for(i=0;i<max-1;i++){ nodes += 'node_' + i + ' [label="'+ array[i] +'"];'; } var afttext = '}'; // 結果をグラフにする。 $("#fizzBuzz").append(Viz(pretext+nodes+edges+afttext)); // コンソールに作成したdot構造を出力する。 console.log(pretext+nodes+edges+afttext); }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Hello, Dots!</title> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2FjavaScript%22%20src%3D%22sample.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2FjavaScript%22%20src%3D%22fizzBuzz.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /> </head> <body> <h1>DOT言語、はじめました。</h1> <div id="header"> Fizz: <input id="fizz" type="text" value="3" /> Buzz: <input id="buzz" type="text" value="5" /> Max: <input id="max" type="text" value="20" /> <button onclick="fizzbuzzInit()">作成</button></div> <div id="fizzBuzz"></div> </body> </html>
ひたすら頂点が、列挙されました。
dot言語では、辺についての記載が無いものは、全て頂点として認識されます。
また、[label]はラベルとなり、頂点の名称として表示されます。
ラベルの記載がない場合は、頂点の名前がそのまま表示されるようになっています。
試しにラベルを無くしてみると、node_xと表示されると思います。
さて、全て同じ頂点だと一見して分かりにくいので、普通の数字とそれ以外で何か区別を付けることにしましょう。
仮に、普通の数字は頂点の記号を丸にし、Fizz、Buzz、FizzBuzzに当たる数字は、記号を四角としてみます。
fizzBuzz.jsを、次のように書き換えてみてください。
/** * グラフ可視化メソッド */ function printGraph(){ // 表示エリアクリア $("#fizzBuzz").empty(); var i; var pretext = 'digraph G{' + 'node [width=0.5];'; var nodes = ''; var edges = ''; for(i=0;i<max-1;i++){ nodes += 'node_' + i + ' [label="'+ array[i] +'"'; if(isNaN(array[i])){ // FizzBuzzに関する頂点は四角形にする。 nodes += ', shape="box"'; } nodes += '];'; } var afttext = '}'; // コンソールに作成したdot構造を出力する。 console.log(pretext+nodes+edges+afttext); // 結果をグラフにする。 $("#fizzBuzz").append(Viz(pretext+nodes+edges+afttext)); }
頂点の記号が変わりました。
頂点は、[shape]で変更することができます。
単純な丸や四角だけでなく、様々な形を指定することができます。
これでぱっと見て、FizzBuzzか否かは判断できるようになりました。
次はFizz、Buzz、FizzBuzzを区別できるようそれぞれ色をつけてみましょう。
配色は、3で割り切れる(=Fizz)ならば緑色、5で割り切れる(=Buzz)ならば橙色、
3と5両方で割り切れるならば赤色とします。
それ以外の普通の数字は、初期状態とします。 fizzBuzz.jsを次のように書き換えてみてください。
/** * グラフ可視化メソッド */ function printGraph(){ // 表示エリアクリア $("#fizzBuzz").empty(); var i; var pretext = 'digraph G{' + 'node [width=0.5];'; var nodes = ''; var edges = ''; for(i=0;i<max-1;i++){ nodes += 'node_' + i + ' [label="'+ array[i] +'"'; if(isNaN(array[i])){ nodes += ', shape="box"'; if(array[i]=="fizz"){ nodes += ', style="filled", color="green"'; } else if(array[i]=="buzz"){ nodes += ', style="filled", color="orange"'; } else if(array[i]=="fizz buzz"){ nodes += ', style="filled", color="red"'; } } nodes += '];'; } var afttext = '}'; // コンソールに作成したdot構造を出力する。 console.log(pretext+nodes+edges+afttext); // 結果をグラフにする。 $("#fizzBuzz").append(Viz(pretext+nodes+edges+afttext)); }
色は、[color]で指定することができます。
また、今回は塗りつぶすために[style=”filled”]も指定してみました。
このあたりで、辺を追加してみましょう。
Fizzから次のFizzの間とBuzzから、次のBuzzまでの間に辺を足してみます。
このとき、FizzからFizzBuzzへと、BuzzからFizzBuzzへの辺も作成しますが、逆へ向かう辺はないものとします。
/** * グラフ可視化メソッド */ function printGraph(){ // 表示エリアクリア $("#fizzBuzz").empty(); var i; var pretext = 'digraph G{' + 'node [width=0.5];'; var nodes = ''; var edges = ''; var oldFizz = null; var oldBuzz = null; for(i=0;i<max-1;i++){ nodes += 'node_' + i + ' [label="'+ array[i] +'"'; if(isNaN(array[i])){ nodes += ', shape="box", style="filled"'; if(array[i]=="fizz"){ nodes += ', color="green"'; if(oldFizz != null){ edges += 'node_' + oldFizz + ' -> node_' + i + ';'; } oldFizz = i; } else if(array[i]=="buzz"){ nodes += ', color="orange"'; if(oldBuzz != null){ edges += 'node_' + oldBuzz + ' -> node_' + i + ';'; } oldBuzz = i; } else if(array[i]=="fizz buzz"){ nodes += ', color="red"'; if(oldFizz != null){ edges += 'node_' + oldFizz + ' -> node_' + i + ';'; oldFizz = null; } if(oldBuzz != null){ edges += 'node_' + oldBuzz + ' -> node_' + i + ';'; oldBuzz = null; } } } nodes += '];'; } var afttext = '}'; // コンソールに作成したdot構造を出力する。 console.log(pretext+nodes+edges+afttext); // 結果をグラフにする。 $("#fizzBuzz").append(Viz(pretext+nodes+edges+afttext)); }
辺が追加されました。
その代わりと言ってはなんですが、少し表示が変わりました。
実は、graphvizには頂点間で関連が強いものほど、近くに配置するという仕組みがあります。
頂点を列挙しただけだと、すべて同じ程度の関連性として扱われ、入力順に出力されます。
しかし、今回のように一部の頂点にのみ辺が追加されると、
辺で結ばれた頂点間の関連性は他の頂点との関連性よりも強くなりますので、
画像のようにまとまって表示されてしまったわけです。
今回の場合、暗黙で「頂点はラベルの数字が小さい順に並んでいる」という前提があったため、
FizzBuzzに該当する数字を表示しませんでしたが、順番が狂ってしまうと対応する数字がわかりませんね。
それでは困ってしまうので、FizzBuzzの文字と数字を両方出すようにしてみましょう。
/** * グラフ可視化メソッド */ function printGraph(){ // 表示エリアクリア $("#fizzBuzz").empty(); var i; var pretext = 'digraph G{' + 'node [width=0.5];'; var nodes = ''; var edges = ''; var oldFizz = null; var oldBuzz = null; for(i=0;i<max-1;i++){ if(!isNaN(array[i])){ nodes += 'node_' + i + ' [label="'+ array[i] +'"'; } else { nodes += 'node_' + i + ' [shape=plaintext, label=' + returnNodeLabel(i, array[i]); if(array[i]=="fizz"){ if(oldFizz != null){ edges += 'node_' + oldFizz + ' -> node_' + i + ';'; } oldFizz = i; } else if(array[i]=="buzz"){ if(oldBuzz != null){ edges += 'node_' + oldBuzz + ' -> node_' + i + ';'; } oldBuzz = i; } else if(array[i]=="fizz buzz"){ if(oldFizz != null){ edges += 'node_' + oldFizz + ' -> node_' + i + ';'; oldFizz = null; } if(oldBuzz != null){ edges += 'node_' + oldBuzz + ' -> node_' + i + ';'; oldBuzz = null; } } } nodes += '];'; } var afttext = '}'; // コンソールに作成したdot構造を出力する。 console.log(pretext+nodes+edges+afttext); // 結果をグラフにする。 $("#fizzBuzz").append(Viz(pretext+nodes+edges+afttext)); } function returnNodeLabel(num, type){ var color="black"; if(type=="fizz"){ color="green"; }else if(type=="buzz"){ color="orange"; }else if(type=="fizz buzz"){ color="red"; } return '< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR> <TD BGCOLOR="'+color+'"><FONT COLOR="white">'+type+'</FONT></TD> </TR> <TR> <TD PORT="f1">'+num+'</TD> </TR> </TABLE> >'; }
実は、graphvizでは一部HTMLに対応しているため、labelにテーブルタグを使うことができます。
今回使用しているviz.jsも、元はgraphvizなので、このような記述と表示ができるのです。
3. まとめ
dot言語の良いところは、画像にこのような細かい変更が入ることになっても、
少しテキストを修正するだけで結果が得られるところにあると思います。
これを普通の画像ソフトや、ましてやExcelで行うことを考えると眩暈がします。
今回、グラフはひたすら横に連なっていきましたが、
紹介できなかったオプション機能で形にバリエーションをつけることができます。
「Fizzとなるものは一番右端に固定で表示する」といった表示や、
「FizzBuzzの文字を中心に放射状に頂点を配置する」なんていう表現も
オプションを1つ2つ付け足すだけでできてしまったりします。
結構違った形を作れるので面白いですよ。
また、本文中では取り上げませんでしたが、viz.jsで記述した内容はsvg形式で出力されています。
そのため、viz.jsで大まかな内容を書き出し、あとからsvgに手を加えるということも可能です。
inkscape等のsvgファイルの編集が容易なソフトがあれば、より手軽に意図した形に整えることもできます。
【おまけ】フィボナッチ数列でFizzBuzzしてみる
「それ、最早FizzBuzzじゃないじゃん」という声が聞こえてきそうですが、
完成間際に根底をひっくり返されることはままあります。
そういう訳で、フィボナッチ数列の場合のFizzBuzzを出してみましょう。
とは言っても、dot言語周りには手は加えません。
先ほどまでの手順で、グラフにするためのテンプレートは作成できましたので、
今まで入力を1からNの連続数としていたところを、1からN個のフィボナッチ数列としてしまいましょう。
// 初期値 var MAX = 20; var FIZZ = 3; var BUZZ = 5; // 変数宣言 var fizzNum = 0; var buzzNum = 0; var max = 0; var array; var nums; /** * 初期化処理 */ function fizzbuzzInit(){ fizzNum = $("#fizzNum").val(); buzzNum = $("#buzzNum").val(); max = $("#max").val(); // 最大値が空欄ならば初期値を入れる if(max==null || max==""){ max=MAX; } if(fizzNum==null || fizzNum=="" ){ fizzNum = FIZZ; } if(buzzNum ==null || buzzNum ==""){ buzzNum = BUZZ; } // 配列初期化 array = new Array(max); nums = new Array(max); makeNumbers(); // fizzbuzz算出 fizzbuzz(); // コンソールで確認してみる。 // console.log(array); printGraph(); } /* * 数列作成 */ function makeNumbers(){ // フィボナッチ数列 nums[0]=1; nums[1]=1; for(var i=2;i<max;i++){ nums[i]=nums[i-1]+nums[i-2]; } } /** * fizzbuzz計算 */ function fizzbuzz(){ var tmp; var n; var i; for(i=0;i<max;i++){ n = nums[i]; if(n%fizzNum==0 && n%buzzNum==0){ // fizzbuzz tmp = "fizz buzz"; } else if(n%fizzNum==0){ // fizz tmp = "fizz"; } else if(n%buzzNum==0){ // buzz tmp = "buzz"; } else { tmp = n; } array[i] = tmp; } } /** * グラフ可視化メソッド */ function printGraph(){ // 表示エリアクリア $("#fizzBuzz").empty(); var i; var pretext = 'digraph G{' + 'node [width=0.5];'; var nodes = ''; var edges = ''; var oldFizz = null; var oldBuzz = null; for(i=0;i<max-1;i++){ if(!isNaN(array[i])){ nodes += 'node_' + i + ' [label="'+ array[i] +'"'; } else { nodes += 'node_' + i + ' [shape=plaintext, label=' + returnNodeLabel(nums[i], array[i]); if(array[i]=="fizz"){ if(oldFizz != null){ edges += 'node_' + oldFizz + ' -> node_' + i + ';'; } oldFizz = i; } else if(array[i]=="buzz"){ if(oldBuzz != null){ edges += 'node_' + oldBuzz + ' -> node_' + i + ';'; } oldBuzz = i; } else if(array[i]=="fizz buzz"){ if(oldFizz != null){ edges += 'node_' + oldFizz + ' -> node_' + i + ';'; oldFizz = null; } if(oldBuzz != null){ edges += 'node_' + oldBuzz + ' -> node_' + i + ';'; oldBuzz = null; } } } nodes += '];'; } var afttext = '}'; // コンソールに作成したdot構造を出力する。 console.log(pretext+nodes+edges+afttext); // 結果をグラフにする。 $("#fizzBuzz").append(Viz(pretext+nodes+edges+afttext)); } function returnNodeLabel(num, type){ var color="black"; if(type=="fizz"){ color="green"; }else if(type=="buzz"){ color="orange"; }else if(type=="fizz buzz"){ color="red"; } var str = '< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">' + ' <TR> <TD BGCOLOR="' + color +'">' + '<FONT COLOR="white">' + type + '</FONT></TD> </TR> ' + ' <TR> <TD PORT="f1">' + num + '</TD> </TR> </TABLE> >' return str; }
簡単にできました。
今回紹介したdot言語は、データ構造が本当にシンプルに書けます。
さらに応用すると、ER図や状態遷移図なんかも簡単に作図できてしまうので、
色々試してみると面白いのではないかと思います。ぜひ遊んでみてください。
最後までお読みいただき、ありがとうございます。