matplotlibでカラーバーを自分で作って横に表示させる

#はじめに

最近グラフを作るのにpythonのmatplotlibを使用しています。
グラフ内の物理量変化を表すために、`plt.fill`という関数を使用しています。

https://cranethree.hatenablog.com/entry/2015/07/25/204608


さらに値の変化を色のグラデーションで表したく、`cm.jet`を使用して領域ごとに計算して色を決めています。


https://qiita.com/hokekiyoo/items/cea310b2c36a01b970a6

塗った結果がこちらです。

![Figure 2022-01-04 200740.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/7bd6df9c-4040-95ba-9910-3deaa1c66b30.png)


ここにカラーバーをつけたいのですが、手動で色を決めて塗ったため、`fig.colorbar(im)`などはできません。
そこでうまくカラーバーを自分で作成して横に表示させて以下のようにする方法を考えました。







![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/f918f054-db36-a3c8-e18a-9c1b5a9072b9.png)


#ベースとなるコード

上のようなコードは複雑なので、以下のコードをベースに作っていきます。




```base.py
import matplotlib.pyplot as plt
import matplotlib.cm as cm

#cross_point_x,cross_point_yは座標を格納した行列
#M_areaはその場所での今回色で表したい物理量を格納した行列

n=9

cross_point_x= [["nan" for j in range(n+2)] for i in range(n+2)]
cross_point_y= [["nan" for j in range(n+2)] for i in range(n+2)]
M_area = [["nan" for j in range(n+2)] for i in range(n+2)]

for i in range(1,n+2):
    for j in range(1,i+1):
        cross_point_x[i][j] = i*j
        cross_point_y[i][j] = i*(n+2-j)
        M_area[i][j] = i*j
        

color_Mmax = 100
color_Mmin=0


for i in range(2,n+2):
    for j in range(2,i):
        testx = [cross_point_x[i-1][j-1], cross_point_x[i-1][j], cross_point_x[i][j], cross_point_x[i][j-1]]
        testy = [cross_point_y[i-1][j-1], cross_point_y[i-1][j], cross_point_y[i][j], cross_point_y[i][j-1]]
        plt.fill(testx,testy,color=cm.jet((M_area[i][j]-color_Mmin)/(color_Mmax-color_Mmin)))
       
plt.show()
```



これを実行すると、以下のグラフが描けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/f118be4c-5830-6098-670e-707b31a3b8a5.png)




#案1　枠外に散布図を描く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/0efa9192-9792-d20a-ec40-1f333fca9ae4.png)

###メリット
きれいに描ける

###デメリット
手動でいろいろ調整しなければいけない

###解説
matplotlibでは散布図を描くと、`fig.colorbar(im)`を使用すると自動的にカラーバーを作ってくれます。
なので、ほしいグラフの描写範囲外に最大値と最小値をとる点をプロットして、カラーバーを作ります。
プロットした結果がこちら

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/5f00f1fe-3754-f0dc-431a-bfe8d276f347.png)

いらない点が2点描写されているので、`xlim`、`ylim`を使用して枠外になるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/0efa9192-9792-d20a-ec40-1f333fca9ae4.png)

このコードは以下のplan1.1pyです。



```plan1.1.py
import matplotlib.pyplot as plt
import matplotlib.cm as cm

#cross_point_x,cross_point_yは座標を格納した行列
#M_areaはその場所での今回色で表したい物理量を格納した行列

n=9

cross_point_x= [["nan" for j in range(n+2)] for i in range(n+2)]
cross_point_y= [["nan" for j in range(n+2)] for i in range(n+2)]
M_area = [["nan" for j in range(n+2)] for i in range(n+2)]

for i in range(1,n+2):
    for j in range(1,i+1):
        cross_point_x[i][j] = i*j
        cross_point_y[i][j] = i*(n+2-j)
        M_area[i][j] = i*j
        
color_Mmax = 100
color_Mmin=0

for i in range(2,n+2):
    for j in range(2,i):
        testx = [cross_point_x[i-1][j-1], cross_point_x[i-1][j], cross_point_x[i][j], cross_point_x[i][j-1]]
        testy = [cross_point_y[i-1][j-1], cross_point_y[i-1][j], cross_point_y[i][j], cross_point_y[i][j-1]]
        plt.fill(testx,testy,color=cm.jet((M_area[i][j]-color_Mmin)/(color_Mmax-color_Mmin)))


#---------------------------------------------
#追加分
#カラーバーを無理やり表示外にかく
#(-10,-10)に値color_Mmin、(-20,-20)に値color_Mmaxの値を持つ
a=[-10,-20]
b=[-10,-20]
c=[color_Mmin,color_Mmax]

#散布図を上から描写
img = plt.scatter(a,b,c=c,cmap=plt.cm.jet,alpha=1)
#カラーバーを自動で作成（plt.scatterを使ったときのみ使用可能）
plt.colorbar(img)

#描写範囲を主導で設定して追加した散布図を範囲外にする
plt.xlim(0,100)
plt.ylim(10,100)

#---------------------------------------------

plt.show()
```


