Erstellen eines html-Widgets mit R

  1. Anlegen eines Metaskripts
  2. Das R-Binding
  3. Das Javascript-Binding
  4. Erstellen des Widget-Pakets
  5. Einbinden in Shiny

Die R-Library htmlwidgets ist ein elegante Schnittstelle zwischen R und JavaScript. JavaScript-Code kann damit in der Konsole ausgegeben oder in Shiny Applikationen eingebunden werden. Dieses Tutorial zeigt, wie eigener JavaScipt-Code in ein R-Paket integriert und anschließend in einer Shiny-Applikation ausgeführt wird.

In unserem Beipiel erzeugen wir ein Header-Widget. Dem Aufruf des Widgets wird ein beliebiger Text und eine URL mitgegeben, sodass beispielsweise der Textzug data-science-architect mit Verlinkung auf diese Homepage als Header in eine Shiny-App eingebunden werden kann. Im R-Studio-Viewer sollte unser Widget folgendermaßen aussehen:

Anlegen eines Metaskripts

Für das Erstellen eines htmlwidgets empfiehlt es sich, zunächst eine Meta-Skriptdatei anzulegen. In dieser halten wir Befehle fest, mit denen das htmlwidgets-Paket erstellt wird.

#--------------- Initiierung ---------------------#
# Lade Ressourcen
install.packages("devtools")
install.packages("htmlwidgets")

# Erstelle Dateisystem für R-Paket
devtools::create("dsa")
setwd("./dsa")

# Erstelle Dateisystem für htmlwidget
htmlwidgets::scaffoldWidget("dsa")

#---------------  Iterative Codeelemente ---------#
# Installiere Paket
devtools::install()

library("dsa")

dsa(text='data-science-architect',
    href='https://www.data-science-architect.de')

In dem Block ‚Initiierung‘ befinden sich Codeelemente, die nur einmalig ausgeführt werden müssen. Dies sind insbesondere 2 wesentliche Schritte, die beim Erstellen eines htmlwidget-Pakets auch immer zu gehen sind:

  1. Erstellen eines R-Pakets. Die Dateistruktur des Pakets wird hier mit der Funktion devtools::create() erzeugt.
  2. Erstellen der Ressourcen für das htmlwidget innerhalb des Pakets. Mit der Funktion htmlwidgets::scaffoldWidget() lässt sich ebenfalls mit einem einzigen Befehlsaufruf sehr bequem die Skelettstruktur der notwendigen Verzeichnisse und Dateien erstellen.

Die erzeugte Paketstruktur durch create():

scaffoldwidget() fügt das Verzeichnis inst hinzu:

Innerhalb dieses Verzeichnisses inst befinden sich die Dateien dsa.js und dsa.yalm. Die js-Datei enthält die Javascript-Funktion des Widgets. Die yalm-Datei ist eine Konfigurationsdatei. Dort werden Dependencies angegeben, auf die Javascript zugreifen muss (in unserem Beispielwidget werden allerdings keine weiteren Ressourcen benötigt). Außerdem wurde eine R-Funktion dsa.R im R-Verzeichnis des Pakets angelegt, auf die wir später im Detail eingehen.

Der untere Anweisungsblock Iterative Codeelemente hilft dabei, den Entwicklungsprozess zu unterstützen. Durch deren Ausführung wird das Paket installiert, in die Session geladen und die Widgetfunktion ausgeführt. Neuer Code kann durch das Ausführen dieser Zeilen schnell ausprobiert werden.

Für die Erstellen eines Widgets müssen 2 wesentliche Elemente entwickelt werden:

  1. Die R-Funktion, mit dem das Widget aufgerufen wird – Das R-Binding
  2. Der Javascript-Code, welcher durch den Aufruf der R-Funktion ausgeführt werden soll – Das Javascript-Binding

Lassen Sie uns im Folgenden diese beiden zentralen Entwicklungsschritte eines htmlwidgets genauer betrachten.

Das R-Binding

Die Funktion des R-Bindings liegt, wie man es aus der Paketerstellung in R kennt, im Ordner R im Hauptverzeichnis des Pakets. In unserem Beispiel werden der Funktion 5 Argumente übergeben:

  1. text: Der Schriftzug des Headers
  2. href: Die URL, auf den die Verlinkung zeigen soll
  3. width: Breite des widgets
  4. heigt: Höhe des Widgets
  5. elementId: Eine feste ID für den html-Tag, in dem das Widget erzeugt wird.

Die Argumente width, heigth und elementID sollten jedem neuen Widget übergeben werden können und bilden ein Standardvorgehen. Sie werden an den internen Funktionsaufruf htmlwidgets::createWidget() weitergeleitet und den gleichnamigen Argumenten zugeordnet. Die Argemente text und href unserer Widgetfunktion sind hingegen individuell für unser Beispielwidget von Bedeutung. Alle individuellen Argumente werden in einer Liste gebündelt und dem Argument x aus createWidget() übergeben. Die in der Liste enthaltenen Elemente werden innerhalb des R-Bindings in ein JSON-Format konvertiert und später der Javascript-Funktion renderValue() des Javascript-Bindings übergeben.

#' <Add Title>
#'
#' <Add Description>
#'
#' @import htmlwidgets
#'
#' @export
dsa <- function(text, href, width = NULL, height = NULL, elementId = NULL) {

  # forward options using x
  x = list(
    text = text,
    href = href
  )

  # create widget
  htmlwidgets::createWidget(
    name = 'dsa',
    x = x,
    width = width,
    height = height,
    package = 'dsa',
    elementId = elementId
  )
}

