【関連記事】
React 再入門 (React hook API) - Qiita
D3 v7 入門 - Enter / Update / Exit - Qiita
D3 v7 応用 - Enter / Update / Exit - Qiita
D3 v7 グラフ - d3-scale、d3-axis、d3-shape - Qiita
React+D3 アプリ作成入門 - Qiita
D3 v7 棒グラフのいろいろ - Qiita
D3 v7 都道府県別人口の treemap - Qiita
本記事は D3 v7 入門のための記事です。特に Data Join (Enter / Update / Exit) の基本的なところをまとめました。これはデータセット(JavaScriptの配列)とView要素(DOM 要素)を結びつけるものです。 少し複雑なので、あいまいさを排除するため、重複した説明がいくつかあります。冗長に感じられたらすみません。
開発環境は テキストエディタまたは Visual Studio でコードを書き、以下の Python 標準の Web サーバを使って動作確認しています。
python -m http.server
http://localhost:8000/test1.html
以下、簡単なサンプルコードを示しながら、D3 の基本的な機能を説明していきたいと思います。上から順番に呼んでいただければと思います。
【参考サイト】
D3 Getting started
d3-selection
d3tutorial
1.selection オブジェクト
D3 の基本的なコンセプトは、DOM 要素を selection オブジェクト にバインドして、その selection オブジェクトを通して DOM 属性(attributes)を操作することです。
selection オブジェクト は、 select または selectAll で選択された DOM 要素を表すオブジェクトです。
- D3 の全ての関数は d3 ネームスペース の元で利用できます。
- select は、最も簡単な形式の場合、引数として CSS selector 文字列を要求します。最初にマッチした要素のみを selection として返します。
- それに対して、selectAll はマッチした要素のリストを selection として返します。
- マッチする要素が無い場合は、両方の関数とも dummy selector を返します。エラーは投げません。
- selection は DOM 要素を操作する関数を提供します:attr, style, classed, text, html など。
以下のコードを使って D3 の基本的な概念である selection オブジェクトや Data Join (Enter / Update / Exit) について見ていきたいと思います。
以下、サンプルコード1を修正しながら、selection オブジェクト がどのように変化していくかを確認します。update selection と enter selection のサンプルです。
<!DOCTYPE html>
<html lang="en">
<body>
<h1>D3 Test</h1>
<div>aaa</div>
<div>bbb</div>
</body>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
var dataset = [1, 2, 3, 4, 5, 6];
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.enter()
.append("div")
.style("color", "red")
.text( d => "data = " + d );
console.log(div);
</script>
</html>
1-1.select("body")
var dataset = [1, 2, 3, 4, 5, 6];
var div = d3.select("body");
console.log(div);
d3.select(selector) :
引数の selector 文字列 にマッチした最初の DOM 要素の selection オブジェクト を返します。
マッチするものが無い場合は空の selection を返します。複数マッチすれば、DOM の順番で、最初の selection を返します。引数の selector は文字列以外に document.body のような node への参照を指定することも可能です。
select("body") で body 要素がマッチし div 変数に代入されます。この selection オブジェクトの _groups プロパティ は body 要素となり、_parent プロパティ は html 要素となります。_groups プロパティ が選択されてる DOM 要素 です。
_groups : [ [body] ]
1-2.selectAll("div")
var dataset = [1, 2, 3, 4, 5, 6];
var div = d3.select("body")
.selectAll("div");
console.log(div);
selection.selectAll(selector) :
selection の各々の要素に対して、引数の selector string にマッチする子孫要素を選択します。
ここでは selectAll("div") は body 要素の子孫 div 要素を選択することになります。つまり 返される selection オブジェクトの _groups プロパティは 2 つの div 要素となり、_parent プロパティは body 要素となります。
_groups : [ [div div] ]
2. Data Join - Enter / Update / Exit 概説
D3 の Data Join とは、データと DOM 要素 (View) を結びつける機能 のことです。Data Join は selection.data(data, key) と後ろに続く一連のメッソドで実現されます。データ(配列やオブジェクト)に対応する DOM 要素を動的に生成・更新・削除することができます。
- Data Join で使われる selection オブジェクトのプロパティ
selection オブジェクトのプロパティ
D3 の selection オブジェクト は、DOM 要素を選択するためのメソッドを提供します。selection オブジェクト のプロパティ _group, _enter, _exit は、データと DOM 要素の結合に関する情報を保持しています。
-
enter selection (_enter プロパティ) : selection オブジェクト がデータと DOM 要素を結合したときに、データが多くて DOM 要素が足りない場合に、新しく作られる DOM 要素のプレースホルダーを表します。
例えば、d3.selectAll(“p”).data([1, 2, 3, 4]) とすると、p 要素と [1, 2, 3, 4] のデータを結合しますが、もし p 要素が3つしかない場合は、データの 4 に対応する要素が足りません。このとき、_enter プロパティ は、データの 4 に対応する要素のプレースホルダーです。このプレースホルダーに対して、append メソッド などを使って、新 DOM 要素を追加できます。 -
update selection (_groups プロパティ) : selection オブジェクト が選択した DOM 要素のグループを表す配列で、この DOM 要素に対して様々な更新を行います。 グループは、親要素によって区切られた DOM 要素の集合です。
例えば、d3.selectAll(“div”).selectAll(“p”) とすると、各div 要素の中の p 要素を選択しますが、このとき、div 要素ごとにグループが作られます。_group プロパティ は、この p 要素グループの配列です。 -
exit selection (_exit プロパティ) : selection オブジェクト がデータと DOM 要素を結合したときに、要素が多くてデータが足りない場合に、データが割り付けられない、削除されるべき DOM 要素を表すものです。
例えば、d3.selectAll(“p”).data([1, 2]) とすると、p 要素と [1, 2] のデータを結合しますが、もし p 要素が 4 つある場合は、データの [1, 2] に対応しない DOM 要素が2つあります。このとき、_exit プロパティ は、対応するデータがない要素の配列です。この selection オブジェクト に対して、exit メソッドと remove メソッド を使って、DOM 要素を削除することができます。
以下、enter, update, exit のイメージです。集合が分かりづらかったらスキップしてください。
enter, update, exit が表すもの
- enter selection: 新しくデータセットに追加され、まだ描かれていないもの
- update selection: 現在のデータセットで既に描かれているもの
- exit selection: 既に描かれているが、データセットから削除されているもの。
集合の言葉で書けば、以下のような感じ。
\displaylines{
update = DATA \cap DOM (共通部分)\\
enter = DATA \setminus DOM (差集合)\\
exit = DOM \setminus DATA (差集合)\\
\\
ここで以下のように定義する。\\
DATA \equiv データ集合、DOM \equiv DOM 要素集合
}
以下、selection オブジェクトの、enter selection と update selection、exit selection のより具体的なイメージです。
update selection と enter selection (div 要素数 = 2 、データ数 = 6 の場合)
DOM 要素 div div - - - -
データ 1 2 3 4 5 6
_groups _enter
update selection と exit selection (div 要素数 = 6 、データ数 = 4 の場合)
DOM 要素 div div div div div div
データ 1 2 3 4 - -
_groups _exit
3.Data Join メソッドの基本的な使い方
Data Join のコンセプトを実現するためには、以下のような2通りのメソッドを利用する方法があります。
- (1)selection.enter, selection.exit, selection.append, selection.remove
- (2)selection.join(enter, update, exit)
まずは、(1)のメソッドの使い方を、その後で(2)を見ていきたいと思います。
selection オブジェクトのメソッドの使い方
選択 DOM 要素は _groups で、メソッドは _groups に作用します。
enter メソッドは _enter を _groups に移動し、exit メソッドは _exit を _groups に移動します。つまり以下のようなことが言えます。
- selection.op1 : updateに関数op1を適用します。
- selection.enter().op2: enterに関数op2を適用します
- selection.exit().op3: exitに関数op3を適用します。
op1やop2,op3 は D3 の slection メソッドで、一般にはチェーン化されています。
Data Join - Enter / Update / Exit の流れ
Data Join の基本的な使い方は、以下のようになります。
-
1.d3.select() や d3.selectAll() などで、既存の DOM 要素 や 新規に作成する DOM 要素の親要素 を選択します。_groups プロパティ が作成されます。
-
2.selection.data() で、データを選択した要素に結びつけます。_enter プロパティ と _exit プロパティ が作成されます。
_enter プロパティ は、データが多過ぎて、DOM 要素とバインドできなかったデータのプレースホルダーで、_exit プロパティ はデータが少な過ぎて、データがバインドできなかった DOM 要素の集まりとなります。 -
3、[update] selection.style() で、_groups プロパティはデータがバインドされている DOM 要素を表しますが、これに対して、attr() や style() などで、要素の属性やスタイルを更新することができます。=>「(Ⅰ)update selection の DOM 更新 - style & text」
-
4.[enter] selection.enter() で、_enter プロパティが _groups プロパティ となり、これが仮の DOM 要素(プレースホルダー)となります。このプレースホルダーに対して、将来的に append() や insert() などで、実際の DOM 要素を追加します。=> 「(Ⅱ)enter selection の DOM 更新 - enter」
-
5.[exit] selection.exit() で、_exit プロパティ が _groups プロパティになります。この DOM 要素に対して、remove() で、要素を削除します。=> 「(Ⅲ)exit selection の DOM 更新 - exit」
3ー1.update selection と enter selection
3-1ー1.data(dataset)
var dataset = [1, 2, 3, 4, 5, 6];
var div = d3.select("body")
.selectAll("div")
.data(dataset);
console.log(div);
selection.data(data, key) :
選択された DOM 要素 (selection オブジェクト) に data 配列をバインドし、新しく更新された selection オブジェクト を返しします。
返された selection オブジェクト では update selection でデータがバインドされた DOM 要素を表し、データ数と DOM 要素数の大小に応じて enter selection と exit selection のプロパティが作られます。前者は DOM 要素を追加、後者は DOM 要素を削除するために使われます。
指定される data は任意の値(数値やオブジェクトなど)の配列です。もしくは データを返す関数 でもいいです。データが割り当てられれば _data_ プロパティに蓄えられます。
key 関数が指定されなければ、dataの中の最初のデータが最初の DOM 要素に割り当てられます。そして2番目のデータが2番目の DOM 要素に割り当てられます。3番目以降も同様です。
key 関数はオプションで、ここでは使わないので説明を省略します
data メッソドで update と enter selection にデータが割り付けられます。
- update selection (_groups プロパティ) の div 要素に、_data_: 1, _data_: 2 が割り付けられている。_parents が body なので、カレント位置は body の下の2つの div 要素を示しています。
- enter selection (_enter プロパティ) が作成され、残りの _data_: 3, . . . _data_: 6 が割り付けられていますが、これはまだ DOM 要素が割り付けられてません。これは将来的に enter メソッド適用することで、_groups プロパティに移動してから、append や insert で DOM 要素を割り当てることが可能となります。
DOM 要素 div div
データ 1 2 3 4 5 6
_groups _enter
_groups : [ [div div] ]
_enter : [ [m m m m] ]
ちなみに現時点での出力 HTML は以下の通りです。これは d3 コードが適用される前と変わっていません。_groups プロパティの2つの div 要素が表示されています。
3-1-2.(Ⅰ)update selection の DOM 更新 - style & text
(Ⅰ)update selection の DOM 更新
data メッソド で作成された _groups プロパティの DOM 要素は、style や text メッソドなどで更新できます。
var dataset = [1, 2, 3, 4, 5, 6];
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.style("color", "red")
.text( d => "data = " + d );
console.log(div);
selection.style(name, value, priority)
selection オブジェクトの DOM 要素(_groups) の style を設定します。name プロパティの値を value にします。priority はオプションです。
selection.text(value)
selection オブジェクトの全 DOM 要素に対して引数の value をテキストコンテントとして設定します。全 DOM 要素は同じテキストコンテントを持つことになります。子要素がある場合もこのテキストコンテントに置き換えられてしまいます。
もし、引数の value に関数 function( d, i ) {...} が指定されたとしましょう。この時、d は _groupe プロパティの要素にバインドされている _data_ の値が順番に渡され、 i はその index が渡されます。最後にその DOM 要素のテキストコンテントとしたいものを返すようにします。コンテントをクリアしたい場合は null を返します。
style & text は _groups プロパティの div 要素に対して作用します。
_enter プロパティはそのまま変化しません。
style & text は _groups プロパティの div 要素に作用しています。これは body 要素の下であり、その結果の出力 HTML は以下のようになります。
3-1-3.(Ⅱ)enter selection の DOM 更新 - enter
(Ⅱ)enter selection の DOM 更新
data メッソド で作成された selection オブジェクト で、enter selection を更新するためには、まず enter メッソドで _enter プロパティ を _groups プロパティに移してから、append 等で必要な DOM 要素を紐づけてた後に DOM 更新を行うことになります。
var dataset = [1, 2, 3, 4, 5, 6];
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.enter();
console.log(div);
selection.enter()
selection オブジェクトの現在選択されている DOM 要素 (_groups プロパティ)を、DOM 要素への対応付けから溢れたデータのプレースホルダ(_enterプロパティ)で、置き換え返します。
_enter プロパティが消えて、新しい _groups プロパティになります。しかしこの時点ではまだ _groups プロパティの要素は DOM 要素とは紐づいていません。前ステップにおける、2つの div 要素は _groups プロパティからは消えていることに注意してください。
DOM 要素 - - - -
データ 3 4 5 6
_groups
_groups : [ [m m m m] ]
3-1-4.append("div")
_enter プロパティ から移動された 新 _groups pプロパティのデータに DOM 要素を紐づけます。
var dataset = [1, 2, 3, 4, 5, 6];
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.enter()
.append("div");
console.log(div);
selection.append(type)
引数の type が文字列(タグネーム)の場合は、selection オブジェクトの各々の要素の後ろに、このタグネームの DOM 要素を新しい子要素として追加します。
もしくはこれが enter selection である場合は update selection の次の兄弟要素の前に追加されます。
最後にそのように更新された新オブジェクトを返します。
後者の場合の enter selection の取扱いによって、新しくバウンドされたデータに関して順番を保持したまま、新要素を DOM に追加できるようになります。
しかし更新中の要素が順番を変える時は、selection.order が必要となることに注意してください。この場合新しいデータは古いデータとの整合性が無くなります。
もし type が関数であった場合は、また別の使い方になりますが、ここでは深入りはしません。
append("div") は新しい _groups プロパティの要素に、新たに子要素として div 要素を追加します。この場合、もともとの _groups は DOM 要素ではないので、単に div 要素の追加の動作となります。
DOM 要素 div div div div
データ 3 4 5 6
_groups
_groups : [ [div div div div] ]
3-1-5.style & text
style & text は 新 _groups に対して作用します。もともとの update selection の先頭の2つの div 要素はそのまま変更されないことに注意してください。
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.enter()
.append("div")
.style("color", "red")
.text( d => "data = " + d );
console.log(div);
style & text は _groupps の div要素に作用し、その結果、出力 HTML は以下のようになります。
3-2.update selection と exit selection
以下、サンプルコード2を修正していきます。exit selection のためのサンプルです。サンプルコードは最初の div 要素が増え、dataset が小さくなっています。
<!DOCTYPE html>
<html lang="en">
<body>
<h1>D3 Test</h1>
<div>aaa</div>
<div>bbb</div>
<div>ccc</div>
<div>ddd</div>
<div>eee</div>
<div>fff</div>
</body>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
var dataset = [1, 2, 3, 4];
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.exit()
.remove();
console.log(div);
</script>
</html>
3-2-1.data(dataset)
var dataset = [1, 2, 3, 4];
var div = d3.select("body")
.selectAll("div")
.data(dataset);
console.log(div);
data メッソドで update と exit selection にデータが割り付けられます。
- update selection (_groups プロパティ) の div 要素に、_data_: 1, _data_: 2, _data_: 3, _data_: 4 が割り付けられている。_parents が body なので、カレント位置は body の下の2つの div 要素を示しています。
- exit selection (_exit プロパティ) が作成され、割り付けるべきデータが不足している2つの div 要素(eee,fff) がはいります。これは将来的に remove メソッド適用されれば削除されます。
DOM 要素 div div div div div div
データ 1 2 3 4 - -
_groups _exit
_groups : [ [div div div div] ]
_exit : [ [div div] ]
3-2ー2.(Ⅲ)exit selection の DOM 更新 - exit
(Ⅲ)exit selection の DOM 更新
data メッソド で作成された selection オブジェクト で、exit selection の DOM 要素を削除するためには、まず exit メッソドで _exit プロパティ を _groups プロパティに移してから、remove で DOM 要素を削除します。
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.exit();
console.log(div);
_exit プロパティが消えて、新しい _groups プロパティになります。
DOM 要素 div div
データ - -
_groups
_groups : [ [div div] ]
3-2-3.style
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.exit()
.style("color", "red");
console.log(div);
新 _groups の DOM 要素に対して style を適用します。旧 _groups の DOM 要素は変更されません。
3-2-4.remove
var div = d3.select("body")
.selectAll("div")
.data(dataset)
.exit()
.remove();
console.log(div);
selection.remove()
DOM から、選択された要素(_groups の要素)を削除し、削除された要素を選択要素とする selection オブジェクトを返します。この選択要素は DOM から既に削除されていることに注意してください。現在、削除された要素を元に戻すような専用の API は存在しませんが、selection.append や selection.insert を使ってそのような機能を実現することは可能です。
4.selection.join(enter, update, exit)
Data Join のコンセプトを実装するためには、以下のような2通りのメソッドを利用する方法があります。
- (1)selection.enter, selection.exit, selection.append, selection.remove
- (2)selection.join(enter, update, exit)
これまで(1)の方法を見てきたので、ここでは(2)の方法を見ていきます。
4-1.join のフルバージョン
以下、サンプルコード3を修正していきます。selection.join(enter, update, exit) は selection オブジェクトの update selection や enter selection、exit selection の操作を代替して簡単にしてくれるものです。
<!DOCTYPE html>
<html lang="en">
<body>
<h1>D3 Test</h1>
<svg width="300" height="300"></svg>
</body>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const data = [1, 2, 3];
const circles = d3
.select("svg")
.selectAll("circle")
.data(data)
.join(
(enter) => enter.append("circle"),
(update) => update,
(exit) => exit.remove()
);
circles.attr("r", 10);
circles.attr("cx", (d, i) => d * 10);
circles.attr("cy", (d, i) => i * 50);
</script>
</html>
4-1ー1.data(data)
const data = [1, 2, 3];
const circles = d3
.select("svg")
.selectAll("circle")
.data(data);
console.log(circles);
data メッソドで enter selection である _enter プロパティにデータが割り付けられます。_groups プロパティや _exit プロパティは空です。
-
enter selection (_enter プロパティ) は、3つのプレイスホルダーであり、それぞれ以下のデータが割り当てられています。
_data_: 1
_data_: 2
_data_: 3 -
update selection (_groups プロパティ) は空です。
-
exit selection (_exit プロパティ) は空です。
_groups : [ [] ]
_enter : [ [m m m] ]
4-1-2.join(enter, update, exit)
const data = [1, 2, 3];
const circles = d3
.select("svg")
.selectAll("circle")
.data(data)
.join(
(enter) => enter.append("circle"),
(update) => update,
(exit) => exit.remove()
);
circles.attr("r", 10);
circles.attr("cx", (d, i) => d * 10);
circles.attr("cy", (d, i) => i * 50);
console.log(circles);
selection.join(enter, update, exit)
data メソッドでバインドされたデータを基に、_enter プロパティや _exit プロパティを適切に処理し、DOM 要素の追加・削除・並べ替え(append,remove,reorders)を行います。最後に enter selection と update selection をマージして返します。
このメッソドは、selection.enter, selection.exit, selection.append, selection.remove, や selection.order の代替手段となる便利なものです,
引数の enter, update, exit は関数ですが、一つの文字列のみを指定する join("circle") などの省略形もあります。この場合以下の意味になります。
- enter: election.enter() と selection.append("circle") を連続して適用した効果を持ちます。
- update: identity 関数が指定されたと解釈されます。
- exit: selection.remove を呼ぶ動作になります。
_groups : [ [circle circle circle] ]
出力HTML
4-2.join 省略バージョン
前述したように、サンプルコードの「オリジナル」部分を下の「省略形」に置き換えることが可能です
.join(
(enter) => enter.append("circle"),
(update) => update,
(exit) => exit.remove()
);
.join("circle")
4-3.selection (enter, update, exit) 毎の更新
enter, update と exit の関数を別々に分けることで、それぞれの selection を自由にコントロールできるようになります。そして selection.data を使うことで、最適なパフォーマンスで DOM 要素の変更を最小化できます。
以下、サンプルコード4を修正していきます。update selection が空にならないように、最初から circle 要素が1個あるサンプルコードを作成します。update selection は赤色、enter selection は緑色にします。
<!DOCTYPE html>
<html lang="en">
<body>
<h1>D3 Test</h1>
<svg width="300" height="300">
<circle></circle>
</svg>
</body>
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
const data = [1, 2, 3];
const circles = d3
.select("svg")
.selectAll("circle")
.data(data)
.join(
(enter) => enter.append("circle").attr("fill", "green"),
(update) => update.attr("fill", "red"),
(exit) => exit.remove()
);
circles.attr("stroke", "black");
circles.attr("r", 10);
circles.attr("cx", (d, i) => d * 10);
circles.attr("cy", (d, i) => i * 50);
console.log(circles);
</script>
</html>
出力HTML
4-3-1. data(data)
const data = [1, 2, 3];
const circles = d3
.select("svg")
.selectAll("circle")
.data(data);
console.log(circles);
data メッソドで まず update selection (_groups プロパティ) とenter selection (_enter プロパティ) にデータが割り付けられます。 exit selection (_exit プロパティ) は空です。
-
enter selection (_enter プロパティ) は、2つのプレイスホルダーであり、それぞれ以下のデータが割り当てられています。
_data_: 2
_data_: 3 -
update selection (_groups プロパティ) は最初の circle 要素が対応し、以下のデータが割り当てられます。
_data_: 1 -
exit selection (_exit プロパティ) は空です。
_groups : [ [circle] ]
_enter : [ [m m] ]
4-3-2.join(enter, update, exit)
const data = [1, 2, 3];
const circles = d3
.select("svg")
.selectAll("circle")
.data(data)
.join(
(enter) => enter.append("circle").attr("fill", "green"),
(update) => update.attr("fill", "red"),
(exit) => exit.remove()
);
circles.attr("stroke", "black");
circles.attr("r", 10);
circles.attr("cx", (d, i) => d * 10);
circles.attr("cy", (d, i) => i * 50);
console.log(circles);
update selection と enter selection がマージされているのが確認できます。 _enter プロパティが無くなり、_groups プロパティにマージされています。
_groups : [ [circle circle circle] ]
マージされる前に、update selection は red に、enter selection は green に塗りつぶされていることに注意してください。つまり、まずは join(enter, update, exit) の enter 関数と update 関数が実行してから、その後にその結果をマージしています。
今回は以上です
///