R Leaflet Tutorial

Your own web map with R Leaflet


Overview

We’ve just created a whole bunch of data and so far it is just a bunch of shapefiles (or geojson files – depending on what you exported) and .csv files. We haven’t even had the chance to look at what we’ve made! While there are a dozen tools we could use to visualize and interact with our data (e.g. Tilemill, Mapbox Studio, CartoDB, Tableau, etc), we are going to stick with R for now and let it “gently” introduce us to the fundamentals of a web map.

Web Map Fundamentals

A number of great people out there have created an overview of web map fundamentals, let’s take a look and learn how it all works:

Alan McConchie & Beth Schechter! – anatomy of a web map

Maptime ATX – web map fundamentals

Joey’s Hello Web Maps Intro

Enter: R Leaflet

So if we know that to make a web map generally is composed of:

  1. map tiles (most of the time)leaflet_tiles
  1. geodatageodata
  2. html/css/javascript (your webstack)htmlcss.png

And we also know that:

  1. we know little or nothing about html/css/javascript
  2. we haven’t made any map tiles

THEN HOW THE HELL ARE WE SUPPOSED TO MAKE OUR OWN WEB MAP???

Well, turns out that others have also been in your same predicament and have developed a library to bring web mapping to R programmers.

Wait a second… so we can make a web map, without coding any html, css, or javascript?

YES! sort of. Some clever people got together and wrote a library in R that takes a very famous and awesome javascript library (yes there are libraries in javascript, and every other language out there!) called “Leaflet.js” and allows you to write R code and export a fully functional web map, with tiles and geodata drawn right on top!

Basically what happens is:

  1. you load up the “leaflet” library for R
  2. you write R code that then gets translated into html/css/javascript
  3. your translated code is written into an “.html” file which includes:
    • your html skeleton
    • the leaflet.js library and leaflet css style
    • your javascript which was translated from your R code

Our First Example

With 5 lines of code, we’re going to make our first interactive web map!

leaflet_test.png

Run these lines of code and create your first interactive map with R!

# install the leaflet library
install.packages("leaflet")

# add the leaflet library to your script
library(leaflet)
    
# initiate the leaflet instance and store it to a variable
m = leaflet()
    
# we want to add map tiles so we use the addTiles() function - the default is openstreetmap
m = addTiles(m)
    
# we can add markers by using the addMarkers() function
m = addMarkers(m, lng=-123.256168, lat=49.266063, popup="T")
    
# we can "run"/compile the map, by running the printing it
m

Now if we export the map and save as webpage…:

leaflet_webpage.png

The convention for naming .html files is “index”, therefore, let’s name our file: index.html

leaflet_html

CRAZY – with just 4 lines of code, you added a pin to a map that now works on the interwebz! Let’s scale this up and use our dataset and see what we can come up with!


Working with our 3-1-1 Data


hex_van

So let’s shift gears and work with our 3-1-1 data. We now we have a bunch of points for the month of January and hexagonal grids with the call densities. How do we begin to interact with the data?

What are the main interactions we are going to work with?:

  1. pop-up details (aka tool tips) on mouse click
  • reveal specific case type
    • reveal counts
  1. toggle layers on and off

** Let’s get started with a rather verbose first example**

Version 1: Working with Point Data

point_van.png

# reading in a csv file - our data_filter.csv from the 311 Tutorial
# this string tells my R where to find data_filter.csv on my laptop.
# you will need to change it to tell your R where to find your own
# file! 
filename = "/Users/andrasszeitz/Desktop/GEOB_472/data_filter.csv"
data_filter = read.csv(filename, header = TRUE)

# if you are not able to read in your own data, use this as a backup
# NOTE - you need to have the `curl` package loaded for this to work
data_filter = read.csv(curl('https://raw.githubusercontent.com/joeyklee/aloha-r/master/data/calls_2014/201401CaseLocationsDetails-geo-filtered.csv'), header=TRUE)

We’re going to use the subset() function to get store our data into separate variables

# subset out your data_filter:
df_graffiti_noise = subset(data_filter, cid == 1)
df_street_traffic_maint = subset(data_filter, cid == 2)
df_garbage_recycling_organics = subset(data_filter, cid == 3)
df_water = subset(data_filter, cid == 4)
df_animal_vegetation = subset(data_filter, cid == 5)
df_other = subset(data_filter, cid == 0)

