Heart Beats on Our Wedding Day

I love data, so I wanted to somehow plot my wedding day. My now-husband wore his Fitbit on his wrist and I wore mine on my ankle (you can see it in the picture below). Fast-forward to eight months later and I have finally gotten around to plotting out our heart rate data. I designed it to match our wedding invites.

Picture of me and my husband on our wedding day. We’re sitting on a couch and you can see my fitbit on my ankle.

Picture of our wedding invites and rings.

Let’s get into it.

# Load packages
library(jsonlite)
library(tidyverse)
library(lubridate)
library(slider)
library(jpeg)
library(patchwork)
library(extrafont)
library(showtext)

Data Wrangling

I used the jsonlite package to read in the heart rate data and format it nicely as a tibble. I then “normalized” our heart rates - mine runs about 5 bpm higher than my partner’s on average and I wanted them to have the same baseline. I then went through and annotated where we were for each timepoint - getting ready, at the church, at the rose garden, and at the reception. Because this data is all tied to time of day I found the lubridate package necessary. Finally, I used the slider package to calculate a rolling average of our heart rates to smooth the data a little bit.

# read in json files
hr_jj <- read_json("jj_heart_rate-2022-07-09.json", simplifyVector = TRUE) %>% 
  as_tibble() %>% mutate(Name = "JJ")
hr_rr <- read_json("rr_heart_rate-2022-07-09.json", simplifyVector = TRUE) %>% 
  as_tibble() %>% mutate(Name = "RR")

# normalize baselines
hr_jj_min <- hr_jj$value$bpm %>% sort() %>% .[1:10] %>% mean() %>% as.integer() - 60
hr_rr_min <- hr_rr$value$bpm %>% sort() %>% .[1:10] %>% mean() %>% as.integer() - 60 + 5
hr <- full_join(hr_jj, hr_rr) %>% 
  mutate(bpm_norm = case_when(Name == "JJ" ~ value$bpm - hr_jj_min,
                              Name == "RR" ~ value$bpm - hr_rr_min))

# annotate events
hr$dateTime <- hr$dateTime %>% mdy_hms()

getting_ready_start <- ymd_hm("2022-07-09 8:50AM")
ceremony_start <- ymd_hm("2022-07-09 1:00PM")
ceremony_end <- ymd_hm("2022-07-09 2:15PM")
rose_garden_start <- ymd_hm("2022-07-09 3:00PM")
rose_garden_end <- ymd_hm("2022-07-09 4:15PM")
reception_start <- ymd_hm("2022-07-09 4:30PM")
reception_end <- ymd_hm("2022-07-09 9:30PM")

hr <- hr %>% 
  mutate(events = case_when(dateTime > getting_ready_start & dateTime < ceremony_start ~ "getting ready",
                            dateTime > ceremony_start & dateTime < ceremony_end ~ "ceremony",
                            dateTime > rose_garden_start & dateTime < rose_garden_end ~ "rose garden",
                            dateTime > reception_start & dateTime < reception_end ~ "reception"))

hr$events <- hr$events %>% as_factor() %>% fct_relevel("getting ready", "ceremony", "rose garden", "reception")

# create rolling average
hr <- hr %>% 
  mutate(bpm_smooth = slide_dbl(bpm_norm, mean, .before = 10, .after = 10))
## # A tibble: 10 × 6
##    dateTime            value$bpm $confidence Name  bpm_norm events    bpm_smooth
##    <dttm>                  <int>       <int> <chr>    <dbl> <fct>          <dbl>
##  1 2022-07-09 08:50:04        70           3 JJ          70 getting …       70.4
##  2 2022-07-09 08:50:19        70           3 JJ          70 getting …       70.4
##  3 2022-07-09 08:50:34        71           3 JJ          71 getting …       70.4
##  4 2022-07-09 08:50:39        70           3 JJ          70 getting …       70.4
##  5 2022-07-09 08:50:44        69           3 JJ          69 getting …       70.4
##  6 2022-07-09 08:50:59        69           3 JJ          69 getting …       70.4
##  7 2022-07-09 08:51:04        70           3 JJ          70 getting …       70.4
##  8 2022-07-09 08:51:19        70           3 JJ          70 getting …       70.4
##  9 2022-07-09 08:51:29        71           3 JJ          71 getting …       70.4
## 10 2022-07-09 08:51:39        70           3 JJ          70 getting …       70.4

