PythonのMatplotlibのグラフに領域を指示して最大値と最小値を取得する(GUI版)

PythonのMatplotlibでtkinterに描画したグラフに対して、マウスでここからここまでという領域を指定して、その領域内のグラフの最大値と最小値を取得してみます。

目次

  1. 指定範囲の最大値と最小値を取得する方法
  2. コード
  3. 使い方
  4. 試してみた

指定範囲の最大値と最小値を取得する方法

Matplotlibのイベントを利用してマウスの座標を取得し、その座標からグラフの最大値と最小値を計算します。

以前の投稿 ではMatplotlibで描画したグラフそのものから座標を取得しましたが、今回はtkinterを使ってGUIにしてみます。データの入力もクリップボード経由にします。

試した環境はWindows10です。

コード

早速ですが、コードです。

import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import numpy as np
import math

class Application(tk.Frame):
    def __init__(self, master=None, limit=None, data=None, plot=None, scatter=None):
        super().__init__(master)
        self.line = limit
        self.max_min_point = scatter
        self.plot_line = plot
        self.master = master
        self.master.title('Matplotlib in tkinter')
        self.pack()
        self.create_widgets()
        self.start_up()

    def create_widgets(self):
        self.canvas_frame = tk.Frame(self.master)
        self.canvas_frame.pack(side=tk.LEFT)
        self.control_frame = tk.Frame(self.master)
        self.control_frame.pack(side=tk.RIGHT)

        self.canvas = FigureCanvasTkAgg(fig, self.canvas_frame)
        self.canvas.draw()
        self.cid = plot_line.figure.canvas.mpl_connect('button_press_event', self.draw_plot2)
        self.cid = plot_line.figure.canvas.mpl_connect('button_release_event', self.draw_plot2)
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.toolbar = NavigationToolbar2Tk(self.canvas, self.canvas_frame)
        self.toolbar.update()
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.btn_clip = tk.Button(self.control_frame, text='Paste Data', command=self.draw_plot)
        self.btn_clip.pack(anchor=tk.NW)

        self.label_max = tk.Label(self.control_frame, text='Max (x y)')
        self.label_max.pack(anchor=tk.NW)

        self.v_max = tk.StringVar()
        self.text_max = tk.Entry(self.control_frame, textvariable=self.v_max)
        self.text_max.pack(anchor=tk.NW)

        self.label_min = tk.Label(self.control_frame, text='Min (x y)')
        self.label_min.pack(anchor=tk.NW)

        self.v_min = tk.StringVar()
        self.text_min = tk.Entry(self.control_frame, textvariable=self.v_min)
        self.text_min.pack(anchor=tk.NW)

    def start_up(self):
        self.v_max.set('Drag to select range')
        self.v_min.set('in plot area')
        self.x =[]
        self.y1 = []
        self.x_input = []
        self.y_input = []
        self.xs = []
        self.ys = []
        self.line.set_data([],[])
        self.line.figure.canvas.draw()
        self.max_min_point.set_data([],[])
        self.max_min_point.figure.canvas.draw()

    def draw_plot(self, event=None):
        self.start_up()
        # クリップボードからデータを取得する
        input_string = self.clipboard_get().splitlines()
        for l in input_string:
            if '\t' in l:
                xy = l.split('\t')
            else:
                xy = l.split(',')
            self.x.append(float(xy[0]))
            self.y1.append(float(xy[1]))
            self.x_input.append(xy[0])
            self.y_input.append(xy[1])
        # グラフを描画する
        dx = math.sqrt(max(self.x) * max(self.x) + min(self.x) * min(self.x)) * 0.1
        dy = math.sqrt(max(self.y1) * max(self.y1) + min(self.y1) * min(self.y1)) * 0.1
        ax.set_xlim(min(self.x)-dx, max(self.x)+dx)
        ax.set_ylim(min(self.y1)-dy, max(self.y1)+dy)
        self.plot_line.set_data(self.x, self.y1)
        self.plot_line.figure.canvas.draw()

    def draw_plot2(self, event):
        lim = ax.get_ylim()
        if event.name == 'button_press_event':
            # 選択範囲の開始位置を描画する
            self.xs.append(event.xdata)
            self.ys.append(lim[0])
            self.xs.append(event.xdata)
            self.ys.append(lim[1])
            self.line.set_data(self.xs, self.ys)
            self.line.figure.canvas.draw()
        else:
            # 選択範囲の終了位置を描画する
            self.xs.append(None)
            self.ys.append(None)
            self.xs.append(event.xdata)
            self.ys.append(lim[0])
            self.xs.append(event.xdata)
            self.ys.append(lim[1])
            self.line.set_data(self.xs, self.ys)
            self.line.figure.canvas.draw()
            # 選択範囲内の最大値と最小値を検索して描画する
            x_selected = []
            y1_selected = []
            x_input_selected = []
            y_input_selected = []
            x_min = min(self.xs[-4], self.xs[-1])
            x_max = max(self.xs[-4], self.xs[-1])
            for i in range(len(self.x)):
                if self.x[i] >= x_min and self.x[i] <= x_max:
                    x_selected.append(self.x[i])
                    y1_selected.append(self.y1[i])
                    x_input_selected.append(self.x_input[i])
                    y_input_selected.append(self.y_input[i])
            if len(x_selected) > 1:
                y1_max_idx = np.argmax(y1_selected)
                y1_min_idx = np.argmin(y1_selected)
                self.v_min.set(x_input_selected[y1_min_idx] + '\t' + y_input_selected[y1_min_idx])
                self.v_max.set(x_input_selected[y1_max_idx] + '\t' + y_input_selected[y1_max_idx])
                self.max_min_point.set_data([x_selected[y1_min_idx], x_selected[y1_max_idx]],[y1_selected[y1_min_idx], y1_selected[y1_max_idx]])
                self.max_min_point.figure.canvas.draw()
            else:
                self.line.set_data([], [])
                self.line.figure.canvas.draw()
                self.max_min_point.set_data([], [])
                self.max_min_point.figure.canvas.draw()
            self.xs = []
            self.ys = []


