A shiny Web App from LEGO truck + trailer

How to Build a Shiny Truck part 2??Let the LEGO truck app pull a trailer. An example of a modularized shiny app.

In September 2018 I used an automotive metaphor explaining a large scale R shiny app. RViews published the article. I would summarize the article in one phrase. Upon building large applications (trucks) in R shiny there are a lot of things to keep in mind. To cover all these things in a single app Im providing this tutorial.

You can find all files of the app under https://github.com/zappingseb/biowarptruck??folder: example_packaged

Summary (skip if you read the article in RViews)

The article I wrote in RViews told the reader to pay regard to the fact that any shiny app might become big someday. Ab initio it must be well planned. Additionally, it should be possible to remove or add any part of your app. Thus it has to be modular. Each module must work as a LEGO brick. LEGO bricks come with different functionalities. These bricks follow certain rules, that make them stick to each other. These rules we call a standard. Modules designed like LEGO bricks increase your flexibility. Hence the re-usability of your modules grows. When you set up your app to like that, you have the possibility to add an unlimited number of LEGO bricks. It can grow. Imagining small scale applications like cars. Large scale applications are trucks. The article explained how to build a LEGO truck.

If you build your car from LEGO / more and different parts can make it a truck.

If you built your app from standardized modules / you have the flexibility to insert a lot more functionalities.

A modularized shiny app??Where to start?

The image below explains the idea of the modularized shiny app.

You start with a core shiny application. See it like the chassis of your car. Its made of LEGO. Any other part made of LEGO a stick to your chassis. Such parts can change its functionality. Different modules will help you build different cars. Additionally, you want to have a brick instruction (plan). The plan tells which parts to take and to increase flexibility. The back pages of your brick instruction can contain a different model from the same bricks. If you can build one app from your modules, you can also build a different app containing the same modules. If this is clear to you, we can start building our app in R-shiny:

Implementation rules:

  • Each module is an R package
  • The core R package defines the standardization of bricks
  • The core app is a basic shiny app
  • The brick instruction (plan) file is not in R

Why these rules exist, will become clear reading the article.

The app we want to build

The app we want to build will create different kinds of outputs from a panel of user inputs. These different outputs will show up inside the app. Additionally, all outputs will go into a PDF file. The example will include two plots in the plot module and one table in the table module. As each module is an R-package, you can imagine adding many more R-packages step by step. A lot of outputs are possible within shiny. The main feature of this app is the possibility to add more and more modules. More modules will not screw up the PDF reporting function or the view function. Modules do not interact at all inside this app.

The core R-package

The core package contains the structure that modules have to follow to fit into the core app. There are two kinds of structures that we will define as R-S4 classes. One that represents modules and one that represents output elements in those modules.

Class diagram of the core application: The left side shows the reports. The app can generate each of those. Each contains a list of elements to go into the report (plots). The right-hand side contains the class definition of such elements. Each element is of kind AnyPlot. This class contains a call (plot_element) that produces the element upon calling evalElement.

For task one, we call the object (class) a Report. The Report is the main brick we define in the core app. It contains:

plots  A list of all elements shown in the report 
filename - The name of the output file (where to report to)
obs - The handling of the input value input$obs
rendered - Whether it shows up in the app right now

Additionally, the Report class carries some functionalities to generate a shiny output. Moreover, it allows creating PDF reports. The functionalities come within the methods shinyElement() and pdfElement(). InR-S4this looks like this:

setClass("Report",representation(plots="list", filename="character", obs="numeric", rendered="logical"))
           
setMethod("pdfElement",signature = "Report",definition = function(object){
  tryCatch({
    pdf(object@filename)
    lapply(object@plots,function(x){
      pdfElement(x)
    })
    dev.off()
    object@rendered <- TRUE
  },error=function(e){warning("plot not rendered")#do nothing
  })
  return(object)
})

setMethod("shinyElement",signature = "Report",definition = function(object){
  renderUI({
    lapply(object@plots,
           function(x){
             logElement(x)
             shinyElement(x)
           })
  })
})

Now we would also like to define, how to structure each element of the Thus. Thus we define a class AnyPlot that carries an expression as its the only slot. TheevalElementmethod will evaluate this expression. ThepdfElementmethod creates an output that can go to PDF. TheshinyElementcreates a PlotOutput by callingshiny::renderPlot(). ThelogElementmethod writes the expression into a logFile. TheR-S4code shows up below:

setClass("AnyPlot", representation(plot_element = "call"))

# constructor
AnyPlot <- function(plot_element=expr(plot(1,1))){
  new("AnyPlot", plot_element = plot_element)
}

