# General.
library(tidyverse)
library(here)
library(patchwork)
# Spatial.
library(sf)
library(sp)
library(spdep)
library(nominatimlite)
# OSM
library(osmdata)
# Google font.
library(showtext)
font_add_google("Jost")
showtext_auto()
Introduction
Our Distance Learning GIScience course is pretty established at Leeds. Students enrol part-time and complete it over a 3-year period; the final year is 100% focussed on research (dissertation). The programme draws students from a range of backgrounds and they often combine study with work in industry or government, usually GIScience or adjacent roles. It’s a great way to study for an MSc and a pleasure to teach: I run my main vis course through the ODL. If you’re interested, see this profile, by Ilya Ilyankou, a not too distant alumnus.
As students are remote and never see campus, I wanted to make a logo that anchored a little on Leeds. The idea is not especially original, but I thought using OSM buildings data surrounding campus might be a good idea. Here I render the logo using {ggplot} + {sf}, with OSM data harvested via the excellent {osmdata} package.
Setup and collect OSM data
First, some standard packages as well as {osmdata} (Mark Padgham et al. 2017), a wrapper to the Overpass API.
To collect the buildings outlines I generate an Overpass query (opq()
) paramterised with a bounding box around the university and on the building
feature. Included in the bounding box are the main university buildings, some surrounding victorian terraces and to the north, Woodhouse Moor Park, of course with no buildings.
The bounding box is created as a simple features geometry object with bbox_to_poly()
. I return from this query a simple features geometry object (osmdata_sf()
); select out the polygon data only; and express coordinates in British National Grid space (crs=27700
).
# Search area around University of Leeds (long-lat).
<- c(-1.5692, 53.8014, -1.5451, 53.8157)
bbox <- bbox_to_poly(bbox)
bbox_sf
# Get buildings within bbox.
<- bbox_sf |>
leeds_buildings opq() |>
add_osm_feature(key = "building") |>
osmdata_sf()
# Extract osm_polyogns.
<- leeds_buildings$osm_polygon |> st_transform(crs=27700) leeds_buildings
Adding a stylised circular mask
I used {ggplot}, with geom_sf()
, to render the simple features geometry file. You will see that all buildings intersecting the bounding box are returned by the Overpass query.
For the logo, I wanted a circular crop that you often see with stylised maps. There are different ways to effect this. Without giving this too much consideration, I generated a polygon object to overlay on the map itself – so defined in the same coordinate space as the map.
Generating this cutout object is quite straightforward. I find centroid coordinates of the buildings and record the width and height of that box. Then I create a squarified polygon, with st_buffer()
centred on the buildings, but slightly larger than its spatial extent; a circular polygon slightly smaller than the bounding box; and use the function st_sym_difference()
to put a hole in the larger (square) polygon.
# Search area around University of Leeds (long-lat).
# And bbox around osm_polygons.
<- st_bbox(leeds_buildings)
buildings_box <- buildings_box$xmax-buildings_box$xmin
buildings_width <- buildings_box$ymax-buildings_box$ymin
buildings_height
# Create a circular buffer around centroid of buildings.
<- buildings_box$xmin + .5*buildings_width
x <- buildings_box$ymin + .5*buildings_height
y
# Create an overlay to effect a curcular crop.
<- st_buffer(st_as_sf(tibble(x,y), coords=c(1,2), crs=27700), dist=buildings_width*.51, endCapStyle="SQUARE")
t <- st_buffer(st_as_sf(tibble(x,y), coords=c(1,2), crs=27700), dist=buildings_width*.42)
u <- st_sym_difference(u,t) v
Plotting the polygon files
The {ggplot2} code is pretty paired back: geom_sf()
will draw out the polygon files. I just make sure to layer the plot specification such that the circular mask (rendered solid white #ffffff
) appears after the buildings.
# Sample 1:4 for each row of the buildings dataset.
<- map_dbl(1:nrow(leeds_buildings), ~sample(1:4,1))
randoms <- leeds_buildings |>
plot # Add in the sampled colour positions. Factor variable, for use in manual scale.
mutate(colour=factor(randoms)) |>
ggplot() +
# Draw buildings.
geom_sf(aes(fill=colour, colour=colour)) +
# Draw mask.
geom_sf(data=v, fill="#ffffff", colour="#ffffff") +
# Du Bois palette.
scale_colour_manual(values=c( "#e94837", "#393a68", "#f3b241", "#190f11"), guide="none") +
scale_fill_manual(values=c( "#e94837", "#393a68", "#f3b241", "#190f11"), guide="none")
On colour, I used a Du Bois-inspired palette. It is beautiful, but also reflects wider efforts across the School of Geography to decolonise our programmes (see also Olman 2022).
To do this, I scale_fill_manual()
with the hex codes of four Du Bois colours. I wanted to randomly colour buildings and so, for each building (row of the leeds_buildings
dataset), randomly sample integer values from 1:4 and create a new column (colour
) with these values. When passing this column to aes(fill=)
and applying a manual colour scale, it must not be a numeric variable and so I cast colour
as a factor.
Inserting the programme name and other text
Again, many different ways of doing this. Here, I cast the buildings object (plot
) as a grob and add it as a ggplot2 layer with annotation_custom()
. I can now position the text in relative plot space, rather than working in geographic coordinate space.
The text labels are generated with annotate()
and manually positioned and sized with some trial-and-error. Note that I extent the plot space, with scale_x_manual()
, in order to accomodate the course title.
ggplot() +
# Add buildings as a plot layer, rescaled in plot coordinate space.
annotation_custom(cowplot::as_grob(p), 0, 1, 0, 1) +
# Add programme name.
annotate("text", label="GISc",
x=1, y=.78, hjust="left", vjust="top", size=220, colour="#190f11") +
annotate("text", label="Distance Learning",
x=1, y=.4, hjust="left", vjust="top", size=70, colour="#190f11") +
annotate(
"text", label="School of Geography | University of Leeds",
x=1, y=.25, hjust="left", vjust="top", size=30, colour="#190f11") +
# Extend plot in x to accomodate labels.
scale_x_continuous(limits=c(0,3), expand=c(0,0)) +
scale_y_continuous(limits=c(0, 1), expand=c(0,0))
References
Citation
@online{beecham2025,
author = {Beecham, Roger},
title = {Designing a Course Logo in \{Ggplot2\}},
date = {2025-03-24},
url = {https://www.roger-beecham.com/blog/2025/03/21/odl-logo/},
doi = {10.59350/b0ppe-4sq67},
langid = {en}
}