# グラフ描画の準備
fig = Figure(figsize=(5, 5), dpi=100)
ax = fig.add_subplot(111)
plot_line, = ax.plot([], [])
select_line, = ax.plot([], [])
max_min_point, = ax.plot([],[],'ro')

root = tk.Tk()
app = Application(master=root, limit=select_line, plot=plot_line, scatter=max_min_point)
app.mainloop()

クリップボードからのデータの取得は、tkinterのclipboard_get()というメソッドを使用します。

ExcelからコピーペーストするとCSVではなくTSV(タブ区切り)になりますので、タブを含んでいる場合はタブで、そうでないばあいはカンマで文字列を分割して数値データに変換します。

グラフをマウスでクリックしたときに発生するイベントをcanvas.mpl_connect()で取得するのですが、これはcanvasの定義部分に書きました。マウスの左ボタンを押したときと離したときで、別々の関数を呼び出すようになっています。

マウスのイベントのプロパティにマウスの座標値がありますので、その座標値を取得してX軸の範囲を決めます。X軸の範囲が決まれば、あとはその中の最大値と最小値を検索すれば良いわけです。

tkinterのラベルは表示された文字列をマウスで選択できないようなので、テキストボックスに結果を表示するようにしました。表示された数値をダブルクリックすればその数値が選択され、トリプルクリックすればテキストボックス内の全ての文字が選択されますので、結果をコピーしてExcel等の他のアプリに移せます。

使い方

まず、データを準備します。テキストエディタでCSVファイルを開くか、下図のようにExcelでデータを開きます。

Excelでデータを表示

プロットしたいデータの範囲を選択して、クリップボードにコピーします。

そうしたら、「Paste Data」ボタンをクリックすると、クリップボードからデータがアプリに読み込まれます。正しくデータが読み込まれるとグラフが表示されます。

グラフが表示されたら、最大値と最小値を検出したい範囲の端にマウスポインタを移動して左ボタンを押します。すると、縦線が引かれます。これが最大値と最小値を検出する範囲の片方の端面になります。

左ボタンを押したままポインタを移動して左ボタンを離すと、2つめの縦線が引かれます。この2つの縦線の中の最大値と最小値が検出され、グラフの右側のテキストボックスに結果が表示されます。

試してみた

実際に実行するとこのようになります。

広告

Pythonでグラフカテゴリの投稿