setMethod("evalElement",signature = "AnyPlot",definition = function(object){
  eval(object@plot_element)
})

setMethod("pdfElement",signature = "AnyPlot",definition = function(object){
  evalElement(object)
})

setMethod("shinyElement",signature = "AnyPlot",definition = function(object){
  renderPlot(evalElement(object))
})

setMethod("logElement",signature = "AnyPlot",definition = function(object){
  write(paste0(deparse(object@plot_element)," evaluated"), file="app.log",append=TRUE)
})

The core app

To keep this example simple, the core app will include all inputs. The outputs of this app will be modular. The core app has to fulfill the following tasks:

  1. have a container to show modules
  2. Read the plan??to add containers
  3. include a button to print modules to PDF
  4. imagine also a button printing modules to .png, .jpg, .xlsx
  5. include the inputs

Showing modules

For task one we use the shinyElement method of a given object and insert this in any output. I decided on a Tab output for each module. So each module gets rendered inside a different tab.

Reading the plan

Now here comes the hard part of the app. As I said I wanted to add two modules. One with plots and one with a table. The plan (config.xml) file has to contain this information. So I use this as a plan file:

<?xml version="1.0" encoding="UTF-8"?>
<modules>
  <module>
    <id>module1</id>
    <name>Plot Module</name>
    <package>module1</package>
    <class>PlotReport</class>
  </module>
  <module>
    <id>module2</id>
    <name>Text Output</name>
    <package>module2</package>
    <class>TableReport</class>
  </module>
</modules>





Construction plan of the web App

You can see I have two modules. There is a package for each module. Inside this package, a class defines (see section module packages) the output. This class is a child of our Report class.

The module shows up as a tab inside our app. We will go through this step by step. First, we need to have a function to load the packages for each module:

library(XML)
load_module <- function(xmlItem){
  devtools::load_all(paste0("./",xmlValue(xmlItem[["package"]]))) 
}

Second, we need a function to generate a tab out of the information of the module:

library(shiny)
module_tab <- function(xmlItem){
  tabPanel(XML::xmlValue(xmlItem[["name"]]),
           uiOutput(xmlValue(xmlItem[["id"]]))
  )
}

As we now have these two functions, we can iterate over the XML file and build up our app. First we need aTabPanelinside the UI such astabPanel(id='modules'). Afterwards, we can read the configuration of the app into theTabPane. Thus we use theappendTabfunction. The functionXML::xmlApplylets us iterate over each node of the XML (config.xml) and perform these tasks.

configuration <- xmlApply(xmlRoot(xmlParse("config.xml")),function(xmlItem){
    load_module(xmlItem)
    
    appendTab("modules",module_tab(xmlItem),select = TRUE)
    
    list(
      name = xmlValue(xmlItem[["name"]]),
      class = xmlValue(xmlItem[["class"]]),
      id = xmlValue(xmlItem[["id"]])
    )
  })

Each module is now loaded into the app in a static manner. The next part will deal with making it reactive.

Rendering content into panels

For Dynamic rendering of the panels, it is necessary to know some inputs. First the tab the user chose. The input$modules variable defines the tab chosen. Additionally the outputs of our shiny app must update by one other input, input$obs . So upon changing the tab or changing the input$obs we need to call an event. This event will call the Constructor function of our S4 object. Following this the shinyElement method renders the output.

The module class gets reconstructed up on changes in the input$modules or input$obs
# Create a reactive to create the Report object due to
  # the chosen module
  report_obj <- reactive({
    module <- unlist(lapply(configuration,function(x)x$name==input$modules))
    if(!any(module))module <- c(TRUE,FALSE)
    do.call(configuration[[which(module)]][["class"]],
            args=list(
              obs = input$obs
    ))
  })
  
  # Check for change of the slider/tab to re-calculate the report modules
  observeEvent({input$obs
    input$modules},{
      
      # Derive chosen tab
      module <- unlist(lapply(configuration,function(x)x$name==input$modules))
      if(!any(module))module <- c(TRUE,FALSE)
      
      # Re-render the output of the chosen tab
      output[[configuration[[which(module)]][["id"]]]] <- shinyElement(  report_obj() )
    })

The reactive report_obj is a function that can call the Constructor of our Report object. Using the observeEvent function for input$obs and input$modules we call this reactive. This allows reacting on user inputs.

Deriving PDF files from reports

Adding a PDF render button to enable the download of PDF files.

The pdfElement function renders the S4 object as a PDF file. If this worked fine the PDF elements add up to the download button.

