213-学习大佬新书--《Mastering Shiny》

刘小泽写于2020.10.30 shiny这个工具简单理解就是:基于R就可以做出来网页 看到大佬最近在推销新的主题 bookdown bs4_book template(看着还真的不错)

在线版在:https://mastering-shiny.org/ 感兴趣的朋友可以点进去看看

0 欢迎语

shiny的目的就是不需要让你学习复杂的HTML、CSS、JavaScript就可以做出精美的网页,它的实现主要依赖了:

  • user interface components can be easily customized or extended
  • server uses reactive programming to let you create any type of back end logic you want

读完这本书,你将可以写出自定义UI的apps,更易维护的代码同时兼顾性能与拓展性

1 前言~到底什么是shiny?

shiny是一个R包,可以让你利用R代码做出一个网页,从而使其他人可以轻松访问。

在过去,R语言用户要想创建网站是比较困难的,因为:

  • 需要深入了解HTML, CSS and JavaScript知识
  • 需要逻辑很清晰,哪里是输入,哪里是输出,而且修改了某个输入,还要自己对应找到输出进行更改

而R提供了便利:

  • 为用户提供了一系列user interface (UI for short)函数,可以替代你生成网页所需要的HTML, CSS, and JavaScript内容
  • 提供了一种新的代码编写方式叫: reactive programming ,可以自动追踪代码块的依赖。输入出现了改动,不需要自己找,程序就会自动帮你寻找需要修改的位置

用shiny能做什么呢?

  • 创建dashboards,追踪任务进程
  • 将数百页的结果报表做成网页的形式,方便查找
  • 创造自动化网页工具
  • 达到交互式的数据显示效果

开始前需要准备的R包:

install.packages(c(
  "gapminder", "ggforce", "globals", "openintro", "shiny", 
  "shinycssloaders", "shinyFeedback", "shinythemes", "testthat", 
  "thematic", "tidyverse", "vroom", "waiter", "xml2", "zeallot" 
))

2 来创造第一个Shiny app

2.1 前言

要知道,每个shiny程序主要包含两个重点:

  • UI (short for user interface) which defines how your app looks
  • server function which defines how your app works
# install.packages("shiny")
library(shiny)

2.2 创建shiny app的目录和文件

创建shiny app的方法有很多,其中最简单的是先创建一个目录,然后在其中创建一个叫做app.R的脚本,这个脚本就会告诉shiny我们的程序要长什么样子(通过ui),应该有什么功能(通过server

# 以下是app.R的内容
library(shiny)
ui <- fluidPage(
  "Hello, world!"
)
server <- function(input, output, session) {
}
shinyApp(ui, server)

Rstudio的小tip:File => New project => New Directory => Shiny Web Application

2.3 运行与终止

使用Cmd/Ctrl + Shift + Enter 来运行这个程序,此时你会看到会弹出一个框,里面写着:

然后注意到一句话:其中127.0.0.1代表自己的电脑,6728是一个随机分配的端口号(每个人的会不同)

注意:
  • 在程序显示Listening的时候,你复制粘贴这个http地址到浏览器,也是可以看到这个效果的(但当你退出这个shiny程序后,再粘贴到浏览器就没用啦)

  • shiny程序运行时候,他会占用Rstudio的console,意味着在结束之前你没办法运行新的命令

  • 终止方法:点弹出的shiny窗口的叉号;按esc或者ctrl + C

2.4 添加UI组件

之前我们的ui非常简陋:

ui <- fluidPage(
    "Hello, world!"
)

现在将它替换成:

ui <- fluidPage(
  selectInput("dataset", label = "Dataset", choices = ls("package:datasets")),
  verbatimTextOutput("summary"),
  tableOutput("table")
)

解释一下这几个新的名词(当然在后面章节会介绍更多):

  • fluidPage():这是一个关于布局的函数,你看它被放在了最外边,那是因为我们肯定要先设计好一个程序界面的布局吧
  • selectInput():这是一个关于输入的函数,让用户可以交互式在程序输入。这里我们的输入框是多选,名称是“Dataset”,可选项是R的内置数据集
  • verbatimTextOutput()tableOutput():是关于输出的函数,前者展示代码,后者展示表格

运行这些代码,就看到了:

2.5 将结果输出

前面只是可以实现了选择,但选择后结果并没有输出,这是因为结果的输出依赖server函数。注意:这里我们只是告诉shiny我们要做什么,而不是真的让shiny这个函数去实现【比如我们只是提供给“shiny”服务员一个菜谱,而不是直接让“shiny”服务员去给我们做菜】

接下来,我们只需要补充上server这个函数,告诉shiny怎么去填充summarytable的结果

server <- function(input, output, session) {
  output$summary <- renderPrint({
    dataset <- get(input$dataset, "package:datasets")
    summary(dataset)
  })
  
  output$table <- renderTable({
    dataset <- get(input$dataset, "package:datasets")
    dataset
  })
}

注意到这里有一个共性就是:

output$ID <- renderTYPE({
  # Expression that generates whatever kind of output
  # renderTYPE expects
})
  • output$ID:指的是你告诉shiny要做的某一项事情(比如在菜单上点了某一道菜),这个ID实际上与ui代码中的xxxOutput(ID)是对应的
  • renderTYPE:其中的TYPE种类多种多样
  • renderTYPE中的数据这里并没有直接指定,而是用input$dataset表示,这样在交互式界面,即使更换了数据,程序也会自动重新计算、更新

2.6 减少重复的代码

可以看到,上面的server例子中,都出现了一句:dataset <- get(input$dataset, "package:datasets")

重复的代码会带来一些坏处:

  • 首先不美观
  • 重复计算
  • 不好维护、纠错

记得在平时R脚本编写中,处理这种重复代码一般有两个方法:

  • 新增一个变量来存储重复代码的结果
  • 将重复运行的代码写入函数

但这里,没有特定数据,因此也得不到一个确切的值,因此第一个方法不成立;第二个方法实现起来好像比重复写两次还麻烦

于是在shiny中,有自己处理重复代码的方式:reactive expressions。它的思路是:利用reactive({...}) 把重复代码加进来,并赋给一个新的变量(或者理解成一个新的函数)。与传统函数不同的是:只运行一次,并将结果放进内存的缓冲区,之后需要的时候再次调用

更新后的server脚本是:

server <- function(input, output, session) {
  dataset <- reactive({
    get(input$dataset, "package:datasets")
  })

  output$summary <- renderPrint({
    summary(dataset())
  })
  
  output$table <- renderTable({
    dataset()
  })
}

2.7 最好的辅助资料——Cheat sheet

  • 全部的在:https://www.rstudio.com/resources/cheatsheets/
  • shiny的在:https://github.com/rstudio/cheatsheets/raw/master/shiny.pdf

2.8 练习

2.8.1 练习一

需求:输入名字,输出问候语

library(shiny)
ui <- fluidPage(
    textInput("name", "What's your name?"),
    textOutput("greeting")
)
server <- function(input, output, session) {
    output$greeting <- renderText({
        paste0("Hello ", input$name)
    })
}
shinyApp(ui, server)

2.8.2 练习二

需求:设置一个滑块,用户滑动得到数值x5的结果

library(shiny)
ui <- fluidPage(
    sliderInput("x", label = "If x is", min = 1, max = 50, value = 30),
    "then x times 5 is",
    textOutput("product")
)

servera <- function(input, output, session) {
    output$product <- renderText({ 
        input$x * 5
    })
}
shinyApp(ui, server)

2.8.3 练习三

需求:在2.8.2基础上,允许用户自定义乘数

library(shiny)
ui <- fluidPage(
    sliderInput("x", label = "If x is", min = 1, max = 50, value = 30),
    sliderInput("y", label = "and x is", min = 1, max = 50, value = 30),
    "then x times y is",
    textOutput("product")
)

server <- function(input, output, session) {
    output$product <- renderText({ 
        input$x * input$y
    })
}
shinyApp(ui, server)

2.8.4 练习四

需求:看下面👇代码的作用,并且改写(利用reactive expressions让代码更简洁)

# 原代码
ui <- fluidPage(
  sliderInput("x", "If x is", min = 1, max = 50, value = 30),
  sliderInput("y", "and y is", min = 1, max = 50, value = 5),
  "then, (x * y) is", textOutput("product"),
  "and, (x * y) + 5 is", textOutput("product_plus5"),
  "and (x * y) + 10 is", textOutput("product_plus10")
)

server <- function(input, output, session) {
  output$product <- renderText({ 
    product <- input$x * input$y
    product
  })
  output$product_plus5 <- renderText({ 
    product <- input$x * input$y
    product + 5
  })
  output$product_plus10 <- renderText({ 
    product <- input$x * input$y
    product + 10
  })
}

它的作用是:输入x、y,分别计算乘积以及乘积后+5、+10结果

看到,这里计算乘积是重复使用的,因此可以将乘积的计算包含进reactive

# 我想到的,可以将重复使用的product <- input$x * input$y,变成一个单独的复用变量
server <- function(input, output, session) {
    multi <- reactive({
        input$x * input$y
    })
    
    output$product <- renderText({ 
        multi()
    })
    output$product_plus5 <- renderText({ 
        multi() + 5
    })
    output$product_plus10 <- renderText({ 
        multi()+ 10
    })
}

3 了解一下基础的UI设计

3.1 前言

前面已经会使用了最简单的app设计代码,可以清楚看到shiny的设计理念就是:把界面语言(前端)与操作语言(后端)分离开。这一章,将会深入探索前端部分,学习HTML输入、输出以及布局。

了解前端的好处就是可以帮助我们构建视觉效果更好的并且使用简单的shiny app,下一章会介绍后端的知识(敬请期待。。。)

3.2 关于输入

前面看到在ui的脚本中,会有:sliderInput(), selectInput(), textInput(), numericInput() 等等操作,比如:

selectInput("dataset", "Dataset", choices = datasets),
verbatimTextOutput("summary"),
plotOutput("plot")

现在来看看,都有哪些常用的ui模块吧

3.2.1 输入的变量名称 =》 inputId

不管是哪种输入模块,都必须一个inputId,这个ID是与后端的计算环节相关联的:我们指定一个名称(name),后端看到这个名称会去计算,计算完再返回前端去显示(input$name

这个id也不是随意命名的,需要符合:

  • 简单的字符串:只能包含字母、数字、下划线(不能有空格、长横线、句号以及其他特殊符号)
  • 必须唯一

许多输入模块除了这个id的设定,还有第二个参数:label ,也就是显示出来的名称是什么,这个label名称并没有严格的限制,只要能让别人知道你这个模块是干什么的即可。

当然,只要名字还不行,有的还需要其他参数(比如:sliderInput中需要设置默认数值)

为了保证代码清晰并且易懂,推荐inputIdlabel 直接写,其他参数需明确指定含义,例如:

sliderInput("min", "Limit (minimum)", value = 50, min = 0, max = 100)

3.2.2 文本收集器

看一眼这个就明白:

ui <- fluidPage(
  textInput("name", "What's your name?"),
  passwordInput("password", "What's your password?"),
  textAreaInput("story", "Tell me about yourself", rows = 3)
)

是这样的结果:

3.2.3 数字收集器

可以使用滑块调整(还可以用两个滑块获取区间数值),或者直接输入数字

ui <- fluidPage(
  numericInput("num", "Number one", value = 0, min = 0, max = 100),
  sliderInput("num2", "Number two", value = 50, min = 0, max = 100),
  sliderInput("rng", "Range", value = c(10, 20), min = 0, max = 100)
)

推荐不要试图用滑块选取比较精确的数值(关于滑块的设置,还有更多:https://shiny.rstudio.com/articles/sliders.html)

3.2.4 日期收集器

单个日期用:dateInput() ;日期区间用:dateRangeInput()

ui <- fluidPage(
  dateInput("dob", "When were you born?"),
  dateRangeInput("holiday", "When do you want to go on vacation next?")
)

日期设置默认采用美国标准,当然可以自定义:format, language, weekstart

3.2.5 选择收集器

单选:可以用 selectInputradioButtons

animals <- c("dog", "cat", "mouse", "bird", "other", "I hate animals")

ui <- fluidPage(
  selectInput("state", "What's your favourite state?", state.name),
  radioButtons("animal", "What's your favourite animal?", animals)
)

  • radioButtons会将所有选项显示出来,直观同时占用了一部分空间,适用于选择数量较少的情况

  • 另外radioButtons还支持符号替代文字,例如:

    ui <- fluidPage(
      radioButtons("rb", "Choose one:",
        choiceNames = list(
          icon("angry"),
          icon("smile"),
          icon("sad-tear")
        ),
        choiceValues = list("angry", "happy", "sad")
      )
    )
    

多选:可以用selectInputcheckboxGroupInput

ui <- fluidPage(
  selectInput(
    "state", "What's your favourite state?", state.name,
    multiple = TRUE
  )
)

ui <- fluidPage(
  checkboxGroupInput("animal", "What animals do you like?", animals)
)

3.2.6 文件收集器

ui <- fluidPage(
  fileInput("upload", NULL)
)

3.2.7 鼠标点击收集器

意思就是:设置一个按钮,你点击一下它,就会触发某种反应,可能是打开了一个网页或者切换到另一个数据

常用的是:actionButtonactionLink ,它们会与server中的observeEvent() 或者eventReactive()对应【server的部分后面再看】

ui <- fluidPage(
  actionButton("click", "Click me!"),
  actionButton("drink", "Drink me!", icon = icon("cocktail"))
)

还有丰富的样式(class)可供选择:

# class的外观范围有:
 "btn-primary", "btn-success", "btn-info", "btn-warning", or "btn-danger"
# class的大小范围有:
"btn-lg", "btn-sm", "btn-xs"

# 示例
ui <- fluidPage(
  fluidRow(
    actionButton("click", "Click me!", class = "btn-danger"),
    actionButton("drink", "Drink me!", class = "btn-lg btn-success")
  ),
  fluidRow(
    actionButton("eat", "Eat me!", class = "btn-block")
  )
)

涉及到了CSS (Cascading Style Sheets 网页样式表)的知识:http://bootstrapdocs.com/v3.3.6/docs/css/#buttons

3.2.8 练习

练习一:文本收集器中设置默认值
ui <- fluidPage(
  textInput('name',"Input box",value = "Your name")
)

练习二:怎么设置日期的滑块

不需要自己绞尽脑汁去想,简单搜索即可:https://stackoverflow.com/questions/40908808/how-to-sliderinput-for-dates

ui <- fluidPage(
  sliderInput("date",
              "When should we deliver?",
              min = as.Date("2020-01-01","%Y-%m-%d"),
              max = as.Date("2020-12-01","%Y-%m-%d"),
              value=as.Date("2020-10-01"),timeFormat="%Y-%m-%d")
))

练习三:选择收集器中进行分组

如果选项太多,我们可以通过分组进行区分,但怎么显示不同的分组呢?

同样是搜索selectInput optgroup:https://stackoverflow.com/questions/33791564/r-shiny-selectinput-multiple-option-groups-not-working-with-one-option

ui <- fluidPage(
  selectInput("test", "I am test", choices = list("Group A" = c("a", "b", "c"), "Group B" = c("d","e","f")))
)

练习四:滑块的设置

设置滑块为0-100,且区间为5,并且设置动画,让滑块自己滑动

ui <- fluidPage(
  sliderInput("num2", "Number two", value = 5, min = 0, max = 100,
              step = 5, animate = T)
)

下面这个三角按钮按下去,滑块就会以每5个数的步长滑动

3.3 关于输出

ui数据输入 =》 server数据计算 =》 ui数据输出,这是一个正常的思维逻辑。

和输入一样,输出也是需要有ID的,得让程序知道我们想要得到哪个的计算结果,例如之前输入数据有一个plot的ID,然后经过server的计算、绘图,最后结果也需要用output$plot来呈现。并且需要注意:每一个ui中输出的output,都伴随着一个server中的render 【render的意思是:“提交”,ui交给server的任务,server干完再提交回去】

输出的东西主要有三类:文字、表格、绘图

3.3.1 文字输出

有两种方法:

  • textOutput() + renderText():一般描述性的文字用即可
  • **verbatimTextOutput() + renderPrint():**代码可以用,其中verbatim单词表示“逐字地”

看到这里,你也许会想,它们之间的区别到底在哪里呢?

如果代码使用verbatimTextOutput()

ui <- fluidPage(
  textOutput("text"),
  verbatimTextOutput("code")
)
server <- function(input, output, session) {
  output$text <- renderText({ 
    "Hello friend!" 
  })
  output$code <- renderPrint({ 
    summary(1:10) 
  })
}
# 结果是:

如果代码也使用textOutput()

ui <- fluidPage(
  textOutput("text"),
  textOutput("code")
)
server <- function(input, output, session) {
  output$text <- renderText({ 
    "Hello friend!" 
  })
  output$code <- renderText({ 
    summary(1:10) 
  })
}
# 结果是:

因此区别就是:verbatimTextOutput真的是逐字逐句输出完整的代码信息

注意:这里renderXXX函数中的{}不是必须的,如果你是单行命令,只使用()也是可以的,例如下面👇这样也是对的:

server <- function(input, output, session) {
  output$text <- renderText("Hello friend!")
  output$code <- renderPrint(summary(1:10))
}

3.3.2 表格输出

也有两种方法:

  • tableOutput() + renderTable() :输出静态数据,并且是全部展示出来【如果表达数据不多还可以用用】
  • dataTableOutput() + renderDataTable():输出动态数据,就像DT::datatable()函数一样【数据量大效果不错】

具体它们的区别,看代码:

ui <- fluidPage(
  tableOutput("static"),
  dataTableOutput("dynamic")
)
server <- function(input, output, session) {
  output$static <- renderTable(head(mtcars)) # 输出部分
  output$dynamic <- renderDataTable(mtcars, options = list(pageLength = 5)) # 输出全部,并可以设置一页显示多少数据
}

3.3.3 输出绘图

plotOutput() + renderPlot():可以输出base包、ggplot2等绘制的图片

ui <- fluidPage(
  plotOutput("plot", width = "400px")
)
server <- function(input, output, session) {
  output$plot <- renderPlot(plot(1:5), res = 96)
}

  • 默认情况下,plotOutput() 会尽可能占满整个输出窗口,当然可以自定义heightwidth参数来限制。
  • 比较推荐的设置是:res = 96,这个分辨率会和Rstudio输出的一致,看起来会比较舒服
  • 当然绘图比较特殊,它既是输出也可作输入。比如plotOutput() 就有很多参数:click, dblclick, hover等等,当设置click = "plot_click"时,便会自动生成一个可复用的输入变量input$plot_click ,方便对图片进一步修整【后面章节会提到】

3.3.4 输出下载

之前说过输入中可以上传文件,那么输出中就可以下载文件,可以使用downloadButton() 或者 downloadLink(),但它的实现需要server的配合【具体使用后面章节会提到】

3.3.5 练习

练习一:修改绘图参数

重复3.3.3的内容,这次设置高为300px,宽为700px

ui <- fluidPage(
  plotOutput("plot", width = "700px",height = "300px")
)
server <- function(input, output, session) {
  output$plot <- renderPlot(plot(1:5), res = 96)
}

练习二:修改表格显示参数

使用 dataTableOutput(),但只显示表格数据,不显示其他的搜索、排序、过滤等等选项

可以看:https://datatables.net/reference/option/dom 或者 https://stackoverflow.com/questions/35624413/remove-search-option-but-leave-search-columns-option

只需要设置dom='t',就会只显示数据框

ui <- fluidPage(
  dataTableOutput("table")
)
server <- function(input, output, session) {
  output$table <- renderDataTable(mtcars, 
                                  options = list(pageLength = 5,
                                                 dom = 't'))
}

3.4 关于布局

前面提到如何设置输入、输出,那么如果我们设置了一堆的组件,怎么摆放也是个问题

这里我们主要关注fluidPage()的使用,它也是众多app使用的布局方案。随着学习的深入,之后也会了解到dashboardsdialog boxes 的使用

3.4.1 概述

布局其实也是有等级的,就像:标题 =》 按钮 =》 文字说明 =》 结果输出 等一系列排列起来的。当你看到下面👇这一堆代码时,别慌

fluidPage(
  titlePanel("Hello Shiny!"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("obs", "Observations:", min = 0, max = 1000, value = 500)
    ),
    mainPanel(
      plotOutput("distPlot")
    )
  )
)

其实我们不需要管每个函数中的各个参数,只看主干即可:

fluidPage(
  titlePanel(),
  sidebarLayout(
    sidebarPanel(
      sliderInput("obs")
    ),
    mainPanel(
      plotOutput("distPlot")
    )
  )
)

现在是不是清晰一点?即使不了解fluidPage()的使用,也能猜出个大概:顶部是个标题框,然后是个侧边栏(其中又包括了一个滑块),最后是主框(包含了一个图片输出)

3.4.2 首先看页面的功能

如果你尝试单独运行fluidPage(),会看到一片空白,但事实上,这个函数背后做了很多工作,比如搭建起了shiny需要的HTML, CSS 和 JS 框架。fluidPage()利用了一个叫做“Bootstrap”的布局系统来提供这些默认的框架设置。

可以这么说,有了fluidPage(),你就有了画图的画板,后面再加上自己摆放的创意就行啦。

至于怎么摆放,虽说看个人意愿,但通过许多人的测试,有两套比较主流的方案:侧边栏方案以及多行组合方案

3.4.2 方案一:侧边栏方案

首先它的布局是:

代码样式是:

fluidPage(
  titlePanel(
    # app title/description
  ),
  sidebarLayout(
    sidebarPanel(
      # inputs
    ),
    mainPanel(
      # outputs
    )
  )
)

再来一个实例:

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)
}

