Add fire planning mini app

This commit is contained in:
2025-10-11 09:48:22 +11:00
parent 5b07c7a82f
commit 8831f816d6
5 changed files with 1183 additions and 6 deletions
+198
View File
@@ -0,0 +1,198 @@
# FIRE Planning Calculator - Educational Enhancements
## Summary of Changes
This document describes the educational content and explanations added to the FIRE Planning mini app to help users understand the calculations and methodology.
---
## 1. Educational Info Section (Top of Page)
**Location:** Below the header, before the input form
**Content:**
- **Green highlighted box** with lightbulb icon
- Explanation of FIRE (Financial Independence, Retire Early) movement
- Detailed explanation of the **4% Safe Withdrawal Rate Rule**
- Formula breakdown: **FIRE Target = Annual Expenses × 25**
- Practical example: A$80,000/year expenses → A$2,000,000 target
- Trinity Study reference
**Purpose:** Educate users before they enter data
---
## 2. Calculation Methodology Box
**Location:** In results section, above summary cards
**Content:**
- **Blue highlighted box** explaining step-by-step calculations
- Shows formulas with **real-time calculated values**:
1. Annual Savings = Income × Savings Rate
2. FIRE Target (4% Rule) = Annual Expenses × 25
3. Yearly Growth = (Net Worth + Savings) × (1 + Return Rate)
4. Inflation-Adjusted Target = FIRE Target × (1 + Inflation)^years
5. Progress % = (Net Worth ÷ Adjusted Target) × 100%
**Purpose:** Transparency in how numbers are calculated
---
## 3. Enhanced Summary Cards
**Added to each card:**
- **Subtitle explanations** below the main value
- Context about what each number means
- Additional details like:
- "Based on 4% rule (25× expenses)"
- "Adjusted for X% annual inflation"
- "In Y years (target year)"
- Suggestions when FIRE not achieved
**Purpose:** Help users interpret each metric
---
## 4. Scenario Analysis Explanation
**Location:** Above scenario tabs
**Content:**
- Explanation of why different scenarios matter
- What each scenario represents:
- **Conservative (6%)**: Bonds/safer investments
- **Neutral (8%)**: Balanced portfolio
- **Optimistic (10%)**: Aggressive stock-heavy portfolio
- Note about historical market variance
**Purpose:** Help users understand risk and return trade-offs
---
## 5. Chart Descriptions
**Added before each chart:**
### Net Worth Growth Chart
- Explains green line (projected net worth with compound growth)
- Explains red dashed line (inflation-adjusted FIRE target)
- Clear indication of what "crossing" means (FIRE achieved!)
### Progress Chart
- Explains the percentage calculation
- Notes that 100% = FIRE achieved
### Projection Table
- Explains what each column represents
- Notes that green rows = FIRE achieved years
**Purpose:** Make visualizations self-explanatory
---
## 6. FAQ Section (Bottom of Page)
**Location:** Below all results and charts
**6 Comprehensive FAQs:**
### Q1: Why multiply annual expenses by 25?
- Explains 4% rule mathematics
- Provides concrete example
- Shows the inverse relationship (1 ÷ 0.04 = 25)
### Q2: What's a realistic return rate?
- Historical S&P 500 returns (~10% nominal)
- Real returns after inflation (7-8%)
- Different investor profiles (conservative/balanced/aggressive)
- Recommended ranges (6-10%)
### Q3: How does compound growth work?
- Formula: New Net Worth = (Previous + Savings) × (1 + Return)
- Step-by-step numerical example
- Explains exponential growth concept
### Q4: Why does FIRE target increase over time?
- Inflation explanation
- Purchasing power concept
- Formula: Future = Current × (1 + Inflation)^years
- Example: A$100k → A$145k in 15 years at 2.5% inflation
### Q5: Should I include my home in net worth?
- Distinction between total net worth and investable assets
- Why primary residence doesn't generate FIRE cash flow
- Recommendation to track both separately
### Q6: Important Assumptions & Limitations
- **Bulleted list of caveats:**
- Constant savings/income assumption
- Taxes and healthcare not included
- Market volatility considerations
- Superannuation/401k exclusion
- Disclaimer: Planning tool, not financial advice
**Purpose:** Address common questions and provide education
---
## Visual Design
All educational sections use:
- **Color-coded backgrounds:**
- Green gradient for FIRE/4% rule intro
- Blue for calculation methodology
- Yellow/cream for FAQ section
- White cards for individual FAQs
- **Icons** for visual interest and quick recognition
- **Proper hierarchy** with headings and spacing
- **Readable font sizes** (text-sm for detailed content)
- **Responsive design** that works on mobile
---
## User Journey
1. **Learn first** - Educational intro sets context
2. **Input data** - Clear help text on each field
3. **Calculate** - See methodology explanation
4. **Understand results** - Enhanced summary cards with context
5. **Visualize** - Charts with descriptions
6. **Deep dive** - Detailed table
7. **Learn more** - FAQ section for deeper understanding
---
## Key Formulas Highlighted
1. **FIRE Target** = Annual Expenses × 25
2. **Annual Savings** = Income × Savings Rate
3. **Compound Growth** = (Net Worth + Savings) × (1 + Return Rate)
4. **Inflation Adjustment** = Target × (1 + Inflation Rate)^years
5. **Progress** = (Net Worth ÷ Adjusted Target) × 100%
---
## Benefits
**Transparency** - Users understand where numbers come from
**Education** - Learn FIRE principles while planning
**Confidence** - Make informed decisions with full context
**Trust** - Clear methodology builds credibility
**Self-service** - FAQ answers common questions without support
---
## Future Enhancements (Optional)
- Interactive tooltips on hover for input fields
- Video tutorials or animated explanations
- Links to Trinity Study and FIRE movement resources
- Glossary of financial terms
- Share/export functionality with explanations included
- Multi-language support for international users
---
**Last Updated:** October 11, 2025
**Version:** 1.1 (Educational Enhancement Release)
+2 -5
View File
@@ -101,11 +101,8 @@ Blog-like content management:
- Tag categorization
- Publication status tracking
#### IPTVCollection & IPTVChannel
IPTV playlist management:
- M3U playlist parsing
- Channel metadata
- Category organization
#### mini_apps
A collection of mini apps which can be located at mini_apps_views.py
#### Analytics Models
- **ClickLog**: Individual click tracking with timestamps
+19 -1
View File
@@ -25,7 +25,16 @@ class MiniAppsListView(TemplateView):
'thumbnail': 'https://images.unsplash.com/photo-1589254065878-42c9da997008?w=400&h=300&fit=crop',
'icon': 'fas fa-volume-up',
'color': '#8e44ad'
},{
},
{
'name': 'FIRE Planning',
'description': 'Plan your Financial Independence Retire Early (FIRE) journey with interactive calculators, projections, and visualizations.',
'url': 'mini-apps-fire-planning',
'thumbnail': 'https://images.unsplash.com/photo-1579621970563-ebec7560ff3e?w=400&h=300&fit=crop',
'icon': 'fas fa-chart-line',
'color': '#27ae60'
},
{
'name': 'Weather Dashboard',
'description': 'Real-time weather information with beautiful visualizations and forecasts.',
'url': '#',
@@ -55,3 +64,12 @@ class TTSView(TemplateView):
context = super().get_context_data(**kwargs)
context['page_title'] = 'Text-to-Speech'
return context
class FIREPlanningView(TemplateView):
template_name = 'links/mini_apps/fire_planning.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = 'FIRE Planning Calculator'
return context
@@ -0,0 +1,963 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "FIRE Planning Calculator" %} - {% trans "Mini Apps" %}{% endblock %}
{% block extra_css %}
<style>
.fire-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.fire-header {
text-align: center;
margin-bottom: 3rem;
}
.fire-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.fire-subtitle {
color: #6b7280;
font-size: 1.125rem;
}
.input-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 2rem;
margin-bottom: 2rem;
}
.input-group {
margin-bottom: 1.5rem;
}
.input-label {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.input-field {
width: 100%;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s ease;
}
.input-field:focus {
outline: none;
border-color: #27ae60;
box-shadow: 0 0 0 3px rgba(39, 174, 96, 0.1);
}
.btn-calculate {
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
color: white;
padding: 1rem 2rem;
border-radius: 8px;
font-weight: 600;
border: none;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
width: 100%;
font-size: 1rem;
}
.btn-calculate:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(39, 174, 96, 0.3);
}
.btn-reset {
background: #6b7280;
color: white;
padding: 1rem 2rem;
border-radius: 8px;
font-weight: 600;
border: none;
cursor: pointer;
transition: background 0.2s ease;
width: 100%;
font-size: 1rem;
margin-top: 0.5rem;
}
.btn-reset:hover {
background: #4b5563;
}
.results-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 2rem;
margin-bottom: 2rem;
display: none;
}
.results-card.show {
display: block;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.summary-card {
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
padding: 1.5rem;
border-radius: 10px;
text-align: center;
}
.summary-card.success {
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
}
.summary-card.warning {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
}
.summary-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.summary-value {
font-size: 1.75rem;
font-weight: 700;
color: #1f2937;
}
.chart-container {
margin-bottom: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
.chart-title {
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: #f3f4f6;
padding: 1rem;
text-align: left;
font-weight: 600;
color: #374151;
border-bottom: 2px solid #e5e7eb;
}
td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e5e7eb;
}
tr:hover {
background: #f9fafb;
}
.fire-achieved {
background: #d1fae5 !important;
}
.input-help {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
}
.scenario-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.scenario-tab {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 2px solid #e5e7eb;
background: white;
cursor: pointer;
font-weight: 600;
transition: all 0.2s ease;
}
.scenario-tab.active {
background: #27ae60;
color: white;
border-color: #27ae60;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 0.5rem;
color: #27ae60;
}
</style>
{% endblock %}
{% block content %}
<div class="fire-container">
<!-- Header -->
<div class="fire-header">
<h1 class="fire-title">
<i class="fas fa-chart-line"></i>
FIRE Planning Calculator
</h1>
<p class="fire-subtitle">
Plan Your Financial Independence and Early Retirement Journey
</p>
</div>
<!-- Educational Info Section -->
<div class="input-card mb-6" style="background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);">
<div class="flex items-start">
<div class="flex-shrink-0 mr-4">
<i class="fas fa-lightbulb text-4xl text-green-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-gray-800 mb-3">
<i class="fas fa-graduation-cap mr-2"></i>
Understanding FIRE & The 4% Rule
</h2>
<div class="text-gray-700 space-y-2 text-sm leading-relaxed">
<p>
<strong>FIRE (Financial Independence, Retire Early)</strong> is a movement focused on achieving financial freedom through aggressive saving and smart investing.
</p>
<p>
<strong>The 4% Rule:</strong> Based on the Trinity Study, this rule suggests you can safely withdraw 4% of your investment portfolio annually in retirement, adjusted for inflation, with a high probability your money will last 30+ years.
</p>
<p>
<strong>Your FIRE Target = Annual Expenses × 25</strong> (because 1 ÷ 0.04 = 25)
</p>
<p class="pt-2 border-t border-green-300 mt-3">
<i class="fas fa-info-circle mr-1"></i>
<strong>Example:</strong> If you need A$80,000/year to live comfortably, your FIRE target is A$2,000,000.
At 4% withdrawal, A$2M × 4% = A$80,000 per year.
</p>
</div>
</div>
</div>
</div>
<!-- Input Form -->
<div class="input-card">
<h2 class="section-title">
<i class="fas fa-edit"></i>
Your Financial Details
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="input-group">
<label class="input-label" for="currentAge">Current Age</label>
<input type="number" id="currentAge" class="input-field" value="35" min="18" max="100">
<div class="input-help">Your current age in years</div>
</div>
<div class="input-group">
<label class="input-label" for="targetAge">Target FIRE Age</label>
<input type="number" id="targetAge" class="input-field" value="50" min="18" max="100">
<div class="input-help">Age you want to achieve financial independence</div>
</div>
<div class="input-group">
<label class="input-label" for="currentNetWorth">Current Net Worth (excl. home) - AUD</label>
<input type="number" id="currentNetWorth" class="input-field" value="300000" step="1000">
<div class="input-help">Investable assets excluding primary residence</div>
</div>
<div class="input-group">
<label class="input-label" for="totalNetWorth">Total Net Worth (incl. home) - AUD</label>
<input type="number" id="totalNetWorth" class="input-field" value="900000" step="1000">
<div class="input-help">Total net worth including property</div>
</div>
<div class="input-group">
<label class="input-label" for="annualExpenses">Annual Expenses (post-retirement) - AUD</label>
<input type="number" id="annualExpenses" class="input-field" value="200000" step="1000">
<div class="input-help">Expected yearly spending after FIRE</div>
</div>
<div class="input-group">
<label class="input-label" for="annualIncome">Annual Income (including RSU) - AUD</label>
<input type="number" id="annualIncome" class="input-field" value="300000" step="1000">
<div class="input-help">Total yearly income before tax</div>
</div>
<div class="input-group">
<label class="input-label" for="savingsRate">Savings Rate (%)</label>
<input type="number" id="savingsRate" class="input-field" value="40" min="0" max="100" step="1">
<div class="input-help">Percentage of income saved annually</div>
</div>
<div class="input-group">
<label class="input-label" for="returnRate">Expected Investment Return (%)</label>
<input type="number" id="returnRate" class="input-field" value="8" min="0" max="30" step="0.1">
<div class="input-help">Average annual investment return rate</div>
</div>
<div class="input-group">
<label class="input-label" for="inflationRate">Inflation Rate (%)</label>
<input type="number" id="inflationRate" class="input-field" value="2.5" min="0" max="20" step="0.1">
<div class="input-help">Expected average annual inflation</div>
</div>
</div>
<div class="mt-6">
<button class="btn-calculate" onclick="calculateFIRE()">
<i class="fas fa-calculator mr-2"></i>
Calculate FIRE Plan
</button>
<button class="btn-reset" onclick="resetForm()">
<i class="fas fa-redo mr-2"></i>
Reset to Defaults
</button>
</div>
</div>
<!-- Results Summary -->
<div id="resultsSection" class="results-card">
<h2 class="section-title">
<i class="fas fa-trophy"></i>
Your FIRE Journey Summary
</h2>
<div class="summary-grid" id="summaryCards">
<!-- Summary cards will be inserted here by JavaScript -->
</div>
<!-- Calculation Methodology Explanation -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6 rounded">
<h4 class="font-semibold text-blue-900 mb-2 flex items-center">
<i class="fas fa-calculator mr-2"></i>
How We Calculate Your FIRE Journey
</h4>
<div class="text-sm text-blue-800 space-y-1">
<p><strong>1. Annual Savings:</strong> Income × Savings Rate = <span id="calcAnnualSavings" class="font-mono bg-blue-100 px-1 rounded">...</span></p>
<p><strong>2. FIRE Target (4% Rule):</strong> Annual Expenses × 25 = <span id="calcFireTarget" class="font-mono bg-blue-100 px-1 rounded">...</span></p>
<p><strong>3. Yearly Growth:</strong> (Net Worth + Annual Savings) × (1 + Return Rate)</p>
<p><strong>4. Inflation-Adjusted Target:</strong> FIRE Target × (1 + Inflation)<sup>years</sup></p>
<p><strong>5. Progress:</strong> (Current Net Worth ÷ Inflation-Adjusted Target) × 100%</p>
<p class="pt-2 text-xs italic">
<i class="fas fa-info-circle mr-1"></i>
FIRE is achieved when your net worth reaches or exceeds your inflation-adjusted target (≥100% progress)
</p>
</div>
</div>
<!-- Scenario Comparison Tabs -->
<div class="mb-4">
<h3 class="text-lg font-semibold mb-2">Scenario Analysis</h3>
<p class="text-sm text-gray-600 mb-3">
<i class="fas fa-chart-line mr-1"></i>
Compare different return rate scenarios. Historical stock market returns vary, so it's wise to plan for multiple outcomes.
<strong>Conservative (6%)</strong> = bonds/safer investments,
<strong>Neutral (8%)</strong> = balanced portfolio,
<strong>Optimistic (10%)</strong> = aggressive stock-heavy portfolio.
</p>
<div class="scenario-tabs">
<button class="scenario-tab active" onclick="switchScenario('neutral')">
Neutral (8%)
</button>
<button class="scenario-tab" onclick="switchScenario('conservative')">
Conservative (6%)
</button>
<button class="scenario-tab" onclick="switchScenario('optimistic')">
Optimistic (10%)
</button>
</div>
</div>
<!-- Net Worth Growth Chart -->
<div class="chart-container">
<h3 class="chart-title">
<i class="fas fa-chart-area mr-2"></i>
Net Worth Projection vs FIRE Target
</h3>
<p class="text-sm text-gray-600 mb-4">
<strong>Green line:</strong> Your projected net worth growth (investments + annual savings × compound returns).
<strong>Red dashed line:</strong> FIRE target adjusted for inflation.
When green crosses above red, you've achieved FIRE! 🎉
</p>
<canvas id="netWorthChart"></canvas>
</div>
<!-- Progress Chart -->
<div class="chart-container">
<h3 class="chart-title">
<i class="fas fa-percentage mr-2"></i>
FIRE Progress Over Time
</h3>
<p class="text-sm text-gray-600 mb-4">
Your FIRE progress as a percentage. 100% means you've reached your target!
This shows: (Current Net Worth ÷ Inflation-Adjusted FIRE Target) × 100%
</p>
<canvas id="progressChart"></canvas>
</div>
<!-- Detailed Table -->
<div class="chart-container">
<h3 class="chart-title">
<i class="fas fa-table mr-2"></i>
Year-by-Year Projection
</h3>
<p class="text-sm text-gray-600 mb-4">
Detailed breakdown of your FIRE journey.
<span class="bg-green-100 px-2 py-1 rounded text-xs font-semibold">Green rows</span> indicate years where you've achieved FIRE (≥100% progress).
</p>
<div class="overflow-x-auto">
<table id="projectionTable">
<thead>
<tr>
<th>Age</th>
<th>Year</th>
<th>Net Worth</th>
<th>FIRE Target</th>
<th>Progress</th>
<th>Annual Savings</th>
</tr>
</thead>
<tbody id="projectionTableBody">
<!-- Table rows will be inserted here by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Educational FAQ Section -->
<div class="input-card" style="background: linear-gradient(135deg, #fff9e6 0%, #fff3cd 100%);">
<h2 class="section-title">
<i class="fas fa-question-circle"></i>
Frequently Asked Questions
</h2>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 shadow-sm">
<h3 class="font-bold text-gray-800 mb-2 flex items-center">
<i class="fas fa-calculator text-yellow-600 mr-2"></i>
Why multiply annual expenses by 25?
</h3>
<p class="text-sm text-gray-700">
The 4% rule (based on historical market data) suggests you can safely withdraw 4% of your portfolio annually.
Since 1 ÷ 0.04 = 25, you need 25 times your annual expenses. For example: need A$60,000/year → target A$1,500,000 portfolio.
</p>
</div>
<div class="bg-white rounded-lg p-4 shadow-sm">
<h3 class="font-bold text-gray-800 mb-2 flex items-center">
<i class="fas fa-chart-line text-green-600 mr-2"></i>
What's a realistic return rate?
</h3>
<p class="text-sm text-gray-700">
Historical stock market returns (S&P 500) average ~10% annually before inflation. After inflation (~2-3%), real returns are ~7-8%.
Conservative investors might use 6%, balanced 8%, aggressive 10%. Diversified portfolios typically aim for 7-9%.
</p>
</div>
<div class="bg-white rounded-lg p-4 shadow-sm">
<h3 class="font-bold text-gray-800 mb-2 flex items-center">
<i class="fas fa-piggy-bank text-blue-600 mr-2"></i>
How does compound growth work?
</h3>
<p class="text-sm text-gray-700">
Each year: <strong>New Net Worth = (Previous Net Worth + Annual Savings) × (1 + Return Rate)</strong>.
Example: Start with A$100,000, save A$30,000/year at 8% return → Year 1: (A$100k + A$30k) × 1.08 = A$140,400.
This compounds over time, creating exponential growth!
</p>
</div>
<div class="bg-white rounded-lg p-4 shadow-sm">
<h3 class="font-bold text-gray-800 mb-2 flex items-center">
<i class="fas fa-fire text-red-600 mr-2"></i>
Why does the FIRE target increase over time?
</h3>
<p class="text-sm text-gray-700">
Inflation! A$100,000 today won't have the same purchasing power in 15 years. At 2.5% inflation, A$100,000 becomes ~A$145,000 in 15 years.
The calculator adjusts your target using: <strong>Future Target = Current Target × (1 + Inflation Rate)<sup>years</sup></strong>
</p>
</div>
<div class="bg-white rounded-lg p-4 shadow-sm">
<h3 class="font-bold text-gray-800 mb-2 flex items-center">
<i class="fas fa-home text-purple-600 mr-2"></i>
Should I include my home in net worth?
</h3>
<p class="text-sm text-gray-700">
For FIRE calculations, use "Net Worth excl. home" - only investable assets that generate income.
Your primary residence doesn't produce cash flow for living expenses (though it does have value and reduces housing costs).
Track total net worth separately for overall wealth monitoring.
</p>
</div>
<div class="bg-white rounded-lg p-4 shadow-sm">
<h3 class="font-bold text-gray-800 mb-2 flex items-center">
<i class="fas fa-exclamation-triangle text-orange-600 mr-2"></i>
Important Assumptions & Limitations
</h3>
<ul class="text-sm text-gray-700 space-y-1 ml-5 list-disc">
<li>Assumes constant savings rate and income (real life varies!)</li>
<li>Doesn't account for taxes, healthcare costs, or unexpected expenses</li>
<li>Market returns fluctuate - use scenario analysis for different outcomes</li>
<li>Superannuation/401k considerations not included (consult financial advisor)</li>
<li>This is a planning tool, not financial advice - do your own research!</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Chart.js Library -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
let netWorthChart = null;
let progressChart = null;
let currentScenario = 'neutral';
let calculationData = {};
// Format currency in AUD
function formatCurrency(value) {
return new Intl.NumberFormat('en-AU', {
style: 'currency',
currency: 'AUD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value);
}
// Format percentage
function formatPercentage(value) {
return value.toFixed(1) + '%';
}
// Get input values
function getInputs() {
return {
currentAge: parseInt(document.getElementById('currentAge').value),
targetAge: parseInt(document.getElementById('targetAge').value),
currentNetWorth: parseFloat(document.getElementById('currentNetWorth').value),
totalNetWorth: parseFloat(document.getElementById('totalNetWorth').value),
annualExpenses: parseFloat(document.getElementById('annualExpenses').value),
annualIncome: parseFloat(document.getElementById('annualIncome').value),
savingsRate: parseFloat(document.getElementById('savingsRate').value) / 100,
returnRate: parseFloat(document.getElementById('returnRate').value) / 100,
inflationRate: parseFloat(document.getElementById('inflationRate').value) / 100
};
}
// Calculate FIRE projections
function calculateProjections(inputs, returnRate) {
const years = inputs.targetAge - inputs.currentAge;
const projections = [];
let netWorth = inputs.currentNetWorth;
let fireTarget = inputs.annualExpenses * 25; // 4% rule
const annualSavings = inputs.annualIncome * inputs.savingsRate;
const currentYear = new Date().getFullYear();
for (let i = 0; i <= years; i++) {
const age = inputs.currentAge + i;
const year = currentYear + i;
// Adjust FIRE target for inflation
const adjustedFireTarget = fireTarget * Math.pow(1 + inputs.inflationRate, i);
// Calculate progress
const progress = (netWorth / adjustedFireTarget) * 100;
projections.push({
age: age,
year: year,
netWorth: netWorth,
fireTarget: adjustedFireTarget,
progress: progress,
annualSavings: annualSavings
});
// Grow net worth for next year
if (i < years) {
netWorth = (netWorth + annualSavings) * (1 + returnRate);
}
}
return projections;
}
// Find FIRE achievement year
function findFIREYear(projections) {
for (let i = 0; i < projections.length; i++) {
if (projections[i].progress >= 100) {
return projections[i];
}
}
return null;
}
// Calculate FIRE
function calculateFIRE() {
const inputs = getInputs();
// Validate inputs
if (inputs.targetAge <= inputs.currentAge) {
alert('Target FIRE Age must be greater than Current Age');
return;
}
// Calculate for all scenarios
calculationData = {
neutral: calculateProjections(inputs, inputs.returnRate),
conservative: calculateProjections(inputs, 0.06),
optimistic: calculateProjections(inputs, 0.10),
inputs: inputs
};
// Show results
document.getElementById('resultsSection').classList.add('show');
// Update summary
updateSummary(calculationData.neutral, inputs);
// Update charts
updateCharts(currentScenario);
// Update table
updateTable(calculationData.neutral);
// Scroll to results
document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' });
}
// Update summary cards
function updateSummary(projections, inputs) {
const fireYear = findFIREYear(projections);
const finalProjection = projections[projections.length - 1];
const initialFireTarget = inputs.annualExpenses * 25;
const annualSavings = inputs.annualIncome * inputs.savingsRate;
// Update calculation explanations
document.getElementById('calcAnnualSavings').textContent =
`${formatCurrency(inputs.annualIncome)} × ${formatPercentage(inputs.savingsRate * 100)} = ${formatCurrency(annualSavings)}`;
document.getElementById('calcFireTarget').textContent =
`${formatCurrency(inputs.annualExpenses)} × 25 = ${formatCurrency(initialFireTarget)}`;
let summaryHTML = `
<div class="summary-card">
<div class="summary-label">FIRE Target (Today)</div>
<div class="summary-value">${formatCurrency(initialFireTarget)}</div>
<div class="text-xs text-gray-600 mt-1">Based on 4% rule (25× expenses)</div>
</div>
<div class="summary-card">
<div class="summary-label">FIRE Target (At Age ${inputs.targetAge})</div>
<div class="summary-value">${formatCurrency(finalProjection.fireTarget)}</div>
<div class="text-xs text-gray-600 mt-1">Adjusted for ${formatPercentage(inputs.inflationRate * 100)} annual inflation</div>
</div>
`;
if (fireYear) {
summaryHTML += `
<div class="summary-card success">
<div class="summary-label">🎉 FIRE Achieved at Age</div>
<div class="summary-value">${fireYear.age}</div>
<div class="text-xs text-gray-600 mt-1">In ${fireYear.age - inputs.currentAge} years (${fireYear.year})</div>
</div>
<div class="summary-card success">
<div class="summary-label">Net Worth at FIRE</div>
<div class="summary-value">${formatCurrency(fireYear.netWorth)}</div>
<div class="text-xs text-gray-600 mt-1">${formatPercentage(fireYear.progress)} of target reached</div>
</div>
`;
} else {
summaryHTML += `
<div class="summary-card warning">
<div class="summary-label">⚠️ FIRE Not Achieved by Age ${inputs.targetAge}</div>
<div class="summary-value">${formatPercentage(finalProjection.progress)}</div>
<div class="text-xs text-gray-600 mt-1">Consider: higher savings rate or later retirement age</div>
</div>
<div class="summary-card">
<div class="summary-label">Projected Net Worth</div>
<div class="summary-value">${formatCurrency(finalProjection.netWorth)}</div>
<div class="text-xs text-gray-600 mt-1">At age ${inputs.targetAge}</div>
</div>
`;
}
summaryHTML += `
<div class="summary-card">
<div class="summary-label">Annual Savings</div>
<div class="summary-value">${formatCurrency(annualSavings)}</div>
<div class="text-xs text-gray-600 mt-1">${formatPercentage(inputs.savingsRate * 100)} of ${formatCurrency(inputs.annualIncome)} income</div>
</div>
<div class="summary-card">
<div class="summary-label">Years to FIRE</div>
<div class="summary-value">${fireYear ? (fireYear.age - inputs.currentAge) : 'N/A'}</div>
<div class="text-xs text-gray-600 mt-1">${fireYear ? `Target year: ${fireYear.year}` : 'Extend timeline or adjust inputs'}</div>
</div>
`;
document.getElementById('summaryCards').innerHTML = summaryHTML;
}
// Update charts
function updateCharts(scenario) {
const projections = calculationData[scenario];
const ages = projections.map(p => p.age);
const netWorths = projections.map(p => p.netWorth);
const fireTargets = projections.map(p => p.fireTarget);
const progress = projections.map(p => Math.min(p.progress, 150)); // Cap at 150% for chart
// Net Worth Chart
if (netWorthChart) {
netWorthChart.destroy();
}
const ctx1 = document.getElementById('netWorthChart').getContext('2d');
netWorthChart = new Chart(ctx1, {
type: 'line',
data: {
labels: ages,
datasets: [{
label: 'Projected Net Worth',
data: netWorths,
borderColor: '#27ae60',
backgroundColor: 'rgba(39, 174, 96, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4
}, {
label: 'FIRE Target',
data: fireTargets,
borderColor: '#e74c3c',
backgroundColor: 'rgba(231, 76, 60, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4,
borderDash: [5, 5]
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
callbacks: {
label: function(context) {
return context.dataset.label + ': ' + formatCurrency(context.parsed.y);
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return formatCurrency(value);
}
}
},
x: {
title: {
display: true,
text: 'Age'
}
}
}
}
});
// Progress Chart
if (progressChart) {
progressChart.destroy();
}
const ctx2 = document.getElementById('progressChart').getContext('2d');
progressChart = new Chart(ctx2, {
type: 'line',
data: {
labels: ages,
datasets: [{
label: 'FIRE Progress (%)',
data: progress,
borderColor: '#3498db',
backgroundColor: 'rgba(52, 152, 219, 0.2)',
borderWidth: 3,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
callbacks: {
label: function(context) {
return 'Progress: ' + formatPercentage(context.parsed.y);
}
}
},
annotation: {
annotations: {
line1: {
type: 'line',
yMin: 100,
yMax: 100,
borderColor: '#27ae60',
borderWidth: 2,
borderDash: [10, 5],
label: {
content: 'FIRE Achieved',
enabled: true,
position: 'end'
}
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 150,
ticks: {
callback: function(value) {
return value + '%';
}
}
},
x: {
title: {
display: true,
text: 'Age'
}
}
}
}
});
}
// Update projection table
function updateTable(projections) {
let tableHTML = '';
projections.forEach((proj, index) => {
const rowClass = proj.progress >= 100 ? 'fire-achieved' : '';
tableHTML += `
<tr class="${rowClass}">
<td>${proj.age}</td>
<td>${proj.year}</td>
<td>${formatCurrency(proj.netWorth)}</td>
<td>${formatCurrency(proj.fireTarget)}</td>
<td>${formatPercentage(proj.progress)}</td>
<td>${formatCurrency(proj.annualSavings)}</td>
</tr>
`;
});
document.getElementById('projectionTableBody').innerHTML = tableHTML;
}
// Switch scenario
function switchScenario(scenario) {
currentScenario = scenario;
// Update active tab
document.querySelectorAll('.scenario-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update charts with new scenario
updateCharts(scenario);
// Update table with new scenario
updateTable(calculationData[scenario]);
// Update summary
updateSummary(calculationData[scenario], calculationData.inputs);
}
// Reset form
function resetForm() {
document.getElementById('currentAge').value = 35;
document.getElementById('targetAge').value = 50;
document.getElementById('currentNetWorth').value = 300000;
document.getElementById('totalNetWorth').value = 900000;
document.getElementById('annualExpenses').value = 200000;
document.getElementById('annualIncome').value = 300000;
document.getElementById('savingsRate').value = 40;
document.getElementById('returnRate').value = 8;
document.getElementById('inflationRate').value = 2.5;
document.getElementById('resultsSection').classList.remove('show');
}
</script>
{% endblock %}
+1
View File
@@ -51,6 +51,7 @@ urlpatterns = [
path('ui/mini-apps/', mini_apps_views.MiniAppsListView.as_view(), name='mini-apps-list'),
path('ui/mini-apps/image-gallery/', mini_apps_views.ImageGalleryView.as_view(), name='mini-apps-image-gallery'),
path('ui/mini-apps/tts/', mini_apps_views.TTSView.as_view(), name='mini-apps-tts'),
path('ui/mini-apps/fire-planning/', mini_apps_views.FIREPlanningView.as_view(), name='mini-apps-fire-planning'),
# TTS API
path('ui/tts-api/', views.generate_tts_api, name='tts-api'),