My life in Months - Making a ‘life plot’ in R using ggplot2

This blog post is inspired by Sharla Gefland twitter post found here, where she made a ‘My Life in Months’ plot.
visualization
Author

Michael Luu

Published

October 7, 2020

This blog post is inspired by Sharla Gefland twitter post found here, where she made a ‘My Life in Months’ plot.

Annotations have always been the bane of my existence in ggplot2, and I figured this would be a fun project to get some practice. Looking at her github repo found here, she made this figure using the waffle plot package found here. Although using the waffle package may simplify some aspects of making this figure, recreating this figure in pure ggplot2 will open up the arguments for further customization that may not be available via the waffle package.

We can start off by creating a tibble for the basis of the plot. The goal here is to create a tibble starting from the starting month (month/year) I was born, until the current month/year. I can create this with the help of the lubridate package, which simplifies the handling of dates in R, and using this package to further extract the month and year information from the date sequence.

df <- tibble(
  date = seq(mdy('9/1/1987'), floor_date(Sys.Date(), 'month'), 'month')
) %>%
  mutate(
    month = month(date),
    year = year(date)
  )

df <- tibble(date = seq(mdy('9/1/1987'), Sys.Date(), '1 month')) %>%
  mutate(month = month(date),
         year = year(date))

df
# A tibble: 428 × 3
   date       month  year
   <date>     <dbl> <dbl>
 1 1987-09-01     9  1987
 2 1987-10-01    10  1987
 3 1987-11-01    11  1987
 4 1987-12-01    12  1987
 5 1988-01-01     1  1988
 6 1988-02-01     2  1988
 7 1988-03-01     3  1988
 8 1988-04-01     4  1988
 9 1988-05-01     5  1988
10 1988-06-01     6  1988
# ℹ 418 more rows

Using the tibble I just created, I can further define the ‘eras’ that I would like to highlight in the life plot.

plot_data <- df %>%
  mutate(
    era = case_when(
      date %in% mdy('9/1/1987'):mdy("9/1/1991") ~ 'Childhood',
      date %in% mdy('10/1/1991'):mdy('6/1/2005') ~ 'K-12 Grade School',
      date %in% mdy('7/1/2005'):mdy('12/1/2009') ~ 'BSc in Biological Sciences',
      date %in% mdy('1/1/2010'):mdy('7/1/2013') ~ 'Pre Graduate Work',
      date %in% mdy('8/1/2013'):mdy('6/1/2015') ~ 'MPH in Biostatistics & Epidemiology',
      date %in% mdy('7/1/2015'):mdy('8/1/2016') ~ 'Data Analyst',
      date %in% mdy('9/1/2016'):Sys.Date() ~ 'Biostatistician'
    )
  ) %>%
  mutate(era = factor(
    era,
    levels = c(
      'Childhood',
      'K-12 Grade School',
      'BSc in Biological Sciences',
      'Pre Graduate Work',
      'MPH in Biostatistics & Epidemiology',
      'Data Analyst',
      'Biostatistician'
    )
  ))

plot_data
# A tibble: 428 × 4
   date       month  year era      
   <date>     <dbl> <dbl> <fct>    
 1 1987-09-01     9  1987 Childhood
 2 1987-10-01    10  1987 Childhood
 3 1987-11-01    11  1987 Childhood
 4 1987-12-01    12  1987 Childhood
 5 1988-01-01     1  1988 Childhood
 6 1988-02-01     2  1988 Childhood
 7 1988-03-01     3  1988 Childhood
 8 1988-04-01     4  1988 Childhood
 9 1988-05-01     5  1988 Childhood
10 1988-06-01     6  1988 Childhood
# ℹ 418 more rows

Next, I’ll create a base plot using ggplot2, where I’ll map the x axis to year, and the y axis to month. I’ll also use the geom, geom_tile() to create the ‘blocks’ that we see in the life plot, where we’ll map the fill to era.

 ggplot(plot_data, aes(y = month, x = year)) + 
  geom_tile(color = 'white', aes(fill = era), size = 1)
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.

Now that we have a simple base plot to work with, we can further customize and clean up the figure. A trick to give us a bigger ‘space’ to work with is to expand the limits of the y and x axis. Furthermore, I will use scale_fill_d3() to add a fill theme to the plot.

