はじめに
前回は、支出データの保存先をPythonのリストからSQLiteに切り替え、再起動してもデータが消えないようにした。今回は、これまで蓄積してきたデータを月別・カテゴリ別に集計し、グラフとして可視化する。家計簿アプリらしい機能に、いよいよ着手する回である。
なお、月をまたいだ集計を確認しやすくするため、今回から初期データを5件(8月分3件・9月分2件)に増やしている。
今回やること
- SQLの
GROUP BYで、月別・カテゴリ別の合計を集計する - 集計結果を表示する新しいページ(
/summary)を作る - Chart.jsを使って、集計結果を棒グラフ・円グラフで表示する
SQLでの集計
SQLには、データをグループ化して合計や件数を求めるGROUP BYという機能がある。
SELECT category, SUM(amount) AS total
FROM expenses
GROUP BY category
ORDER BY total DESC;
これは、「カテゴリごとにamount(金額)を合計し、合計金額が多い順に並べる」というSQL文である。前回学んだsqlite3コマンドで、実際にこの文を打って結果を確認してみるのもよい。
月別の集計には、日付から「年月」部分だけを取り出すstrftime('%Y-%m', date)という関数を使う。
SELECT strftime('%Y-%m', date) AS month, SUM(amount) AS total
FROM expenses
GROUP BY month
ORDER BY month;
コード解説
app.py(集計部分)
@app.route("/summary")
def summary():
db = get_db()
monthly = db.execute(
"SELECT strftime('%Y-%m', date) AS month, SUM(amount) AS total "
"FROM expenses GROUP BY month ORDER BY month"
).fetchall()
by_category = db.execute(
"SELECT category, SUM(amount) AS total "
"FROM expenses GROUP BY category ORDER BY total DESC"
).fetchall()
db.close()
return render_template("summary.html", monthly=monthly, by_category=by_category)
前回までのindexやaddと同じ形で、get_db()から接続を取得し、SQL文を実行して結果をfetchall()で受け取っている。今回は2種類の集計結果(monthlyとby_category)を、同時にテンプレートへ渡している点が新しい。
templates/summary.html(抜粋)
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="monthlyChart" width="600" height="300"></canvas>
<canvas id="categoryChart" width="400" height="400"></canvas>
<script>
const monthlyLabels = {{ monthly | map(attribute='month') | list | tojson }};
const monthlyTotals = {{ monthly | map(attribute='total') | list | tojson }};
const categoryLabels = {{ by_category | map(attribute='category') | list | tojson }};
const categoryTotals = {{ by_category | map(attribute='total') | list | tojson }};
new Chart(document.getElementById("monthlyChart"), {
type: "bar",
data: {
labels: monthlyLabels,
datasets: [{
label: "月別合計(円)",
data: monthlyTotals
}]
},
options: {
responsive: false
}
});
new Chart(document.getElementById("categoryChart"), {
type: "pie",
data: {
labels: categoryLabels,
datasets: [{
data: categoryTotals
}]
},
options: {
responsive: false
}
});
</script>
※ Chart.jsは初期状態ではresponsive: trueとなっており、画面サイズに合わせて自動でグラフの大きさを調整する。ただし<canvas>を囲む要素の高さを固定していない場合、意図せずグラフが縦に伸び続けてしまうことがある。今回は<canvas>タグに指定したwidth・heightをそのまま使いたいため、responsive: falseを指定している。
いくつか、これまでに出てこなかった要素を説明する。
- Chart.js:グラフを描画するためのJavaScriptライブラリ。
<script>タグでCDN(インターネット上に公開されているファイル)を読み込むだけで使えるので、npmのようなパッケージ管理ツールは不要である <canvas>タグ:グラフの描画先になる領域。Chart.jsは、この<canvas>要素を指定してグラフを描く{{ monthly | map(attribute='month') | list | tojson }}:Jinja2のフィルタを3つ連結している。map(attribute='month')で各行からmonthの値だけを取り出し、listでリスト化し、tojsonでJavaScriptが読める形式(JSON)に変換している。これにより、Python側で集計した結果を、そのままJavaScript側の変数として埋め込める
動作確認
flask --app app run --debugで起動し、トップページの「集計を見る」リンクから/summaryにアクセスする。月別の棒グラフと、カテゴリ別の円グラフが表示されていれば成功である。
一覧ページで支出を追加・削除したあと、再度「集計を見る」を開くと、グラフにもその内容が反映されていることを確認しておくとよい。


今回のまとめ
- SQLの
GROUP BYを使うと、データベース側で集計処理をまとめて行える strftime('%Y-%m', date)で、日付から年月部分だけを取り出せる- Chart.jsは、CDNの
<script>タグを読み込むだけで使える、手軽なグラフ描画ライブラリである tojsonフィルタを使うと、Python側のデータをJavaScript側にそのまま渡せる
次回予告
次回はいよいよ最終回。これまでCodespaces上だけで動かしてきたこの家計簿アプリを、Renderを使ってインターネット上に公開する。
ソースコード(筆者のリポジトリ)
GitHub:pc-labo-python-learning/05_summary



コメント