Building GitHub style timeseries charts for some tracked acitivites in 2025, building custom functions for rounded tiles, and interactivity with {ggiraph}.
Data Visualization
#TidyTuesday
Interactive
{ggiraph}
Author
Aditya Dahiya
Published
January 8, 2026
A geom_tile() customized to produce rounded corners, and three facets (three habits tracked) with opacity reflecting intensity of activity on a particular date.
Loading Libraries
Code
# Loading the relevant packageslibrary(tidyverse) # Data Wranglinglibrary(gt) # Displaying beautiful tableslibrary(ggiraph) # Interactive Graphicslibrary(showtext) # Display fancy text in ggplot2library(fontawesome) # Icons and Fonts library(janitor) # Cleaning tidying raw datalibrary(patchwork) # Composing plots in Rlibrary(ggtext) # Displaying custom text in ggplot2library(paletteer) # Load colour palettes
Visualization Parameters
Code
# Font for titlesfont_add_google("Oswald",family ="title_font") # Font for the captionfont_add_google("Saira Extra Condensed",family ="caption_font") # Font for plot textfont_add_google("Saira Condensed",family ="body_font") showtext_auto()mypal <-c("#FFA400", "#EF3B2C", "#41AB5D", "grey30", "grey40")# A base Colourbg_col <-"white"seecolor::print_color(bg_col)# Colour for highlighted texttext_hil <- mypal[5]seecolor::print_color(text_hil)# Colour for the texttext_col <- mypal[4]seecolor::print_color(text_col)# Define Base Text Sizebts <-90# Caption stuff for the plotsysfonts::font_add(family ="Font Awesome 6 Brands",regular = here::here("docs", "Font Awesome 6 Brands-Regular-400.otf"))github <-""github_username <-"aditya-dahiya"xtwitter <-""xtwitter_username <-"@adityadahiyaias"social_caption_1 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{github};</span> <span style='color: {text_hil}'>{github_username} </span>")social_caption_2 <- glue::glue("<span style='font-family:\"Font Awesome 6 Brands\";'>{xtwitter};</span> <span style='color: {text_hil}'>{xtwitter_username}</span>")plot_caption <-paste0("**Data & Code:** ", social_caption_1, " | **Graphics:** ", social_caption_2 )rm(github, github_username, xtwitter, xtwitter_username, social_caption_1, social_caption_2)
Loading the data, E.D.A., and cleaning the names
Code
###################################################################### Set the working directory (I didnt want to host raw data on GitHub)#####################################################################list.files("loop_habits")# Define the directory containing the CSV filesdirectory <-"loop_habits"# List all CSV files in the directorycsv_files <-list.files(path = directory, pattern ="\\.csv$", full.names =TRUE )# Extract base names (without file extension) to use as object namesfile_names <- tools::file_path_sans_ext(basename(csv_files)) |>tolower()# Read each CSV file and assign it to an object with the corresponding namepurrr::walk2( csv_files, file_names, ~assign(.y, read_csv(.x), envir = .GlobalEnv) )# Apply janitor::clean_names() to all created objectspurrr::walk( file_names, ~ { cleaned_data <-get(.x) |>clean_names() # Retrieve object, clean namesassign(.x, cleaned_data, envir = .GlobalEnv) # Reassign cleaned data back to the same object } )# Remove the temporary filesrm(csv_files, directory, file_names)
Cleaning up the data and keeping only the relevant variables
Write a custom function geom_rtile() for plotting geom_tile() with rounded corners
Code
# Custom functions: creating a geom_rtile()# Credits: https://stackoverflow.com/questions/64355877/round-corners-in-ggplots-geom-tile-possible`%||%`<-function(a, b) {if (is.null(a)) b else a}GeomRtile <-ggproto("GeomRtile", statebins:::GeomRrect, # 1) only change compared to ggplot2:::GeomTileextra_params =c("na.rm"),setup_data =function(data, params) { data$width <- data$width %||% params$width %||%resolution(data$x, FALSE) data$height <- data$height %||% params$height %||%resolution(data$y, FALSE)transform(data,xmin = x - width /2, xmax = x + width /2, width =NULL,ymin = y - height /2, ymax = y + height /2, height =NULL ) },default_aes =aes(fill ="grey20", colour =NA, size =0.1,alpha =NA, width =NA, height =NA ),required_aes =c("x", "y"),# These aes columns are created by setup_data(). They need to be listed here so# that GeomRect$handle_na() properly removes any bars that fall outside the defined# limits, not just those for which x and y are outside the limitsnon_missing_aes =c("xmin", "xmax", "ymin", "ymax"),draw_key = draw_key_polygon)geom_rtile <-function(mapping =NULL, data =NULL,stat ="identity", position ="identity",radius = grid::unit(6, "pt"), # 2) add radius argument ...,na.rm =FALSE,show.legend =NA,inherit.aes =TRUE) {layer(data = data,mapping = mapping,stat = stat,geom = GeomRtile, # 3) use ggproto object hereposition = position,show.legend = show.legend,inherit.aes = inherit.aes,params = rlang::list2(radius = radius,na.rm = na.rm, ... ) )}
Plotting in {ggplot2} with facets - a static graphic
Code
bts =90# Add text to plot-------------------------------------------------plot_title <-"Habits tracking (2025)"plot_subtitle <-str_wrap("2025 was transformative! Atomic Habits (read in June 2024) and Can’t Hurt Me (read in Oct 2024) helped me focus on my daily habits over the entire year, with some amazing results for both health and family.", 70)df1 <- checkmarks1 |>filter(year ==2025) |>select( date, day_number, month_year, week_number, day_of_week, walk, exercise, social ) |>pivot_longer(cols =c(walk, exercise, social),names_to ="activity",values_to ="value" ) |>group_by(activity) |>mutate(alpha_var = value /max(value, na.rm =TRUE) ) |>ungroup() |>mutate(activity =case_when( activity =="walk"~"Walking everyday\n(minutes per day)", activity =="exercise"~"Kids' studies\n(minutes per day)", activity =="sleep"~"Hours spent sleeping each night", activity =="social"~"Socializing\n(approx time each day)" ) ) |>group_by(activity) |>arrange(desc(alpha_var)) |>mutate(rank_num =row_number(),text_var =if_else( rank_num <=35,as.character(value),"" ) ) |>select(-rank_num)g <- df1 |>ggplot(mapping =aes(x = week_number,y = day_of_week,fill = activity, alpha = alpha_var ) ) +geom_rtile(radius =unit(bts/20, "pt"),colour = bg_col,linewidth =1 ) +geom_text(mapping =aes(label = text_var ),size = bts /10,family ="body_font",hjust =0.5,vjust =0,nudge_y =0.05,colour = bg_col ) +geom_text(data = df1 |>filter(text_var !=""),mapping =aes(label =paste0(parse_number(date), " ", month_year ) ),size = bts /15,family ="body_font",hjust =0.5,vjust =0,nudge_y =-0.15 ) +coord_fixed(clip ="off") +scale_x_continuous(limits =c(0, 54),expand =expansion(c(0, 0)),breaks =c(3, 11, 17, 25, 35, 45, 52),labels =c("Jan", "Mar", "Apr", "Jun", "Sep", "Nov", "Dec") ) +scale_y_discrete(breaks = checkmarks$day_of_week |>levels(),limits = checkmarks$day_of_week |>levels(),labels = checkmarks$day_of_week |>levels(),expand =expansion(0) ) + paletteer::scale_fill_paletteer_d("MetBrewer::Egypt") +# paletteer::scale_fill_paletteer_c(# "grDevices::Purple-Yellow",# direction = -1,# trans = "sqrt"# ) +scale_alpha_continuous(range =c(0, 1),na.value =0 ) +facet_wrap(~activity,ncol =1 ) +guides(fill ="none",alpha ="none" ) +labs(x =NULL, y =NULL,title = plot_title,subtitle = plot_subtitle,caption = plot_caption ) +theme_minimal(base_family ="body_font",base_size = bts ) +theme(# Overall Plotlegend.position ="bottom",legend.key.height =unit(bts /80, "mm"),legend.key.width =unit(bts /2, "mm"),panel.grid =element_blank(),plot.title.position ="plot",# All marginsplot.margin =margin(10,0,10,0, "mm"),panel.background =element_blank(),panel.border =element_blank(),# All texts appearing in the plottext =element_text(colour = text_col,margin =margin(0,0,0,0, "mm"),hjust =0.5, vjust =0.5 ),plot.title =element_text(margin =margin(10,0,5,0, "mm"),hjust =0.5,colour = text_hil,size = bts *3.5,lineheight =0.3,face ="bold" ),plot.subtitle =element_text(margin =margin(5,0,5,0, "mm"),hjust =0.5,lineheight =0.3, family ="body_font",colour = text_hil,size = bts *2 ),plot.caption =element_textbox(hjust =0.5,margin =margin(15,0,5,0, "mm"),colour = text_hil,family ="caption_font" ),strip.text =element_text(margin =margin(5,0,5,0, "mm"),hjust =0.5, colour = text_hil,size =1.5* bts,lineheight =0.3 ),axis.text.y =element_text(margin =margin(0,-2,0,0, "mm") ),axis.text.x =element_text(margin =margin(5,0,0,0, "mm"),size =1.2* bts ),axis.ticks =element_blank(),axis.ticks.length =unit(0, "mm") )ggsave(filename = here::here("data_vizs","tidy_github_2025_activity1.png" ),plot = g,width =400,height =500,units ="mm",bg = bg_col)