D3.js https://d3js.org/ の勉強


Javascript で Web に動的なチャートを表示したいと色々ライブラリを探したが、簡便さとカスタマイズ性の両方を兼ね備えたライブラリが見つからず D3.js に辿り着いた。D3 というのは jQuery のように DOM を操作する事でデータをグラフィカルに Web 上に表示するライブラリで、まったく簡便では無いが便利な機能が沢山詰まっていてチャート表示に関係する作業が色々捗るという物だ。

特に DOM 要素とデータを組み合わせて動的に更新してゆく select という操作が D3 の特徴だ。select は一見 jQuery にある DOM の便利選択機能と同じ物だが、そこから発展して、できるだけ既存の DOM を変更しないで新しい data に応じて画面を更新する機能になっている。いわば select とは React.js などで流行りの仮想 DOM と同じ効果を明示的に行う機能だ。

ここでは、D3.js の冒頭の説明に従って機能を見てゆく。

Select の基本

例として次のような HTML を作る。

<div>
    <div class="num">One</div>
    <div class="num">Two</div>
    <div class="num">Three</div>
    <div class="num">Four</div>
</div>

<script src="https://d3js.org/d3.v5.min.js"></script>

select を使ってマッチした先頭の DOM 要素を選択してスタイルを設定する。

d3.select(".num")
  .style("color", "red");

selectAll でマッチした全部の DOM 要素を選択してスタイルを設定する。

d3.selectAll(".num")
  .style("font-size", "2em");

append() で要素を追加する事も出来る。

d3.selectAll(".num")
  .append("span")
  .text(" apple")

style 第二引数に固定値ではなく関数オブジェクトを使って動的にスタイルを設定する。関数オブジェクトの引数には data (後述) と index (マッチした要素の添字) が渡される。

d3.selectAll(".num")
  .style("background-color", (data, index) => `hsl(${30 * index}, 50%, 90%)`);

data() を使ってマッチした各要素に data を割り当てる。割り当てたデータは style 等に渡される関数の最初の引数として渡される。
DOM 要素の数と data の数が対応している部分を update selection と呼ぶ。

d3.selectAll(".num")
  .data([1, 2, 3, 4])
  .style("margin-left", (data, index) => `${data}em`);

Enter selection と Exit selection

D3 の最重要概念である Update, Enter, Exit については Thinking with Joins が参考になる。まず次の HTML で enter selection について調べる。

<div class="fruitlist">
    <div class="fruit">Apple</div>
    <div class="fruit">Banana</div>
    <div class="fruit">Cherry</div>
</div>

data() で data を割り当てた後、DOM 要素より data の数が多い時、data の数が多い部分を enter selection と呼ぶ。
enter() で enter selection に要素を追加出来る。
append() でデータを追加するには selection.selectAll() を使って親要素から選択する必要が有る。https://github.com/d3/d3-selection#selection_selectAll

d3.select(".fruitlist")
  .selectAll(".fruit")
  .data([1, 2, 3, 4, 5])
  .enter()
  .append("div")
  .text(d => `Extra fruit number ${d}`)

次に 次の HTML で Exit selection を調べる。

<div>
    <div class="lunch">Burger</div>
    <div class="lunch">Hotdog</div>
    <div class="lunch">Curry</div>
    <div class="lunch">Pasta</div>
    <div class="lunch">Pizza</div>
</div>

DOM 要素より data の数が少ない時、data の数が少ない部分を exit selection と呼ぶ。exit() で exit selection を削除出来る。

d3.selectAll(".lunch")
  .data([1, 2, 3])
  .exit()
  .remove()

Update, Enter, Exit を使って必要な時だけ DOM を更新する。

D3 のチュートリアルを読むと存在しない要素を selectAll() してから enter() して append() するというパターンがいきなり出てきて面食らう。存在しない要素を selectAll() する理由は、データ更新時に前回の DOM 要素を再利用するためだ。つまり、最初は存在しない要素を selectAll() するが二度目には存在している。このパターンを使ってみる。

<style>
 .chart div {
     background-color: powderblue;
     text-align: right;
     margin: 1px;
     transition: width 0.5s ease-in-out;
     animation: fade-in 0.5s ease 0s 1 normal;
 }
 @keyframes fade-in {
     0% { opacity: 0 }
     100% {opacity: 1 }
 }
</style>
<div class="chart"/>

この例では div 要素は最初空だ。データの更新がわかりやすいように簡単なアニメーションを付けている。以下のコードでランダムにデータを更新する。

 function barchart(data) {
     const chart = d3.select(".chart")
                     .selectAll("div")
                     .data(data)
                     .style("width", function(d) { return d + "px"; })
                     .text( d => d);

     chart.enter()
                   .append("div")
                   .style("width", function(d) { return d + "px"; })
                   .text( d => d)

     chart.exit().remove();
 }

 function updatebar() {
     ndata = Math.floor(Math.random() * 5) + 5; // [5, 10)
     data = d3.range(0, ndata).map(() => Math.floor(Math.random() * 300));
     barchart(data);
 }

 setInterval(updatebar, 1000);

gist: https://gist.github.com/propella/53a8cf3d450c034199dab0dd9f888a0e