第 6 章 Shiny

Shiny 包(Chang等,2021年)构建了由 R 驱动的交互式 Web 应用程序。要从 R Markdown 文档中调用 Shiny 代码,在 YAML 元数据添加 runtime: shiny 即可。Shiny 是用于构建基于 R 的 Web 应用程序的功能非常强大的框架。本书不全面介绍 Shiny(这是一个太大的话题),超出了本书的范围。 我们建议不熟悉 Shiny 的读者在阅读本章之前,先从 https://shiny.rstudio.com 网站了解有关它的更多信息。

与创建静态报告的传统工作流不同,你可以创建允许读者更改分析基础参数的文档,并在 Shiny 的 R Markdown 文档中立即看到结果。

一张图片胜过千言万语,当你与它互动时,一份 Shiny 文档可能会向你展示上千张图片。读者不再被报告中固定的分析和结论所束缚。他们可能会自己探索其他可能性,可能会有新的发现或得出不同的结论。

6.1 入门指南

你可以通过在 YAML 元数据中加入 runtime: shiny,这样就可以把任何面向 html 的 R Markdown 文档变成 Shiny 文档了,例如:

---
title: "Shiny Document"
output: html_document
runtime: shiny
---

注意,R Markdown 文档的输出格式必须是 HTML 格式。也就是说,文档最后生成的是一个 web 页面(*.html 文件)。

注意: 非 html 格式,如 pdf_documentword_document 将不能与 Shiny 同时运行。另外,一些演示格式也是 HTML 格式,比如ioslides_presentationslidy_presentation

你也通过RStudio构建一个新的Shiny文档。步骤如下:File -> new File -> R Markdown 并选择Shiny,具体见图6.1

Create a new Shiny document in RStudio.

图 6.1: Create a new Shiny document in RStudio.

要在 RStudio 中运行 Shiny 文档,你需要单击编辑器工具栏上的 “Run Document” 按钮(当 RStudio 检测到这是 Shiny 文档时,它会自动将 Knit 按钮替换为 run document)。如果你没使用 RStudio,或者想在 R 控制台运行文档进行故障排除,你可以调用函数 rmarkdown::run() 并将文件名传递给它。

你可以在文档中嵌入 Shiny 的输入和输出。输入发生变化时,输出将自动更新。在下面的示例中,我们创建一个名称为 rows 的数字输入( numericInput ),然后在生成输出时通过 input$rows 引用其值:

```{r, echo=FALSE}
numericInput("rows", "How many cars?", 5)

renderTable({
  head(cars, input$rows)
})
```
Increase the number of rows in the table in a Shiny document.

图 6.2: Increase the number of rows in the table in a Shiny document.

在上面的示例中,输出代码包装在对 renderTable() 的调用中。 Shiny 中还有许多其他渲染功能可用于绘图、打印的 R 输出等。 本示例使用 renderPlot() 创建动态绘图输出:

```{r, echo=FALSE}
sliderInput("bins", "Number of bins:", 30, min = 1, max = 50)

renderPlot({
  x    = faithful[, 2]  # Old Faithful Geyser data
  bins = seq(min(x), max(x), length.out = input$bins + 1)

  # draw the histogram with the specified number of bins
  hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
```
Change the number of bins of a histogram in a Shiny document.

图 6.3: Change the number of bins of a histogram in a Shiny document.

6.2 部署

Shiny 文档必须由后台实时 R 会话支持。当你在本地运行一个 Shiny 文档时,它会使用你的本地 R 会话。通常只有你才能看到文档并与之交互。如果你希望与没有安装 R 的用户共享文档,或者不希望在本地运行文档,则必须将文档部署到服务器上,并共享文档的 URL。那么其他人只需要一个 web 浏览器就可以访问你的文档。

有两种方法可以部署 Shiny 文档。你可以使用 RStudio 提供的托管服务,也可以设置自己的服务器。

第一种方法从技术上来说比较容易,但是有时你可能不被允许使用外部托管服务,因此莫必须在自己的服务器上安装必需的软件(Shiny Server 或 RStudio Connect) 才能部署 Shiny 文档。

6.2.1 ShinyApps.io

