Shiny从入门到入定——6-布局、主题、HTML
发表于:2024-01-25 | 分类: IT
字数统计: 4.4k | 阅读时长: 17分钟 | 阅读量:

6 布局,主题,HTML

  • 第6章详细介绍了在页面上布局输入和输出组件的各种方法,以及如何使用主题定制它们的外观。

6.1 前言

在本章中,你将解锁一些控制应用程序整体外观的新工具。我们将首先讨论页面布局(包括单页和“多页”),让你组织你的输入和输出。然后你将学习Bootstrap,Shiny使用的CSS工具包,以及如何用主题定制它的整体视觉外观。最后,我们将简要讨论一下幕后发生了什么,这样如果你知道HTML和CSS,你就可以进一步定制Shiny应用程序。

1
library(shiny)

6.2 单页布局

在第2章中,你学习了构成应用程序交互组件的输入和输出。但是我没有谈到如何在页面上布局它们,而是使用 fluidPage() 尽可能快地将它们拼凑在一起。虽然这对于学习 Shiny 来说是很好的,但它并不能创建可用或视觉上吸引人的应用程序,所以现在是时候学习一些更多的布局函数了。

布局功能提供了应用程序的高级视觉结构。布局由函数调用的层次结构创建,其中R中的层次结构与生成的HTML中的层次结构相匹配。这有助于您理解布局代码。例如,当您查看这样的布局代码时:

1
2
3
4
5
6
7
8
9
10
11
fluidPage(
titlePanel("Hello Shiny!"),
sidebarLayout(
sidebarPanel(
sliderInput("obs", "Observations:", min = 0, max = 1000, value = 500)
),
mainPanel(
plotOutput("distPlot")
)
)
)

关注函数调用的层次结构:

1
2
3
4
5
6
7
fluidPage(
titlePanel(),
sidebarLayout(
sidebarPanel(),
mainPanel()
)
)

即使你还没有学习这些函数,通过阅读它们的名字,你也可以猜到发生了什么。你可能会想象这段代码会生成一个经典的应用程序设计:顶部有一个标题栏,后面是一个侧边栏(包含一个滑块)和主面板(包含一个绘图)。通过缩进轻松查看层次结构的能力是使用一致风格的一个好主意的原因之一。

在本节的剩余部分中,我将讨论帮助您设计单页应用程序的功能,然后在下一节中讨论多页应用程序。我还建议您查看Shiny Application布局指南;它有点过时,但包含一些有用的精华。

6.2.1 页面功能

最重要的,但最无趣的布局函数是 fluidPage(),到目前为止,您在几乎每个示例中都看到了它。但是它做了什么,如果单独使用它会怎么样?图6.1显示了结果:它看起来像一个非常无聊的应用程序,但幕后有很多事情,因为 fluidPage() 设置了 Shiny 所需的所有 HTML、CSS 和 JavaScript。

图6.1 仅由 fluidPage() 构成的UI

除了fluidPage(),Shiny还提供了其他几个页面函数,在更特殊的情况下可以派上用场:fixedPage()fillPage()fixedPage()的工作原理类似于fluidPage(),但有一个固定的最大宽度,可以防止你的应用程序在更大的屏幕上变得不合理地宽。fillPage()填充浏览器的整个高度,如果你想制作一个占据整个屏幕的图表,它很有用。你可以在他们的文档中找到详细信息。

6.2.2 带侧边栏的页面

要制作更复杂的布局,您需要在 fluidPage() 中调用布局函数。例如,要制作左侧有输入、右侧有输出的两列布局,可以使用 sidebarLayout()(以及它的朋友 titlePanel()sidebarPanel()mainPanel())。以下代码显示了生成图 6.2 的基本结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
fluidPage(
titlePanel(
# app title/description
),
sidebarLayout(
sidebarPanel(
# inputs
),
mainPanel(
# outputs
)
)
)

图6.2 基础app的侧边栏结构

为了让其更加真实,让我们增加一个输入和输出以创建一个演示中心极限定理的非常简单的应用程序,如图6.3所示。如果你自己运行这个应用程序,你可以增加样本数量以看到分布变得更加正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ui <- fluidPage(
titlePanel("Central limit theorem"),
sidebarLayout(
sidebarPanel(
numericInput("m", "Number of samples:", 2, min = 1, max = 100)
),
mainPanel(
plotOutput("hist")
)
)
)
server <- function(input, output, session) {
output$hist <- renderPlot({
means <- replicate(1e4, mean(runif(input$m)))
hist(means, breaks = 20)
}, res = 96)
}

图6.3 常见的应用程序设计是将控件放在侧边栏中,并将结果显示在主面板中

