Dynamische UI Elemente in Shiny – Teil 1


Bei STATWORX setzen wir regelmäßig unsere Projektergebnisse mit Hilfe von Shiny um. Es ist nicht nur eine einfache Möglichkeit, potenziellen Nutzern die Interaktion mit Ihrem R-Code zu ermöglichen, sondern es macht auch Spaß, eine gut aussehende App zu gestalten.
Eine der größten Stärken von Shiny ist seine inhärente Reaktivität, denn reaktiv auf Benutzereingaben zu sein, ist schließlich der Hauptzweck von Webanwendungen. Leider scheinen viele Apps nur die Reaktionsfähigkeit von Shiny auf der Serverseite zu nutzen, während die Benutzeroberfläche komplett statisch bleibt. Das muss nicht unbedingt schlecht sein. Einige Apps würden nicht davon profitieren, dynamische UI-Elemente zu haben. Wenn man sie trotzdem hinzufügt, könnte die App überladen wirken. Aber in vielen Fällen kann die Hinzufügung von Reaktivität zur UI nicht nur zu weniger Unordnung auf dem Bildschirm führen, sondern auch zu saubererem Code. Und das mögen wir doch alle, oder?
Eine Werkzeugkiste für Reaktivität: renderUI
Shiny bietet nativ praktische Werkzeuge, um die Benutzeroberfläche jeder App reaktiv auf Eingaben zu machen. Im heutigen Blogeintrag werden wir uns insbesondere die renderUI-Funktion in Verbindung mit lapply und do.call ansehen.
renderUI ist hilfreich, weil es uns von den Fesseln befreit, genau festlegen zu müssen, welche Art von Objekt wir in unserer Render-Funktion darstellen möchten. renderUI kann jedes UI-Element rendern. Wir könnten zum Beispiel den Typ des Inhalts unseres uiOutput reaktiv auf Eingaben machen, anstatt ihn fest vorzugeben.
Reaktivität mit lapply einführen
Stellen Sie sich eine Situation vor, in der Sie damit beauftragt sind, ein Dashboard zu erstellen, das dem Benutzer drei verschiedene KPIs für drei verschiedene Länder zeigt. Der offensichtlichste Ansatz wäre, die Position jeder KPI-Box auf der UI-Seite der App festzulegen und jedes Element auf der Serverseite mit Hilfe von shinydashboard::renderValueBox zu erstellen, wie im folgenden Beispiel gezeigt.
Der übliche Weg
library(shiny)
library(shinydashboard)
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(),
dashboardBody(column(width = 4,
fluidRow(valueBoxOutput("ch_1", width = 12)),
fluidRow(valueBoxOutput("jp_1", width = 12)),
fluidRow(valueBoxOutput("ger_1", width = 12))),
column(width = 4,
fluidRow(valueBoxOutput("ch_2", width = 12)),
fluidRow(valueBoxOutput("jp_2", width = 12)),
fluidRow(valueBoxOutput("ger_2", width = 12))),
column(width = 4,
fluidRow(valueBoxOutput("ch_3", width = 12)),
fluidRow(valueBoxOutput("jp_3", width = 12)),
fluidRow(valueBoxOutput("ger_3", width = 12)))
)
)
server <- function(input, output) {
output(dollar sign)ch_1 <- renderValueBox({
valueBox(value = "CH",
subtitle = "Box 1")
})
output(dollar sign)ch_2 <- renderValueBox({
valueBox(value = "CH",
subtitle = "Box 2")
})
output(dollar sign)ch_3 <- renderValueBox({
valueBox(value = "CH",
subtitle = "Box 3",
width = 12)
})
output(dollar sign)jp_1 <- renderValueBox({
valueBox(value = "JP",
subtitle = "Box 1",
width = 12)
})
output(dollar sign)jp_2 <- renderValueBox({
valueBox(value = "JP",
subtitle = "Box 2",
width = 12)
})
output(dollar sign)jp_3 <- renderValueBox({
valueBox(value = "JP",
subtitle = "Box 3",
width = 12)
})
output(dollar sign)ger_1 <- renderValueBox({
valueBox(value = "GER",
subtitle = "Box 1",
width = 12)
})
output(dollar sign)ger_2 <- renderValueBox({
valueBox(value = "GER",
subtitle = "Box 2",
width = 12)
})
output(dollar sign)ger_3 <- renderValueBox({
valueBox(value = "GER",
subtitle = "Box 3",
width = 12)
})
}
shinyApp(ui = ui, server = server)
Das könnte eine funktionierende Lösung für die aktuelle Aufgabe sein, aber sie ist kaum elegant. Die Valueboxes nehmen viel Platz in unserer App ein, und selbst wenn sie verkleinert oder verschoben werden können, müssen wir immer alle Boxen betrachten, unabhängig davon, welche gerade von Interesse sind. Der Code ist ebenfalls stark repetitiv und besteht größtenteils aus kopierten Code-Schnipseln. Eine viel elegantere Lösung wäre es, nur die Boxen für jede interessierende Einheit (in unserem Fall Länder) anzuzeigen, die vom Benutzer ausgewählt wurden. Hier kommt renderUI ins Spiel.
renderUI ermöglicht es uns nicht nur, UI-Objekte jeder Art zu rendern, sondern integriert sich auch gut mit der lapply-Funktion. Das bedeutet, dass wir nicht jede Valuebox einzeln rendern müssen, sondern lapply diese repetitive Aufgabe für uns übernehmen kann.
Der reactive Weg
Angenommen, wir haben eine Art von Eingabe namens „select“ in unserer App, wird der folgende Code-Schnipsel eine Valuebox für jedes Element erzeugen, das mit dieser Eingabe ausgewählt wurde. Die erzeugten Boxen zeigen den Namen jedes einzelnen Elements als Wert an und haben ihren Untertitel auf „Box 1“ gesetzt.
lapply(seq_along(input(dollar sign)select), function(i) {
fluidRow(
valueBox(value = input(dollar sign)select[i],
subtitle = "Box 1",
width = 12)
)
})
Wie funktioniert das genau? Die lapply-Funktion iteriert über jedes Element unserer Eingabe „select“ und führt den Code, den wir ihr geben, einmal pro Element aus. In unserem Fall bedeutet das, dass lapply die Elemente unserer Eingabe nimmt und für jedes eine Valuebox eingebettet in eine fluidrow erstellt (technisch gesehen gibt sie einfach den entsprechenden HTML-Code aus, der das erstellen würde).
Dies hat mehrere Vorteile:
- Nur Boxen für ausgewählte Elemente werden angezeigt, wodurch visuelle Unordnung reduziert und das Wesentliche hervorgehoben wird.
- Wir haben effektiv 3 renderValueBox-Aufrufe in einen einzigen renderUI-Aufruf kondensiert, wodurch kopierte Abschnitte in unserem Code reduziert werden.
Wenn wir dies auf unsere App anwenden, sieht unser Code ungefähr so aus:
library(shiny)
library(shinydashboard)
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(
selectizeInput(
inputId = "select",
label = "Select countries:",
choices = c("CH", "JP", "GER"),
multiple = TRUE)
),
dashboardBody(column(4, uiOutput("ui1")),
column(4, uiOutput("ui2")),
column(4, uiOutput("ui3")))
)
server <- function(input, output) {
output(dollar sign)ui1 <- renderUI({
req(input(dollar sign)select)
lapply(seq_along(input(dollar sign)select), function(i) {
fluidRow(
valueBox(value = input(dollar sign)select[i],
subtitle = "Box 1",
width = 12)
)
})
})
output(dollar sign)ui2 <- renderUI({
req(input(dollar sign)select)
lapply(seq_along(input(dollar sign)select), function(i) {
fluidRow(
valueBox(value = input(dollar sign)select[i],
subtitle = "Box 2",
width = 12)
)
})
})
output(dollar sign)ui3 <- renderUI({
req(input(dollar sign)select)
lapply(seq_along(input(dollar sign)select), function(i) {
fluidRow(
valueBox(value = input(dollar sign)select[i],
subtitle = "Box 3",
width = 12)
)
})
})
}
shinyApp(ui = ui, server = server)
Die Benutzeroberfläche reagiert nun dynamisch auf unsere Eingaben im selectizeInput. Das bedeutet, dass Benutzer bei Bedarf immer noch alle KPI-Boxen anzeigen können – aber sie müssen es nicht. Meiner Meinung nach ist diese Flexibilität genau das, wofür Shiny entwickelt wurde – um Benutzern zu ermöglichen, dynamisch mit R-Code zu interagieren. Wir haben auch effektiv den kopierten Code bereits um 66 % reduziert! Es gibt immer noch einige Wiederholungen in den mehrfachen renderUI-Funktionsaufrufen, aber die Serverseite unserer App ist bereits viel angenehmer zu lesen und zu verstehen als das statische Beispiel unserer vorherigen App.