Great, now each of our data categories lives in its own variable name.

Initiate Base Map Tiles

Now we’re going to initiate our leaflet map:

# initiate leaflet
m = leaflet()

# add openstreetmap tiles (default)
m = addTiles(m)

# you can now see that our maptiles are rendered
m

Define Symbology for Point Layers

Now we are going to define some colors using leaflet’s special colorFactor() function:

colorFactors = colorFactor(c('red', 'orange', 'purple', 'blue', 'pink', 'brown'),
                           domain = data_filter$cid)

What the colorFactor function does is take our list of colors and “maps” them to the domain that we defined. For example, when we apply the colorFactor() function to our data, it will color a point “red”, if the “cid” in the data is equal to “0”, “orange” if the “cid” is equal to 1, etc.

The next step is to add our points to the map. We can do so using the addCircleMarkers() function:

m = addCircleMarkers(m, 
                     lng = df_graffiti_noise$lon_offset, # we feed the longitude coordinates 
                     lat = df_graffiti_noise$lat_offset,
                     popup = df_graffiti_noise$Case_Type, 
                     radius = 2, 
                     stroke = FALSE, 
                     fillOpacity = 0.75, 
                     color = colorFactors(df_graffiti_noise$cid),
                     group = "1 - graffiti & noise"
                     )

Here’s what each argument is saying:

  • lng: we add our markers to our m
  • lat:we feed the latitude coordinates
  • popup:each feature will show the Case_Type on click
  • radius: we set the circle radius size
  • stroke: we set stroke to false
  • fillOpacity: we set a fill opacity
  • color: the colorFactors() function is applying the color based on the value of the “cid”
  • group:we name the group

Now repeat this function across the other layers:

m = addCircleMarkers(m, 
                     lng = df_street_traffic_maint$lon_offset, lat=df_street_traffic_maint$lat_offset, 
                     popup = df_street_traffic_maint$Case_Type, 
                     radius = 2, 
                     stroke = FALSE, fillOpacity = 0.75,
                     color = colorFactors(df_street_traffic_maint$cid),
                     group = "2 - street & traffic & maintenance")
m = addCircleMarkers(m, 
                     lng = df_garbage_recycling_organics$lon_offset, lat=df_garbage_recycling_organics$lat_offset, 
                     popup = df_garbage_recycling_organics$Case_Type, 
                     radius = 2, 
                     stroke = FALSE, fillOpacity = 0.75,
                     color = colorFactors(df_garbage_recycling_organics$cid),
                     group = "3 - garbage related")
m = addCircleMarkers(m, 
                     lng = df_water$lon_offset, lat=df_water$lat_offset, 
                     popup = df_water$Case_Type, 
                     radius = 2, 
                     stroke = FALSE, fillOpacity = 0.75,
                     color = colorFactors(df_water$cid),
                     group = "4 - water related")
m = addCircleMarkers(m, 
                     lng = df_animal_vegetation$lon_offset, lat=df_animal_vegetation$lat_offset, 
                     popup = df_animal_vegetation$Case_Type, 
                     radius = 2,
                     stroke = FALSE, fillOpacity = 0.75,
                     color = colorFactors(df_animal_vegetation$cid),
                     group = "5 - animals & vegetation")
m = addCircleMarkers(m, 
                     lng = df_other$lon_offset, lat=df_other$lat_offset, 
                     popup = df_other$Case_Type, 
                     radius = 2,
                     stroke = FALSE, fillOpacity = 0.75,
                     color = colorFactors(df_other$cid),
                     group = "0 - other")

Other Options for Base Map Tiles

Now we’re going to add some additional map tiles by using the addTiles() and the addProviderTiles() functions:

m = addTiles(m, group = "OSM (default)") 
m = addProviderTiles(m,"Stamen.Toner", group = "Toner")
m = addProviderTiles(m, "Stamen.TonerLite", group = "Toner Lite")

We can now add in our layer toggle control using the addLayersControl() function – notice baseGroups are our tileLayers and the overlayGroups are our data layers:

m = addLayersControl(m,
                     baseGroups = c("Toner Lite","Toner"),
                     overlayGroups = c("1 - graffiti & noise", "2 - street & traffic & maintenance",
                                       "3 - garbage related","4 - water related", "5 - animals & vegetation",
                                       "0 - other")
)

