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

fa-solid fa-triangle-exclamation fa-xl 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. With step = 7, each frame of the animation will advance one week of data.
Copy
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.
Copy
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:

Copy
 <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.


  Please feel free to leave any questions or comments below, or send me a message here