Engaging and Beautiful Data Visualizations with ggplot2

Working with Text

Cédric Scherer // posit::conf // September 2023

Setup

library(ggplot2)
library(dplyr)
library(stringr)

bikes <- readr::read_csv(
  here::here("data", "london-bikes-custom.csv"), 
  col_types = "Dcfffilllddddc"
)

theme_set(theme_light(base_size = 14, base_family = "Asap SemiCondensed"))

theme_update(
  panel.grid.minor = element_blank(),
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot"
)

Labels + theme()

Working with Labels

g <- ggplot(
    bikes,
    aes(x = temp, y = count,
        color = season)
  ) +
  geom_point(
    alpha = .5
  ) +
  labs(
    x = "Temperature (°C)",
    y = "Reported bike shares",
    title = "TfL bike sharing trends",
    subtitle = "Reported bike rents versus air temperature in London",
    caption = "Data: TfL",
    color = "Season:",
    tag = "1."
  )

g

Customize Labels via theme()

g + theme(
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot"
)

Customize Labels via theme()

g + theme(
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot",
  axis.text = element_text(
    color = "#28a87d"
  )
)

Customize Labels via theme()

g + theme(
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot",
  axis.text = element_text(
    color = "#28a87d",
    family = "Spline Sans Mono",
    face = "italic",
    lineheight = 1.3, # no effect here
    angle = 45,
    hjust = 1,
    vjust = 0,
    margin = margin(10, 0, 20, 0)
  )
)

Customize Labels via theme()

g + theme(
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot",
  axis.text = element_text(
    color = "#28a87d",
    family = "Spline Sans Mono",
    face = "italic",
    lineheight = 1.3, # no effect here
    angle = 45,
    hjust = 1, # no effect here
    vjust = 0, # no effect here
    margin = margin(10, 0, 20, 0) # no effect here
  )
)

Customize Labels via theme()

g + theme(
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot",
  axis.text = element_text(
    color = "#28a87d",
    family = "Spline Sans Mono",
    face = "italic",
    lineheight = 1.3, # no effect here
    angle = 45,
    hjust = 1, # no effect here
    vjust = 0, # no effect here
    margin = margin(10, 0, 20, 0) # no effect here
  ),
  axis.text.x = element_text(
    hjust = 1,
    vjust = 0,
    margin = margin(10, 0, 20, 0) # trbl
  )
)

Customize Labels via theme()

g + theme(
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot",
  axis.text = element_text(
    color = "#28a87d",
    family = "Spline Sans Mono",
    face = "italic",
    lineheight = 1.3, # no effect here
    angle = 45,
    hjust = 1, # no effect here
    vjust = 0, # no effect here
    margin = margin(10, 0, 20, 0) # no effect here
  ),
  plot.tag = element_text(
    margin = margin(0, 12, -8, 0) # trbl
  )
)

Customize Labels via theme()

g + theme(
  plot.title = element_text(face = "bold"),
  plot.title.position = "plot",
  axis.text = element_text(
    color = "#28a87d",
    family = "Spline Sans Mono",
    face = "italic",
    hjust = 1,
    vjust = 0,
    angle = 45,
    lineheight = 1.3, # no effect here
    margin = margin(10, 0, 20, 0), # no effect here
    debug = TRUE
  ),
  plot.tag = element_text(
    margin = margin(0, 12, -8, 0), # trbl
    debug = TRUE
  )
)

Labels + scale_*()

Format Labels via scale_*

g <- g + labs(tag = NULL, title = NULL, 
              subtitle = NULL)

g +
  scale_y_continuous(
    breaks = 0:4*15000
  )

Format Labels via scale_*

g +
  scale_y_continuous(
    breaks = scales::breaks_pretty(n = 10)
  )

Format Labels via scale_*

g +
  scale_y_continuous(
    breaks = 0:4*15000,
    labels = scales::comma_format()
  )

Format Labels via scale_*