Making the Base Graph

We have to start somewhere: here is what the basic bpm vs time line graph looks like without much customization.

hr %>% 
  filter(dateTime < as.Date("2022-07-10")) %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name)) +
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p")

I knew I wanted to break the data up into time windows indicating the different parts of the day so I first tried using facet_wrap() to get a quick look at how that would shake out.

hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         !is.na(events)) %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name)) +
  facet_wrap(~events, nrow = 1, scales = "free_x") +
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p")

The facets really helped to contextualize the signal, which I liked, but because the different events were different lengths of time I needed the facets to be different sizes. For customizing at this degree I really like using the patchwork package which lets you stitch together individual graphs with a custom layout. Now instead of using one graph with facets, I’m stitching together 4 graphs and specifying the size of each.

getting_ready <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "getting ready") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name)) +
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p") +
  ylim(c(60,125)) +
  labs(title = "Getting Ready") +
  theme(axis.title.x = element_blank())

ceremony <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "ceremony") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name)) +
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p") +
  ylim(c(60,125)) +
  labs(title = "Ceremony") +
  theme(axis.title.x = element_blank(), axis.title.y = element_blank())

rose_garden <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "rose garden") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name)) +
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p") +
  ylim(c(60,125)) +
  labs(title = "Rose Garden") +
  theme(axis.title.y = element_blank())

reception <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "reception") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name)) +
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p") +
  ylim(c(60,125)) +
  labs(title = "Reception") +
  theme(axis.title.x = element_blank(), axis.title.y = element_blank())

layout <- c("AAAABCDDDDD")

getting_ready + ceremony + rose_garden + reception + plot_layout(design = layout, guides = "collect") & 
  theme(legend.position = "bottom")

This was where I started to see the bones of the final product: I started customizing.

Building the Final Graph

The final graph is actually two parts stitched together: the top part is the actual graph part, and the bottom part just contains the text and flowers images. I used patchwork again to stitch them together.

The “Graph” Graph

For the graph portion I basically used the same code as above, but started to adjust the theme heavily. I used the extrafont and showtext packages for fonts, making it easy to download and use fonts from Google Fonts. I talk more about how to do this in my Sick with Mono post. (I also talk about working with datetime data in the same post.) On top of the graph I also used text and arrow annotations in the graphs to label specific heart rate events.

colors <- c("RR" = "#DDCB68", "JJ" = "#D84F7D")
font_add_google("Montserrat") # default font
font_add_google("Codystar") # decorative dot font
showtext_auto(enable = TRUE)

getting_ready <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "getting ready") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name), size = 0.7) +
  ### add annotation text: friends arriving in the morning
  geom_text(data = tibble(), mapping = aes(x = ymd_hm("2022-07-09 10:59AM"), y = 95), label = toupper("friends arrive"), 
            hjust = 0, vjust = 0, size = 2, family = "Montserrat", color = "#F3EFEF") +
  annotate(geom = "curve", size = 0.3, color = "#F3EFEF",
           x = ymd_hm("2022-07-09 10:55AM"), y = 96, 
           xend = ymd_hm("2022-07-09 10:09AM"), yend = 85, 
           curvature = 0.4, arrow = arrow(length = unit(2, "mm"))) +
  ### 
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p", expand = c(0,0)) +
  ylim(c(60,125)) +
  labs(title = toupper("Getting Ready")) +
  scale_color_manual(values = colors) +
  theme_bw() +
  theme(axis.title.x = element_blank(),
        axis.text.y = element_text(family = "Montserrat", color = "#F3EFEF"),
        plot.title = element_text(family = "Montserrat", color = "#F3EFEF", size = 10, vjust = -2.5, lineheight = 0.8),
        legend.position = c(0.3,0.85),
        legend.direction = "horizontal",
        legend.title = element_blank(),
        legend.background = element_rect(fill = "transparent", color = "#F3EFEF", size = 0.7),
        legend.text = element_text(family = "Montserrat", color = "#F3EFEF"),
        legend.key = element_rect(fill = "transparent"))

