A shiny Web App from LEGO— truck + trailer

[This article was first published on Sebastian Wolf blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

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 I’m 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. It’s 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() . In R-S4 this 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 it’s the only slot. The evalElement method will evaluate this expression. The pdfElement method creates an output that can go to PDF. The shinyElement creates a PlotOutput by calling shiny::renderPlot(). The logElement method writes the expression into a logFile. The R-S4 code 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 a TabPanel inside the UI such as tabPanel(id='modules') . Afterwards, we can read the configuration of the app into the TabPane . Thus we use the appendTab function. The function XML::xmlApply lets 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 of AnyPlot by contains='AnyPlot' inside there class definition. PlotReport is the class for the Report of this package. It contains both of these plots inside the plots slot. 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 the plots slot. This one element is not a plot. That is why it contains a data.frame call 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 a data.frame call inside shiny, we have to overwrite the shinyElement method. Instead of returning a renderPlot output we will return a renderDataTable output. Additionally the pdfElement method has to return a gridExtra::grid.table output.

# 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. You’re 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 you’re good to go. I cannot tell you if it’s more fun building or more fun rolling.

Dear Reader: It’s 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 on github. In case of any comment, leave it on my LinkedIn profile http://linkedin.com/in/zappingseb.

To leave a comment for the author, please follow the link and comment on their blog: Sebastian Wolf blog.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)