incl汇编
先进的R Shiny技巧系列 (The advanced R Shiny tips series)
前言 (PREAMBLE)
Shiny has become an extremely popular solution to build interactive web apps straight from R. So popular that most data & analytics teams in companies have adopted this framework as part of their toolkit.
Shiny已成为直接从R生成交互式Web应用程序的极受欢迎的解决方案。 如此流行,以致公司中的大多数数据和分析团队都采用了该框架作为其工具包的一部分。
However, R Shiny is often said to be a good framework for fast prototyping, but not robust enough for industrialized, scalable solutions running into production.
然而,R Shiny通常被认为是用于快速原型制作的良好框架,但对于投入生产的工业化,可扩展解决方案而言,不够强大。
With this series, I share some advanced tips with the ambition to overcome these objections and to help you build robust/ sustainable/ scalable applications, ready for business usage in production!
在本系列中,我将分享一些高级技巧,以克服这些反对并帮助您构建健壮/可持续/可扩展的应用程序,以准备在生产中用于企业 !
Production being defined as follows:
生产定义如下:
Software environments that are used and relied on by real users, with real consequences if things go wrong — Joe Cheng, CTO@RStudio
实际用户使用和依赖的软件环境,如果出现问题将产生实际后果— Joe Cheng,CTO @ RStudio
Note: in order to get the most from this tutorial, interested reader should already be familiar with basic Shiny principles (e.g. reactivity), R programing (e.g. function definition, tidyverse, purrr::map()), web development (e.g. html tags and css), and RStudio projects.
注意 :为了从本教程中获得最大收益,有兴趣的读者应该已经熟悉了Shiny的基本原理(例如React性),R编程(例如函数定义,tidyverse, purrr :: map() ),Web开发(例如html标签)和CSS),以及RStudio项目 。
目标与动机 (OBJECTIVE AND MOTIVATION)
In this tutorial, I will focus on describing a reproducible way to dynamically generate UI components based on user inputs.
在本教程中,我将重点介绍一种可重现的方法,该方法可根据用户输入动态生成UI组件 。
This feature is very useful in countless situations, as users of your applications are likely to display an a-priori unknown number of information on their dashboard. To list just a few situations:
此功能在无数情况下非常有用,因为应用程序的用户可能会在其仪表板上显示先验未知数量的信息 。 仅列出几种情况:
Add/ remove stocks from a user portfolio management application
从用户投资组合管理应用程序添加/删除库存
Consider/ ignore specific events in a time series forecasting application
考虑/忽略时间序列预测应用程序中的特定事件
Include/ exclude stores in a retailer performance analysis application
在零售商绩效分析应用程序中包括/排除商店
- … …
In addition, I’ll also describe how to define observers to trigger actions from dynamically generated buttons which is not a trivial task (as it requires a bit of JavaScript).
另外,我还将描述如何定义观察者以触发来自动态生成的按钮的动作,这不是一件容易的事(因为它需要一些JavaScript)。
闪亮的APP同伴 (A SHINY APP COMPANION)
This tutorial comes with a R Shiny demo application which you can access here.
本教程随附R Shiny演示应用程序,您可以在此处访问。
In this particular educational example, you (the user) can select as many Stranger Things characters as desired, and display basic information for each of them, through what I’ll call an infocard.
在这个特殊的教育范例, 你 (用户) 可根据需要尽可能多的奇怪的事情字符选择,并显示他们每个人的基本信息 ,通过我称之为一个信息卡 。
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/bb5ca877b50a2832db96f522e8ad584e.png)
Each character’s infocard offers additional capabilities:
每个角色的信息卡 提供附加功能:
Access to the detailed Wikipedia page of the actor (clicking on the ‘More’ button)
访问演员的详细维基百科页面 (单击“更多”按钮)
Possibility to remove the infocard (clicking on the ‘Remove’ button)
删除信息卡的可能性 (点击“删除”按钮)
My advice: in order to get the most out of this tutorial, I recommend to try rebuilding the application by combining the various pieces of code provided throughout the article. Main blocks are numbered in a logical way so that you do not get lost along the way. If necessary, the entire code is also available on my bitbucket account here. Please have a look to see where to store images in the project structure.
我的建议 :为了充分利用本教程,我建议尝试通过结合本文中提供的各种代码来尝试重建应用程序。 主块以合理的方式编号,这样您就不会迷路。 如有必要,整个代码也可以在我的bitbucket帐户上找到 。 请看一下将图像存储在项目结构中的位置。
0 —分析顺序 (0 — SEQUENCE OF ANALYSIS)
My approach to guide you through the code is the following:
我指导您完成代码的方法如下:
Set the scene: description of required R packages, underlying dataset, and application backbone
场景设置 :所需R包,基础数据集和应用程序主干的描述
Define user inputs: description of HTML widgets used in the application to capture user inputs
定义用户输入 :应用程序中用于捕获用户输入HTML小部件的描述
Define infocard function: a function to generate HTML code which will be very useful to generate infocards ‘on the fly’, in combination with the purrr::map() function
定义信息卡功能 :与purrr :: map()函数结合使用的,用于生成HTML代码的功能,对于“即时”生成信息卡非常有用。
Server side definition: creation of reactive values and observers to make the application ‘shine’
服务器端定义 :创建React性值和观察者以使应用程序“发光”
1 —设置场景 (1 — SET THE SCENE)
To build the R Shiny companion app for this tutorial:
要为本教程构建R Shiny配套应用,请执行以下操作:
I used the 4 following packages: shiny, shinythemes, shinyWidgets, and tidyverse
我用了4个以下软件包 : 有光泽 , shinythemes , shinyWidgets ,和tidyverse
# PART 1 - SETTING THE SCENE# section 1.0 - load required packageslibrary(shiny) # web app framework for R
library(shinythemes) # themes for shiny
library(shinyWidgets) # custom input widgets for shiny
library(tidyverse) # collection of R packages for data science
I built a small dataset with basic information relative to main Stranger Things’ characters (source: Wikipedia):
我建立了一个小型数据集,其中包含有关Stranger Things主要角色的基本信息(来源: Wikipedia ):
# section 1.1 - build datasetcrm_tbl <- tibble(
character = c("Joyce Byers", "Jim Hopper",
"Mike Wheeler", "Jane Hopper",
"Dustin Henderson", "Lucas Sinclair",
"Nancy Wheeler", "Jonathan Byers",
"Karen Wheeler", "Will Byers",
"Max Mayfield", "Steve Harrington"),
actor = c("Winona Ryder", "David Harbour",
"Finn Wolfhard", "Millie Bobby Brown",
"Gaten Matarazzo", "Caleb McLaughlin",
"Natalia Dyer", "Charlie Heaton",
"Cara Buono", "Noah Schnapp",
"Sadie Sink", "Joe Keery"),
image = c("img/winona_ryder.jpg", "img/david_harbour.jpg",
"img/finn_wolfhard.jpg", "img/millie_bobby_brown.jpg",
"img/gaten_matarazzo.jpg", "img/caleb_mclaughlin.jpg",
"img/natalia_dyer.jpg", "img/charlie_heaton.jpg",
"img/cara_buono.jpg", "img/noah_schnapp.jpg",
"img/sadie_sink.jpg", "img/joe_keery.jpg"),
wiki = c("https://en.wikipedia.org/wiki/Winona_Ryder",
"https://en.wikipedia.org/wiki/David_Harbour",
"https://en.wikipedia.org/wiki/Finn_Wolfhard",
"https://en.wikipedia.org/wiki/Millie_Bobby_Brown",
"https://en.wikipedia.org/wiki/Gaten_Matarazzo",
"https://en.wikipedia.org/wiki/Caleb_McLaughlin",
"https://en.wikipedia.org/wiki/Natalia_Dyer",
"https://en.wikipedia.org/wiki/Charlie_Heaton",
"https://en.wikipedia.org/wiki/Cara_Buono",
"https://en.wikipedia.org/wiki/Noah_Schnapp",
"https://en.wikipedia.org/wiki/Sadie_Sink",
"https://en.wikipedia.org/wiki/Joe_Keery")
)
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/02270aa4ddc37695a16006de26a66b34.png)
I chose to initialise the application with 2 active characters, meaning that 2 infocards will be displayed when the app starts:
我选择使用2个活动字符初始化应用程序,这意味着在应用程序启动时将显示2个信息卡:
# section 1.2 - set initial selection current_user_selection <- c("Jim Hopper", "Joyce Byers")
Lastly I built a basic backbone for the application, made of:
最后,我为该应用程序构建了一个基本的主干 ,该主干由以下各项组成:
a navbarPage (blank bootstrap 3 page with top level navigation bar)
一个navbarPage (带有顶层导航栏的空白引导3页面)
a left sidebar (I used shiny::wellPanel() function)
左侧边栏(我使用了Shiny :: wellPanel()函数)
- an infocard area 信息卡区域
# PART 2 - UI PART# app backboneui <- navbarPage(
title = "R Shiny advanced tips series",
collapsible = TRUE,
windowTitle = "R Shiny tips", tabPanel(
title = "Demo",# section 2.1 - header # section 2.2 - sidebar panel
div(
class = "container",
column(
width = 3,wellPanel(p("PLACEHOLDER - USER INPUTS")
)
),
column(
width = 9,
div(
# render thumbnails dynamically (ui)p("PLACEHOLDER - INFOCARDS")
)
)
)
)
)# PART 3 - SERVER PART
server <- function(input, output, session) {
}# PART 4 - RUN APPLICATION
shinyApp(ui = ui, server = server)
The header (backbone’s section 2.1) is a simple container where I display basic information about the application. I took advantage of existing bootstrap classes to make the text eye-catching (see page-header and lead classes)
标头(主干的2.1节)是一个简单的容器,在其中显示有关应用程序的基本信息。 我利用现有的引导程序类使文本引人注目(请参阅页面标题和潜在客户类)
# >> insert in section 2.1div(
class = "container",
h1(class = "page-header",
"Generate dynamic UI components",
tags$small("- Thomas Filaire")),
p(class = "lead",
"Learn how to generate UI components dynamically based on user inputs"),
p("Note: this application is the companion of my Medium post; read it to understand its underlying principles!")
)
2 — DEFINE USER INPUTS
2 —定义用户输入
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/2e68d6341f9a958eaba720aaa2129e49.png)
To illustrate the dynamic UI functionality, I built a very simple selector for the user to choose her/his favorite Stranger Things character. Once selected, just click on the ‘Add’ button to show the corresponding infocard.
为了说明动态UI功能,我构建了一个非常简单的选择器 ,供用户选择她/他最喜欢的Stranger Things角色。 选择后,只需单击“添加” 按钮即可显示相应的信息卡。
While the basic selectInput() from shiny package is great and I use it most of the time, I made the choice to opt for the shinyWidget::pickerInput() alternative, which presents many benefits, in particular live search capabilities. Test it and I’m sure you’ll adopt it!
尽管Shiny包中的基本selectInput()非常好,并且我大部分时间都在使用它,但我还是选择了ShinyWidget :: pickerInput()替代方案,它具有很多优点,特别是实时搜索功能 。 测试它,我相信您会采用它!
# >> insert inside wellPaneldiv(
shinyWidgets::pickerInput(
inputId = "pi_character_selector",
label = "Select character(s)",
choices = crm_tbl %>% pull(character),
multiple = FALSE,
selected = "Mike Wheeler",
options = pickerOptions(
actionsBox = FALSE,liveSearch = TRUE,
size = 10
)
)
),
div(actionButton(
class = "btn-primary",
inputId = "ab_show_character",
label = "Add"
)
)
)
3 —定义信息卡功能 (3 — DEFINE INFOCARD FUNCTION)
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/00db219f069787af3509323fa56c16d4.png)
I built a function to generate infocard (thus an HTML piece of code), which I will call later on from the server side in a purrr::map() function to apply it to a list of Stranger Things characters.
我构建了一个函数来生成信息卡 (因此是一段HTML代码),稍后我将在服务器端通过purrr :: map()函数调用该信息卡,以将其应用于陌生人事物字符列表。
As an R Shiny programers you should embrace functional programing as it forces you to create functions that can easily be analyzed in isolation, and avoid unnecessary duplication of code, improving readability.
作为R Shiny程序员,您应该接受函数式编程,因为它会迫使您创建可以轻松地独立分析的函数,并避免不必要的代码重复,从而提高了可读性。
The function takes a character’s name as an input, and return the infocard (HTML object). 3 key important things to notice:
该函数以字符名称作为输入,并返回信息卡 (HTML对象)。 注意的3个重要重要事项:
note how rendering HTML piece of code is as easy as generating numbers, list, chart, etc., which we usually do with functions.
请注意, 呈现HTML代码段与生成数字,列表,图表等一样容易 ,而这通常是我们使用函数来完成的。
note the usage of bootstrap 3's pre-existing classes (img-rounded, caption, btn-default), plus the rm_btn class assigned to the ‘remove button’ on which I will further elaborate in part 4 of this tutorial.
请注意引导程序3预先存在的类 (img舍入,标题,btn-default)以及分配给“删除按钮”的rm_btn类的用法,我将在本教程的第4部分中进一步阐述。
note the usage of the immensely helpful stringr::str_glue() function which I used to assign unique ids to each button. This is a key part as ids must be unique and are required to trigger actions when respective buttons are clicked.
请注意非常有用的stringr :: str_glue()函数的用法,该函数曾经为每个按钮分配唯一的ID。 这是关键部分,因为ID必须是唯一的,并且单击相应按钮时才需要ID来触发操作 。
str_glue() makes is easy to insert the value of a variable within a character string. In order to make each button’s id unique I adopted the following pattern:
str_glue()使在字符串中插入变量的值变得容易。 为了使每个按钮的ID唯一,我采用了以下模式:
for ‘more’ buttons: ab_more_character_name (ab stands for actionbutton).
对于“更多”按钮: ab_more_character_name (ab代表actionbutton)。
for ‘remove’ buttons: ab_remove_character_name.
对于“删除”按钮: ab_remove_character_name 。
E.g. ab_more_joyce_byers, ab_remove_joyce_byers, etc.
例如ab_more_joyce_byers,ab_remove_joyce_byers等。
I also used functions from the stringr package to lowercase/uppercase selected character’s name and to add/remove “_” so as to make string a valid id.
我还使用了stringr包中的函数来对所选字符的名称进行小写/大写,并添加/删除“ _”以使字符串成为有效的ID。
# >> insert infocard definition as section 1.3 in PART 1 - SETTING THE SCENE of your app.R file# section 1.3 - define custom infocardmy_infocard <- function(selected_character) {
# selected_character == value from user pickerInput
# e.g. "Mike Wheeler"
selected_image <- crm_tbl %>%
filter(character == selected_character) %>%
pull(image) # get image url from dataset
selected_actor <- crm_tbl %>%
filter(character == selected_character) %>%
pull(actor) # get actor's name from dataset
selected_wiki <- crm_tbl %>%
filter(character == selected_character) %>%
pull(wiki) # get wikipedia's link from dataset
# piece of UI to render dynamically
column(
width = 4,
div(
class = "thumbnail text-center",
# picture
img(
class = "img-rounded",
style = "height: 300px;",
src = selected_image
),
# main information
div(
class = "caption",
h4(selected_character),
p(selected_actor)
),
# link to wikipedia's pageactionButton(
class = "btn-default",
inputId = str_glue("ab_more_{selected_character %>%
tolower() %>%
str_replace_all(' ', '_')}"),
label = "More",
onclick = str_glue("window.open('{selected_wiki}', '_blank')")
), # remove buttonactionButton(
class = "btn-default rm_btn",
inputId = str_glue("ab_remove_{selected_character %>%
tolower() %>%
str_replace_all(' ', '_')}"),
label = "Remove"
)
)
)
}
4 —服务器端定义 (4 — SERVER SIDE DEFINITION)
Once we have defined the backbone of the application, and built the function to generate the custom-made infocards, let’s jump on the server side.
一旦定义了应用程序的主干,并构建了用于生成定制信息卡的功能,就让我们跳到服务器端。
First, I used an eventReactive() function to capture any new character which the user selects through the pickerInput widget. eventReactive() allows the reactive components to update only when the user confirms her/his choice by clicking on the ‘Add’ button (which id is input$ab_show_character).
首先,我使用eventReactive()函数捕获用户通过pickerInput小部件选择的任何新字符。 eventReactive()仅在用户通过单击“添加”按钮(其ID为input $ ab_show_character)确认她/他的选择时,才允许更新React性组件。
# >> insert in server part # get user's selected character when 'Add' button is clicked
current_character <- eventReactive(
eventExpr = input$ab_show_character, {
input$pi_character_selector
})
Whenever a new character is selected by the user, information must be captured and added to previous list of selected characters. I used a reactiveValues() to store selected characters, initialized with current_user_selection defined as per part 1.
每当用户选择一个新字符时,都必须捕获信息并将其添加到以前的所选字符列表中。 我使用了reactValues()来存储选定的字符,并使用第1部分中定义的current_user_selection进行了初始化。
An observeEvent updates reactive_values whenever the ‘Add’ button is clicked. I inserted a unique() in the pipeline to make sure infocards are note created multiple times, and thus are displayed only once.
每当单击“添加”按钮时,observeEvent都会更新react_values。 我插一个独特的()的管道,以确保InfoCard的是笔记创建多次,从而只显示一次。
# >> insert in server part# store user's selection
reactive_values <- reactiveValues()
reactive_values$character_list <- current_user_selection# add character when relevant
observeEvent(input$ab_show_character, {
reactive_values$character_list <-
c(reactive_values$character_list,
current_character()) %>% unique()
})
Based on reactive_values$character_list (the list of user’s chosen characters), I generate as many infocards as necessary inside a renderUI block, which is then ‘rendered’ in the UI via a corresponding uiOutput:
上reactive_values基于$ character_list(的用户选择的字符列表),我生成尽可能多的InfoCard根据需要renderUI块,然后将其“呈现”在经由相应的uiOutput界面内:
# >> insert in the main area of ui sectionuiOutput(outputId = "infocards")
To build the UI part, I passed the list of characters within the purrr::map() function so as to call the custom my_infocard() function as many times as required. As purrr::map() returns a list, I pipe the result with the tagList() function to bind outputs together and generate a unique (and potentially big) HTML section.
为了构建UI部分,我在purrr :: map()函数中传递了字符列表,以便根据需要多次调用自定义my_infocard()函数。 当purrr :: map()返回一个列表时,我将结果与tagList()函数进行管道传输,以将输出绑定在一起,并生成一个唯一的(可能很大的)HTML部分。
See in the screenshot below a visual explanation of how tagList() works:
在下面的屏幕截图中,可以看到有关tagList()工作原理的直观说明:
![Image for post](https://i-blog.csdnimg.cn/blog_migrate/461a7907d9c28effc5b6f8ab37ba2730.png)
At this stage of development, the application should be working almost as desired.
在开发的这个阶段,该应用程序应几乎可以按预期运行。
The final touch
最后一点
The last functionality to add is the ability to remove an infocard by clicking on the ‘Remove’ button.
最后要添加的功能是可以通过单击“删除”按钮来删除信息卡。
I used a bit of JavaScript here. I will not cover JavaScript in details (I’m far from being a expert in this domain), but simply notice here that:
我在这里使用了一些JavaScript。 我不会详细介绍JavaScript(我远不是该领域的专家),只是在这里注意:
JavaScript is inserted in the HTML code of the R Shiny application via tags (head and script)
通过标签 (标头和脚本)将JavaScript插入R Shiny应用程序HTML代码中。
The code is simply storing button id (rm_btn_id) whenever a button with the rm_btn class is clicked
每当单击具有rm_btn类的按钮时,该代码仅存储按钮id(rm_btn_id)
# >> insert in the tabPanel, below title# JS function to identify button id when clicked
tags$head(
tags$script(
HTML("$(document).on('click', '.rm_btn', function () {
Shiny.onInputChange('rm_btn_id', this.id);
});"
)
)
),
As ‘Remove’ buttons have the rm_btn class (see definition in section 1.3), Shiny will get access to their respective id whenever they are clicked. Finally, I take advantage of this in order to remove the corresponding character from the characters’ list, through an observeEvent().
由于“删除”按钮具有rm_btn类(请参见第1.3节中的定义),因此只要单击Shiny,它们就可以访问其各自的ID。 最后,我利用了这一点,以便通过observeEvent()从字符列表中删除相应的字符。
# >> insert in server part# remove infocardobserveEvent(input$rm_btn_id, {
reactive_values$character_list <-
reactive_values$character_list %>%
.[reactive_values$character_list %>%
toupper() != input$rm_btn_id %>%
str_remove("ab_remove_") %>%
toupper() %>%
str_replace("_", " ")]
})
And we’re done! Congratulations if you’ve been able to made it so far!.
我们完成了! 祝贺您到目前为止已经成功!
关键要点 (KEY TAKEAWAYS)
- shinythemes is a great package to super-easily make your application look professional Shinythemes是一个很棒的软件包,可以非常轻松地使您的应用程序看起来专业
- the sequence : list of user inputs + map( “function that generates HTML code”) + tagList() is extremely powerful and — in my opinion — readable. 顺序:用户输入列表+ map(“生成HTML代码的函数”)+ tagList()非常强大,并且-在我看来-可读。
- create function to generate pieces of HTML to avoid duplication of code and easily use the above sequence 创建函数以生成HTML片段,以避免代码重复并轻松使用上述序列
翻译自: https://towardsdatascience.com/dynamic-ui-in-shiny-incl-demo-app-a6fb791be4c6
incl汇编