ceremony <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "ceremony") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name), size = 0.7) +
  ### add annotation text: walking down the aisle
  geom_text(data = tibble(), mapping = aes(x = ymd_hm("2022-07-09 1:27PM"), y = 107), 
            label = str_wrap(toupper("walking down the aisle"), width = 8),
            hjust = 0.5, vjust = 0, size = 2, family = "Montserrat", color = "#F3EFEF", lineheight = 0.8) +
  annotate(geom = "curve", size = 0.3, color = "#F3EFEF",
           x = ymd_hm("2022-07-09 1:25PM"), y = 105,
           xend = ymd_hm("2022-07-09 1:25PM"), yend = 93,
           curvature = 0, arrow = arrow(length = unit(2, "mm"))) +
  ### 
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p") +
  ylim(c(60,125)) +
  labs(title = toupper("Ceremony")) +
  scale_color_manual(values = colors) +
  theme_bw() +
  theme(axis.title.x = element_blank(), axis.title.y = element_blank(),
        axis.text.y = element_blank(),
        plot.title = element_text(family = "Montserrat", color = "#F3EFEF", size = 10, vjust = -2.5, hjust = 0.5, lineheight = 0.8),
        legend.position = "none")

rose_garden <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "rose garden") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name), size = 0.7) +
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p") +
  ylim(c(60,125)) +
  labs(title = toupper("Rose\nGarden")) +
  scale_color_manual(values = colors) +
  theme_bw() +
  theme(axis.title.x = element_blank(), axis.title.y = element_blank(),
        axis.text.y = element_blank(),
        plot.title = element_text(family = "Montserrat", color = "#F3EFEF", size = 10, vjust = -1, lineheight = 0.8),
        legend.position = "none")

reception <- hr %>% 
  filter(dateTime < as.Date("2022-07-10"),
         events == "reception") %>% 
  ggplot(aes(x = dateTime, y = bpm_smooth)) +
  geom_line(aes(color = Name), size = 0.7) +
  
  ### add annotation text: tequila
  geom_text(data = tibble(), mapping = aes(x = ymd_hm("2022-07-09 5:28PM"), y = 70), 
            label = toupper("tequila"),
            hjust = 0.5, vjust = 0, size = 2, family = "Montserrat", color = "#F3EFEF") +
  annotate(geom = "curve", size = 0.3, color = "#F3EFEF",
           x = ymd_hm("2022-07-09 5:28PM"), y = 73,
           xend = ymd_hm("2022-07-09 5:28PM"), yend = 83,
           curvature = 0, arrow = arrow(length = unit(2, "mm"))) +
  ### 
  
  ### add annotation text: dancing
  geom_text(data = tibble(), mapping = aes(x = ymd_hm("2022-07-09 7:45PM"), y = 120), 
            label = toupper("dancing"),
            hjust = 1, vjust = 0, size = 2, family = "Montserrat", color = "#F3EFEF") +
  annotate(geom = "curve", size = 0.3, color = "#F3EFEF",
           x = ymd_hm("2022-07-09 7:48PM"), y = 120,
           xend = ymd_hm("2022-07-09 8:06PM"), yend = 105,
           curvature = -0.4, arrow = arrow(length = unit(2, "mm"))) +
  ### 
  
  ### add annotation text: couples pictures
  geom_text(data = tibble(), mapping = aes(x = ymd_hm("2022-07-09 7:00PM"), y = 75), 
            label = str_wrap(toupper("couples pictures downtown"), width = 8),
            hjust = 0.5, vjust = 0, size = 2, family = "Montserrat", color = "#F3EFEF", lineheight = 0.8) +
  annotate(geom = "curve", size = 0.3, color = "#F3EFEF",
           x = ymd_hm("2022-07-09 7:00PM"), y = 74,
           xend = ymd_hm("2022-07-09 8:37PM"), yend = 60,
           curvature = 0.3, arrow = arrow(length = unit(2, "mm"))) +
  ### 
  
  scale_x_datetime(date_breaks = "1 hour", date_labels = "%l%p", expand = c(0,0)) +
  ylim(c(60,125)) +
  labs(title = toupper("Reception")) +
  scale_color_manual(values = colors) +
  theme_bw() +
  theme(axis.title.x = element_blank(), axis.title.y = element_blank(),
        axis.text.y = element_blank(),
        plot.title = element_text(family = "Montserrat", color = "#F3EFEF", size = 10, vjust = -2.5, lineheight = 0.8),
        legend.position = "none")

