From e6cb19c86f62f194f010abad41dc5ae76e2f9127 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 31 Aug 2025 05:10:51 +0000 Subject: [PATCH] Add app.R this is shiny app for analysis of CSV file --- app.R | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 app.R diff --git a/app.R b/app.R new file mode 100644 index 0000000..ab38eaf --- /dev/null +++ b/app.R @@ -0,0 +1,545 @@ +# Theme only; all other functions are namespaced with pkg::fun +library(shinythemes) + +# ---------------- CSS: reduce layout thrash ---------------- +app_head <- shiny::tags$head( + shiny::tags$style(shiny::HTML(" + .sidebar-panel, .well { will-change: contents; } + .form-group { margin-bottom: 10px; } + ")) +) + +# ---------------- Helpers ---------------- +parse_clean <- function(x) readr::parse_number(as.character(x)) + +# Detect columns that LOOK numeric (e.g., '$1.2B', '12.3%', '1,234') +get_numeric_candidates <- function(df, min_non_na = 1) { + keep <- sapply(df, function(x) { + parsed <- suppressWarnings(readr::parse_number(as.character(x))) + sum(!is.na(parsed)) >= min_non_na + }) + names(df)[keep] +} + +# Quick DCF (10y two-stage + terminal multiple) +quick_dcf_value_per_share <- function( + fcf_t0, growth_1_5, growth_6_10, discount, terminal_mult, shares_out, net_debt = 0 +){ + if (!is.finite(fcf_t0) || fcf_t0 <= 0 || !is.finite(shares_out) || shares_out <= 0) + return(NA_real_) + fcf <- numeric(10) + fcf[1] <- fcf_t0 * (1 + growth_1_5) + for (t in 2:5) fcf[t] <- fcf[t-1] * (1 + growth_1_5) + for (t in 6:10) fcf[t] <- fcf[t-1] * (1 + growth_6_10) + term_val <- fcf[10] * terminal_mult + disc <- (1 + discount)^(1:10) + ev <- sum(fcf / disc) + term_val / disc[10] + as.numeric((ev - net_debt) / shares_out) +} + +clean_data <- function(df){ + names(df) <- trimws(names(df)) + num_cols <- c( + "Analyst Price Target","Analyst Price Target %", + "One Day%","One Month%","Three Month%","Six Month%","One Year%", + "Year To Date%","3 Years%","5 Years%","Price", + "Market Cap","Relative Volume","P/E Ratio", + "Best Price Target Upside","Beta","PEG Ratio", + "Enterprise Value (Last Report)","Employees","Smart Score", + "Outstanding Shares","Net Debt","Free Cash Flow","Revenue" + ) + + df <- df |> + dplyr::mutate(dplyr::across(tidyr::any_of(num_cols), parse_clean)) |> + dplyr::mutate(dplyr::across(where(is.character), ~ tidyr::replace_na(.x, "Unknown"))) + + # Parse any other numeric-looking columns + extra_numeric <- setdiff(get_numeric_candidates(df), num_cols) + if (length(extra_numeric)) { + df <- df |> + dplyr::mutate(dplyr::across(dplyr::all_of(extra_numeric), + ~ suppressWarnings(readr::parse_number(as.character(.x))))) + } + + numeric_columns <- names(df)[sapply(df, is.numeric)] + + # Impute by Sector if present; else global median + if ("Sector" %in% names(df)) { + df <- df |> + dplyr::group_by(.data$Sector) |> + dplyr::mutate(dplyr::across(dplyr::all_of(numeric_columns), ~ ifelse( + is.na(.x), + ifelse(all(is.na(.x)), + stats::median(df[[dplyr::cur_column()]], na.rm = TRUE), + stats::median(.x, na.rm = TRUE)), + .x))) |> + dplyr::ungroup() + } else { + df <- df |> + dplyr::mutate(dplyr::across(dplyr::all_of(numeric_columns), + ~ ifelse(is.na(.x), stats::median(.x, na.rm = TRUE), .x))) + } + + if (all(c("Enterprise Value (Last Report)","Market Cap") %in% names(df))) { + df <- df |> + dplyr::mutate(EV_MarketCap = `Enterprise Value (Last Report)` / `Market Cap`) + } + df +} + +dual_range_ui <- function(minId, maxId, sliderId, label, minV, maxV, stepV){ + shiny::tagList( + shiny::fluidRow( + shiny::column(6, shiny::numericInput(minId, paste(label, "Min:"), value = minV, step = stepV)), + shiny::column(6, shiny::numericInput(maxId, paste(label, "Max:"), value = maxV, step = stepV)) + ), + shiny::sliderInput(sliderId, paste("Range for", label), min = minV, max = maxV, value = c(minV, maxV), step = stepV) + ) +} + +# Debounced, loop-safe two-way sync +sync_dual <- function(input, session, sliderId, minId, maxId, delay_ms = 200){ + min_rx <- shiny::debounce(shiny::reactive(input[[minId]]), delay_ms) + max_rx <- shiny::debounce(shiny::reactive(input[[maxId]]), delay_ms) + + # Slider -> numerics + shiny::observeEvent(input[[sliderId]], { + v <- input[[sliderId]]; if (is.null(v) || length(v) != 2) return() + if (!identical(input[[minId]], v[1])) shiny::updateNumericInput(session, minId, value = v[1]) + if (!identical(input[[maxId]], v[2])) shiny::updateNumericInput(session, maxId, value = v[2]) + }, ignoreInit = TRUE, priority = 10) + + # Numerics (debounced) -> slider + shiny::observeEvent({ min_rx(); max_rx() }, { + s <- isolate(input[[sliderId]]); if (is.null(s) || length(s) != 2) return() + a <- suppressWarnings(as.numeric(min_rx())) + b <- suppressWarnings(as.numeric(max_rx())) + if (!is.finite(a) || !is.finite(b)) return() + if (a > b) { tmp <- a; a <- b; b <- tmp } + new_val <- c(a, b) + if (!identical(s, new_val)) { + shiny::freezeReactiveValue(input, sliderId) + shiny::updateSliderInput(session, sliderId, value = new_val) + } + }, ignoreInit = TRUE, priority = 10) +} + +# ---------------- UI ---------------- +ui <- shiny::fluidPage( + app_head, + theme = shinythemes::shinytheme("flatly"), + shiny::titlePanel("Stock Screener • Stable Dual-Inputs + 2-min Valuation"), + shiny::sidebarLayout( + shiny::sidebarPanel( + width = 4, # stabilize sidebar width + shiny::fileInput("file", "Upload CSV", accept = ".csv"), + shiny::uiOutput("sectorFilterUI"), + shiny::uiOutput("peRangeUI"), + shiny::uiOutput("smartScoreRangeUI"), + shiny::uiOutput("employeeSliderUI"), + + # Placeholders so controls render immediately + shiny::selectInput("xAxis", "X-Axis", choices = "-- load a CSV --", selected = "-- load a CSV --"), + shiny::uiOutput("xRangeUI"), + shiny::selectInput("yAxis", "Y-Axis", choices = "-- load a CSV --", selected = "-- load a CSV --"), + shiny::uiOutput("yRangeUI"), + + shiny::checkboxGroupInput("positiveFilters", "Show only positive values for:", + choices = c("One Day%","One Month%","Three Month%","Six Month%","Year To Date%","5 Years%")), + shiny::checkboxInput("showTable", "Display filtered table", FALSE), + + shiny::downloadButton("downloadReport", "Download HTML Report"), + shiny::downloadButton("downloadData", "Download Filtered CSV") + ), + shiny::mainPanel( + shiny::tabsetPanel( + shiny::tabPanel("Scatter Plot", plotly::plotlyOutput("customPlot", height = "600px")), + shiny::tabPanel("Outliers", DT::DTOutput("outlierTable")), + shiny::tabPanel("Correlation Heatmap", shiny::plotOutput("heatmapPlot", height = "600px")), + shiny::tabPanel("Filtered Table", shiny::conditionalPanel("input.showTable == true", DT::DTOutput("filteredTable"))), + + # 2-minute Valuation + shiny::tabPanel("Valuation (2-min)", + shiny::fluidRow( + shiny::column( + width = 4, + shiny::uiOutput("val_ticker_ui"), + shiny::hr(), + shiny::numericInput("val_fcf", "Current FCF (or Owner Earnings)", value = NA, min = 0, step = 1), + shiny::numericInput("val_growth_1_5", "Growth Y1–5 (decimal, e.g., 0.10 = 10%)", value = 0.10, step = 0.01), + shiny::numericInput("val_growth_6_10","Growth Y6–10 (decimal)", value = 0.05, step = 0.01), + shiny::numericInput("val_discount", "Discount Rate (decimal)", value = 0.10, step = 0.005), + shiny::numericInput("val_term_mult","Terminal EV/FCF Multiple (Year 10)", value = 15, step = 0.5), + shiny::numericInput("val_shares", "Shares Outstanding", value = NA, min = 0, step = 1), + shiny::numericInput("val_net_debt","Net Debt (Debt - Cash)", value = 0, step = 1), + shiny::actionButton("val_calc", "Calculate") + ), + shiny::column( + width = 8, + shiny::uiOutput("val_summary_ui"), + shiny::br(), + shiny::plotOutput("val_heatmap", height = "400px"), + shiny::br(), + shiny::downloadButton("val_download", "Download Valuation Report") + ) + ) + ) + ) + ) + ) +) + +# ---------------- Server ---------------- +server <- function(input, output, session){ + + # Robust CSV reader: try comma, fallback to semicolon + dataset <- shiny::reactive({ + shiny::req(input$file) + df_try <- tryCatch(readr::read_csv(input$file$datapath, show_col_types = FALSE), + error = function(e) NULL) + if (is.null(df_try) || ncol(df_try) <= 1) { + df_try <- tryCatch(readr::read_delim(input$file$datapath, delim = ";", show_col_types = FALSE), + error = function(e) NULL) + } + shiny::validate(shiny::need(!is.null(df_try) && ncol(df_try) > 0, + "Could not read the CSV. Make sure it has a header row.")) + clean_data(df_try) + }) + + output$sectorFilterUI <- shiny::renderUI({ + shiny::req(dataset()) + if ("Sector" %in% names(dataset())) { + shiny::selectInput("sectorFilter", "Sector", + choices = c("All", sort(unique(dataset()$Sector))), + selected = "All") + } + }) + + # Populate X/Y dropdowns (numeric + numeric-looking) + shiny::observeEvent(dataset(), { + df <- dataset() + already_numeric <- names(df)[vapply(df, is.numeric, logical(1))] + parsed_candidates <- get_numeric_candidates(df) + num_cols <- sort(unique(c(already_numeric, parsed_candidates))) + + if (length(num_cols) == 0) { + shiny::showNotification("No numeric columns detected in the uploaded CSV.", + type = "error", duration = 8) + shiny::updateSelectInput(session, "xAxis", choices = "-- no numeric columns --", selected = "-- no numeric columns --") + shiny::updateSelectInput(session, "yAxis", choices = "-- no numeric columns --", selected = "-- no numeric columns --") + return() + } + + default_x <- intersect(c("P/E Ratio","Price","Market Cap"), num_cols) + default_y <- intersect(c("Smart Score","Analyst Price Target %","Analyst Price Target"), num_cols) + + shiny::onFlushed(function() { + shiny::updateSelectInput(session, "xAxis", + choices = num_cols, + selected = if (length(default_x)) default_x[1] else num_cols[1]) + shiny::updateSelectInput(session, "yAxis", + choices = num_cols, + selected = if (length(default_y)) default_y[1] else num_cols[1]) + }, once = TRUE) + }, ignoreInit = FALSE) + + # -------- Range UIs (parse_number before range()) -------- + output$peRangeUI <- shiny::renderUI({ + shiny::req(dataset()); if (!"P/E Ratio" %in% names(dataset())) return(NULL) + vals <- suppressWarnings(readr::parse_number(as.character(dataset()[["P/E Ratio"]]))) + rng <- round(range(vals, na.rm = TRUE), 2) + dual_range_ui("peMin","peMax","peRange","P/E Ratio", rng[1], rng[2], 0.01) + }) + output$smartScoreRangeUI <- shiny::renderUI({ + dual_range_ui("smartMin","smartMax","smartScoreRange","Smart Score", 0, 10, 1) + }) + output$employeeSliderUI <- shiny::renderUI({ + shiny::req(dataset()); if (!"Employees" %in% names(dataset())) return(NULL) + vals <- suppressWarnings(readr::parse_number(as.character(dataset()[["Employees"]]))) + rng <- round(range(vals, na.rm = TRUE), 0) + dual_range_ui("empMin","empMax","employeeRange","Employees", rng[1], rng[2], 1) + }) + output$xRangeUI <- shiny::renderUI({ + shiny::req(dataset(), input$xAxis) + vals <- suppressWarnings(readr::parse_number(as.character(dataset()[[input$xAxis]]))) + rng <- round(range(vals, na.rm = TRUE), 2) + shiny::validate(shiny::need(all(is.finite(rng)), paste("No numeric data detected for", input$xAxis))) + dual_range_ui("xMin","xMax","xRange", input$xAxis, rng[1], rng[2], 0.01) + }) + output$yRangeUI <- shiny::renderUI({ + shiny::req(dataset(), input$yAxis) + vals <- suppressWarnings(readr::parse_number(as.character(dataset()[[input$yAxis]]))) + rng <- round(range(vals, na.rm = TRUE), 2) + shiny::validate(shiny::need(all(is.finite(rng)), paste("No numeric data detected for", input$yAxis))) + dual_range_ui("yMin","yMax","yRange", input$yAxis, rng[1], rng[2], 0.01) + }) + + # -------- Stable sync for all pairs -------- + shiny::observe({ if (!is.null(input$peRange)) sync_dual(input, session, "peRange","peMin","peMax") }) + shiny::observe({ if (!is.null(input$smartScoreRange)) sync_dual(input, session, "smartScoreRange","smartMin","smartMax") }) + shiny::observe({ if (!is.null(input$employeeRange)) sync_dual(input, session, "employeeRange","empMin","empMax") }) + shiny::observe({ if (!is.null(input$xRange)) sync_dual(input, session, "xRange","xMin","xMax") }) + shiny::observe({ if (!is.null(input$yRange)) sync_dual(input, session, "yRange","yMin","yMax") }) + + # -------- Filtering (parse numbers before filtering) -------- + filteredData <- shiny::reactive({ + shiny::req(dataset(), input$xAxis, input$yAxis) + d <- dataset() + + if (!is.null(input$peRange) && "P/E Ratio" %in% names(d)) { + pe <- suppressWarnings(readr::parse_number(as.character(d[["P/E Ratio"]]))) + d <- d[!is.na(pe) & pe >= input$peRange[1] & pe <= input$peRange[2], , drop = FALSE] + } + if (!is.null(input$smartScoreRange) && "Smart Score" %in% names(d)) { + ss <- suppressWarnings(readr::parse_number(as.character(d[["Smart Score"]]))) + d <- d[!is.na(ss) & ss >= input$smartScoreRange[1] & ss <= input$smartScoreRange[2], , drop = FALSE] + } + + xv <- suppressWarnings(readr::parse_number(as.character(d[[input$xAxis]]))) + yv <- suppressWarnings(readr::parse_number(as.character(d[[input$yAxis]]))) + if (!is.null(input$xRange)) d <- d[!is.na(xv) & xv >= input$xRange[1] & xv <= input$xRange[2], , drop = FALSE] + if (!is.null(input$yRange)) d <- d[!is.na(yv) & yv >= input$yRange[1] & yv <= input$yRange[2], , drop = FALSE] + + if (!is.null(input$employeeRange) && "Employees" %in% names(d)) { + emp <- suppressWarnings(readr::parse_number(as.character(d[["Employees"]]))) + d <- d[!is.na(emp) & emp >= input$employeeRange[1] & emp <= input$employeeRange[2], , drop = FALSE] + } + + for (col in input$positiveFilters) { + if (col %in% names(d)) { + pv <- suppressWarnings(readr::parse_number(as.character(d[[col]]))) + d <- d[!is.na(pv) & pv > 0, , drop = FALSE] + } + } + if (!is.null(input$sectorFilter) && "Sector" %in% names(d) && input$sectorFilter != "All") { + d <- dplyr::filter(d, Sector == input$sectorFilter) + } + d + }) + + # -------- Outputs -------- + output$customPlot <- plotly::renderPlotly({ + shiny::req(input$xAxis, input$yAxis) + plotly::plot_ly( + filteredData(), + x = ~suppressWarnings(readr::parse_number(as.character(get(input$xAxis)))), + y = ~suppressWarnings(readr::parse_number(as.character(get(input$yAxis)))), + text = ~Ticker, + type = 'scatter', mode = 'markers+text', textposition = 'top center' + ) |> + plotly::layout(title = paste(input$xAxis, "vs", input$yAxis), + xaxis = list(title = input$xAxis), + yaxis = list(title = input$yAxis)) + }) + + output$filteredTable <- DT::renderDT({ + shiny::req(input$showTable) + DT::datatable(filteredData(), options = list(scrollX = TRUE)) + }) + + output$outlierTable <- DT::renderDT({ + shiny::req(filteredData(), input$xAxis, input$yAxis) + d <- filteredData() + x <- suppressWarnings(readr::parse_number(as.character(d[[input$xAxis]]))) + y <- suppressWarnings(readr::parse_number(as.character(d[[input$yAxis]]))) + out <- d[ + (x < stats::quantile(x, 0.05, na.rm = TRUE) | x > stats::quantile(x, 0.95, na.rm = TRUE)) | + (y < stats::quantile(y, 0.05, na.rm = TRUE) | y > stats::quantile(y, 0.95, na.rm = TRUE)), + , drop = FALSE + ] + DT::datatable(out, options = list(scrollX = TRUE)) + }) + + output$heatmapPlot <- shiny::renderPlot({ + shiny::req(filteredData()) + num <- dplyr::select(filteredData(), dplyr::where(is.numeric)) + if (ncol(num) < 2) return(NULL) + corr <- stats::cor(num, use = "complete.obs") + m <- reshape2::melt(corr) + ggplot2::ggplot(m, ggplot2::aes(Var1, Var2, fill = value)) + + ggplot2::geom_tile() + + ggplot2::scale_fill_gradient2(low = "blue", mid = "white", high = "red", midpoint = 0) + + ggplot2::theme_minimal() + + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, hjust = 1)) + + ggplot2::labs(title = "Correlation Heatmap", x = "", y = "") + }) + + # ----- Valuation tab ----- + output$val_ticker_ui <- shiny::renderUI({ + shiny::req(dataset()) + df <- dataset() + choice_col <- if ("Ticker" %in% names(df)) "Ticker" else if ("Name" %in% names(df)) "Name" else NULL + if (is.null(choice_col)) shiny::helpText("No Ticker/Name column found.") else + shiny::selectInput("val_ticker", "Select Ticker/Name", choices = unique(df[[choice_col]])) + }) + + shiny::observeEvent(input$val_ticker, { + shiny::req(dataset(), input$val_ticker) + df <- dataset() + row <- df[(df$Ticker == input$val_ticker) | (df$Name == input$val_ticker), , drop = FALSE] + if (nrow(row) == 0) return() + + fcf_guess <- if ("Free Cash Flow" %in% names(row)) row$`Free Cash Flow`[1] else NA + if (!is.finite(fcf_guess) || is.na(fcf_guess) || fcf_guess <= 0) { + if (all(c("Market Cap","P/E Ratio") %in% names(row))) { + if (is.finite(row$`Market Cap`[1]) && is.finite(row$`P/E Ratio`[1]) && row$`P/E Ratio`[1] > 0) { + fcf_guess <- 0.8 * (row$`Market Cap`[1] / row$`P/E Ratio`[1]) + } + } + } + + shares_guess <- if ("Outstanding Shares" %in% names(row)) row$`Outstanding Shares`[1] else NA + if (!is.finite(shares_guess) || is.na(shares_guess) || shares_guess <= 0) { + if (all(c("Market Cap","Price") %in% names(row))) { + if (is.finite(row$`Market Cap`[1]) && is.finite(row$Price[1]) && row$Price[1] > 0) { + shares_guess <- row$`Market Cap`[1] / row$Price[1] + } + } + } + + net_debt_guess <- if ("Net Debt" %in% names(row)) row$`Net Debt`[1] else 0 + + shiny::updateNumericInput(session, "val_fcf", value = if (is.finite(fcf_guess)) round(fcf_guess, 2) else NA) + shiny::updateNumericInput(session, "val_shares", value = if (is.finite(shares_guess)) round(shares_guess, 0) else NA) + shiny::updateNumericInput(session, "val_net_debt", value = if (is.finite(net_debt_guess)) round(net_debt_guess, 2) else 0) + }, ignoreInit = TRUE) + + output$val_summary_ui <- shiny::renderUI({ + shiny::req(input$val_calc) + shiny::isolate({ + price_now <- NA_real_ + df <- dataset() + if ("Price" %in% names(df) && !is.null(input$val_ticker)) { + row <- df[(df$Ticker == input$val_ticker) | (df$Name == input$val_ticker), , drop = FALSE] + if (nrow(row)) price_now <- row$Price[1] + } + iv <- quick_dcf_value_per_share( + fcf_t0 = input$val_fcf, + growth_1_5 = input$val_growth_1_5, + growth_6_10 = input$val_growth_6_10, + discount = input$val_discount, + terminal_mult= input$val_term_mult, + shares_out = input$val_shares, + net_debt = input$val_net_debt + ) + mos <- if (is.finite(iv) && is.finite(price_now)) (iv - price_now) / price_now else NA_real_ + + verdict <- if (is.finite(mos)) { + if (mos >= 0.3) "BUY (≥30% MOS)" + else if (mos >= 0.0) "HOLD (0–30% MOS)" + else "SELL (<0% MOS)" + } else "—" + + shiny::tagList( + shiny::h4("Valuation Summary"), + shiny::HTML(sprintf("Intrinsic Value / Share: %s
", + if (is.finite(iv)) format(round(iv, 2), big.mark = ",") else "—")), + shiny::HTML(sprintf("Current Price: %s
", + if (is.finite(price_now)) format(round(price_now, 2), big.mark = ",") else "—")), + shiny::HTML(sprintf("Margin of Safety: %s", + if (is.finite(mos)) paste0(round(100*mos, 1), "%") else "—")), + shiny::hr(), + shiny::strong("Verdict: "), shiny::span(verdict) + ) + }) + }) + + output$val_heatmap <- shiny::renderPlot({ + shiny::req(input$val_calc) + shiny::isolate({ + disc_grid <- seq(max(0.05, input$val_discount - 0.03), + input$val_discount + 0.03, by = 0.01) + mult_grid <- seq(max(5, input$val_term_mult - 5), + input$val_term_mult + 5, by = 1) + grid <- expand.grid(discount = disc_grid, multiple = mult_grid) + grid$iv <- mapply(function(d, m) { + quick_dcf_value_per_share( + fcf_t0 = input$val_fcf, + growth_1_5 = input$val_growth_1_5, + growth_6_10 = input$val_growth_6_10, + discount = d, + terminal_mult= m, + shares_out = input$val_shares, + net_debt = input$val_net_debt + ) + }, grid$discount, grid$multiple) + ggplot2::ggplot(grid, ggplot2::aes(x = discount, y = multiple, fill = iv)) + + ggplot2::geom_tile() + + ggplot2::scale_fill_gradient(low = "white", high = "darkgreen", na.value = "grey90") + + ggplot2::scale_x_continuous(labels = scales::percent) + + ggplot2::labs(title = "Intrinsic Value Sensitivity", + x = "Discount Rate", y = "Terminal EV/FCF Multiple", + fill = "IV/Share") + + ggplot2::theme_minimal() + }) + }) + + # ----- Downloads ----- + output$val_download <- shiny::downloadHandler( + filename = function(){ "valuation_report.html" }, + content = function(file){ + tempRmd <- tempfile(fileext = ".Rmd") + report <- sprintf(" + --- + title: '2-Minute Valuation Report' + output: html_document + --- + ```{r setup, include=FALSE} + library(dplyr); library(ggplot2); library(scales) + quick_dcf_value_per_share <- %s + ``` + ## Inputs + - Ticker/Name: %s + - FCF now: %s + - Growth Y1–5: %s + - Growth Y6–10: %s + - Discount: %s + - Terminal EV/FCF: %s + - Shares Out: %s + - Net Debt: %s + + ```{r} + iv <- quick_dcf_value_per_share(%s, %s, %s, %s, %s, %s, %s) + iv + ``` + ", deparse(quick_dcf_value_per_share), + ifelse(is.null(input$val_ticker), "—", input$val_ticker), + input$val_fcf, input$val_growth_1_5, input$val_growth_6_10, + input$val_discount, input$val_term_mult, input$val_shares, input$val_net_debt, + input$val_fcf, input$val_growth_1_5, input$val_growth_6_10, + input$val_discount, input$val_term_mult, input$val_shares, input$val_net_debt + ) + writeLines(report, tempRmd) + rmarkdown::render(tempRmd, output_file = file, envir = new.env(parent = globalenv())) + } + ) + + output$downloadReport <- shiny::downloadHandler( + filename = function(){ "stock_analysis_report.html" }, + content = function(file){ + tempRmd <- tempfile(fileext = ".Rmd") + report <- " + --- + title: 'Stock Analysis Report' + output: html_document + --- + ```{r setup, include=FALSE} + library(dplyr) + ``` + ```{r} + df <- read.csv('${input$file$datapath}') + summary(df) + ``` + " + writeLines(report, tempRmd) + rmarkdown::render(tempRmd, output_file = file, envir = new.env(parent = globalenv())) + } + ) + + output$downloadData <- shiny::downloadHandler( + filename = function(){ 'filtered_stock_data.csv' }, + content = function(file){ utils::write.csv(filteredData(), file, row.names = FALSE) } + ) +} + +shiny::shinyApp(ui = ui, server = server)