4  Making Maps with R

Table of content for chapter 09

Chapter section list

  1. Introduction (Section 4.1)
  2. Static Maps are the most common type of visual output from geocomputation. This is therefore the longest and most detailed part of this chapter (Section 4.2).
    • Scales control how the values are represented on the map and in the legend, and they largely depend on the selected visual variable (Section 4.2.4). We can distinguish between interval (Section 4.2.4.2), continuous
      1. and categorical scales
      2. applying categorical, sequential or diverging palettes (Section 4.2.4.5).
    • Legends have a certain title, position, orientation to place it on the graphics. But you can also disable legend (Section 4.2.5).
    • Layouts refer to the combination of all map elements into a cohesive map (Section 4.2.6).
    • Faceted maps are composed of many maps arranged side-by-side, and sometimes stacked vertically. Facets enable the visualization of how spatial relationships change with respect to another variable, such as time (Section 4.2.7). They can also used with special parameters for animated maps (Section 4.3).
    • Inset maps are smaller maps rendered within or next to the main map (Section 4.2.8). It is a complex chapter which uses details of the {grid} package like grid::viewport(), grid::grid.newpage() and the sf::bbox() function to return the bounding of a simple feature (set). As I do not need inset maps at the moment for my own research I skipped this section.
  3. Animated maps show how spatial distributions of variables change (e.g., over time) sometimes better than tiny faceted maps (Section 4.3).
  4. Interactive maps is the next level to explore data. Interactivity can take many forms: Starting with interactive labels through pop-ups when you hover over the region or click with the mouse, continuing with the ability to pan around and zoom into any part of a geographic dataset overlaid on a ‘web map’ to show context to the more advanced level of the ability to tilt and rotate maps (Section 4.4).

4.1 Introduction

I am following here Chapter 9 Making Maps with R respectively chapter 8 of the printed book 2nd edition, explaining mainly the use of {tmap}. In addition to the example and code demonstrations I will also use my own WHR dataset.

To facilitate working with the WHR dataset I have joined the ladder score values in a new column “WHR” with the world dataset of {tmap} and stored as world_whr_2024.rds in “data/chapter09”

R Code 4.1 : Join WHR ladder scores with {tmap} World dataset

Code
my_create_folder("data/chapter09")

whr_2024 <-  base::readRDS("data/chapter09/whr_final_2024.rds") |> 
    dplyr::select(iso3, ladder_score) |> 
    dplyr::rename(WHR = ladder_score)

World <- tmap::World

world_whr_2024 <- dplyr::left_join(
    World,
    whr_2024,
    dplyr::join_by(iso_a3 == iso3)
)

my_save_data_file("chapter09", world_whr_2024, "world_whr_2024.rds")

dplyr::glimpse(world_whr_2024)
#> Rows: 177
#> Columns: 19
#> $ iso_a3       <chr> "AFG", "ALB", "DZA", "AGO", "ATA", "ARG", "ARM", "AUS", "…
#> $ name         <chr> "Afghanistan", "Albania", "Algeria", "Angola", "Antarctic…
#> $ sovereignt   <chr> "Afghanistan", "Albania", "Algeria", "Angola", "Antarctic…
#> $ continent    <fct> Asia, Europe, Africa, Africa, Antarctica, South America, …
#> $ area         [km^2] 642393.199 [km^2], 28298.421 [km^2], 2312302.795 [km^2],…
#> $ pop_est      <dbl> 38041754, 2854191, 43053054, 31825295, 4490, 44938712, 29…
#> $ pop_est_dens <dbl> 5.921880e+01, 1.008604e+02, 1.861912e+01, 2.547161e+01, 3…
#> $ economy      <fct> 7. Least developed region, 6. Developing region, 6. Devel…
#> $ income_grp   <fct> 5. Low income, 4. Lower middle income, 3. Upper middle in…
#> $ gdp_cap_est  <dbl> 507.1007, 5353.1806, 3973.9573, 2790.7047, 200000.0000, 9…
#> $ life_exp     <dbl> 61.982, 76.463, 76.377, NA, NA, 75.390, 72.043, 84.526, 8…
#> $ well_being   <dbl> 2.436034, 5.255482, 5.217018, NA, NA, 5.908279, 5.300569,…
#> $ footprint    <dbl> 1.1396346, 3.6742617, 3.1109787, NA, NA, 6.3910028, 3.429…
#> $ HPI          <dbl> 16.21067, 46.33355, 47.44608, NA, NA, 43.79233, 43.00195,…
#> $ inequality   <dbl> NA, 29.4, 27.6, 51.3, NA, 40.7, 27.9, 34.3, 30.7, 26.6, N…
#> $ gender       <dbl> 0.665, 0.116, 0.460, 0.520, NA, 0.292, 0.198, 0.063, 0.04…
#> $ press        <dbl> 19.09, 54.10, 41.98, 52.44, NA, 63.13, 71.60, 73.42, 74.6…
#> $ WHR          <dbl> 1.364, 5.411, 5.571, NA, NA, 6.397, 5.494, 6.974, 6.810, …
#> $ geometry     <MULTIPOLYGON [°]> MULTIPOLYGON (((66.217 37.3..., MULTIPOLYGON…

4.2 Static maps

Static maps are the most common type of visual output from geocomputation. They are usually stored in standard formats including .png and .pdf for graphical raster and vector outputs, respectively. Initially, static maps were the only type of maps that R could produce. Things have advanced very much in the last decade, and many map-making techniques, functions, and packages have been developed since then. However, despite the innovation of interactive mapping, static plotting was still the emphasis of geographic data visualization in R.

The generic base::plot() function is often the fastest way to create static maps from vector and raster spatial objects. Sometimes, simplicity and speed are priorities, especially during the development phase of a project, and this is where base::plot() excels. The base R approach is also extensible, with base::plot() offering dozens of arguments. Another approach is the {grid} package which allows low-level control of static maps. But these notes focus on {tmap} and emphasizes the essential aesthetic and layout options.

4.2.1 {tmap} basics

Code Collection 4.1 : {tmap} basics

R Code 4.2 : New Zealand Layers

Code
# get nz data
nz <- spData::nz

# Add fill layer to nz shape
nz1 <- tmap::tm_shape(nz) +
  tmap::tm_fill() 
# Add border layer to nz shape
nz2 <- tmap::tm_shape(nz) +
  tmap::tm_borders() 
# Add fill and border layers to nz shape
nz3 <- tmap::tm_shape(nz) +
  tmap::tm_fill() +
  tmap::tm_borders()
  
tmap::tmap_arrange(nz1, nz2, nz3)
Figure 4.1: New Zealand’s shape plotted with fill (left), border (middle) and fill and border (right) layers added using tmap functions.

R Code 4.3 : World countries layers

Code
```{r}
#| label: fig-whr-world-with-layers
#| fig-asp: 0.5
#| fig-cap: "Shape of world countries plotted with fill and border layers added using tmap functions."

world_whr_2024 <- base::readRDS("data/chapter09/world_whr_2024.rds")

world_whr_2024 |> 
      tmap::tm_shape() +
      tmap::tm_fill() +
      tmap::tm_borders() +
      tmap::tm_crs("+proj=robin")
```
#> [tip] Consider a suitable map projection, e.g. by adding `+ tm_crs("auto")`.
#> This message is displayed once per session.
Figure 4.2: Shape of world countries plotted with fill and border layers added using tmap functions.

R Code 4.4 : Numbered R Code Title

Code
```{r}
#| label: fig-whr-ladder-scores
#| fig-asp: 0.4
#| fig-cap: "World Happiness Report Ladder scores"

# works better with Antarctica and earth_boundary = TRUE
tmap::tm_shape(world_whr_2024) +
tmap::tm_polygons("WHR") +
tmap::tm_crs("+proj=robin") +
# tmap::tm_title("World Happiness") +
tmap::tm_layout(
    earth_boundary = FALSE, 
    frame = FALSE
    )
```
Figure 4.3: World Happiness Report Ladder scores

4.2.2 Map objects

Map objects can be stored as class tmap into the R memory. You can add new shapes later with + tm_shape(new_obj)

(The book example are not useful for my applications as it add raster objects and uses geometry operations.)

4.2.3 Visual variables

The plots in the previous section demonstrate tmap’s default aesthetic settings. Gray shades are used for tmap::tm_fill() layers and a continuous black line is used to represent lines created with tmap::tm_borders(). Of course, these default values and other aesthetics can be overridden.

  • fill: fill color of a polygon
  • col: color of a polygon border, line, point, or raster
  • lwd: line width
  • lty: line type
  • size: size of a symbol
  • shape: shape of a symbol

Additionally, you may customize the fill and border color transparency using fill_alpha and col_alpha.

4.2.3.1 Fixed values

Code Collection 4.2 : Visualize fixed aesthetic arguments on different layer types

R Code 4.5 : NZ: Visualize aesthetic arguments with fixed values

Code
ma1 = tmap::tm_shape(nz) + tmap::tm_polygons(fill = "red")
ma2 = tmap::tm_shape(nz) + tmap::tm_polygons(fill = "red", fill_alpha = 0.3)
ma3 = tmap::tm_shape(nz) + tmap::tm_polygons(col = "blue")
ma4 = tmap::tm_shape(nz) + tmap::tm_polygons(lwd = 3)
ma5 = tmap::tm_shape(nz) + tmap::tm_polygons(lty = 2)
ma6 = tmap::tm_shape(nz) + tmap::tm_polygons(fill = "red", fill_alpha = 0.3,
                                 col = "blue", lwd = 3, lty = 2)
tmap::tmap_arrange(ma1, ma2, ma3, ma4, ma5, ma6)
Figure 4.4: Impact of changing commonly used fill and border aesthetics to fixed values.

