D3.jsのenter()が分からなかったのでコードリーディングしたメモ
前回、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)
- 作者: 森藤大地,あんちべ
- 出版社/メーカー: 技術評論社
- 発売日: 2014/02/20
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る