Hands-on Exercise 8.2 - Visualising Geospatial Point Data

Author

Nguyen Nguyen Ha (Summer)

Published

June 12, 2025

Modified

June 12, 2025

1. Overview

Proportional symbol maps (also called graduated symbol maps) use symbol size to represent variations in the magnitude of a discrete, rapidly changing phenomenon, such as population counts. Similar to choropleth maps, they can be classed or unclassed:

  • Classed (Range-Graded or Graduated Symbols): Symbols are grouped into predefined size categories.

  • Unclassed (Proportional Symbols): Symbol areas are directly proportional to the mapped attribute values.

In this hands-on exercise, you will learn to create a proportional symbol map in R using the tmap package to visualize the number of wins at Singapore Pools’ outlets.

2. Getting Started

The below code chunk use p_load() to load in the relevant R packages.

pacman::p_load(sf, tmap, tidyverse)

3. Geospatial Data Wrangling

3.1. The Data

This hands-on exercise use a dataset called SGPools_svy21.csv.

The dataset consists of seven columns, where the XCOORD and YCOORD columns are the x-coordinates and y-coordinates of SingPools outlets and branches in Singapore SVY21 Projected Coordinates System.

3.2. Data Import and Preparation

The code chunk below uses read_csv() function of readr package to import SGPools_svy21.csv into R as a tibble data frame.

sgpools <- read_csv("data/aspatial/SGPools_svy21.csv")

After importing the data file into R, we use list() to examine if the data file has been imported correctly.

list(sgpools) 
[[1]]
# A tibble: 306 × 7
   NAME           ADDRESS POSTCODE XCOORD YCOORD `OUTLET TYPE` `Gp1Gp2 Winnings`
   <chr>          <chr>      <dbl>  <dbl>  <dbl> <chr>                     <dbl>
 1 Livewire (Mar… 2 Bayf…    18972 30842. 29599. Branch                        5
 2 Livewire (Res… 26 Sen…    98138 26704. 26526. Branch                       11
 3 SportsBuzz (K… Lotus …   738078 20118. 44888. Branch                        0
 4 SportsBuzz (P… 1 Sele…   188306 29777. 31382. Branch                       44
 5 Prime Serango… Blk 54…   552542 32239. 39519. Branch                        0
 6 Singapore Poo… 1A Woo…   731001 21012. 46987. Branch                        3
 7 Singapore Poo… Blk 64…   370064 33990. 34356. Branch                       17
 8 Singapore Poo… Blk 88…   370088 33847. 33976. Branch                       16
 9 Singapore Poo… Blk 30…   540308 33910. 41275. Branch                       21
10 Singapore Poo… Blk 20…   560202 29246. 38943. Branch                       25
# ℹ 296 more rows

3.3. Creating a sf data frame from an aspatial data frame

The code chunk below converts sgpools data frame into a simple feature data frame using st_as_sf() of sf packages.

sgpools_sf <- st_as_sf(sgpools, 
                       coords = c("XCOORD", "YCOORD"),
                       crs= 3414)

Key Takeaways from the arguments above:

  • coords Argument: The column name for x-coordinates must be specified first, followed by the y-coordinates.

  • crs Argument: Requires the coordinate system in EPSG format. For example, EPSG:3414 represents Singapore’s SVY21 Projected Coordinate System. You can find EPSG codes for other countries at epsg.io.

The basic information of the newly created sgpools_sf can be displayed as below.

list(sgpools_sf)
[[1]]
Simple feature collection with 306 features and 5 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 7844.194 ymin: 26525.7 xmax: 45176.57 ymax: 47987.13
Projected CRS: SVY21 / Singapore TM
# A tibble: 306 × 6
   NAME                         ADDRESS POSTCODE `OUTLET TYPE` `Gp1Gp2 Winnings`
 * <chr>                        <chr>      <dbl> <chr>                     <dbl>
 1 Livewire (Marina Bay Sands)  2 Bayf…    18972 Branch                        5
 2 Livewire (Resorts World Sen… 26 Sen…    98138 Branch                       11
 3 SportsBuzz (Kranji)          Lotus …   738078 Branch                        0
 4 SportsBuzz (PoMo)            1 Sele…   188306 Branch                       44
 5 Prime Serangoon North        Blk 54…   552542 Branch                        0
 6 Singapore Pools Woodlands C… 1A Woo…   731001 Branch                        3
 7 Singapore Pools 64 Circuit … Blk 64…   370064 Branch                       17
 8 Singapore Pools 88 Circuit … Blk 88…   370088 Branch                       16
 9 Singapore Pools Anchorvale … Blk 30…   540308 Branch                       21
10 Singapore Pools Ang Mo Kio … Blk 20…   560202 Branch                       25
# ℹ 296 more rows
# ℹ 1 more variable: geometry <POINT [m]>

The output shows that sgppols_sf is in point feature class with epsg ID = 3414. The bbox provides information of the extend of the geospatial data.

4. Drawing Proportional Symbol Map

First we turn on the view mode of tmap.

tmap_mode("view")

4.1. It all started with an interactive point symbol map

The code chunks below creates an interactive point symbol map.

tm_shape(sgpools_sf)+
tm_bubbles(col = "red",
           size = 1,
           border.col = "black",
           border.lwd = 1)

4.2. Lets make it proportional

Next we need to assign a numerical variable to the size visual attribute. The code chunk below assigns Gp1Gp2Winnings size visual attribute.

tm_shape(sgpools_sf)+
tm_bubbles(col = "red",
           size = "Gp1Gp2 Winnings",
           border.col = "black",
           border.lwd = 1)

4.3. Lets give it a different colour

The proportional symbol map can be further improved by using the colour visual attribute. The code chunks below assignes OUTLET_TYPE as the colour attribute variable.

tm_shape(sgpools_sf)+
tm_bubbles(col = "OUTLET TYPE", 
          size = "Gp1Gp2 Winnings",
          border.col = "black",
          border.lwd = 1)

4.4. I have a twin brothers :)

Tmap’s view mode also works with faceted plots. The argument sync in tm_facets() can be used in this case to produce multiple maps with synchronised zoom and pan settings.

tm_shape(sgpools_sf) +
  tm_bubbles(col = "OUTLET TYPE", 
          size = "Gp1Gp2 Winnings",
          border.col = "black",
          border.lwd = 1) +
  tm_facets(by= "OUTLET TYPE",
            nrow = 1,
            sync = TRUE)

Before ending the session, we switch tmap’s view back to plot mode using the code chunk below.

tmap_mode("plot")