R Code 4.6 : WHR: Visualize aesthetic arguments with fixed values

Code
wrld1 = tmap::tm_shape(world_whr_2024) + tmap::tm_polygons(fill = "red")
wrld2 = tmap::tm_shape(world_whr_2024) + tmap::tm_polygons(fill = "red", fill_alpha = 0.3)
wrld3 = tmap::tm_shape(world_whr_2024) + tmap::tm_polygons(col = "blue")
wrld4 = tmap::tm_shape(world_whr_2024) + tmap::tm_polygons(lwd = 3)
wrld5 = tmap::tm_shape(world_whr_2024) + tmap::tm_polygons(lty = 2)
wrld6 = tmap::tm_shape(world_whr_2024) + tmap::tm_polygons(fill = "red", fill_alpha = 0.3,
                                 col = "blue", lwd = 3, lty = 2)
tmap::tmap_arrange(wrld1, wrld2, wrld3, wrld4, wrld5, wrld6)
Figure 4.5: Impact of changing commonly used fill and border aesthetics to fixed values.

4.2.3.2 Variable values

Using variable values, e.g. values from a column of the dataset, is the essential technique to show colored country differences on a map. Other issues (choosing an appropriate color palette, adapting text and position of the legend etc.) are — seen from a general perspective — only details.

Code Collection 4.3 : Visualize variable aesthetic arguments on different layer types

R Code 4.7 : NZ: Visualize aesthetic arguments with NZ variables

Code
tmap::tm_shape(nz) + tmap::tm_fill(fill = "Land_area")
Figure 4.6: Fill regions of New Zealand with values of column ‘Land_area’

R Code 4.8 : WHR: Visualize aesthetic arguments with WHR variables

Code
well_being_HPI <- tmap::tm_shape(world_whr_2024) + tmap::tm_fill(fill = "well_being")
well_being_WHR <- tmap::tm_shape(world_whr_2024) + tmap::tm_fill(fill = "WHR")

tmap::tmap_arrange(well_being_HPI, well_being_WHR)
Figure 4.7: Fill countries of the world with well-being values of HPI and WHR

If you compare the well-being index from the Happiness Planet Index (HPI) and from the World Happiness Reports (WHR) you can see that the results are very similar. But there are some differences: For instance in countries with missing values, or Mexico, Colombia and Argentina have higher values in the WHR index. To understand these differences I would have to dive deeper in the different construction of the indexes.

Each visual variable has three related additional arguments, with suffixes of .scale, .legend, and .free. For example, the tmap::tm_fill() function has arguments such as fill, fill.scale, fill.legend, and fill.free.

  • The .scale argument determines how the provided values are represented on the map and in the legend (Section 4.2.4),
  • The .legend argument is used to customize the legend settings, such as its title, orientation, or position (Section 4.2.5)
  • The .free argument is relevant only for maps with many facets to determine if each facet has the same or different scale and legend.

4.2.4 Scales

4.2.4.1 Setting breaks

Scales control how the values are represented on the map and in the legend, and they largely depend on the selected visual variable. For example, when our visual variable is fill, then fill.scale controls how the colors of spatial objects are related to the provided values; and when our visual variable is size, then size.scale controls how the sizes represent the provided values.

By default, the used scale is tmap::tm_scale(), which selects the visual settings automatically given by the input data type (factor, numeric, and integer). There are also specific functions that depend on the data type and visual variable, such as

The tm_scale function applies different scale functions depending on the data type and visual variable, such as tm_scale_ordinal, tm_scale_categorical, tm_scale_intervals, tm_scale_continuous, etc

Let’s see how the scales work by customizing polygons’ fill colors. Color settings are an important part of map design – they can have a major impact on how spatial variability is portrayed.

Code Collection 4.4 : Different color settings and scales for income variable

R Code 4.9 : Different break settings and color palettes for NZ medium income

Code
nz_scale1 <- tmap::tm_shape(nz) + tmap::tm_polygons(fill = "Median_income")
nz_scale2 <- tmap::tm_shape(nz) + tmap::tm_polygons(fill = "Median_income",
                        fill.scale = tmap::tm_scale(breaks = c(0, 30000, 40000, 50000)))
nz_scale3 <- tmap::tm_shape(nz) + tmap::tm_polygons(fill = "Median_income",
                           fill.scale = tmap::tm_scale(n = 10))
nz_scale4 <- tmap::tm_shape(nz) + tmap::tm_polygons(fill = "Median_income",
                           fill.scale = tmap::tm_scale(values = "brewer.bu_gn"))

tmap::tmap_arrange(
  nz_scale1,
  nz_scale2,
  nz_scale3,
  nz_scale4
)
Figure 4.8: Break settings. The results show (from top left to bottom right): default settings (= ‘pretty’ breaks), manual breaks, n breaks, and the impact of changing the palette.

This figure shows four ways of splitting the input data values into a set of intervals.

  • Top left: The default setting uses ‘pretty’ breaks, e.g., rounds breaks into whole numbers where possible and spaces them evenly.
  • Top right: breaks allows you to manually set the breaks
  • Bottom right: n sets the number of bins into which numeric variables are categorized
  • Bottom left: values defines the color scheme, for example, brewer.bu_gn
{tmap} version 4 has new names for color palettes

The value for the example color scheme is in the book BuGn. But this value results into a warning:

[cols4all] color palettes: use palettes from the R package {cols4all}. Run cols4all::c4a_gui() to explore them. The old palette name “BuGn” is named “brewer.bu_gn”.

Multiple palettes called “bu_gn” found: “brewer.bu_gn”, “matplotlib.bu_gn”. The first one, “brewer.bu_gn”, is returned.

{cols4all} is a new R package for selecting color palettes, with a special orientation to include palettes for people with color vision deficiency.

R Code 4.10 : GDP per capita with standard ‘pretty’ breaks

Code
world_no_ata <- world_whr_2024 |> 
  dplyr::filter(name != "Antarctica")

tmap::tm_shape(world_no_ata) + 
  tmap::tm_polygons(
    fill = "gdp_cap_est",
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "bottom")
      )
  )
Figure 4.9: GDP per capita with standard (‘pretty’) breaks

Because of it extraordinary GDP per capita value (200,000 US $) I have removed the uninhabited Antarctica from the map. It has a population of only 4490 people, supposedly researchers working in this huge area.

R Code 4.11 : GDP per capita with breaks defined

Code
```{r}
#| label: fig-settings-manual-breaks
#| fig-asp: 0.5
#| fig-cap: "GDP per capita with breaks defined" 

tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
      fill = "gdp_cap_est",
      fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "center")
      ),              
      fill.scale = tmap::tm_scale(
        breaks = c(0, 5000, 10000, 15000, 20000, 200000)
        )
      )
```
Figure 4.10: GDP per capita with breaks defined

To differentiate better the lower end of the distribution I have chosen small interval breaks until 20,000 US$ and put together all those countries that GDP per capita is higher than 20,000 US$.

I contrast to the previous example I have additionally I have set the aspect ratio of the figure. (See the code chunk option fig-asp: 0.5.) This removes the white space above and below the figure.

R Code 4.12 : GDP per capita with number of breaks specified (n = 10)

Code
tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
     fill = "gdp_cap_est",
     fill.scale = tmap::tm_scale(n = 10),
     fill.legend = tmap::tm_legend(
        title = "Estimated GDP per capita", 
        orientation = "landscape")
     )
#> [plot mode] fit legend/component: Some legend items or map compoments do not
#> fit well, and are therefore rescaled.
#> ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.
Figure 4.11: GDP per capita with a specified number of breaks (n = 10)

If the data would have missing values I had to choose another color palette or tho change the default color for NAs. Otherwise the standard gray color for missing values (NAs) would be difficult to distinguish from the color of the lowest break (0-20,000).

R Code 4.13 : Choosing another color palette

Code
world_no_ata <- world_whr_2024 |> 
  dplyr::filter(name != "Antarctica")

tmap::tm_shape(world_no_ata) + 
  tmap::tm_polygons(fill = "gdp_cap_est",
     fill.scale = tmap::tm_scale(
       values = "matplotlib.rainbow"
       ),
     fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "bottom")
      )
 )
Figure 4.12: Different color palette

In this example I haven chosen a color palette with different colors instead of just the same color with different saturation. The matplotlib.rainbow palette is very helpful to visually distinguish differences in the countries. It is much better suited than the standard “blues” or other gradient scale versions.

In this example you can’t see the only red country Luxembourg with over 100,000 $ GDP per capita, because it is too small. But you will see a tiny orange spot in Europe. This is Switzerland the only country slightly over 80,000 $ GDP per capita. You will get a sightly better view if you click on this last figure to get a larger image.

Choose the desired color palette with the interactive {shiny} website calling cols4all::ca4_gui() from the console.
Arguments work for other types of visual variables too

All of the above arguments (breaks, n, and values) also work for other types of visual variables. For example, values expects a vector of colors or a palette name for fill.scale or col.scale, a vector of sizes for size.scale, or a vector of symbols for shape.scale.

4.2.4.2 Interval scales

The tmap::tm_scale_intervals() function splits the input data values into a set of intervals. In addition to setting breaks manually, {tmap} allows users to specify algorithms to create breaks with the style argument automatically. The default is tmap::tm_scale_intervals(style = "pretty"), which rounds breaks into whole numbers where possible and spaces them evenly.

  • style = “pretty”: default value, rounds breaks into whole numbers where possible and spaces them evenly.
  • style = “equal”: divides input values into bins of equal range and is appropriate for variables with a uniform distribution (not recommended for variables with a skewed distribution as the resulting map may end up having little color diversity).
  • style = “quantile”: ensures the same number of observations fall into each category (with the potential downside that bin ranges can vary widely).
  • style = “jenks”: identifies groups of similar values in the data and maximizes the differences between categories.
  • style = “log10_pretty”: a common logarithmic (the logarithm to base 10) version of the regular pretty style used for variables with a right-skewed distribution.