g +
  scale_y_continuous(
    breaks = 0:4*15000,
    labels = scales::comma_format(
      suffix = " bikes"
    ),
    name = NULL
  )

Format Labels via scale_*

g +
  scale_y_continuous(
    breaks = 0:4*15000,
    labels = scales::comma_format(
      scale = .001
    ),
    name = "Reported bike shares in thousands"
  )

Format Labels via scale_*

g +
  scale_y_continuous(
    breaks = 0:4*15000,
    labels = function(y) y / 1000,
    name = "Reported bike shares in thousands"
  )

Format Labels via scale_*

g +
  scale_x_continuous(
    labels = function(y) paste0(y, "°C"),
    name = "Temperature"
  )

Format Labels via scale_*

g +
  scale_color_discrete(
    name = NULL,
    labels = str_to_title
  )

Style Labels of Dual Axis Plots

sec <- 
  bikes |> 
  group_by(
    month = lubridate::month(date, label = TRUE)
  ) |> 
  summarize(n = sum(count), temp = mean(temp)) |> 
  ggplot(aes(x = month)) +
  geom_col(aes(y = n), fill = "grey70") +
  geom_point(aes(y = temp * 10^5), color = "firebrick") +
  geom_line(aes(y = temp * 10^5, group = 1), color = "firebrick")

Style Labels of Dual Axis Plots

sec +
  scale_y_continuous(
    labels = scales::label_comma(scale = 1/10^6, suffix = "M"),
    name = "Rented bikes",
    sec.axis = sec_axis(
      trans = ~ . / 10^5,
      name = "Average daily temperature",
      labels = scales::label_comma(suffix = "°C")
    )
  )

Style Labels of Dual Axis Plots

sec +
  scale_y_continuous(
    labels = scales::label_comma(scale = 1/10^6, suffix = "M"),
    name = "Rented bikes",
    sec.axis = sec_axis(
      trans = ~ . / 30000,
      name = "Average daily temperature",
      labels = scales::label_comma(suffix = "°C")
    )
  ) +
  theme(
    axis.title.y.left = element_text(color = "grey60", face = "bold"),
    axis.title.y.right = element_text(color = "firebrick", face = "bold",
                                      margin = margin(l = 10, r = 0))
  )

Styling Labels
with {ggtext}

Styling Labels with {ggtext}

g +
  ggtitle("**TfL bike sharing trends by _season_**")

Styling Labels with {ggtext}

g +
  ggtitle("**TfL bike sharing trends by _season_**") +
  theme(
    plot.title = ggtext::element_markdown()
  )

Styling Labels with {ggtext}

g +
  ggtitle("<b style='font-family:Times;font-size:25pt'>TfL</b> bike sharing trends by <i style='color:#28A87D;'>season</i>") +
  theme(
    plot.title = ggtext::element_markdown()
  )

<b style='font-family:Times;font-size:25pt;'>TfL</b> bike sharing trends by <i style='color:#28A87;'>season</i>

Styling Labels with {ggtext}

g +
  ggtext::geom_richtext(
    aes(x = 18, y = 48500,
        label = "What happened on these<br>two <b style='color:#F7B01B;'>summer days</b>?"),
    stat = "unique"
  ) +
  scale_color_manual(
    values = c("#6681FE", "#1EC98D", "#F7B01B", "#A26E7C")
  )

What happened on these<br>two <b style='color:#F7B01B;'>summer days</b>?

Styling Labels with {ggtext}

g +
  ggtext::geom_richtext(
    aes(x = 18, y = 48500,
        label = "What happened on these<br>two <b style='color:#F7B01B;'>summer days</b>?"),
    stat = "unique", 
    color = "grey20",
    family = "Asap SemiCondensed",
    fill = NA, 
    label.color = NA
  ) +
  scale_color_manual(
    values = c("#6681FE", "#1EC98D", "#F7B01B", "#A26E7C")
  )

What happened on these<br>two <b style='color:#F7B01B;'>summer days</b>?

Styling Labels with {ggtext}

