advent calendar 24日目 selectAll(), data(), enter(), append()に翻弄されるひとたちに

ようやくボクのクリスマスが終わります。

d3は可愛いんですが「selectAll() -> data() -> enter() -> append()」は経験から学ぶのが最も早い、としか答えられません。 クリスマスプレゼントとして、そんな方々のためにUnderstanding selectAll, data, enter, append sequence in D3.jsというエントリが2012/1に出ております。

理解の一助となればと訳しておきました!これほんとに理解できる記事です!


D3.js始めたばっかりの諸君らが、Webで例を見ながら学習するときに、ぶっちゃけ「selectAll(), data(), enter(), append()」という一連の処理はイミフだと思う。そう、これらの関数は、なにをしてるのかわかりづらい。少なくともこれらの機能を理解するのは簡単じゃないと思う。よくわからない諸君らの一助となればと思い例と説明を上げてみることにする。

これらのメソッドが何をしているかを理解するための鍵は、 http://mbostock.github.com/d3/ の[Keys section(今はリンク先には無い)]を見てくれ。

key functionはまた、enterとexitのselectionsを決定する。
古いデータの中に対応するkeyが見当たらない新しいデータはenter selectionとなる。
新しいデータの中に対応するkeyが見当たらなかった古いデータはexit selectionとなる。
どちらにも存在するデータは更新されるselectionとなるのだ。

Mike Bostock氏はこのスレッドの中で言及している。

f:id:muddydixon:20130825191506j:plain

焦らなくていい。あとで挙げる例と説明を見てもらえれば、多分理解できると思う。 以下の3つの例すべてで HTML paragraph element (

)を生成している。 しかし、D3.jsを使って作っているならば、諸君らが生成するいかなる他のelementで会っても同じ原則が適用される。

次の2つの例とその結果を注意深く見て欲しい。そして、例と結果の違いに着目してくれ。


例1

<!DOCTYPE html>
 <html>
 <head>
     <script type="text/javascript"
                src="http://mbostock.github.com/d3/d3.js">
     </script>
     <style type="text/css">
         p {
             font-size: 20px;
             color: orangered;
         }
     </style>
 </head>
 <body>
     <div id="example1"></div>
     <script type="text/javascript">
         pdata = [10,12,6,8,15];
 
         selectDIV = d3.select("#example1");
 
        selectDIV.selectAll("p")
             .data(pdata)
             .enter()
             .append("p")
             .text(function(d){return d;});
     </script>
 </body>
 </html>

結果1

10
12
6
8
15

例2

<!DOCTYPE html>
 <html>
 <head>
     <script type="text/javascript" 
                src="http://mbostock.github.com/d3/d3.js"
     ></script>
     <style type="text/css">
         p {
             font-size: 20px;
             color: orangered;
         }
     </style>
 </head>
 <body>
     <div id="example2">
        <p>Already existing paragraph 1</p>
        <p>Already existing paragraph 2</p>
     </div>
     <script type="text/javascript">
         pdata = [10,12,6,8,15];
 
         selectDIV = d3.select("#example2");
 
        selectDIV.selectAll("p")
             .data(pdata)
             .enter()
             .append("p")
             .text(function(d){return d;});
     </script>
 </body>
 </html>

結果2

Already existing paragraph 1
Already existing paragraph 2
6
8
15

2例のコードの違いは、d3のJavaScriptが呼び出される前に、例2では2つのparagraph element ("Already existing paragraph 1"、"Already existing paragraph 2") の存在だけだ。 そして、与えられたデータの配列では存在しているにもかかわらず、結果2では、10と12は見つけられない。なぜか?これこそが諸君が求めているものだとおもう。

1.

selectDiv.selectAll("p")selectDivの中にある全ての<p>...</p>を返す。 例1では空だし、例2ではすでにある2つのparagraph Elementが返される。

2.

.data(pdata) は2引数を引き受ける 1. data 配列 2. data 配列の要素に関連付けられている key の関数。key - これこそが enter selection を決定する。.enter().append()メソッドで指定したノードにデータ配列の中のどの要素をひもづけるかを決める、と考えればよい。

たぶん「は?key functionとかねーし。あと、keyとかいってるけど、それがどうやってenter selectionを決定してるかわかんねーし」と思うと思う。うん、当たり前だと思う。これについて説明しよう。

D3.jsの創造主がこのポストで説明しているが、key functionが.data()メソッドで明示的に指定されてない場合には、デフォルトの値として、indexが使われるんだ。この場合は、data配列のindexだ。 例えば、例1では key function が指定されていないから、10, 12, 6, 8そして15の key は 0, 1, 2, 3 そして 4となるってことだ。

この key は次のような方法でenter selectionを決定する。data配列(今の場合はpdataだ)の中の要素のkeyは既存のselection(.selectAll("p")メソッドで返されるselectionだ)のkeyと比較される。指定したdata配列の中で既存の要素のkeyと異なるkeyを持つすべての要素が、enter selectionとなるのだ。もし、既存の要素のkeyとすべてかぶった場合にはenter selectionは無い

どちらの例でもkey functionは明示的に指定されていない。つまり、keyはそれぞれの配列のindexとなる。 例1では、.selectAll("p")はからの配列を返すので、対応するkeyも空だ。ところが指定したpdata、10, 12, 6, 8, 15に対応するkeyは0, 1, 2, 3, 4だ。すべてのkeyが異なっているので、pdataのすべての要素がenter selectionになるんだ!

他方で、例2では .selectAll("p")は2つの配列を返し、これらのkeyは0, 1となる。pdataのkeyは例1同様0,1,2,3,4となっている。

10と12は既存のp要素と同じkeyを持っているので、10と12はenter selectionには含まれない!!

3.

.enter().append("p")メソッドはenter selectionの中の要素の数だけ<p>...</p>を生成する。.append()で指定した要素が生成されのだ。

例2で 10と12はenter selectionには入らなかったので、これらに対応するp要素は生成されず、結果として見ることができなかったのだ。


例2と似た例3を作ってみた。違いは、key functionを明示的に指定したことだ。これだけのことで、pdataの要素すべてが結果として現れる。

例3

<!DOCTYPE html>
 <html>
 <head>
     <script type="text/javascript" 
                src="http://mbostock.github.com/d3/d3.js"
     ></script>
     <style type="text/css">
         p {
             font-size: 20px;
             color: orangered;
         }
     </style>
 </head>
 <body>
     <div id="example3">
        <p>Already existing paragraph 1</p>
        <p>Already existing paragraph 2</p>
     </div>
     <script type="text/javascript">
         pdata = [10,12,6,8,15];
 
         selectDIV = d3.select("#example3");
 
        selectDIV.selectAll("p")
             .data(pdata, function(d){return d;})
             .enter()
             .append("p")
             .text(function(d){return d;});
     </script>
 </body>
 </html>

結果3

Already existing paragraph 1
Already existing paragraph 2
10
12
6
8
15

.data()メソッドの2つ目の引数に注目してくれ。key functionとして値自身を返すようにした。 結果として、要素の値自信がkeyとなる。つまり、要素10のkeyは10だし、12のkeyは12、6のkeyは6だ。 既存のselectionの要素のkeyは無い。結果として、例2とちがって例3では、新しいdata要素は既存の要素とは異なるkeyを持つこととなり、enter selectionになり、.enter().append("p")で生成されるんだ。

f:id:muddydixon:20130825200920j:plain