pythonで3D, 2Dのリアルタイムグラフを作る

こんなこと既に誰かがやっていてqiitaにイイ感じにまとめているだろう、と思っていましたが、3Dグラフや、リアルタイムに更新する方法に関しては情報が少なかったのでここにまとめておきます。

### 目的:

- pythonで3D, 2Dのリアルタイムグラフを作る
- グラフの見た目を整える

###  メモ：

- グラフ化には matplotlib を使用
- プロットするデータは乱数で作成する
  - このデータ配列を各種測定データに置き換えれば、様々なセンサーから得られたデータをリアルタイムに可視化できるはずです

### 完成イメージ：

matplotlibデフォルトグラフ
<img width="635" alt="3dGraph.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472945/c7a6624a-5725-3d26-9e15-cd2a6409d1b5.png">

調整したグラフ
<img width="993" alt="3dGraph-stylish.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472945/573cc391-a69b-32ee-3aaa-9f7e65de530c.png">
このような3Dグラフをリアルタイムに更新していく方法を紹介します。

# 2Dリアルタイムグラフ

###  まずはリアルタイムグラフを表示するために必要な最低限のコードを書いてみましょう

はじめにライブラリを読み込みます。

```python
import numpy as np # プロットするデータ配列を作成するため
import matplotlib.pyplot as plt # グラフ作成のため
```

次に各種パラメーターの定義をします。
このようにパラメーターの設定をコードの先頭の方でやると、少しだけプロットするframeを増やしたい、といった微調整が簡単に行えるのでオススメです。

```python
dataLength = 100  # １つのデータの配列の点数
frame = 50  # プロットするフレーム数
sleepTime = 0.0001  # １フレーム表示する時間[s]
```

グラフを表示させます。

```python
for i in range(frame): # フレーム回数分グラフを更新
    data = np.random.rand(dataLength) # プロットするデータを作成
    plt.plot(data) # データをプロット
    plt.draw() # グラフを画面に表示開始
    plt.pause(sleepTime) # SleepTime時間だけ表示を継続
    plt.cla() # プロットした点を消してグラフを初期化
```	

イメージとしては、

- データを取得 (今回は１次元乱数を使用)
- データをプロット (plt.plot())
- グラフを一定時間表示 （plt.draw() & plt.pause()）
- グラフを初期化 (plt.cla())

という過程をfor文で繰り返すことで、動画のようにグラフを更新していきます。

表示するデータは先ほど定義したdataLengthの長さをもつ１次元配列なので、これを各種センサーで測定したデータに置き換えればリアルタイムに様々なデータを表示できます。

このグラフには軸やタイトルがありませんが、動画のように更新されていくことが確認できるはずです。

グラフの見た目を整える作業は後で紹介します。

---

2次元リアルタイムグラフのコードまとめ

```python			
# ２次元リアルタイムグラフの雛形

# Library
import numpy as np # プロットするデータ配列を作成するため
import matplotlib.pyplot as plt # グラフ作成のため

# params
dataLength = 100  # １つのデータの配列の点数
frame = 50  # プロットするフレーム数
sleepTime = 0.0001  # １フレーム表示する時間[s]

# plotting
for i in range(frame): # フレーム回数分グラフを更新
    data = np.random.rand(dataLength) # プロットするデータを作成
    plt.plot(data) # データをプロット
    plt.draw() # グラフを画面に表示開始
    plt.pause(sleepTime) # SleepTime時間だけ表示を継続
    plt.cla() # プロットした点を消してグラフを初期化
```

# 3Dリアルタイムグラフ

2次元グラフの例の時と同じように、まずはグラフを動的に表示させる必要最低限のコードを書いてみましょう。

３次元グラフでは使用するライブラリが一つ増えます。

```python
import numpy as np # プロットするデータ配列を作成するため
import matplotlib.pyplot as plt # グラフ表示のため
from mpl_toolkits.mplot3d import Axes3D # ３Dグラフ作成のため
```

パラメータの定義を行います。

```python
frame = 100  # プロットするフレーム数
sleepTime = 0.001  # １フレーム表示する時間[s]
dataLength = 10  # プロットするデータ配列の点数
```