base_plot <- ggplot(plot_data, aes(y = month, x = year)) + 
  geom_tile(color = 'white', aes(fill = era), size = 1) + 
  scale_y_continuous(breaks = -6:18, limits = c(-6, 18)) +
  scale_x_continuous(breaks = 1980:2020) +
  labs(y = 'Month', x = 'Year') + 
  theme(legend.position = 'bottom') + 
  scale_fill_d3()
  

base_plot

Annotations have always been tricky, because we have to specifically define the coordinates of the annotations we are trying to add. I’m going to start off small with a small annotation on the top left corner with an arrow point to the top left square. The segments are created using the geom_curve() and the text annotations are created using annotate() via geom_text()

## annotate the definition of 1 square = 1 month
plot <- base_plot +
  geom_curve(
    x = 1987,
    y = 12,
    xend = 1986,
    yend = 14,
    curvature = -.4,
    arrow = arrow(length = unit(0.01, "npc"), ends = 'first'),
    color = 'black'
  ) + 
  annotate(
    'text',
    x = 1985,
    y = 15,
    hjust = 0,
    label = '1 square = 1 month',
    family = "Segoe Script"
  )

plot
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Next I’ll start to map out exactly where I want each of the labels for the eras to be placed. This definitely took a while, and it helps if you have some forethought on where you want to place the labels.

### set colors 
pallete_colors <- pal_d3("category10")(10)

## set size
annotation_size <- 5

plot <- plot + 
  annotate(
    'text',
    x = 1989,
    y = -1,
    label = 'Childhood',
    color = pallete_colors[[1]],
    size = annotation_size,
    family = "Segoe Script"
  )  +
  annotate(
    'text',
    x = 1998,
    y = -1,
    label = 'K-12 Grade School',
    color = pallete_colors[[2]],
    size = annotation_size,
    family = "Segoe Script"
  ) +
  annotate(
    'text',
    x = 2007.5,
    y = -1,
    label = 'BSc in Biological Sciences',
    color = pallete_colors[[3]],
    size = annotation_size,
    family = "Segoe Script"
  ) +
  annotate(
    'text',
    x = 2011,
    y = 14,
    label = 'Pre Graduate Employment',
    color = pallete_colors[[4]],
    size = annotation_size,
    family = "Segoe Script"
  ) +
  annotate(
    'text',
    x = 2013,
    y = -3,
    label = 'MPH in Biostatistics & Epidemiology',
    color = pallete_colors[[5]],
    size = annotation_size,
    family = "Segoe Script"
  ) +
  annotate(
    'text',
    x = 2012.5,
    y = 16,
    label = 'Data Analyst',
    color = pallete_colors[[6]],
    size = annotation_size,
    family = "Segoe Script"
  ) +
  annotate(
    'text',
    x = 2018.5,
    y = -1,
    label = 'Biostatistician',
    color = pallete_colors[[7]],
    size = annotation_size,
    family = "Segoe Script"
  ) 

plot
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Now that we have the text placed in all the designated coordinates, we can start working on the arrows.

## add additional curve segments for labels

plot <- plot + 
  geom_curve(
    x = 1989,
    y = 1,
    xend = 1989,
    yend = -.5,
    curvature = .2,
    arrow = arrow(length = unit(0.01, 'npc'), ends = 'first'),
    color = 'black'
  ) +
  geom_curve(
    x = 1998,
    y = 1,
    xend = 1998,
    yend = -.5,
    curvature = .2,
    arrow = arrow(length = unit(0.01, 'npc'), ends = 'first'),
    color = 'black'
  ) +
  geom_curve(
    x = 2007,
    y = 1,
    xend = 2007,
    yend = -.5,
    curvature = -.2,
    arrow = arrow(length = unit(0.01, 'npc'), ends = 'first'),
    color = 'black'
  ) +
  geom_curve(
    x = 2011,
    y = 12,
    xend = 2011,
    yend = 13.5,
    curvature = -.2,
    arrow = arrow(length = unit(0.01, 'npc'), ends = 'first'),
    color = 'black'
  ) +
  geom_curve(
    x = 2015,
    y = 12,
    xend =  2015,
    yend = 16,
    arrow = arrow(length = unit(0.01, 'npc'), ends = 'first'),
    color = 'black',
    curvature = .8
  ) +
  geom_curve(
    x = 2014,
    y = 1,
    xend =  2014,
    yend = -2.5,
    arrow = arrow(length = unit(0.01, 'npc'), ends = 'first'),
    curvature = -0.2,
    color = 'black'
  ) +
  geom_curve(
    x = 2018,
    y = 1,
    xend =  2018,
    yend = -0.5,
    arrow = arrow(length = unit(0.01, 'npc'), ends = 'first'),
    curvature = -0.2,
    color = 'black'
  ) 

