How to Generate Word Docs in Shiny with officer, flextable, and shinyglide

Reading time:
time
min
By:
Ivan Millanes
November 10, 2022

Sometimes, when you develop a Shiny application you want to download the results of their analysis as a report. Generating reports from Shiny applications is a very broad topic. So in this tutorial, we'll focus on generating Word documents from table data. The example dataset used considers a scenario in which we want to report whether groups defined in a clinical trial are similar with respect to certain variables (e.g. age, gender, etc.). <blockquote>Before you get started, is your data clean? Check out these <a href="https://appsilon.com/data-cleaning-in-r/" target="_blank" rel="noopener">two R packages for cleaning and validating datasets for a smooth experience</a>.</blockquote> The objective of this tutorial is to provide tools that can be extended or used in your project. The final application is not production-ready. But we have plenty of other tutorials on our blog to help you in your project's journey. <ul><li><a href="#preview">Preview Application</a></li><li><a href="#tutorial">Tutorial</a></li><li><a href="#packages">Packages Overview</a></li><li><a href="#UI">Step 1: UI</a></li><li><a href="#server">Step 2: Server</a></li><li><a href="#handle">Handling Input Data</a></li><li><a href="#word">Create Word doc</a></li><li><a href="#app">Full Application Code</a></li></ul> <hr /> <h2 id="preview">Shiny Application Preview and Generated Word Doc</h2> In this tutorial, I'll show you how to create the following application: <img class="alignnone size-full wp-image-16514" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c992e07d803bce5dbd4_Full-Shiny-App-for-generating-Word-Docs-from-Table-Data.gif" alt="Full Shiny App for generating Word Docs from Table Data" width="1898" height="935" /> &nbsp; The downloaded Word document looks like this: <img class="alignnone size-full wp-image-16524" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c99832c47d67310ddd6_word_document-generated-from-table-data-in-Shiny-application-using-officer-and-flextable.webp" alt="word document generated from table data in Shiny application using officer and flextable" width="817" height="615" /> <h2 id="tutorial">Generating Word Documents in R Shiny Tutorial</h2> <h3>R Version</h3> This tutorial uses R's native pipe <code>'|&gt;'</code> so in order to follow along, your R version should be '4.1.0' or greater. <h3>Install Required Packages</h3> The following code will install the missing required packages to follow the tutorial. <pre><code> # Required packages list.of.packages &lt;- c("atable",                      "DT",                      "flextable",                      "officer",                      "readr",                      "remotes",                      "rmarkdown",                      "shiny",                      "shinyglide",                      "shinyWidgets") </code></pre> <h3>New Packages</h3> <pre><code> new.packages &lt;- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])] </code></pre> <h3>Install Missing Packages</h3> <pre><code> if(length(new.packages)) install.packages(new.packages) </code></pre> <h3>Data Source</h3> The application allows the user to upload their own data. It expects an <a href="https://www.lexjansen.com/pharmasug/2020/EP/PharmaSUG-2020-EP-337.pdf" target="_blank" rel="nofollow noopener">ADaM compliant ADSL dataset</a>. <blockquote>Looking to step-up your table data visualizations? <a href="https://appsilon.com/top-r-packages-for-table-data/" target="_blank" rel="noopener">Explore these top R package or visualizing table data for stunning tables in minutes</a>.</blockquote> In our application, we use a subset of <a href="https://github.com/insightsengineering/scda.2022" target="_blank" rel="nofollow noopener">synthetic CDISC data released during 2022</a>. The following code was used to create the example '<a href="https://appsilon.com/wp-content/uploads/2022/11/adsl_data.csv" target="_blank" rel="nofollow noopener">csv</a>' file: <pre><code> # Install packages remotes::install_github("insightsengineering/scda@*release") remotes::install_github("insightsengineering/scda.2022@*release") # Load libraries library(scda) library(scda.2022) library(dplyr) library(readr) # Get adsl data adsl_data &lt;- scda::synthetic_cdisc_data("rcd_2022_02_28")$adsl |&gt;  select(USUBJID, AGE, SEX, RACE, DTHFL, TRT01A, BMRKR1, DTHCAT) # Save local copy to upload to application write_csv(adsl_data, "adsl_data.csv") </code></pre> If you want to see what the first few rows of the dataset look like, run: <pre><code> ```{r, eval=TRUE} readr::read_csv("adsl_data.csv") |&gt;  head(10) |&gt;  flextable::flextable() |&gt;  flextable::autofit() ```</code></pre> <h2 id="packages">Packages Overview for Generating Word Docs from Table Data</h2> Below is a list of packages that implement the core functionality of the application. <ul><li><a href="https://cran.rstudio.com/web/packages/atable/vignettes/atable_usage.pdf" target="_blank" rel="nofollow noopener">atable</a> - supports the analysis and reporting of controlled clinical trials</li><li><a href="https://ardata-fr.github.io/flextable-book/" target="_blank" rel="nofollow noopener">flextable</a> - provides a framework to easily create tables for reporting and publications, with functions that let users create, modify, and format their tables</li><li><a href="https://github.com/davidgohel/officer" target="_blank" rel="nofollow noopener">officer</a> - lets R users manipulate Word (.docx) and PowerPoint (.pptx) documents</li><li><a href="https://juba.github.io/shinyglide/" target="_blank" rel="nofollow noopener">shinyglide</a> - an R package that provides carousel-like or assistant-like components to Shiny applications, thanks to the <a href="https://glidejs.com/" target="_blank" rel="nofollow noopener">Glide</a> JavaScript library</li></ul> Next, we'll begin creating the Shiny application. <h2>Building the Shiny Application for Generating Word Docs</h2> In this section, we'll go step-by-step showing how to create your own Shiny application and how to implement the packages mentioned above. <h3>Step 0: Minimal Shiny App</h3> Let's start by defining the most minimal Shiny application: <pre><code> library(shiny) ui &lt;- fluidPage() server &lt;- function(input, output, session) {} shinyApp(ui, server) </code></pre> <h3 id="UI">Step 1: Defining Shiny UI</h3> Here, we'll build the application UI. We'll add the different elements step-by-step so that you can see how each function changes the UI. <h4>Step 1.1: Adding a Title to Shiny</h4> We use 'titlePanel()' to create a panel containing the application's title. <pre><code> library(shiny) ui &lt;- fluidPage(  # Add application title  titlePanel("Compare Treatment Groups in Clinical Trials")   ) server &lt;- function(input, output, session) {   } shinyApp(ui, server) </code></pre> If you run the code above, the application should like this: <img class="alignnone size-full wp-image-16516" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c9a832c47d67310dfa9_step1_1-Adding-title-to-Shiny-titlePanel.webp" alt="Adding title to Shiny titlePanel" width="973" height="325" /> <blockquote>Looking for custom R/Shiny styling and layouts? <a href="https://appsilon.com/professional-shiny-app-ui/" target="_blank" rel="noopener">Try imola and shiny.fluent for a professional Shiny UI</a>.</blockquote> <h4>Step 1.2: Add Glide Functionality</h4> Now, we want our app to have a glide component with two screens. To achieve this, we'll use functions from the shinyglide package: <ul><li>glide() - to insert the glide component</li><li>screen() - to insert screens in the glide component</li></ul> <pre><code> library(shiny) library(shinyglide) ui &lt;- fluidPage(  # Add application title  titlePanel("Compare Treatment Groups in Clinical Trials"),  # Add glide functionality  glide(    height = "100%",    controls_position = "top",    screen("Placeholder screen 1"),    screen("Placeholder screen 2")  )   ) server &lt;- function(input, output, session) {   } shinyApp(ui, server) </code></pre> Here we set 'height = "100%"' and 'controls_position = "top"': <ul><li>height - controls the height of the glide (another option could have been something like "400px")</li><li>controls_position - determines where to place the controls (it can be either 'top' or 'bottom')</li></ul> If you run the code above, the application should look like this: <img class="alignnone size-full wp-image-16518" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c9b0473cffa87831a4c_step1_2-Adding-Shiny-Glide-functionality.gif" alt="Shiny Glide functionality" width="893" height="599" /> Let's continue by filling the contents of the screens. <h4>Step 1.3: Fill Screen Contents</h4> In Screen 1, we want the user to be able to upload and preview a dataset. To do so, we need to: <ul><li>add a title to the screen using <code>h1()</code>.</li><li>create a layout (<code>sidebarLayout()</code>) with a sidebar (<code>sidebarPanel()</code>) and the main area (<code>mainPanel()</code>).</li><li>place the file upload control in the sidebar using <code>fileInput()</code>. We restrict the type of files that can be uploaded to <code>.csv</code> and <code>.tsv</code> by setting <code>accept = c(".csv", ".tsv")</code>.</li><li>add a <code>dataTable</code> preview of the uploaded data in the main area. This requires us to use the 'DT' package.</li></ul> In Screen 2, we want the user to be able to create a custom report. The report will consist of a user-defined title and a table comparing user-selected variables across user-selected groups. We will allow the user to preview the results and export them to Word. To do this let's: <ul><li>add a title to the screen using <code>h1()</code>.</li><li>create a layout (<code>sidebarLayout()</code>) with a sidebar (<code>sidebarPanel()</code>) and the main area (<code>mainPanel()</code>).</li><li>add input controls to the sidebar:<ul><li><code>textInput</code> will be used to define the title of the report and the file name of the exported Word document</li><li><code>pickerInput</code> (from shinyWidgets package) will be used to select the grouping variable and to select the variables that we will compare.</li></ul> </li> <li>add actions buttons to the sidebar: one button will be used to preview results and the other will export the report as a Word document.</li> <li>add an <code>htmlOutput</code> to the main area to preview the results.</li> </ul> Regarding, pickerInput we can allow a single grouping variable using <code>multiple = FALSE</code> and more than one comparison variable with <code>multiple = TRUE</code>. You might notice that <code>choices = NULL</code> in both cases. This is because until the user uploads a dataset, we don't know the variables we can choose from. We will take care of this in the server. <pre><code> library(shiny) library(shinyglide) library(DT) library(shinyWidgets) ui &lt;- fluidPage(  # Add application title  titlePanel("Compare Treatment Groups in Clinical Trials"),  # Add glide functionality  glide(    height = "100%",    controls_position = "top",    screen(      # Contents of screen 1      h1("Upload Data"),      sidebarLayout(        sidebarPanel(          # Upload data          fileInput(            inputId = "raw_data",            label = "Upload .csv or .tsv",            accept = c(".csv", ".tsv")          )        ),        mainPanel(          # Preview data          DTOutput("preview_data")        )      )    ),    screen(      # Contents of screen 2      h1("Create Report"),      sidebarLayout(        sidebarPanel(          # Set word document title          textInput(            inputId = "document_title",            label = "Set Word Document Title",            value = "My Title"          ),          # Select grouping variable          pickerInput(            inputId = "group_var",            label = "Select Group Variable",            choices = NULL,            multiple = FALSE          ),          # Select the variables used to compare groups          pickerInput(            inputId = "comparison_vars",            label = "Select Variables to Compare",            choices = NULL,            multiple = TRUE          ),          # Set word document filename          textInput(            inputId = "filename",            label = "Set Word Document Filename",            value = "my_comparison"          ),          # Preview results          actionButton(            inputId = "preview_results",            label = "Preview Results"          ),          # Download word document          downloadButton(            outputId = "download_word_document",            label = "Download Word Document"          )        ),        mainPanel(          # Preview results          htmlOutput("result")        )      )    )  ) ) server &lt;- function(input, output, session) {   } shinyApp(ui, server) </code></pre> If you run the code above, the application should look like this: <img class="alignnone size-full wp-image-16520" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c9c13bb0a8ba82fbd27_step1_3-Filling-Shiny-screen-contents.gif" alt="Filling in Shiny screen content" width="893" height="599" /> Before diving into the server, there is one more thing we will modify in the UI. Currently, the user can move to the second screen without uploading any file. If there is no data, not only will we not have any variables to select but also the UX makes no sense to proceed and click a download button. Fortunately, we can enable the Next button based on a condition. In our Shiny app, we'll enable the button only when the preview table exists. We will use the <code>next_condition</code> argument of <code>screen()</code>. The <code>next_condition</code> expects JavaScript code. <pre><code> library(shiny) library(shinyglide) library(DT) library(shinyWidgets) ui &lt;- fluidPage(  # Add application title  titlePanel("Compare Treatment Groups in Clinical Trials"),  # Add glide functionality  glide(    height = "100%",    controls_position = "top",    screen(      # Contents of screen 1      h1("Upload Data"),      sidebarLayout(        sidebarPanel(          # Upload data          fileInput(            inputId = "raw_data",            label = "Upload .csv or .tsv",            accept = c(".csv", ".tsv")          )        ),        mainPanel(          # Preview data          DTOutput("preview_data")        )      ),      # Disable Next button until data is uploaded      next_condition = "output.preview_data !== undefined"    ),    screen(      # Contents of screen 2      h1("Create Report"),      sidebarLayout(        sidebarPanel(          # Set word document title          textInput(            inputId = "document_title",            label = "Set Word Document Title",            value = "My Title"          ),          # Select grouping variable          pickerInput(            inputId = "group_var",            label = "Select Group Variable",            choices = NULL,            multiple = FALSE          ),          # Select the variables used to compare groups          pickerInput(            inputId = "comparison_vars",            label = "Select Variables to Compare",            choices = NULL,            multiple = TRUE          ),          # Set word document filename          textInput(            inputId = "filename",            label = "Set Word Document Filename",            value = "my_comparison"          ),          # Preview results          actionButton(            inputId = "preview_results",            label = "Preview Results"          ),          # Download word document          downloadButton(            outputId = "download_word_document",            label = "Download Word Document"          )        ),        mainPanel(          # Preview results          htmlOutput("result")        )      )    )  ) ) server &lt;- function(input, output, session) {   } shinyApp(ui, server) </code></pre> If you run the code above, the application should look like this: <img class="alignnone size-full wp-image-16522" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c9d99f2296f27b448db_step1_3_2-Next-button-in-Shiny-using-next_condition.gif" alt="Next button in Shiny using next_condition" width="892" height="540" /> If you upload data, you are still unable to move to the second screen. This is because we haven't yet defined the output in the server. Let's do that in the next section. <h3 id="server">Step 2: Defining the Server</h3> In this section, we'll explain what is achieved by each code chunk in the server. In our case, I'll go one code chunk at a time and show the full working application code at the end (including both the UI and Server). <h4 id="handle">How to Handle Input Data</h4> The following code is used to handle <code>fileInput</code>. <ul><li>We use req to ensure <code>input$raw_data</code> is available before proceeding with the action</li><li>We capture the file extension of the input using <code>tools::file_ext</code></li><li>We use <code>switch</code> to read the data using either <code>readr::read_csv</code> or <code>readr::read_delim</code> based on the file extension</li><li>We use <code>validate()</code> to provide a meaningful message when the user tries to upload an invalid file</li><li style="list-style-type: none;"></li></ul> <pre><code>  # Handle input data  data &lt;- reactive({    req(input$raw_data)    ext &lt;- file_ext(input$raw_data$name)    switch(ext,           csv = read_csv(input$raw_data$datapath),           tsv = read_delim(input$raw_data$datapath, delim = "\t"),           validate("Invalid file. Please upload a .csv or .tsv file")    )  }) </code></pre> <h4>Create a Table to Preview Data</h4> The code below uses <code>DT::datatable</code> to build the table that previews uploaded data. <ul><li><code>rownames = FALSE</code> excludes rownames from the table</li><li><code>options = list(searching = FALSE, filter = FALSE)</code> removes both searching and filtering from the <code>datatable</code></li></ul> <pre><code> # Create DataTable that shows uploaded data  output$preview_data &lt;- renderDT({ data() |&gt;      datatable(        rownames = FALSE,        options = list(searching = FALSE, filter = FALSE)      )  }) </code></pre> <h4>Update Input Choices</h4> We can use the following code to update `choices` when data is uploaded. The possible values will be the colnames of the uploaded dataset. <pre><code> # Update input choices when data is uploaded  observeEvent(data(), {    var_names = colnames(data())        updatePickerInput(      session = session,      inputId = "group_var",      choices = var_names    )        updatePickerInput(      session = session,      inputId = "comparison_vars",      choices = var_names    )  }) </code></pre> <h4>Create Comparison Table using atable and flextable</h4> This reactive uses <code>atable::atable</code> and <code>flextable</code> to create the comparison table. We use <code>flextable::autofit()</code> to provide some styling to the table. This reactive will be used by other server functions. <pre><code> # Create comparison table  word_table &lt;- reactive({ atable( x = data(), target_cols = input$comparison_vars, group_col = input$group_var, format_to = "word" ) |&gt;      regulartable() |&gt;      autofit()  }) </code></pre> <h4 id="word">Create a Word Document using officer and flextable</h4> We use functions from `officer` and `flextable` packages to create the reactive that contains the Word Document. <ul><li><code>officer::read_docx()</code> creates a default empty file</li><li><code>officer::body_add_par()</code> is used to add a paragraph of text.<ul><li>We define a title using <code>style = "heading 1"</code>. As you see, the title is the one supplied by the user in the <code>textInput</code></li><li>We can add an empty line by suplying <code>value = ""</code></li></ul> </li> <li><code>flextable::body_add_flextable()</code> adds a flextable into a Word Document</li> </ul> <pre><code> # Create word document  word_document &lt;- reactive({ read_docx() |&gt;      body_add_par(value = input$document_title, style = "heading 1") |&gt;      body_add_par(value = "", style = "Normal") |&gt;      body_add_flextable(word_table())  }) </code></pre> <h4>Preview Word Results in Shiny</h4> When the button to preview results is clicked: <ul><li>A Word Document named <code>preview_results.docx</code> is created in the root of the directory with the contents of <code>word_document()</code></li><li><code>preview_results.docx</code> is converted to HTML using <code>rmarkdown::pandoc_convert</code>. This step is necessary to display the preview of the results.</li><li>We build the output that preview the results using <code>includeHTML()</code>. <code>req</code> is used to avoid error messages before the button is clicked for the first time.</li></ul> <pre><code> # Create files to preview results  observeEvent(input$preview_results, {    # Save current file    word_document() |&gt;      print(target = "preview_results.docx")        # Transform to html    pandoc_convert(      input = "preview_results.docx",      to = "html",      output = "preview_results.html"    )  })    # Preview result  #  Trigger only when the button is clicked  output$result &lt;- renderUI({    req(input$preview_results)    includeHTML("preview_results.html")  }) </code></pre> <h4>Download Word Document</h4> The following code is used to export the results to Word. We use the user-defined filename and make use of <code>print()</code> again to save the file. <pre><code> # Download Handler  output$download_word_document &lt;- shiny::downloadHandler( filename = function() { paste(input$filename, ".docx", sep = "") }, content = function(file) { word_document() |&gt;        print(target = file)    }  ) </code></pre> <h2 id="app">Full Shiny Application Code: Generating Word Docs from Table Data Using officer, flextable, and shinyglide</h2> And there you have it! A Shiny app that permits users to generate word documentation from table data. We also added a 'Next' button that ensures users don't get confused by proceeding to the next screen without adding data. How does your team use Shiny to handle data and reporting? We'd love to hear about your experiences. Share your Shiny stories with us on <a href="http://twitter.com/appsilon" target="_blank" rel="noopener">Twitter</a> or <a href="https://www.linkedin.com/company/appsilon" target="_blank" rel="noopener">LinkedIn</a> and how you plan on improving on what we built together today! If you'd like to add more to your Shiny creation, be sure to check out our <a href="https://appsilon.com/blog/" target="_blank" rel="noopener">Shiny blog</a> and learn from our Shiny devs, UI designers, and engineers. We cover topics ranging from <a href="https://appsilon.com/shiny-application-layouts/" target="_blank" rel="noopener">Shiny layouts</a>, <a href="https://appsilon.com/supply-chain-management-with-r-shiny/" target="_blank" rel="noopener">business use cases with R and Shiny</a>, and much more! <pre><code> # Load libraries library(atable) library(DT) library(flextable) library(officer) library(rmarkdown) library(shiny) library(shinyglide) library(shinyWidgets) # Define UI ui &lt;- fluidPage(  # Add application title  titlePanel("Compare Treatment Groups in Clinical Trials"),  # Add glide functionality  glide(    height = "100%",    controls_position = "top",    screen(      # Contents of screen 1      h1("Upload Data"),      sidebarLayout(        sidebarPanel(          # Upload data          fileInput(            inputId = "raw_data",            label = "Upload .csv or .tsv",            accept = c(".csv", ".tsv")          )        ),        mainPanel(          # Preview data          DTOutput("preview_data")        )      ),      # Disable Next button until data is uploaded      next_condition = "output.preview_data !== undefined"    ),    screen(      # Contents of screen 2      h1("Create Report"),      sidebarLayout(        sidebarPanel(          # Set word document title          textInput(            inputId = "document_title",            label = "Set Word Document Title",            value = "My Title"          ),          # Select grouping variable          pickerInput(            inputId = "group_var",            label = "Select Group Variable",            choices = NULL,            multiple = FALSE          ),          # Select the variables used to compare groups          pickerInput(            inputId = "comparison_vars",            label = "Select Variables to Compare",            choices = NULL,            multiple = TRUE          ),          # Set word document filename          textInput(            inputId = "filename",            label = "Set Word Document Filename",            value = "my_comparison"          ),          # Preview results          actionButton(            inputId = "preview_results",            label = "Preview Results"          ),          # Download word document          downloadButton(            outputId = "download_word_document",            label = "Download Word Document"          )        ),        mainPanel(          # Preview results          htmlOutput("result")        )      )    )  ) ) # Define server server &lt;- function(input, output, session) {  # Handle input data  data &lt;- reactive({    req(input$raw_data)    ext &lt;- tools::file_ext(input$raw_data$name)    switch(ext,           csv = readr::read_csv(input$raw_data$datapath),           tsv = readr::read_delim(input$raw_data$datapath, delim = "\t"),           validate("Invalid file. Please upload a .csv or .tsv file")    )  })    # Create DataTable that shows uploaded data  output$preview_data &lt;- DT::renderDT({ data() |&gt;      DT::datatable(        rownames = FALSE,        options = list(          searching = FALSE,          filter = FALSE        )      )  })    # Update input choices when data is uploaded  observeEvent(data(), {    var_names = colnames(data())        updatePickerInput(      session = session,      inputId = "group_var",      choices = var_names    )        updatePickerInput(      session = session,      inputId = "comparison_vars",      choices = var_names    )  })    # Create comparison table  word_table &lt;- reactive({ atable::atable( x = data(), target_cols = input$comparison_vars, group_col = input$group_var, format_to = "word" ) |&gt;      flextable::regulartable() |&gt;      flextable::autofit()  })    # Create word document  word_document &lt;- reactive({ read_docx() |&gt;      body_add_par(value = input$document_title, style = "heading 1") |&gt;      body_add_par(value = "", style = "Normal") |&gt;      body_add_flextable(word_table())  })    # Create files to preview results  observeEvent(input$preview_results, {    # Save current file    word_document() |&gt;      print(target = "preview_results.docx")        # Transform to html    pandoc_convert(      input = "preview_results.docx",      to = "html",      output = "preview_results.html"    )  })    # Preview result  #  Trigger only when the button is clicked  output$result &lt;- renderUI({    req(input$preview_results)    includeHTML("preview_results.html")  })    # Download Handler  output$download_word_document &lt;- shiny::downloadHandler( filename = function() { paste(input$filename, ".docx", sep = "") }, content = function(file) { word_document() |&gt;        print(target = file)    }  ) } # Run app shinyApp(ui, server) </code></pre> &nbsp;

Have questions or insights?

Engage with experts, share ideas and take your data journey to the next level!

Is Your Software GxP Compliant?

Download a checklist designed for clinical managers in data departments to make sure that software meets requirements for FDA and EMA submissions.
Explore Possibilities

Share Your Data Goals with Us

From advanced analytics to platform development and pharma consulting, we craft solutions tailored to your needs.

Talk to our Experts
r
shiny
reporting