Climate projections by cities: R + Shiny + rCharts + leaflet

[This article was first published on R – SNAP Tech, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

I have approached a final draft of my Community Charts version 4 Lite, now with leaflet map integration. This R Shiny web application shows projected climate trends for various Alaska and western Canada communities. Note that if no other users are already connected to the app, it will take a moment (maybe ten seconds?) to load the initial data set into the global environment, but there is an indicator bar at the top of the screen in this case.

The graphing is done with the rCharts package and the graphs now have responsive width. The interface is much cleaner after removing the screen space-wasting bootstrap button groups from prior versions in favor of drop down menus. The key addition this helped make room for is an interactive map which I incorporated into the app using the leaflet package. Like the Shiny package, the leaflet package is also by RStudio. It makes using leaflet through R quite easy. Full app source code is available.

cc4liteFinal

Syncing a leaflet map and a selection menu with event observation

I already had a menu for typing/selecting a community for graphing climate trends. What should happen if I suddenly add a competing widget, in this case a leaflet map, for selecting a community? The cool thing about the leaflet map within the Shiny app is that I can use it as an alternative to the community selection menu without having to replace the menu. The user now has an option of how they want to provide location input. Of course, some conflicts do need to be resolved. Having two different input controls for selecting the same thing is obviously problematic if not done right. What happens if I select community A from the dropdown menu and then click on a circle marker in the map to select community B?

First the data for the initial selection will be graphed. When the next selection is made using the map, either nothing will happen because the map selection is not actually tied to the graph like the menu is, or if they both are tied then it could supersede the menu and a new graph may be drawn. However, then there is a graph for a given community, but not the one listed as selected in the communities menu! A third possibility, and an easy one, is to throw errors of various kinds due to conflicts.

Clearly, the goal when offering two methods to the user for making a selection is to code the respective input controls to mutually update each other so as to always be in sync. It is straightforward enough to allow the menu input to operate as it normally would, but also add an observer to note any map marker clicks and, if a click occurs, force-update the dropdown menu selection to match. In ui.R everything remains simple. The two relevant lines in this app are:

selectInput("location", "Community", c("", locs), selected="", multiple=F)) # locs is a nested list of communities

leafletOutput("Map")

As you can see, nothing unsual is required on the UI side for selectInput even though that map is going to be used for providing the same location reactive input selection to server.R for data subsetting and graphing. On the server side, first that leaflet map reactive output must be generated. Again, nothing unusual:

# cities.meta below is a data frame with locations, populations, etc.
output$Map <- renderLeaflet({
  leaflet() %>% addProviderTiles("CartoDB.Positron") %>%
  setView(lng=-140, lat=57, zoom=4) %>%
  addCircleMarkers(data=cities.meta, radius = ~sqrt(10*PopClass),
    color = ~palfun(PopClass), stroke=FALSE, fillOpacity=0.5, layerId = ~Location)
})

Now, here is how to update the menu if the user clicks a location on the map:

observeEvent(input$Map_marker_click, {
  p <- input$Map_marker_click
  if(!is.null(p$id)){
    if(is.null(input$location)) updateSelectInput(session, "location", selected=p$id)
    if(!is.null(input$location) && input$location!=p$id) updateSelectInput(session, "location", selected=p$id)
  }
})

We could get away without that last if statement which only allows updating of the menu if it does not already match the map selection. However, we also need to update the map selection based on any changes the user may make to the menu selection, be that to select another city or remove any selection. This has infinite recursion written all over it. Change a selection in either the map or the menu, and the other will update, but once it updates, this change can trigger the first input to update again, and so on… That’s bad.

Honestly, I haven’t tested all possibilities because I didn’t find the idea of trying to achieve infinite recursion very enticing. So it is possible that I was at less risk than I thought. Nevertheless, I felt it was best practice to use some conditional statements to protect against this. At least most of the conditionals are in fact necessary. Without them, I didn’t end up with any infinite recursion, but I did experience buggy behavior and a failure of proper mutual updating of the two inputs.