Über lapply hinaus: Weitergehen mit do.call
Wir haben gerade gesehen, dass renderUI mit Hilfe von lapply ganze UI-Elemente dynamisch erzeugen kann. Das ist jedoch nicht das volle Ausmaß dessen, was renderUI leisten kann. Einzelne Teile eines UI-Elements können auch dynamisch erzeugt werden, wenn wir Funktionen verwenden, die es uns ermöglichen, die dynamisch erzeugten Teile eines UI-Elements als Argumente an den Funktionsaufruf zu übergeben, der das Element erstellt. Innerhalb des reaktiven Kontexts von renderUI können wir Funktionen nach Belieben aufrufen, was bedeutet, dass wir mehr Werkzeuge als nur lapply zur Verfügung haben. Hier kommt do.call ins Spiel.
Die do.call-Funktion ermöglicht es uns, Funktionsaufrufe durch Übergeben einer Liste von Argumenten an die betreffende Funktion auszuführen. Das mag wie Funktionsception klingen, aber bleiben Sie dran.
Der do.call folgen
Angenommen, wir möchten ein tabsetPanel erstellen, aber anstatt die Anzahl der angezeigten Tabs festzulegen, lassen wir die Benutzer entscheiden. Die Lösung für diese Aufgabe ist ein zweistufiger Prozess:
1. Wir verwenden lapply, um über eine vom Benutzer gewählte Anzahl zu iterieren und die angegebene Anzahl von Tabs zu erstellen.
2. Wir verwenden do.call, um die shiny::tabsetPanel-Funktion auszuführen, wobei die Tabs aus Schritt 1 als einfaches Argument an do.call übergeben werden.
Das würde ungefähr so aussehen:
# create tabs from input
myTabs <- lapply(1:input(dollar sign)slider, function(i) {
tabPanel(title = glue("Tab {i}"),
h3(glue("Content {i}"))
)
})
# execute tabsetPanel with tabs added as arguments
do.call(tabsetPanel, myTabs)
Dies erzeugt das HTML für ein tabsetPanel mit einer vom Benutzer gewählten Anzahl von Tabs, die alle einen einzigartigen Titel haben und mit Inhalt gefüllt werden können. Sie können es mit dieser Beispiel-App ausprobieren:
library(shiny)
library(shinydashboard)
library(glue)
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(
sliderInput(inputId = "slider", label = NULL, min = 1, max = 5, value = 3, step = 1)
),
dashboardBody(
fluidRow(
box(width = 12,
p(mainPanel(width = 12,
column(width = 6, uiOutput("reference")),
column(width = 6, uiOutput("comparison"))
)
)
)
)
)
)
server <- function(input, output) {
output(dollar sign)reference <- renderUI({
tabsetPanel(
tabPanel(
"Reference",
h3("Reference Content"))
)
})
output(dollar sign)$comparison <- renderUI({
req(input(dollar sign)slider)
myTabs <- lapply(1:input(dollar sign)slider, function(i) {
tabPanel(title = glue("Tab {i}"),
h3(glue("Content {i}"))
)
})
do.call(tabsetPanel, myTabs)
})
}
shinyApp(ui = ui, server = server)

Wie Sie sehen, bietet renderUI einen sehr flexiblen und dynamischen Ansatz für das UI-Design, wenn es in Verbindung mit lapply und dem fortgeschritteneren do.call verwendet wird. Probieren Sie diese Werkzeuge das nächste Mal aus, wenn Sie eine App erstellen, und bringen Sie die gleiche Reaktivität in Shiny's UI, die Sie bereits in dessen Serverteil genutzt haben.