Origin of class interval computation

Although style is an argument of {tmap} functions, in fact it originates as an argument in classInt::classIntervals(). See the help pages for {classInt) especially the article on head/tail breaks (Pareto 80/20 rule).

Code Collection 4.5 : Setting intervals with algorithms via “styles”

R Code 4.14 : Different interval scale methods using the style argument

Code
tmap::tmap_options(
  legend.position = 
    tmap::tm_pos_in(pos.h = "left", pos.v = "top")
)


nz_int1 <- tmap::tm_shape(nz) +
  tmap::tm_polygons(fill = "Median_income",
          fill.scale = tmap::tm_scale_intervals(style = "pretty")) +
  tmap::tm_credits('style = "pretty"', size = 1)

nz_int2 <- tmap::tm_shape(nz) +
  tmap::tm_polygons(fill = "Median_income",
          fill.scale = tmap::tm_scale_intervals(style = "equal")) +
  tmap::tm_credits('style = "equal"', size = 1)

nz_int3 <- tmap::tm_shape(nz) +
  tmap::tm_polygons(fill = "Median_income",
          fill.scale = tmap::tm_scale_intervals(style = "quantile")) +
  tmap::tm_credits('style = "quantile"', size = 1)

nz_int4 <- tmap::tm_shape(nz) +
  tmap::tm_polygons(fill = "Median_income",
          fill.scale = tmap::tm_scale_intervals(style = "jenks")) +
  tmap::tm_credits('style = "jenks"', size = 1)

nz_int5 <- tmap::tm_shape(nz) +
  tmap::tm_polygons(
    fill = "Population",
    fill.scale = (
      tmap::tm_scale_intervals(
        style = "log10_pretty",
        values = "brewer.bu_pu"
        )
    ) 
  ) +
  tmap::tm_credits('style = \n"log10_pretty"', size = 1)


tmap::tmap_arrange(nz_int1, nz_int2, nz_int3, nz_int4, nz_int5)
Figure 4.13: Different interval scale methods using the style argument

Here I have added the interval style for each example as annotation. I couldn’t find an annotation function, so I had used tmap::tm_credits().

Note also the different color palette in the last example style = "log10_pretty". I have called cols4all::c4a_gui() from the console, set the number of colors to 3 and looked for a palette with blue and this reddish color at the high end (brewer.bu_pu).

R Code 4.15 : Set interval scale using style = "pretty"

Code
tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
    fill = "WHR",
    fill.scale = tmap::tm_scale_intervals(
        style = "pretty",
        values = "matplotlib.rainbow"
        ),
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "center")
      )
  )
Figure 4.14: Set interval scale for WHR ladder scores using the standard style = 'pretty'.

These are the standard intervals. The code line fill.scale = tmap::tm_scale_intervals(style = "pretty") wouldn’t be necessary. It results in nice intervals from 1 to 8.

Choosing the “matplotlib.rainbow” color palette helps to distinguish the different categories. So you can see that the only two countries with a blueish color in Asia (Afghanistan) and in Africa (Sierra Leone). It is easy to see that Afghanistan is the most unhappiness country falling the worst category between 1 and 2 (value = 1.368), whereas Sierra Leone belongs to the somewhat better next category. Sierra Leone actually has a value of almost 3 (value = 2.998). This is the disadvantage of interval categories: Each country belongs to one category, but you can’t see where they are positioned inside their categories.

R Code 4.16 : Set interval scale using style = "equal"

Code
tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
    fill = "WHR",
    fill.scale = tmap::tm_scale_intervals(
        style = "equal",
        values = "matplotlib.rainbow"
        ),
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "center")
      )
  )
Figure 4.15: Set interval scale for WHR ladder scores using style = 'equal'.

Here we get categories with ugly boundaries. It does not have an advantage to the better to interpret “pretty” solution.

R Code 4.17 : Set interval scale using style = "equal" with 12 breaks

Code
tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
    fill = "WHR",
    fill.scale = tmap::tm_scale_intervals(
        style = "equal",
        n = 12,
        values = "cols4all.friendly13"
        ),
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "center")
      )
  )
Figure 4.16: Set interval scale for WHR ladder scores using the style = 'equal' with 12 breaks.

Again we have ugly boundaries for each category. But this time there is an advantage: The 12 categories give a more detailed picture, especially in the higher end of the cantril ladder scores.

But the set of interval is still not ideal: For instance the second worst category has no element in it and at the high end we have not enough differentiation.

The “cols4all.friendly13” scale has 13 different colors, but for me there are two colors a that hare difficult to distinguish. So I decided to go with just 12 colors.

R Code 4.18 : Set interval scale using style = "quantile"

Code
tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
    fill = "WHR",
    fill.scale = tmap::tm_scale_intervals(
        style = "quantile",
        values = "cols4all.friendly13"
        ),
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "center")
      )
  )
Figure 4.17: Set interval scale for WHR ladder scores using style = 'quantile'.

This set of intervals gives a good rough picture of the WHR results: We have just 5 categories for 140 countries with ladder scores. In each category are placed 28 countries. So can we for instance decide the relative position of each country:

Label Color
worst red colored countries
bad ochre / dark yellow colored countries
middle blue colored countries
good dark green colored countries
excellent violet / purple colored countries

Sure this is a very coarse classification but it gives an idea about the distribution of the happiness index in the world.

: Set interval scale using style = "quantile" with 12 breaks

Code
tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
    fill = "WHR",
    fill.scale = tmap::tm_scale_intervals(
        style = "quantile",
        values = "cols4all.friendly13",
        n = 12
        ),
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "center")
      )
  )
Figure 4.18: Set interval scale for WHR ladder scores using style = 'quantile'.

Because of the many colors the interpretation is not easy. But you get a good idea in which value regions are a concentration of countries. The two broad categories at both ends are necessary to get the same amount of countries as in the much thinner inner categories.

R Code 4.19 : Set interval scale using style = "log10_pretty" for country populations

Code
tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
    fill = "pop_est",
    fill.scale = tmap::tm_scale_intervals(
        style = "log10_pretty",
        values = "cols4all.friendly13"
        ),
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_in("left", "center")
      )
  )
Figure 4.19: Set interval scale for WHR ladder scores using style = 'log10_pretty' for country populations

This is not a sensible map. I think style = 'log10_pretty' is better suited for instance to show an individual income distribution where 1% of the people owns the overwhelming part of the wealth. Or other content with a similar very skewed distribution.

4.2.4.3 Continuous scales

The tmap::tm_scale_continuous() function is used for continuous data. In case of variables with a skewed distribution, you can also use its variants – tmap::tm_scale_continuous_log() and tm_scale_continuous_log1p().

Generally there are two main groups of continuous color palettes:

  • Sequential palettes. These color palettes follow a gradient, for example from light to dark colors (light colors often tend to represent lower values), and are appropriate for continuous (numeric) variables. Sequential continuous palettes can be single (“brewer.blues” for example goes from light to dark blue, see Figure 4.20 left) or multi-color/hue (“yl_gn_bu” for example is a gradient from light yellow to blue via green, see Figure 4.20 middle). The difference in the legend is that there are no lower and upper limits of bins as can be seen in Figure 4.20 right. The right example uses the same color palette as the middle graph but this time segmented by bins.

  • Diverging palettes, typically range between three distinct colors (purple-white-green in Figure 4.20) and are usually created by joining two single-color sequential palettes with the darker colors at each end. Their main purpose is to visualize the difference from an important reference point, e.g., a certain temperature, the median household income or the mean probability for a drought event. By default the midpoint values is set to 0 if negative and positive values are present. But the reference point’s value can be adjusted in {tmap} using the midpoint argument.

Code Collection 4.6 : Using a continuous scale

R Code 4.20 : Continuous scale of median income in New Zealand

Code
nz_1 <- tmap::tm_shape(nz) +
  tmap::tm_polygons(
    fill = "Median_income",
    fill.scale = tmap::tm_scale_continuous(
        values = "brewer.blues"
      )
  ) 

nz_2 <- tmap::tm_shape(nz) + 
  tmap::tm_polygons(
    fill = "Median_income", 
    fill.scale = tmap::tm_scale_continuous(
      values = "brewer.yl_gn_bu"
      )
    ) 

nz_3 <- tmap::tm_shape(nz) + 
  tmap::tm_polygons(
    fill = "Median_income", 
    fill.scale = tmap::tm_scale(
      values = "brewer.yl_gn_bu"
      )
    ) 

nz_4 <- tmap::tm_shape(nz) +
  tmap::tm_polygons(
    fill = "Median_income", 
    fill.scale = tmap::tm_scale_continuous(
      midpoint = 28000,
      values = "pu_gn_div"
      ) 
    ) +
  tmap::tm_credits("midpoint = 28,000", size = 0.7)

nz_5 <- tmap::tm_shape(nz) + 
  tmap::tm_polygons(
    fill = "Sex_ratio", 
    fill.scale = tmap::tm_scale_continuous(
      midpoint = 1.0000000,
      values = "pu_gn_div")
    ) +
  tmap::tm_credits("midpoint = 1.000000", size = 0.7)


tmap::tmap_arrange(nz_1, nz_2, nz_3, nz_4, nz_5)
Figure 4.20: Using a continuous map (left) instead of bins (middle, right) for displaying median income in New Zealand

The standard scale tmap::tm_scale() sets breaks automatically to create bins with a lower and upper limit (middle and right). In contrast tmap::tm_scale_continuous() (left) provides a continuum scale without bins and borders.

