Plots not printed over multiple pages - r

I have developed app to allow users to download all the plots in the app to PDF file. The code somewhat works, but not as I expected.
The number of plots are dynamic (as it depends on the data). I have use marrangeGrob function but all my plots are still on the same page, instead of seeing one on each page
Is there a setting I have missed? Also, I would like to adjust the height and the width for each chart as well (make the width shorter and the height longer), is there an option to do so?
Here is my code:
library(shiny)
library(data.table)
library(tidyverse)
library(shinydashboard)
### UI & Module -------------------
plotUI <- function(id) {
ns <- NS(id)
plotOutput(ns("myplot"))
}
plotServer <- function(id,groups) {
moduleServer(
id,
function(input, output, session) {
filtered.data <- reactive(iris %>% filter(Species == groups))
create_graph <- reactive(filtered.data() %>% ggplot(aes(x = Sepal.Length, y = Sepal.Width)) +geom_col())
output$myplot <- renderPlot(create_graph())
return(list(graph = create_graph))
}
)
}
### UI part -----------------------
ui <- dashboardPage(
title="test",
dashboardHeader(),
dashboardSidebar(downloadButton('download')),
dashboardBody(uiOutput('tablist'))
)
### Server part ----------------------------
server = function(input, output) {
groups <- reactive(unique(iris$Species))
ntabs <- reactive(length(groups()))
allplots <- reactiveVal()
observeEvent(ntabs(),{
plot <- lapply(1:ntabs(), function (i) {
plotServer(paste0("plot",i),groups()[i])
})
allplots(plot)
})
output$tablist = renderUI({
addtabs <- lapply(1:ntabs(),function (i) {
tabPanel(
groups()[i],
plotUI(paste0("plot",i)),
)
})
do.call(tabsetPanel, addtabs)
})
output$download <- downloadHandler(
filename = function() {
paste0("plots-", Sys.Date(), ".pdf")
}, content = function(file) {
plot.list <- allplots()
ggsave(file,marrangeGrob(lapply(plot.list, function(x) x$graph()),
ncol = 1, nrow = length(plot.list)))
}
)
}
### Running part ----------------------------
shinyApp(ui, server,enableBookmarking = "server")

If we specify ncol = 1 and nrow = 1, every plot goes in a separate page because the layout gets "recycled".
output$download <- downloadHandler(
filename = function() {
paste0("plots-", Sys.Date(), ".pdf")
}, content = function(file) {
plot.list <- allplots()
ggsave(file, marrangeGrob(
grobs = plot.list |> lapply(\(x) x$graph()),
ncol = 1,
nrow = 1
))
}
)

Related

How to have sequential Modal dialogs in Shiny

I need a shiny app to do the following:
The user clicks a button
N pop-ups appear to the user asking for input
Then the user downloads the information displayed in the app with a download button
I've been able to achieve points 1 & 2, however I haven't been able to get to 3 because of the fact that the user inputs are reactive values. Here is a sample of code that almost works:
library(shiny)
library(shinyalert)
test <- c("C", "D", "F")
NUM_MODALS <- length(test)
ui <- fluidPage(
shinyalert::useShinyalert(),
actionButton("show", "Show modal dialog"),
lapply(seq(NUM_MODALS), function(id) {
div(id, ":", textOutput(paste0("modal", id), inline = TRUE))
}),
downloadButton("downloadData", "Download")
)
server <- function(input, output) {
observeEvent(input$show, {
for(id in 1:NUM_MODALS){
shinyalert::shinyalert(
type = "input",
text = paste("¿Cuál es la industria de la siguiente empresa?:", test[id]),
inputPlaceholder = "Cuidado con mayúsculas/minúsculas",
inputId = paste0("modal", id)
)
}
})
lapply(seq(NUM_MODALS), function(id) {
output[[paste0("modal", id)]] <- renderText({paste(test[id],input[[paste0("modal", id)]])})
})
export <- reactive(c(input$modal1, input$modal2, input$modal3))
export2 <- isolate(export)
print(export2)
#browser()
output$downloadData <- downloadHandler(
filename = function() {
paste('data-', Sys.Date(), '.csv', sep='')
},
content = function(filesillo) {
fs <- c()
tmpdir <- tempdir()
setwd(tempdir())
path <- paste("prueba.txt", sep = "")
fs <- c(fs, path)
write.csv(export2, filesillo)
}
)
}
shinyApp(ui = ui, server = server)
Instead of the inputs being assigned as a reactive, you can assign to reactiveValues in an observe.
export <- reactiveValues(
dat = NULL
)
observe({
export$dat <- dplyr::bind_rows(
modal1 = input$modal1,
modal2 = input$modal2,
modal3 = input$modal3
)
})
# export <- reactive(c(input$modal1, input$modal2, input$modal3))
# export2 <- isolate(export)
# print(export2)
#browser()
Then in your downloadHandler
#write.csv(export2, filesillo)
write.csv(export$dat, filesillo)
This will output a csv with modal inputs as columns

