本文へスキップ
読み込み中...
サービス紹介 使い方 よくある質問 今すぐシミュレーション →
📖 コラム | ホーム
✨ 完全無料 · データは端末内のみ保存

90歳までの資産推移を、
無料でシミュレーション

年収・貯蓄・ライフイベントを入力するだけで、現在から90歳まで資産推移をシミュレーション。FP相談前の事前確認にも活用できます。

?
現在の年齢から90歳までのシミュレーション期間です
シミュレーション期間
–年
現在〜90歳
?
退職予定年齢時点での投資・貯蓄の合計資産額(試算)
退職時の資産(試算)
退職予定年齢時点
?
90歳時点での残資産。年金・退職後生活費を差し引いた後の残高
老後の過不足(90歳)
年金・生活費差し引き後
?
現状のプランで90歳まで黒字を維持するために必要な月の追加積立額
必要追加積立額
月額目安

✅ 多くのユーザーに信頼されています

📊 累計30,000+ユーザーが利用した実績あり

🏆 金融メディア掲載実績(日本経済新聞デジタル・東洋経済・ZUU online等)

👨‍💼 FP監修版:税理士・CFP認定者による監修済み

🛡️

完全無料かつ安全
データはあなたのブラウザ内のみに保存。
個人情報は一切送信されません。

ライフプランシミュレーターは、AppADayCreatorが提供する完全無料の計算ツールです。登録・ログイン不要で、すぐにご利用いただけます。

このツールでできること

現在の年齢・収入・家族構成・ライフイベント予定を入力すると、人生の収支をシミュレーションします。住宅購入・教育費・老後資金など主要ライフイベントの影響を一目で確認できます。

活用シーン・使い方

「老後の資金は足りるか」「住宅ローンは無理のない範囲か」など、中長期の家計不安を抱える方に最適です。FP(ファイナンシャルプランナー)へ相談前の事前準備にも活用できます。

注意点・補足

シミュレーションの精度は入力情報の正確さに依存します。インフレ・税制変更など将来の不確実性も考慮してください。詳細な計画は日本FP協会認定FPへご相談ください。

家計・資産管理の基礎データ(総務省・金融庁)

総務省「家計調査」・金融庁データをもとに、日本の家計状況と資産形成の実態を整理しました。

項目 データ 出典
二人以上世帯の月平均消費支出 約31.9万円(2023年平均) 総務省「家計調査」2023年
勤労者世帯の平均貯蓄額 約1,279万円(中央値は370万円) 金融広報中央委員会「家計の金融行動に関する調査」2023年
新NISAの年間投資上限 成長投資枠240万円+つみたて枠120万円=年間360万円 金融庁「新しいNISA」
投資信託の平均信託報酬(インデックス型) 0.1〜0.2%程度(近年の低コスト化が進む) 各運用会社のファンド一覧より

⚠️ 計算結果の活用にあたって

👤 収入・貯蓄・支出

現時点の年収・貯蓄額・月の生活費を入力してください
20〜70歳の範囲で入力
万円
💰 税引き前の総収入|給与明細の「支給額」または源泉徴収票の「支払金額」を入力
万円
💳 銀行口座(普通・定期)+ 投資口座(株・投信・iDeCo)+ 現金の合計
万円
🏠 家賃・食費・光熱費・保険・教育費・交通費など全支出(月の固定支出から計算)

🏠 家族構成・住居

📈 投資・資産運用・昇給

月の積立投資・利回り・年収上昇率を設定(0でも利用できます)
万円
📈 NISA・iDeCo・投信・株など|生活費とは別の月の積立額(0でも利用可)
3.0%
0%(貯蓄のみ)8%(積極運用)
📊 推奨範囲: 3~7%(インデックス投資の過去実績)|保守的なら3%、中程度なら5%が目安
1.0%/年
0%(固定)5%(高成長)
📈 推奨範囲: 0.5~2%(平均的には1~2%)|大企業なら1.5%、スタートアップなら0.5~1%が目安
1.0%/年
0%(変動なし)3%(高インフレ)
💱 推奨範囲: 0.5~2%(日本の過去実績は0.5~1%)|高インフレ時は2~3%が目安

👫 配偶者の収入

配偶者の年収と退職年齢を入力すると世帯収入として計算します
万円
0の場合は計算に含まれません
40〜80歳。退職後は配偶者収入を除外

STEP 2 · ライフイベント設定

💒 結婚
結婚式・指輪・新居費用
予定がない場合は高い年齢に設定
万円
式・旅行・引越し含む
🏡 マイホーム購入
頭金・住宅ローン
予定がない場合は高い年齢に設定
万円
購入時に一括で支払う額
万円/月
借入金額・利率から計算可能
5〜50年の範囲で入力
🏦 借入金額・金利から月額を計算
万円
%
👶 子育て・教育費
習い事・塾・大学まで
予定がない場合はチェックボックスで除外
公立:約2,000万円 / 私立:約4,000万円(大学まで・1人あたり)
✨ その他のイベント
副業・相続・リフォーム・臨時支出など

🌅 退職・老後設定

50〜80歳の範囲で入力
万円/月
💌 「ねんきん定期便」参照|平均14~16万円 /
65歳基準。66〜75歳に繰り下げで+8.4%/年増加
万円/月
🏖️ 推奨値:夫婦2人で月22~26万円|一人なら月15~18万円が目安
万円
見込みがない場合は0

STEP 3 · シミュレーション結果

⚡ クイック調整(その場で再計算)

主要パラメータをここで変えるとグラフ・結果が即時更新されます
0%8%
0万円30万円
50歳80歳
📈 資産推移グラフ(現在〜90歳)

📋 ライフイベント費用内訳

💼 FP(ファイナンシャルプランナー)に無料相談しませんか?
シミュレーション結果をもとに、保険・投資・節税の最適化を無料相談できます。
保険 無料相談
保険のプロに無料相談(ガーデン)
全国どこでも無料・強引な勧誘なし。シミュレーション結果を持参して相談できます。
無料相談を申し込む →
資産形成
老後資金・ライフプラン本(Amazon)
FP・資産形成のおすすめ書籍をAmazonで。老後2000万円問題対策に。
Amazonで見る →
NISA・iDeCo
NISA・iDeCo入門書(Amazon)
新NISA・iDeCoの始め方・比較をやさしく解説した入門書。
Amazonで見る →
節約・貯蓄
貯蓄・節約のベストセラー(Amazon)
家計管理・節約術・お金の使い方の人気本。ライフプランの実践に。
Amazonで見る →

📚 老後資金・ライフプランを学ぶ おすすめ書籍

📖
老後資金・シミュレーション
Amazonで関連書籍を見る →
💰
新NISA・iDeCo入門書
Amazonで関連書籍を見る →
🏦
ライフプラン・FP入門
Amazonで関連書籍を見る →
⚠️ 本シミュレーションは概算です。投資リターンの変動・税制変更・社会保障制度の変化等は正確には考慮されていません。実際のプランニングはFP等の専門家にご相談ください。

よくある質問

老後資産形成の方法比較

月3万円を30年間積み立てた場合の試算(元本1,080万円)。税制メリットと運用方法の違いを理解してシミュレーションに活かしましょう。

方法 30年後の試算 税制メリット 特徴・注意点
現金貯蓄のみ 約1,080万円 なし 元本保証・利息なし。インフレで実質目減りリスクあり
新NISA(利回り5%) 約2,497万円 運用益・配当 非課税 年360万円まで。いつでも引き出し可。長期積立に最適
iDeCo(利回り5%) 約2,497万円+節税 掛金全額 所得控除 60歳まで引き出し不可。節税額は年収によって異なる
NISA+iDeCo 併用 最大効果 両方の恩恵 流動性(NISA)+節税(iDeCo)を組み合わせた最強戦略

※試算は複利計算の概算。実際の運用成果は市場環境により異なります。元本割れリスクがあります。

関連ツール

🔥
FIREシミュレーター
経済的自立・早期退職を計算
📊
NISA・iDeCoシミュレーター
新NISAの最適積立額を計算
🏦
家計分析ツール
支出構造を可視化・改善
🛡️

おすすめサービス

このページをシェア

Twitter Facebook LINE

📧 お問い合わせ

ご意見・ご要望・不具合のご報告は appadaycreator@gmail.com または お問い合わせフォーム へ|プライバシーポリシー

${title}

生成日: ${dateStr}

📊 シミュレーション概要

現在年齢: ${S.age}歳

退職予定年齢: ${S.retireAge}歳

年収: ${S.income}万円

月生活費: ${S.expenses}万円

初期貯蓄: ${S.savings}万円

月積立額: ${S.invest}万円

💰 主な結果

📋 免責事項

本シミュレーション結果は参考情報です。実際の投資判断や資産管理については、ファイナンシャルプランナーなどの専門家にご相談ください。
📖 マネー・育児・キャリアのコラムを読む →

📖 お役立ちコラム