friends <- readr::read_csv(
  here::here("data", "friends-mentions-partners.csv")
)

friends
# A tibble: 725 × 6
      id season episode key               partners          mentions
   <dbl>  <dbl>   <dbl> <chr>             <chr>                <dbl>
 1     1      1       1 Ross & Rachel     Ross & Rachel            4
 2     1      1       1 Rachel & Joey     Rachel & Joey            1
 3     2      1       2 Ross & Rachel     Ross & Rachel            1
 4     2      1       2 Ross              Ross & Carol             2
 5     2      1       2 Rachel & Joey     Rachel & Joey            1
 6     2      1       2 Rachel            Rachel & Barry           3
 7     2      1       2 Monica & Chandler Monica & Chandler        1
 8     5      1       5 Ross & Rachel     Ross & Rachel            3
 9     5      1       5 Ross              Ross & Carol             1
10     5      1       5 Chandler          Chandler & Janice        2
# ℹ 715 more rows

Styling Labels with {ggtext}

match_colors <-
  tibble(
    key = c("Chandler", "Joey", "Monica", "Monica & Chandler", 
            "Phoebe", "Rachel", "Rachel & Joey", "Ross", "Ross & Rachel"),
    color = c("#48508c", "#55331d", "#a64d64", "#774f78", 
              "#5b7233", "#ba2a22", "#882f20", "#f6ab18", "#d86b1d")
  )

match_colors
# A tibble: 9 × 2
  key               color  
  <chr>             <chr>  
1 Chandler          #48508c
2 Joey              #55331d
3 Monica            #a64d64
4 Monica & Chandler #774f78
5 Phoebe            #5b7233
6 Rachel            #ba2a22
7 Rachel & Joey     #882f20
8 Ross              #f6ab18
9 Ross & Rachel     #d86b1d

Styling Labels with {ggtext}

friends |> 
  mutate(key = if_else(
    !partners %in% c("Ross & Rachel", "Rachel & Joey", "Monica & Chandler"),
    word(partners, 1), partners
  )) |> 
  left_join(
    match_colors
  )
# A tibble: 725 × 7
      id season episode key               partners          mentions color  
   <dbl>  <dbl>   <dbl> <chr>             <chr>                <dbl> <chr>  
 1     1      1       1 Ross & Rachel     Ross & Rachel            4 #d86b1d
 2     1      1       1 Rachel & Joey     Rachel & Joey            1 #882f20
 3     2      1       2 Ross & Rachel     Ross & Rachel            1 #d86b1d
 4     2      1       2 Ross              Ross & Carol             2 #f6ab18
 5     2      1       2 Rachel & Joey     Rachel & Joey            1 #882f20
 6     2      1       2 Rachel            Rachel & Barry           3 #ba2a22
 7     2      1       2 Monica & Chandler Monica & Chandler        1 #774f78
 8     5      1       5 Ross & Rachel     Ross & Rachel            3 #d86b1d
 9     5      1       5 Ross              Ross & Carol             1 #f6ab18
10     5      1       5 Chandler          Chandler & Janice        2 #48508c
# ℹ 715 more rows

Styling Labels with {ggtext}

friends_render <- friends |> 
  mutate(key = if_else(
    !partners %in% c("Ross & Rachel", "Rachel & Joey", "Monica & Chandler"),
    word(partners, 1), partners
  )) |> 
  left_join(
    match_colors
  ) |> 
  mutate(
    partners = if_else(
      key %in% c("Ross & Rachel", "Rachel & Joey", "Monica & Chandler"),
      paste0("<b style='color:", color, "'>", partners, "</b>"),
      str_replace(partners, key, paste0("<b style='color:", color, "'>", key, "</b>"))
    )
  )

Styling Labels with {ggtext}

