Spatial Data - Displaying time series, spatial and space-time data with R

Table of Contents

1. Proportional symbol mapping

airMadrid.png

airMadrid_krige.png

1.1. Interactive Graphics

1.1.1. mapView

mapview_simple.png

mapview_popup_images.png

mapview_popup_graphs.png

mapview_sync.png

1.1.2. rgl

rgl_bubble.png

2. Choropleth maps

pcMax.png

whichMax.png

mapLegends.png

3. Cartogram

cartContPopSpain.png

cartDorlPopSpain.png

cartNContPopSpain.png

4. Raster maps

leveplotSISavBoundaries.png

hillShading.png

divPalSISav_classInt.png

landClass.png

populationNASA.png

popLandClass.png

4.1. Interactive Graphics

DEM_WebGL.png

mapview_CMSAF_SIAR.png

5. Vector fields

vectorplot.png

streamplot.png

6. Physical maps

brazilGG.png

7. Reference maps

cedeiraOSMlattice.png

8. Code

8.1. Proportional symbol mapping

Download

##################################################################
## Initial configuration
##################################################################
## Clone or download the repository and set the working directory
## with setwd to the folder where the repository is located.
 
library("lattice")
library("ggplot2")
## latticeExtra must be loaded after ggplot2 to prevent masking of its
## `layer` function.
library("latticeExtra")

library("RColorBrewer")

source("configLattice.R")
##################################################################

library("sp")
library("sf")

NO2sf <- st_read(dsn = "data/Spatial/", layer = "NO2sf")

airPal <- colorRampPalette(c("springgreen1", "sienna3", "gray5"))(5)

##################################################################
## Proportional symbol: ggplot
##################################################################

## Create a categorical variable
NO2sf$Mean <- cut(NO2sf$mean, 5)

ggplot(data = NO2sf) + 
    geom_sf(aes(size = Mean, fill = Mean),
            pch = 21, col = 'black') +
    scale_fill_manual(values = airPal) +
    theme_bw()

##################################################################
## Proportional symbol: sppplot
##################################################################

NO2sp <- as(NO2sf, "Spatial")

spplot(NO2sp["mean"],
       col.regions = airPal, ## Palette
       cex = sqrt(1:5), ## Size of circles
       edge.col = "black", ## Color of border
       scales = list(draw = TRUE), ## Draw scales
       key.space = "right") ## Put legend on the right

##################################################################
## Optimal classification and sizes to improve discrimination
##################################################################

library("classInt")
## The number of classes is chosen between the Sturges and the
## Scott rules.
nClasses <- 5
intervals <- classIntervals(NO2sp$mean, n = nClasses, style = "fisher")
## Number of classes is not always the same as the proposed number
nClasses <- length(intervals$brks) - 1

op <- options(digits = 4)
tab <- print(intervals)
options(op)

## Complete Dent set of circle radii (mm)
dent <- c(0.64, 1.14, 1.65, 2.79, 4.32, 6.22, 9.65, 12.95, 15.11)
## Subset for our dataset
dentAQ <- dent[seq_len(nClasses)]
## Link Size and Class: findCols returns the class number of each
## point; cex is the vector of sizes for each data point
idx <- findCols(intervals)
cexNO2 <- dentAQ[idx]

## spplot version
NO2sp$classNO2 <- factor(names(tab)[idx])  

## Definition of an improved key with title and background
NO2key <- list(x = 0.99, y = 0.01, corner = c(1, 0),
               title = expression(NO[2]~~(paste(mu, plain(g))/m^3)),
               cex.title = 0.8, cex = 1,
               background = "gray92")

pNO2 <- spplot(NO2sp["classNO2"],
               col.regions = airPal,
               cex = dentAQ * 0.8,
               edge.col = "black",
               scales = list(draw = TRUE),
               key.space = NO2key)
pNO2

## ggplot2 version
NO2sf$classNO2 <- factor(names(tab)[idx])  

ggplot(data = NO2sf) +
    geom_sf(aes(size = classNO2, fill = classNO2),
            pch = 21, col = "black") +
    scale_fill_manual(values = airPal) +
    scale_size_manual(values = dentAQ * 2)  +
    xlab("") + ylab("") + theme_bw()

##################################################################
## Spatial context with underlying layers and labels
##################################################################

##################################################################
## OpenStreetMap
##################################################################

library("osmdata")

madridBox <- st_bbox(NO2sf)

qosm <- opq(madridBox) %>%
  add_osm_feature(key = "highway",
                  value = "residential")

qsf <- osmdata_sf(qosm)

library("ggrepel")

ggplot()+
  ## Layers are drawn sequentially, so the NO2sf layer must be in
  ## the last place to be on top
  geom_sf(data = qsf$osm_lines["name"],
          size = 0.3,
          color = "lightgray") +
  geom_sf(data = NO2sf,
          aes(size = classNO2,
              fill = classNO2),
          pch = 21, col = "black") +
  ## Labels for each point, with position according to the circle size
  ## and the rest of labels
  geom_text_repel(data = NO2sf,
                  aes(label = substring(codEst, 7),
                      geometry = geometry,
                      point.size = classNO2),
                  stat = "sf_coordinates") + 
  scale_fill_manual(values = airPal) +
  scale_size_manual(values = dentAQ * 2) +
  labs(x = NULL, y = NULL) + theme_bw()

qsp <- osmdata_sp(qosm)

qspLines <- list("sp.lines", qsp$osm_lines["name"],
                 lwd = 0.1)

spplot(NO2sp["classNO2"],
       col.regions = airPal,
       cex = dentAQ,
       edge.col = "black",
       alpha = 0.8,
       sp.layout = qspLines,
       scales = list(draw = TRUE),
       key.space = NO2key)

##################################################################
## Shapefiles
##################################################################

## Madrid districts
unzip("data/Spatial/distr2022.zip", exdir = tempdir())

distritosMadridSF <- st_read(dsn = tempdir(),
                             layer = "dist2022")
## Filter the streets of the Municipality of Madrid
distritosMadridSF <- distritosMadridSF[distritosMadridSF$CMUN == "079",]
## Assign the geographical reference
distritosMadridSF <- st_transform(distritosMadridSF,
                                  crs = "WGS84")