運営: AppADayCreator | 株式会社Tech&Brace
`; const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `lifeplan_${now.toISOString().split('T')[0]}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('success', 'PDF をダウンロードしました'); } catch(e) { console.error('PDF生成エラー:', e); showToast('error', 'PDF生成に失敗しました'); } } // ===== 履歴機能 ===== const HISTORY_KEY = 'lifeplan-history'; const MAX_HISTORY = 10; function saveHistory() { try { const now = new Date(); const item = { timestamp: now.getTime(), dateStr: now.toLocaleDateString('ja-JP'), timeStr: now.toLocaleTimeString('ja-JP'), age: S.age, income: S.income, savings: S.savings, expenses: S.expenses, rrate: S.rrate, invest: S.invest, retireAge: S.retireAge }; const hist = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); hist.unshift(item); if (hist.length > MAX_HISTORY) hist.pop(); localStorage.setItem(HISTORY_KEY, JSON.stringify(hist)); } catch(e) { console.error('履歴保存エラー:', e); } } function renderHistory() { try { const hist = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); const histDiv = document.getElementById('history-list'); if (!histDiv) return; histDiv.innerHTML = hist.length === 0 ? '

履歴はまだありません

' : hist.map((h, i) => `
${h.dateStr} ${h.timeStr}
年齢${h.age}歳 · 年収${h.income}万円 · 月積立${h.invest}万円
`).join(''); } catch(e) { console.error('履歴表示エラー:', e); } } function loadHistoryItem(idx) { try { const hist = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); if (idx < 0 || idx >= hist.length) return; const h = hist[idx]; S.age = h.age; S.income = h.income; S.savings = h.savings; S.expenses = h.expenses; S.rrate = h.rrate; S.invest = h.invest; S.retireAge = h.retireAge; fillForm(); goStep(1); showToast('info', '履歴を復元しました'); } catch(e) { console.error('履歴復元エラー:', e); } } // ===== Init ===== function init() { try { showLoad(true); const fromParams = loadFromParams(); if(!fromParams) { const saved = localStorage.getItem(KEY); if (saved) { const parsed = JSON.parse(saved); S = Object.assign({}, DEF, parsed); S.ev = Object.assign({}, DEF.ev, parsed.ev || {}); } } fillForm(); renderFAQ(); if(fromParams) { setTimeout(() => showToast('info', 'シェアされたシミュレーション設定を読み込みました'), 600); // URLシェア時は自動でシミュレーション実行してSTEP3を表示 setTimeout(() => simulate(), 800); } else { setTimeout(() => showLoad(false), 400); // [V3] ローディング後12秒でコラムポップアップを表示(ユーザーが操作開始後のタイミング) setTimeout(() => { const rb = document.getElementById('related-blog'); if(rb) rb.style.display = 'block'; }, 12000); } } catch(e) { console.error('Init error:', e); showLoad(false); showToast('error', '設定の読み込みに失敗しました'); } } // ===== Loading ===== function showLoad(v, msg) { try { const overlay = document.getElementById('loading-overlay'); overlay.classList.toggle('hidden', !v); const txt = document.getElementById('loading-text'); const step = document.getElementById('loading-step'); // [V2] プログレスバーアニメーション制御 const bar = document.getElementById('load-progress-bar'); if (bar) { if (v) { bar.style.transition = 'none'; bar.style.width = '5%'; bar.style.animation = 'load-fill 2.5s ease-out forwards'; } else { bar.style.animation = 'none'; bar.style.transition = 'width .3s ease'; bar.style.width = '100%'; setTimeout(() => { bar.style.width = '5%'; bar.style.transition = 'none'; }, 350); } } if (txt && v) txt.textContent = msg || '読み込み中...'; if (step) { if (v && msg && msg !== '読み込み中...') { step.style.display = 'block'; // ステップアニメーション: 1秒ごとにドットを増やす let dots = 0; step._timer = setInterval(() => { dots = (dots + 1) % 4; step.textContent = '▶ 計算中' + '.'.repeat(dots); }, 500); } else { step.style.display = 'none'; if (step._timer) { clearInterval(step._timer); step._timer = null; } } } } catch(e) { console.error(e); } } // ===== Toast ===== function showToast(type, msg) { try { const t = document.getElementById('toast'); t.className = `toast ${type}`; t.textContent = type==='success' ? '✓ '+msg : type==='error' ? '✕ '+msg : 'ℹ '+msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 3000); } catch(e) { console.error(e); } } // ===== Fill form from state ===== function fillForm() { try { const set = (id, v) => { const e = document.getElementById(id); if(e) e.value = v; }; set('age', S.age); set('income', S.income); set('savings', S.savings); set('expenses', S.expenses); set('invest', S.invest); set('rrate', S.rrate); set('igrate', S.incomeGrowth ?? 1); set('inflrate', S.inflRate ?? 1); set('spouse-income', S.spouseIncome ?? 0); set('spouse-retire', S.spouseRetireAge ?? 60); document.getElementById('rrval').textContent = S.rrate.toFixed(1) + '%'; document.getElementById('igval').textContent = (S.incomeGrowth ?? 1).toFixed(1) + '%/年'; document.getElementById('inflval').textContent = (S.inflRate ?? 1).toFixed(1) + '%/年'; selRbyVal('family', S.family); selRbyVal('housing', S.housing); updateSpouseVisibility(); const ev = S.ev; ['marry','house','child'].forEach(k => { const on = ev[k]; if(on) { document.getElementById('tg-'+k).classList.add('on'); document.getElementById('tc-'+k).classList.add('show'); } }); if(ev.custom) { document.getElementById('tg-custom').classList.add('on'); document.getElementById('tc-custom').classList.add('show'); renderCustomEvents(); } set('marry-age', ev.mage); set('marry-cost', ev.mcost); set('house-age', ev.hage); set('house-down', ev.hdown); set('house-loan', ev.hloan); set('house-yrs', ev.hyrs); set('child-age', ev.cage); selRbyVal('nchild', String(ev.nchild)); selRbyVal('edu', ev.edu); set('retire-age', S.retireAge); set('pension', S.pension); set('pension-start', S.pensionStart ?? 65); set('retire-exp', S.retireExp); set('lumpsum', S.lumpsum); updateHero(); updateIncomeHint(); } catch(e) { console.error('FillForm error:', e); } } // ===== Read form into state ===== function readForm() { try { const n = id => parseFloat(document.getElementById(id)?.value||0)||0; S.age = Math.max(20, Math.min(70, n('age'))); S.income = n('income'); S.savings = n('savings'); S.expenses = n('expenses'); S.invest = n('invest'); S.rrate = n('rrate'); S.incomeGrowth = n('igrate'); S.inflRate = n('inflrate'); S.spouseIncome = n('spouse-income'); S.spouseRetireAge = n('spouse-retire'); S.ev.mage = n('marry-age'); S.ev.mcost = n('marry-cost'); S.ev.hage = n('house-age'); S.ev.hdown = n('house-down'); S.ev.hloan = n('house-loan'); S.ev.hyrs = n('house-yrs'); S.ev.cage = n('child-age'); S.ev.nchild = parseInt(getRV('nchild')||'1'); S.ev.edu = getRV('edu')||'public'; S.retireAge = n('retire-age'); S.pension = n('pension'); S.pensionStart = n('pension-start'); S.retireExp = n('retire-exp'); S.lumpsum = n('lumpsum'); } catch(e) { console.error('ReadForm error:', e); } } // ===== Radio ===== function selR(el) { try { const g = el.dataset.g; document.querySelectorAll(`[data-g="${g}"]`).forEach(b => { b.classList.remove('sel'); if(b.hasAttribute('aria-checked')) b.setAttribute('aria-checked','false'); }); el.classList.add('sel'); if(el.hasAttribute('aria-checked')) el.setAttribute('aria-checked','true'); if(g==='family') { S.family = el.dataset.v; updateSpouseVisibility(); } if(g==='housing') S.housing = el.dataset.v; onInp(); } catch(e) { console.error(e); } } function getRV(g) { return document.querySelector(`[data-g="${g}"].sel`)?.dataset.v||''; } function selRbyVal(g, v) { document.querySelectorAll(`[data-g="${g}"]`).forEach(b => b.classList.toggle('sel', b.dataset.v===v)); } // ===== Toggle events ===== function togEv(k) { try { const tg = document.getElementById('tg-'+k); const tc = document.getElementById('tc-'+k); const on = tg.classList.toggle('on'); tc.classList.toggle('show', on); S.ev[k] = on; onInp(); } catch(e) { console.error('TogEv error:', e); } } // ===== Range ===== function onRR(el) { document.getElementById('rrval').textContent = parseFloat(el.value).toFixed(1)+'%'; S.rrate = parseFloat(el.value); onInp(); } function onIG(el) { document.getElementById('igval').textContent = parseFloat(el.value).toFixed(1)+'%/年'; S.incomeGrowth = parseFloat(el.value); onInp(); } function onInfl(el) { document.getElementById('inflval').textContent = parseFloat(el.value).toFixed(1)+'%/年'; S.inflRate = parseFloat(el.value); onInp(); } function updateSpouseVisibility() { const card = document.getElementById('spouse-card'); if(!card) return; const show = S.family === 'couple' || S.family === 'children'; card.classList.toggle('show', show); } // ===== On input ===== function onInp() { try { readForm(); save(); updateHero(); updateIncomeHint(); } catch(e) { console.error('OnInp error:', e); } } function save() { try { localStorage.setItem(KEY, JSON.stringify(S)); } catch(e) { console.error('Save error:', e); } } // ===== Calculation engine ===== // 所得税(実効税率)+社会保険料(厚生年金9.15%+健保5%+雇用0.6%≒15%) function effectiveRate(inc) { const tax = inc>=1000 ? .33 : inc>=800 ? .28 : inc>=600 ? .23 : inc>=400 ? .20 : .15; return Math.min(tax + 0.15, 0.55); } // 2プール方式: 投資プール(rrate%運用) + 現金プール(0%) // st: 省略時はグローバルS、シナリオ計算時は任意の状態を渡す function calc(st) { try { st = st || S; const {age, income, savings, expenses, invest, rrate, retireAge, pension, retireExp, lumpsum, ev, incomeGrowth} = st; const pensionStart = Math.max(retireAge, st.pensionStart ?? 65); const inflR = (st.inflRate ?? 1) / 100; const spouseInc = st.spouseIncome ?? 0; const spouseRetAge = st.spouseRetireAge ?? 60; const r = rrate / 100; const growR = (incomeGrowth ?? 1) / 100; const evm = {}; const evList = []; if(ev.marry) { evm[ev.mage] = (evm[ev.mage]||0) + ev.mcost*10000; evList.push({icon:'💒',name:'結婚',age:ev.mage,cost:ev.mcost,clr:'#f472b6'}); } if(ev.house) { evm[ev.hage] = (evm[ev.hage]||0) + ev.hdown*10000; evList.push({icon:'🏡',name:'マイホーム頭金',age:ev.hage,cost:ev.hdown,clr:'#60a5fa'}); } if(ev.child) { const cpc = ev.edu==='public' ? 2000*10000 : 4000*10000; for(let i=0;i { if(!c.age || !c.amount) return; const amt = c.amount * 10000; if(c.type === 'income') { // 収入イベント: cashPoolへ加算(evmの符号が逆になるため専用フラグで管理) evList.push({icon:'💰',name:c.name||'一時収入',age:c.age,income:c.amount,clr:'#22d3ee',isIncome:true}); } else { evm[c.age] = (evm[c.age]||0) + amt; evList.push({icon:'📝',name:c.name||'その他支出',age:c.age,cost:c.amount,clr:'#a78bfa'}); } }); } // 収入イベントのマップを別途作成 const incomeEv = {}; if(ev.custom && ev.customs) { ev.customs.filter(c=>c.type==='income'&&c.age&&c.amount).forEach(c => { incomeEv[c.age] = (incomeEv[c.age]||0) + c.amount*10000; }); } // 初期貯蓄は全て投資プールで運用開始(簡略化) let investPool = savings * 10000; let cashPool = 0; const data = []; for(let yr = age; yr <= 90; yr++) { const isRet = yr >= retireAge; const evCost = evm[yr] || 0; if(yr === retireAge && lumpsum > 0) cashPool += lumpsum * 10000; if(!isRet) { // 昇給考慮の年収→手取り(配偶者収入も加算) const curInc = income * Math.pow(1 + growR, yr - age); const net = curInc * 10000 * (1 - effectiveRate(curInc)); // 配偶者が現役の場合は配偶者の手取りも加算 const spouseNet = (spouseInc > 0 && yr < spouseRetAge) ? spouseInc * 10000 * (1 - effectiveRate(spouseInc)) : 0; // インフレ率を生活費に適用 const inflMult = Math.pow(1 + inflR, yr - age); const annExp = expenses * 12 * 10000 * inflMult; const annSav = Math.max(0, net + spouseNet - annExp); const investAnn = Math.min(invest * 12 * 10000, annSav); // 積立投資:投資プールへ const cashAnn = annSav - investAnn; // 残り:現金プールへ const hLoan = ev.house && yr >= ev.hage && yr < ev.hage + ev.hyrs ? ev.hloan * 12 * 10000 : 0; const incAdd = incomeEv[yr] || 0; investPool = (investPool + investAnn) * (1 + r); cashPool = cashPool + cashAnn - evCost - hLoan + incAdd; } else { // 退職後:投資は継続運用、年金・生活費は現金プールで管理(生活費にインフレ適用) const inflMult = Math.pow(1 + inflR, yr - age); const incAdd = incomeEv[yr] || 0; investPool = investPool * (1 + r); const pensionAmt = yr >= pensionStart ? pension * 12 * 10000 : 0; cashPool = cashPool + pensionAmt - retireExp * 12 * 10000 * inflMult - evCost + incAdd; } // 現金不足 → 投資プールから引き出し if(cashPool < 0 && investPool > 0) { const withdraw = Math.min(-cashPool, investPool); cashPool += withdraw; investPool -= withdraw; } data.push({age: yr, assets: Math.round(investPool + cashPool)}); } // FIRE達成年齢: 資産が(月生活費×12×25)を超える最初の年を探索(4%ルール) const fireTarget = st.expenses * 12 * 10000 * 25; let fireAge = null; for(const d of data) { if(d.age < (st.retireAge || 65) && d.assets >= fireTarget) { fireAge = d.age; break; } } return {data, evList, fireAge}; } catch(e) { console.error('Calc error:', e); showToast('error', '計算エラーが発生しました'); return {data:[], evList:[]}; } } // ===== Update hero summary ===== function updateHero() { try { document.getElementById('sy').textContent = (90-S.age)+'年'; const {data} = calc(); if(!data||data.length===0) return; const atr = data.find(d=>d.age===S.retireAge)||data[data.length-1]; const ate = data[data.length-1]; const rm = Math.round(atr.assets/10000); const em = Math.round(ate.assets/10000); const wY = Math.max(1, S.retireAge-S.age); const need = S.retireExp*12*30*10000 - S.pension*12*30*10000; const short = need - (atr?atr.assets:0); const mneed = short>0 ? Math.round(short/wY/12/10000) : 0; const fmt = n => n>=0 ? `+${Math.round(n).toLocaleString()}万円` : `${Math.round(n).toLocaleString()}万円`; const sa = document.getElementById('sa'); sa.textContent = rm.toLocaleString()+'万円'; sa.className = 'sc-val '+(rm>=0?'ok':'ng'); const sg = document.getElementById('sg'); sg.textContent = fmt(em); sg.className = 'sc-val '+(em>=0?'ok':'ng'); document.getElementById('ss').textContent = mneed>0 ? `+${mneed}万円` : '充足'; } catch(e) { console.error('UpdateHero error:', e); } } // ===== バリデーション ===== // ===== Simulate ===== function simulate() { try { readForm(); save(); if(!validateInputs()) return; showLoad(true, 'シミュレーション計算中...'); const {data, evList, fireAge} = calc(); setTimeout(() => { try { chartData = data; chartDataEvList = evList; chartTab = 'all'; ['all','pre','post'].forEach(t => document.getElementById('ctab-'+t)?.classList.toggle('active', t==='all')); renderRCards(data, fireAge); renderHealthScore(data); renderFulfillmentBar(data); renderRetirementSummary(data); renderAdvice(data); renderChart(data, evList); renderChartJs(data, evList); renderEvList(evList); renderAssetTable(data, evList); setTimeout(() => { const body = document.getElementById('asset-table-body'); if(body && body.style.display==='none') toggleAssetTable(); }, 150); syncQAPanel(); goTo(3); showLoad(false); showToast('success', 'シミュレーション完了!'); } catch(e2) { showLoad(false); showToast('error', '表示処理でエラーが発生しました'); console.error(e2); } }, 700); } catch(e) { showLoad(false); showToast('error', 'シミュレーション中にエラーが発生しました'); console.error(e); } } // ===== Result cards ===== function renderRCards(data, fireAge) { try { if(!data||!data.length) return; const atr = data.find(d=>d.age===S.retireAge)||data[data.length-1]; const ate = data[data.length-1]; const minA = Math.min(...data.map(d=>d.assets)); const zeroAge = data.find(d=>d.assets<0)?.age; const rm = Math.round(atr.assets/10000); const em = Math.round(ate.assets/10000); const cards = [ {icon:'🏦',label:`${S.retireAge}歳時点の資産`,val:`${rm.toLocaleString()}万円`,cls:rm>=0?'ok':'ng',sub:'退職金・積立含む'}, {icon:'🎯',label:'90歳時点の資産',val:`${em.toLocaleString()}万円`,cls:em>=0?'ok':'ng',sub:em>=0?'老後資金は充足':'資産が枯渇します'}, {icon:zeroAge?'⚠️':'✅',label:'資産枯渇リスク',val:zeroAge?`${zeroAge}歳で枯渇`:'リスクなし',cls:zeroAge?'ng':'ok',sub:zeroAge?'対策が必要です':'現在のプランで安全'}, {icon:'📉',label:'シミュレーション期間の最低額',val:`${Math.round(minA/10000).toLocaleString()}万円`,cls:minA>=0?'ok':'ng',sub:'期間中の最低資産額'} ]; // FIREカード(達成可能な場合のみ) if(fireAge !== null && fireAge !== undefined) { cards.push({icon:'🔥',label:'FIRE達成可能年齢(4%ルール)',val:`${fireAge}歳`,cls:'ok',sub:`生活費の25年分を${fireAge}歳で達成`}); } else { cards.push({icon:'🔥',label:'FIRE達成年齢(4%ルール)',val:'計算中…',cls:'warn',sub:'現プランではFIRE困難(積立増額で改善)'}); } document.getElementById('rcards').innerHTML = cards.map(c=>`
${c.icon}
${c.label}
${c.val}
${c.sub}
`).join(''); } catch(e) { console.error('RCards error:', e); } } // ===== 老後資金充足度インジケーター ===== function renderFulfillmentBar(data) { try { const wrap = document.getElementById('fulfillment-bar-wrap'); if(!wrap||!data||!data.length) return; const at90 = data[data.length-1].assets; // 退職後30年の必要額 = 月生活費 × 12 × 30 const needed = S.retireExp * 12 * 30 * 10000; // 退職時資産 + 年金総額(年金が始まる年齢から90歳まで) const pensionYears = Math.max(0, 90 - Math.max(S.retireAge, S.pensionStart ?? 65)); const totalPension = S.pension * 12 * pensionYears * 10000; const retireAssets = (data.find(d=>d.age===S.retireAge)||data[0]).assets; const totalAvail = Math.max(0, retireAssets + totalPension); const pct = Math.min(100, Math.max(0, Math.round(totalAvail / needed * 100))); const cls = pct >= 100 ? '#0d9488' : pct >= 70 ? '#f59e0b' : '#ef4444'; const badge = pct >= 100 ? '🟢 充足' : pct >= 70 ? '🟡 要対策' : '🔴 不足'; const badgeMsg = pct >= 100 ? '現在のプランで老後30年分の生活費をカバーできます' : pct >= 70 ? '老後資金が一部不足する可能性があります。積立増額を検討しましょう' : '老後資金が大幅に不足する見込みです。FPへの相談を強くおすすめします'; wrap.innerHTML = `
📊 老後資金充足度
${badge}(${pct}%)
${badgeMsg}
退職後30年の必要額: ${Math.round(needed/10000).toLocaleString()}万円 | 準備できる資産+年金: ${Math.round(totalAvail/10000).toLocaleString()}万円
`; setTimeout(() => { const bar = document.getElementById('fulfillment-progress'); if(bar) bar.style.width = pct + '%'; }, 60); } catch(e) { console.error('FulfillmentBar error:', e); } } // ===== 退職後月次収支サマリー ===== function renderRetirementSummary(data) { try { const wrap = document.getElementById('retirement-summary-wrap'); if(!wrap||!data||!data.length) return; const inflMult = v => Math.pow(1 + (S.inflRate ?? 1) / 100, v - S.age); const pensionStart = Math.max(S.retireAge, S.pensionStart ?? 65); // 退職時・70歳・80歳の月次収支 const checkpoints = [ {label:`${S.retireAge}歳 退職直後`, age: S.retireAge}, {label:'70歳', age: 70}, {label:'80歳', age: 80}, ].filter(c => c.age >= S.age && c.age <= 90); const rows = checkpoints.map(c => { const pensionMon = c.age >= pensionStart ? S.pension : 0; const expMon = Math.round(S.retireExp * inflMult(c.age) * 10) / 10; const diff = Math.round((pensionMon - expMon) * 10) / 10; return {label: c.label, pension: pensionMon, exp: expMon, diff}; }); wrap.innerHTML = `

