-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod_interactivity.qmd
342 lines (237 loc) · 18.6 KB
/
mod_interactivity.qmd
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
---
title: "Creating Interactive Apps"
code-annotations: hover
execute:
eval: false
---
## Overview
Shiny is a popular tool that allows users to build interactive web applications without the normally pre-requisite web development expertise. In addition to Shiny apps being simpler to build for the programmer they are often used to allow visitors to perform coding tasks without ever actually writing code. These are huge advantages because they reduce or eliminate significant technical barriers in developing truly interactive applications.
In synthesis contexts, Shiny can be used for a variety of valuable purposes. You can use it to develop dashboards for sharing data with related communities, allow your team to quickly "play with" exploratory graphs, or even to create a data submission portal (as is the case with some <u>R</u>esearch <u>C</u>oordination <u>N</u>etworks or "RCNs").
Note that Shiny can be built in either R or Python 'under the hood' but for the purposes of this module we'll focus on R.
## Learning Objectives
After completing this topic you will be able to:
- <u>Define</u> the three fundamental components of a Shiny app
- <u>Explain</u> benefits and limitations of interactive approaches to data exploration
- <u>Generate</u> an interactive app with Shiny
- <u>Use</u> text formatting methods in a Shiny app
- <u>Explore</u> available Shiny layout options
- <u>Create</u> a Shiny app
- <u>Describe</u> (briefly) the purpose of deploying a Shiny app
## Preparation
This is a "bonus" module and thus was created for asynchronous learners. There is no suggested preparatory work.
## Needed Packages
If you'd like to follow along with the code chunks included throughout this module, you'll need to install the following packages:
```{r install-pkgs}
#| eval: false
# Note that these lines only need to be run once per computer
## So you can skip this step if you've installed these before
install.packages("tidyverse")
install.packages("shiny")
install.packages("htmltools")
install.packages("lterdatasampler")
```
We'll load the Tidyverse meta-package here to have access to many of its useful tools when we need them later as well as the `shiny` package.
```{r libs}
#| message: false
# Load needed libraries
library(tidyverse); library(shiny)
```
## Shiny Fundamentals
All Shiny apps are composed of three pieces: a <u>u</u>ser <u>i</u>nterface (UI), a server, and a call to the `shinyApp` function. The user interface includes everything that the user sees and can interact with; note that this includes _both_ inputs and outputs. The server is responsible for all code operations performed on user inputs in order to generate outputs specified in the UI. The server is _not_ available to the user. Finally, the `shinyApp` function simply binds together the UI and server and creates a living app. The app appears either in your RStudio or in a new tab on a web browser depending on your settings.
For those of you who write your own functions, you may notice that the syntax of Shiny is **very** similar to the syntax of functions. If you have not already, your quality of life will benefit greatly if you turn on "rainbow parentheses" in RStudio (Tools {{< fa arrow-right >}} Global Options {{< fa arrow-right >}} Code {{< fa arrow-right >}} Display {{< fa arrow-right >}} Check "Use rainbow parentheses" box).
Let's consider an artificially simple Shiny app so you can get a sense for the fundamental architecture of this tool.
```{r basic-shiny}
# Define the UI
basic_ui <- shiny::fluidPage( # <1>
"Hello there!"
)
# Define the server
basic_server <- function(input, output){ } # <2>
# Generate the app
shiny::shinyApp(ui = basic_ui, server = basic_server)
```
1. The `fluidPage` function is important for leaving flexibility in UI layout which we'll explore later in the module
2. Because this app has no inputs or outputs, it doesn't need anything in the 'server' component (though it still does require an empty server!)
If you copy and run the above code, you should see an app that is a blank white page with "Hello there!" written in the top left in plain text. Congratulations, you have now made your first Shiny app! Now, your reason for exploring this module likely involves an app that actually does something but the fundamental structure of all apps--even skeletal apps like this one--is the same. More complicated apps will certainly have more content in the UI and server sections but all Shiny apps will have this tripartite structure.
## Interactive Apps
Now that we've covered non-reactive apps, let's create an interactive one! It is important to remember that the user interface needs to contain _both_ the inputs the user can make _and_ the outputs determined by those inputs. The server will be responsible for turning the inputs into outputs but if you want your interactive app to actually show the user the interactivity you need to be careful to include the outputs in the UI.
Essentially all Shiny UI functions use the same syntax of `<value class>Input` or `<value class>Output`. So, determining how you want the user to engage with your app is sometimes as straightforward as identifying the class of the value you want them to interact with. <u>Shiny calls these helper functions "widgets".</u>
Let's consider an app that accepts a single number and returns the square root of that number.
```{r reactive-shiny}
# Define the UI ----
reactive_ui <- shiny::fluidPage(
# Create input
shiny::numericInput(inputId = "num_in", # <1>
label = "Type a number",
value = 16),
# Include some plain text for contextualizing the output
"Square root is: ", # <2>
# Create output
shiny::textOutput(outputId = "num_out")
) # Close UI
# Define server ----
reactive_server <- function(input, output){
# Reactively accept the input and take the square root of it
root <- shiny::reactive({ # <3>
sqrt(x = input$num_in) # <4>
})
# Make that value an output of the server/app
output$num_out <- shiny::renderText( # <5>
expr = root() # <6>
)
} # Close server
# Generate the app ----
shiny::shinyApp(ui = reactive_ui, server = reactive_server)
```
1. Note that the argument name is capital "I" but _lowercase_ "d". Typing `inputID` is a common and frustrating source of error for Shiny app developers
2. Every element of the UI--except the last one--needs to end with a comma
3. All reactive elements (i.e., those that change as soon as the user changes an input) need to be specified inside of `reactive` with _both_ parentheses and curly braces
4. The name of this input exactly matches the `inputId` we defined in the UI. That it _is_ an input is defined by our use of the `numericInput` widget
5. The name of this output exactly matches the `outputId` we told the UI to expect.
6. Reactive elements essentially become functions in their own right! So, when we want to use them, we need to include empty parentheses next to their name
We included a _lot_ of footnote annotations in that code chunk to help provide context but there are a few small comments that are worthwhile to bring up at this stage.
1. UI outputs and server renders _must_ match
The widget you use in the UI to return an output must correspond to the function used in the server to generate that output. In this example, we use `textOutput` in the UI so in the server we use `renderText`. Essentially all widgets in Shiny use this `<class>Output` versus `render<Class>` syntax which can be a big help to visual checks that your app is written correctly. You will need to be sure that whatever the 'class' is, it is _lowercase_ in the UI but _title case_ in the server (i.e., only first letter capitalized).
2. Use section header format
This app is relatively short but we think effectively hints at how long and convoluted purpose-built Shiny apps can easily become. So, we recommend using section headers in your Shiny app code. You can do this by putting either four hyphens or four hashtags at the end of a comment line (e.g., `# Section 1 ####` or `# My header ----`). Headings defined in this way will appear in the bottom left of the "Source" pane of RStudio next to a light orange hashtag symbol. Clicking the text in that area will open a drop-down menu showing all headings in your current file and clicking one of the other headings will instantly jump you to that heading. This can be _incredibly_ convenient when you're trying to navigate a several hundred line long Shiny app. While rainbow parentheses can be useful for avoiding typos _within a section_, section headers make it much easier to avoid typos _across_ sections.
If you don't use headings already (or your cursor is on a line before the first heading), the relevant bit of the "Source" pane will just say "(Top Level)" and will not have the golden hashtag symbol.
## Including Data
You can also use your Shiny app to work with a full data table! When running your app locally, you only need to read in the data as you normally would then run the app. By having read in the data you will ensure the object is in your environment and accessible to the app. However, keep in mind this will only work in "local" (i.e., non-deployed) contexts. See our--admittedly brief--discussion of deployment at the end of this module.
Let's explore an example using data about fiddler crabs (_Minuca pugnax_) at the [Plum Island Ecosystems (PIE) LTER](https://pie-lter.ecosystems.mbl.edu/welcome-plum-island-ecosystems-lter) site from the [`lterdatasampler` R package](https://lter.github.io/lterdatasampler/). The app we're about to create will make a graph between any two (numeric) columns.
```{r data-shiny}
# Load lterdatasampler package
library(lterdatasampler)
# Load fiddler crab data
data(pie_crab) # <1>
# Define the UI ----
data_ui <- shiny::fluidPage(
# Let the user choose which the X axis
shiny::selectInput(inputId = "x_vals",
label = "Choose the X-axis",
choices = setdiff(x = names(pie_crab), # <2>
y = c("date", "site", "name")),
selected = "latitude"),
# Also the Y axis
shiny::selectInput(inputId = "y_vals",
label = "Choose the Y-axis",
choices = setdiff(x = names(pie_crab),
y = c("date", "site", "name")),
selected = "size"),
# Return the desired plot
shiny::plotOutput(outputId = "crab_graph")
) # Close UI
# Define the server ----
data_server <- function(input, output){
# Reactively identify X & Y axess
picked_x <- shiny::reactive({ input$x_vals }) # <3>
picked_y <- shiny::reactive({ input$y_vals })
# Create the desired graph
output$crab_graph <- shiny::renderPlot(
ggplot(pie_crab, aes(x = .data[[picked_x()]], y = .data[[picked_y()]])) + # <4>
geom_point(aes(fill = .data[[picked_x()]]), pch = 21, size = 2.5) +
labs(x = stringr::str_to_title(picked_x()),
y = stringr::str_to_title(picked_y())) +
theme_bw()
) # Close plot rendering
} # Close server
# Generate the app ----
shiny::shinyApp(ui = data_ui, server = data_server)
```
1. Note the loading of the data is done _outside_ of the app! You can have the app load its own data but that is more complicated than this example needs to be.
2. To make our life easier in the server we can exclude non-number columns
3. See how we're reactively grabbing both axes?
4. `ggplot2` requires special syntax to specify axes with quoted column names (which is how reactive Shiny elements from that widget are returned)
## Layouts
Experimenting with different app layouts can be a fun step in the process of making an app that is as effective as possible! We do recommend that during app development you stick with a very simple user interface because it'll be easier to make sure your inputs and outputs work as desired. Once you are satisfied with those elements you can relatively easily chengs the UI to help guide users through your app.
As implied by that preface, <u>layouts are exclusively an element of the user interface</u>! This is great when you have an app with a complicated server component because you won't need to mess with that at all to get the UI looking perfect. In the examples below, we'll generate a non-interactive app so that we can really emphasize the 'how to' perspective of using different layouts.
### Sidebar
One of the more common Shiny UI choices is to use a sidebar. The sidebar typically takes up about one third of the width of the app while the remaining two thirds is taken up by the main panel. The sidebar can be nice place to put all the user inputs and have the outputs display in the main panel. This format allows for really clear visual separation between where you want the user to interact with the app versus where the results of their choices can be viewed.
```{r layout-sidebar}
# Define the UI
sidebar_ui <- shiny::fluidPage(
# Define the layout type
shiny::sidebarLayout( # <1>
# Define what goes in the eponymous sidebar
shiny::sidebarPanel(
"Hello from the sidebar!"
), # <2>
# Define what goes in the main panel
shiny::mainPanel(
"Hello from the main panel!"
) ) ) # <3>
# Define the server
basic_server <- function(input, output){ }
# Generate the app
shiny::shinyApp(ui = sidebar_ui, server = basic_server)
```
1. Notice that everything else in the UI is wrapped inside this function. If you want something above/below the sidebar vs. main panel you'll need to put that content outside of this function's parentheses but still in the `fluidPage` parentheses
2. Be careful not to forget this comma separating the `sidebarPanel` and `mainPanel` functions!
3. Three closing parentheses are needed to close the UI elements. This is why it's _really_ helpful to use rainbow parentheses in your coding environment!
### Tab Panels
If you feel that your app is better represented in separate pages, tab panels may be a better layout choice! The result of this layout is a series of discrete tabs along the top of your app. If the user clicks one of them they'll be able to look at a separate chunk of your app. Inputs in any tab are available to the app's server and can be outputs in any tab (remember that their is a shared server so it is impossible for it to be otherwise!). Generally it may be a good idea to have inputs and outputs in the same tab so that users can see the interactive app responding to their inputs rather than needing to click back and forth among tabs to see the results of their inputs. For example, you could have an app where users choose what goes on either axis of several graph types and put each graph type on its own tab of the larger Shiny app.
```{r layout-tabs}
# Define the UI
tabs_ui <- shiny::fluidPage(
# Define the layout type
shiny::tabsetPanel( # <1>
# Define what goes in the first tab
shiny::tabPanel(title = "Tab 1",
"Hello from the first tab!"
), # <2>
# And in the second
shiny::tabPanel(title = "Tab 2",
"Welcome to the second tab!"
),
# And so on
shiny::tabPanel(title = "Tab 3",
"Hello yet again!"
) ) ) # <3>
# Define the server
basic_server <- function(input, output){ }
# Generate the app
shiny::shinyApp(ui = tabs_ui, server = basic_server)
```
1. This function is comparable to `sidebarLayout` in that if you want stuff above/below the tab panel area you'll need to be outside of this function's parentheses but still in the `fluidPage` parentheses
2. Again, just like the `sidebarLayout` subfunctions, you'll need a comma after each UI element except the last one
3. Here we're closing all of the nested UI functions
### Other Layouts
We just briefly covered two layout options but hopefully this is a nice indication for the kind of flexibility in user interface that you can expect of Shiny apps! For more information, check out Posit's [Shiny Application Layout Guide](https://shiny.posit.co/r/articles/build/layout-guide/). That resource has some really nice examples of these and other layout options that will be well worth checking out as you begin your journey into Shiny.
## Text Formatting
Beyond making your app have an intuitive layout it can be really helpful to be able to do even simple text formatting to assist your app's users. For instance, you may want to use sub-headings within the same UI layout component but still want to draw a distinction between two sets of inputs. Additionally you may want to emphasize some tips for best results or hyperlink to your group's other products. All of these can be accomplished using text formatting tools that are readily available within Shiny.
```{r text-shiny}
# Load the `htmltools` library
library(htmltools)
# Define the UI
text_ui <- shiny::fluidPage(
# Let's make some headings
htmltools::h1("This is a Big Heading"), # <1>
htmltools::h3("Smaller heading"),
htmltools::h5("Even smaller heading!"),
# Now we'll format more text in various (non-heading) ways
htmltools::strong("Bold text"),
htmltools::br(), # <2>
htmltools::a(href = "https://lter.github.io/ssecr/mod_interactivity.html",
"This text is hyperlinked",
target = "_blank"), # <3>
htmltools::br(),
htmltools::code("This is 'code' text") ) # <4>
# Define the server
basic_server <- function(input, output){ }
# Generate the app
shiny::shinyApp(ui = text_ui, server = basic_server)
```
1. Headings (of any size) automatically include a line break after the heading text
2. The `br` function creates a line break
3. When the `target` argument is set to "_blank" it will open a new tab when users click the hyperlinked text. This is ideal because if a user left your app to visit the new site they would lose all of their inputs
4. Code text looks `like this`
## Deployment
When Shiny apps are only being used by those in your team, keeping them as a code script works well. However, if you'd like those outside of your team to be able to find your app as they would any other website you'll need to <u>deploy</u> your Shiny app. This process is outside of the scope of this module but is often the end goal of Shiny app development.
Take a look at [Posit's instructions for deployment](https://shiny.posit.co/r/articles/share/deployment-web/) for more details but essentially "deployment" is the process of getting your local app hosted on shinyapps.io which gives it a link that anyone can use to access/run your app on their web browser of choice.
## Additional Interactivity Resources
### Papers & Documents
-
### Workshops & Courses
- Posit. [Welcome to Shiny](https://shiny.posit.co/r/getstarted/shiny-basics/lesson1/index.html). **2024**.
- Lyon, N.J., _et al._ [Shiny Apps for Sharing Science](https://njlyon0.github.io/asm-2022_shiny-workshop/). **2022**.
### Websites
-