-
Notifications
You must be signed in to change notification settings - Fork 566
/
action-dynamic.Rmd
1018 lines (826 loc) · 40.6 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
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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Dynamic UI {#action-dynamic}
```{r, include = FALSE}
source("common.R")
source("demo.R")
```
So far, we've seen a clean separation between the user interface and the server function: the user interface is defined statically when the app is launched so it can't respond to anything that happens in the app.
In this chapter, you'll learn how to create **dynamic** user interfaces, changing the UI using code run in the server function.
There are three key techniques for creating dynamic user interfaces:
- Using the `update` family of functions to modify parameters of input controls.
- Using `tabsetPanel()` to conditionally show and hide parts of the user interface.
- Using `uiOutput()` and `renderUI()` to generate selected parts of the user interface with code.
These three tools give you considerable power to respond to the user by modifying inputs and outputs.
I'll demonstrate some of the more useful ways in which you can apply them, but ultimately you're only constrained by your creativity.
At the same time, these tools can make your app substantially more difficult to reason about, so deploy them sparingly, and always strive to use the simplest technique that solves your problem.
```{r setup}
library(shiny)
library(dplyr, warn.conflicts = FALSE)
```
## Updating inputs
We'll begin with a simple technique that allows you to modify an input after it has been created: the update family of functions.
Every input control, e.g. `textInput()`, is paired with an **update function**, e.g. `updateTextInput()`, that allows you to modify the control after it has been created.
Take the example in the code below, with the results shown in Figure \@ref(fig:update-basics).
The app has two inputs that control the range (the `min` and `max`) of another input, a slider.
The key idea is to use `observeEvent()`[^action-dynamic-1] to trigger `updateSliderInput()` whenever the `min` or `max` inputs change.
[^action-dynamic-1]: I introduced `observeEvent()` in Section \@ref(observers) and will discuss in more detail in Section \@ref(observers-details).
```{r}
ui <- fluidPage(
numericInput("min", "Minimum", 0),
numericInput("max", "Maximum", 3),
sliderInput("n", "n", min = 0, max = 3, value = 1)
)
server <- function(input, output, session) {
observeEvent(input$min, {
updateSliderInput(inputId = "n", min = input$min)
})
observeEvent(input$max, {
updateSliderInput(inputId = "n", max = input$max)
})
}
```
```{r update-basics, fig.cap = demo$caption("The app on load (left), after increasing max (middle), and then decreasing min (right)."), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/update-basics", ui, server)
demo$resize(350)
s1 <- demo$takeScreenshot("onload")
s2 <- demo$setInputs(max = 4)$takeScreenshot("max-increase")
s3 <- demo$setInputs(min = -1)$takeScreenshot("min-decrease")
knitr::include_graphics(c(s1, s2, s3))
demo$deploy()
```
The update functions look a little different to other Shiny functions: they all take name of the input (as a string) as the the `inputId` argument[^action-dynamic-2].
The remaining arguments correspond to the arguments to the input constructor that can be modified after creation.
[^action-dynamic-2]: The first argument, `session`, exists for backward compatibility but is very rarely needed.
To help you get the hang of the update functions, I'll show a couple more simple examples, then we'll dive into a more complicated case study using hierarchical select boxes, and finish off by discussing the problem of circular references.
### Simple uses
The simplest uses of the update functions are to provide small conveniences for the user.
For example, maybe you want to make it easy to reset parameters back to their initial value.
The following snippet shows how you might combine an `actionButton()`, `observeEvent()` and `updateSliderInput()`, with the output shown in Figure \@ref(fig:update-reset).
```{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, {
updateSliderInput(inputId = "x1", value = 0)
updateSliderInput(inputId = "x2", value = 0)
updateSliderInput(inputId = "x3", value = 0)
})
}
```
```{r update-reset, fig.cap = demo$caption("The app on load (left), after dragging some sliders (middle), then clicking reset (right)."), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/update-reset", ui, server)
demo$resize(350)
s1 <- demo$takeScreenshot("onload")
s2 <- demo$setInputs(x1 = 5, x2 = -5)$takeScreenshot("set")
s3 <- demo$click("reset")$takeScreenshot("reset")
knitr::include_graphics(c(s1, s2, s3))
demo$deploy()
```
A similar application is to tweak the text of an action button so you know exactly what it's going to do.
Figure \@ref(fig:update-button) shows the results of the code below.
```{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(inputId = "simulate", label = label)
})
}
```
```{r update-button, fig.cap = demo$caption("The app on load (left), after setting simulations to 1 (middle), then setting simulations to 100 (right)."), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/update-button", ui, server)
demo$resize(350)
s1 <- demo$takeScreenshot("onload")
s2 <- demo$setInputs(n = 1)$takeScreenshot("set1")
s3 <- demo$setInputs(n = 100)$takeScreenshot("set100")
demo$deploy()
knitr::include_graphics(c(s1, s2, s3))
```
There are many ways to use update functions in this way; be on the look out for ways to give more information to the user when you are working on sophisticated applications.
A particularly important application is making it easier to select from a long list of possible options, through step-by-step filtering.
That's often a problem for "hierarchical select boxes".
### Hierarchical select boxes {#hierarchical-select}
A more complicated, but particularly useful, application of the update functions is to allow interactive drill down across multiple categories.
I'll illustrate their usage with some imaginary data for a sales dashboard that comes from <https://www.kaggle.com/kyanyoga/sample-sales-data>.
```{r}
sales <- vroom::vroom("sales-dashboard/sales_data_sample.csv", col_types = list(), na = "")
sales %>%
select(TERRITORY, CUSTOMERNAME, ORDERNUMBER, everything()) %>%
arrange(ORDERNUMBER)
```
For this demo, 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 a customer to see all orders.
- Select an order to see the underlying rows.
The essence of the UI is simple: I'll create three select boxes and one output table.
The choices for the `customername` and `ordernumber` select boxes will be dynamically generated, so I set `choices = NULL`.
```{r}
ui <- fluidPage(
selectInput("territory", "Territory", choices = unique(sales$TERRITORY)),
selectInput("customername", "Customer", choices = NULL),
selectInput("ordernumber", "Order number", choices = NULL),
tableOutput("data")
)
```
In the server function, I work top-down:
1. I create a reactive, `territory()`, that contains the rows from `sales` that match the selected territory.
2. Whenever `territory()` changes, I update the list of `choices` in the `input$customername` select box.
3. I create another reactive, `customer()`, that contains the rows from `territory()` that match the selected customer.
4. Whenever `customer()` changes, I update the list of `choices` in the `input$ordernumber` select box.
5. I display the selected orders in `output$data`.
You can see that organisation below:
```{r}
server <- function(input, output, session) {
territory <- reactive({
filter(sales, TERRITORY == input$territory)
})
observeEvent(territory(), {
choices <- unique(territory()$CUSTOMERNAME)
updateSelectInput(inputId = "customername", choices = choices)
})
customer <- reactive({
req(input$customername)
filter(territory(), CUSTOMERNAME == input$customername)
})
observeEvent(customer(), {
choices <- unique(customer()$ORDERNUMBER)
updateSelectInput(inputId = "ordernumber", choices = choices)
})
output$data <- renderTable({
req(input$ordernumber)
customer() %>%
filter(ORDERNUMBER == input$ordernumber) %>%
select(QUANTITYORDERED, PRICEEACH, PRODUCTCODE)
})
}
```
```{r update-nested, fig.cap = demo$caption('I select "EMEA" (left), then "Lyon Souveniers" (middle), then (right) look at the orders.'), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/update-nested", ui, server)
demo$resize(400, 500)
demo$dropDown("territory", 2)
s1 <- demo$takeScreenshot("territory")
demo$setInputs(territory = "EMEA")
demo$dropDown("customername", 2)
s2 <- demo$takeScreenshot("customername")
demo$setInputs(customername = "Lyon Souveniers")
s3 <- demo$takeScreenshot("orders")
knitr::include_graphics(c(s1, s2, s3))
demo$deploy()
```
Try out this simple example at `r demo$link()`, or see a more fully fleshed out application at <https://github.com/hadley/mastering-shiny/tree/main/sales-dashboard>.
### Freezing reactive inputs
Sometimes this sort of hierarchical selection can briefly create an invalid set of inputs, leading to a flicker of undesirable output.
For example, consider this simple app where you select a dataset and then select a variable to summarise:
```{r}
ui <- fluidPage(
selectInput("dataset", "Choose a dataset", c("pressure", "cars")),
selectInput("column", "Choose column", character(0)),
verbatimTextOutput("summary")
)
server <- function(input, output, session) {
dataset <- reactive(get(input$dataset, "package:datasets"))
observeEvent(input$dataset, {
updateSelectInput(inputId = "column", choices = names(dataset()))
})
output$summary <- renderPrint({
summary(dataset()[[input$column]])
})
}
```
```{r echo = FALSE, message = FALSE}
demo <- demoApp$new("action-dynamic/freeze", ui, server)
demo$deploy()
```
If you try out the live app at `r demo$link()`, you'll notice that when you switch datasets the summary output will briefly flicker.
That's because `updateSelectInput()` only has an effect after all outputs and observers have run, so there's temporarily a state where you have dataset B and a variable from dataset A, so that the output contains `summary(NULL)`.
You can resolve this problem by "freezing" the input with `freezeReactiveValue()`.
This ensures that any reactives or outputs that use the input won't be updated until the next full round of invalidation[^action-dynamic-3].
[^action-dynamic-3]: To be more precise, any attempt to read a frozen input will result in `req(FALSE).`
```{r}
server <- function(input, output, session) {
dataset <- reactive(get(input$dataset, "package:datasets"))
observeEvent(input$dataset, {
freezeReactiveValue(input, "column")
updateSelectInput(inputId = "column", choices = names(dataset()))
})
output$summary <- renderPrint({
summary(dataset()[[input$column]])
})
}
```
Note that there's no need to "thaw" the input value; this happens automatically after Shiny detects that the session and server are once again in sync.
You might wonder when you should use `freezeReactiveValue()`: it's actually good practice to **always** use it when you dynamically change an input `value`.
The actual modification takes some time to flow to the browser then back to Shiny, and in the interim any reads of the value are at best wasted, and at worst lead to errors.
Use `freezeReactiveValue()` to tell all downstream calculations that an input value is stale and they should save their effort until it's useful.
### Circular references
There's an important issue we need to discuss if you want to use the update functions to change the current `value`[^action-dynamic-4] of an input.
From Shiny's perspective, using an update function to modify `value` is no different to the user modifying the value by clicking or typing.
That means an update function can trigger reactive updates in exactly the same way that a human can.
This means that you are now stepping outside of the bounds of pure reactive programming, and you need to start worrying about circular references and infinite loops.
[^action-dynamic-4]: This is generally only a concern when you are changing the `value`, but be some other parameters can change the value indirectly.
For example, if you modify the `choices` for `selectInput()`, or `min` and `max` for `sliderInput()`, the current `value` will be modified if it's no longer in the allowed set of values.
For example, take the following simple app.
It contains a single input control and an observer that increments its value by one.
Every time `updateNumericInput()` runs, it changes `input$n`, causing `updateNumericInput()` to run again, so the app gets 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(inputId = "n", value = input$n + 1)
)
}
```
You're unlikely to create such an obvious problem in your own app, but it can crop up if you update multiple controls that depend on one another, as in the next example.
### Inter-related inputs
One place where it's easy to end up with circular references is when you have multiple "sources of truth" in an app.
For example, imagine that you want to create a temperature conversion app where you can either enter the temperature in Celsius or in Fahrenheit:
```{r}
ui <- fluidPage(
numericInput("temp_c", "Celsius", NA, step = 1),
numericInput("temp_f", "Fahrenheit", NA, step = 1)
)
server <- function(input, output, session) {
observeEvent(input$temp_f, {
c <- round((input$temp_f - 32) * 5 / 9)
updateNumericInput(inputId = "temp_c", value = c)
})
observeEvent(input$temp_c, {
f <- round((input$temp_c * 9 / 5) + 32)
updateNumericInput(inputId = "temp_f", value = f)
})
}
```
```{r, echo = FALSE, message = FALSE}
demo <- demoApp$new("action-dynamic/temperature", ui, server)
demo$deploy()
```
If you play around with this app, `r demo$link()`, you'll notice that it *mostly* works, but you might notice that it'll sometimes trigger multiple changes.
For example:
- Set 120 F, then click the down arrow.
- F changes to 119, and C is updated to 48.
- 48 C converts to 118 F, so F changes again to 118.
- Fortunately 118 F is still 48 C, so the updates stop there.
There's no way around this problem because you have one idea (the temperature) with two expressions in the app (Celsius and Fahrenheit).
Here we are lucky that cycle quickly converges to a value that satisfies both constraints.
In general, you are better off avoiding these situations, unless you are willing to very carefully analyse the convergence properties of the underlying dynamic system that you've created.
### Exercises
1. Complete the user interface below with a server function that updates `input$date` so that you can only select dates in `input$year`.
```{r}
ui <- fluidPage(
numericInput("year", "year", value = 2020),
dateInput("date", "date")
)
```
2. Complete the user interface below with a server function that updates `input$county` choices based on `input$state`.
For an added challenge, also change the label from "County" to "Parish" for Louisiana and "Borough" for Alaska.
```{r, messages = FALSE}
library(openintro, warn.conflicts = FALSE)
states <- unique(county$state)
ui <- fluidPage(
selectInput("state", "State", choices = states),
selectInput("county", "County", choices = NULL)
)
```
3. Complete the user interface below with a server function that updates `input$country` choices based on the `input$continent`.
Use `output$data` to display all matching rows.
```{r}
library(gapminder)
continents <- unique(gapminder$continent)
ui <- fluidPage(
selectInput("continent", "Continent", choices = continents),
selectInput("country", "Country", choices = NULL),
tableOutput("data")
)
```
4. Extend the previous app so that you can also choose to select all continents, and hence see all countries.
You'll need to add `"(All)"` to the list of choices, and then handle that specially when filtering.
5. What is at the heart of the problem described at <https://community.rstudio.com/t/29307>?
## Dynamic visibility
The next step up in complexity is to selectively show and hide parts of the UI.
There are more sophisticated approaches if you know a little JavaScript and CSS, but there's a useful technique that doesn't require any extra knowledge: concealing optional UI with a tabset (as introduced in Section \@ref(tabsets)).
This is a clever hack that allows you to show and hide UI as needed, without having to re-generate it from scratch (as you'll learn in the next section).
```{r}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput("controller", "Show", choices = paste0("panel", 1:3))
),
mainPanel(
tabsetPanel(
id = "switcher",
type = "hidden",
tabPanelBody("panel1", "Panel 1 content"),
tabPanelBody("panel2", "Panel 2 content"),
tabPanelBody("panel3", "Panel 3 content")
)
)
)
)
server <- function(input, output, session) {
observeEvent(input$controller, {
updateTabsetPanel(inputId = "switcher", selected = input$controller)
})
}
```
```{r dynamic-panels, fig.cap = demo$caption("Selecting panel1 (left), then panel2 (middle), then panel3 (right)."), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/dynamic-panels", ui, server)
demo$resize(300, 220)
s1 <- demo$takeScreenshot()
demo$setInputs(controller = "panel2")
s2 <- demo$takeScreenshot("panel2")
demo$setInputs(controller = "panel3")
s3 <- demo$takeScreenshot("panel3")
knitr::include_graphics(c(s1, s2, s3))
demo$deploy()
```
There are two main ideas here:
- Use tabset panel with hidden tabs.
- Use `updateTabsetPanel()` to switch tabs from the server.
This is a simple idea, but when combined with a little creativity, it gives you a considerable amount of power.
The following two sections illustrate a couple of small examples of how you might use it in practice.
### Conditional UI
Imagine that you want an app that allows the user to simulate from the normal, uniform, and exponential distributions.
Each distribution has different parameters, so we'll need some way to show different controls for different distributions.
Here, I'll put the unique user interface for each distribution in its own `tabPanel()`, and then arrange the three tabs into a `tabsetPanel()`.
```{r}
parameter_tabs <- tabsetPanel(
id = "params",
type = "hidden",
tabPanel("normal",
numericInput("mean", "mean", value = 1),
numericInput("sd", "standard deviation", min = 0, value = 1)
),
tabPanel("uniform",
numericInput("min", "min", value = 0),
numericInput("max", "max", value = 1)
),
tabPanel("exponential",
numericInput("rate", "rate", value = 1, min = 0),
)
)
```
I'll then embed that inside a fuller UI which allows the user to pick the number of samples and shows a histogram of the results:
```{r}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput("dist", "Distribution",
choices = c("normal", "uniform", "exponential")
),
numericInput("n", "Number of samples", value = 100),
parameter_tabs,
),
mainPanel(
plotOutput("hist")
)
)
)
```
Note that I've carefully matched the `choices` in `input$dist` to the names of the tab panels.
That makes it easy to write the `observeEvent()` code below that automatically switches controls when the distribution changes.
The rest of the app uses techniques that you're already familiar with.
See the final result in Figure \@ref(fig:dynamic-conditional).
```{r}
server <- function(input, output, session) {
observeEvent(input$dist, {
updateTabsetPanel(inputId = "params", selected = input$dist)
})
sample <- reactive({
switch(input$dist,
normal = rnorm(input$n, input$mean, input$sd),
uniform = runif(input$n, input$min, input$max),
exponential = rexp(input$n, input$rate)
)
})
output$hist <- renderPlot(hist(sample()), res = 96)
}
```
```{r dynamic-conditional, fig.cap = demo$caption("Results for normal (left), uniform (middle), and exponential (right) distributions."), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/dynamic-conditional", ui, server)
demo$resize(400)
s1 <- demo$takeScreenshot("normal")
demo$setInputs(dist = "uniform")
s2 <- demo$takeScreenshot("uniform")
demo$setInputs(dist = "exponential")
s3 <- demo$takeScreenshot("exponential")
knitr::include_graphics(c(s1, s2, s3))
demo$deploy()
```
Note that the value of (e.g.) `input$mean` is independent of whether or not its visible to the user.
The underlying HTML control still exists; you just can't see it.
### Wizard interface {#dynamic-wizard}
You can also use this idea to create a "wizard", a type of interface that makes it easier to collect a bunch of information by spreading it across multiple pages.
Here we embed action buttons within each "page", making it easy to go forward and back.
The results are shown in Figure \@ref(fig:wizard).
```{r}
ui <- fluidPage(
tabsetPanel(
id = "wizard",
type = "hidden",
tabPanel("page_1",
"Welcome!",
actionButton("page_12", "next")
),
tabPanel("page_2",
"Only one page to go",
actionButton("page_21", "prev"),
actionButton("page_23", "next")
),
tabPanel("page_3",
"You're done!",
actionButton("page_32", "prev")
)
)
)
server <- function(input, output, session) {
switch_page <- function(i) {
updateTabsetPanel(inputId = "wizard", selected = paste0("page_", i))
}
observeEvent(input$page_12, switch_page(2))
observeEvent(input$page_21, switch_page(1))
observeEvent(input$page_23, switch_page(3))
observeEvent(input$page_32, switch_page(2))
}
```
```{r wizard, fig.cap = demo$caption("A wizard interface portions complex UI over multiple pages. Here we demonstrate the idea with a very simple example, clicking next to advance to the next page."), echo = FALSE, message = FALSE, out.width = "33%"}
demo <- demoApp$new("action-dynamic/wizard", ui, server)
demo$resize(200)
demo$takeScreenshot("1")
demo$click("page_12")
demo$wait()
demo$takeScreenshot("2")
demo$click("page_23")
demo$takeScreenshot("3")
demo$deploy()
```
Note the use of the `switch_page()` function to reduce the amount of duplication in the server code.
We'll come back to this idea in Chapter \@ref(scaling-functions), and then create a module to automate wizard interfaces in Section \@ref(module-wizard).
### Exercises
1. Use a hidden tabset to show additional controls only if the user checks an "advanced" check box.
2. Create an app that plots `ggplot(diamonds, aes(carat))` but allows the user to choose which geom to use: `geom_histogram()`, `geom_freqpoly()`, or `geom_density()`. Use a hidden tabset to allow the user to select different arguments depending on the geom: `geom_histogram()` and `geom_freqpoly()` have a binwidth argument; `geom_density()` has a `bw` argument.
3. Modify the app you created in the previous exercise to allow the user to choose whether each geom is shown or not (i.e. instead of always using one geom, they can picked 0, 1, 2, or 3). Make sure that you can control the binwidth of the histogram and frequency polygon independently.
## Creating UI with code {#programming-ui}
Sometimes none of the techniques described above gives you the level of dynamism that you need: the update functions only allow you to change existing inputs, and a tabset only works if you have a fixed and known set of possible combinations.
Sometimes you need to create different types or numbers of inputs (or outputs), depending on other inputs.
This final technique gives you the ability to do so.
It's worth noting that you've always created your user interface with code, but so far you've always done it before the app starts.
This technique gives you the ability to create and modify the user interface while the app is running.
There are two parts to this solution:
- `uiOutput()` inserts a placeholder in your `ui`.
This leaves a "hole" that your server code can later fill in.
- `renderUI()` is called within `server()` to fill in the placeholder with dynamically generated UI.
We'll see how this works with a simple example, and then dive into some realistic uses.
### Getting started {#dynamic-basics}
Let's begin with a simple app that dynamically creates an input control, with the type and label control by two other inputs.
The resulting app is shown in Figure \@ref(fig:render-simple).
```{r}
ui <- fluidPage(
textInput("label", "label"),
selectInput("type", "type", c("slider", "numeric")),
uiOutput("numeric")
)
server <- function(input, output, session) {
output$numeric <- renderUI({
if (input$type == "slider") {
sliderInput("dynamic", input$label, value = 0, min = 0, max = 10)
} else {
numericInput("dynamic", input$label, value = 0, min = 0, max = 10)
}
})
}
```
```{r render-simple, fig.cap = demo$caption("App on load (left), then changing type to numeric (middle), then label to 'my label'."), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/render-simple", ui, server)
demo$resize(400)
demo$takeScreenshot("onload")
demo$setInputs(type = "numeric")
demo$takeScreenshot("numeric")
demo$setInputs(label = "My label")
demo$takeScreenshot("label")
demo$deploy()
```
If you run this code yourself, you'll notice that it takes a fraction of a second to appear after the app loads.
That's because it's reactive: the app must load, trigger a reactive event, which calls the server function, yielding HTML to insert into the page.
This is one of the downsides of `renderUI()`; relying on it too much can create a laggy UI.
For good performance, strive to keep fixed as much of the user interface as possible, using the techniques described earlier in the chapter.
There's one other problem with this approach: when you change controls, you lose the currently selected value.
Maintaining existing state is one of the big challenges of creating UI with code.
This is one reason that selectively showing and hiding UI is a better approach if it works for you --- because you're not destroying and recreating the controls, you don't need to do anything to preserve the values.
However, in many cases, we can fix the problem by setting the `value` of the new input to the current value of the existing control:
```{r}
server <- function(input, output, session) {
output$numeric <- renderUI({
value <- isolate(input$dynamic)
if (input$type == "slider") {
sliderInput("dynamic", input$label, value = value, min = 0, max = 10)
} else {
numericInput("dynamic", input$label, value = value, min = 0, max = 10)
}
})
}
```
The use of `isolate()` is important.
We'll come back to what it does in Section \@ref(isolate), but here it ensures that we don't create a reactive dependency that would cause this code to re-run every time `input$dynamic` changes (which will happen whenever the user modifies the value).
We only want it to change when `input$type` or `input$label` changes.
### Multiple controls {#multiple-controls}
Dynamic UI is most useful when you are generating an arbitrary number or type of controls.
That means that you'll be generating UI with code, and I recommend using functional programming for this sort of task.
Here I'll use `purrr::map()` and `purrr::reduce()`, but you could certainly do the same with the base `lapply()` and `Reduce()` functions.
```{r}
library(purrr)
```
If you're not familiar with the `map()` and `reduce()` of functional programming, you might want to take a brief detour to read [*Functional programming*](https://adv-r.hadley.nz/functionals.html) before continuing.
We'll also come back to this idea in Chapter \@ref(scaling-functions).
These are complex ideas, so don't stress out if it doesn't make sense on your first read through.
To make this concrete, imagine that you'd like the user to be able to supply their own colour palette.
They'll first specify how many colours they want, and then supply a value for each colour.
The `ui` is pretty simple: we have a `numericInput()` that controls the number of inputs, a `uiOutput()` where the generated text boxes will go, and a `textOutput()` that demonstrates that we've plumbed everything together correctly.
```{r}
ui <- fluidPage(
numericInput("n", "Number of colours", value = 5, min = 1),
uiOutput("col"),
textOutput("palette")
)
```
The server function is short but contains some big ideas:
```{r}
server <- function(input, output, session) {
col_names <- reactive(paste0("col", seq_len(input$n)))
output$col <- renderUI({
map(col_names(), ~ textInput(.x, NULL))
})
output$palette <- renderText({
map_chr(col_names(), ~ input[[.x]] %||% "")
})
}
```
- I use a reactive, `col_names()`, to store the names of each of the colour inputs I'm about to generate.
- I then use `map()` to create a list of `textInput()`s, one each for each name in `col_names()`.
`renderUI()` then takes this list of HTML components and adds it to UI.
- I need to use a new trick to access the values the input values.
So far we've always accessed the components of `input` with `$`, e.g. `input$col1`.
But here we have the input names in a character vector, like `var <- "col1"`.
`$` no longer works in this scenario, so we need to swich to `[[`, i.e. `input[[var]]`.
- I use `map_chr()` to collect all values into a character vector, and display that in `output$palette`.
Unfortunately there's a brief period, just before the new inputs are rendered by the browser, where their values are `NULL`.
This causes `map_chr()` to error, which we fix by using the handy `%||%` function: it returns the right-hand side whenever the left-hand side is `NULL`.
You can see the results in Figure \@ref(fig:render-palette).
```{r render-palette, fig.cap = demo$caption("App on load (left), after setting n to 3 (middle), then entering some colours (right)."), out.width = "33%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/render-palette", ui, server)
demo$resize(400)
s1 <- demo$takeScreenshot("onload")
demo$setInputs(n = 3)
s2 <- demo$takeScreenshot("change-n")
demo$setInputs(col1 = "red", col2 = "yellow", col3 = "orange")
s3 <- demo$takeScreenshot("set-cols")
knitr::include_graphics(c(s1, s2, s3))
demo$deploy()
```
If you run this app, you'll discover a really annoying behaviour: whenever you change the number of colours, all the data you've entered disappears.
We can fix this problem by using the same technique as before: setting `value` to the (isolated) current value.
I'll also tweak the appearance to look a little nicer, including displaying the selected colours in a plot.
Sample screenshots are shown in Figure \@ref(fig:render-palette-full).
```{r}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
numericInput("n", "Number of colours", value = 5, min = 1),
uiOutput("col"),
),
mainPanel(
plotOutput("plot")
)
)
)
server <- function(input, output, session) {
col_names <- reactive(paste0("col", seq_len(input$n)))
output$col <- renderUI({
map(col_names(), ~ textInput(.x, NULL, value = isolate(input[[.x]])))
})
output$plot <- renderPlot({
cols <- map_chr(col_names(), ~ input[[.x]] %||% "")
# convert empty inputs to transparent
cols[cols == ""] <- NA
barplot(
rep(1, length(cols)),
col = cols,
space = 0,
axes = FALSE
)
}, res = 96)
}
```
```{r render-palette-full, fig.cap = demo$caption("Filling out the colours of the rainbow (left), then reducing the number of colours to 3 (right); note that the existing colours are preserved."), out.width = "50%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/render-palette-full", ui, server)
demo$resize(800)
demo$setInputs(
col1 = "red",
col2 = "yellow",
col3 = "orange",
col4 = "green",
col5 = "blue"
)
s1 <- demo$takeScreenshot("rainbow")
demo$setInputs(n = 3)
s2 <- demo$takeScreenshot("change-n")
knitr::include_graphics(c(s1, s2))
demo$deploy()
```
### Dynamic filtering {#dynamic-filter}
To finish off the chapter, I'm going to create an app that lets you dynamically filter any data frame.
Each numeric variable will get a range slider and each factor variable will get a multi-select, so (e.g.) if a data frame has three numeric variables and two factors, the app will have three sliders and two select boxes.
I'll start with a function that creates the UI for a single variable.
It'll return a range slider for numeric inputs, a multi-select for factor inputs, and `NULL` (nothing) for all other types.
```{r}
make_ui <- function(x, var) {
if (is.numeric(x)) {
rng <- range(x, na.rm = TRUE)
sliderInput(var, var, min = rng[1], max = rng[2], value = rng)
} else if (is.factor(x)) {
levs <- levels(x)
selectInput(var, var, choices = levs, selected = levs, multiple = TRUE)
} else {
# Not supported
NULL
}
}
```
Then I'll write the server side equivalent of this function: it takes the variable and value of the input control, and returns a logical vector saying whether or not to include each observation.
Using a logical vector makes it easy to combine the results from multiple columns.
```{r}
filter_var <- function(x, val) {
if (is.numeric(x)) {
!is.na(x) & x >= val[1] & x <= val[2]
} else if (is.factor(x)) {
x %in% val
} else {
# No control, so don't filter
TRUE
}
}
```
I can then use these functions "by hand" to generate a simple filtering UI for the `iris` dataset:
```{r}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
make_ui(iris$Sepal.Length, "Sepal.Length"),
make_ui(iris$Sepal.Width, "Sepal.Width"),
make_ui(iris$Species, "Species")
),
mainPanel(
tableOutput("data")
)
)
)
server <- function(input, output, session) {
selected <- reactive({
filter_var(iris$Sepal.Length, input$Sepal.Length) &
filter_var(iris$Sepal.Width, input$Sepal.Width) &
filter_var(iris$Species, input$Species)
})
output$data <- renderTable(head(iris[selected(), ], 12))
}
```
```{r render-filter-1, fig.cap = "Simple filter interface for the iris dataset", echo = FALSE, out.width = "75%"}
demo <- demoApp$new("action-dynamic/render-filter-1", ui, server)
demo$resize(800)
demo$takeScreenshot()
```
You might notice that I got sick of copying and pasting so the app only works with three columns.
I can make it work with all the columns by using a little functional programming:
- In `ui` use `map()` to generate one control for each variable.
- In `server()`, I use `map()` to generate the selection vector for each variable.
Then I use `reduce()` to take the logical vector for each variable and combine into a single logical vector by `&`-ing each vector together.
Again, don't worry too much if you don't understand exactly what's happening here.
The main take away is that once you master functional programming, you can write very succinct code that generate complex, dynamic apps.
```{r}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
map(names(iris), ~ make_ui(iris[[.x]], .x))
),
mainPanel(
tableOutput("data")
)
)
)
server <- function(input, output, session) {
selected <- reactive({
each_var <- map(names(iris), ~ filter_var(iris[[.x]], input[[.x]]))
reduce(each_var, ~ .x & .y)
})
output$data <- renderTable(head(iris[selected(), ], 12))
}
```
```{r render-filter-2, fig.cap = "Using functional programming to build a filtering app for the `iris` dataset.", echo = FALSE, out.width = "75%"}
demo <- demoApp$new("action-dynamic/render-filter-2", ui, server)
demo$resize(800)
demo$takeScreenshot()
```
From there, it's a simple generalisation to work with any data frame.
Here I'll illustrate it using the data frames in the datasets package, but you can easily imagine how you might extend this to user uploaded data.
See the result in Figure \@ref(fig:filtering-final).
```{r}
dfs <- keep(ls("package:datasets"), ~ is.data.frame(get(.x, "package:datasets")))
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput("dataset", label = "Dataset", choices = dfs),
uiOutput("filter")
),
mainPanel(
tableOutput("data")
)
)
)
server <- function(input, output, session) {
data <- reactive({
get(input$dataset, "package:datasets")
})
vars <- reactive(names(data()))
output$filter <- renderUI(
map(vars(), ~ make_ui(data()[[.x]], .x))
)
selected <- reactive({
each_var <- map(vars(), ~ filter_var(data()[[.x]], input[[.x]]))
reduce(each_var, `&`)
})
output$data <- renderTable(head(data()[selected(), ], 12))
}
```
```{r filtering-final, fig.cap = demo$caption("A dynamic user interface automatically generated from the fields of the selected dataset."), out.width = "100%", fig.show = "hold", fig.align = "default", echo = FALSE, message = FALSE, cache = FALSE}
demo <- demoApp$new("action-dynamic/filtering-final", ui, server)
demo$setInputs(dataset = "Formaldehyde")
demo$resize(800)
demo$takeScreenshot()
demo$deploy()
```
### Dialog boxes
Before we finish up, I wanted to mention a related technique: dialog boxes.
You've seen them already in Section \@ref(feedback-modal), where the contents of the dialog was a fixed text string.
But because `modalDialog()` is called from within the server function, you can actually dynamically generate content in the same way as `renderUI()`.
This is a useful technique to have in your back pocket if you want to force the user to make some decision before continuing on with the regular app flow.
### Exercises
1. Take this very simple app based on the initial example in the section:
```{r}
ui <- fluidPage(
selectInput("type", "type", c("slider", "numeric")),
uiOutput("numeric")
)
server <- function(input, output, session) {
output$numeric <- renderUI({
if (input$type == "slider") {
sliderInput("n", "n", value = 0, min = 0, max = 100)
} else {
numericInput("n", "n", value = 0, min = 0, max = 100)
}
})
}
```
How could you instead implement it using dynamic visibility?
If you implement dynamic visibility, how could you keep the values in sync when you change the controls?
2. Explain how this app works.
Why does the password disappear when you click the enter password button a second time?
```{r}
ui <- fluidPage(
actionButton("go", "Enter password"),
textOutput("text")
)
server <- function(input, output, session) {
observeEvent(input$go, {
showModal(modalDialog(
passwordInput("password", NULL),
title = "Please enter your password"
))
})
output$text <- renderText({
if (!isTruthy(input$password)) {
"No password"
} else {
"Password entered"
}