Reading through spreadsheets can sometimes be boring. It is useful to be able to generate high-quality, interactive maps to display your work in R so that others can enjoy exploring it. One of the easiest ways to do this is with Leaflet. Leaflet is a package in R which allows you to quickly build interactive maps in a Java Script interface.

You don’t need to be massively confident in either R or HTML in order to make a decent map following this guide.(I have 0 experience doing any form of Computer Science/Coding/Web Development).

For this guide, I’m going to build a map which displays the results of the 2024 UK General Election.

1. Finding the Data

Firstly, we need some data on the shapes we want on the map.

I found some data on the shapes of UK Parliamentary Constituencies from the ONS Open Geography Portal (Link). These shapes are called Polygons. (Note: I’m going to avoid going into unnecessary detail in explaining how Geospatial objects work in R. It isn’t strictly necessary to understand what’s going on in this guide.)

On this website, download the geoJSON files for the map.

We also need some data on the 2024 election results. I found this data on the House of Commons Library website (Link). On this website, download the “Detailed results by constituency (csv)” file.

Save both of these files in a folder along with a new R script.

2. Loading the Data

In the R script, firstly install and load the necessary packages for this project.

I’m going to be using the Tidyverse package, as well as the sf package, the leaflet package and the htmlwidgets package. Ensure these are installed and loaded before proceeding.

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(sf)
## Linking to GEOS 3.12.2, GDAL 3.9.3, PROJ 9.4.1; sf_use_s2() is TRUE
library(leaflet)
library(htmlwidgets)

Loading the CSV should be easy by now. Use read.csv and save it as a Data Frame called details. Ignore my error message here and insert the path to your file.

details <- read.csv("path/to/your/file.csv")
## Error in file(file, "rt"): cannot open the connection

Loading the geoJSON file is just as simple. In this case, we are going to use the sf function, st_read. Use st_read to load the geoJSON file and save it as an object called boundaries. Again, ignore my error message here.

boundaries <- st_read("path/to/your/file.geojson")
## Error: Cannot open "path/to/your/file.geojson"; The file doesn't seem to exist.
## Reading layer `Westminster_Parliamentary_Constituencies_July_2024_Boundaries_UK_BUC_4872633423108313063' from data source `C:\Users\MatthewGStubbs\Desktop\Local Files\University\Year 3\EC349 Data Science for Economists\R Guides for Neil\Westminster_Parliamentary_Constituencies_July_2024_Boundaries_UK_BUC_4872633423108313063.geojson' 
##   using driver `GeoJSON'
## Simple feature collection with 650 features and 9 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -8.649996 ymin: 49.88234 xmax: 1.763706 ymax: 60.86087
## Geodetic CRS:  WGS 84

3. Building a Very Simple Map

We first want to create a very simple map to test whether our geospatial files are working properly. This can be done by using the leaflet function. Create an object called map which uses the boundaries data. Pipe this into a function called addTiles() - this will add a background to our map and can be removed if desired. Then, Pipe this into a function called addPolygons().

Call this object and it will display the map in viewer section on R studio (Note: This may take a few seconds to load).

map <- leaflet(data = boundaries)%>%
  addTiles()%>%
  addPolygons()
map

4. Adding Data to the Map

Although this map does show where all the constituencies are, it is not very useful as it does not provide any other information. We need to add data to this map.

If we examine the structure of the boundaries object and the details data frame, we observe that they both have a similar looking ID variable.

