D3.js はデータ可視化のインタラクションをサポートするためのさまざまなツールを提供しており、その中でd3.transition
は画像にアニメーションを簡単かつ効率的に追加することを可能にします。
API の観点から見ると、d3.transition
は非常にシンプルで、Jquery に似た使い方をします。しかし、理想的なアニメーション効果を設計するには、D3 によるグラフィック描画の核心概念であるGeneral Update Pattern
に触れなければなりません。D3 のデータ駆動特性の核心と実現はこのパターンに依存しており、アニメーションやインタラクションも自然にここから始まります。
すべてのグラフィックが Update Pattern に従う必要はありません。たとえば、一度きりの描画や、インタラクションのない静的なグラフィックなどです。しかし、動的データが関与する場合、この Update Pattern はメンテナンスしやすいコードを書くのに役立つだけでなく、D3 の強力な機能をより良く発揮させることができます。
General Update Pattern#
D3 のデータ駆動モデルは上の図のように、d3.data()
を使用してデータArray
を DOM 要素にバインドする際、データと要素の間には 3 つの段階があります。
-
Enter 既存のデータがあるが、ページにはそれに対応する DOM がまだ存在しない
-
Update データ要素が DOM 要素にバインドされる
-
Exit データ要素が削除されたが、DOM 要素はまだ存在している、つまりバインドされた要素を失った DOM
この点については詳細に説明しませんが、ドキュメントを参照してください。ここでは V4 と V5 バージョンのGeneral Update Pattern
を直接紹介します。簡単な例を挙げます。
現在、データ ['a', 'b', 'c'....] のような文字列の配列があると仮定し、D3 を使用して SVG でそれをページに表示したいとします。
V4#
selection.enter()
, selection.exit()
とselection
(update)を使用して、それぞれのロジックを指定します。内容は以下の通りです。
const d3Pattern = (dataSet) => {
const text = g.selectAll('text').data(dataSet) // データバインディング
text.enter() // enter()はバインドされたデータだがまだDOM要素が生成されていない部分を返す
.append('text')
.attr('class', 'new')
.text(d => d)
.merge(text) // mergeの後のコードはenterとupdateの両方に適用され、簡略化された書き方
.attr('x', (d, i) => 18 * i)
text.attr('class', 'update') // text自体はupdate部分
text.exit().remove() // exit()はデータが削除されたが、DOMにまだ存在する要素を返す
}
V5#
d3 V5.8.0 では新しい APIselection.join
が導入されました。
この API の利点は、特別な enter\exit プロセスを定義する必要のない比較的単純な D3 グラフィックの動作を簡略化できることです。上記のコードを V5 バージョンで書くと、以下のようになります。
const d3Pattern = (dataSet) => {
const text = g.selectAll('text').data(dataSet) // データバインディング
text.join(
enter => enter.append('text')
.attr('calss', 'new')
.text(d => d),
update => update.attr('class', 'update')
)
.attr('x', (d, i) => 18 * i)
// joinのexitはデフォルトでexit().remove()なので省略できます
}
selection.join()
を使用すると、手動でselection.exit().remove()
を書く必要がなくなります。これは、selection.join()
のこの関数のデフォルトのexit()
関数がすでに用意されているためです。この API の下で、D3 の Update Pattern は次のように書くことができます。
selection.join(
enter => // enter.. ,
update => // update.. ,
exit => // exit..
)
// 注意、enter、updateなどの関数は必ずreturnする必要があります。そうすることでselectionに対して連鎖的な呼び出しが可能になります。
もちろん、この API の利点は、一般的な使用シーン(enter、exit などに特別なアニメーションや操作を加える必要がない場合)では、完全に簡略化できることです。たとえば:
svg.selectAll("circle")
.data(data)
.join("circle")
.attr("fill", "none")
.attr("stroke", "black");
上記の書き方は、完全に以下と等価です。
svg.selectAll("circle")
.data(data)
.join(
enter => enter.append("circle"),
update => update,
exit => exit.remove()
)
.attr("fill", "none")
.attr("stroke", "black");
V4 バージョンの以下と等価です。
circles = svg.selectAll('circle')
.data(data)
circles.enter()
.append('circle')
.merge('circle')
.attr('fill', 'none')
.attr('stroke', 'black')
circles.exit().remove()
もちろん、V5 は V4 の update Pattern と完全に互換性があります。V4 でも V5 の新しい API でも、この Update Pattern の本質は変わらず、D3 は依然としてデータバインディング、enter/update/exit の作業モードを持っています。
Pattern 内の key#
d3.data
()
を使用してデータと DOM をバインドする際、対応する関係は、最初の要素が最初の DOM に対応し、2 番目の要素が 2 番目の DOM に対応するなどです。しかし、Array
が変更されると、たとえば再ソートや挿入などの操作が行われると、配列内の要素が DOM とのバインディング関係に微妙な変化をもたらすことがあります。
最も直感的な例は、文字を動的に変更する例です。
図のように、新しく追加された文字は常に最後に並びます。実際には、データが DOM にバインドされたままであれば、理論的には新しい文字がランダムに生成されると、中間に出現する機会が完全にあるはずです。
データバインディング時に一意の key 値を渡すことで、このような状況を回避できます。
selection.data(data, d => d.id)
以上のステップを完了すれば、定期的にd3Pattern
関数を呼び出し、異なるデータを渡すことで、上の図の効果を実現できます。
Transition#
さて、前述の内容を踏まえて、ついに主役のd3.transition
に到達しました。しかし、実際には関連する API は数えるほどしかありません。D3 でインタラクティブでクールな遷移効果を描くためには、Update Pattern をしっかり理解することが重要です。
基本アニメーションの使用#
transition
の使用は、Jquery と非常に似ています。使用する際は、選択した要素に対して呼び出し、変更する属性を指定するだけです。つまり、selection.transition().attr(...)
です。
たとえば、キャンバス上に四角形があり、その要素がrect
であるとします。位置をデフォルトの場所から 30 の位置に移動させ、アニメーションを加えたい場合、コードは次のようになります。
rect.transition()
.attr('x', 30) // 新しい位置を設定
効果は以下の通りです。
アニメーションの基本的な使用は、これほど簡単です。以下に関連する API を簡単に見てみましょう。
メソッド | 説明 |
---|---|
selection.transition() | 選択された要素のために遷移をスケジュールします |
transition.duration() | 各要素のアニメーションの持続時間をミリ秒で指定します |
transition.ease() | イージング関数を指定します。例:linear、elastic、bounce |
transition.delay() | 各要素のアニメーションの遅延をミリ秒で指定します |
ここで注意すべきは、d3 の API はすべてチェーン呼び出しをサポートしているため、たとえば上記の例でアニメーション時間を 1 秒に設定したい場合、次のようにできます。
rect.transition()
.duration(1000)
.attr('x', 30) // 新しい位置を設定
同様に、ease と delay をそれぞれアニメーション曲線と遅延として設定できます。
Update Pattern 下のアニメーション#
最初の例に戻り、ここでは V4 バージョンの Update Pattern を例に挙げます。
transition はselection
に適用されるため、使用を便利にするために、アニメーションを事前に定義できます。
const t = d3.transtion().duration(750)
次に、新しく追加されたテキストが上から落ちてくるようにし、位置が更新されるときにアニメーション効果を持たせたい場合、enter()
時に位置が上から下に移動するプロセス(transition)を設定する必要があります。
const d3Pattern = (dataSet) => {
const t = d3.transtion().duration(750) // アニメーションを定義
const text = g.selectAll('text').data(dataSet)
text.enter()
.append('text')
.attr('class', 'new')
.text(d => d)
.attr('y', -60)
.transition(t)
.attr('y', 0)
.attr('x', (d, i) => 18 * i)
text.attr('class', 'update')
.attr('y', 0)
.transition(t)
.attr('x', (d, i) => 18 * i)
text.exit()
.transition(t)
.attr('y', 60)
.remove()
}
見ると、元々の堅苦しいスタイルが、ずっと動きのあるものに変わりました。もちろん、exit のテキストに赤色を加え、落下アニメーションを追加して全体をより動的にすることもできます。exit 部分に相応の処理を行うだけです:
text.exit()
.transition(t)
.attr('y', 60)
.remove()
図のように、下に落ちるアニメーションと透明度の変化が加えられたアニメーション効果です。
実戦応用#
たとえば、すでに静的な棒グラフがあり、マウスがホバーしたときに動的な効果が変化することを希望する場合、以下のようになります。
棒グラフの実装については詳しく説明しませんが、核心コードについて説明します。考え方は上で述べたものと完全に同じです。
-
マウス移動イベントを監視する
-
現在のバーを選択し、transition で属性を変更する
-
マウス移出を監視する
-
現在のバーを選択し、マウス移出時に属性を元に戻す
核心コードは以下の通りです。
svgElement
.on('mouseenter', (actual, i) => {
d3.select(this)
.transition()
.duration(300)
.attr('opacity', 0.6)
.attr('x', (a) => xScale(a.language) - 5)
.attr('width', xScale.bandwidth() + 10)
})
.on('mouseleave', (actual, i) => {
d3.select(this)
.transition()
.duration(300)
.attr('opacity', 1)
.attr('x', (a) => xScale(a.language))
.attr('width', xScale.bandwidth())
})
この棒グラフのソースコードとチュートリアルは、D3.js Tutorial: Building Interactive Bar Charts with JavaScriptに由来しています。
補間アニメーション#
色の変化や数字の跳ね上がりなどの特別な遷移の場合、補間関数がないと、直接transition().attr()
を使用して実現することはできません。
そのため、d3 はこのようなアニメーションを実現するための補間関数と補間アニメーションのインターフェースを提供しています。もちろん、ほとんどのシーンでは、非補間アニメーションで十分です。
特別な補間#
一般的な属性補間に関して、d3 は非常に便利なエントリを提供しており、それぞれattrTween
(属性補間)/styleTween
(スタイル補間)/textTween
(文字補間)です。
これらの補間は、色や線の太さなどの「属性」補間に主に使用され、attrTween()
やstyleTween
を使用できます。数字の変化や連続的な跳ね上がりにはtextTween
を使用します。これらの使い方は似ています。以下のように:
// 色の補間、赤から青に変化
transition.attrTween('fill', function() {
return d3.interpolateRgb("red", "blue");
})
transition.styleTween('color', function() {
return d3.interpolateRgb("red", "blue");
})
補間関数の最初のパラメータは変更する内容や属性で、機能はtransition().attr()
内の attr の内容に似ています。2 番目のパラメータは返される補間関数で、d3 が提供する補間関数を使用できますし、カスタム補間関数を作成することもできます。
簡単な例を挙げると、たとえば赤から青に変化させたい場合、補間関数内で自分で定義した関数func(t)
を返すことができます。この関数はアニメーション時間内に繰り返し実行され、t は [0, 1] の範囲になります。この考え方を利用して、上記の効果をカスタム関数で実現することができます。
svg.append('text')
.text('A')
.on('mouseenter', function() {
d3.select(this)
.transition()
.attrTween('fill', function() {
return function(i) {
return `rgb(${i * 255}, 0, ${255-(i * 255)})`
}
})
})
.on('mouseleave', function() {...})
次に、カスタム関数について説明します。たとえば、赤から青に変化させる場合、補間関数内で自分で定義した関数func(t)
を返すことができます。この関数はアニメーション時間内に繰り返し実行され、t は [0, 1] の範囲になります。この考え方を利用して、上記の効果をカスタム関数で実現することができます。
svg.append('text')
.text('A')
.on('mouseenter', function() {
d3.select(this)
.transition()
.attrTween('fill', function() {
return function(i) {
return `rgb(${i * 255}, 0, ${255-(i * 255)})`
}
})
})
.on('mouseleave', function() {
... // 上と同様
})
これらの 2 つの方法は、動的な効果を実現できます。
補間アニメーションにおいて、核心は補間内容の生成です。d3 は多くの補間を提供しており、関連するリストは以下の通りです。たとえば、数字の跳ね上がりアニメーションを使用する場合、d3.interpolatorRound(start,end)
を使用して整数の補間を生成できます。d3.interpolateRgb(color, color2)
を使用して色の補間を生成することもできます。具体的な補間関数の使い方は、関連 API を参照してください。
-
d3.interpolatNumber
-
d3.interpolatRound
-
d3.interpolatString
-
d3.interpolatRgb
-
d3.interpolatHsl
-
d3.interpolatLab
-
d3.interpolatHcl
-
d3.interpolatArray
-
d3.interpolatObject
-
d3.interpolatTransform
-
d3.interpolatZoom
一般的な補間#
もちろん、前述の API に加えて、より一般的な補間関数 API であるd3.tween()
もあります。
attrTween()
などと同様に、2 番目のパラメータも補間関数を渡します。異なるのは、最初のパラメータが、変更したい内容をより一般的に受け入れることができる点です。たとえば、上記のfill
属性を使用する場合、一般的な補間関数の書き方は次のようになります。
selection.transition()
.tween('attr.fill', function() {
return function(i) {
return `rgb(${i * 255}, 0, ${255-(i * 255)})`
}
})
したがって、一般的な API と前述の特別な 3 つの API の使い方は非常に似ています。唯一の違いは、一般的な API の最初のパラメータがより広範な変更属性を受け入れることができる点です。
ここでは多くの例を挙げませんが、補間関数の参考例はここで確認できます。