## Madrid streets
unzip("data/Spatial/call2022.zip", exdir = tempdir())

streetsMadridSF <- st_read(dsn = tempdir(),
                           layer = "Gdie_g_calles")                           
streetsMadridSF <- streetsMadridSF[streetsMadridSF$CDMUNI == "079",]
streetsMadridSF <- st_transform(streetsMadridSF,
                                crs = "WGS84")

ggplot()+
  geom_sf(data = streetsMadridSF,
          size = 0.1,
          color = "darkgray") +
  geom_sf(data = distritosMadridSF,
          fill = "lightgray",
          alpha = 0.2,
          size = 0.15,
          color = "black") +
  geom_sf(data = NO2sf,
          aes(size = classNO2,
              fill = classNO2),
          pch = 21, col = "black") + 
  geom_text_repel(data = NO2sf,
                  aes(label = substring(codEst, 7),
                      geometry = geometry,
                      point.size = classNO2),
                  size = 2.5,
                  stat = "sf_coordinates") + 
  scale_fill_manual(values = airPal) +
  scale_size_manual(values = dentAQ * 2) +
  labs(x = NULL, y = NULL) + theme_bw()

distritosMadridSP <- as(distritosMadridSF, "Spatial")
streetsMadridSP <- as(streetsMadridSF, "Spatial")
## Lists using the structure accepted by sp.layout, with the polygons,
## lines, and points, and their graphical parameters
spDistricts <- list("sp.polygons", distritosMadridSP,
                    fill = "gray97", lwd = 0.3)
spStreets <- list("sp.lines", streetsMadridSP,
                  lwd = 0.05)

## spplot with sp.layout version
spplot(NO2sp["classNO2"],
       col.regions = airPal,
       cex = dentAQ,
       edge.col = "black",
       alpha = 0.8,
       sp.layout = list(spDistricts, spStreets),
       scales = list(draw = TRUE),
       key.space = NO2key)

## lattice with layer version
pNO2 +
    ## Polygons and lines *below* (layer_) the figure
    layer_(
    {
        sp.polygons(distritosMadridSP,
                    fill = "gray97",
                    lwd = 0.3)
        sp.lines(streetsMadridSP,
                 lwd = 0.05)
    })

##################################################################
## Spatial interpolation
##################################################################

library("gstat")

## Sample 10^5 points locations within the bounding box of NO2sp using
## regular sampling
airGrid <- spsample(NO2sp, type = "regular", n = 1e5)
## Convert the SpatialPoints object into a SpatialGrid object
gridded(airGrid) <- TRUE
## Compute the IDW interpolation
airKrige <- krige(mean ~ 1, NO2sp, airGrid)

spplot(airKrige["var1.pred"], ## Variable interpolated
       col.regions = colorRampPalette(airPal)) +
  layer({ ## Overlay boundaries and points
    sp.polygons(distritosMadridSP,
                fill = "transparent",
                lwd = 0.3)
    sp.lines(streetsMadridSP,
             lwd = 0.07)
    sp.points(NO2sp,
              pch = 21,
              alpha = 0.8,
              fill = "gray50",
              col = "black")
  })

##################################################################
## Interactive graphics
##################################################################

##################################################################
## mapView
##################################################################

library("mapview")

pal <- colorRampPalette(c("springgreen1", "sienna3", "gray5"))

mapview(NO2sp,
        zcol = "mean", ## Variable to display
        cex = "mean", ## Use this variable for the circle sizes
        col.regions = pal,
        label = NO2sp$Name,
        legend = TRUE)

##################################################################
## Tooltips with images and graphs
##################################################################

library("leafpop")

img <- paste("images/Spatial/", NO2sp$codEst, ".jpg", sep = "")

mapview(NO2sp,
        zcol = "mean",
        cex = "mean",
        col.regions = pal, 
        label = NO2sp$Name,
        popup = popupImage(img, src = "local", embed = TRUE),
        map.type = "Esri.WorldImagery",
        legend = TRUE)

## Read the time series
airQuality <- read.csv2("data/Spatial/airQuality.csv")
## We need only NO2 data (codParam 8)
NO2 <- subset(airQuality, codParam == 8)
## Time index in a new column
NO2$tt <- with(NO2,
               as.Date(paste(year, month, day, sep = "-")))
## Stations code
stations <- unique(NO2$codEst)
## Loop to create a scatterplot for each station.
pList <- lapply(stations,
                function(i)
                    xyplot(dat ~ tt, data = NO2,
                           subset = (codEst == i),
                           type = "l",
                           xlab = "", ylab = "")
                )

mapview(NO2sp,
        zcol = "mean",
        cex = "mean",
        col.regions = pal, 
        label = NO2sp$Name,
        popup = popupGraph(pList),
        map.type = "Esri.WorldImagery",
        legend = TRUE)

##################################################################
## Synchronise multiple graphics  
##################################################################

library("leafsync")

## Map of the average value
mapMean <- mapview(NO2sp, zcol = "mean", cex = "mean",
                   col.regions = pal, legend = TRUE,
                   map.types = "OpenStreetMap.Mapnik",
                   label = NO2sp$Name)

## Map of the median
mapMedian <- mapview(NO2sp, zcol = "median", cex = "median",
                     col.regions = pal, legend = TRUE,
                     #map.type = "NASAGIBS.ViirsEarthAtNight",
                     label = NO2sp$Name)

## Map of the standard deviation
mapSD <- mapview(NO2sp, zcol = "sd", cex = "sd",
                 col.regions = pal, legend = TRUE,
                 map.type = "Esri.WorldImagery",
                 label = NO2sp$Name)

## All together
sync(mapMean, mapMedian, mapSD, ncol = 3)

##################################################################
## GeoJSON and OpenStreepMap
##################################################################

st_write(NO2sf,
         dsn = "data/Spatial/NO2.geojson",
         layer = "NO2sp",
         driver = "GeoJSON")

##################################################################
## Keyhole Markup Language
##################################################################

st_write(NO2sf,
         dsn = "data/Spatial/NO2_mean.kml",
         layer = "mean",
         driver = "KML")

##################################################################
## 3D visualization
##################################################################

library("rgl")