How to conditionally download a plot?

The below reproducible code allows the user to select either a data table or a plot of the data for viewing (via input$view). I'm trying to create a conditional around the downloadHandler() so that if the user is viewing the data table and chooses to download, then the data is downloaded; otherwise if the user is viewing the plot and chooses to download then a plot in PNG format is downloaded. I'm running into issues around input$view reactivity. How would I modify the code below to conditionally download whichever (data or plot) the user is viewing?
The code as posted below works for viewing either data or plot, but only allows the data table to be downloaded. Offending lines of code that otherwise cause a crash are commented out.
Reproducible code:
library(shiny)
library(ggplot2)
ui <- fluidPage(
radioButtons("view",
label = "View data or plot",
choiceNames = c('Data','Plot'),
choiceValues = c('viewData','viewPlot'),
selected = 'viewData',
inline = TRUE
),
conditionalPanel("input.view == 'viewData'",tableOutput("DF")),
conditionalPanel("input.view == 'viewPlot'",plotOutput("plotDF")),
downloadButton("download","Download",style = "width:20%;")
)
server <- function(input, output, session) {
data <- data.frame(Period = c(1,2,3,4,5,6),Value = c(10,20,15,40,35,30))
data1 <- reactiveValues()
inputView <- reactive(input$view) # attempt to make input$view reactive
observeEvent(input$view,{data1$plot <- ggplot(data, aes(Period,Value)) + geom_line()})
output$DF <- renderTable(data)
output$plotDF <- renderPlot(data1$plot)
output$download <-
# if(inputView() == 'viewData'){
downloadHandler(
filename = function()
paste("dataDownload","csv",sep="."),
content = function(file){
write.table(
data,
na = "",
file,
sep = ",",
col.names = TRUE,
row.names = FALSE)
}
)
# }
# else{
# downloadHandler(
# filename = function(){paste("plotDownload",'.png',sep='')},
# content = function(file){
# ggsave(file,plot=data1$plot)
# }
# )
# }
}
shinyApp(ui, server)
Try this
library(shiny)
library(ggplot2)
ui <- fluidPage(
radioButtons("view",
label = "View data or plot",
choiceNames = c('Data','Plot'),
choiceValues = c('viewData','viewPlot'),
selected = 'viewData',
inline = TRUE
),
conditionalPanel("input.view == 'viewData'",tableOutput("DF")),
conditionalPanel("input.view == 'viewPlot'",plotOutput("plotDF")),
#downloadButton("download","Download",style = "width:20%;")
uiOutput("plotrtable")
)
server <- function(input, output, session) {
data <- data.frame(Period = c(1,2,3,4,5,6),Value = c(10,20,15,40,35,30))
data1 <- reactiveValues()
inputView <- reactive(input$view) # attempt to make input$view reactive
observeEvent(input$view,{data1$plot <- ggplot(data, aes(Period,Value)) + geom_line()})
output$DF <- renderTable(data)
output$plotDF <- renderPlot(data1$plot)
output$plotrtable <- renderUI({
if(input$view == 'viewData'){downloadButton("download","Download",style = "width:20%;") }
else {downloadButton("downloadp","Download",style = "width:20%;") }
})
output$download <- downloadHandler(
filename = function()
paste("dataDownload","csv",sep="."),
content = function(file){
write.table(
data,
na = "",
file,
sep = ",",
col.names = TRUE,
row.names = FALSE)
}
)
output$downloadp <- downloadHandler(
filename = function(){paste("plotDownload",'.png',sep='')},
content = function(file){
ggsave(file,plot=data1$plot)
}
)
}
shinyApp(ui, server)

Shiny module for downloading all plots from dashboard works locally but not in Docker