🌅 退職後 月次収支サマリー

年金収入・生活費・月次差額(インフレ調整済み)
${rows.map(r=>`
${r.label}
年金: ${r.pension}万円/月
生活費: ${r.exp}万円/月
差額: ${r.diff>=0?'+':''}${r.diff}万円/月
`).join('')}
${rows.some(r=>r.diff<0)?`
⚠ 差額がマイナスの時期は貯蓄・投資を取り崩します。${S.retireExp * 12 * 10 * 10000 < 0 ? '' : '10年分の取り崩し予想額: 約' + Math.round(rows.find(r=>r.diff<0).diff * -12 * 10 * 10) / 10 + '万円'}
`:''}
`; } catch(e) { console.error('RetirementSummary error:', e); } } // ===== Advice ===== function renderAdvice(data) { try { const wrap = document.getElementById('advice-wrap'); if(!wrap||!data||!data.length) return; const at90 = data[data.length-1].assets; const tips = []; if(at90 < 0) { // 追加積立で黒字化できる額を0.5万刻みで探す let extra = 0; for(let e = 0.5; e <= 50; e += 0.5) { const {data:d2} = calc({...S, invest: S.invest + e}); if(d2[d2.length-1]?.assets >= 0) { extra = e; break; } } if(extra > 0) { tips.push({icon:'💡', clr:'#0d9488', title:`月${extra}万円の追加積立で黒字化できます`, body:`現在の積立額(月${S.invest}万円)に加えて月${extra}万円増やすと、90歳時点でプラスに転じます。新NISAやiDeCoを活用して積立を増やしましょう。`}); } else { tips.push({icon:'⚠️', clr:'#ef4444', title:'現状のプランでは老後資金が大幅に不足します', body:'月50万円の追加積立でも黒字化できません。収入アップ・支出削減・退職延長など複合対策が必要です。FP(ファイナンシャルプランナー)に無料相談することをおすすめします。'}); } } else { tips.push({icon:'✅', clr:'#0d9488', title:'現在のプランで老後資金は充足しています', body:`90歳時点の資産は${Math.round(at90/10000).toLocaleString()}万円のプラスです。引き続き積立を継続しながら、余剰資金のさらなる活用も検討しましょう。`}); } // シナリオ:退職を5年延ばす if(S.retireAge < 70) { const newAge = S.retireAge + 5; const {data:dR} = calc({...S, retireAge: newAge}); const diff = Math.round((dR[dR.length-1].assets - at90) / 10000); if(Math.abs(diff) >= 30) { tips.push({icon:'🗓️', clr:'#8b5cf6', title:`退職を${newAge}歳に延ばすと${diff>=0?'+':''}${diff.toLocaleString()}万円の変化`, body:`5年間の追加収入と年金受給前の引き出し削減により、老後資金が大きく${diff>=0?'改善':'悪化(逆効果)となる場合は生活費見直しが先決'}します。`}); } } // シナリオ:支出を月2万円削減 if(S.expenses > 7) { const {data:dE} = calc({...S, expenses: S.expenses - 2}); const diff = Math.round((dE[dE.length-1].assets - at90) / 10000); if(diff >= 30) { tips.push({icon:'💰', clr:'#f59e0b', title:`月2万円の支出削減で90歳時点が+${diff.toLocaleString()}万円改善`, body:'固定費(保険・サブスク・通信費・電気代)の見直しで月2万円の削減は十分可能です。家計分析ツールで支出を可視化して無駄を特定しましょう。'}); } } // 複合シナリオ:積立増額 + 退職延長 if(S.retireAge < 70 && at90 < 0) { const combo = {...S, invest: S.invest + 1, retireAge: S.retireAge + 3}; const {data:dC} = calc(combo); const diffC = Math.round((dC[dC.length-1].assets - at90) / 10000); if(diffC >= 50) { tips.push({icon:'🔧', clr:'#7c3aed', title:`組み合わせ効果:月+1万円積立&退職+3年で+${diffC.toLocaleString()}万円`, body:`積立額を月1万円増やし(${S.invest}→${S.invest+1}万円)、退職を${S.retireAge+3}歳に延ばす組み合わせで大きな改善が見込めます。単独の施策よりも相乗効果が期待できます。`}); } } wrap.style.display = 'block'; wrap.innerHTML = `