## rgl does not understand Spatial* objects
NO2df <- as.data.frame(NO2sp)

## Color of each point according to its class
airPal <- colorRampPalette(c("springgreen1", "sienna3", "gray5"))(5)
colorClasses <- airPal[NO2df$classNO2]

plot3d(x = NO2df$coords.x1, 
       y = NO2df$coords.x2,
       z = NO2df$alt, 
       xlab = "Longitude", 
       ylab = "Latitude", 
       zlab = "Altitude", 
       type = "s", 
       col = colorClasses,
       radius = NO2df$mean/10)

8.2. Choropleth maps

Download

##################################################################
## Initial configuration
##################################################################
## Clone or download the repository and set the working directory
## with setwd to the folder where the repository is located.
 
library("lattice")
library("ggplot2")
## latticeExtra must be loaded after ggplot2 to prevent masking of its
## `layer` function.
library("latticeExtra")

library("RColorBrewer")

source("configLattice.R")
##################################################################

library("sp")

library("sf")

##################################################################
## Read data
##################################################################

sfMapVotes <- st_read("data/Spatial/sfMapVotes.shp")

sfMapVotes$whichMax <- factor(sfMapVotes$whichMax)
sfMapVotes$PROV <- factor(sfMapVotes$PROV)

summary(sfMapVotes)

##################################################################
## Province Boundaries
##################################################################

sfProvs <- st_read("data/Spatial/spain_provinces.shp",
                   crs = 25830)

##################################################################
## Quantitative variable
##################################################################

## Number of intervals (colors)
N <- 6
## Sequential palette
quantPal <- brewer.pal(n = N, "Oranges")

ggplot(sfMapVotes) +
  ## Display the pcMax variable...
  geom_sf(aes(fill = pcMax),
          ## without drawing municipality boundaries
          color = "transparent") +
  scale_fill_gradientn(colours = quantPal) +
  ## And overlay provinces boundaries
  geom_sf(data = sfProvs,
          fill = 'transparent',
          ## but do not include them in the legend
          show.legend = FALSE) +
  theme_bw()

spMapVotes <- as(sfMapVotes, "Spatial")
spProvs <- as(sfProvs, "Spatial")

## Number of cuts
ucN <- 1000
## Palette created with interpolation
ucQuantPal <- colorRampPalette(quantPal)(ucN)

## Province boundaries
provinceLines <- list("sp.polygons",
                      spProvs,
                      lwd = 0.1,
                      # draw the lines after the data
                      first = FALSE)

## Main plot
spplot(spMapVotes["pcMax"],
       col.regions = ucQuantPal,
       cuts = ucN,
       ## Do not draw municipality boundaries
       col = "transparent",
       ## Overlay province boundaries
       sp.layout = provinceLines)

##################################################################
## Data classification
##################################################################

ggplot(as.data.frame(spMapVotes),
       aes(pcMax,
           fill = whichMax,
           colour = whichMax)) +
    geom_density(alpha = 0.1) +
    theme_bw()

library("classInt")

## Compute intervals with the same number of elements
intQuant <- classIntervals(sfMapVotes$pcMax,
                           n = N, style = "quantile")
## Compute intervals with the natural breaks algorithm
intFisher <- classIntervals(sfMapVotes$pcMax,
                            n = N, style = "fisher")

plot(intQuant, pal = quantPal, main = "")

plot(intFisher, pal = quantPal, main = "")

## spplot solution

## Add a new categorical variable with cut, using the computed breaks
spMapVotes$pcMaxInt <- cut(spMapVotes$pcMax,
                           breaks = intFisher$brks,
                           include.lowest = TRUE)

spplot(spMapVotes["pcMaxInt"],
       col = "transparent",
       col.regions = quantPal,
       sp.layout = provinceLines)

## sf and geom_sf
sfMapVotes$pcMaxInt <- cut(sfMapVotes$pcMax,
                           breaks = intFisher$brks,
                           include.lowest = TRUE)

ggplot(sfMapVotes) +
  geom_sf(aes(fill = pcMaxInt),
          color = "transparent") +
  scale_fill_brewer(palette = "Oranges") +
  geom_sf(data = sfProvs,
          fill = "transparent",
          show.legend = FALSE) +
  theme_bw()

##################################################################
## Qualitative variable
##################################################################

classes <- levels(spMapVotes$whichMax)
nClasses <- length(classes)

qualPal <- brewer.pal(nClasses, "Dark2")

## spplot solution
spplot(spMapVotes["whichMax"],
       col.regions = qualPal,
       col = 'transparent',
       sp.layout = provinceLines)

## geom_sf solution
ggplot(sfMapVotes) +
  geom_sf(aes(fill = whichMax),
          color = "transparent") +
  scale_fill_brewer(palette = "Dark2") +
  geom_sf(data = sfProvs,
          fill = "transparent",
          show.legend = FALSE) +
  theme_bw()

##################################################################
## Small multiples
##################################################################

ggplot(sfMapVotes) +
  geom_sf(aes(fill = pcMaxInt),
          color = "transparent") +
  ## Define the faceting using two rows
  facet_wrap(~whichMax, nrow = 2) +
  scale_fill_brewer(palette = "Oranges") +
  geom_sf(data = sfProvs,
          fill = "transparent",
          size = 0.1,
          show.legend = FALSE) +
  theme_bw()

##################################################################
## Bivariate map
##################################################################

## PP and Cs -> Right
## PSOE and UP -> Left
levels(sfMapVotes$whichMax) <-
  c("ABS", "Right", "OTH", "Right", "Left", "Left")

## Number of steps.
Nint <- 4
## ABS - Greys, Right - Blues, OTH - Greens, Left - Reds
multiPal <- lapply(c("Greys", "Blues", "Greens", "Reds"),
                   function(pal) brewer.pal(Nint, pal))
multiPal <- do.call(rbind, multiPal)

library("biscale")


sfClass <- bi_class(sfMapVotes,
               x = whichMax,
               y = pcMax,
               style = "fisher",
               dim = 4)

bipal <- c(multiPal)

nms <- outer(1:4, 1:4, paste, sep = "-")
names(bipal) <- c(nms)

bilegend <- bi_legend(pal = bipal,
                      dim = 4,
                      xlab = "ABS-Right-OTH-Left",
                      ylab = "% of votes ",
                      size = 8)

