Vincent Ko

VK's Blog

D3.js アニメーション

D3.js はデータ可視化のインタラクションをサポートするためのさまざまなツールを提供しており、その中でd3.transitionは画像にアニメーションを簡単かつ効率的に追加することを可能にします。

API の観点から見ると、d3.transitionは非常にシンプルで、Jquery に似た使い方をします。しかし、理想的なアニメーション効果を設計するには、D3 がグラフィックを描画する際のコアコンセプトであるGeneral Update Patternに触れざるを得ません。D3 のデータ駆動特性の核心と実現はこのパターンに依存しており、アニメーションとインタラクションは自然にここから始まります。

すべてのグラフィックが Update Pattern に従う必要はありません。たとえば、一度きりの描画やインタラクションのない静的なグラフィックなどです。しかし、動的データが関与する場合、この Update Pattern はメンテナンスしやすいコードを書くのに役立つだけでなく、D3 の強力な機能をより良く発揮させることができます。

General Update Pattern#

image-20200308211328582

D3 のデータ駆動モデルは上の図のように、d3.data()を使用してデータArrayと DOM 要素をバインドする際、データと要素の間には 3 つの段階があります。

  • Enter 既存のデータがあるが、ページにはそれに対応する DOM がまだ存在しない

  • Update データ要素が DOM 要素にバインドされる

  • Exit データ要素が削除されたが、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 では新しい API、selection.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 とのバインディング関係に微妙な変化をもたらすことがあります。

最も直感的な例は、文字を動的に変更する例です。

Kapture 2020-03-08 at 21.56.19

図のように、新しく追加された文字は常に最後に並びます。実際には、データが DOM バインディングを一貫して保持している場合、理論的には新しい文字がランダムに生成されても、真ん中に出現する機会があるはずです。

データバインディング時に一意の key 値を渡すことで、このような状況を回避できます。

selection.data(data, d => d.id)

以上のステップを完了すれば、定期的に関数d3Patternを呼び出し、異なるデータを渡すことで、上の図の効果を実現できます。

完全なコード

Transition#

さて、前述の内容を踏まえて、ついに主役のd3.transitionに到達しましたが、実際には関連する API は数えるほどしかありません。d3 にインタラクティブでクールな遷移効果を描かせるためには、Update Pattern を十分に理解することが重要です。

基本アニメーションの使用#

transitionの使用は、jQuery と非常に似ています。使用する際は、選択した要素に対して呼び出し、変更する属性を指定するだけです。つまり、selection.transition().attr(...)です。

Kapture 2020-03-08 at 22.17.46

たとえば、キャンバス上に四角形があり、その要素が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()
}

Kapture 2020-03-08 at 22.40.14

見ると、元々の堅苦しいスタイルが、ずっと動きのあるものに変わりました。もちろん、退出 (exit) するテキストに赤色を加え、落下アニメーションを追加することで、全体をより動的にすることもできます。exit 部分に相応の処理を行うだけです。

text.exit()
.transition(t)
.attr('y', 60)
.remove()

Kapture 2020-03-08 at 22.46.24

図のように、下に落ちるアニメーションと透明度の変化を加えたアニメーション効果が得られます。

完全なコード

実戦アプリケーション#

たとえば、すでに静的な棒グラフがあり、マウスをホバーしたときに動的な効果が変化することを望む場合、以下の図のようになります。

Kapture 2020-03-08 at 23.03.53

棒グラフの実装についてはここでは詳述しませんが、コアコードの考え方は上記と完全に同じです。

  1. マウス移動イベントをリッスンする

  2. 現在のバーを選択し、transition で属性を変更する

  3. マウス移出をリッスンする

  4. 現在のバーを選択し、マウス移出時に属性を元に戻す

コアコードは以下の通りです。

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 の最初のパラメータがより広範な変更属性を受け入れることができる点です。

ここではこれ以上の例は挙げませんが、補間関数の参考例はこちらで確認できます。

参考資料#

  1. D3.js Tutorial: Building Interactive Bar Charts with JavaScript

  2. How to work with D3.js’s general update pattern

  3. Interaction and Animation: D3 Transitions, Behaviors, and Brushing

  4. d3-selection-join

  5. sortable-bar-chart

  6. Using Basic and Tween Transitions in d3.js

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。