CHAGEの裏側: D3.jsの紹介
2019年09月27日 金曜日
CONTENTS
CHAGE開発者のヒラマツです。
CHAGEの紹介(IIJ内製調査システム CHAGE のご紹介)と並行して、CHAGEの中が気になる人に、CHAGEで使われている技術をちょっと濃い目に解説するという企画です。
今回は、以下のサイトのD3.jsというJavaScriptライブラリの紹介です。
- https://d3js.org/ D3.js – Data-Driven Documents
D3.jsは、比較的軽い動作で、動的に(JavaScriptで)、いろいろなWebドキュメントを作れる便利なライブラリです。D3.jsを使うと、ドキュメントを動的に生成するWebサイトが、コスパよく作れます。多様な表現がコンパクトに書けて、それでいて、柔軟にカスタマイズできます。
おかげで、CHAGEのWeb UIは、D3.jsを使うことで、比較的、楽に、軽量に実装できました。
D3.jsの表現がどれぐらい多様なのかは、説明しきれないので、まずは、D3.jsサイトの膨大なサンプルをざっと眺めてください。
眺めるとわかりますが、D3.jsの表現の種類は、折れ線グラフ、棒グラフ、ノードツリー、ヒートマップ、日本語での呼び方がわからないグラフなどなど、本当に、多量というか大量です。
しかし、これだけ多才なD3.jsですが、残念ながら、日本ではあまり流行っていません。 それはもったいないので、微力ながら、D3.jsの使い方を紹介してみます。
この紹介で、目指すのは、ちゃんとした理解ではなく、「D3.jsサイトのサンプルコードをなんとなく読めて、データを差し替えて、なんとなく動かせる」ところです。
このD3.jsの紹介では、以下の知識を前提にしているので、ご了承ください。
- HTMLファイルを作れる。
- 簡単なJavaScriptのコードが書ける。
- DOMを知ってる。
- CSSのセレクタが使える。
- 作ったHTMLファイルをブラウザで開ける。
- SVGを知ってる。
D3.jsの最大の問題は、敷居が高いこと。
そこで、D3.jsを始めるにあたって、敷居が高い部分を厳選して、サンプルコードを通して、段階的に、体験してもらう内容にしました。
早く体験できるよう、説明も、意図的に短くざっくりです。 この紹介で物足りなくなったら、ぜひ、D3.jsのドキュメントを見てください。
D3.jsの特徴
まずは、能書きから。(D3.jsを、試すか悩んだ人向け)
D3.jsは、D3(Data-Driven Documents)という名前通りの、データから動的にドキュメントが作れるライブラリです。 グラフィックライブラリとして紹介されたりしますが、 グラフィック専用ではなく、いろいろなドキュメントが作れて、汎用性は高いです。
私の感想みたいなものですが、D3.jsは、以下のような特性を持ってます。
- 配列の要素とDOM要素を結び付けて、DOM要素を編集する。
- ここが一般的なライブラリとかなり違う。
- 大きめなデータでも柔軟に動作する。
- CSSセレクタでDOM要素を選択して、操作する。
- 比較的短いコードでDOM要素を操作できる。
- D3.jsで生成するDOM要素は、自由にできる。
- 実質、見た目のカスタマイズし放題。
- 作り方の方針を指定して、D3.jsに自動で繰り返させる感じ。
- 生のJavaScriptを尊重した作りになっている。
- JavaScriptコードを混ぜて、ふつうに使える。
- 仮想DOMは使わないので、他のライブラリの併用が楽。
- ツリーなどの複雑な構造をサポートする機能が充実してる。
- カスタマイズ性と楽さが、うまくバランスしてる。
- CSVやTSVなどの使いやすいデータ形式との変換機能も持っている。
- XML縛りや、JSON縛りじゃないので、データを用意するのが楽。
- 例えば、Excelでデータを見たり作ったり出来る。
このような特性のおかげで、ちょっと頑張るとその分結果が出る、コスパが良い「JavaScript」汎用ライブラリだと思います。とても汎用的かつ便利なので、おそらく、私も当分使います。
サンプルコードについて
この後は、簡単に読める小さなサンプルコードを使って、D3.jsの機能を小出しに紹介して行きます。
20行前後のとても小さなHTMLが7つです。読むのも簡単です。
D3.jsをダウンロードして、各Stepのサンプルコードをコピペして、動かして、ぜひ、D3.jsの基本的な機能を試してみてください。
サンプルコードは、D3.jsのファイルがlib/d3.v5.min.js
として置かれている前提になってます。
ですので、サンプルコードを試す前に、 ダウンロードしたアーカイブファイル内のd3.min.js
ファイルを取り出し、 そのファイルを、サンプルコードのディレクトリにlib
ディレクトリを作って、 d3.v5.min.js
の名前で配置しておきます。
ファイル名を変えている理由は、 D3.jsは、動作が大きく変わる時に、メジャーバージョン番号が変わるからです。
このように、バージョン入りのファイル名なら、アップデートの影響を小さくできます。
Step.1 要素選択と要素追加とテキスト設定
DOM要素を1つ選んで、子要素の追加と設定を行う例です。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>D3.js example</title> <script src="lib/d3.v5.min.js"></script> </head> <body> <div id="target"></div> </body> <script> var target = d3.select("#target"); // 1. target.append("p").text("Hello D3.js"); // 2. 3. </script> </html>
このサンプルを開くと、 「Hello D3.js」のテキストが追加されて、表示されます。 (すぐに処理が終わるので、初めから存在してたように見えますが)
このコードは、以下のことをやっています。
d3.select()
でdivタグ(id=”target”をセレクタで指定)のDOM要素を選択append()
でpタグの要素を追加text()
で追加した要素のテキストを設定
d3.select()
に指定するパラメータが#target
がCSSのセレクタなので、D3.jsは、CSSのセレクタの知識が必須になってます。
こんな簡単なコードで、DOM要素を追加できました。 もちろん、同様な処理を繰り返せば、複雑な構造も作れます。
Step.2 clickハンドラで、子要素選択と色変更
Step.1とは違い、今度は、要素を追加しません。
ボタンclick時に、 HTMLで記述済みのp要素を選択して、書き換えを行います。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>D3.js example</title> <script src="lib/d3.v5.min.js"></script> </head> <body> <div id="target"> <p>Hello D3.js</p> <button type=button onclick="change()">Change</button> </div> </body> <script> var target = d3.select("#target"); // 1. function change(){ target.select("p") // 2. .style("color", "red"); // 3. } </script> </html>
Changeボタンをクリックすると、Helloで始まるテキストの色が赤になります。
動的に変わることを体感するために、ボタンのクリックでの動作にしてみました。
ボタンのクリックで呼ばれるchange()
関数では、以下のことをやっています。
d3.select()
でdiv要素は、選択済み。select()
で子要素のp要素を選択("p"
も、やっぱりCSSのセレクタ)。style()
でp要素のcolorスタイルをred(赤)に変更。
style()
メソッドの引数の、"color"
部分はCSSのプロパティ名、"red"
部分は設定値です。
ということは、CSSの変えたいプロパティを知っていれば、動的にスタイルを変えることが出来るわけです。
そして、変更可能なのはスタイルだけではありません。
DOM変更のAPIには、style()
やtext()
以外に、attr()
、classed()
、property()
などがあります。(詳しいことは、D3.jsのAPIのModifying Elementsに書いてあります)
これで、既存のDOM要素の変更も出来ることがわかったと思います。
Step.3 複数要素選択と色変更
要素1つではなく、複数の要素を一度に変更する例です。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>D3.js example</title> <script src="lib/d3.v5.min.js"></script> </head> <body> <div id="target"> <p>Hello D3.js</p> <p>Hello D3.js</p> <p>Hello D3.js</p> <button id="update" type=button onclick="change()">Change</button> </div> </body> <script> var target = d3.select("#target"); // 1. function change(){ target.selectAll("p") // 2. .style("color", "red"); // 3. } </script> </html>
ボタンのクリックで呼ばれるchange()
で、以下のことをやっています。
d3.select()
でdiv要素は、選択済み。selectAll()
でdiv要素の全p要素を選択("p"
は、やはりCSSのセレクタ)。style()
で全p要素のcolorスタイルをred(赤)に変更。
これで、ループなしに、複数要素を一度に変更できました。 selectAll()
の引数はCSSのセレクタなので、柔軟にDOM要素を選んで、まとめて変更が可能です。
Step.4 データに合わせて複数要素の追加
配列データから、複数のDOM要素を作成する例です。
データを使うので、やっと、D3.jsらしくなってきました。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>D3.js example</title> <script src="lib/d3.v5.min.js"></script> </head> <body> <div id="target"> </div> </body> <script> var data = ["red", "blue", "green"]; var target = d3.select("#target").selectAll("p").data(data); // 1. 2. 3. target.enter().append("p") // 4. 5. .text(function(d){ return "color: " + d; }) // 6. .style("color", function(d){ return d; }); // 7. </script> </html>
dataの配列データを使って、以下のような処理でp要素を追加しています。
d3.select()
でdiv要素を選択selectAll()
でdiv要素内の全p要素を選択data()
でp要素にdata(配列)のデータを紐付けるenter()
で不足分のp要素を準備(何もないので、データ全部を不足分として準備)append()
で不足分のp要素を追加text()
でp要素のテキストをdataの値に合わせて設定style()
でp要素の色を配列の要素の値に設定
D3.jsの特徴的な操作なのですが、text()
、style()
の引数に、設定値ではなく、関数が指定されています。 data()
に渡した配列の要素が関数に渡されて、その結果がp要素に設定されます。
text()
の処理を例に上げると、function(d){ return "color: " + d; }
のd
に、"red"
、"blue"
、"green"
が順番に渡されて、"color: " + d
の計算結果がp要素に設定されるわけです。"red"
に対応するp要素は、"color: red"
テキストと、"red"
(赤)の色が設定されるわけです。(もちろん、"blue"
、"green"
も、同様に処理されます)
このように、データを使って関数経由で要素を生成すれば、データの数や内容に合わせて、動的かつ柔軟にドキュメントが作れます。「Data-Driven Documents」らしい部分ですね。
とても、便利ですから、CHAGEでも似たような方法で、動的にメニューを作成しています。
Step.5 複数要素とデータの連携
配列データを使って、表示を動的に更新する例です。
ボタンをクリックすると、データに合わせて、数も表示内容も変更されます。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>D3.js example</title> <script src="lib/d3.v5.min.js"></script> </head> <body> <div id="target"></div> <button id="update" type=button onclick="change()">Change</button> </body> <script> var target = d3.select("#target"); // 1. function update(data){ var elements = target.selectAll("p").data(data); // 2. 3. elements.exit().remove(); // 4. 5. elements.enter().append("p").merge(elements) // 6. 7. 8. .text(function(d){ return "color: " + d; }) // 9. .style("color", function(d){ return d; }); // 10. } function change(){ update(["red", "black", "blue", "gray"]); } update(["red", "blue", "green"]); </script> </html>
実際のDOM更新処理をしているupdate()
関数では以下のような処理をしています。
d3.select()
で、div要素を選択selectAll()
で、div要素内の全p要素を選択data()
で、p要素にdata(配列)を紐付ける(結果は、elementsに保存)exit()
で、余計な要素を選択(要素は減ってないので、この例では無選択)remove()
で、余計な要素を削除(この例では、何も消えない)enter()
で、不足分のp要素を選択append()
で、不足分のp要素を追加merge()
で、既存分と新規加分の要素を結合して、全要素を作成text()
で、全要素にp要素のテキストをdataの値に合わせて設定style()
で、全要素にp要素の色をdataの値に合わせて設定
少し複雑ですが、よく見れば、 Step.4との処理の違いは、「余分な要素の削除」(exit()
+remove()
)と「既存要素と新規要素の結合」(merge()
)に関する処理が追加だけです。
この追加された処理は、要は、すでにあるDOM要素を再利用して、差分更新しているだけなんです。
でも、逆に、この少しの複雑さだけで、仮想DOMなしで、DOMの差分更新して高速化とか、結構すごいと思いませんか?
そして、これが、D3.jsの基本動作なので、理解できれば、理解がぐっと早まります。
ちょっと複雑ですが、ぜひ、このサンプルコードを書き換えて(データの数を変えたり、必要な処理をわざと削ったりして)、どのように動くのかを確認し、きちんと理解してみてください。
Step.5の補足
Step.5のサンプルでは、分かりやすさを重視して、色のみの単純なデータを使いました。
ですが、これを発展させれば、グラフィックに必要なデータ(形や位置など)を用意して、SVG要素を配置すれば、SVGなグラフィックが作れます。
でも、親子関係のデータでツリーの図を描画する場合、親子関係が分かるように、親子ノードをずらした描画位置も、適切に用意しなければなりません。 このように、うまくグラフィックを書くためには、元データだけでは足りないので、大抵は、データを補う必要があります。
しかし、D3.jsには、便利な、グラフィック用データ生成を支援する機能がたくさんあるので、比較的簡単に、必要なデータが用意できてしまいます。
その機能の1つが、CHAGEでも使っている、ツリーレイアウトを作るd3.tree()
です。 簡単に説明すると、d3.tree()
は、描画範囲と親子関係のデータから、ノードの適切な描画位置を計算して補ってくれます。
ですので、d3.tree()
などの支援機能が補った描画位置にSVG要素を作成すれば、少し楽してグラフィックが描画出来るわけです。
Step.6 DOM要素の値の取得
Step.5までで、基本動作の説明は紹介できたと思いますが、知っていれば、簡単なのに、ぐっと実用的になる部分について、あと2つほど紹介します。
まずは、DOM要素の値を取得する例です。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>D3.js example</title> <script src="lib/d3.v5.min.js"></script> </head> <body> <div id="target"></div> <input id="source" type="text" value="test data"/> <button id="update" type=button onclick="update()">Update</button> </body> <script> function update(){ var target = d3.select("#target"); // 1. var source = d3.select("#source"); // 2. var data = source.property("value"); // 3. target.append("p").text(data); // 4. 5. } </script> </html>
サンプルを実行して、テキスト入力部分を好きな文字列に変更して、Updateボタンを押してみてください。
ユーザの入力結果を反映させる処理ができました。
Updateボタンが押された時に実行されるupdate()
関数では以下のような処理をしています。
d3.select()
でdiv要素を選択d3.select()
でinput要素を選択property()
でinput要素のテキストを取得(dataに代入)append()
でdiv要素にp要素を追加text()
で追加したp要素のテキストをinput要素の値(data)に設定
property()
は、設定値の引数を省略して、パラメータ名の引数のみにすることで、値を取得する動作に変わります。
そして、DOM要素を設定用の他のAPIも、同様に、設定値の引数を省略することで、取得する動作に変わります。
これで、DOM要素の作成変更だけでなく、DOM要素の情報を取得できるようになりました。 この機能を使えば、ユーザのいろいろな入力を動作へ反映できるわけです。
Step.7 DOM要素の値の取得とハンドラの設定
最後は、clickハンドラを設定する例です。
動き自体は、Step.6のコードとほぼ同じで、D3.jsの機能で動的にclickハンドラを設定するように変更しました。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>D3.js example</title> <script src="lib/d3.v5.min.js"></script> </head> <body> <div id="target"></div> <input id="source" type="text" value="test data"/> <button id="update" type=button disabled>Update</button> </body> <script> function update(){ var target = d3.select("#target"); var source = d3.select("#source"); var data = source.property("value"); target.append("p").text(data); } d3.select("#update") // 1. .on("click", update) // 2. .attr("disabled", null); // 3. </script> </html>
update()
関数は、Step.6と同じなので、それ以外のコードのみ解説します。
起動時に、以下の処理を行うことで、button要素のclickでupdate()
関数が呼ばれるように設定しています。
d3.select()
でbutton要素を選択on()
でbutton要素のclickハンドラにupdate()
関数を設定attr()
でbutton要素を有効化
このように、on()
にハンドラ名とハンドラ関数を指定するだけで、ハンドラが設定できます。 もちろん、ハンドラ名を変えれば、click以外の他のハンドラも同様に変更できます。
動的にハンドラを変更できました。これで、Webアプリの状態にあわせて、柔軟なアプリの制御が出来るようになります。
この例では、準備完了まで、ボタンを無効(disabled)にして、安全に準備できたわけです。
紹介のおしまい
ここまで読んでくださって、サンプルコードを理解した皆さんなら、おそらく「D3.jsで、テキストベースのWebアプリなら作れる」と思います。
そして、「D3.jsのサイトから好きなサンプルを探して、好きなデータに差し替えて、試せる」ようになってると思います。
私も、D3.jsをよく知らないまま、D3.jsサイトのサンプルで試行錯誤しているうちに、D3.jsが何となくわかってきて、いつの間にかCHAGE WebUIぐらいはグリグリ動かせるようになっていました。 そこで、この紹介では、私が、サンプルを試す前にわかっていたら楽だったと思う部分を、厳選して紹介してみました。
みなさんも、ぜひ、D3.jsサイトのサンプルを試して、ぜひ、グリグリ動くWebUIの世界を楽しんでください。
Ajax的な動作が簡単に書けるD3.jsのFetchesも紹介しようか、とか直前まで悩んでいたぐらいで、書ききれた気はしてませんが…、D3.jsの紹介は、この程度で終わりにしておきます。