1. 開発の背景
一昨年の大学祭の模擬店ではフライドポテトを販売していましたが、会計がかなり大変でした。
そのときは、紙に正の字を書いて数量を計算するという方法を採用していました(去年の模擬店も同様です)。
今年は私自身が模擬店の担当になったわけではなかったのですが、少しでも負担を減らして楽にできたら良いなと思い、開発することに決めました。
2. 実装までの流れ
初めはDjangoでやろうと思ったのですが、URLの設定や画面遷移などに失敗してしまいました。修正しようと思ったのですが、中々うまく行きませんでした。留学生の友達(インドネシア人の方です)がFlaskの方が簡単という話をしていたのを思い出し、Flaskでの実装に切り替えました。
コーディングはChatGPT(4o)とGitHub Copilotを使いながら爆速で開発を進めます。
3. 実装した画面の説明
3.1. 「売上計算」画面
メインの画面です。お客様の購入数量に応じて合計金額を計算します。

☝ 売上計算画面
3.2. 「売上履歴」画面
「売上計算」画面で計算した売上の履歴が保存されています。「CSVでダウンロード」を押すと、売上履歴をダウンロードして後で分析することができます。

☝ 売上履歴画面
3.3. 「商品一覧」画面
「売上計算」画面で表示される商品名と金額を登録・編集します。

☝ 売上計算画面
3.4. 「売上ダッシュボード」画面
合計数量や時間ごとの売り上げなどを、数値とグラフで表示します。