プロットするデータを載せるfigureオブジェクトを作ります。
プロットするデータは逐一変化しますが、同じfigureオブジェクトにプロットするので、for文毎にfigureオブジェクトを作成する必要はありません。

```python
fig = plt.figure() # figureオブジェクトを作る
ax = Axes3D(fig)
```

グラフを表示します。

```python
for i in range(frame):
    # getting data
    x = np.arange(0, 10, 1) # 0 ~ 10 の１次元配列
    y = np.arange(0, 10, 1) # 0 ~ 10 の１次元配列
    z = np.random.rand(10) # 0 ~ 10 の１次元乱数配列
    # plotting
    ax.plot(x, y, z)
    plt.draw()
    plt.pause(sleepTime)
    plt.cla()
```
３次元グラフでも基本的にやることは同じで、

- データを取得 (今回は１次元乱数配列を使用)
- データをプロット (ax.plot())
- グラフを一定時間表示 （plt.draw() & plt.pause()）
- グラフを初期化 (plt.cla())

をframe回数分繰り返し行うことで、グラフを更新していきます。


３次元グラフの場合は、散布図、折れ線、等高線など、様々なプロットするオプションがありますが、プロットする種類によって指定するx, y, z座標の形式が微妙に違ったりするので、注意が必要です。これについては後にもう少し詳しく記述します。


今回はプロットする各点のx, yは固定（np.arange(0, 10, 1))　して、zを乱数にすることで、グラフが動的に変化していく様子を確認します。

---

３Dリアルタイムグラフのコードまとめ

```python
# 3次元リアルタイムグラフの雛形
# Library	
import numpy as np # プロットするデータ配列を作成するため
import matplotlib.pyplot as plt # グラフ表示のため
from mpl_toolkits.mplot3d import Axes3D # ３Dグラフ作成のため

# params
frame = 100  # プロットするフレーム数
sleepTime = 0.001  # １フレーム表示する時間[s]
dataLength = 10  # プロットするデータ配列の点数

# making 3d figure object
fig = plt.figure() # figureオブジェクトを作る
ax = Axes3D(fig)

for i in range(frame):
    # getting data
    x = np.arange(0, 10, 1) # 0 ~ 10 の１次元配列
    y = np.arange(0, 10, 1) # 0 ~ 10 の１次元配列
    z = np.random.rand(10) # 0 ~ 10 の１次元乱数配列
    # plotting
    ax.plot(x, y, z)
    plt.draw()
    plt.pause(sleepTime)
    plt.cla()
```

各軸の表示領域を指定していないので見栄えは悪いですが、2Dの例と同様にグラフが動的に変化していく様子が確認できるはずです。

また、３次元グラフでは更新されているグラフをクリック＆ドラッグすることで回転させることができます。

# グラフの見た目を整える

### 各軸の表示最大、最小値固定

例えばz軸を固定したい場合はプロットを更新するfor文内に

```python
zMax = 1 # z軸最大値
zMin = 0 # z軸最小値
ax.set_zlim(zMin, zMax)
```
を追加します。

### 各軸のタイトル表示

```python
xlabel = "x axis"
ylabel = "y axis"
zlabel = "z axis"
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_zlabel(zlabel)
```
をプロットを更新するfor文内に追加します。

for文の外に記述した場合、plt.cla()でグラフを初期化するときにラベルも初期化されてしまうので注意が必要です。

### 軸の最大、最小値固定、ラベル表示を追加したコードまとめ

```python
# Library	
import numpy as np # プロットするデータ配列を作成するため
import matplotlib.pyplot as plt # グラフ表示のため
from mpl_toolkits.mplot3d import Axes3D # ３Dグラフ作成のため

# params
frame = 100  # プロットするフレーム数
sleepTime = 0.001  # １フレーム表示する時間[s]
dataLength = 10  # プロットするデータ配列の点数
zMax = 1 # z軸最大値
zMin = 0 # z軸最小値
xlabel = "x axis" # x軸ラベル
ylabel = "y axis"
zlabel = "z axis"

# making 3d figure object
fig = plt.figure() # figureオブジェクトを作る
ax = Axes3D(fig)

for i in range(frame):
    # getting data
    x = np.arange(0, 10, 1) # 0 ~ 10 の１次元配列
    y = np.arange(0, 10, 1) # 0 ~ 10 の１次元配列
    z = np.random.rand(10) # 0 ~ 10 の１次元配列

    # plot setting
    ax.set_zlim(zMin, zMax) # z軸固定
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_zlabel(zlabel)

    # plotting
    ax.plot(x, y, z)
    plt.draw()
    plt.pause(sleepTime)
    plt.cla()
```