R Code 4.21 : Cantril ladder values for countries mapped onto a continuous scale

Code
tmap::tm_shape(world_no_ata) +
  tmap::tm_polygons(
    fill = "WHR",
    fill.scale = 
      tmap::tm_scale_continuous(
        values = "brewer.blues"
      ),
    fill.legend = tmap::tm_legend(
      orientation = "landscape",
      position = tmap::tm_pos_out("center", "bottom"),
      width = 50
    )
  )
Figure 4.21: Using a continuous scale instead of setting breaks for bins to display the Happiness ladder scores in the countries of the world

Without width = 50 the scale would be auto scaled. The width is calculated from the font size. With standard values width = 50 is the maximum width without where no auto scaling is necessary.

4.2.4.4 Categorical scales

Categorical palettes tmap::tm_scale_categorical() consist of easily distinguishable colors and are most appropriate for categorical data without any particular order such as state names or land cover classes.

Ordinal scales tmap::tm_scale_ordinal() are also categorical scales but assume ordered categories used for instance for land use or habitat suitability. Ordered classes can represent levels of suitability from least to most suitable.

For categorical or ordered scales are different default color palettes used (see the tmap::tmap_options("values.var")).

Colors should be intuitive: rivers should be blue, for example, and pastures green. Avoid too many categories: maps with large legends and many colors can be difficult to interpret.

: Title for code collection

R Code 4.22 : Using categorical scale for NZ parts

Code
tmap::tm_shape(nz) +
  tmap::tm_polygons(
    fill = "Island",
    fill.scale = tmap::tm_scale_categorical(
        values = "brewer.accent"
      )
  ) 

tmap::tm_shape(nz) +
  tmap::tm_polygons(
    fill = "Name",
    col = "MAP_COLORS",
    fill.scale = tmap::tm_scale_categorical(
        values = "poly.palette36"
      )
  ) + 
  tmap::tm_place_legends_right()
Figure 4.22: Using categorical scales to distinguish north/south parts of New Zealand
Figure 4.23: Using categorical scales to distinguish regions of New Zealand

The regions in the lower graphics are difficult to distinguish because there are too many colors. I have used col = "MAP_COLORS" to create unique fill colors for adjacent regions. (A slightly improvement of the lower graphics would maybe if I ordered the items of the legend manually according to the position of the colored part.)

R Code 4.23 : Categorization of world economies

Code
tmap::tm_shape(world_no_ata) +
  tmap::tm_polygons(
    fill = "economy",
    fill.scale = tmap::tm_scale_categorical(
        values = "brewer.accent"
      ),
    fill.legend = tmap::tm_legend(
        position = tmap::tm_pos_out("center", "bottom")
      ) 
  ) 
Figure 4.24: Categorization of world economies

R Code 4.24 : Income group categorization of world’s countries

Code
tmap::tm_shape(world_no_ata) +
  tmap::tm_polygons(
    fill = "income_grp",
    fill.scale = tmap::tm_scale_categorical(
        values = "brewer.accent"
      ),
    fill.legend = tmap::tm_legend(
        orientation = "landscape",
        position = tmap::tm_pos_out("center", "bottom"),
        width = 50
      )
  ) 
#> [plot mode] fit legend/component: Some legend items or map compoments do not
#> fit well, and are therefore rescaled.
#> ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.
Figure 4.25: Income group categorization of world’s countries”

4.2.4.5 Scale summary

Examples of categorical, sequential and diverging palettes.
Figure 4.26: Examples of categorical, sequential and diverging palettes.

There are two important principles for consideration when working with colors: perceptibility and accessibility. Firstly, colors on maps should match our perception. This means that certain colors are viewed through our experience and also cultural lenses. For example, green colors usually represent vegetation or lowlands, and blue is connected with water or coolness. Color palettes should also be easy to understand to effectively convey information. It should be clear which values are lower and which are higher, and colors should change gradually. Secondly, changes in colors should be accessible to the largest number of people. Therefore, it is important to use colorblind friendly palettes as often as possible.

4.2.5 Legends

After we decided on our visual variable and its properties, we should move our attention toward the related map legend style. Using the tmap::tm_legend() function, we may change its title, position, orientation, or even disable it. The most important argument in this function is title, which sets the title of the associated legend. In general, a map legend title should provide two pieces of information: what the legend represents and what the units are of the presented variable. The following code chunk demonstrates this functionality by providing a more attractive name than the variable name “Land_area” (note the use of base::expression() to create superscript text).

Code Collection 4.7 : Customizing legends

R Code 4.25 : Changing the legend title

Code
legend_title = base::expression("Area (km"^2*")")

tmap::tm_shape(nz) +
  tmap::tm_polygons(
    fill = "Land_area", 
    fill.legend = tmap::tm_legend(
      title = legend_title,
      orientation = "portrait",
      position = tmap::tm_pos_in("left", "top")
      )
    )

R Code 4.26 : Customizing of the legend orientation and position

Code
tmap::tm_shape(nz) +
  tmap::tm_polygons(fill = "Land_area",
      fill.legend = tmap::tm_legend(
      title = legend_title,
      orientation = "portrait",
      position = tmap::tm_pos_in(
        pos.h = "left", 
        pos.v = "top"
        )
      )
  )

The default legend orientation in {tmap} is “portrait”, however, an alternative legend orientation, “landscape”, is also possible. Other than that, we can also customize the location of the legend using the position argument.

The legend position (and also the position of several other map elements in tmap) can be customized using one of a few functions. (I have the relevant functions for oeintation and position already used in Section 4.2.4.)

The two most important functions for the positions are:

  • tmap::tm_pos_out(): the default, adds the legend outside of the map frame area. We can customize its location with two values that represent the horizontal position (“left”, “center”, or “right”), and the vertical position (“bottom”, “center”, or “top”)
  • tmap::tm_pos_in(): puts the legend inside of the map frame area. We may decide on its position using two arguments, where the first one can be “left”, “center”, or “right”, and the second one can be “bottom”, “center”, or “top”.

Alternatively, we may just provide a vector of two values (or two numbers between 0 and 1) here – and in such case, the legend will be put inside the map frame.


4.2.6 Layouts

The map layout refers to the combination of all map elements into a cohesive map. Map elements include among others

The color settings covered in the previous section relate to the palette and breakpoints used to affect how the map looks. Both may result in subtle changes that can have an equally large impact on the impression left by your maps.

4.2.6.1 Graticules and grid

Graticules versus Grid
  • tmap::tm_grid() is used to draw grid lines based on the projection of the input data, e.g. its role is to represent the input data’s coordinates.
  • tm_graticules() is used to draw lines of latitude (parallels) and longitude (meridians) in degrees, providing a geographic reference regardless of the data’s projection.

See for details: Grids and graticules in the tmap package

R Code 4.27 : Difference between graticules and grid

Code
nz01 <- tmap::tm_shape(nz) + 
  tmap::tm_polygons() +
  tmap::tm_grid()

nz02 <- tmap::tm_shape(nz) + 
  tmap::tm_polygons() +
  tmap::tm_graticules()