bimap <- ggplot() +
  geom_sf(data = sfClass,
          aes(fill = bi_class),
          color = "white",
          size = 0.1,
          show.legend = FALSE) +
  bi_scale_fill(pal = bipal, dim = 4) +
  bi_theme()

library("cowplot")

ggdraw() +
  draw_plot(bimap, 0, 0, 1, 1) +
  draw_plot(bilegend, 0.05, 0.1,
            width = 0.2, height = 0.2)

## Define the intervals
intFisher <- classIntervals(spMapVotes$pcMax,
                            n = Nint, style = "fisher")
## ... and create a categorical variable with them
spMapVotes$pcMaxInt <- cut(spMapVotes$pcMax,
                           breaks = intFisher$brks)

levels(spMapVotes$whichMax) <-
  c("ABS", "Right", "OTH", "Right", "Left", "Left")

classes <- levels(spMapVotes$whichMax)
nClasses <- length(classes)

pList <- lapply(1:nClasses, function(i)
{
  ## Only those polygons corresponding to a level are selected
  mapClass <- subset(spMapVotes,
                     whichMax == classes[i])
  ## Palette
  pal <- multiPal[i, ]
  ## Produce the graphic
  pClass <- spplot(mapClass, "pcMaxInt",
                   col.regions = pal,
                   col = "transparent",
                   colorkey = FALSE)
})
names(pList) <- classes
p <- Reduce("+", pList)

op <- options(digits = 4)
tabFisher <- print(intFisher)
intervals <- names(tabFisher)
options(op)

library("grid")

legend <- layer(
{
    ## Position of the legend
    x0 <- 1000000
    y0 <- 4200000
    ## Width of the legend 
    w <- 120000
    ## Height of the legend
    h <- 100000
    ## Colors
    grid.raster(multiPal, interpolate = FALSE,
                x = unit(x0, "native"),
                y = unit(y0, "native"),
                width = unit(w, "native"),
                height = unit(h, "native"))
    ## x-axis (quantitative variable)
    Ni <- length(intervals)
    grid.text(intervals,                      
              y = unit(y0 - 1.25 * h/2, "native"),
              x = unit(seq(x0 - w * (Ni -1)/(2*Ni),
                           x0 + w * (Ni -1)/(2*Ni),
                           length = Ni),
                       "native"),
              just = "top",
              rot = 45, 
              gp = gpar(fontsize = 6))
    ## y-axis (qualitative variable)
    grid.text(classes,
              y = unit(seq(y0 + h * (nClasses -1)/(2*nClasses),
                           y0 - h * (nClasses -1)/(2*nClasses),
                           length = nClasses),
                       "native"),
              x = unit(x0 + w/2, "native"),
              just = "left",
              gp = gpar(fontsize = 6))
})

## Main plot
p + legend

##################################################################
## Interactive Graphics
##################################################################

library("mapview")

sfMapVotes0 <- st_read("data/Spatial/sfMapVotes0.shp",
                       crs = 25830)

## Quantitative variable, pcMax
mapView(sfMapVotes0,
        zcol = "pcMax", ## Choose the variable to display
        legend = TRUE,
        col.regions = quantPal)

## Qualitative variable, whichMax
mapView(sfMapVotes0,
        zcol = "whichMax",
        legend = TRUE,
        col.regions = qualPal)

8.3. Cartogram maps

Download

##################################################################
## Initial configuration
##################################################################
## Clone or download the repository and set the working directory
## with setwd to the folder where the repository is located.

##################################################################

library("ggplot2")
library("sf")
library("cartogram")

sfPopGDPSpain <- st_read("data/Spatial/sfPopGDPSpain.shp")

ggplot(sfPopGDPSpain) +
  geom_point(aes(y = Province, x = Population/1e6)) +
  xlab("Population (million)") + 
  theme_bw()

ggplot(sfPopGDPSpain) +
  geom_point(aes(y = Province, x = GDP/1e6)) +
  xlab("GDP (million euros)") + 
  theme_bw()

ggplot(sfPopGDPSpain, aes(y = Population/1e6, x = GDP/1e6)) +
  geom_point() +
  geom_smooth() + 
  xlab("GDP (million euros)") + ylab("Population (million)") +
  theme_bw()

cartPopNCont <- cartogram_ncont(sfPopGDPSpain, weight = "Population")

ggplot(cartPopNCont) +
  geom_sf(aes(fill = Population)) + 
  scale_fill_distiller(palette = "Blues", direction = 1) + 
  theme_bw()

cartGDPNCont <- cartogram_ncont(sfPopGDPSpain, weight = "GDP")

ggplot(cartGDPNCont) +
  geom_sf(aes(fill = GDP)) + 
  scale_fill_distiller(palette = "Blues", direction = 1) + 
  theme_bw()

cartPopCont <- cartogram_cont(sfPopGDPSpain,
                              weight = "Population")

ggplot(cartPopCont) +
  geom_sf(aes(fill = Population)) + 
  scale_fill_distiller(palette = "Blues", direction = 1) + 
  theme_bw()

cartGDPCont <- cartogram_cont(sfPopGDPSpain,
                              weight = "GDP")

ggplot(cartPopCont) +
  geom_sf(aes(fill = GDP)) + 
  scale_fill_distiller(palette = "Blues", direction = 1) + 
  theme_bw()

cartPopDorl <- cartogram_dorling(sfPopGDPSpain, weight = "Population")

ggplot(cartPopDorl) +
  geom_sf(aes(fill = Population)) + 
  scale_fill_distiller(palette = "Blues", direction = 1) + 
  theme_bw()

cartGDPDorl <- cartogram_dorling(sfPopGDPSpain, weight = "GDP")

ggplot(cartGDPDorl) +
  geom_sf(aes(fill = GDP)) + 
  scale_fill_distiller(palette = "Blues", direction = 1) + 
  theme_bw()

8.4. Raster maps

Download

##################################################################
## Initial configuration
##################################################################
## Clone or download the repository and set the working directory
## with setwd to the folder where the repository is located.
library("lattice")
library("ggplot2")
## latticeExtra must be loaded after ggplot2 to prevent masking of its
## `layer` function.
library("latticeExtra")