friends_render |> select(key, color, partners) |> unique()
# A tibble: 25 × 3
   key               color   partners                                      
   <chr>             <chr>   <chr>                                         
 1 Ross & Rachel     #d86b1d <b style='color:#d86b1d'>Ross & Rachel</b>    
 2 Rachel & Joey     #882f20 <b style='color:#882f20'>Rachel & Joey</b>    
 3 Ross              #f6ab18 <b style='color:#f6ab18'>Ross</b> & Carol     
 4 Rachel            #ba2a22 <b style='color:#ba2a22'>Rachel</b> & Barry   
 5 Monica & Chandler #774f78 <b style='color:#774f78'>Monica & Chandler</b>
 6 Chandler          #48508c <b style='color:#48508c'>Chandler</b> & Janice
 7 Rachel            #ba2a22 <b style='color:#ba2a22'>Rachel</b> & Paolo   
 8 Phoebe            #5b7233 <b style='color:#5b7233'>Phoebe</b> & David   
 9 Rachel            #ba2a22 <b style='color:#ba2a22'>Rachel</b> & Tag     
10 Ross              #f6ab18 <b style='color:#f6ab18'>Ross</b> & Julie     
# ℹ 15 more rows

Styling Labels with {ggtext}

ggplot(friends_render,
       aes(x = id, y = partners)) + 
  theme(axis.text.y = ggtext::element_markdown(hjust = 0))

Styling Labels with {ggtext}

ggplot(friends_render,
  aes(x = id, y = partners)) + 
  geom_point(aes(size = mentions, color = color), alpha = .3) +
  scale_color_identity() +
  scale_size_area(max_size = 5, guide = "none") +
  coord_cartesian(expand = FALSE, clip = "off") +
  labs(x = "Episodes", y = NULL) +
  theme_minimal(base_family = "Asap SemiCondensed") +
  theme(
    axis.text.y = ggtext::element_markdown(hjust = 0),
    axis.text.x = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.minor = element_blank()
  )

Facet Labellers

Facet Labellers

g +
  facet_wrap(
    ~ day_night,
    labeller = label_both
  )

Facet Labellers

g +
  facet_wrap(
    ~ is_workday + day_night,
    labeller = label_both
  )

Facet Labellers

g +
  facet_wrap(
    ~ is_workday + day_night,
    labeller = labeller(
      day_night = str_to_title
    )
  )

Facet Labellers

codes <- c(
  `TRUE` = "Workday",
  `FALSE` = "Weekend or Holiday"
)

g +
  facet_wrap(
    ~ is_workday + day_night,
    labeller = labeller(
      day_night = str_to_title,
      is_workday = codes
    )
  )

Facet Labellers

codes <- c(
  `TRUE` = "Workday",
  `FALSE` = "Weekend or Holiday"
)

g +
  facet_wrap(
    ~ is_workday + day_night,
    labeller = labeller(
      .default = str_to_title,
      is_workday = codes
    )
  )

Facet Labeller

g +
  facet_grid(
    day_night ~ is_workday,
    labeller = labeller(
      day_night = str_to_title,
      is_workday = codes
    )
  ) +
  theme(
    legend.position = "top"
  )

Handling Long Labels

Handling Long Labels

ggplot(
    filter(bikes, !is.na(weather_type)),
    aes(x = weather_type,
        y = count)
  ) +
  geom_boxplot()

Handling Long Labels with {stringr}

ggplot(
    filter(bikes, !is.na(weather_type)),
    aes(x = weather_type,
        y = count)
  ) +
  geom_boxplot() +
  scale_x_discrete(
    guide = guide_axis(
      n.dodge = 2
    )
  )

Handling Long Labels with {stringr}

ggplot(
    filter(bikes, !is.na(weather_type)),
    aes(x = str_wrap(weather_type, 6),
        y = count)
  ) +
  geom_boxplot()

Handling Long Titles

g + 
  ggtitle("TfL bike sharing trends in 2015 and 2016 by season for day and night periods") +
  theme(
    plot.title = element_text(size = 20),
    plot.title.position = "plot"
  )

Handling Long Titles with

g + 
  ggtitle("TfL bike sharing trends in 2015 and 2016\nby season for day and night periods") +
  theme(
    plot.title = element_text(size = 20),
    plot.title.position = "plot"
  )