3.4.3 方案二:多行组合方案

首先它的布局是:

代码样式是:

fluidPage(
  fluidRow(
    column(4, 
      ...
    ),
    column(8, 
      ...
    )
  ),
  fluidRow(
    column(6, 
      ...
    ),
    column(6, 
      ...
    )
  )
)
  • 注意其中column的第一个参数指的是宽度,并且每一行规定:不管有多少column,加起来宽度是12

3.4.5 主题

https://rstudio.github.io/shinythemes/ 以及 https://dreamrs.github.io/fresh/ 都提供了大量的主题

# install.packages("shinythemes")
ui <- fluidPage(
  # 加下面这句
  theme = shinythemes::shinytheme('sandstone'),
  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)
}

3.4.6 练习

练习:将侧边栏改成右侧

只需要在sidebarLayout设置position即可

ui <- fluidPage(
  theme = shinythemes::shinytheme('sandstone'),
  titlePanel("Central limit theorem"),
  sidebarLayout(
    position="right",
    sidebarPanel(
      numericInput("m", "Number of samples:", 2, min = 1, max = 100)
    ),
    mainPanel(
      plotOutput("hist")
    )
  )
)

未完待续。。。

Yunze Liu
Yunze Liu
Bioinformatics Sharer

Co-founder of Bioinfoplanet(生信星球)

Next
Previous

Related