library("RColorBrewer")

source("configLattice.R")

##################################################################
## Quantitative data
##################################################################

library("raster")
library("terra")
library("sf")

library("rnaturalearth")
library("geodata")

library("viridisLite")

library("rasterVis")

SISavr <- raster("data/Spatial/SISav.nc")

SISavt <- rast("data/Spatial/SISav.nc")

levelplot(SISavr)

boundarySF <- ne_countries(country = "spain", scale = 50)
## Crop to the limits of the raster object
boundarySF <- st_crop(boundarySF,
                      xmin = xmin(SISavt), ymin = ymin(SISavt),
                      xmax = xmax(SISavt), ymax = ymax(SISavt))

##ggplot2 version
gplot(SISavt) +
  geom_sf(data = boundarySF, fill = "transparent")

## lattice version
## Convert the sf object to sp
boundarySP <- as(boundarySF, "Spatial")

## Display the data ...
levelplot(SISavt) +
  ## ... and overlay the SpatialLines object
  layer(sp.lines(boundarySP,
                 lwd = 0.5))

##################################################################
## Hill shading
##################################################################

DEM <- elevation_30s("ESP", path = tempdir())

slope <- terrain(DEM, "slope", unit = "radians")
aspect <- terrain(DEM, "aspect", unit = "radians")
hs <- shade(slope = slope, aspect = aspect,
            angle = 60, direction = 45)

DEMr <- raster(DEM)
sloper <- terrain(DEMr, "slope")
aspectr <- terrain(DEMr, "aspect")
hsr <- hillShade(slope = sloper, aspect = aspectr,
                 angle = 60, direction = 45)

## hillShade theme: gray colors and semitransparency
hsTheme <- GrTheme(regions = list(alpha = 0.5))

levelplot(SISavt,
          par.settings = YlOrRdTheme,
          margin = FALSE, colorkey = FALSE) +
  ## Overlay the hill shade raster
  levelplot(hs, par.settings = hsTheme, maxpixels = 1e6) +
  ## and the countries boundaries
  layer(sp.lines(boundarySP, lwd = 0.5))

##################################################################
## Diverging palettes
##################################################################

meanRad <- global(SISavt, "mean")
meanRad <- as.numeric(meanRad)
SISavt <- SISavt - meanRad

meanRad <- cellStats(SISavr, "mean")
SISavr <- SISavr - meanRad

xyplot(layer ~ y, data = SISavt,
       groups = cut(x, 5),
       par.settings = rasterTheme(symbol = magma(n = 5,
                                                 begin = 0, end = 0.9,
                                                 direction = -1)),
       xlab = "Latitude", ylab = "Solar radiation (scaled)",  
       auto.key = list(space = "right",
                       title = "Longitude",
                       cex.title = 1.3))

divPal <- brewer.pal(n = 9, "PuOr")
divPal[5] <- "#FFFFFF"

showPal <- function(pal)
{
    N <- length(pal)
    image(1:N, 1, as.matrix(1:N), col = pal,
          xlab = "", ylab = "",
          xaxt = "n", yaxt = "n",
          bty = "n")
}

showPal(divPal)

divTheme <- rasterTheme(region = divPal)

levelplot(SISavt, contour = TRUE, par.settings = divTheme)

rng <- range(SISavt[])
## Number of desired intervals
nInt <- 15
## Increment corresponding to the range and nInt
inc0 <- diff(rng)/nInt
## Number of intervals from the negative extreme to zero
n0 <- floor(abs(rng[1])/inc0)
## Update the increment adding 1/2 to position zero in the center of an interval
inc <- abs(rng[1])/(n0 + 1/2)
## Number of intervals from zero to the positive extreme
n1 <- ceiling((rng[2]/inc - 1/2) + 1)
## Collection of breaks
breaks <- seq(rng[1], by = inc, length= n0 + 1 + n1)

## Midpoints computed with the median of each interval
idx <- findInterval(SISavt[], breaks, rightmost.closed = TRUE)
mids <- tapply(SISavt[], idx, median)
## Maximum of the absolute value both limits
mx <- max(abs(breaks))

break2pal <- function(x, mx, pal){
    ## x = mx gives y = 1
    ## x = 0 gives y = 0.5
    y <- 1/2*(x/mx + 1)
    rgb(pal(y), maxColorValue = 255)
}

## Interpolating function that maps colors with [0, 1]
## rgb(divRamp(0.5), maxColorValue=255) gives "#FFFFFF" (white)
divRamp <- colorRamp(divPal)
## Diverging palette where white is associated with the interval
## containing the zero
pal <- break2pal(mids, mx, divRamp)
showPal(pal)

levelplot(SISavt,
          par.settings = rasterTheme(region = pal),
          at = breaks, contour = TRUE)

divTheme <- rasterTheme(regions = list(col = pal))

levelplot(SISavt,
          par.settings = divTheme,
          at = breaks,
          contour = TRUE)

cl <- classIntervals(SISavt[], style = "kmeans")
breaks <- cl$brks

## Repeat the procedure previously exposed, using the 'breaks' vector
## computed with classIntervals
idx <- findInterval(SISavt[], breaks, rightmost.closed = TRUE)
mids <- tapply(SISavt[], idx, median)

mx <- max(abs(breaks))
pal <- break2pal(mids, mx, divRamp)

## Modify the vector of colors in the 'divTheme' object
divTheme$regions$col <- pal

levelplot(SISavt,
          par.settings = divTheme,
          at = breaks,
          contour = TRUE)

##################################################################
## Categorical data
##################################################################

## raster
myExtR <- extent(65, 135, 5, 55)

popR <- raster("data/Spatial/875430rgb-167772161.0.FLOAT.TIFF")
popR <- crop(popR, myExtR)
popR[popR==99999] <- NA

landClassR <- raster("data/Spatial/241243rgb-167772161.0.TIFF")
landClassR <- crop(landClassR, myExtR)

## terra
myExtT <- ext(65, 135, 5, 55)

popT <- rast("data/Spatial/875430rgb-167772161.0.FLOAT.TIFF")
names(popT) <- "population"
popT <- crop(popT, myExtT)
popT[popT==99999] <- NA

