6. インタラクション#

「グラフィックは一度きり‘描かれる’のではなく、データの相互作用によって構成されるすべての関係を明らかにするまで‘構築’され、再構築される。最良のグラフィック操作は、意思決定者自身が行うものである。」ジャック・ベルタン (Jacques Bertin)

可視化は、データを理解するための強力な手段を提供します。しかし、単一の画像が提供できる答えは、せいぜい数個の質問に対するものです。_インタラクション_を通じて、静的な画像を探索ツールに変えることができます。興味のあるポイントを強調表示し、詳細なパターンを明らかにするためにズームし、複数のビューをリンクして多次元の関係を考察することが可能になります。

インタラクションの中心には、選択 (selection) の概念があります。これは、どの要素や領域に関心があるのかをコンピューターに指示する手段です。たとえば、ポイントにマウスをホバーさせる、複数のマークをクリックする、あるいはデータのサブセットを強調表示するために領域を囲むボックスを描画するなどがあります。

Altairは、視覚的エンコーディングやデータ変換と並んで、インタラクティブな選択を作成するための_選択_抽象化を提供します。この選択は以下の3つの側面を包含します:

  1. マウスホバー、クリック、ドラッグ、スクロール、タッチイベントなど、ポイントや関心のある領域を選択するための入力イベントの処理。

  2. 入力を一般化して選択ルール(または述語)を形成し、特定のデータレコードが選択内にあるかどうかを判断。

  3. 選択述語を使用して、条件付きエンコーディングフィルター変換、または_スケールドメイン_を駆動することで、可視化を動的に構成。

このノートブックでは、インタラクティブな選択を紹介し、動的クエリ、パン&ズーム、オンデマンドでの詳細表示、ブラッシング&リンクなど、さまざまなインタラクション技術を作成する方法を探ります。

このノートブックは、データ可視化カリキュラムの一部です。

import pandas as pd
import altair as alt

6.1. Datasets#

以下のデータセットを、vega-datasetsコレクションから可視化します:

  • 1970年代から1980年代初期のcarsデータセット

  • データ変換ノートブックで使用したmoviesデータセット

  • 10年間のS&P 500sp500)株価データセット

  • テクノロジー企業のstocksデータセット

  • 出発時刻、飛行距離、到着遅延を含むflightsデータセット

cars = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/cars.json'
movies = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json'
sp500 = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/sp500.csv'
stocks = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/stocks.csv'
flights = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/flights-5k.json'

6.2. Introducing Selections#

以下の手順で、基本的な選択を実装してみましょう。単にポイントをクリックして、それをハイライトします。carsデータセットを使用して、馬力(horsepower)と1ガロンあたりの走行距離(miles per gallon)の散布図を作成し、エンジンのシリンダー数を色でエンコードします。

また、alt.selection_single()を呼び出して選択インスタンスを作成します。これにより、_単一の値_を基準に定義された選択を行うことができます。デフォルトでは、マウスクリックで選択された値が決定されます。選択をチャートに登録するには、.add_selection()メソッドを使用して追加する必要があります。

選択が定義されたら、それを_条件付きエンコーディング_のパラメータとして使用できます。条件付きエンコーディングでは、データレコードが選択に含まれるかどうかに応じて異なるエンコーディングを適用します。以下のコードを参考にしてください:

color=alt.condition(selection, 'Cylinders:O', alt.value('grey'))

このエンコーディング定義では、selectionに含まれるデータポイントはCylinderフィールドに基づいて色付けされ、一方で選択されていないデータポイントはデフォルトのgreyが使用されることを示しています。空の選択状態では、_すべての_データポイントが含まれるため、最初はすべてのポイントが色付けされます。

以下のチャートで異なるポイントをクリックしてみてください。何が起こりますか?(背景をクリックすると選択状態がクリアされ、”空”の選択状態に戻ります。)

selection = alt.selection_single();
  
alt.Chart(cars).mark_circle().add_selection(
    selection
).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    color=alt.condition(selection, 'Cylinders:O', alt.value('grey')),
    opacity=alt.condition(selection, alt.value(0.8), alt.value(0.1))
)
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:65: AltairDeprecationWarning: 'selection_single' is deprecated.  Use 'selection_point'
  warnings.warn(message, AltairDeprecationWarning, stacklevel=1)
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:65: AltairDeprecationWarning: 'add_selection' is deprecated. Use 'add_params' instead.
  warnings.warn(message, AltairDeprecationWarning, stacklevel=1)

