Embedding Shiny Apps for Interactive Data Analysis
New Zealand Earthquake App
For demonstration purposes, I created an app that takes a year's worth of earthquake data from the NZ Geonet data source and pushed that into a Leaflet map. There are sliders to allow you to filter by magnitude and date range and even an animation option to watch events over a specified date range (see warning below).
Warning
If you're running the animation on the above example, it's hosted on a free shinyapps server, which is a bit underpowered for the task. I recommend you select a narrow magnitude and date range to show only a few data points at a time to make it a little smoother. The app itself is not very mobile friendly, so apologies if you're not viewing this on desktop - creating an appropriate flex grid in Shiny is surprisingly fiddly and time-consuming.
Creating the App
It's worth taking a look at the Shiny App Gallery to get an idea of the capabilities of Shiny and following the comprehensive tutorial on R Studio's website if you're interested in developing apps yourself.
The code for this app can be found on my GitHub, which I will explain below.
Loading the Data
In keeping the demo simple, I've just used a static local data source obtained from Kaggle. Real world examples would pull the data dynamically with parameters set by the program or directly by the user.
The data needed a little tweaking. Firstly, the data straddles the international data line, causing a mix of positive and negative longitudes. The negative values are added to 360 to create a continuous longitude range. There are a few negative magnitude observations that also need removing.
Finally, an extra column for the pop-up text is added to save recalculating this each time the data points are loaded.
Building the UI
- I've used the Boostrap page method to give this more of a web page feel.
- Some custom CSS adds styling for the map, background and control panel.
- The
leafletOutput
adds the base map which fills the entire app area. - One
absolutePanel
simply adds the title at the top, while a second houses the two sliders and a brief set of instructions as HTML text. - The second (date) slider includes the line
animate = animationOptions(interval = 1500, loop = FALSE)
to add the animation ability. Withstep = 7
, each frame of the animation will advance one week of data.
library(shiny)
library(dplyr)
library(leaflet)
load('./data/nzeq.Rdata')
nzeq <<- nzeq %>%
mutate(longitude=if_else(longitude<0,360+longitude, longitude)) %>%
filter(magnitude > 0) %>%
mutate(popUpText=paste(
as.Date(origintime,"%Y-%m-%d"),"",
"Magnitude: ",round(magnitude,1),"",
"Depth: ",round(depth,1), "km")
) %>%
arrange(magnitude)
currentGroup <<- 0
shinyUI(
bootstrapPage(
tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
leafletOutput("eqPlot", width = "100%", height = "100%"),
tags$style("
#controls {
background-color: #fff;
opacity: 0.5;
padding: 10px 25px 15px 25px;
}
#controls:hover{
opacity: 0.8;
}
#title {
background-color: #ffffff0;
opacity: 0.9;
padding: 0px 10px 0px 15px;
}
"),
absolutePanel(top=10, left=30, id = "title", class = "panel panel-default",
h3("New Zealand Earthquake Data (01 June 2019 - 31 May 2020)")
),
absolutePanel(top=80, left=30, id = "controls", class = "panel panel-default",
h4("Select Filters"),
sliderInput("magnitude",
"Magnitude:",
min = 0,
max = 6,
value = c(0,6),
step = 0.2),
sliderInput("date",
"Date:",
min = as.Date("2019-06-01","%Y-%m-%d"),
max = as.Date("2020-06-01","%Y-%m-%d"),
value=as.Date(c("2019-06-01", "2020-06-01")),
step=7,
timeFormat="%Y-%m-%d",
animate = animationOptions(interval = 1500, loop = FALSE)
),
HTML("Instructions
Select a magnitude range to update the map with just those
events.
Select a date range to update the map with only events in that
date range.
To view an animation over time, select a narrow date range and
click the small triangle at the right of the date slider.
For a smoother animation, it's recommended to use a short date
range (2 weeks for example) and a filtered magnitude (4 to 6) for
example). Note the slider determines the width of time displayed
in each frame, not the start and end dates for the animation.
Code available on GitHub
")
)
)
)
Building the Server
- First, we create a reactive filtered data set that will represent only the data requested by the user via the sliders. This will run whenever any user input happens. For heavy processing, this can be shifted to only trigger when a button is clicked for example.
palMap
just creates a continuous colour scale for use on the map.- Next, the base map (
eqPlot
) is defined with bounds according to the maximum and minimum coordinates in the data set. This is only called once when the app is loaded, the action happens in the layers above the base map. Doing this prevents an excessive amount of loading each time the parameters are changed, and also retains any zoom level selected by the user as the changes are redrawn. - Note that a minimum zoom level is set on the map, partly to keep the map focused on the data but also to prevent accidental zooming when scrolling down the page with the mouse wheel. In the R version of Leaflet, there is currently no way to disable scroll-wheel zooming.
- Finally, the
observe
function is called whenever some input from the user happens. The colour palette is loaded and the filtered data set is recalculated. The markers for the newly filtered data set are added with radius and colour set by magnitude and pop-up text for each point configured. These are assigned to a new group ID and displayed before removing the old points - this is an important step to smooth transitions. When running the animation, clearing all markers and then adding new ones will cause significant blinking.
library(shiny)
library(leaflet)
shinyServer(function(input, output) {
filteredData <- reactive({
nzeq %>%
filter(magnitude >= input$magnitude[1] & magnitude <= input$magnitude[2]) %>%
filter(origintime >= input$date[1] & origintime <= input$date[2]) %>%
arrange(magnitude)
})
palMap <- reactive({
colorNumeric(
palette = "RdYlGn",
domain = nzeq$magnitude,
reverse = TRUE)
})
output$eqPlot <- renderLeaflet({
nzeq %>%
leaflet(options=leafletOptions(minZoom = 5)) %>%
addTiles() %>%
fitBounds(
~min(longitude),
~min(latitude),
~max(longitude),
~max(latitude)
) %>%
addLegend("bottomright", pal = palMap(), values = ~nzeq$magnitude,
title = "Magnitude",
opacity = 1
)
})
observe({
pal <- palMap()
filtered <- filteredData()
map <- leafletProxy("eqPlot", data = filtered)
if (nrow(filtered)>0){
newGroup <- currentGroup + 1
map %>%
addCircleMarkers(group = as.character(newGroup),
radius = ~((magnitude/2)^2),
color = ~pal(magnitude),
fillColor = ~pal(magnitude),
fillOpacity = 0.6,
fill = T,
stroke = F,
popup = ~(popUpText)
)
if (currentGroup!=0) {map %>% clearGroup(as.character(currentGroup))}
currentGroup <<- newGroup
}
})
})
Publishing the App
The quickest and easiest way to publish is to run the app and click the "Publish" button in the top right corner. If you haven't already configured a connection to either the R Studio server or your own, the wizard will take you through the process. The Publish wizard takes care of all the build, test, compile, and upload processes, and sends all the necessary commands to the remote server to install and restart (if necessary) the app there.
Publishing configuration is kept in an rsconnect
folder and allows you to easily republish any subsequent updates.
That's your app uploaded and running on a web server.
Embedding the App
- Now your app is published to a Shiny App server (either your own or to R Studio's shinyapps.io), copy the URL.
- Add an iframe to your page and simply use the URL of your published shiny app page.
For example:
<iframe height="800" width="100%" frameborder="no" src="https://username.shinyapps.io/app-name/"></iframe>
That's all there is to it.
Conclusion
Shiny Apps can be as simple or complex as your needs dictate. Publishing them and embedding them on an existing website is a simple and straightforward process.