Next we need two observers for updating the map. One observes any changes due to clicking on the map itself (something I don’t need to do with the menu) and the other observes for changes to the menu.

observeEvent(input$Map_marker_click, {
  p <- input$Map_marker_click
  if(p$id=="Selected"){
    leafletProxy("Map") %>% removeMarker(layerId="Selected")
  } else {
    leafletProxy("Map") %>% setView(lng=p$lng, lat=p$lat, input$Map_zoom) %>%
    addCircleMarkers(p$lng, p$lat, radius=10, color="black",
      fillColor="orange", fillOpacity=1, opacity=1, stroke=TRUE, layerId="Selected")
  }
})

observeEvent(input$location, {
  p <- input$Map_marker_click
  p2 <- subset(cities.meta, Location==input$location)
  if(nrow(p2)==0){
    leafletProxy("Map") %>% removeMarker(layerId="Selected")
  } else {
    leafletProxy("Map") %>% setView(lng=p2$Lon, lat=p2$Lat, input$Map_zoom) %>%
    addCircleMarkers(p2$Lon, p2$Lat, radius=10, color="black",
      fillColor="orange", fillOpacity=1, opacity=1, stroke=TRUE, layerId="Selected")
  }
})

Note that the purpose of the first is just to draw a unique circle on the map signifying that a map marker has been clicked. When a marker, which has a location-based ID such as “Anchorage, Alaska”, is clicked, a new circle marker is drawn on top and given the ID, “Selected”. If the user clicks the same location a second time, they are actually clicking this top layer circle marker ID, “Selected”, hence the conditional check which removes or deselects that location.

This would be plenty if there was no selectInput control. Since there is, we need the second observeEvent, which is analogous to the one I used for updating selectInput. In this one, if the user deletes the selection in the menu, the corresponding marker is removed from the map (if there is one to delete). Otherwise, the “Selected” ID marker is updated in the map. This automatically removes any existing layer of the same class (in this case a marker) with an identical ID, so there is no need to explicitly call removeMarker first, similar to the previous observeEvent call.

With this kind of event observation I can allow the user to use either the type-filter selectize-based approach via the menu or panning, zooming, and clicking on a map visually to select a community, whichever method they prefer. The two inputs remain synchronized with each other and no odd app behavior occurs regarding either reactive input. Technically, I suppose I should refer to the leaflet map as a reactive output. It has the appearance, through use of obervers, of being an input.

A quick note on the responsive theme of the app

The general Shiny app web page and widgets it contains, as well as the rCharts plot and the leaflet map, all consist of responsive elements which can automatically adjust their size (or at least width) and/or display style with respect to different device screens. One thing I really like about this app is that I can actually interact with it on my Android phone. It’s best when held in landscape mode of course, and it just barely fits, but it displays nicely and in a way that I can scroll up or down to access what can’t all be fit into one cellphone screen. Here are two screenshots of the full scrolling view. I had to split them on my pc since I couldn’t fit it all on the screen at once.

cc4L_phoneView_2

cc4L_phoneView_1

The only notable issue is if the Highcharts-based plot made using the rCharts package has text lines for the title and/or subtitle which are too long. They can begin to run into each other and/or the color key as width becomes constrained. I wouldn’t normally care to use an app like this on a tiny device, but it is nice to see that an app designed for a computer screen can still be nicely accessible on a much smaller device with little additional effort.

Other R packages used in this app include shinythemes, shinyBS, and plyr. The piping done with %>% in the code above comes from the magrittr package via plyr. More information about the charts and data can be found in the Community Charts v4 documentation. The documentation website was produced using rmarkdown and knitr, streamlined and semi-automated with my developmental rpm code.

To leave a comment for the author, please follow the link and comment on their blog: R – SNAP Tech.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)