Python
matplotlib

Python matplotlibでフローチャートのようなものを強引に描く(2)

はじめに

仕事でフローチャートのようなものを作る必要が生じた.
普段は英語版でいいのだが,今回は,英語版・日本語版双方を作る必要が生じたので,「matplotlibでフローチャートのようなものを描く」プログラムの見直しを行った.それほど頻繁に使うものでもないので,あとで使うときに,何をやっているか,すぐに思い出せるよう配慮した(つもり).

前の投稿はこれ.
Python, matplotlibでフローチャートのようなものを強引に描く

今回のプログラムの特徴は(といっても大したことはないが...)

  • 基本はグラフ用紙の上にボックス・テキスト・矢印を,目で見て修正しながら仕上げる(前と同じ)
  • 日本語も含め,フォントを選択できるようにした(変数:fp).
  • 作図領域の横幅は固定であるが,縦は縦横比(変数:Raspect)で調整するようにした.
  • チャート作成時は,グリッド・座標軸を表示し,完成したと思ったらグリッド・座標軸を消して画像をファイル出力する(前と同じ:変数:iflag).iflag==0 なら,plt.axis('off') で軸を非表示にして,グリッドも描かない.

作業環境は以下の通り.

  • MacBook Pro (Retina, 13-inch, Mid 2014)
  • macOS High Sierra
  • Python 3.6.4

ここで紹介する事例は以下の3点である.

  • グラフ用紙(チャートを書くためのグラフ用紙:内容は含まず)
  • 日本語を用いた簡単なチャート
  • 少し複雑な英語版チャート

グラフ用紙

このプログラムは,座標軸とグリッドを表示するだけであり,中身は含まれていない.
すなわち,座標軸とグリッド以外表示しない.
しかし,これをベースに中身を書き加えて行けるよう,フォント指定などの部分は含んでいる.

注意事項は以下の通り.

  • フォント指定のため,from matplotlib.font_manager import FontProperties をインポートしておく.
  • フォントはフルパスで指定する.フォントのフルパスは,ImageMagick がインストールされていれば convert -list font で確認するのが便利.
  • 座標軸・グリッドを表示したければ,iflag=1,非表示にしたければ iflag=0 とする.
  • 描画する座標の範囲は,xmin, xmax, ymin, ymax で指定する
  • y軸は,ax1.set_ylim([ymax,ymin]) により,下方向に数値が増加するよう指定.
  • 描画範囲の縦横比(アスペクトレシオ)は,Raspect で指定.ここでは Raspect=1 としている.
# Flowchart
from math import *                                   # 数学関数使用
import matplotlib.pyplot as plt                      # 基本 pyplot
from matplotlib.patches import Polygon,Rectangle     # Polygon, Rectangle使用
from matplotlib.ticker import *                      # MultipleLocator使用
from matplotlib.font_manager import FontProperties   # フォント指定のため

# フォントをフルパスで指定
#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) # 使用フォント指定


# iflag=0: without axis and grid, iflag=1: with axis and grid
iflag=1 # グリッド・座標軸表示有無の指定

# definition if the range of axis
xmin=-8
xmax=8
ymin=0
ymax=5
Raspect=1 # 縦横比指定
iw=abs(xmax-xmin)/2
ih=abs(ymin-ymax)/abs(xmax-xmin)*iw*Raspect

fig = plt.figure(figsize=(iw,ih),facecolor='w')
ax1 = fig.add_subplot(1,1,1, adjustable='box', aspect=Raspect)
ax1.set_xlim([xmin,xmax])
ax1.set_ylim([ymax,ymin])
if iflag==0: # グリッド・座標軸非表示
    plt.axis('off')
else:
    ax1.tick_params(labelsize=10)
    ax1.xaxis.set_major_locator(MultipleLocator(1))
    ax1.yaxis.set_major_locator(MultipleLocator(1))
    plt.grid(which='major',lw=0.3, color='#777777',linestyle='-')

fsz=20 # font size
plt.rcParams["font.size"] = fsz
plt.rcParams['font.family'] ='sans-serif'
#################################################
#     (something to draw)                       #
#################################################

#================================================
if iflag==0:
    fnameF='fig_gra1.png'
    plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
plt.show()

作例

日本語を用いた簡単なチャート

