├── apps.png ├── example_packaged ├── graphics │ ├── car1.png │ ├── pdf.png │ ├── image1.jpg │ ├── images.pptx │ ├── fulltruck.png │ ├── constructor.png │ ├── class_diagram.png │ ├── construction_plan.png │ ├── class_diagram_core.png │ └── construction_plan_done.png ├── config.xml ├── module1 │ ├── DESCRIPTION │ └── R │ │ └── module1.R ├── module2 │ ├── DESCRIPTION │ └── R │ │ └── module2.R ├── README.md ├── core │ ├── DESCRIPTION │ └── R │ │ └── core.R └── app.R ├── biowarptruck.Rproj ├── example_apps ├── app_stiff.R ├── app_stiff_report.R ├── app_object.R ├── app_stiff_report_multi.R ├── app_object_report.R └── app_object_report_multi.R ├── README.Rmd └── README.md /apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/apps.png -------------------------------------------------------------------------------- /example_packaged/graphics/car1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/car1.png -------------------------------------------------------------------------------- /example_packaged/graphics/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/pdf.png -------------------------------------------------------------------------------- /example_packaged/graphics/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/image1.jpg -------------------------------------------------------------------------------- /example_packaged/graphics/images.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/images.pptx -------------------------------------------------------------------------------- /example_packaged/graphics/fulltruck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/fulltruck.png -------------------------------------------------------------------------------- /example_packaged/graphics/constructor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/constructor.png -------------------------------------------------------------------------------- /example_packaged/graphics/class_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/class_diagram.png -------------------------------------------------------------------------------- /example_packaged/graphics/construction_plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/construction_plan.png -------------------------------------------------------------------------------- /example_packaged/graphics/class_diagram_core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/class_diagram_core.png -------------------------------------------------------------------------------- /example_packaged/graphics/construction_plan_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zappingseb/biowarptruck/HEAD/example_packaged/graphics/construction_plan_done.png -------------------------------------------------------------------------------- /biowarptruck.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | -------------------------------------------------------------------------------- /example_packaged/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | module1 5 | Plot Module 6 | module1 7 | PlotReport 8 | 9 | 10 | module2 11 | Text Output 12 | module2 13 | TableReport 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example_packaged/module1/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: module1 2 | Version: 1.0.0 3 | Date: 2018-08-22 4 | Title: A package defining which a plot module App 5 | Authors@R: c( 6 | person("Sebastian", "Wolf", , "zappingseb@gmail.com", role = c("aut","cre")) 7 | ) 8 | Depends: 9 | R (>= 3.1.3), 10 | appCore 11 | Description: This package provides some functionality to create plots for 12 | one observation 13 | Collate: 14 | module1.R 15 | VignetteBuilder: knitr 16 | License: GPL (>= 2) 17 | RoxygenNote: 6.0.1 18 | -------------------------------------------------------------------------------- /example_packaged/module2/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: module2 2 | Version: 1.0.0 3 | Date: 2018-08-22 4 | Title: A package defining which a text output module App 5 | Authors@R: c( 6 | person("Sebastian", "Wolf", , "zappingseb@gmail.com", role = c("aut","cre")) 7 | ) 8 | Depends: 9 | R (>= 3.1.3), 10 | appCore, 11 | gridExtra 12 | Description: This package provides some functionality to create a table for 13 | one observation 14 | Collate: 15 | module2.R 16 | VignetteBuilder: knitr 17 | License: GPL (>= 2) 18 | RoxygenNote: 6.0.1 19 | -------------------------------------------------------------------------------- /example_packaged/README.md: -------------------------------------------------------------------------------- 1 | to run this you need to: 2 | 3 | ``` 4 | install.packages(c("devtools","rlang","XML","methods","gridExtra")) 5 | ``` 6 | 7 | Afterwards start the app by 8 | 9 | ``` 10 | shiny::runApp('example_packaged') 11 | ``` 12 | 13 | --- 14 | 15 | 16 | The app is described inside this 17 | 18 | [Medium Article](https://medium.com/p/c977015bc6a9?source=your_stories_page---------------------------) 19 | 20 | --- 21 | 22 | **In any case of trouble do not hesitate to contact me** 23 | 24 | http://linkedin.com/in/zappingseb 25 | 26 | or 27 | 28 | https://github.com/zappingseb 29 | -------------------------------------------------------------------------------- /example_apps/app_stiff.R: -------------------------------------------------------------------------------- 1 | server <- function(input, output) { 2 | # Output Gray Histogram 3 | output$distPlot <- renderPlot({ 4 | x = sample(x = seq(from=0,to=100,by=0.1), size=input$obs,replace=T) 5 | plot( 6 | x = x, 7 | x+sample(x = 0.1:4, size=input$obs,replace=T)*sample(x = c(-1,1), size=input$obs,replace=T) 8 | ) 9 | }) 10 | 11 | } 12 | 13 | # Simple shiny App containing the standard histogram + PDF render and Download button 14 | ui <- fluidPage( 15 | sidebarLayout( 16 | sidebarPanel( 17 | sliderInput( 18 | "obs", 19 | "Number of observations:", min = 10, max = 500, value = 100) 20 | ), 21 | mainPanel( 22 | plotOutput("distPlot") 23 | ) 24 | ) 25 | ) 26 | shinyApp(ui = ui, server = server) 27 | -------------------------------------------------------------------------------- /example_packaged/core/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: appCore 2 | Version: 1.0.0 3 | Date: 2018-08-22 4 | Title: A package defining which elements can appear inside an app 5 | Authors@R: c( 6 | person("Sebastian", "Wolf", , "zappingseb@gmail.com", role = c("aut","cre")) 7 | ) 8 | Depends: 9 | R (>= 3.1.3), 10 | shiny, 11 | methods, 12 | rlang, 13 | XML, 14 | devtools 15 | Description: This provides a framework for R packages developed for 16 | a regulatory environment. It is based on the 'testthat' unit testing system and provides the adapter 17 | functionalities for XML-based test case definition as well as for standardized 18 | reporting of the test results. 19 | Collate: 20 | core.R 21 | VignetteBuilder: knitr 22 | License: GPL (>= 2) 23 | RoxygenNote: 6.0.1 24 | -------------------------------------------------------------------------------- /example_apps/app_stiff_report.R: -------------------------------------------------------------------------------- 1 | server <- function(input, output) { 2 | # Output Gray Histogram 3 | output$distPlot <- renderPlot({ 4 | hist(rnorm(input$obs), col = 'darkgray', border = 'white') 5 | }) 6 | # Observe PDF button and create PDF 7 | observeEvent(input$"renderPDF",{ 8 | pdf("test.pdf") 9 | hist(rnorm(input$obs), col = 'darkgray', border = 'white') 10 | dev.off() 11 | output$renderedPDF <- renderText("PDF rendered") 12 | 13 | }) 14 | 15 | # Observe Download Button and return rendered PDF 16 | output$downloadPDF <- downloadHandler( 17 | filename = "test.pdf", 18 | content = function(file) { 19 | file.copy("test.pdf", file, overwrite = TRUE) 20 | } 21 | ) 22 | } 23 | 24 | # Simple shiny App containing the standard histogram + PDF render and Download button 25 | ui <- fluidPage( 26 | sidebarLayout( 27 | sidebarPanel( 28 | sliderInput("obs", "Number of observations:", min = 10, max = 500, value = 100), 29 | actionButton("renderPDF","Render a Report"), 30 | textOutput("renderedPDF"), 31 | downloadButton("downloadPDF","Get rendered PDF") 32 | ), 33 | mainPanel( 34 | plotOutput("distPlot") 35 | ) 36 | ) 37 | ) 38 | shinyApp(ui = ui, server = server) 39 | -------------------------------------------------------------------------------- /example_packaged/module2/R/module2.R: -------------------------------------------------------------------------------- 1 | 2 | setClass("TableElement", representation(obs="numeric"), contains = "AnyPlot") 3 | 4 | 5 | # Define Children of Report class to enable different reports easily --------------- 6 | setClass("TableReport",contains = "Report") 7 | 8 | 9 | 10 | 11 | #' Construct the TableElement class 12 | #' 13 | #' The class constructed will contain a random data.frame with one 14 | #' column and 5 elements as its plot_element 15 | #' 16 | #' @param obs (\code{numeric}) How many observations to show in the histogram 17 | #' 18 | #' @return \code{TableElement} an object of class TableElement 19 | #' 20 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 21 | TableElement <- function(obs=100){ 22 | new("TableElement", 23 | plot_element = expr(data.frame(x=sample(x=!!obs,size=5))) 24 | ) 25 | } 26 | 27 | 28 | #' Constructor for a TableReport 29 | TableReport <- function(obs=100){ 30 | new("TableReport", 31 | plots=list( 32 | TableElement(obs=obs) 33 | ), 34 | filename="test_text.pdf", 35 | obs=obs, 36 | rendered=F 37 | ) 38 | } 39 | 40 | 41 | # Table Methods ------------------------------------------------------------- 42 | 43 | setMethod("shinyElement",signature = "TableElement",definition = function(object){ 44 | renderDataTable(evalElement(object)) 45 | }) 46 | 47 | #' Method to plot a Plot element 48 | setMethod("pdfElement",signature = "TableElement",definition = function(object){ 49 | grid.table(evalElement(object)) 50 | }) 51 | -------------------------------------------------------------------------------- /example_packaged/module1/R/module1.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Define Classes to use inside the apps ------------------------------------------------------------ 4 | setClass("HistPlot", representation(color="character",obs="numeric"), contains = "AnyPlot") 5 | setClass("ScatterPlot", representation(obs="numeric"), contains = "AnyPlot") 6 | setClass("PlotReport",contains = "Report") 7 | 8 | 9 | #' Construct the HistPlot class 10 | #' 11 | #' The class constructed will contain a Histogram as 12 | #' its plot element 13 | #' 14 | #' @param color (\code{character}) A color in which to show the bars of the plot 15 | #' @param obs (\code{numeric}) How many observations to show in the histogram 16 | #' 17 | #' @return \code{HistPlot} an object of class HistPlot 18 | #' 19 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 20 | HistPlot <- function(color="darkgrey",obs=100){ 21 | new("HistPlot", 22 | plot_element = expr(hist(rnorm(!!obs), col = !!color, border = 'white')), 23 | color = color, 24 | obs = obs 25 | ) 26 | } 27 | 28 | #' Construct the ScatterPlot class 29 | #' 30 | #' The class constructed will contain a random scatterplot as 31 | #' its plot element 32 | #' 33 | #' @param obs (\code{numeric}) How many observations to show in the histogram 34 | #' 35 | #' @return \code{ScatterPlot} an object of class ScatterPlot 36 | #' 37 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 38 | ScatterPlot <- function(obs=100){ 39 | new("ScatterPlot", 40 | plot_element = expr(plot(sample(!!obs),sample(!!obs))), 41 | obs = obs 42 | ) 43 | } 44 | 45 | #' Constructor of a PlotReport 46 | PlotReport <- function(obs=100){ 47 | new("PlotReport", 48 | plots = list( 49 | HistPlot(color="darkgrey", obs=obs), 50 | ScatterPlot(obs=obs) 51 | ), 52 | filename="test_plots.pdf", 53 | obs=obs, 54 | rendered=FALSE 55 | ) 56 | } -------------------------------------------------------------------------------- /example_apps/app_object.R: -------------------------------------------------------------------------------- 1 | library(methods) 2 | library(rlang) 3 | 4 | 5 | setGeneric("plotElement",where = parent.frame(),def = function(object){standardGeneric("plotElement")}) 6 | setGeneric("shinyElement",where = parent.frame(),def = function(object){standardGeneric("shinyElement")}) 7 | setClass("AnyPlot", representation(plot_element = "call")) 8 | setClass("HistPlot", representation(color="character",obs="numeric"), contains = "AnyPlot") 9 | 10 | AnyPlot <- function(plot_element=expr(plot(1,1))){ 11 | new("AnyPlot", 12 | plot_element = plot_element 13 | ) 14 | } 15 | 16 | HistPlot <- function(color="darkgrey",obs=100){ 17 | new("HistPlot", 18 | plot_element = expr(hist(rnorm(!!obs), col = !!color, border = 'white')), 19 | color = color, 20 | obs = obs 21 | ) 22 | } 23 | 24 | #' Method to plot a Plot element 25 | setMethod("plotElement",signature = "AnyPlot",definition = function(object){ 26 | eval(object@plot_element) 27 | }) 28 | #' Method to render a Plot Element 29 | setMethod("shinyElement",signature = "AnyPlot",definition = function(object){ 30 | renderPlot(plotElement(object)) 31 | }) 32 | 33 | 34 | 35 | server <- function(input, output, session) { 36 | 37 | # Create a reactive to create the Report object 38 | report_obj <- reactive(HistPlot(obs=input$obs)) 39 | 40 | # Check for change of the slider to change the plots 41 | observeEvent(input$obs,{ 42 | output$renderedPDF <- renderText("") 43 | output$renderPlot <- shinyElement( report_obj() ) 44 | } ) 45 | 46 | } 47 | 48 | # Simple shiny App containing the standard histogram + PDF render and Download button 49 | ui <- fluidPage( 50 | sidebarLayout( 51 | sidebarPanel( 52 | sliderInput("obs", "Number of observations:", min = 10, max = 500, value = 100) 53 | ), 54 | mainPanel( 55 | plotOutput("renderPlot") 56 | ) 57 | ) 58 | ) 59 | shinyApp(ui = ui, server = server) -------------------------------------------------------------------------------- /example_apps/app_stiff_report_multi.R: -------------------------------------------------------------------------------- 1 | server <- function(input, output) { 2 | # Output Gray Histogram 3 | output$distPlot <- renderPlot({ 4 | write(paste0("hist(rnorm(",input$obs,"), col = 'darkgray', border = 'white') evaluated"),file="app.log",append=TRUE) 5 | hist(rnorm(input$obs), col = 'darkgray', border = 'white') 6 | }) 7 | # Output Blue Histogram 8 | output$distPlot2 <- renderPlot({ 9 | write(paste0("hist(rnorm(",input$obs,"), col = 'blue', border = 'white') evaluated"),file="app.log",append=TRUE) 10 | hist(rnorm(input$obs), col = 'blue', border = 'white') 11 | }) 12 | # Output Blue Histogram 13 | output$scatterplot <- renderPlot({ 14 | write( 15 | paste0("plot(sample(",input$obs,"), sample(",input$obs,") evaluated"),file="app.log",append=TRUE) 16 | plot(sample(input$obs), sample(input$obs)) 17 | }) 18 | 19 | # Observe PDF button and create PDF 20 | observeEvent(input$"renderPDF",{ 21 | tryCatch({ 22 | pdf("test.pdf") 23 | hist(rnorm(input$obs), col = 'blue', border = 'white') 24 | hist(rnorm(input$obs), col = 'darkgray', border = 'white') 25 | plot(sample(input$obs), sample(input$obs)) 26 | dev.off() 27 | output$renderedPDF <- renderText("PDF rendered") 28 | },error=function(e){output$renderedPDF <- renderText("PDF could not be rendered")}) 29 | 30 | 31 | }) 32 | 33 | # Observe Download Button and return rendered PDF 34 | output$downloadPDF <- downloadHandler( 35 | filename = "test.pdf", 36 | content = function(file) { 37 | file.copy("test.pdf", file, overwrite = TRUE) 38 | } 39 | ) 40 | } 41 | 42 | # Simple shiny App containing the standard histogram + PDF render and Download button 43 | ui <- fluidPage( 44 | sidebarLayout( 45 | sidebarPanel( 46 | sliderInput("obs", "Number of observations:", min = 10, max = 500, value = 100), 47 | actionButton("renderPDF","Render a Report"), 48 | textOutput("renderedPDF"), 49 | downloadButton("downloadPDF","Get rendered PDF") 50 | ), 51 | mainPanel( 52 | plotOutput("distPlot"), 53 | plotOutput("distPlot2"), 54 | plotOutput("scatterplot") 55 | ) 56 | ) 57 | ) 58 | shinyApp(ui = ui, server = server) 59 | -------------------------------------------------------------------------------- /example_apps/app_object_report.R: -------------------------------------------------------------------------------- 1 | library(methods) 2 | library(stringr) 3 | library(rlang) 4 | library(glue) 5 | 6 | setGeneric("plotElement",where = parent.frame(),def = function(object){standardGeneric("plotElement")}) 7 | setGeneric("pdfElement",where = parent.frame(),def = function(object){standardGeneric("pdfElement")}) 8 | setGeneric("shinyElement",where = parent.frame(),def = function(object){standardGeneric("shinyElement")}) 9 | 10 | setClass("AnyPlot", representation(plot_element = "call")) 11 | setClass("HistPlot", representation(color="character",obs="numeric"), contains = "AnyPlot") 12 | setClass("Report",representation(plots="list",filename="character",obs="numeric",rendered="logical")) 13 | 14 | AnyPlot <- function(plot_element=expr(plot(1,1))){ 15 | new("AnyPlot", 16 | plot_element = plot_element 17 | ) 18 | } 19 | 20 | HistPlot <- function(color="darkgrey",obs=100){ 21 | new("HistPlot", 22 | plot_element = expr(hist(rnorm(!!obs), col = !!color, border = 'white')), 23 | color = color, 24 | obs = obs 25 | ) 26 | } 27 | 28 | #' Method to plot a Plot element 29 | setMethod("plotElement",signature = "AnyPlot",definition = function(object){ 30 | eval(object@plot_element) 31 | }) 32 | #' Method to render a Plot Element 33 | setMethod("shinyElement",signature = "AnyPlot",definition = function(object){ 34 | renderPlot(plotElement(object)) 35 | }) 36 | 37 | #' Method to generate a PDF Report from a Report Element 38 | setMethod("pdfElement",signature = "AnyPlot",definition = function(object){ 39 | pdf("test.pdf") 40 | plotElement(object) 41 | dev.off() 42 | }) 43 | 44 | 45 | server <- function(input, output) { 46 | 47 | # Create a reactive to create the HistPlot object 48 | report_obj <- reactive(HistPlot(obs=input$obs)) 49 | 50 | # Check for change of the slider to change the plots 51 | observeEvent(input$obs,{ 52 | output$renderedPDF <- renderText("") 53 | output$reportReport <- shinyElement( report_obj() ) 54 | } ) 55 | 56 | # Observe PDF button and create PDF 57 | observeEvent(input$"renderPDF",{ 58 | report <- pdfElement(report_obj()) 59 | output$renderedPDF <- renderText("PDF rendered") 60 | 61 | }) 62 | 63 | # Observe Download Button and return rendered PDF 64 | output$downloadPDF <- downloadHandler( 65 | filename = "test.pdf", 66 | content = function(file) { 67 | file.copy("test.pdf", file, overwrite = TRUE) 68 | } 69 | ) 70 | } 71 | 72 | # Simple shiny App containing the standard histogram + PDF render and Download button 73 | ui <- fluidPage( 74 | sidebarLayout( 75 | sidebarPanel( 76 | sliderInput("obs", "Number of observations:", min = 10, max = 500, value = 100), 77 | actionButton("renderPDF","Render a Report"), 78 | textOutput("renderedPDF"), 79 | downloadButton("downloadPDF","Get rendered PDF") 80 | ), 81 | mainPanel( 82 | plotOutput("reportReport") 83 | ) 84 | ) 85 | ) 86 | shinyApp(ui = ui, server = server) -------------------------------------------------------------------------------- /example_packaged/app.R: -------------------------------------------------------------------------------- 1 | library(devtools) 2 | 3 | # Derive the core package to have all basics inside 4 | devtools::load_all("./core") 5 | 6 | #- Read the plan functions -------------------------- 7 | 8 | #' Load an app Module Package 9 | #' @param xmlItem (\code{xmlNode}) 10 | #' @export 11 | #' 12 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 13 | #' 14 | load_module <- function(xmlItem){ 15 | # Load the desired module package 16 | devtools::load_all(paste0("./",xmlValue(xmlItem[["package"]]))) 17 | } 18 | 19 | #' Create a TabPanel from an XMLItem 20 | #' 21 | #' @param xmlItem (\code{xmlNode}) 22 | #' 23 | #' @export 24 | #' 25 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 26 | #' 27 | module_tab <- function(xmlItem){ 28 | # Return a shiny tabPanel for the package 29 | tabPanel(xmlValue(xmlItem[["name"]]), 30 | uiOutput(xmlValue(xmlItem[["id"]])) 31 | ) 32 | } 33 | 34 | server <- function(input,output){ 35 | 36 | # Derive the infos from the configuration and store it inside a list 37 | configuration <- xmlApply(xmlRoot(xmlParse("config.xml")),function(xmlItem){ 38 | load_module(xmlItem) 39 | 40 | # Append Tabs to the Reporting Window 41 | appendTab("modules",module_tab(xmlItem),select = TRUE) 42 | 43 | list( 44 | name = xmlValue(xmlItem[["name"]]), 45 | class = xmlValue(xmlItem[["class"]]), 46 | id = xmlValue(xmlItem[["id"]]) 47 | ) 48 | }) 49 | 50 | 51 | # Create a reactive to create the Report object due to 52 | # the chosen module 53 | report_obj <- reactive({ 54 | module <- unlist(lapply(configuration,function(x)x$name==input$modules)) 55 | if(!any(module))module <- c(TRUE,FALSE) 56 | do.call(configuration[[which(module)]][["class"]], 57 | args=list( 58 | obs = input$obs 59 | )) 60 | }) 61 | 62 | # Check for change of the slider/tab to re-calculate the report modules 63 | observeEvent({input$obs 64 | input$modules},{ 65 | 66 | # Clear all produced outputs 67 | output$renderedPDF <- renderText("") 68 | file.remove(list.files(pattern=".pdf")) 69 | 70 | # Derive chosen tab 71 | module <- unlist(lapply(configuration,function(x)x$name==input$modules)) 72 | if(!any(module))module <- c(TRUE,FALSE) 73 | 74 | # Re-render the output of the chosen tab 75 | output[[configuration[[which(module)]][["id"]]]] <- shinyElement( report_obj() ) 76 | }) 77 | 78 | # Observe PDF button and create PDF 79 | observeEvent(input$"renderPDF",{ 80 | 81 | # Create PDF 82 | report <- pdfElement(report_obj()) 83 | 84 | # If the PDF was successfully rendered update text message 85 | if(report@rendered){ 86 | output$renderedPDF <- renderText("PDF rendered") 87 | }else{ 88 | output$renderedPDF <- renderText("PDF could not be rendered") 89 | } 90 | }) 91 | 92 | # Observe Download Button and return rendered PDF 93 | output$downloadPDF <- 94 | downloadHandler( 95 | filename = report_obj()@filename, 96 | content = function(file) { 97 | file.copy( report_obj()@filename, file, overwrite = TRUE) 98 | } 99 | ) 100 | } 101 | 102 | # Simple shiny App containing the standard histogram + PDF render and Download button 103 | ui <- fluidPage( 104 | sidebarLayout( 105 | sidebarPanel( 106 | sliderInput("obs", "Number of observations:", min = 10, max = 500, value = 100), 107 | actionButton("renderPDF","Render a Report"), 108 | textOutput("renderedPDF"), 109 | downloadButton("downloadPDF","Get rendered PDF") 110 | ), 111 | mainPanel( 112 | tabsetPanel(id='modules') 113 | )#mainPanel 114 | )#sidebarlayout 115 | )#fluidPage 116 | 117 | 118 | shinyApp(ui = ui, server = server) -------------------------------------------------------------------------------- /example_apps/app_object_report_multi.R: -------------------------------------------------------------------------------- 1 | library(methods) 2 | library(stringr) 3 | library(rlang) 4 | library(glue) 5 | 6 | setGeneric("plotElement",where = parent.frame(),def = function(object){standardGeneric("plotElement")}) 7 | setGeneric("pdfElement",where = parent.frame(),def = function(object){standardGeneric("pdfElement")}) 8 | setGeneric("shinyElement",where = parent.frame(),def = function(object){standardGeneric("shinyElement")}) 9 | setGeneric("logElement",where = parent.frame(),def = function(object){standardGeneric("logElement")}) 10 | 11 | setClass("AnyPlot", representation(plot_element = "call")) 12 | setClass("HistPlot", representation(color="character",obs="numeric"), contains = "AnyPlot") 13 | setClass("ScatterPlot", representation(obs="numeric"), contains = "AnyPlot") 14 | setClass("Report",representation(plots="list",filename="character",obs="numeric",rendered="logical")) 15 | 16 | AnyPlot <- function(plot_element=expr(plot(1,1))){ 17 | new("AnyPlot", 18 | plot_element = plot_element 19 | ) 20 | } 21 | 22 | HistPlot <- function(color="darkgrey",obs=100){ 23 | new("HistPlot", 24 | plot_element = expr(hist(rnorm(!!obs), col = !!color, border = 'white')), 25 | color = color, 26 | obs = obs 27 | ) 28 | } 29 | ScatterPlot <- function(obs=100){ 30 | new("ScatterPlot", 31 | plot_element = expr(plot(sample(!!obs),sample(!!obs))), 32 | obs = obs 33 | ) 34 | } 35 | 36 | Report <- function(obs=100){ 37 | new("Report", 38 | plots = list( 39 | HistPlot(color="darkgrey", obs=obs), 40 | HistPlot(color="blue", obs=obs), 41 | ScatterPlot(obs=obs) 42 | ), 43 | filename="test.pdf", 44 | obs=obs, 45 | rendered=FALSE 46 | ) 47 | } 48 | 49 | 50 | #' Method to log a Plot Element 51 | setMethod("logElement",signature = "AnyPlot",definition = function(object){ 52 | # print(deparse(object@plot_element)) 53 | write(paste0(deparse(object@plot_element)," evaluated"),file="app.log",append=TRUE) 54 | }) 55 | #' Method to plot a Plot element 56 | setMethod("plotElement",signature = "AnyPlot",definition = function(object){ 57 | eval(object@plot_element) 58 | }) 59 | #' Method to render a Plot Element 60 | setMethod("shinyElement",signature = "AnyPlot",definition = function(object){ 61 | renderPlot(plotElement(object)) 62 | }) 63 | 64 | #' Method to generate a PDF Report from a Report Element 65 | setMethod("pdfElement",signature = "Report",definition = function(object){ 66 | tryCatch({ 67 | pdf(object@filename) 68 | lapply(object@plots,function(x){ 69 | plotElement(x) 70 | }) 71 | dev.off() 72 | object@rendered <- TRUE 73 | },error=function(e){warning("plot not rendered")#do nothing 74 | }) 75 | return(object) 76 | }) 77 | 78 | #' Tell how to generate the shiny Element for a report 79 | setMethod("shinyElement",signature = "Report",definition = function(object){ 80 | renderUI({ 81 | lapply(object@plots, 82 | function(x){ 83 | logElement(x) 84 | shinyElement(x) 85 | }) 86 | }) 87 | }) 88 | 89 | 90 | server <- function(input, output, session) { 91 | 92 | # Create a reactive to create the Report object 93 | report_obj <- reactive(Report(obs=input$obs)) 94 | 95 | # Check for change of the slider to change the plots 96 | observeEvent(input$obs,{ 97 | output$renderedPDF <- renderText("") 98 | output$reportReport <- shinyElement( report_obj() ) 99 | } ) 100 | 101 | # Observe PDF button and create PDF 102 | observeEvent(input$"renderPDF",{ 103 | report <- pdfElement(report_obj()) 104 | if(report@rendered){ 105 | output$renderedPDF <- renderText("PDF rendered") 106 | }else{ 107 | output$renderedPDF <- renderText("PDF could not be rendered") 108 | } 109 | }) 110 | 111 | # Observe Download Button and return rendered PDF 112 | output$downloadPDF <- downloadHandler( 113 | filename = report_obj()@filename, 114 | content = function(file) { 115 | file.copy(report_obj()@filename, file, overwrite = TRUE) 116 | } 117 | ) 118 | } 119 | 120 | # Simple shiny App containing the standard histogram + PDF render and Download button 121 | ui <- fluidPage( 122 | sidebarLayout( 123 | sidebarPanel( 124 | sliderInput("obs", "Number of observations:", min = 10, max = 500, value = 100), 125 | actionButton("renderPDF","Render a Report"), 126 | textOutput("renderedPDF"), 127 | downloadButton("downloadPDF","Get rendered PDF") 128 | ), 129 | mainPanel( 130 | uiOutput("reportReport") 131 | ) 132 | ) 133 | ) 134 | shinyApp(ui = ui, server = server) -------------------------------------------------------------------------------- /example_packaged/core/R/core.R: -------------------------------------------------------------------------------- 1 | # Define Generics --------------------------------------------------------------------------------- 2 | setGeneric("evalElement",def = function(object){standardGeneric("evalElement")}) 3 | setGeneric("pdfElement",def = function(object){standardGeneric("pdfElement")}) 4 | setGeneric("shinyElement",def = function(object){standardGeneric("shinyElement")}) 5 | setGeneric("logElement",def = function(object){standardGeneric("logElement")}) 6 | 7 | # Define Report and Plot Classes -------------------------------------------------------------------- 8 | 9 | 10 | #' Report Module for apps 11 | #' 12 | #' @slot plots (\code{list}) A list of \link{AnyPlot} objects 13 | #' @slot filename (\code{character}) The name of the PDF to output 14 | #' @slot obs (\code{numeric}) The number of observations to be used to generate the report 15 | #' @slot rendered (\code{logical}) Whether the PDF was rendered or not 16 | #' 17 | #' @export 18 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 19 | setClass("Report", 20 | representation( 21 | plots="list", 22 | filename="character", 23 | obs="numeric", 24 | rendered="logical")) 25 | 26 | # Report Methods ------------------------------------------------------------ 27 | 28 | #' Method to generate a PDF Report from a Report Element 29 | #' 30 | #' This function generates a PDF with all elements of the report 31 | #' and stores it on the drive. The PDF will have the name of the 32 | #' \code{filename} slot of the object. 33 | #' 34 | #' @param object \code{Report} An object 35 | #' 36 | #' @return object \code{Report} with a changed \code{rendered} slot 37 | #' @export 38 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 39 | setMethod("pdfElement",signature = "Report",definition = function(object){ 40 | tryCatch({ 41 | pdf(object@filename) 42 | lapply(object@plots,function(x){ 43 | pdfElement(x) 44 | }) 45 | dev.off() 46 | object@rendered <- TRUE 47 | },error=function(e){warning("plot not rendered")#do nothing 48 | }) 49 | return(object) 50 | }) 51 | 52 | #' Method to generate the shiny Element for a Report 53 | #' 54 | #' Log the call of each object inside the \code{plots} slot and additionally render 55 | #' it into the shiny output 56 | #' 57 | #' @param object \code{Report} An object 58 | #' 59 | #' @return The output of a \link[shiny]{renderUI} output of shiny 60 | #' 61 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 62 | #' @export 63 | setMethod("shinyElement",signature = "Report",definition = function(object){ 64 | renderUI({ 65 | lapply(object@plots, 66 | function(x){ 67 | logElement(x) 68 | shinyElement(x) 69 | }) 70 | }) 71 | }) 72 | 73 | #- Anyplot class ---------------------------------------------------------------- 74 | 75 | #' Plot Module for apps 76 | #' 77 | #' @slot plot_element (\code{call}) A call that can upon \code{eval} return a plot element 78 | #' 79 | #' @export 80 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 81 | setClass("AnyPlot", representation(plot_element="call")) 82 | 83 | 84 | # Constructors of AnyPlot classes --------------------------------------------------------------------------- 85 | 86 | #' Construct the AnyPlot class 87 | #' @param plot_element (\code{call}) A call returning a plot 88 | #' @export 89 | #' @return \code{AnyPlot} an object of class AnyPlot 90 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 91 | AnyPlot <- function(plot_element=expr(plot(1,1))){ 92 | new("AnyPlot", 93 | plot_element = plot_element 94 | ) 95 | } 96 | 97 | # Plot Methods ------------------------------------------------------------ 98 | 99 | #' Method to log a Plot Element 100 | #' 101 | #' @param object \code{AnyPlot} An object 102 | #' 103 | #' @return nothing is returned, the call of the object is written into a logfile 104 | #' @export 105 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 106 | setMethod("logElement",signature = "AnyPlot",definition = function(object){ 107 | write(paste0(deparse(object@plot_element)," evaluated"), file="app.log",append=TRUE) 108 | }) 109 | 110 | #' Method to plot a Plot element 111 | #' 112 | #' @param object \code{AnyPlot} An object 113 | #' 114 | #' @return A plot object, fully rendered 115 | #' @export 116 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 117 | setMethod("evalElement",signature = "AnyPlot",definition = function(object){ 118 | eval(object@plot_element) 119 | }) 120 | #' Method to return a PDF element 121 | #' @param object \code{AnyPlot} An object 122 | #' 123 | #' @return Same call as \link{evalElement} 124 | #' @export 125 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 126 | setMethod("pdfElement",signature = "AnyPlot",definition = function(object){ 127 | evalElement(object) 128 | }) 129 | 130 | #' Method to shiny output a Plot 131 | #' 132 | #' @param object \code{AnyPlot} An object 133 | #' 134 | #' @return A Shiny element created by \link[shiny]{renderPlot} 135 | #' 136 | #' @author Sebastian Wolf (\email{zappingseb@@gmail.com}) 137 | #' @export 138 | setMethod("shinyElement",signature = "AnyPlot",definition = function(object){ 139 | renderPlot(evalElement(object)) 140 | }) 141 | 142 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to Build a Shiny "Truck"! 3 | author: Sebastian Wolf 4 | date: '2018-08-28' 5 | slug: how-to-build-shiny-trucks-not-shiny-cars 6 | categories: 7 | - R Language 8 | - Shiny 9 | tags: 10 | - R Language 11 | - Shiny 12 | summary: '' 13 | draft: yes 14 | output: md_document 15 | --- 16 | 17 | ```{r setup, include = FALSE} 18 | # packages required for this post 19 | for (pkg in c('methods', 'rlang')) 20 | if (!requireNamespace(pkg)) install.packages(pkg) 21 | 22 | knitr::opts_chunk$set(echo=TRUE) 23 | ``` 24 | 25 | 26 | ## Why is this about trucks? 27 | 28 | Last month, at the [R/Pharma](https://www.rinpharma.com) conference that took place on the Harvard Campus, I presented bioWARP, a large [Shiny](https://shiny.rstudio.com/) application containing more than 500,000 lines of code. Although several other Shiny apps were presented at the conference, I noticed that none of them came close to being as big as bioWARP. And I asked myself, why? 29 | 30 | I concluded that most people just don't need to built them that big! So now, I would like to explain why we needed such a large app and how we went about building it. 31 | 32 | 33 | 34 |
35 | To give you an idea of the scale I am talking about an automotive methaphor might be useful. A typical Shiny app I see in my daily work has about 50 or even less interaction items. Let's imagine this as a car. With less than 50 interactions think of a small car like a mini cooper. Compared to these applications, with more than 500 interactions, bioWARP is a truck, maybe even a "monster" truck. So why do my customers want to drive trucks when everyone else is driving cars? 36 | 37 | 38 | Images by [Paul V](https://flic.kr/p/B4TwtZ) and [DaveR](https://flic.kr/p/q33yzD) 39 |
40 |
41 | Peterbilt Truck 42 | 43 | Red Mini Cooper 44 | 45 |
46 |
47 | 48 | ## Why do we need a truck? 49 | 50 | Building software often starts with checking the user requirements. So when we started the development of our statistical web application, we did that, too. Asking a lot of people inside our department we noticed, that the list of requirements was huge: 51 | 52 | **Main user requirements** 53 | 54 | * Pretty Design which works universally 55 | * Interactive elements 56 | * Mathematical correctness of all results 57 | 58 | **Main application features** 59 | 60 | * Session logging 61 | * Standardized PDF reports of all results 62 | * Ability to restore sessions 63 | * Harmonize it with other software applications 64 | * Everything has to be tested 65 | * Help pages 66 | 67 | More requirements came then from all the analysis people perform on daily basis. They wanted to have some tasks integrated into our app: 68 | 69 | **Mathematical tasks** 70 | 71 | * Linear regression app 72 | * Descriptive statistics app 73 | * Homogeneity test app 74 | * T-Test app 75 | * Bootstrap simulation app 76 | * Sensitivity/Specificity app 77 | * Linearity app 78 | * Clustering app 79 | * BoxPlotting app 80 | 81 | Additionally it was required to write the whole application in **R** as all our mathematical packages are written in **R**. So we decided for doing it all with [shiny](https://shiny.rstudio.com/) because it already covers 2 of the 3 main user requirements, being pretty and being interactive. 82 | 83 | ## How did we build the truck? 84 | 85 | ### Modularity + Standardization 86 | 87 | Inside our department we were running some large scale desktop applications already. When it came to testing we always noticed, that testing takes forever. If one single software gathers data, calculates statistics, provides plot outputs and renders PDF reports, this is a huge truck and you can just test it by driving it a thousand miles and see if it still works. The idea we came up with was building our truck out of Lego bricks. Each Lego brick can be tested on its own. If a Lego wheel runs, the truck will run. The wheel holder part is universal and if we change the size of the wheels, we can still run the truck, in case each wheel was tested. What this is called, is modularity. There exist different solutions in R and shiny which can be combined to make things modular: 88 | 89 | 1) Shiny Modules 90 | 2) Object orientation 91 | 3) R-packages 92 | 4) clever namespacing 93 | 94 | As Shiny modules were not existing when we started, we chose option 2 and 3. 95 | 96 | As an example, I'll compare two simple Shiny apps representing two cars here. One is written using object orientation, one as a simple Shiny application. The image below shall illustrate, that the `renderPlot` function in a standard shiny app includes a plot, in this case using the `hist` function. So whenever you add a new plot, its function has to be called inside. 97 | 98 | In the object oriented app the `renderPlot` function calls the `shinyElement` method of a generic plot object we created and called `AnyPlot`. The fist advantage is that plot can easily be exchanged. (Please look into the code if you wonder if this really is so.) To describe that advantage, you can imagine a normal car, built of car parts. Our car is really a a Lego car, using even smaller **standardized** parts (Lego bricks), to construct each part of the car. So instead of the grille made of one piece of steal, we constructed it of many little grey Lego bricks. Changing the grille for an update of the car does not need to reconstruct the whole front. Just use green bricks instead of grey bricks e.g. They should have the same shape. 99 | 100 | By going into the code of the two applications, you see there is a straight forward disadvantage of object orientation. There is much more code. We have to define what a Lego brick is and what features it shall have. 101 | 102 | ![](apps.png) 103 | 104 | [Object oriented shiny app](https://github.com/zappingseb/biowarptruck/blob/master/example_apps/app_object.R) 105 | ```{r, echo=TRUE,eval=F} 106 | library(methods) 107 | library(rlang) 108 | 109 | 110 | setGeneric("plotElement",where = parent.frame(),def = function(object){standardGeneric("plotElement")}) 111 | setGeneric("shinyElement",where = parent.frame(),def = function(object){standardGeneric("shinyElement")}) 112 | 113 | setClass("AnyPlot", representation(plot_element = "call")) 114 | setClass("HistPlot", representation(color="character",obs="numeric"), contains = "AnyPlot") 115 | 116 | AnyPlot <- function(plot_element=expr(plot(1,1))){ 117 | new("AnyPlot", 118 | plot_element = plot_element 119 | ) 120 | } 121 | 122 | HistPlot <- function(color="darkgrey",obs=100){ 123 | new("HistPlot", 124 | plot_element = expr(hist(rnorm(!!obs), col = !!color, border = 'white')), 125 | color = color, 126 | obs = obs 127 | ) 128 | } 129 | 130 | #' Method to plot a Plot element 131 | setMethod("plotElement",signature = "AnyPlot",definition = function(object){ 132 | eval(object@plot_element) 133 | }) 134 | #' Method to render a Plot Element 135 | setMethod("shinyElement",signature = "AnyPlot",definition = function(object){ 136 | renderPlot(plotElement(object)) 137 | }) 138 | 139 | 140 | 141 | server <- function(input, output, session) { 142 | 143 | # Create a reactive to create the Report object 144 | report_obj <- reactive(HistPlot(obs=input$obs)) 145 | 146 | # Check for change of the slider to change the plots 147 | observeEvent(input$obs,{ 148 | output$renderedPDF <- renderText("") 149 | output$renderPlot <- shinyElement( report_obj() ) 150 | } ) 151 | 152 | } 153 | 154 | # Simple shiny App containing the standard histogram + PDF render and Download button 155 | ui <- fluidPage( 156 | sidebarLayout( 157 | sidebarPanel( 158 | sliderInput( 159 | "obs", 160 | "Number of observations:", min = 10, max = 500, value = 100) 161 | ), 162 | mainPanel( 163 | plotOutput("renderPlot") 164 | ) 165 | ) 166 | ) 167 | shinyApp(ui = ui, server = server) 168 | ``` 169 | 170 | [Standard shiny app](https://github.com/zappingseb/biowarptruck/blob/master/example_apps/app_stiff.R) 171 | ```{r, echo=TRUE,eval=F} 172 | 173 | server <- function(input, output) { 174 | # Output Gray Histogram 175 | output$distPlot <- renderPlot({ 176 | hist(rnorm(input$obs), col = 'darkgray', border = 'white') 177 | }) 178 | 179 | } 180 | 181 | # Simple shiny App containing the standard histogram + PDF render and Download button 182 | ui <- fluidPage( 183 | sidebarLayout( 184 | sidebarPanel( 185 | sliderInput( 186 | "obs", 187 | "Number of observations:", min = 10, max = 500, value = 100) 188 | ), 189 | mainPanel( 190 | plotOutput("distPlot") 191 | ) 192 | ) 193 | ) 194 | shinyApp(ui = ui, server = server) 195 | ``` 196 | 197 | 198 | But an advantage of the object orientation is that you can now output the plot in a lot of different formats. We solved this by introducing methods called `pdfElement`, 199 | `logElement` or `archiveElement`. To get a deeper look you can check out some examples stored on [github](https://github.com/zappingseb/biowarptruck/tree/master/example_apps). These show differences between object oriented and standard [shiny](https://shiny.rstudio.com/) apps. You can see that duplicated code is reduced in object oriented apps, additionally the code of the [shiny](https://shiny.rstudio.com/) app itself does not change for object oriented apps. But the code constructing the objects shown on the page changes. While for the standard apps the [shiny](https://shiny.rstudio.com/) code itself also changes everytime an element is updated. 200 | 201 | The main advantage of this approach is, that you can keep your [shiny](https://shiny.rstudio.com/) app exactly the same whatever it calculates or whatever it reports. Inside our department this meant, whenever somebody wants a different plot inside an app, we do not have to touch our main app again. Whenever somebody wanted to change just the linear regression app, we did not have to touch other apps. The look and feel, the logging, the PDF report, stays exactly the same. Those 3 functionalities shall never be touched in case no update of those were needed. 202 | 203 | ### Packaging 204 | 205 | As you know we did not build a singular app, we had to build many for the different mathematical analysis. So we decided for each app we will construct a separate R-package. This means we had to define one Class that defines what an app will look like in a *core*-package. This can be seen as the Lego theme. So our app whould be Lego city, where you have trucks and cars. Other apps may be more advanced and range inside Lego Technic. 206 | 207 | Now each contributer to our shiny app build a package that contains a child of our *core* class. We called this class **Module**. So we got a lot of **Module**-packages. This is not a [shiny-module](https://shiny.rstudio.com/articles/modules.html), but it's modular. Our app now allows bringing together a lot of those modules and making it bigger and bigger and bigger. It get's more *HP* and I wouldn't call it a car anymore. Yeah, we have a truck! Made of Lego bricks! 208 | 209 | truck peterbilt 210 | 211 | [Image by Barney Sharman](https://www.flickr.com/photos/157267479@N02/27368344058/in/photostream/) 212 | 213 | The modularization and packaging now enables fast testing. Why? Each package can be tested using basic [testthat](https://github.com/r-lib/testthat) functionalities. So first we tested our *core* application package, that allows adding building blocks. Afterwards we tested each single package on its own. Finally, the whole application is tested. Our truck is ready to roll. Upon updates, we do not have to test the whole truck again. If we want to have larger tires, we just update the tire package, but not the *core*-package or any other packages. 214 | 215 | ### Config files 216 | 217 | The truck is made of bricks, actually the same bricks we used to build the car. Just many more of them. Now the hard part is putting them all together and not losing track. 218 | 219 | We are dealing with many the different **Modules** that we were writing. Each 220 | **Module** comes in one package. The main issue we had was that we wanted all apps to be deeply tested. During development of course not all apps were tested right away, so we had to give them a tag (tested yes/no). Additionally some apps required help pages, others don't. Some apps came with example data sets, some don't. Some apps had a nice title in them already, for some it shall be easy to configure. For each **Module** we'll also have to source `js` and `css` files, which we allowed to be additionally added for each app. The folder where to source them shall be chosen by the app author. We wanted to provide as much flexibility as possible while keeping our standards for Lego bricks (Look&Feel, logging, plotting and reporting). A simple example for such an app can be found on [github](https://github.com/zappingseb/biowarptruck/tree/master/example_packaged). 221 | 222 | We came up with the idea of config `XML` files. So the XML file contains all the information needed to tell what needs to be set for each **Module**. An example XML is given below which you can see as the LEGO manual. These small configurations allow managing the apps. We also build an `XML` that allows the apps to use features of what we call *core*-package. This `XML` file is rather difficult to set up. But imagine it tells which Plot shall be logged, which input shall be used and which plots shall go into the PDF report. It allows fast development while sticking to standards. 223 | 224 |
225 |
drawing 226 |
from LegoBrickinstructions.com
227 |
228 | ```{XML} 229 | 230 | modulepackage1 231 | modulepackage1_Module 232 | Great BoxPlot Module 233 | GBM 234 | . 235 | 236 | help/index.html 237 | 238 | help/about.html 239 | 240 | 241 | 242 | 243 | 244 | 245 | ``` 246 | 247 |
248 | 249 | Inside the config file you can clearly see that now the title of the app and the location of help pages, example data sets is given. Even the name of the class that describes the **Module** is given. This allows us to rapidly add modules to our main app environment. 250 | 251 | At the end our truck is made of many parts, that all increase its power and strength. As we now have around 16 modules in our real (in production) app and each has between 20 and 50 inputs, the truck has 500 inputs. All which look similar and can be used to produced standardized PDF reports. The truck can even become a monster truck and thanks to the config files will still be easy to manage. 252 | 253 | ## My message to shiny.car and shiny.truck developers 254 | 255 | 1. Please do not start building a car until you know how many parts it will have at the end. Always consider it 256 | might become a truck. At first, always define your requirements. 257 | 2. Use modularization! Use [shiny modules](https://shiny.rstudio.com/articles/modules.html) or inheritance provided by object orientation ( [s4](http://adv-r.had.co.nz/S4.html) or [s6](https://cran.r-project.org/web/packages/R6/vignettes/Introduction.html) ). Both keep you from changing a lot of code on minor changes in requirements. 258 | 3. Use standardization! Try to have all your inputs and outputs as standardized as possible. If you use simple output bricks it's easy to output them in your preferred format. Features like logging, PDF reporting or even testing will be way easier with standardized elements. Standardized inputs allow your users to be comfortable with new apps way faster. 259 | 4. Don't build real trucks, build Lego trucks. 260 | 261 | 262 |
263 | Peterbilt Truck 264 | 265 | Red Mini Cooper 266 | 267 |
268 |
269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Why is this about trucks? 2 | ------------------------- 3 | 4 | Last month, at the [R/Pharma](https://www.rinpharma.com) conference that 5 | took place on the Harvard Campus, I presented bioWARP, a large 6 | [Shiny](https://shiny.rstudio.com/) application containing more than 7 | 500,000 lines of code. Although several other Shiny apps were presented 8 | at the conference, I noticed that none of them came close to being as 9 | big as bioWARP. And I asked myself, why? 10 | 11 | I concluded that most people just don’t need to built them that big! So 12 | now, I would like to explain why we needed such a large app and how we 13 | went about building it. 14 | 15 | To give you an idea of the scale I am talking about an automotive 16 | methaphor might be useful. A typical Shiny app I see in my daily work 17 | has about 50 or even less interaction items. Let’s imagine this as a 18 | car. With less than 50 interactions think of a small car like a mini 19 | cooper. Compared to these applications, with more than 500 interactions, 20 | bioWARP is a truck, maybe even a “monster” truck. So why do my customers 21 | want to drive trucks when everyone else is driving cars? 22 | 23 | Images by [Paul V](https://flic.kr/p/B4TwtZ) and 24 | [DaveR](https://flic.kr/p/q33yzD) 25 | 26 | Peterbilt Truck 27 | 28 | Red Mini Cooper 29 | 30 | 31 | Why do we need a truck? 32 | ----------------------- 33 | 34 | Building software often starts with checking the user requirements. So 35 | when we started the development of our statistical web application, we 36 | did that, too. Asking a lot of people inside our department we noticed, 37 | that the list of requirements was huge: 38 | 39 | **Main user requirements** 40 | 41 | - Pretty Design which works universally 42 | - Interactive elements 43 | - Mathematical correctness of all results 44 | 45 | **Main application features** 46 | 47 | - Session logging 48 | - Standardized PDF reports of all results 49 | - Ability to restore sessions 50 | - Harmonize it with other software applications 51 | - Everything has to be tested 52 | - Help pages 53 | 54 | More requirements came then from all the analysis people perform on 55 | daily basis. They wanted to have some tasks integrated into our app: 56 | 57 | **Mathematical tasks** 58 | 59 | - Linear regression app 60 | - Descriptive statistics app 61 | - Homogeneity test app 62 | - T-Test app 63 | - Bootstrap simulation app 64 | - Sensitivity/Specificity app 65 | - Linearity app 66 | - Clustering app 67 | - BoxPlotting app 68 | 69 | Additionally it was required to write the whole application in **R** as 70 | all our mathematical packages are written in **R**. So we decided for 71 | doing it all with [shiny](https://shiny.rstudio.com/) because it already 72 | covers 2 of the 3 main user requirements, being pretty and being 73 | interactive. 74 | 75 | How did we build the truck? 76 | --------------------------- 77 | 78 | ### Modularity + Standardization 79 | 80 | Inside our department we were running some large scale desktop 81 | applications already. When it came to testing we always noticed, that 82 | testing takes forever. If one single software gathers data, calculates 83 | statistics, provides plot outputs and renders PDF reports, this is a 84 | huge truck and you can just test it by driving it a thousand miles and 85 | see if it still works. The idea we came up with was building our truck 86 | out of Lego bricks. Each Lego brick can be tested on its own. If a Lego 87 | wheel runs, the truck will run. The wheel holder part is universal and 88 | if we change the size of the wheels, we can still run the truck, in case 89 | each wheel was tested. What this is called, is modularity. There exist 90 | different solutions in R and shiny which can be combined to make things 91 | modular: 92 | 93 | 1. Shiny Modules 94 | 2. Object orientation 95 | 3. R-packages 96 | 4. clever namespacing 97 | 98 | As Shiny modules were not existing when we started, we chose option 2 99 | and 3. 100 | 101 | As an example, I’ll compare two simple Shiny apps representing two cars 102 | here. One is written using object orientation, one as a simple Shiny 103 | application. The image below shall illustrate, that the `renderPlot` 104 | function in a standard shiny app includes a plot, in this case using the 105 | `hist` function. So whenever you add a new plot, its function has to be 106 | called inside. 107 | 108 | In the object oriented app the `renderPlot` function calls the 109 | `shinyElement` method of a generic plot object we created and called 110 | `AnyPlot`. The fist advantage is that plot can easily be exchanged. 111 | (Please look into the code if you wonder if this really is so.) To 112 | describe that advantage, you can imagine a normal car, built of car 113 | parts. Our car is really a a Lego car, using even smaller 114 | **standardized** parts (Lego bricks), to construct each part of the car. 115 | So instead of the grille made of one piece of steal, we constructed it 116 | of many little grey Lego bricks. Changing the grille for an update of 117 | the car does not need to reconstruct the whole front. Just use green 118 | bricks instead of grey bricks e.g. They should have the same shape. 119 | 120 | By going into the code of the two applications, you see there is a 121 | straight forward disadvantage of object orientation. There is much more 122 | code. We have to define what a Lego brick is and what features it shall 123 | have. 124 | 125 | ![](apps.png) 126 | 127 | [Object oriented shiny 128 | app](https://github.com/zappingseb/biowarptruck/blob/master/example_apps/app_object.R) 129 | 130 | library(methods) 131 | library(rlang) 132 | 133 | 134 | setGeneric("plotElement",where = parent.frame(),def = function(object){standardGeneric("plotElement")}) 135 | setGeneric("shinyElement",where = parent.frame(),def = function(object){standardGeneric("shinyElement")}) 136 | 137 | setClass("AnyPlot", representation(plot_element = "call")) 138 | setClass("HistPlot", representation(color="character",obs="numeric"), contains = "AnyPlot") 139 | 140 | AnyPlot <- function(plot_element=expr(plot(1,1))){ 141 | new("AnyPlot", 142 | plot_element = plot_element 143 | ) 144 | } 145 | 146 | HistPlot <- function(color="darkgrey",obs=100){ 147 | new("HistPlot", 148 | plot_element = expr(hist(rnorm(!!obs), col = !!color, border = 'white')), 149 | color = color, 150 | obs = obs 151 | ) 152 | } 153 | 154 | #' Method to plot a Plot element 155 | setMethod("plotElement",signature = "AnyPlot",definition = function(object){ 156 | eval(object@plot_element) 157 | }) 158 | #' Method to render a Plot Element 159 | setMethod("shinyElement",signature = "AnyPlot",definition = function(object){ 160 | renderPlot(plotElement(object)) 161 | }) 162 | 163 | 164 | 165 | server <- function(input, output, session) { 166 | 167 | # Create a reactive to create the Report object 168 | report_obj <- reactive(HistPlot(obs=input$obs)) 169 | 170 | # Check for change of the slider to change the plots 171 | observeEvent(input$obs,{ 172 | output$renderedPDF <- renderText("") 173 | output$renderPlot <- shinyElement( report_obj() ) 174 | } ) 175 | 176 | } 177 | 178 | # Simple shiny App containing the standard histogram + PDF render and Download button 179 | ui <- fluidPage( 180 | sidebarLayout( 181 | sidebarPanel( 182 | sliderInput( 183 | "obs", 184 | "Number of observations:", min = 10, max = 500, value = 100) 185 | ), 186 | mainPanel( 187 | plotOutput("renderPlot") 188 | ) 189 | ) 190 | ) 191 | shinyApp(ui = ui, server = server) 192 | 193 | [Standard shiny 194 | app](https://github.com/zappingseb/biowarptruck/blob/master/example_apps/app_stiff.R) 195 | 196 | server <- function(input, output) { 197 | # Output Gray Histogram 198 | output$distPlot <- renderPlot({ 199 | hist(rnorm(input$obs), col = 'darkgray', border = 'white') 200 | }) 201 | 202 | } 203 | 204 | # Simple shiny App containing the standard histogram + PDF render and Download button 205 | ui <- fluidPage( 206 | sidebarLayout( 207 | sidebarPanel( 208 | sliderInput( 209 | "obs", 210 | "Number of observations:", min = 10, max = 500, value = 100) 211 | ), 212 | mainPanel( 213 | plotOutput("distPlot") 214 | ) 215 | ) 216 | ) 217 | shinyApp(ui = ui, server = server) 218 | 219 | But an advantage of the object orientation is that you can now output 220 | the plot in a lot of different formats. We solved this by introducing 221 | methods called `pdfElement`, `logElement` or `archiveElement`. To get a 222 | deeper look you can check out some examples stored on 223 | [github](https://github.com/zappingseb/biowarptruck/tree/master/example_apps). 224 | These show differences between object oriented and standard 225 | [shiny](https://shiny.rstudio.com/) apps. You can see that duplicated 226 | code is reduced in object oriented apps, additionally the code of the 227 | [shiny](https://shiny.rstudio.com/) app itself does not change for 228 | object oriented apps. But the code constructing the objects shown on the 229 | page changes. While for the standard apps the 230 | [shiny](https://shiny.rstudio.com/) code itself also changes everytime 231 | an element is updated. 232 | 233 | The main advantage of this approach is, that you can keep your 234 | [shiny](https://shiny.rstudio.com/) app exactly the same whatever it 235 | calculates or whatever it reports. Inside our department this meant, 236 | whenever somebody wants a different plot inside an app, we do not have 237 | to touch our main app again. Whenever somebody wanted to change just the 238 | linear regression app, we did not have to touch other apps. The look and 239 | feel, the logging, the PDF report, stays exactly the same. Those 3 240 | functionalities shall never be touched in case no update of those were 241 | needed. 242 | 243 | ### Packaging 244 | 245 | As you know we did not build a singular app, we had to build many for 246 | the different mathematical analysis. So we decided for each app we will 247 | construct a separate R-package. This means we had to define one Class 248 | that defines what an app will look like in a *core*-package. This can be 249 | seen as the Lego theme. So our app whould be Lego city, where you have 250 | trucks and cars. Other apps may be more advanced and range inside Lego 251 | Technic. 252 | 253 | Now each contributer to our shiny app build a package that contains a 254 | child of our *core* class. We called this class **Module**. So we got a 255 | lot of **Module**-packages. This is not a 256 | [shiny-module](https://shiny.rstudio.com/articles/modules.html), but 257 | it’s modular. Our app now allows bringing together a lot of those 258 | modules and making it bigger and bigger and bigger. It get’s more *HP* 259 | and I wouldn’t call it a car anymore. Yeah, we have a truck! Made of 260 | Lego bricks! 261 | 262 | truck peterbilt 263 | 264 | [Image by Barney 265 | Sharman](https://www.flickr.com/photos/157267479@N02/27368344058/in/photostream/) 266 | 267 | The modularization and packaging now enables fast testing. Why? Each 268 | package can be tested using basic 269 | [testthat](https://github.com/r-lib/testthat) functionalities. So first 270 | we tested our *core* application package, that allows adding building 271 | blocks. Afterwards we tested each single package on its own. Finally, 272 | the whole application is tested. Our truck is ready to roll. Upon 273 | updates, we do not have to test the whole truck again. If we want to 274 | have larger tires, we just update the tire package, but not the 275 | *core*-package or any other packages. 276 | 277 | ### Config files 278 | 279 | The truck is made of bricks, actually the same bricks we used to build 280 | the car. Just many more of them. Now the hard part is putting them all 281 | together and not losing track. 282 | 283 | We are dealing with many the different **Modules** that we were writing. 284 | Each **Module** comes in one package. The main issue we had was that we 285 | wanted all apps to be deeply tested. During development of course not 286 | all apps were tested right away, so we had to give them a tag (tested 287 | yes/no). Additionally some apps required help pages, others don’t. Some 288 | apps came with example data sets, some don’t. Some apps had a nice title 289 | in them already, for some it shall be easy to configure. For each 290 | **Module** we’ll also have to source `js` and `css` files, which we 291 | allowed to be additionally added for each app. The folder where to 292 | source them shall be chosen by the app author. We wanted to provide as 293 | much flexibility as possible while keeping our standards for Lego bricks 294 | (Look&Feel, logging, plotting and reporting). A simple example for such 295 | an app can be found on 296 | [github](https://github.com/zappingseb/biowarptruck/tree/master/example_packaged). 297 | 298 | We came up with the idea of config `XML` files. So the XML file contains 299 | all the information needed to tell what needs to be set for each 300 | **Module**. An example XML is given below which you can see as the LEGO 301 | manual. These small configurations allow managing the apps. We also 302 | build an `XML` that allows the apps to use features of what we call 303 | *core*-package. This `XML` file is rather difficult to set up. But 304 | imagine it tells which Plot shall be logged, which input shall be used 305 | and which plots shall go into the PDF report. It allows fast development 306 | while sticking to standards. 307 | 308 | drawing 309 |
from LegoBrickinstructions.com
310 | 311 | 312 | modulepackage1 313 | modulepackage1_Module 314 | Great BoxPlot Module 315 | GBM 316 | . 317 | 318 | help/index.html 319 | 320 | help/about.html 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | Inside the config file you can clearly see that now the title of the app 329 | and the location of help pages, example data sets is given. Even the 330 | name of the class that describes the **Module** is given. This allows us 331 | to rapidly add modules to our main app environment. 332 | 333 | At the end our truck is made of many parts, that all increase its power 334 | and strength. As we now have around 16 modules in our real (in 335 | production) app and each has between 20 and 50 inputs, the truck has 500 336 | inputs. All which look similar and can be used to produced standardized 337 | PDF reports. The truck can even become a monster truck and thanks to the 338 | config files will still be easy to manage. 339 | 340 | My message to shiny.car and shiny.truck developers 341 | -------------------------------------------------- 342 | 343 | 1. Please do not start building a car until you know how many parts it 344 | will have at the end. Always consider it might become a truck. At 345 | first, always define your requirements. 346 | 2. Use modularization! Use [shiny 347 | modules](https://shiny.rstudio.com/articles/modules.html) or 348 | inheritance provided by object orientation ( 349 | [s4](http://adv-r.had.co.nz/S4.html) or 350 | [s6](https://cran.r-project.org/web/packages/R6/vignettes/Introduction.html) 351 | ). Both keep you from changing a lot of code on minor changes in 352 | requirements. 353 | 3. Use standardization! Try to have all your inputs and outputs as 354 | standardized as possible. If you use simple output bricks it’s easy 355 | to output them in your preferred format. Features like logging, PDF 356 | reporting or even testing will be way easier with standardized 357 | elements. Standardized inputs allow your users to be comfortable 358 | with new apps way faster. 359 | 4. Don’t build real trucks, build Lego trucks. 360 | 361 | Peterbilt Truck 362 | 363 | 364 | Red Mini Cooper 365 | 366 | --------------------------------------------------------------------------------