Das Javascript-Binding

Durch den Aufruf von htmlwidgets::scaffoldwidget() haben wir in dem Verzeichnis inst die Datei dsa.js erstellt. Diese enthält den im Widget auszuführenden Javascript-Code. Eine Skelettstruktur der Funktion ist durch scaffoldwidget bereits erstellt worden. Für unser Beispielwidget müssen wir nun innerhalb der renderValue-Funktion das Javascript-Binding definieren. Für unser Beispielwidget erstellen wir grob gesagt ein div, welches einen <a>-Tag mit zugehöriger Hyperreferenz enthält. Wir sehen am Quellcode, dass der Funktion ein Argument x übergeben wird. Dieses enthält die Elemente der Liste x, die im R-Binding definiert wurden. In unserem Fall sind das die Elemente text und href. Mit der im Javascript üblichen Schreibweise von x.text bzw. x.href können wir auf diese Elemente zugreifen.

HTMLWidgets.widget({

  name: 'dsa',

  type: 'output',

  factory: function(el, width, height) {

    return {

      renderValue: function(x) {

        var div = document.createElement("div");
        div.classList.add('myDiv');
        div.style.margin = "auto";
        div.style.align = "center";
        div.style.width = "350px";
        div.style.background = "lightgrey";
        div.style.padding = "20px";
        div.style.borderRadius = "15px"
        div.setAttribute("align","center")
        document.querySelector(".dsa").appendChild(div)

        var dsa_tag = document.createElement("a")
        dsa_tag.classList.add('dsa_tag');
        dsa_tag.style.align = "center";
        dsa_tag.style.margin = "auto";
        dsa_tag.style.color = "#c4751b";
        dsa_tag.style.fontSize = "26px";
        dsa_tag.style.fontWeight = "bold";
        dsa_tag.style.textDecoration = "none";

        document.querySelector(".myDiv").appendChild(dsa_tag);

        var text = document.createTextNode(x.text)
        document.querySelector(".dsa_tag").appendChild(text)

        dsa_tag.setAttribute("href",x.href)

      },

      resize: function(width, height) {

      }

    };
  }
});

Erstellen des Widget-Pakets

Alle zwingend notwendigen Komponenten unseres Widgets sind nun erstellt. Das Paket kann nun installiert werden. Wir führen dazu die folgenden Zeilen aus unserem Metaskript aus und sollten im R-Studio-Viewer unser Widget angezeigt bekommen:

# Installiere Paket
devtools::install()

library("dsa")

dsa(text='data-science-architect',
    href='https://www.data-science-architect.de',
    height='200px')

Einbinden des Widgets in Shiny

Durch das Ausführen der htmlwidgets::scaffoldwidget()-Funktion, sind keine weiteren manuellen Schritte notwendig, um das Widget innerhalb einer Shiny-App zu nutzen. Die im R-Ordner angelegte dsa.R-Datei, enthält einen Block in dem die Funktion (renderDsa) für die serverseitige Einbettung des Widgets angelegt ist und eine Funktion (dsaOutput), die die clientseitige Einbettung in die App ermöglicht.

Wir sollten uns nochmal vergegenwärtigen: In Shiny-Applikationen – wie an anderen Web-Applikationen auch – werden server- und clientseitige Komponenten voneinander getrennt. Die Datenverarbeitugen erfolgen in der Regel serverseitig, und, insbesondere in Shiny Applikationen, umfassen die clientseitigen Aufgaben lediglich die Darstellung des Web-Contents. Dieser Aufteilung entsprechend erstellt renderDsa() den Content, während dsaOutput() lediglich ein html-Element <div> erstellt, in dem später der Output dargestellt wird. Das ist eine der Grundfunktionalitäten von shiny: Der Content vom Backend wird an genau dieses div im Frontend gerouted.

Um zu sehen, ob das Einbetten unseres Widgets in eine Shiny-App funktioniert, erstellen wir ein neues shiny-Projekt. Wenn wir die Shiny-App mit R-Studio anlegen, liegen in der Hauptordner des Projekts bereits die zentralen shiny-Dateien server.R und ui.R vor.
In die server.R-Datei laden wir unser Widgetpaket und binden die renderDsa-Funktion ein (den automatisch erstellten Beispielcode belassen wir in der Datei).

library(shiny)
library(dsa)

shinyServer(function(input, output) {

  output$dsa <- renderDsa(
    dsa(text='data-science-architect',
        href='https://www.data-science-architect.de')
    )

  output$distPlot <- renderPlot({

    # generate bins based on input$bins from ui.R
    x    <- faithful[, 2]
    bins <- seq(min(x), max(x), length.out = input$bins + 1)

    # draw the histogram with the specified number of bins
    hist(x, breaks = bins, col = 'darkgray', border = 'white')

  })

})

Anschließend wird der erzeugte Content in das User-Interface eingebunden.

library(shiny)
library(dsa)

shinyUI(fluidPage(

  # Application title
  titlePanel(dsaOutput('dsa',
                       height='180px')),

  # Sidebar with a slider input for number of bins
  sidebarLayout(
    sidebarPanel(
      sliderInput("bins",
                  "Number of bins:",
                  min = 1,
                  max = 50,
                  value = 30)
    ),

    # Show a plot of the generated distribution
    mainPanel(
      plotOutput("distPlot")
    )
  )
))

Mit diesen Schritten haben wir bereits ein lauffähiges Widget erzeugt. Am Ende sollte das Ergebnis so aussehen: