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で損益分岐点計算アプリを開発してみた

Last updated at Posted at 2024-10-23

はじめに

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>

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?