str(boundaries)
## Classes 'sf' and 'data.frame':   650 obs. of  10 variables:
##  $ FID      : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ PCON24CD : chr  "E14001063" "E14001064" "E14001065" "E14001066" ...
##  $ PCON24NM : chr  "Aldershot" "Aldridge-Brownhills" "Altrincham and Sale West" "Amber Valley" ...
##  $ PCON24NMW: chr  " " " " " " " " ...
##  $ BNG_E    : int  484716 404720 374132 440478 497309 450035 611218 392654 495172 438918 ...
##  $ BNG_N    : int  155270 301030 389051 349674 118530 356564 142572 398807 216598 232636 ...
##  $ LONG     : num  -0.786 -1.932 -2.39 -1.398 -0.616 ...
##  $ LAT      : num  51.3 52.6 53.4 53 51 ...
##  $ GlobalID : chr  "b6eea658-887c-43bd-81a6-e879ccb73f1f" "d1f29811-2c62-4dcb-b0dc-06db905c7d6a" "5345726c-b6b4-4d12-a310-8b2ab27bccee" "4c232f33-f30b-4526-bfec-d5aa8840fe74" ...
##  $ geometry :sfc_MULTIPOLYGON of length 650; first list element: List of 1
##   ..$ :List of 1
##   .. ..$ : num [1:19, 1:2] -0.775 -0.744 -0.731 -0.729 -0.746 ...
##   ..- attr(*, "class")= chr [1:3] "XY" "MULTIPOLYGON" "sfg"
##  - attr(*, "sf_column")= chr "geometry"
##  - attr(*, "agr")= Factor w/ 3 levels "constant","aggregate",..: NA NA NA NA NA NA NA NA NA
##   ..- attr(*, "names")= chr [1:9] "FID" "PCON24CD" "PCON24NM" "PCON24NMW" ...
str(details)
## 'data.frame':    650 obs. of  32 variables:
##  $ ONS.ID               : chr  "W07000081" "S14000060" "S14000061" "S14000062" ...
##  $ ONS.region.ID        : chr  "W92000004" "S92000003" "S92000003" "S92000003" ...
##  $ Constituency.name    : chr  "Aberafan Maesteg" "Aberdeen North" "Aberdeen South" "Aberdeenshire North and Moray East" ...
##  $ County.name          : logi  NA NA NA NA NA NA ...
##  $ Region.name          : chr  "Wales" "Scotland" "Scotland" "Scotland" ...
##  $ Country.name         : chr  "Wales" "Scotland" "Scotland" "Scotland" ...
##  $ Constituency.type    : chr  "County" "Burgh" "Burgh" "County" ...
##  $ Declaration.time     : logi  NA NA NA NA NA NA ...
##  $ Member.first.name    : chr  "Stephen" "Kirsty" "Stephen" "Seamus" ...
##  $ Member.surname       : chr  "Kinnock" "Blackman" "Flynn" "Logan" ...
##  $ Member.gender        : chr  "Male" "Female" "Male" "Male" ...
##  $ Result               : chr  "Lab hold" "SNP hold" "SNP hold" "SNP gain from Con" ...
##  $ First.party          : chr  "Lab" "SNP" "SNP" "SNP" ...
##  $ Second.party         : chr  "RUK" "Lab" "Lab" "Con" ...
##  $ Electorate           : int  72580 75925 77328 70058 70199 78553 70268 70680 74025 75790 ...
##  $ Valid.votes          : int  35755 42095 46345 38188 36666 48544 40912 41201 51452 43392 ...
##  $ Invalid.votes        : int  79 115 178 170 95 179 156 74 184 135 ...
##  $ Majority             : int  10354 1760 3758 942 7547 5683 4294 6122 4174 8794 ...
##  $ Con                  : int  2903 5881 11300 12513 1696 14081 15901 3127 16624 7892 ...
##  $ Lab                  : int  17838 12773 11455 3876 18871 19764 11607 18039 20798 18395 ...
##  $ LD                   : int  916 2583 2921 2782 725 4052 1755 1151 4727 2065 ...
##  $ RUK                  : int  7484 3781 3199 5562 2971 8210 9903 3804 4961 9601 ...
##  $ Green                : int  1094 1275 1609 0 0 2155 1746 1421 3699 1926 ...
##  $ SNP                  : int  0 14533 15213 13455 11324 0 0 11917 0 0 ...
##  $ PC                   : int  4719 0 0 0 0 0 0 0 0 1938 ...
##  $ DUP                  : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ SF                   : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ SDLP                 : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ UUP                  : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ APNI                 : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ All.other.candidates : int  801 1269 648 0 1079 282 0 1742 643 1575 ...
##  $ Of.which.other.winner: int  0 0 0 0 0 0 0 0 0 0 ...

It is called PCON24CD in the boundaries object and ONS.ID in the details Data Frame. This is the Office for National Statistics identifier for each constituency. This is useful as it allows us to join the data together. Join the boundaries and details objects into a single map_data object using a left join.

map_data <- left_join(boundaries, details, by = c("PCON24CD" = "ONS.ID"))

This is nice but it would also be good to have the colour of each party that won a seat displayed in the map. I manually found the colours associated with each UK political party on Wikipedia (Link) and saved them in a data frame using the code below. I recommend copying this code into your file if you are following along.

parties <- unique(details$First.party)
colour <- c(    "#E4003B",  "#FDF38E", "#0087DC", "#12B6CF", "#FAA61A", "#D46A4C", "#326760","#2AA82C", "#DCDCDC", "#02A95B", "#005B54", "black","#F6CB2F", "#0C3A6A", "#48A5EE")
party_colours <- cbind(parties, colour)%>%
  data.frame()

The name of the political parties is common between the party_colours Data Frame and the map_data object so we can use another left_join.

map_data_2 <- left_join(map_data, party_colours, by = c("First.party" = "parties"))

We can now go back to the map, add the colours and fiddle with the graphics settings a bit. Change the data argument over to map_data_2. Add the following arguments inside the addPolygons function: fillColor, fillOpacity, color and weight (Note: the English spelling of colour will cause an error here so the American spelling must be used – Bloody Nora!). The fillColor and color arguments are for the insides of the polygon and the borders respectively. In order to colour based on something in the data a ~ needs to be used before calling. This applies more generally. Weight controls the weight of the borders and fillOpacity controls the Opacity of the colours – 1 being fully opaque.