TfL bike sharing trends in 2015 and 2016\nby season for day and night periods

Handling Long Titles with {ggtext}

g +
  ggtitle("TfL bike sharing trends in 2015 and 2016 by season for day and night periods") +
  theme(
    plot.title =
      ggtext::element_textbox_simple(size = 20),
    plot.title.position = "plot"
  )

Handling Long Titles with {ggtext}

g +
  ggtitle("TfL bike sharing trends in 2015 and 2016 by season for day and night periods") +
  theme(
    plot.title = ggtext::element_textbox_simple(
      margin = margin(t = 12, b = 12),
      lineheight = .9
    ),
    plot.title.position = "plot"
  )

Handling Long Titles with {ggtext}

g +
  ggtitle("TfL bike sharing trends in 2015 and 2016 by season for day and night periods") +
  theme(
    plot.title = ggtext::element_textbox_simple(
      margin = margin(t = 12, b = 12),
      fill = "grey90",
      lineheight = .9
    ),
    plot.title.position = "plot"
  )

Handling Long Titles with {ggtext}

g +
  ggtitle("TfL bike sharing trends in 2015 and 2016 by season for day and night periods") +
  theme(
    plot.title = ggtext::element_textbox_simple(
      margin = margin(t = 12, b = 12),
      padding = margin(rep(12, 4)),
      fill = "grey90",
      box.color = "grey40",
      r = unit(9, "pt"),
      halign = .5,
      face = "bold",
      lineheight = .9
    ),
    plot.title.position = "plot"
  )

Annotations

Add Single Text Annotations

ga <- 
  ggplot(bikes, 
         aes(x = temp, y = count)) +
  geom_point(
    aes(color = count > 40000),
    size = 2
  ) +
  scale_color_manual(
    values = c("grey", "firebrick"),
    guide = "none"
  )

ga

Add Single Text Annotations

ga +
  annotate(
    geom = "text",
    x = 18,
    y = 48000,
    label = "What happened here?"
  )

Style Text Annotations

ga +
  annotate(
    geom = "text",
    x = 18,
    y = 48000,
    label = "What happened here?",
    color = "firebrick",
    size = 6,
    family = "Asap SemiCondensed",
    fontface = "bold",
    lineheight =  .8
  )

Add Multiple Text Annotations

ga +
  annotate(
    geom = "text",
    x = c(18, max(bikes$temp)),
    y = c(48000, 1000),
    label = c("What happened here?", "Powered by TfL"),
    color = c("firebrick", "black"),
    size = c(6, 3),
    family = c("Asap SemiCondensed", "Hepta Slab"),
    fontface = c("bold", "plain"),
    hjust = c(.5, 1)
  )

“Point’n’Click” Annotations

ggannotate::ggannotate(g)
A screenshot of the Shiny app provided by the ggannotate package which allows to place a text annotation by clicking and returns the code.

Add Boxes

ga + 
  annotate(
    geom = "text",
    x = 19.5,
    y = 42000,
    label = "What happened here?",
    family = "Asap SemiCondensed",
    size = 6,
    vjust = 1.3
  ) +
  annotate(
    geom = "rect",
    xmin = 17, 
    xmax = 22,
    ymin = 42000, 
    ymax = 54000,
    color = "firebrick", 
    fill = NA
  )

Add Lines

ga +
  annotate(
    geom = "text",
    x = 10,
    y = 38000,
    label = "The\nhighest\ncount",
    family = "Asap SemiCondensed",
    size = 6,
    lineheight =  .8
  ) +
  annotate(
    geom = "segment",
    x = 13, 
    xend = 18.2,
    y = 38000, 
    yend = 51870
  )

Add Lines

ga +
  annotate(
    geom = "text",
    x = 10,
    y = 38000,
    label = "The\nhighest\ncount",
    family = "Asap SemiCondensed",
    size = 6,
    lineheight =  .8
  ) +
  annotate(
    geom = "curve",
    x = 13, 
    xend = 18.2,
    y = 38000, 
    yend = 51870
  )