# make the map
m

Version 2: Optimizing version 1

point_van2.png

We already subset our data from before:

# subset out your data_filter:
df_graffiti_noise = subset(data_filter, cid == 1)
df_street_traffic_maint = subset(data_filter, cid == 2)
df_garbage_recycling_organics = subset(data_filter, cid == 3)
df_water = subset(data_filter, cid == 4)
df_animal_vegetation = subset(data_filter, cid == 5)
df_other = subset(data_filter, cid == 0)

Here’ we are:

  1. adding those variables to a list so we can loop through them
  2. creating a list of our layers that we want the toggle name to be
  3. looping though the list
# Now we're going to put those variables into a **list** so we can loop through them:
data_filterlist = list(df_graffiti_noise = subset(data_filter, cid == 1),
                df_street_traffic_maint = subset(data_filter, cid == 2),
                df_garbage_recycling_organics = subset(data_filter, cid == 3),
                df_water = subset(data_filter, cid == 4),
                df_animal_vegetation = subset(data_filter, cid == 5),
                df_other = subset(data_filter, cid == 0))

# Remember we also had these groups associated with each variable? Let's put them in a list too:
layerlist = c("1 - graffiti & noise", "2 - street & traffic & maintenance",
              "3 - garbage related","4 - water related", "5 - animals & vegetation",
              "0 - other")

# We can keep that same color variable:
colorFactors = colorFactor(c('red', 'orange', 'purple', 'blue', 'pink', 'brown'), domain=data_filter$cid)

# Now we have our loop - each time through the loop, it is adding our markers to the map object:
for (i in 1:length(data_filterlist)){
  m = addCircleMarkers(m, 
                        lng=data_filterlist[[i]]$lon_offset,
                        lat=data_filterlist[[i]]$lat_offset, 
                        popup=data_filterlist[[i]]$Case_Type, 
                        radius=2,
                        stroke = FALSE, 
                        fillOpacity = 0.75,
                        color = colorFactors(data_filterlist[[i]]$cid),
                        group = layerlist[i]
              )
}

Now this time we flip “overlayGroups” and “baseGroups” so that we can get the radiobutton functionality – that way only 1 category of calls are shown:

m = addTiles(m, "Stamen.TonerLite", group = "Toner Lite") 
m = addLayersControl(m,
                      overlayGroups = c("Toner Lite"),
                      baseGroups = layerlist
                      )
m

Version 3: Working with Polygons

hex_van2.png

Remember we creates that hexagonal grid to aggregate our data to? Well, let’s work with it to get a general impression of the call density across the city:

If you haven’t already, you should have the rgdal and the GISTools libraries loaded:

library(rgdal)
library(GISTools)

Now we need to read in our hexgrid we aggregted our data to:

# define the filepath
hex_1401_fn = 'https://raw.githubusercontent.com/joeyklee/aloha-r/master/data/calls_2014/geo/hgrid_250m_1401_counts.geojson'

# read in the geojson
hex_1401 = readOGR(hex_1401_fn, "OGRGeoJSON")
## OGR data source with driver: GeoJSON 
## Source: "https://raw.githubusercontent.com/joeyklee/aloha-r/master/data/calls_2014/geo/hgrid_250m_1401_counts.geojson", layer: "OGRGeoJSON"
## with 1418 features
## It has 5 fields

Now initiate a new map object but this time with the Stamen Toner lite style:

m = leaflet()
m = addProviderTiles(m, "Stamen.TonerLite", group = "Toner Lite")

Like any choropleth map, we need to set a color scale. We can do so by using the colorNumeric() function which is part of the R leaflet package.

We use the “Greens” color and set the “domain” to the column called “data” in our geojson file.

Here’s the exciting stuff. Let’s add our polygon to the map:

# Create a continuous palette function
pal = colorNumeric(
  palette = "Greens",
  domain = hex_1401$data
)

# add the polygons to the map
m = addPolygons(m, 
                data = hex_1401,
                stroke = FALSE, 
                smoothFactor = 0.2, 
                fillOpacity = 1,
                color = ~pal(hex_1401$data),
                popup = paste("Number of calls: ", hex_1401$data, sep="")
                )

