Allow user to upload a shapefile in shiny - r

I am making a shiny app that allows the user to upload a shapefile using the sf package. When I select the .shp file via the Browse window, I get an error. How can I allow the user to upload a shapefile, that then get it read by st_read' or readOGR. And, I don't know why st_read is going to C:\Users\Ed\AppData... as this is not location of the shapefile.
library(shiny)
library(shinydahsboard)
library(sf)
UI
ui = navbarPage("Project Eddy", theme = shinytheme("sandstone"),
tabPanel("Location",
sidebarLayout(sidebarPanel(fileInput("shp", "Please choose a Shapefile",
multiple = F,
".shp")),
mainPanel(plotlyOutput(outputId = "Area")))))
Server
server = function(input, output, session) {
myshp.df = reactive({
# input$shp will be NULL initially. After the user selects
# and uploads a file, head of that data file by default,
# or all rows if selected, will be shown.
req(input$shp)
df = st_read(dsn = input$shp$datapath,
quite = T)
if(input$disp == "head") {
return(head(df))
}
else {
return(df)
}
})
output$Area = renderPlotly({
req(myshp.df())
a = myshp.df
c = leaflet(a) %>%
addPolygons(stroke = FALSE, fillOpacity = 0.5, smoothFactor = 0.5) %>%
addProviderTiles('Esri.WorldImagery')
})
})
Error
Warning in CPL_read_ogr(dsn, layer, query, as.character(options), quiet, :
GDAL Error 4: Unable to open C:\Users\Ed\AppData\Local\Temp\RtmpioUU3m\b0cd5b1eb5c4fe4219e6c114\0.shx or C:\Users\Ed\AppData\Local\Temp\RtmpioUU3m\b0cd5b1eb5c4fe4219e6c114\0.SHX. Set SHAPE_RESTORE_SHX config option to YES to restore or create it.
Warning: Error in : Cannot open "C:\Users\Ed\AppData\Local\Temp\RtmpioUU3m\b0cd5b1eb5c4fe4219e6c114\0.shp"; The source could be corrupt or not supported. See `st_drivers()` for a list of supported formats.
128:

ESRI shapefiles are known troublemakers, as they live over multiple files - the single *.shp file is not enough for your shiny app to work with.
Consider a solution proposed by user fiorepalombina on RStudio Community forum: https://community.rstudio.com/t/shinyfiles-and-shapefiles/89099

To read-in a shapefile, the user must submit at minimum the mandatory files (.shp, .shx and .dbf). Once files are uploaded, you can access the location and name via $datapath and $name.
By default, shiny names file inputs like this:
C:\Users\DWISME~1\AppData\Local\Temp\17\RtmpiOjVGv/6903ae29a41daccceee4b8a5/0.dbf
C:\Users\DWISME~1\AppData\Local\Temp\17\RtmpiOjVGv/6903ae29a41daccceee4b8a5/1.prj
C:\Users\DWISME~1\AppData\Local\Temp\17\RtmpiOjVGv/6903ae29a41daccceee4b8a5/2.sbn
C:\Users\DWISME~1\AppData\Local\Temp\17\RtmpiOjVGv/6903ae29a41daccceee4b8a5/3.sbx
C:\Users\DWISME~1\AppData\Local\Temp\17\RtmpiOjVGv/6903ae29a41daccceee4b8a5/4.shp
C:\Users\DWISME~1\AppData\Local\Temp\17\RtmpiOjVGv/6903ae29a41daccceee4b8a5/5.shx
My approach is to create a function that accesses the file input location and change the directory:
library(shiny)
library(sf)
library(purrr)
ui <- fluidPage(
br(),
fluidRow(column(6, offset = 3,
fileInput("shp", label = "Input Shapfile (.shp,.dbf,.sbn,.sbx,.shx,.prj)",
width = "100%",
accept = c(".shp",".dbf",".sbn",".sbx",".shx",".prj"), multiple=TRUE))),
br(),
fluidRow(column(8, offset = 2,
p("input$shp$datapath" , style = "font-weight: bold"),
verbatimTextOutput("shp_location", placeholder = T))),
br(),
fluidRow(column(8, offset = 2,
p("input$shp$name" , style = "font-weight: bold"),
verbatimTextOutput("shp_name", placeholder = T)))
)
server <- function(input, output, session) {
# Read-in shapefile function
Read_Shapefile <- function(shp_path) {
infiles <- shp_path$datapath # get the location of files
dir <- unique(dirname(infiles)) # get the directory
outfiles <- file.path(dir, shp_path$name) # create new path name
name <- strsplit(shp_path$name[1], "\\.")[[1]][1] # strip name
purrr::walk2(infiles, outfiles, ~file.rename(.x, .y)) # rename files
x <- read_sf(file.path(dir, paste0(name, ".shp"))) # read-in shapefile
return(x)
}
# Read-shapefile once user submits files
observeEvent(input$shp, {
user_shp <- Read_Shapefile(input$shp)
plot(user_shp) # plot to R console
# Print original file path location and file name to UI
output$shp_location <- renderPrint({
full_path <- strsplit(input$shp$datapath," ")
purrr::walk(full_path, ~cat(.x, "\n"))
})
output$shp_name <- renderPrint({
name_split <- strsplit(input$shp$name," ")
purrr::walk(name_split, ~cat(.x, "\n"))
})
})
}
shinyApp(ui, server)

Related

Get correct input value to display in download file name with rmarkdown word doc from downloadHandler in Shiny App

I have built a Shiny app and my client wants to be able to download some of its content to a Word document. Using the steps laid out here (with a correction to the sample code per this post), I have a working download button. Instead of a generic default file name like "report.docx", I want to generate a standard file name that includes the name of the subject of the report, as selected from an input value selection from a dropdown list in the UI of the Shiny app. This seems like it should be pretty easy, and I get very close: I can get the file name to include the default value for that input selection, but I can't get it to update correctly when the user makes a new selection.
The following code is a simplified example of what I want and generates the issue I am experiencing:
Data:
cities <- c("Atlanta", "Boston", "Chicago", "Detroit")
values <- c(100, 200, 300, 400)
test_df <- cbind.data.frame(cities, values)
saveRDS(test_df, file = "C:/Repos/Test Report Name/app/test_df.rds")
UI:
library(shinydashboard)
library(shinyjs)
library(shinycssloaders)
library(shinyWidgets)
library(reactable)
ui <- fluidPage(
mainPanel(
fluidRow(
selectInput('test',
label = 'Test',
choices = sort(unique(test_df$cities)),
multiple = FALSE,
selected = sort(unique(test_df$cities))[1])
),
fluidRow(
reactableOutput("data")
),
fluidRow(
downloadButton("report", "Generate Report")
)
)
)
Server:
library(tidyverse)
library(reactable)
library(shinyWidgets)
server <- function(input, output) {
output$data <- renderReactable({
x <- test_df %>%
dplyr::filter(cities %in% input$test)
reactable(x)
})
output$report <- downloadHandler(
# For PDF output, change this to "report.pdf"
filename = paste(input$test, "Summary", Sys.Date(), ".docx"), ### This paste function is the crucial part; I get the default value (Atlanta) no matter which option I select.
content = function(file) {
# Copy the report file to a temporary directory before processing it, in
# case we don't have write permissions to the current working dir (which
# can happen when deployed).
tempReport <- file.path(tempdir(), "report.Rmd")
file.copy("report.Rmd", tempReport, overwrite = TRUE)
# Set up parameters to pass to Rmd document
params <- list(selection = input$test)
# Knit the document, passing in the `params` list, and eval it in a
# child of the global environment (this isolates the code in the document
# from the code in this app).
output <- rmarkdown::render(input = tempReport,
params = params,
envir = new.env(parent = globalenv())
)
file.copy(output, file)
}
)
}
Markdown file:
---
title: "`r params$selection` Report"
output: word_document
params:
selection: NA
---
```{r}
library(tidyverse)
x <- test_df %>%
filter(cities %in% params$selection)
print(x)
```
So, I run this, and everything works, except for the file name. I select Boston in the dropdown, and I get markdown file with Boston in the title and Boston data in the table, but the file is still called "Atlanta Summary 1-3-2023.docx". I have tried creating new variables in the server file that just take the input$test value, such as below:
print_input_name <- reactive({
x <- input$test
x
})
## Error in as.vector: cannot coerce type 'closure' to vector of type 'character'
print_input_name <- eventReactive({
x <- input$test
x
})
## Error in is_quosure(expr) : argument "expr" is missing, with no default
I tried wrapping all that downloadHandler() statement in observeEvent(input$test, {...}) but that didn't work either. I also tried just naming the object after the input directly (x <- input$test) and naming the object output$x, but that didn't work either. Also trying calling it params$selection like it is in the RMD file but that obviously didn't work. So I'm stumped about how to get that input selection to be stored as an object in the server that I can reference. Any help appreciated, I am pretty new to Shiny and am still learning the ins and outs of reactive elements.
According to the docs (?downloadHandler) the filename argument should be
A string of the filename, [...]; or a function that returns such a string. (Reactive values and functions may be used from this function.)
Hence to fix your issue use
filename = function() paste(input$test, "Summary", Sys.Date(), ".docx")
See also the examples section of the docs.
Complete reproducible code:
cities <- c("Atlanta", "Boston", "Chicago", "Detroit")
values <- c(100, 200, 300, 400)
test_df <- data.frame(cities, values)
library(shiny)
library(reactable)
library(tidyverse)
ui <- fluidPage(
mainPanel(
fluidRow(
selectInput("test",
label = "Test",
choices = sort(unique(test_df$cities)),
multiple = FALSE,
selected = sort(unique(test_df$cities))[1]
)
),
fluidRow(
reactableOutput("data")
),
fluidRow(
downloadButton("report", "Generate Report")
)
)
)
server <- function(input, output) {
output$data <- renderReactable({
x <- test_df %>%
dplyr::filter(cities %in% input$test)
reactable(x)
})
output$report <- downloadHandler(
filename = function() paste(input$test, "Summary", Sys.Date(), ".docx"),
content = function(file) {
tempReport <- file.path(tempdir(), "report.Rmd")
file.copy("report.Rmd", tempReport, overwrite = TRUE)
params <- list(selection = input$test)
output <- rmarkdown::render(
input = tempReport,
params = params,
envir = new.env(parent = globalenv())
)
file.copy(output, file)
}
)
}
shinyApp(ui, server)
#>
#> Listening on http://127.0.0.1:3354

Bug with ShinyFeedback not displaying correct label with File Input

I'm building an app that takes excel files as an input. To protect against errors, I first check if the file is an .xls or .xslx, and then I check if it contains the right content based on the sheet names.
I'm encountering that the first time I upload the right file, the app works as expected:
But when I hit 'browse' and try to upload again, I get this:
It only shows the right label on the first time.
This doesn't happen if I upload an excel file with the wrong info multiple times
If I then try to upload a different type of file, I get this:
Here's the simple code
library(shiny)
library(readxl)
library(tools)
library(shinyFeedback)
ui <- fluidPage(
useShinyFeedback(),
fileInput("file", "upload file", multiple = FALSE),
tableOutput("data")
)
server <- function(input, output, session) {
data <- reactive({
req(input$file)
path <- input$file$datapath
good_file <- file_ext(path) == "xls" || file_ext(path) == "xlsx"
feedbackDanger(inputId = "file",
show=!good_file,
color = "orange",
text = "Upload only .xls or .xlsx files")
req(good_file)
good_sheet <- "Special Sheet" %in% excel_sheets(path)
feedbackDanger(inputId = "file",
show=!good_sheet,
color = "#E6007E",
text = "File must contain the special sheet'")
req(good_sheet)
read_excel(path)
})
output$data = renderTable(data())
}
shinyApp(ui, server)
Not sure, but perhaps this is due to the fact that the two feedbacks are the same.
Here is a solution:
server <- function(input, output, session) {
data <- reactive({
req(input$file)
hideFeedback("file") # hide feedback, if any
path <- input$file$datapath
good_file <- file_ext(path) == "xls" || file_ext(path) == "xlsx"
if(!good_file){
showFeedbackDanger(inputId = "file",
color = "orange",
text = "Upload only .xls or .xlsx files")
}
req(good_file)
good_sheet <- "Special Sheet" %in% excel_sheets(path)
if(!good_sheet){
showFeedbackDanger(inputId = "file",
color = "#E6007E",
text = "File must contain the special sheet'")
}
req(good_sheet)
read_excel(path)
})
output$data = renderTable(data())
}

How to download multiple files from R/Shiny app?

There are a number of different Q/A's regarding this topic on SO, but none that I have been able to find that fit my use-case. I am also very surprised that RStudio / the Shiny developers themselves have not come out with some documentation on how to do this. Regardless, take this example application:
library(shiny)
library(glue)
library(tidyverse)
# Define UI for application
ui <- fluidPage(
# Application title
titlePanel("Test Multi-File Download"),
p("I hope this works!"),
downloadButton(
outputId = "download_btn",
label = "Download",
icon = icon("file-download")
)
)
# Define server logic
server <- function(input, output) {
#datasets stored in reactiveValues list
to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
blahblah <- iris
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".csv", sep = "")
},
content = function(file){
#works
#readr::write_csv(blahblah, file)
#Attempt 1
# #create some temp directory
# temp_directory <- tempdir()
# browser()
# reactiveValuesToList(to_download) %>%
# #x is data, y is name
# imap(function(x,y){
# #browser()
# #check if data is not null
# if(!is.null(x)){
# #create file name based on name of dataset
# file_name <- glue("{y}_data.csv")
# #write file to temp directory
# readr::write_csv(x, file_name)
# }
# })
# zip::zip(
# zipfile = file,
# files = ls(temp_directory),
# root = temp_directory
# )
}
)
}
# Run the application
shinyApp(ui = ui, server = server)
I have some datasets that are stored in a reactiveValues list, and I would like the user to be able to download them all. Ideally, I'd like for them just to be able to download multiple files all at once, rather than having to zip them up, and then download a .zip file. Another option I would be okay with is to add each dataset to an Excel sheet, then download the multi-sheet Excel file.
My general thought process (on the former) is as follows:
Download button gets pressed
create some temporary directory
write (the not NULL) datasets contained in to_download reactiveValues list to this directory
zip the temp directory and download
I feel like I am very close, however I have not been able to successfully get this work yet. Any ideas?
Edit 1: I am aware of the proposed answer here, but would like to avoid using setwd() because I believe it is bad practice to mess with working directories from within a Shiny application.
A few things edited and it's working:
using dir instead of ls inside the zip::zip call to show the contents of the temp directory (ls lists R environment rather than directory contents)
as a further suggestion: making a new, unique folder inside tempdir() to ensure only relevant files are added.
library(shiny)
library(glue)
library(tidyverse)
# Define UI for application
ui <- fluidPage(
# Application title
titlePanel("Test Multi-File Download"),
p("I hope this works!"),
downloadButton(
outputId = "download_btn",
label = "Download",
icon = icon("file-download")
)
)
# Define server logic
server <- function(input, output) {
#datasets stored in reactiveValues list
to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
blahblah <- iris
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".zip", sep = "")
},
content = function(file){
temp_directory <- file.path(tempdir(), as.integer(Sys.time()))
dir.create(temp_directory)
reactiveValuesToList(to_download) %>%
imap(function(x,y){
if(!is.null(x)){
file_name <- glue("{y}_data.csv")
readr::write_csv(x, file.path(temp_directory, file_name))
}
})
zip::zip(
zipfile = file,
files = dir(temp_directory),
root = temp_directory
)
},
contentType = "application/zip"
)
}
shinyApp(ui = ui, server = server)
In my own Shiny app I had used a multi-worksheet approach as you suggested above. An alternative setup which works to produce a multi-sheet xlsx workbook using openxlsx could be:
...
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".xlsx", sep = "")
},
content = function(file){
wb <- createWorkbook()
reactiveValuesToList(to_download) %>%
imap(function(x,y){
if(!is.null(x)){
addWorksheet(wb, sheetName = y)
writeData(wb, x, sheet = y)
}
})
saveWorkbook(wb, file = file)
},
contentType = "file/xlsx"
)
...
Created on 2021-12-16 by the reprex package (v2.0.1)

