Tkinterでグラフを表示したい！

# はじめに
pyVISAの記事で、「LabVI*Wの代わりになる！」というようなことを書いていました。

https://qiita.com/YujiMatsu/items/8e0437b33555647b0fc4

ただ、LabV*EWのいいところって、GUI上にグラフを用意して、測定値データをリアルタイムで確認できたりするところにあるんですよね。。。

そこで、Pythonで利用できる`Tkinter`を使って、測定値のデータをGUI上にリアルタイムで表示するプログラムを考えてみました。

こんな風に出力されるGUIを書いてみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355664/09181964-f40d-e25e-3c6a-f98938d2b32a.gif" width="500">


# 環境
- python3.9.7
- MacOS 14.2.1
※ Windowsでも動くと思います。

# 計測値データを作成
計測器が周りに無かったので(泣)、計測データっぽいものを~~捏造~~作成してみましょう。
線形に増加する値に、乱数を乗せるプログラムを書きました。
今回はこれで作成したデータを一定間隔で読み出し、擬似的に計測器からデータが返ってきた状況を表現してみようと思ます。
```python
import numpy as np

def generate_random_data(x_range, random_range):
    np.random.seed(123)
    x = np.linspace(*x_range)
    random_values = np.random.uniform(*random_range, size=x.shape)
    y = x + random_values
    return x, y

if __name__=='__main__':
    x_range = (0, 30)
    random_range = (0, 2)
    x, y = generate_random_data(x_range=x_range, random_range=random_range)

    # save
    np.savetxt('data.csv', np.column_stack((x, y)), delimiter=',')
```

# `Tkinter`のGUI上にグラフを表示
`Tkinter`の書き方は色々と派閥があるようですが、私は`class`を使った書き方が好きです。
ボタンのコマンドとして、メソッドを用意する、といった設計ができるためです。
GUI上の情報なども、メソッドを用意しておけばインスタンスから取得することもできます。

まずは下の`class`をベースに、グラフを表示する方法を書いていきます。
```python
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import numpy as np

class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.master.title('matplotlib graph')

        # matplotlib配置用フレーム
        frame = tk.Frame(self.master)

        # matplotlibの描画領域の作成
        fig = Figure()
        self.ax = fig.add_subplot(1, 1, 1)
        # matplotlibの行場領域とウィジェットの関連付け
        self.fig_canvas = FigureCanvasTkAgg(fig, frame)
        # matplotlibのツールバーを作成
        self.toolbar = NavigationToolbar2Tk(self.fig_canvas, frame)
        # matploglibのグラフをフレームに配置
        self.fig_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        # フレームをウィンドウに配置
        frame.pack()

        # ボタンの作成
        button = tk.Button(self.master, text='Draw Graph', command=self.button_click)
        # 配置
        button.pack(side=tk.BOTTOM)

    def button_click(self):
        with open('data.csv', 'r') as f:
            data = np.loadtxt(f, delimiter=',')
            x = data[:, 0]
            y = data[:, 1]
        self.ax.plot(x, y)
        self.fig_canvas.draw()

if __name__=='__main__':
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()
```
プログラムを実行し、`Draw Graph`のボタンを押せばグラフが表示されるはずです。

# 計測器「っぽい」動作をする関数を用意
計測器のように、呼び出すたびに値を返すような関数を作ってみます。
先のモジュールで作成した`data.csv`を読み取り、呼び出すたびに上から順に値を返すような関数を書いてみます。
ここでは`iterator`を使って実装してみました。
```python
class Iterator(object):
    def __init__(self, array):
        self._array = array
        self._i = 0

    def __iter__(self):
        return self

    def __len__(self):
        return self._array.shape[0]

    def __next__(self):
        if self._i == len(self._array.shape[0]):
            raise StopIteration()
        x, y = self._array[self._i]
        self._i += 1
        return x, y
```

# リアルタイムに表示する
`Tkinter`でGUIを表示する場合、`mainloop()`を使っています。
インスタンスからGUIの表示内容を更新することは不可なので、`class`にメソッドを追加していきます。

グラフをリアルタイムに生成するメソッドは、以下のように書きました。
計測器を使うときのイメージは、`itr`からデータを呼び出す`itr.__next__()`を、計測器からデータを読み込む関数に置き換えれば良いと思います。
```python
def run_gengraph(self):
    with open('data.csv', 'r') as f:
        data = np.loadtxt(f, delimiter=',')

    # iteratorのインスタンス生成
    itr = Iterator(data)

    # グラフ描画用のlist
    x_ = []
    y_ = []

    for _ in range(len(itr)):
        # itrからデータを取得
        # 計測データはここで受ければOK
        x, y = itr.__next__()
        # リストに追加
        x_.append(x)
        y_.append(y)
        # 描画
        self.ax.clear() # グラフのリセット
        self.ax.plot(x_, y_)
        self.fig_canvas.draw()
        # GUIの更新
        self.master.update()
        # 待機時間
        time.sleep(1)
```
ちょっとハマったところは、ただ描画するだけではGUIの更新がされませんでした。
`self.master.update()`を追加することで更新され、ブラフにプロットがどんどん追加されるようになりました。
また、`self.ax.clear()`を追加することで、プロットごとに線が追加されることなく更新される様になってます。
（いずれもChatGPTに聞くと教えてくれました。ありがたや〜）

最終的にGUIを記述している`class`は以下の通りです。
```python
class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.master.title('matplotlib graph')

        # matplotlib配置用フレーム
        frame = tk.Frame(self.master)

        # matplotlibの描画領域の作成
        fig = Figure()
        self.ax = fig.add_subplot(1, 1, 1)
        # matplotlibの行場領域とウィジェットの関連付け
        self.fig_canvas = FigureCanvasTkAgg(fig, frame)
        # matplotlibのツールバーを作成
        self.toolbar = NavigationToolbar2Tk(self.fig_canvas, frame)
        # matploglibのグラフをフレームに配置
        self.fig_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        # フレームをウィンドウに配置
        frame.pack()

        # ボタンの作成
        button = tk.Button(self.master, text='Draw Graph', command=self.run_gengraph)
        # 配置
        button.pack(side=tk.BOTTOM)

    def run_gengraph(self):
        with open('data.csv', 'r') as f:
            data = np.loadtxt(f, delimiter=',')

        # iteratorのインスタンス生成
        itr = Iterator(data)

        # グラフ描画用のlist
        x_ = []
        y_ = []

        for _ in range(len(itr)):
            # itrからデータを取得
            x, y = itr.__next__()
            # リストに追加
            x_.append(x)
            y_.append(y)
            # 描画
            self.ax.clear() # グラフのリセット
            self.ax.plot(x_, y_)
            self.fig_canvas.draw()
            # GUIの更新
            self.master.update()
            # 待機時間
            time.sleep(1)
```


# 最後に
`Tkinter`を使ってGUI上にグラフを描画し、更新されるデータをリアルタイムで描画する方法をまとめました。
これでLabVIE*に少しでも近づけたかな？
今後は本家のグラフデザインに少しでも近づけるのと、やはり計測器を繋げたときにどんなプログラムになるのかをまとめたいです。


# Github

https://github.com/yujimats/tkinter_graph.git

# 参考にした記事

https://imagingsolution.net/program/python/matplotlib/python_tkinter_matplotlib/

https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09