landClassT <- rast("data/Spatial/241243rgb-167772161.0.TIFF")
names(landClassT) <- "landClass"
landClassT <- crop(landClassT, myExtT)

landClassR[landClassR %in% c(0, 254)] <- NA
## Only four groups are needed:
## Forests: 1:5
## Shrublands, etc: 6:11
## Agricultural/Urban: 12:14
## Snow: 15:16
landClassR <- cut(landClassR, c(0, 5, 11, 14, 16))
## Add a Raster Attribute Table and define the raster as categorical data
landClassR <- ratify(landClassR)
## Configure the RAT: first create a RAT data.frame using the
## levels method; second, set the values for each class (to be
## used by levelplot); third, assign this RAT to the raster
## using again levels
rat <- levels(landClassR)[[1]]
rat$classes <- c("Forest", "Land", "Urban", "Snow")
levels(landClassR) <- rat

landClassT[landClassT %in% c(0, 254)] <- NA
landClassT <- classify(landClassT, c(0, 5, 11, 14, 16))

rat <- levels(landClassT)[[1]]
names(rat) <- c("ID", "classes")
rat$classes <- c("Forest", "Land", "Urban", "Snow")
levels(landClassT) <- rat

qualPal <- c("palegreen4", # Forest
             "lightgoldenrod", # Land
             "indianred4", # Urban
             "snow3")      # Snow

qualTheme <- rasterTheme(region = qualPal,
                         panel.background = list(col = "lightskyblue1")
                         )

levelplot(landClassT, maxpixels = 3.5e5,
          par.settings = qualTheme)

pPop <- levelplot(popT, zscaleLog = 10,
                  par.settings = BTCTheme,
                  maxpixels = 3.5e5)
pPop

## Join the RasterLayer objects to create a RasterStack object.
s <- stack(popR, landClassR)
names(s) <- c("pop", "landClass")

## Join the SpatRaster objects to create a multilayer object.
st <- c(popT, landClassT)
names(st) <- c("pop", "landClass")

densityplot(~log10(pop), ## Represent the population
            groups = landClass, ## grouping by land classes
            data = s,
            ## Do not plot points below the curves
            plot.points = FALSE)

##################################################################
## Bivariate legend
##################################################################

classes <- rat$classes
nClasses <- length(classes)

logPopAt <- c(0, 0.5, 1.85, 4)

nIntervals <- length(logPopAt) - 1

multiPal <- sapply(1:nClasses, function(i)
{
    colorAlpha <- adjustcolor(qualPal[i], alpha = 0.4)
    colorRampPalette(c(qualPal[i],
                       colorAlpha),
                     alpha = TRUE)(nIntervals)
})

pList <- lapply(1:nClasses, function(i){
    landSub <- landClassR
    ## Those cells from a different land class are set to NA...
    landSub[!(landClassR == i)] <- NA
    ## ... and the resulting raster masks the population raster
    popSub <- mask(popR, landSub)
    ## Palette
    pal <- multiPal[, i]

    pClass <- levelplot(log10(popSub),
                        at = logPopAt,
                        maxpixels = 3.5e5,
                        col.regions = pal,
                        colorkey = FALSE,
                        margin = FALSE)
})

p <- Reduce('+', pList)

library("grid")

legend <- layer(
{
    ## Center of the legend (rectangle)
    x0 <- 125
    y0 <- 22
    ## Width and height of the legend
    w <- 10
    h <- w / nClasses * nIntervals
    ## Legend
    grid.raster(multiPal, interpolate = FALSE,
                      x = unit(x0, "native"),
                      y = unit(y0, "native"),
                width = unit(w, "native"))
    ## Axes of the legend
    ## x-axis (qualitative variable)
    grid.text(classes,
              x = unit(seq(x0 - w * (nClasses -1)/(2*nClasses),
                           x0 + w * (nClasses -1)/(2*nClasses),
                           length = nClasses),
                       "native"),
              y = unit(y0 + h/2, "native"),
              just = "bottom",
              rot = 10,
              gp = gpar(fontsize = 6))
    ## y-axis (quantitative variable)
    yLabs <- paste0("[",
                    paste(logPopAt[-nIntervals],
                          logPopAt[-1], sep = ","),
                    "]")
    grid.text(yLabs,
              x = unit(x0 + w/2, "native"),
              y = unit(seq(y0 - h * (nIntervals -1)/(2*nIntervals),
                           y0 + h * (nIntervals -1)/(2*nIntervals),
                           length = nIntervals),
                       "native"),
              just = "left",
              gp = gpar(fontsize = 6))

})

p + legend

##################################################################
## 3D visualization
##################################################################

plot3D(DEMr, maxpixels = 5e4)

library("rgl")

writeSTL("docs/images/rgl/DEM.stl")

##################################################################
## mapview
##################################################################

library("mapview")

mvSIS <- mapview(SISavr, legend = TRUE)

SIAR <- read.csv("data/Spatial/SIAR.csv")

spSIAR <- SpatialPointsDataFrame(coords = SIAR[, c("lon", "lat")], 
                                 data = SIAR,
                                 proj4str = CRS(projection(SISavr)))

sfSIAR <- st_as_sf(SIAR,
                   coords = c("lon", "lat"),
                   crs = crs(SISavt))

mvSIAR <- mapview(sfSIAR,
                  label = sfSIAR$Estacion)

mvSIS + mvSIAR

8.5. Vector fields

Download

##################################################################
## Initial configuration
##################################################################
## Clone or download the repository and set the working directory
## with setwd to the folder where the repository is located.

library("raster")
library("rasterVis")
library("RColorBrewer")

## Local vector direction 
wDir <- raster('data/Spatial/wDir')/180*pi
## Local vector magnitude
wSpeed <- raster('data/Spatial/wSpeed')
## Vector field encoded in a RasterStack with two layers, magnitude
## and direction
windField <- stack(wSpeed, wDir)
names(windField) <- c('magnitude', 'direction')

##################################################################
## Arrow plot
##################################################################

vectorTheme <- BTCTheme(regions = list(alpha = 0.7))