plot
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Now that we have most of the annotations on there, we can add some supplemental annotations, e.g. adding an annotations regarding each column is 1 year, and the segments to finish off the look.

## let's add a label for 1 column equals 1 year of age 

plot <- plot + 
  annotate(
    'text',
    x = 1985,
    y = 6,
    label = '1 year',
    angle = 90,
    size = 7,
    color = 'black',
    family = "Segoe Script"
  ) + 
  annotate(
    'text',
    x = 1988,
    y = 13,
    label = 'age',
    size = 5,
    color = 'black',
    family = "Segoe Script"
  ) +
  geom_segment(
    x = 1988.75,
    y = 13,
    xend = 1993,
    yend = 13,
    arrow = arrow(ends = 'last', length = unit(.01, units = 'npc')),
    color = 'black'
  ) +
  geom_segment(
    x = 1985,
    xend = 1985,
    y = 8,
    yend = 12,
    color = 'black'
  ) +
  geom_segment(
    x = 1985,
    xend = 1985,
    y = 1,
    yend = 4,
    color = 'black'
  ) +
  geom_segment(
    x = 1984.5,
    xend = 1985.5,
    y = 12,
    yend = 12,
    color = 'black'
  ) +
  geom_segment(
    x = 1984.5,
    xend = 1985.5,
    y = 1,
    yend = 1,
    color = 'black'
  ) 

plot
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

We’re almost there - now that we have all the annotations we want on there, we can remove the legend and use a theme to further remove the grid as well as the x and y axis.

plot <- plot +
  theme_void() +
  theme(
    legend.position = 'none'
  )

plot
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Let’s finish off this off by adding a title

## lets add a title
plot <- plot + 
  annotate(
    'text',
    x = 1987,
    y = -5,
    label = 'Michael Luu',
    size = 25,
    hjust = 0,
    fontface = 'bold.italic'
  )

plot
Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x$y, :
font family not found in Windows font database

Full resolution figure can be found here along with the github repo for the full code here

Session info

sessionInfo()
R version 4.2.2 (2022-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 22621)

Matrix products: default

locale:
[1] LC_COLLATE=English_United States.utf8 
[2] LC_CTYPE=English_United States.utf8   
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.utf8    

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] extrafont_0.19  ggsci_3.0.0     lubridate_1.9.2 forcats_1.0.0  
 [5] stringr_1.5.0   dplyr_1.1.1     purrr_1.0.1     readr_2.1.4    
 [9] tidyr_1.3.0     tibble_3.2.1    ggplot2_3.4.2   tidyverse_2.0.0

loaded via a namespace (and not attached):
 [1] compiler_4.2.2    pillar_1.9.0      tools_4.2.2       digest_0.6.31    
 [5] timechange_0.2.0  jsonlite_1.8.4    evaluate_0.19     lifecycle_1.0.3  
 [9] gtable_0.3.3      pkgconfig_2.0.3   rlang_1.1.0       cli_3.6.1        
[13] rstudioapi_0.14   yaml_2.3.6        xfun_0.38         fastmap_1.1.0    
[17] Rttf2pt1_1.3.12   withr_2.5.0       knitr_1.41        systemfonts_1.0.4
[21] hms_1.1.3         generics_0.1.3    vctrs_0.6.1       htmlwidgets_1.6.2
[25] grid_4.2.2        tidyselect_1.2.0  glue_1.6.2        R6_2.5.1         
[29] textshaping_0.3.6 fansi_1.0.4       rmarkdown_2.19    farver_2.1.1     
[33] extrafontdb_1.0   tzdb_0.3.0        magrittr_2.0.3    scales_1.2.1     
[37] htmltools_0.5.4   colorspace_2.1-0  renv_0.16.0       ragg_1.2.5       
[41] labeling_0.4.2    utf8_1.2.3        stringi_1.7.8     munsell_0.5.0    

Reuse

Citation

BibTeX citation:
@online{luu2020,
  author = {Luu, Michael},
  title = {My Life in {Months} - {Making} a “Life Plot” in {R} Using
    Ggplot2},
  date = {2020-10-07},
  langid = {en}
}
For attribution, please cite this work as:
Luu, Michael. 2020. “My Life in Months - Making a ‘Life Plot’ in R Using Ggplot2 .” October 7, 2020.