Code
downloadButton <- function(...) {
tag <- shiny::downloadButton(...)
tag$attribs$download <- NULL
tag
}
Chapter section list
R Code 9.1 : File upload UI
Try in interactive mode adding / changing the arguments label
, width
, buttonLabel
and placeholder
to see how it affects the UI appearance.
The UI needed to support file uploads is simple: just add shiny::fileInput()
to your UI.
Like most other UI components, there are only two required arguments: id
and label
. The width
, buttonLabel
and placeholder
arguments allow you to tweak the appearance in other ways. I won’t discuss them here, but you can read more about them in File Upload Control — fileInput.
Handling fileInput()
on the server is a little more complicated than other inputs. Most inputs return simple vectors, but fileInput()
returns a data frame with four columns:
shiny.maxRequestSize
option prior to starting Shiny. For example, to allow up to 10 MB run options(shiny.maxRequestSize = 10 * 1024^2
).R Code 9.2 : File upload server
fileInput()
does not show multiple uploaded files. One can see only the last one.
This issue was on May 2, 2021 opened at GitHub, but it is still not closed. I do not know with my rudimentary knowledge at the moment (2025-06-08) how to solve this problem.
If the user is uploading a dataset, there are two details that you need to be aware of:
input$upload
is initialized to NULL
on page load, so you’ll need req(input$upload)
to make sure your code waits until the first file is uploaded.accept
argument allows you to limit the possible inputs. The easiest way is to supply a character vector of file extensions, like accept = ".csv"
. But the accept argument is only a suggestion to the browser, and is not always enforced, so it’s good practice to also validate it (e.g. Section 8.1.1) yourself. The easiest way to get the file extension in R is tools::file_ext()
, just be aware it removes the leading .
from the extension.Putting all these ideas together gives us the following app where you can upload a .csv
or .tsv
file and see the first n
rows. See it in action in https://hadley.shinyapps.io/ms-upload-validate.
R Code 9.3 : Uploading data
Note that since multiple = FALSE
(the default), input$file
will be a single row data frame, and input$file$name
and input$file$datapath
will be a length-1 character vector.
downloadButton(id)
or downloadLink(id)
to give the user something to click to download a file. You can customize their appearance using the same class and icon arguments as for actionButtons()
, as described in Section 2.2.7.downloadButton()
is not paired with a render function. Instead, you use downloadHandler()
.downloadHandler()
has two arguments, both functions:
filename
should be a function with no arguments that returns a file name (as a string). The job of this function is to create the name that will be shown to the user in the download dialog box.content
should be a function with one argument, file
, which is the path to save the file. The job of this function is to save the file in a place that Shiny knows about, so it can then send it to the user. This is an unusual interface, but it allows Shiny to control where the file should be saved (so it can be placed in a secure location) while you still control the contents of that file.Next we’ll put these pieces together to show how to transfer data files or reports to the user.
The following app shows off the basics of data download by allowing you to download any dataset in the datasets package as a tab separated file.
.tsv
instead of csv
I recommend using .tsv
(tab separated value) instead of .csv
(comma separated values) because many European countries use commas to separate the whole and fractional parts of a number (e.g. 1,23 vs 1.23). This means they can’t use commas to separate fields and instead use semi-colons in so-called “c”sv files! You can avoid this complexity by using tab separated files, which work the same way everywhere.
R Code 9.4 : Downloading data
To properly download the file in shinylive
you need a workaround. Put the following function outside server()
:
downloadButton <- function(...) {
tag <- shiny::downloadButton(...)
tag$attribs$download <- NULL
tag
}
This workaround is not necessary in an app.R
file.
Note the use of validate()
to only allow the user to download datasets that are data frames. A better approach would be to pre-filter the list, but this lets you see another application of validate()
.
As well as downloading data, you may want the users of your app to download a report that summarizes the result of interactive exploration in the Shiny app. This is quite a lot of work, because you also need to display the same information in a different format, but it is very useful for high-stakes apps.
One powerful way to generate such a report is with a parameterised RMarkdown document. A parameterised RMarkdown file has a params field in the YAML metadata:
title: My Document
output: html_document
params:
year: 2018
region: Europe
printcode: TRUE data: file.csv
Inside the document, you can refer to these values using params$year
, params$region
etc. The values in the YAML metadata are defaults; you’ll generally override them by providing the params argument in a call to rmarkdown::render()
. This makes it easy to generate many different reports from the same .Rmd.
Here’s a simple example adapted from https://shiny.rstudio.com/articles/generating-reports.html, which describes this technique in more detail. The key idea is to call rmarkdown::render()
from the content argument of downloadHander()
. If you want to produce other output formats, just change the output format in the .Rmd
, and make sure to update the extension (e.g. to .pdf
). See it in action at https://hadley.shinyapps.io/ms-download-rmd.
R Code 9.5 : Downloading reports
The code chunk does not produce an error. But whenever you click the button “Generate report” the following message is created in a new browser window:
An error has occurred! pandoc version 1.12.3 or higher is required and was not found (see the help page ?rmarkdown::pandoc_available).
I tried to solve the problem with hints from StackOverflow, especially the answer edited by Yihui Xie.
Sys.getenv("RSTUDIO_PANDOC")
#> [1] "/Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64"
But to add Sys.setenv("RSTUDIO_PANDOC"=Sys.getenv("RSTUDIO_PANDOC"))
did not work.
I tried it also with another pandoc version installed at my macOS with homebrew:
rmarkdown::pandoc_exec()
#> [1] "/opt/homebrew/bin/pandoc"
I assume there are path problems with shinylive
.
It’ll generally take at least a few seconds to render a .Rmd
, so this is a good place to use a notification from Section 8.2.
There are a couple of other tricks worth knowing about:
Then replace “report.Rmd” with report_path
in the call to rmarkdown::render()
:
render()
in a separate R session using the {callr} package:render_report <- function(input, output, params) {
rmarkdown::render(input,
output_file = output,
params = params,
envir = new.env(parent = globalenv())
)
}
server <- function(input, output) {
output$report <- downloadHandler(
filename = "report.html",
content = function(file) {
params <- list(n = input$slider)
callr::r(
render_report,
list(input = report_path, output = file, params = params)
)
}
)
}
R Code 9.6 : Downloading reports
The last two code chunks didn’t work.
At first I got the message with a code version I can’t reproduce > Warning: Error in : ! in callr subprocess
> Caused by error in abs_path(input)
:
> ! The file ’/var/folders/sd/g6yc4rq1731__gh38rw8whvc0000gr/T//RtmplpyWiO/file422772b5bfd1.Rmd’ does not exist.
In the current code version I got an error message whenever I try to include the report.Rmd
file.
error: object ‘params’ not found
Resource 9.1 : Downloading reports with shiny and looking for shinylive
error messages
shinylive
I got the error message reported in Warning 9.4.!!
) with the dot-dot operator (..()
).shinylive-r
issue. At least I got exactly the same error message as in the question. But even with the different hints to solve the problem, notably the edited answer by Yihui Xie and the following several comments, I could not solve the shinylive-r
error message.To finish up, we’ll work through a small case study where we upload a file (with user supplied separator), preview it, perform some optional transformations using the {janitor} package (Firke 2024), and then let the user download it as a .tsv
.
To make it easier to understand how to use the app, I’ve used sidebarLayout()
to divide the app into three main steps:
All three parts get assembled into a single fluidPage()
and the server logic follows the same organization.
R Code 9.7 : Upload a file, clean it, and then download the results
Use the {ambient} package (Pedersen and Peck 2022) to generate worley noise and download a PNG of it.
There is a conflict between the two versions. The first one (Shiny) does not work correctly at the beginning. One has to restart the app (clicking the right top arrow in the code window). The drop down menu changes it form and the reactive values work as intended. (The same problem occurs with the {bslib} version if it is the first one. There is no problem if there is only one version of the app.)
Exercise 9.1 : Generate Worley noise and download a PNG of the resulting image
R Code 9.8 : Generate noise with two variable parameters
R Code 9.9 : Generate noise with eight variable parameters
I was surprised how easy the sidebar layout was created with {bslib}. The only issue I discovered so far: The default layout is much bigger and therefore not so convenient as in the {shiny} layout version. But I assume that there are option to change the overall appearance easily.
See Dashboards, an article that shows using {bslib} to create the user interface (UI) for Shiny dashboards.
In this exercise I had to overcome several difficulties:
Exercise interpretation: First of all I had to interpret the challenge. I don’t now anything about multidimensional noise generator. So I had to use the two provided examples to play around. Soon I learned that there are several parameters that change the resulting noise picture. I concluded that it would be nice to have an environment where you could experiment with the different parameters. This would be a really advantage of a Shiny app in contrast to pages with fixed parameters.
I learned that Solution to Mastering Shiny and Mastering Shiny Solution did not capture this idea. The first one skipped this exercise, the second one provided a solution without Shiny.
content(file) function: My biggest problem was to download the PNG. I didn’t understand that the argument file
for the function content()
is only the way of Shiny to save the file internally. So the app author must not assign a value to file
.
PNG function: I never used so far the png()
function with the dev.off()
command at the end of the transaction.
Not knowing about png()
with my content(file)
misunderstanding it took me several hours to find the solution.
Resource 9.2 : PNG and other graphic devices
Create an app that lets you upload a CSV file, select a variable, and then perform a t.test()
on that variable. After the user has uploaded the csv file, you’ll need to use updateSelectInput()
to fill in the available variables. See (XXX_10.1?) for details.
I have extended the task with three more options:
t.test()
requires numeric data, load only columns with numeric data into updateSelectInput()
.0
.Resource 9.3 : How to calculate and report a t test?
Exercise 9.2 : Upload file, select a variable and perform a t test
Create an app that lets the user upload a csv file, select one variable, draw a histogram, and then download the histogram. For an additional challenge, allow the user to select from .png
, .pdf
, and .svg
output formats.
I have developed several version of this exercise provided in different tabs:
base R plot: This is the minimum solution using base R plot. It took my a long time to learn and understand how to reference a specific column for the plot. The $
does not work, you have to use [[
like data()[[input$my_column]]
. See SO: Shiny R Histogram.
I used to replicate the code for plotting the histogram in renderPlot()
and downloadHandler()
. By creating a reactive for the plot code I could prevent duplication. See: SO: How to avoid code duplication in a render() function in Shiny for R.
ggplot: This is the minimal solution with {ggplot2}. The reference to a column with {ggplot2} is even more complicated with .data[[input$my_column]]
. The .data
part is the tidy eval pronoun. See: SO: What is the difference between .
and .data
.
Tidy evaluation is a concept used in the tidyverse, particularly in packages like dplyr and ggplot2, to handle the evaluation of expressions in a way that allows for more flexible and powerful programming. It is especially useful when you want to build functions that can take column names as arguments and use them within dplyr verbs.
It is a very complex topic and I will not open this Pandora box here, but these two references might provide a good start: Programming with {dplyr} and Evaluate an expression with quosures and pronoun support from the {rlang} package. (The {rlang} package is in this context important as it collects functions for the core {tidyverse} features like tidy evaluation.)
Hist button: In the previous versions the histogram graphics appeared automatically with the first column in the selectInput()
menu. In this version I have added a button to start drawing the histogram explicitly.
shinyjs: In all the previous trials was the downloadButton()
active, even if there was nothing to download. I learned that I could prevent this easily with {shinyjs}. See SO: shiny app: disable downloadbutton
dynamic-UI: With a dynamic UI there is another solution to present an active downloadButton()
. The user only get the button when there is something to download. (It seems to me that this will be covered in the next book chapter on Dynamic UI. See (XXX_10?).) But here I have used SO: Display download button in Shiny R only when output appears in Main Panel
I do not know what is the better UI alternative. To have an inactive button that changes or appearing a complete new UI element during the processing.
radioButtons: In this exercise the user can decide if (s)he wants the graphic as base R plot or with {ggplot2}.
I personally prefer the ggplot()
version for two reasons: (ggplot2) is the modern plotting variant and has in the meanwhile a huge ecosystem on supporting packages and I am more experienced with {ggplot2} than with base R graphic.
The first time I selected a tab of Exercise 9.3 I got briefly error: figure margin too large
. The problem is that the reserved plotting error is in the beginning too small. To prevent this error message I had to change plotOutput("hist")
to plotOutput("hist", width = '600px', height = '400px')
.
By this occasion I also changed the resolution in renderPlot()
from the standard 72 to the better resolution of 96. For instance: renderPlot(my_hist(), res = 96)
. If there is a function with several lines (and not one liner function my_hist()
as in the just mentioned example), then you have to include the res = 96
addition between the ending })
like }, res = 96)
.
Exercise 9.3 : Upload file, draw histogram and download in different formats
R Code 9.10 : Minimal version with base R plot
R Code 9.11 : Minimal version with {ggplot2}
R Code 9.13 : Enable download button with {shinyjs}
R Code 9.14 : Dynamic UI: Display download button only when there is something to download
Resource 9.4 : Used resources to solve exercise 3: Download plot with shiny
ggplot2()
: Posit forum..
and .data
: StackOverflowI also used two GitHub Gist articles, even if there were not decisive for my solution they provided general background to work with shiny plots:
Write an app that allows the user to create a Lego mosaic from any .png
file using Ryan Timpe’s {brickr} package. Once you’ve completed the basics, add controls to allow the user to select the size of the mosaic (in bricks), and choose whether to use “universal” or “generic” color palettes.
It turned out, that I couldn’t run my solution because the {brickr} was not available for shinylive
(webR / WebAssembly). I think the reason is that the current version of the package is neither on CRAN anymore (it was until the previous version 0.3.5) nor is there a GitHub release available.
Error in get_github_wasm_assets()
:
! Can’t find GitHub release for github::ryantimpe/brickr@HEAD
! Ensure a GitHub release exists for the package repository reference: “HEAD”.
ℹ Alternatively, install a CRAN version of this package to use the default Wasm
binary repository.
Caused by error in gh::gh()
:
! GitHub API error (404): Not Found
✖ URL not found:
https://api.github.com/repos/ryantimpe/brickr/releases/tags/HEAD
ℹ Read more at
https://docs.github.com/rest/releases/releases#get-a-release-by-tag-name
I have therefore only the apps source code provided. To run the apps, copy it form the code chunks below or fork this repo and run the provided apps.
Resource 9.5 : Compiling packages for Webassembly and webR
About providing / compiling R packages there is a daunting amount of literature available. I do not understand the connection of all of these resources and therefore is it extremely difficult for me to compile the {bricksr} package for WebAssembly myself.
Nonetheless I have collected links to the (presumed) most important resources to solve my problem:
rwasm
V8
webR
YouTube Videos by Geroge Stagg
Procedure 9.1 : Exercise 4 of chapter 9: LEGO bricks
Process
button and to provide a spinning wheel provided by {waiter}. See Listing / Output 9.4.I’ve got several more ideas to improve the app.
Code Collection 9.1 : Build LEGO mosaics from any .png
file using {bricksr}
R Code 9.16 : Play with the demo code to understand the core functionality of {bricksr}
library(brickr)
demo_img = tempfile()
download.file("http://ryantimpe.com/files/mf_unicorn.PNG", demo_img, mode = "wb")
(
mosaic1 <- png::readPNG(demo_img) |>
image_to_mosaic(img_size = 36) |>
build_mosaic()
)
R Code 9.17 : Replicate {bricksr} demo code with a Shiny app
library(shiny)
library(bslib)
library(base64enc)
library(brickr)
ui <- page_sidebar(
titlePanel("Emulate LEGO Bricks"),
sidebar = sidebar(
fileInput("upload", "Load PNG image", accept = c(".png"))
),
uiOutput("image"),
plotOutput("plot", width = "600px", height = "400px")
)
server <- function(input, output, session) {
base64 <- reactive({
req(input$upload)
dataURI(file = input$upload$datapath, mime = "image/png")
})
output$plot <- renderPlot({
req(base64())
mosaic1 <- png::readPNG(input$upload$datapath) |>
image_to_mosaic(img_size = c(72, 48), color_palette = "generic") |>
build_mosaic()
mosaic1
}
)
output$image <- renderUI({
req(base64())
tags$div(
tags$img(src = base64(), width = "100%"),
style = "width: 400px;"
)
})
}
shinyApp(ui, server)
R Code 9.18 : Create interactive functionality to build mosaics with {bricksr}
library(shiny)
library(bslib)
library(base64enc)
library(brickr)
ui <- page_sidebar(
titlePanel("Emulate LEGO Bricks"),
sidebar = sidebar(
fileInput("upload", "Load PNG image", accept = c(".png", ".PNG")),
numericInput("width", "Width", 36),
numericInput("height", "Height", 36),
selectInput("pal", "Color Palette:",
c("Generic" = "generic",
"Universal" = "universal")),
actionButton("process", "Process",
icon = icon("cog", lib = "glyphicon"),
disabled = TRUE)
),
uiOutput("image"),
plotOutput("plot", width = "600px", height = "400px")
)
server <- function(input, output, session) {
base64 <- reactive({
req(input$upload)
updateActionButton(
session, "process",
disabled = FALSE)
dataURI(file = input$upload$datapath, mime = "image/png")
})
output$plot <- renderPlot({
req(input$process > 0)
my_mosaic <-
png::readPNG(input$upload$datapath) |>
image_to_mosaic(img_size =
c(isolate(input$width), isolate(input$height)),
color_palette = isolate(input$pal)) |>
build_mosaic()
my_mosaic
})
output$image <- renderUI({
input$upload
tags$div(
tags$img(src = base64(), width = "100%"),
style = "width: 400px;"
)
})
}
shinyApp(ui, server)
R Code 9.19 : Improving User Interface of the LEGO bricks shiny app
library(shiny)
library(bslib)
library(base64enc)
library(brickr)
library(waiter)
waiting_screen <- tagList(
spin_flower(),
h4("Emulating picture with Lego bricks...")
)
ui <- page_sidebar(
useWaiter(),
titlePanel("Emulate LEGO Bricks"),
sidebar = sidebar(
fileInput("upload", "Load PNG image", accept = c(".png", ".PNG")),
numericInput("width", "Width", 36),
numericInput("height", "Height", 36),
selectInput("pal", "Color Palette:",
c("Generic" = "generic",
"Universal" = "universal")),
actionButton("process", "Process",
icon = icon("cog", lib = "glyphicon"),
disabled = TRUE)
),
uiOutput("image"),
plotOutput("plot", width = "600px", height = "400px")
)
server <- function(input, output, session) {
# create a waiter
w <- Waiter$new()
base64 <- reactive({
req(input$upload)
updateActionButton(
session, "process",
disabled = FALSE)
dataURI(file = input$upload$datapath, mime = "image/png")
})
output$plot <- renderPlot({
req(input$process > 0)
# w$show()
waiter_show(html = waiting_screen, color = "black")
my_mosaic <-
png::readPNG(input$upload$datapath) |>
image_to_mosaic(img_size =
c(isolate(input$width), isolate(input$height)),
color_palette = isolate(input$pal)) |>
build_mosaic()
# w$hide()
waiter_hide()
my_mosaic
})
output$image <- renderUI({
input$upload
tags$div(
tags$img(src = base64(), width = "100%"),
style = "width: 400px;"
)
})
}
shinyApp(ui, server)
The app from the case study Section 9.3 contains a very large reactive. Break it up into multiple pieces so that (e.g.) janitor::make_clean_names()
is not re-run when input$empty
changes.
tidied <- reactive({
out <- raw()
if (input$snake) {
names(out) <- janitor::make_clean_names(names(out))
}
if (input$empty) {
out <- janitor::remove_empty(out, "cols")
}
if (input$constant) {
out <- janitor::remove_constant(out)
}
out })
R Code 9.20 : Code reorganizing of the final version of the case study in Section 9.3
After two days I gave up on this exercise and looked at the solution in Mastering Shiny Solutions.
I knew that the main change has to be happen in the cleaning function. Exactly the part of code that was repeated in the exercise formulation. But I did not know how to collect the results of of the three cleaning actions into on tableRender()
. I tried it with reactiveVal()
and managed the separate cleaning procedure but only for the first time. I didn’t succeed to develop an app where the user could also take back the cleaning action through by resetting the selection.
I learned two things from the solution:
Each change of an input results is a call to each available reactive function. Thinking now about it, this seems obvious. In contrast to the eventReactive()
handler that only is activated by a change of a specific input value a reactive expression is called always when an input value has changed.
But most important I did not know that “if a reactive expression is marked as invalidated, any other reactive expressions that recently called it are also marked as invalidated. In this way, invalidations ripple through the expressions that depend on each other” (`reactive()``` help page).
This “ripple through the expression” is good visible as the result of every previous cleaning action is the start value of the out
variable. Important is here the order of the different reactives. The raw data frame (raw()
) must be included in the first reactive and is followed by one reactive after another applying or not applying each cleaning function. In this case is the order from top to bottom, because each reactive was invalidated by the previous one. this structure was a surprise for me, as I had a mental model of Shiny where code sequences never matters.
Attention: The video starts at minute 11:20.↩︎