vectorplot(windField,
           isField = TRUE, ##RasterStack is a vector field
           aspX = 5, aspY = 5, ##Multipliers to adjust the relation
                               ##between slope/aspect and
                               ##horizontal/vertical displacements in
                               ##the figure.
           scaleSlope = FALSE, ## Slope values are *not* scaled
           par.settings = vectorTheme, 
           colorkey = FALSE,
           scales = list(draw = FALSE))

##################################################################
## Streamlines
##################################################################

myTheme <- streamTheme(
    region = rev(brewer.pal(n = 4, "Greys")),
    symbol = rev(brewer.pal(n = 9, "Blues")))

streamplot(windField, isField = TRUE,
           par.settings = myTheme,
           droplet = list(pc = 12), ## Amount of droplets, percentage of cells
           streamlet = list(L = 5, ## Length of the streamlet
                            h = 5), ## Calculation step
           scales = list(draw = FALSE),
           panel = panel.levelplot.raster)

8.6. Physical maps

Download

##################################################################
## Initial configuration
##################################################################
## Clone or download the repository and set the working directory
## with setwd to the folder where the repository is located.
library("lattice")
library("ggplot2")
## latticeExtra must be loaded after ggplot2 to prevent masking of its
## `layer` function.
library("latticeExtra")

library("RColorBrewer")

source("configLattice.R")

##################################################################
## Physical maps
##################################################################

library("terra")

library("sf")
library("sp")

library("rasterVis")
library("tidyterra")

library("colorspace")

library("ggrepel")

library("rnaturalearth")
library("rnaturalearthhires")
library("geodata")

##################################################################
## Retrieving data from DIVA-GIS, GADM and Natural Earth Data
##################################################################

     brazilAdm <- ne_states(country = "brazil")

     brazilDEM <- elevation_30s("BRA", mask = FALSE,
                                path = tempdir())

     worldRiv <- ne_download(type = "rivers_lake_centerlines",
                             category = "physical", 
                             scale = 10)

     ## only those features labeled as "River" are needed
     worldRiv <- worldRiv[worldRiv$featurecla == "River",]

     ## Ensure the CRS of brazilDEM matches the CRS of worldRiv
     crs(brazilDEM) <- crs(worldRiv)

     ## and intersect it with worldRiv to extract brazilian rivers
     ## from the world database
     brazilRiv <- st_crop(worldRiv, brazilDEM)

##################################################################
## Labels
##################################################################

## Locations of labels of each polygon
centroids <- brazilAdm[ , c("longitude", "latitude")]
## Extract the data
centroids <- st_drop_geometry(centroids)
## Location of the "Brazil" label (average of the centroids)
xyBrazil <- apply(centroids, 2, mean)

admNames <- strsplit(as.character(brazilAdm$name), ' ')
  
admNames <- sapply(admNames,
                   FUN = function(s){
                       sep = if (length(s)>2) '\n' else  ' '
                       paste(s, collapse = sep)
                   })

##################################################################
## Overlaying layers of information
##################################################################

terrainTheme <- rasterTheme(region = terrain_hcl(15),
                            panel.background = list(col = "lightskyblue1"))

altPlot <- levelplot(brazilDEM, par.settings = terrainTheme,
                     maxpixels = 1e6, panel = panel.levelplot.raster,
                     margin = FALSE, colorkey = FALSE)

print(altPlot)

ggplot() +
  geom_spatraster(data = brazilDEM,
                  show.legend = FALSE) +
  scale_fill_whitebox_c("high_relief",
                        na.value = "aquamarine") +
  theme_bw()

brazilRivsp <- as_Spatial(brazilRiv)

brazilAdmsp <- as_Spatial(brazilAdm)

## lattice version
altPlot + layer({
  ## Rivers
  sp.lines(brazilRivsp, col = 'darkblue', lwd = 0.2)
  ## Administrative boundaries
  sp.polygons(brazilAdmsp, col = 'black', lwd = 0.2)
  ## Centroids of administrative boundaries ...
  panel.points(centroids, col = 'black')
  ## ... with their labels
  panel.text(centroids, labels = admNames, pos = 3,
             cex = 0.7, fontfamily = 'Palatino', lineheight=.8)
  ## Country name
  panel.text(xyBrazil[1], xyBrazil[2], label = 'B R A Z I L',
             cex = 1.5, fontfamily = 'Palatino', fontface = 2)
  })

## ggplot2 version
ggplot() +
  geom_spatraster(data = brazilDEM,
                  show.legend = FALSE) +
  scale_fill_whitebox_c("high_relief",
                        na.value = "aquamarine") +
  geom_sf(data = brazilAdm,
          col = "black",
          linewidth = 0.15,
          fill = "transparent") +
  geom_sf(data = brazilRiv,
          col = "darkblue",
          linewidth = 0.1) +
  geom_point(data = centroids,
                     aes(x = longitude,
                         y = latitude)) +
  geom_text_repel(data = centroids,
                          aes(x = longitude,
                              y = latitude,
                              label = admNames),
                  family = "Palatino") +
  geom_text(aes(xyBrazil[1], xyBrazil[2],
                label = 'B R A Z I L'),
            size = 7,
            family = 'Palatino') +
  theme_bw()

8.7. Data

Download

##################################################################
## Initial configuration
##################################################################
## Clone or download the repository and set the working directory
## with setwd to the folder where the repository is located.

  ##################################################################
  ## Air Quality in Madrid
  ##################################################################

##################################################################
## Retrieve data
##################################################################

airStations <- read.csv2("data/Spatial/airStations.csv")
head(airStations)

airQuality <- read.csv2("data/Spatial/airQuality.csv")
head(airQuality)

##################################################################
## Combine data and spatial locations
##################################################################

library("sf")
  
## Spatial location of stations
airStations <- st_as_sf(airStations,
                        coords = c("long", "lat"),
                        crs = 4326)

NO2 <- subset(airQuality, codParam == 8)

NO2agg <- aggregate(dat ~ codEst, data = NO2,
                    FUN = function(x) {
                      c(mean = signif(mean(x), 3),
                        median = median(x),
                        sd = signif(sd(x), 3))
                    })
NO2agg <- do.call(cbind, NO2agg)
NO2agg <- as.data.frame(NO2agg)

