はじめに
10年以上前にPHPにて作成したミニアプリをJSで作り直しました。
コード
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>