# patching the individual graphs together
layout <- c("AAAABCDDDDD")

pltA <- getting_ready + ceremony + rose_garden + reception + plot_layout(design = layout) & 
  labs(y = toupper("Heart Rate (bpm)")) &
  theme(plot.background = element_rect(fill = "#1E3D42", color = "#1E3D42"),
        panel.background = element_rect(fill = "#1E3D42", color = "#1E3D42"),
        panel.border = element_rect(color = "#F3EFEF", size = 1),
        axis.text.x = element_text(family = "Montserrat", color = "#F3EFEF"),
        axis.title = element_text(family = "Montserrat", color = "#F3EFEF"),
        axis.ticks = element_line(color = "#F3EFEF"),
        panel.grid = element_blank())

pltA

The “Image” Graph

For the decorative portion below the graph I created an “empty” graph using the ggplot theme theme_void(). I used ggplot::geom_text() for the text portions and patchwork::inset_element() to add the images. The images I had were just screen grabs from our invite mockups. These were jpeg files that I read in with jpeg::readJPEG() and converted to grobs using grid::rasterGrob(). patchwork doesn’t make it easy to flip an image, so I did that outside of R and used two separate images for the two insets.

image_plt <-  ggplot() +
  geom_text(aes(x = 0.5, y = 0), label = toupper("- July 9, 2022 -"),
            hjust = 0.5, vjust = 0, size = 4, family = "Montserrat", color = "#F3EFEF") +
  
  geom_text(aes(x = 0.5, y = 0.35), label = toupper("JJ + RR"),
            hjust = 0.5, vjust = 0, size = 25, family = "Codystar", color = "#F3EFEF") +
  geom_text(aes(x = 0.5, y = 0.15), label = toupper("Jennifer Jahncke & Ramiro Romo Jr."),
            hjust = 0.5, vjust = 0, size = 4, family = "Montserrat", color = "#F3EFEF") +
  
  coord_cartesian(ylim = c(0,1), xlim = c(0.05,1)) +
  inset_element(p = readJPEG("flowers-mirror.jpg", native = FALSE) %>% grid::rasterGrob(), 
                left = -0.05,
                bottom = 0,
                right = 0.25,
                top = 1,) + 
  inset_element(p = readJPEG("flowers.jpg", native = FALSE) %>% grid::rasterGrob(), 
                left = 0.7,
                bottom = 0,
                right = 1,
                top = 1) & 
  theme_void() +
  theme(plot.background = element_rect(fill = "#1E3D42", color = "#1E3D42"),
        panel.background = element_rect(fill = "#1E3D42", color = "#1E3D42"))

image_plt

Putting them together

Now that I have the two separate components (pltA and image_plt), I used patchwork to stitch them together and add a title. I technically made the title as a subtitle but that was just so that I could adjust the theme of the title-title without affecting the titles of the subgraphs. This was lazy - really the titles of the subgraphs should be subtitles and the title of the overall graph should be a title but because of how I had put the graphs together prior to getting to this part I just used this workaround. And here is the final graph!

pltA / image_plt + plot_layout(heights = c(3,2)) + 
  plot_annotation(subtitle = toupper("Heart Beats on Our Wedding Day")) & 
  theme(plot.background = element_rect(fill = "#1E3D42", color = "#1E3D42"),
        plot.subtitle = element_text(family = "Codystar", color = "#F3EFEF", size = 33, hjust = 0.5, vjust = -1.5, face = "bold"))

Jennifer Jahncke
Jennifer Jahncke
Postdoctoral Scholar

I am a neuroscientist studying how brain cells form and maintain connections.

Related