The world’s leading publication for data science, AI, and ML professionals.

Create Interactive Map Applications in R and R Shiny for Exploring Geospatial Data

A Web Application using a Map for Input Selection

Animation by Author
Animation by Author

The important stories that numbers can tell often involve locations. Geospatial data (or spatial data, also known as Geo Data) refers to any data that is indicated by or related to a geographic location (REF). Geospatial data combines the location information such as address, ZIP code, or zone with the corresponding attribute information associated with the location.

For most people, it is really hard to explore geospatial data without using a map. With this application, we demonstrate how to create a free and open source (FOSS) solution using R and R Shiny, that allows users to interact with the geospatial data, and quickly find what they want to see using an interactive map.

Web Application Design

We choose to use the Freight Analysis Framework (FAF) data for this demonstration. FAF is a public data source that provides estimates and forecasts of Freight movements among states and major metropolitan areas by all modes of transportation.

The geographic information associated with the FAF data is defined as zones. There are in total 129 FAF zones in the continental U.S. region and every zone can be either an origin zone or a destination zone. In other words, there are 129×129 possible combinations of the origin-destination zone pairs.

Users can explore the freight movement data between any origin and destination zone pair of their choices. Instead of selecting a zone from a long list of 129 zones, this tool offers an intuitive way to select a zone directly from the map where users simultaneously get the zone information of location, size and boundary.

Freight Analysis Framework Zones (Image by Author)
Freight Analysis Framework Zones (Image by Author)

Users can choose what data they want to see by selecting origin and destination zone directly from the map by clicking on the zone. They can zoom in and out or pan the map to find the zone of interest.

If an origin is clicked, the centroid of the origin zone will be displayed in green. If a destination zone is clicked, the centroid of the destination zone will be displayed in red. We will explain later in this article how to determine the zone type (i.e., origin vs. destination) when a zone is selected.

Once both the origin and destination zones are selected, the R script at the backend of the application will query the FAF data and display the results as data tables and charts for the selected origin-destination zone pair.

We will explain step by step the implementation of this web application as follows:

  1. A brief introduction of R and R Shiny
  2. Layout the user interface (UI)
  3. Set up the server to generate outputs
  4. Publish Shiny app for free

Note: the code and data of this article can be found at this GitHub repo.

1. A brief introduction of R and R Shiny

R is a language and environment for statistical computing and graphics. One of R’s strength is the ease with which well-designed publication-quality plots can be produced. (REF)

Shiny is a web application framework for R. It combines the computational power of R with the interactivity of the modern web. Shiny provides superb capability for developers to create web applications that can be deployed using your own server or R Shiny’s hosting services (REF).

Structure of a Shiny App

A Shiny app is a directory containing two R scripts, i.e., ui.R and server.R __ and other input files to the app.

ui.R controls the layout and appearance of the app and creates the user interface in a Shiny application. It provides interactivity to the application by taking user input and dynamically displaying the generated output on the screen.

server.R contains the instructions for building the app logic, so it acts like the brain of the app. It converts the inputs given by user into desired outputs, such as tables and charts, to be displayed on the screen.

Alternatively, Shiny app can consist of a single file called app.R which contains both the UI and server components. The basic structure of a Shiny app with one file (i.e., app.R) looks like this:

library(shiny)
ui <- fluidPage(
  # front end interface
)
server <- function(input, output, session) {
  # back end logic
}
shinyApp(ui = ui, server = server)

It is generally considered a good practice to keep two separate R files, i.e., ui.R and server.R, especially for larger applications where having separate ui.R and server.R files makes the code easier to manage.

2. Layout the user interface (UI)

First we load all the required libraries to the app. The application interface is usually created using fluidPage __ which ensures that the page is laid out dynamically based on the resolution of each device (REF), so that the application interface runs smoothly on different devices with different screen resolutions.

We use fluidRow and column to build our custom layout up from a grid system. Rows are created by the fluidRow() function and include columns defined by the column() function (REF).

We also use Shiny’s HTML tag functions to add content to the user interface, such as br, div, span, HTML, etc. These functions parallel common HTML5 tags (REF).

The title of the application "Freight Analysis Framework FAF4 vs. FAF5, Year 2017" added using a span() function is displayed at the top of the interface, followed by a fluidRow() with a map called "Zone" on the left, and a group of static (e.g., span("select")) and dynamic text outputs (e.g., htmlOutput("od_info")) on the right.

Shiny apps contain input and output objects. Inputs permit users interact with the app by modifying their values (We will discuss input objects later). Outputs are objects that are shown in the app (REF). The output object is always used in concert with a render function, for instance:

ui <- fluidPage(
  leafletOutput("Zone")
)
server <- function(input, output, session) {
  output$Zone <- renderLeaflet({
    # generate the map
  })
}