map <- leaflet(data = map_data_2)%>%
  addTiles()%>%
  addPolygons(fillColor = ~colour,
              fillOpacity = 0.75,
              color = "black",
              weight = 0.25)
map

This map is now much prettier. Next we will add some information to it.

5. Popups

It is possible to add Popups so that when you click on each individual constituency, you learn more details about it. This is technically quite simple to do but require some knowledge of HTML (knowledge I don’t really have) so I’d recommend using ChatGPT (other LLMs are available) to help with the construction of this. The popup argument needs to be a character which has text written in HTML style to be displayed. See below my attempt to get each constituency to display its name, the winning party and the name of the MP.

map <- leaflet(data = map_data_2)%>%
  addTiles()%>%
  addPolygons(fillColor = ~colour,
              fillOpacity = 0.75,
              color = "black",
              weight = 0.25,
              popup = paste0("<b>Constituency: </b>", map_data_2$PCON24NM, "<br>",
                             "<b>Party: </b>", map_data_2$First.party, "<br>",
                             "<b>MP Name: </b>", map_data_2$Member.first.name, 
                             " ", map_data_2$Member.surname))
map

This then creates an interactive popup that allows you to visualise the information present in the original data set. Top tip: Create your popups in the original data set when cleaning and assign it to a variable called something like popup_text to call in this function. This prevents it becoming too complicated to look at and can enable more interesting popups.

I have added a more enhanced version of this map below which I have created through enhancing the features of this data.

## Reading layer `Westminster_Parliamentary_Constituencies_July_2024_Boundaries_UK_BGC_1778715659092224182' from data source `C:\Users\MatthewGStubbs\Desktop\Electoral Boundaries\Westminster_Parliamentary_Constituencies_July_2024_Boundaries_UK_BGC_1778715659092224182.geojson' 
##   using driver `GeoJSON'
## Simple feature collection with 650 features and 9 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -116.1487 ymin: 5352.6 xmax: 655653.8 ymax: 1220302
## Projected CRS: OSGB36 / British National Grid
## [1] "<strong>Constituency Name:</strong> Aberafan Maesteg<br><strong>Winning Party: </strong><span style=\"color:#E4003B;\">Labour</span><br><strong>MP Name:</strong> Stephen Kinnock<br><strong>Turnout: </strong>49% <br>Labour: 50%<br>Reform UK: 21%<br>Plaid_Cymru: 13%<br>Conservative: 8%<br>Green: 3%<br>Liberal Democrat: 3%<br>Other: 2%<br>"

6. Adding Markers

Suppose you want to add a marker to the map which displays point of interest. I’ve decided to point out the centre of the universe: The Warwick Economics Department. Call the addMarker function and set the lng and lat arguments to the coordinates provided. If you had points in your data, you could call those instead.

map <- leaflet(data = map_data_2)%>%
  addTiles()%>%
  addPolygons(fillColor = ~colour,
              fillOpacity = 0.75,
              color = "black",
              weight = 0.5,
              popup = paste0("<b>Constituency: </b>", map_data_2$PCON24NM, "<br>",
                             "<b>Party: </b>", map_data_2$First.party, "<br>",
                             "<b>MP Name: </b>", map_data_2$Member.first.name,
                             " ", map_data_2$Member.surname))%>%
  addMarkers(lng = -1.5622, lat = 52.3810, popup = "The Department of Economics, University of Warwick")
map

We can see that the Economics Department is located in the Coventry South constituency, a Labour seat held by Zarah Sultana.

7. Saving/Sharing Your Map

It’s all well and good having this map in R but surely we want to share it with its intended audience. Using the function saveWidget from the htmlwidgets package allows the map to be downloaded in html form. This can be emailed to anyone you want to see it.

saveWidget(map, "path/to/your/file.html")
## Error in normalizePath(path.expand(path), winslash, mustWork): path[1]="path/to/your": The system cannot find the path specified

Alternatively, you can publish this map in an R markdown file or integrate it into a shiny app.

8. Final Thought/Tips for R

Hopefully this has been useful for you – it is very easy to create a leaflet map in R.

I have a few general tips for R that I want to talk about here. 1. Debugging is much easier with ChatGPT – it can be really good at translating what an error message means or give ideas on how to do more things in R (for instance, ask it what other features can be implemented in a leaflet map). 2. I use the args function all of the time. Typing it into the console with the name of any function inside will give a list of the argument you can/need to use with the function. Massive time saver. 3. Try to learn how to do the things you’ve learnt in Stata in R as well. One day, you will not have access to Stata anymore and R is free – don’t let all those hours learning econometrics disappear with the Stata license.

Finally, if you have found this guide useful, I’d love to hear from you. My email is . If you have any questions (about Leaflet or R more broadly) feel free to email me and I will do my best to answer (although, long conversations about debugging can get very tiresome).

Hope you enjoyed!

Matthew