tmap::tmap_arrange(nz01, nz02)
Figure 4.27: Using tmap::tm_grid() (left) and using `tmap::tm_graticules() with the degree sign (right)

4.2.6.2 Components

Components are non-spatial parts of the plots:

See also: tmap basics: components.

R Code 4.28 : Showing layout components

Code
tmap::tm_shape(nz) + 
  tmap::tm_polygons() +
  # tmap::tm_graticules() +
  tmap::tm_compass(type = "8star", position = c("left", "top")) +
  tmap::tm_scalebar(breaks = c(0, 100, 200), text.size = 1, position = c("left", "top")) +
  tmap::tm_title("New Zealand") +
  tmap::tm_logo(base::system.file("help", "figures", "logo.png", package = "tmap")) +
  tmap::tm_credits("Source: https://www.stats.govt.nz/")
Figure 4.28: New Zealand map with additonal layout components: title, compass, scale bar, credits and logo

4.2.6.3 Layout settings

{tmap} also allows a wide variety of layout settings to be changed. To delve into the meaning of all this parameters call the appropriate help file with ?tmap::tm_layout. The following huge list gives you a first impression of the many possible settings.

Code
base::args(tmap::tm_layout)
#> function (scale, asp, bg, bg.color, outer.bg, outer.bg.color, 
#>     frame, frame.color, frame.alpha, frame.lwd, frame.r, frame.double_line, 
#>     outer.margins, inner.margins, inner.margins.extra, meta.margins, 
#>     meta.auto_margins, between_margin, panel.margin, grid.mark.height, 
#>     xylab.height, coords.height, xlab.show, xlab.text, xlab.size, 
#>     xlab.color, xlab.rotation, xlab.space, xlab.fontface, xlab.fontfamily, 
#>     xlab.alpha, xlab.side, ylab.show, ylab.text, ylab.size, ylab.color, 
#>     ylab.rotation, ylab.space, ylab.fontface, ylab.fontfamily, 
#>     ylab.alpha, ylab.side, panel.type, panel.wrap.pos, panel.xtab.pos, 
#>     unit, color.sepia_intensity, color.saturation, color_vision_deficiency_sim, 
#>     text.fontface, text.fontfamily, text.alpha, component.position, 
#>     component.offset, component.stack_margin, component.autoscale, 
#>     component.resize_as_group, component.frame_combine, component.stack, 
#>     legend.stack, chart.stack, component.equalize, component.frame, 
#>     component.frame.color, component.frame.alpha, component.frame.lwd, 
#>     component.frame.r, component.bg, component.bg.color, component.bg.alpha, 
#>     legend.show, legend.design, legend.orientation, legend.position, 
#>     legend.width, legend.height, legend.reverse, legend.na.show, 
#>     legend.title.color, legend.title.size, legend.title.fontface, 
#>     legend.title.fontfamily, legend.title.alpha, legend.xlab.color, 
#>     legend.xlab.size, legend.xlab.fontface, legend.xlab.fontfamily, 
#>     legend.xlab.alpha, legend.ylab.color, legend.ylab.size, legend.ylab.fontface, 
#>     legend.ylab.fontfamily, legend.ylab.alpha, legend.text.color, 
#>     legend.text.size, legend.text.fontface, legend.text.fontfamily, 
#>     legend.text.alpha, legend.frame, legend.frame.color, legend.frame.alpha, 
#>     legend.frame.lwd, legend.frame.r, legend.bg, legend.bg.color, 
#>     legend.bg.alpha, legend.only, legend.absolute_fontsize, legend.settings.standard.portrait, 
#>     legend.settings.standard.landscape, add_legend.position, 
#>     chart.show, chart.plot.axis.x, chart.plot.axis.y, chart.position, 
#>     chart.width, chart.height, chart.reverse, chart.na.show, 
#>     chart.title.color, chart.title.size, chart.title.fontface, 
#>     chart.title.fontfamily, chart.title.alpha, chart.xlab.color, 
#>     chart.xlab.size, chart.xlab.fontface, chart.xlab.fontfamily, 
#>     chart.xlab.alpha, chart.ylab.color, chart.ylab.size, chart.ylab.fontface, 
#>     chart.ylab.fontfamily, chart.ylab.alpha, chart.text.color, 
#>     chart.text.size, chart.text.fontface, chart.text.fontfamily, 
#>     chart.text.alpha, chart.frame, chart.frame.color, chart.frame.alpha, 
#>     chart.frame.lwd, chart.frame.r, chart.bg, chart.bg.color, 
#>     chart.bg.alpha, chart.object.color, title.size, title.color, 
#>     title.fontface, title.fontfamily, title.alpha, title.padding, 
#>     title.frame, title.frame.color, title.frame.alpha, title.frame.lwd, 
#>     title.frame.r, title.position, title.width, credits.size, 
#>     credits.color, credits.fontface, credits.fontfamily, credits.alpha, 
#>     credits.padding, credits.position, credits.width, credits.height, 
#>     compass.north, compass.type, compass.text.size, compass.size, 
#>     compass.show.labels, compass.cardinal.directions, compass.text.color, 
#>     compass.color.dark, compass.color.light, compass.lwd, compass.margins, 
#>     compass.position, inset.position, logo.height, logo.margins, 
#>     logo.between_margin, logo.position, inset_map.height, inset_map.width, 
#>     inset_map.margins, inset_map.between_margin, inset_map.position, 
#>     inset_map.frame, inset.height, inset.width, inset.margins, 
#>     inset.between_margin, inset.frame, inset.bg, inset.bg.color, 
#>     inset.bg.alpha, inset_grob.height, inset_grob.width, inset_gg.height, 
#>     inset_gg.width, scalebar.breaks, scalebar.width, scalebar.allow_clipping, 
#>     scalebar.text.size, scalebar.text.color, scalebar.text.fontface, 
#>     scalebar.text.fontfamily, scalebar.color.dark, scalebar.color.light, 
#>     scalebar.lwd, scalebar.size, scalebar.margins, scalebar.position, 
#>     grid.show, grid.labels.pos, grid.x, grid.y, grid.n.x, grid.n.y, 
#>     grid.crs, grid.col, grid.lwd, grid.alpha, grid.labels.show, 
#>     grid.labels.size, grid.labels.col, grid.labels.fontface, 
#>     grid.labels.fontfamily, grid.labels.rot, grid.labels.format, 
#>     grid.labels.cardinal, grid.labels.margin.x, grid.labels.margin.y, 
#>     grid.labels.space.x, grid.labels.space.y, grid.labels.inside_frame, 
#>     grid.ticks, grid.lines, grid.ndiscr, mouse_coordinates.position, 
#>     minimap.server, minimap.toggle, minimap.position, panel.show, 
#>     panel.labels, panel.label.size, panel.label.color, panel.label.fontface, 
#>     panel.label.fontfamily, panel.label.alpha, panel.label.bg, 
#>     panel.label.bg.color, panel.label.bg.alpha, panel.label.frame, 
#>     panel.label.frame.color, panel.label.frame.alpha, panel.label.frame.lwd, 
#>     panel.label.frame.r, panel.label.height, panel.label.rot, 
#>     qtm.scalebar, qtm.minimap, qtm.mouse_coordinates, earth_boundary, 
#>     earth_boundary.color, earth_boundary.lwd, earth_datum, space.color, 
#>     check_and_fix, basemap.show, basemap.server, basemap.alpha, 
#>     basemap.zoom, tiles.show, tiles.server, tiles.alpha, tiles.zoom, 
#>     attr.color, crs_extra, crs_global, crs_basemap, title = NULL, 
#>     ...) 
#> NULL

Some of the above list of arguments are used in the following graphics:

R Code 4.29 : Examples for different arguments for layout settings

Code
map_nz <- tmap::tm_shape(nz) + tmap::tm_polygons()

map_nz1 <- map_nz + tmap::tm_layout(scale = 4)
map_nz2 <- map_nz + tmap::tm_layout(bg.color = "lightblue")
map_nz3 <- map_nz + tmap::tm_layout(frame = FALSE)

tmap::tmap_arrange(map_nz1, map_nz2, map_nz3)
Figure 4.29: Examples for different arguments for layout settings. Changing the overall scale of the map to scale = 4 (left), setting the background color bg.color = 'lightblue' (center) and removing the border around the graph with frame = FALSE

A helpful function for designing a specific layout is tmap::tmap_design_mode(). When the so-called “design mode” is enabled, inner and outer margins, legend position, and aspect ratio are shown explicitly in plot mode. Also, information about aspect ratios is printed in the console. This function sets the global option tmap.design.mode. It can be used as toggle function without arguments.

R Code 4.30 : Turning design mode on/off to check and change specific layout options

Code
#> design.mode: ON
Code
map_nz
#> ----------------W (in)--H (in)--asp---
#> | device        7.000 5.000 1.400 |
#> | plot area     3.324 4.800 0.692 |
#> | facets area   3.324 4.800 0.692 |
#> | map area      3.324 4.800 0.692 |
#> -----------------------------------
Code
#> design.mode: OFF
Figure 4.30: Turning on/off the design mode to check and change specific layout options.

The desing mode is especially valuable if you want to change the aspect ratio to remove white space around the figure.

Resource 4.1 : Resources for changing layout settings in {tmap}

4.2.7 Faceted maps

Faceted maps are composed of many maps arranged side-by-side, and sometimes stacked vertically. Facets enable the visualization of how spatial relationships change with respect to another variable, such as time. The changing populations of settlements, for example, can be represented in a faceted map with each panel representing the population at a particular moment in time.

R Code 4.31 : Faceted maps

Code
urb_1950_2035 <- spData::urban_agglomerations
world <- tmap::World

urb_1970_2030 <- urb_1950_2035 |> 
   dplyr::filter(year %in% c(1970, 1990, 2010, 2030))

tmap::tm_shape(world) +
  tmap::tm_polygons(fill = "white") +
  tmap::tm_shape(urb_1970_2030) +
  tmap::tm_symbols(
    fill = "red", 
    col = "black",
    size = "population_millions",
    size.legend = tmap::tm_legend(
      title = "Urban populations in million")
    ) +
  tmap::tm_facets_wrap(by = "year", nrow = 2) +
  tmap::tm_layout(
    legend.orientation = "landscape",
    legend.position = tmap::tm_pos_out(
      cell.h = "center",
      cell.v = "bottom"
    )
  )
Figure 4.31: Faceted map showing the top 30 largest urban agglomerations from 1970 to 2030 based on population projections by the United Nations.

How to change the legend title?

I just learned that the legend title has to be changed with the argument that calls the database column. In this case it is size = "population_millions", therefore I had to use size.legend = tmap::tm_legend() (and not size.legend = tmap::tm_legend() as I tried all the time!)

Revising my notes I learned that the first paragraph in (Section 4.2.4) stated exactly this procedure: But fill.scale, size.scale does not only control how the selected visual variable is presented but also the legend title.

The preceding code chunk demonstrates key features of faceted maps created using the tmap::tm_facets_wrap() function:

  • Shapes that do not have a facet variable are repeated (countries in world in this case)
  • The by argument which varies depending on a variable (“year” in this case)
  • The nrow/ncol setting specifying the number of rows and columns that facets should be arranged into

Alternatively, it is possible to use the tmap::tm_facets_grid() function that allows to have facets based on up to three different variables: one for rows, one for columns, and possibly one for pages.

In addition to their utility for showing changing spatial relationships, faceted maps are also useful as the foundation for animated maps (see Section 4.3).

4.2.8 Inset maps (empty)

An inset map is a smaller map rendered within or next to the main map. It could serve many different purposes, including providing a context or bringing some non-contiguous regions closer to ease their comparison. They could be also used to focus on a smaller area in more detail or to cover the same area as the map, but representing a different topic.

4.3 Animated maps

4.3.1 Introduction

Faceted maps, described in Section (Section 4.2.7), can show how spatial distributions of variables change (e.g., over time), but the approach has disadvantages. Facets become tiny when there are many of them. Furthermore, the fact that each facet is physically separated on the screen or page means that subtle differences between facets can be hard to detect.

Animated maps solve these issues. Although they depend on digital publication, this is becoming less of an issue as more and more content moves online. Animated maps can still enhance paper reports: you can always link readers to a webpage containing an animated (or interactive) version of a printed map to help make it come alive.

4.3.2 Comparing downloads

There are several animation packages, including {tmap} which includes with tmap::tmap_animation() a specialized function for geospatial animations.

R Code 4.32 : Comparing downloads of several animation packages

Code
my_pkgs_dl(
  pkgs = c(
    "tmap", 
    "gganimate", 
    "plotly", 
    "animation", 
    "animint2", 
    "loon", 
    "tweenR"),
  period = "last-month", 
  days = 30
)
#> # A tibble: 7 × 4
#>   package   average from       to        
#>   <chr>       <dbl> <date>     <date>    
#> 1 plotly       7554 2025-04-28 2025-05-27
#> 2 tmap         1086 2025-04-28 2025-05-27
#> 3 gganimate     687 2025-04-28 2025-05-27
#> 4 animation     202 2025-04-28 2025-05-27
#> 5 animint2       17 2025-04-28 2025-05-27
#> 6 loon            9 2025-04-28 2025-05-27
#> 7 tweenR          0 2025-04-28 2025-05-27

I have no much experience with animations packages. But it is evident for me that the first three are very important:

  • {plotly): I have already started to learn {plotly} and produced some personal notes. I think I will use it to get more information out of map graphics via interactivity, for instance moving the cursor over the map and showing the name of the country and/or some vital statistics.
  • {gganimate} is important because it is fully compatible with {ggplot2} and it can be used for specifying transitions and animations in a flexible and extensible way. But at the moment (2025-05-22) I do not have any experience with this package.

Here I will concentrate on {tmap}.

4.3.3 Ladder score

Instead of reproducing the book’s example, I will try to use the WHR dataset. I want to see the changes over time for the cantril ladder score of the countries. My main question is: What does an animation of the well-being scores looks like and is it valuable graphical representation to understand the data better?

As a tempalte for my own code I am going to use the animated map showing the top 30 largest urban agglomerations from 1950 to 2030 based on population projects by the United Nations.

R Code 4.33 : Producing faceted maps for the animation

Run this code chunk manually if the file still needs to be downloaded.
Code
whr_2011_2024 <- base::readRDS("data/chapter09/whr_final.rds") |> 
  dplyr::filter(wb_group_name == "World") |> 
  dplyr::select(1:3, 6)

world_map_countries <- base::readRDS("data/chapter09/world_map2.rds") |> 
  tibble::as_tibble() |> 
  sf::st_as_sf() |> 
  dplyr::rename(iso3 = adm0_a3, name = admin) |> 
  dplyr::select(iso3, name)

my_save_data_file("chapter09", world_map_countries, "world_map_countries.rds")

world_whr_2011_2024 <- 
  dplyr::left_join(
    whr_2011_2024,
    world_map_countries,
    dplyr::join_by(iso3 == iso3)
  ) |> 
  sf::st_as_sf()

m_save <- tmap::tm_shape(world_whr_2011_2024) +
  tmap::tm_polygons(
    fill = "ladder_score", 
    col = "black",
    fill.scale = tmap::tm_scale(
       values = "matplotlib.rainbow"
       ),
    fill.legend = tmap::tm_legend(title = "Well-being Score"),
    fill.free = FALSE
    ) +
  tmap::tm_facets_wrap(by = "year", nrow = 1, ncol = 1, free.coords = FALSE) +
  tmap::tm_layout(
    legend.orientation = "landscape",
    legend.position = tmap::tm_pos_out("center", "bottom")
  ) 

tmap::tmap_animation(
  tm = m_save, 
  filename = "data/chapter09/ladder_score.gif",
  width = 1200, 
  height = 800,
  delay = 800,
  asp = 1.8,
  outer.margins = 0)

(For this R code chunk is no output available)

R Code 4.34 : Run the animation

Code
magick::image_read("data/chapter09/ladder_score.gif")
Figure 4.32: Show well-being country scores over the years 2011-2024 (2013 is missing). Higher values are higher happiness.

Wait some time for seeing the first change. I have set delay = 800 to provide time for inspecting each year. But even with this long time the result is not very satisfying: It is difficult to get a better understanding about the trends. What I would need is to choose when to change to get which year and this option combined with the interactive inspection of data by hovering the mouse pointer over the map.

4.4 Interactive maps

4.4.1 Introduction

We will explore how to make slippy maps with {tmap} (the syntax of which we have already learned), {mapview}, {mapdeck} and finally with the very popular {leaflet} (which provides low-level control over interactive maps and has many supporting packages).

Resource 4.2 : Leaflet and packages extending its function (Leaflet ecosphere)

I just learned that there are many packages that extend the functionality of {leafelet}. In alphabetical order:

  • {leafem}: {leaflet} extension for {mapview}
  • {leafgl}: High-performance ‘WebGL’ rendering for {leaflet}
  • {leaflegend}: Add custom legends to {leaflet} maps
  • {leaflet}: Create interactive web maps with the JavaScript ‘leaflet’ library
  • {leaflet.extras}: Extra functionality for {leaflet}
  • {leaflet.providers}: Leaflet providers
  • {leafpop}: Include tables, images and graphs in {leaflet} pop-ups
  • {leafsync}: Small multiples (facets) for {leaflet} web maps

4.4.2 Comparing downloads

There are several animation packages, including {tmap} which includes with tmap::tmap_animation() a specialized function for geospatial animations.

R Code 4.35 : Comparing downloads of several interactive map packages

Code
my_pkgs_dl(
  pkgs = c(
    "tmap", 
    "plotly", 
    "mapview",
    "mapdeck",
    "googleway",
    "mapsf",
    "mapmisc",
    "leaflet",
    "Rgooglemaps",
    "ggmap",
    "mapedit"
    ),
  period = "last-month", 
  days = 30
)
#> # A tibble: 11 × 4
#>    package     average from       to        
#>    <chr>         <dbl> <date>     <date>    
#>  1 plotly         7554 2025-04-28 2025-05-27
#>  2 leaflet        2996 2025-04-28 2025-05-27
#>  3 tmap           1086 2025-04-28 2025-05-27
#>  4 ggmap           891 2025-04-28 2025-05-27
#>  5 mapview         654 2025-04-28 2025-05-27
#>  6 mapedit          81 2025-04-28 2025-05-27
#>  7 googleway        80 2025-04-28 2025-05-27
#>  8 mapsf            69 2025-04-28 2025-05-27
#>  9 mapdeck          57 2025-04-28 2025-05-27
#> 10 mapmisc          13 2025-04-28 2025-05-27
#> 11 Rgooglemaps       0 2025-04-28 2025-05-27

4.4.3 Preparing data

R Code 4.36 : Prepare data for calling leaflet WHR 2024 data

Run this code chunk manually if the file still needs to be downloaded.
Code
whr_2024 <- base::readRDS("data/chapter09/whr_final.rds") |> 
  dplyr::filter(
    wb_group_name == "World" & year == 2024) |> 
  dplyr::select(1:3, 6)

world_map_countries <- base::readRDS("data/chapter09/world_map_countries.rds")  

world_whr_2024 <- 
  dplyr::left_join(
    whr_2024,
    world_map_countries,
    dplyr::join_by(iso3 == iso3)
  ) |> 
  sf::st_as_sf()

leaflet_whr_2024 <- tmap::tm_shape(world_whr_2024) + 
  tmap::tm_polygons(
    fill = "ladder_score", 
    id = "name", 
    hover = TRUE,
    fill.scale = tmap::tm_scale(
       values = "matplotlib.rainbow",
       value.na = "yellow")
     )

my_save_data_file("chapter09", leaflet_whr_2024, "leaflet_whr_2024.rds")

(For this R code chunk is no output available)

Hovering about the menu “paper pile” you can choose the kind of background map. The background labels appear whenever there are no data for countries and the map is resized to greater details. But my specific yellow color does not work for NA’s countries as they all appear with a gray background.

4.4.4 Applying {leaflet} via {tmap}

A unique feature of {tmap} is its ability to create static and interactive maps using the same code. Maps can be viewed interactively at any point by switching to view mode, using the command tmap::tmap_mode("view"). This should be the same as tmap::tmap_leaflet(data, show = TRUE) with the advantage that you can specify to show or hide the map and that you don’t need to place the code to return to plot mode with tmap::tmap_mode("plot").

The function tmap::tmap_leaflet(leaflet_whr_2024, show = TRUE) did not work for me.

4.4.4.1 Turn on “view” mode

R Code 4.37 : Turn on “View” mode

Code
tmap::tmap_mode("view")
#> ℹ tmap mode set to "view".

4.4.4.2 Basemap provider

Code Collection 4.8 : Map with standard & specific provider

R Code 4.38 : Map of well-being scores in 2024 in standard version

Code
leaflet_whr_2024 <- base::readRDS("data/chapter09/leaflet_whr_2024.rds")

leaflet_whr_2024
Figure 4.33: Interactive map of the well-being data in 2024 with standard providers

Hovering the mouse cursor over the paper pile symbol (top left) opens a menu where you can choose different map styles. But you can also pick one of very many map server provider as it is shown in the next tab.

R Code 4.39 : Map of well-being scores in 2024 with specific map provider

Code
leaflet_whr_2024 + tmap::tm_basemap(server = "USGS.USTopo")
Figure 4.34: Interactive map with specified base map ‘Esri.WorldTopoMap’

There are many options to choose a provider for the server parameter. Choose one from the leaflet provider preview.

tmap::tm_basemap() draws the tile layer as basemap, i.e. as bottom layer. In contrast, there is also tmap::tm_tiles() that draws the tile layer as overlay layer, where the stacking order corresponds with the order in which this layer is called, just like other map layers.

There are two parameters groupand group.control to manage the map layers:

  • group: Name of the group to which this layer belongs. This is only relevant in view mode, where layer groups can be switched.

  • group.control: In view mode, the group control determines how layer groups can be switched on and off. Options are:

  • “radio” for radio buttons (meaning only one group can be shown),

  • “check” for check boxes (so multiple groups can be shown), and

  • “none” for no control (the group cannot be (de)selected).

4.4.4.3 Facets: “pretty” breaks

R Code 4.40 : Compare 2011 with 2024 WHR data with pretty breaks

Code
whr_final <- base::readRDS("data/chapter09/whr_final.rds") 
world_map_countries <- base::readRDS("data/chapter09/world_map_countries.rds") 


whr_2011.2024 <- whr_final |> 
  dplyr::filter(
    wb_group_name == "World" & (year == 2011 | year == 2024)
    ) |> 
  dplyr::select(1,2,6) |> 
  tidyr::pivot_wider(
    names_from = year,
    values_from = ladder_score
  )

world_whr_2011.2024 <- 
  dplyr::left_join(
    whr_2011.2024,
    world_map_countries,
    dplyr::join_by(iso3 == iso3)
  ) |> 
  sf::st_as_sf()


tmap::tm_shape(world_whr_2011.2024) + 
  tmap::tm_polygons(c("2011", "2024"),
        fill.scale = tmap::tm_scale(
          values = "cols4all.friendly13"
        ),
        fill.legend = tmap::tm_legend(
          title = "Score"
        ),
        fill.free = FALSE
  ) +
  tmap::tm_facets_vstack(ncol = 1, sync = TRUE)
Figure 4.35: Compare 2011 with 2024 WHR data of world’s countries (pretty breaks)

The breaks aren’t optimal. For instance Russia has improved considerable from 5.284 in 2011 to 5.945, but you can’t see this progress because both values are between 5 to 6. Here I should consider more sensible breaks as I have tried in the next code chunk.

Adding {sf} class to {tibble} class

Without sf::st_as_sf() rendering the whole file would not work (but knitting just this code chunk would work). The reason is that I need to be aware the {tibble} classes "tbl_df" "tbl" "data.frame" needed to convert to (sf) classes with the result "sf" "tbl_df" "tbl" "data.frame".

4.4.4.4 Facets: quantile breaks

R Code 4.41 : Compare 2011 with 2024 WHR data with quantile breaks

Code
tmap::tm_shape(world_whr_2011.2024) + 
  tmap::tm_polygons(c("2011", "2024"),
        fill.scale = tmap::tm_scale_intervals(
          values = "cols4all.friendly13" ,
          n = 7,
          style = "quantile"
        ),
        fill.legend = tmap::tm_legend(
          title = "Score"
        ),
        fill.free = FALSE
  ) +
  tmap::tm_facets_vstack(ncol = 1, sync = TRUE)
Figure 4.36: Compare 2011 with 2024 WHR data of world’s countries (quantile breaks)

4.4.4.5 Back to “plot” mode

R Code 4.42 : Back to “plot” mode

Code
tmap::tmap_mode("plot")
#> ℹ tmap mode set to "plot".

4.4.5 Other interactive map packages

4.4.5.1 {mapview}

If you are not proficient with {tmap}, the quickest way to create interactive maps in R may be with {mapview.} The following ‘one liner’ is a reliable way to interactively explore a wide range of geographic data formats.

Code Collection 4.9 : Basic {mapview}: One-liner

R Code 4.43 : Basic {mapview}: One-liner nz

Code
mapview::mapview(nz)

R Code 4.44 : Basic {mapview}: One-liner Breweries

Code
mapview::mapview(mapview::breweries)

R Code 4.45 : Basic {mapview}: One-liner world

Code
mapview::mapview(world_map_countries)

{mapview} has a concise syntax, yet, it is powerful. By default, it has some standard GIS functionality such as mouse position information, attribute queries (via pop-ups), scale bar, and zoom-to-layer buttons. It also offers advanced controls including the ability to ‘burst’ datasets into multiple layers and the addition of multiple layers with + followed by the name of a geographic object. Additionally, it provides automatic coloring of attributes via the zcol argument. In essence, it can be considered a data-driven {leaflet} API (see below for more information about {leaflet}).

Given that {mapview} always expects a spatial object (including sf and SpatRaster) as its first argument, it works well at the end of piped expressions.

Consider the following example where {sf} is used to intersect lines and polygons and then is visualized with {mapview}. It uses some advanced {sf} function I had never used before.

R Code 4.46 : Example that intersects lines and polygons with {sf} visualized with {viewmap}

Code
oberfranken = base::subset(
  mapview::franconia, district == "Oberfranken"
  )
mapview::trails |>
  sf::st_transform(sf::st_crs(oberfranken)) |>
  sf::st_intersection(oberfranken) |>
  sf::st_collection_extract("LINESTRING") |>
  mapview::mapview(color = "red", lwd = 3, layer.name = "trails") +
  mapview::mapview(mapview::franconia, zcol = "district") +
  mapview::breweries
#> Warning: attribute variables are assumed to be spatially constant throughout
#> all geometries

One important thing to keep in mind is that {mapview} layers are added via the + operator (similar to {ggplot2} or {tmap}). By default, {mapview} uses the leaflet JavaScript library to render the output maps, which is user-friendly and has a lot of features.

However, some alternative rendering libraries could be more performant (work more smoothly on larger datasets). {mapview} allows to set alternative rendering (graphics) libraries ({leafgl} and {mapdeck}) in the mapview::mapviewOptions()1. For further information on {mapview}, see the package’s website at: r-spatial.github.io/mapview/: Beside the standard reference part of available functions there are also seven articles explaining different aspects of {mapview}.

4.4.5.2 {googleway}

There are other ways to create interactive maps with R. The {googleway} package, for example, provides an interactive mapping interface that is flexible and extensible.

Googleway provides access to Google Maps APIs, and the ability to plot an interactive Google Map overlayed with various layers and shapes, including markers, circles, rectangles, polygons, lines (polylines) and heatmaps. You can also overlay traffic information, transit and cycling routes. (From the (new) googleway-vignette, which is more uptodate than the reference at the documentation website.)

As I can see there are an awful lot of functions covered in this package. But surprisingly it is not downloaded many times (somewhat under 100 downloads/month)

4.4.5.3 {mapdeck}

Another approach by the same author that developed {googleway} is {mapdeck}, a combination of Mapbox and Deck.gl framework2. Its use of WebGL enables it to interactively visualize large datasets up to millions of points. The package uses Mapbox access tokens, which you must register for before using the package.

R Code 4.47 : Demonstration of {mapdeck}

mapdeck::set_token(Sys.getenv("MAPBOX_PUBLIC_TOKEN"))
crash_data = utils::read.csv("https://git.io/geocompr-mapdeck")
crash_data = stats::na.omit(crash_data)
ms = mapdeck::mapdeck_style("dark")
mapdeck::mapdeck(style = ms, pitch = 45, location = c(0, 52), zoom = 4) |>
  mapdeck::add_grid(data = crash_data, lat = "lat", lon = "lng", cell_size = 1000,
           elevation_scale = 50, colour_range = grDevices::hcl.colors(6, "plasma"))

(For this R code chunk is no output available)

The above code chunk worked in RStudio but didn’t show up in my Brave browser. At first I thought that it is a problem of WebGL compatibility. But as I can see the spinning cube my browser should support WebGL. But there are other options why it might fail referenced by the {mapdeck} package developer David Cooley or on the Mapbox website itself.

As I couldn’t find a solution and as I am not convinced to use {mpadeck} on a daily basis I bypassed this problem by using the RStudio result below as an iframe. But I had also to cancel the evaluation of the code because otherwise all the other code after this code chunk doesn’t appear in the browser.

R Code 4.48 : Presenting the results of the demo code in R Code 4.47 as HTML iframe


You can zoom into England and drag the map in the browser to see the crash statistics. In addition you can also rotate and tilt the map when pressing Cmd/Ctrl. Multiple layers can be added with the pipe operator, as demonstrated in the {mapdeck} vignettes. {mapdeck} also supports sf objects, as can be seen by replacing the mapdeck::add_grid() function call in the preceding code chunk with mapdeck::add_polygon(data = lnd, layer_id = "polygon_layer"), to add polygons representing London to an interactive tilted map.

4.4.5.4 {leaflet}

The last example is {leaflet} which is the most mature and widely used interactive mapping package in R. {leaflet} provides a relatively low-level interface to the Leaflet JavaScript library and many of its arguments can be understood by reading the documentation of the original JavaScript library.

Leaflet maps are created with leaflet::leaflet(), the result of which is a leaflet map object which can be piped to other leaflet functions. This allows multiple map layers and control settings to be added interactively, as demonstrated in the code below. (See the {leaflet} R documentation for details.)

R Code 4.49 : Multiple map layers demonstration of {leaflet}

Code
pal = leaflet::colorNumeric("RdYlBu", domain = spData::cycle_hire$nbikes)
leaflet::leaflet(data = spData::cycle_hire) |> 
  leaflet::addProviderTiles(leaflet::providers$CartoDB.Positron) |>
  leaflet::addCircles(col = ~pal(nbikes), opacity = 0.9) |> 
  leaflet::addPolygons(data = spData::lnd, fill = FALSE) |> 
  leaflet::addLegend(pal = pal, values = ~nbikes) |> 
  leaflet::setView(lng = -0.1, 51.5, zoom = 12) |> 
  leaflet::addMiniMap()
See my section to learn how to use {leaflet} in Appendix B

4.5 Mapping Applications

4.5.1 Limatation of interactivity

The interactive web maps demonstrated in Section 4.4 can go far. Careful selection of layers to display, basemaps and pop-ups can be used to communicate the main results of many projects involving geocomputation. But the web mapping approach to interactivity has also some limitations:

  • Although the map is interactive in terms of panning, zooming and clicking, the code is static, meaning the user interface is fixed.
  • All map content is generally static in a web map, meaning that web maps cannot scale to handle large datasets easily.
  • Additional layers of interactivity, such a graphs showing relationships between variables and ‘dashboards’, are difficult to create using the web mapping-approach.

4.5.2 Geospatial framework & map servers

Overcoming these limitations involves going beyond static web mapping and toward geospatial frameworks and map servers. Products in this field include

  • GeoDjango which extends the Django web framework and is written in Python,
  • MapServer (a framework for developing web applications, largely written in C and C++) and
  • GeoServer (a mature and powerful map server written in Java). Each of these is scalable, enabling maps to be served to thousands of people daily, assuming there is sufficient public interest in your maps!

The bad news is that such server-side solutions require much skilled developer time to set up and maintain, often involving teams of people with roles such as a dedicated geospatial database administrator (DBA).

4.5.3 {shiny}

Fortunately for R programmers, web mapping applications can now be rapidly created with {shiny} (Chang et al. 2024). As described in the open source book Mastering Shiny (Wickham 2021) and in the shiny documentation, {shiny} is an R package and framework for converting R code into interactive web applications. You can embed interactive maps in {shiny} apps thanks to functions such as leaflet::renderLeaflet().

R Code 4.50 : Demo of a {Shiny} driven interactive map

library(shiny)    # for shiny apps
library(leaflet)  # renderLeaflet function
library(spData)   # loads the world dataset 
ui = fluidPage(
  sliderInput(inputId = "life", "Life expectancy", 49, 84, value = 80),
      leafletOutput(outputId = "map")
  )
server = function(input, output) {
  output$map = renderLeaflet({
    leaflet() |> 
      # addProviderTiles("OpenStreetMap.BlackAndWhite") |>
      addPolygons(data = spData::world[world$lifeExp < input$life, ])})
}
shinyApp(ui, server)

(For this R code chunk is no output available)

This minimum example does not work. Rendering just the code chunk inside RStudio I got the warning message “Warning: Error in FUN: x must be a vector, not a object.” and a separate page/tab in the browser with the ‘Life expectancy’ slider but nothing else.

Knitting the whole Quarto file I got no warning but the message on which port shiny is listening with an empty grey rectangle.

Note 4.1: Deploying a serverless Shiny application for R within Quarto

I assumed for the problem above that it is not possible to include a Shiny application inside a Quarto document without a shiny server. But I came across the GitHub repository Creating an R Shinylive App inside a Quarto Document featuring a tutorial for a R-shinylive app in Quarto followed up by another serverless demonstration of a shiny app dashboard.

Later I learned that there is a {shinylive} package. See the docs and the GitHub repo.

Exporting ‘shiny’ applications with ‘shinylive’ allows you to run them entirely in a web browser, without the need for a separate R server. The traditional way of deploying ‘shiny’ applications involves i a separate server and client: the server runs R and ‘shiny’, and clients connect via the web browser. When an application is deployed with ‘shinylive’, R and ‘shiny’ run in the web browser (via ‘webR’): the browser is effectively both the client and server for the application. This allows for your ‘shiny’ application exported by ‘shinylive’ to be hosted by a static web server.

The original URL I found was the tutorial developed by TheCoatlessProfessor (James J Balamuta) based on its GitHub repo featuring a demo and several introductory articles. It is based on a talk by Joe Cheng at the posit conference 2023 on the shinylive project and has a follow up article from march 2025 with an update and a link to a demo dashboard website.

I think it pays the effort to study this possibility because it would a huge advantage if I could provide my result as an interactive shiny dashboard!

Learning {leaflet} combined with {shiny}

There are several hints about the details of the demonstration in R Code 4.50 in the Geocomputing book which could helpful to learn how to integrate interactive maps with {leaflet} into {shiny}. I will skip these parts here at the moment. Perhaps I will come back to it later in another trial or occasion to interpret my datasets.

4.6 Other mapping packages

4.6.1 Generic map packages

We have learned that there are many geocpomputing packages around. You will get an overview about the functionality of these packages skimming two task views:

  • Spatial: CRAN Task View: Analysis of Spatial Data (Bivand and Nowosad 2025). Base R includes many functions that can be used for reading, visualising, and analysing spatial data. The focus in this view is on “geographical” spatial data, where observations can be identified with geographical locations, and where additional information about these locations may be retrieved if the location is recorded with care. This is the more updated task view, but I noticed that {googleway} was not mentioned.
  • SpatioTemporal: CRAN Task View: Handling and Analyzing Spatio-Temporal Data (Pebesma and Bivand 2022). This task view aims at presenting R packages that are useful for the analysis of spatio-temporal data. (This task view seems a little bit outdated because the last update was 2022-10-01.)

The chapter on mapping application features especially {ggplot2} which supports the {sf} package with the ggplot2::geom_sf() function but is also part of an ecosphere that includes map-drawing relevant packages as for instance {ggspatial}, {ggmap}, and {mapsf}

  • {ggmap} (Kahle and Wickham 2013) contains a collection of functions to visualize spatial data and models on top of static maps from various online sources (e.g Google Maps and Stamen Maps). It includes tools common to those tasks, including functions for geolocation and routing.
  • {ggspatial} (Dunnington 2023) is a spaital framework for {gplot2}. It enhances {ggplot2}’s mapping capabilities by providing options to add a north arrow (ggspatial::annotation_north_arrow()) and a scale bar (ggspatial::annotation_scale()), or to add background tiles (ggspatial::annotation_map_tile()). It also accepts various spatial data classes with ggspatial::layer_spatial().
  • {mapsf} (Giraud 2024) create and integrates thematic maps in your workflow. This package helps to design various cartographic representations such as proportional symbols, choropleth or typology maps. It also offers several functions to display layout elements that improve the graphic presentation of maps (e.g. scale bar, north arrow, title, labels). {mapsf} maps {sf} objects on {base} graphics.

Another benefit of maps based on {ggplot2} is that they can easily be given a level of interactivity when printed using the function plotly::ggplotly() from the {plotly} package.

{rasterVis} is another geocomputing package facilitating visualization methods for raster data. As I am mostly interested in visualizing vector data I am not going into the detail of this package.

4.6.1.1 Summary table

Selected general-purpose mapping packages.
Package Title
ggplot2 Create Elegant Data Visualisations Using the Grammar of Graphics
googleway Accesses Google Maps APIs to Retrieve Data and Plot Maps
ggspatial Spatial Data Framework for ggplot2
leaflet Create Interactive Web Maps with Leaflet
mapview Interactive Viewing of Spatial Data in R
plotly Create Interactive Web Graphics via 'plotly.js'
rasterVis Visualization Methods for Raster Data
tmap Thematic Maps

4.6.2 Specific map packages

Several packages focus on specific map types. Such packages create cartograms that distort geographical space, create line maps, transform polygons into regular or hexagonal grids, visualize complex data on grids representing geographic topologies, and create 3D visualizations.

4.6.2.1 Specific map packages

Selected specific-purpose mapping packages, with associated metrics.
Package Title
cartogram Create Cartograms with R
geogrid Turn Geospatial Polygons into Regular or Hexagonal Grids
geofacet ‘ggplot2’ Faceting Utilities for Geographical Data
linemap Line Maps
tanaka Design Shaded Contour Lines (or Tanaka) Maps
rayshader Create Maps and Visualize Data in 2D and 3D

Of special interest is the {cartogram} package (Jeworutzki 2023). A cartogram is a type of thematic map in which geographic areas are resized or distorted based on a quantitative variable, such as population. The goal is to make the area sizes proportional to the selected variable while preserving geographic positions as much as possible. There is also a {tmap.cartogram} package as an extension to the {*+tmap} packages providing new layer functions to {tmap**} for creating various types of cartograms.

But I have to look much more into the details of all these packages mentioned here. I will do this step by step whenever I feel the need born from my research process working on my different dataset about well-beeing and inequality data.

References

Bivand, Roger, and Jakub Nowosad. 2025. “CRAN Task View: Analysis of Spatial Data.” https://CRAN.R-project.org/view=Spatial.
Chang, Winston, Joe Cheng, JJ Allaire, Carson Sievert, Barret Schloerke, Yihui Xie, Jeff Allen, Jonathan McPherson, Alan Dipert, and Barbara Borges. 2024. “Shiny: Web Application Framework for r.” https://doi.org/10.32614/CRAN.package.shiny.
Dunnington, Dewey. 2023. “Ggspatial: Spatial Data Framework for Ggplot2.” https://doi.org/10.32614/CRAN.package.ggspatial.
Giraud, Timothée. 2024. “Mapsf: Thematic Cartography.” https://doi.org/10.32614/CRAN.package.mapsf.
Jeworutzki, Sebastian. 2023. “Cartogram: Create Cartograms with r.” https://doi.org/10.32614/CRAN.package.cartogram.
Kahle, David, and Hadley Wickham. 2013. “Ggmap: Spatial Visualization with Ggplot2” 5. https://journal.r-project.org/archive/2013-1/kahle-wickham.pdf.
Pebesma, Edzer, and Roger Bivand. 2022. “CRAN Task View: Handling and Analyzing Spatio-Temporal Data.” https://CRAN.R-project.org/view=SpatioTemporal.
Wickham, Hadley. 2021. Mastering Shiny: Build Interactive Apps, Reports, and Dashboards Powered by R. O’Reilly UK Ltd.

  1. gl — often used for some packages at the end of their names, like {lefgl} — is the abbreviation for “graphics library”.↩︎

  2. Deck.gl has been developed under the stewardship of Uber’s Engineering organization but has now moved to an open governance model hosted by the OpenJS Foundation and accessible in a GitHub repository.↩︎