【Python】【matplotlib】マウスのホイールでグラフを拡大縮小する

2024-04-03matplotlib,python

はじめに

Pythonのmatplotlibを使って表示されたグラフに対して、マウスのホイールを使って拡大縮小できるようにしてみたので、そのメモです。グラフを頻繁に拡大縮小するような作業がある場合には便利だと思います。

やりたいこと

  1. マウスのホイールを回すとx軸を拡大縮小する
  2. Ctrlキーを押しながらホイールを回すとy軸を拡大縮小する
  3. 拡大縮小の中心はカーソル位置とする

なお、この後のエントリにて、表示範囲をパン(並行移動)する機能を追加で実装しています。こちらも合わせてご覧ください。

マウス操作・キーボード操作の受け取り方法

グラフで独自のマウス操作・キーボード操作を行うためには、マウスやキーボードによって発生した操作をイベントとして受け取る必要があります。これには、mpl_connect()を使います。

mpl_connect()

mpl_connect()の使い方は以下のようになります。

import matplotlib.pyplot as plt
fig, ax = plt.subplots()
fig.canvas.mpl_connect(イベント名, コールバック関数)

イベント名は文字列で指定します。以下のものが定義されています。

イベント名内容
'button_press_event'マウスのボタンがクリックされたとき
'button_release_event'マウスのボタンが離されたとき
'draw_event'グラフが描画されたとき
'key_press_event'キーが押されたとき
'key_release_event'キーが離されたとき
'motion_notify_event'グラフ内でカーソルが移動したとき
'pick_event'データ点がクリックされたとき
'resize_event'グラフの大きさが変化したとき
'scroll_event'マウスのホイールが回されたとき
'figure_enter_event'グラフにカーソルが入ったとき
'figure_leave_event'グラフからカーソルが出たとき
'axes_enter_event'個別のグラフにカーソルが入ったとき
'axes_leave_event'個別ののグラフからカーソルが出たとき
'close_event'グラフが閉じられたとき

使用例

たとえば、マウスのホイールが回されたときには、'scroll_event'を指定します。

import matplotlib.pyplot as plt

def on_scroll(event):
    if event.button == 'up':
        print("wheel up")
    elif event.button == 'down':
        print("wheel down")

fig, ax = plt.subplots()
fig.canvas.mpl_connect('scroll_event', on_scroll)

ホイールを回した方向はevent.buttonで取得できます。上方向がup、下方向がdownです。上の例では、ホイールを回すごとに回した方向がコンソールに出力されます。

Ctrlキーとマウス操作を組み合わせる

キーボード操作とホイール操作を組み合わせる場合、キーボード操作のイベントも取得する必要があります。これには、'key_press_event''key_release_event'も使う必要があります。Ctrlキーが押された状態を保持するため、グローバル変数で共有する必要があります。

実装例

import matplotlib.pyplot as plt

ctrl_flag = False

def on_key_press(event):
    global ctrl_flag

    if event.key == 'control':
        ctrl_flag = True

def on_key_release(event):
    global ctrl_flag

    if event.key == 'control':
        ctrl_flag = False

def on_scroll(event):
    global ctrl_flag

    if event.button == 'up':
        if ctrl_flag:
            print("wheel up(ctrl)")
        else:
            print("wheel up")
    elif event.button == 'down':
        if ctrl_flag:
            print("wheel down(ctrl)")
        else:
            print("wheel down")

fig, ax = plt.subplots()
fig.canvas.mpl_connect('key_press_event', on_key_press)
fig.canvas.mpl_connect('key_release_event', on_key_release)
fig.canvas.mpl_connect('scroll_event', on_scroll)

押されたキーの内容はevent.keyで取得できます。Ctrlキーが押されたらグローバル変数のフラグを立て、離されたらフラグを解除します。

ホイールによる拡大縮小の実装

上記を応用して、実際に実装してみます。なお複数のグラフを作成した場合、グローバル変数を使うと特定のグラフのみ拡大縮小に対応させる場合などで使いにくくなるので、クラスで実装することとします。

import matplotlib.pyplot as plt