In ui we use leafletOutput(), and in server() we use renderLeaflet(). Inside renderLeaflet() we write the instructions to return a leaflet map.

Types of output objects shown in this app include:

leaflet is used to create an interactive map, e.g., leafletOutput("Zone")

html is used to display a reactive output variable as HTML, e.g., htmlOutput("od_info")

DT is used to display data in an interactive table, e.g., DT:dataTableOutput("od_vol")

plotly is used to create interactive graphs with data, e.g., plotlyOutput("od_ton_chart")

3. Set up the server to generate outputs

Import data files

The data files are in the same location as the R files. We use read.csv() function to read the CSV files (centroid.csv and od_mode_vol_45.csv), and use readOGR() function of the rgdal package to read the shapefile of FAF zone (faf4_zone2.shp).

Each feature in the zone shapefile is a multi-polygon. The following is a sample of the attribute data from the shapefile. Each row in the table defines a multi-polygon feature (i.e., zone) with zone ID, zone name and the geometry.

Data Sample from faf4_zone2 (Image by Author)
Data Sample from faf4_zone2 (Image by Author)

Each row in centroid.csv defines a FAF zone centroid (i.e, the center location of the zone) with zone ID, zone name, longitude and latitude. Note that the zone ID and zone name in centroid.csv correspond to those in the shapefile. Therefore we can use zone ID as the key to refer from zone polygon to zone centroid and vice versa.

Data Sample from centroid.csv (Image by Author)
Data Sample from centroid.csv (Image by Author)

od_mode_vol_45.csv __ contains the freight data in terms of weight and value by transportation mode (e.g., truck, rail, water, air, etc), for each origin-destination zone pair (defined using zone IDs). The data in this file was engineered from the original FAF version 5 (FAF5) data and FAF version 4 (FAF4) data.

FAF5 data provides freight movement estimates of year 2017; while FAF4 data provides freight movement forecasts for year 2017. This application compares the 2017 data of FAF5 vs. FAF4, for each origin-destination pair.

The origin ID and destination ID in file od_mode_vol_45.csv correspond to the zone ID field in centroid.csv __ and faf4_zone2.shp.

Data Sample from od_mode_vol_45.csv (Image by Author)
Data Sample from od_mode_vol_45.csv (Image by Author)

Use global variables to track zone selection

Let’s address the elephant in the room first. The app needs to tell whether the zone clicked (or selected) is supposed to be an origin zone or a destination zone.

We assume that when a user starts using the app, the first zone clicked would be the origin zone, and the second zone clicked would be the destination zone.

However, what if the user wants to continue using the app by selecting another pair of origin and destination zones? What if the user continuously clicks on the map and keeps changing the selection?

To solve this problem, we need to keep track of the total number of times a user clicks on the map.

The following global variables are used to keep track of users’ zone selection actions. As we need to access and change the value of these variables in different functions, it is necessary that we define these variables as global variables.

click_count <- 0
type <- 0
origin <- ""
dest <- ""
origin_id <- 0
dest_id <- 0
  • Variable click_count is used for tracking the accumulative total number of clicks on the map since the session starts. Note that its initial value is set to 0.
  • Variable type is used for translating the click_count into zone type, i.e., origin zone or destination zone. It’s value is calculated by modulus click_count by 2:
# with click_count initiated with value 0
type <<- click_count%%2
if (type ==0 ){
  # treat as origin zone
}     
if (type == 1){
  # treat as destination zone
}
# add one to the accumulative map click counts
click_count <<- click_count+1

The above code will be embedded in a function, and to change the value of a global variable inside a function, we need to use the global assignment operator <<- . When use the assignment operator <<-, the variable belongs to the global scope.

  • Variable origin and dest are used for storing the descriptive zone name of the selected origin and destination zones.
  • Variable origin_id and dest_id are used for storing the zone IDs of the selected origin and destination zones.

Reactive programming in Shiny

Shiny uses a reactive programming model to simplify the development of R-powered web applications. Shiny takes input from the user interface and listen for changes with observe or reactive.

The reactive source typically is user input through a browser interface. We create reactivity by including the value of the input input$variable_selected in the reactive expressions (such as render*() and reactive() function) in server() that build the outputs. When input changes, all the outputs that depend on the input will be automatically updated (or rebuilt) using the updated input value.

Leaflet maps and objects send input values (or events) to Shiny as the user interacts with them. Object event names generally use this pattern:

input$MAPID_OBJCATEGORY_EVENTNAME

For leafletOutput("Zone"), clicking on one of the polygon shape (i.e., FAF zone) would update the Shiny input at input$Zone_shape_click.

The clicking event is set to either NULL if the event has never happened, or a list() that includes: the latitude and longitude of the object is available; otherwise, the mouse cursor; and the layerId, if any.

