forked from hadley/mastering-shiny
-
Notifications
You must be signed in to change notification settings - Fork 0
/
action-transfer.Rmd
348 lines (272 loc) · 11.5 KB
/
action-transfer.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# Uploads and downloads {#action-transfer}
```{r, include = FALSE}
source("common.R")
```
Transferring files to and from the user is a common feature of apps. It's most commonly used to upload data for analysis, or download the results either as a dataset or as a report. This chapter shows the UI and server components you'll need to files in and out of your app.
```{r setup}
library(shiny)
```
## Upload
### UI
The UI needed to support file uploads is simple. Like most other UI components, you just need to supply the `id` and `label` arguments: `fileInput(id, label)`. The `width`, `buttonLabel` and `placeholder` arguments allow you to tweak the appearance in other ways. I won't discuss them further here, but you can read more about them in `?fileInput`.
```{r}
ui <- fluidPage(
fileInput("file", "Upload a file")
)
```
### Server
Most of the complication arises on the server side because unlike other inputs which return short vectors, `input$file` returns a data frame with four columns:
* `name`: the original file name on the uploader's computer.
* `size`: the file size in bytes. By default, you can only upload files up to
5 MB is file. You can increase this limit to (e.g.) 10 MB with
`options(shiny.maxRequestSize = 10 * 1024^2)`
* `type`: the "mime type" of the file. This is a formal specification of the
file type, which is usually derived from the extension. It is rarely needed.
* `datapath`: the path to where the path has been uploaded to the server.
The file is always saved to a temporary directory and given a temporary
number. Treat this path as ephemeral: if the user uploads more files, it
will go away.
I think the easiest way to get to understand this data structure is to make a simple app. Run the following code and upload a few files to get a sense of what data Shiny is providing.
```{r}
ui <- fluidPage(
fileInput("upload", NULL, buttonLabel = "Upload...", multiple = TRUE),
tableOutput("files")
)
server <- function(input, output, session) {
output$files <- renderTable(input$upload)
}
```
Note my use of the `label` and `buttonLabel` arguments to mildly customise the appearance, and use of `multiple = TRUE` to allow the user to upload multiple files.
### Uploading data
If the user is uploading a dataset, there are three details that you need to be aware of:
* `input$file` is initialised to `NULL` on page load, and so you'll need
`req(input$file)` to make sure your reactive code waits until the first file
is uploaded.
* The `accept` argument allows you to limit the possible inputs. The easiest
way is to supply a character vector of file extensions, like
`accept = ".csv"`. (You can also use mime types, see `?fileInput` for more
details.)
* The `accept` argument is only a suggestion to the browser, and is not always
enforced, so you should also use `validate()` to check the
extension[^extensions] in your server function.
[^extensions]: Note that the browser defintion of an extension is different to the definiton used by the browser - the browser uses `.csv` where `file_ext()` returns `.csv`.
Putting all these ideas together gives us the following app where you can upload a `.csv` file and see the first few rows:
```{r}
ui <- fluidPage(
fileInput("file", NULL, accept = ".csv"),
tableOutput("head")
)
server <- function(input, output, session) {
data <- reactive({
req(input$file)
ext <- tools::file_ext(input$file$filename)
validate(need(ext == "csv", "Please upload a CSV file"))
vroom::vroom(input$file$datapath, delim = ",")
})
output$head <- renderTable({
head(data(), 5)
})
}
```
Note that since `multiple = FALSE` (the default), `input$file` is a single row data frame, and `input$file$filename` is a length-1 character vector.
## Download
### UI
Again, the UI is straightforward: use either `downloadButton(id)` or `downloadLink(id)` to give the user something to click to download a file. They do have o ther arguments, but you'll hardly ever need them; see `?downloadButton` for the details.
```{r}
ui <- fluidPage(
downloadButton("download1"),
downloadLink("download2")
)
```
Classes: <http://bootstrapdocs.com/v3.3.6/docs/css/#buttons>
### Server
The server side of downloads is different to the other ouputs as you don't use a render function. Instead, you use `downloadHandler()`, which takes two arguments:
* `filename` is a function with no arguments that returns a single string
giving the name of the file. This is the string that will appear in
the download dialog box. If the file name is constant (i.e. it doesn't
depend on anything else in the app), you can skip the function and just set
it to a string.
* `content` is a function with a single argument, `file`, which is the path
where your should place the file to be downloaded. The return argument of
this function is ignored; it's called purely for its side-effect of creating
a file at the given path.
For example, if you wanted to provide a `.csv` file, your server function might contain a snippet like this:
```{r, eval = FALSE}
output$download <- downloadHandler(
filename = function() {
paste0(input$dataset, ".csv")
},
content = function(file) {
write.csv(data(), file)
}
)
```
### Downloading data
The following app shows off the basics by creating an app that lets you download any dataset in the datasets package as a tsv[^tsv-csv] file.
```{r}
ui <- fluidPage(
selectInput("dataset", "Pick a dataset", ls("package:datasets")),
tableOutput("preview"),
downloadButton("download", "Download .tsv")
)
server <- function(input, output, session) {
data <- reactive({
get(input$dataset, "package:datasets")
})
output$preview <- renderTable({
head(data())
})
output$download <- downloadHandler(
filename = function() {
paste0(input$dataset, ".tsv")
},
content = function(file) {
vroom::vroom_write(data(), file)
}
)
}
```
[^tsv-csv]: `.tsv` file is to be preferred over a `.csv` because many European countries use commas to separate the whole and fractional parts of a number (e.g. `1,23` not `1.23`). This means they can't use commas to separate fields and instead use semi-colons. It's best to just side-step this issue together, and instead just use tab separatea files.
### Downloading reports
Another common use of downloads is to prepare a report that summarises the result of interactive exploration in the Shiny app. One powerful way to generate such a report is with a parameterised RMarkdown document, <https://bookdown.org/yihui/rmarkdown/parameterized-reports.html>. A parameterised RMarkdown file has a `params` field in the YAML metadata:
```yaml
title: My Document
output: html_document
params:
year: 2018
region: Europe
printcode: TRUE
data: file.csv
```
Inside the document you can refer to those values with `params$year`, `params$region` etc.
What makes this technique powerful is you can also set the parameters using code, by calling `rmarkdown::render()` with the `params` argument. This makes it possible to generate a range of RMarkdown reports from a single app.
Here's a simple example taken from <https://shiny.rstudio.com/articles/generating-reports.html>, which describes this technique in more detail.
```{r}
ui <- fluidPage(
sliderInput("n", "Number of points", 1, 100, 50),
downloadButton("report", "Generate report")
)
server <- function(input, output, session) {
output$report <- downloadHandler(
filename = "report.html",
content = function(file) {
params <- list(n = input$n)
rmarkdown::render("report.Rmd",
output_file = file,
params = params,
envir = new.env(parent = globalenv())
)
}
)
}
```
If you want to produce other output formats, just change the output format in the `.Rmd`, and make sure to update the extension.
There are a few other tricks worth knowing about:
* If the report takes some time to generate, use one of the techniques from
Chapter \@ref(action-feedback) to let the user know that your app is
working.
* Often, when deploying the app, you can't write to the working directory,
which RMarkdown will attempt to do. You can work around this by copying
the report to a temporary directory:
```{r}
report_path <- tempfile(fileext = ".Rmd")
file.copy("report.Rmd", report_path, overwrite = TRUE)
```
Then replaaing `"report.Rmd"` in the call to `rmarkdown::render()` with
`report_path`.
* By default, RMarkdown will render the report in the current process, which
means that it will inherit many settings from the Shiny app (like loaded
packages, options, etc). For greater robustness, I recommend running in
fresh R session by using the callr package.
```{r, eval = FALSE}
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)
)
}
)
}
```
You can see all these pieces put together in `rmarkdown-report/`.
Later, in Chapter XYZ, we'll come back to generating a complete report of all the code that your app has executed.
## Case study
We'll put all the pieces together in a small case study where we upload a file (with user supplied separator), preview it, perform some optional transformations using the [janitor package](http://sfirke.github.io/janitor), by Sam Firke, 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:
1. Uploading and parsing the file.
2. Cleaning the file.
3. Downloading the file.
```{r}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
fileInput("file", "Data", buttonLabel = "Upload..."),
textInput("delim", "Delimiter (leave blank to guess)", ""),
numericInput("skip", "Rows to skip", 0, min = 0)
),
mainPanel(
h2("Raw data"),
tableOutput("preview1")
)
),
sidebarLayout(
sidebarPanel(
checkboxInput("snake", "Rename columns to snake case?"),
checkboxInput("constant", "Remove constant columns?"),
checkboxInput("empty", "Remove empty cols?")
),
mainPanel(
h2("Cleaner data"),
tableOutput("preview2")
)
),
fluidRow(
column(width = 4, downloadButton("download", class = "btn-block"))
)
)
```
And this same organisation makes it easier to understand the app:
```{r}
server <- function(input, output, session) {
raw <- reactive({
req(input$file)
delim <- if (input$delim == "") NULL else input$delim
vroom::vroom(input$file$datapath, delim = delim, skip = input$skip)
})
output$preview1 <- renderTable(raw())
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
})
output$preview2 <- renderTable(tidied())
output$download <- downloadHandler(
filename = function() {
paste0(tools::file_path_sans_ext(input$file$name), ".tsv")
},
content = function(file) {
vroom::vroom_write(tidied(), file)
}
)
}
```
### Exercises