このようなグラフの見た目の調整は基本的に静止したグラフ表示の場合と変わらないので、”matplotlib 3d 軸ラベル” のように検索して、サンプルコードを参考にすれば大体は機能します。

plt.cla()で初期化されなければプロットを更新するfor文内に記述する必要はありませんが、どの機能が該当するかは実際に試して判断した方が早いと思います。

### 表示するデータを実戦に近づける

私が３Dリアルタイムグラフを作成した主な理由は、複数の1次元配列データを同時に確認したかったからです。これができれば、各種測定データを置き換えることで、例えば複数の会社の株価を同時にリアルタイムに表示するといったことが可能です。というわけで、複数の1次元配列データを３Dグラフに表示してみましょう。

今回は ax.plot_wireframe() という方法でプロットしてみます。今までとコードはそんなに変わりません。基本的には ax.plot() の部分だけです。

[matplotlib wireframe 公式ドキュメント](https://matplotlib.org/3.1.0/gallery/mplot3d/wire3d_zero_stride.html)

まずは各種パラメーターを定義します。#paramsの下に追記してください。

```python
dataLength = 10  # １次元配列データの長さ
dataAmount = 20 # １度にプロットする１次元配列データの個数
```

次にプロットするデータを用意します。これはfor文の中に書きます。厳密に言えば乱数であるz以外はfor文で繰り返し定義する必要はありませんが、今は分かりやすさ優先で同じ場所に書いてしまいましょう。

```python
x = np.arange(0, dataLength, 1) # 0 ~ 10 の１次元配列
y = np.arange(0, dataAmount, 1) # 0 ~ 10 の１次元配列
gx, gy = np.meshgrid(x, y)  # grid
z = np.random.rand(y.shape[0], x.shape[0]) # 2次元乱数配列
```

最後にwireframe形式でプロットします。今まで使っていたax.plot()をコメントアウトして、ax.plot_wireframe()を追加します。

```python
# ax.plot(x, y, z) # 直線プロット
ax.plot_wireframe(gx, gy, z) # wireframeプロット
```
複数の１次元配列を表示する３Dグラフのコードまとめです。

```python
# 複数の１次元配列を表示する3次元リアルタイムグラフ
# Library	
import numpy as np # プロットするデータ配列を作成するため
import matplotlib.pyplot as plt # グラフ表示のため
from mpl_toolkits.mplot3d import Axes3D # ３Dグラフ作成のため

# params
frame = 30  # プロットするフレーム数
sleepTime = 0.5  # １フレーム表示する時間[s]
dataLength = 10  # １次元配列データの長さ
dataAmount = 20 # １度にプロットする１次元配列データの個数
zMax = 1 # z軸最大値
zMin = 0 # z軸最小値
xlabel = "x axis"
ylabel = "y axis"
zlabel = "z axis"

# making 3d figure object
fig = plt.figure() # figureオブジェクトを作る
ax = Axes3D(fig)

for i in range(frame):
    # getting data
    x = np.arange(0, dataLength, 1) # 0 ~ 10 の１次元配列
    y = np.arange(0, dataAmount, 1) # 0 ~ 10 の１次元配列
    gx, gy = np.meshgrid(x, y)  # grid
    z = np.random.rand(y.shape[0], x.shape[0]) # 2次元乱数配列

    # data shape checking
    print('x.shape', x.shape)
    print('y.shape', y.shape)
    print('gx.shape', gx.shape)
    print('gy.shape', gy.shape)
    print('z.shape', z.shape)
    
    # plot setting
    ax.set_zlim(zMin, zMax) # z軸固定
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_zlabel(zlabel)

    # plotting
    # ax.plot(x, y, z) # 直線プロット
    ax.plot_wireframe(gx, gy, z)
    plt.draw()
    plt.pause(sleepTime)
    plt.cla()
```
このままでもプロットすることはできますが、wireが張り巡らされていて、少し見にくいかもしれません。

[matplotlib wireframe 公式ドキュメント](https://matplotlib.org/3.1.0/gallery/mplot3d/wire3d_zero_stride.html)を参考に、wireの表示数を減らしてみましょう。

```python
ax.plot_wireframe(gx, gy, z, rstride=1, cstride=0) # y軸方向のwireの繋がりを削除
```
今回はwireframeを使用しましたが、他にも散布図を示すax.scatter()など色々なプロット方法があります。今まで示したコードのax.plot()を置き換えれば、様々な形式でプロットされたグラフでもリアルタイムに更新されていくはずです。自分のデータをどのように可視化したいか、イメージがはっきりしている場合は、"matplotlib 3d グラフ"、でGoogle画像検索を行うと、比較的簡単に欲しいサンプルコードに辿り着けます。

# グラフをカッコよくする

ここからはグラフの見た目をよりスタイリッシュにするコードを紹介します。ほとんどが自己満足の世界なので、興味のない方は読まなくても大丈夫です。

まずは使う色を決めましょう。自分でセンスの良い色を選べなくても、"色　組み合わせ"、"graph color set"、などと検索すればいい感じな色を紹介しているサイトがたくさん見つかるはずです。

```python
colorsets = 'skyblue'

# making stylish graph
if colorsets == 'skyblue':
    color1 = '#FFFFFF'  # background
    color2 = '#C7D7FF'  # background2
    color3 = '#5BB1E3'  # accent color
    color4 = '#4C4C4C'  # axis

elif colorsets == 'mint':
    color1 = '#222831'  # background
    color2 = '#393e46'  # background2
    color3 = '#6779FE'  # accent color
    color4 = '#eeeeee'  # axis
```

このようにいい感じな組み合わせの色をcolorsetsとしてまとめておけば、colorsetsを変更するだけで簡単にグラフのテーマカラーを変更できます。

次に　plt.rcParams を設定します。rcParamsの設定は最初の１度だけで済み、for文で繰り返し行う必要がないのでリアルタイムグラフの見た目を調整する際には優先して変更する部分です。

[matplotlib rcparams 公式ドキュメント](https://matplotlib.org/3.1.1/tutorials/introductory/customizing.html)

```python
xfigSize = 14 #　グラフを表示するウインドウのx方向の大きさ
yfigSize = 10 #　グラフを表示するウインドウのy方向の大きさ

plt.rcParams.update({
    'figure.figsize': [xfigSize, yfigSize],
    # 'axes.grid': False,
    'axes3d.grid': True,
    'grid.alpha': 0.2,
    'grid.linestyle': '-',
    'axes.grid.which': 'major',
    'axes.grid.axis': 'y',
    'grid.linewidth': 0.1,
    'font.size': 10,
    'grid.color': color4,
    'xtick.color': color4,  # x軸の色
    'ytick.color': color4,  # y軸の色
    'figure.facecolor': color1,  # 枠の外の背景色
    'figure.edgecolor': color1,
    'axes.edgecolor': color1,  # 枠色
    'axes.facecolor': 'none',  # 背景色　noneにするとfigure.facecolorと同じ色になる
    'axes.labelcolor': color4,  # 軸ラベル色
    'figure.dpi': 75.0,
    'figure.frameon': False,
})
```

ここは
[ 公式ドキュメント](https://matplotlib.org/3.1.1/tutorials/introductory/customizing.html)
を参考に色々試してみて、自分の気に入る設定を探してみて下さい。

背景を透明にします。これもfor文の外で定義できます。

```python
ax.w_xaxis.set_pane_color((0., 0., 0., 0.))  # ３dグラフの背景を透明にする　最初の一回だけでOK
ax.w_yaxis.set_pane_color((0., 0., 0., 0.))  # ３dグラフの背景を透明にする
ax.w_zaxis.set_pane_color((0., 0., 0., 0.))  # ３dグラフの背景を透明にする
```

最後にプロットする配列の色や透明度を指定します。

```python
Alpha = 1.0 # プロットした線の透明度 0:透明、1:不透明

ax.plot_wireframe(gx, gy, z, rstride=1, cstride=0, color=color3, alpha=Alpha)
```

見た目を調整したコードまとめ

```python
# 複数の１次元配列を表示する3次元リアルタイムグラフ
# Library	
import numpy as np # プロットするデータ配列を作成するため
import matplotlib.pyplot as plt # グラフ表示のため
from mpl_toolkits.mplot3d import Axes3D # ３Dグラフ作成のため

# params
frame = 15  # プロットするフレーム数
sleepTime = 0.5  # １フレーム表示する時間[s]
dataLength = 10  # １次元配列データの長さ
dataAmount = 3 # １度にプロットする１次元配列データの個数
zMax = 1 # z軸最大値
zMin = 0 # z軸最小値
xlabel = "x axis"
ylabel = "y axis"
zlabel = "z axis"
colorsets = 'skyblue'
xfigSize = 14 #　グラフを表示するウインドウのx方向の大きさ
yfigSize = 10 #　グラフを表示するウインドウのy方向の大きさ
Alpha = 1.0 # プロットした線の透明度

# making stylish graph
if colorsets == 'skyblue':
    color1 = '#FFFFFF'  # background
    color2 = '#C7D7FF'  # background2
    color3 = '#5BB1E3'  # accent color red: DE5E34 blue: 5BB1E3
    color4 = '#4C4C4C'  # axis

plt.rcParams.update({
    'figure.figsize': [xfigSize, yfigSize],
    # 'axes.grid': False,
    'axes3d.grid': True,
    'grid.alpha': 0.2,
    'grid.linestyle': '-',
    'axes.grid.which': 'major',
    'axes.grid.axis': 'y',
    'grid.linewidth': 0.1,
    'font.size': 10,
    'grid.color': color4,
    'xtick.color': color4,  # x軸の色
    'ytick.color': color4,  # y軸の色
    'figure.facecolor': color1,  # 枠の外の背景色
    'figure.edgecolor': color1,
    'axes.edgecolor': color1,  # 枠色
    'axes.facecolor': 'none',  # 背景色　noneにするとfigure.facecolorと同じ色になった
    'axes.labelcolor': color4,  # 軸ラベル色
    'figure.dpi': 75.0,
    'figure.frameon': False,
})

# making 3d figure object
fig = plt.figure() # figureオブジェクトを作る
ax = Axes3D(fig)

ax.w_xaxis.set_pane_color((0., 0., 0., 0.))  # ３dグラフの背景を透明にする　最初の一回だけでOK
ax.w_yaxis.set_pane_color((0., 0., 0., 0.))  # ３dグラフの背景を透明にする
ax.w_zaxis.set_pane_color((0., 0., 0., 0.))  # ３dグラフの背景を透明にする

for i in range(frame):
    # getting data
    x = np.arange(0, dataLength, 1) # 0 ~ 10 の１次元配列
    y = np.arange(0, dataAmount, 1) # 0 ~ 10 の１次元配列
    gx, gy = np.meshgrid(x, y)  # grid
    z = np.random.rand(y.shape[0], x.shape[0]) # 0 ~ 10 の１次元配列

    # data shape checking
    print('x.shape', x.shape)
    print('y.shape', y.shape)
    print('gx.shape', gx.shape)
    print('gy.shape', gy.shape)
    print('z.shape', z.shape)
    
    # plot setting
    ax.set_zlim(zMin, zMax) # z軸固定
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_zlabel(zlabel)

    # plotting
    # ax.plot(x, y, z) # 直線プロット
    ax.plot_wireframe(gx, gy, z, rstride=1, cstride=0, color=color3, alpha=Alpha)
    plt.draw()
    plt.pause(sleepTime)
    plt.cla()	
```
以上が３次元リアルタイムグラフについての解説です。グラフを満足いく見た目に調整するのは案外手間がかかりますが、リアルタイムプロットするだけなら20行程度のコードで書けます。必要に応じてコードを調整して使って下さい。

今後はより高速にプロットする手法を探していきたいと思います。
というのも、リアルタイムに各センサーで取得したデータを可視化する際、グラフ表示等のデータ処理はなるべく高速で行いすぐに次のデータを取得する必要があるからです。
もしmatplotlibよりも高速に3Dグラフをリアルタイムで表示できる方法を知っていれば教えて頂けると助かります。