Creating a London Population Map with D3po

[This article was first published on pacha.dev/blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

If this post is useful to you I kindly ask a minimal donation on Buy Me a Coffee. It shall be used to continue my Open Source efforts.

You can send me questions for the blog using this form and subscribe to receive an email when there is a new post.

I got this badly worded question for the blog: “Please clarify. This two are not working. From which package is po_tooltip. Thanks.”

I appreciate the thanks, but remember the help me to help you rule: “Please provide a minimal, reproducible example when asking for help.” Or at least explain what your question is about.

Based on the question, it is from the Gini Index post.

I will try to explain by creating a population map of London using the D3po package.

All the po_*() functions are part of the D3po package, including po_tooltip().

Load these R packages to import and manipulate the data:

library(d3po)
library(dplyr)
library(sf)
library(rvest)
library(janitor)

There is a better resolution map of London boroughs provided by TfL. This map looks better compared to D3po provideds subnational map (low resolution).

Download the GeoJSON file to show that D3po can work with any spatial data in sf format.

url <- "https://hub.arcgis.com/api/v3/datasets/0a92a355a8094e0eb20a7a66cf4ca7cf_10/downloads/data?format=geojson&spatialRefId=4326&where=1%3D1"
finp <- "~/Documents/blog-materials/2025/11/14/london-population/london_boroughs.geojson"

if (!file.exists(finp)) {
  download.file(url, destfile = finp, mode = "wb")
}

Read the GeoJSON file using st_read() from the sf package and clean the column names with clean_names() from the janitor package:

boroughs <- st_read(finp) %>%
  clean_names()
Reading layer `London_Boroughs' from data source 
  `/home/pacha/Documents/blog-materials/2025/11/14/london-population/london_boroughs.geojson' 
  using driver `GeoJSON'
Simple feature collection with 33 features and 13 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -0.5103558 ymin: 51.28676 xmax: 0.3340441 ymax: 51.69188
Geodetic CRS:  WGS 84
boroughs
Simple feature collection with 33 features and 13 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -0.5103558 ymin: 51.28676 xmax: 0.3340441 ymax: 51.69188
Geodetic CRS:  WGS 84
First 10 features:
          borough number code   hectares                 descript0      x
1         Bromley     19 00AF 15014.5152 CIVIL ADMINISTRATION AREA 542896
2        Lewisham     07 00AZ  3532.3405 CIVIL ADMINISTRATION AREA 537667
3      Wandsworth     10 00BJ  3522.0032 CIVIL ADMINISTRATION AREA 526129
4          Merton     22 00BA  3760.9196 CIVIL ADMINISTRATION AREA 525475
5       Redbridge     14 00BC  5645.0083 CIVIL ADMINISTRATION AREA 543914
6          Barnet     30 00AC  8673.7261 CIVIL ADMINISTRATION AREA 524028
7  City of London     00 00AA   315.2813 CIVIL ADMINISTRATION AREA 532464
8          Sutton     21 00BF  4385.0950 CIVIL ADMINISTRATION AREA 526976
9       Southwark     08 00BE  2989.7206 CIVIL ADMINISTRATION AREA 533855
10         Ealing     27 00AJ  5552.7807 CIVIL ADMINISTRATION AREA 515888
        y area objectid                file_name shape_area shape_length
1  165656    0        1 GREATER_LONDON_AUTHORITY  150145152    75909.143
2  174002    0        2 GREATER_LONDON_AUTHORITY   35323405    40992.749
3  174114    0        3 GREATER_LONDON_AUTHORITY   35220032    37353.847
4  169422    0        4 GREATER_LONDON_AUTHORITY   37609196    32293.920
5  189463    0        5 GREATER_LONDON_AUTHORITY   56450083    45688.184
6  192316    0        6 GREATER_LONDON_AUTHORITY   86737261    50866.472
7  181220    0        7 GREATER_LONDON_AUTHORITY    3152813     9651.891
8  164132    0        8 GREATER_LONDON_AUTHORITY   43850950    39753.841
9  176787    0        9 GREATER_LONDON_AUTHORITY   29897206    33664.416
10 181715    0       10 GREATER_LONDON_AUTHORITY   55527807    47268.052
                              global_id                       geometry
1  86b54395-dc20-4e52-bc8c-7b79188f035f POLYGON ((0.01215857 51.299...
2  84f88d72-30c1-47b6-bc71-163246413f0d POLYGON ((-0.04613459 51.45...
3  617038fb-5459-4133-96ae-1743ba48321c POLYGON ((-0.2540111 51.437...
4  6362d58b-7052-4655-a812-3487752e770e POLYGON ((-0.2181117 51.380...
5  3d6ac4f8-be2c-4a89-a684-e50e11c602e9 POLYGON ((0.02036894 51.556...
6  e424d66c-44fe-4e0e-b58b-d1851cfe71b1 POLYGON ((-0.1998716 51.670...
7  417f3ab6-bfb7-4770-827f-80526d360a25 POLYGON ((-0.1115267 51.515...
8  6dfcbe71-9609-488c-8852-df930b685ada POLYGON ((-0.1442461 51.339...
9  161208ac-2edc-471f-81dc-ec7e494032e5 POLYGON ((-0.07829781 51.42...
10 4d06499b-5d29-43e8-a4fa-c32be7b23119 POLYGON ((-0.3354244 51.496...

Extract the borough names:

names1 <- pull(boroughs, borough)

names1
 [1] "Bromley"              "Lewisham"             "Wandsworth"          
 [4] "Merton"               "Redbridge"            "Barnet"              
 [7] "City of London"       "Sutton"               "Southwark"           
[10] "Ealing"               "Brent"                "Croydon"             
[13] "Richmond upon Thames" "Hillingdon"           "Haringey"            
[16] "Kensington & Chelsea" "Kingston upon Thames" "Waltham Forest"      
[19] "Barking & Dagenham"   "Newham"               "Enfield"             
[22] "Hammersmith & Fulham" "Havering"             "Greenwich"           
[25] "Hackney"              "Westminster"          "Camden"              
[28] "Tower Hamlets"        "Hounslow"             "Harrow"              
[31] "Bexley"               "Islington"            "Lambeth"             

Now we need the population for the 33 boroughs. CityPopulation.DE has a table we can read using rvest:

url_pop <- "https://www.citypopulation.de/en/uk/greaterlondon/"
finp2 <- "~/Documents/blog-materials/2025/11/14/london-population/london_population.rds"

if (file.exists(finp2)) {
  pop_table <- readRDS(finp2)
} else {
  page <- read_html(url_pop)

  tables <- page %>% html_nodes("table")

  pop_table <- tables[[1]] %>%
    html_table() %>%
    clean_names()

  pop_table <- pop_table %>%
    select(name, pop = population_estimate2024_06_30)

  pop_table <- pop_table %>%
    mutate(pop = as.numeric(gsub(",", "", pop)))

  names2 <- pull(pop_table, name)

  names2

  # names that do not match
  setdiff(names1, names2)
  setdiff(names2, names1)

  # replace the " and " with " & " in boroughs
  # replace "City of Westminster" with "Westminster"
  pop_table <- pop_table %>%
    mutate(borough = case_when(
      name == "City of Westminster" ~ "Westminster",
      grepl(" and ", name) ~ gsub(" and ", " & ", name),
      TRUE ~ name
    )) %>%
    select(-name)

  saveRDS(pop_table, finp2)
}

Up to this point we can show two maps:

  1. Inhabitants per borough
  2. Inhabitants per square km
boroughs <- boroughs %>%
  left_join(pop_table, by = "borough") %>%
  mutate(
    area_km2 = hectares / 100,
    pop_per_km2 = pop / area_km2
  )

Define a color gradient for the maps a and create the maps using d3po:

my_gradient <- c("#b2d8d8", "#66b2b2", "#008080", "#006666", "#004c4c")

d3po(boroughs, width = 800, height = 600) %>%
  po_geomap(daes(group = borough, size = pop, color = my_gradient, gradient = T, tooltip = borough)) %>%
  po_labels(
    title = "Population in London Boroughs (2024)",
    subtitle = "Source: CityPopulation.DE & TFL London Boroughs"
  )
To leave a comment for the author, please follow the link and comment on their blog: pacha.dev/blog.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)