Pygletでのデータ可視化関数備忘録


![ezgif-5-4a1c732d66.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/9d161303-7263-6bcb-e936-ce97632f3e11.gif)

　pygletを用いて、ウィンドウ形式で情報を可視化するコード。行列や棒グラフをリアルタイムで表示させる。


# 内容
- [可視化関数](#可視化関数)
    - [行列表示関数](#行列表示関数)
    - [棒グラフ](#棒グラフ)
    - [文字列表示](#文字列表示)
- [実行可能なサンプル](#実行可能なサンプル)
---

　関数に登場するAppenderには[任意の登録用変数名].appendを入れる。（[参考](https://qiita.com/Kuramubon/items/8de156a346382a7a7074#append%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)）
　batchにはpygletの一括描画用バッチを入れる。
　具体例は[サンプル](#実行可能なサンプル)を参照。

# 可視化関数
## 行列表示関数

### shapeを使って、隙間付きで表示する

![screenshot.76.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/91ff0dbf-abd5-40bb-7646-69af3ed4f999.jpeg)

```python:
def matrixDisplay(window :pyglet.window.Window, Appender, batch, matrix :np.array, x, y, width = 10, fill_rate = 0.8, high_color = (50,0,150), low_color = (240,240,240), Trans = False):
    lower_val = matrix.min()
    max_val = matrix.max() - lower_val
    if matrix.ndim == 1:  # 1次元配列の場合
        matrix = matrix.reshape(-1, 1)  # 2次元配列に変換
    else:
        matrix = np.flipud(matrix)
    if Trans:
        matrix = matrix.T

    for i in range(matrix.shape[0]):
        for j in range(matrix.shape[1]):
            level = (matrix[i,j]-lower_val)/(max_val+1e-16)
            interpolated_color = tuple([int(low_color[k]*(1 - level) + high_color[k]*level) for k in range(3)])
            Appender(pyglet.shapes.Rectangle(x+j*width, y+i*width, width*fill_rate, width*fill_rate, color=interpolated_color, batch=batch))
    # キャプション用の座標を返す
    name_x = max(0, min(window.width, x + matrix.shape[1] / 2.0 * width))
    name_y = max(0, min(window.height, y - 15))
    return (name_x, name_y)
```

### 画像に変換してSpriteとして表示する

![screenshot.75.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/f938d046-f2aa-9ba5-015b-2b0f16c0afb4.jpeg)

np.arrayで画像データ（大きさ二次元＋RGB一次元）の三次元行列を作成、pygletのspriteとして表示する。

```python:
def matrixImageDisplay(window :pyglet.window.Window, Appender, batch, matrix :np.array, x, y, scale :float = 1.0, high_color = (50,0,150), low_color = (240,240,240), Trans = False):
    lower_val = matrix.min()
    max_val = matrix.max() - lower_val
    if matrix.ndim == 1:  # 1次元配列の場合
        matrix = matrix.reshape(-1, 1)  # 2次元配列に変換
    else:
        matrix = np.flipud(matrix)
    if Trans:
        matrix = matrix.T

    data = np.zeros((matrix.shape + (3,)), dtype=np.uint8)
    for i in range(matrix.shape[0]):
        for j in range(matrix.shape[1]):
            level = (matrix[i,j]-lower_val)/(max_val+1e-16)
            data[i,j] = [int(low_color[k]*(1 - level) + high_color[k]*level) for k in range(3)]
    # 行列を画像に変換する
    raw_image = pyglet.image.ImageData(data.shape[1], data.shape[0], 'RGB', data.tobytes())
    tempSprite = pyglet.sprite.Sprite(raw_image, x, y, batch=batch)
    # 拡大時に画像がぼやけるのを防止するコード
    pyglet.gl.glTexParameteri(pyglet.gl.GL_TEXTURE_2D, pyglet.gl.GL_TEXTURE_MAG_FILTER, pyglet.gl.GL_NEAREST)
    pyglet.gl.glTexParameteri(pyglet.gl.GL_TEXTURE_2D, pyglet.gl.GL_TEXTURE_MIN_FILTER, pyglet.gl.GL_NEAREST)
    # 画像の拡大
    tempSprite.scale = scale
    Appender(tempSprite)
    # キャプション用の座標を返す
    name_x = max(0, min(window.width, x + matrix.shape[1] / 2.0 * scale))
    name_y = max(0, min(window.height, y - 15))
    return (name_x, name_y)
```

## 棒グラフ

### 単純な棒グラフ

![screenshot.79.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/50a1fe1f-8707-b9bc-70bd-c5e7bff8eeb2.jpeg)

一次元のnp.array配列を入れれば、それが棒グラフになる。

```python:
def barplotDisplay(window :pyglet.window.Window, Appender, batch, array, x, y, width = 30, height = 50, fill_rate = 0.8, bar_color = (50,50,255), line_color = (0,0,0), line_width = 1):
    array = array/array.sum()
    for i in range(len(array)):
       Appender(pyglet.shapes.Rectangle(x+i*width+int(width*(1-fill_rate)), y, width*fill_rate, int(array[i]*height), color=bar_color, batch=batch))
    Appender(pyglet.shapes.Line(x, y, x + len(array)*width, y, line_width, color = line_color, batch=batch))
    Appender(pyglet.shapes.Line(x, y, x, y + height, line_width, color = line_color, batch=batch))
    name_x = max(0, min(window.width, x + int(len(array)*width/2)))
    name_y = max(0, min(window.height, y + height + 20))
    return (name_x, name_y)
```

### 積み上げ棒グラフ

![screenshot.77.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/220d54d5-6d76-9063-bdfe-b4191c9365b7.jpeg)

二次元の配列np.arrayを代入。列が各棒に対応し、行が積み上げる値。
負の値は下方向に延びる。

```python:
def stackedBarplotDisplay(window :pyglet.window.Window, Appender, batch, matrix :np.array, x, y, width = 30, height = 50, fill_rate = 0.8, bar_color_pallete :list = [(220,220,50), (240,30,30), (50,200,50), (120,170,200), (200,100,220)], line_color = (0,0,0), line_width = 1):
    assert matrix.shape[1] <= len(bar_color_pallete), f"Y elements ({matrix.shape[1]}) is larger than Color pallete ({len(bar_color_pallete)}) Please make color pallete larger than Name List."
    posmax = np.where(matrix>0,matrix,0.).sum(axis=1).max()
    negmax = np.where(matrix<0,matrix,0.).sum(axis=1).min()
    scale :float = height / (posmax - negmax) if posmax != negmax else 0.
    baseline :int = int(y - negmax * scale)
    margin :int = int(width*(1-fill_rate))
    for i in range(matrix.shape[0]):
        posStack :float = 0.
        negStack :float = 0.
        for h in range(matrix.shape[1]): # 十分な数のcolor palleteを用意する必要がある
            if matrix[i,h] < 0:
                negStack += matrix[i,h] * scale
                Appender(pyglet.shapes.Rectangle(x+i*width+margin, baseline + negStack, width*fill_rate, -matrix[i,h]*scale, color=bar_color_pallete[h], batch=batch))
            elif matrix[i,h] > 0:
                Appender(pyglet.shapes.Rectangle(x+i*width+margin, baseline + posStack, width*fill_rate, matrix[i,h]*scale, color=bar_color_pallete[h], batch=batch))
                posStack += matrix[i,h] * scale
    Appender(pyglet.shapes.Line(x, baseline, x + matrix.shape[0]*width, baseline, line_width, color = line_color, batch=batch))
    Appender(pyglet.shapes.Line(x, y, x, y + height, line_width, color = line_color, batch=batch))
    name_x = max(0, min(window.width, x + int(matrix.shape[0]*width/2)))
    name_y = max(0, min(window.height, y + height + 20))
    return (name_x, name_y)
```

凡例用（凡例名をリスト形式で入力）

![screenshot.80.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/74191230-913d-a69c-9fc2-fd18de126a0a.jpeg)

```python:
def colorLegend(Appender, batch, x :int, y: int, name_list :list, font_name = 'Times New Roman', font_size = 9, rate = 1.5, color_pallete :list = [(220,220,50), (240,30,30), (50,200,50), (120,170,200), (200,100,220)], font_color = (0, 0, 0)):
    margin = int(font_size*rate)
    assert len(name_list) <= len(color_pallete), f"Name List length({len(name_list)}) is larger than Color pallete ({len(color_pallete)}) Please make color pallete larger than Name List."
    for i in range(len(name_list)):
        Appender(pyglet.shapes.Rectangle(x, y-i*margin-int(font_size*.5), font_size, font_size, color=color_pallete[i], batch=batch))
        Appender(pyglet.text.Label(name_list[i], font_name=font_name, font_size=font_size,
                                    x=x+margin, y=y-i*margin, color = font_color + (255,), #color and alpha
                                    anchor_x='left', anchor_y='center', batch=batch))
```

## 文字列表示

![screenshot.78.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/b06e0b48-f0ef-a182-718a-fa567b0b2ddf.jpeg)

```python:
black = (0, 0, 0)
eleAppender(pyglet.text.Label(f"Hello\nWorld\nT={T/3:.2f}",
    font_name='Times New Roman', font_size=11,
    x=620, y=window.height - 70, width = 240,
    color = black + (255,), anchor_x='left', anchor_y='center',
    multiline = True, batch=PygletBatch))
```

```/n```を使えば、改行が可能。このとき、```multiline=True```と```width=240```（所謂パワポのテキストボックスの横幅的なもの）を設定する必要がある。
また、```black = (0, 0, 0)```で色を変数化しているが、文字を描画する際は透明度Alphaが指定できるため、```black + (255,) = (0, 0, 0, 255)```でRGBAに変更している。

# 実行可能なサンプル

```python:UI_Sample.py
import pyglet
import numpy as np
import random
import time

#========関数========

def matrixDisplay(window :pyglet.window.Window, Appender, batch, matrix :np.array, x, y, width = 10, fill_rate = 0.8, high_color = (50,0,150), low_color = (240,240,240), Trans = False):
    lower_val = matrix.min()
    max_val = matrix.max() - lower_val
    if matrix.ndim == 1:  # 1次元配列の場合
        matrix = matrix.reshape(-1, 1)  # 2次元配列に変換
    else:
        matrix = np.flipud(matrix)
    if Trans:
        matrix = matrix.T

    for i in range(matrix.shape[0]):
        for j in range(matrix.shape[1]):
            level = (matrix[i,j]-lower_val)/(max_val+1e-16)
            interpolated_color = tuple([int(low_color[k]*(1 - level) + high_color[k]*level) for k in range(3)])
            Appender(pyglet.shapes.Rectangle(x+j*width, y+i*width, width*fill_rate, width*fill_rate, color=interpolated_color, batch=batch))
    # キャプション用の座標を返す
    name_x = max(0, min(window.width, x + matrix.shape[1] / 2.0 * width))
    name_y = max(0, min(window.height, y - 15))
    return (name_x, name_y)

def matrixImageDisplay(window :pyglet.window.Window, Appender, batch, matrix :np.array, x, y, scale :float = 1.0, high_color = (50,0,150), low_color = (240,240,240), Trans = False):
    lower_val = matrix.min()
    max_val = matrix.max() - lower_val
    if matrix.ndim == 1:  # 1次元配列の場合
        matrix = matrix.reshape(-1, 1)  # 2次元配列に変換
    else:
        matrix = np.flipud(matrix)
    if Trans:
        matrix = matrix.T

    data = np.zeros((matrix.shape + (3,)), dtype=np.uint8)
    for i in range(matrix.shape[0]):
        for j in range(matrix.shape[1]):
            level = (matrix[i,j]-lower_val)/(max_val+1e-16)
            data[i,j] = [int(low_color[k]*(1 - level) + high_color[k]*level) for k in range(3)]
    # 行列を画像に変換する
    raw_image = pyglet.image.ImageData(data.shape[1], data.shape[0], 'RGB', data.tobytes())
    tempSprite = pyglet.sprite.Sprite(raw_image, x, y, batch=batch)
    # 拡大時に画像がぼやけるのを防止するコード
    pyglet.gl.glTexParameteri(pyglet.gl.GL_TEXTURE_2D, pyglet.gl.GL_TEXTURE_MAG_FILTER, pyglet.gl.GL_NEAREST)
    pyglet.gl.glTexParameteri(pyglet.gl.GL_TEXTURE_2D, pyglet.gl.GL_TEXTURE_MIN_FILTER, pyglet.gl.GL_NEAREST)
    # 画像の拡大
    tempSprite.scale = scale
    Appender(tempSprite)
    # キャプション用の座標を返す
    name_x = max(0, min(window.width, x + matrix.shape[1] / 2.0 * scale))
    name_y = max(0, min(window.height, y - 15))
    return (name_x, name_y)

def barplotDisplay(window :pyglet.window.Window, Appender, batch, array, x, y, width = 30, height = 50, fill_rate = 0.8, bar_color = (50,50,255), line_color = (0,0,0), line_width = 1):
    array = array/array.sum()
    for i in range(len(array)):
       Appender(pyglet.shapes.Rectangle(x+i*width+int(width*(1-fill_rate)), y, width*fill_rate, int(array[i]*height), color=bar_color, batch=batch))
    Appender(pyglet.shapes.Line(x, y, x + len(array)*width, y, line_width, color = line_color, batch=batch))
    Appender(pyglet.shapes.Line(x, y, x, y + height, line_width, color = line_color, batch=batch))
    name_x = max(0, min(window.width, x + int(len(array)*width/2)))
    name_y = max(0, min(window.height, y + height + 20))
    return (name_x, name_y)

def stackedBarplotDisplay(window :pyglet.window.Window, Appender, batch, matrix :np.array, x, y, width = 30, height = 50, fill_rate = 0.8, bar_color_pallete :list = [(220,220,50), (240,30,30), (50,200,50), (120,170,200), (200,100,220)], line_color = (0,0,0), line_width = 1):
    assert matrix.shape[1] <= len(bar_color_pallete), f"Y elements ({matrix.shape[1]}) is larger than Color pallete ({len(bar_color_pallete)}) Please make color pallete larger than Name List."
    posmax = np.where(matrix>0,matrix,0.).sum(axis=1).max()
    negmax = np.where(matrix<0,matrix,0.).sum(axis=1).min()
    scale :float = height / (posmax - negmax) if posmax != negmax else 0.
    baseline :int = int(y - negmax * scale)
    margin :int = int(width*(1-fill_rate))
    for i in range(matrix.shape[0]):
        posStack :float = 0.
        negStack :float = 0.
        for h in range(matrix.shape[1]): #need enough color pallete for h
            if matrix[i,h] < 0:
                negStack += matrix[i,h] * scale
                Appender(pyglet.shapes.Rectangle(x+i*width+margin, baseline + negStack, width*fill_rate, -matrix[i,h]*scale, color=bar_color_pallete[h], batch=batch))
            elif matrix[i,h] > 0:
                Appender(pyglet.shapes.Rectangle(x+i*width+margin, baseline + posStack, width*fill_rate, matrix[i,h]*scale, color=bar_color_pallete[h], batch=batch))
                posStack += matrix[i,h] * scale
    Appender(pyglet.shapes.Line(x, baseline, x + matrix.shape[0]*width, baseline, line_width, color = line_color, batch=batch))
    Appender(pyglet.shapes.Line(x, y, x, y + height, line_width, color = line_color, batch=batch))
    name_x = max(0, min(window.width, x + int(matrix.shape[0]*width/2)))
    name_y = max(0, min(window.height, y + height + 20))
    return (name_x, name_y)

def colorLegend(Appender, batch, x :int, y: int, name_list :list, font_name = 'Times New Roman', font_size = 9, rate = 1.5, color_pallete :list = [(220,220,50), (240,30,30), (50,200,50), (120,170,200), (200,100,220)], font_color = (0, 0, 0)):
    margin = int(font_size*rate)
    assert len(name_list) <= len(color_pallete), f"Name List length({len(name_list)}) is larger than Color pallete ({len(color_pallete)}) Please make color pallete larger than Name List."
    for i in range(len(name_list)):
        Appender(pyglet.shapes.Rectangle(x, y-i*margin-int(font_size*.5), font_size, font_size, color=color_pallete[i], batch=batch))
        Appender(pyglet.text.Label(name_list[i], font_name=font_name, font_size=font_size,
                                    x=x+margin, y=y-i*margin, color = font_color + (255,), #color and alpha
                                    anchor_x='left', anchor_y='center', batch=batch))

#================

config = pyglet.gl.Config(double_buffer=True)
#visible=Falseで作成してすぐにはウィンドウを表示しない
window = pyglet.window.Window(640, 480, visible=False, config=config, resizable=False)
window.set_caption('UI Test') #ウィンドウのタイトル
pyglet.gl.glClearColor(1,1,1,1) #ウィンドウの背景色 Red,Green,Blue,Alpha 0.0~1.0で指定
window.set_location(20,50) #ウィンドウの位置を100, 200の位置に取る。自身の画面に応じて変えること

PygletBatch = pyglet.graphics.Batch() #バッチの定義

black = (0, 0, 0) #黒色

dt = 0 #ループ毎のΔt
LoopFlag = True #メインループの終了判定用フラグ
PauseFlag = False #一時停止判定用フラグ

@window.event
def on_close(): #ウィンドウの×ボタンがクリックされたときのイベント
    global LoopFlag #外の変数にアクセスするために必要
    LoopFlag = False #ループフラグを折ってループから抜ける。

@window.event
def on_draw(): #windowのイベントとして描画処理を書く
    window.clear()
    PygletBatch.draw() #描写

@window.event
def on_key_press(symbol, modifiers): #押されたときのみイベントが発生する
    global LoopFlag
    if symbol == pyglet.window.key.ESCAPE:
        LoopFlag = False #ESCキーが押されたらループフラグを折ってループから抜ける。

@window.event
def on_mouse_press(x, y, button, modifiers): #ウィンドウをクリックするとイベントが発生する
    global PauseFlag
    if button == pyglet.window.mouse.LEFT:
        PauseFlag = not PauseFlag #左クリックされたら一時停止

#変数
T :int = 0

window.set_visible() #ここでウィンドウを表示
while LoopFlag: #メインループ
    dt = pyglet.clock.tick() #前回tick()を呼び出してから経った時間（秒）を返す
    clockTime = time.time() #現時刻（秒）を取得
    elementList = [] #図形登録用リスト
    Appender = elementList.append #登録用の関数

    #文字
    Appender(pyglet.text.Label(f'T={T}\ndt={dt:.4f}', #\nで改行
                    font_name='Times New Roman', font_size=24, x=20, y=window.height - 20, width=240, color = black + (255,), #RGB+Alpha
                    anchor_x='left', anchor_y='top', multiline = True, batch=PygletBatch))
    if not PauseFlag: T += 1

    #行列
    A_matrix = np.array([[random.random()*5, random.random()*5, random.random()*5, random.random()*5],[0,5,0,0],[0,2,5,2]]) if not PauseFlag else A_matrix
    # 画像として表示
    matrixImageDisplay(window, Appender, PygletBatch, A_matrix, 120, window.height - 150, 15.0)
    # shapesを使って表示（要素間に余白アリ）
    matrixNamePos = matrixDisplay(window, Appender, PygletBatch, A_matrix, 220, window.height - 150, 15.0, fill_rate=0.8)
    #行列の下にキャプションをつける
    Appender(pyglet.text.Label('matrix', font_name='Times New Roman', font_size=9, x=matrixNamePos[0], y=matrixNamePos[1], color = black + (255,), anchor_x='center', anchor_y='center', batch=PygletBatch))

    #棒グラフ
    barplotDisplay(window, Appender, PygletBatch, np.array([50, 50+T%50, 30, 20]), 20, window.height - 150, line_width=2, width = 10)
    #積み上げグラフ
    stackedBarplotDisplay(window, Appender, PygletBatch, A_matrix, 330, window.height - 150, line_width=2, height = 100, width = 10)
    # 凡例
    colorLegend(Appender, PygletBatch, 300, window.height - 60, ["A","B","C","D"])

    #25ms程度経過するように待つ
    time.sleep(max(0.0, 0.025 - (clockTime - time.time())))

    # pygletのイベント実行
    windowlist = list(pyglet.app.windows)
    for w in windowlist: #現在存在するwindow全てについて更新
        w.switch_to()
        w.dispatch_events()
        w.dispatch_event('on_draw')
        w.flip()

window.close() #ループを抜けたら終了
```