In our case, a layerId field based on zone ID is included in the polygons object; so the value of the zone ID will be returned in event$Id of the input$Zone_shape_click event.

addPolygons(data=zone.rg, col="black", weight = 1, layerId = ~id, label = zone_labels, highlight = highlightOptions(color = "blue",weight = 2, bringToFront = F, opacity = 0.7))

We want the server side of the application to respond to clicking on the map, so we need to create reactive expressions to incorporate the user choice using reactive() functions. Using reactive() functions also help to reduce duplications in the code.

selected_zone() returns the centroid of the zone clicked (or selected).

selected_zone <- reactive({
      p <- input$Zone_shape_click
      subset(centroid, id==p$id )
    })

selected_od() returns the information of the selected origin-destination zone pair (including zone names and zone IDs). Note that it waits until both the origin and the destination zone is selected to return the value.

If a new origin zone is selected, the destination zone selected in the previous round will be reset.

selected_od <- reactive({
      p <- input$Zone_shape_click
      # return selected origin-destination zone pair
    })

Using Leaflet with Shiny

Leaflet is one of the most popular open-source JavaScript libraries for interactive maps. The Leaflet package includes powerful and convenient features for integrating with Shiny applications (REF). A Leaflet map can be created with these basic steps:

  1. Create a map widget by calling leaflet()
  2. Add layers (i.e., features) to the map by using layer functions (e.g., addTiles, addMarkers, addPolygons) to modify the map widget.
  3. Repeat step 2 as desired.
  4. Print the map widget to display it.
output$Zone <- renderLeaflet({
  # create zone labels
  # create map widget called m
  # add base map and the FAF zone polygon layer
})

Observers use eager evaluation strategy, i.e., as soon as their dependencies change, they schedule themselves to re-execute.

Now we update the map corresponding to input$Zone_shape_click. The observer will automatically re-execute when input$Zone_shape_click change.

observe({
      p <- input$Zone_shape_click # get input value
      if (is.null(p))             
        return()

      m2<-leafletProxy("Zone", session = session) # get map widget

      # create zone labels

      selected <- selected_zone() # get selected zone centroid
      # create selected zone label

      type <<- click_count%%2
      if (type ==0 ){ 
        # clear the centroids displayed
        # add a marker, the centroid of the new origin zone
      }

      if (type == 1){
        # add a marker, the centroid of the new destination zone
      }
      click_count <<- click_count+1    # keep track of map clicks
    })

First get the selected zone, and determine if the selected zone is an origin zone or a destination zone using the logic explained earlier. Update the map by displaying the centroid of the selected zone, and use different colors for origin and destination.

To modify a map that’s already running in the page, we use the leafletProxy() function in place of the leaflet() call.

Normally we use leaflet to create the static aspects of the map, and leafletProxy to manage the dynamic elements, so that the map can be updated at the point of a click without redrawing the entire map. Details on how to use this function are given in the RStudio website.

Adding Outputs

Now we show the data in the Shiny app by including several outputs for interactive visualization.

The output$od_info __ object is a reactive endpoint, and it uses the reactive source input$Zone_shape_click. Whenever input$Zone_shape_click changes, output$od_info is notified that it needs to re-execute. This output dynamically displays the names of the selected origin zone and destination zone.

Output from od_info (Image by Author)
Output from od_info (Image by Author)

We use the R package DT to display the data objects (matrices or data frames) as tables on HTML pages. In output$od_vol, we first get the information on the selected origin and destination zone through selected_od() and then filter the FAF data stored in od_mode_vol to get the results for the selected origin destination zone pair.

FAF data comparison for selected origin destination zone pair (Image by Author)
FAF data comparison for selected origin destination zone pair (Image by Author)

In addition to showing the query results as a DataTable, we also display the results using bar charts and donut charts.

Image by Author
Image by Author

We use the renderPlotly() function to render a reactive expression that generates plotly graphs responding to the input value input$Zone_shape_click __ through selected_od()__.

To make a donut chart, the hole= argument of add_pie() function is used to set the fraction of the radius to cut out of the pie. The value can be between 0.01 and 0.99 .

4. Publish Shiny app for free

Shinyapps.io is a software as a service (SaaS) product for hosting Shiny apps in the cloud. RStudio takes care of all of the details of hosting the app and maintaining the server, so we can focus on writing apps.

A Shiny app can be deployed on shinyapps.io in just a few minutes. Just follow this detailed step by step instructions.

Shinyapps.io offers __ both free and paid plans. With the free plan, you get up to 5 Shiny apps deployed simultaneously, and up to 25 active hours per month shared by all the applications hosted under the account.


Join Medium with my referral link – Huajing Shi

R codes and input data are available at my GitHub repo.


References

  1. The basic parts of a Shiny app
  2. Getting started with shinyapps.io
  3. Creating Interactive data visualization using Shiny App in R (with examples)

Related Articles