let’s break this down:

  • m: this is our map object
  • data: this is our spatialPolygonsDataframe
  • smoothFactor: adding some smooth operator
  • fillOpacity: changing how transparent object fill is
  • color: we let our colorNumeric() function handle the color
  • popup: we set popup on click to our call density

And what is a choropleth map without a legend? Let’s add one using the addLegend() function:

m = addLegend(m, "bottomright", pal = pal, values = hex_1401$data,
              title = "3-1-1 call density",
              labFormat = labelFormat(prefix = " "),
              opacity = 0.75
)

m

Version 3: Synthesis

hex_van3.png

Now that we’ve got the two pieces of our map – toggleable & clickable point layers and hex grid – let’s put them together.

# initiate leaflet map layer
m = leaflet()
m = addProviderTiles(m, "Stamen.TonerLite", group = "Toner Lite") 

# --- hex grid --- #
# store the file name of the geojson hex grid
hex_1401_fn = 'https://raw.githubusercontent.com/joeyklee/aloha-r/master/data/calls_2014/geo/hgrid_250m_1401_counts.geojson'

# read in the data
hex_1401 = readOGR(hex_1401_fn, "OGRGeoJSON")
## OGR data source with driver: GeoJSON 
## Source: "https://raw.githubusercontent.com/joeyklee/aloha-r/master/data/calls_2014/geo/hgrid_250m_1401_counts.geojson", layer: "OGRGeoJSON"
## with 1418 features
## It has 5 fields
# Create a continuous palette function
pal = colorNumeric(
  palette = "Greens",
  domain = hex_1401$data
)

# add hex grid
m = addPolygons(m, 
                data = hex_1401,
                stroke = FALSE, 
                smoothFactor = 0.2, 
                fillOpacity = 1,
                color = ~pal(hex_1401$data),
                popup= paste("Number of calls: ",hex_1401$data, sep=""),
                group = "hex"
)

# add legend
m = addLegend(m, "bottomright", pal = pal, values = hex_1401$data,
              title = "3-1-1 call density",
              labFormat = labelFormat(prefix = " "),
              opacity = 0.75
)

# --- points data --- #

# subset out your data:
df_graffiti_noise = subset(data_filter, cid == 1)
df_street_traffic_maint = subset(data_filter, cid == 2)
df_garbage_recycling_organics = subset(data_filter, cid == 3)
df_water = subset(data_filter, cid == 4)
df_animal_vegetation = subset(data_filter, cid == 5)
df_other = subset(data_filter, cid == 0)

data_filterlist = list(df_graffiti_noise = subset(data_filter, cid == 1),
                df_street_traffic_maint = subset(data_filter, cid == 2),
                df_garbage_recycling_organics = subset(data_filter, cid == 3),
                df_water = subset(data_filter, cid == 4),
                df_animal_vegetation = subset(data_filter, cid == 5),
                df_other = subset(data_filter, cid == 0))
layerlist = c("1 - graffiti & noise", "2 - street & traffic & maintenance",
              "3 - garbage related","4 - water related", "5 - animals & vegetation",
              "0 - other")


colorFactors = colorFactor(c('red', 'orange', 'purple', 'blue', 'pink', 'brown'), domain=data_filter$cid)
for (i in 1:length(data_filterlist)){
  m = addCircleMarkers(m, 
                       lng=data_filterlist[[i]]$lon_offset, lat=data_filterlist[[i]]$lat_offset, 
                       popup=data_filterlist[[i]]$Case_Type, 
                       radius=2,
                       stroke = FALSE, fillOpacity = 0.75,
                       color = colorFactors(data_filterlist[[i]]$cid),
                       group = layerlist[i]
  )

}


m = addLayersControl(m,
                     overlayGroups = c("Toner Lite", "hex"),
                     baseGroups = layerlist
)

# show map
m

Congratulations you just made your first super awesome and fully functional web map using R!

Now, play with your maps and try to identify weaknesses and strengths of your map. Are the colors appropriate? could they be better? what’s missing? REFINE YOUR STUFF


Review:


In this tutorial we covered a lot of ground. We’re neither masters of R nor data viz experts, however we got the chance to go through the entire data viz pipeline:

  • acquire: pulled 3-1-1 data from github
  • parse: manipulated table data and geocoded the data
  • filter: removed points that fell outside of the bounding box of Vancouver
  • mine: classified the data & added jitter
  • represent: web mapping
  • refine: web mapping
  • interact: web mapping
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s