🎯 改善アドバイス

シミュレーション結果に基づく具体的な改善シナリオ
${tips.map(t=>`
${t.icon}
${t.title}
${t.body}
`).join('')}
`; } catch(e) { console.error('Advice error:', e); } } // ===== SVG Chart ===== function renderChart(data, evList) { try { const sk = document.getElementById('chart-skeleton'); const svg = document.getElementById('chart'); if(!data||data.length<2) { sk.style.display='block'; svg.style.display='none'; return; } const isMobile = window.innerWidth < 640; const W = isMobile ? 500 : 800; const H = isMobile ? 220 : 300; const pL = isMobile ? 55 : 72; const pR = isMobile ? 16 : 24; const pT = isMobile ? 22 : 28; const pB = isMobile ? 32 : 40; const cW=W-pL-pR, cH=H-pT-pB; const lfs = isMobile ? 11 : 10; // Y軸ラベルフォントサイズ const dfs = isMobile ? 10 : 9; // データラベルフォントサイズ // シナリオ計算(チェックボックスON時のみ) const scenarioOn = document.getElementById('scenario-toggle')?.checked; let optData = [], pesData = []; if(scenarioOn) { const sameRange = d => d.age >= data[0].age && d.age <= data[data.length-1].age; try { optData = calc({...S, rrate: S.rrate + 2}).data.filter(sameRange); } catch(e2) {} try { pesData = calc({...S, rrate: Math.max(0, S.rrate - 1.5)}).data.filter(sameRange); } catch(e2) {} } // スケール:シナリオON時は全データを含めて算出 const allScaleData = scenarioOn ? [...data, ...optData, ...pesData] : data; const assets = allScaleData.map(d=>d.assets/10000); const minA = Math.min(0,...assets); const maxA = Math.max(0,...assets); const range = maxA-minA||1; const xS = i => pL+(i/(data.length-1))*cW; const yS = v => pT+(1-(v-minA)/range)*cH; const zY = yS(0); const pts = data.map((d,i)=>[xS(i),yS(d.assets/10000)]); const line = 'M'+pts.map(p=>p.join(',')).join('L'); const areaPos = line+`L${pts[pts.length-1][0]},${zY}L${pts[0][0]},${zY}Z`; const areaNeg = line+`L${pts[pts.length-1][0]},${zY}L${pts[0][0]},${zY}Z`; const rIdx = data.findIndex(d=>d.age===S.retireAge); // クリップパスでゼロライン上下に色分け let xml = ` `; // Grid [0,.25,.5,.75,1].forEach(t => { const v = minA+t*range, y = yS(v); const fmt = Math.abs(v)>=10000 ? (v/10000).toFixed(1)+'億円' : Math.round(v).toLocaleString()+'万'; xml += ``; xml += `${fmt}`; }); // マイルストーンライン(500万・1000万・2000万・5000万・1億) const milestones = [500, 1000, 2000, 5000, 10000]; milestones.forEach(m => { if(m > maxA || m < minA) return; const my = yS(m); const mfmt = m >= 10000 ? (m/10000).toFixed(0)+'億' : m.toLocaleString()+'万'; xml += ``; xml += `${mfmt}`; }); // Zero line xml += ``; // ライフイベントマーカー if(evList && evList.length) { const drawnAges = new Set(); evList.forEach(ev => { if(drawnAges.has(ev.age)) return; const eidx = data.findIndex(d=>d.age===ev.age); if(eidx<0) return; let clr = ev.clr || '#94a3b8'; const mx = xS(eidx); xml += ``; xml += `${ev.icon}`; drawnAges.add(ev.age); }); } // Retire line if(rIdx>=0) { const rx = xS(rIdx); xml += ``; xml += `退職 ${S.retireAge}歳`; } // 正の面グラフ(緑)・負の面グラフ(赤) xml += ``; xml += ``; xml += ``; // シナリオライン(楽観/悲観) if(scenarioOn) { const drawScenarioLine = (sData, color) => { if(sData.length < 2) return; const sPts = sData.map(d => { const idx = data.findIndex(dd => dd.age === d.age); return idx >= 0 ? [xS(idx), yS(d.assets/10000)] : null; }).filter(Boolean); if(sPts.length >= 2) xml += ``; }; drawScenarioLine(optData, '#0ea5e9'); drawScenarioLine(pesData, '#f97316'); } // データポイント(モバイルは特殊点のみ・ラベル重なり防止) const valueLabelStep = isMobile ? 20 : 10; // 重なり防止: 前の特殊ラベルのx座標を記録 const drawnLabelX = []; const minLabelDist = isMobile ? 44 : 36; data.forEach((d,i) => { const isSpecial = d.age===S.age||d.age===S.retireAge||d.age===90; const isStep = !isMobile && d.age%valueLabelStep===0; if(isStep||isSpecial) { const [x,y]=pts[i], v=Math.round(d.assets/10000), clr=d.assets>=0?'#0d9488':'#ef4444'; const fmt = Math.abs(v)>=10000 ? (v/10000).toFixed(1)+'億' : v.toLocaleString()+'万'; xml += ``; // 直近の描画済みラベルと近すぎる場合はスキップ(重なり防止) const tooClose = drawnLabelX.some(lx => Math.abs(x - lx) < minLabelDist); if(!tooClose && (isSpecial||(!isMobile&&isStep))) { // ラベルがグラフ上端に近い場合は下にずらす const labelY = y < pT + 18 ? y + 18 : y - 9; xml += `${fmt}`; drawnLabelX.push(x); } xml += `${d.age}歳`; } }); // 凡例(右上) const legW = isMobile ? 110 : 130; const legH = scenarioOn ? (isMobile ? 68 : 70) : (isMobile ? 44 : 46); const lx = W - pR - legW, ly = pT + 2; const lf = isMobile ? 10 : 9; xml += ``; xml += ``; xml += `現状プラン`; xml += ``; xml += `ゼロライン`; xml += ``; xml += `退職時点`; if(scenarioOn) { // 楽観・悲観の90歳時資産値を凡例に表示 const optAt90 = optData.length ? Math.round(optData[optData.length-1].assets/10000) : null; const pesAt90 = pesData.length ? Math.round(pesData[pesData.length-1].assets/10000) : null; const optFmt = optAt90 !== null ? (Math.abs(optAt90)>=10000 ? (optAt90/10000).toFixed(1)+'億' : optAt90.toLocaleString()+'万') : ''; const pesFmt = pesAt90 !== null ? (Math.abs(pesAt90)>=10000 ? (pesAt90/10000).toFixed(1)+'億' : pesAt90.toLocaleString()+'万') : ''; xml += ``; xml += `楽観+2%${optFmt?': '+optFmt:''}`; xml += ``; xml += `悲観-1.5%${pesFmt?': '+pesFmt:''}`; } svg.setAttribute('viewBox', `0 0 ${W} ${H}`); svg.innerHTML = xml; sk.style.display = 'none'; svg.style.display = 'block'; // ツールチップ(mouse + タッチ対応) const tip = document.getElementById('chart-tip'); const wrap = svg.parentElement; function showTip(clientX, clientY) { const svgRect = svg.getBoundingClientRect(); const wrapRect = wrap.getBoundingClientRect(); const scaleX = W / svgRect.width; const mx = (clientX - svgRect.left) * scaleX; const idx = Math.min(data.length-1, Math.max(0, Math.round((mx - pL) / cW * (data.length-1)))); const d = data[idx]; const v = Math.round(d.assets/10000); const clr = d.assets>=0 ? '#0d9488' : '#ef4444'; const fmt = Math.abs(v)>=10000 ? (v/10000).toFixed(1)+'億円' : v.toLocaleString()+'万円'; tip.innerHTML = `${d.age}歳 ${fmt}`; const tx = Math.min(clientX - wrapRect.left + 10, wrapRect.width - 145); const ty = Math.max(4, clientY - wrapRect.top - 48); tip.style.left = tx + 'px'; tip.style.top = ty + 'px'; tip.style.display = 'block'; } svg.addEventListener('mousemove', e => showTip(e.clientX, e.clientY)); svg.addEventListener('mouseleave', () => { tip.style.display='none'; }); // タッチ操作: touchstart/touchmoveでツールチップ表示(スクロールを妨げない) svg.addEventListener('touchstart', function(e) { if(e.touches.length) showTip(e.touches[0].clientX, e.touches[0].clientY); }, {passive:true}); svg.addEventListener('touchmove', function(e) { if(e.touches.length) showTip(e.touches[0].clientX, e.touches[0].clientY); }, {passive:true}); svg.addEventListener('touchend', () => { setTimeout(()=>{ tip.style.display='none'; }, 1200); }); } catch(e) { console.error('Chart error:', e); } } // ===== Event list ===== function renderEvList(evList) { try { const el = document.getElementById('evlist'); const extra = []; if(S.ev.house) { const loanEnd = S.ev.hage + S.ev.hyrs; const totalRepay = Math.round(S.ev.hloan * 12 * S.ev.hyrs); extra.push({icon:'🎉',name:`ローン完済(総返済額 ${totalRepay.toLocaleString()}万円)`,age:loanEnd,cost:null,badge:true,clr:'#0ea5e9'}); } const all = [ ...evList, ...extra, {icon:'🌅',name:'退職',age:S.retireAge,cost:null,clr:'#f59e0b'}, {icon:'🧓',name:'年金受給開始',age:Math.max(S.retireAge, S.pensionStart ?? 65),income:S.pension*12,cost:null,clr:'#8b5cf6'} ].sort((a,b)=>a.age-b.age); el.innerHTML = all.length ? all.map(ev=>`
${ev.icon}
${ev.name}
${ev.age}歳
${ev.cost!=null?`-${ev.cost.toLocaleString()}万円`:ev.income?`+${ev.income.toLocaleString()}万円/年`:'–'}
`).join('') : '

ライフイベントが設定されていません。STEP2に戻って設定してください。

'; } catch(e) { console.error('EvList error:', e); } } // ===== FAQ ===== function renderFAQ() { try { document.getElementById('faqlist').innerHTML = FAQS.map((f,i)=>`
${f.a}
`).join(''); } catch(e) { console.error('FAQ error:', e); } } function togFAQ(i,el) { try { const a = document.getElementById('fa'+i); const open = a.classList.toggle('show'); el.classList.toggle('open', open); el.setAttribute('aria-expanded', open); } catch(e) { console.error(e); } } // ===== Navigation ===== function goTo(n) { try { [1,2,3].forEach(i => { document.getElementById('sp'+i)?.classList.toggle('active', i===n); const b = document.getElementById('sbtn'+i); if(b) { b.classList.toggle('active', i===n); b.classList.toggle('done', i rc.scrollIntoView({behavior:'smooth',block:'start'}), 80); else window.scrollTo({top:0,behavior:'smooth'}); } else { window.scrollTo({top:0,behavior:'smooth'}); } } catch(e) { console.error('GoTo error:', e); } } function saveResult() { try { save(); showToast('success', '計算結果を保存しました'); } catch(e) { showToast('error', '保存に失敗しました'); console.error(e); } } function printResult() { try { save(); window.print(); } catch(e) { showToast('error', '印刷に失敗しました'); console.error(e); } } function shareResult() { try { const sa = document.getElementById('sa')?.textContent || '–'; const sg = document.getElementById('sg')?.textContent || '–'; const ss = document.getElementById('ss')?.textContent || '–'; const text = `【ライフプランシミュレーション結果】\n退職時資産: ${sa}\n90歳時資産増減: ${sg}\n月々の追加貯蓄: ${ss}\n老後資金を無料でシミュレーション👇`; const url = buildShareUrl(); // モバイルのネイティブシェアを優先、PCではダイアログ表示 if(navigator.share && /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent)) { navigator.share({ title: 'ライフプランシミュレーション結果', text, url }).catch(()=>{}); return; } // シェアダイアログを表示 const modal = document.getElementById('share-modal'); const input = document.getElementById('share-url-input'); if(modal && input) { input.value = url; const encoded = encodeURIComponent(text + '\n' + url); const urlEncoded = encodeURIComponent(url); const twBtn = document.getElementById('share-twitter-btn'); const lineBtn = document.getElementById('share-line-btn'); const fbBtn = document.getElementById('share-facebook-btn'); if(twBtn) twBtn.href = `https://twitter.com/intent/tweet?text=${encoded}`; if(lineBtn) lineBtn.href = `https://line.me/R/msg/text/?${encoded}`; if(fbBtn) fbBtn.href = `https://www.facebook.com/sharer/sharer.php?u=${urlEncoded}`; modal.classList.remove('hidden'); setTimeout(() => input.select(), 100); } } catch(e) { showToast('error', 'シェアに失敗しました'); console.error(e); } } function downloadCSV() { try { const {data, evList} = calc(); if(!data||!data.length) { showToast('error', 'シミュレーションを先に実行してください'); return; } const evMap = {}; evList.forEach(ev => { evMap[ev.age] = (evMap[ev.age]||[]).concat(ev.name); }); const rows = [['年齢','資産合計(万円)','ライフイベント']]; data.forEach(d => { rows.push([d.age, Math.round(d.assets/10000), (evMap[d.age]||[]).join('/')]); }); const csv = '' + rows.map(r => r.map(v => `"${v}"`).join(',')).join('\n'); const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'lifeplan-simulation.csv'; a.click(); URL.revokeObjectURL(url); showToast('success', 'CSVをダウンロードしました'); } catch(e) { showToast('error', 'ダウンロードに失敗しました'); console.error(e); } } function setChartTab(tab) { try { chartTab = tab; ['all','pre','post'].forEach(t => document.getElementById('ctab-'+t)?.classList.toggle('active', t===tab)); if(!chartData||!chartData.length) return; let filtered = chartData; if(tab === 'pre') filtered = chartData.filter(d => d.age <= S.retireAge); if(tab === 'post') filtered = chartData.filter(d => d.age >= S.retireAge); if(filtered.length < 2) filtered = chartData; renderChart(filtered, chartDataEvList); renderChartJs(filtered, chartDataEvList); } catch(e) { console.error(e); } } function toggleScenario() { try { if(!chartData||!chartData.length) return; let filtered = chartData; if(chartTab === 'pre') filtered = chartData.filter(d => d.age <= S.retireAge); if(chartTab === 'post') filtered = chartData.filter(d => d.age >= S.retireAge); if(filtered.length < 2) filtered = chartData; renderChart(filtered, chartDataEvList); renderChartJs(filtered, chartDataEvList); } catch(e) { console.error(e); } } // Chart.jsを使ったグラフ描画 let chartInstance = null; function renderChartJs(data, evList) { try { if(!data || data.length < 2) return; const container = document.getElementById('chartjs-container'); const canvas = document.getElementById('assetChart'); if(!container || !canvas) return; const scenarioOn = document.getElementById('scenario-toggle')?.checked; const sameRange = d => d.age >= data[0].age && d.age <= data[data.length-1].age; let optData = [], pesData = []; if(scenarioOn) { try { optData = calc({...S, rrate: S.rrate + 2}).data.filter(sameRange); } catch(e2) {} try { pesData = calc({...S, rrate: Math.max(0, S.rrate - 1.5)}).data.filter(sameRange); } catch(e2) {} } const labels = data.map(d => d.age.toString()); const assetData = data.map(d => Math.round(d.assets / 1000000) / 10); // 億円単位 const datasets = [{ label: '基本シナリオ', data: assetData, borderColor: '#0d9488', backgroundColor: 'rgba(13,148,136,0.1)', fill: true, tension: 0.4, borderWidth: 2.5, pointRadius: 0, pointHoverRadius: 6, pointBackgroundColor: '#0d9488', }]; if(scenarioOn && optData.length > 1) { datasets.push({ label: '楽観(利回り +2%)', data: optData.map(d => Math.round(d.assets / 1000000) / 10), borderColor: '#10b981', borderDash: [5, 5], fill: false, tension: 0.4, borderWidth: 2, pointRadius: 0, }); } if(scenarioOn && pesData.length > 1) { datasets.push({ label: '悲観(利回り -1.5%)', data: pesData.map(d => Math.round(d.assets / 1000000) / 10), borderColor: '#ef4444', borderDash: [5, 5], fill: false, tension: 0.4, borderWidth: 2, pointRadius: 0, }); } const ctx = canvas.getContext('2d'); if(chartInstance) chartInstance.destroy(); chartInstance = new Chart(ctx, { type: 'line', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { font: { size: 12 }, color: 'var(--text)' } }, tooltip: { backgroundColor: '#1e293b', titleColor: '#fff', bodyColor: '#fff', padding: 8, callbacks: { label: function(context) { let val = context.parsed.y; let fmt = val >= 10 ? val.toFixed(1) + '億円' : Math.round(val * 10) + '千万円'; return context.dataset.label + ': ' + fmt; } } } }, scales: { x: { grid: { color: 'rgba(226,232,240,0.3)' }, ticks: { color: 'var(--muted)', font: { size: 11 } } }, y: { beginAtZero: false, grid: { color: 'rgba(226,232,240,0.3)' }, ticks: { color: 'var(--muted)', font: { size: 11 }, callback: function(val) { return val >= 10 ? val.toFixed(1) + '億' : Math.round(val * 10) + '千万'; } } } } } }); } catch(e) { console.error('ChartJS error:', e); } } // ===== バリデーション ===== // ===== フィールド単体バリデーション(blur時リアルタイム表示)===== function validateField(id) { try { const el = document.getElementById(id); if (!el) return; const field = el.closest('.field'); // 既存エラーをクリア const prevErr = field?.querySelector('.field-error'); if (prevErr) prevErr.remove(); el.closest('.inp-wrap')?.classList.remove('err'); el.setAttribute('aria-invalid', 'false'); // aria-describedbyからエラーIDを除去 const prevDesc = el.getAttribute('aria-describedby') || ''; el.setAttribute('aria-describedby', prevDesc.replace(id + '-error', '').trim()); const val = parseFloat(el.value); let msg = ''; switch(id) { case 'age': if (isNaN(val) || val < 20 || val > 70) msg = '年齢は20〜70歳の範囲で入力してください'; break; case 'income': if (isNaN(val) || val < 100) msg = '年収は100万円以上で入力してください(万円単位・税込み)'; else if (val > 5000) msg = '年収は5,000万円以下で入力してください'; break; case 'savings': if (isNaN(val) || val < 0) msg = '貯蓄額は0以上で入力してください'; break; case 'expenses': if (isNaN(val) || val < 5) msg = '月の生活費は5万円以上で入力してください(万円単位)'; else if (val > 100) msg = '月の生活費は100万円以下で入力してください'; break; case 'invest': if (!isNaN(val) && val < 0) msg = '月の積立投資額は0以上で入力してください'; break; case 'retire-age': if (isNaN(val) || val < 50 || val > 80) msg = '退職予定年齢は50〜80歳の範囲で入力してください'; else if (S.age && val <= S.age) msg = '退職年齢は現在の年齢より大きくしてください'; break; case 'pension': if (isNaN(val) || val < 0) msg = '年金受給額は0以上で入力してください(0なら年金なし)'; break; case 'retire-exp': if (isNaN(val) || val < 5) msg = '老後の月生活費は5万円以上で入力してください(万円単位)'; break; } if (msg) { el.closest('.inp-wrap')?.classList.add('err'); el.setAttribute('aria-invalid', 'true'); const errDiv = document.createElement('div'); errDiv.className = 'field-error'; errDiv.id = id + '-error'; errDiv.setAttribute('role', 'alert'); errDiv.textContent = '⚠ ' + msg; field?.appendChild(errDiv); // aria-describedbyにエラーIDを追加 const desc = (el.getAttribute('aria-describedby') || '').replace(id + '-error', '').trim(); el.setAttribute('aria-describedby', (desc ? desc + ' ' : '') + id + '-error'); } } catch(e) { console.error('ValidateField error:', e); } } function validateInputs() { try { const errs = []; document.querySelectorAll('.field-error').forEach(e => e.remove()); document.querySelectorAll('.inp-wrap.err').forEach(e => e.classList.remove('err')); document.querySelectorAll('[aria-invalid="true"]').forEach(e => e.setAttribute('aria-invalid', 'false')); if(isNaN(S.age) || S.age < 20 || S.age > 70) errs.push({id:'age', msg:'年齢は20〜70歳の範囲で入力してください'}); if(S.income < 100) errs.push({id:'income', msg:'年収は100万円以上で入力してください(万円単位・税込み)'}); if(S.savings < 0) errs.push({id:'savings', msg:'貯蓄額は0以上で入力してください'}); if(S.expenses < 5) errs.push({id:'expenses', msg:'月の生活費は5万円以上で入力してください(万円単位)'}); if(isNaN(S.retireAge) || S.retireAge < 50 || S.retireAge > 80) errs.push({id:'retire-age', msg:'退職予定年齢は50〜80歳の範囲で入力してください'}); if(S.retireAge <= S.age) errs.push({id:'retire-age', msg:'退職年齢は現在の年齢より大きくしてください'}); if(S.ev.marry && S.ev.mage < S.age) errs.push({id:'marry-age', msg:'結婚年齢は現在の年齢以降にしてください'}); if(S.ev.house && S.ev.hage < S.age) errs.push({id:'house-age', msg:'マイホーム購入年齢は現在の年齢以降にしてください'}); if(S.ev.child && S.ev.cage < S.age) errs.push({id:'child-age', msg:'子どもの誕生時年齢は現在の年齢以降にしてください'}); if(S.ev.custom && S.ev.customs) { S.ev.customs.forEach((c, i) => { if(c.age && c.age < S.age) errs.push({id:`cev-age-${i}`, msg:`「${c.name||'カスタムイベント'}」の年齢は現在の年齢以降にしてください`}); }); } if(S.expenses < 5) showToast('info', '月の生活費が少ない値です(万円単位で入力)'); errs.forEach(err => { const el = document.getElementById(err.id); if(el) { el.closest('.inp-wrap')?.classList.add('err'); el.setAttribute('aria-invalid', 'true'); const errDiv = document.createElement('div'); errDiv.className = 'field-error'; errDiv.id = err.id + '-error'; errDiv.setAttribute('role', 'alert'); errDiv.textContent = '⚠ ' + err.msg; el.closest('.field')?.appendChild(errDiv); // aria-describedbyにエラーIDを追加 const desc = (el.getAttribute('aria-describedby') || '').replace(err.id + '-error', '').trim(); el.setAttribute('aria-describedby', (desc ? desc + ' ' : '') + err.id + '-error'); } }); if(errs.length) { showToast('error', errs[0].msg); // エラーのある入力フィールドへスクロール const firstErr = document.getElementById(errs[0].id); if(firstErr) firstErr.scrollIntoView({behavior:'smooth', block:'center'}); } return errs.length === 0; } catch(e) { console.error('Validate error:', e); return true; } } // ===== 年金受給開始年齢 ===== // 65歳基準のベース年金額を記憶(自動調整のため) let _basePension = null; function onPensionStart(el) { try { S.pensionStart = Math.min(75, Math.max(60, parseFloat(el.value) || 65)); const hint = document.getElementById('pension-start-hint'); const adjBtn = document.getElementById('pension-auto-adjust-btn'); if(hint) { const diff = S.pensionStart - 65; if(diff === 0) { hint.textContent = ''; if(adjBtn) adjBtn.style.display = 'none'; _basePension = null; } else { // ベース年金額を記憶(初回または65歳に戻ったとき) if(_basePension === null) _basePension = S.pension; if(diff > 0) { const pct = Math.round(diff * 8.4 * 10) / 10; const adj = Math.round(_basePension * (1 + diff * 0.084) * 10) / 10; hint.textContent = `▶ ${diff}年繰り下げで年金額が約+${pct}%増加`; if(adjBtn) { adjBtn.textContent = `📐 自動調整: ${adj}万円/月 に更新する`; adjBtn.dataset.adj = adj; adjBtn.style.display = 'block'; } } else { const pct = Math.round(Math.abs(diff) * 4.8 * 10) / 10; const adj = Math.round(_basePension * (1 - Math.abs(diff) * 0.048) * 10) / 10; hint.textContent = `▶ ${Math.abs(diff)}年繰り上げで年金額が約-${pct}%減少`; if(adjBtn) { adjBtn.textContent = `📐 自動調整: ${adj}万円/月 に更新する`; adjBtn.dataset.adj = adj; adjBtn.style.display = 'block'; } } } } onInp(); } catch(e) { console.error(e); } } function applyAdjustedPension() { try { const btn = document.getElementById('pension-auto-adjust-btn'); const adj = parseFloat(btn?.dataset.adj); if(!adj || isNaN(adj)) return; const el = document.getElementById('pension'); if(el) { el.value = adj; S.pension = adj; } save(); updateHero(); showToast('success', `年金額を${adj}万円/月に更新しました`); } catch(e) { console.error(e); } } // ===== 年金推計 ===== function calcEstPension() { try { const years = Math.max(1, S.age - 22); const raw = S.income * 0.005481 * years / 12; const est = Math.round(Math.max(5, Math.min(30, raw)) * 2) / 2; document.getElementById('pension').value = est; S.pension = est; save(); showToast('info', `年収${S.income}万円・勤続約${years}年から${est}万円/月と推計しました(参考値)`); updateHero(); } catch(e) { showToast('error', '推計に失敗しました'); console.error(e); } } // ===== 資産推移テーブル ===== function renderAssetTable(data, evList) { try { const wrap = document.getElementById('asset-table-wrap'); if(!wrap||!data||!data.length) return; const evMap = {}; (evList||[]).forEach(ev => { evMap[ev.age] = (evMap[ev.age]||[]).concat(ev.icon+ev.name); }); const rows = data.filter(d => d.age===S.age || d.age%5===0 || d.age===S.retireAge || d.age===90); wrap.innerHTML = `
📊 年齢別資産推移テーブル
5歳刻みで資産額・ライフイベントを確認(クリックで展開)
`; } catch(e) { console.error('AssetTable error:', e); } } function toggleAssetTable() { try { const body = document.getElementById('asset-table-body'); const chevron = document.getElementById('asset-table-chevron'); if(!body) return; const open = body.style.display==='none'; body.style.display = open ? 'block' : 'none'; if(chevron) chevron.style.transform = open ? 'rotate(180deg)' : ''; } catch(e) { console.error(e); } } // ===== カスタムイベント ===== function togCustom() { try { const tg = document.getElementById('tg-custom'); const tc = document.getElementById('tc-custom'); const on = tg.classList.toggle('on'); tc.classList.toggle('show', on); S.ev.custom = on; if(on && S.ev.customs.length===0) addCustomEvent(); onInp(); } catch(e) { console.error(e); } } function addCustomEvent() { try { if(!S.ev.customs) S.ev.customs = []; if(S.ev.customs.length >= 5) { showToast('info', '最大5件まで追加できます'); return; } const idx = S.ev.customs.length; S.ev.customs.push({type:'expense', age:50, amount:100, name:''}); renderCustomEvents(); save(); } catch(e) { console.error(e); } } function removeCustomEvent(idx) { try { S.ev.customs.splice(idx, 1); renderCustomEvents(); save(); onInp(); } catch(e) { console.error(e); } } function onCustomChange(idx, field, val) { try { if(!S.ev.customs[idx]) return; if(field==='age'||field==='amount') S.ev.customs[idx][field] = parseFloat(val)||0; else S.ev.customs[idx][field] = val; save(); onInp(); } catch(e) { console.error(e); } } function renderCustomEvents() { try { const list = document.getElementById('custom-ev-list'); if(!list) return; list.innerHTML = (S.ev.customs||[]).map((c, i) => `
万円
`).join(''); } catch(e) { console.error(e); } } // ===== クイック調整パネル ===== function onQA(key, val) { try { val = parseFloat(val); if(key === 'rr') { S.rrate = val; document.getElementById('qa-rr-val').textContent = val.toFixed(1) + '%'; document.getElementById('rrate').value = val; document.getElementById('rrval').textContent = val.toFixed(1) + '%'; } else if(key === 'inv') { S.invest = val; document.getElementById('qa-inv-val').textContent = val + '万円'; document.getElementById('invest').value = val; } else if(key === 'ret') { S.retireAge = val; document.getElementById('qa-ret-val').textContent = val + '歳'; document.getElementById('retire-age').value = val; } save(); const {data, evList, fireAge} = calc(); chartData = data; chartDataEvList = evList; renderRCards(data, fireAge); renderHealthScore(data); renderFulfillmentBar(data); renderRetirementSummary(data); renderAdvice(data); let filtered = data; if(chartTab === 'pre') filtered = data.filter(d => d.age <= S.retireAge); if(chartTab === 'post') filtered = data.filter(d => d.age >= S.retireAge); if(filtered.length < 2) filtered = data; renderChart(filtered, evList); renderChartJs(filtered, evList); renderEvList(evList); renderAssetTable(data, evList); updateHero(); } catch(e) { console.error('onQA error:', e); } } function syncQAPanel() { try { const rr = document.getElementById('qa-rr'); const inv = document.getElementById('qa-inv'); const ret = document.getElementById('qa-ret'); if(rr) { rr.value = S.rrate; document.getElementById('qa-rr-val').textContent = S.rrate.toFixed(1) + '%'; } if(inv) { inv.value = S.invest; document.getElementById('qa-inv-val').textContent = S.invest + '万円'; } if(ret) { ret.value = S.retireAge; document.getElementById('qa-ret-val').textContent = S.retireAge + '歳'; } } catch(e) { console.error('syncQAPanel error:', e); } } // ===== 年収手取りヒント ===== function updateIncomeHint() { try { const hint = document.getElementById('income-hint'); if(!hint || !S.income) return; const netYearly = Math.round(S.income * (1 - effectiveRate(S.income))); const netMonthly = Math.round(netYearly / 12 * 10) / 10; hint.textContent = `手取り目安: 月約${netMonthly}万円(年${netYearly}万円)`; updateSavingsIndicator(netYearly); } catch(e) {} } // ===== 収支サマリーインジケーター(改善1) ===== function updateSavingsIndicator(netYearly) { try { const box = document.getElementById('savings-indicator'); if(!box || !S.income) return; const net = netYearly !== undefined ? netYearly : Math.round(S.income * (1 - effectiveRate(S.income))); const annExp = S.expenses * 12; const annInv = S.invest * 12; const surplus = net - annExp - annInv; const savingRate = net > 0 ? Math.round((annInv + Math.max(0, surplus)) / net * 1000) / 10 : 0; const surplusClr = surplus >= 0 ? 'var(--primary)' : 'var(--danger)'; const rateClr = savingRate >= 20 ? 'var(--primary)' : savingRate >= 10 ? 'var(--warn)' : 'var(--danger)'; const ratePct = Math.min(100, savingRate / 30 * 100); // 30%で最大バー document.getElementById('si-net').textContent = net.toLocaleString() + '万円'; document.getElementById('si-exp').textContent = annExp.toLocaleString() + '万円'; document.getElementById('si-inv').textContent = annInv.toLocaleString() + '万円'; const surpEl = document.getElementById('si-surplus'); surpEl.textContent = (surplus >= 0 ? '+' : '') + surplus.toLocaleString() + '万円'; surpEl.style.color = surplusClr; const rateLabel = document.getElementById('si-rate-label'); rateLabel.textContent = savingRate.toFixed(1) + '%'; rateLabel.style.color = rateClr; const fill = document.getElementById('si-rate-fill'); fill.style.width = ratePct + '%'; fill.style.background = rateClr; box.style.display = 'block'; } catch(e) {} } // ===== 住宅ローン月額計算機(改善4) ===== function calcLoanMonthly(principal, rateAnnual, years) { // 元利均等返済の月額計算 const r = rateAnnual / 100 / 12; const n = years * 12; if(r === 0) return principal / n; return principal * r * Math.pow(1 + r, n) / (Math.pow(1 + r, n) - 1); } function applyLoanCalc() { try { const principal = parseFloat(document.getElementById('loan-principal')?.value) || 3000; const rate = parseFloat(document.getElementById('loan-rate')?.value) || 1.0; const years = parseFloat(document.getElementById('loan-years')?.value) || 35; const monthly = calcLoanMonthly(principal, rate, years); const monthlyMan = Math.round(monthly * 10) / 10; const total = Math.round(monthly * years * 12 / 10000) * 10000; const result = document.getElementById('loan-calc-result'); if(result) result.innerHTML = `月額: ${monthlyMan.toFixed(1)}万円/月(総返済額: 約${Math.round(total/10000).toLocaleString()}万円)`; } catch(e) { console.error(e); } } function setLoanMonthly(val) { try { const el = document.getElementById('house-loan'); if(el) { el.value = val; S.ev.hloan = val; onInp(); } showToast('success', `住宅ローン月額を${val}万円に設定しました`); } catch(e) { console.error(e); } } // ===== プランの健全度スコア(改善5) ===== function calcHealthScore(data) { try { if(!data || !data.length) return null; // 老後充足度スコア(25点満点) const at90 = data[data.length-1].assets; const needed = S.retireExp * 12 * 30 * 10000; const pensionYears = Math.max(0, 90 - Math.max(S.retireAge, S.pensionStart ?? 65)); const retireAssets = (data.find(d=>d.age===S.retireAge)||data[0]).assets; const totalAvail = Math.max(0, retireAssets + S.pension * 12 * pensionYears * 10000); const fulfillPct = Math.min(1, totalAvail / (needed || 1)); const scoreA = Math.round(fulfillPct * 25); // 貯蓄率スコア(25点満点)- 現在の手取り年収ベース const netYearly = Math.round(S.income * (1 - effectiveRate(S.income))); const annInv = S.invest * 12; const annSurplus = netYearly - S.expenses * 12 - annInv; const savingRate = netYearly > 0 ? (annInv + Math.max(0, annSurplus)) / netYearly * 100 : 0; const scoreB = Math.min(25, Math.round(savingRate / 20 * 25)); // 運用分散スコア(25点満点)- 積立している・利回りが現実的 const hasInvest = S.invest > 0 ? 1 : 0; const rateOk = S.rrate >= 1 && S.rrate <= 7 ? 1 : 0; const earlyStart = S.age < 45 ? 1 : S.age < 55 ? 0.5 : 0; const scoreC = Math.round((hasInvest * 10 + rateOk * 8 + earlyStart * 7)); // 早期備えスコア(25点満点)- 退職前の準備期間 const prepYears = S.retireAge - S.age; const scoreD = prepYears >= 30 ? 25 : prepYears >= 20 ? 20 : prepYears >= 10 ? 13 : 5; const total = scoreA + scoreB + scoreC + scoreD; return {total, scoreA, scoreB, scoreC, scoreD}; } catch(e) { console.error(e); return null; } } function renderHealthScore(data) { try { const wrap = document.getElementById('health-score-wrap'); if(!wrap) return; const sc = calcHealthScore(data); if(!sc) { wrap.innerHTML = ''; return; } const {total, scoreA, scoreB, scoreC, scoreD} = sc; const clr = total >= 80 ? '#0d9488' : total >= 60 ? '#f59e0b' : '#ef4444'; const msg = total >= 80 ? '優良:このまま継続しましょう' : total >= 60 ? '良好:さらなる改善で安心感アップ' : '要改善:FPへの相談を検討しましょう'; const bgClr = total >= 80 ? 'rgba(13,148,136,.1)' : total >= 60 ? 'rgba(245,158,11,.1)' : 'rgba(239,68,68,.1)'; const bars = [ {label:'老後充足度', val:scoreA, max:25}, {label:'貯蓄率', val:scoreB, max:25}, {label:'資産運用', val:scoreC, max:25}, {label:'準備期間', val:scoreD, max:25}, ]; wrap.innerHTML = `
🏆 プランの健全度スコア
${total} / 100
${msg}
${bars.map(b=>`
${b.label}
${b.val}/${b.max}
`).join('')}
老後充足度・貯蓄率・資産運用・準備期間の4軸で評価
`; // バーアニメーション setTimeout(() => { bars.forEach(b => { const el = document.getElementById('hsbar-'+b.label); if(el) el.style.width = (b.val/b.max*100) + '%'; }); }, 80); } catch(e) { console.error('HealthScore error:', e); } } // ===== URLシェアダイアログ(改善3) ===== function buildShareUrl() { const params = new URLSearchParams({ age: S.age, inc: S.income, sav: S.savings, exp: S.expenses, rr: S.rrate, inv: S.invest, ig: S.incomeGrowth ?? 1, infl: S.inflRate ?? 1, ra: S.retireAge, pen: S.pension, re: S.retireExp, ls: S.lumpsum, ps: S.pensionStart ?? 65 }); const evf = `${S.ev.marry?'m1':'m0'}${S.ev.house?'h1':'h0'}${S.ev.child?'c1':'c0'}`; params.set('ev', evf); if(S.ev.marry) { params.set('ma', S.ev.mage); params.set('mc', S.ev.mcost); } if(S.ev.house) { params.set('ha', S.ev.hage); params.set('hd', S.ev.hdown); params.set('hl', S.ev.hloan); params.set('hy', S.ev.hyrs); } if(S.ev.child) { params.set('ca', S.ev.cage); params.set('nc', S.ev.nchild); params.set('edu', S.ev.edu); } if(S.ev.custom && S.ev.customs && S.ev.customs.length > 0) { try { params.set('ce', btoa(unescape(encodeURIComponent(JSON.stringify(S.ev.customs))))); } catch(ex) {} } return 'https://appadaycreator.com/lifeplan-simulator/?' + params.toString(); } function closeShareModal() { document.getElementById('share-modal')?.classList.add('hidden'); } function copyShareUrl() { try { const input = document.getElementById('share-url-input'); if(!input) return; input.select(); navigator.clipboard.writeText(input.value) .then(() => showToast('success', 'URLをクリップボードにコピーしました')) .catch(() => { document.execCommand('copy'); showToast('success', 'URLをコピーしました'); }); } catch(e) { console.error(e); } } function resetAll() { try { document.getElementById('confirm-modal').classList.remove('hidden'); } catch(e) { showToast('error', 'リセットに失敗しました'); console.error(e); } } function closeModal() { document.getElementById('confirm-modal').classList.add('hidden'); } function confirmReset() { closeModal(); S = JSON.parse(JSON.stringify(DEF)); save(); fillForm(); showToast('info', 'リセットしました'); } init();