LoginSignup
9
13

More than 1 year has passed since last update.

【Python】jupyterでplotlyのクリックイベントを起こす

Last updated at Posted at 2022-05-16

pythonでjupyter notebook上でplotlyを使ってこんなことが出来ます。
first.gif
plotlyで散布図をクリックしたときに何か起こせないの気持ち悪いなあ、JavaScript使わないとできないの不便だなあ、ということを1年くらい思っていました。Pythonだけでできました。私が知らなかっただけでした。

なかなか便利そうな匂いがしますが、公式のドキュメントにもあまり情報がないみたいです1。ということで、これを使えるようになることがこの記事の目標です
とりあえずコード全体はこちらになります。以下一歩一歩見ていきます。

df = pd.read_csv('./data/articles.csv')
fig = go.FigureWidget(px.scatter(df, x='page_views_count', y='likes_count'))

n = len(df)
scatter = fig.data[0]
colors = ['blue'] * n
scatter.marker.color = colors
scatter.marker.size = [10] * n

def update_point(trace, points, selector):
    c = list(scatter.marker.color)
    s = list(scatter.marker.size)
    for i in points.point_inds:
        c[i] = 'red'
        s[i] = 20
        with fig.batch_update():
            scatter.marker.color = c
            scatter.marker.size = s

scatter.on_click(update_point)
fig

環境

Windows 11
Python 3.8.5
jupyter 1.0.02
plotly 5.7.0

使用データ

本記事はdelikaさんの下記イベントに参加しています。参加するつもりだったのですが、いつのまにかGWが終わり5月も中旬になってました。おかしいなぁ。
データはイベントで用意されているQiitaの投稿に関するダミーデータを用います3

Widgetについて理解する

plotlyのこの機能はipywidgetsを使っています4。ipywidgetはこんな感じのことができます

import ipywidgets as widgets

def on_click_callback(clicked_button):
    print('Clicked')

button = widgets.Button(description='Click me')
button.on_click(on_click_callback)
button

button.gif

on_click というメソッドで、クリックしたときに呼び出されるcallback関数を登録することができます。この例だと、ボタンをclickしたときに on_click_callback というコールバック関数が呼び出されるわけです。

plotlyでは、go.FigureWidgetを使うことでWidgetとして動かせる(=クリックイベントを起こせる)グラフを作れます。FigureWidgetのTrace( fig.data でアクセスできるScatterなどのgraph_objs。今回の例ではscatter)が on_click というメソッドを持っていて、この例のボタンと同じように機能します。

fig = go.FigureWidget(px.scatter(df, x='page_views_count', y='likes_count'))
scatter = fig.data[0]

def update_point(trace, points, selector):
    pass

scatter.on_click(update_point)

callback関数を理解する

ではcallback関数を見ていきましょう。なんとなく色とサイズを変えていることはイメージできると思います。
引数から説明していきましょう。

def update_point(trace, points, selector):
    c = list(scatter.marker.color)
    s = list(scatter.marker.size)
    for i in points.point_inds:
        c[i] = 'red'
        s[i] = 20
        with fig.batch_update():
            scatter.marker.color = c
            scatter.marker.size = s

callbackの引数①:trace

traceは scatter.on_click(update_point)scatter に相当するものです5

callbackの引数②:points

こちらはクリックされたポイントの情報が入ります。 update_point を以下のように書き換えて適当な点をクリックすると

def update_point_example(trace, points, selector):
    print(points)

image.png
このような内容が出力されます。これは、398行目をクリックして、そのxの値(page_views_count)が484、yの値(likes_count)が1177ということを示しています6

update_point の例だと、このクリックした points に該当する点の色とサイズを point_inds 経由で指定しています。ちなみに on_click だと一度にクリックされるポイントの数は高々ひとつだと思うので、for文で回している理由はあまりないです7

callbackの引数③:selector

これは今回の例では使われていませんが、先ほどと同じようにprintして中身を確かめましょう8。すると、以下が出力されます。
image.png
ctrlやaltなどが出てくるので、もしやと思ってctrlを押しながらクリックすると、 ctrl=True に変わります。つまり、押し方よって挙動を変えられるという訳です9
ちなみに button、buttonsは右クリックをすると値が変わりました。metaはちょっとよく分からなかったです。

batch_updateは何をしてるか

複数の処理を一度に行うために batch_update を使います。 batch_update がない、以下のgifを見比べると分かりやすいです。
no_batch.gif
2段階で処理がなされていることが分かります。これをいっぺんに変えるのが batch_update の役割です。


これで、冒頭に述べた例がどういう仕組みで動いているか理解できたと思います。

注意点!!
plotlyでは fig.show() と書くことが一般的ですが、FigureWidgetの場合は単に fig あるいは、 display(fig) とする必要があります(fig.show() とすると、クリックしても何も反応しないグラフが表示されます)。

以下、色々なTipsや応用例などをご紹介していきます。

複数のtraceがある場合どうすればいいか

plotlyを使っていると、Traceが複数ある図を作るケースも多くあります。たとえば、plotly.expressで color を指定すると複数Traceになります。先ほどみたように on_click はTraceの中身に対して呼ぶものなので、for文で全部に対して on_clike を定義してあげればOKです。

df['type'] = np.random.randint(0, 2, len(df))
fig = go.FigureWidget(px.histogram(df, x='likes_count', color='type'))
# len(fig.data) # 2

def update_point(trace, points, selector):
    print('clicked')

for hist in fig.data:
    hist.on_click(update_point)
fig