Add Arrows

ga +
  annotate(
    geom = "text",
    x = 10,
    y = 38000,
    label = "The\nhighest\ncount",
    family = "Asap SemiCondensed",
    size = 6,
    lineheight =  .8
  ) +
  annotate(
    geom = "curve",
    x = 13, 
    xend = 18.2,
    y = 38000, 
    yend = 51870,
    curvature = .25,
    arrow = arrow()
  )

Add Arrows

ga +
  annotate(
    geom = "text",
    x = 10,
    y = 38000,
    label = "The\nhighest\ncount",
    family = "Asap SemiCondensed",
    size = 6,
    lineheight =  .8
  ) +
  annotate(
    geom = "curve",
    x = 13, 
    xend = 18.2,
    y = 38000, 
    yend = 51870,
    curvature = .25,
    arrow = arrow(
      length = unit(10, "pt"),
      type = "closed",
      ends = "both"
    )
  )

Add Arrows

ga +
  annotate(
    geom = "text",
    x = 10,
    y = 38000,
    label = "The\nhighest\ncount",
    family = "Asap SemiCondensed",
    size = 6,
    lineheight =  .8
  ) +
  annotate(
    geom = "curve",
    x = 13, 
    xend = 18.2,
    y = 38000, 
    yend = 51870,
    curvature = .8,
    angle = 130,
    arrow = arrow(
      length = unit(10, "pt"),
      type = "closed"
    )
  )

Highlight Hot Days

gh <- 
  ggplot(
    data = filter(bikes, temp >= 27),
    aes(x = date, y = temp)
  ) +
  geom_point(
    data = bikes,
    color = "grey65", alpha = .3
  ) +
  geom_point(size = 2.5)

gh

Annotations with geom_text()

gh +
  geom_text(
    aes(label = format(date, "%m/%d")),
    nudge_x = 10,
    hjust = 0
  )

Annotations with geom_label()

gh +
  geom_label(
    aes(label = format(date, "%m/%d")),
    nudge_x = .3,
    hjust = 0
  )

Annotations with {ggrepel}

gh +
  ggrepel::geom_text_repel(
    aes(label = format(date, "%m/%d"))
  )

Annotations with {ggrepel}

gh + 
  ggrepel::geom_text_repel(
    aes(label = format(date, "%m/%d")),
    family = "Spline Sans Mono",
    size = 4.5,
    fontface = "bold"
  )

Annotations with {ggrepel}

gh +
  ggrepel::geom_text_repel(
    aes(label = format(date, "%m/%d")),
    family = "Spline Sans Mono",
    # space between points + labels
    box.padding = .8,
    # always draw segments
    min.segment.length = 0
  )

Annotations with {ggrepel}

gh +
  ggrepel::geom_text_repel(
    aes(label = format(date, "%y/%m/%d")),
    family = "Spline Sans Mono",
    # force to the right
    xlim = c(NA, as.Date("2015-06-01")), 
    hjust = 1
  )

Annotations with {ggrepel}

gh +
  ggrepel::geom_text_repel(
    aes(label = format(date, "%y/%m/%d")),
    family = "Spline Sans Mono",
    xlim = c(NA, as.Date("2015-06-01")),
    # style segment
    segment.curvature = .01,
    arrow = arrow(length = unit(.02, "npc"), type = "closed")
  )

Annotations with {ggrepel}

gh +
  ggrepel::geom_text_repel(
    aes(label = format(date, "%y/%m/%d")),
    family = "Spline Sans Mono",
    xlim = c(NA, as.Date("2015-06-01")),
    # style segment
    segment.curvature = .001,
    segment.inflect = TRUE
  )

Annotations with {ggforce}

g +
  ggforce::geom_mark_rect(
    aes(label = "Outliers?",
        filter = count > 40000)
  )

Annotations with {ggforce}

