🕒 Python + matplotlibで24時間円グラフを描く
🎯 目的
1日の行動スケジュール(睡眠・通勤・業務など)を 10分単位の精度で24時間円グラフ化 するPythonスクリプトを、matplotlib を用いて作成します。
資料で必要だったのでExcelやスプレッドシートを利用して作成しようとしましたが
地味に難しくて面倒だったのでPythonで作ることにしました。
日本語ラベル対応・カスタムフォント・目盛表示も含まれています。
🧰 開発環境
- OS:Windows 10 / 11
- エディタ:Visual Studio Code
- 言語:Python 3.11(3.8以上ならOK)
📦 必要なライブラリのインストール
以下のコマンドで必要なパッケージをインストールします。
pip install matplotlib numpy※ Python 3.8以上で動作確認済み。
📁 プロジェクト構成(例)
Python
└──daily_schedule/
├── schedule_chart.py
└── output/
└── daily_schedule_chart_final_v3.pngPython
└──daily_schedule/
├── schedule_chart.py
└── output/
└── daily_schedule_chart_final_v3.png🖋 プログラム
まずは作成物をどうぞ。
僕と同じ初心者の方はとりあえずそのままコピーして、
そのまま貼り付けてschedule_chart.pyのファイルを作成してください。
Excelとかスプレッドシートみたいに使えるように引数やInputを用意し、
それに対応できるよう改修していましたが
ごちゃつくので今回は固定値で設定したもので進めます。
各々でやってみてください。
参考
- matplotlibドキュメント
チートシートなどもいいかも
https://matplotlib.org/stable/api/pyplot_summary.html#module-matplotlib.pyplot - コラム, 小澤昌樹氏のデータ分析コラム 第14回「Matplotlibの使い方」
色んなグラフの出力形式が簡潔に記載されていてわかりやすかったです。
https://www.pythonic-exam.com/archives/8956
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import numpy as np
import os
from datetime import datetime, timedelta
# 日本語フォントの設定 (Windows向け)
# ご自身の環境に合うフォントパスを指定してください。
# 例: 'C:/Windows/Fonts/YuGothM.ttc' または 'C:/Windows/Fonts/msgothic.ttc'
font_path = 'C:/Windows/Fonts/msgothic.ttc' # または YuGothM.ttc, YuGothR.ttc など
try:
if not os.path.exists(font_path):
raise FileNotFoundError(f"指定されたフォントファイルが見つかりません: {font_path}\n"
"ご自身のWindowsにインストールされている日本語フォントのパスに変更してください。")
fm.fontManager.addfont(font_path)
# フォントファイルを指定したら、そのフォントのファミリー名を指定します
# 例: 'msgothic.ttc' なら 'MS Gothic', 'yugothic.ttc' なら 'Yu Gothic'
plt.rcParams['font.family'] = 'MS Gothic' # 使用するフォントファミリー名を設定
except FileNotFoundError as e:
print(e)
print("フォント設定なしで続行します。日本語が豆腐になる可能性があります。")
plt.rcParams['font.family'] = 'sans-serif'
except Exception as e:
print(f"フォント設定中にエラーが発生しました: {e}")
plt.rcParams['font.family'] = 'sans-serif'
# データの定義
# 時間配分をtimedeltaで計算
# 時刻は24時間表記で指定
# TODO:一旦、固定値のままやるけど、ここは後で引数(input)や外出しにする
schedule = [
{"label": "睡眠", "start": "00:00", "end": "07:00"},
{"label": "朝食・準備", "start": "07:00", "end": "07:50"},
{"label": "通勤", "start": "07:50", "end": "08:40"},
{"label": "業務", "start": "08:40", "end": "11:30"}, # 業務1
{"label": "昼休", "start": "11:30", "end": "12:30"},
{"label": "業務", "start": "12:30", "end": "17:40"}, # 業務2
{"label": "通勤", "start": "17:40", "end": "19:00"},
{"label": "夕食・家事", "start": "19:00", "end": "20:30"},
{"label": "自由", "start": "20:30", "end": "23:00"},
{"label": "無", "start": "23:00", "end": "24:00"} # 24:00 は翌日の00:00として扱う
]
sizes = []
labels = []
# TODO:値のチェックを入れる
for item in schedule:
start_time_str = item["start"]
end_time_str = item["end"]
# "24:00" を "00:00" として扱い、日をまたぐフラグを設定
is_next_day = False
if end_time_str == "24:00":
end_time_str_parsed = "00:00"
is_next_day = True
else:
end_time_str_parsed = end_time_str
# 日付は仮で設定し、時間のみを考慮
start_dt = datetime.strptime(start_time_str, "%H:%M")
end_dt = datetime.strptime(end_time_str_parsed, "%H:%M")
# 終了時刻が開始時刻より前の場合、または "24:00" で翌日扱いの場合、翌日として扱う
if end_dt < start_dt or is_next_day:
time_diff_hours = (end_dt + timedelta(days=1) - start_dt).total_seconds() / 3600
else:
time_diff_hours = (end_dt - start_dt).total_seconds() / 3600
sizes.append(time_diff_hours)
labels.append(item["label"])
# 各セグメントの色 (元のschedule_chart.jpgの配色に近づける)
# TODO:一旦、固定値のままやるけど、ここは後で引数(input)や外出しにする
colors = [
'#3B609E', # 睡眠 (濃い青)
'#6A9E4D', # 朝食・準備 (緑)
'#4D99B1', # 通勤1 (水色)
'#DC5D5C', # 業務1 (赤)
'#F2DC73', # 昼休 (黄)
'#E77A3D', # 業務2 (オレンジ)
'#4D99B1', # 通勤2 (水色)
'#7D4D9E', # 夕食・家事 (紫)
'#EBD748', # 自由 (明るい黄)
'#DD4D4D' # 無 (赤)
]
# グラフの開始角度 (0時が上、時計回り)
# Matplotlibのpieチャートはデフォルトで90度(12時の位置)から反時計回りに描画される
# 0時が上になるように90度から時計回りに描画
start_angle = 90
# 円グラフの作成
fig, ax = plt.subplots(figsize=(10, 10)) # グラフのサイズを設定
# 円グラフ (ドーナツ型ではないのでwidthを削除)
wedges, _ = ax.pie(sizes, colors=colors, startangle=start_angle,
counterclock=False) # 時計回り
# ラベルの配置調整
# 各ウェッジの中心角度を計算し、ラベルを配置
# TODO:一旦、固定値のままやるけど、ここは後で引数(input)や外出しにする⇒これに対応させる
for i, (wedge, label) in enumerate(zip(wedges, labels)):
angle = (wedge.theta2 + wedge.theta1) / 2 # ウェッジの中心角度
# ラベルのオフセットを調整する係数
# 短いセグメントや端にあるラベルは少し外側に、長いセグメントのラベルは内側に
# 元の画像に合わせて調整
text_radius_factor = 0.75 # デフォルトの距離 (円の中心からの相対距離)
text_color = 'black' # デフォルトの文字色
if label == '睡眠':
text_radius_factor = 0.65 # 中央よりやや内側
text_color = 'white'
elif label == '朝食・準備':
text_radius_factor = 0.75
text_color = 'white'
elif label == '通勤':
text_radius_factor = 0.75
# 最初の通勤(index 2)は水色背景なので白文字、2番目の通勤(index 6)も水色なので白文字
text_color = 'white' if i in [2, 6] else 'black'
elif label == '業務':
text_radius_factor = 0.65
text_color = 'white'
elif label == '昼休':
text_radius_factor = 0.75
text_color = 'black'
elif label == '夕食・家事':
text_radius_factor = 0.65
text_color = 'white'
elif label == '自由':
text_radius_factor = 0.75
text_color = 'black'
elif label == '無':
text_radius_factor = 0.75
text_color = 'white' # 無も赤背景なので白文字
else:
text_radius_factor = 0.75
text_color = 'black'
# 座標計算
# 円グラフの中心からの距離を調整(radiusはwedgeの半径)
x = wedge.r * text_radius_factor * np.cos(np.deg2rad(angle))
y = wedge.r * text_radius_factor * np.sin(np.deg2rad(angle))
ax.text(x, y, label, ha='center', va='center', fontsize=12, color=text_color,
bbox=dict(boxstyle="round,pad=0.3", fc="none", ec="none", alpha=0.0)) # 背景ボックスを透明に
# 各時間の目盛りの追加 (円グラフの外周に配置)
circle_radius = 1.0 # 円グラフの半径 (Matplotlibのpieはデフォルトで1.0)
for hour in range(24):
# 0時が上 (90度) から時計回り
angle_rad = np.deg2rad(90 - (hour / 24) * 360)
# 目盛り線の外側
x_tick_outer = circle_radius * np.cos(angle_rad)
y_tick_outer = circle_radius * np.sin(angle_rad)
# 目盛り線の内側 (中心からの距離)
x_tick_inner = (circle_radius - 0.03) * np.cos(angle_rad) # 少しだけ内側から
y_tick_inner = (circle_radius - 0.03) * np.sin(angle_rad)
# 短い目盛り線を引く
ax.plot([x_tick_inner, x_tick_outer], [y_tick_inner, y_tick_outer], color='gray', linestyle='-', linewidth=0.7)
# 時間の数字 (目盛り線の少し外側)
text_radius_hour = circle_radius + 0.1 # 数字を配置する半径
text_x = text_radius_hour * np.cos(angle_rad)
text_y = text_radius_hour * np.sin(angle_rad)
ax.text(text_x, text_y, f'{hour}時', ha='center', va='center', fontsize=10)
ax.set_aspect('equal') # 円を正円に保つ
ax.set_title('1日の時間配分', fontsize=16, pad=20) # グラフのタイトルと余白
# グラフをPNG画像として保存
output_filename = 'daily_schedule_chart_final_v3.png'
fig.savefig(output_filename, bbox_inches='tight', dpi=300)
print(f"グラフが '{output_filename}' として保存されました。")
plt.show() # グラフを表示import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import numpy as np
import os
from datetime import datetime, timedelta
# 日本語フォントの設定 (Windows向け)
# ご自身の環境に合うフォントパスを指定してください。
# 例: 'C:/Windows/Fonts/YuGothM.ttc' または 'C:/Windows/Fonts/msgothic.ttc'
font_path = 'C:/Windows/Fonts/msgothic.ttc' # または YuGothM.ttc, YuGothR.ttc など
try:
if not os.path.exists(font_path):
raise FileNotFoundError(f"指定されたフォントファイルが見つかりません: {font_path}\n"
"ご自身のWindowsにインストールされている日本語フォントのパスに変更してください。")
fm.fontManager.addfont(font_path)
# フォントファイルを指定したら、そのフォントのファミリー名を指定します
# 例: 'msgothic.ttc' なら 'MS Gothic', 'yugothic.ttc' なら 'Yu Gothic'
plt.rcParams['font.family'] = 'MS Gothic' # 使用するフォントファミリー名を設定
except FileNotFoundError as e:
print(e)
print("フォント設定なしで続行します。日本語が豆腐になる可能性があります。")
plt.rcParams['font.family'] = 'sans-serif'
except Exception as e:
print(f"フォント設定中にエラーが発生しました: {e}")
plt.rcParams['font.family'] = 'sans-serif'
# データの定義
# 時間配分をtimedeltaで計算
# 時刻は24時間表記で指定
# TODO:一旦、固定値のままやるけど、ここは後で引数(input)や外出しにする
schedule = [
{"label": "睡眠", "start": "00:00", "end": "07:00"},
{"label": "朝食・準備", "start": "07:00", "end": "07:50"},
{"label": "通勤", "start": "07:50", "end": "08:40"},
{"label": "業務", "start": "08:40", "end": "11:30"}, # 業務1
{"label": "昼休", "start": "11:30", "end": "12:30"},
{"label": "業務", "start": "12:30", "end": "17:40"}, # 業務2
{"label": "通勤", "start": "17:40", "end": "19:00"},
{"label": "夕食・家事", "start": "19:00", "end": "20:30"},
{"label": "自由", "start": "20:30", "end": "23:00"},
{"label": "無", "start": "23:00", "end": "24:00"} # 24:00 は翌日の00:00として扱う
]
sizes = []
labels = []
# TODO:値のチェックを入れる
for item in schedule:
start_time_str = item["start"]
end_time_str = item["end"]
# "24:00" を "00:00" として扱い、日をまたぐフラグを設定
is_next_day = False
if end_time_str == "24:00":
end_time_str_parsed = "00:00"
is_next_day = True
else:
end_time_str_parsed = end_time_str
# 日付は仮で設定し、時間のみを考慮
start_dt = datetime.strptime(start_time_str, "%H:%M")
end_dt = datetime.strptime(end_time_str_parsed, "%H:%M")
# 終了時刻が開始時刻より前の場合、または "24:00" で翌日扱いの場合、翌日として扱う
if end_dt < start_dt or is_next_day:
time_diff_hours = (end_dt + timedelta(days=1) - start_dt).total_seconds() / 3600
else:
time_diff_hours = (end_dt - start_dt).total_seconds() / 3600
sizes.append(time_diff_hours)
labels.append(item["label"])
# 各セグメントの色 (元のschedule_chart.jpgの配色に近づける)
# TODO:一旦、固定値のままやるけど、ここは後で引数(input)や外出しにする
colors = [
'#3B609E', # 睡眠 (濃い青)
'#6A9E4D', # 朝食・準備 (緑)
'#4D99B1', # 通勤1 (水色)
'#DC5D5C', # 業務1 (赤)
'#F2DC73', # 昼休 (黄)
'#E77A3D', # 業務2 (オレンジ)
'#4D99B1', # 通勤2 (水色)
'#7D4D9E', # 夕食・家事 (紫)
'#EBD748', # 自由 (明るい黄)
'#DD4D4D' # 無 (赤)
]
# グラフの開始角度 (0時が上、時計回り)
# Matplotlibのpieチャートはデフォルトで90度(12時の位置)から反時計回りに描画される
# 0時が上になるように90度から時計回りに描画
start_angle = 90
# 円グラフの作成
fig, ax = plt.subplots(figsize=(10, 10)) # グラフのサイズを設定
# 円グラフ (ドーナツ型ではないのでwidthを削除)
wedges, _ = ax.pie(sizes, colors=colors, startangle=start_angle,
counterclock=False) # 時計回り
# ラベルの配置調整
# 各ウェッジの中心角度を計算し、ラベルを配置
# TODO:一旦、固定値のままやるけど、ここは後で引数(input)や外出しにする⇒これに対応させる
for i, (wedge, label) in enumerate(zip(wedges, labels)):
angle = (wedge.theta2 + wedge.theta1) / 2 # ウェッジの中心角度
# ラベルのオフセットを調整する係数
# 短いセグメントや端にあるラベルは少し外側に、長いセグメントのラベルは内側に
# 元の画像に合わせて調整
text_radius_factor = 0.75 # デフォルトの距離 (円の中心からの相対距離)
text_color = 'black' # デフォルトの文字色
if label == '睡眠':
text_radius_factor = 0.65 # 中央よりやや内側
text_color = 'white'
elif label == '朝食・準備':
text_radius_factor = 0.75
text_color = 'white'
elif label == '通勤':
text_radius_factor = 0.75
# 最初の通勤(index 2)は水色背景なので白文字、2番目の通勤(index 6)も水色なので白文字
text_color = 'white' if i in [2, 6] else 'black'
elif label == '業務':
text_radius_factor = 0.65
text_color = 'white'
elif label == '昼休':
text_radius_factor = 0.75
text_color = 'black'
elif label == '夕食・家事':
text_radius_factor = 0.65
text_color = 'white'
elif label == '自由':
text_radius_factor = 0.75
text_color = 'black'
elif label == '無':
text_radius_factor = 0.75
text_color = 'white' # 無も赤背景なので白文字
else:
text_radius_factor = 0.75
text_color = 'black'
# 座標計算
# 円グラフの中心からの距離を調整(radiusはwedgeの半径)
x = wedge.r * text_radius_factor * np.cos(np.deg2rad(angle))
y = wedge.r * text_radius_factor * np.sin(np.deg2rad(angle))
ax.text(x, y, label, ha='center', va='center', fontsize=12, color=text_color,
bbox=dict(boxstyle="round,pad=0.3", fc="none", ec="none", alpha=0.0)) # 背景ボックスを透明に
# 各時間の目盛りの追加 (円グラフの外周に配置)
circle_radius = 1.0 # 円グラフの半径 (Matplotlibのpieはデフォルトで1.0)
for hour in range(24):
# 0時が上 (90度) から時計回り
angle_rad = np.deg2rad(90 - (hour / 24) * 360)
# 目盛り線の外側
x_tick_outer = circle_radius * np.cos(angle_rad)
y_tick_outer = circle_radius * np.sin(angle_rad)
# 目盛り線の内側 (中心からの距離)
x_tick_inner = (circle_radius - 0.03) * np.cos(angle_rad) # 少しだけ内側から
y_tick_inner = (circle_radius - 0.03) * np.sin(angle_rad)
# 短い目盛り線を引く
ax.plot([x_tick_inner, x_tick_outer], [y_tick_inner, y_tick_outer], color='gray', linestyle='-', linewidth=0.7)
# 時間の数字 (目盛り線の少し外側)
text_radius_hour = circle_radius + 0.1 # 数字を配置する半径
text_x = text_radius_hour * np.cos(angle_rad)
text_y = text_radius_hour * np.sin(angle_rad)
ax.text(text_x, text_y, f'{hour}時', ha='center', va='center', fontsize=10)
ax.set_aspect('equal') # 円を正円に保つ
ax.set_title('1日の時間配分', fontsize=16, pad=20) # グラフのタイトルと余白
# グラフをPNG画像として保存
output_filename = 'daily_schedule_chart_final_v3.png'
fig.savefig(output_filename, bbox_inches='tight', dpi=300)
print(f"グラフが '{output_filename}' として保存されました。")
plt.show() # グラフを表示- 生成されるグラフ
🖋 スクリプト内容の説明
🔠 1. フォントの設定
font_path = 'C:/Windows/Fonts/msgothic.ttc'matplotlibは日本語フォントを自動認識しないため、明示的に Windows のmsgothic.ttcを指定します。- 他に
'YuGothM.ttc'や'Meiryo.ttc'も使用可。
🗓 2. スケジュールの定義
schedule = [
{"label": "睡眠", "start": "00:00", "end": "07:00"},
...
]schedule = [
{"label": "睡眠", "start": "00:00", "end": "07:00"},
...
]- 各活動の開始・終了時刻を24時間形式で定義。
"24:00"はdatetime.strptime非対応のため、"00:00"+ 翌日扱いに調整。
3. 時間差の計算
if end_dt < start_dt or is_next_day:
time_diff_hours = (end_dt + timedelta(days=1) - start_dt).total_seconds() / 3600if end_dt < start_dt or is_next_day:
time_diff_hours = (end_dt + timedelta(days=1) - start_dt).total_seconds() / 3600timedeltaで時間差を計算。例えば08:50 - 07:00→ 1.83時間として処理。
🎨 4. 円グラフの描画
start_angle = 90
fig, ax = plt.subplots(figsize=(10, 10))
ax.pie(sizes, ...)start_angle = 90
fig, ax = plt.subplots(figsize=(10, 10))
ax.pie(sizes, ...)counterclock=False→ 時計回りに設定。startangle=90→ 0時(深夜)が真上になるよう調整。
🏷 5. 日本語ラベルとラベル配置
ax.text(x, y, label, ha='center', ...)- 各セクションの中心角度からラベルを配置。
- 色に応じて文字色も変更(白背景には黒文字など)。
⏱ 6. 時間目盛の追加
for hour in range(24):
...
ax.text(text_x, text_y, f'{hour}時', ...)for hour in range(24):
...
ax.text(text_x, text_y, f'{hour}時', ...)- グラフの外周に0時〜23時の目盛りとラベルを追加。
- 時計盤のように見えるよう半径・角度で調整。
VSCodeのターミナルからの実行方法(初心者向け)
Pythonスクリプトを .py ファイルとして保存した後、VSCodeのターミナルを使って実行する手順は以下のとおりです。
初心者向けなのでご存じの方はスキップしてください。
1. スクリプトファイルを作成
- 例:
daily_schedule.pyという名前で保存します。
作成済みの方はpathの確認でOK(パスは自分で作成したものとあっていればOKです)
例:D:\Python\daily_schedule\daily_schedule.py2. VSCodeでファイルのあるフォルダを開く
- VSCode を起動
- メニューから「ファイル > フォルダーを開く」で、スクリプトがあるフォルダ(例:
C:\Users\yourname\Documents\Python)を選択
3. ターミナルを開く
- メニューから「ターミナル > 新しいターミナル」を選択(または
Ctrl + Shift + @)
D:
cd Python\Python\daily_schedule\D:
cd Python\Python\daily_schedule\5. スクリプトを実行
ここのpythonはフォルダ名とは関係ありません。
pythonのプログラムを実行しますよということなのでフォルダ名に関わらずこのままでよいです。
python daily_schedule.py※ python でエラーが出る場合は、代わりに python3 を使ってください:
python3 daily_schedule.py✅ 実行後の表示例
もしかしたらそのままウィンドウが表示されるだけかもしれません。
表示されたウィンドウを終了したらそのままプログラムが終了します。
ターミナル側もプログラムの終了を検知すると操作可能になるので焦らずウィンドウを閉じましょう。
グラフが 'daily_schedule_chart_final_v3.png' として保存されました。🖼 出力ファイルの確認
daily_schedule.pyと同じフォルダにdaily_schedule_chart_final_v3.pngが保存されています。- ファイルをダブルクリックすれば、画像としてスケジュールが確認できます。
🛠 よくあるトラブル対処
✅ テスト確認ポイント
テスト1:フォントが豆腐になっていないか
- フォントパスが正しいことを確認
- エラーが出る場合は
C:/Windows/Fonts/YuGothR.ttcなどに変更
※豆腐⇒文字が□みたいなフォント無し表示みたいになること
下記失敗例
この時はドーナツ型でやろうとしてたのですが円に切り替えたのでそこは無視でお願いします🙇
ちなみに生成されるファイル名も違います。すみません。
テスト2:ラベルの位置確認
自由時間や業務の位置が適切か、グラフ全体のバランスを目視で確認
テスト3:PNG出力の確認
output_filename = 'daily_schedule_chart_final_v3.png'fig.savefig()によりPNG画像として保存されるdpi=300なので印刷にも耐える解像度
📸 実行結果イメージ
※ 実際の画像は output/ フォルダに保存されます。
グラフが 'daily_schedule_chart_final_v3.png' として保存されました。🧩 応用例
scheduleなどの項目を外出しにする(設定ファイル等に書く)・引数やInputを用意しそれに対応できるよう改修する- 10分刻みでさらに細かくするには
scheduleに項目追加 - 土日版や在宅勤務版などのパターンも追加
matplotlibのdonut chartにするにはwedgeprops={'width':0.3}を追加
(自分はドーナツチャートだと上手く作れませんでした。)
📎 補足:よく使う日本語フォントパス一覧(Windows)
📌 おわりに
このスクリプトをベースに、自分専用のライフログ円グラフや、社員の1日モデルをビジュアルで示す資料なども作成できます。
自分はPythonはあまり詳しくないので色々実験してみようと思います。皆さんもぜひ学習の合間にどうぞ。