I have an app that is creating a dynamic number of images, based on various user inputs. The plotting is being done using renderUI following this link, but with modifications required for my own setup. I now need to export these plots, but can't figure out how to make that happen. I know how to export an individual plot (which is included in the example below), but am looking to modify the code below to be able to export a dynamic number of models.
Would appreciate any suggestions!
library(shiny)
library(dplyr)
library(ggplot2)
# fake data
df <- data.frame(x = 1:10, y = letters[1:10]) %>%
mutate(Plot = x %/% 3.1 + 1)
# function for plotting dynamic number of plots
get_plot_output_list <- function(input_n, df) {
# Insert plot output objects the list
plot_output_list <- lapply(1:input_n, function(i) {
sub <- df %>% filter(Plot == i)
plotname <- paste("plot", i, sep="")
plot_output_object <- plotOutput(plotname, height = 280, width = 250)
plot_output_object <- renderPlot({
ggplot(sub) + geom_point(aes(x = x, y = y))
})
})
do.call(tagList, plot_output_list) # needed to display properly.
}
ui <- navbarPage("My app", id = "nav",
tabPanel("Single plot",
fluidRow(column(9, plotOutput("plot1")),
column(2, downloadButton('ExportPlot1', label = "Download plot1")))),
tabPanel("Multiple plots",
fluidRow(column(9,
selectInput("NPlots", label = "Select number of plots to make", choices = 1:3, selected = 1),
uiOutput("plots")),
column(2, downloadButton('ExportPlots', label = "Download all dynamic plots")))))
server <- (function(input, output) {
observe({
output$plots <- renderUI({ get_plot_output_list(input$NPlots, df) })
})
plot.calc <- reactive({
p <- ggplot(df) + geom_point(aes(x = x, y = y))
output <- list(p = p)
})
output$plot1 <- renderPlot({ plot.calc()$p })
output$ExportPlot1 <- downloadHandler(
filename = 'Plot1.html',
content = function(file) {
src <- normalizePath(c('Plot1.Rmd')) # SEE HERE
owd <- setwd(tempdir())
on.exit(setwd(owd))
file.copy(src, c('Plot1.Rmd'), overwrite = TRUE) # SEE HERE
params <- list(Plot1 = plot.calc()$p)
Sys.setenv(RSTUDIO_PANDOC="C:/Program Files/RStudio/bin/pandoc")
out <- rmarkdown::render('Plot1.Rmd', output_file = file, params = params, envir = new.env(parent = globalenv()))
file.rename(out, file)
})
})
shinyApp(ui, server)
Rmd file:
---
title: "Untitled"
author: "test"
date: "24 3 2021"
output: html_document
params:
Plot1: NA
---
My plot
```{r, echo = FALSE, warning = FALSE, fig.width = 6.4, fig.height = 3.5}
params$Plot1
```
When you separate the plot generation and the actual plotting, you can pass the generated plots to the Rmd. BTW you don't need observe when you work with reactives like input$NPlots:
library(shiny)
library(dplyr)
library(ggplot2)
# fake data
df <- data.frame(x = 1:10, y = letters[1:10]) %>%
mutate(Plot = x %/% 3.1 + 1)
generate_plots <- function(input_n, df) {
plot_output_list <- lapply(1:input_n, function(i) {
sub <- df %>% filter(Plot == i)
p <- ggplot(sub) + geom_point(aes(x = x, y = y))
p
})
plot_output_list
}
ui <- navbarPage("My app", id = "nav",
tabPanel("Single plot",
fluidRow(column(9, plotOutput("plot1")),
column(2, downloadButton('ExportPlot1', label = "Download plot1")))),
tabPanel("Multiple plots",
fluidRow(column(9,
selectInput("NPlots", label = "Select number of plots to make", choices = 1:3, selected = 1),
uiOutput("plots")),
column(2, downloadButton('ExportPlots', label = "Download all dynamic plots")))))
server <- (function(input, output) {
plot_data <- reactive({
generate_plots(input$NPlots, df)
})
output$plots <- renderUI({
plot_output_list <- lapply(seq_len(length(plot_data())), function(i) {
plotname <- paste("plot", i, sep="")
plot_output_object <- plotOutput(plotname, height = 280, width = 250)
plot_output_object <- renderPlot({
plot_data()[[i]]
})
})
do.call(tagList, plot_output_list) # needed to display properly.
})
plot.calc <- reactive({
p <- ggplot(df) + geom_point(aes(x = x, y = y))
output <- list(p = p)
})
output$plot1 <- renderPlot({ plot.calc()$p })
output$ExportPlot1 <- downloadHandler(
filename = 'Plot1.html',
content = function(file) {
src <- normalizePath(c('Plot1.Rmd')) # SEE HERE
owd <- setwd(tempdir())
on.exit(setwd(owd))
file.copy(src, c('Plot1.Rmd'), overwrite = TRUE) # SEE HERE
params <- list(Plot1 = plot.calc()$p,
Plot_list = plot_data())
Sys.setenv(RSTUDIO_PANDOC="C:/Program Files/RStudio/bin/pandoc")
out <- rmarkdown::render('Plot1.Rmd', output_file = file, params = params, envir = new.env(parent = globalenv()))
file.rename(out, file)
})
output$ExportPlots <- downloadHandler(
filename = 'Plots.html',
content = function(file) {
src <- normalizePath(c('Plots.Rmd')) # SEE HERE
owd <- setwd(tempdir())
on.exit(setwd(owd))
file.copy(src, c('Plots.Rmd'), overwrite = TRUE) # SEE HERE
params <- list(Plot_list = plot_data())
Sys.setenv(RSTUDIO_PANDOC="C:/Program Files/RStudio/bin/pandoc")
out <- rmarkdown::render('Plots.Rmd', output_file = file, params = params, envir = new.env(parent = globalenv()))
file.rename(out, file)
})
})
shinyApp(ui, server)
Plots.Rmd
---
title: "Untitled"
author: "test"
date: "24 3 2021"
output: html_document
params:
Plot_list: NA
---
Multiple Plots
```{r, echo = FALSE, warning = FALSE, fig.width = 6.4, fig.height = 3.5}
purrr::walk(params$Plot_list, print)
```
Related
I have an example RShiny app with reactive filters that I added download button buttons for downloading .csv and Rmd report (as html). The download report handler links to a parameterized Rmd file.
I have no problem downloading the filtered data in .csv. I can also download the report, but it is not filtered with the slider/selection inputs.
I think it is something with the params arg, but I am getting stuck on that. Below is the RShiny code and the .Rmd code below:
library(dplyr)
library(ggplot2)
library(shiny)
library(shinydashboard)
library(ggthemes)
setwd(wd)
cat1 <- as.character(c(1:10))
cat2 <- c("a", "b", "a", "a", "a", "b", "b", "a", "a", "b")
cat3 <- c(1,3,6,9,12,15,18,21,24,27)
cat4 <- c("one", "one", "one", "two", "two", "four", "three", "five", "three", "four")
df <- data.frame(cat1, cat2, cat3, cat4)
#--------------------------------------------
ui <-
fluidPage(
theme = bs_theme(version = 4, bootswatch = "lumen"),
fluidRow(
column(9,
offset = 0,
span("Example")
)
),
sidebarLayout(
position = "left",
sidebarPanel(
width = 3, offset = 0,
selectInput("set",
label = "Set:",
choices = c("All", unique(df$cat2))
),
sliderInput(inputId = "age",
label = "Choose Age Range:",
min = min(df$cat3),
max = 30,
value=c(1, 30),
step = 3)
),
mainPanel(
width = 9, offset=0,
tabsetPanel(
tabPanel('Dashboard',
br(),
dashboardPage(
dashboardHeader(disable = TRUE),
dashboardSidebar(disable = TRUE),
dashboardBody(
box(
title = "Group distribution",
width = 6,
background = "light-blue",
solidHeader = TRUE,
plotOutput("group_bar", height = 300)
)
)
),
downloadButton("data", "Download Data"),
downloadButton("report", "Download Report")
)
)
)
)
)
#------------------------------------------------
server <- function(input, output, session) {
rval_filters <- reactive({
req(input$set)
req(input$age)
data <- df
#filter data set
if (input$set != "All"){
data <- data %>%
filter(cat2 %in% input$set)
} else {
data
}
#filter based on age range
data <- data %>%
filter(cat3 >= input$age[1] & cat3 <= input$age[2])
data
})
# plot by group
output$group_bar <- renderPlot({
group <- rval_filters() %>%
#summarize
group_by(cat4) %>%
summarise(n = n())
plot_bar <- ggplot(group, aes(x= n, y = reorder(cat4, n))) +
geom_bar(stat= "identity",fill = "#4C7A99") +
theme_minimal()+
labs(x = "Count")
plot_bar
})
output$data <- downloadHandler(
filename = function(){
paste0("report", ".csv")
},
content = function(file){
write.csv(rval_filters(), file)
}
)
output$report <- downloadHandler(
filename = "report.html",
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(
n = df,
plot = plot_bar
)
# 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).
rmarkdown::render(tempReport, output_file = file,
params = params,
envir = new.env(parent = globalenv())
)
}
)
}
# Run app ----
shinyApp(ui, server)
#-----------RMD----------------------
---
title: "Dynamic report"
output: html_document
params:
n: NA
plot: "NULL"
---
```{r}
# The `params` object is available in the document.
params$n
# A plot
params$plot
The issue is that you pass plot_bar to the plot parameter of the Rmd. However, plot_bar is a local variable defined and only accessible within your renderPlot. To export your plot use a reactive to create the plot. This reactive could then be called inside renderPlot to plot your chart and also be passed to the plot parameter of the Rmd:
plot_bar <- reactive({
group <- rval_filters() %>%
group_by(cat4) %>%
summarise(n = n())
plot_bar <- ggplot(group, aes(x = n, y = reorder(cat4, n))) +
geom_bar(stat = "identity", fill = "#4C7A99") +
theme_minimal() +
labs(x = "Count")
plot_bar
})
# plot by group
output$group_bar <- renderPlot({
plot_bar()
})
output$report <- downloadHandler(
filename = "report.html",
content = function(file) {
tempReport <- file.path(tempdir(), "report.Rmd")
file.copy("report.Rmd", tempReport, overwrite = TRUE)
# Set up parameters to pass to Rmd document
params <- list(
n = df,
plot = plot_bar()
)
rmarkdown::render(tempReport,
output_file = file,
params = params,
envir = new.env(parent = globalenv())
)
}
)
I want to build a shiny app that counts the number of clicks I make on any image, but I don't know how to make the counter increase, it just plots the number 1
I tried to create loops inside renderPlot but it doesn't work.
It is necessary to change the path of the files to a directory that contains .jpg images
library(shiny)
ui <- fluidPage(
titlePanel("Click Count"),
sidebarPanel(selectInput("IMAGE", "Sample image:",
list.files(path = "~",
pattern = ".jpg",
full.names = TRUE,
include.dirs = FALSE))),
fluidRow(
plotOutput("IMG", click = "countClick", "100%", "500px")
),
verbatimTextOutput("info")
)
server <- function(input, output, session){
# Creating a reactive variable that recognizes the selected image
img <- reactive({
f <- input$IMAGE
imager::load.image(f)
})
# Creating a spot where i can store reactive values
initX <- 1
initY <- 2
source_coords <- reactiveValues(xy = c(x=initX,y=initY))
# Coords
dest_coords <- reactiveValues(x=initX, y = initY)
observeEvent(plot_click(),{
dest_coords$x <- c(dest_coords$x, floor(plot_click()$x))
dest_coords$y <- c(dest_coords$y, floor(plot_click()$y))
})
plot_click <- debounce(reactive(input$countClick), 300)
output$IMG <- renderPlot({
plot(img(), axes = FALSE)
n <- 0
ex <- expression(n+1)
text(dest_coords$x, dest_coords$y,eval(ex),cex = 1 ,col = 'red')
})
output$info <- renderPrint({
req(input$countClick)
x <- round(input$countClick$x,2)
y <- round(input$countClick$y,2)
cat("[", x, ", ", y, "]", sep = "")
})
}
shinyApp(ui, server)
countClick is not a good name because input$countClick does not contain the numbers of clicks.
Not tested:
numberOfClicks <- reactiveVal(0)
dest_coords <- reactiveValues(x = initX, y = initY)
observeEvent(plot_click(),{
numberOfClicks(numberOfClicks() + 1)
dest_coords$x <- c(dest_coords$x, floor(plot_click()$x))
dest_coords$y <- c(dest_coords$y, floor(plot_click()$y))
})
plot_click <- debounce(reactive(input$countClick), 300)
output$IMG <- renderPlot({
plot(img(), axes = FALSE)
n <- numberOfClicks()
text(dest_coords$x, dest_coords$y, n, cex = 1 ,col = 'red')
})
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)
I'm trying to make a reactive data table in R Shiny that has a button you can press to compile an RMarkdown document. Ultimately, I'm trying to combine the solutions from these two links:
R Shiny: Handle Action Buttons in Data Table and https://shiny.rstudio.com/articles/generating-reports.html. Here is what I have so far:
library(shiny)
library(shinyjs)
library(DT)
shinyApp(
ui <- fluidPage(
DT::dataTableOutput("data")
),
server <- function(input, output) {
useShinyjs()
shinyInput <- function(FUN, len, id, ...) {
inputs <- character(len)
for (i in seq_len(len)) {
inputs[i] <- as.character(FUN(paste0(id, i), ...))
}
inputs
}
df <- reactiveValues(data = data.frame(
Portfolio = c('Column1', 'Column2'),
Option_1 = shinyInput(downloadButton, 2, 'compile_', label = "Compile Document", onclick = 'Shiny.onInputChange(\"compile_document\", this.id)' ),
stringsAsFactors = FALSE,
row.names = 1:2
))
output$data <- DT::renderDataTable(
df$data, server = FALSE, escape = FALSE, selection = 'none', filter='top'
)
output$compile_document <- downloadHandler(
filename = "report.html",
content = function(file) {
tempReport <- file.path(tempdir(), "report.Rmd")
file.copy("report.Rmd", tempReport, overwrite = TRUE)
params <- list(n = input$slider)
rmarkdown::render(tempReport, output_file = file,
params = params,
envir = new.env(parent = globalenv())
)
}
)
}
)
Here is the RMarkdown document I'd like to compile:
---
title: "Dynamic report"
output: html_document
params:
n: NA
---
```{r}
# The `params` object is available in the document.
params$n
```
A plot of `params$n` random points.
```{r}
plot(rnorm(params$n), rnorm(params$n))
```
The pieces all seem to be there, but I can't connect the "Compile Document" button to the download handler.
Here is a way that does not use downloadHandler.
library(shiny)
library(DT)
library(base64enc)
library(rmarkdown)
js <- '
Shiny.addCustomMessageHandler("download", function(b64){
const a = document.createElement("a");
document.body.append(a);
a.download = "report.docx";
a.href = b64;
a.click();
a.remove();
})
'
buttonHTML <- function(i){
as.character(
actionButton(
paste0("button_", i), label = "Report",
onclick = sprintf("Shiny.setInputValue('button', %d);", i)
)
)
}
dat <- data.frame(
PortFolio = c("Column 1", "Column 2")
)
dat$Action <- sapply(1:nrow(dat), buttonHTML)
ui <- fluidPage(
tags$head(tags$script(HTML(js))),
br(),
sliderInput("slider", "Sample size", min = 10, max = 50, value = 20),
br(),
DTOutput("dtable")
)
server <- function(input, output, session){
output[["dtable"]] <- renderDT({
datatable(dat, escape = -ncol(dat)-1)
})
observeEvent(input[["button"]], {
showNotification("Creating report...", type = "message")
tmpReport <- tempfile(fileext = ".Rmd")
file.copy("report.Rmd", tmpReport)
outfile <- file.path(tempdir(), "report.html")
render(tmpReport, output_file = outfile,
params = list(
data = dat[input[["button"]], -ncol(dat)],
n = input[["slider"]]
)
)
b64 <- dataURI(
file = outfile,
mime = "text/html"
)
session$sendCustomMessage("download", b64)
})
}
shinyApp(ui, server)
The rmd file:
---
title: "Dynamic report"
output: html_document
params:
data: "x"
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```
Row contents:
```{r}
params$data
```
A plot of `params$n` random points:
```{r}
plot(rnorm(params$n), rnorm(params$n))
```
The shiny app below is taken out of gallery. It allow user to choose a variable, build a linear regression and download report.
What if I do not know in advance how many plots and models user wants to build and include into report. Is it possible to create a report with dynamically added plots?
Server.R
function(input, output) {
regFormula <- reactive({
as.formula(paste('mpg ~', input$x))
})
output$regPlot <- renderPlot({
par(mar = c(4, 4, .1, .1))
plot(regFormula(), data = mtcars, pch = 19)
})
output$downloadReport <- downloadHandler(
filename = function() {
paste('my-report', sep = '.', switch(
input$format, PDF = 'pdf', HTML = 'html', Word = 'docx'
))
},
content = function(file) {
src <- normalizePath('report.Rmd')
# temporarily switch to the temp dir, in case you do not have write
# permission to the current working directory
owd <- setwd(tempdir())
on.exit(setwd(owd))
file.copy(src, 'report.Rmd', overwrite = TRUE)
library(rmarkdown)
out <- render('report.Rmd', switch(
input$format,
PDF = pdf_document(), HTML = html_document(), Word = word_document()
))
file.rename(out, file)
}
)
}
ui.R
fluidPage(
title = 'Download a PDF report',
sidebarLayout(
sidebarPanel(
helpText(),
selectInput('x', 'Build a regression model of mpg against:',
choices = names(mtcars)[-1]),
radioButtons('format', 'Document format', c('PDF', 'HTML', 'Word'),
inline = TRUE),
downloadButton('downloadReport')
),
mainPanel(
plotOutput('regPlot')
)
)
)
report.Rmd
Here is my regression model:
```{r model, collapse=TRUE}
options(digits = 4)
fit <- lm(regFormula(), data = mtcars)
b <- coef(fit)
summary(fit)
```
The fitting result is $mpg = `r b[1]` + `r b[2]``r input$x`$.
Below is a scatter plot with the regression line.
```{r plot, fig.height=5}
par(mar = c(4, 4, 1, 1))
plot(regFormula(), data = mtcars, pch = 19, col = 'gray')
abline(fit, col = 'red', lwd = 2)
```
Well, it looks like I have found the answer. The problem was in local/global variables. I had to put list initialisation outside server function. Also I had to use <<- instead of <- to assign new element to the plot rather than create new plot every time.
Many thanks to Peter Ellis to support!
So, the solution is (I have slightly changed initial code to focus on the important part):
server.R
library(ggplot2); library(shiny); library(grid); library(gridExtra)
plist <- list() # IMPORTANT - outside server function
shinyServer(function(input, output) {
output$regPlot <- renderPlot({
p <- do.call("grid.arrange", c(plotList(),
ncol=floor(sqrt(length(plotList())+1)),
top = "test"))
})
plotList <- eventReactive(input$plt2rprt, {
p <- ggplot(data = mtcars, aes_string(x = input$x, y = "mpg")) +
geom_point()
# isolate(
plist[[length(plist)+1]] <<- p #IMPORTATNT <<- instead of <-
# )
return(plist)
})
output$lengthOfList <- renderText({length(plotList())})
output$lll <- renderText({length(plist)})
output$downloadReport <- downloadHandler(
filename = function() {
paste('my-report', sep = '.', switch(
input$format, PDF = 'pdf', HTML = 'html', Word = 'docx'
))
},
content = function(file) {
src <- normalizePath('report.Rmd')
owd <- setwd(tempdir())
on.exit(setwd(owd))
file.copy(src, 'report.Rmd', overwrite = TRUE)
library(rmarkdown)
out <- render('report.Rmd', switch(
input$format,
PDF = pdf_document(), HTML = html_document(), Word = word_document()
))
file.rename(out, file)
}
)
}) #ShinyServer
ui.R
fluidPage(
title = 'Download a PDF report',
sidebarLayout(
sidebarPanel(
helpText(),
selectInput('x', 'Build a regression model of mpg against:',
choices = names(mtcars)[-1]),
actionButton("plt2rprt", label = "Include into report"),
hr(),
radioButtons('format', 'Document format', c('PDF', 'HTML', 'Word'),
inline = TRUE),
downloadButton('downloadReport')
),
mainPanel(
plotOutput('regPlot'),
#verbatimTextOutput("count"),
hr(),
textOutput("lengthOfList"),
textOutput("lll"),
helpText("test-test-test")
)
)
)
report.Rmd
Length of list of plots `r length(plotList())`
```{r plot, fig.height=5}
do.call("grid.arrange", c(plotList(),
ncol=floor(sqrt(length(plotList())+1)),
top = "test"))
```