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 要素はまだ存在している、つまりバインドされた要素を失った 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 とのバインディング関係に微妙な変化をもたらすことがあります。

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

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

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