forked from hadley/mastering-shiny
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaction-dynamic.Rmd
247 lines (188 loc) · 9.71 KB
/
action-dynamic.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
# Dynamic UI {#action-dynamic}
```{r, include = FALSE}
source("common.R")
```
As well as updating the content of the outputs, sometimes you will actually want to add more, or modify the inputs, or generally large parts of your app with code. Generally, you want to avoid these techniques except when they are necessary as they add substantially more moving parts to the process. But when you need them, you need them.
In Chapter XYZ, we'll come back to more advanced tecnhiques that require you know a little more about HTML and javascript.
```{r setup}
library(shiny)
```
## Modify existing inputs
Every input control, e.g. `textInput()`, is paired with a update function, e.g. `updateTextInput()`, that allows you to modify the control after it has been created in the UI. Take this very simple example. The app has three numeric inputs, where the first two control the range (the `min` and `max`) of the last.
```{r}
ui <- fluidPage(
numericInput("min", "min", 0),
numericInput("max", "max", 10),
numericInput("n", "n", 5)
)
server <- function(input, output, session) {
observeEvent(input$min, {
updateNumericInput(session, "n", min = input$min)
})
observeEvent(input$max, {
updateNumericInput(session, "n", max = input$max)
})
}
```
Note that I've used `observeEvent()` here, although `observe()` would also have worked and would yield shorter code. I think it's best to be very clear exactly what change you are listening for, and what action you want to take in response.
For historical reasons, calls to `updateXXXXInput()` look a little different to other Shiny functions, as you need to pass `session` as the first argument. (If you're using an older `server` function template, you might not have `session` in the arguments, so make sure you add it if its missing.)
All updates are performed "simultaneously" (for the purposes of reactivity) once all outputs are done.
### Simple uses
The simplest uses of the update functions are to make provenience small conveniences for the user. For example, maybe you want to make it easy to reset some parameters back to their starting place:
```{r}
ui <- fluidPage(
sliderInput("x1", "x1", 0, min = -10, max = 10),
sliderInput("x2", "x2", 0, min = -10, max = 10),
sliderInput("x3", "x3", 0, min = -10, max = 10),
actionButton("reset", "Reset")
)
server <- function(input, output, session) {
observeEvent(input$reset, {
updateNumericInput(session, "x1", value = 0)
updateNumericInput(session, "x2", value = 0)
updateNumericInput(session, "x3", value = 0)
})
}
```
Or maybe you want a action button to be explicit about what it's going to do:
```{r}
ui <- fluidPage(
numericInput("n", "Simulations", 10),
actionButton("simulate", "Simulate")
)
server <- function(input, output, session) {
observeEvent(input$n, {
label <- paste0("Simulate ", input$n, " times")
updateActionButton(session, "simulate", label = label)
})
}
```
### Hierarchical select boxes
A more complicated, but particularly useful, application of the update functions is to allow interactive drill down across multiple categories. I'm going to start some imaginary data for a sales dashboard, coming from <https://www.kaggle.com/kyanyoga/sample-sales-data>.
For our purposes, I'm going to focus on a natural hierarchy in the data:
* Each territory contains customers
* Each customer has multiple orders
* Each order contains rows
I want to create a user interface where you can:
* Select a territory to see all customers.
* Select customer to see all orders.
* Select order to see data.
The basic idea is to start with UI that contains three select boxes, and one output table. The choices for the `customername` and `ordernumber` will be dynamically supplied so, I explicitly set `choices = NULL`.
```{r}
sales <- vroom::vroom("sales-dashboard/sales_data_sample.csv")
ui <- fluidPage(
selectInput("territory", "Territory", choices = unique(sales$TERRITORY)),
selectInput("customername", "Customer", choices = NULL),
selectInput("ordernumber", "Order number", choices = NULL),
tableOutput("data")
)
```
Then in the server function, I work top-down, first creating a filtered subset that only contains the selected territory and using that to update `input$customername`, then creating a subset contains the given customer and using that to update `input$ordernumber`.
```{r}
server <- function(input, output, session) {
territory <- reactive({
filter(sales, TERRITORY == input$territory)
})
observeEvent(territory, {
choices <- unique(territory()$CUSTOMERNAME)
updateSelectInput(session, "customername", choices = choices)
})
customer <- reactive({
filter(territory(), CUSTOMERNAME == input$customername)
})
observeEvent(input$customername, {
choices <- unique(customer()$ORDERNUMBER)
updateSelectInput(session, "ordernumber", choices = choices)
})
output$data <- renderTable({
filter(customer(), ORDERNUMBER == input$ordernumber)
})
}
```
You can see a more fleshed out application of this principle in <https://github.com/hadley/mastering-shiny/tree/master/sales-dashboard>.
### Circular references
Ther'es an important drawback to using the update functions that you need to be aware of. From Shiny's perspectve, when modifying `value`, the update functions act exactly as if the user has altered the value, so that the update functions can trigger reactive updates in exactly that same way a human can. This means that you're now stepping outside of the bounds of pure reactive programming, and you need to start worrying about circular references and creating infinite loops.
For example, take the following simple app. It contains a single input control, and a observer that increments its value by one. Every time `updateNumericInput()` runs, it invalidates `input$n`, causing `updateNumericInput()` to run again, so the app is stuck in an infinite loop constantly increasing the value of `input$n`.
```{r}
ui <- fluidPage(
numericInput("n", "n", 0)
)
server <- function(input, output, session) {
observeEvent(input$n,
updateNumericInput(session, "n", value = input$n + 1)
)
}
```
You're unlikely to create such an obvious problem in your own app, but beware this potential problem if you are updating multiple controls that depend on each other. You will need to carefully reason through the updates to ensure that you're not creating an infinite loop of updates. This is a very good reason that you should only update functions for the most important cases.
This is generally only a concern when you are changing the `value`, be aware that changing some other settings can implicit change the value, e.g. changing set of `choices` for `inputSelect()`.
### Inter-related inputs
One place where it's easy to end up with circular references if you're attempting to connect together multiple inputs. For example, imagine you want to create a temperatue conversion app where you can either enter the temperature in Celsius or in Fahrenheit:
```{r}
ui <- fluidPage(
numericInput("temp_c", "Celsius", NA),
numericInput("temp_f", "Fahrenheit", NA)
)
c2f <- function(x) {
round((x * 9 / 5) + 32)
}
f2c <- function(x) {
round((x - 32) * 5 / 9)
}
```
The naive approach
```{r}
server <- function(input, output, session) {
observeEvent(input$temp_f, {
updateNumericInput(session, "temp_c", value = f2c(input$temp_f))
})
observeEvent(input$temp_c, {
updateNumericInput(session, "temp_f", value = c2f(input$temp_c))
})
}
```
This _mostly_ works, but if you play around with it carefully you'll notice that sometimes will trigger multiple changes. For example:
* Set 120 F
* Then click down. F changes to 119, and C is updated to 48.
* But 48 C is converted to 118 F, so F changes again to 118.
* Fortunately 118 F is still 48 C, so the updates stop there.
<!-- https://community.rstudio.com/t/mutually-dependent-numericinput-in-shiny/29307 -->
It's not currently possible to solve problem elegantly with Shiny because you need some way to make udpates condition on what control is currently selected. If there was some way to listen to "focus" events
```{r, eval = FALSE}
ui <- fluidPage(
numericInput("temp_c", "Celsius", NA),
numericInput("temp_f", "Fahrenheit", NA),
focusInput("focus")
)
server <- function(input, output, session) {
observeEvent(input$temp_f, {
if (input$focus != "temp_f")
return()
updateNumericInput(session, "temp_f", (input$temp_c * 9 / 5) + 32)
})
observeEvent(input$temp_c, {
if (input$focus != "temp_c")
return()
updateNumericInput(session, "temp_f", (input$temp_c * 9 / 5) + 32)
})
}
```
## `uiOutput()` and `renderUI()`
There's a special UI component that allows you to generate components of the UI on the server.
`tagList()` if you need mutliple controls. Want to keep as much as of the fixed structure in UI as possible. Better peformance. Simpler reasoning.
Output control. Replaces previously generated HTML.
```{r}
ui <- fluidPage(
textInput("label", "label"),
numericInput("value", "value", value = 0),
uiOutput("numeric"),
textOutput("selected")
)
server <- function(input, output, session) {
output$numeric <- renderUI({
numericInput("dynamic", input$label, input$value)
})
output$selected <- renderText(input$dynamic)
}
```
Notice that the value you have selected is wiped out when you change the label. This is one of the reasons why, where possible, you should use an update function instead of `renderUI()`. Also note that it takes a fraction of a second to appear after the app loads - that's because it has to be rendered by the server function.
Note that you are now creating IDs in two places so that when you add to `ui`, you have to be careful not to call the control `dynamic`. Later, in Chapter \@ref(action-modules) we'll see how modules can help avoid this problem by namespacing the control.