Downloading large files using Download Handler gives error Error in .jcall: java.lang.OutOfMemoryError: GC overhead limit exceeded

I am building an app which uploads a .xlsx file using fileInput. The app let's you make some edits to the data table and the user can download the edited data table. However while trying to download large files (12 mbs only) it throws the following error -
Warning: Error in .jcall: java.lang.OutOfMemoryError: GC overhead limit exceeded [No stack trace available] and it doesn't let me download the file. Rather in the saving folder the downloadhandler id "dl" is shown instead of the name of the file I have passed which in this case is "test.xlsx" as one can figure out from the code. Also nothing gets downloaded. Mind you this is just for heavier files (I have tried with files 12mb in size)
NOTE - I have already called options(shiny.maxRequestSize = 200*1024^2) in this app.
I have also used options(java.parameters = "- Xmx1024m") at some point in the app
The relevant server code is as follows -
library(shiny)
library(DT)
library(readxl)
ui <- fluidPage(
fileInput("file", "Import File", accept = ".xlsx"),
DTOutput("t1"),
uiOutput("selectfile"),
downloadButton("dl","Export to Excel")
)
server <- function(input, output, session) {
x <- reactiveValues()
observe({
xdf <- reactive({
req(input$file)
inFile <- input$file
if(is.null(inFile)) return(NULL)
file.rename(inFile$datapath, paste0(inFile$datapath, ".xlsx"))
read_excel(paste0(inFile$datapath, ".xlsx"),sheet = 1)
})
x$df <- as.data.frame(xdf())
})
output$t1 <- renderDT({x$df},filter="top", class = 'hover cell-border stripe', selection = 'none',
editable= list(target = 'cell'), extensions= 'Buttons',
options = list(dom = 'Bfrtip',pageLength =10,
buttons = c('copy','csv','excel','pdf','print'), scrollX=TRUE),server=FALSE)
observeEvent(input[["t1_cell_edit"]], {
cell <- input[["t1_cell_edit"]]
str(cell)
x$df <<- editData(x$df, cell)
x$df <<- as.data.frame(x$df)
})
output$dl <- downloadHandler(
filename = "test1.xlsx",
content = function(file) {
write.xlsx2(x$df, file, sheetName = "Sheet A", row.names = FALSE)
}
)
}
shinyApp(ui, server)
Can someone please help solve this error?
Solution
I have used the "writexl" library as a solution and the write_xl function works wonders! no dependencies on Java and is a life saver. Code attached below -
library(writexl)
df1 <- data.frame(name = c("Jon", "Bill", "Maria"),
age = c(23,41,32))
df2<- data.frame(name = c("Jack", "Bob", "Mia"),
age = c(27,29,22))
write_xlsx(list("sheet 1" = df1,"sheet 2" = df2), file)
Link to source here