Here's a Shiny module that I'm using to download all plots from a dashboard. You should be able to run it by copy-pasting the code and calling the function mod_download_plots_app.
mod_download_plots_ui <- function(id){
ns <- NS(id)
tagList(
shinyjs::useShinyjs(), # we need this to be able to keep the button disabled until input changes
shinyjs::disabled(
downloadButton(
ns("download_all_plots"),
"Download all plots",
style = "color: #333; margin: 15px;" # default style doesn't work well in the sidebar
)
)
)
}
mod_download_plots_server <- function(id, analysis, plots_info) {
stopifnot(is.reactive(analysis), is.list(plots_info))
moduleServer(id, function(input, output, session) {
observe(
if (analysis() != "empty_choice") {
shinyjs::enable("download_all_plots")
} else {
shinyjs::disable("download_all_plots")
}
)
save_plot <- function(plot_info, plot_name, prefix, increment) {
incProgress(increment)
pixels_per_inch <- 100
file_path <- file.path(tempdir(), paste0(prefix, "_", plot_name, ".png"))
ggsave(
filename = file_path, plot = plot_info$plot(),
width = plot_info$width() / pixels_per_inch, height = plot_info$height() / pixels_per_inch
)
}
name_zip_file <- function() {
paste0(analysis(), "-", Sys.Date(), ".zip")
}
zip_all_plots <- function(file) {
withProgress(message = "Exporting plots to png files", {
increment <- 1 / length(plots_info)
plot_files <- purrr::imap_chr(plots_info, save_plot, prefix = analysis(), increment = increment)
zip::zip(file, files = plot_files, mode = "cherry-pick")
})
}
output$download_all_plots <- downloadHandler(
filename = name_zip_file,
content = zip_all_plots
)
})
}
mod_download_plots_app <- function() {
library(shiny)
library(ggplot2)
ui <- fluidPage(
mod_download_plots_ui("zip")
)
server <- function(input, output, session) {
plot1 <- list(plot = reactive(qplot(x = cyl, y = mpg, data = mtcars)), width = reactive(100), height = reactive(200))
plot2 <- list(plot = reactive(qplot(x = am, y = mpg, data = mtcars)), width = reactive(800), height = reactive(400))
mod_download_plots_server(
"zip",
reactive("selected_analysis"),
tibble::lst(plot1, plot2)
)
}
shinyApp(ui, server)
}
When I run it locally, it works as it should. But when I run it on Docker, I get:
Warning: Error in : Result 1 must be a single string, not NULL of length 0
[No stack trace available]
that appears when running the line:
plot_files <- purrr::imap_chr(plots_info, save_plot, prefix = analysis(), increment = increment)
Our docker library versions are not completely the same as my local ones - they are half a year old but I don't believe this would cause a problem.
Create a new directory with these two files:
Dockerfile
FROM rocker/shiny-verse:4.1
RUN R -e "install.packages('shinyjs')"
COPY app.R /srv/shiny-server/app.R
app.R
mod_download_plots_ui <- function(id) {
ns <- NS(id)
tagList(
shinyjs::useShinyjs(), # we need this to be able to keep the button disabled until input changes
shinyjs::disabled(
downloadButton(
ns("download_all_plots"),
"Download all plots",
style = "color: #333; margin: 15px;" # default style doesn't work well in the sidebar
)
)
)
}
mod_download_plots_server <- function(id, analysis, plots_info) {
stopifnot(is.reactive(analysis), is.list(plots_info))
moduleServer(id, function(input, output, session) {
observe(
if (analysis() != "empty_choice") {
shinyjs::enable("download_all_plots")
} else {
shinyjs::disable("download_all_plots")
}
)
save_plot <- function(plot_info, plot_name, prefix, increment) {
incProgress(increment)
pixels_per_inch <- 100
file_path <- file.path(tempdir(), paste0(prefix, "_", plot_name, ".png"))
ggsave(
filename = file_path, plot = plot_info$plot(),
width = plot_info$width() / pixels_per_inch, height = plot_info$height() / pixels_per_inch
)
}
name_zip_file <- function() {
paste0(analysis(), "-", Sys.Date(), ".zip")
}
zip_all_plots <- function(file) {
withProgress(message = "Exporting plots to png files", {
increment <- 1 / length(plots_info)
plot_files <- purrr::imap_chr(plots_info, save_plot, prefix = analysis(), increment = increment)
zip::zip(file, files = plot_files, mode = "cherry-pick")
})
}
output$download_all_plots <- downloadHandler(
filename = name_zip_file,
content = zip_all_plots
)
})
}
mod_download_plots_app <- function() {
library(shiny)
library(ggplot2)
ui <- fluidPage(
mod_download_plots_ui("zip")
)
server <- function(input, output, session) {
plot1 <- list(plot = reactive(qplot(x = cyl, y = mpg, data = mtcars)), width = reactive(100), height = reactive(200))
plot2 <- list(plot = reactive(qplot(x = am, y = mpg, data = mtcars)), width = reactive(800), height = reactive(400))
mod_download_plots_server(
"zip",
reactive("selected_analysis"),
tibble::lst(plot1, plot2)
)
}
shinyApp(ui, server)
}
# This script must both define and call this function
mod_download_plots_app()
Then go to the directory and build and run the app in bash:
cd my_project_dir
docker build --tag shiny_docker_downlaod_app .
docker run -p 3838:3838 shiny_docker_downlaod_app
Go to localhost:3838 to view your app.