An extra label checks the success of the PDF rendering.

# Observe PDF button and create PDF
  observeEvent(input$"renderPDF",{
    
    # Create PDF
    report <- pdfElement(report_obj())
    
    # If the PDF was successfully rendered update text message
    if(report@rendered){
      output$renderedPDF <- renderText("PDF rendered")
    }else{
      output$renderedPDF <- renderText("PDF could not be rendered")
    }
  })
  
  # Observe Download Button and return rendered PDF
  output$downloadPDF <- 
    downloadHandler(
      filename =  report_obj()@filename,
      content = function(file) {
        file.copy( report_obj()@filename, file, overwrite = TRUE)
      }
    )

We finished the core app. You can find the app here: app.R and the core package here: core.

The last step is to put the whole truck together.

Module packages

The two module packages will now contain two classes. Both must be children of the class Report. Each element inside these classes must be a child class of the class AnyPlot. Red bricks in the next picture represent Reports and yellow bricks represent AnyPlots.

Final app: The truck consists of a core app with a PlotReport and a TableReport. These consist of three AnyPlot elements that the trailer of the truck carries.

Plot package

The first module package will produce a scatter plot and a histogram plot. Both are children ofAnyPlotbycontains='AnyPlot'inside there class definition.PlotReportis the class for theReportof this package. It contains both of these plots inside theplotsslot. See the code below for the constructors of those classes.


# Define Classes to use inside the apps ------------------------------------------------------------
setClass("HistPlot", representation(color="character",obs="numeric"), contains = "AnyPlot")
setClass("ScatterPlot", representation(obs="numeric"), contains = "AnyPlot")
setClass("PlotReport",contains = "Report")

HistPlot <- function(color="darkgrey",obs=100){
  new("HistPlot",
      plot_element = expr(hist(rnorm(!!obs), col = !!color, border = 'white')),
      color = color,
      obs = obs
  )
}

ScatterPlot <- function(obs=100){
  new("ScatterPlot",
      plot_element = expr(plot(sample(!!obs),sample(!!obs))),
      obs = obs
  )
}

#' Constructor of a PlotReport
PlotReport <- function(obs=100){
  new("PlotReport",
      plots = list(
        HistPlot(color="darkgrey", obs=obs),
        ScatterPlot(obs=obs)
      ),
      filename="test_plots.pdf",
      obs=obs,
      rendered=FALSE
  )
}

Table package

The table package follows the same rules as the plot package. The main difference is that there is only one element inside theplotsslot. This one element is not a plot. That is why it contains adata.framecall as its expression.

setClass("TableElement", representation(obs="numeric"), contains = "AnyPlot")
setClass("TableReport",contains = "Report")

TableElement <- function(obs=100){
  new("TableElement",
      plot_element = expr(data.frame(x=sample(x=!!obs,size=5)))
  )
}

#' Constructor for a TableReport
TableReport <- function(obs=100){
  new("TableReport",
      plots=list(
        TableElement(obs=obs)
      ),
      filename="test_text.pdf",
      obs=obs,
      rendered=F
  )
}

To render adata.framecall inside shiny, we have to overwrite theshinyElementmethod. Instead of returning arenderPlotoutput we will return arenderDataTableoutput. Additionally thepdfElementmethod has to return agridExtra::grid.tableoutput.

# Table Methods -------------------------------------------------------------
setMethod("shinyElement",signature = "TableElement",definition = function(object){
  renderDataTable(evalElement(object))
})

setMethod("pdfElement",signature = "TableElement",definition = function(object){
  grid.table(evalElement(object))
})

Packaging advantage

A major advantage of packaging each module is the definition of dependencies. The DESCRIPTION file specifies all dependencies of the module package. For example, the table module needs the gridExtra package. The core app package needs shiny, methods, XML, devtools . The app does not need extra library calls. Any co-worker can install all dependencies

Final words

Now you must have the tools to start building up your own large scale shiny application. Modularize the app using packages. Standardize it using S4 or any other object-oriented R style. Set the app up using an XML or JSON documents. Youre good to go. Set up the core package and the module packages inside one directory. You can load them with devtools and start building your shiny file app.R . You can now build your own app exchanging the module packages.

Like every kid, you can now enjoy playing with your truck afterward and youre good to go. I cannot tell you if its more fun building or more fun rolling.

Dear Reader: Its always a pleasure to write about my work on building modular shiny apps. I thank you for reading until the end of this article. If you liked the article, you can star the repository ongithub. In case of any comment, leave it on my LinkedIn profilehttp://linkedin.com/in/zappingseb.