morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】matplotlibでマウスクリックイベントを紐づける

はじめに

今回はmatplotlibで作成して表示したグラフに対し、マウスでクリックしたり、動かしたりするようなイベントを紐づける方法について紹介します。
これでどういうことができるようになるかですが、今回は以下のことをやってみたいと思います。

  • グラフエリア内をマウスでクリックした際にその座標を取得する
  • マウスでクリックして点を描画する

他にもこの機能を使えば、いろんなことができそうですが、長くなりそうなので次回以降に回します。

matplotlibで紐づけられるイベント

イベント一覧は以下です。

イベント 内容
'button_press_event' マウスボタンをクリック
'button_release_event' マウスボタンを離す
'close_event' figureを閉じる
'draw_event' グラフを更新する
'key_press_event' キーを押す
'key_release_event' キーを離す
'motion_notify_event' マウスを動かす
'pick_event' figure内の要素を選ぶ
'resize_event' ウィンドウサイズを変える
'scroll_event' スクロールホイールを操作
'figure_enter_event' カーソルがfigureに入る
'figure_leave_event' カーソルがfigureから出る
'axes_enter_event' カーソルがaxes内に入る
'axes_leave_event' カーソルがaxes内から出る

以下の公式ドキュメントに詳細及びコードサンプルも載っています
https://matplotlib.org/stable/users/explain/event_handling.html#

それでは実際にやっていきましょう

コード実装、解説

マウスでクリックした場所の座標を取得

これは公式に実装例があったので参考にさせてもらいました。

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.random.rand(10))

def onclick(event):
    print('{} click: button={}, x={}, y={}, xdata={}, ydata={}'.format(
        'double' if event.dblclick else 'single', event.button,
         event.x, event.y, event.xdata, event.ydata,
    ))

cid = fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()

動作は以下のようになります。

ポイント

イベントの紐づけに共通するのは、イベント発生時に呼ばれる関数を定義して、それをfigureに紐づけることです。
クリック時に呼ばれる関数

def onclick(event):
    print('{} click: button={}, x={}, y={}, xdata={}, ydata={}'.format(
        'double' if event.dblclick else 'single', event.button,
         event.x, event.y, event.xdata, event.ydata,
    ))

引数にeventを入れておくことで、イベントのクラス変数を呼び出すことができます。
今回の場合はevent.buttonがどのボタンが押されたかで、1が左クリック、2がミドルクリック、3が右クリックになるみたいです。
event.x, event.yはグラフのウィンドウ内でのマウスのx,y座標です(左下が0)。
event.xdata, event.ydataはaxes内でのマウスのx,y座標です。
axesの外でクリックするとNoneを返します

紐づけ

cid = fig.canvas.mpl_connect('button_press_event', onclick)

クリックしたらその座標を表示するので'button_press_event'をfigureに紐づけます。

axesの外でクリックした場合の対処

座標値Noneが出力されてしまうので、axesの外でクリックした際は出力しないように改善します。
onclick関数を以下のように書き換えればOK。

def onclick(event):
    if event.xdata == None or event.ydata == None:
        return
    else:
        print('{} click: button={}, x={}, y={}, xdata={}, ydata={}'.format(
            'double' if event.dblclick else 'single', event.button,
             event.x, event.y, event.xdata, event.ydata,
        ))
マウスでクリックした点の座標をグラフ内に表示

ついでに座標をグラフ内に表示してみます。
onclick関数を以下のように書き直せばOKです。
タイトルにクリックした座標を出力するようにしてみましょう

def onclick(event):
    if event.xdata == None or event.ydata == None:
        return
    else:
        print('{} click: button={}, x={}, y={}, xdata={}, ydata={}'.format(
            'double' if event.dblclick else 'single', event.button,
             event.x, event.y, event.xdata, event.ydata,
        ))
        ax.set_title('x = {}, y = {}'.format(event.xdata, event.ydata))
        plt.draw()

グラフを更新する必要があるので、plt.draw()を追加しグラフ要素を再描画します。
グラフはこんな感じになります



マウスクリックで点を描画する

先ほどまでのコードを使って、クリックしたらその座標に点を打つプログラムを作ってみましょう

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ln, = ax.plot([],[],'bo')

def onclick(event):
    x = event.xdata
    y = event.ydata

    ln.set_data(x,y)
    ax.set_title('x = {}, y = {}'.format(event.xdata, event.ydata))
    plt.draw()

fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()

動作はこんな感じです。

ポイント

点のプロットはlnという名前で事前に定義しておき、空のデータを入れておきます。
マウスをクリックした際に、ln.set_data(x, y)でその空のデータの中に毎回x,y座標を設定することで点が描画されるという構成になっています。
こちらもグラフ要素を更新するのでplt.draw()は必須です。
以前のmatplotlibでアニメーションを作った記事でも同じような方法でグラフを更新していました。
【python】matplotlibでアニメーションを作る方法について解説 - morikomorou’s blog

カーソルを表示する

クリックするまでグラフ上にどのように点が描けるのかわかりづらいので、カーソルを追尾して点を描画してみます。
マウス追尾用にcurというplot要素を追加しておきます。
マウスが動いているというイベントに対応させたいので、'motion_notify_event'を追加で紐づけ、curを操作します。
以下のようにコードを変更します。

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ln, = ax.plot([],[],'bo')
cur, = ax.plot([],[],'ro')

def motion(event):
    x = event.xdata
    y = event.ydata

    cur.set_data(x,y)
    plt.draw()

def onclick(event):
    x = event.xdata
    y = event.ydata

    ln.set_data(x,y)
    ax.set_title('x = {}, y = {}'.format(event.xdata, event.ydata))
    plt.draw()

fig.canvas.mpl_connect('button_press_event', onclick)
fig.canvas.mpl_connect('motion_notify_event', motion)
plt.show()

動作は以下です。

だいぶわかりやすくなったんじゃないでしょうか?

クロスヘアを表示する

点を打ちたいポイントx座標、y座標を確認してから点を描画したいという人もいるかと思います。
そういう方のために、マウスカーソルを点ではなくクロスヘアで追尾してみましょう。
コードは下記に変更します。

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ln, = ax.plot([],[],'bo')
ax.set_xlim([-0.05, 0.05])
ax.set_ylim([-0.05, 0.05])
cur_v = ax.axvline(-1)
cur_h = ax.axhline(-1)

def motion(event):
    x = event.xdata
    y = event.ydata
    cur_v.set_xdata(x)
    cur_h.set_ydata(y)
    plt.draw()

def onclick(event):
    x = event.xdata
    y = event.ydata

    ln.set_data(x,y)
    ax.set_title('x = {}, y = {}'.format(event.xdata, event.ydata))
    plt.draw()

fig.canvas.mpl_connect('button_press_event', onclick)
fig.canvas.mpl_connect('motion_notify_event', motion)
plt.show()

動作は以下です。

クロスヘアが最初に範囲外にプロットしておくためにx軸、y軸の範囲をset_xlim, set_ylimで設定後、クロスヘアの座標の初期値をその範囲外にしておきました。

終わりに

グラフとイベントの紐づけは今回のような謎のクリックプロットだけではなく、様々な使い道があると思いますのでおいおい記事にしていきたいと思います。