6.2.3 多行

在幕后,sidebarLayout()建立在灵活的多行布局之上,您可以直接使用它来创建更具视觉复杂性的应用程序。像往常一样,您从fluidPage()开始。然后,您使用fluidRow()创建行,使用column()创建列。以下模板生成图6.4所示的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fluidPage(
fluidRow(
column(4,
...
),
column(8,
...
)
),
fluidRow(
column(6,
...
),
column(6,
...
)
)
)

图6.4 简单多行应用程序的基础结构

每一行由12列组成,column() 的第一个参数给出了要占用的列数。12列布局为您提供极大的灵活性,因为您可以轻松创建2、3或4列布局,或使用窄列创建间隔。您可以在第4.4节中看到这种布局的示例。

如果你想了解更多关于使用网格系统进行设计的知识,我强烈推荐由约瑟夫·穆勒-布罗克曼撰写的关于这个主题的经典文本:《平面设计中的网格系统》

6.2.4 练习

6.2.4.1 阅读 sidebarLayout() 的文档,确定侧边栏和主面板的宽度(以列为单位)。你能使用 fluidRow() 和 column() 重新创建它的外观吗?你缺少什么?

6.2.4.2 修改中心极限定理应用程序,将侧边栏放在右侧而不是左侧。

6.2.4.3 创建一个包含两个图的应用程序,每个图占一半宽度。将控件放在图下方的全宽容器中。

6.3 多页布局

随着应用程序复杂性的增加,可能无法将所有内容都放在一个页面上。在本节中,您将学习 tabPanel() 的各种用途,以创建多个页面的假象。这是一种假象,因为您仍然只有一个应用程序和一个基础 HTML 文件,但现在它被分解成多个部分,一次只能看到一个部分。

多页应用程序与模块搭配得特别好,你将在第19章了解模块。模块允许你以与划分用户界面相同的方式划分server函数,创建仅通过定义良好的连接进行交互的独立组件。

6.3.1 选项卡集

将页面分解成块的简单方法是使用 tabsetPanel() 及其亲密朋友 tabPanel()。正如您在下面的代码中看到的,tabsetPanel() 为任意数量的 tabPanels() 创建一个容器,而 tabPanels() 反过来又包含任何其他 HTML 组件。图 6.5 显示了一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
ui <- fluidPage(
tabsetPanel(
tabPanel("Import data",
fileInput("file", "Data", buttonLabel = "Upload..."),
textInput("delim", "Delimiter (leave blank to guess)", ""),
numericInput("skip", "Rows to skip", 0, min = 0),
numericInput("rows", "Rows to preview", 10, min = 1)
),
tabPanel("Set parameters"),
tabPanel("Visualise results")
)
)

图6.5 tabsetPanel()允许用户选择单个tabPanel()进行查看

如果您想知道用户选择了哪个选项卡,您可以将 id 参数提供给 tabsetPanel() 并将它设为输入。图6.6显示了这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
textOutput("panel")
),
mainPanel(
tabsetPanel(
id = "tabset",
tabPanel("panel 1", "one"),
tabPanel("panel 2", "two"),
tabPanel("panel 3", "three")
)
)
)
)
server <- function(input, output, session) {
output$panel <- renderText({
paste("Current panel: ", input$tabset)
})
}

图6.6 当您使用id参数时,tabset成为输入。这允许您根据当前可见的选项卡使您的应用程序表现不同。

注意,tabsetPanel()可以在应用程序中的任何地方使用;如果需要,将选项卡集嵌套在其他组件(包括选项卡集!)中是完全可以的。

6.3.2 导航列表和导航栏

因为标签是水平显示的,所以你可以使用的标签数量有一个基本的限制,特别是如果它们的标题很长。navbarPage()navbarMenu()提供了两种替代布局,让你使用更多标签和更长的标题。

navlistPanel()类似于tabsetPanel(),但不会水平显示标签标题,而是在侧边栏中垂直显示。它还允许您使用纯字符串添加标题,如下面的代码所示,生成图6.7。

1
2
3
4
5
6
7
8
9
10
ui <- fluidPage(
navlistPanel(
id = "tabset",
"Heading 1",
tabPanel("panel 1", "Panel one contents"),
"Heading 2",
tabPanel("panel 2", "Panel two contents"),
tabPanel("panel 3", "Panel three contents")
)
)

图6.7 navlistPanel()垂直显示选项卡标题,而不是水平显示

另一种方法是使用navbarPage():它仍然产生水平选项卡标题,但您可以使用navbarMenu()添加下拉菜单以实现额外的层次结构。图6.8显示了一个简单的示例。

