1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chart.jsで損益分岐点計算アプリを開発してみた

1
Last updated at Posted at 2024-10-23

Chart.jsで損益分岐点計算アプリを開発してみたのアイキャッチ

この記事でわかること

  • Chart.jsで損益分岐点計算アプリを開発してみた で扱う実装の全体像
  • ローカルで動かす前に確認したい前提
  • 公開・再利用するときに気をつけたい点

はじめに

10年以上前にPHPにて作成したミニアプリをJSで作り直しました。

デモ

画面:
FireShot Capture 014 - 損益分岐点計算 - bep.wealthy-design.com.png

コード

bep.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>損益分岐点計算</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
    <style>
        /* スタイリングをモダンなデザインに調整 */
        body {
            font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 2rem;
            background: #f8f9fa;
            color: #333;
        }

        /* 見出しのデザイン */
        .header {
            text-align: center;
            margin-bottom: 3rem;
        }

        h1 {
            color: #2c3e50;
            font-size: 2.5rem;
            font-weight: 700;
            margin-bottom: 1rem;
        }

        /* 計算部分のデザイン */
        .calculator-container {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 2rem;
            background: white;
            padding: 2rem;
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        /* 入力セクションのデザイン */
        .input-section {
            padding: 1.5rem;
            background: #f8f9fa;
            border-radius: 8px;
        }

        /* 入力フィールドのデザイン */
        .input-group {
            margin-bottom: 1.5rem;
            display: flex;
            align-items: center;
        }

        .input-group label {
            flex: 1;
            margin-bottom: 0;
            color: #2c3e50;
            font-weight: 600;
        }

        .input-group input {
            width: 50%;
            padding: 0.75rem;
            border: 2px solid #e9ecef;
            border-radius: 6px;
            font-size: 1rem;
            transition: border-color 0.3s ease;
        }

        .input-group input:focus {
            outline: none;
            border-color: #4a90e2;
        }

        /* 結果表示セクションのデザイン */
        .results {
            background: #fff;
            padding: 1.5rem;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
        }

        .result-item {
            display: flex;
            justify-content: space-between;
            margin-bottom: 1rem;
            padding: 0.75rem;
            background: #f8f9fa;
            border-radius: 6px;
        }

        .result-label {
            color: #2c3e50;
            font-weight: 600;
        }

        .result-value {
            color: #4a90e2;
            font-weight: 700;
        }

        /* グラフセクションのデザイン */
        .chart-container {
            margin-top: 2rem;
            padding: 1.5rem;
            background: white;
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            height: 800px;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>損益分岐点計算</h1>
    </div>

    <div class="calculator-container">
        <div class="input-section">
            <div class="input-group">
                <label for="sales">売上高 (円)</label>
                <input type="text" id="sales" oninput="validateNumber(this)" placeholder="0">
            </div>
            <div class="input-group">
                <label for="fixedCost">固定費 (円)</label>
                <input type="text" id="fixedCost" oninput="validateNumber(this)" placeholder="0">
            </div>
            <div class="input-group">
                <label for="variableCost">変動費 (円)</label>
                <input type="text" id="variableCost" oninput="validateNumber(this)" placeholder="0">
            </div>
        </div>

        <div class="results">
            <!-- 各種結果を表示するエリア -->
            <div class="result-item">
                <span class="result-label">変動費率</span>
                <span class="result-value" id="variableCostRate">-</span>
            </div>
            <div class="result-item">
                <span class="result-label">損益分岐点</span>
                <span class="result-value" id="breakEvenPoint">-</span>
            </div>
            <div class="result-item">
                <span class="result-label">損益分岐点比率</span>
                <span class="result-value" id="breakEvenPointRatio">-</span>
            </div>
            <div class="result-item">
                <span class="result-label">限界利益</span>
                <span class="result-value" id="marginalProfit">-</span>
            </div>
            <div class="result-item">
                <span class="result-label">限界利益率</span>
                <span class="result-value" id="marginalProfitRatio">-</span>
            </div>
        </div>
    </div>

    <div class="chart-container">
        <canvas id="chart"></canvas>
    </div>

    <script>
        let chart = null;

        // 数値をフォーマットする関数
        function formatNumber(num) {
            return new Intl.NumberFormat('ja-JP').format(Math.round(num));
        }

        // パーセント表示用のフォーマット関数
        function formatPercent(num) {
            return new Intl.NumberFormat('ja-JP', {
                style: 'percent',
                minimumFractionDigits: 1,
                maximumFractionDigits: 1
            }).format(num);
        }

        // 入力が数字かどうかをチェックし、数字以外を受け付けない関数
        function validateNumber(input) {
            // 数字のみを受け付ける正規表現
            input.value = input.value.replace(/[^0-9]/g, '');
        }

        // 計算処理と画面更新を行う関数
        function calculateAndUpdate() {
            const sales = Number(document.getElementById('sales').value) || 0;
            const fixedCost = Number(document.getElementById('fixedCost').value) || 0;
            const variableCost = Number(document.getElementById('variableCost').value) || 0;

            // 売上が0の場合は処理を終了
            if (sales === 0) return;

            // 変動費率や損益分岐点の計算
            const variableCostRate = variableCost / sales;
            const marginalProfitRate = 1 - variableCostRate;
            const breakEvenPoint = fixedCost / marginalProfitRate;
            const breakEvenPointRatio = breakEvenPoint / sales;
            const marginalProfit = sales - variableCost;
            const marginalProfitRatio = marginalProfit / sales;

            // 結果の表示を更新
            document.getElementById('variableCostRate').textContent = formatPercent(variableCostRate);
            document.getElementById('breakEvenPoint').textContent = formatNumber(breakEvenPoint) + '円';
            document.getElementById('breakEvenPointRatio').textContent = formatPercent(breakEvenPointRatio);
            document.getElementById('marginalProfit').textContent = formatNumber(marginalProfit) + '円';
            document.getElementById('marginalProfitRatio').textContent = formatPercent(marginalProfitRatio);

            // グラフの更新
            updateChart(sales, fixedCost, variableCostRate);
        }

        // グラフの更新処理
        function updateChart(sales, fixedCost, variableCostRate) {
            const ctx = document.getElementById('chart').getContext('2d');

            const points = 10;  // グラフの分割点数
            const step = sales / points;
            const labels = [];
            const fixedCostData = [];
            const variableCostData = [];
            const totalCostData = [];
            const salesLineData = [];

            for (let i = 0; i <= points; i++) {
                const x = step * i;
                labels.push(formatNumber(x));
                fixedCostData.push(fixedCost);
                variableCostData.push(x * variableCostRate);
                totalCostData.push(fixedCost + (x * variableCostRate));
                salesLineData.push(x);
            }

            // 既存のチャートがある場合は破棄
            if (chart) {
                chart.destroy();
            }

            // 新しいチャートを作成
            chart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: [
                        {
                            label: '売上高',
                            data: salesLineData,
                            borderColor: '#4a90e2',
                            backgroundColor: 'rgba(74, 144, 226, 0.1)',
                            borderWidth: 2,
                            fill: false,
                            tension: 0.1
                        },
                        {
                            label: '総費用',
                            data: totalCostData,
                            borderColor: '#e74c3c',
                            backgroundColor: 'rgba(231, 76, 60, 0.1)',
                            borderWidth: 2,
                            fill: false,
                            tension: 0.1
                        },
                        {
                            label: '固定費',
                            data: fixedCostData,
                            borderColor: '#2ecc71',
                            backgroundColor: 'rgba(46, 204, 113, 0.1)',
                            borderWidth: 2,
                            fill: false,
                            tension: 0.1
                        }
                    ]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    interaction: {
                        intersect: false,
                        mode: 'index'
                    },
                    plugins: {
                        legend: {
                            position: 'top',
                            labels: {
                                usePointStyle: true,
                                padding: 20
                            }
                        },
                        title: {
                            display: true,
                            text: '損益分岐点グラフ',
                            padding: {
                                top: 10,
                                bottom: 30
                            },
                            font: {
                                size: 16
                            }
                        },
                        tooltip: {
                            position: 'nearest'
                        }
                    },
                    scales: {
                        x: {
                            title: {
                                display: true,
                                text: '売上高 (円)',
                                padding: {
                                    top: 10
                                }
                            },
                            grid: {
                                display: true,
                                drawBorder: true,
                                drawOnChartArea: true
                            }
                        },
                        y: {
                            title: {
                                display: true,
                                text: '金額 (円)',
                                padding: {
                                    bottom: 10
                                }
                            },
                            grid: {
                                display: true,
                                drawBorder: true,
                                drawOnChartArea: true
                            }
                        }
                    }
                }
            });
        }

        // イベントリスナーの設定
        document.getElementById('sales').addEventListener('input', calculateAndUpdate);
        document.getElementById('fixedCost').addEventListener('input', calculateAndUpdate);
        document.getElementById('variableCost').addEventListener('input', calculateAndUpdate);
    </script>
</body>
</html>

実務で使うときの確認ポイント

  • ライブラリ、ランタイム、ブラウザのバージョン差分を確認する。
  • 公開する場合は、入力値の扱い、依存ライブラリ、ライセンス、公開URLを確認する。
  • サンプルをそのまま本番に使わず、エラー処理、ログ、セキュリティを追加する。

おわりに

ここまで読んでいただきありがとうございました。
Wealthy Designでは、AWS・Webシステム開発・AI活用など、現場で使える技術を大切にしています。

会社の取り組みは、会社サイトにまとめています。

AWS・Web開発・AI活用のご相談や、採用・カジュアル面談については、Wealthy Design公式LINEでもお問い合わせいただけます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?