Python matplotlib 説明図を書いてみる（改訂版）

2018.08.28：プログラムの使い回しを考えてコード部分を書き換えました．やっていることに変わりはありません．最近，[boostnoteの記事](https://qiita.com/damyarou/items/cf1d65a5c62c53e66b26)を書きましたが，これに保存するコードそのものが，ある程度使いまわしを考慮したものでないと，結局個別に苦労することになってしまうため，使い回しを意識したコーディングの重要性を改めて感じています．「使い回しの良さ」はプロのプログラマでなくても，仕事で楽をしようとしてプログラムを利用する人たち（時間に押される人たちというべきか？）共通の着眼点のような気がします．

画像形式（2018.08.28 追記）：自分が取り扱う画像は，グラフや説明図が多いので，昔はpngを使っていたのですが，TeX（basic TeX）の dvopdfmx で pdf 化する時，とても遅いのと不安定（画像が表示されたりされなかったりする）なので，最近は jpg を使っています．TeX での扱いにおいて安定性が高いです（pdf 化した時，画像が表示されないということが無い）

# はじめに
こんな図をmatplotlibで作ってみました．「matplotlibでこんなことやっている人もいるんだ」という事例紹介です．このような説明図は作るのが面倒くさく時間がかかるため，比較的余裕がある日や休日・夜間などを利用してプログラミングしています．結局はお客さんに提出する報告書に載るので，実寸感覚のない図面は採用する気がしない乗せる気がしない（発注者側にいた時，寸法感覚のない図面を平気で報告書に載せてくる人たちがいたので，立場が変わって気をつけるようにしています）．でも，一回作っておくと色々使い回しができて便利です．また図全体を使わなくても，いくつか作例を持っていると，「こういう場合はこうする」というTipsとしても役に立ちます．

たわみ線，モーメント図，応力分布は一応理論通りに計算したものを描いています．
引数の指定が少し煩雑ですが，寸法線（両矢印）や分布荷重（片矢印）を描くには，annotateが便利です．


<table>
<tr>
<td><img src="https://qiita-image-store.s3.amazonaws.com/0/129300/099ca809-cc94-84d6-fef3-c75e232f130f.jpeg" width=600px></td>
<td><img src="https://qiita-image-store.s3.amazonaws.com/0/129300/985bed21-631c-eae9-f172-9ab94dc629b1.jpeg" width=500px></td>
</tr>
<tr><td>スラブ設計のためのストリップ法の説明図</td><td>圧縮を受ける領域の円孔周りの応力分布</td></tr>
</table>

<table>
<tr>
<td>
<img src="https://qiita-image-store.s3.amazonaws.com/0/129300/b4bd1508-d0ee-d7d5-297f-898ffd91358e.jpeg" width=400px>
</td>
<td>
<img src="https://qiita-image-store.s3.amazonaws.com/0/129300/7ce4a72f-7dbc-0bd5-9070-eba8c1f1fa48.jpeg" width=300px>
</td>
</tr>
<tr>
<td>モール・クーロン破壊基準に対するせん断安全率説明図</td>
<td>日本語を含む簡単な説明図</td>
</tr>
</table>

上記モール円の図は，[Python matplotlib 矢印をannotateで描く](https://qiita.com/damyarou/items/839325f73c3118f2be3b) で紹介したものですが，プログラムとして共通性のあるこちらの記事にも，書き換えたコードとともに掲載することにしました．

なお，当方の環境は以下のとおりです．

+ MacBook Pro (Retina, 13-inch, Mid 2014)
+ macOS High Sierra
+ Python 3.7.0
+ matplotlib 2.2.2

# 作り方
下の図に示すように，方眼紙を作り，その中に必要事項を書き込んでいきます．昔なら定規と鉛筆で描いて切り貼りで原稿を作っていたものをmatplotlibにやらせて画像を作成しているだけです．描き終わったら方眼紙目盛りを消して画像化し，最近はPythonプログラム（昔はI mageMagick）で余白処理をして完成です．余白処理は[ここの最後](https://qiita.com/damyarou/items/fa631320ca4c0d635ebf)を参照．

<img src="https://qiita-image-store.s3.amazonaws.com/0/129300/b9503051-baf3-57ce-1e1e-8e1c7fd339eb.png" width=600px>


# プログラム構成
いずれの作図プログラムも以下の考え方で構成しました．これは，使い回しの時便利になるよう考えてみた構成です．

+ モジュール読み込み（共通で使える）
+ annotateを使う部分の関数（必要なものを選択して再利用）
+ 主作図部分の関数（描画対象個別に作成）
+ フラフ全体枠定義部の関数（main）（概ね共通で使える）
+ ```if __name__ == '__main__': main()```で関数mainを呼んで実行（共通）[こちらの記事：「pythonで小さなツールを作る時のtips」](https://qiita.com/m_mizutani/items/c48f67f871d1d41ff4b9) を見て「なるほど」と思い実践しました．


# モール円とせん断安全率説明のプログラム
## モジュール読み込み

```py
import numpy as np
import matplotlib.pyplot as plt
```

## 座標軸描画関数
この関数を使い回す可能性は低いですが，annotateを含んでおり引数が多いので，関数として独立させました．

```py

def draw_axes(xmin,xmax,ymin,ymax):
    col='#000000'
    arst='<|-,head_width=0.3,head_length=0.5'
    aprop=dict(arrowstyle=arst,connectionstyle='arc3',facecolor=col,edgecolor=col,lw=1)
    plt.annotate(r'$\sigma$',
        xy=(xmin,0), xycoords='data',
        xytext=(xmax-0.5,0), textcoords='data', fontsize=20, ha='left',va='center', arrowprops=aprop)
    plt.annotate(r'$\tau$',
        xy=(0,0), xycoords='data',
        xytext=(0,ymax-1), textcoords='data', fontsize=20, ha='center',va='bottom', arrowprops=aprop)
```

## 両矢印の寸法線＋テキスト描画関数
この関数は使い回しができそうです．機能は，両矢印の真ん中上部あるいは下部に指定したテキストを描画するものです．斜めの寸法線も描画可能です（完璧ではないかもしれませんが）．annotateでは矢印のみを描き，テキストは別途plt.textで描画しています．ただし用いる座標が共通するので，同じ関数内に両矢印描画とテキスト描画を詰め込みました．引数の概要は以下の通り．

+ (x1,y1)：両矢印始点
+ (x2,y2)：両矢印終点
+ ss：描画文字列
+ dd：文字列中央の寸法線からの距離
+ ii：１なら寸法線の上側，２なら寸法線の下側にテキストを描画

```py
def barrow(x1,y1,x2,y2,ss,dd,ii):
    col='#333333'
    arst='<->,head_width=3,head_length=10'
    aprop=dict(arrowstyle=arst,connectionstyle='arc3',facecolor=col,edgecolor=col,shrinkA=0,shrinkB=0,lw=1)
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0, arrowprops=aprop)
    al=np.sqrt((x2-x1)**2+(y2-y1)**2)
    cs=(x2-x1)/al
    sn=(y2-y1)/al
    rt=np.degrees(np.arccos(cs))
    if y2-y1<0: rt=-rt
    if ii==1:
        xs=0.5*(x1+x2)-dd*sn
        ys=0.5*(y1+y2)+dd*cs
        plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)
    else:
        xs=0.5*(x1+x2)+dd*sn
        ys=0.5*(y1+y2)-dd*cs
        plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)
```

## 主作図部分
ここが作図の主要部分です．描画したい各点の座標を計算するために，材料特性の数値が必要となりますが，これは別途関数 ```material()``` で定義し，この関数の最初で読み込んでいます．あとはやりたいことをどかどか書き込んでいきます．

```py
def drawfig(xmin,xmax,ymin,ymax,fsz):
    # definition of parameter 
    c,phi,sigt,sig1,sig2=material() # basic properties of material
    rr=0.5*(sig1-sig2)    # radius of Mohr circle
    sig0=0.5*(sig1+sig2)  # average stress
    dd1=(c-sig0*np.tan(np.radians(phi)))*np.cos(np.radians(phi)) # value of D1
    dd2=dd1-rr            # value of d1
    cs=np.cos(np.radians(phi))
    sn=np.sin(np.radians(phi))
    # x-y axses
    draw_axes(xmin,xmax,ymin,ymax)
    # failure criterion
    x1=sig0-1
    y1=c-np.tan(np.radians(phi))*x1
    xt=sigt
    yt=c-np.tan(np.radians(phi))*xt
    plt.plot([x1,xt,xt],[y1,yt,0],'-',color='#0000ff',lw=2)
    # Mohr circle
    theta=np.linspace(0,np.pi,180,endpoint=True)
    x=rr*np.cos(theta)+sig0
    y=rr*np.sin(theta)
    plt.plot(x,y,'-',color='#0000ff',lw=2)
    # intersection point
    xd1=sig0+dd1*sn # x-coordinate for line D1
    yd1=dd1*cs      # y-coordinate on Mohr-Coulomb line
    ds=0.2
    xd2=xd1+ds      # x-coordinate for line d2 on criterion
    yd2=c-xd2*np.tan(np.radians(phi)) # y-coordinate for line d2 on criterion
    xd3=xd2-dd2*sn  # x-coordinate for line d1 on Mohr circle
    yd3=yd2-dd2*cs  # y-coordinate for line d1 on Mohr circle
    x=[sig2,sig0,sig1,sigt,0,xd1]
    y=[0,0,0,0,c,yd1]
    plt.plot(x,y,'o',markersize=6,color='#000000') # plot of points

    # drawing of dimension line and text
    dd=0.35
    x1=sig0      ; y1=0     ; x2=xd1 ; y2=yd1; ss=r'$D_1$'; barrow(x1,y1,x2,y2,ss,dd,1)
    x1=xd3       ; y1=yd3   ; x2=xd2 ; y2=yd2; ss=r'$d_1$'; barrow(x1,y1,x2,y2,ss,dd,2)
    x1=sig0      ; y1=-1    ; x2=sigt; y2=y1 ; ss=r'$D_2$'; barrow(x1,y1,x2,y2,ss,dd,2)
    x1=sig1      ; y1=-0.3  ; x2=sigt; y2=y1 ; ss=r'$d_2$'; barrow(x1,y1,x2,y2,ss,dd,2)
    x1=sig0-rr*cs; y1=rr*sn ; x2=sig0; y2=0  ; ss=r'$\frac{\sigma_1-\sigma_2}{2}$'; barrow(x1,y1,x2,y2,ss,dd,1)

    # drawing of text
    dd=0.1
    xs=sig2-dd; ys=dd  ; ss=r'$\sigma_2$'                   ; plt.text(xs,ys,ss,color='#000000',ha='right',va='bottom')
    xs=sig0   ; ys=-dd ; ss=r'$\frac{\sigma_1+\sigma_2}{2}$'; plt.text(xs,ys,ss,color='#000000',ha='center',va='top')
    xs=sig1+dd; ys=dd  ; ss=r'$\sigma_1$'                   ; plt.text(xs,ys,ss,color='#000000',ha='left',va='bottom')
    xs=sigt+dd; ys=dd  ; ss=r'$\sigma_t$'                   ; plt.text(xs,ys,ss,color='#000000',ha='left',va='bottom')
    xs=dd     ; ys=c+dd; ss=r'$c$'                          ; plt.text(xs,ys,ss,color='#000000',ha='left',va='bottom')
    xs=sigt-1 ; ys=yt  ; ss=r'$\phi$'                       ; plt.text(xs,ys,ss,color='#000000',ha='center',va='bottom')
    plt.plot([sigt,sigt-1.5],[yt,yt],'-',color='#000000',lw=1)
    xs=0.5*(sig0+sig1); ys=c-xs*np.tan(np.radians(phi))+0.7; ss="Mohr-Coulomb's\nFailure Criterion"
    plt.text(xs,ys,ss,ha='center',va='center',rotation=-phi,fontsize=fsz-2)
```

## 材料特性定義のための関数
このように材料物性に依存する図を書く場合，材料特性の定義が必要となります．ただ特殊なので，関数```main``` にはいれず，独立させました．材料力学をやったことがある方ならご存知とは思いますし，プログラム内に下手な英語でコメントしているので，具体的説明は省きます．

```py
def material():
    # definition of basic parameter
    c=3      # cohesion
    phi=30   # internal friction angle
    sigt=2   # tensile strength
    sig1=-1  # 1st principal stress
    sig2=-7  # 2nd principal stress
    return c,phi,sigt,sig1,sig2
```

## グラフ全体枠定義部の関数（main）
作図領域を定め，作業用のグラフ用紙を作図し，関数```drawfig```を呼んで描画します．この部分は，この```main()``` 関数の呼び出し・実行部と合わせて，若干の修正により色々使い回しができると思います．
この手の説明図は，基本的に縦横比は１に固定しています．
また画像サイズは，この事例では横を８（800px）に固定し，指定した作図範囲に合わせて高さを自動で決めています．図の高さはピクセル単位で整数化すべきなのでしょうが，エラーにならないので無視しています．
この事例では，変数 iflag=1 としてグラフ用紙を表示しながら作業し，完成したら iflag=0 としてグラフ用紙表示を消すとともに画像ファイルとして出力するようにしています．

```py
def main():
    iflag=1 # 1: show grid, 0: no grid and save image
    xmin=-8  # lower limit of x-axis
    xmax=4   # upper limit of x-axis
    dx=1     # increment of x-axis
    ymin=-2  # lower limit of y-axis
    ymax=7   # upper limit of y-axis
    dy=1     # increment of y-axis

    fsz=20   # fontsize
    iw=8     # image width
    ih=iw*(ymax-ymin)/(xmax-xmin)
    fig=plt.figure(figsize=(iw,ih),facecolor='w')
    plt.rcParams['font.size']=fsz
    plt.rcParams['font.family']='sans-serif'

    plt.xlim([xmin,xmax])
    plt.ylim([ymin,ymax])
    if iflag==0:
        plt.axis('off')
    else:
        plt.xlabel('x_axis')
        plt.ylabel('y_axis')
        plt.xticks(np.arange(xmin,xmax+dx,dx))
        plt.yticks(np.arange(ymin,ymax+dy,dy))
        plt.grid(color='#999999',linestyle='solid')
    plt.gca().set_aspect('equal',adjustable='box')

    drawfig(xmin,xmax,ymin,ymax,fsz)
    
    plt.tight_layout()
    if iflag==0:
        fnameF='fig_mohr.jpg'
        plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
    plt.show()
```

## メイン関数呼び出しと実行
以前はこれを使わず，本来はグローバル変数なのにそれを引数で関数に渡すなど，おかしなことをやっていましたが，この表記とすることで，変数はすべてローカルとなり，引数のやりとりの意味がはっきりしたような気がします．

```py
#==============
# Execution
#==============
if __name__ == '__main__': main()
```



# ストリップ法説明図のプログラム
## 簡単な説明
プログラムの全体構成は，モール円のものとほぼ同じです．
ここでは，寸法線＋テキストを表示する関数```barrow```に加えて，荷重を示す片矢印を表示する必要があるので，これを表示する```sarrow```という関数を定義しています．

関数```sarrow```のみを以下に示します．色や太さなどの矢印の特性は固定とし，矢の先端座標（x1,y1）と矢印の始点(x2,y2)のみを引数にしています．この関数を for 文の中で繰り返し呼び出すことにより分布荷重を描画します．

```py
def sarrow(x1,y1,x2,y2):
    col='#000000'
    sv=0
    aprop=dict(shrink=sv,width=1,headwidth=5,headlength=8,
               connectionstyle='arc3',facecolor=col,edgecolor=col)
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0, arrowprops=aprop)
```

## プログラム全文
プログラム全文は以下に示すとおり．


```py:
import numpy as np
import matplotlib.pyplot as plt


def barrow(x1,y1,x2,y2,ss,dd,ii):
    col='#333333'
    arst='<->,head_width=3,head_length=10'
    aprop=dict(arrowstyle=arst,connectionstyle='arc3',facecolor=col,edgecolor=col,shrinkA=0,shrinkB=0,lw=1)
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0, arrowprops=aprop)
    al=np.sqrt((x2-x1)**2+(y2-y1)**2)
    cs=(x2-x1)/al
    sn=(y2-y1)/al
    rt=np.degrees(np.arccos(cs))
    if y2-y1<0: rt=-rt
    if ii==1:
        xs=0.5*(x1+x2)-dd*sn
        ys=0.5*(y1+y2)+dd*cs
        plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)
    else:
        xs=0.5*(x1+x2)+dd*sn
        ys=0.5*(y1+y2)-dd*cs
        plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)


def sarrow(x1,y1,x2,y2):
    col='#000000'
    sv=0
    aprop=dict(shrink=sv,width=1,headwidth=5,headlength=8,
               connectionstyle='arc3',facecolor=col,edgecolor=col)
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0, arrowprops=aprop)

        
def drawfig(xmin,xmax,ymin,ymax,fsz):
    # draw plate including boundary condition (hatch)
    lx=5; ly=4; dd=0.5
    plt.fill([-dd,lx+dd,lx+dd,-dd],[-dd,-dd,ly+dd,ly+dd],fill=False, hatch='//',lw=0)
    plt.fill([0,lx,lx,0],[0,0,ly,ly],facecolor='#ffffff',edgecolor='#000000',lw=3)
    plt.plot([0,lx],[ly/2,ly/2],color='#000000',lw=1)
    plt.plot([lx/2,lx/2],[0,ly],color='#000000',lw=1)
    # boundary conditions
    ss='Fixed'
    xs=lx/4; ys=ly-0.05; rt=0; plt.text(xs,ys,ss,ha='center',va='top',rotation=rt,fontsize=fsz-2)
    xs=lx/4; ys=0+0.05; rt=0; plt.text(xs,ys,ss,ha='center',va='bottom',rotation=rt,fontsize=fsz-2)
    xs=0+0.05; ys=ly/4*3; rt=90; plt.text(xs,ys,ss,ha='left',va='center',rotation=rt,fontsize=fsz-2)
    xs=lx-0.05; ys=ly/4*3; rt=90; plt.text(xs,ys,ss,ha='right',va='center',rotation=rt,fontsize=fsz-2)
    # draw deflection curve
    p=1; EI=1
    xx=np.linspace(0,lx,21)
    y=p*lx**4/24/EI*((xx/lx)**2-2*(xx/lx)**3+(xx/lx)**4)
    y=-y/np.max(y)*0.5+ly/2
    plt.plot(xx,y,color='#000000',lw=1)
    yy=np.linspace(0,ly,21)
    x=p*ly**4/24/EI*((yy/ly)**2-2*(yy/ly)**3+(yy/ly)**4)
    x=x/np.max(x)*0.5+lx/2
    plt.plot(x,yy,color='#000000',lw=1)
    plt.text(4,1.4,r'$\delta_x$',va='center',ha='center',fontsize=fsz,rotation=0)
    plt.text(3.3,3,r'$\delta_y$',va='center',ha='center',fontsize=fsz,rotation=90)
    # draw dimension lines
    dd=0.4
    x1= 0; y1=ly+1; x2=lx; y2=y1; ss=r'$l_x$'; barrow(x1,y1,x2,y2,ss,dd,1)
    x1=-1; y1=0   ; x2=x1; y2=ly; ss=r'$l_y$'; barrow(x1,y1,x2,y2,ss,dd,1)

    # draw moment diagram
    y0=-3; x0=8.5
    mx=p*lx**2/2*(-1/6+xx/lx-xx**2/lx**2)
    my=p*ly**2/2*(-1/6+yy/ly-yy**2/ly**2)
    mx=mx/np.min(mx)*1.0+y0
    my=-my/np.min(my)*1.0+x0
    plt.plot(xx,mx,color='#000000',lw=1)
    plt.fill_between(xx,mx,y0,color='#aaaaaa')
    plt.plot([0,0,lx,lx],[y0+1,y0,y0,y0+1],color='#000000',lw=1)
    plt.plot(my,yy,color='#000000',lw=1)
    plt.fill_betweenx(yy,my,x0,color='#aaaaaa')
    plt.plot([x0-1,x0,x0,x0-1],[0,0,ly,ly],color='#000000',lw=1)
    # draw loads
    wx=0.5
    xx1=np.arange(0,5+0.5,0.5)
    for x1 in xx1:
        x2=x1;y1=y0+1.5; y2=y1+wx; sarrow(x1,y1,x2,y2)
    wy=0.8
    yy1=np.arange(0,4+0.5,0.5)
    for y1 in yy1:
        x1=x0-1.5; x2=x1-wy; y2=y1; sarrow(x1,y1,x2,y2)
    # draw text information
    ss=r'$w_x$'      ; xs=lx/2  ; ys=y0+1.2; rt=0 ; plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)
    ss=r'$w_y$'      ; xs=x0-1.2; ys=ly/2  ; rt=90; plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)
    ss='Moment $M_x$'; xs=lx/2  ; ys=y0+0.4; rt=0 ; plt.text(xs,ys,ss,ha='center',va='center',rotation=rt,fontsize=fsz-2)
    ss='Moment $M_y$'; xs=x0-0.4; ys=ly/2  ; rt=90; plt.text(xs,ys,ss,ha='center',va='center',rotation=rt,fontsize=fsz-2)
    ss=r'$-\frac{w_x\cdot l_x{}^2}{12}$'; xs=lx    ; ys=y0+1.1;rt=0; plt.text(xs,ys,ss,ha='left',va='center',rotation=rt)
    ss=r'$\frac{w_x\cdot l_x{}^2}{24}$' ; xs=lx/2  ; ys=y0-1.0;rt=0; plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)
    ss=r'$-\frac{w_y\cdot l_y{}^2}{12}$'; xs=x0-1  ; ys=ly;rt=90   ; plt.text(xs,ys,ss,ha='center',va='bottom',rotation=rt)
    ss=r'$\frac{w_y\cdot l_y{}^2}{24}$' ; xs=x0+1.0; ys=ly/2;rt=90 ; plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)

    ss='Strip method for All edges fixed rectangular plate\nunder uniformly distributed load $w$'
    xs=0.5*(xmin+xmax); ys=ymax; rt=0
    plt.text(xs,ys,ss,ha='center',va='top',rotation=rt,fontsize=fsz-2)


def main():
    iflag=1 # 1: show grid, 0: no grid and save image
    xmin=-2; xmax=11; dx=1
    ymin=-5; ymax= 7; dy=1

    fsz=20
    iw=8
    ih=iw*(ymax-ymin)/(xmax-xmin)
    fig=plt.figure(figsize=(iw,ih),facecolor='w')
    plt.rcParams['font.size']=fsz
    plt.rcParams['font.family']='sans-serif'

    plt.xlim([xmin,xmax])
    plt.ylim([ymin,ymax])
    if iflag==0:
        plt.axis('off')
    else:
        plt.xlabel('x_axis')
        plt.ylabel('y_axis')
        plt.xticks(np.arange(xmin,xmax+dx,dx))
        plt.yticks(np.arange(ymin,ymax+dy,dy))
        plt.grid(color='#999999',linestyle='solid')
    plt.gca().set_aspect('equal',adjustable='box')

    drawfig(xmin,xmax,ymin,ymax,fsz)
    
    plt.tight_layout()
    if iflag==0:
        fnameF='fig_strip.jpg'
        plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
    plt.show()


#==============
# Execution
#==============
if __name__ == '__main__': main()
```



# 圧縮を受ける円孔周り応力分布説明図のプログラム
## 簡単な説明
ここでは，両矢印寸法線＋テキスト描画関数```barrow```および荷重表示用片矢印描画関数```sarrow```に加え，本来の annotation を行う関数```atext```を定義しています．

関数```atext```のみを以下に示します．
引数の説明は以下の通り．

+ (x1,y1)：矢印先端座標
+ (x2,y2)：矢印始点（テキスト描画）座標
+ ss：表示テキスト
+ fsz：フォントサイズ

connectionstyle を arc3 に固定していますが，直線の矢印＋説明テキストという組み合わせでは使い回しができると思います．

```py
def atext(x1,y1,x2,y2,ss,fsz):
    col='#000000'
    sv=0
    aprop=dict(shrink=sv,width=0.3,headwidth=4,headlength=8,
               connectionstyle='arc3',facecolor=col,edgecolor=col)
    plt.annotate(ss,
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=fsz-3, arrowprops=aprop)
```

## プログラム全文
プログラム全文は以下に示すとおりです．

```py
import numpy as np
import matplotlib.pyplot as plt


def barrow(x1,y1,x2,y2,ss,dd,ii):
    col='#333333'
    arst='<->,head_width=3,head_length=10'
    aprop=dict(arrowstyle=arst,connectionstyle='arc3',facecolor=col,edgecolor=col,shrinkA=0,shrinkB=0,lw=1)
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0, arrowprops=aprop)
    al=np.sqrt((x2-x1)**2+(y2-y1)**2)
    cs=(x2-x1)/al
    sn=(y2-y1)/al
    rt=np.degrees(np.arccos(cs))
    if y2-y1<0: rt=-rt
    if ii==1:
        xs=0.5*(x1+x2)-dd*sn
        ys=0.5*(y1+y2)+dd*cs
        plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)
    else:
        xs=0.5*(x1+x2)+dd*sn
        ys=0.5*(y1+y2)-dd*cs
        plt.text(xs,ys,ss,ha='center',va='center',rotation=rt)


def sarrow(x1,y1,x2,y2):
    col='#000000'
    sv=0
    aprop=dict(shrink=sv,width=1,headwidth=5,headlength=8,
               connectionstyle='arc3',facecolor=col,edgecolor=col)
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0, arrowprops=aprop)


def atext(x1,y1,x2,y2,ss,fsz):
    col='#000000'
    sv=0
    aprop=dict(shrink=sv,width=0.3,headwidth=4,headlength=8,
               connectionstyle='arc3',facecolor=col,edgecolor=col)
    plt.annotate(ss,
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=fsz-3, arrowprops=aprop)

        
def drawfig(xmin,xmax,ymin,ymax,fsz):
    # draw circular hole
    r=1
    theta=np.linspace(0,2*np.pi,360)
    x=r*np.cos(theta)
    y=r*np.sin(theta)
    plt.fill(x,y,facecolor='#eeeeee',edgecolor='#000000',lw=2) # circular hole
    dd=0.15; x1=0; y1=0; x2=r*np.cos(np.pi/6); y2=r*np.sin(np.pi/6); ss=r'$r$'; barrow(x1,y1,x2,y2,ss,dd,2)
    plt.text(-0.15,-0.15,r'$o$',ha='center',va='center',rotation=0)
    # axis for sig_x including tick marks
    ds=0.1;dds=0.1
    plt.plot([0,0,2],[4,1,1],color='#000000',lw=1)
    plt.hlines([2,3,4],-ds,ds,color='#000000',linestyle='solid')
    plt.vlines([0.5,1,1.5,2],1-ds,1+ds,color='#000000',linestyle='solid')
    plt.text(0,4+dds,r'$y/r$',va='bottom',ha='center',color='#000000')
    plt.text(2+dds,1,r'$\sigma_x/q$',va='center',ha='left',color='#000000')
    # axis for sig_y including tick marks
    plt.plot([1,1,4],[-2,0,0],color='#000000',lw=1)
    plt.hlines([-0.5,-1,-1.5,-2],1-ds,1+ds,color='#000000',linestyle='solid')
    plt.vlines([2,3,4],-ds,ds,color='#000000',linestyle='solid')
    plt.text(4+dds,0,r'$x/r$',va='center',ha='left',color='#000000')
    plt.text(1,-2-dds,r'$\sigma_y/q$',va='top',ha='center',color='#000000')
    # draw digit
    for i in range(1,4):
        plt.text(-2.5*ds,1+float(i),str(i+1),va='center',ha='center',fontsize=fsz-3)
        plt.text(0.5*float(i),1-2.5*ds,str(i),va='center',ha='center',fontsize=fsz-3)
        plt.text(1+float(i),2.5*ds,str(i+1),va='center',ha='center',fontsize=fsz-3)
        plt.text(1-2.5*ds,-0.5*float(i),str(i),va='center',ha='center',fontsize=fsz-3)
    # draw load
    xx1=np.arange(-1.5,1.5+0.25,0.25)
    sp=0.5
    for x1 in xx1:
        x2=x1;
        y1=-2.6; y2=y1-sp; sarrow(x1,y1,x2,y2)
        y1= 4.6; y2=y1+sp; sarrow(x1,y1,x2,y2)
    plt.text(1.75, 4.9,r'$q$ (compression)',va='center',ha='left',color='#000000')
    plt.text(1.75,-2.9,r'$q$ (compression)',va='center',ha='left',color='#000000')
    # draw stress distribution
    tt=np.arange(1,4+0.1,0.1)
    sigx=r**2/2/tt**2-3*r**4/2/tt**4
    sigy=1+r**2/2/tt**2+3*r**4/2/tt**4
    plt.fill_betweenx(tt,0,-0.5*sigx,facecolor='#cccccc')
    plt.fill_between(tt,0,-0.5*sigy,facecolor='#cccccc')
    plt.plot(-0.5*sigx,tt,color='#555555',lw=2)
    plt.plot(tt,-0.5*sigy,color='#555555',lw=2)
    plt.text(2.5,-0.3,'Compression',va='center',ha='center',color='#000000',fontsize=fsz-3)
    # annotation
    ss='Tension force\n$T=-0.1925\cdot r\cdot q$'
    x1=0.2; y1=1.1; x2=x1+0.25; y2=y1+1; atext(x1,y1,x2,y2,ss,fsz)
    ss=r'$\sqrt{3}\cdot r$'
    x1=0; y1=np.sqrt(3); x2=x1-0.5; y2=y1; atext(x1,y1,x2,y2,ss,fsz)
    

def main():
    iflag=1 # 1: show grid, 0: no grid and save image
    xmin=-2; xmax=5; dx=1
    ymin=-4; ymax=6; dy=1

    fsz=20
    iw=8
    ih=iw*(ymax-ymin)/(xmax-xmin)
    fig=plt.figure(figsize=(iw,ih),facecolor='w')
    plt.rcParams['font.size']=fsz
    plt.rcParams['font.family']='sans-serif'

    plt.xlim([xmin,xmax])
    plt.ylim([ymin,ymax])
    if iflag==0:
        plt.axis('off')
    else:
        plt.xlabel('x_axis')
        plt.ylabel('y_axis')
        plt.xticks(np.arange(xmin,xmax+dx,dx))
        plt.yticks(np.arange(ymin,ymax+dy,dy))
        plt.grid(color='#999999',linestyle='solid')
    plt.gca().set_aspect('equal',adjustable='box')

    drawfig(xmin,xmax,ymin,ymax,fsz)
    
    plt.tight_layout()
    if iflag==0:
        fnameF='fig_hole.jpg'
        plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
    plt.show()


#==============
# Execution
#==============
if __name__ == '__main__': main()
```

# 日本語を含む簡単な説明図
## 簡単な説明
日本語を含む場合のケースです，プログラム作成上の注意事項としては以下の通り．

+ 日本語フォントを扱うため，FontProperties をインポート
+ 日本語フォント名は，関数 main() でフルパスで FontProperties 内（変数：fp）に指定．
+ 日本語のフォントサイズもFontProperties内に指定．plt.text 内のfontsizeで制御しようとしましたがうまくいきませんでした．
+ 下向き矢印を描く関数 varrow を定義．矢印とボックスの線が重なるため，わずかにシュリンケージを入れました（sv=0.05）．また太い矢印とするため，arrowprops（変数：aprop）のwidth, headwidth, headlength の値をいじっています．
+ 文字を中心とした図の場合，縦横比が１では行間が空きすぎるので，main関数の中で縦横比を指定できるようにしました（変数：raspect）．この事例では raspect=0.8 としています．これにより描画領域の縦横比を調整するとともに，```plt.gca().set_aspect(raspect,adjustable='box')``` でも縦横比の調整を行っています．


## プログラム全文

```py
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties


def varrow(x1,y1,x2,y2):
    col='#aaaaaa'
    sv=0.05
    aprop=dict(shrink=sv,width=10,headwidth=30,headlength=15,
               connectionstyle='arc3',facecolor=col,edgecolor=col)
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0, arrowprops=aprop)


def drawfig(xmin,xmax,ymin,ymax,fp):
    # store text drawn
    ss=[]
    ss=ss+[u'グラフ用紙の作成']
    ss=ss+[u'出力したいテキストの入力']
    ss=ss+[u'ボックスの座標設定\n（頂部・底部座標設定）']
    ss=ss+[u'ボックス描画']
    ss=ss+[u'矢印描画']
    ss=ss+[u'テキスト描画']
    ss=ss+[u'画像出力']
    # definition of box coordinates in vertical direction
    yy1=np.array([1,3,5,8,10,12,14]) # top of box
    yy2=np.array([2,4,7,9,11,13,15]) # bottom of box
    yy0=0.5*(yy1+yy2) # center of box for text drawing
    # box drawing
    ddx=xmax-1
    for y1,y2 in zip(yy1,yy2):
        plt.fill([-ddx,ddx,ddx,-ddx],[y2,y2,y1,y1],facecolor='#eeeeee',edgecolor='#000000',lw=1)
    # arrow drawing
    x1=0; x2=0
    for i in range(len(yy2)-1):
        y2=yy2[i];y1=yy1[i+1]
        varrow(x1,y1,x2,y2)
    # text drawing
    xs=0
    for i,ys in enumerate(yy0):
        plt.text(xs,ys,ss[i],color='#000000',va='center',ha='center',fontproperties=fp)
 

def main():
    #fon='/Library/Fonts/Arial.ttf'
    #fon='/System/Library/Fonts/Helvetica.ttc'
    #fon='/Library/Fonts/Tahoma.ttf'
    #fon='/Library/Fonts/Times New Roman.ttf'
    fon='/Users/kk/Library/Fonts/ipaexg.ttf'
    fp=FontProperties(fname=fon,size=16)

    iflag=0 # 1: show grid, 0: no grid and save image
    xmin=-5; xmax=5; dx=1
    ymin=0; ymax=16; dy=1
    raspect=0.8 # definition of aspect ratio

    fsz=10
    iw=5
    ih=iw*(ymax-ymin)/(xmax-xmin)*raspect
    fig = plt.figure(figsize=(iw,ih),facecolor='w')
    plt.rcParams['font.size']=fsz
    
    plt.xlim([xmin,xmax])
    plt.ylim([ymax,ymin])
    if iflag==0:
        plt.axis('off')
    else:
        plt.xlabel('x_axis')
        plt.ylabel('y_axis')
        plt.xticks(np.arange(xmin,xmax+dx,dx))
        plt.yticks(np.arange(ymin,ymax+dy,dy))
        plt.grid(color='#999999',linestyle='solid')
    plt.gca().set_aspect(raspect,adjustable='box')

    drawfig(xmin,xmax,ymin,ymax,fp)
    
    plt.tight_layout()
    if iflag==0:
        fnameF='fig_gra.jpg'
        plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
    plt.show()


#==============
# Execution
#==============
if __name__ == '__main__': main()
```

以　上