-
-
Notifications
You must be signed in to change notification settings - Fork 590
/
09-mapping.Rmd
1226 lines (999 loc) · 78.2 KB
/
09-mapping.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# (PART) Extensions {-}
# Making maps with R {#adv-map}
```{r, include=FALSE}
source("code/before_script.R")
```
## Prerequisites {-}
- This chapter requires the following packages that we have already been using:
```{r 08-mapping-1, message=FALSE }
library(sf)
library(terra)
library(dplyr)
library(spData)
library(spDataLarge)
```
- The main package used in this chapter is **tmap**.
We recommend you to install its development version from the [r-universe](https://r-universe.dev/) repository, which is updated more frequently than the CRAN version:
```{r}
#| eval: false
install.packages("tmap", repos = c("https://r-tmap.r-universe.dev",
"https://cloud.r-project.org"))
```
- It uses the following visualization packages (also install **shiny** if you want to develop interactive mapping applications):
```{r 08-mapping-2, message=FALSE}
library(tmap) # for static and interactive maps
library(leaflet) # for interactive maps
library(ggplot2) # tidyverse data visualization package
```
- You also need to read-in a couple of datasets as follows for Section \@ref(spatial-ras):
```{r 04-spatial-operations-1-1}
nz_elev = rast(system.file("raster/nz_elev.tif", package = "spDataLarge"))
```
## Introduction
A satisfying and important aspect of geographic research is communicating the results.
Map-making\index{map-making} --- the art of cartography --- is an ancient skill involving communication, attention to detail, and an element of creativity.
Static mapping in R is straightforward with the `plot()` function, as we saw in Section \@ref(basic-map).
It is possible to create advanced maps using base R methods [@murrell_r_2016].
The focus of this chapter, however, is cartography with dedicated map-making packages.
When learning a new skill, it makes sense to gain depth-of-knowledge in one area before branching out.
Map-making is no exception, hence this chapter's coverage of one package (**tmap**) in depth rather than many superficially.
In addition to being fun and creative, cartography also has important practical applications.
A carefully crafted map can be the best way of communicating the results of your work, but poorly designed maps can leave a bad impression.
Common design issues include poor placement, size and readability of text and careless selection of colors, as outlined in the [style guide](https://files.taylorandfrancis.com/TJOM-suppmaterial-quick-guide.pdf) of the *Journal of Maps*.
Furthermore, poor map-making can hinder the communication of results [@brewer_designing_2015]:
> Amateur-looking maps can undermine your audience’s ability to understand important information and weaken the presentation of a professional data investigation.
Maps have been used for several thousand years for a wide variety of purposes.
Historic examples include maps of buildings and land ownership in the Old Babylonian dynasty more than 3000 years ago and Ptolemy's world map in his masterpiece *Geography*\index{geography} nearly 2000 years ago [@talbert_ancient_2014].
Map-making has historically been an activity undertaken only by, or on behalf of, the elite.
This has changed with the emergence of open source mapping software such as the R package **tmap** and the 'print layout' in QGIS\index{QGIS}, which allow anyone to make high-quality maps, enabling 'citizen science'.
Maps are also often the best way to present the findings of geocomputational research in a way that is accessible.
Map-making is therefore a critical part of geocomputation\index{geocomputation} and its emphasis is not only on describing, but also *changing* the world.
This chapter shows how to make a wide range of maps.
The next section covers a range of static maps, including aesthetic considerations, facets and inset maps.
Sections \@ref(animated-maps) to \@ref(mapping-applications) cover animated and interactive maps (including web maps and mapping applications).
Finally, Section \@ref(other-mapping-packages) covers a range of alternative map-making packages including **ggplot2** and **cartogram**.
## Static maps
\index{map-making!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 with the release of **sp** [see @pebesma_classes_2005], 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 a decade later [@cheshire_spatial_2015].
The generic `plot()` function is often the fastest way to create static maps from vector and raster spatial objects (see Sections \@ref(basic-map) and \@ref(basic-map-raster)).
Sometimes, simplicity and speed are priorities, especially during the development phase of a project, and this is where `plot()` excels.
The base R approach is also extensible, with `plot()` offering dozens of arguments.
Another approach is the **grid** package which allows low-level control of static maps, as illustrated in chapter [14](https://www.stat.auckland.ac.nz/~paul/RG2e/chapter14.html) of @murrell_r_2016.
This part of the book focuses on **tmap** and emphasizes the essential aesthetic and layout options.
\index{tmap (package)}
**tmap** is a powerful and flexible map-making package with sensible defaults.
It has a concise syntax that allows for the creation of attractive maps with minimal code which will be familiar to **ggplot2** users.
It also has the unique capability to generate static and interactive maps using the same code via `tmap_mode()`.
Finally, it accepts a wider range of spatial classes (including **sf** and **terra** objects) than alternatives such as **ggplot2**.
### tmap basics
\index{tmap (package)!basics}
Like **ggplot2**, **tmap** is based on the idea of a 'grammar of graphics' [@wilkinson_grammar_2005].
This involves a separation between the input data and the aesthetics (how data are visualized): each input dataset can be 'mapped' in a range of different ways including location on the map (defined by data's `geometry`), color, and other visual variables.
The basic building block is `tm_shape()` (which defines input data: a vector or raster object), followed by one or more layer elements such as `tm_fill()` and `tm_dots()`.
This layering is demonstrated in the chunk below, which generates the maps presented in Figure \@ref(fig:tmshape):
```{r 08-mapping-3, eval=FALSE}
# Add fill layer to nz shape
tm_shape(nz) +
tm_fill()
# Add border layer to nz shape
tm_shape(nz) +
tm_borders()
# Add fill and border layers to nz shape
tm_shape(nz) +
tm_fill() +
tm_borders()
```
```{r tmshape, echo=FALSE, message=FALSE, fig.cap="New Zealand's shape plotted with fill (left), border (middle) and fill and border (right) layers added using tmap functions.", fig.scap="New Zealand's shape plotted using tmap functions."}
source("https://github.com/geocompx/geocompr/raw/main/code/09-tmshape.R", print.eval = TRUE)
```
The object passed to `tm_shape()` in this case is `nz`, an `sf` object representing the regions of New Zealand (see Section \@ref(intro-sf) for more on `sf` objects).
Layers are added to represent `nz` visually, with `tm_fill()` and `tm_borders()` creating shaded areas (left panel) and border outlines (middle panel) in Figure \@ref(fig:tmshape), respectively.
\index{map-making!layers}
This is an intuitive approach to map-making:
the common task of *adding* new layers is undertaken by the addition operator `+`, followed by `tm_*()`.
The asterisk (\*) refers to a wide range of layer types which have self-explanatory names including:
- `tm_fill()`: shaded areas for (multi)polygons
- `tm_borders()`: border outlines for (multi)polygons
- `tm_polygons()`: both, shaded areas and border outlines for (multi)polygons
- `tm_lines()`: lines for (multi)linestrings<!--tm_iso()`?-->
- `tm_symbols()`: symbols for (multi)points, (multi)linestrings, and (multi)polygons
- `tm_raster()`: colored cells of raster data (there is also `tm_rgb()` for rasters with three layers)
- `tm_text()`: text information for (multi)points, (multi)linestrings, and (multi)polygons
This layering is illustrated in the right panel of Figure \@ref(fig:tmshape), the result of adding a border *on top of* the fill layer.
```{block2 qtm, type = 'rmdnote'}
`qtm()` is a handy function to create **q**uick **t**hematic **m**aps (hence the snappy name).
It is concise and provides a good default visualization in many cases:
`qtm(nz)`, for example, is equivalent to `tm_shape(nz) + tm_fill() + tm_borders()`.
Further, layers can be added concisely using multiple `qtm()` calls, such as `qtm(nz) + qtm(nz_height)`.
The disadvantage is that it makes aesthetics of individual layers harder to control, explaining why we avoid teaching it in this chapter.
```
### Map objects {#map-obj}
A useful feature of **tmap** is its ability to store *objects* representing maps.
The code chunk below demonstrates this by saving the last plot in Figure \@ref(fig:tmshape) as an object of class `tmap` (note the use of `tm_polygons()` which condenses `tm_fill() + tm_borders()` into a single function):
```{r 08-mapping-4}
map_nz = tm_shape(nz) + tm_polygons()
class(map_nz)
```
`map_nz` can be plotted later, for example by adding other layers (as shown below) or simply running `map_nz` in the console, which is equivalent to `print(map_nz)`.
New *shapes* can be added with `+ tm_shape(new_obj)`.
In this case, `new_obj` represents a new spatial object to be plotted on top of preceding layers.
When a new shape is added in this way, all subsequent aesthetic functions refer to it, until another new shape is added.
This syntax allows the creation of maps with multiple shapes and layers, as illustrated in the next code chunk which uses the function `tm_raster()` to plot a raster layer (with `col_alpha` set to make the layer semi-transparent):
```{r 08-mapping-5, results='hide'}
map_nz1 = map_nz +
tm_shape(nz_elev) + tm_raster(col_alpha = 0.7)
```
Building on the previously created `map_nz` object, the preceding code creates a new map object `map_nz1` that contains another shape (`nz_elev`) representing average elevation across New Zealand (see Figure \@ref(fig:tmlayers), left).
More shapes and layers can be added, as illustrated in the code chunk below which creates `nz_water`, representing New Zealand's [territorial waters](https://en.wikipedia.org/wiki/Territorial_waters), and adds the resulting lines to an existing map object.
```{r 08-mapping-6}
nz_water = st_union(nz) |>
st_buffer(22200) |>
st_cast(to = "LINESTRING")
map_nz2 = map_nz1 +
tm_shape(nz_water) + tm_lines()
```
There is no limit to the number of layers or shapes that can be added to `tmap` objects, and the same shape can even be used multiple times.
The final map illustrated in Figure \@ref(fig:tmlayers) is created by adding a layer representing high points (stored in the object `nz_height`) onto the previously created `map_nz2` object with `tm_symbols()` (see `?tm_symbols` for details on **tmap**'s point plotting functions).
The resulting map, which has four layers, is illustrated in the right-hand panel of Figure \@ref(fig:tmlayers):
```{r 08-mapping-7}
map_nz3 = map_nz2 +
tm_shape(nz_height) + tm_symbols()
```
\index{map-making!metaplot}
A useful and little known feature of **tmap** is that multiple map objects can be arranged in a single 'metaplot' with `tmap_arrange()`.
This is demonstrated in the code chunk below which plots `map_nz1` to `map_nz3`, resulting in Figure \@ref(fig:tmlayers).
```{r tmlayers, message=FALSE, fig.cap="Maps with added layers to the final map of Figure 9.1.", fig.scap="Additional layers added to the output of Figure 9.1."}
tmap_arrange(map_nz1, map_nz2, map_nz3)
```
More elements can also be added with the `+` operator.
Aesthetic settings, however, are controlled by arguments to layer functions.
### Visual variables
\index{map-making!aesthetics}
\index{map-making!visual variables}
The plots in the previous section demonstrate **tmap**'s default aesthetic settings.
Gray shades are used for `tm_fill()` and `tm_symbols()` layers and a continuous black line is used to represent lines created with `tm_lines()`.
Of course, these default values and other aesthetics can be overridden.
The purpose of this section is to show how.
There are two main types of map aesthetics: those that change with the data and those that are constant.
Unlike **ggplot2**, which uses the helper function `aes()` to represent variable aesthetics, **tmap** accepts a few aesthetic arguments, depending on a selected layer type:
- `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, we may customize the fill and border color transparency using `fill_alpha` and `col_alpha`.
To map a variable to an aesthetic, pass its column name to the corresponding argument, and to set a fixed aesthetic, pass the desired value instead.^[
If there is a clash between a fixed value and a column name, the column name takes precedence. This can be verified by running the next code chunk after running `nz$red = 1:nrow(nz)`.
]
The impact of setting these with fixed values is illustrated in Figure \@ref(fig:tmstatic).
```{r tmstatic, message=FALSE, fig.cap="Impact of changing commonly used fill and border aesthetics to fixed values.", fig.scap="The impact of changing commonly used aesthetics."}
ma1 = tm_shape(nz) + tm_polygons(fill = "red")
ma2 = tm_shape(nz) + tm_polygons(fill = "red", fill_alpha = 0.3)
ma3 = tm_shape(nz) + tm_polygons(col = "blue")
ma4 = tm_shape(nz) + tm_polygons(lwd = 3)
ma5 = tm_shape(nz) + tm_polygons(lty = 2)
ma6 = tm_shape(nz) + tm_polygons(fill = "red", fill_alpha = 0.3,
col = "blue", lwd = 3, lty = 2)
tmap_arrange(ma1, ma2, ma3, ma4, ma5, ma6)
```
Like base R plots, arguments defining aesthetics can also receive values that vary.
Unlike the base R code below (which generates the left panel in Figure \@ref(fig:tmcol)), **tmap** aesthetic arguments will not accept a numeric vector:
```{r 08-mapping-9, eval=FALSE}
plot(st_geometry(nz), col = nz$Land_area) # works
tm_shape(nz) + tm_fill(fill = nz$Land_area) # fails
#> Error: palette should be a character value
```
Instead `fill` (and other aesthetics that can vary such as `lwd` for line layers and `size` for point layers) requires a character string naming an attribute associated with the geometry to be plotted.
Thus, one would achieve the desired result as follows (Figure \@ref(fig:tmcol), right panel):
```{r 08-mapping-10, fig.show='hide', message=FALSE}
tm_shape(nz) + tm_fill(fill = "Land_area")
```
```{r tmcol, message=FALSE, fig.cap="Comparison of base (left) and tmap (right) handling of a numeric color field.", fig.scap="Comparison of base graphics and tmap", echo=FALSE, fig.show='hold', warning=FALSE, message=FALSE, fig.height=6, out.width="45%"}
plot(nz["Land_area"])
tm_shape(nz) + tm_fill(fill = "Land_area")
```
Each visual variable has three related additional arguments, with suffixes of `.scale`, `.legend`, and `.free`.
For example, the `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 \@ref(scales)), while the `.legend` argument is used to customize the legend settings, such as its title, orientation, or position (Section \@ref(legends)).
The `.free` argument is relevant only for maps with many facets to determine if each facet has the same or different scale and legend.
### Scales
\index{tmap (package)!scales}
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 `col`, then `col.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 `tm_scale()`, which selects the visual settings automatically given by the input data type (factor, numeric, and integer).
\index{tmap (package)!color breaks}
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 as illustrated in Figure \@ref(fig:tmpal).
This figure shows four ways of coloring regions in New Zealand depending on median income, from left to right (and demonstrated in the code chunk below):
- The default setting uses 'pretty' breaks, described in the next paragraph
- `breaks` allows you to manually set the breaks
- `n` sets the number of bins into which numeric variables are categorized
- `values` defines the color scheme, for example, `BuGn`
```{r 08-mapping-12, eval=FALSE}
tm_shape(nz) + tm_polygons(fill = "Median_income")
tm_shape(nz) + tm_polygons(fill = "Median_income",
fill.scale = tm_scale(breaks = c(0, 30000, 40000, 50000)))
tm_shape(nz) + tm_polygons(fill = "Median_income",
fill.scale = tm_scale(n = 10))
tm_shape(nz) + tm_polygons(fill = "Median_income",
fill.scale = tm_scale(values = "BuGn"))
```
```{r tmpal, message=FALSE, fig.cap="Color settings. The results show (from left to right): default settings, manual breaks, n breaks, and the impact of changing the palette.", fig.scap="Color settings.", echo=FALSE, fig.asp=0.56, warning=FALSE}
source("https://github.com/geocompx/geocompr/raw/main/code/09-tmpal.R", print.eval = TRUE)
```
```{block2 break-style-note0, type='rmdnote'}
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`.
```
\index{tmap (package)!break styles}
We are also able to customize scales using a family of functions that start with the `tm_scale_` prefix.
The most important ones are `tm_scale_intervals()`, `tm_scale_continuous()`, and `tm_scale_categorical()`.
```{r}
#| eval: false
#| echo: false
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_intervals())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_continuous())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_categorical())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_continuous_log())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_continuous_log1p())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_discrete())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_ordinal())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_rgb())
tm_shape(nz) + tm_fill(fill = "Land_area", fill.scale = tm_scale_continuous())
tm_shape(nz) + tm_symbols(size = "Land_area", size.scale = tm_scale_intervals())
```
\index{tmap (package)!interval scale}
The `tm_scale_intervals()` function splits the input data values into a set of intervals.
In addition to manually setting `breaks`, **tmap** allows users to specify algorithms to create breaks with the `style` argument automatically.
The default is `tm_scale_intervals(style = "pretty")`, which rounds breaks into whole numbers where possible and spaces them evenly.
Other options are listed below and presented in Figure \@ref(fig:break-styles).
- `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
```{block2 break-style-note, type='rmdnote'}
Although `style` is an argument of **tmap** functions, in fact it originates as an argument in `classInt::classIntervals()` --- see the help page of this function for details.
```
```{r break-styles, message=FALSE, fig.cap="Different interval scale methods set using the style argument in tmap.", fig.scap="Different binning methods using tmap.", echo=FALSE, warning=FALSE, fig.width=8}
source("code/09-break-styles.R", print.eval = TRUE)
```
\index{tmap (package)!continuous scale}
The `tm_scale_continuous()` function presents a continuous color field and is particularly suited for continuous rasters (Figure \@ref(fig:concat), left panel).
In case of variables with q skewed distribution, you can also use its variants -- `tm_scale_continuous_log()` and `tm_scale_continuous_log1p()`.
\index{tmap (package)!categorical scale}
Finally, `tm_scale_categorical()` was designed to represent categorical values and ensures that each category receives a unique color (Figure \@ref(fig:concat), right panel).
```{r concat, message=FALSE, fig.cap="Continuous and categorical scales in tmap.", echo=FALSE, fig.width=8}
library(tmap)
library(spData)
library(spDataLarge)
m_cont1 = tm_shape(nz) +
tm_polygons(fill = "Median_income", fill.scale = tm_scale_continuous(n = 5)) +
tm_title('tm_scale_continuous()', fontfamily = "monospace") +
tm_layout(legend.position = tm_pos_auto_in(), scale = 0.9)
m_cat1 = tm_shape(nz) +
tm_polygons(fill = "Island", fill.scale = tm_scale_categorical()) +
tm_title('tm_scale_categorical()', fontfamily = "monospace") +
tm_layout(legend.position = tm_pos_auto_in(), scale = 0.9)
tmap_arrange(m_cont1, m_cat1)
```
\index{tmap (package)!palettes}
Palettes define the color ranges associated with the bins and determined by the `tm_scale_*()` functions, and its `breaks` and `n` arguments described above.
It expects a vector of colors or a new color palette name, which can be found interactively with `cols4all::c4a_gui()`.
You can also add a `-` as the color palette name prefix to reverse the palette order.
```{block2 visual-vars-values, type='rmdnote'}
All of the default `values` of the visual variables, such as default color palettes for different types of input variables, can be found with `tmap_options()`.
For example, run `tmap_options()$values.var`.
```
\index{color palettes}
There are three main groups of color palettes\index{map-making!color palettes}: categorical, sequential and diverging (Figure \@ref(fig:colpal)), and each of them serves a different purpose.^[
A fourth group of color palettes, called bivariate, also exists.
They are used when we want to represent relations between two variables on one map.
]
Categorical palettes consist of easily distinguishable colors and are most appropriate for categorical data without any particular order such as state names or land cover classes.
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 uninterpretable.^[`fill = "MAP_COLORS"` can be used in maps with a large number of individual polygons (for example, a map of individual countries) to create unique fill colors for adjacent polygons.]
The second group is sequential palettes.
These 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 palettes can be single (`greens` goes from light to dark green, for example) or multi-color/hue (`yl_gn_bu` is gradient from light yellow to blue via green, for example), as demonstrated in the code chunk below --- output not shown, run the code yourself to see the results!
```{r 08-mapping-13, eval=FALSE}
tm_shape(nz) +
tm_polygons("Median_income", fill.scale = tm_scale(values = "greens"))
tm_shape(nz) +
tm_polygons("Median_income", fill.scale = tm_scale(values = "yl_gn_bu"))
```
The third group, diverging palettes, typically range between three distinct colors (purple-white-green in Figure \@ref(fig:colpal)) 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.
The reference point's value can be adjusted in **tmap** using the `midpoint` argument.
```{r 08-mapping-13b, eval=FALSE}
tm_shape(nz) +
tm_polygons("Median_income",
fill.scale = tm_scale_continuous(values = "pu_gn_div",
midpoint = 28000))
```
```{r colpal, echo=FALSE, message=FALSE, fig.cap="Examples of categorical, sequential and diverging palettes.", out.width="75%"}
library(cols4all)
many_palette_plotter = function(color_names, n, titles){
n_colors = length(color_names)
ylim = c(0, n_colors)
par(mar = c(0, 5, 0, 0))
plot(1, 1, xlim = c(0, max(n)), ylim = ylim,
type = "n", axes = FALSE, bty = "n", xlab = "", ylab = "")
for(i in seq_len(n_colors)){
one_color = cols4all::c4a(n = n, palette = color_names[i])
rect(xleft = 0:(n - 1), ybottom = i - 1, xright = 1:n, ytop = i - 0.2,
col = one_color, border = "light gray")
}
text(rep(-0.1, n_colors), (1: n_colors) - 0.6, labels = titles, xpd = TRUE, adj = 1)
}
all_default_pals = tmap_options()$values.var$fill
many_palette_plotter(c(all_default_pals$div, all_default_pals$seq, all_default_pals$unord), 7,
titles = c("Diverging", "Sequential", "Categorical"))
```
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.^[See the "Color vision" options and the "Color Blind Friendliness" panel in `cols4all::c4a_gui()`.]
### Legends
\index{tmap (package)!legends}
After we decided on our visual variable and its properties, we should move our attention toward the related map legend style.
Using the `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 `expression()` to create superscript text):
```{r 08-mapping-11}
#| eval: false
legend_title = expression("Area (km"^2*")")
tm_shape(nz) +
tm_polygons(fill = "Land_area", fill.legend = tm_legend(title = legend_title))
```
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.
```{r}
#| eval: false
tm_shape(nz) +
tm_polygons(fill = "Land_area",
fill.legend = tm_legend(title = legend_title,
orientation = "landscape",
position = tm_pos_out("center", "bottom")))
```
```{r}
#| eval: false
#| echo: false
legend_title = expression("Area (km"^2*")")
map_nza = tm_shape(nz) +
tm_polygons(fill = "Land_area", fill.legend = tm_legend(title = legend_title), position = tm_pos_out("right", "top"))
map_nza2 = tm_shape(nz) +
tm_polygons(fill = "Land_area",
fill.legend = tm_legend(title = legend_title,
orientation = "landscape",
position = tm_pos_out("center", "bottom")))
tmap_arrange(map_nza, map_nza2)
```
The legend position (and also the position of several other map elements in **tmap**) can be customized using one of a few functions.
The two most important are:
- `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"`)
- `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.
### Layouts
\index{tmap (package)!layouts}
The map layout refers to the combination of all map elements into a cohesive map.
Map elements include among others the objects to be mapped, the map grid, the scale bar, the title, and margins, while 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.
Additional map elements such as graticules \index{tmap (package)!graticules}, north arrows\index{tmap (package)!north arrows}, scale bars\index{tmap (package)!scale bars} and map titles have their own functions: `tm_graticules()`, `tm_compass()`, `tm_scalebar()`, and `tm_title()` (Figure \@ref(fig:na-sb)).^[Another additional map elements include `tm_grid()`, `tm_logo()` and `tm_credits()`.]
```{r na-sb, message=FALSE, fig.cap="Map with additional elements: a north arrow and scale bar.", out.width="65%", fig.asp=1, fig.scap="Map with a north arrow and scale bar."}
map_nz +
tm_graticules() +
tm_compass(type = "8star", position = c("left", "top")) +
tm_scalebar(breaks = c(0, 100, 200), text.size = 1, position = c("left", "top")) +
tm_title("New Zealand")
```
**tmap** also allows a wide variety of layout settings to be changed, some of which, produced using the following code (see `args(tm_layout)` or `?tm_layout` for a full list), are illustrated in Figure \@ref(fig:layout1).
```{r 08-mapping-14, eval=FALSE}
map_nz + tm_layout(scale = 4)
map_nz + tm_layout(bg.color = "lightblue")
map_nz + tm_layout(frame = FALSE)
```
```{r layout1, message=FALSE, fig.cap="Layout options specified by (from left to right) scale, bg.color, and frame arguments.", fig.scap="Layout options specified by the tmap arguments.", echo=FALSE, fig.asp=0.56}
source("https://github.com/geocompx/geocompr/raw/main/code/09-layout1.R", print.eval = TRUE)
```
The other arguments in `tm_layout()` provide control over many more aspects of the map in relation to the canvas on which it is placed.
Here are some useful layout settings (some of which are illustrated in Figure \@ref(fig:layout2)):
- Margin settings including `inner.margin` and `outer.margin`
- Font settings controlled by `fontface` and `fontfamily`
- Legend settings including options such as `legend.show` (whether or not to show the legend) `legend.orientation`, `legend.position`, and `legend.frame`
- Frame width (`frame.lwd`) and an option to allow double lines (`frame.double.line`)
- Color settings controlling `color.sepia.intensity` (how *yellowy* the map looks) and `color.saturation` (a color-grayscale)
```{r layout2, message=FALSE, fig.cap="Selected layout options.", echo=FALSE, warning=FALSE, fig.width=8}
source("code/09-layout2.R", print.eval = TRUE)
```
### Faceted maps
\index{map-making!faceted maps}
\index{tmap (package)!faceted maps}
Faceted maps, also referred to as 'small multiples', are composed of many maps arranged side-by-side, and sometimes stacked vertically [@meulemans_small_2017].
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.
The time dimension could be represented via another *visual variable* such as color.
However, this risks cluttering the map because it will involve multiple overlapping points (cities do not tend to move over time!).
Typically all individual facets in a faceted map contain the same geometry data repeated multiple times, once for each column in the attribute data (this is the default plotting method for `sf` objects, see Chapter \@ref(spatial-class)).
However, facets can also represent shifting geometries such as the evolution of a point pattern over time.
This use case of a faceted plot is illustrated in Figure \@ref(fig:urban-facet).
```{r urban-facet, message=FALSE, fig.cap="Faceted map showing the top 30 largest urban agglomerations from 1970 to 2030 based on population projections by the United Nations.", fig.scap="Faceted map showing urban agglomerations.", fig.asp=0.5}
urb_1970_2030 = urban_agglomerations |>
filter(year %in% c(1970, 1990, 2010, 2030))
tm_shape(world) +
tm_polygons() +
tm_shape(urb_1970_2030) +
tm_symbols(fill = "black", col = "white", size = "population_millions") +
tm_facets_wrap(by = "year", nrow = 2)
```
The preceding code chunk demonstrates key features of faceted maps created using the `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 `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 \@ref(animated-maps)).
### Inset maps
\index{map-making!inset maps}
\index{tmap (package)!inset maps}
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 (Figure \@ref(fig:insetmap1)) or bringing some non-contiguous regions closer to ease their comparison (Figure \@ref(fig:insetmap2)).
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.
In the example below, we create a map of the central part of New Zealand's Southern Alps.
Our inset map will show where the main map is in relation to the whole New Zealand.
The first step is to define the area of interest, which can be done by creating a new spatial object, `nz_region`.
```{r 08-mapping-16}
nz_region = st_bbox(c(xmin = 1340000, xmax = 1450000,
ymin = 5130000, ymax = 5210000),
crs = st_crs(nz_height)) |>
st_as_sfc()
```
In the second step, we create a base-map showing New Zealand's Southern Alps area.
This is a place where the most important message is stated.
```{r 08-mapping-17}
nz_height_map = tm_shape(nz_elev, bbox = nz_region) +
tm_raster(col.scale = tm_scale_continuous(values = "YlGn"),
col.legend = tm_legend(position = c("left", "top"))) +
tm_shape(nz_height) + tm_symbols(shape = 2, col = "red", size = 1) +
tm_scalebar(position = c("left", "bottom"))
```
The third step consists of the inset map creation.
It gives a context and helps to locate the area of interest.
Importantly, this map needs to clearly indicate the location of the main map, for example by stating its borders.
```{r 08-mapping-18}
nz_map = tm_shape(nz) + tm_polygons() +
tm_shape(nz_height) + tm_symbols(shape = 2, col = "red", size = 0.1) +
tm_shape(nz_region) + tm_borders(lwd = 3) +
tm_layout(bg.color = "lightblue")
```
One of the main differences between regular charts (e.g., scatterplots) and maps is that the input data determine the aspect ratio of maps.
Thus, in this case, we need to calculate the aspect ratios of our two main datasets, `nz_region` and `nz`.
The following function, `norm_dim()` returns the normalized width (`"w"`) and height (`"h"`) of the object (as `"snpc"` units understood by the graphic device).
```{r, message=FALSE}
library(grid)
norm_dim = function(obj){
bbox = st_bbox(obj)
width = bbox[["xmax"]] - bbox[["xmin"]]
height = bbox[["ymax"]] - bbox[["ymin"]]
w = width / max(width, height)
h = height / max(width, height)
return(unit(c(w, h), "snpc"))
}
main_dim = norm_dim(nz_region)
ins_dim = norm_dim(nz)
```
Next, knowing the aspect ratios, we need to specify the sizes and locations of our two maps -- the main map and the inset map -- using the `viewport()` function.
A viewport is part of a graphics device we use to draw the graphical elements at a given moment.
The viewport of our main map is just the representation of its aspect ratio.
```{r}
main_vp = viewport(width = main_dim[1], height = main_dim[2])
```
On the other hand, the viewport of the inset map needs to specify its size and location.
Here, we would make the inset map twice smaller as the main one by multiplying the width and height by 0.5, and we will locate it 0.5 cm from the bottom right of the main map frame.
```{r}
ins_vp = viewport(width = ins_dim[1] * 0.5, height = ins_dim[2] * 0.5,
x = unit(1, "npc") - unit(0.5, "cm"), y = unit(0.5, "cm"),
just = c("right", "bottom"))
```
Finally, we combine the two maps by creating a new, blank canvas, printing out the main map, and then placing the inset map inside of the main map viewport.
```{r insetmap1, message=FALSE, fig.cap="Inset map providing a context -- location of the central part of the Southern Alps in New Zealand.", fig.scap="Inset map providing a context.", fig.width=9}
grid.newpage()
print(nz_height_map, vp = main_vp)
pushViewport(main_vp)
print(nz_map, vp = ins_vp)
```
Inset maps can be saved to file either by using a graphic device (see Section \@ref(visual-outputs)) or the `tmap_save()` function and its arguments: `insets_tm` and `insets_vp`.
Inset maps are also used to create one map of non-contiguous areas.
Probably, the most often used example is a map of the United States, which consists of the contiguous United States, Hawaii and Alaska.
It is very important to find the best projection for each individual inset in these types of cases (see Chapter \@ref(reproj-geo-data) to learn more).
We can use US National Atlas Equal Area for the map of the contiguous United States by putting its EPSG code in the `crs` argument of `tm_shape()`.
```{r 08-mapping-19}
us_states_map = tm_shape(us_states, crs = "EPSG:9311") +
tm_polygons() +
tm_layout(frame = FALSE)
```
The rest of our objects, `hawaii` and `alaska`, already have proper projections; therefore, we just need to create two separate maps:
```{r 08-mapping-20}
hawaii_map = tm_shape(hawaii) +
tm_polygons() +
tm_title("Hawaii") +
tm_layout(frame = FALSE, bg.color = NA,
title.position = c("LEFT", "BOTTOM"))
alaska_map = tm_shape(alaska) +
tm_polygons() +
tm_title("Alaska") +
tm_layout(frame = FALSE, bg.color = NA)
```
The final map is created by combining, resizing and arranging these three maps:
```{r insetmap2, message=FALSE, fig.cap="Map of the United States.", warning=FALSE}
us_states_map
print(hawaii_map, vp = grid::viewport(0.35, 0.1, width = 0.2, height = 0.1))
print(alaska_map, vp = grid::viewport(0.15, 0.15, width = 0.3, height = 0.3))
```
The code presented above is compact and can be used as the basis for other inset maps, but the results, in Figure \@ref(fig:insetmap2), provide a poor representation of the locations and sizes of Hawaii and Alaska.
For a more in-depth approach, see the [`us-map`](https://geocompx.github.io/geocompkg/articles/us-map.html) vignette from the **geocompkg**.
## Animated maps
\index{map-making!animated maps}
\index{tmap (package)!animated maps}
Faceted maps, described in Section \@ref(faceted-maps), 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.
There are several ways to generate animations in R, including with animation packages such as **gganimate**, which builds on **ggplot2** (see Section \@ref(other-mapping-packages)).
This section focuses on creating animated maps with **tmap** because its syntax will be familiar from previous sections and the flexibility of the approach.
Figure \@ref(fig:urban-animated) is a simple example of an animated map.
Unlike the faceted plot, it does not squeeze multiple maps into a single screen and allows the reader to see how the spatial distribution of the world's most populous agglomerations evolve over time (see the book's website for the animated version).
```{r urban-animated, message=FALSE, fig.cap="Animated map showing the top 30 largest urban agglomerations from 1950 to 2030 based on population projects by the United Nations. Animated version available online at: r.geocompx.org.", fig.scap="Animated map showing the top 30 largest 'urban agglomerations'.", echo=FALSE, fig.height=3.3}
if (knitr::is_latex_output()){
knitr::include_graphics("images/urban-animated.png")
} else if (knitr::is_html_output()){
knitr::include_graphics("images/urban-animated.gif")
}
```
```{r 08-mapping-21, echo=FALSE, eval=FALSE}
source("https://github.com/geocompx/geocompr/raw/main/code/09-urban-animation.R")
```
The animated map illustrated in Figure \@ref(fig:urban-animated) can be created using the same **tmap** techniques that generate faceted maps, demonstrated in Section \@ref(faceted-maps).
There are two differences, however, related to arguments in `tm_facets_wrap()`:
- `nrow = 1, ncol = 1` are added to keep one moment in time as one layer
- `free.coords = FALSE`, which maintains the map extent for each map iteration
These additional arguments are demonstrated in the subsequent code chunk^[There is also a shortcut for this approach: `tm_facets_pagewise()`.]:
```{r 08-mapping-22}
urb_anim = tm_shape(world) + tm_polygons() +
tm_shape(urban_agglomerations) + tm_symbols(size = "population_millions") +
tm_facets_wrap(by = "year", nrow = 1, ncol = 1, free.coords = FALSE)
```
The resulting `urb_anim` represents a set of separate maps for each year.
The final stage is to combine them and save the result as a `.gif` file with `tmap_animation()`.
The following command creates the animation illustrated in Figure \@ref(fig:urban-animated), with a few elements missing, that we will add during the exercises:
```{r 08-mapping-23, eval=FALSE}
tmap_animation(urb_anim, filename = "urb_anim.gif", delay = 25)
```
Another illustration of the power of animated maps is provided in Figure \@ref(fig:animus).
This shows the development of states in the United States, which first formed in the east and then incrementally to the west and finally into the interior.
Code to reproduce this map can be found in the script `code/09-usboundaries.R` in the book GitHub repository.
```{r 08-mapping-24, echo=FALSE, eval=FALSE}
source("https://github.com/geocompx/geocompr/raw/main/code/09-usboundaries.R")
```
```{r animus, echo=FALSE, message=FALSE, fig.cap="Animated map showing population growth, state formation and boundary changes in the United States, 1790-2010. Animated version available online at r.geocompx.org.", fig.scap="Animated map showing boundary changes in the United States."}
u_animus_html = "https://user-images.githubusercontent.com/1825120/38543030-5794b6f0-3c9b-11e8-9da9-10ec1f3ea726.gif"
u_animus_pdf = "images/animus.png"
if (knitr::is_latex_output()){
knitr::include_graphics(u_animus_pdf)
} else if (knitr::is_html_output()){
knitr::include_graphics(u_animus_html)
}
```
## Interactive maps
\index{map-making!interactive maps}
\index{tmap (package)!interactive maps}
While static and animated maps can enliven geographic datasets, interactive maps can take them to a new level.
Interactivity can take many forms, the most common and useful of which is the ability to pan around and zoom into any part of a geographic dataset overlaid on a 'web map' to show context.
Less advanced interactivity levels include pop-ups which appear when you click on different features, a kind of interactive label.
More advanced levels of interactivity include the ability to tilt and rotate maps, as demonstrated in the **mapdeck** example below, and the provision of "dynamically linked" sub-plots which automatically update when the user pans and zooms [@pezanowski_senseplace3_2018].
The most important type of interactivity, however, is the display of geographic data on interactive or 'slippy' web maps.
The release of the **leaflet** package in 2015 (that uses the leaflet JavaScript library) revolutionized interactive web map creation from within R, and a number of packages have built on these foundations adding new features (e.g., **leaflet.extras2**) and making the creation of web maps as simple as creating static maps (e.g., **mapview** and **tmap**).
This section illustrates each approach in the opposite order.
We will explore how to make slippy maps with **tmap** (the syntax of which we have already learned), **mapview**\index{mapview (package)}, **mapdeck**\index{mapdeck (package)} and finally **leaflet**\index{leaflet (package)} (which provides low-level control over interactive maps).
A unique feature of **tmap** mentioned in Section \@ref(static-maps) 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_mode("view")`.
This is demonstrated in the code below, which creates an interactive map of New Zealand based on the `tmap` object `map_nz`, created in Section \@ref(map-obj), and illustrated in Figure \@ref(fig:tmview):
```{r 08-mapping-25, eval=FALSE}
tmap_mode("view")
map_nz
```
```{r tmview, message=FALSE, fig.cap="Interactive map of New Zealand created with tmap in view mode. Interactive version available online at: r.geocompx.org.", fig.scap="Interactive map of New Zealand.", echo=FALSE}
if (knitr::is_latex_output()){
knitr::include_graphics("images/tmview-1.png")
} else if (knitr::is_html_output()){
# tmap_mode("view")
# m_tmview = map_nz
# tmap_save(m_tmview, "tmview-1.html")
# file.copy("tmview-1.html", "../geocompx.org/static/img/tmview-1.html")
knitr::include_url("https://geocompx.org/static/img/tmview-1.html")
}
```
Now that the interactive mode has been 'turned on', all maps produced with **tmap** will launch (another way to create interactive maps is with the `tmap_leaflet()` function).
Notable features of this interactive mode include the ability to specify the basemap with `tm_basemap()` (or `tmap_options()`) as demonstrated below (result not shown):
```{r 08-mapping-26, eval=FALSE}
map_nz + tm_basemap(server = "OpenTopoMap")
```
An impressive and little-known feature of **tmap**'s view mode is that it also works with faceted plots.
The argument `sync` in `tm_facets_wrap()` can be used in this case to produce multiple maps with synchronized zoom and pan settings, as illustrated in Figure \@ref(fig:sync), which was produced by the following code:
```{r 08-mapping-27, eval=FALSE}
world_coffee = left_join(world, coffee_data, by = "name_long")
facets = c("coffee_production_2016", "coffee_production_2017")
tm_shape(world_coffee) + tm_polygons(facets) +
tm_facets_wrap(nrow = 1, sync = TRUE)
```
```{r sync, message=FALSE, fig.cap="Faceted interactive maps of global coffee production in 2016 and 2017 in sync, demonstrating tmap's view mode in action.", fig.scap="Faceted interactive maps of global coffee production.", echo=FALSE}
knitr::include_graphics("images/interactive-facets.png")
```
Switch **tmap** back to plotting mode with the same function:
```{r 08-mapping-28}
tmap_mode("plot")
```
If you are not proficient with **tmap**, the quickest way to create interactive maps in R may be with **mapview**\index{mapview (package)}.
The following 'one liner' is a reliable way to interactively explore a wide range of geographic data formats:
```{r 08-mapping-29, eval=FALSE}
mapview::mapview(nz)
```
```{r mapview, message=FALSE, fig.cap="Illustration of mapview in action.", echo=FALSE}
knitr::include_graphics("images/mapview.png")
# knitr::include_graphics("https://user-images.githubusercontent.com/1825120/39979522-e8277398-573e-11e8-8c55-d72c6bcc58a4.png")
# mv = mapview::mapview(nz)
# mv@map
```
**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\index{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** (Figure \@ref(fig:mapview2)).
```{r 08-mapping-30, eval=FALSE}
library(mapview)
oberfranken = subset(franconia, district == "Oberfranken")
trails |>
st_transform(st_crs(oberfranken)) |>
st_intersection(oberfranken) |>
st_collection_extract("LINESTRING") |>
mapview(color = "red", lwd = 3, layer.name = "trails") +
mapview(franconia, zcol = "district") +
breweries
```
```{r mapview2, message=FALSE, fig.cap="Using mapview at the end of an sf-based pipe expression.", echo=FALSE, warning=FALSE}
knitr::include_graphics("images/mapview-example.png")
# knitr::include_graphics("https://user-images.githubusercontent.com/1825120/39979271-5f515256-573d-11e8-9ede-e472ca007d73.png")
```
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 libraries (`"leafgl"` and `"mapdeck"`) in the `mapviewOptions()`.^[You may also try to use `mapviewOptions(georaster = TRUE)` for more performant visualizations of large raster data.]
For further information on **mapview**, see the package's website at: [r-spatial.github.io/mapview/](https://r-spatial.github.io/mapview/articles/).
There are other ways to create interactive maps with R.
The **googleway** package\index{googleway (package)}, for example, provides an interactive mapping interface that is flexible and extensible
(see the [`googleway-vignette`](https://cran.r-project.org/package=googleway/vignettes/googleway-vignette.html) for details).
Another approach by the same author is **[mapdeck](https://github.com/SymbolixAU/mapdeck)**, which provides access to Uber's `Deck.gl` framework\index{mapdeck (package)}.
Its use of WebGL enables it to interactively visualize large datasets up to millions of points.
The package uses Mapbox [access tokens](https://docs.mapbox.com/help/getting-started/access-tokens/), which you must register for before using the package.
```{block2 08-mapping-31, type='rmdnote'}
Note that the following block assumes the access token is stored in your R environment as `MAPBOX=your_unique_key`.
This can be added with `usethis::edit_r_environ()`.
```
A unique feature of **mapdeck** is its provision of interactive 2.5D perspectives, illustrated in Figure \@ref(fig:mapdeck).
This means you can can pan, zoom and rotate around the maps, and view the data 'extruded' from the map.
Figure \@ref(fig:mapdeck), generated by the following code chunk, visualizes road traffic crashes in the UK, with bar height representing casualties per area.
```{r 08-mapping-32, engine='zsh', echo=FALSE, eval=FALSE}
https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv
curl -i https://git.io -F "url=https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv" \
-F "code=geocompr-mapdeck"
```
```{r 08-mapping-33, eval=FALSE}
library(mapdeck)
set_token(Sys.getenv("MAPBOX"))
crash_data = read.csv("https://git.io/geocompr-mapdeck")
crash_data = na.omit(crash_data)
ms = mapdeck_style("dark")
mapdeck(style = ms, pitch = 45, location = c(0, 52), zoom = 4) |>
add_grid(data = crash_data, lat = "lat", lon = "lng", cell_size = 1000,
elevation_scale = 50, colour_range = hcl.colors(6, "plasma"))
```
```{r mapdeck, echo=FALSE, fig.cap="Map generated by mapdeck, representing road traffic casualties across the UK. Height of 1-km cells represents number of crashes.", fig.scap="Map generated by mapdeck."}
knitr::include_graphics("images/mapdeck-mini.png")
```
You can zoom and drag the map in the browser, in addition to rotating and tilting it when pressing `Cmd`/`Ctrl`.
Multiple layers can be added with the pipe operator, as demonstrated in the [`mapdeck` vignettes](https://cran.r-project.org/package=mapdeck/vignettes/mapdeck.html).
**mapdeck** also supports `sf` objects, as can be seen by replacing the `add_grid()` function call in the preceding code chunk with `add_polygon(data = lnd, layer_id = "polygon_layer")`, to add polygons representing London to an interactive tilted map.
```{r 08-mapping-35, eval=FALSE, echo=FALSE}
library(mapdeck)
set_token(Sys.getenv("MAPBOX"))
df = read.csv("https://git.io/geocompr-mapdeck")
ms = mapdeck_style('dark')
mapdeck(style = ms, pitch = 45, location = c(0, 52), zoom = 4) |>
add_polygon(data = lnd, layer_id = "polygon_layer")
```
Last is **leaflet** which is the most mature and widely used interactive mapping package in R\index{leaflet (package)}.
**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 (see [leafletjs.com](https://leafletjs.com/)).
Leaflet maps are created with `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 which generates Figure \@ref(fig:leaflet) (see [rstudio.github.io/leaflet/](https://rstudio.github.io/leaflet/) for details).
```{r leaflet-code, echo=TRUE, eval=FALSE}
pal = colorNumeric("RdYlBu", domain = cycle_hire$nbikes)
leaflet(data = cycle_hire) |>
addProviderTiles(providers$CartoDB.Positron) |>
addCircles(col = ~pal(nbikes), opacity = 0.9) |>
addPolygons(data = lnd, fill = FALSE) |>
addLegend(pal = pal, values = ~nbikes) |>
setView(lng = -0.1, 51.5, zoom = 12) |>
addMiniMap()
```
```{r leaflet, message=FALSE, fig.cap="The leaflet package in action, showing cycle hire points in London. See interactive version [online](https://geocompr.github.io/img/leaflet.html).", fig.scap="The leaflet package in action.", echo=FALSE}
if (knitr::is_latex_output() || knitr::is_html_output()){
knitr::include_graphics("images/leaflet-1.png")
} else {
# pre-generated for https://github.com/ropensci/stplanr/issues/385
# pal = colorNumeric("RdYlBu", domain = cycle_hire$nbikes)
# m = leaflet(data = cycle_hire) |>
# addProviderTiles(providers$CartoDB.Positron) |>
# addCircles(col = ~pal(nbikes), opacity = 0.9) |>
# addPolygons(data = lnd, fill = FALSE) |>
# addLegend(pal = pal, values = ~nbikes) |>
# setView(lng = -0.1, 51.5, zoom = 12) |>
# addMiniMap()
# htmlwidgets::saveWidget(m, "leaflet.html")
# browseURL("leaflet.html")
# file.rename("leaflet.html", "~/geocompr/geocompr.github.io/static/img/leaflet.html")
# abort old way of including - mixed content issues
knitr::include_url("https://geocompr.github.io/img/leaflet.html")
}
```
## Mapping applications
\index{map-making!mapping applications}
The interactive web maps demonstrated in Section \@ref(interactive-maps) 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 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
Overcoming these limitations involves going beyond static web mapping and toward geospatial frameworks and map servers.
Products in this field include [GeoDjango](https://docs.djangoproject.com/en/4.0/ref/contrib/gis/)\index{GeoDjango} (which extends the Django web framework and is written in [Python](https://github.com/django/django))\index{Python}, [MapServer](https://github.com/mapserver/mapserver)\index{MapServer} (a framework for developing web applications, largely written in C and C++)\index{C++} and [GeoServer](https://github.com/geoserver/geoserver) (a mature and powerful map server written in Java\index{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](https://wiki.gis.com/wiki/index.php/Database_administrator)).
Fortunately for R programmers, web mapping applications can now be rapidly created with **shiny**.\index{shiny (package)}
As described in the open source book [Mastering Shiny](https://mastering-shiny.org/), **shiny** is an R package and framework for converting R code into interactive web applications [@wickham_mastering_2021].
You can embed interactive maps in shiny apps thanks to functions such as <!--`tmap::renderTmap()` and -->[`leaflet::renderLeaflet()`](https://rstudio.github.io/leaflet/shiny.html).
This section gives some context, teaches the basics of **shiny** from a web mapping perspective, and culminates in a full-screen mapping application in less than 100 lines of code.
**shiny** is well documented at [shiny.posit.co](https://shiny.posit.co/), which highlights the two components of every **shiny** app: 'front end' (the bit the user sees) and 'back end' code.
In **shiny** apps, these elements are typically created in objects named `ui` and `server` within an R script named `app.R`, which lives in an 'app folder'.
This allows web mapping applications to be represented in a single file, such as the [`CycleHireApp/app.R`](https://github.com/geocompx/geocompr/blob/main/apps/CycleHireApp/app.R) file in the book's GitHub repo.
```{block2 shiny, type = 'rmdnote'}
In **shiny** apps these are often split into `ui.R` (short for user interface) and `server.R` files, naming conventions used by `shiny-server`, a server-side Linux application for serving shiny apps on public-facing websites.
`shiny-server` also serves apps defined by a single `app.R` file in an 'app folder'.
Learn more at: https://github.com/rstudio/shiny-server.
```
Before considering large apps, it is worth seeing a minimal example, named 'lifeApp', in action.^[
The word 'app' in this context refers to 'web application' and should not be confused with smartphone apps, the more common meaning of the word.
]
The code below defines and launches --- with the command `shinyApp()` --- a lifeApp, which provides an interactive slider allowing users to make countries appear with progressively lower levels of life expectancy (see Figure \@ref(fig:lifeApp)):
```{r 08-mapping-37, eval=FALSE}
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 = world[world$lifeExp < input$life, ])})
}
shinyApp(ui, server)
```
```{r lifeApp, echo=FALSE, message=FALSE, fig.cap="Screenshot showing minimal example of a web mapping application created with shiny.", fig.scap="Minimal example of a web mapping application."}
# knitr::include_graphics("https://user-images.githubusercontent.com/1825120/39690606-8f9400c8-51d2-11e8-84d7-f4a66a477d2a.png")
knitr::include_graphics("images/shiny-app.png")
```