ただしひとつ罠があります。上の例だと、このようになってしまいます。
for.gif
1回のクリックに対して、Traceの数ぶんだけcallback関数が呼ばれるため、ここでは2回 print('clicked') が実行されてしまっています。

これはクリックした点や棒を持たないTraceもcallback関数を呼び出すことが原因です。したがって対処するには、if points.point_inds: とか for point in points.point_inds: とかで、空のpointに対しては何も動かさないようにcallback関数を書き換えてあげればOKです10

def update_point(trace, points, selector):
    if points.point_inds:
        print('clicked')

figureの見た目を変える以外の使い方

いくらでもあると思いますが、自分がパッと思いついたものを羅列しておきます。他にもアイデアなどあったらぜひコメントで教えてください!

複数のグラフを連動させる

こちらで紹介されている例です。上のグラフで日付を選ぶと、その日のデータが下の左右のグラフに出力されます。
image.png

hoverで表示している内容をcopyする

plotlyの使いずらいポイントのひとつに、hoverされた情報をコピーできない点があります
pyperclipとかを使えば対処できます。

import pyperclip as pc

def update_point(trace, points, selector):
    for i in points.point_inds:
        pc.copy(i)
        print('copied!')

urlを開く

webbrowserを使ってクリックしたら開くようにするとか、

import webbrowser

def update_point(trace, points, selector):
    webbrowser.open('http://example.com')

リンク付きのHTMLを表示するとかができそうです。

from IPython.display import HTML

def update_point(trace, points, selector):
    link_text = f'<a href={url}>open link</a>'
    display(HTML(link_text))

一度出力を消す

printとかを多用してると溜まって見にくいので、こうすることができます(が、レスポンスは悪いのでおそらく微妙です)。これまでにクリックした一連のものを対象にしたdataframeを出力するとかができます。

from IPython.display import clear_output

def update_point(trace, points, selector):
    # 一旦出力を消す
    clear_output(True)
    # figureを再表示
    display(fig)
    print('clicked')

jupyter以外で使う

ipywidgetを用いているので、jupyter以外での使用は私の知る限り難しいと思います。JupyterLabでは使用できますが、extensionとかの設定がいるかもしれません11。Google Colab だとできないみたいです12

Dashで似たようなことをやる方法は以下で紹介されています。

streamlitでは、以下を使うとできるかもですが、本記事で紹介する方法とは別物と思われます(特に確認などは出来てません汗)。

クリック以外のイベント

on_hover

scatter.on_hover(update_point)
fig

hover.gif

クリックでなく、ホバーしたときにアクションが起こるようになります。

on_unhover

※ on_hoverとほぼ同じなので略

on_selection

plotlyのレイアウトの dragmode を変更して利用します。矩形で選択したものを対象にします。update_point 関数のなかでfor文を使って点を取り出していたのは、これでも同じように使えるというメリットがあります。

fig.update_layout(dragmode="select") 
scatter.on_selection(update_point)
fig

selection.gif

on_deselect

def deselect_point(trace, points):
    print('deselected')

fig.update_layout(dragmode="select") 
scatter.on_deselect(deselect_point)
fig

同じくdragmodeにて、selectionが解消された時に実行されます。この場合、callback関数は選択していた対象は受け取らないため points は空になります。また selector も受け取らないことに注意です。

on_change

def zoom(layout, x_range):
    print('xaxis updated, new range is: ', x_range)

fig.layout.on_change(zoom, 'xaxis.range')
fig

change.gif
こいつだけ若干異質で、何かが変わったことに駆動されてcallback関数が実行されます。on_change の一つ目の引数がcallback関数で、二つ目の引数に「何が変わったときにcallback関数を呼ぶか」を指定します。ここではx軸の範囲aが変わったときをトリガーにしています。

最後に

色々できて面白いですね。jupyter以外で使えなさそうなので、使いどころは難しい気もしますが。最後に、リサーチ中に発掘したplotly, matplotlibのあまり知られていなさそうな情報を共有して終わりにします!

jupyterでならmatplotlibでもインタラクティブなグラフは作れるみたいです。

しかも点自体を動かせるプロットも可能とのこと。

plotlyはお絵かきできる機能もあるんですね。

  1. https://plotly.com/python/click-events/ 。こちらはこのドキュメントとほぼ同じ例です。ただし色とデータだけ変えています。

  2. 以下で述べる通りipywidgetsを用いているので、Google colaboratoryでは使用できないみたいです。jupyter Labでは使用できますが、extensionとかの設定がいるかもしれません (https://plotly.com/python/getting-started/#jupyterlab-support) 。

  3. https://delika.io/qiita_delika_article_campaign/QiitadelikaDummy

  4. ipywidgetsについての詳細は「Python: ipywidgets で Jupyter に簡単な UI を作る 」などが参考になります。

  5. そのため、 update_point 関数において scattertrace で書き換えても同じ挙動をします。また、 update_point を次のように変えてクリックしてもエラーは起きません:def update_point(trace, points, selector): assert trace == scatter

  6. trace_name は特に定義してないので空です。 trace_index は、 f はtraceをひとつしか持ってないので0になってます。それにしても(ダミーデータだからですが)likesの数がPV数を上回るというおそろしい記事ですね。

  7. 後述する on_selection とかだとこれがありがたいので、このまま使っています。

  8. def update_point_example(trace, points, selector): print(selector)

  9. Windowsでやってます。Macだとctrlがcommandとかになるんですかね?

  10. この例だとpointが適当な命名なのかは微妙なところですが、先ほどと変えても混乱しそうなのでこのままにしてます。

  11. https://plotly.com/python/getting-started/#jupyterlab-support

  12. https://github.com/plotly/plotly.py/issues/2202

9
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
13