Matplotlib 軸上マウスオーバーの検出、座標系と変換

# <font color="Red">Matplotlibマウスオーバー：軸上の検出</font>
結論イメージから。
コード全体は[**こちら**](#コード全体)

軸上にマウスオーバーで```Mouse is on X( or Y) Axis```の表示。
軸上にないときはwindow上の位置を表示。

|アイドル状態|マウスがX軸上|マウスがY軸上|
|:-|:-|:-|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800268/3fe8eafc-f14e-402b-a251-f1be5338bc00.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800268/b2617ffe-bcb8-42df-bbf1-d46d2e7614e6.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800268/2ce4d08c-1d47-4e0a-8fe4-029504720ec7.png)|


# <font color="Red">目次</font>
- [**Matplotlibマウスオーバー：軸上の検出**](#matplotlibマウスオーバー軸上の検出)
- [**構成要素**](#構成要素)
    - [**ポイント抜粋**](#ポイント抜粋)
    - [**座標系**](#座標系)
    - [**知っておくべきこと**](#知っておくべきこと) 
- [**コード**](#コード)
    - １．[matplotlibをcanvasに](#１matplotlibをcanvasに)
    - ２．[マウス位置取得（display座標系）](#２マウス位置取得display座標系)
    - ３．[display座標系をdata座標系に変換](#３display座標系をdata座標系に変換)
    - ４．[「軸上」判定して、yesの場合に処理](#４軸上判定してyesの場合に処理)
- [**コード全体**](#コード全体)


# <font color="Red">構成要素</font>
- 使用した要素
    - matplotlib
    - tkinter
    - matplotlib拡張：```from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk```
- 肝となる情報
    - 座標系：
        - Display座標系
        - Data座標系

**座標系**、変換行列を返すことを理解すればOK。

## <font color="HotPink">ポイント抜粋</font>

```python
...
# マウスの動きをキャッチするイベントハンドラを設定
canvas.mpl_connect("motion_notify_event", lambda event: on_mouse_move(event,ax))
...
# マウスの位置を表示する関数
def on_mouse_move(event,ax):
    x, y = event.x, event.y
    # 変換行列：display座標系→data座標系への変換
    invMatrix = ax.transData.inverted()
    # data座標系でのマウス位置を取得
    (data_x, data_y) = invMatrix.transform([x,y])
    # mouseの位置をcanavs上にlabelとして表示
    label.config(text=f"Mouse position: ({x}, {y}), in data: ({data_x},{data_y})")
...
# マウス位置が軸上か判定する関数
def mouse_on_axis(data_x, data_y, ax):
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()
    if data_y < ylim[0] and xlim[0]<data_x<xlim[1]:
        label.config(text=f"Mouse is on X axis")
    elif data_x < xlim[0] and ylim[0]<data_y<ylim[1]:
        label.config(text=f"Mouse is on Y axis")
```

## <font color="HotPink">座標系</font>
matplotlib公式、Transformations Tutrialより、3つだけ抜粋。

|座標系|説明|各座標系からDisplay座標系への変換|
|:-|:-|:-|
|data|axes内のデータ座標系<br>散布図プロットした際の(x,y)の値等価|ax.transData|
|axes|dataと同じ領域を比率表示。左下を(0,0)、右上を(1,1)<br>annotateの'axes fraction'と同じイメージ|ax.transAxes|
|display|出力のネイティブ座標系、window左下が(0,0)<br>単位はシステム依存(バックエンドに依る)<br>Aggの場合はpixel、svg、pdfの場合はpoints|None, or IdentityTransform()|

ほか、figure、xaxisなどがあるが割愛。
補足：ax.trans*XXX*.<font color="Red">**inverted()**</font>とすると、各display座標系から**システム座標系への逆変換**が可能。

## <font color="HotPink">知っておくべきこと</font>
取得する座標系と、評価するための座標系
- マウスevent取得値 = display座標系（Window基準値）
- 「軸上」の検出 = data座標系（負の値はグラフ外 = 軸上）
→　<font color="Red">__座標変換__</font>が必要

(event.x, event.y)[display座標系] →　(data_x, data_y)[data座標系]

<details><summary>詳細の説明　：　(クリックで展開)</summary>

- 後述の「マウス移動検出」で得られるevent.x、event.yはdisplay座標系
- 「軸上」判定は、data座標系を用いると大雑把に以下に定義できる
    - y軸上にマウスがある　＝　x値がマイナス＠data座標系
    - x軸上にマウスがある　＝　y値がマイナス＠data座標系

よって、座標変換が必要となる。
display座標系　→　data座標系　→　data座標系上での判定
<font color="Red">変換行列</font>を取得して演算する、という処理（関数がありユーザは意識しなくてもよい）
この変換行列が、__ax.transData__ など。
戻り値はAffine変換の行列。

</details>

# <font color="Red">コード</font>
章分けして以下を書く。
１．[matplotlibをcanvasに](#１matplotlibをcanvasに)
２．[マウス位置取得（display座標系）](#２マウス位置取得display座標系)
３．[display座標系をdata座標系に変換](#３display座標系をdata座標系に変換)
４．[「軸上」判定して、yesの場合に処理](#４軸上判定してyesの場合に処理)

## <font color="HotPink">１．matplotlibをcanvasに</font>
ポイント
- tkinterのwindowを作成　：　root=tk.Tk()
- fig（ax）にグラフを作成　：　ax.scatter()
- FigureCanvasTkAggで、fig, rootを引数としたcanvasを作成　：　canvas = FigureCanvasTkAgg(fig, master=root)

```python
### matplotlibを自作のcanvasに描画する
# -> matplotlib.backend.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk

import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as tk

# Windowクローズ時の処理を定義
def on_closing():
    print("Window closed")
    root.quit()
    root.destroy()

# Tkinterウィンドウの作成
root = tk.Tk()
root.title("Matplotlib with Tkinter")

# グラフの描画
fig, ax = plt.subplots()
ax.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])

# FigureCanvasTkAggオブジェクトの作成
canvas = FigureCanvasTkAgg(fig, master=root)
# .draw():canvas上にfigを描画、およびfigの更新を再描画
canvas.draw()

# canvasをtkinterのwidgetとして、pack()で配置
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)

# 補足：matplotlib付属のtoolbarを表示
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
toolbar.pack(side=tk.BOTTOM, fill=tk.X)  # 位置を変更


# クローズで終了する為に、Windowを閉じたときに事前定義のon_close()を呼び出し
root.protocol("WM_DELETE_WINDOW", on_closing)

# ウィンドウのメインループ
root.mainloop()
print("finish")
```


## <font color="HotPink">２．マウス位置取得（display座標系）</font>
ポイント
- マウス位置を取得する関数を定義、「event」変数から位置を取得 ```def on_mouse_event(event)```
- マウスイベントは```canvas.mpl_connect("motin_notify_event",on_mouse_move)```　で

```python
def on_mouse_move(event):
    x, y = event.x, event.y
###　中略　###
# マウスの動きをキャッチするイベントハンドラを設定
canvas.mpl_connect("motion_notify_event", on_mouse_move)
###　後略 ###
```

ここでは、(x,y)は**display座標系**が取得される（windowの左下隅を(0,0)とした値、単位はピクセル）。
コード全体。

```python
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as tk

# マウスの位置を表示する関数
def on_mouse_move(event):
    x, y = event.x, event.y
    # mouseの位置をcanavs上にlabelとして表示
    label.config(text=f"Mouse position: ({x}, {y})")

# ~~~中略:canvas定義からmainloop()の間に記述する~~~

# マウスの動きをキャッチするイベントハンドラを設定
canvas.mpl_connect("motion_notify_event", on_mouse_move)

# マウスの位置を表示するラベルを作成してウィンドウに配置
label = tk.Label(root, text="Move the mouse to see its position")
label.pack()

# ~~~以下略。前項１．を参照~~~
```


## <font color="HotPink">３．display座標系をdata座標系に変換</font>
ポイント
```python
# 変換行列invMatrix定義
invMatrix = ax.transData.inverted()
# data座標系に変換
(data_x, data_y) = invMatrix.transform([x,y])
```
- 前項の「event.x」、「event.y」は<font color="Red">display座標系</font>の位置
- 逆変換行列(display座標系→Data座標系)を取得　：　```invMatrix = ax.transData.inverted()```
- ```.transform()```で、display→dataへ座標系変換　：　```(data_x, data_y) = invMatrix.transform([x,y])```
- 1つにまとめて、<font color="Red">```ax.transform.inverted().transform([x,y])```</font>でも可能
transform()の引数はarray-likeなので、list型で渡してtupleで受ける、という形にしている。

当該箇所のコード（マウス位置取得関数部）
```python
# マウスの位置を表示する関数
def on_mouse_move(event):
    x, y = event.x, event.y
    # 変換行列：display座標系→data座標系への変換
    invMatrix = ax.transData.inverted()
    # data座標系でのマウス位置を取得
    (data_x, data_y) = invMatrix.transform([x,y])
    # mouseの位置をcanavs上にlabelとして表示
    label.config(text=f"Mouse position: ({x}, {y}), in data: ({data_x},{data_y})")
```
あとは、「軸上」判定。次に示す。

## <font color="HotPink">４．「軸上」判定して、yesの場合に処理</font>
ポイント
```python
# マウスの位置を表示する関数
def on_mouse_move(event,ax):
    x, y = event.x, event.y
    # 変換行列：display座標系→data座標系への変換
    invMatrix = ax.transData.inverted()
    # data座標系でのマウス位置を取得
    (data_x, data_y) = invMatrix.transform([x,y])
    # mouseの位置をcanavs上にlabelとして表示
    label.config(text=f"Mouse position: ({x}, {y}), in data: ({data_x},{data_y})")

    mouse_on_axis(data_x, data_y, ax)

# xlim、ylim（xaxis, yaxisオブジェクト）をcanvas(fig,master=root)から受け取る場合。
# 引数付き呼び出しをする。
# イベントハンドラから「lambda関数」を使用して引数を渡す。
# canvas全体をクラスとして作成し、メンバ関数にイベントを作成するならself.axから取得すればよい
# 今回はグローバルなaxを使用しているので実は引数付き呼び出し不要
def mouse_on_axis(data_x, data_y, ax):
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()
    if data_y < ylim[0] and xlim[0]<data_x<xlim[1]:
        label.config(text=f"Mouse is on X axis")
    elif data_x < xlim[0] and ylim[0]<data_y<ylim[1]:
        label.config(text=f"Mouse is on Y axis")
```

- 軸上：data座標系で、「x < xlim[0], ylim[0] < y < ylim[1]」のときY軸上と判断
- 軸上判定の関数を作り、呼び出して判定する

# <font color="Red">コード全体</font>
結論コード全体

```python
### matplotlibを自作のcanvasに描画する
# -> matplotlib.backend.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk

import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as tk

# Windowクローズ時の処理を定義
def on_closing():
    print("Window closed")
    root.quit()
    root.destroy()

# マウスの位置を表示する関数
def on_mouse_move(event,ax):
    x, y = event.x, event.y
    # 変換行列：display座標系→data座標系への変換
    invMatrix = ax.transData.inverted()
    # data座標系でのマウス位置を取得
    (data_x, data_y) = invMatrix.transform([x,y])
    # mouseの位置をcanavs上にlabelとして表示
    label.config(text=f"Mouse position: ({x}, {y}), in data: ({data_x},{data_y})")

    mouse_on_axis(data_x, data_y, ax)

# xlim、ylim（xaxis, yaxisオブジェクト）をcanvas(fig,master=root)から受け取る場合。
# 引数付き呼び出しをする。
# イベントハンドラから「lambda関数」を使用して引数を渡す。
# canvas全体をクラスとして作成し、メンバ関数にイベントを作成するならself.axから取得すればよい
# 今回はグローバルなaxを使用しているので実は引数付き呼び出し不要
def mouse_on_axis(data_x, data_y, ax):
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()
    if data_y < ylim[0] and xlim[0]<data_x<xlim[1]:
        label.config(text=f"Mouse is on X axis")
    elif data_x < xlim[0] and ylim[0]<data_y<ylim[1]:
        label.config(text=f"Mouse is on Y axis")

# Tkinterウィンドウの作成
root = tk.Tk()
root.title("Matplotlib with Tkinter")

# グラフの描画
fig, ax = plt.subplots()
ax.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])

# FigureCanvasTkAggオブジェクトの作成
canvas = FigureCanvasTkAgg(fig, master=root)
# .draw():canvas上にfigを描画、およびfigの更新を再描画
canvas.draw()

# canvasをtkinterのwidgetとして、pack()で配置
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)

# 補足：matplotlib付属のtoolbarを表示
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
toolbar.pack(side=tk.BOTTOM, fill=tk.X)  # 位置を変更

# マウスの動きをキャッチするイベントハンドラを設定
canvas.mpl_connect("motion_notify_event", lambda event: on_mouse_move(event,ax))

# マウスの位置を表示するラベルを作成してウィンドウに配置
label = tk.Label(root, text="Move the mouse to see its position")
label.pack()

# クローズで終了する為に、Windowを閉じたときに事前定義のon_close()を呼び出し
root.protocol("WM_DELETE_WINDOW", on_closing)

# ウィンドウのメインループ
root.mainloop()
print("finish")

```