D3.jsとは、データを視覚化するためのドメイン固有言語である。JSライブラリだと思って取り組むと、足元を掬われて後頭部を強打し、もがき苦しむ羽目になる。
D3はData-Driven Documentsの略で、日本語ではデータ駆動文書となる。
D3.jsの概要
このライブラリは単にデータを視覚化するのではなく、データに命を吹き込み、文書化することを目指している。それを象徴するのが、標準のDOM APIをラップした、D3.js独自のDOM操作メソッド群である。
データを軸にHTML文書を丸ごと扱ってやろうという強固で誇り高い意志が伺える。
できること
D3.jsの守備範囲はとても広いため、一度に理解しようとすると大変である。懸命にドキュメントを読んでも、目が滑ることおびただしい。大枠を掴むために、まずはD3.jsで達成できることをまとめてみる。
できること | 概要 |
---|---|
DOM操作全般 | D3.jsは、データ駆動と呼ぶに相応しい、独自のDOM操作メソッドを用意している。ブラウザ標準のDOM操作と大きく異なるため、まずはD3.js独自のDOM操作に慣れることが大切である。 |
ファイルの読み込み | データ駆動というだけあって、データを読み込むためのメソッドも網羅されている。CSVやJSON、GeoJSON、TopoJSON、XMLにHTMLなど、テキストデータであればおおよそ読み込める。もちろんテキストファイルも読み込める。 |
データの視覚化 | D3.jsでは、SVG、HTML(tableやcanvasなど)を使ってデータを柔軟に表示できる。ただ柔軟であるぶん、手仕事でのDOM操作が必要。メソッド一発でいい感じにグラフ化などはできない。少なくとも標準ではサポートされていない。 |
対話的なアニメーション | データの視覚化と地続きである。D3.jsは、凝ったことをやろうと思えばいくらでもできる。ゼロからの自作は手間が掛かるが、Observable(データ視覚化作品を共有するプラットフォーム)などに膨大な参考例がまとめられている。勉強が捗る環境はある。 |
できないこと
D3.jsには、ブラウザ標準のDOM APIを置き換える勢いで豊富なメソッドが用意されている。そのため、データ視覚化の観点で言えばできないことはほぼない。
ただし、手間なくいい感じのグラフを出力したい、という需要には適さない。D3.jsのDOM操作はクセが強く、サンプルをコピペし、修正して使うのも楽ではない。
また、DOM操作に加えて、基本的なSVGの知識も求められる。SVGをゴリゴリ書くよりよほど楽だが、D3.jsをデータを手軽にグラフ化できるライブラリ、と評価できる人は多くないと想像する。
アメリカのジャーナリスト、アマンダ・コックスさんは、D3.jsに関して以下のような言葉を残している。
Use D3 if you think it’s perfectly normal to write a hundred lines of code for a bar chart.
(訳:棒グラフのために100行のコードを書くことが完璧に普通のことだと思うなら、D3をお使いなさい)
D3.jsの大まかな使い方
D3.jsのDOM操作は、宣言的なアプローチをとる。長々しく命令を書く代わりに、何のデータをどうしたいかを端的に書く。
以下のようなイメージである。
端的すぎて、大部分が意味不明だ。ただ、何やら要素や要素の集合を選択して、データと対応させたり、要素を追加したりしていることは伝わるかと思う。
D3.jsでは、要素(の集合)とデータをSelection
オブジェクトというオブジェクトにラップして操作する。このSelectionは、Web APIのSelectionとは全くの別物である。
D3.jsのSelection
オブジェクトには、DOM操作用のメソッドとデータ操作用のメソッドが豊富に備わっている。これら2系統のメソッドをいい感じに繋いで望む結果を得るのが、D3.jsの大まかな使い方である。
基本はDOM操作
D3.jsによるグラフの描画は、基本的にSVGで行う。グラフ本体も軸もテキストもぜんぶSVGで描く。したがってSVGの知識が不可欠である。
グラフをSVGで地道にマークアップするのは絶望的な作業だ。D3.jsはこの作業を楽にする。例えば棒グラフは、以下のようなコードで書ける。
これは、標準のSVG要素や属性値を、ほぼそのままD3.jsに置き換えたコードである。各メソッドの引数を見れば、どのメソッドが何をしているか類推できる。
ただ、これだけでは、簡単なグラフを作るのも一苦労である。
例えば棒グラフに軸やラベルを追加することを考える。まず<line>
要素やら<text>
要素やらを作らねばならない。描画エリアのサイズを踏まえて、各要素のサイズや表示位置を計算する必要もある。
叫び出したくなる。
こうした普遍的な視覚化要素を柔軟に出力できる仕組みが、D3.jsには備わっている。ただ、それでも数が多くて一度に扱うのが難しいので、ここでは取り上げない。
DOMをラップするSelection
D3.jsでのDOM操作は、Selection
オブジェクトを通して行う。
Selection
オブジェクトは、d3.select()
あるいはd3.selectAll()
で作成する。d3
オブジェクトはSelection
オブジェクトではないため、データ視覚化の下準備として必ずこれらのメソッドを使う必要がある。
選択系 | 概要 |
---|---|
select() | セレクタにマッチする最初の要素をSelection にして返す。 |
selectAll() | セレクタにマッチするすべての要素をまとめ、Selection にして返す。 |
両方とも、引数にはセレクタを指定する文字列を渡す。
これらは、内部でdocument.querySelector()
、document.querySelectorAll()
をそれぞれ呼び出しており、取得したElement
オブジェクト(あるいはNodeList
オブジェクト)をもとにSelection
オブジェクトを作って返す。
このとき、Selection
オブジェクトは以下の2つのプロパティを持っている。
プロパティ | 中身 |
---|---|
_groups | 取得したElement あるいはNodeList の配列。 |
_parents | _groupsの各要素の親要素。初回の選択ではdocument.documentElement がpushされる。 |
_groups
プロパティが配列なのは、セレクタが複数(例:p.1st-section, p.2nd-section
)の場合を想定してのものである。
アンダーバーの付いているプロパティは、D3.jsの内部でのみ使われる。ユーザーが触るものではないが、本記事では理解を深めるために、私が理解できた範囲で取り上げてみたいと思う。
Selection
オブジェクトは、この2つのプロパティでDOM要素を保持し、主に以下のようなメソッドを使って操作する(メソッドの完全なリストは公式ドキュメントのここで参照できる)。
属性値を取得・設定するメソッド
メソッド名 | 概要 |
---|---|
style() | 選択された要素のスタイルを取得または設定。 |
attr() | 静的な属性値の取得または設定。 |
property() | 動的な属性値(HTMLのformタグ内で使われる各種属性値など)の取得または設定。 |
それぞれの引数は、値を設定する場合は("属性名","属性値")
。取得する場合は("属性名")
とする。
また、引数に関数を指定することもできる。その関数は、現在のデータアイテム、現在のインデックス、現在のグループ(選択中のノードリスト)という3つの引数を受け取る。
関数の中では、ノードリスト[インデックス]
という形でDOMの各属性値を操作できる。
コンテンツを取得・設定するメソッド
メソッド名 | 概要 |
---|---|
text() | 選択された要素のテキストコンテンツを取得または設定。 |
html() | 選択された要素のHTMLコンテンツを取得または設定(HTMLソースを文字列として渡すと、選択された要素の子要素として追加してくれる)。 |
引数には、文字列と関数を指定できる。関数が受け取る引数は、現在のデータアイテム、現在のインデックス、現在のグループ(選択中のノードリスト)の3つである。
また、値を省略すると、現在のコンテンツが返る。
イベントリスナを設定するメソッド
メソッド名 | 概要 |
---|---|
on() | 選択された要素にイベントリスナーを設定または取得。 |
on()
が受け取る引数は、DOM APIのaddEventListener()メソッドと共通している。ただ、D3のイベントリスナは、引数にイベントオブジェクトだけでなく、そのDOMに対応するデータアイテムも受け取れる。
引数を省略して呼び出すと、イベントリスナが返される。
DOM要素を追加・削除するメソッド
メソッド名 | 概要 |
---|---|
append() | 選択された要素に新しい要素を追加。 |
insert() | 指定された要素の前に新しい要素を挿入。 |
remove() | 選択された要素を削除。 |
append()
メソッドの引数は追加する要素のタグ名、insert()
メソッドは挿入する要素のタグ名と、挿入ターゲットとなる要素のセレクタである。
また、ともに関数も受け取れる。関数が受け取る引数は、現在のデータアイテム、現在のインデックス、現在のグループ(選択中のノードリスト)の3つである。
remove()
メソッドは引数を取らない。
データとDOM要素を紐づけるメソッド
メソッド名 | 概要 |
---|---|
data() | 引数に渡したデータを、選択された各要素にバインドした、新しいSelection を返す。 |
join() | データに応じて要素の追加・更新・削除を行う。 |
data()
メソッドは、データの配列のほかに、第二引数に関数も受け取れる。この関数が受け取る引数も、現在のデータアイテム、現在のインデックス、現在のグループ(選択中のノードリスト)の3つである。
また、join()
メソッドには、データに応じて要素を挿入、更新、削除する関数をそれぞれ渡せる。第一引数に渡す挿入関数は、挿入したいタグの文字列を代わりに指定することができる。更新、削除に使うメソッドも、省略すればデータに応じて処理してくれる。
そのため、多くはjoin("挿入したいタグ名")
のような形で呼び出す。
data()
メソッドとjoin()
メソッドを組み合わせると、データに応じて自動で要素を追加、更新、削除を行う処理が直感的に書ける。
昔と今をつなぐはなし
join()
が登場する前は、以下メソッドが使われていた。直感的でないためユーザーが書くことは推奨されないが、現在も内部ではこれらのメソッドが使われている。
メソッド名 | 概要 |
---|---|
enter() | データが関連付けられた新しいSelection を返す。 |
exit() | データが関連付けられなくなった新しいSelection を返す。 |
属性値に静的な値ではなく関数を渡せる(動的に値を設定できる)、複数のDOM要素を配列操作の要領で一括操作できる、など特徴的な部分はあるが、おおよそのメソッドは名称から使い方を類推できるかと思う。
ただし、データを紐づけるタイプのメソッドは注意が必要である。上記の3つのメソッドが返すSelection
オブジェクトは、Selection
オブジェクトではあるもののタイプが異なる。
data()
は、DOM要素とデータを結びつける。この操作によって返されるのは、既存のDOM要素を保持するSelection
オブジェクト(Update selection)である。そのままtext()
やstyle()
で操作できる。
一方、enter()
は、データに対して不足するDOM要素(のプレースホルダ)を保持するSelection
オブジェクト(Enter selection)を返す。このため、enter()
が返すSelection
オブジェクトに対しては、新たなDOM要素を追加するappend()
メソッドを呼び出す。
exit()
は、データが削除されて余分になったDOM要素(exit selection)を保持する。これらの要素は、もはやデータに対応していないため、通常はremove()
を使って削除する。
こうした仕組みの背景には、D3.js特有の「General Update」パターン(=「Update-Enter-Exit」パターン)がある。
General Updateパターン
ブラウザ標準のDOM APIでは、DOM要素を作ってからそこにデータを挿入していく。D3.jsでは、これが逆転する。データ駆動という名前の通り、データに基づいて要素が作成される。
データに基づいて要素を管理するためには、データとDOM要素を紐づける必要がある。そのための仕組みとして用いられる設計が、D3.js特有のGeneral Update
パターンである。
このパターンは、以下のような流れでDOM要素の更新、作成あるいは削除を管理する。
- データ表示に使うDOM要素を受け取る。
- 表示するデータを受け取る。
- DOM要素とデータを照らし、データ表示に使うDOM要素の過不足を判断する
- 足りている部分は、DOM要素の更新に必要な情報を作成・保持する。
- 足りない場合、新たなDOM要素を作るための情報を作成・保持する。
- 余る場合、該当のDOM要素を削除するための情報を作成・保持する。
データ表示に使う一連の要素をまず指定し、次いで表示するデータを指定する。これによりデータと表示用の要素が照合され、要素の新規追加、更新、削除に必要な情報が整理される。この段階では、要素の追加や削除は行われない。あくまで、指定されたデータに対して、表示用要素の追加・更新・削除に必要な情報が明らかにされるのみである。
D3.jsの文脈では、このように表示用の要素に任意のデータを紐づける処理を結合(join)と呼ぶ。また、結合により整理される3つの状態を、それぞれ「Enter」、「Update」、「Exit」と呼ぶ。
静的なデータはもちろん、動的なデータであっても、これら3つの状態を再計算(=データ結合)することでリアクティブに視覚化できる。この更新パターン(General Updateパターン)が、D3.jsの大きな特徴である。
結合とバインド
結合は、データと表示用の要素を照合し、Enter(作成する要素)、Update(更新する要素)、Exit(削除する要素)という3つの情報を整理することである。バインドもこれと同じニュアンスで使われることが多い。
ただ、より正確には、結合の過程で表示用のDOM要素に_data_
プロパティを追加し、直接データを持たせることである(と私は解釈した)。
Selection
に対するデータの結合は、data()
によって行う。例えばd3.selectAll('p').data(['0','1'])
のように書く。
selectAll()
やselect()
が返すSelection
オブジェクトは、2つのプロパティ(_parents
と_groups
)しか持たない。data()
が返すSelection
は、これらに_enter
と_exit
が追加されている。
いずれも配列だが、中身のオブジェクトの種類は異なる。
プロパティ名 | 概要 |
---|---|
_enter | データに対して不足するDOM要素分の、EnterNode オブジェクトの配列 |
_exit | データに対して余分なDOM要素の配列 |
EnterNode
オブジェクトは、新規に作られるSelection
オブジェクトのプレースホルダ的なオブジェクトである。_data_
プロパティを持ち、データを保持している。
ちなみに_update
プロパティは存在しない。データの数に対し足りている分のDOM要素は、_groups
の中身が該当する。
selection.data()の処理の流れ
data()
は、以下のような流れで上記の各プロパティを整理する。
_groups
プロパティで保持している各DOM要素に_data_
プロパティを追加して、そこに引数で受け取ったデータを格納する。- DOM要素が不足する場合は
_enter
プロパティに、_data_
プロパティを持つEnterNode
オブジェクトをpushする。 - DOM要素が余る場合は、
_exit
プロパティに該当のDOM要素をpushする。 - 上記を適用した新たな
Selection
オブジェクト(Update Selectionと呼ぶらしい)を返す。
data()
は引数に渡されたデータに基づいて新たなSelection
を生成する。このSelection
は、それぞれのDOM要素とデータに基づく更新(update)、追加(enter)、削除(exit)の情報を表現している。
ややこしいのはこの後である。
_groups
プロパティに含まれるDOM要素にデータを反映する場合は、そのまま設定用のメソッド(text()
、style()
、attr()
など)を呼ぶ。
一方、_enter
に分類されたデータは、このままではどうにもできない。まずDOM要素を作成する必要がある。
このために、_enter
の中身からSelection
オブジェクト(Enter Selection)を作成して返すenter()
をまず実行し、そのSelection
オブジェクトのappend()
を呼び出して、データを紐付けたDOM要素を作成・選択する。ここでようやく設定用のメソッドが実行できる。
_exit
の場合も同様に、_exit
の中身からSelection
オブジェクト(Exit Selection)を作って返すexit()
を実行して、その上でremove()
を呼び出して余分なDOM要素を削除する。
以下は、3つのメソッドを使ったサンプルコードである。
えらい長くなってしまった。ともかくD3.jsはこうしたユニークな仕組みを土台にデータをビジュアル化している。
参考資料
- 概要
- Selection関係
- General Update関係
- 最初からこれ読んでおけばよかった関係