This tutorial focuses on the two main tools for looking at mapping Spatial* objects – the basic plot
command, and the more refined plot command spplot
from the sp
library. Note there are also ways to use tools like ggplot
and ggmap
(designed to make working with maps in ggplot
easier),, but in this workshop we’ll focus on plot
and spplot
.
Although there are other tools available for more sophisticated applications, most plotting situations can be handled through the use of two functions:
plot
: plot shapes associated with Spatial* or Raster objectsspplot
: plot shapes associated with Spatial* objects AND color them based on attributes in a associated DataFrame.The syntax for each should be relatively familiar to anyone used to working with plot
in other settings – just pass the sp object to plot!
palo_alto <- readOGR("RGIS3_Data/palo_alto", "palo_alto")
OGR data source with driver: ESRI Shapefile
Source: "RGIS3_Data/palo_alto", layer: "palo_alto"
with 371 features and 5 fields
Feature type: wkbPolygon with 2 dimensions
plot(palo_alto)
It’s also easy to add some basic options using standard plot
modifiers:
plot(palo_alto, border = "red")
title(main = "Palo Alto", sub = "By Census Tracts")
When plotting lines, you can also use lots of the basic plot
options for lines. For example, the lwd
option will determine the width of lines, and col
their color.
freeways <- readOGR("RGIS3_Data/palo_alto", "palo_alto_freeways")
OGR data source with driver: ESRI Shapefile
Source: "RGIS3_Data/palo_alto", layer: "palo_alto_freeways"
with 955 features and 45 fields
Feature type: wkbLineString with 2 dimensions
par(mfrow = c(1, 2))
plot(freeways, col = "red", bg = "blue")
plot(freeways, lwd = 10, col = "green")
Points work similarly, with pch
determining shape and cex
determining size. You can find a table of symbol-to-number mappings here
plot
It’s also easy to plot multiple layers using plot
with the add
option:
stopifnot(proj4string(palo_alto) == proj4string(freeways)) # Check in same projection before combining!
plot(palo_alto)
plot(freeways, col = "blue", add = T)
spplot
is an extension of plot
specifically for making maps of Spatial* objects. In particular, it’s very useful for filling in polygon colors based on attributes in an associated DataFrame (what are called “chloropleth” maps). Just pass an Spatial*DataFrame object and the name of columns you want to plot (if you don’t pass specific column names, a separate figure will be created for each column.)
Note the col="transparent"
option just supresses the plotting of polygon borders for a slightly better ascetic.
spplot(palo_alto, "PrCpInc", main = "Palo Alto Demographics", sub = "Average Per Capita Income",
col = "transparent")
A big list of example graphs with associated code can be found here
By default, spplot
will zoom to the extent of the Spatial* object being plotted. However, this can be overridden by setting the xlim
and ylim
parameters, which determine the edges of the plot.
However, these can be a little hard to work with. With that in mind, the following code allows the user to modify the zoom and shift the center of the plot with three more intuitive parameters:
# Change these parameters
scale.parameter = 0.5 # scaling paramter. less than 1 is zooming in, more than 1 zooming out.
xshift = -0.1 # Shift to right in map units.
yshift = 0.2 # Shift to left in map units.
original.bbox = palo_alto@bbox # Pass bbox of your Spatial* Object.
# Just copy-paste the following
edges = original.bbox
edges[1, ] <- (edges[1, ] - mean(edges[1, ])) * scale.parameter + mean(edges[1,
]) + xshift
edges[2, ] <- (edges[2, ] - mean(edges[2, ])) * scale.parameter + mean(edges[2,
]) + yshift
# In `spplot`, set xlim to edges[1,] and ylim to edges[2,]
spplot(palo_alto, "PrCpInc", main = "Palo Alto Demographics", sub = "Average Per Capita Income",
col = "transparent", xlim = edges[1, ], ylim = edges[2, ])
spplot
If you don’t like the default color scheme, you can use your own with ColorBrewer, which you can install with install.packages("RColorBrewer")
. Once loaded, you can see a list of all the color pallets that come with RColorBrewer
with the command:
library(RColorBrewer)
display.brewer.all()
Once you’ve picked a palette you like, create a palette object as follows, where n
is the number of cuts you want to use, and name
is the name of the color ramp. Note that different palettes have different limits on the maximium and minimium number of cuts allowed.
my.palette <- brewer.pal(n = 7, name = "OrRd")
Then you can just pass this pallet to spplot
, making sure to set the number of cuts to 1 minus the number of colors.
spplot(palo_alto, "PrCpInc", col.regions = my.palette, cuts = 6, col = "transparent")
If you don’t want evenly spaced cutoffs between colors, you can use the classInt
library to make custom cuts.
library(classInt)
breaks.qt <- classIntervals(palo_alto$PrCpInc, n = 6, style = "quantile", intervalClosure = "right")
spplot(palo_alto, "PrCpInc", col = "transparent", col.regions = my.palette,
at = breaks.qt$brks)
Pick a city to plot. Three cities are included in RGIS3_Data, or find your own.
Read-in the city polygons.
Use spplot
to plot average incomes for your city with default colors.
Now use RColorBrewer to map income using a custom color scheme.
Use the classInt
library to cut income by quantiles rather than even intervals.
spplot
spplot
allows users to add multiple layers or symbols using the sp.layout
argument. To use sp.layout
, you first create a new list where:
You then pass this list to the sp.layout
argument. For example, if I wanted to overlay the buffered grant locations on electoral districts, I would use the following codes:
# Create a layer-list
freeway.layer <- list("sp.lines", freeways, col = "green")
# Plot with layer-list passed to `sp.layout`
spplot(palo_alto, "PrCpInc", sp.layout = freeway.layer, col = "transparent")
If you want to add multiple layers, just combine the layer-lists as follows. Note that order of items in the list will determine plotting order! Here’s an example where I plot freeways twice so I can get a different color scheme.
# Create a layer-list
freeway.layer <- list("sp.lines", freeways, col = "green", lwd = 10)
freeway2.layer <- list("sp.lines", freeways, col = "red", lwd = 2)
# Plot with layer-list passed to `sp.layout`
spplot(palo_alto, "PrCpInc", sp.layout = list(freeway.layer, freeway2.layer),
col = "transparent")
You can also use the sp.layout
option to add other things, like compass arrows or scales. In most cases, the key to this is to set “offset”, which defines the location of the bottom left hand corner of what you’re working with. Getting offsets right will kinda drive you crazy.
Here’s a small handfull:
palo_alto_proj <- spTransform(palo_alto, CRS("+init=EPSG:32611")) # Can't make a scale if not projected!
palo_alto_proj@bbox # Check dimensions to help guide offset choices
min max
x 39130.08 103617.3
y 4092787.57 4160027.3
scale = list("SpatialPolygonsRescale", layout.scale.bar(), scale = 25000, fill = c("transparent",
"black"), offset = c(41000, 4100000))
# The scale argument sets length of bar in map units
text1 = list("sp.text", c(41000, 4104000), "0")
text2 = list("sp.text", c(66000, 4104000), "25 km")
arrow = list("SpatialPolygonsRescale", layout.north.arrow(), offset = c(90000,
4150000), scale = 5000)
spplot(palo_alto_proj, "PrCpInc", sp.layout = list(scale, text1, text2, arrow))
Using the same city data you used for Exercise 1, overlay the freeway shapefile over your city. Make the freeways red.
Add a compass somewhere reasonable on the map.
BONUS: Add a scale bar.
Dot-Density plots are a favorite these days, and they can be made relatively easily with the use of spplot
and the dotsInPolys
tool from the maptools
library. Not much to explain here, so here’s just an example of making a dot-density plot for Santa Clara county from census polygons!
library(maptools)
# Get census polygons
census <- readOGR("RGIS3_data/palo_alto", "palo_alto")
OGR data source with driver: ESRI Shapefile
Source: "RGIS3_data/palo_alto", layer: "palo_alto"
with 371 features
It has 5 fields
# Get feel for data with white population (largest group)
spplot(census, "White")
# Setting a seed is a good idea -- since points are random, it's helpful for
# replication to make sure this code will always make the same result.
set.seed(47)
# Create a fixed number of points at random locations within each polygon
# based on a polygon variable. Since the field values here are the number
# of people, we can get one dot per 300 people as follows:
people.per.dot = 700
dots.w <- dotsInPolys(census, as.integer(census$White/people.per.dot))
dots.w$ethnicity <- "White"
dots.h <- dotsInPolys(census, as.integer(census$hispanc/people.per.dot))
dots.h$ethnicity <- "Hispanic"
# Gather all the dots into a single SpatialPoints
dots.all <- rbind(dots.w, dots.h)
proj4string(dots.all) <- CRS(proj4string(palo_alto))
# Since ethnicity is a string, order is alphabetical. You can change if you
# want by making these categoricals!
my.palette <- c("red", "blue")
point.size <- 0.5
# Make sure stored as a factor
dots.all$ethnicity <- as.factor(dots.all$ethnicity)
# Make sp.layout list for the actually boundaries
census.tract.layer <- list("sp.polygons", census)
spplot(dots.all, "ethnicity", sp.layout = census.tract.layer, col.regions = my.palette,
cex = point.size, main = "Demographic Distribution of Santa Clara County")
Make a dot-density plot for your own city!
Change the number of people per dot and the size of dots.
Overlay your freeway data. How do freeways affect segregation?
Both plot
and spplot
can suppot overlaying Spatial* objects over basemaps, with differing degrees of difficulty.
Before getting into specifics, one very important caveat: basemaps are generally just rasters, but not all functions for adding basemaps to your maps will include information on the projection of the basemap raster.
Warning: Almost all basemaps you will get from common sources (like google maps) use a very special projection – WGS84 Web Mercator (EPSG:3857). If you use a tool like ggmap
, be very careful with this, as the maps it generates are in latitudes and longitudes (which makes you think it’s regular WGS84), but if you treat the data as WGS84, you will have serious alignment issues. ALl tools presented here will address that issue.
If you don’t need to color your plots according to an attribute of your Spatial* data, the easiest way to plot basemaps is with the gmap
command from the dismo
library. gmap
returns a google map, and you can modify the type
argument to get different types of google maps (it will accept ‘roadmap’, ‘satellite’, ‘hybrid’, ‘terrain’).
Note that unlike the somewhat more popular get_map
function in the ggmap
library, the gmap
function in dismo
creates a RasterLayer complete with correct projection information!
library(dismo)
library(raster)
# Pass object whose extent you plan to plot as first argument and map-type
# as second.
base.map <- gmap(palo_alto, type = "terrain")
reprojected.palo_alto <- spTransform(palo_alto, base.map@crs)
plot(base.map)
plot(reprojected.palo_alto, add = T, border = "red", col = "transparent")
Unfortunately, adding a basemap to an spplot
is much more complicated for a few reasons. Most of the code below can just be copy-pasted to make basemaps.
The reason – for those of you who are interested – is that the library that creates basemaps in a format that work with spplot
wants to know about the bounds of the figure in latitude and longitude. The problem is that the projection these basemaps use does not use latitude and longitude as it’s native coordinates. Thus you have to convert your image into the right projection (WGS84 Web Mercator), find the dimensions of your plot in those units, pass some of those back to the basemap function as longitudes and latitudes, then tell spplot what the dimensions of the basemap are in the WGS84 Web Mercator units. If you don’t do this, you’ll get this problem.
library(ggmap)
# REPROJECT YOUR DATA TO EPSG 3857
to.plot.web.merc <- spTransform(dots.all, CRS("+init=EPSG:3857"))
# COPY AND PASTE SEGEMENT 1 Series of weird conversions to deal with
# inconsistencies in units for API.
box <- to.plot.web.merc@bbox
midpoint <- c(mean(box[1, ]), mean(box[2, ]))
left.bottom <- c(box[1, 1], box[2, 1])
top.right <- c(box[1, 2], box[2, 2])
boundaries <- SpatialPoints(rbind(left.bottom, top.right))
proj4string(boundaries) <- CRS("+init=EPSG:3857")
boundaries.latlong <- c(t(spTransform(boundaries, CRS("+init=EPSG:4326"))@coords))
# END COPY-PASTE SEGMENT 1
# SET MAP TYPE HERE, LEAVE OTHER PARAMETERS AS THEY ARE
gmap <- get_map(boundaries.latlong, maptype = "terrain", source = "stamen",
crop = TRUE)
# COPY-PASTE SEGMENT 2 Create object that sp.layout likes.
long.center <- midpoint[1]
lat.center <- midpoint[2]
height <- box[2, 2] - box[2, 1]
width <- box[1, 2] - box[1, 1]
sp.raster <- list("grid.raster", gmap, x = long.center, y = lat.center, width = width,
height = height, default.units = "native", first = TRUE)
# END COPY-PASTE SEGMENT 2
# NORMAL PLOTTING TRICKS - HAVE FUN HERE!
# Housecleaning and set colors
to.plot.web.merc$ethnicity <- as.factor(to.plot.web.merc$ethnicity)
my.palette <- c("red", "blue")
point.size <- 0.5
# Plot!
spplot(to.plot.web.merc, "ethnicity", sp.layout = sp.raster, col.regions = my.palette,
cex = point.size, main = "Demographic Distribution of Santa Clara County")
Plot the census tracts of your city over a base-map using the plot
command.
Plot your demographic dot-plot with a basemap using the spplot
command.
The ggplot2
library is a popular graphing library, and it can be used for mapping spatial data. The main trick is that ggplot
will only accept DataFrames as inputs, so you have to first convert your spatial data into DataFrames using the fortify
command from the ggplot2
library.
I am not a frequent ggplot
user, so I’ll just include some workable code (taken almost verbatum from Claudia Engel’s excellent tutorials) as guidance to interested parties.
library(ggplot2)
# create a unique ID for the later join
palo_alto$id = rownames(as.data.frame(palo_alto))
# turn SpatialPolygonsDataframe into a data frame
# (note that the rgeos library is required to use fortify)
palo_alto.pts <- fortify(palo_alto, region="id") #this only has the coordinates
palo_alto.df <- merge(palo_alto.pts, palo_alto, by="id", type='left') # add the attributes back
# calculate even breaks breaks
palo_alto.df$qt <- cut(palo_alto.df$PrCpInc, 5)
# plot
ggplot(palo_alto.df, aes(long,lat,group=group, fill=qt)) + # the data
geom_polygon() + # make polygons
scale_fill_brewer("Per Cap Income", palette = "OrRd") + # fill with brewer colors
theme(line = element_blank(), # remove the background, tickmarks, etc
axis.text=element_blank(),
axis.title=element_blank(),
panel.background = element_blank()) +
coord_equal()
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.