日本語を用いた簡単なチャートの事例を示す.

やっていることは以下の通り.

  • フォントは ipaexg.ttf (fon='/Users/kk/Library/Fonts/ipaexg.ttf'
  • グリッド・座標軸を表示(iflag=1
  • 描画範囲は,x 方向は -8〜+8,y 方向は 0〜25 まで.
  • 縦横比は Raspect=0.8 に指定.
  • フォントサイズは fsz=16 に指定.
  • リスト ss[] に描画したい文字列を格納
  • Rectangle で描画したボックスの中に文字列を描画していく.
  • 矢印は,少しカッコつけて自前で Polygon で定義
# Flowchart
from math import *
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon,Rectangle
from matplotlib.ticker import *
from matplotlib.font_manager import FontProperties

#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)

# iflag=0: without axis and grid, iflag=1: with axis and grid
iflag=1

# definition if the range of axis
xmin=-8
xmax=8
ymin=0
ymax=25
Raspect=0.8
iw=abs(xmax-xmin)/2
ih=abs(ymin-ymax)/abs(xmax-xmin)*iw*Raspect

fig = plt.figure(figsize=(iw,ih),facecolor='w')
ax1 = fig.add_subplot(1,1,1, adjustable='box', aspect=Raspect)
ax1.set_xlim([xmin,xmax])
ax1.set_ylim([ymax,ymin])
if iflag==0: 
    plt.axis('off')
else:
    ax1.tick_params(labelsize=10)
    ax1.xaxis.set_major_locator(MultipleLocator(1))
    ax1.yaxis.set_major_locator(MultipleLocator(1))
    plt.grid(which='major',lw=0.3, color='#777777',linestyle='-')

fsz=16 # font size
plt.rcParams["font.size"] = fsz
plt.rcParams['font.family'] ='sans-serif'
#################################################
#     (something to draw)                       #
#################################################
# store texts drawn
ss=[]
ss=ss+[u'グラフ用紙の作成']
ss=ss+[u'出力したいテキストの入力']
ss=ss+[u'ボックス頂部の座標設定']
ss=ss+[u'矢印頂部の座標設定\n(ボックス底部の座標)']
ss=ss+[u'矢印形状の定義']
ss=ss+[u'ボックスおよびテキスト描画']
ss=ss+[u'矢印描画']
ss=ss+[u'画像出力']

# define top of box
dyt=2 # height of text box
dya=0.75 # height of allow
yt=[]
for i in range(0,len(ss)):
    if i==0:
        _y=1
    else:
        _y=_y+dyt+dya
    yt=yt+[float(_y)]

# define top of allow
ya=[]
for i in range(0,len(ss)-1):
    if i==0:
        _y= 1+dyt
    else:
        _y=_y+dya+dyt
    ya=ya+[float(_y)]
# define arrow shape
# total length of arrow: dya
dxa=dya*2 # totalwidth of arrow 
x1=dxa/4; y1=0      #       7 + 1
x2=x1   ; y2=dya/2  #       +   +
x3=dxa/2; y3=y2     #       +   +
x4=0    ; y4=dya    #   5 + 6    2 + 3 
x5=-x3  ; y5=y3     #     +       +
x6=-x2  ; y6=y2     #       +   +
x7=-x1  ; y7=y1     #         4

# draw text and box
xi=-5.5 # left edge of box
dx=11   # width of box
xx=0    # center of text
for i in range(0,len(ss)):
    yi=yt[i]
    ax1.add_patch(Rectangle((xi,yi),dx,dyt,fc='#ffffff',ec='#000000'))
    yy=yi+dyt/2
    ax1.text(xx,yy,ss[i],fontsize=fsz,ha='center',va='center',fontproperties=fp)

# draw arrows
for i in range(0,len(ya)):
    yi=ya[i]
    yi1=y1+yi
    yi2=y2+yi
    yi3=y3+yi
    yi4=y4+yi
    yi5=y5+yi
    yi6=y6+yi
    yi7=y7+yi
    xy=([x1,yi1],[x1,yi1],[x2,yi2],[x3,yi3],[x4,yi4],[x5,yi5],[x6,yi6],[x7,yi7])
    ax1.add_patch(Polygon(xy,closed=True,fc='#eeeeee',ec='#000000'))


#================================================
if iflag==0:
    fnameF='fig_gra2.png'
    plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
plt.show()

作例

少し複雑な英語版チャート

少し複雑な英語版チャートの事例を示す.

やっていることは以下の通り.

  • フォントは tahoma.ttf (fon='/Library/Fonts/Tahoma.ttf'
  • グリッド・座標軸を表示(iflag=1
  • 描画範囲は,x 方向は -8〜+8,y 方向は 0〜40 まで.
  • 縦横比は Raspect=0.7 に指定.
  • フォントサイズは fsz=12 に指定.
  • リスト a[] に描画したい文字列を格納
  • リスト nln=[] は1アイテム(1つのかたまり)の中の行数を格納する
  • リスト nsp=[] は1アイテムを示すボックスの形を格納する.0 はボックス(長方形),1 はひし形.
  • Polygon でボックスおよび判断を示すひし形を描画し,その中に文字列を描画していく.
  • 濃い灰色のボックスにはメインアイテムの文字列を,薄い灰色のボックスにはサブアイテムの文字列を描画している.
  • ボックスとボックスを結ぶ線は plot で先に描画してしまう.
  • 矢印は,頭の部分だけ arrow によりあとから描画する.
# Flowchart
from math import *
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon,Rectangle
from matplotlib.ticker import *
from matplotlib.font_manager import FontProperties

#fon='/Library/Fonts/Arial.ttf'
#fon='/System/Library/Fonts/Helvetica.ttc'
fon='/Library/Fonts/Tahoma.ttf'
#fon='/Library/Fonts/Times New Roman.ttf'
fp=FontProperties(fname=fon)

# iflag=0: without axis and grid, iflag=1: with axis and grid
iflag=1

# definition if the range of axis
xmin=-8
xmax=8
ymin=0
ymax=40
Raspect=0.7
iw=abs(xmax-xmin)/2
ih=abs(ymin-ymax)/abs(xmax-xmin)*iw*Raspect

fig = plt.figure(figsize=(iw,ih),facecolor='w')
ax1 = fig.add_subplot(1,1,1, adjustable='box', aspect=Raspect)
ax1.set_xlim([xmin,xmax])
ax1.set_ylim([ymax,ymin])
if iflag==0: 
    plt.axis('off')
else:
    ax1.tick_params(labelsize=10)
    ax1.xaxis.set_major_locator(MultipleLocator(1))
    ax1.yaxis.set_major_locator(MultipleLocator(1))
    plt.grid(which='major',lw=0.3, color='#777777',linestyle='-')

fsz=12 # font size
plt.rcParams["font.size"] = fsz
plt.rcParams['font.family'] ='sans-serif'
#################################################
#     (something to draw)                       #
#################################################
# Store texts drawn
a=[] # text
nln=[] # number of lines in 1 item
nsp=[] # shape (0: rectangle, 1: diamond)
nln=nln+[int(1)]
nsp=nsp+[int(0)]
a=a+['Set target output $P=P_1,P_2,..,P_N$'] 
nln=nln+[int(4)]
nsp=nsp+[int(0)]
a=a+['Set design conditions'] 
a=a+['    $\eta$ : combined efficiency']
a=a+['    $v$ : waterway velocity']
a=a+['    $n$ : waterway roughness coefficient']
nln=nln+[int(1)]
nsp=nsp+[int(0)]
a=a+['Set initial FSL $FSL=FSL_0$']
nln=nln+[int(1)]
nsp=nsp+[int(0)]
a=a+['$FSL=FSL+\Delta H$']
nln=nln+[int(4)]
nsp=nsp+[int(0)]
a=a+['Set parameters']
a=a+['    $MOL$ considering $R_{pt}\leq 1.2$']
a=a+['    $NWL=FSL-(FSL-MOL)\:/\:3$']
a=a+['    $V_e$ : net storage,    $H$ : dam height']
nln=nln+[int(1)]
nsp=nsp+[int(0)]
a=a+['$P_i=P_1,P_2,..,P_N$']
nln=nln+[int(1)]
nsp=nsp+[int(0)]
a=a+['Set initial discharge $Q=Q_0$']
nln=nln+[int(1)]
nsp=nsp+[int(0)]
a=a+['$Q=Q+\Delta Q$']
nln=nln+[int(4)]
nsp=nsp+[int(0)]
a=a+['Output calculation'] 
a=a+['    (1) Re-calculate head loss ($h_L$)']
a=a+['    (2) Re-calculate output']
a=a+['$P_Q=9.8 \\times (NWL-h_L) \\times Q \\times \eta$']
nln=nln+[int(1)]
nsp=nsp+[int(1)]
a=a+['$P_i \leq P_Q$']
nln=nln+[int(2)]
nsp=nsp+[int(0)]
a=a+['Calculate generating duration $T$']
a=a+['$T=V_e \:/\: Q \:/\: 3600$ (hr)']
nln=nln+[int(2)]
nsp=nsp+[int(0)]
a=a+['Output results']
a=a+['$P_i \quad H \quad T \quad Q \quad h_L \quad \dots$'];

dh=1       # height of 1 line
dx=10      # width of box
x0=2       # location of center
xs=x0-dx/2 # starting coordinate of x of box
xe=x0+dx/2 # ending coordinate of x of box
i=-1       # initialization of line counter

# Draw text and box
for nl, ns in zip(nln,nsp):
    if ns==0: # rectangle
        # main item
        if i==-1: ye=0
        ys=ye+dh; ye=ys+dh
        xy=([xs,ys],[xe,ys],[xe,ye],[xs,ye])
        poly = Polygon(xy, facecolor='#dddddd', edgecolor='#000000',lw=1)
        ax1.add_patch(poly)
        i=i+1; ax1.text(x0,ys+dh*1/2,a[i],fontsize=fsz,ha='center',va='center',fontproperties=fp)
        if 2<=nl: # sub-items
            ys=ye; ye=ys+(nl-1)*dh
            xy=[(xs,ys),(xe,ys),(xe,ye),(xs,ye)]
            poly = Polygon(xy, facecolor='#eeeeee', edgecolor='#000000',lw=1)
            ax1.add_patch(poly)
            for j in range(0,nl-1):
                i=i+1
                if a[i][0]=='$':
                    ax1.text(x0,ys+dh*(2*j+1)/2,a[i],fontsize=fsz,ha='center',va='center',fontproperties=fp)
                else:
                    ax1.text(xs,ys+dh*(2*j+1)/2,a[i],fontsize=fsz,ha='left',va='center',fontproperties=fp)
    if ns==1: # diamond (main item)
        ys=ye+dh; ye=ys+2*dh
        xy=([x0,ys],[x0+dx/4,0.5*(ys+ye)],[x0,ye],[x0-dx/4,0.5*(ys+ye)])
        poly = Polygon(xy, facecolor='#dddddd', edgecolor='#000000',lw=1)
        ax1.add_patch(poly)
        i=i+1; ax1.text(x0,0.5*(ys+ye),a[i],fontsize=fsz,ha='center',va='center',fontproperties=fp)

# draw lines on center line
eln=[3,8,10,12,17,19,21,23,28,31,34,37] # y-coordinate of line end
for el in eln: 
    ax1.plot([x0,x0],[el-dh, el],color='#000000',lw=1)
# draw arrows on center line
hw=0.3; hl=0.4
for el in eln[0:len(eln)-1]:
    ax1.arrow(x0, el-hl,0, 0.01,fc='#000000',ec='#000000',head_width=hw,head_length=hl)

# draw lines
x1=x0-dx/4  ; y1=29
x2=x0-dx/2-1; y2=29
x3=x0-dx/2-1; y3=21.5
x4=x0-dx/2  ; y4=21.5
ax1.plot([x1,x2,x3,x4],[y1,y2,y3,y4],color='#000000',lw=1)
x1=x0-dx/2  ; y1=34.5
x2=x0-dx/2-2; y2=34.5
x3=x0-dx/2-2; y3=17.5
x4=x0-dx/2  ; y4=17.5
ax1.plot([x1,x2,x3,x4],[y1,y2,y3,y4],color='#000000',lw=1)
x1=x0       ; y1=37
x2=x0-dx/2-3; y2=37
x3=x0-dx/2-3; y3=10.5
x4=x0-dx/2  ; y4=10.5
ax1.plot([x1,x2,x3,x4],[y1,y2,y3,y4],color='#000000',lw=1)
# draw arrows
hw=0.3; hl=0.4
ax1.arrow(x0-dx/2-hl,21.5,0.01,0,fc='#000000',ec='#000000',head_width=hw,head_length=hl)
ax1.arrow(x0-dx/2-hl,17.5,0.01,0,fc='#000000',ec='#000000',head_width=hw,head_length=hl)
ax1.arrow(x0-dx/2-hl,10.5,0.01,0,fc='#000000',ec='#000000',head_width=hw,head_length=hl)
# draw text
ax1.text(x0-dx/4-0.1,29.5,'No',fontsize=fsz,ha='right',va='center',fontproperties=fp)
ax1.text(x0+0.5,30.5,'Yes',fontsize=fsz,ha='left',va='center',fontproperties=fp)


#================================================
if iflag==0:
    fnameF='fig_gra3.png'
    plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
plt.show()

作例

追記:日本語を用いた簡単なチャート(annotateで矢印)

  • 矢印をannotateで書いてみました.
  • ax1.xxx は使わず,全て plt.xxx で書いてみました.
# Flowchart
from math import *
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon,Rectangle
from matplotlib.ticker import *
from matplotlib.font_manager import FontProperties

#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)

# iflag=0: without axis and grid, iflag=1: with axis and grid
iflag=1

# definition if the range of axis
xmin=-8
xmax=8
ymin=0
ymax=25
Raspect=0.8
iw=abs(xmax-xmin)/2
ih=abs(ymin-ymax)/abs(xmax-xmin)*iw*Raspect

fig = plt.figure(figsize=(iw,ih),facecolor='w')
plt.gca().set_aspect(Raspect,adjustable='box')
plt.xlim([xmin,xmax])
plt.ylim([ymax,ymin])
if iflag==0: 
    plt.axis('off')
else:
    plt.gca().tick_params(labelsize=10)
    plt.gca().xaxis.set_major_locator(MultipleLocator(1))
    plt.gca().yaxis.set_major_locator(MultipleLocator(1))
    plt.grid(which='major',lw=0.3, color='#777777',linestyle='-')

fsz=16 # font size
plt.rcParams["font.size"] = fsz
plt.rcParams['font.family'] ='sans-serif'
#################################################
#     (something to draw)                       #
#################################################
# store texts drawn
ss=[]
ss=ss+[u'グラフ用紙の作成']
ss=ss+[u'出力したいテキストの入力']
ss=ss+[u'ボックス頂部の座標設定']
ss=ss+[u'矢印頂部の座標設定\n(ボックス底部の座標)']
ss=ss+[u'矢印形状の定義']
ss=ss+[u'ボックスおよびテキスト描画']
ss=ss+[u'矢印描画']
ss=ss+[u'画像出力']

# define top of box
dyt=2 # height of text box
dya=0.75 # height of allow
yt=[]
for i in range(0,len(ss)):
    if i==0:
        _y=1
    else:
        _y=_y+dyt+dya
    yt=yt+[float(_y)]

# define top of allow
ya=[]
for i in range(0,len(ss)-1):
    if i==0:
        _y= 1+dyt
    else:
        _y=_y+dya+dyt
    ya=ya+[float(_y)]

# draw text and box
xi=-5.5 # left edge of box
dx=11   # width of box
xx=0    # center of text
for i in range(0,len(ss)):
    yi=yt[i]
    plt.fill([xi,xi,xi+dx,xi+dx],[yi,yi+dyt,yi+dyt,yi],fc='#ffffff',ec='#000000')
    yy=yi+dyt/2
    plt.text(xx,yy,ss[i],fontsize=fsz,ha='center',va='center',fontproperties=fp)

# draw arrow
sv=0
for y2 in ya:
    y1=y2+dya
    x1=0; x2=x1
    plt.annotate('',
        xy=(x1,y1), xycoords='data',
        xytext=(x2,y2), textcoords='data', fontsize=0,
        arrowprops=dict(shrink=sv,width=20,headwidth=40,headlength=10,
                        connectionstyle='arc3',facecolor='#eeeeee',edgecolor='#000000'))

#================================================
if iflag==0:
    fnameF='fig_gra2.png'
    plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
plt.show()

以 上