`xlim`や`ylim`は自分で調整しなければいけないので、ここがめんどくさいポイントです。




#案2　subplotを使用して横に別の図として描写する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/2bda2a0c-3747-71df-7af2-9156eb0ca3ff.png)

###メリット
描写範囲を手動で設定する必要がない

###デメリット
コードがちょっと複雑

###解説
matplotlibには、`subpot`といって図を複数並べて描写する関数があります。
さらに、カラーバーのみを図としてプロットする関数もあります。

https://illumination-k.dev/posts/python/colorbar_and_normvalue

https://bourbaki.biz/matplotlib-colorbarbase-document/

これらを組み合わせて描写した結果が以下です。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/2bda2a0c-3747-71df-7af2-9156eb0ca3ff.png)

コードはこちら





```plan2.1.py
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib import gridspec
import matplotlib as mpl

#cross_point_x,cross_point_yは座標を格納した行列
#M_areaはその場所での今回色で表したい物理量を格納した行列

n=9

cross_point_x= [["nan" for j in range(n+2)] for i in range(n+2)]
cross_point_y= [["nan" for j in range(n+2)] for i in range(n+2)]
M_area = [["nan" for j in range(n+2)] for i in range(n+2)]

for i in range(1,n+2):
    for j in range(1,i+1):
        cross_point_x[i][j] = i*j
        cross_point_y[i][j] = i*(n+2-j)
        M_area[i][j] = i*j
        

color_Mmax = 100
color_Mmin=0

#---------------------------------------
#図を二つ作成
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
#---------------------------------------

for i in range(2,n+2):
    for j in range(2,i):
        testx = [cross_point_x[i-1][j-1], cross_point_x[i-1][j], cross_point_x[i][j], cross_point_x[i][j-1]]
        testy = [cross_point_y[i-1][j-1], cross_point_y[i-1][j], cross_point_y[i][j], cross_point_y[i][j-1]]
        ax1.fill(testx,testy,color=cm.jet((M_area[i][j]-color_Mmin)/(color_Mmax-color_Mmin)))
#ax1.fillというようにサブプロットに変更


#--------------------------------------------------------------
#カラーバーの手動作成
norm = mpl.colors.Normalize(vmin=color_Mmin, vmax=color_Mmax)
cbar = mpl.colorbar.ColorbarBase(
    ax=ax2,
    cmap=plt.cm.jet,
    norm=norm,
    orientation="vertical",
    label="Mach number",
)
#--------------------------------------------------------------

plt.show()
```


##図の比率を変更する


`plan2.1.py`のコードだと、カラーバーが太すぎるという問題があります。
そこで、カラーバーの比率を変えます。

https://www.delftstack.com/ja/howto/matplotlib/how-to-make-different-subplot-sizes-in-matplotlib/


`図：カラーバー=50:1`とした結果がこちらです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/96bd2731-e3e1-3ec2-a758-738a38247dd0.png)


```plan2.2.py
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib import gridspec
import matplotlib as mpl


#cross_point_x,cross_point_yは座標を格納した行列
#M_areaはその場所での今回色で表したい物理量を格納した行列

n=9

cross_point_x= [["nan" for j in range(n+2)] for i in range(n+2)]
cross_point_y= [["nan" for j in range(n+2)] for i in range(n+2)]
M_area = [["nan" for j in range(n+2)] for i in range(n+2)]

for i in range(1,n+2):
    for j in range(1,i+1):
        cross_point_x[i][j] = i*j
        cross_point_y[i][j] = i*(n+2-j)
        M_area[i][j] = i*j
        

color_Mmax = 100
color_Mmin=0


#----------------------------------------------------
fig = plt.figure()
#ここ追加、width_ratiosで比率を変える
spec = gridspec.GridSpec(ncols=2, nrows=1,
                         width_ratios=[50, 1])

ax1 = fig.add_subplot(spec[0])
ax2 = fig.add_subplot(spec[1])
#----------------------------------------------------




for i in range(2,n+2):
    for j in range(2,i):
        testx = [cross_point_x[i-1][j-1], cross_point_x[i-1][j], cross_point_x[i][j], cross_point_x[i][j-1]]
        testy = [cross_point_y[i-1][j-1], cross_point_y[i-1][j], cross_point_y[i][j], cross_point_y[i][j-1]]
        ax1.fill(testx,testy,color=cm.jet((M_area[i][j]-color_Mmin)/(color_Mmax-color_Mmin)))



norm = mpl.colors.Normalize(vmin=color_Mmin, vmax=color_Mmax)
cbar = mpl.colorbar.ColorbarBase(
    ax=ax2,
    cmap=plt.cm.jet,
    norm=norm,
    orientation="vertical",
    label="Mach number",
)


plt.show()
```




#まとめ


異なる2つの方法を紹介しました。
それぞれに特徴があるので、使いやすい方法を使ってみてください。