## Link aggregated data with stations to obtain a sf object
## Code and codEst are the stations codes
idxNO2 <- match(airStations$Code, NO2agg$codEst)
NO2sf <- cbind(airStations[, c("Name", "alt")],
               NO2agg[idxNO2, ])

## Save the result
st_write(NO2sf, dsn = "data/Spatial/", layer = "NO2sf",
         driver = "ESRI Shapefile")

##################################################################
## Spanish General Elections
##################################################################

dat2016 <- read.csv("data/Spatial/GeneralSpanishElections2016.csv")

population <- dat2016$Población
census <- dat2016$Total.censo.electoral
validVotes <- dat2016$Votos.válidos
## Election results per political party and municipality
votesData <- dat2016[, -(1:13)]
## Abstention as an additional party
votesData$ABS <- census - validVotes
## UP is a coalition of several parties
UPcols <- grep("PODEMOS|ECP", names(votesData))
votesData$UP <- rowSums(votesData[, UPcols])
votesData[, UPcols] <- NULL
## Winner party at each municipality
whichMax <- apply(votesData,  1, function(x)names(votesData)[which.max(x)])
## Results of the winner party at each municipality
Max <- apply(votesData, 1, max)
## OTH for everything but PP, PSOE, UP, Cs, and ABS
whichMax[!(whichMax %in% c("PP", "PSOE", "UP", "C.s", "ABS"))] <- "OTH"
## Percentage of votes with the electoral census
pcMax <- Max/census * 100

## Province-Municipality code. sprintf formats a number with leading zeros.
PROV <- sprintf("%02d", dat2016$Código.de.Provincia)
MUN <- sprintf("%03d", dat2016$Código.de.Municipio)
PROVMUN <- paste0(PROV, MUN)
                          
votes2016 <- data.frame(PROV, MUN, PROVMUN,
                        population, census, validVotes,
                        whichMax, Max, pcMax)

write.csv(votes2016, "data/Spatial/votes2016.csv", row.names = FALSE)

##################################################################
## Administrative boundaries
##################################################################

library("sf")

old <- setwd(tempdir())

download.file("https://www.ine.es/pcaxis/mapas_completo_municipal.zip",
              "mapas_completo_municipal.zip")
unzip("mapas_completo_municipal.zip")

sfMun <- st_read("esp_muni_0109.shp", crs = 25830,
                 stringsAsFactors = TRUE)
sfMun <- subset(sfMun, !is.na(sfMun$PROVMUN))

setwd(old)

votes2016 <- read.csv("data/Spatial/votes2016.csv",
                      colClasses = c("factor", "factor", "factor",
                                     "numeric", "numeric", "numeric",
                                     "factor", "numeric", "numeric"))
## Match polygons and data with the PROVMUN column
idx <- match(sfMun$PROVMUN, votes2016$PROVMUN)
  
##Places without information
idxNA <- which(is.na(idx))

##Information to be added to the sf object
dat2add <- votes2016[idx, c("PROV", "population", "census", "validVotes",
                            "whichMax", "Max", "pcMax")]

## Spatial object with votes data
sfMapVotes <- cbind(sfMun, dat2add)

## Drop those places without information
sfMapVotes0 <- sfMapVotes[-idxNA, ]

## Save the result
st_write(sfMapVotes0, "data/Spatial/sfMapVotes0.shp")

## Extract Canarias islands from the sf object
canarias <-  substr(sfMapVotes0$PROVMUN, 1, 2) %in% c("35",  "38")
peninsula <- sfMapVotes0[!canarias,]
island <- sfMapVotes0[canarias,]

## Shift the island extent box to position them at the bottom right corner
dbbox <- st_bbox(peninsula) - st_bbox(island)
dxy <- dbbox[c("xmax", "ymin")]
island$geometry <- island$geometry + dxy

## Bind Peninsula (without islands) with shifted islands
st_crs(island) <- st_crs(peninsula)
sfMapVotes <- rbind(peninsula, island)

## Save the result
st_write(sfMapVotes, "data/Spatial/sfMapVotes.shp", append = FALSE)

##################################################################
##   GDP and Population
##################################################################

## Population of each province
popSpain <- read.csv("data/SpatioTime/PopSpain.csv")
popSpain2020 <- subset(popSpain, Year == 2020)
popSpain2020$PROV <- substring(popSpain2020$Province, 1, 2)

## GDP of each province
GDPSpain2020 <- read.csv("data/Spatial/GDPSpain2020.csv")
GDPSpain2020$PROV <- substring(GDPSpain2020$Province, 1, 2)

popGDPSpain2020 <- merge(popSpain2020, GDPSpain2020[, c("PROV", "GDP")])

library("sf")

sfProv <- st_read("data/Spatial/spain_provinces_2.shp", crs = 25830,
                 stringsAsFactors = TRUE)

## Merge data with the polygons
sfPopGDPSpain <- merge(sfProv, popGDPSpain2020,
                      by = "PROV")

st_write(sfPopGDPSpain, "data/Spatial/sfPopGDPSpain.shp")

  ##################################################################
  ## CM SAF
  ##################################################################

library("raster")
  
tmp <- tempdir()
unzip("data/Spatial/SISmm2008_CMSAF.zip", exdir = tmp)
filesCMSAF <- dir(tmp, pattern = "SISmm")
SISmm <- stack(paste(tmp, filesCMSAF, sep = "/"))
## CM-SAF data is average daily irradiance (W/m2). Multiply by 24
## hours to obtain daily irradiation (Wh/m2)
SISmm <- SISmm * 24

## Monthly irradiation: each month by the corresponding number of days
daysMonth <- c(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
SISm <- SISmm * daysMonth / 1000 ## kWh/m2
## Annual average
SISav <- sum(SISm)/sum(daysMonth)
writeRaster(SISav, file = "data/Spatial/SISav.nc")

library("raster")
## https://neo.gsfc.nasa.gov/view.php?datasetId=SEDAC_POP
pop <- raster("data/Spatial/875430rgb-167772161.0.FLOAT.TIFF")
## https://neo.gsfc.nasa.gov/view.php?datasetId=MCD12C1_T1
landClass <- raster("data/Spatial/241243rgb-167772161.0.TIFF")

HOME

Maintained by Oscar Perpiñán Lamigueiro.