This vignette collects examples of glyph maps made with cubble. The syntax used here is introduced in 3. Visualization: glyph map basics.
Heavy rain washes stormwater and sewage overflow into the sea, so beaches can be unsafe for swimming for some days after a storm. NSW Beachwatch monitors the water quality at swim sites across Sydney, measuring enterococci, a bacteria that indicates faecal pollution. The data, together with Sydney’s daily rainfall, were featured in TidyTuesday on 2025-05-20.
We matched each water sample from 2015 onwards to the most recent day
with at least 20 mm of rain, a “very heavy rain” day in the ETCCDI
climate-extremes indices, which gives the number of days since rain when
the sample was taken (0 to 6). For each site and each number of days, we
then calculated the proportion of samples above 40 enterococci per 100
mL, the limit for the best water-quality grade in Australia’s guidelines
for recreational water (NHMRC, 2008). The sites are grouped into the
inner harbour (Sydney Harbour sites at least 7 km from the harbour
entrance) and the ocean and outer harbour, and only sites whose glyphs
don’t overlap are kept. The code for these steps is in
data-raw/sydney-beach.R in the cubble GitHub repository,
and the result is shipped with the package:
beach <- read.csv(system.file("extdata/sydney-beach.csv", package = "cubble"))
as_tibble(beach)
#> # A tibble: 140 × 8
#> swim_site region long lat days_after_rain unsafe n water
#> <chr> <chr> <dbl> <dbl> <int> <dbl> <int> <chr>
#> 1 Balmoral Baths Sydney Harbour 151. -33.8 0 0.412 17 Ocean…
#> 2 Balmoral Baths Sydney Harbour 151. -33.8 1 0.6 10 Ocean…
#> 3 Balmoral Baths Sydney Harbour 151. -33.8 2 0.385 13 Ocean…
#> 4 Balmoral Baths Sydney Harbour 151. -33.8 3 0.375 8 Ocean…
#> 5 Balmoral Baths Sydney Harbour 151. -33.8 4 0.2 5 Ocean…
#> 6 Balmoral Baths Sydney Harbour 151. -33.8 5 0.1 10 Ocean…
#> 7 Balmoral Baths Sydney Harbour 151. -33.8 6 0 9 Ocean…
#> 8 Bondi Beach Sydney City 151. -33.9 0 0.294 17 Ocean…
#> 9 Bondi Beach Sydney City 151. -33.9 1 0.5 12 Ocean…
#> 10 Bondi Beach Sydney City 151. -33.9 2 0.210 19 Ocean…
#> # ℹ 130 more rowsBefore making the glyph map, let’s look at two sites in an ordinary
line plot. These are the time series that the glyphs will draw: the
number of days since rain on the x-axis, which will be
x_minor, and the proportion of unsafe samples on the
y-axis, which will be y_minor:
Both sites are most polluted on the day after the storm. Bondi Beach, on the ocean, then clears quickly: only about one in five samples is above the guideline by the second day. Tambourine Bay, deep in the inner harbour, stays polluted for three days, with three in four samples above the guideline, before it clears on the fourth day.
To compare all the sites, we draw each site’s line plot as a glyph at
its location. The glyph aesthetics are specified once in
ggplot(), together with colour and
group, and are used by both geom_glyph_box()
and geom_glyph(). The land and the site names don’t use the
glyph aesthetics, so they are added with
inherit.aes = FALSE. The two sites above are boxed:
sydney <- sf::st_read(
system.file("extdata/sydney-land.geojson", package = "cubble"),
quiet = TRUE)
gw <- 0.028; gh <- 0.012 # glyph width and height (degrees)
# tightest extent around the glyph boxes and the names under them
xl <- range(beach$long) + c(-2, 2) * gw / 2
yl <- range(beach$lat) + c(-0.011, gh / 2 + 0.002)
eg_box <- distinct(eg, long, lat) # the two sites above
# site names go under the glyph, or to its left when the glyph below
# would overlap the name
sites <- distinct(beach, swim_site, long, lat)
site_names <- sites |>
rowwise() |>
mutate(left = any(abs(long - sites$long) < gw &
lat - sites$lat > 0 & lat - sites$lat < gh / 2 + 0.011)) |>
ungroup()
ggplot(beach, aes(x_major = long, y_major = lat,
x_minor = days_after_rain, y_minor = unsafe,
colour = water, group = swim_site)) +
geom_sf(data = sf::st_crop(sydney, xmin = xl[1], xmax = xl[2],
ymin = yl[1], ymax = yl[2]),
fill = pal$base, colour = NA, inherit.aes = FALSE) + # land
geom_glyph_box(width = gw, height = gh, colour = pal$grid, fill = "white",
alpha = 0.08, linewidth = 0.2) +
geom_glyph(width = gw, height = gh, linewidth = 0.55, key_glyph = "path") +
annotate("rect", xmin = eg_box$long - gw / 2, xmax = eg_box$long + gw / 2,
ymin = eg_box$lat - gh / 2, ymax = eg_box$lat + gh / 2,
fill = NA, colour = pal$ink, linewidth = 0.9) +
geom_text(data = filter(site_names, !left), # site names
aes(x = long, y = lat - 0.009, label = swim_site),
inherit.aes = FALSE, size = 3.2, colour = pal$ink) +
geom_text(data = filter(site_names, left),
aes(x = long - gw / 2 - 0.002, y = lat, label = swim_site),
inherit.aes = FALSE, size = 3.2, colour = pal$ink, hjust = 1) +
scale_colour_manual(values = water_col, name = NULL) +
guides(colour = guide_legend(override.aes = list(linewidth = 1.5))) +
coord_sf(xlim = xl, ylim = yl, expand = FALSE, clip = "off") +
theme_void(base_size = 14) +
theme(legend.position = "bottom",
panel.background = element_rect(fill = "#e8f3ff", colour = NA)) # seaNotice that the glyphs don’t carry all the details of the line plot above: there are no axes, tick labels, or labelled points inside each glyph. This is intentional. A glyph is too small to be read precisely, and its purpose is to show the main structure of the time series, such as whether it drops quickly or stays high, so that this structure can be compared across locations. We therefore recommend showing a legend or a guide alongside the glyph map, such as the line plot above, that shows the details of the time series for one or two locations and explains how to read each glyph.
The glyph map shows a clear difference between the two groups of sites. The glyphs for the ocean and outer harbour sites drop quickly: most of them are back to a low proportion of unsafe samples by the second day after a storm, as the ocean flushes the pollution away. The glyphs for the inner harbour sites stay high for longer and only come down by the fourth or fifth day, since the water in the inner harbour is more sheltered and mixes more slowly. Two sites outside the inner harbour, Coogee Beach and Rose Bay Beach, are exceptions: their glyphs stay high for several days after a storm, and both sites are graded Poor by NSW Beachwatch.
National Health and Medical Research Council (2008). Guidelines for Managing Risks in Recreational Water. Australian Government, Canberra.