class MplZoomHelper:
    def __init__(self, ax):
        self.ax = ax
        self.ctrl_flag = False
        self.zoom_rate = 0.9

        ax.figure.canvas.mpl_connect('key_press_event', self.on_key_press)
        ax.figure.canvas.mpl_connect('key_release_event', self.on_key_release)
        ax.figure.canvas.mpl_connect('scroll_event', self.on_scroll)

    def on_key_press(self, event):
        if event.inaxes is self.ax and event.key == 'control':
            self.ctrl_flag = True

    def on_key_release(self, event):
        if event.inaxes is self.ax and event.key == 'control':
            self.ctrl_flag = False

    def on_scroll(self, event):
        if event.inaxes is not self.ax:
            return

        if self.ctrl_flag:
            self.on_scroll_y(event)
        else:
            self.on_scroll_x(event)

        self.ax.figure.canvas.draw()

    def on_scroll_x(self, event):
        x_pos = event.xdata
        min_value, max_value = self.ax.get_xlim()

        if event.button == 'up':
            new_min_value = x_pos - (x_pos - min_value) * self.zoom_rate
            new_max_value = (max_value - x_pos) * self.zoom_rate + x_pos
        elif event.button == 'down':
            new_min_value = x_pos - (x_pos - min_value) / self.zoom_rate
            new_max_value = (max_value - x_pos) / self.zoom_rate + x_pos

        self.ax.set_xlim(new_min_value, new_max_value)

    def on_scroll_y(self, event):
        y_pos = event.ydata
        min_value, max_value = self.ax.get_ylim()

        if event.button == 'up':
            new_min_value = y_pos - (y_pos - min_value) * self.zoom_rate
            new_max_value = (max_value - y_pos) * self.zoom_rate + y_pos
        elif event.button == 'down':
            new_min_value = y_pos - (y_pos - min_value) / self.zoom_rate
            new_max_value = (max_value - y_pos) / self.zoom_rate + y_pos

        self.ax.set_ylim(new_min_value, new_max_value)

if __name__ == "__main__":
    fig, axes = plt.subplots(2, 2)
    axes[0, 0].plot([-1, 2, 3], [2, 4, 3])
    axes[1, 1].plot([0, 5, 9], [-2, 1, 10])

    mzh1 = MplZoomHelper(axes[0, 0])
    mzh2 = MplZoomHelper(axes[1, 1])
    plt.show()

コンストラクタ

MplZoomHelperオブジェクトのコンストラクタを使い、拡大縮小機能を使いたいグラフを登録します。コンストラクタでは、メンバ変数として個別のグラフ(self.ax)、Ctrlキーの状態(self.ctrl_flag)、拡大縮小の割合(self.zoom_rate)を保持します。その後、コールバック関数を登録します。
コンストラクタの引数は個別のグラフ(Axesオブジェクト)なので、イベントを受け取るためのコールバック関数を登録するためには、ax.figureとしてFigureオブジェクトを取得するようにします。

on_key_press(), on_key_release()

Ctrlキーの状態を取得し、記録します。この時、event.inaxesを見て、登録したグラフ(self.ax)上で発生したイベントかどうか調べます。

on_scroll()

マウスのホイールによる拡大縮小を実行します。on_key_press(), on_key_release()と同様に、event.inaxesを見て、登録したグラフ(self.ax)上で発生したイベントかどうか調べます。登録したグラフ上で発生していた場合のみ、拡大縮小を実行します。

拡大縮小した結果は、再描画しないとグラフに反映されません。そのために、self.ax.figure.canvas.draw()を実施します。

on_scroll_x(), on_scroll_y()

実際の拡大縮小を実施するところです。カーソル位置(event.xdata, event.ydata)を中心として、self.zoom_rateの割合で拡大縮小を実施します。

まとめ

matplotlibで表示されたグラフに対して、マウスのホイールによる拡大縮小を実施する方法です。

  1. mpl_connect()を使って、'scroll_event'を受け取るコールバック関数を登録します。
  2. コールバック関数でホイールを回した方向を取得します。
  3. ホイールを回した方向に応じて、グラフの両端の値を変更します。
  4. 変更した結果を画面に反映するため、再描画します。

matplotlib,python

Posted by izadori