I have a Shiny app which reads in data from a spreadsheet and produces two plots - a box plot and a histogram. I want to be able to download each plot separately (as png) using a download button when I have made various adjustments using sliders etc.
My plots work, and the download buttons work, but will only download one of the plots (apparently the one that was drawn most recently). In a reactive environment this is not very predictable and does not do what I want.
My question is, how do I specify in the download function which of the two plots to download?
An abbreviated version of my code is here:
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
<... various components ...>
}}
h3("Save the Box Plot"),
downloadButton("SaveBox", label = "Save Box Plot as PNG"),
h3("Save the Histogram"),
downloadButton("SaveHist", label = "Save Histogram as PNG")
),
mainPanel(
## Display the Boxplot
plotOutput("BoxPlot"),
## Display the Histogram
plotOutput("Hist"),
)
)
)
server <- function(input, output) {
############
## Read file and process data
graphData <- reactive ({
... draw box plot ...
})
################################
## Display the box plot
output$BoxPlot <- renderPlot({
...
})
################################
## Display the histogram
output$Hist <- renderPlot({
... draw histogram ...
})
################################
## Save to PNG with the plot title as a default file name.
## Skip saving if no file has been loaded
## Save file using the system file save utility so you can name it and put it where you want
output$SaveBox = downloadHandler(
filename = function(file){
ifelse (is.null(input$DataFile),return(), str_c(input$Title, ".png"))
},
content = function(file) {
ggsave(file, width = 290, height = 265, units = "mm", device = "png")
}
)
output$SaveHist = downloadHandler(
filename = function(file){
ifelse (is.null(input$DataFile),return(), str_c(input$HistTitle, ".png"))
},
content = function(file) {
ggsave(file, width = 290, height = 265, units = "mm", device = "png")
}
)
)
The issue is that you have to tell ggsave which plot you want to save. If not specified the last plot will be saved. To fix you code you could move the plot codes into reactives, which could then be called both in the renderPlots and passed to the plot argument of ggsave so that the right plot is saved:
I adjusted your code a bit to make it a reproducible example using mtcars as example data:
library(shiny)
library(ggplot2)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
h3("Save the Box Plot"),
downloadButton("SaveBox", label = "Save Box Plot as PNG"),
h3("Save the Histogram"),
downloadButton("SaveHist", label = "Save Histogram as PNG")
),
mainPanel(
## Display the Boxplot
plotOutput("BoxPlot"),
## Display the Histogram
plotOutput("Hist")
)
)
)
server <- function(input, output) {
## Read file and process data
graphData <- reactive({
mtcars
})
box_plot <- reactive({
ggplot(graphData(), aes(mpg)) +
geom_boxplot()
})
hist_plot <- reactive({
ggplot(graphData(), aes(mpg)) +
geom_histogram()
})
## Display the box plot
output$BoxPlot <- renderPlot({
box_plot()
})
## Display the histogram
output$Hist <- renderPlot({
hist_plot()
})
output$SaveBox <- downloadHandler(
filename = function(file) {
"box_plot.png"
#ifelse(is.null(input$DataFile), return(), str_c(input$Title, ".png"))
},
content = function(file) {
ggsave(file, plot = box_plot(), width = 290, height = 265, units = "mm", device = "png")
}
)
output$SaveHist <- downloadHandler(
filename = function(file) {
"hist_plot.png"
#ifelse(is.null(input$DataFile), return(), str_c(input$HistTitle, ".png"))
},
content = function(file) {
ggsave(file, , plot = hist_plot(), width = 290, height = 265, units = "mm", device = "png")
}
)
}
shinyApp(ui, server)
Related
I am developing a package in R and its plotting functions include a line with pdf() or png() to save the figure. However, when I tried to create a Shiny app from this package the plots did not appear on the app. I verified that the pdf() function prevents the plot from being displayed. Is there a way to show the plots without changing the whole package? I was thinking about saving the image and rendering it, but maybe there is a more efficient answer.
I created a sample code just to illustrates the problem. The test_plot function shows an example of the structure of the functions in my package.
test_plot <- function(){
data=head(mtcars, 30)
g1 <- ggplot(data, aes(x=wt, y=mpg)) +
geom_point() + # Show dots
geom_text(
label=rownames(data),
nudge_x = 0.25, nudge_y = 0.25,
check_overlap = T
)
pdf(file = 'test.pdf', width = 5, height = 5)
print(g1)
}
The renderPlot just calls the test_plot. If I remove the pdf() from the code the figure is displayed correctly.
server <- function(input, output) {
output$distPlot <- renderPlot({
test_plot()
})
}
Perhaps try separating the renderPlot() from the PNG file itself, and allow the user to download the PNG with a downloadHandler():
library(shiny)
library(tidyverse)
# Define UI for application that draws a histogram
ui <- fluidPage(
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
sliderInput(
inputId = "n", label = "Select rows",
min = 1, max = nrow(mtcars),
value = 20, round = TRUE
),
downloadButton(outputId = "download")),
# Show a plot of the generated distribution
mainPanel(plotOutput("plot"))
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
# render plot
plot_reactive <- reactive({
p <- head(mtcars, input$n) %>%
ggplot(aes(x = wt, y = mpg)) +
geom_point()
})
output$plot <- renderPlot(print(plot_reactive()))
# download plot
output$download <- downloadHandler(
filename = function(){ paste0(input$n, "_mtcars.png") },
content = function(file){ ggsave(file, plot_reactive()) }
)
}
# Run the application
shinyApp(ui = ui, server = server)
I seek a method in R shiny that I can include inside a render or an observe to check if a certain value has changed.
For example :
library(shiny)
library(shinycssloaders)
ui <- fluidPage(
fluidRow(
column(5,
imageOutput("image") %>% withSpinner()
),
actionButton("button", "redo")
)
)
server <- function(input, output, session) {
data = reactiveVal(data.frame(
gp = factor(rep(letters[1:3], each = 10)),
y = rnorm(30)
))
getWidth = function(image)
paste0(session$clientData[[paste0("output_", image, "_width")]], "px")
output$image = renderImage({
input$button
outfile = tempfile(fileext = ".png")
p = ggplot(data(), aes(gp, y)) +
geom_point()
Sys.sleep(2) # to symbolise a plot which is very slow to appear
ggsave(filename = outfile, p)
return(list(src = outfile, width = getWidth("image")))
}, deleteFile = F)
}
shinyApp(ui, server)
Here just when I resize the window, the image is resaved, I do not want that. But I want that code to save the file if and only if data() or input$button is changed.
The only solution I see so far is to copy the data in an independent variable and to check if the value has changed. If the data change, save the new plot, change the value of the independent variable.
But I am not convinced that is it a good solution because the value data will be copied twice. For this dataset it not very severe, but for a dataset with millions lines the strain is harder. Or a graph that takes more than 10 seconds to save.
Thank you,
My suggestion would be
Use renderPlot instead of renderImage
Create the plot in a reactive expression
Save only when the plot changes (now it only reacts to data changes not to resizes) or the button is pressed, by using an observeEvent with those two events as triggers.
Find a working example below. If you want to change the size of the saved plot do it in the ggsave.
library(shiny)
library(shinycssloaders)
library(tidyverse)
ui <- fluidPage(
fluidRow(
column(5,
imageOutput("image") %>% withSpinner()
),
actionButton("button", "redo")
)
)
server <- function(input, output, session) {
data = reactiveVal(data.frame(
gp = factor(rep(letters[1:3], each = 10)),
y = rnorm(30)
))
p <- reactive({ggplot(data(), aes(gp, y)) +
geom_point()
})
observeEvent(c(p(), input$button), {
outfile = tempfile(fileext = ".png")
ggsave(filename = outfile, p())
})
output$image = renderPlot({
Sys.sleep(2) # to symbolise a plot which is very slow to appear
p()
})
}
shinyApp(ui, server)
I have the shiny app below in which I want to download a plotly plot using downloadhandler(). But as you will see when I run the app in web browser and download the image the lower section of the histogram is missing. Why does this happen? Can this be fixed or be downloaded alternativelly? In case you wonder why I have used uiOutput() for download button and the movies dataset it is because this is how my original and more complex app works. Before you begin:
library(webshot)
install_phantomjs()
#uir.r
library(shiny)
library(plotly)
library(ggplot2movies) # movies is no longer contained within ggplot2 https://cran.r-project.org/web/packages/ggplot2movies/index.html
shinyUI(fluidPage(
titlePanel("Movie Ratings!"),
sidebarPanel(
uiOutput("down")
),
mainPanel(
plotlyOutput("trendPlot")
)
))
#server.r
library(shiny)
library(plotly)
library(ggplot2movies) # movies is no longer contained within ggplot2 https://cran.r-project.org/web/packages/ggplot2movies/index.html
shinyServer(function(input, output) {
output$down<-renderUI({
output$downloadData <- downloadHandler(
filename = function(){
paste0(paste0("pic"), ".png")
},
content = function(file) {
export(reg(), file=file)
})
downloadButton("downloadData", "Download")
})
reg<-reactive({
movies
# Create axes titles as lists
x <- list(
title = "A",
dtick = 5
)
y <- list(
title = "B"
)
# Create the plotly histogram
plot_ly(alpha = 0.9) %>%
add_histogram(x = as.factor(movies$rating)) %>%
# Add titles in plot and axes
layout(barmode = "overlay",title = "SAD",xaxis=x,yaxis=y)
})
output$trendPlot <- renderPlotly({
reg()
})
})
SPOLIED IMAGE
I don't get renderImage()to work with a .svg file.
My minimal example (adapted from the corresponding RStudio-tutorial):
ui.R
shinyUI(pageWithSidebar(
headerPanel("renderSVG example"),
sidebarPanel(
sliderInput("obs", "Number of observations:",
min = 0, max = 1000, value = 500),
actionButton("savePlot", "savePlot")
),
mainPanel(
# Use imageOutput to place the image on the page
imageOutput("plot"),
imageOutput("plot_as_svg")
)
))
server.R
require("ggplot2")
library(svglite)
shinyServer(function(input, output, session) {
## create plot with renderPlot()
output$plot<- renderPlot({
hist(rnorm(input$obs), main="Generated in renderPlot()")
})
## create .svg file of the plot and render it with renderImage()
output$plot_as_svg <- renderImage({
# A temp file to save the output.
# This file will be removed later by renderImage
outfile <- tempfile(fileext='.svg')
# Generate the svg
svglite(outfile, width=400, height=300)
hist(rnorm(input$obs), main="Generated in renderImage()")
dev.off()
# Return a list containing the filename
list(src = outfile,
contentType = 'text/svg+xml',
width = 400,
height = 300,
alt = "This is alternate text")
}, deleteFile = TRUE)
})
Any ideas where the problem is?
I struggled with this same issue, and there is nothing out on the web that I have found to address this problem which is frustrating. Here is the solution that worked for me:
ui.R:
shinyUI(pageWithSidebar(
headerPanel("renderSVG example"),
sidebarPanel(
sliderInput("obs", "Number of observations:",
min = 0, max = 1000, value = 500),
actionButton("savePlot", "savePlot")
),
mainPanel(
# Use imageOutput to place the image on the page
imageOutput("plot"),
imageOutput("plot_as_svg")
)
))
server.R:
require("ggplot2")
shinyServer(function(input, output, session) {
## create plot with renderPlot()
output$plot<- renderPlot({
hist(rnorm(input$obs), main="Generated in renderPlot()")
})
## create .svg file of the plot and render it with renderImage()
output$plot_as_svg <- renderImage({
width <- session$clientData$output_plot_width
height <- session$clientData$output_plot_height
mysvgwidth <- width/96
mysvgheight <- height/96
# A temp file to save the output.
# This file will be removed later by renderImage
outfile <- tempfile(fileext='.svg')
# Generate the svg
svg(outfile, width=mysvgwidth, height=mysvgheight)
hist(rnorm(input$obs), main="Generated in renderImage()")
dev.off()
# Return a list containing the filename
list(src = normalizePath(outfile),
contentType = 'image/svg+xml',
width = width,
height = height,
alt = "My Histogram")
})
})
Notice I did not use the svglite package, just the svg device from the grDevices (base) package. I also normalized the path in the source because I am on a Windows machine (I believe this will change my source from forward slashes to back slashes, but maybe someone will comment on that).
Also, I created four new variables to house the svg width and height and image width and height to still be fluid with the page. I'm sure there is an easier workaround than this, but this is what I found that worked.
Inspired by R_User123456789s solution (here) for base graphics above I got it the following way with ggplot2
ui.r
shinyUI(pageWithSidebar(
headerPanel("renderSVG example"),
sidebarPanel(
sliderInput("obs", "Number of observations:",
min = 0, max = 1000, value = 500)
),
mainPanel(
# Use imageOutput to place the image on the page
imageOutput("plot"),
imageOutput("plot_as_svg")
)
))
server.r
require("ggplot2")
shinyServer(function(input, output, session) {
## create plot with renderPlot()
output$plot<- renderPlot({
hist(rnorm(input$obs), main="Generated in renderPlot() as png")
})
## create .svg file of the plot and render it with renderImage()
output$plot_as_svg <- renderImage({
width <- session$clientData$output_plot_width
height <- session$clientData$output_plot_height
mysvgwidth <- width/96
mysvgheight <- height/96
# A temp file to save the output.
# This file will be removed later by renderImage
outfile <- tempfile(fileext='.svg')
# Generate the svg
#to see actually what will be plotted and compare
qplot(clarity, data=diamonds, fill=cut, geom="bar")
#save the plot in a variable image to be able to export to svg
image=qplot(clarity, data=diamonds[1:input$obs,], fill=cut, geom="bar", main = "ggplot as svg")
#This actually save the plot in a image
ggsave(file=outfile, plot=image, width=mysvgwidth, height=mysvgheight)
# Return a list containing the filename
list(src = normalizePath(outfile),
contentType = 'image/svg+xml',
width = width,
height = height,
alt = "My svg Histogram")
})
})
I have part of a shiny function where the user selects the download image type (.png, .tiff etc) and clicks a button to download it. But all options download in .png format and I can't seem to find out what is wrong.
Note that the preview is always png. The download function creates different file types upon clicking the button. Another point to note is the use of file.copy() in downloadhandler rather than something like
png(name)
plot()
dev.off()
This is because my plotting function is complex and file.copy() is more practical.
The code is below.
#ui.R ----------------------------------------------------------
shinyUI(fluidPage(
titlePanel("Download test"),
sidebarLayout(
sidebarPanel(
numericInput("fheight", "Height (cm)", min=2, max=15, step=1, value = 10),
numericInput("fwidth", "Width (cm)", min=2, max=15, step=1, value = 10),
selectInput("fres", "Res", choices=c("100","200","300"), selected = "100"),
selectInput("fformat", "File type", choices=c("png","tiff","jpeg","pdf"), selected = "png", multiple = FALSE, selectize = TRUE),
downloadButton('bn_download', 'Download Plot')
),
# Show a plot of the generated distribution
mainPanel(
imageOutput("plotoutput")
)
)
))
# server.R ----------------------------------------------------------
shinyServer(function(input, output) {
# store some values
store <- reactiveValues(dname="AwesomeDownload")
# data creation
fn_data <- reactive({
df <- data.frame(x=rnorm(50),y=rnorm(50))
})
# create filename
fn_downloadname <- reactive({
if(input$fformat=="png") filename <- paste0(store$dname,".png",sep="")
if(input$fformat=="tiff") filename <- paste0(store$dname,".tif",sep="")
if(input$fformat=="jpeg") filename <- paste0(store$dname,".jpg",sep="")
if(input$fformat=="pdf") filename <- paste0(store$dname,".pdf",sep="")
return(filename)
})
# render png preview
output$plotoutput <- renderImage({
df <- fn_data()
fheight <- input$fheight
fwidth <- input$fwidth
fres <- as.numeric(input$fres)
png(paste0(store$dname,".png",sep=""), height=fheight, width=fwidth, res=fres, units="cm")
plot(df)
dev.off()
return(list(src = paste0(store$dname,".png",sep=""),
contentType = "image/png",
width = round((input$fwidth*as.numeric(input$fres))/2.54, 0),
height = round((input$fheight*as.numeric(input$fres))/2.54, 0),
alt = "plot"))
},deleteFile=TRUE)
# download function
fn_download <- function()
{
df <- fn_data()
fheight <- input$fheight
fwidth <- input$fwidth
fres <- as.numeric(input$fres)
if(input$fformat=="pdf") fheight <- round(fheight*0.3937,2)
if(input$fformat=="pdf") fwidth <- round(fwidth*0.3937,2)
if(input$fformat=="png") png(fn_downloadname(), height=fheight, width=fwidth, res=fres, units="cm")
if(input$fformat=="tiff") tiff(fn_downloadname(), height=fheight, width=fwidth, res=fres, units="cm",compression="lzw")
if(input$fformat=="jpeg") jpeg(fn_downloadname(), height=fheight, width=fwidth, res=fres, units="cm",quality=100)
if(input$fformat=="pdf") pdf(fn_downloadname(), height=fheight, width=fwidth)
plot(df)
dev.off()
}
# download handler
output$bn_download <- downloadHandler(
filename = fn_downloadname(),
content = function(file) {
fn_download()
file.copy(fn_downloadname(), file, overwrite=T)
}
)
})
Removing parenthesis in the download filename fixed the issue. Yeah, don't even ask. I have no idea why this is so either. But it works.
Joe Cheng's answer:
Change this line:
filename = fn_downloadname(),
to this:
filename = fn_downloadname,