g +
  ggforce::geom_mark_rect(
    aes(label = "Outliers?",
        filter = count > 40000),
    color = "black",
    label.family = "Asap SemiCondensed"
  )

Annotations with {ggforce}

g +
  ggforce::geom_mark_rect(
    aes(label = "Outliers?",
        filter = count > 40000),
    description = "What happened on\nthese two days?",
    color = "black",
    label.family = "Asap SemiCondensed"
  )

Annotations with {ggforce}

g +
  ggforce::geom_mark_rect(
    aes(label = "Outliers?",
        filter = count > 40000),
    description = "What happened on\nthese two days?",
    color = "black",
    label.family = "Asap SemiCondensed",
    expand = unit(8, "pt"),
    radius = unit(12, "pt"),
    con.cap = unit(0, "pt"),
    label.buffer = unit(15, "pt"),
    con.type = "straight",
    label.fill = "transparent"
  )

Annotations with {ggforce}

g +
  ggforce::geom_mark_circle(
    aes(label = "Outliers?",
        filter = count > 40000),
    description = "What happened on\nthese two days?",
    color = "black",
    label.family = "Asap SemiCondensed",
    expand = unit(8, "pt"),
    con.cap = unit(0, "pt"),
    label.buffer = unit(15, "pt"),
    con.type = "straight",
    label.fill = "transparent"
  )

Annotations with {ggforce}

g +
  ggforce::geom_mark_hull(
    aes(label = "Outliers?",
        filter = count > 40000),
    description = "What happened on\nthese two days?",
    color = "black",
    label.family = "Asap SemiCondensed",
    expand = unit(8, "pt"),
    con.cap = unit(0, "pt"),
    label.buffer = unit(15, "pt"),
    con.type = "straight",
    label.fill = "transparent"
  )

Annotations with {geomtextpath}

bikes |>
  filter(year == "2016") |>
  group_by(month, day_night) |> 
  summarize(count = sum(count)) |> 
  ggplot(aes(x = month, y = count, 
             color = day_night,
             group = day_night)) +
  geom_line(linewidth = 1) +
  coord_cartesian(expand = FALSE) +
  scale_y_continuous(
    labels = scales::label_comma(
      scale = 1/10^3, suffix = "K"
    ),
    limits = c(0, 850000)
  ) +
  scale_color_manual(
    values = c("#FFA200", "#757BC7"),
    name = NULL
  )

Annotations with {geomtextpath}

bikes |>
  filter(year == "2016") |>
  group_by(month, day_night) |> 
  summarize(count = sum(count)) |> 
  ggplot(aes(x = month, y = count, 
             color = day_night,
             group = day_night)) +
  geomtextpath::geom_textline(
    aes(label = day_night),
    linewidth = 1,
    vjust = -.5, 
    family = "Asap SemiCondensed",
    fontface = "bold"
  ) +
  coord_cartesian(expand = FALSE) +
  scale_y_continuous(
    labels = scales::label_comma(
      scale = 1/10^3, suffix = "K"
    ),
    limits = c(0, 850000)
  ) +
  scale_color_manual(
    values = c("#FFA200", "#757BC7"),
    guide = "none"
  )

Annotations with {geomtextpath}

bikes |>
  filter(year == "2016") |>
  group_by(month, day_night) |> 
  summarize(count = sum(count)) |> 
  mutate(day_night = if_else(
    day_night == "day", 
    "Day period (6am-6pm)", 
    "Night period (6pm-6am)"
  )) |> 
  ggplot(aes(x = month, y = count, 
             color = day_night,
             group = day_night)) +
  geomtextpath::geom_textline(
    aes(label = day_night),
    linewidth = 1,
    vjust = -.5, 
    hjust = .05,
    family = "Asap SemiCondensed",
    fontface = "bold"
  ) +
  coord_cartesian(expand = FALSE) +
  scale_y_continuous(
    labels = scales::label_comma(
      scale = 1/10^3, suffix = "K"
    ),
    limits = c(0, 850000)
  ) +
  scale_color_manual(
    values = c("#FFA200", "#757BC7"),
    guide = "none"
  )