1
2
3
4
5
6
7
8
9
10
11
ui <- navbarPage(
"Page title",
tabPanel("panel 1", "one"),
tabPanel("panel 2", "two"),
tabPanel("panel 3", "three"),
navbarMenu("subpanels",
tabPanel("panel 4a", "four-a"),
tabPanel("panel 4b", "four-b"),
tabPanel("panel 4c", "four-c")
)
)

图6.8 navbarPage()在页面顶部生成一个水平导航栏

这些布局为你提供了相当大的能力来创建丰富而令人满意的应用程序。要更进一步,你需要更多地了解底层设计系统。

6.4 引导程序(Bootstrap)

要继续你的应用程序定制之旅,你需要更多地了解 Shiny 所使用的 Bootstrap 框架。Bootstrap 是一系列 HTML 约定、CSS 样式和 JS 代码片段的集合,打包成一种方便的形式。Bootstrap 最初是 Twitter 开发的一个框架,在过去的 10 年里,它已经发展成为网络上最流行的 CSS 框架之一。Bootstrap 在 R 中也很受欢迎——你无疑已经看到过许多由 rmarkdown::html_document() 生成的文档,并使用了许多由 pkgdown 制作的包网站,这两个网站也都使用了 Bootstrap。

作为 Shiny 开发人员,您不需要过多考虑 Bootstrap,因为 Shiny 会自动为您生成与 Bootstrap 兼容的 HTML。但是知道 Bootstrap 存在是件好事,因为这样:

  • 你可以使用bslib::bs_theme()来定制代码的视觉外观,第6.5节。

  • 你可以使用class参数来使用Bootstrap class名定制一些布局、输入和输出,正如你在第2.2.7节中所看到的。

  • 你可以自己编写函数来生成 Shiny 不提供的 Bootstrap 组件,如Utility classes中所述。

也可以使用完全不同的CSS框架。许多现有的R包通过包装流行的Bootstrap替代品来简化这一过程:

你可以在 https://github.com/nanxstats/awesome-shiny-extensions 上找到一个更完整、维护更活跃的列表。

6.5 主题

Bootstrap在R社区中无处不在,很容易让人产生审美疲劳:过了一段时间,每个Shiny应用程序和Rmd看起来都差不多。解决方案是使用bslib包进行主题化。bslib是一个相对较新的包,允许您覆盖许多Bootstrap默认值,以创建独特的外观。在我写这篇文章的时候,bslib主要适用于Shiny,但正在努力将其增强的主题化功能引入RMarkdown、pkgdown等。

如果你正在为公司开发应用程序,我强烈建议你花点时间在主题化上——将你的应用程序主题化,以匹配你的企业风格指南,这是一种让你看起来很好的简单方法。

6.5.1 开始

使用 bslib::bs_theme() 创建一个主题,然后使用页面布局函数的theme参数将其应用于应用程序:

1
2
3
fluidPage(
theme = bslib::bs_theme(...)
)

如果不特别指定,Shiny将使用其自创建以来基本上使用的经典Bootstrap v3主题。默认情况下,bslib::bs_theme()将使用 Bootstrap v4。如果您只使用内置组件,使用Bootstrap v4替代v3不会造成问题。如果您使用了自定义HTML,它可能会造成问题,因此您可以通过设置version = 3来强制它保持v3。

6.5.2 Shiny主题

更改应用程序整体外观的最简单方法是使用bootswatch参数对bslib::bs_theme()选择一个预先制作的bootswatch主题。图6.9显示了以下代码的结果,切换其他主题为“darkly”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ui <- fluidPage(
theme = bslib::bs_theme(bootswatch = "darkly"),
sidebarLayout(
sidebarPanel(
textInput("txt", "Text input:", "text here"),
sliderInput("slider", "Slider input:", 1, 100, 30)
),
mainPanel(
h1(paste0("Theme: darkly")),
h2("Header 2"),
p("Some text")
)
)
)

图6.9 相同的app采用了用四个bootswatch主题风格:darkly、flatly、sandstone和united

此外,你可以利用bs_theme()的其他参数创建自己的主题,如 bg (背景颜色),fg(前景色)和 base_font

1
2
3
4
5
theme <- bslib::bs_theme(
bg = "#0b3d91",
fg = "white",
base_font = "Source Sans Pro"
)

一种预览和自定义主题的简单方法是使用bslib::bs_theme_preview(theme)。这将打开一个Shiny应用程序,展示该主题在应用于许多标准控件时的外观,同时还为你提供交互式控件,用于自定义最重要的参数。

6.5.3 图表主题

