Select points on maps

When you want to show spatial data with additional information for each data point you can use shiny for interactive exploration.

Richard Vogg https://github.com/richardvogg
02-12-2021

The result

This is what we want to achieve. The user can select one or more points on the map to see additional information for the position.

Packages

To run the example successfully, I needed the following packages.

The data

We will work with some example data, consisting in random points and for each of the points some temporal information.

lon lat month_1 month_2 month_3 month_4 month_5 month_6 month_7
9.0 52.2 30 31 41 58 76 91 76
9.5 53.6 24 34 26 43 56 65 69
12.3 49.4 78 94 108 128 134 136 134
8.3 52.2 28 42 52 65 67 70 90
8.2 50.1 58 76 78 74 73 95 108

We first convert the data to be in tidy format (using pivot_longer from the {tidyr} package), i.e. we will have one column called month and one column containing the values.

lon lat month value
9 52.2 1 30
9 52.2 2 31
9 52.2 3 41
9 52.2 4 58
9 52.2 5 76
9 52.2 6 91
9 52.2 7 76
9 52.2 8 74

Quick exploration

Let’s see what we can do with this data. First, we can use borders to show a quick map.

map <- ggplot(df)+
  geom_point(aes(x=lon,y=lat))+
  borders(regions="Germany")+
  coord_quickmap()+
  theme_void()

map

As we want to be able to select points interactively, we want to display which points were selected. This is what the map would look like.

exmpl <- df %>% slice(1) %>% select(lon,lat)

map + 
  geom_point(data=exmpl,aes(x=lon,y=lat),size=3,col="red")

Last, we want to show the temporal development for the selected points. This can be an easy line chart, for the filtered point.

df %>%
  filter(lon == exmpl$lon,lat == exmpl$lat) %>%
  mutate(month=factor(month.abb[month],levels=month.abb)) %>%
    ggplot(aes(x=month,group=1))+
    geom_line(aes(y=value),size=2)

Functions

To make the Shiny app short and concise, I will put the functionalities which create the three plots into functions.

Shiny App

First, we will define the user interface. In this case we have a sidebar panel which contains the map and a main panel where the line plot will appear when points are selected.

The important lines to notice are that in plotOutput for the map, we will add the parameters click and brush. Doing this, we can later define what happens when someone clicks on the points.

ui <- fluidPage(

    sidebarLayout(
        sidebarPanel(
          p("Please select one or various points."),
            plotOutput("example_map", 
                       click = "plot1_click",
                       brush = "plot1_brush"
            )
        ),

        mainPanel(
            plotOutput("time_plot")
        )
    )
)

Inside the server function, we have several steps, which I will explain one by one. First, we need some variables that will be updated when the user selects points. lat, lon and time are empty in the beginning. For map, we will plot the initial map which shows all points.

vals <- reactiveValues(lat=NULL,
                       lon=NULL,
                       map=plot_map(df), 
                       time = NULL) 

For the two plot outputs it is easy, we will just return the respective variable of the reactive value, i.e. the map and the line chart.

output$example_map <- renderPlot({
  return(vals$map)
})
    
output$time_plot <- renderPlot({
  return(vals$time)
})

The key for the triggered action is the process of selecting a point. Remember that in the ui part we specified another parameter for the map: click = "plot1_click". Here we will specify what happens when someone clicks on the map.

First, we will check if there is a close point to the click in the dataset df. If this is not the case, nothing happens. Otherwise, we will assign the coordinates of the selected point to vals$lat and vals$lon our reactive variables. Additional we will change the map from the standard map to the map with selected red points and will show the line plot for the selected points. As these are reactive values, our plots will be updated automatically.

observeEvent(input$plot1_click, {
    point <- nearPoints(df %>% distinct(lon,lat), input$plot1_click, addDist = FALSE)
    if(length(point[[1]])==0) {} #happens when no point is selected
    else {
        vals$lon <- point[[1]]
        vals$lat <- point[[2]]
        vals$map <- plot_selected_point_in_map(df,vals$lon,vals$lat)
        vals$time <-  show_detail(df,vals$lon,vals$lat)
    }
})

You will see that the brush option is very similar. Note, that there are two other options, hover and dblclick which can trigger actions when you just hover over a points or double click. This would be the complete server function when putting the steps together.

server <- function(input, output) {
    
    vals <- reactiveValues(lat=NULL,
                           lon=NULL,
                           map=plot_map(df),
                           time = NULL)
    
    output$example_map <- renderPlot({
        return(vals$map)
    })
    
    output$time_plot <- renderPlot({
      return(vals$time)
        
    })
    
    observeEvent(input$plot1_click, {
        point <- nearPoints(df %>% distinct(lon,lat), input$plot1_click, addDist = FALSE)
        if(length(point[[1]])==0) {} #happens when no point is selected
        else {
            vals$lon <- point[[1]]
            vals$lat <- point[[2]]
            vals$map <- plot_selected_point_in_map(df,vals$lon,vals$lat)
            vals$time <- show_detail(df,vals$lon,vals$lat)
        }
        
    })
    
    observeEvent(input$plot1_brush, {
        point <- brushedPoints(df %>% distinct(lon,lat), input$plot1_brush)
        if(length(point[[1]])==0) {} #happens when no point is selected
        else {
            vals$lon <- point[[1]]
            vals$lat <- point[[2]]
            vals$map <- plot_selected_point_in_map(df,vals$lon,vals$lat)
            vals$time <- show_detail(df,vals$lon,vals$lat)
        }
    })
}

Closing comments

library(wakefield)

df <- r_data_frame(
  n=50,
  lon = runif(), 
  lat = runif(), 
  r_series(age,j=12,integer=TRUE, relate = "+5_10", name="month")
) %>%
  mutate(lon=round(7.5+(12.5-7.5)*lon,1),
         lat = round(48+(54-48)*lat,1))

Later I transformed the data to long format.

df <- df %>%
  tidyr::pivot_longer(cols=starts_with("month"), names_to = "month",values_to="value") %>%
  mutate(month=readr::parse_number(month))