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.
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:
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:
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:
- have a container to show modules
- Read the plan — to add containers
- include a button to print modules to PDF
- imagine also a button printing modules to “.png”, “.jpg”, “.xlsx”
- 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>
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 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
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.
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.
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.
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.
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.
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 clap for it on Medium or star the repository on github. In case of any comment, leave it here or on my LinkedIn profile http://linkedin.com/in/zappingseb.
Further reading
- How to build custom inputs in shiny apps
- How to get people R-ready in an hour
- Tutorial: An app visualizing biopsy data
A shiny Web App from LEGO— truck + trailer was originally published in Towards Data Science on Medium, where people are continuing the conversation by highlighting and responding to this story.
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.