Building Interactive Tools in R: An Intro to Shiny Apps
1 Introduction
This tutorial was developed for students at Trine University as part of a growing effort to equip learners with modern, real-world data skills. Whether you’re majoring in business, engineering, computer science, or health sciences, learning to use tools like R and Shiny can open doors to careers in data analysis, UX design, financial modeling, and more. This resource is designed to be approachable, hands-on, and immediately useful — no advanced programming background required.
By walking through the creation of a Retirement Contribution Analysis app, students gain exposure to real-world challenges in financial planning, data visualization, and interactive dashboard development. The goal is not just to teach R, but to show how it can be used to solve meaningful problems, simulate scenarios, and communicate insights clearly. This project can also be used as a portfolio piece or capstone example for students preparing for internships or entry into the data-driven workforce.
1.1 Case Study: Client Retirement Profiles
In this project, the goal is to model retirement scenarios for distinct clients, each with unique financial, demographic, and lifestyle characteristics. The purpose of this simulation is to analyze how different variables — such as contribution rates, healthcare costs, inflation, investment returns, and retirement age — impact long-term financial sustainability. This kind of scenario-based learning is widely supported in business analytics education (Evans, 2020).
Here are a few brief client examples:
- Client A: $250,000 in current savings, contributes 5% + 5%, modest salary growth, low health risk, moderate lifestyle.
- Client B: $100,000 saved, contributes 10% + 6%, high salary but delayed retirement at 70, high expenses.
- Client C: Self-employed with no employer match, contributes 15%, starts late with only $50,000 saved.
- Client D: Strong investor with a 6% return, contributes only 4%, lives frugally post-retirement.
- Client E: High-income earner ($200k+), but late saver. Aggressively invests and expects early retirement at 60.
- Client F: Minimal contributions but expects inheritance and low living costs.
- Client G: Focuses on Roth accounts, assumes tax-free withdrawals, low inflation environment.
- Client H: High healthcare costs, conservative investor, and long life expectancy due to healthy lifestyle.
The diversity in these profiles makes them perfect for teaching how small parameter changes can drastically affect long-term outcomes.
1.2 Backround
This tutorial assumes some basic familiarity with R, but no prior experience with Shiny is required. All code is provided, and each section builds incrementally toward a fully functioning app.
📌 Note: This project can serve as an example of a capstone or portfolio piece for students studying Data Science, UX Design, Financial Analytics, or Applied Statistics.
1.2.1 CC License Info
1.3 R & RStudio
R is freely distributed online and serves as the computational engine behind this app (R Core Team, 2024), it can be downloaded from: http://cran.r-project.org/. At the top of the page – under the heading “Download and Install R” – there are also download links for Windows users, Mac users, and Linux users. After you have installed R, download and install RStudio from: http://rstudio.org. RStudio provides a convenient interface for using R interactively under any platform and is also freely available.(Posit, 2024)
1.4 Introduction to Shiny
Shiny is an R package from RStudio that makes it easy to build interactive web applications (Chang et al., 2023). With Shiny, you can turn statistical analyses, data visualizations, and reports into dynamic, user-friendly dashboards—no HTML, CSS, or JavaScript required.
Shiny bridges the gap between traditional data analysis and real-world communication. Instead of sharing static charts or PDF reports, you can create applications that let users explore data, change inputs, and see results update instantly.
Whether you’re studying statistics, finance, public health, education, or business, Shiny equips you with tools to translate R skills into powerful, interactive tools used in industry and academia.(Chang, Cheng, Allaire, Xie, & McPherson, 2023)
1.5 What is Shiny?
Shiny is a powerful R package developed by Posit (formerly RStudio) that allows you to turn R code into interactive web applications — without needing to learn JavaScript, HTML, or CSS.
With just R and a bit of practice, you can create dashboards, data visualizations, simulations, and tools that anyone can use in their browser. It’s a perfect way to bridge data analysis and user experience, allowing people who don’t code to explore data insights dynamically.
1.6 Key Concepts in Shiny
Shiny apps have two main components:
1.6.1 1. UI (User Interface)
This is what the user sees — sliders, buttons, inputs, and output plots or tables. It’s created using layout functions like fluidPage() and sidebarLayout().
- Server The server contains the logic. It listens for changes in the UI and updates the output accordingly. This is where the math goes!
- Reactive Programming Shiny uses reactive programming — a way of building apps that update automatically whenever the user changes something.
Key parts:
input$: Holds UI values
output$: Holds results that get shown
renderPlot(), renderText(): Create output
reactive(): Stores reactive values
observeEvent(): Runs code when a specific input changes (like a button click)
- Running the App To launch the app, wrap everything in shinyApp():
🛠️ Summary Shiny helps you turn R projects into live web apps
It’s perfect for students, researchers, and analysts
You don’t need to learn HTML or JavaScript
You build apps using just R — through ui, server, and shinyApp()
This skill opens doors for internships, job interviews, data storytelling, and more
1.7 Why Use Shiny for This?
🗣️ Why Build an App When a simple R script will Work? Yes — it’s absolutely true that with a well-written R script, we can calculate and visualize retirement outcomes for each of the clients in one go. So why build an app? Because static code is rarely the end goal. Insight usually is. With a static script, you’re locked into one scenario per client. Any change — a retirement age, contribution rate, inflation assumption — means editing code, rerunning it, and re-generating plots. Building an app allows users to run scenarios instantly — a core part of data-driven decision-making (Evans, 2020).
The app lets users: Instantly update parameters See changes reflected in real time Compare different “what-if” scenarios without touching a line of code
Our target audience will always include professionals who don’t know R — and shouldn’t have to.
With the Shiny app:End Users just open a browser, They adjust sliders, click buttons, and get insights No R knowledge required — the interface becomes the communicator. Building an app allows users to run scenarios instantly — a core part of data-driven decision-making (Evans, 2020).
An app isn’t just a calculator — it’s a communication tool.It turns abstract financial models into interactive dashboards people can explore. The app tells a story. The script just does math. The base script solves one task — but the app grows with us.
We can (and did):Add PDF/CSV export Build a FIRE simulator Model taxes and healthcare Add voice narration and dark mode, This is no longer just a simple fanatical calculator — it’s a full retirement planning simulation platform.
Shiny apps transforms R code into insight, usability, and communication. It makes our work: Accessible, Interactive, Real-world ready. We’re not just coding for answers. We’re building tools for people.
LETS GET STARTED!
2 R: Installing packages and libraries
To install the R packages and libraries necessary to follow along with this text in RStudio, open a RStudio session and paste in the console
2.1 Step 1: Install Required Packages
2.2 Step 2: App File Structure
Suggested project layout:
📂 RetirementApp/
├── app.R # Main Shiny application
└── retirement_app_guide.qmd # This tutorial
2.3 Step 3: Creating the UI
Here’s how we structure the user interface using fluidPage in Shiny:
ui <- fluidPage(
titlePanel("Retirement Contribution Analysis App"),
sidebarLayout(
sidebarPanel(
numericInput("current_age", "Current Age", value = 37, min = 18, max = 100),
numericInput("retirement_age", "Retirement Age", value = 65, min = 30, max = 100),
numericInput("annual_salary", "Annual Salary ($)", value = 145000),
sliderInput("employee_contribution_rate", "Employee Contribution Rate (%)", min = 0, max = 100, value = 6),
sliderInput("employer_contribution_rate", "Employer Contribution Rate (%)", min = 0, max = 100, value = 6),
sliderInput("return_rate_before_retirement", "Return Rate Before Retirement (%)", min = 0, max = 15, value = 4),
sliderInput("return_rate_after_retirement", "Return Rate After Retirement (%)", min = 0, max = 15, value = 3),
numericInput("current_savings", "Current Savings ($)", value = 259000),
numericInput("post_retirement_expenses", "Post-Retirement Expenses ($)", value = 90000)
),
mainPanel(
plotlyOutput("plot1"),
DTOutput("dataTable")
)
)
)2.4 Step 4: Server Logic
Define your core computation and output rendering in the server function: Sample Calculations
server <- function(input, output) {
results <- reactive({
annual_savings <- input$annual_salary * ((input$employee_contribution_rate + input$employer_contribution_rate) / 100)
future_value <- numeric()
total <- input$current_savings
for (year in input$current_age:(input$retirement_age - 1)) {
total <- total * (1 + input$return_rate_before_retirement / 100) + annual_savings
future_value <- c(future_value, total)
}
data.frame(Age = input$current_age:(input$retirement_age - 1), Balance = future_value)
})
output$plot1 <- renderPlotly({
df <- results()
ggplotly(ggplot(df, aes(x = Age, y = Balance)) +
geom_line(color = "blue") +
scale_y_continuous(labels = scales::dollar_format()) +
theme_minimal())
})
output$dataTable <- renderDT({
datatable(results(), options = list(pageLength = 10))
})
}2.5 Step 5: Running the App
At the bottom of your app.R file, run:
Now your Retirement Calculator is live and ready for testing.
2.6 Step 5.5: Advanced Features Overview
Before diving into optional features, here’s what this app includes beyond the basics:
- 🌗 Dark Mode toggle
- 🎙️ Voice narration using browser speech synthesis
- 🔥 FIRE (Financial Independence Retire Early) calculator
- 📤 Export to PDF and CSV
- 📊 Realistic tax bracket modeling
- 💡 Life expectancy and healthcare cost estimators
- 📈 Simulated investment growth under uncertainty
Each feature will be added incrementally in the steps that follow.
2.7 Step 6: Advanced Features Overview
This app includes several advanced features: - Dark Mode toggle with custom CSS - Speech synthesis for accessibility - FIRE (Financial Independence Retire Early) calculator - PDF and CSV export of results - Real tax bracket modeling - Life expectancy adjustments based on lifestyle factors
We’ll walk through how to add each of these next.
2.8 Step 6: Enable Dark Mode (Optional)
Add the following code in the tags$head() section:
And enable toggle with:
2.9 Step 7: Add Voice Narration (Optional)
Insert this JavaScript inside a tags$script() block to add voice support:
var isMuted = false;
function readTextAloud() {
if (isMuted) return;
var text = document.getElementById("finalMessageText").innerText;
var speech = new SpeechSynthesisUtterance();
speech.text = text;
window.speechSynthesis.speak(speech);
}Trigger this using:
💡 Tip:
To preview available voices in your browser, try running this in the browser console:You can create a voice selector in the UI with
selectInput()and assign it dynamically in JS if desired.
2.10 Step 8: Export to PDF
Use this code to render results into a PDF report:
2.11 Example Screen shot of App
3 Appendix
3.1 Why Teach Shiny at Trine University?
This tutorial is designed for Trine University undergraduate and graduate students who are learning R and want to apply it to real-world problems. Whether you’re studying Data Analytics, Engineering, Business, or Education, Shiny empowers you to:
- Build interactive tools for your projects or capstones
- Communicate results through dynamic visualizations
- Practice reactive programming, a skill relevant to many modern tech jobs
- Create web-based apps that can be shared with supervisors, faculty, or employers
- Prototype tools without needing web development experience
Learning Shiny helps bridge the gap between raw analysis and real-world impact
At Trine University, we believe students should learn skills that go beyond the classroom. Teaching Shiny empowers students to:
Apply real-world data science in a practical, engaging way.
Build interactive tools for research, capstone projects, and internships.
Demonstrate coding and communication skills in portfolios or job interviews.
Work with reproducible, open-source tools that scale from small projects to full applications.
By learning Shiny, students gain an edge—not just in R programming, but in the ability to tell data-driven stories that matter.
3.2 Why Use Shiny?
- 🔧 Interactivity: Let users explore your data and models in real time
- 📊 Visualization: Turn static plots into clickable, zoomable graphics
- 🧩 Modularity: Build custom dashboards with user controls
- 🌐 Accessibility: Share your work online, as a live app
- 📈 Integration: Easily combine with packages like
ggplot2,plotly,DT, anddplyr
| #### Platform Notes & Environment Information |
| 🧪 Tested Environment: |
| - R version ≥ 4.3.0 - RStudio ≥ 2023.09+ - Chrome or Edge browser (for voice synthesis compatibility) |
| ⚙️ Functionality Notes: |
- PDF export uses the Cairo package for improved font rendering.- Voice narration uses browser-native JavaScript APIs ( speechSynthesis).- The app generates interactive plotly charts and downloadable reports.- Some features (e.g., dark mode styling or narration) may not render the same in every browser. |
| 🖥️ This tutorial and app were developed and tested using Windows 11 and RStudio (latest version). |
| - All features, including PDF export, speech synthesis, and dark mode, work smoothly on Windows. - For best results, run this project inside RStudio Desktop, not the R GUI or terminal. - The app should also run on macOS and Linux, but some features (like voice narration or Cairo PDF rendering) may behave differently depending on your system configuration. |
> ✅ Tip: If you’re on Windows and get an error with PDF generation, make sure the Cairo package is installed and that your system has proper font support. You can also install Ghostscript for advanced PDF rendering if needed. ## Case Study: Eight Client Retirement Profiles |
| This Shiny App models and visualizes retirement savings growth and depletion based on a user’s input. It simulates pre- and post-retirement scenarios, FIRE planning, and investment comparisons. Tax calculations in the model are based on the IRS 2024 tax brackets (U.S. Internal Revenue Service, 2024). Also safe withdrawal rate principles, such as the 4% rule, are based on past market simulations (Bengen, 1994; Cooley et al., 1998). |
| Retirement age and planning factors reflect Social Security policies (U.S. Social Security Administration, 2024). |
| Healthcare and lifestyle costs use average spending data from the U.S. Bureau of Labor Statistics (U.S. Bureau of Labor Statistics, 2023). |
| Users can: - Estimate savings growth with salary increases and investment returns - Visualize when savings will run out post-retirement - Simulate FIRE scenarios - Compare 401(k) vs Roth IRA investments - Enable dark mode - Export data as PDF or CSV - Get spoken feedback via voice narration |
3.2.1 🗣️ 3. UI
3.2.1.2 Sever
- Total Contribution: Savings over time
- Annual Savings: Yearly contributions
- Post-Retirement Depletion: When savings run out
- Comparison: Pre vs post-retirement side-by-side
- Heatmap Analysis: Sensitivity on expenses and return rate
- Savings vs Expenses: Line plot comparing savings vs costs
- Contribution Breakdown: Pie chart of employer/employee contributions
- Savings Longevity: Bar chart if savings last year-by-year
- Life Expectancy vs Depletion: Life vs financial survival
- Data Table: Combined raw data
- FIRE Calculator: FIRE number + years to retirement
- 401(k) vs Roth IRA: Simulated growth by account type
3.2.2 🗣️ 3. Speech Narration
3.2.2.1 results() Reactive Block
Calculates savings and depletion based on inputs:
3.2.2.1.1 Pre-Retirement
- Computes salary with raise
- Calculates annual savings
- Compounds savings with return rate
3.2.2.1.2 Post-Retirement
- Withdraws inflation-adjusted, tax-adjusted expenses
- Accounts for healthcare and lifestyle
- Applies U.S. tax brackets to simulate withdrawals
Output:
data: Pre-retirement detailspost_retirement_data: Post-retirement datacombined_data: Full datasetfinal_age: Depletion agelife_expectancy: Lifestyle-adjusted expectancy
3.2.3 🗣️ 3. Speech Narration
- Adds narration using browser’s SpeechSynthesis API
- Reads summary aloud when user clicks “Calculate”
- Can mute/unmute voice
- Replaces phrases to sound conversational
- Customizes voice, pitch, and rate
3.2.4 🌙 4. Dark Mode
- Adds custom CSS for dark styling
- Button toggles
dark-modeclass - Affects all UI elements, buttons, and outputs
3.2.5 🖨️ 5. PDF Export
Uses Cairo, grid, and gridExtra to render a multipage PDF:
- Page 1: Text summary with savings & depletion
- Page 2: Top rows of data table
- Page 3+: Embedded PNG plots
Text includes:
- Projected retirement balance
- Depletion age
- Personalized recommendations
3.2.6 💾 6. CSV Export
Enables full download of combined data table as CSV.
3.2.7 🔥 7. FIRE Calculator
This module uses safe withdrawal rate principles, such as the 4% rule, based on past market simulations (Bengen, 1994; Cooley et al., 1998)
- Calculates FIRE number:
Expenses ÷ Withdrawal Rate - Calculates years to FIRE:
FIRE ÷ Annual Savings - Displays growth plot and summary
- Alerts if FIRE is realistic or not
3.2.8 📊 8. Investment Growth Comparison
Simulates account balances for:
- 401(k)
- Roth 401(k)
- Roth IRA
- Traditional IRA
Simulates market fluctuation using rnorm().
Considers 20% tax on pre-tax accounts.
3.2.9 ✅ 9. Final Summary Message
Dynamic message panel displays:
- Projected savings
- Depletion age
- Life expectancy
- Warnings if you outlive savings
- “On track” or “Not on track” feedback
- Detailed breakdown of each output tab
- Smart recommendations based on inputs:
- Delay retirement?
- Save more?
- Consider Roth conversion?
- Reduce expenses?
- Increase returns?
- Avoid high-risk portfolios?
3.2.10 🛡️ 10. Resilience and Handling
- Uses
req()to prevent plot errors - Handles NA/null inputs
- Uses
observeEvent()for efficiency - Uses
Sys.sleep()for smoother UX
3.2.11 ✅ Summary
This app provides a comprehensive, interactive retirement planning tool. It’s designed for realism and customization, including:
- Lifecycle-based financial planning
- Optional FIRE and tax modeling
- Real-time feedback with narration
- Exportable, user-friendly outputs
It’s ideal for educators, financial advisors, or anyone curious about retirement readinessChallenge: Modify the growth_projection() function to compare different contribution strategies (e.g., consistent vs. front-loaded savings).
3.2.12 Done!
You now have a fully functional Retirement Contribution Analysis App built in Shiny with optional features like dark mode, narration, and export tools. 🎉
Contact: joshualizardi@Gmail.com
3.2.13 Next Steps and Challenges
Here are a few ideas to extend your app:
- 📈 Add Monte Carlo simulations for investment variability
- 🧾 Generate customized PDF reports with
rmarkdown::render() - 🧮 Add Social Security or pension calculators
- 🔒 Create login-based access to save/load user profiles
- 🇺🇸 Add state-specific tax brackets
3.2.13.1 💪 Challenge: Add Roth IRA Tax-Free Projections
Try modifying the growth_projection() function to simulate Roth IRA growth assuming: - No taxes on withdrawals - Different contribution limits
3.3 Full Code (+)
# ========================================================
# 🚀 Retirement Calculator PLUS 🔥
# Copyright (c) 2025 Joshua Lizardi & Joshua Maier
# All rights reserved.
#
# This software is provided "as is", without warranty of any kind,
# express or implied. Redistribution or commercial use is prohibited
# without explicit permission from the author.
#
# Created by Joshua Lizardi
# For licensing inquiries, contact: joshua.lizardi@email.com
# ========================================================
# Load required libraries
library(shiny)
library(ggplot2)
library(plotly)
library(DT)
library(dplyr)
library(scales) # For currency formatting
library(ggplot2)
library(gridExtra)
library(grid)
library(Cairo)
library(png)
# --- Define UI ---
ui <- fluidPage(
titlePanel("Retirement Contribution Analysis App"),
tags$head(
tags$style(HTML("
body.dark-mode { background-color: #121212; color: white; }
.dark-mode .well, .dark-mode .panel { background-color: #333 !important; color: white; }
.dark-mode input, .dark-mode select, .dark-mode .btn { background-color: #555 !important; color: white !important; }
.dark-mode .btn-primary { background-color: #007bff !important; }
.dark-mode .tab-pane { background-color: #222 !important; }
.dark-mode .shiny-output-error { color: red !important; }
")),
tags$script(HTML("
$(document).on('click', '#toggle_dark_mode', function() {
$('body').toggleClass('dark-mode');
});
"))
),
actionButton("toggle_dark_mode", "🌙 Toggle Dark Mode", class = "btn btn-dark"),
fluidRow(
column(12,
div(style = "text-align: Left; margin-bottom: 15px;",
actionButton("calculate", "🚀 Calculate", class = "btn btn-primary"),
actionButton("mute_audio", "🔇 Mute Audio", class = "btn btn-secondary"), # ✅ Mute Button
tags$script(HTML("
var isMuted = false;
var speechInstance = null;
function readTextAloud() {
if (isMuted) return;
var text = document.getElementById('finalMessageText').innerText;
if (text.trim() === '') return;
window.speechSynthesis.cancel();
speechInstance = new SpeechSynthesisUtterance();
// 🎙️ Dynamic Speech Enhancements
var voices = window.speechSynthesis.getVoices();
speechInstance.voice = voices.find(v => v.name.includes('Google UK English Male')) || voices[1];
speechInstance.rate = 0.95;
speechInstance.pitch = 1.1;
// 🎭 Add natural speech patterns
text = text.replace('Summary of Results', 'Hello, My name is Ella, Im here to help, Alright, here is the breakdown.');
text = text.replace('Projected savings at retirement:', 'So by the time you retire, you should have around');
text = text.replace('Estimated depletion age:', 'But here is the catch, your savings might last until');
text = text.replace('Final Recommendations:', 'Now, let me give you some advice.');
// ✨ Add strategic pauses
text = text.replace(/\\n/g, '... ');
speechInstance.text = text;
if (!isMuted) {
window.speechSynthesis.speak(speechInstance);
}
}
// ✅ Trigger reading when 'Calculate' is clicked
$(document).on('click', '#calculate', function() {
setTimeout(readTextAloud, 2000);
});
// ✅ Toggle mute state when 'Mute Audio' button is clicked
$(document).on('click', '#mute_audio', function() {
isMuted = !isMuted;
if (isMuted) {
window.speechSynthesis.cancel();
}
var btnText = isMuted ? '🔊 Unmute Audio' : '🔇 Mute Audio';
$('#mute_audio').text(btnText);
});
"))
)
)
)
,
sidebarLayout(
sidebarPanel(
h3("Retirement Data"),
tags$hr(), # Adds a horizontal line
downloadButton("downloadData", "Download Results"),
numericInput("current_age", "Current Age", value = 37, min = 18, max = 100),
numericInput("retirement_age", "Retirement Age", value = 65, min = 30, max = 100),
numericInput("current_savings", "Current Savings ($)", value = 259000, min = 0),
numericInput("annual_salary", "Annual Salary ($)", value = 145000, min = 0),
tags$hr(), # Separates sections
sliderInput("salary_increase_rate", "Salary Increase Rate (%)", min = 0, max = 10, value = 2, step = 0.1),
sliderInput("employee_contribution_rate", "Employee Contribution Rate (%)", min = 0, max = 100, value = 6),
sliderInput("employer_contribution_rate", "Employer Contribution Rate (%)", min = 0, max = 100, value = 6),
numericInput("pre_tax_contribution", "Pre-Tax Contribution ($)", value = 6000, min = 0),
sliderInput("return_rate_before_retirement", "Return Rate Before Retirement (%)", min = 0, max = 15, value = 4),
sliderInput("return_rate_after_retirement", "Return Rate After Retirement (%)", min = 0, max = 15, value = 3),
numericInput("post_retirement_expenses", "Post-Retirement Expenses ($)", value = 90000, min = 0),
sliderInput("post_retirement_tax_rate", "Post-Retirement Tax Rate (%)", min = 0, max = 50, value = 15),
sliderInput("inflation_rate", "Inflation Rate (%)", min = 0, max = 10, value = 2),
sliderInput("healthcare_cost", "Estimated Annual Healthcare Costs ($)", min = 2000, max = 20000, value = 5000, step = 500),
sliderInput("health_risk_factor", "Health Risk Factor (0 = Low, 1 = High)", min = 0, max = 1, value = 0.5, step = 0.1),
sliderInput("lifestyle_factor", "Lifestyle & Longevity Score (0 = Unhealthy, 10 = Very Healthy)",
min = 0, max = 10, value = 5, step = 1)
),
mainPanel(
tabsetPanel(
tabPanel("Total Contribution", plotlyOutput("plot1")),
tabPanel("Annual Savings", plotlyOutput("plot2")),
tabPanel("Post-Retirement Depletion", plotlyOutput("plot3")),
tabPanel("Comparison", plotlyOutput("plot4")),
tabPanel("Heatmap Analysis", plotlyOutput("heatmap")),
tabPanel("Savings vs. Expenses", plotlyOutput("savings_vs_expenses")),
tabPanel("Contribution Breakdown", plotlyOutput("contribution_pie")),
tabPanel("Savings Longevity", plotlyOutput("savings_longevity")),
tabPanel("Life Expectancy vs. Depletion", plotlyOutput("life_vs_depletion")),
tabPanel("Data Table",
downloadButton("downloadTableCSV", "📥 Download Data Table (CSV)"),
DTOutput("dataTable")),
tabPanel("🔥 FIRE Calculator",
fluidRow(
column(4,
div(
h3("FIRE Inputs"),
numericInput("fire_annual_expenses", "Annual Expenses ($)", value = 50000, min = 0),
sliderInput("fire_withdrawal_rate", "Withdrawal Rate (%)", min = 3, max = 5, value = 4, step = 0.1),
sliderInput("fire_savings_rate", "Savings Rate (%)", min = 0, max = 100, value = 30),
actionButton("calculate_fire", "🔥 Calculate FIRE", class = "btn btn-danger btn-lg w-100")
)
),
column(8,
div(
h3("🔥 FIRE Number & Projections"),
verbatimTextOutput("fire_summary"),
plotOutput("fire_plot")
)
)
)
),
tabPanel("401(k) vs Roth IRA Growth",
actionButton("calculate_growth", "📈 Simulate Growth", class = "btn btn-primary"),
plotOutput("investmentPlot"),
verbatimTextOutput("summaryOutput"))
),
div(id = "finalMessageText", uiOutput("final_message")),
# ✅ Add this right below
div(class = "footer", HTML("© 2025 Joshua Lizardi & Joshua Maier"))
)
)
)
# --- Define Server Logic ---
server <- function(input, output) {
results <- reactive({
input$calculate
isolate({
salary_increase_rate <- input$salary_increase_rate / 100
employee_contribution_rate <- input$employee_contribution_rate / 100
employer_contribution_rate <- input$employer_contribution_rate / 100
return_rate_before_retirement <- input$return_rate_before_retirement / 100
return_rate_after_retirement <- input$return_rate_after_retirement / 100
post_retirement_tax_rate <- input$post_retirement_tax_rate / 100
inflation_rate <- input$inflation_rate / 100
# Base life expectancy (actuarial average)
base_life_expectancy <- 85
# Adjust based on lifestyle factor (every point adds or subtracts ~1.5 years)
life_expectancy <- round(base_life_expectancy + ((input$lifestyle_factor - 5) * 1.5))
# Ensure it doesn't go below 65 or above 110
life_expectancy <- max(65, min(life_expectancy, 110))
# --- Pre-Retirement Calculations ---
age_seq <- seq(input$current_age, input$retirement_age)
annual_savings_list <- numeric(length(age_seq))
total_contribution_list <- numeric(length(age_seq))
# Initialize starting contribution correctly
total_contribution <- input$current_savings
for (i in seq_along(age_seq)) {
n <- i # Keeps the correct step count
annual_savings_list[i] <- round((employee_contribution_rate * (input$annual_salary * ((1 + salary_increase_rate)^(n - 1)))) * 2)
# First-year contribution remains as initial savings
if (i == 1) {
total_contribution_list[i] <- total_contribution
} else {
# Correct formula: Apply growth to previous contribution first, then add new savings
total_contribution <- total_contribution * (1 + return_rate_before_retirement) + annual_savings_list[i]
total_contribution_list[i] <- round(total_contribution)
}
}
# Create dataframe with correct values
data <- tibble(Age = age_seq, Annual_Savings = annual_savings_list, Total_Contribution = total_contribution_list, Phase = "Pre-Retirement")
# --- Post-Retirement Calculations ---
total_contribution <- last(data$Total_Contribution)
post_retirement_expenses <- (input$post_retirement_expenses * (1 + post_retirement_tax_rate)) +
(input$healthcare_cost * (1 + input$health_risk_factor))
post_retirement_data <- tibble(Age = integer(), Total_Contribution = numeric(), Expenses = numeric(), Phase = character()) # ✅ Ensure "Expenses" column is included
current_age <- input$retirement_age
# Function to calculate tax using progressive brackets
calculate_tax <- function(income, brackets) {
tax_due <- 0
for (i in 1:nrow(brackets)) {
if (income > brackets$Upper[i]) {
tax_due <- tax_due + (brackets$Upper[i] - brackets$Lower[i]) * brackets$Rate[i]
} else {
tax_due <- tax_due + (income - brackets$Lower[i]) * brackets$Rate[i]
break
}
}
return(tax_due)
}
# Define tax brackets for single filers
tax_brackets <- data.frame(
Lower = c(0, 11600, 47150, 100525, 191950, 243725, 609350),
Upper = c(11600, 47150, 100525, 191950, 243725, 609350, Inf),
Rate = c(0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37)
)
# Update post-retirement calculations
post_retirement_data <- tibble(Age = integer(), Total_Contribution = numeric(), Expenses = numeric(), Phase = character())
current_age <- input$retirement_age
while (total_contribution > 0 && current_age <= 120) {
post_retirement_expenses_inflated <- post_retirement_expenses * ((1 + inflation_rate)^(current_age - input$retirement_age))
# Apply progressive tax to withdrawals
tax_due <- calculate_tax(post_retirement_expenses_inflated, tax_brackets)
post_retirement_expenses_after_tax <- post_retirement_expenses_inflated - tax_due
total_contribution <- (total_contribution - post_retirement_expenses_after_tax) * (1 + return_rate_after_retirement)
post_retirement_data <- add_row(
post_retirement_data,
Age = current_age,
Total_Contribution = round(total_contribution),
Expenses = round(post_retirement_expenses_after_tax),
Phase = "Post-Retirement"
)
current_age <- current_age + 1
}
combined_data <- bind_rows(data, post_retirement_data)
return(list(data = data, post_retirement_data = post_retirement_data, combined_data = combined_data, final_age = current_age, life_expectancy = life_expectancy # ✅ Add this!
))
})
})
# ✅ Data Table
# ✅ Updated Data Table to Include Post-Retirement Data
output$dataTable <- renderDT({
req(results()$combined_data) # Ensure data is available
# Create a combined table
datatable(
results()$combined_data,
options = list(pageLength = 15, scrollX = TRUE), # Adds scrolling & pagination
rownames = FALSE
)
})
# ✅ Download Data
output$downloadData <- downloadHandler(
filename = function() {
paste("Retirement_Analysis_Report_", Sys.Date(), ".pdf", sep = "")
},
content = function(file) {
temp_dir <- tempdir()
# Generate ggplot objects from the data
plot1 <- ggplot(results()$data, aes(x = Age, y = Total_Contribution)) +
geom_line(color = "blue") +
ggtitle("Total Contribution Over Time") +
theme_minimal()
plot2 <- ggplot(results()$data, aes(x = Age, y = Annual_Savings)) +
geom_col(fill = "red") +
ggtitle("Annual Savings Breakdown") +
theme_minimal()
plot3 <- ggplot(results()$post_retirement_data, aes(x = Age, y = Total_Contribution)) +
geom_line(color = "blue") +
ggtitle("Post-Retirement Fund Depletion") +
theme_minimal()
# Save plots as images
ggsave(filename = file.path(temp_dir, "plot1.png"), plot = plot1, width = 8, height = 5)
ggsave(filename = file.path(temp_dir, "plot2.png"), plot = plot2, width = 8, height = 5)
ggsave(filename = file.path(temp_dir, "plot3.png"), plot = plot3, width = 8, height = 5)
# Create an empty vector to store recommendations
recommendations <- c()
# 1️⃣ Check if savings last long enough
if (results()$final_age < 85) {
recommendations <- c(recommendations, "🔺 Increase your savings rate or consider delaying retirement to extend your savings.")
} else {
recommendations <- c(recommendations, "✅ Your savings plan looks solid! Consider optimizing your investments for growth.")
}
# 2️⃣ Analyze contribution rate
total_contribution_rate <- input$employee_contribution_rate + input$employer_contribution_rate
if (total_contribution_rate < 10) {
recommendations <- c(recommendations, "📌 Consider increasing your retirement contributions to at least 10-15% of your salary.")
}
# 3️⃣ Post-retirement expenses warning
expense_ratio <- input$post_retirement_expenses / (last(results()$data$Total_Contribution) / (120 - input$retirement_age))
if (expense_ratio > 0.07) {
recommendations <- c(recommendations, "⚠️ Your post-retirement expenses are high relative to savings. Consider adjusting spending habits.")
}
# 4️⃣ Investment strategy
if (input$return_rate_before_retirement < 5) {
recommendations <- c(recommendations, "📈 Your return rate before retirement is low. Consider diversifying your investments for better growth.")
}
# Convert recommendations to formatted text
final_recommendations <- paste(recommendations, collapse = "\n")
# Final message with recommendations
final_message_text <- paste(
"Summary of Results\n",
"Projected savings at retirement: $", formatC(last(results()$data$Total_Contribution), format = "f", big.mark = ",", digits = 2), "\n",
"Estimated depletion age: ", results()$final_age, "\n\n",
"Final Recommendations:\n", final_recommendations
)
# Get final message text
final_message_text <- paste(
"Summary of Results\n",
"Projected savings at retirement: $", formatC(last(results()$data$Total_Contribution), format = "f", big.mark = ",", digits = 2), "\n",
"Estimated depletion age: ", results()$final_age, "\n\n",
"Final Recommendations:\n",
ifelse(results()$final_age >= 90, "Your plan looks good!", "Consider increasing contributions!")
)
# Start PDF generation
pdf(file, width = 8.5, height = 11, family = "sans", pointsize = 12)
# Add summary text
grid::grid.newpage()
grid::grid.text(final_message_text, x = 0.1, y = 0.9, just = "left", gp = grid::gpar(fontsize = 12))
# Add a new page for the table
grid::grid.newpage()
# Convert data table to matrix for proper rendering (limit to first 10 rows)
table_data <- results()$data %>%
head(100) %>%
as.matrix()
# Render the table in the PDF
gridExtra::grid.table(table_data)
# Add a new page for plots
grid::grid.newpage()
# Add plots to the PDF
img_paths <- list.files(temp_dir, pattern = "plot.*\\.png", full.names = TRUE)
for (img in img_paths) {
grid::grid.newpage()
img_raster <- png::readPNG(img)
grid::grid.raster(img_raster)
}
# ✅ Ensure the PDF is closed properly
dev.off()
# ✅ Double-check that the PDF file is accessible before returning it
Sys.sleep(1) # Pause for 1 second to allow file system to release lock
}
)
# ✅ Download Just the Data Table as CSV
output$downloadTableCSV <- downloadHandler(
filename = function() {
paste("Retirement_Data_Table_", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
write.csv(results()$combined_data, file, row.names = FALSE)
}
)
# ✅ Plots with formatted currency
output$plot1 <- renderPlotly({
req(results()$data)
ggplotly(ggplot(results()$data, aes(x = Age, y = Total_Contribution)) +
geom_line() +
scale_y_continuous(labels = dollar_format(prefix = "$")) +
theme_minimal())
})
output$plot2 <- renderPlotly({
req(results()$data)
ggplotly(ggplot(results()$data, aes(x = Age, y = Annual_Savings)) +
geom_col(fill = "red") +
scale_y_continuous(labels = dollar_format(prefix = "$")) +
theme_minimal())
})
output$plot3 <- renderPlotly({
req(results()$post_retirement_data)
ggplotly(ggplot(results()$post_retirement_data, aes(x = Age, y = Total_Contribution)) +
geom_line(color = "blue") +
scale_y_continuous(labels = dollar_format(prefix = "$")) +
theme_minimal())
})
output$plot4 <- renderPlotly({
req(results()$combined_data) # Ensure data exists before using
# Create a local copy of the data before modifying
combined_data <- results()$combined_data
# Ensure factor order
combined_data$Phase <- factor(combined_data$Phase, levels = c("Pre-Retirement", "Post-Retirement"))
ggplotly(
ggplot(combined_data, aes(x = Age, y = Total_Contribution)) +
geom_line(color = "blue") +
facet_wrap(~Phase, scales = "fixed") +
scale_y_continuous(labels = scales::dollar_format(prefix = "$")) +
theme_minimal()
)
})
output$heatmap <- renderPlotly({
req(results()) # Ensure results() is available
expenses_seq <- seq(90000, 150000, by = 15000) # Optimized for performance
returns_seq <- seq(0.02, 0.05, by = 0.005)
# Ensure total_contribution is correctly retrieved once
total_contribution_start <- last(results()$data$Total_Contribution)
# Expand grid for combinations
heatmap_data <- expand.grid(PostRetirementExpenses = expenses_seq, ReturnRate = returns_seq)
# Compute depletion age for each row
heatmap_data$DepletionAge <- mapply(function(exp, ret_rate) {
total_contribution <- total_contribution_start
age <- input$retirement_age
while (total_contribution > 0 && age <= 120) {
total_contribution <- (total_contribution - exp) * (1 + ret_rate)
age <- age + 1
}
return(min(age, 120)) # Cap depletion age at 120
}, heatmap_data$PostRetirementExpenses, heatmap_data$ReturnRate)
# Plot heatmap
ggplotly(
ggplot(heatmap_data, aes(x = PostRetirementExpenses, y = ReturnRate, fill = DepletionAge)) +
geom_tile() +
scale_fill_gradient(low = "yellow", high = "red") +
theme_minimal() +
labs(x = "Post-Retirement Expenses ($)", y = "Return Rate", fill = "Depletion Age")
)
})
# ✅ Savings vs. Expenses Line Chart
output$savings_vs_expenses <- renderPlotly({
req(results()$post_retirement_data)
ggplotly(
ggplot(results()$post_retirement_data, aes(x = Age)) +
geom_line(aes(y = Total_Contribution, color = "Savings"), size = 1.2) +
geom_line(aes(y = Expenses, color = "Expenses"), size = 1.2, linetype = "dashed") + # ✅ Ensure Expenses is referenced correctly
labs(title = "Projected Savings vs. Retirement Expenses",
x = "Age",
y = "Amount ($)",
color = "Legend") +
scale_color_manual(values = c("Savings" = "blue", "Expenses" = "red")) +
theme_minimal()
)
})
# ✅ Contribution Breakdown Pie Chart
output$contribution_pie <- renderPlotly({
data_pie <- data.frame(
Source = c("Employee Contribution", "Employer Contribution"),
Amount = c(input$employee_contribution_rate, input$employer_contribution_rate)
)
plot_ly(data_pie, labels = ~Source, values = ~Amount, type = "pie",
marker = list(colors = c("blue", "green")),
textinfo = "label+percent",
hoverinfo = "text") %>%
layout(title = "Retirement Contribution Breakdown")
})
# ✅ Savings Longevity Bar Chart
output$savings_longevity <- renderPlotly({
age_seq <- seq(input$retirement_age, input$retirement_age + 30)
depletion_points <- sapply(age_seq, function(x) {
ifelse(x <= results()$final_age, "Savings Left", "Depleted")
})
data_longevity <- data.frame(Age = age_seq, Status = depletion_points)
ggplotly(
ggplot(data_longevity, aes(x = Age, fill = Status)) +
geom_bar() +
scale_fill_manual(values = c("Savings Left" = "blue", "Depleted" = "red")) +
labs(title = "Savings Longevity After Retirement",
x = "Age",
y = "Savings Status",
fill = "Legend") +
theme_minimal()
)
})
output$life_vs_depletion <- renderPlotly({
req(results()$final_age, results()$life_expectancy) # ✅ Correct
# Create a dataframe for visualization
df_life_vs_depletion <- data.frame(
Category = c("Savings Depleted", "Estimated Life Expectancy"),
Age = c(results()$final_age, results()$life_expectancy) # ✅ Use results()
)
# Generate the bar chart
ggplotly(
ggplot(df_life_vs_depletion, aes(x = Category, y = Age, fill = Category)) +
geom_bar(stat = "identity", width = 0.6) +
scale_fill_manual(values = c("Savings Depleted" = "red", "Estimated Life Expectancy" = "blue")) +
geom_text(aes(label = Age), vjust = -0.5, size = 5) + # Add text labels
labs(
title = "Life Expectancy vs. Savings Depletion",
x = "Category",
y = "Age"
) +
theme_minimal()
)
})
# ✅ Display Final Age Message
output$final_message <- renderUI({
req(results()$final_age)
final_age <- results()$final_age
total_at_retirement <- last(results()$data$Total_Contribution)
# ✅ Base life expectancy (actuarial average)
base_life_expectancy <- 85
# ✅ Adjust based on lifestyle factor (every point adds or subtracts ~1.5 years)
life_expectancy <- round(base_life_expectancy + ((input$lifestyle_factor - 5) * 1.5))
# ✅ Ensure it doesn't go below 65 or above 110
life_expectancy <- max(65, min(life_expectancy, 110))
# ✅ Check if savings deplete before life expectancy
depletion_warning <- if (final_age < life_expectancy) {
tags$p("⚠️ Warning: Your estimated life expectancy exceeds your savings depletion age!
You may run out of money before you pass away.",
style = "color: red; font-weight: bold;")
} else {
tags$p("✅ Your savings are projected to last through your expected lifespan.",
style = "color: green; font-weight: bold;")
}
# ✅ Dynamic "On Track" vs. "Not On Track" Message
savings_status <- if (final_age < 85) {
tags$p("⚠️ Your savings may not last long enough! Consider increasing contributions or adjusting expenses.",
style = "color: red; font-weight: bold;")
} else {
tags$p("✅ Your savings plan is on track.",
style = "color: green; font-weight: bold;")
}
# ✅ Function to compute investment growth with random fluctuations
growth_projection <- function(initial, annual_contrib, rate, years) {
# ✅ Ensure all values are properly initialized
if (is.null(initial) || is.null(annual_contrib) || is.null(rate) || is.null(years) || is.na(years)) {
return(rep(NA, 1)) # Return NA if any input is missing
}
years <- as.integer(years) # ✅ Convert to integer to avoid length errors
if (years <= 0) {
return(rep(NA, 1)) # ✅ Handle invalid cases (e.g., negative or zero years)
}
balance <- numeric(years + 1)
balance[1] <- initial
for (i in 2:(years + 1)) {
random_rate <- rnorm(1, mean = rate, sd = 2) # Simulates market fluctuations
balance[i] <- balance[i - 1] * (1 + random_rate / 100) + annual_contrib
}
return(balance)
}
observeEvent(input$calculate_fire, {
# FIRE Number Calculation
showNotification("🔥 Calculating FIRE... Please wait!", type = "message", duration = 2)
Sys.sleep(2) # Simulating processing delay (you can remove this in real calculations)
fire_number <- input$fire_annual_expenses / (input$fire_withdrawal_rate / 100)
# Compute years to FIRE based on savings rate
savings_rate <- input$fire_savings_rate / 100
annual_savings <- input$annual_salary * savings_rate
years_to_fire <- ifelse(annual_savings > 0, fire_number / annual_savings, Inf)
# Generate a projection plot for savings growth
fire_savings <- cumsum(rep(annual_savings, round(years_to_fire)))
fire_years <- seq(1, length(fire_savings))
# Display results
output$fire_summary <- renderText({
paste0(
"🔥 FIRE Number: $", formatC(fire_number, format = "f", big.mark = ",", digits = 2), "\n",
"💰 Estimated Years to FIRE: ", round(years_to_fire, 1), " years\n",
ifelse(years_to_fire < 40, "✅ You can retire early! 🚀", "⚠️ You may need to save more to retire early.")
)
})
output$fire_plot <- renderPlot({
plot(fire_years, fire_savings, type = "l", col = "red", lwd = 2,
xlab = "Years", ylab = "Total Savings ($)",
main = "🔥 FIRE Savings Growth Over Time")
abline(h = fire_number, col = "blue", lty = 2)
legend("bottomright", legend = c("Savings Growth", "FIRE Goal"),
col = c("red", "blue"), lty = c(1, 2), lwd = 2)
})
})
# ✅ Simulating Investment Growth
observeEvent(input$calculate_growth, {
# Handle missing inputs with default values
years <- ifelse(is.null(input$years) || is.na(input$years) || input$years <= 0, 30, input$years)
rate <- ifelse(is.null(input$return_rate_before_retirement) || is.na(input$return_rate_before_retirement), 7, input$return_rate_before_retirement)
contrib <- ifelse(is.null(input$pre_tax_contribution) || is.na(input$pre_tax_contribution), 6000, input$pre_tax_contribution)
match_amt <- ifelse(is.null(input$employer_contribution_rate) || is.na(input$employer_contribution_rate), 5, input$employer_contribution_rate) / 100 * contrib
# Ensure valid numbers before running calculations
years <- max(1, as.integer(years)) # Minimum of 1 year
rate <- as.numeric(rate)
contrib <- as.numeric(contrib)
match_amt <- as.numeric(match_amt)
if (any(is.na(c(years, rate, contrib, match_amt)))) {
return(NULL) # Prevent errors if any values are still NA
}
# Run projections
pre_tax_401k <- growth_projection(0, contrib + match_amt, rate, years)
roth_401k <- growth_projection(0, contrib + match_amt, rate, years)
roth_ira <- growth_projection(0, contrib, rate, years)
traditional_ira <- growth_projection(0, contrib, rate, years)
# Apply 20% tax on withdrawals for pre-tax accounts
pre_tax_401k_after_tax <- pre_tax_401k * 0.8
traditional_ira_after_tax <- traditional_ira * 0.8
# Render the investment growth plot
output$investmentPlot <- renderPlot({
years_seq <- 0:years
plot(years_seq, pre_tax_401k_after_tax, type = "l", col = "blue", lwd = 2, ylim = c(0, max(pre_tax_401k)),
xlab = "Years", ylab = "Balance ($)", main = "Investment Growth Comparison")
lines(years_seq, roth_401k, col = "red", lwd = 2)
lines(years_seq, roth_ira, col = "green", lwd = 2)
lines(years_seq, traditional_ira_after_tax, col = "purple", lwd = 2)
legend("topleft", legend = c("401(k) (After Tax)", "Roth 401(k)", "Roth IRA", "Traditional IRA (After Tax)"),
col = c("blue", "red", "green", "purple"), lty = 1, lwd = 2)
})
# Render the summary output
output$summaryOutput <- renderText({
paste(
"Summary of Investment Growth:\n",
"- 401(k) and Traditional IRA contributions are pre-tax, but withdrawals are taxed (assumed 20% tax).\n",
"- Roth 401(k) and Roth IRA contributions are after-tax, so withdrawals are tax-free.\n",
"- Employer matching (only in 401(k) plans) significantly boosts the final balance.\n",
"- Random market fluctuations are included to reflect real-world investment conditions.\n",
"- Over ", years, " years, the highest projected growth is in Roth accounts due to tax-free withdrawals."
)
})
})
# ✅ Create dynamic recommendations
recommendations <- c()
# 1️⃣ Check if retirement is too soon
if ((input$retirement_age - input$current_age) < 15) {
recommendations <- c(recommendations, "🕒 Consider delaying retirement to allow more time for savings growth.")
}
# 2️⃣ Check if savings are too low for retirement
if (total_at_retirement < (input$post_retirement_expenses * 20)) {
recommendations <- c(recommendations, "💰 Your projected savings may not be enough. Consider increasing your contributions or working longer.")
}
# 3️⃣ Annual salary vs savings
savings_percentage <- (total_at_retirement / (input$annual_salary * (input$retirement_age - input$current_age))) * 100
if (savings_percentage < 50) {
recommendations <- c(recommendations, "📊 Your savings rate is low compared to your salary. Aim for 10-15% of your salary towards retirement.")
}
# 4️⃣ Salary increase rate effect
if (input$salary_increase_rate < 2) {
recommendations <- c(recommendations, "📈 Your salary growth is low. Consider investing in career development to increase earnings.")
}
# 5️⃣ Contribution rates check
total_contribution_rate <- input$employee_contribution_rate + input$employer_contribution_rate
if (total_contribution_rate < 10) {
recommendations <- c(recommendations, "📌 Consider increasing your retirement contributions to at least 10-15% of your salary.")
} else if (input$employer_contribution_rate < 5) {
recommendations <- c(recommendations, "🏢 Your employer contribution is low. Check if you're maximizing the company match.")
}
# 6️⃣ Investment strategy check
if (input$return_rate_before_retirement < 5) {
recommendations <- c(recommendations, "📈 Your return rate before retirement is low. Consider diversifying your portfolio to improve growth.")
}
if (input$return_rate_after_retirement > 6) {
recommendations <- c(recommendations, "⚠️ Your post-retirement return rate is high, which may mean your investments are too risky.")
}
# 7️⃣ Post-retirement expenses check
expense_ratio <- input$post_retirement_expenses / (total_at_retirement / (120 - input$retirement_age))
if (expense_ratio > 0.07) {
recommendations <- c(recommendations, "⚠️ Your post-retirement expenses are high relative to your savings. Consider reducing discretionary spending.")
}
# 8️⃣ Inflation rate check
if (input$inflation_rate > 3) {
recommendations <- c(recommendations, "🌍 Inflation is high. Invest in inflation-protected assets like TIPS or diversified funds.")
}
# 9️⃣ Tax efficiency recommendation
if (input$post_retirement_tax_rate > 20) {
recommendations <- c(recommendations, "📝 Your tax rate is high. Consider Roth conversions or tax-efficient withdrawal strategies.")
}
# Convert recommendations to formatted text
final_recommendations <- if (length(recommendations) > 0) {
tags$ul(lapply(recommendations, tags$li))
} else {
tags$p("✅ Your retirement plan looks well-balanced.")
}
shortfall <- if (total_at_retirement <= 0) {
tags$p("⚠️ Your savings will not be sufficient. Consider increasing contributions or adjusting expenses.",
style = "color: red; font-weight: bold;")
} else {
tags$p("✅ Your savings plan is on track.",
style = "color: green; font-weight: bold;")
}
# ✅ Render final UI with summary and recommendations
tagList(
tags$div(style = "border: 1px solid #ddd; padding: 15px; border-radius: 8px; background-color: #f9f9f9; margin-top: 15px;",
tags$h3("📊 Summary of Results", style = "color: #2c3e50;"),
tags$p(
tags$b("Projected savings at retirement: "),
tags$span(style = "font-weight: bold; color: #007bff;",
paste0("$", formatC(total_at_retirement, format = "f", big.mark = ",", digits = 2))),
tags$b(" | Estimated depletion age: "),
tags$span(style = "font-weight: bold; color: #007bff;", final_age)
),
tags$p(
tags$b("Estimated Life Expectancy: "),
tags$span(style = "font-weight: bold; color: #007bff;", paste(life_expectancy, "years"))
),
depletion_warning # ✅ Show the warning here!
),
tags$h4("📈 Explanation of Visuals", style = "color: #007bff; margin-top: 10px;"),
tags$ul(
tags$li(tags$b("'Total Contribution'"), ": Tracks savings growth over time."),
tags$li(tags$b("'Annual Savings'"), ": Displays yearly contributions."),
tags$li(tags$b("'Post-Retirement Depletion'"), ": Predicts when savings run out."),
tags$li(tags$b("'Comparison'"), ": Breaks down savings before & after retirement."),
tags$li(tags$b("'Heatmap Analysis'"), ": Shows different savings scenarios."),
tags$li(tags$b("'Savings vs. Expenses'"), ": Compares your projected savings balance with post-retirement expenses to highlight when you might run out of money."),
tags$li(tags$b("'Contribution Breakdown'"), ": A pie chart showing the percentage of contributions from you vs. your employer."),
tags$li(tags$b("'Savings Longevity'"), ": A bar chart visualizing how long your savings are expected to last after retirement."),
tags$li(tags$b("'Life Expectancy vs. Depletion'"), ": A warning if your projected savings run out before your estimated lifespan."),
tags$li(tags$b("'Healthcare Cost Estimator (Coming Soon)'"), ": Will help factor in estimated medical costs."),
tags$li(tags$b("'Life Expectancy Calculator (Coming Soon)'"), ": Will refine predictions based on lifestyle and health factors."),
tags$h4("💰 Tax Considerations in Projections", style = "color: #2c3e50; margin-top: 10px;"),
tags$ul(
tags$li("📝 Withdrawals from pre-tax retirement accounts (e.g., 401(k), Traditional IRA) are taxed using real U.S. tax brackets."),
tags$li("📉 The model applies a progressive tax system to post-retirement withdrawals before deducting expenses."),
tags$li("📊 Higher withdrawal amounts could push you into a higher tax bracket, leading to increased tax liability."),
tags$li("⚖️ Lowering withdrawal rates, delaying retirement, or using Roth accounts could help minimize taxes."),
tags$li("📢 Consider working with a financial planner to optimize tax-efficient withdrawal strategies.")
),
),
tags$h4("💡 Recommendations", style = "color: #2c3e50; margin-top: 10px;"),
final_recommendations,
savings_status # ✅ Replaces the old shortfall check with a smarter one
)
})
}
# --- Run the Shiny App ---
shinyApp(ui = ui, server = server)3.3.1 Optional: Deployment Tips
Once your app is complete, you can deploy it using:
- Shinyapps.io After creating a free account at https://www.shinyapps.io/, to link your account with your RStudio application; In RStudio, open the Tools menu and select Global Options. Click on the Publishing tab on the left. Click on Connect on the right. Select shinyapps.io. Follow the instructions to input your token. After building a Shiny app, to publish it online, Open the app in RStudio. Click on the Publish icon () in the top-right of the Source pane, or the arrow next to it. Select files to be published: your app.R file and any other data files or images. Select your account. Provide a title for your app. Click Publish.
3.3.2 Be mindful of sensitive data if deploying publicly!
4 References
Bengen, W. P. (1994). Determining withdrawal rates using historical data. Journal of Financial Planning, 7(4), 171–180. https://www.onefpa.org/journal/Pages/Determining%20Withdrawal%20Rates%20Using%20Historical%20Data.aspx
Chang, W., Cheng, J., Allaire, J. J., Xie, Y., & McPherson, J. (2023). Shiny: Web application framework for R (R package version 1.8.0). https://CRAN.R-project.org/package=shiny
Cooley, P. L., Hubbard, C. M., & Walz, D. T. (1998). Retirement savings: Choosing a sustainable withdrawal rate. AAII Journal. https://www.aaii.com/journal/article/retirement-savings-choosing-a-sustainable-withdrawal-rate
Evans, J. R. (2020). Business analytics: Methods, models, and decisions (3rd ed.). Pearson.
Posit, PBC. (2024). RStudio: Integrated development environment for R. https://posit.co/download/rstudio-desktop/
R Core Team. (2024). R: A language and environment for statistical computing (Version 4.3.1). R Foundation for Statistical Computing. https://www.r-project.org/
U.S. Bureau of Labor Statistics. (2023). Consumer Expenditures Survey. https://www.bls.gov/cex/
U.S. Internal Revenue Service. (2024). Tax brackets and rates for 2024. https://www.irs.gov/newsroom/irs-provides-tax-inflation-adjustments-for-tax-year-2024
U.S. Social Security Administration. (2024). Retirement benefits. https://www.ssa.gov/benefits/retirement/
Wickham, H., & Grolemund, G. (2017). R for data science: Import, tidy, transform, visualize, and model data. O’Reilly Media. https://r4ds.hadley.nz/
5 About Me
My name is Joshua Lizardi. For the past 7 years, I have worked for various institutions teaching a wide range of courses in Math, Statistics and Technology. These included Quantitative Reasoning, Calculus ,Applied Technical Mathematics, Remedial Mathematics, Statistics, Computers & Office Automation, Introductory College Algebra, Intermediate College Algebra, Remedial Mathematics, Business Statistics.
I hold a bachelor’s in mathematics (Mercy College), a master’s in applied mathematics (Purdue University), and a master’s in data analytics (Western Governors University). I also hold a few certifications including “SAS Certified Statistical Business Analyst SAS 9”, “SAS Certified Base Programmer SAS 9”, “Oracle Database SQL Certified Associate”.
Subjects like mathematics, statistics, and computer science should not be taught as if they were spectator sports, the best way to learn these subjects is to perform them. Although understanding textbooks and lecture notes is valuable, the learning that comes from one’s own attempts at solving problems is the key to becoming competent in the subject overall. I have always been passionate about mathematics statistics and computer science, and I enjoy encouraging students to see the utility of these subjects.
SPECIALTIES
Applied Mathematics Applied Statistics Data Analytics Data Science Machine Learning Artificial Intelligence
SKILLS
R Python SQL SAS MiniTab Tableau Power BI Microsoft Office
https://www.youracclaim.com/users/joshua-lizardi