你可以把 Shiny 文档发布到 ShinyApps (https://shinyapps.io) 进行托管服务。

要做到这一点,就应该确保你有:

  1. 在 ShinyApps 上注册一个账号(使用注册表单申请一个账号)。

  2. 最新版本的 rsconnect 包。你可以按照如下方式安装它

    install.packages("rsconnect")

然后,从包含文档的工作目录中执行:

rsconnect::deployApp()

如果你使用的是 RStudio,那么在运行一个 Shiny 文档时,还可以使用窗口右上角的 Publish 按钮(参见图 6.4)。

Deploy a Shiny document to ShinyApps.io.

图 6.4: Deploy a Shiny document to ShinyApps.io.

如果目录中有一个名为 index.Rmd 的文件,它将用作该目录的默认文档,否则,如果要访问此 Rmd 文档,则应在 URL 中指定 Rmd 文件的显式路径。

例如,部署到 ShinyApps 的 index.Rmd 的 URL 可以采用以下形式 https://example.shinyapps.io/appName/,并且 test.Rmd 的 URL 可以采用以下形式 https://example.shinyapps.io/appName/test.Rmd

6.2.2 Shiny Server / RStudio Connect

Both Shiny Server (https://www.rstudio.com/products/shiny/shiny-server/) and RStudio Connect (https://www.rstudio.com/products/connect/) can be used to publish Shiny documents. They require knowledge about Linux. Installing and configuring them should normally be a task for your system administrator if you are not familiar with Linux or do not have the privilege.

Shiny Server (https://www.rstudio.com/products/shiny/shiny-server/) 和 RStudio Connect (https://www.rstudio.com/products/connect/)都可以发布 Shiny 文档。 但是他们需要有关 Linux 的知识,如果你不熟悉 Linux 或没有特权,则安装和配置它们通常应该是系统管理员的任务。

6.3 嵌入 Shiny 应用程序

除了在 R Markdown 中嵌入单个 Shiny 的输入和输出外,还可以在文档中嵌入一个独立的 Shiny 应用程序。有两种方法可以做到这一点:

  1. 使用 shinyApp() 函数内联定义应用程序;

  2. 使用 shinyApp() 函数引用外部应用程序目录。

这两个函数在 shiny 包中都是可用的(不是 rmarkdown),当在YAML元数据指定 runtime: shiny 时,它们会被自动加载。因此,你不必调用 library(shiny) 来加载 shiny。

6.3.1 内联应用程序

这个例子使用了内联定义:

```{r, echo=FALSE}
shinyApp(

  ui = fluidPage(
    selectInput("region", "Region:",
                choices = colnames(WorldPhones)),
    plotOutput("phonePlot")
  ),

  server = function(input, output) {
    output$phonePlot = renderPlot({
      barplot(WorldPhones[,input$region]*1000,
              ylab = "Number of Telephones", xlab = "Year")
    })
  },

  options = list(height = 500)
)
```

注意,使用 height 参数来确定嵌入式应用程序应该占用多少垂直空间。

6.3.2 外联应用程序

这个示例嵌入了一个定义在另一个目录中的 Shiny 应用程序。

```{r, echo = FALSE}
shinyAppDir(
  system.file("examples/06_tabsets", package="shiny"),
  options = list(width = "100%", height = 700)
)
```

注意,在上面的所有代码块中,都使用了 echo = FALSE 的选项。这是为了防止块中的 R 代码与 Shiny 组件一起呈现到输出文档中。

6.4 Shiny 小部件

Shiny 小部件使你能够使用一个函数调用创建包含在 R Markdown 文档中的可重复使用的 Shiny 组件。还可以直接从控制台调用 Shiny 小部件(在创作过程中很有用),并在 RStudio Viewer 窗格或外部 web 浏览器中显示它们的输出。

6.4.1 shinyApp() 函数

Shiny 窗口小部件的核心是使用 shinyApp() 函数创建的微型应用程序。 你无需像经典的 Shiny 应用程序那样创建 ui.Rserver.R(或 app.R),而是将 UI 和服务器定义作为参数传递给 shinyApp() 函数。 我们在第 6.3.1 节中给出了一个示例。

最简单的 Shiny 小部件类型只是一个返回 shinyApp() 的 R 函数。

6.4.2 例子:k - means 聚类

rmdexamples 包 (https://github.com/rstudio/rmdexamples) 包括一个以上面所诉形式实现的Shiny小部件示例。 kmeans_cluster() 函数接受单个数据集(dataset)参数,并返回一个小部件来显示k-Means聚类的结果。你可以像这样在 R Markdown 文档中使用它:

```{r, echo = FALSE}
library(rmdexamples)
kmeans_cluster(iris)
```

6.5 显示了在运行文档时小部件的样子。

A Shiny widget to apply k-Means clustering on a dataset.

图 6.5: A Shiny widget to apply k-Means clustering on a dataset.

下面是 kmeans_cluster() 函数的源代码:

kmeans_cluster = function(dataset) {

  library(shiny)  
  vars = names(dataset)

  shinyApp(
    ui = fluidPage(
      fluidRow(style = "padding-bottom: 20px;",
        column(4, selectInput('xcol', 'X Variable', vars)),
        column(4, selectInput('ycol', 'Y Variable', vars,
                              selected = vars[2])),
        column(4, numericInput('clusters', 'Cluster count', 3,
                               min = 1, max = 9))
      ),
      fluidRow(
        plotOutput('kmeans', height = "400px")  
      )
    ),
    server = function(input, output, session) {
      # Combine the selected variables into a new data frame
      selectedData = reactive({
        dataset[, c(input$xcol, input$ycol)]
      })
      clusters = reactive({
        kmeans(selectedData(), input$clusters)
      })
      output$kmeans = renderPlot(height = 400, {
        res = clusters()
        par(mar = c(5.1, 4.1, 0, 1))
        plot(selectedData(),
             col = res$cluster, pch = 20, cex = 3)
        points(res$centers, pch = 4, cex = 4, lwd = 4)
      })
    },
    options = list(height = 500)
  )
}

6.4.3 小部件的大小和布局

Shiny 小部件可以嵌入在不同的地方,包括标准的全宽页面、页面内的小列,甚至在 HTML5 幻灯片中。为了让小部件的大小和布局在所有这些上下文中都能很好地工作,我们建议小部件的总高度不大于500像素。这并不是一个硬性的规则,但是在HTML5幻灯片中,通常只能显示低于500px的内容,所以如果你想让你的小部件在演示中可用,这是一个很好的建议。

当然,你还可以在创建小部件的函数中添加一个显式的 height 参数(默认值为500)。

6.5 多页

你可以使用 Markdown 链接语法并指定文档的相对路径,链接到其他 Shiny 文档上,例如: [另一个Shiny文档](another.Rmd)。如果在一个页面上单击指向另一个 Rmd 文档的链接,该 Rmd 文档将作为当前交互式闪亮文档启动。

目前,一次只能激活一个文档,因此尽管可以通过 R 脚本 global.R 进行一些原始的全局共享,但文档无法轻易共享状态(请参阅帮助页面 ?rmarkdown::run)。

默认情况下,只能链接到调用 rmarkdown::run() 的文件所在目录子树中的 R Markdown 文件(例如,你无法连接到 ../foo.Rmd)。 但是,你可以使用 rmarkdown::run()dir 参数来定义根目录。

6.6 延迟渲染

一个 Shiny 文档通常在每次显示时都被呈现,并且在呈现完成之前不会显示给用户。因此,较大或包含昂贵计算的文档可能需要一些时间来加载。

如果你的文档包含不需要立即渲染的交互式 Shiny 组件,你可以在 rmarkdown::render_delayed() 函数中封装 Shiny 代码。这个函数保存它的参数,直到文档呈现完成并显示给用户,然后计算它,并在计算完成时将其注入输出文档。

下面我们将演示 render_delayed()是如何工作的。 render_delayed() 调用中包含的代码只有在加载了文档并显示给用户之后才会执行

```{r, echo = FALSE}
numericInput("rows", "How many cars?", 5)

rmarkdown::render_delayed({
  renderTable({
    head(cars, input$rows)
  })
})
```

6.7 渲染函数的输出参数

在一个典型的 Shiny 应用程序中,你可以使用 plotOutput()verbatimTextOutput()等函数在 UI 中指定输出元素,并使用 renderPlot()renderPrint() 等函数呈现其内容。

相比之下,在一个 Shiny 文档中,UI 元素通常是在调用 renderXXX() 函数时隐式和自动创建的。例如,你可能希望使用 renderPlot() 函数,而预先没有创建对应的 plotOutput()。在这种情况下,Shiny 将相应的输出对象与每个 renderXXX() 函数关联起来,让你可以在一个完整的 Shiny 应用程序之外使用 Shiny 代码。但是,在这个过程中可能会丢失一些功能。特别地,plotOutput() 可以接受一些可选参数来设置宽度和高度等内容,或者允许你单击或刷过绘图(并存储该信息)。

要将选项从 renderXXX() 传递到 xxxOutput(),可以使用 outputArgs 参数(如果特定的 renderXXX() 函数可用)。 例如:假设你要绘制一张宽度为200px,高度为100px的图形时。 你可以设定为如下形式:

```{r, echo = FALSE}
renderPlot({
  plot(yourData)
}, outputArgs = list(width = "200px", height = "100px")
)
```

无论你想设置多少个输出参数,outputArgs 总是设定为一个列表(默认是一个空列表,它不设置输出参数)。如果你试图传入一个不存在的参数,那么将得到以下的错误提示信息(在本例中,你试图设置一个名为 not_an_argument 的参数):

**Error**: Unused argument: in `outputArgs`, `not_an_argument`
is not an valid argument for the output function

要查看运行中的 outputArgs,请运行下面的 R Markdown 文档或访问在线版 https://gallery.shinyapps.io/output-args/。文档是交互式的:在图像上刷新一下,看到 xmin, xmax, yminymax 值的变化(正好印在图片下方)。

---
title: Setting output args via render functions
runtime: shiny
output: html_document
---

This interactive Rmd document makes use of the `outputArgs`
argument now available to all Shiny `render` functions. To
give an example, this allows you to set arguments to
`imageOutput` through `renderImage`. This means that you
don't have to create a `ui` object just to be able to brush
over an image. Note that this only applies to snippets of
Shiny code during an interactive Rmd (and not to embedded
full apps -- the ones you need to call `shinyApp` to run).

## Brushing over an image (and storing the data)

```{r setup, echo=FALSE}
library(datasets)

generateImage = function() {
  outfile = tempfile(fileext = '.png')
  png(outfile)
  par(mar = c(0,0,0,0))
  image(volcano, axes = FALSE)
  contour(volcano, add = TRUE)
  dev.off()
  list(src = outfile)
}
```

```{r image}
renderImage({
  generateImage()
}, deleteFile = TRUE, 
   outputArgs = list(brush = brushOpts(id = "plot_brush"),
                     width = "250",
                     height = "250px")
)
```

Here is some of the brushing info sent to the server:
(brush over the image to change the data)

```{r brush info}
renderText({ 
  print(input$plot_brush)
  brush = input$plot_brush
  paste0("xmin: ", brush$xmin, "; ",
         "xmax: ", brush$xmax, "; ",
         "ymin: ", brush$ymin, "; ",
         "ymax: ", brush$ymax)
})
```

---

### Resizing a plot

```{r plot}
renderPlot({ 
  plot(cars) 
}, outputArgs = list(width = "75%", 
                     height = "250px")
)
```

6.7.1 附加说明

我们要强调的是,你只能在一个 Shiny 的 R Markdown 文档中使用这个功能(也就是说,你必须在 YAML 元数据中设置 runtime: shiny )。但即使是这样,这也只适用于在 UI 中没有相应的显式输出元素的情况下呈现输出的 Shiny 代码片段。如果你在你的文档中嵌入一个完整的 Shiny 的应用程序,并尝试使用 outputArgs ,这时它将被忽略,并将以下警告打印到 R Markdown 控制台中(在这种情况下,你的 ui 函数将类似于 ui = plotOutput("plot") ):

Warning in `output$plot`(...) :
Unused argument: outputArgs. The argument outputArgs is only
meant to be used when embedding snippets of Shiny code in an
R Markdown code chunk (using runtime: shiny). When running a
full Shiny app, please set the output arguments directly in
the corresponding output function of your UI code.

如果你尝试在其他任何上下文中使用 outputArgs,例如:在常用的(即非嵌入式)Shiny 应用程序内部使用 outputArgs,也会发生同样的情况。 基本原理解释如下:如果你已经在所有输出对象都明确指定的情况下指定了 ui 函数,则应在此处直接设置其参数,而不要采用这种绕行方式。