前回、D3.jsでレーダーチャート書いてて、enter() で何が起きてるのかよく分からなかったのでソースコードを読んでみました。そのときのメモです。
長くなりました。一番下の方にまとめもあります。
読んだD3.jsのバージョンは、3.4.2です。
読んでみたらenter()はただのgetterメソッドで特に何の処理もしてませんでした。代わりにdata()ががんばってたんですねー。
ソースコード読んでみて、やっとD3 - セレクションの仕組みに書いてある意味が分かった気がします。
下のような円を描くコードを元にソースコードを読んでみました。
var svg = d3.select('body') .append('svg') .attr('width', w) .attr('height', h); svg.selectAll('path') .data(dataset) .enter() .append("circle") .attr("cx", function(d, i) { return (i * 50) + 25; }) .attr("cy", h/2) .attr("r", function(d) { return d; });
セレクションを取得する部分を読む
まずは、select()が何をしてるか見てみる。
var svg = d3.select('body')
d3.jsの関連してる部分をのぞく。
// d3.js // 9行目 var d3_document = document, d3_documentElement = d3_document.documentElement, d3_window = window; // 38行目 var d3_select = function(s, n) { return n.querySelector(s); }, d3_selectAll = function(s, n) { return n.querySelectorAll(s); }, // 534行目 d3.select = function(node) { var group = [ typeof node === "string" ? d3_select(node, d3_document) : node ]; group.parentNode = d3_documentElement; return d3_selection([ group ]); }; d3.selectAll = function(nodes) { var group = d3_array(typeof nodes === "string" ? d3_selectAll(nodes, d3_document) : nodes); group.parentNode = d3_documentElement; return d3_selection([ group ]); };
まずは、document.querySelectorを使って、selectの引数で指定したDOMのElementオブジェクトの配列を作る。今回の例だとbody要素のElementオブジェクトを保持する配列。その配列をgroup変数に代入して、parentNodeプロパティを設定して、d3_selection([group])で処理してる。d3_selection関数を呼ぶときに、[[element1, element2, ... ]]のような2次元配列を引数にしてるんですね。
d3_selection関数は以下。
// d3.js // 478行目 var d3_subclass = {}.__proto__ ? function(object, prototype) { object.__proto__ = prototype; } : function(object, prototype) { for (var property in prototype) object[property] = prototype[property]; }; function d3_selection(groups) { d3_subclass(groups, d3_selectionPrototype); return groups; }
d3_subclassで、groups、つまりElementオブジェクトの配列を要素に持つ2次元配列([[ element1, element2, ...]])に、d3_selectionPrototypeのプロパティをコピーして返却してる。
そして、d3_selectionPrototypeオブジェクトには以下のようなプロパティが設定されてる。
d3_selectionPrototype.attr = function(name, value) { ... d3_selectionPrototype.append = function(name, value) { ... d3_selectionPrototype.data = function(name, value) { ... // 他にもいろいろ
ふむふむ。
selectでは、[[element1, element2, ...]]というような2次元配列を返してて、この2次元配列にはd3_selectionPrototypeからプロパティがコピーされてる。この拡張された2次元配列を、セレクションって呼んでいるんだな。
そんでもって、セレクションが保持してる[element1, element2, ...]という配列にparentNodeプロパティが追加されたオブジェクトをグループと呼ぶと。
今回の例のd3.select('body')で取得するセレクションは以下のようなイメージ。

一般化すると以下のようなイメージ。たぶん、この記事(Nested Selections)を読むとより理解が深まりそうな気がする。

SVG要素をDOMとして追加する部分を読む
d3.select('body').append('svg')っていう感じでセレクションのappendメソッドを呼んでる。
appendのソースコードを読んだところ、createElementNSでElementオブジェクトを生成してappendChildしてる。appendの返り値はappendChildしたElementオブジェクトを保持するセレクションになる。
ここまでが、以下のコードでbody要素内にsvg要素が生成される部分。
var svg = d3.select('body') .append('svg') .attr('width', w) .attr('height', h);
データバインドしてる部分を読む
これからは、datasetを使って、円を描く部分。
svg.selectAll('path') .data(dataset) .enter() .append("circle") .attr("cx", function(d, i) { return (i * 50) + 25; }) .attr("cy", h/2) .attr("r", function(d) { return d; });
selectAllは、さっきのselect('body')で見たのとほとんど同じで、違うのはquerySelectorではなくquerySelectorAllが使われてる点。つまり、Elementオブジェクト1件ではなく、複数件取得する場合がある。
ただ、svg要素はさっき作ったばかりで、svg要素内は空だから、svg.selectAll('path')はElementオブジェクトを1つも保持していないセレクションを返すことになる。
このセレクションのdataメソッドを呼び出す。dataメソッドは大きめの関数。
// d3.js // 749行目 d3_selectionPrototype.data = function(value, key) { var i = -1, n = this.length, group, node; // 70行くらい var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]); if (typeof value === "function") { while (++i < n) { bind(group = this[i], value.call(group, group.parentNode.__data__, i)); } } else { while (++i < n) { bind(group = this[i], value); } } update.enter = function() { return enter; }; update.exit = function() { return exit; }; return update; }
iとnの値を見ると、bind関数が1回だけ呼ばれそう。bind関数は、dataメソッドの中で定義されてる。
// d3.js // 760行目 function bind(group, groupData) { var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData; if (key) { // 省略 } else { for (i = -1; ++i < n0; ) { node = group[i]; nodeData = groupData[i]; if (node) { node.__data__ = nodeData; updateNodes[i] = node; } else { enterNodes[i] = d3_selection_dataNode(nodeData); } } for (;i < m; ++i) { enterNodes[i] = d3_selection_dataNode(groupData[i]); } for (;i < n; ++i) { exitNodes[i] = group[i]; } } enterNodes.update = updateNodes; enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode; enter.push(enterNodes); update.push(updateNodes); exit.push(exitNodes); } // 831行目 function d3_selection_dataNode(data) { return { __data__: data }; }
bind関数内で、セレクションのElementオブジェクトの数とdatasetの要素数に応じて、updateNodes、enterNodes、exitNodes、それぞれの配列にオブジェクトがpushされる。
pushされるオブジェクトには、__data__プロパティが設定されてる。これがデータバインド。
どういう風にオブジェクトがpushされるかというと、Elementオブジェクトの数のよりdatasetの要素数の方が多いとき、例えばElementオブジェクトの数が3で、datasetの要素数が7のとき
- updateNodesには
- 3つのElementオブジェクトそれぞれの
__data__プロパティに、datasetの最初から3つの値をそれぞれセットしてから、push
- 3つのElementオブジェクトそれぞれの
- enterNodesには
datasetの残りの4つの値を使って、{__data__: data}というオブジェクトを生成し、push
- exitNodes
- なにも
pushしない
- なにも
逆に、Elementオブジェクトの数が7で、datasetの要素数が3のとき
- updateNodesには
- 7つのElementオブジェクトのうち、最初から3つのオブジェクトに
__data__プロパティをセットしてpush
- 7つのElementオブジェクトのうち、最初から3つのオブジェクトに
- enterNodesには
- なにも
pushしない
- なにも
- exitNodes
- 残りの4つのElementオブジェクトを
push
- 残りの4つのElementオブジェクトを
これら3つの配列は、bind関数ではなく、外側のdataメソッドで定義されている3つのセレクション、enter、update、exitにpushされる。
dataメソッドをもう一度見てみると、これらの値がdataメソッドの返り値として使われてることが分かる。
// d3.js // 749行目 d3_selectionPrototype.data = function(value, key) { // 省略 var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]); // 省略 update.enter = function() { return enter; }; update.exit = function() { return exit; }; return update; }
dataメソッドで返ってくるのは、セレクションで、保持してるElementの配列はupdateになっている。さらに、dataメソッドで返ってくるセレクションには、enterとexitのgetterメソッド、enter()とexit()が設定されている。
はぁー、なるほどー。enter()って既存のDOMよりdatasetの数が多い場合に、余分なデータの配列を持つセレクションを返却してるだけなのか。
[[{__data__: 10}, {__data__: 20}, ...]] こんな感じの。
そんでもって、余分なデータの配列はenter()を呼んだ時じゃなくて、data()を呼び出した時点で作られてるんだね。
enter()後の処理を読む
appendではグループが保持するオブジェクトの数だけappendChildしている。enter()は上記の通りenterNodesを保持するセレクションを返すだけ。つまり、enter().append('circle')とするとenterNodesの数だけcircle要素がappendChildされることになる。
まとめ
- セレクションは、Elementの配列の配列である2次元配列に、色々なプロパティを追加して拡張したもの。
- セレクションが保持するElementの配列には
parentNodeプロパティが追加されていて、この配列をグループと呼ぶ。 - セレクションの
dataメソッドは仕事が多い。超ざっくり言うと新たなセレクションupdateを生成し返却する。
なんか理解が少し進んだ気がします!
今日、下の本を読んでたらコラムにdata、enter周りについて書いてあるのを見つけました!
![エンジニアのための データ可視化[実践]入門 ~D3.jsによるWebの可視化 (Software Design plus) エンジニアのための データ可視化[実践]入門 ~D3.jsによるWebの可視化 (Software Design plus)](http://ecx.images-amazon.com/images/I/5163XBvWrkL._SL160_.jpg)
エンジニアのための データ可視化[実践]入門 ~D3.jsによるWebの可視化 (Software Design plus)
- 作者: 森藤大地,あんちべ
- 出版社/メーカー: 技術評論社
- 発売日: 2014/02/20
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る