もちろん、一度に1つのデータポイントをハイライトするだけでは、それほど興味深くありません。しかし、単一値選択は、より強力なインタラクションのための有用な構成要素を提供します。さらに、単一値選択はAltairが提供する3種類の選択タイプの1つに過ぎません:

  • selection_single - 単一の離散値を選択します(デフォルトではクリックイベントに基づきます)。

  • selection_multi - 複数の離散値を選択します。最初の値はマウスクリックで選択され、追加の値はシフト+クリックでトグルします。

  • selection_interval - 連続した値の範囲を選択します。マウスドラッグで開始します。

これらの選択タイプを並べて比較してみましょう。コードを整理するために、最初に散布図仕様を生成する関数(plot)を定義します。この関数に選択を渡すことで、チャートに適用されるようにします:

def plot(selection):
    return alt.Chart(cars).mark_circle().add_selection(
        selection
    ).encode(
        x='Horsepower:Q',
        y='Miles_per_Gallon:Q',
        color=alt.condition(selection, 'Cylinders:O', alt.value('grey')),
        opacity=alt.condition(selection, alt.value(0.8), alt.value(0.1))
    ).properties(
        width=240,
        height=180
    )

plot関数を使用して、選択タイプごとに1つずつ、3つのチャートバリアントを作成してみましょう。

  • **single**チャート:先ほどの例を再現します。

  • **multi**チャート:シフト+クリック操作をサポートし、複数のポイントを選択に含めることができます。

  • **interval**チャート:マウスドラッグで選択領域(または_ブラシ_)を生成します。作成後、ブラシをドラッグして異なるポイントを選択したり、ブラシ内でスクロールしてブラシサイズをスケーリング(ズーム)することができます。

以下の各チャートとインタラクションしてみてください!

alt.hconcat(
  plot(alt.selection_single()).properties(title='Single (Click)'),
  plot(alt.selection_multi()).properties(title='Multi (Shift-Click)'),
  plot(alt.selection_interval()).properties(title='Interval (Drag)')
)
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:65: AltairDeprecationWarning: 'selection_multi' is deprecated.  Use 'selection_point'
  warnings.warn(message, AltairDeprecationWarning, stacklevel=1)

上記の例では、各選択タイプのデフォルトのインタラクション(クリック、シフト+クリック、ドラッグ)を使用していますが、Vegaイベントセレクタ構文を使用して、インタラクションをさらにカスタマイズできます。たとえば、singleおよびmultiチャートを修正して、clickイベントの代わりにmouseoverイベントでトリガーするように設定できます。

2つ目のチャートでShiftキーを押しながらマウスを動かし、データで「ペイント」してみてください!

alt.hconcat(
  plot(alt.selection_single(on='mouseover')).properties(title='Single (Mouseover)'),
  plot(alt.selection_multi(on='mouseover')).properties(title='Multi (Shift-Mouseover)')
)

Altairの選択の基本を理解したところで、これから選択が可能にするさまざまなインタラクション技術を見ていきましょう!

6.3. Dynamic Queries#

_動的クエリ_は、関心のあるパターンを特定するためのデータの迅速かつ可逆的な探索を可能にします。Ahlberg, Williamson, & Shneidermanによって定義された動的クエリは以下を特徴とします:

  • クエリを視覚的に表現する

  • クエリ範囲の明確な制限を提供する

  • データとクエリ結果の視覚的表現を提供する

  • クエリ調整後に即時フィードバックを提供する

  • 初心者がほとんどトレーニングなしで作業を開始できるようにする

一般的なアプローチとして、スライダー、ラジオボタン、ドロップダウンメニューなどの標準的なユーザーインターフェースウィジェットを使用してクエリパラメータを操作します。動的クエリウィジェットを生成するには、選択のbind操作を、クエリしたい1つ以上のデータフィールドに適用します。

映画の評価(Rotten TomatoesとIMDB)をプロットした散布図を例に、動的クエリを使用して表示をフィルタリングするインタラクティブな散布図を作成してみましょう。Major_Genreフィールドを選択に追加し、映画ジャンルでのインタラクティブなフィルタリングを可能にします。

まず、moviesデータからユニーク(非ヌル)のジャンルを抽出してみましょう:

df = pd.read_json(movies) # load movies data
genres = df['Major_Genre'].unique() # get unique field values
genres = list(filter(lambda d: d is not None, genres)) # filter out None values
genres.sort() # sort alphabetically

後で使用するために、MPAA_Ratingフィールドのユニークな値のリストも定義しておきましょう:

mpaa = ['G', 'PG', 'PG-13', 'R', 'NC-17', 'Not Rated']

次に、ドロップダウンメニューにバインドされたsingle選択を作成します。