Line Chart with stat_summary()

bikes |>
  filter(year == "2016") |>
  ggplot(aes(x = month, y = count, 
             color = day_night,
             group = day_night)) +
  stat_summary(
    geom = "line", fun = sum,
    linewidth = 1
  ) +
  coord_cartesian(expand = FALSE) +
  scale_y_continuous(
    labels = scales::label_comma(
      scale = 1/10^3, suffix = "K"
    ),
    limits = c(0, 850000)
  ) +
  scale_color_manual(
    values = c("#FFA200", "#757BC7"),
    name = NULL
  )

Line Chart with stat_summary()

bikes |>
  filter(year == "2016") |>
  ggplot(aes(x = month, y = count, 
             color = day_night,
             group = day_night)) +
  geomtextpath::geom_textline(
    aes(label = day_night), 
    stat = "summary", fun = sum,
    linewidth = 1
  ) +
  coord_cartesian(expand = FALSE) +
  scale_y_continuous(
    labels = scales::label_comma(
      scale = 1/10^3, suffix = "K"
    ),
    limits = c(0, 850000)
  ) +
  scale_color_manual(
    values = c("#FFA200", "#757BC7"),
    name = NULL
  )

Recap

  • style labels such as title, axis and legend texts with theme()
  • format data-related labels with the labels argument of scale_*()
  • adjust strip text with the facet_*(labeller) functionality
  • add data-related annotations with geom_text|label()
  • … and data-unrelated annotations with annotate()
  • {ggtext} allows to render labels with markdown and basic html
  • {ggtext} also allows to add dynamic linebreaks and images
  • {ggrepel} ensures clever placement of annotations
  • ggforce::geom_mark_*() provide a set of advanced annotations

Exercises

Exercise 1

  • Take a look at the following visualization.
    • For each group of text labels, note how one would add and modify them.
    • Discuss how to automate the placement of the labels in- and outside of the bars.

Exercise 2

  • Create a function that plots the famous Gapminder chart, highlighting one of the continents.
    • Extend the code in 02-text-exercises.qmd to annotate a continent your choice of with {ggforce}.
    • Turn the code into a function with the utility to annotate any continent.
    • Optional: Create a second function to highlight a country.
# install.packages("gapminder")

(gm2007 <- filter(gapminder::gapminder, year == 2007))

ggplot(gm2007, aes(x = gdpPercap, y = lifeExp)) +
  geom_point( 
    aes(size = pop), alpha = .5
  ) +
  scale_x_log10(
    breaks = c(500, 2000, 8000, 32000),
    labels = scales::label_dollar(accuracy = 1)
  ) +
  scale_size(
    range = c(1, 12), name = "Population:", 
    breaks = c(10, 100, 1000)*1000000, 
    labels = scales::label_comma(scale = 1 / 10^6, suffix = "M")
  ) +
  labs(x = "GDP per capita", y = "Life expectancy") +
  theme_minimal(base_family = "Asap SemiCondensed") +
  theme(panel.grid.minor = element_blank())
# A tibble: 142 × 6
   country     continent  year lifeExp       pop gdpPercap
   <fct>       <fct>     <int>   <dbl>     <int>     <dbl>
 1 Afghanistan Asia       2007    43.8  31889923      975.
 2 Albania     Europe     2007    76.4   3600523     5937.
 3 Algeria     Africa     2007    72.3  33333216     6223.
 4 Angola      Africa     2007    42.7  12420476     4797.
 5 Argentina   Americas   2007    75.3  40301927    12779.
 6 Australia   Oceania    2007    81.2  20434176    34435.
 7 Austria     Europe     2007    79.8   8199783    36126.
 8 Bahrain     Asia       2007    75.6    708573    29796.
 9 Bangladesh  Asia       2007    64.1 150448339     1391.
10 Belgium     Europe     2007    79.4  10392226    33693.
# ℹ 132 more rows