{"id":2799,"date":"2023-01-02T03:28:07","date_gmt":"2023-01-02T03:28:07","guid":{"rendered":"http:\/\/optimumsportsperformance.com\/blog\/?p=2799"},"modified":"2023-01-02T12:47:52","modified_gmt":"2023-01-02T12:47:52","slug":"r-shiny-app-with-pdf-save-report-capabilities","status":"publish","type":"post","link":"https:\/\/optimumsportsperformance.com\/blog\/r-shiny-app-with-pdf-save-report-capabilities\/","title":{"rendered":"R {shiny} app with PDF save report capabilities"},"content":{"rendered":"<p>Over the previous several articles I&#8217;ve shared different approaches to sharing and communicating athlete data. Over this time I got a question about {<strong>shiny<\/strong>} apps and if I had a way to easily build in capabilities to save the report as a PDF for those times when you want to save the report as a PDF to email out or print the report and take it to a decision-maker.<\/p>\n<p>Today I&#8217;ll go over two of the easiest ways I can think of to add some PDF save functionality to your {<strong>shiny<\/strong>} app. Before we jump in, if you are looking to just get started with {<strong>shiny<\/strong>} apps, aside from searching my blog for the various apps I&#8217;ve built (there are several!), <strong><span style=\"color: #0000ff;\"><a style=\"color: #0000ff;\" href=\"https:\/\/twitter.com\/ellis_hughes\">Ellis Hughes<\/a><\/span><\/strong> and I did a 4 part series on building a {<strong>shiny<\/strong>} app from scratch:<\/p>\n<ul>\n<li><strong><span style=\"color: #0000ff;\"><a style=\"color: #0000ff;\" href=\"https:\/\/optimumsportsperformance.com\/blog\/tidyx-25-intro-to-shiny-apps\/\">TidyX 25: Intro to shiny apps<\/a><\/span><\/strong><\/li>\n<li><strong><span style=\"color: #0000ff;\"><a style=\"color: #0000ff;\" href=\"https:\/\/optimumsportsperformance.com\/blog\/tidyx-26-shiny-apps-part-1-creating-an-nba-dashboard\/\">TidyX 26: Shiny apps part 1 &#8211; Creating an NBA dashboard<\/a><\/span><\/strong><\/li>\n<li><a href=\"https:\/\/optimumsportsperformance.com\/blog\/tidyx-27-shiny-apps-part-2-adding-tabs-and-improving-ui\/\"><strong><span style=\"color: #0000ff;\">TIdyX 27: Shiny apps part 2 &#8211; Adding tabs and improving UI<\/span><\/strong><\/a><\/li>\n<li><strong><span style=\"color: #0000ff;\"><a style=\"color: #0000ff;\" href=\"https:\/\/optimumsportsperformance.com\/blog\/tidyx-28-shiny-apps-part-3-k-nearest-neighbor-and-reactivity\/\">TidyX 28: Shiny apps part 3 &#8211; K-nearest neighbor and reactivity<\/a><\/span><\/strong><\/li>\n<\/ul>\n<p>Alright, now to jump into building a {<strong>shiny<\/strong>} app with the ability to save as PDF. As always, you can access the full code to the article on my <strong><span style=\"color: #0000ff;\">GITHUB page<\/span><\/strong>.<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>Loading Packages &amp; Data<\/strong><\/span><\/p>\n<p>As always, we need to load the packages that we need and some data. For this, I&#8217;ll keep things simple and just use the <strong>mtcars<\/strong> data that is available in base R, since I&#8217;m mainly concerned with showing how to build the app, not the actual data analysis.<\/p>\n<pre class=\"brush: r; title: ; notranslate\" title=\"\">\r\n#### packages ----------------------------------------------\r\nlibrary(shiny)\r\nlibrary(shinyscreenshot)\r\nlibrary(DT)\r\nlibrary(gridExtra)\r\nlibrary(ggpubr)\r\nlibrary(tidyverse)\r\n\r\n## data ----------------------------------------------------\r\ndat &lt;- mtcars %&gt;%\r\n  mutate(cyl = as.factor(cyl),\r\n         car_type = rownames(.)) %&gt;%\r\n  relocate(car_type, .before = mpg)\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>App 1: Printing the app output as its own report<\/strong><\/span><\/p>\n<p>The user interface for this app will allow the user to select a Cylinder (cyl) number and the two plots and table will update with the available info.<\/p>\n<p>The server of this app is where the magic happens. What the user sees on the web app is not exactly what it looks like when saved as a PDF. To make this version work, I need to store my outputs in their own elements and then take those elements and output them as an export. I do this by saving a copy within the render function for each of the outputs. I also create an empty reactive values element within the server, which sets each plot and table to NULL, but serves as a container to store the output each time the user changes the cylinder number.<\/p>\n<p>You&#8217;ll notice in the <strong>output$tbl<\/strong> section of the server, I produce one table for viewing within the app while the second table is stored for PDF purposes. I do this because I like the <strong>ggtextable()<\/strong> table better than the simple base R one, as it has more customizable options. Thus, I use that one for the PDF report. Here is what the server looks like:<\/p>\n<pre class=\"brush: r; title: ; notranslate\" title=\"\">\r\nserver &lt;- function(input, output){\r\n  \r\n  ## filter cylinder\r\n  cyl_df &lt;- reactive({\r\n    \r\n    req(input$cyl)\r\n    \r\n    d &lt;- dat %&gt;%\r\n      filter(cyl == input$cyl)\r\n    d\r\n    \r\n  })\r\n  \r\n  \r\n  ## output plt1\r\n  output$plt1 &lt;- renderPlot({\r\n    \r\n    vals$plt1 &lt;- cyl_df() %&gt;%\r\n      ggplot(aes(x = wt, y = mpg)) +\r\n      geom_point(size = 4) +\r\n      theme_bw() +\r\n      labs(x = &quot;wt&quot;,\r\n           y = &quot;mpg&quot;,\r\n           title = &quot;mpg ~ wt&quot;) +\r\n    theme(axis.text = element_text(size = 12, face = &quot;bold&quot;),\r\n          axis.title = element_text(size = 15, face = &quot;bold&quot;),\r\n          plot.title = element_text(size = 20))\r\n    \r\n    vals$plt1\r\n    \r\n    \r\n  })\r\n  \r\n  ## output table\r\n  output$tbl &lt;- renderTable({\r\n    \r\n    tbl_df &lt;- cyl_df() %&gt;%\r\n      setNames(c(&quot;Car Type&quot;, &quot;MPG&quot;, &quot;CYL&quot;, &quot;DISP&quot;, &quot;HP&quot;, &quot;DRAT&quot;, &quot;WT&quot;, &quot;QSEC&quot;, &quot;VS&quot;, &quot;AM&quot;, &quot;GEAR&quot;, &quot;CARB&quot;))\r\n    \r\n    # store table for printing\r\n    vals$tbl &lt;- ggtexttable(tbl_df,\r\n                            rows = NULL,\r\n                            cols = c(&quot;Car Type&quot;, &quot;MPG&quot;, &quot;CYL&quot;, &quot;DISP&quot;, &quot;HP&quot;, &quot;DRAT&quot;, &quot;WT&quot;, &quot;QSEC&quot;, &quot;VS&quot;, &quot;AM&quot;, &quot;GEAR&quot;, &quot;CARB&quot;),\r\n                            theme = ttheme('minimal',\r\n                                           base_size = 12))\r\n    \r\n    # return table for viewing\r\n    tbl_df\r\n    \r\n  })\r\n  \r\n  \r\n  ## output plt2\r\n  output$plt2 &lt;- renderPlot({\r\n    \r\n    vals$plt2 &lt;- cyl_df() %&gt;%\r\n      ggplot(aes(x = disp, y = hp)) +\r\n      geom_point(size = 4) +\r\n      theme_bw() +\r\n      labs(x = &quot;disp&quot;,\r\n           y = &quot;hp&quot;,\r\n           title = &quot;hp ~ disp&quot;) +\r\n      theme(axis.text = element_text(size = 12, face = &quot;bold&quot;),\r\n            axis.title = element_text(size = 15, face = &quot;bold&quot;),\r\n            plot.title = element_text(size = 20))\r\n    \r\n    vals$plt2\r\n    \r\n  })\r\n  \r\n  \r\n  ## The element vals will store all plots and tables\r\n  vals &lt;- reactiveValues(plt1=NULL,\r\n                         plt2=NULL,\r\n                         tbl=NULL)\r\n  \r\n  \r\n  ## clicking on the export button will generate a pdf file \r\n  ## containing all stored plots and tables\r\n  output$export = downloadHandler(\r\n    filename = function() {&quot;plots.pdf&quot;},\r\n    content = function(file) {\r\n      pdf(file, onefile = TRUE, width = 15, height = 9)\r\n      grid.arrange(vals$plt1,\r\n                   vals$tbl,\r\n                   vals$plt2,\r\n                   nrow = 2,\r\n                   ncol = 2)\r\n      \r\n      dev.off()\r\n    })\r\n}\r\n\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p>Here is what the shiny app will look like when you run it:<\/p>\n<p><a href=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.01.42-PM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-2800\" src=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.01.42-PM-1024x604.png\" alt=\"\" width=\"625\" height=\"369\" srcset=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.01.42-PM-1024x604.png 1024w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.01.42-PM-300x177.png 300w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.01.42-PM-768x453.png 768w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.01.42-PM-624x368.png 624w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.01.42-PM.png 1504w\" sizes=\"auto, (max-width: 625px) 100vw, 625px\" \/><\/a><\/p>\n<p>When the user clicks the <strong>Download<\/strong> button on the upper left, they can save a PDF, which looks like this:<\/p>\n<p><a href=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.03.21-PM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-2801\" src=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.03.21-PM-1024x614.png\" alt=\"\" width=\"625\" height=\"375\" srcset=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.03.21-PM-1024x614.png 1024w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.03.21-PM-300x180.png 300w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.03.21-PM-768x461.png 768w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.03.21-PM-624x374.png 624w\" sizes=\"auto, (max-width: 625px) 100vw, 625px\" \/><\/a><\/p>\n<p>Notice that we are returned the plots and table from the {<strong>shiny<\/strong>} app, however we don&#8217;t have the overall title. I&#8217;m sure we could remedy this within the server, but what if we want to simply produce a PDF that looks exactly like what we see in the web app?<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>App 2: Take a screen shot of your shiny app!<\/strong><\/span><\/p>\n<p>If we want to have the downloadable output look exactly like the web app, we can use the package {<strong>shinyscreentshot<\/strong>}.<\/p>\n<p>The user interface of the app will remain the same. The server will change as you no longer need to store the plots. You simply need to add an <strong>observeEvent()<\/strong> function and tell R that you want to take a screenshot of the page once the button is pressed!<\/p>\n<p>Since we are taking a screen shot I also took the liberty of changing the table of data to a <strong>{DT<\/strong>} table. I like {<strong>DT<\/strong>} tables better because they are interactive and have more functionality. In the previous {<strong>shiny<\/strong>} app it was harder to use that sort of interactive table and store it for PDF printing. Since we are taking a screenshot, it opens up a lot more options for us to customize the output.<\/p>\n<p>Here is what the server looks likes:<\/p>\n<pre class=\"brush: r; title: ; notranslate\" title=\"\">\r\nserver &lt;- function(input, output){\r\n  \r\n  ## filter cylinder\r\n  cyl_df &lt;- reactive({\r\n    \r\n    req(input$cyl)\r\n    \r\n    d &lt;- dat %&gt;%\r\n      filter(cyl == input$cyl)\r\n    d\r\n    \r\n  })\r\n  \r\n  \r\n  ## output plt1\r\n  output$plt1 &lt;- renderPlot({ cyl_df() %&gt;%\r\n      ggplot(aes(x = wt, y = mpg)) +\r\n      geom_point(size = 4) +\r\n      theme_bw() +\r\n      labs(x = &quot;wt&quot;,\r\n           y = &quot;mpg&quot;,\r\n           title = &quot;mpg ~ wt&quot;) +\r\n    theme(axis.text = element_text(size = 12, face = &quot;bold&quot;),\r\n          axis.title = element_text(size = 15, face = &quot;bold&quot;),\r\n          plot.title = element_text(size = 20))\r\n    \r\n  })\r\n  \r\n  ## output table\r\n  output$tbl &lt;- renderDT({ cyl_df() %&gt;%\r\n      datatable(class = 'cell-border stripe',\r\n                rownames = FALSE,\r\n                filter = &quot;top&quot;,\r\n                options = list(pageLength = 4),\r\n                colnames = c(&quot;Car Type&quot;, &quot;MPG&quot;, &quot;CYL&quot;, &quot;DISP&quot;, &quot;HP&quot;, &quot;DRAT&quot;, &quot;WT&quot;, &quot;QSEC&quot;, &quot;VS&quot;, &quot;AM&quot;, &quot;GEAR&quot;, &quot;CARB&quot;))\r\n    \r\n  })\r\n  \r\n  ## output plt2\r\n  output$plt2 &lt;- renderPlot({ cyl_df() %&gt;%\r\n      ggplot(aes(x = disp, y = hp)) +\r\n      geom_point(size = 4) +\r\n      theme_bw() +\r\n      labs(x = &quot;disp&quot;,\r\n           y = &quot;hp&quot;,\r\n           title = &quot;hp ~ disp&quot;) +\r\n    theme(axis.text = element_text(size = 12, face = &quot;bold&quot;),\r\n          axis.title = element_text(size = 15, face = &quot;bold&quot;),\r\n          plot.title = element_text(size = 20))\r\n    \r\n    \r\n  })\r\n  \r\n  observeEvent(input$go, {\r\n    screenshot()\r\n  })\r\n}\r\n\r\n<\/pre>\n<p>The new web app looks like this:<\/p>\n<p><a href=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.12.24-PM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-2802\" src=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.12.24-PM-1024x538.png\" alt=\"\" width=\"625\" height=\"328\" srcset=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.12.24-PM-1024x538.png 1024w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.12.24-PM-300x158.png 300w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.12.24-PM-768x404.png 768w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.12.24-PM-624x328.png 624w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/Screen-Shot-2023-01-01-at-7.12.24-PM.png 1690w\" sizes=\"auto, (max-width: 625px) 100vw, 625px\" \/><\/a><\/p>\n<p>Looks pretty similar, just with a nicer table. If the user clicks the <strong>Screenshot Report<\/strong> at the upper left, R will save a png file of the report, which looks like this:<\/p>\n<p><a href=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/shinyscreenshot.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-2803\" src=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/shinyscreenshot-1024x497.png\" alt=\"\" width=\"625\" height=\"303\" srcset=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/shinyscreenshot-1024x497.png 1024w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/shinyscreenshot-300x146.png 300w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/shinyscreenshot-768x373.png 768w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/shinyscreenshot-624x303.png 624w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2023\/01\/shinyscreenshot.png 1834w\" sizes=\"auto, (max-width: 625px) 100vw, 625px\" \/><\/a><\/p>\n<p>As you can see, this produces a downloadable report that is exactly like what the user sees on their screen.<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>Wrapping Up<\/strong><\/span><\/p>\n<p>There are two simple ways to build some save functions directly into your {<strong>shiny<\/strong>} apps. Again, if you&#8217;d like the full code, you can access it on my <strong><span style=\"color: #0000ff;\"><a style=\"color: #0000ff;\" href=\"https:\/\/github.com\/pw2\/R-Tips-Tricks\/blob\/master\/shiny%20app%20with%20print%20report%20functionality.R\">GITHUB page<\/a><\/span><\/strong>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Over the previous several articles I&#8217;ve shared different approaches to sharing and communicating athlete data. Over this time I got a question about {shiny} apps and if I had a way to easily build in capabilities to save the report as a PDF for those times when you want to save the report as a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[45,43,42,27],"tags":[],"class_list":["post-2799","post","type-post","status-publish","format-standard","hentry","category-r-tips-tricks","category-sports-analytics","category-sports-science","category-strength-and-conditioning"],"_links":{"self":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts\/2799","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/comments?post=2799"}],"version-history":[{"count":3,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts\/2799\/revisions"}],"predecessor-version":[{"id":2807,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts\/2799\/revisions\/2807"}],"wp:attachment":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/media?parent=2799"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/categories?post=2799"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/tags?post=2799"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}