CHAGEの裏側: D3.jsの紹介

2019年09月27日 金曜日


【この記事を書いた人】
ヒラマツ

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

「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デモ

<!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」のテキストが追加されて、表示されます。 (すぐに処理が終わるので、初めから存在してたように見えますが)

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

  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デモ

<!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()関数では、以下のことをやっています。

  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デモ

<!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()で、以下のことをやっています。

  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デモ

<!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要素を追加しています。

  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デモ

<!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()関数では以下のような処理をしています。

  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デモ

<!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()関数では以下のような処理をしています。

  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デモ

<!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()関数が呼ばれるように設定しています。

  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
関連記事