title | author | date | output | ||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Building A Shiny App |
Alex Drake |
12 April 2017 |
|
We've previously had a look at how to manipulate our data, create functions and even map our data using Leaflet
. The next step is to see how we can combine this to create an easy to understand output. One of the best ways to do this is to use Shiny
to create an application.
First load the packages we need for this lesson - install Shiny
if you haven't already got it!
library(jsonlite)
library(leaflet)
library(shiny)
library(tidyverse)
Shiny
allows you to create a powerful web application within an R environment and uses R functions to write the JavaScrip, HTML and CSS needed to display your app. What is great about Shiny
is that you can perform any R calculation in the background and display the results as your output.
In order for Shiny
to perform properly, we need to create a server function to perform calculations and a user interface (UI) to display our outputs and allow user inputs. We're going to create a simple app to demonstrate how this works with an empty server and a UI with a basic set up. We'll then launch this so using shinyApp
so we can see what it looks like.
Note that we could create the UI and server as separate R files and combine them later, however for simplicity we're going to look at single file apps only. You'll want to copy and paste this into your R session to run it as Rmarkdown won't render an interactive application!
# The User Interface
ui <- basicPage("Look, a Shiny app!")
# The Server
server <- function(input, output, session){
# Currently blank
}
# Now launch the app
shinyApp(ui = ui,
server = server)
Shiny
uses Twitter Bootstrap to scaffold and provide minimal styling to your applications - running shinyApp
then compiles your code into web-friendly languages. For the most part, the inbuilt UI functions are sufficient to create a powerful app but you'll eventually find the need to add some custom HTML/CSS/JavaScript in to get the best bang for your buck. We're only going to look at the inbuilt functions today but I can recommend W3 Schools for those who are curious/want to take their app to the next level.
There are lots of different layout styles available to you in Shiny
, you can find a full list of them here Shiny Application layout guide. We're going to focus on using fluidPage
to create an app with a sidebarPanel
, mainPanel
and not much else. You can see this more clearly in the example below.
# UI
ui <- fluidPage(
# Our page title
titlePanel("Simple App")
# Our page layout
sidebarLayout(
# sidebar contents
sidebarPanel("sidebar panel")
# main page contents
mainPanel("main panel")
)
)
# server
server <- function(input, output, session){
# Currently blank
}
shinyApp(ui = ui,
server = server)
So far so good. Now let's jazz it up a bit by adding a page title and some elements into out sidebar
and mainPanel
. We're going to use some ACCStats data from the TfL API so we'll create the appropriate elements from now on before testing our app IRL.
First we want to createt our header using headerPanel()
and format the size using one of the inbuilt functions, h4()
ie header size 4. The header functions work in a similar way to the styles in MS Word so have a play around with h1()
, h2()
etc if you want!
The next step is to define the user inputs based on potential queries to the TfL API. The ACCStats data is split by year and incident severity, so we'll allow the user to select these options with the selectInput()
function. Note that we can select multiple = T/F
if we do/don't want the user to be able to select multiple options.
We will also define where the outputs will be displayed using leafletOutput
(for a map output) and plotOutput
(a chart of our results).
ui <- fluidPage(title = "ACCStats Map",
headerPanel(title = h4("ACCStats Map")),
sidebarPanel(
selectInput(inputId = "years",
label = "Year: ",
choices = c(2013, 2014, 2015),
multiple = F),
selectInput(inputId = "sev",
label = "Severity: ",
choices = c("Fatal", "Serious", "Slight"),
selected = c("Fatal", "Serious", "Slight"),
multiple = T)
),
mainPanel(
leafletOutput(outputId = "map", height = 500),
br(),
plotOutput(outputId = "plot", height = 350)
)
)
server <- function(input, output, session){
}
shinyApp(ui = ui,
server = server)
You should have now created an application with a series of user inputs but no outputs in the mainPanel
- this is because we need to define our outputs using the server
function.
Currently our users can interact with the application but without code in our server, it cannot react to any input changes.
In order to react, we need to pass in our inputs, using the inputId
found within our selectInput
functions and also render
our outputs (leafletOutput
etc) using Shiny
render*
functions.
For example, if we want to access our severity input we would call input$sev
within our server and then combine this with either renderLeaflet
or renderPlot
- check the Shiny
cheatsheet for other render*
options!
That said, our first step is to finish defining our constants ie what we want the app to display regardless of user input. We can do this by creating a simple leaflet
map in our sever.
ui <- fluidPage(title = "ACCStats Map",
headerPanel(title = h4("ACCStats Map")),
sidebarPanel(
selectInput(inputId = "years",
label = "Year: ",
choices = c(2013, 2014, 2015),
multiple = F),
selectInput(inputId = "sev",
label = "Severity: ",
choices = c("Fatal", "Serious", "Slight"),
selected = c("Fatal", "Serious", "Slight"),
multiple = T)
),
mainPanel(
leafletOutput(outputId = "map", height = 500),
br(),
plotOutput(outputId = "plot", height = 350)
)
)
server <- function(input, output, session){
# define the fixed map feature
output$map <- renderLeaflet({
leaflet() %>%
addProviderTiles("CartoDB.Positron",
options = providerTileOptions(noWrap = TRUE)) %>%
fitBounds(lng1 = -0.2, lng2 = 0.0,
lat1 = 51.4, lat2 = 51.6)
})
# define the plot output
output$plot <- renderPlot({
# blank for now
})
}
shinyApp(ui = ui,
server = server)
We now have a basic app ready to react to changes in our user input. We'll need to respond to these changes using the reactive
and observe
functions.
Reactivity is important in Shiny
as it allows the user to tailor the app's outputs to suit their needs but it's important to understand how it works if you want to create a slick application with minimal hangtime. For example, we could send a request to the API every time the user changes an input - whilst this would provide what the user wanted, it would be really inefficient and 'laggy'. The other option would be to do one import of a larger data set and then refine what is displayed within the app.
We can do this using reactive expressions, which cache the results of any procedure that happens in response to user input (such as accessing a database!). We can even use the reactive expression to carry out calculations or cleanse the data into a state so that it is useful for other elements within our app.
First, define your TfL API AppId and AppKey. Remember you can get these from the 'API Credentials' page if you've forgotten them since last time.
AppId <- "your app id here"
AppKey <- "your app key here"
Note that the following functions won't work on their own as they must be called within your server
function. However, it is useful to look at each section as a standalone before lumping everything together!
acc_data <- reactive({
# define request URL
requestUrl <- paste0("https://api-argon.tfl.gov.uk/AccidentStats/",
input$years,
"?app_id=", AppId,
"&app_key=", AppKey)
# get around the pesky corporate firewall
l <- readLines(requestUrl, encoding="UTF-8", warn = FALSE)
d <- fromJSON(l)
# select required columns
d_clean <- d %>%
select(id, lat, lon, location, date, severity, borough)
# change the date format
d_clean$date <- strptime(d_clean$date,
format = "%Y-%m-%dT%H:%M:%SZ")
# add hour column
d_clean$hour <- as.numeric(format(d_clean$date, format = "%H"))
# create hour summary
df_hour <- d_clean %>%
select(hour, severity) %>%
group_by(hour, severity) %>%
summarise(count = length(severity))
# return a list of the required data
return(list(df = d_clean, df_smry = df_hour))
})
This function pulls in data from the TfL API based on the user input for year, removes columns not required, cleans the date format and also creates a summary table based on the hour that an incident occurred. We can pull both the cleaned data frame and the summary data frame from the function by using return(list(...))
BUT will only do it when the user changes the year!
Observers are equally important when making an application that runs smoothly. They are similar to reactive expressions but also have some key differences - they can access reactive values and expressions but do not return any values. Instead, observers are used to send data to the web browser and there are a few ways of declaring them in your app script - today we'll look at two of them.
We've already created our map, as shown when we run our application. What we now want to do is update it to display our user requested data ie the data passed in from our reactive expression, acc_data()
. For efficiency, we do not want to redraw the basemap each time the data is refreshed and so we use leafletProxy()
as our reactive element, and wrap it inside an observe()
function.
We must define which element of acc_data()
we want to use and then let R know which leaflet map we want to add the additional layers to ('map'). Note the use of clearGroup()
to make sure we only render the new data on the map.
# create the observer to render appropriate points on map
observe({
# create a list of dataframes returned from acc_data()
df <- acc_data()
# pull through the required dataframe from the list
df <- df$df
# add the layers to the leaflet map called 'map'
leafletProxy("map") %>%
clearGroup("accs") %>%
addCircleMarkers(data = df,
lng = df$lon,
lat = df$lat,
weight = 1,
color = "Blue",
fillOpacity = 0.4,
group = "accs",
clusterOptions = markerClusterOptions())
})
We can take this observer function further by also reacting to changes in the 'Severity' level by adding an extra line. This will make sure that the map will display different data if either the 'Severity' level or 'Year' has changed.
# create the observer to render appropriate points on map
observe({
df <- acc_data()
df <- df$df
# limit the data to user 'Severity' choice
df <- df[df$severity %in% input$sev,]
leafletProxy("map") %>%
clearGroup("accs") %>%
addCircleMarkers(data = df,
lng = df$lon,
lat = df$lat,
weight = 1,
color = "Blue",
fillOpacity = 0.4,
group = "accs",
clusterOptions = markerClusterOptions())
})
We can also use the render...()
family of functions to observe/react to user changes, but to do this we must first assign it to an output object as follows:
output$foo <- renderText({ bar })
On first glance this would suggest that observers do return values however, the function renderText()
is not an observer/endpoint unless it is assigned to output$foo. This assignment wraps renderText()
into another function which is an observer and is required if we want to send the data to the browser.
We will demonstrate this by using the renderPlot()
placeholder we created earlier and display the summary data from acc_data()
. Note that we again limit the displayed data by the 'Severity' user input.
output$plot <- renderPlot({
# create a list of dataframes returned from acc_data()
df <- acc_data()
# pull through the required dataframe from the list
df <- df$df_smry
# limit the data to user 'Severity' choice
df <- df[df$severity %in% input$sev,]
# create a stacked bar chart
p <- ggplot(data = df, aes(x = hour, y = count, fill = severity)) +
geom_bar(stat="identity") +
theme_classic()
p
})
Now let's put all of the code together to complete our app. Remember to declare your own AppID and AppKey otherwise you won't be able to access any of the data!
Note that we can declare our AppId, AppKey and year choices outside of the UI as this makes it easier to change them at a later date
AppId <- "your app id here"
AppKey <- "your app key here"
choices <- c(2013,2014,2015)
ui <- fluidPage(title = "ACCStats Map",
headerPanel(title = h4("ACCStats Map")),
sidebarPanel(
selectInput(inputId = "years",
label = "Year: ",
choices = choices,
selected = max(choices),
multiple = F),
selectInput(inputId = "sev",
label = "Severity",
choices = c("Fatal","Serious","Slight"),
selected = c("Fatal","Serious","Slight"),
multiple = T)
),
mainPanel(
leafletOutput(outputId = "map",
height = 500),
br(),
plotOutput(outputId = "plot",
height = 350)
)
)
server <- function(input, output, session){
# define the fixed map feature
output$map <- renderLeaflet({
leaflet() %>%
addProviderTiles("CartoDB.Positron",
options = providerTileOptions(noWrap = TRUE)) %>%
fitBounds(lng1 = -0.2, lng2 = 0.0,
lat1 = 51.4, lat2 = 51.6)
})
# define our reactive expression
acc_data <- reactive({
# define request URL
requestUrl <- paste0("https://api-argon.tfl.gov.uk/AccidentStats/",
input$years,
"?app_id=", AppId,
"&app_key=", AppKey)
# get around the pesky corporate firewall
l <- readLines(requestUrl, encoding="UTF-8", warn = FALSE)
d <- fromJSON(l)
# select required columns
d_clean <- d %>%
select(id, lat, lon, location, date, severity, borough)
# change the date format
d_clean$date <- strptime(d_clean$date,
format = "%Y-%m-%dT%H:%M:%SZ")
# add hour column
d_clean$hour <- as.numeric(format(d_clean$date, format = "%H"))
# create hour summary
df_hour <- d_clean %>%
select(hour, severity) %>%
group_by(hour, severity) %>%
summarise(count = length(severity))
# return a list of the required data
return(list(df = d_clean, df_smry = df_hour))
})
# create the observer to render appropriate points on map
observe({
df <- acc_data()
df <- df$df
df <- df[df$severity %in% input$sev,]
leafletProxy("map") %>%
clearGroup("accs") %>%
addCircleMarkers(data = df,
lng = df$lon,
lat = df$lat,
weight = 1,
color = "Blue",
fillOpacity = 0.4,
popup = paste0("Date: ",df$date, br(),
"Severity: ", df$severity),
group = "accs",
clusterOptions = markerClusterOptions())
})
# create observer to render plots
output$plot <- renderPlot({
df <- acc_data()
df <- df$df_smry
df <- df[df$severity %in% input$sev,]
p <- ggplot(data = df, aes(x = hour, y = count, fill = severity)) +
geom_bar(stat="identity") +
theme_classic()
p
})
}
shinyApp(ui = ui,
server = server)
Well done, you've created your first app! The world of Shiny
is now open to you, so here is some useful reading material for your long and arduous journey
- R Shiny Cheatsheet - the official RShiny cheatsheet.
- R Shiny Examples - some shiny examples from the folks at RStudio.
- ZevRoss Shiny Tutorial - features some great tutorials with 40 examples.