Pythonで学ぶ 家計簿アプリ開発(第5回)~ 家計簿アプリらしくなってきた集計機能 ~

GitHubでつくって学ぶ

はじめに

前回は、支出データの保存先を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)

前回までのindexaddと同じ形で、get_db()から接続を取得し、SQL文を実行して結果をfetchall()で受け取っている。今回は2種類の集計結果(monthlyby_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>タグに指定したwidthheightをそのまま使いたいため、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

コメント

タイトルとURLをコピーしました