PySimpleGUIでグラフを描く

# この記事を読んでできるもの
![graph_example.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11002/8cd9bcf0-b8df-170e-b058-4912040d8052.jpeg)

PySimpleGUIで以下のことができるようになります。

- 棒グラフ、折れ線グラフなどが描画できるようになります。
- 定期的に表示を更新するグラフ

PySimpleGUIの基本的な操作については[Tkinterを使うのであればPySimpleGUIを使ってみたらという話](https://qiita.com/dario_okazaki/items/656de21cab5c81cabe59)を参考にしてください。

# 概要
Pythonの利用方法としてデータ解析を行う人は多いと思います。
また解析した結果を折れ線グラフや棒グラフなどで可視化したいという人もいるかと思います。

PySimpleGUIでグラフを利用する場合は大まかに言って２通りあります

- [graph-element](https://pysimplegui.readthedocs.io/en/latest/#graph-element)を使って描画する
- matplotlibを埋め込んで描画する

今回はgraph-elementを使って作成する方法を説明します。

# graph-elementについて
graph-elementは描画に使うクラスです。tkinterではcanvasを使用して描画しますがPySimpleGUIのgraph-elementはcanvsの座標系を工夫してより使いやすいものにしたものです。

基本的な使い方としては
`Graph`クラスを使用して描画エリアの大きさ、描画エリアの左下の座標（原点）、右上の座標を指定します。

```
graph = sg.Graph((500,500), (0,0), (500, 500))
```

あとはそのグラフに関して各描画のメソッドを使用していきます。

- DrawCircle :円を描画
- DrawLine :線を引く
- DrawPoint :点を配置する
- DrawRectangle :四角形を描画する
- DrawOval :円弧を絵描画する
- DrawImage :画像を表示する
- DrawText :文字を描画する

このうち、棒グラフや円グラフを描画する際に使うのは以下になります。

- DrawLine :折れ線グラフの線や、補助線、目盛りを引くのに使用します
- DrawPoint :折れ線グラフのプロットされた点を引いたり、サイン波のような円弧を引くのに使用します
- DrawText :グラフのラベルや、プロットごとの値を表示するのに使用します


また以下のメソッドも使用します。

- Erase ：描画領域をクリアします。グラフを切り替える際に、描画していたグラフを削除するnonのに仕様用します
- Move　：描画されているものをx方向、y方向に移動させます。リアルタイムでグラフを動かすのに使用します


## コード
公式のサンプルコードをまとめたものを掲載します。
ボタンでグラフの内容を切り替えます

実行画面
![graph_example.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11002/881ac147-6539-4a93-9203-e6fd0a66d119.jpeg)


コード

```Python
import PySimpleGUI as sg
from random import randint
import math
"""
以下の公式サンプルを一つに統合しています
https://pysimplegui.trinket.io/demo-programs#/graph-element/graph-element-bar-chart
https://pysimplegui.trinket.io/demo-programs#/graph-element/graph-element-sine-wave
https://pysimplegui.trinket.io/demo-programs#/graph-element/graph-element-line-graph-with-labels
https://pysimplegui.trinket.io/demo-programs#/graph-element/animated-line-graph
"""

# 定数
GRAPH_SIZE = (500, 500)
DATA_SIZE = (500, 500)

# 
PLOTS_NUMBER = 30
RAND_MAX = 400

# 折れ線用
LINE_BAR_WIDTH = 10
LINE_BAR_SPACING = 16
LINE_EDGE_OFFSET = 3

# 棒グラフ
BAR_WIDTH = 50
BAR_SPACING = 75
EDGE_OFFSET = 3

# サイングラフ
SIZE_X = GRAPH_SIZE[0]//2
SIZE_Y = GRAPH_SIZE[1]//2
NUMBER_MARKER_FREQUENCY = 25

# アニメ―ション
GRAPH_STEP_SIZE = 5
DELAY = 15  # 時間間隔

# レイアウト
# グラフの描画領域の設定
graph = sg.Graph(GRAPH_SIZE, (0, 0), DATA_SIZE,
                 key='-GRAPH-', background_color='white',)

layout = [[sg.Text('chart demo')],
          [graph],
          [sg.Button('LINE'), sg.Button('chart'), sg.Button('両方'), sg.Button('円グラフ'), sg.Button('サイン波'),sg.Button('アニメーション') ]]

window = sg.Window('シンプルなグラフのサンプル', layout)

before_value = 0  # 折れ線グラフの初期化
delay = x = lastx = lasty = 0  # アニメーションの初期化

is_animated = False


def draw_axis():
    """ X軸とY軸に補助線を引く

    グラフエレメントの左下を原点(0,0)と設定しているのを
    グラフ領域の真ん中を原点として移動している
    """

    graph.draw_line((0, SIZE_Y), (SIZE_X*2, SIZE_Y))  # 原点
    graph.draw_line((SIZE_X, 0), (SIZE_X, SIZE_Y*2))

    for x in range(0, SIZE_X*2, NUMBER_MARKER_FREQUENCY):
        graph.draw_line((x, SIZE_Y-3), (x, SIZE_Y+3))  # 目盛りを引く
        if x != 0:
            graph.draw_text(str(x-SIZE_X), (x, SIZE_Y-10),
                            color='green')  # 目盛りの値を引く

    for y in range(0, SIZE_Y*2+1, NUMBER_MARKER_FREQUENCY):
        graph.draw_line((SIZE_X-3, y), (SIZE_X+3, y))
        if y != 0:
            graph.draw_text(str(y-SIZE_Y), (SIZE_X-10, y), color='blue')


while True:

    if is_animated:
        # 定期的に　'__TIMEOUT__' イベントが発行される
        event, values = window.Read(timeout=DELAY)
    else:
        event, values = window.Read()

    if event is None:
        break

    if event == 'LINE':
        is_animated = False
        # ラベル付き折れ線グラフを表示
        graph.Erase()  # グラフの表示両機を削除

        for i in range(PLOTS_NUMBER):
            graph_value = randint(0, 400)
            if i > 0:
                graph.DrawLine(((i-1) * LINE_BAR_SPACING + LINE_EDGE_OFFSET + LINE_BAR_WIDTH/2,  before_value),
                               (i * LINE_BAR_SPACING + LINE_EDGE_OFFSET + LINE_BAR_WIDTH/2, graph_value), color='green', width=1)

            # 折れ線のラベル（yの値）を表示
            graph.DrawText(text=graph_value, location=(
                i * LINE_BAR_SPACING+EDGE_OFFSET+2, graph_value+10))

            graph.DrawPoint((i * LINE_BAR_SPACING + LINE_EDGE_OFFSET +
                             LINE_BAR_WIDTH/2, graph_value), size=3, color='green',)

            before_value = graph_value

    if event == 'chart':
        is_animated = False

        # 棒グラフを削除
        graph.Erase()
        for i in range(PLOTS_NUMBER):
            graph_value = randint(0, 400)
            graph.DrawRectangle(top_left=(i * BAR_SPACING + EDGE_OFFSET, graph_value),
                                bottom_right=(i * BAR_SPACING + EDGE_OFFSET + BAR_WIDTH, 0), fill_color='blue')
            graph.DrawText(text=graph_value, location=(
                i*BAR_SPACING+EDGE_OFFSET+25, graph_value+10))

    if event == '両方':
        is_animated = False

        # 折れ線グラフと棒グラフの両方を表示
        graph.Erase()
        for i in range(PLOTS_NUMBER):
            graph_value = randint(0, 400)
            graph.DrawRectangle(top_left=(i * BAR_SPACING + EDGE_OFFSET, graph_value),
                                bottom_right=(i * BAR_SPACING + EDGE_OFFSET + BAR_WIDTH, 0), fill_color='blue')
            graph.DrawText(text=graph_value, location=(
                i*BAR_SPACING+EDGE_OFFSET+25, graph_value+10))

            if i > 0:
                graph.DrawLine(((i-1) * LINE_BAR_SPACING + LINE_EDGE_OFFSET + LINE_BAR_WIDTH/2,  before_value),
                               (i * LINE_BAR_SPACING + LINE_EDGE_OFFSET + LINE_BAR_WIDTH/2, graph_value), color='green', width=1)

            graph.DrawText(text=graph_value, location=(
                i * LINE_BAR_SPACING+EDGE_OFFSET+2, graph_value+10))

            graph.DrawPoint((i * LINE_BAR_SPACING + LINE_EDGE_OFFSET +
                             LINE_BAR_WIDTH/2, graph_value), size=3, color='green',)
            before_value = graph_value

    if event == '円グラフ':
        is_animated = False
        graph.erase()
        
        # create_arc()のfillはないので塗りつぶしはできない
        graph.DrawArc( (50,50), (DATA_SIZE[0]-50, DATA_SIZE[1]-50), extent=-200, start_angle=90)
        graph.DrawArc( (50,50), (DATA_SIZE[0]-50, DATA_SIZE[1]-50), extent=-400, start_angle=-110 ,arc_color="yellow")
        graph.DrawArc( (50,50), (DATA_SIZE[0]-50, DATA_SIZE[1]-50), extent=-50,  start_angle=-510 , arc_color="blue")
        graph.DrawArc( (50,50), (DATA_SIZE[0]-50, DATA_SIZE[1]-50), extent=-50,  start_angle=-560 , arc_color="red")
        graph.DrawArc( (50,50), (DATA_SIZE[0]-50, DATA_SIZE[1]-50), extent=-20,  start_angle=-610 , arc_color="green")

    if event == 'サイン波':
        is_animated = False
        graph.erase()

        draw_axis()
        prev_x = prev_y = None
        for x in range(0, SIZE_X*2):
            y = math.sin(x/75)*100 + SIZE_Y
            if prev_x is not None:
                graph.draw_line((prev_x, prev_y), (x, y), color='red')
            prev_x, prev_y = x, y

    if event == 'アニメーション' or is_animated:

        if not is_animated:
            graph.Erase()  # グラフを一回削除

        is_animated = True

        # 時系列にそって動くグラフを表示
        step_size, delay = GRAPH_STEP_SIZE, DELAY
        y = randint(0, GRAPH_SIZE[1])
        if x < GRAPH_SIZE[0]:  # 　初回
            # window['-GRAPH-'].DrawLine((lastx, lasty), (x, y), width=1)
            graph.DrawLine((lastx, lasty), (x, y), width=1)
        else:
            # window['-GRAPH-'].Move(-step_size, 0)  # グラフ全体を左にずらす
            # window['-GRAPH-'].DrawLine((lastx, lasty), (x, y), width=1)
            graph.Move(-step_size, 0)  # グラフ全体を左にずらす
            graph.DrawLine((lastx, lasty), (x, y), width=1)
            x -= step_size
            lastx, lasty = x, y
        lastx, lasty = x, y
        x += step_size

window.Close()
```

公式のグラフと違うのはサイン波の座標は今回のキャンバス全体の原点が（0,0）開始だったので、(250,250）の座標をずらして原点にしています。公式のサンプルですと、左端の座標を（250,-250）としています。
また円グラフは公式ではサンプルがなく`DrawArc`メソッドを使用して独自に実装してみましたが、円の中を塗りつぶす`fill`パラメータがないのと、アングルの指定が独特で手間がかかるので円グラフを作るのはやめた方がいいかと思います

PySimpleGUIのgraph-elementを使用することで、例えばエクセルのグラフ機能で作るような、棒グラフや折れ線グラフといった簡単なグラフは作成可能になります。

# 公式のグラフ関係のサンプルについて
PySimpleGUIはサンプルファイルが多くて、実装の参考になるもので役に立つサンプルがたくさんあります。

- 公式のデモ
    - https://github.com/PySimpleGUI/PySimpleGUI/tree/master/DemoPrograms

- 公式のデモ２
    - https://pysimplegui.trinket.io/demo-programs#/demo-programs/the-basic-pysimplegui-program

この中から個人的に役に立つ面白いと感じたサンプルを紹介します

## matplotlibとの連携
![Demo_Matplotlib.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11002/e88c1c0a-fa08-a8fe-3826-d9a76fbc3976.png)

 - https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Matplotlib.py
- matplotlibとの連携です。 matplotlibを使ったデモはほかにもいくつかあります

## ソートするアニメーション
![visualizing-sorts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11002/ffe3663b-1978-236e-e462-8d67b0997aeb.png)

- https://pysimplegui.trinket.io/demo-programs#/graph-element/visualizing-sorts
- 棒グラフの値をバブルソートで並べ替えるアニメーションのグラフです

## CPUの使用率を表示するリアルタイムのグラフ
![PySimpleGUI_Rainmeter_CPU_Cores.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11002/544f3dca-6f1c-7dcb-7eba-e7701d193cb6.png)

- https://github.com/PySimpleGUI/PySimpleGUI-Rainmeter-CPU-Cores/blob/master/PySimpleGUI_Rainmeter_CPU_Cores.py
- CPUの値を表示するグラフです。背景が半透明だったりタブを非表示にしています

# まとめ
PySimpleGUIを使うとグラフも簡単に作れることが分かったかと思います。
PySimpleGUIの公式サンプルではほかにもグラフを使用したサンプルがいくつかあるので興味があったら覗いてみてください。