download CSV using filtered dataframe RShiny

I've made an shiny app where I'm filtering a dataset using some values and then I would like to be able to download that filtered dataset. However, I'm struggling to understand how I can pass the filtered dataset to the csv downloader. It is a very large dataset so can't use the buttons available in renderDataTable (I think?) Does anyone have any ideas of how I can do this?
Example app:
### data ###
egDf <- data.frame(col1 = sample(letters,10000,replace=T), col2 = sample(letters,10000, replace=T))
### modules ###
chooseCol1UI <- function(id){
ns <- NS(id)
uiOutput(ns('chooserCol1'))
}
chooseCol1 <- function(input, output, session, data){
output$chooserCol1 <- renderUI({
ns <- session$ns
pickerInput(inputId = ns('chosenCol1'),
label = 'Col1',
choices = paste(sort(unique(egDf$col1))),
options = list(`actions-box` = TRUE),
multiple = TRUE)
})
return(reactive(input$chosenCol1))
}
csvDownloadUI <- function(id, label = "Download CSV") {
ns <- NS(id)
downloadButton(ns("downloadData"), label)
}
csvDownload <- function(input, output, session, data) {
output$downloadData <- downloadHandler(
filename = function() {
paste(names(data), Sys.Date(), '.csv', sep='')
},
content = function(file) {
write.csv(data, file, row.names = FALSE)
}
)
}
displayTableUI <- function(id){
ns <- NS(id)
DT::dataTableOutput(ns('displayer'))
}
displayTable <- function(input, output, session, data, col1Input){
output$displayer <- DT::renderDataTable(egDf %>% filter(col1 %in% col1Input()))
}
### server ###
server <- function(input,output){
chosenCol1 <- callModule(chooseCol1,
id = 'appChooseCol1', data = egDf)
callModule(module = displayTable, id = "appDisplayTable",
col1Input = chosenCol1)
}
### ui ###
ui <- fluidPage(
sidebarPanel(
chooseCol1UI("appChooseCol1")),
mainPanel(displayTableUI("appDisplayTable")))
### app ###
shinyApp(ui = ui, server = server)
A few years ago I made an app with such a button. In my case I created a reactive expression in the server.R file that is being passed to the downloadHandler.
Here's the app and here's the github code. Head to the server.R file and search for the "download" string.
In the app you'll find a blue download button in the "Data" tab. The app let's you apply filters that applies in the datatable, that you can download via the button.
Edit: here's the server portion of code of interest:
#data download button
output$idDwn <- downloadHandler(
filename = function() {
paste('uCount ', format(Sys.time(), "%Y-%m-%d %H.%M.%S"), '.csv', sep='')
},
content = function(file) {
write.csv(datasetInputFilters(), file)
}
)
I would create eventReactive function that allows your col1Input.
# Reactive function based on input
react_df <- eventReactive(input$chosenCol1, {
return(egDf %>% filter(col1 %in% input$chosenCol1))
})
output$displayer <- renderDataTable(react_df())
# Download box
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep="")
},
content = function(file) {
output_d <- react_df()
write.csv(output_d, file, row.names = FALSE)
}
)
I dealt with this issue recently and unfortunately that solution didn't work for me. But simply using writexl::write_xlsx() instead of write.csv() was enough.

Download table as image r

I want to be able to download a table as an image(PNG or JPEG). Let's assume that my dataframe is df
output$statsTable <- renderTable({
#Printing the table
df
})
output$downloadStatsTable <- downloadHunter(
filename = function() {
paste(getwd(), '/test.png', sep = '')
},
content = function(con) {
p <- grid.table(df)
device <- function(..., width, height) grDevices::png(..., width = 12, height = 9, res = 300, units = "in")
ggsave(file, plot = p, device = device)
}
)
To download table as image you can use grid.table function from library gridExtra. Here is a code which you could use as a template:
library(gridExtra)
library(shiny)
df <- head(datasets::iris)
ui <- fluidPage(
tableOutput("statsTable"),
downloadButton('downloadStatsTable ', 'Download')
)
server <- function(input, output) {
output$statsTable <- renderTable({
#Printing the table
df
})
output$downloadStatsTable <- downloadHandler(
# Create the download file name
filename = function() {
paste("data-", Sys.Date(), ".jpeg", sep="")
},
content = function(file) {
grid.table(df)
jpeg(file=file)
grid.table(df) #Create image of the data frame
dev.off()
})
}
runApp(list(ui = ui, server = server), launch.browser = TRUE)
Hope it helps!

Resources