☝ 売上ダッシュボード画面
4. 動画
こちらが完成したアプリの動画です! Render.comにデプロイして動かしてみました。
5. ソースコード
今回のアプリはFlaskで作ったので、ここでは画面遷移やデータベースの処理をするPythonコードと、各画面のhtmlファイルを掲載を一部掲載します。全ソースコードはGitHubに載せました。
⇩⇩⇩ ソースコード(app.py) ⇩⇩⇩
from flask import Flask, render_template, request, redirect, url_for, flash, send_file, send_from_directory
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired
import os
import csv
import io
from datetime import datetime, timedelta
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get('SECRET_KEY', 'default-secret-key')
# SQLite データベースの設定
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'sales.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# Flask-login の設定
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
# ユーザーモデル
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), nullable=False, unique=True)
password_hash = db.Column(db.String(120), nullable=False)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
def init_admin_user():
# 環境変数から取得
admin_username = os.environ.get('ADMIN_USERNAME')
admin_password = os.environ.get('ADMIN_PASSWORD')
if not admin_username or not admin_password:
raise ValueError("環境変数 'ADMIN_USERNAME' および 'ADMIN_PASSWORD' を設定してください。")
# すでに管理者ユーザーが存在しない場合にのみ追加
if not User.query.filter_by(username=admin_username).first():
admin = User(username=admin_username)
admin.set_password(admin_password) # ハッシュ化してパスワードを設定
db.session.add(admin)
db.session.commit()
print(f"Admin user '{admin_username}' added.")
# 商品情報のモデル
class Item(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
price = db.Column(db.Integer, nullable=False)
# 売り上げ情報のモデル
class Sale(db.Model):
id = db.Column(db.Integer, primary_key=True)
item_name = db.Column(db.String(80), nullable=False)
price = db.Column(db.Integer, nullable=False)
quantity = db.Column(db.Integer, nullable=False)
total = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, default=lambda: datetime.utcnow() + timedelta(hours=9)) # JST に変更
def __repr__(self):
return f'<Sale {self.item_name}, {self.total}>'
# 商品データをデータベースに初期化する関数
def init_items():
item_data = [
{"name": "白玉団子(黒蜜きなこ)", "price": 200},
{"name": "白玉団子(みたらし)", "price": 200},
{"name": "白玉団子(五郎島金時&あんこ)", "price": 200},
]
for item in item_data:
if not Item.query.filter_by(name=item["name"]).first():
new_item = Item(name=item["name"], price=item["price"])
db.session.add(new_item)
db.session.commit()
# データベースの初期化
@app.before_request
def setup_database():
db.create_all()
# init_items()
init_admin_user()
# ログインフォーム
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
# ログインルート
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# ユーザーが存在するか確認
user = User.query.filter_by(username=username).first()
# デバッグメッセージを追加
if user:
print(f"User '{username}' found.")
else:
print(f"User '{username}' not found.")
# パスワードが一致するか確認
if user and user.check_password(password):
print("Password matched!") # パスワードが一致した場合のデバッグメッセージ
login_user(user)
return redirect(url_for('index'))
else:
print("Password did not match or user not found.") # 一致しなかった場合のデバッグメッセージ
flash('Invalid username or password', 'danger') # ログイン失敗メッセージ
return render_template('login.html')
# ログアウトルート
@app.route('/logout')
# @login_required
def logout():
logout_user()
flash('ログアウトしました', 'info')
return redirect(url_for('login'))
# メインページのルート
@app.route('/', methods=['GET', 'POST'])
# @login_required
def index():
items = Item.query.all()
total = 0
sales_records = []
message = None # 初期化
# 商品が登録されていない場合のメッセージを設定
if not items:
message = "「商品一覧」ページから商品を登録してください"
if request.method == 'POST' and items:
quantities = request.form.getlist('quantities[]')
for item in items:
quantity = request.form.get(f'quantities[{item.id}]', 0)
quantity = int(quantity)
if quantity > 0:
total += item.price * quantity
new_sale = Sale(item_name=item.name, price=item.price, quantity=quantity, total=item.price * quantity)
db.session.add(new_sale)
sales_records.append({'item_name': item.name, 'price': item.price, 'quantity': quantity, 'total': item.price * quantity})
db.session.commit()
return render_template('index.html', items=items, total=total, sales_records=sales_records, message=message)
# 売上履歴のルート
@app.route('/sales', methods=['GET', 'POST'])
# @login_required
def sales():
items = Item.query.all()
total_sales = 0
sales_records = []
if request.method == 'POST':
quantities = request.form.getlist('quantities[]')
for item in items:
quantity = request.form.get(f'quantities[{item.id}]', 0)
quantity = int(quantity)
if quantity > 0:
total = item.price * quantity
total_sales += total
new_sale = Sale(item_name=item.name, price=item.price, quantity=quantity, total=total)
db.session.add(new_sale)
sales_records.append(new_sale)
db.session.commit()
# 作成日時で昇順にソートして売上データを取得
sales_records = Sale.query.order_by(Sale.created_at.desc()).all()
total_sales = sum(record.total for record in sales_records)
return render_template('sales.html', items=items, total_sales=total_sales, sales_records=sales_records)
# 売上履歴の削除ルート
@app.route('/delete/<int:id>', methods=['POST'])
# @login_required
def delete(id):
sale_to_delete = Sale.query.get_or_404(id)
try:
db.session.delete(sale_to_delete)
db.session.commit()
return redirect('/sales')
except Exception as e:
return f'削除に失敗しました: {str(e)}'
# 商品情報編集のルート
@app.route('/edit_item/<int:id>', methods=['GET', 'POST'])
# @login_required
def edit_item(id):
item_to_edit = Item.query.get_or_404(id)
if request.method == 'POST':
item_to_edit.name = request.form['item_name']
item_to_edit.price = int(request.form['price'])
db.session.commit()
return redirect(url_for('index'))
return render_template('edit_item.html', item=item_to_edit)
# 商品一覧のルート
@app.route('/items', methods=['GET'])
# @login_required
def items():
all_items = Item.query.all()
return render_template('items.html', items=all_items)
# 商品追加のルート
@app.route('/add_item', methods=['GET', 'POST'])
# @login_required
def add_item():
if request.method == 'POST':
item_name = request.form['name']
item_price = int(request.form['price'])
new_item = Item(name=item_name, price=item_price)
db.session.add(new_item)
db.session.commit()
return redirect(url_for('items'))
return render_template('add_item.html')
# 商品削除のルート
@app.route('/delete_item/<int:id>', methods=['POST'])
# @login_required
def delete_item(id):
item_to_delete = Item.query.get_or_404(id)
try:
db.session.delete(item_to_delete)
db.session.commit()
return redirect(url_for('items'))
except Exception as e:
return f'削除に失敗しました: {str(e)}'
# 売上ダッシュボードの√
@app.route('/dashboard')
def dashboard():
# 合計売上個数、売上額、直近1時間の売上額
total_quantity = db.session.query(db.func.sum(Sale.quantity)).scalar() or 0
total_sales = db.session.query(db.func.sum(Sale.total)).scalar() or 0
# JST に直近1時間の時間を取得
one_hour_ago = datetime.utcnow() + timedelta(hours=9) - timedelta(hours=1)
sales_last_hour = db.session.query(db.func.sum(Sale.total)).filter(Sale.created_at >= one_hour_ago).scalar() or 0
# 時間ごとの売上個数の取得
sales = Sale.query.all()
hourly_sales = {}
product_sales = {}
for sale in sales:
# JST に変換して時間ごとの売上
hour = (sale.created_at).strftime('%m/%d %H:00')
if hour not in hourly_sales:
hourly_sales[hour] = 0
hourly_sales[hour] += sale.quantity
# 商品ごとの売上(円グラフ用)
if sale.item_name not in product_sales:
product_sales[sale.item_name] = 0
product_sales[sale.item_name] += sale.quantity
# 時間順に並べ替え
sorted_hours = sorted(hourly_sales.keys())
quantities = [hourly_sales.get(hour, 0) for hour in sorted_hours]
# 累計売上個数の計算
cumulative_quantities = []
cumulative_sum = 0
for q in quantities:
cumulative_sum += q
cumulative_quantities.append(cumulative_sum)
# 商品名と売上数量(円グラフ用)
product_names = list(product_sales.keys())
product_quantities = list(product_sales.values())
return render_template(
'dashboard.html',
total_quantity=total_quantity,
total_sales=total_sales,
sales_last_hour=sales_last_hour,
sales=sales,
sorted_hours=sorted_hours,
quantities=quantities,
cumulative_quantities=cumulative_quantities,
product_names=product_names,
product_quantities=product_quantities
)
# 売上データのCSVダウンロード用のルート
@app.route('/download_csv')
def download_csv():
# 売上データの取得(sales_records など)
sales_records = Sale.query.all() # 既存の関数または処理で売上データを取得
# CSVをメモリ上で生成
output = io.StringIO()
writer = csv.writer(output)
# CSVのヘッダー行
writer.writerow(['商品名', '単価', '数量', '合計金額', '日時'])
# CSVのデータ行
for record in sales_records:
writer.writerow([
record.item_name,
record.price,
record.quantity,
record.total,
record.created_at.strftime('%Y-%m-%d %H:%M:%S')
])
# ポインタを先頭に戻す
output.seek(0)
# CSVをダウンロード用にレスポンス
return send_file(
io.BytesIO(output.getvalue().encode('utf-8-sig')),
mimetype='text/csv',
as_attachment=True,
download_name='sales_records.csv'
)
# クローラー対策用ファイルへのアクセス用ルート
@app.route('/robots.txt')
def robots():
return send_from_directory(app.static_folder, 'robots.txt')
if __name__ == '__main__':
app.run(debug=False)
⇩⇩⇩ ベーステンプレート(base.html) ⇩⇩⇩
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}模擬店会計App{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand ms-3" href="/#">模擬店会計App.</a> <!-- 左側に余白を追加 -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto me-3"> <!-- 右側に余白を追加 -->
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index') }}">売上計算</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('sales') }}">売上履歴</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('items') }}">商品一覧</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard') }}">売上ダッシュボード</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% block content %}
<!-- 子テンプレートでコンテンツを定義 -->
{% endblock %}
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.7/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
</body>
</html>
⇩⇩⇩ 売上計算画面(index.html) ⇩⇩⇩
{% extends "base.html" %}
{% block title %}模擬店会計App{% endblock %}
{% block content %}
<h1>売上計算</h1>
<!-- {% if message %}
<div class="alert alert-warning">{{ message }}</div>
{% endif %} -->
{% if items %}
<form method="POST" action="{{ url_for('index') }}" class="mb-3" style="margin-bottom: 20px;"> <!-- 下にマージンを追加 -->
<div class="form-row">
{% for item in items %}
<div class="form-group col-md-3 mb-2"> <!-- 列の幅を指定し、行間にマージンを追加 -->
<label for="item_{{ item.id }}">{{ item.name }} (¥{{ item.price }})</label>
<div class="input-group">
<input type="number" name="quantities[{{ item.id }}]" min="0" value="0" class="form-control text-center" id="item_{{ item.id }}" onchange="validateQuantity({{ item.id }})" style="max-width: 80px;"> <!-- 幅を指定 -->
<div class="input-group-append" style="margin-left: 5px;"> <!-- 左にマージンを追加 -->
<button type="button" class="btn btn-secondary" onclick="changeQuantity({{ item.id }}, -1)">ー</button>
<button type="button" class="btn btn-secondary" onclick="changeQuantity({{ item.id }}, 1)">+</button>
</div>
</div>
</div>
{% endfor %}
</div>
<button type="submit" class="btn btn-primary" style="margin-top: 20px;">合計金額を計算</button>
</form>
{% else %}
<div class="alert alert-info">
「商品一覧」ページから商品を登録してください >>
<a href="{{ url_for('items') }}" class="alert-link">商品一覧ページへ</a>
</div>
{% endif %}
<br> <!-- フォームの間に隙間を作るための改行 -->
{% if total > 0 %}
<h4>購入された商品</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>商品名</th>
<th>単価</th>
<th>数量</th>
<th>合計金額</th>
</tr>
</thead>
<tbody>
{% for record in sales_records %}
<tr>
<td>{{ record.item_name }}</td>
<td>¥{{ record.price }}</td>
<td>{{ record.quantity }}</td>
<td>¥{{ record.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2 class="mt-4">合計金額: ¥{{ total }}</h2>
{% endif %}
<script>
function changeQuantity(itemId, delta) {
const input = document.getElementById(`item_${itemId}`);
let currentQuantity = parseInt(input.value) || 0;
currentQuantity += delta;
// 最小値は0に制限
if (currentQuantity < 0) {
currentQuantity = 0;
}
input.value = currentQuantity;
}
function validateQuantity(itemId) {
const input = document.getElementById(`item_${itemId}`);
let currentQuantity = parseInt(input.value) || 0;
// 負の値が入力された場合は0にリセット
if (currentQuantity < 0) {
input.value = 0;
}
}
</script>
{% endblock %}
⇩⇩⇩ 売上履歴画面(sales.html) ⇩⇩⇩
{% extends "base.html" %}
{% block title %}模擬店会計App{% endblock %}
{% block content %}
<h1 class="mt-4">売上履歴</h1>
<a href="{{ url_for('index') }}" class="btn btn-secondary my-2">メインページに戻る</a>
<a href="{{ url_for('download_csv') }}" class="btn btn-primary my-2">CSVでダウンロード</a> <!-- 追加されたボタン -->
{% if sales_records %}
<div class="table-responsive">
<table class="table table-bordered table-striped mt-4">
<thead class="thead-light">
<tr>
<th>商品名</th>
<th>単価</th>
<th>数量</th>
<th>合計金額</th>
<th>日時</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for record in sales_records %}
<tr>
<td>{{ record.item_name }}</td>
<td>¥{{ record.price }}</td>
<td>{{ record.quantity }}</td>
<td>¥{{ record.total }}</td>
<td>{{ record.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>
<form action="{{ url_for('delete', id=record.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('本当に削除しますか?')">削除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-4"><strong>総売上:¥{{ total_sales }}</strong></p>
{% else %}
<p class="mt-4">売上履歴はありません</p>
{% endif %}
{% endblock %}
⇩⇩⇩ 商品一覧画面(items.html) ⇩⇩⇩
{% extends "base.html" %}
{% block title %}模擬店会計App.{% endblock %}
{% block content %}
<h1 class="mt-4">商品一覧</h1>
{% if items %}
<table class="table table-striped mt-3">
<thead>
<tr>
<th>ID</th>
<th>商品名</th>
<th>価格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>¥{{ item.price }}</td>
<td>
<a href="{{ url_for('edit_item', id=item.id) }}" class="btn btn-warning btn-sm">編集</a>
<form action="{{ url_for('delete_item', id=item.id) }}" method="POST" style="display:inline;">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('本当に削除しますか?')">削除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="alert alert-info">商品が登録されていません</p>
{% endif %}
<div class="mt-4">
<a href="{{ url_for('index') }}" class="btn btn-secondary">メインページへ戻る</a>
<a href="{{ url_for('add_item') }}" class="btn btn-primary">商品を追加</a>
</div>
{% endblock %}
⇩⇩⇩ 商品編集画面(edit_item.html) ⇩⇩⇩
{% extends "base.html" %}
{% block title %}模擬店会計App.{% endblock %}
{% block content %}
<h1 class="mt-4">商品を編集</h1>
<form method="POST" class="mt-4">
<div class="mb-3">
<label for="item_name" class="form-label">商品名:</label>
<input type="text" id="item_name" name="item_name" class="form-control" value="{{ item.name }}" required>
</div>
<div class="mb-3">
<label for="price" class="form-label">価格:</label>
<input type="number" id="price" name="price" class="form-control" value="{{ item.price }}" required min="0">
</div>
<button type="submit" class="btn btn-primary">更新</button>
<a href="{{ url_for('index') }}" class="btn btn-secondary">戻る</a>
</form>
{% endblock %}
⇩⇩⇩ 商品追加画面(add_item.html) ⇩⇩⇩
{% extends "base.html" %}
{% block title %}模擬店会計App.{% endblock %}
{% block content %}
<h1 class="mt-4">商品を追加</h1>
<form method="POST">
<div class="form-group mt-3">
<label for="name">商品名:</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="form-group mt-3">
<label for="price">価格:</label>
<input type="number" class="form-control" id="price" name="price" required min="0">
</div>
<button type="submit" class="btn btn-primary mt-3">追加</button>
</form>
<a href="{{ url_for('items') }}" class="btn btn-secondary mt-3">商品一覧に戻る</a>
{% endblock %}
⇩⇩⇩ 売上ダッシュボード画面(dashboard.html) ⇩⇩⇩
{% extends "base.html" %}
{% block title %}模擬店会計App.{% endblock %}
{% block content %}
<h1 class="mt-4">売上ダッシュボード</h1>
<a href="{{ url_for('index') }}" class="btn btn-secondary my-2">メインページに戻る</a>
<!-- カードに累計数量と合計金額を表示 -->
<div class="row">
<div class="col-md-3">
<div class="card text-white bg-primary mt-2">
<div class="card-header">累計</div>
<div class="card-body">
<h4 class="card-title">{{ total_quantity }} 個</h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success mt-2">
<div class="card-header">合計金額</div>
<div class="card-body">
<h4 class="card-title">¥{{ total_sales }}</h4>
</div>
</div>
</div>
</div>
<!-- グラフで 売上情報を可視化 -->
<div class="row">
<div class="col-lg-6">
<div class="chart-container">
<h2 class="mt-4">売上数量(累計)</h2>
<canvas id="cumulativeSalesChart"></canvas>
</div>
</div>
<div class="col-lg-6">
<h2 class="mt-4">1時間ごとの売上数量</h2>
<div class="chart-container">
<canvas id="hourlySalesChart"></canvas>
</div>
</div>
</div>
<h2 class="mt-2">商品の人気</h2>
<div class="row">
<div class="col-lg-6">
<div class="chart-container">
<canvas id="productSalesChart"></canvas>
</div>
</div>
</div>
<!-- Chart.js を読み込み -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<script>
// 累計売上数量を表示する折れ線グラフ
var cumulativeCtx = document.getElementById('cumulativeSalesChart').getContext('2d');
var cumulativeSalesChart = new Chart(cumulativeCtx, {
type: 'line',
data: {
labels: {{ sorted_hours|tojson }},
datasets: [{
label: '数量',
data: {{ cumulative_quantities|tojson }},
borderColor: 'rgba(153, 102, 255, 1)',
borderWidth: 2,
fill: false
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
}
},
scales: {
x: {
display: true,
title: {
display: true,
text: '時間'
}
},
y: {
display: true,
title: {
display: true,
text: '数量'
}
}
}
}
});
// 単位時間ごとの売上数量を表示する棒グラフ
var hourlyCtx = document.getElementById('hourlySalesChart').getContext('2d');
var hourlySalesChart = new Chart(hourlyCtx, {
type: 'bar',
data: {
labels: {{ sorted_hours|tojson }},
datasets: [{
label: '数量',
data: {{ quantities|tojson }},
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
}
},
scales: {
x: {
display: true,
title: {
display: true,
text: '時間'
}
},
y: {
display: true,
title: {
display: true,
text: '数量'
}
}
}
}
});
// 円グラフを描画 (売れた商品の割合)
var productCtx = document.getElementById('productSalesChart').getContext('2d');
var productSalesChart = new Chart(productCtx, {
type: 'pie',
data: {
labels: {{ product_names|tojson|default('[]') }},
datasets: [{
label: '数量',
data: {{ product_quantities|tojson|default('[]') }},
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
datalabels: {
formatter: (value, ctx) => {
let sum = ctx.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
let percentage = (value * 100 / sum).toFixed(1) + "%";
return percentage;
},
color: '#777',
font: {
weight: 'bold'
}
}
}
},
plugins: [ChartDataLabels] // Chart.jsのプラグインとしてdatalabelsを有効化
});
</script>
{% endblock %}