The case presented here is adapted from how the R Shiny App for a Library Survey was created. This is the third post of the series to make an interactive data visualization web app:
Now that we have done with the dirty work of data cleaning, and we have got familiar with visually presenting data with ggplot2
, let’s get started with creating a Shiny App. We will make use of the data objects and plot prototypes that we saw in the first two posts, building up the Shiny app like assembling the jigsaw puzzle pieces.
Learn Shiny from RStudio provides step-by-step tutorials (textual and video) on getting started with Shiny App. The Articles section offers many useful topics on building Shiny App once you have grasped the basics.
Here we will skip many “what and how” parts of building a Shiny App and go ahead to decompose this R Shiny App for a Library Survey. We will take a top down approach to so do.
Basically we need a ui
object that controls how the interface looks like, a server
object that takes charges of the inputs and outputs, and a global.R
that takes care of the extra work that we need for building up the App (e.g. creating objects for later use). Read more here for the structure of a Shiny App. ()
The first thing we want to do is laying out the overall user interface and the arrangement of each dashboard. RStudio’s Application layout guide introduced four layout features:
We will use all of them in our design except for the sidebar lists.
In our case, we use the top navigation bar as the highest level of navigation structure.
library(shiny)
library(shinythemes)
ui <-
navbarPage("Demo", collapsible = TRUE, inverse = TRUE, theme = shinytheme("spacelab"),
tabPanel("Participation"),
tabPanel("Service Use",
fluidPage(
tabsetPanel(
tabPanel("Accessing Website"),
tabPanel("Visiting Library"),
tabPanel("Attending Workshops"),
tabPanel("Exploring Technology")
))),
tabPanel("Space & Study Habits",
fluidPage(
tabsetPanel(
tabPanel("Study Habit"),
tabPanel("Space Preference - Mid & Final Terms"),
tabPanel("Space Preference - Most Days"),
tabPanel("Space Preference - Student Submissions")
))),
tabPanel("Outreach"),
tabPanel("About")
)
server <- function(input, output) {}
shinyApp(ui = ui, server = server)
We started with creating a navigation bar organized by topics of the survey (service use, space use & study habits, outreach), in addition to a “survey summary” page and an “about” page. We did that with navbarPage()
. Under “Service Use” we further created a layer evaluating services we offer (website access, library visits, workshop attendance, technology exploration) with Tabsets. Similarly we created this nested navigation structure for “Space and Study Habits”.
The logic behind this layout is grouping blocks with similar topics into one place. This will also create visual consistency and repetition, which will be applied throughout the App.
shinythemes
package changes the overall look of a Shiny App with many built-in themes as options. More on the package here.
In the following sections, we will build up the App piece by piece.
In the first dashboard on service use, we will be exploring the sidebar layout, Shiny widgets and reactive expressions. Important techniques to this dashboard are using reactive expressions to facilitate automation and repetition (e.g. simultaneously adjust the output of a plot by selecting the survey question, participant subgroup and plot type) in inputs and outputs.
We talked about how to create those bar charts here.
Each plot got an outpud id with plotOutput(outputId = "id")
in ui
, and was rendered in server
with output$id <- renderPlot({})
. This refers to the reactive programming models that Shiny relies on.
Note that when rendering plots the function is not always renderPlot()
, which depends on the plot library you use. For instance, it is renderPlotly()
to render Plotly
plots, and renderWordcloud2()
to render plots produced by wordcloud2()
.
Shiny offers many widgets to make use of. Widgets are web elements that we can interact with. Available widgets include checkbox, date input and range, slider, select box, file input, radio button, action button etc.
We used the radio buttons here, which will also be applied across almost all dashboards, for two reasons. (1)Most of our tasks are concerned with visualizing categories, and groups of radio buttons supports this need when each group refers to a category. (2)We wanted to create consistency across the dashboards in this App. It is also easier for users to follow the logic of the visualization when they browse among the dashboards.
First of all, the radion button needs a name and a label, which all widgets have. In addition to the name(id) and label, we also specified the choices
argument with all the options for a user to choose from. Below is what we did.
radioButtons(inputId = "question1",
label = "Choose a question to explore",
choices = c("Email/chat with library staff",
"Find books",
"Search for articles"))
Then in the server
object we output what we defined earlier.
dataInput <- reactive({
switch(input$question1,
"Email/chat with library staff" = survey$Q1.1,
"Find books" = survey$Q1.2,
"Search for articles" = survey$Q1.3)
})
Above we have created a dashboard that allows one to examine the website access by each survey question, subgroups of participants by country/status/major, and plot types, all at the same time. We automated that process by creating two reactive expressions, groupInput
and dataInput
. groupInput
handles which subgroup to look at, and dataInput
handles which survey question to look at.
A reactive expression is a expression whose result will change over time. It uses widget input, which may change, and returns a value, which will be updated when the widget input has changed.
groupInput <- reactive({
switch(input$group1,
"Student Status" = survey$status,
"Country / Region of Origin" = survey$country,
"Major" = survey$major)
})
dataInput <- reactive({
switch(input$question1,
"Email/chat with library staff" = survey$Q1.1,
"Find books" = survey$Q1.2,
"Search for articles" = survey$Q1.3)
})
Then later in plotting, we can simply use what we defined earlier as arguments in the ggplot()
function.
server <- function(input, output) {
output$plot_access_web <- renderPlot({
if (input$plot1 == "Stacked Bars") {
ggplot(survey, aes(groupInput())) +
geom_bar(aes(fill = dataInput()),
position = position_stack(reverse = TRUE),
width = 0.4, alpha = 0.75) +
scale_fill_manual(values = c(palette[[1]][4], palette[[2]][1], palette[[3]][1])) +
scale_x_discrete(limits = rev(levels(groupInput())))
} else if (input$plot1 == "Grouped Bar Charts") {
} else if (input$plot1 == "Stacked Bars (Percent)") {
}
})
}
In this second dashboard on survey summary, we will further explore the input and output in Shiny and the layout system. Techniques important to this dashboard is using action buttons to display extra information complementary to the main visualization, using texts as visualization and using tables to provide more information to the plots.
The survey summary dashboard uses the grid layout system, where we get to define how much space the rows and columns should take. The column widths are based on the Bootstrap 12-wide grid system. Basically one page is divided into 12 columns; we then decide how many columns we want to allocate to one block.
In this case, the dashboard is divided into four rows (1 for help text
and 3 for outputs). Within each row of the output, we allocated 6 columns to the left block of texts with button (including 1 column of blank space), 5 to the plot (including 2 columns of blank space), and 1 to be the blank space to the right. offset = #
pushes the block to the right by # columns.
library(shiny)
library(shinythemes)
source("global.R")
ui <-
navbarPage("Demo", collapsible = TRUE, inverse = TRUE, theme = shinytheme("spacelab"),
tabPanel("Participation",
fluidPage(
fluidRow(
column(5, offset = 1,
helpText("Click More Stats button to find out more information of each group.",
style = "font-size:115%;font-style:italic;" ),
br())),
fluidRow(
column(3, offset = 2, br(), br(),
h1("320", align = "center",
style = "font-size: 350%; letter-spacing: 3px;"),
h3("Student Participants",
align = "center", style = "opacity: 0.75;"), br(),
div(actionButton(inputId = "more1", "More Stats"),
style = "margin:auto; width:30%;")),
column(6, plotOutput(outputId = "g1", height = "400px")),
column(1)),
fluidRow(
column(3, offset = 2, br(), br(),
h1("48", align = "center",
style = "font-size: 350%; letter-spacing: 3px;"),
h3("Countries / Regions of Origin",
align = "center", style = "opacity: 0.75;"), br(),
div(actionButton(inputId = "more2", "More Stats"),
style = "margin:auto; width:30%;")),
column(6, plotOutput(outputId = "g2", height = "400px")),
column(1)),
fluidRow(
column(3, offset = 2, br(), br(),
h1("16", align = "center",
style = "font-size: 350%; letter-spacing: 3px;"),
h3("Majors", align = "center",
style = "opacity: 0.75; letter-spacing: 1px;"),
br(),
div(actionButton(inputId = "more3", "More Stats"),
style = "margin:auto; width:30%;")),
column(6, plotOutput(outputId = "g3", height = "600px")),
column(1))
)),
tabPanel("Service Use",
fluidPage(
tabsetPanel(
tabPanel("Accessing Website"),
tabPanel("Visiting Library"),
tabPanel("Attending Workshops"),
tabPanel("Exploring Technology")
))),
tabPanel("Space & Study Habits",
fluidPage(
tabsetPanel(
tabPanel("Study Habit"),
tabPanel("Space Preference - Mid & Final Terms"),
tabPanel("Space Preference - Most Days"),
tabPanel("Space Preference - Student Submissions")
))),
tabPanel("Outreach"),
tabPanel("About")
)
server <- function(input, output) {
## charts
output$g1 <- renderPlot({plot1})
output$g2 <- renderPlot({plot2})
output$g3 <- renderPlot({plot3})
## button
observeEvent(input$more1, {
showModal(modalDialog(
renderTable({tb[[1]]}),
easyClose = TRUE,
footer = modalButton("Close")
))
})
observeEvent(input$more2, {
showModal(modalDialog(
renderTable({tb[[2]]}),
easyClose = TRUE,
footer = modalButton("Close")
))
})
observeEvent(input$more3, {
showModal(modalDialog(
renderTable({tb[[3]]}),
easyClose = TRUE,
footer = modalButton("Close")
))
})
}
shinyApp(ui = ui, server = server)
You may have noticed that we have applied many internal styles to the components in the above dashboard. Styles will be discussed below. For now we will skip that.
The lollipop charts (plot1
to plot3
) were created in the global.R
. We talked about how to create those lollipops here.
plot1 <-
ggplot(tb[[1]], aes(`#`, reorder(tb[[1]][,1], -`#`), label = `#`)) +
geom_segment(aes(x = 0, y = reorder(tb[[1]][,1], -`#`), xend = `#`, yend = reorder(tb[[1]][,1], -`#`)),
size = 0.5, color = "grey50") +
geom_point(size = 10) +
geom_text(color = "white", size = 4) +
coord_flip() +
theme(axis.text.x = element_text(size = 14, angle = 90, hjust = 1),
axis.text.y = element_text(size = 14),
axis.title.y = element_blank(),
axis.title.x = element_blank(),
axis.ticks.x = element_line(size = 0),
plot.margin = unit(c(1,2,2,3), "cm"))
A plot is not the only element that we can rely on in visualization. Texts, especially large texts, can be useful when there are not too much information to display but when there is an important message to convey.
For instance, we chose to highlight the number of survey participants, number of the particpants’ countries/regions, and number of the participants’ majors using the h1()
HTML tag function. This helps the numbers stand out and also tells the audience that those numbers matter. The large texts aim to catch the readers’ attention.
fluidRow(
column(3, offset = 2, br(), br(),
h1("320", align = "center", style = "font-size: 350%; letter-spacing: 3px;"),
h3("Student Participants", align = "center", style = "opacity: 0.75;"), br(),
div(actionButton(inputId = "more1", "More Stats"), style = "margin:auto; width:30%;")),
column(6, plotOutput(outputId = "g1", height = "400px")),
column(1)
)
The data we used for summary tables were stored in the list tb
, which we created previously. This is a list of three matrices of summarizing participant stats by subgroups (status/country/major).
## [[1]]
## Student Status #
## 1 Freshman 131
## 2 Sophomore 80
## 4 Senior 71
## 3 Junior 19
## 5 Other Programs 19
##
## [[2]]
## Country / Region #
## 1 China 178
## 3 Other 71
## 2 U.S. 69
## 4 Undefined 2
##
## [[3]]
## Major #
## 1 Business, Finance & Economics 118
## 8 Undefined 57
## 2 Humanities & Social Sciences 32
## 6 CS & Engineering 27
## 3 Data Science & Interactive Media Business 24
## 5 Science 21
## 7 Mathematics 21
## 4 Interactive Media Arts 20
Using a list to store data subsets helps with automating the whole process because we won’t need to create many single data objects for each request, but just subset the list for the parts we need.
This is more obvious in the piece below, where dtset
is a list consisting of three matrices summarizing top reasons of visiting the library by subgroups(status/country/major).
## [[1]]
## Reason Freshman Sophomore
## 12 Find a quiet place to study 94 55
## 6 Print, photocopy, scan 76 50
## 1 Work on a class assignment/paper 76 44
## 13 Borrow books and materials 38 25
## 10 Get readings from Course Reserve 13 12
## 5 Use a group study room 18 12
## 4 Use a library computer 16 10
## 8 Meet up with friends 17 12
## 9 Hang out between classes 14 5
## 11 Get help from a librarian 7 7
## 3 Use specialized databases (e.g. Bloomberg, Wind) 4 3
## 14 Attend a library workshop 13 0
## 2 Watch video or listen audio 5 4
## 7 Other 2 1
## Junior Senior Other Programs Total
## 12 11 45 16 205
## 6 10 52 14 188
## 1 9 28 10 157
## 13 9 34 1 106
## 10 3 15 2 43
## 5 3 7 1 40
## 4 3 7 5 36
## 8 1 4 2 34
## 9 2 2 4 23
## 11 1 8 0 23
## 3 2 8 0 17
## 14 2 1 1 16
## 2 0 1 1 10
## 7 1 1 0 5
##
## [[2]]
## Reason China U.S. Other Total
## 12 Find a quiet place to study 123 47 50 170
## 6 Print, photocopy, scan 104 45 52 149
## 1 Work on a class assignment/paper 89 46 31 135
## 13 Borrow books and materials 63 15 28 78
## 5 Use a group study room 32 4 5 36
## 10 Get readings from Course Reserve 30 6 8 36
## 4 Use a library computer 11 20 10 31
## 8 Meet up with friends 23 4 9 27
## 9 Hang out between classes 16 6 5 22
## 3 Use specialized databases (e.g. Bloomberg, Wind) 12 3 2 15
## 11 Get help from a librarian 13 2 7 15
## 14 Attend a library workshop 9 5 3 14
## 2 Watch video or listen audio 7 2 2 9
## 7 Other 2 2 1 4
##
## [[3]]
## Reason
## 12 Find a quiet place to study
## 6 Print, photocopy, scan
## 1 Work on a class assignment/paper
## 13 Borrow books and materials
## 10 Get readings from Course Reserve
## 5 Use a group study room
## 4 Use a library computer
## 8 Meet up with friends
## 11 Get help from a librarian
## 9 Hang out between classes
## 3 Use specialized databases (e.g. Bloomberg, Wind)
## 14 Attend a library workshop
## 2 Watch video or listen audio
## 7 Other
## Business, Finance & Economics Humanities & Social Sciences
## 12 80 21
## 6 78 23
## 1 63 18
## 13 36 13
## 10 10 6
## 5 17 3
## 4 15 3
## 8 17 1
## 11 9 2
## 9 6 1
## 3 10 3
## 14 5 1
## 2 7 0
## 7 1 1
## Data Science & Interactive Media Business Interactive Media Arts
## 12 17 10
## 6 12 10
## 1 16 9
## 13 9 8
## 10 2 4
## 5 2 5
## 4 3 5
## 8 1 2
## 11 1 3
## 9 5 1
## 3 0 1
## 14 4 1
## 2 0 0
## 7 0 1
## Science CS & Engineering Mathematics Undefined Total
## 12 12 18 16 47 174
## 6 15 13 14 37 165
## 1 8 16 11 26 141
## 13 11 9 4 17 90
## 10 4 4 8 7 38
## 5 3 4 3 4 37
## 4 2 3 1 9 32
## 8 2 4 2 7 29
## 11 1 3 2 2 21
## 9 1 3 0 10 17
## 3 0 1 1 1 16
## 14 3 2 0 1 16
## 2 1 1 1 1 10
## 7 0 0 0 2 3
When accessing the subsets of data we need, or specify arguments in ggplot()
, what we did was to use the dataInput2()[[1]]
or groupInput2()[,1]
.
server <- function(input, output) {
dataInput2 <- reactive({
switch(input$question2,
"My top reasons for visiting Library" = dtset,
"I asked for help by ..." = dtset2)
})
groupInput2 <- reactive({
switch(input$group2,
"Student Status" = dataInput2()[[1]],
"Country / Region of Origin" = dataInput2()[[2]],
"Major" = dataInput2()[[3]])
})
output$plot_visit_lib <- renderPlot({
ggplot(groupInput2(), aes(x = reorder(groupInput2()[,1], Total), y = Total, fill = factor(Level, levels = c("High","Medium","Low")))) +
geom_bar(stat = "identity", alpha = 0.75) +
scale_fill_manual(values = c(palette[[1]][4], palette[[2]][1], palette[[3]][1]), name="Level of\nFrequency") +
coord_flip() +
theme(axis.text.x = element_text(size = 15),
axis.text.y = element_text(size = 15, margin = margin(0,3,0,0)),
axis.title.y = element_blank(),
axis.title.x = element_text(size = 15, margin = margin(15,0,0,0)),
axis.ticks.x = element_line(size = 0),
legend.title = element_text(size = 15),
legend.text = element_text(size = 15),
plot.margin = unit(c(0,0,1,0), "cm"))
})
output$table_visit_lib <- renderTable({
groupInput2()
})
}
Tables in dashboards are useful in that they can provide details of the data used in plots.
In Dashboard 2, we saw a lot of elements that control the styles of an app. Here we will take a closer look at how to customize our user interface.
You may also noticed that we also used inline styles many times. Usually we did this with style
argument like below.
h3("Student Participants", align = "center", style = "opacity: 0.75;")
We also put the action buttons in div()
s so that we can apply inline styles to them.
div(actionButton(inputId = "more1", "More Stats"), style = "margin:auto; width:30%;")
Once we have built up the Shiny App, we can run a Shiny App in many ways:
Share your apps introduces how to share or host a Shiny App with the methods mentioned above.
shinyapps.io offers free services to host Shiny Apps, and it is easy to deploy the app to the cloud with R. Here is how.