webpackでdagre-d3を使って状態マシン図を実装する際に困ったことまとめ(導入編)

  • 2
    Like
  • 0
    Comment

この記事はIS17er Advent Calendar19日目の記事として書かれたものです。
18日目の素晴らしい記事はこちら。本来AWSについて書きたかったのですがあまりに時間と進捗がなかったので昨日と本日デバッグで困った点をまとめただけの記事になりました。

dagre-d3とは

dagre-d3とは有向グラフを簡単に実装するためのd3の拡張モジュールです。
できることを説明するより実際に見てもらったほうがすごさが伝わると思いますのでいくつかサンプルを紹介します。
1. Dagre Interactive Demo
Edgeの始点終点を入力してあげるとインタラクティブにレンダーされ、グラフがうまい具合に生成されます(おそらく閉路がない限りはトポロジカルソートされたグラフが生成されるはずです)。
Screen Shot 2016-12-19 at 4.17.52 PM.png
2. Sentence Tokenization
文章を構造に注目して単語単位に分けるプログラムっぽいです(詳細は知りません)。
Screen Shot 2016-12-19 at 4.20.46 PM.png
3. Clusters
このようにグループを含む状態マシン図も実装することができます。
Screen Shot 2016-12-19 at 4.22.45 PM.png

dagre-d3の使い方

htmlファイルにjavascriptを直書きするのであれば
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="dagre-d3.js"></script>

としてやれば問題なく動きます(上に貼ったサンプルのソースコードを御覧ください)。
ここで注意してほしいのは、dagre-d3.jsはこの記事の執筆時点(2016/12/19)ではd3.v4に対応していない、という点です。これは試しに
<script src="http://d3js.org/d3.v3.min.js"></script>
の部分を
<script src="http://d3js.org/d3.v4.min.js"></script>
とするとレンダーされないことからわかります。

webpackで使用する場合(typings使用版)

d3もdagre-d3もnpmパッケージが配布されているのでとりあえず
$ npm install d3@3.5.17 --save
$ npm install dagre-d3 --save
をしましょう。--saveオプションはpackage.jsonにdependenciesを追加します。
ここで注意してほしいのはd3のバージョンを最新にしないことです。

typescriptファイル内で
import * as d3 from 'd3';
import * as dagreD3 from 'dagre-d3';
のようにしてモジュールをインポートするには、d3およびdagre-d3をtypingsでもinstallする必要があります。
$ typings install dt~d3 --save --global
$ typings install dt~dagre-d3 --save --global
こうすると上で行ったようなことが可能です。

webpackで使用する場合(alias指定版)

上のように指定して問題なく動けば最高なのですが、僕の設定で実行すると実行時に非常に長いwarningsが表示され、コンソールが非常に重くなったのでaliasを用いることにしました。
なお、webpackのコンソールに表示するwarningsをなくしたいのであれば、webpackの設定ファイル(webpack.config.jsなど)に次のような記述をすれば大丈夫です(ブラウザのコンソールではそのままwarningsが表示されます。ブラウザのコンソールでwarningsを表示させないようにするにはこうすればいいです)。

webpack.config.js(一部)
module.exports = {
 devServer: {
  stats: {
   warnings: false
  }
 }
}

しかし、いずれにせよこれらの方法は重要なwarningsを非表示にする可能性もあり、危険です。そこで、最終手段ですが次のようにモジュールをインポートすることにしました。

webpack.config.js(一部)
module.exports = {
 resolve: {
  extensions: ['', '.js'],
  root: 'src',
  modulesDirectories: ['node_modules'],
  alias: {
   'd3': 'node_modules/d3/d3.min.js', // intentionally using d3 v3
   'dagreD3': 'node_modules/dagre-d3/dist/dagre-d3.min.js'
  }
 },
 module: {
  noParse: /node_modules\/dagre-d3\/dist\/dagre-d3.min.js/,
 }
}

いちいち忠告しなくていいかもしれませんが、上のwebpack.config.jsmodule.exports以下は、皆さんの既存のコードのdevServer部分、およびresolve部分に追記してほしい内容を記述したものであり、module.exports以下をまるまる書き換えてしまってはダメです。

