1. Introduction

Dataset retrieved from: https://api.data.gov.sg/v1/environment/psi

In this visualisation, we will be attempting to visualise the 24h PSI readings for Singapore. These readings are split by region in the official government sources, so initially the idea was to create a heatmap using the PSI values. However, after looking at the types of data provided, specifically point coordinates for each region, I concluded that it would be easier to generalise the PSI readings into point locations. Drafted design

The main struggle was learning to implement API and JSON reading to retrieve real-time data for visualisation purposes, as well as exploring the different libraries required to prepare the data and create maps. For example, while researching I realised I ran into a 443 https error while using the commonly-recommended RCurl library, and had to search for an alternative, httr. Understanding the functions used to generate the maps required documentation reading as well - for example, without closely looking at the allowed variables to plot the regional dots on the map, I would not have thought to convert the PSI reading data into categorical variables, instead of leaving it as ordinal variables.

2. Visualisation

Packages required for visualisation

Here, we prepare the packages required for this visualisation.

packages <- c('tidyverse', 'rjson','jsonlite','httr', 'sf', 'tmap', 'lobstr' ,"shiny")

for(p in packages){
if (!require(p,character.only = T)){
  install.packages(p)
  }
  library(p,character.only = T)
}
## Loading required package: tidyverse
## -- Attaching packages --------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.3.3     v purrr   0.3.4
## v tibble  3.0.6     v dplyr   1.0.4
## v tidyr   1.1.2     v stringr 1.4.0
## v readr   1.4.0     v forcats 0.5.1
## -- Conflicts ------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
## Loading required package: rjson
## Loading required package: jsonlite
## 
## Attaching package: 'jsonlite'
## The following objects are masked from 'package:rjson':
## 
##     fromJSON, toJSON
## The following object is masked from 'package:purrr':
## 
##     flatten
## Loading required package: httr
## Loading required package: sf
## Linking to GEOS 3.9.0, GDAL 3.2.1, PROJ 7.2.1
## Loading required package: tmap
## Loading required package: lobstr
## Loading required package: shiny
## 
## Attaching package: 'shiny'
## The following object is masked from 'package:jsonlite':
## 
##     validate

Calling PSI API and reading resulting JSON

When calling the API, we will be extracting location (longitude/latitude) information, as well as the associated PSI levels for geographical visualisation. We use unnest() to obtain separate longitude and latitude values for each region.

url = "https://api.data.gov.sg/v1/environment/psi"
url <- URLencode(url)

psi_df <- fromJSON(content(GET(url),"text"))
## No encoding supplied: defaulting to UTF-8.
psi_level <- psi_df$items$readings$psi_twenty_four_hourly
t_psi_level<-as.data.frame(t(psi_level))
query_time <- psi_df$items$update_timestamp

region <-as_tibble(psi_df$region_metadata$name)
coordinates <-psi_df$region_metadata$label_location
coordinates <- as_tibble(coordinates) %>% unnest()
## Warning: `cols` is now required when using unnest().
## Please use `cols = c()`
locations <- merge(region, coordinates, by ="row.names", all.x=TRUE)
locations$Row.names <-NULL
head(locations)
##      value latitude longitude
## 1     west  1.35735    103.70
## 2 national  0.00000      0.00
## 3     east  1.35735    103.94
## 4  central  1.35735    103.82
## 5    south  1.29587    103.82
## 6    north  1.41803    103.82

With longitudinal and latitudinal information, we need to convert them to points so that we can display it on a map. We convert the numerical psi data into categorical information as well, to be able to plot it in the map later using tm_dots() function.

locations_data <- locations %>%
  st_as_sf(coords = c("longitude","latitude")) %>% 
  mutate( latitude= st_coordinates(.)[,1],
          longitude= st_coordinates(.)[,2])
locations_sf <- st_as_sf(locations_data, coords = c("LONGITUDE", "LATITUDE"), crs = 3414)

psi_areas <- merge(x = locations_sf, y = t_psi_level,by.x="value",by=0,all=TRUE)

psi_areas$"1" = as.factor(psi_areas$"1")
psi_areas %>%
  rename(
    "psi" = "1",
    region = value)
## Simple feature collection with 6 features and 4 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 0 ymin: 0 xmax: 103.94 ymax: 1.41803
## CRS:           NA
##     region latitude longitude psi               geometry
## 1  central   103.82   1.35735  49 POINT (103.82 1.35735)
## 2     east   103.94   1.35735  54 POINT (103.94 1.35735)
## 3 national     0.00   0.00000  54            POINT (0 0)
## 4    north   103.82   1.41803  46 POINT (103.82 1.41803)
## 5    south   103.82   1.29587  49 POINT (103.82 1.29587)
## 6     west   103.70   1.35735  45  POINT (103.7 1.35735)
str(psi_areas)
## Classes 'sf' and 'data.frame':   6 obs. of  5 variables:
##  $ value    : chr  "central" "east" "national" "north" ...
##  $ latitude : Named num  104 104 0 104 104 ...
##   ..- attr(*, "names")= chr [1:6] "4" "3" "2" "6" ...
##  $ longitude: Named num  1.36 1.36 0 1.42 1.3 ...
##   ..- attr(*, "names")= chr [1:6] "4" "3" "2" "6" ...
##  $ 1        : Factor w/ 4 levels "45","46","49",..: 3 4 4 2 3 1
##  $ geometry :sfc_POINT of length 6; first list element:  'XY' num  103.82 1.36
##  - attr(*, "sf_column")= chr "geometry"
##  - attr(*, "agr")= Factor w/ 3 levels "constant","aggregate",..: NA NA NA NA
##   ..- attr(*, "names")= chr [1:4] "value" "latitude" "longitude" "1"

Plotting and visualising the map

Here, we plot the map and the relevant information is displayed in the map legend/labels. A negative scaled palette is used, so that the lighter colours indicate a a lower PSI reading/better air quality.

tmap_mode("view") +
  tm_shape(psi_areas)+tm_dots(size = .3,col="1", palette = "-viridis",popup.vars = c("Region: "="value", "24h PSI level: " = "1"),title="PSI reading")
## tmap mode set to interactive viewing
## Warning: Currect projection of shape psi_areas unknown. Long-lat (WGS84) is
## assumed.

3. Embedding the visualisation in Shiny App

ui <- fluidPage(
    tags$h2("Assignment 5"),
    titlePanel("Singapore 24h PSI readings"),
    tmapOutput("my_tmap"),
    h5(paste("Last updated: ",substr(query_time,12,19)),align='right'))
    
server <- function(input,output,session){
  output$my_tmap <- renderTmap({ 
    tm <- tm_shape(psi_areas)+tm_dots(size = .3,col="1", palette = "-viridis",popup.vars = c("Region: "="value", "24h PSI level: " = "1"),title="PSI reading")
  })
}
shinyApp(ui, server)

Shiny applications not supported in static R Markdown documents

4, Takeaways

Shiny visualisation The final visualisation is a simple map split by the 5 main regions of Singapore. By clicking on each region, we are able to view the specific PSI reading for that region. The legend and all the displayed values gives us a rough estimate of Singapore’s overall air quality. The colour-coding of the different PSI levels also lets us see, at a glance, which regions have better air quality.

From this basic visualisation, we can see that: 1. The PSI levels towards the south-eastern side of Singapore is worse, i.e. the air quality there is lower. 2. Overall, Singapore at the time of reading (6th April) has a normal air quality, although it is on the high side leaning towards moderate air quality.