Passing reactive data to global environment

I want to use Shiny within RMarkdown for users to upload data (xlsx file).
Then I want to pass all the worksheets as R data frames (w/o reactivity) to run rest of the RMarkdown file.
I mainly want to convert them into data frames so I can use reticulate to run Python code as well.
I've tried this, and it doesn't seem to quite work:
library(dplyr)
library(miniUI)
library(shiny)
library(XLConnect)
launch_shiny <- function() {
ui <- miniPage(
gadgetTitleBar("Input Data"),
miniContentPanel(
fileInput(inputId = "my.file", label = NULL, multiple = FALSE)
)
)
server <- function(input, output, session) {
wb <- reactive({
new.file <- input$my.file
loadWorkbook(
filename = new.file$datapath,
create = FALSE,
password = NULL
)
})
observeEvent(input$done, {
stopApp(c(wb()))
})
}
runGadget(ui, server)
}
test <- launch_shiny()
df1 <- readWorksheet(object = test, sheet = "sheet1")
df2 <- readWorksheet(object = test, sheet = "sheet2")
It throws this error:
Error in (function (classes, fdef, mtable) :
unable to find an inherited method for function ‘readWorksheet’ for signature ‘"list", "character"’
I can return one sheet at a time using stopApp(readWorksheet(object = wb(), sheet = "sheet1")), but I can't seem to return an entire workbook or multiple data frames at the same time.
I don't really want to read in xlsx, save each sheet as csv in working directory, then read those files in again.
Would anyone have a good suggestion on how to get around this?
The documentation of fileInput() states in the details:
datapath
The path to a temp file that contains the data that was
uploaded. This file may be deleted if the user performs another upload
operation.
Meaning that the datapath given in the input variable is a temporary file that is no longer accessible after you close the App, which is what the function readWorksheet will try to do.
So you'll have to read the sheets in the server and return the dataframes somehow.
I did that by defining a second reactive value which is basically a list of dataframes returned by applying lapply on all the sheets in wb, in this case test will be this list of data frames.
There might be other ways (more efficient, or suits your purpose better) to do this, but here it is:
library(dplyr)
library(miniUI)
library(shiny)
library(XLConnect)
launch_shiny <- function() {
ui <- miniPage(
gadgetTitleBar("Input Data"),
miniContentPanel(
fileInput(inputId = "my.file", label = NULL,
multiple = FALSE)
)
)
server <- function(input, output, session) {
wb <- reactive({
new.file <- input$my.file
loadWorkbook(
filename = new.file$datapath,
create = FALSE,
password = NULL
)
})
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
df_lst <- reactive({
# read all sheets into a list
lapply(getSheets(wb()),
function(sheet){
readWorksheet(object = wb(),
sheet = sheet)
})
})
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
observeEvent(input$done, {
# get the list of dfs from the app
stopApp(c(df_lst()))
})
}
runGadget(ui, server)
}
test <- launch_shiny()

Resources