Blog

Shiny Module

16.07.2019 10:15
von Andreas Neudecker

Ordnung ist das halbe Leben.. ein deutsches Sprichwort das man vielleicht nicht zwingend leben muss. Doch beim Programmieren ist es essentiell. Denn wenn man nicht ein wenig Zeit in die Ordnung und Struktur eines Projekts investiert, erhöht sich die Zeit die man sich mit der Suche und der Beseitigung von Fehlern beschäftigt um ein Vielfaches.

Wenn Sie schonmal eine Shiny App geschrieben haben, mussten Sie sich Gedanken über das User-Interface (UI) und die Anbindung an den Server machen. Der Code um das UI zu erzeugen landete vermutlich in der Datei ui.R, die Server Funktion in einer server.R Datei (oder Sie haben beides in einer app.R Datei zusammengefasst). Sobald die App etwas komplexer wird, werden auch die beiden Dateien größer und schnell unübersichtlich. Typischerweise fängt man dann an einzelne Bausteine oder Funktionen in weitere Dateien auszulagern, um den Überblick in ui.R und server.R nicht zu verlieren. Diese Dateien wachsen ebenfalls weiter an, werden unübersichtlich und sind häufig Fehlerquellen, die enorm viel Zeit und Nerven kosten, obwohl dies leicht zu vermeiden wäre.

Wie in unserem Blogartikel zum Thema Using Modules in R stellen Module eine Abstraktionsstufe zwischen Funktionen und Paketen dar. Sie sind quasi die Aufbewahrungsboxen in unseren Paketen, die es uns enorm erleichtern, eine Struktur im Projekt zu etablieren und Ordnung zu halten.

In diesem Artikel schauen wir uns an, wie man auch eine Shiny App mit übersichtlichem Code, wiederverwertbaren und automatisiert testbaren Bausteinen (Modulen) baut. Dafür gehen wir zunächst auf die Paketstruktur und das Testen einer App ein, bevor wir uns den eigentlichen Modulen widmen.

Paketstruktur

Auch wenn man nur kleine Anwendungen schreibt, ist es sinnvoll, sie in einem R-Paket zu verpacken. Dadurch bekommt man eine versionierte Version der App und kann einfach auf Vorgängerversionen zurückspringen, wenn in einer neuen Version etwas nicht so läuft wie es soll. Außerdem ermöglicht es, Logik aus der App herauszuhalten und automatisiert testbar in dem Paket unterzubringen.

Um die Paketstruktur zu erstellen können Sie die Ordnerstruktur von Hand anlegen oder die package.skeleton() Funktion nutzen.

Die App packen wir in einen Ordner mit beliebigem Namen (z.B. app) im inst Ordner. Um die App zu starten legen wir noch eine Funktion startApplication() im R Ordner ab:

#' Start Application
#'
#' @param port port of web application
#'
#' @export
startApplication <- function(port = 4242, appFolder = "app") {
  runApp(
    system.file(appFolder, package = "myPackageName"),
    port = port,
    host = "0.0.0.0"
  )
}

Die Paketstruktur sieht dann in etwa folgendermaßen aus.

Und der typische Workflow kann dann z.B. so aussehen:

devtools::install()    # neue Version des Pakets installieren
library(myPackageName) # Paket laden
startApplication()     # App starten

Server-Funktion ohne Logik

Automatisierte Tests in Shiny Apps einzubauen ist nicht trivial, da man das Zusammenspiel vieler UI-Bausteine und Server-Komponenten simulieren muss. Das Paket shinytest stellt jedoch ein Framework zum Testen bereit. Meistens kann man aber auch schon im Code Vorkehrungen treffen um eine möglichst robuste App zu schreiben, ohne dass man shinytest benötigt.

Dazu ist es nötig, dass die server.R Datei möglichst wenig Logik enthält. Dazu sollte man R Funktionen in der neuen Paketstruktur verwenden, die dann im Rahmen von R CMD check getestet werden. Idealerweise besteht jede reaktive Funktion in Shiny nur aus einer Zeile Code. Entweder werden darin andere Input-Elemente, reaktive Werte oder andere reaktive Funktionen verwendet, oder eine einzelne R Funktion aufgerufen:

data <- observeEvent(input$button, getData())

model <- reactive(runModel(data(), param1, param2)

results <- reactive(extractResults(model())

output$text <- renderText(prepareResults(results()))

Module

Es ist möglich, größere Komponenten einer App in einem Modul zusammenzufassen. Dies hat gleich mehrere Vorteile

  • Ein Modul hat eine bestimmte Aufgabe
  • Eigener Namensraum
  • Definiertes Interface mit Modul
  • Wiederverwendbarkeit (in und außerhalb des Projekts)

Auf http://inwtlab.shinyapps.io/exportPlotModule finden Sie beispielsweise ein Modul, das den Export von Plots aus einer Shiny Applikation ermöglicht.

Wie auch für eine Shiny App selbst müssen wir sowohl das UI als auch die Server-Funktion für ein Modul implementieren. Fangen wir mit dem UI an. Wir brauchen eine Funktion, die UI Elemente für das Modul erzeugt. Der Name der Funktion ist beliebig, es ist aber sinnvoll, wenn er mit dem Modulnamen anfängt.

plotExportUI <- function(id) {
  ns <- NS(id)

  tagList(
    selectInput(ns("type"), label = "Type", choices = c("png", "pdf", "tiff", "svg")),
    plotOutput(ns("preview")),
    downloadButton(ns("download"), "Download")
  )
}

Der Code ns erzeugt die Namensraum-Funktion ns, die eine beliebige ID eines UI-Elements in eine ID im Namensraum des Moduls umwandelt. Ansonsten werden in der Funktion nur verschiedene UI-Elemente definiert und über tagList gesammelt zurückgegeben.

Die Server-Funktion sieht so aus. Auch hier ist der Name beliebig, fängt aber wieder sinnvollerweise mit dem Modulnamen an.

plotExport <- function(input, output, session, plotObj) {
  output$preview <- renderPlot({
    plotObj()
  })

  output$download <- downloadHandler(
     filename = function(){
       paste0("plot.", input$type)
    },
    content = function(file){
      switch(
        input$type,
        png = png(file),
        pdf = pdf(file),
        tiff = tiff(file),
        svg = svg(file)
      )
      print(plotObj())
      dev.off()
    }
  )
}

Die Funktion sieht aus wie eine normale Server-Funktion mit einem zusätzlichen Parameter plotObj. Dies enthält das plot-Objekt als reaktives Element.

Nun schauen wir uns an, wie das Modul aufgerufen wird. Das passiert wenig überraschend ebenfalls an zwei Stellen. In der ui.R wird das Modul wie ein normales UI-Element aufgerufen:

[...]
plotExportUI(id = "export")
[...]

Der id-Parameter definiert hierbei den Namensraum des Moduls. Es ist auch möglich das Modul mehrmals in einer App zu verwenden - man muss nur darauf achten eine andere id zu verwenden.

In der server.R muss das Modul ebenfalls gestartet werden:

[...]
  plot <- reactive({
    [...]
    ggplot(d, aes_string(x = input$xcol, y = input$ycol, col = "clusters")) +
      geom_point(size = 4)
  })

  callModule(plotExport, "export", plot)
[...]

plotExport ist der Name der Server-Funktion des Moduls, "export" ist die gleiche id wie für das UI Elment des Moduls. Zusätzlich übergibt man noch das reaktive Element plot . Man könnte an dieser Stelle auch feste Parameter verwenden. Wenn man allerdings ein reaktives Element übergeben will, muss man bei input Variablen aufpassen. Um beispielsweise die Variable input$abc zu übergeben, müsste man callModule so aufrufen:

callModule([...], param = reactive(input$abc))

Man muss also die Variable in reactive einschließen um sicherzugehen, dass sie auch als reaktive Variable übergeben wird. Das gleiche gilt für reaktive Werte, die man mit reactiveValues erzeugt hat.

Das UI und die Server-Funktion des Moduls kann man problemlos in einem R-Paket ablegen und so dann auch für andere verfügbar machen.

Zurück