CHAGEの裏側: D3.jsの紹介

2019年09月27日 金曜日

「CHAGEの裏側: D3.jsの紹介」のイメージ

CHAGE開発者のヒラマツです。

CHAGEの紹介(IIJ内製調査システム CHAGE のご紹介)と並行して、CHAGEの中が気になる人に、CHAGEで使われている技術をちょっと濃い目に解説するという企画です。
今回は、以下のサイトのD3.jsというJavaScriptライブラリの紹介です。

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つ選んで、子要素の追加と設定を行う例です。

Step.1デモ

このサンプルを開くと、 「Hello D3.js」のテキストが追加されて、表示されます。 (すぐに処理が終わるので、初めから存在してたように見えますが)

このコードは、以下のことをやっています。

  1. d3.select()でdivタグ(id=”target”をセレクタで指定)のDOM要素を選択
  2. append()でpタグの要素を追加
  3. text()で追加した要素のテキストを設定

d3.select()に指定するパラメータが#targetがCSSのセレクタなので、D3.jsは、CSSのセレクタの知識が必須になってます。
こんな簡単なコードで、DOM要素を追加できました。 もちろん、同様な処理を繰り返せば、複雑な構造も作れます。

Step.2 clickハンドラで、子要素選択と色変更

Step.1とは違い、今度は、要素を追加しません。
ボタンclick時に、 HTMLで記述済みのp要素を選択して、書き換えを行います。

Step.2デモ

Changeボタンをクリックすると、Helloで始まるテキストの色が赤になります。
動的に変わることを体感するために、ボタンのクリックでの動作にしてみました。

ボタンのクリックで呼ばれるchange()関数では、以下のことをやっています。

  1. d3.select()でdiv要素は、選択済み。
  2. select()で子要素のp要素を選択("p"も、やっぱりCSSのセレクタ)。
  3. 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つではなく、複数の要素を一度に変更する例です。

Step.3デモ

ボタンのクリックで呼ばれるchange()で、以下のことをやっています。

  1. d3.select()でdiv要素は、選択済み。
  2. selectAll()でdiv要素の全p要素を選択("p"は、やはりCSSのセレクタ)。
  3. style()で全p要素のcolorスタイルをred(赤)に変更。

これで、ループなしに、複数要素を一度に変更できました。 selectAll()の引数はCSSのセレクタなので、柔軟にDOM要素を選んで、まとめて変更が可能です。

Step.4 データに合わせて複数要素の追加

配列データから、複数のDOM要素を作成する例です。
データを使うので、やっと、D3.jsらしくなってきました。

Step.4デモ

dataの配列データを使って、以下のような処理でp要素を追加しています。

  1. d3.select()でdiv要素を選択
  2. selectAll()でdiv要素内の全p要素を選択
  3. data()でp要素にdata(配列)のデータを紐付ける
  4. enter()で不足分のp要素を準備(何もないので、データ全部を不足分として準備)
  5. append()で不足分のp要素を追加
  6. text()でp要素のテキストをdataの値に合わせて設定
  7. 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 複数要素とデータの連携

配列データを使って、表示を動的に更新する例です。
ボタンをクリックすると、データに合わせて、数も表示内容も変更されます。

Step.5デモ

実際のDOM更新処理をしているupdate()関数では以下のような処理をしています。

  1. d3.select()で、div要素を選択
  2. selectAll()で、div要素内の全p要素を選択
  3. data()で、p要素にdata(配列)を紐付ける(結果は、elementsに保存)
  4. exit()で、余計な要素を選択(要素は減ってないので、この例では無選択)
  5. remove()で、余計な要素を削除(この例では、何も消えない)
  6. enter()で、不足分のp要素を選択
  7. append()で、不足分のp要素を追加
  8. merge()で、既存分と新規加分の要素を結合して、全要素を作成
  9. text()で、全要素にp要素のテキストをdataの値に合わせて設定
  10. 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要素の値を取得する例です。

Step.6デモ

サンプルを実行して、テキスト入力部分を好きな文字列に変更して、Updateボタンを押してみてください。
ユーザの入力結果を反映させる処理ができました。

Updateボタンが押された時に実行されるupdate()関数では以下のような処理をしています。

  1. d3.select()でdiv要素を選択
  2. d3.select()でinput要素を選択
  3. property()でinput要素のテキストを取得(dataに代入)
  4. append()でdiv要素にp要素を追加
  5. text()で追加したp要素のテキストをinput要素の値(data)に設定

property()は、設定値の引数を省略して、パラメータ名の引数のみにすることで、値を取得する動作に変わります。
そして、DOM要素を設定用の他のAPIも、同様に、設定値の引数を省略することで、取得する動作に変わります。

これで、DOM要素の作成変更だけでなく、DOM要素の情報を取得できるようになりました。 この機能を使えば、ユーザのいろいろな入力を動作へ反映できるわけです。

Step.7 DOM要素の値の取得とハンドラの設定

最後は、clickハンドラを設定する例です。
動き自体は、Step.6のコードとほぼ同じで、D3.jsの機能で動的にclickハンドラを設定するように変更しました。

Step.7デモ

update()関数は、Step.6と同じなので、それ以外のコードのみ解説します。
起動時に、以下の処理を行うことで、button要素のclickでupdate()関数が呼ばれるように設定しています。

  1. d3.select()でbutton要素を選択
  2. on()でbutton要素のclickハンドラにupdate()関数を設定
  3. 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の紹介は、この程度で終わりにしておきます。

ヒラマツ

2019年09月27日 金曜日

セキュリティ本部 セキュリティ情報統括室に所属 システム開発者。2000年問題で「2038年問題は定年で対応しなくていい!」とフラグを...。

Related
関連記事