こうすることでtypescript内で
const d3 = require('d3');
const dagreD3 = require('dagreD3');

とすることでモジュールをインポートできます。

これは実質的には
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="dagre-d3.js"></script>

とまったく同じことであり、javascriptを控えめにしたという感じです。

問題点まとめ

つまり何が問題だったかというと
1. dagre-d3がd3の最新版であるd3 v4に対応していない
2. typings installすると謎のwarningsが大量に出る(プログラム自体は一応動く)
ということで、これについての対策は
1. d3のv3.x.xをインストールする
2. aliasに登録してtypescript内でrequireする
ということでした。

実装編

タイトルに(導入編)と入れたのですが実装編について書く予定はないので、申し訳程度に実装方法も載せておきます(サンプルのソースにコメントアウトで追記しています)。

sample.html(一部)
<div>
  <svg><g/></svg>
</div>
sample.js
// label(ノード内の文字)をもつグラフを作成
var g = new dagreD3.graphlib.Graph()
.setGraph({})
.setDefaultEdgeLabel(function() { return {}; });

// ノードを追加していく。第一引数はid, 第二引数でラベルおよびclass(css)を指定
// 本来はデータをfor文で回して追加していくべき
g.setNode(0,  { label: "TOP",       class: "type-TOP" });
g.setNode(1,  { label: "S",         class: "type-S" });
g.setNode(2,  { label: "NP",        class: "type-NP" });
g.setNode(3,  { label: "DT",        class: "type-DT" });
g.setNode(4,  { label: "This",      class: "type-TK" });
g.setNode(5,  { label: "VP",        class: "type-VP" });
g.setNode(6,  { label: "VBZ",       class: "type-VBZ" });
g.setNode(7,  { label: "is",        class: "type-TK" });
g.setNode(8,  { label: "NP",        class: "type-NP" });
g.setNode(9,  { label: "DT",        class: "type-DT" });
g.setNode(10, { label: "an",        class: "type-TK" });
g.setNode(11, { label: "NN",        class: "type-NN" });
g.setNode(12, { label: "example",   class: "type-TK" });
g.setNode(13, { label: ".",         class: "type-." });
g.setNode(14, { label: "sentence",  class: "type-TK" });

// ノードはもともと長方形だがコーナーを丸くしている
g.nodes().forEach(function(v) {
 var node = g.node(v);
 node.rx = node.ry = 5;
});

// エッジを追加していく
// g.setEdge(3,4);はidが3のノードからidが4のノードへ向かう矢印を追加
// これも本来for文で回すべき
g.setEdge(3, 4);
g.setEdge(2, 3);
g.setEdge(1, 2);
g.setEdge(6, 7);
g.setEdge(5, 6);
g.setEdge(9, 10);
g.setEdge(8, 9);
g.setEdge(11,12);
g.setEdge(8, 11);
g.setEdge(5, 8);
g.setEdge(1, 5);
g.setEdge(13,14);
g.setEdge(1, 13);
g.setEdge(0, 1)

// renderという関数を用意している。これが最終的に図を生成する関数
var render = new dagreD3.render();

// d3.select("svg");により,htmlにあるsvg要素が選択される。svgが複数ある場合は注意。その場合,svgの親要素をselectして,それにsvgをappendすればよい
var svg = d3.select("svg");

// svgの子要素gの部分に,グラフgをレンダー
render(d3.select("svg g"), g);

// svgのサイズがデフォルトのままでは見切れてしまうので大きさを調整
svg.attr("height", g.graph().height);
svg.attr("width", g.graph().width);

次にcssです。dagre-d3(およびd3)ではcssが命です。cssがないと意味不明なグラフになります。

sample.css
g.type-TK > rect {
  fill: #00ffd0;
}

text {
  font-weight: 300;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
  font-size: 14px;
}

.node rect {
  stroke: #999;
  fill: #fff;
  stroke-width: 1px; // edgeの先の▲の大きさです
}

.edgePath path {
  stroke: #333;
  stroke-width: 1px; // pathの太さです
}

以上になります!お疲れさまでした!

【追記】
こちらにv4に対応したforkがありました