如果你已经对你的应用程序样式进行了大量自定义,你可能还想自定义图表以匹配整体风格。幸运的是,这非常简单,多亏了thematic包,它可以自动对ggplot2、lattice和基础图表进行主题化。只需在server函数中调用thematic_shiny()。这将自动确定你的应用程序主题的所有设置,如图6.10所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
library(ggplot2)

ui <- fluidPage(
theme = bslib::bs_theme(bootswatch = "darkly"),
titlePanel("A themed plot"),
plotOutput("plot"),
)

server <- function(input, output, session) {
thematic::thematic_shiny()

output$plot <- renderPlot({
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
geom_smooth()
}, res = 96)
}

图6.10 使用thematic::thematic_shiny()确保ggplot2自动匹配应用程序主题

6.5.4 练习

6.4.5.1 使用bslib::bs_theme_preview()制作你能想到的最丑的主题。

6.6 面罩之下

Shiny的设计是为了让R用户不需要了解HTML的细节。但是,如果你知道一些HTML和CSS,你仍然可以进一步定制Shiny。不幸的是,教授HTML和CSS不在本书的范围内,但MDN的HTMLCSS基础教程是一个很好的起点。

最重要的是要知道,所有输入、输出和布局函数背后都没有魔法:它们只是生成HTML。您可以通过在控制台中直接执行UI函数来查看HTML:

1
2
3
fluidPage(
textInput("name", "What's your name?")
)
1
2
3
4
5
6
<div class="container-fluid">
<div class="form-group shiny-input-container">
<label for="name">What's your name?</label>
<input id="name" type="text" class="form-control" value=""/>
</div>
</div>

请注意,这是 <body> 标签的内容;Shiny 的其他部分负责生成 <head>。如果你想包含额外的 CSS 或 JS 依赖项,你需要学习 htmltools::htmlDependency()。两个好的起点是 https://blog.r-hub.io/2020/08/25/js-r/#web-dependency-managementhttps://unleash-shiny.rinterface.com/htmltools-dependencies.html

可以将自己的 HTML 添加到 ui。一种方法是通过 HTML() 函数包含文本 HTML。在下面的例子中,我使用“原始字符常量”,r"()",以便更容易在字符串中包含引号:

1
2
3
4
5
6
7
8
9
10
ui <- fluidPage(
HTML(r"(
<h1>This is a heading</h1>
<p class="my-class">This is some text!</p>
<ul>
<li>First bullet</li>
<li>Second bullet</li>
</ul>
)")
)

如果您是 HTML/CSS 专家,您可能想知道是否可以完全跳过 fluidPage() 并提供原始 HTML。有关更多详细信息,请参阅“使用 HTML 构建整个 UI”。

或者,您可以使用Shiny提供的HTML辅助函数。对于最重要的元素,如h1()p(),有常规函数,所有其他元素都可以通过其他标签辅助函数访问。命名参数成为属性,未命名参数成为子元素,因此我们可以将上面的HTML重新创建为:

1
2
3
4
5
6
7
8
ui <- fluidPage(
h1("This is a heading"),
p("This is some text", class = "my-class"),
tags$ul(
tags$li("First bullet"),
tags$li("Second bullet")
)
)

使用代码生成 HTML 的一个优点是,你可以将现有的 Shiny 组件交织到自定义结构中。例如,以下代码生成了一段包含两个输出的文本段落,其中一个为粗体:

1
2
3
4
5
6
7
tags$p(
"You made ",
tags$b("$", textOutput("amount", inline = TRUE)),
" in the last ",
textOutput("days", inline = TRUE),
" days "
)

请注意使用inline = TRUEtextOutput()默认是生成一个完整的段落。

要了解更多关于使用HTML、CSS和JavaScript制作引人注目的用户界面的知识,我强烈推荐David Granjon的《Shiny的杰出用户界面》

6.7 总结

本章为你提供了制作复杂而吸引人的Shiny应用程序所需的工具。你已经学习了Shiny函数,这些函数允许你布局单页和多页应用程序(如fluidPage()tabsetPanel()),以及如何使用主题定制整体视觉外观。你还学到了一些关于底层的东西:你知道Shiny使用Bootstrap,并且输入和输出函数只返回HTML,你也可以自己创建自己的HTML。

在下一章中,您将了解有关应用程序中另一个重要视觉组件的更多信息:图形。

加关注

关注公众号“生信之巅”。

生信之巅微信公众号 生信之巅小程序码

敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!Notice: When you use the scripts in this article, please cite the link of this webpage. Thank you!

上一篇:
Shiny从入门到入定——7-图形
下一篇:
Shiny从入门到入定——5-工作流