Designing a course logo in {ggplot2}

I’ve recently started as Programme Lead on our GIScience Distance Learning MSc. Naturally I spent some time playing around with logos. Here’s my attempt using {ggplot2} + {sf}, data via {osmdata}.
ggplot
osm data
Author
Published

Monday, March 24, 2025

Doi

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.

The new logo for our GIScience ODL programme.

The new logo for our GIScience ODL programme.

Setup and collect OSM data

First, some standard packages as well as {osmdata} (Mark Padgham et al. 2017), a wrapper to the Overpass API.

# 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()

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.

Building polygons returned by the Overpass query.

Building polygons returned by the Overpass query.

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).
bbox <- c(-1.5692, 53.8014, -1.5451, 53.8157)
bbox_sf <- bbox_to_poly(bbox)

# Get buildings within bbox.
leeds_buildings <- bbox_sf |>
  opq() |>
  add_osm_feature(key = "building") |>
  osmdata_sf() 

# Extract osm_polyogns.
leeds_buildings <- leeds_buildings$osm_polygon |> st_transform(crs=27700)

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.

Building polygons overlaid with a circular mask (a polygon-with-a-hole).

Building polygons overlaid with a circular mask (a polygon-with-a-hole).

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.
buildings_box <- st_bbox(leeds_buildings)
buildings_width <- buildings_box$xmax-buildings_box$xmin
buildings_height <- buildings_box$ymax-buildings_box$ymin

# Create a circular buffer around centroid of buildings.
x <- buildings_box$xmin + .5*buildings_width
y <- buildings_box$ymin + .5*buildings_height

# Create an overlay to effect a curcular crop.
t <- st_buffer(st_as_sf(tibble(x,y), coords=c(1,2), crs=27700), dist=buildings_width*.51, endCapStyle="SQUARE")
u <- st_buffer(st_as_sf(tibble(x,y), coords=c(1,2), crs=27700), dist=buildings_width*.42)
v <- st_sym_difference(u,t)

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.
randoms <- map_dbl(1:nrow(leeds_buildings), ~sample(1:4,1))
plot <- leeds_buildings |>
  # 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).

Building polygons coloured using a Du Bois-inspired palette.

Building polygons coloured using a Du Bois-inspired palette.

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

Mark Padgham, Bob Rudis, Robin Lovelace, and Maëlle Salmon. 2017. “Osmdata.” Journal of Open Source Software 2 (14): 305. https://doi.org/10.21105/joss.00305.
Olman, Lynda C. 2022. “Decolonizing the Color-Line: A Topological Analysis of w.e.b. Du Bois’s Infographics for the 1900 Paris Exposition.” Journal of Business and Technical Communication 36 (2): 127–64. https://doi.org/10.1177/10506519211064613.

Citation

BibTeX 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}
}
For attribution, please cite this work as:
Beecham, Roger. 2025. “Designing a Course Logo in {Ggplot2}.” March 24, 2025. https://doi.org/10.59350/b0ppe-4sq67.