以下の動的クエリメニューを使用してデータを探索してください。評価はジャンルによってどのように変化しますか?コードをどのように修正すれば、Major_GenreではなくMPAA_Rating(G、PG、PG-13など)をフィルタリングすることができますか?

selectGenre = alt.selection_single(
    name='Select', # name the selection 'Select'
    fields=['Major_Genre'], # limit selection to the Major_Genre field
    init={'Major_Genre': genres[0]}, # use first genre entry as initial value
    bind=alt.binding_select(options=genres) # bind to a menu of unique genre values
)

alt.Chart(movies).mark_circle().add_selection(
    selectGenre
).encode(
    x='Rotten_Tomatoes_Rating:Q',
    y='IMDB_Rating:Q',
    tooltip='Title:N',
    opacity=alt.condition(selectGenre, alt.value(0.75), alt.value(0.05))
)
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:65: AltairDeprecationWarning: 'selection_single' is deprecated.  Use 'selection_point'
  warnings.warn(message, AltairDeprecationWarning, stacklevel=1)
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:417: AltairDeprecationWarning: Use 'value' instead of 'init'.
  warnings.warn(
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [9], line 1
----> 1 selectGenre = alt.selection_single(
      2     name='Select', # name the selection 'Select'
      3     fields=['Major_Genre'], # limit selection to the Major_Genre field
      4     init={'Major_Genre': genres[0]}, # use first genre entry as initial value
      5     bind=alt.binding_select(options=genres) # bind to a menu of unique genre values
      6 )
      8 alt.Chart(movies).mark_circle().add_selection(
      9     selectGenre
     10 ).encode(
   (...)
     14     opacity=alt.condition(selectGenre, alt.value(0.75), alt.value(0.05))
     15 )

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:66, in _deprecate.<locals>.new_obj(*args, **kwargs)
     63 @functools.wraps(obj)
     64 def new_obj(*args, **kwargs):
     65     warnings.warn(message, AltairDeprecationWarning, stacklevel=1)
---> 66     return obj(*args, **kwargs)

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:753, in selection_single(**kwargs)
    747 @utils.deprecation.deprecated(
    748     message="'selection_single' is deprecated.  Use 'selection_point'"
    749 )
    750 @utils.use_signature(core.PointSelectionConfig)
    751 def selection_single(**kwargs):
    752     """'selection_single' is deprecated.  Use 'selection_point'"""
--> 753     return _selection(type="point", **kwargs)

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:478, in _selection(type, **kwds)
    475 else:
    476     raise ValueError("""'type' must be 'point' or 'interval'""")
--> 478 return param(select=select, **param_kwds)

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:444, in param(name, value, bind, empty, expr, **kwds)
    442     parameter.param_type = "selection"
    443 else:
--> 444     parameter.param = core.SelectionParameter(
    445         name=parameter.name, bind=bind, value=value, expr=expr, **kwds
    446     )
    447     parameter.param_type = "selection"
    449 return parameter

TypeError: altair.vegalite.v5.schema.core.SelectionParameter() got multiple values for keyword argument 'value'

上記の構成では、選択の複数の側面を活用しています:

  • 選択に名前('Select')を付けています。この名前は必須ではありませんが、生成された動的クエリメニューのラベルテキストに影響を与えることができます。(名前を削除するとどうなりますか?試してみてください!

  • 選択を特定のデータフィールド(Major_Genre)に制約しています。以前にsingle選択を使用したときは、選択は個々のデータポイントにマッピングされていましたが、選択を特定のフィールドに制限することで、Major_Genreフィールド値が選択した値と一致する_すべての_データポイントを選択できます。

  • init=...を使用して、選択に初期値を設定しています。

  • 選択をインターフェースウィジェットにbindしています。この場合、binding_selectを介してドロップダウンメニューにバインドしています。

  • これまでと同様に、条件付きエンコーディングを使用して、不透明度チャネルを制御しています。

6.3.1. 複数の入力に選択をバインド#

1つの選択インスタンスを_複数_の動的クエリウィジェットにバインドすることができます。上記の例を修正して、Major_GenreMPAA_Ratingの両方のフィルタを提供し、メニューではなくラジオボタンを使用します。この場合、single選択はジャンルとMPAAレーティング値の単一の_ペア_に対して定義されます。

ジャンルとレーティングの驚くべき組み合わせを探してください。GまたはPGレーティングのホラー映画はありますか?

# single-value selection over [Major_Genre, MPAA_Rating] pairs
# use specific hard-wired values as the initial selected values
selection = alt.selection_single(
    name='Select',
    fields=['Major_Genre', 'MPAA_Rating'],
    init={'Major_Genre': 'Drama', 'MPAA_Rating': 'R'},
    bind={'Major_Genre': alt.binding_select(options=genres), 'MPAA_Rating': alt.binding_radio(options=mpaa)}
)
  
# scatter plot, modify opacity based on selection
alt.Chart(movies).mark_circle().add_selection(
    selection
).encode(
    x='Rotten_Tomatoes_Rating:Q',
    y='IMDB_Rating:Q',
    tooltip='Title:N',
    opacity=alt.condition(selection, alt.value(0.75), alt.value(0.05))
)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [10], line 3
      1 # single-value selection over [Major_Genre, MPAA_Rating] pairs
      2 # use specific hard-wired values as the initial selected values
----> 3 selection = alt.selection_single(
      4     name='Select',
      5     fields=['Major_Genre', 'MPAA_Rating'],
      6     init={'Major_Genre': 'Drama', 'MPAA_Rating': 'R'},
      7     bind={'Major_Genre': alt.binding_select(options=genres), 'MPAA_Rating': alt.binding_radio(options=mpaa)}
      8 )
     10 # scatter plot, modify opacity based on selection
     11 alt.Chart(movies).mark_circle().add_selection(
     12     selection
     13 ).encode(
   (...)
     17     opacity=alt.condition(selection, alt.value(0.75), alt.value(0.05))
     18 )

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:66, in _deprecate.<locals>.new_obj(*args, **kwargs)
     63 @functools.wraps(obj)
     64 def new_obj(*args, **kwargs):
     65     warnings.warn(message, AltairDeprecationWarning, stacklevel=1)
---> 66     return obj(*args, **kwargs)

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:753, in selection_single(**kwargs)
    747 @utils.deprecation.deprecated(
    748     message="'selection_single' is deprecated.  Use 'selection_point'"
    749 )
    750 @utils.use_signature(core.PointSelectionConfig)
    751 def selection_single(**kwargs):
    752     """'selection_single' is deprecated.  Use 'selection_point'"""
--> 753     return _selection(type="point", **kwargs)

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:478, in _selection(type, **kwds)
    475 else:
    476     raise ValueError("""'type' must be 'point' or 'interval'""")
--> 478 return param(select=select, **param_kwds)

File ~/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:444, in param(name, value, bind, empty, expr, **kwds)
    442     parameter.param_type = "selection"
    443 else:
--> 444     parameter.param = core.SelectionParameter(
    445         name=parameter.name, bind=bind, value=value, expr=expr, **kwds
    446     )
    447     parameter.param_type = "selection"
    449 return parameter

TypeError: altair.vegalite.v5.schema.core.SelectionParameter() got multiple values for keyword argument 'value'

豆知識: 映画『ジョーズ (Jaws)』や『ジョーズ2 (Jaws 2)』が公開された当時、PG-13のレーティングは存在していませんでした。初めてPG-13のレーティングを受けた映画は、1984年の『若き勇者たち (Red Dawn)』でした。

6.3.2. ビジュアライゼーションを動的クエリとして使用する#

標準的なインターフェースウィジェットは、_可能な_クエリパラメータの値を表示しますが、これらの値の_分布_を視覚化することはできません。また、1回に1つの値しか選択できない入力ウィジェットではなく、マルチバリューや範囲選択など、よりリッチなインタラクションを使用したい場合もあります。

これらの課題に対応するために、データを視覚化しながら動的クエリをサポートする追加のチャートを作成することができます。映画の年間ごとの本数のヒストグラムを追加し、範囲選択を使用して選択した期間中の映画を動的にハイライトしてみましょう。

年ごとのヒストグラムとインタラクションして、さまざまな時代の映画を探索してください。年をまたいだサンプリングバイアスの証拠が見られますか?(年と評論家の評価にはどのような関係がありますか?)

1930年から2040年までの年が含まれています!未来の映画が制作準備中なのでしょうか、それとも「1世紀の誤差」があるのでしょうか?また、タイムゾーンによっては1969年または1970年に小さなピークが表示されることがあります。なぜそのようなことが起こるのでしょうか?(ノートブックの最後で説明があります!)

brush = alt.selection_interval(
    encodings=['x'] # limit selection to x-axis (year) values
)

# dynamic query histogram
years = alt.Chart(movies).mark_bar().add_selection(
    brush
).encode(
    alt.X('year(Release_Date):T', title='Films by Release Year'),
    alt.Y('count():Q', title=None)
).properties(
    width=650,
    height=50
)

# scatter plot, modify opacity based on selection
ratings = alt.Chart(movies).mark_circle().encode(
    x='Rotten_Tomatoes_Rating:Q',
    y='IMDB_Rating:Q',
    tooltip='Title:N',
    opacity=alt.condition(brush, alt.value(0.75), alt.value(0.05))
).properties(
    width=650,
    height=400
)

alt.vconcat(years, ratings).properties(spacing=5)
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:65: AltairDeprecationWarning: 'add_selection' is deprecated. Use 'add_params' instead.
  warnings.warn(message, AltairDeprecationWarning, stacklevel=1)

上記の例では、チャート間の_リンクされた選択_を使用して動的クエリを提供しています:

  • interval選択(brush)を作成し、encodings=['x']を設定して選択をx軸のみに制限し、1次元の選択範囲を作成します。

  • 年ごとの映画本数のヒストグラムに対して、.add_selection(brush)を使用してbrushを登録します。

  • 条件付きエンコーディングでbrushを使用し、散布図のopacityを調整します。

このように、1つのチャートで要素を選択し、他の1つまたは複数のチャートでリンクされたハイライトを表示するインタラクション技術は、ブラッシング&リンク (Brushing & Linking)と呼ばれます。

6.4. パン&ズーム#

映画の評価散布図は、一部の場所で密集しており、より密な領域のポイントを詳しく調べるのが難しい場合があります。_パン_や_ズーム_といったインタラクション技術を使用すると、密集した領域をより詳細に調べることができます。

まず、Altairの選択を使用してパンとズームをどのように表現するかを考えてみましょう。チャートの「ビューポート」を定義するものは何でしょうか? 軸のスケールドメインです!

スケールドメインを変更することで、視覚化されるデータ値の範囲を変更できます。これをインタラクティブに行うには、interval選択をスケールドメインにバインドし、コードでbind='scales'を指定します。その結果、ドラッグやズーム可能なブラシではなく、プロットエリア全体をドラッグやズームで移動できるようになります。

以下のチャートで、ビューをパン(平行移動)するためにクリック&ドラッグしたり、ズーム(拡大縮小)するためにスクロールしてください。提供された評価値の精度について何か新しい発見がありますか?

alt.Chart(movies).mark_circle().add_selection(
    alt.selection_interval(bind='scales')
).encode(
    x='Rotten_Tomatoes_Rating:Q',
    y=alt.Y('IMDB_Rating:Q', axis=alt.Axis(minExtent=30)), # use min extent to stabilize axis title placement
    tooltip=['Title:N', 'Release_Date:N', 'IMDB_Rating:Q', 'Rotten_Tomatoes_Rating:Q']
).properties(
    width=600,
    height=400
)

ズームインすると、評価値の精度が限られていることがわかります!Rotten Tomatoesの評価は整数で、IMDBの評価は小数点第1位まで切り捨てられています。その結果、ズームしても同じ評価値を持つ複数の映画が重なって表示されます(オーバープロット)。

上記のコードを読むと、yエンコーディングチャネルでalt.Axis(minExtent=30)というコードがあることに気づくかもしれません。minExtentパラメータは、軸の目盛りやラベルのために最低限のスペースを確保するようにします。なぜこれをするのでしょうか?パンやズームを行うと、軸ラベルが変化し、軸タイトルの位置がシフトする可能性があります。minExtentを設定することで、プロット内の気を散らすような動きを減らすことができます。minExtentの値を変更してみてください(たとえば、ゼロに設定してください)。その後ズームアウトして、長い軸ラベルが表示領域に入ると何が起こるか確認してください。

Altairには、プロットにパン&ズーム機能を追加するための簡略化された方法も用意されています。選択を直接作成する代わりに、.interactive()を呼び出すことで、Altairがチャートのスケールにバインドされたインターバル選択を自動的に生成します:

alt.Chart(movies).mark_circle().encode(
    x='Rotten_Tomatoes_Rating:Q',
    y=alt.Y('IMDB_Rating:Q', axis=alt.Axis(minExtent=30)), # use min extent to stabilize axis title placement
    tooltip=['Title:N', 'Release_Date:N', 'IMDB_Rating:Q', 'Rotten_Tomatoes_Rating:Q']
).properties(
    width=600,
    height=400
).interactive()

デフォルトでは、選択のスケールバインディングにはxおよびyの両方のエンコーディングチャネルが含まれています。しかし、パンとズームを1つの次元(軸)に制限したい場合はどうすればよいでしょうか?encodings=['x']を指定することで、選択をxチャネルのみに制限することができます:

alt.Chart(movies).mark_circle().add_selection(
    alt.selection_interval(bind='scales', encodings=['x'])
).encode(
    x='Rotten_Tomatoes_Rating:Q',
    y=alt.Y('IMDB_Rating:Q', axis=alt.Axis(minExtent=30)), # use min extent to stabilize axis title placement
    tooltip=['Title:N', 'Release_Date:N', 'IMDB_Rating:Q', 'Rotten_Tomatoes_Rating:Q']
).properties(
    width=600,
    height=400
)

単一の軸に沿ってズームする場合、可視化されたデータの形状が変化し、データ内の関係性の認識に影響を与える可能性があります。適切なアスペクト比を選択することは、重要なビジュアライゼーションデザインの課題です!

6.5. ナビゲーション: オーバービュー + ディテール#

パンおよびズームでは、チャートの「ビューポート」を直接調整します。一方、_オーバービュー + ディテール_という関連するナビゲーション戦略では、オーバービュー表示を使用して_すべて_のデータを表示し、選択を通じて別のフォーカス表示をパンおよびズームできるようにします。

以下では、S&P 500株価指数の10年間の価格変動を示す2つのエリアチャートを表示しています。初期状態では、両方のチャートが同じデータ範囲を表示します。下部のオーバービュー チャートでクリック&ドラッグして、フォーカス表示を更新し、特定の時間範囲を調べてください。

brush = alt.selection_interval(encodings=['x']);

base = alt.Chart().mark_area().encode(
    alt.X('date:T', title=None),
    alt.Y('price:Q')
).properties(
    width=700
)
  
alt.vconcat(
    base.encode(alt.X('date:T', title=None, scale=alt.Scale(domain=brush))),
    base.add_selection(brush).properties(height=60),
    data=sp500
)

以前のパン&ズームのケースとは異なり、ここでは単一のインタラクティブチャートのスケールに選択を直接バインドするのではなく、選択を_別のチャート_のスケールドメインにバインドしたいと考えています。そのためには、フォーカスチャートのxエンコーディングチャネルを更新し、スケールのdomainプロパティにbrush選択を参照するよう設定します。

選択範囲が未定義(選択が空)である場合、Altairはブラシを無視し、基礎となるデータを使用してドメインを決定します。一方、ブラシ範囲が作成されると、Altairはその範囲をフォーカスチャートのスケールdomainとして使用します。

6.6. オンデマンド詳細表示#

一度可視化の中で興味深いポイントを見つけたら、それらについてもっと詳しく知りたくなることがよくあります。_オンデマンド詳細表示_は、選択された値についてより多くの情報をインタラクティブに照会する方法を指します。_ツールチップ_は、オンデマンドで詳細を提供する便利な手段の1つです。しかし、ツールチップは通常、1回に1つのデータポイントの情報しか表示しません。もっと多くの情報を表示するにはどうすればよいでしょうか?

映画の評価散布図には、Rotten TomatoesとIMDBの評価が一致しない興味深い外れ値がいくつか含まれています。インタラクティブにポイントを選択し、そのラベルを表示するプロットを作成しましょう。ホバーまたはクリックインタラクションのどちらでもフィルタクエリをトリガーするために、Altairの構成演算子 |(”または”)を使用します。

以下の散布図でポイントにマウスを移動すると、ハイライトとタイトルラベルが表示されます。Shiftキーを押しながらクリックすると、注釈が永続化され、複数のラベルを一度に表示できます。Rotten Tomatoesの批評家に愛される一方で、IMDBの一般観客には支持されない映画(またはその逆)はどれですか?同じ名前を持つ2つの異なる映画が誤って統合された可能性のあるエラーを見つけてみてください!

hover = alt.selection_single(
    on='mouseover',  # select on mouseover
    nearest=True,    # select nearest point to mouse cursor
    empty='none'     # empty selection should match nothing
)

click = alt.selection_multi(
    empty='none' # empty selection matches no points
)

# scatter plot encodings shared by all marks
plot = alt.Chart().mark_circle().encode(
    x='Rotten_Tomatoes_Rating:Q',
    y='IMDB_Rating:Q'
)
  
# shared base for new layers
base = plot.transform_filter(
    hover | click # filter to points in either selection
)

# layer scatter plot points, halo annotations, and title labels
alt.layer(
    plot.add_selection(hover).add_selection(click),
    base.mark_point(size=100, stroke='firebrick', strokeWidth=1),
    base.mark_text(dx=4, dy=-8, align='right', stroke='white', strokeWidth=2).encode(text='Title:N'),
    base.mark_text(dx=4, dy=-8, align='right').encode(text='Title:N'),
    data=movies
).properties(
    width=600,
    height=450
)
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/vegalite/v5/api.py:398: AltairDeprecationWarning: The value of 'empty' should be True or False.
  warnings.warn(
/Users/yuichiyazaki/.pyenv/versions/miniforge3-4.10.3-10/lib/python3.9/site-packages/altair/utils/deprecation.py:65: AltairDeprecationWarning: 'selection_multi' is deprecated.  Use 'selection_point'
  warnings.warn(message, AltairDeprecationWarning, stacklevel=1)

上記の例では、散布図に次の3つの新しいレイヤーを追加しています:

  1. 円形の注釈(アノテーション)。

  2. 視認性を高めるための白い背景テキスト。

  3. 映画タイトルを表示する黒いテキスト。

さらに、この例では2つの選択を組み合わせて使用しています:

  1. nearest=Trueを含むシングル選択(hover)。これは、マウスが移動すると自動的に最も近いデータポイントを選択します。

  2. シフトクリックで永続的な選択を作成するマルチ選択(click)。

両方の選択でempty='none'が設定されており、選択が空の場合はポイントを含めないことを示しています。これらの選択は1つのフィルタ条件に結合されます。つまり、hoverまたはclickの論理的な_または_(or)を使用して、いずれかの選択に含まれるポイントを含めます。この条件を使用して、新しいレイヤーをフィルタリングし、選択されたポイントに対してのみ注釈とラベルを表示します。

選択とレイヤーを使用することで、オンデマンドで詳細を表示するためのさまざまなデザインを実現できます!例えば、以下はテクノロジー株価のログスケールの時系列チャートで、マウスカーソルに最も近い日付にガイドラインとラベルを付けたものです:

# select a point for which to provide details-on-demand
label = alt.selection_single(
    encodings=['x'], # limit selection to x-axis value
    on='mouseover',  # select on mouseover events
    nearest=True,    # select data point nearest the cursor
    empty='none'     # empty selection includes no data points
)

# define our base line chart of stock prices
base = alt.Chart().mark_line().encode(
    alt.X('date:T'),
    alt.Y('price:Q', scale=alt.Scale(type='log')),
    alt.Color('symbol:N')
)

alt.layer(
    base, # base line chart
    
    # add a rule mark to serve as a guide line
    alt.Chart().mark_rule(color='#aaa').encode(
        x='date:T'
    ).transform_filter(label),
    
    # add circle marks for selected time points, hide unselected points
    base.mark_circle().encode(
        opacity=alt.condition(label, alt.value(1), alt.value(0))
    ).add_selection(label),

    # add white stroked text to provide a legible background for labels
    base.mark_text(align='left', dx=5, dy=-5, stroke='white', strokeWidth=2).encode(
        text='price:Q'
    ).transform_filter(label),

    # add text labels for stock prices
    base.mark_text(align='left', dx=5, dy=-5).encode(
        text='price:Q'
    ).transform_filter(label),
    
    data=stocks
).properties(
    width=700,
    height=400
)

これまでに学んだことを実践してみましょう:上記の映画散布図(年ごとの動的クエリがあるもの)を修正して、interval選択範囲に含まれるデータのIMDB(またはRotten Tomatoes)の平均評価を表示するruleマークを含めることができますか?

6.7. ブラッシング&リンクの再考#

以前このノートブックで、_ブラッシング&リンク_の例を見ました: 動的クエリヒストグラムを使用して映画評価散布図のポイントをハイライトする方法です。ここでは、リンクされた選択に関連する追加の例を見てみましょう。

carsデータセットに戻り、repeat演算子を使用して、マイレージ、加速、馬力の間の関係を示す散布図行列(SPLOM)を構築することができます。interval選択を定義し、繰り返し散布図仕様_内_に含めることで、すべてのプロット間でリンクされた選択を有効にします。

以下のプロットのいずれかでクリック&ドラッグして、ブラッシング&リンクを実行してください!

brush = alt.selection_interval(
    resolve='global' # resolve all selections to a single global instance
)

alt.Chart(cars).mark_circle().add_selection(
    brush
).encode(
    alt.X(alt.repeat('column'), type='quantitative'),
    alt.Y(alt.repeat('row'), type='quantitative'),
    color=alt.condition(brush, 'Cylinders:O', alt.value('grey')),
    opacity=alt.condition(brush, alt.value(0.8), alt.value(0.1))
).properties(
    width=140,
    height=140
).repeat(
    column=['Acceleration', 'Horsepower', 'Miles_per_Gallon'],
    row=['Miles_per_Gallon', 'Horsepower', 'Acceleration']
)

上記では、interval選択にresolve='global'が使用されている点に注目してください。デフォルト設定の'global'は、すべてのプロットにおいて一度に1つのブラシのみがアクティブになることを意味します。しかし、場合によっては複数のプロットでブラシを定義し、その結果を組み合わせたいことがあります。

  • resolve='union'を使用すると、選択はすべてのブラシの_和集合_になります。つまり、任意のブラシ内にあるポイントは選択されます。

  • 一方で、resolve='intersect'を使用すると、選択はすべてのブラシの_積集合_になります。つまり、すべてのブラシ内にあるポイントのみが選択されます。

resolveパラメータを'union''intersect'に設定して、選択ロジックがどのように変化するか試してみてください。

6.7.1. クロスフィルタリング#

これまで見てきたブラッシング&リンクの例では、選択に応じて不透明度の値を変更する条件付きエンコーディングを使用していました。もう1つの方法として、1つのビューで定義された選択を使用して、別のビューの内容を_フィルタリング_することもできます。

flightsデータセットを使って、delay(フライトの到着が早いか遅いかを示す、分単位)、飛行distance(マイル単位)、出発time(1日の時間帯)を表すヒストグラムのコレクションを作成してみましょう。repeat演算子を使用してヒストグラムを作成し、x軸にinterval選択を追加して、ブラシを積集合で解決します。

具体的には、各ヒストグラムは2つのレイヤーで構成されます:灰色の背景レイヤーと青色の前景レイヤー。前景レイヤーはブラシ選択の積集合によってフィルタリングされます。この結果、3つのチャート間で_クロスフィルタリング_のインタラクションが実現します!

以下のチャートでブラシ範囲をドラッグしてください。到着遅延が長いまたは短いフライトを選択すると、距離や時間の分布はどのように変化しますか?

brush = alt.selection_interval(
    encodings=['x'],
    resolve='intersect'
);

hist = alt.Chart().mark_bar().encode(
    alt.X(alt.repeat('row'), type='quantitative',
        bin=alt.Bin(maxbins=100, minstep=1), # up to 100 bins
        axis=alt.Axis(format='d', titleAnchor='start') # integer format, left-aligned title
    ),
    alt.Y('count():Q', title=None) # no y-axis title
)
  
alt.layer(
    hist.add_selection(brush).encode(color=alt.value('lightgrey')),
    hist.transform_filter(brush)
).properties(
    width=900,
    height=100
).repeat(
    row=['delay', 'distance', 'time'],
    data=flights
).transform_calculate(
    delay='datum.delay < 180 ? datum.delay : 180', # clamp delays > 3 hours
    time='hours(datum.date) + minutes(datum.date) / 60' # fractional hours
).configure_view(
    stroke='transparent' # no outline
)

クロスフィルタリングを通じて、遅延したフライトがより遅い時間に出発する傾向があることを観察できます。この現象は頻繁に飛行機を利用する人にはおなじみでしょう:遅延が1日の中で伝播し、その飛行機による後続の旅程に影響を与えることがあります。時間通りに到着する確率を高めるには、早朝のフライトを予約しましょう!

複数のビューとインタラクティブな選択の組み合わせにより、基本的なヒストグラムであっても、データセットに対して質問を投げかけるための強力な入力デバイスに変えることができる、価値のある多次元的な推論が可能になります!

6.8. サマリー#

Altairでサポートされているインタラクションオプションの詳細については、Altairインタラクティブ選択のドキュメントをご参照ください。また、複数のインタラクション技術を組み合わせたり、モバイルデバイスでのタッチ入力をサポートしたりするためのイベントハンドラのカスタマイズについては、Vega-Lite選択のドキュメントをご覧ください。

さらに学びたい方へ:

6.8.1. 付録: 時間の表現について#

以前、1969年または1970年の映画の数に小さな山があることを観察しました。この山はどこから来ているのでしょうか?そして、なぜ1969年または1970年なのでしょうか?その答えは、欠損データとコンピュータが時間をどのように表現しているかの組み合わせに起因します。

内部的には、日時はUNIXエポックに基づいて表現されます。この基準では、時間の「ゼロ」はUTC時刻で1970年1月1日午前0時(本初子午線に沿った時刻)に対応します。ところが、リリース日が欠損している(null)映画がいくつかあります。これらのnull値は時間の「0」として解釈され、結果的にUTC時刻で1970年1月1日午前0時に対応します。もしあなたがアメリカ大陸に住んでいる場合(つまり「早い」タイムゾーンにいる場合)、この時刻は現地時間では1969年12月31日の遅い時間に対応します。一方、本初子午線近くまたはその東に住んでいる場合、この時刻は現地時間で1970年1月1日となります。

このエピソードから得られる教訓は?データに対して常に懐疑的であるべきであり、データがどのように表現されているか(日時、浮動小数点数、緯度と経度、など)が、時には分析に影響を与えるアーティファクトを引き起こす可能性があることを心に留めておくべきです!