ggplot2
Misc
Packages
- {ggreveal} - Reveal a ggplot Incrementally (e.g. groups, facets)
- {thematic} - Enables automatic styling of R plots in Shiny, R Markdown, and RStudio
- {hrbrthemes} - hrbrmstr’s collection of themes
- Has 49 dependencies. A zero-dependency
theme_ipsom_rc
(roboto condensed font) can be had in {tinythemes}
- Has 49 dependencies. A zero-dependency
- {{brand_yml}} - Create reports, apps, dashboards, plots and more that match your company’s brand guidelines with a single
_brand.yml
file- For Quarto dashboards, websites, reports, etc.
- For Python shiny, seaborn, matplotlib, etc.
- For R shiny, ggplot2, plot, RMarkdown
- {ggblanket} - Similar ggplot ui but with simpler structure. Stuff that would require multiple geoms are replaced with one geom and basic arguments. Compatible with popular ggplot extensions
Resources
- Docs, Aesthetic Specifications, A Complete Guide to Scales
- ggplot2: Elegant Graphics for Data Analysis (Wickam, Navarro, Pederson)
- Modern Data Visualization with R
- R Graphics Cookbook
- ggplot2 extension cookbook - step-by-step, how-to extension examples (mostly Stat/geom_*)
Don’t use stat calculating geoms and set axis limits with scale_y_continuous
- See examples of the behavior in this thread
Defaults for any {ggplot2} geom using the default_aes field (i.e.
GeomBlah$default_aes
)Basic Function
<- viz_monthly function(df, y_var, threshhold = NULL) { ggplot(df, aes(x = .data[["day"]], y = .data[[y_var]])) + geom_line() + geom_hline(yintercept = threshhold, color = "red", linetype = 2) + scale_x_continuous(breaks = seq(1, 29, by = 7)) + theme_minimal() }
Storing geom settings in a dataframe (source)
<- data.frame(x = seq(from = 1, to = 20, by = 1), df y = rnorm(20, mean = 50, sd = 4), limits = "c(0,30)") ggplot(data = df, aes(x = x, y = y)) + geom_line() + scale_y_continuous(limits= eval(parse(text=df$limits)))
FIltering Data in Geoms (source)
|> mtcars rownames_to_column() |> ggplot(aes(mpg, hp)) + geom_point() + geom_label( aes(label = rowname), data = ~slice_max(.x, hp,n = 2), nudge_x = 3 )
Dynamic Axis Limits (source)
ggplot(...) + + ... scale_x_continuous( breaks = seq(2021, 2023, 1), limits = c(min(df$year) - .1, max(df$year) + .1), position = "top", labels = c("Baseline", "2022", "2023") )
Themes
Docs
Theme Elements Cheatsheet (source)
Use the same font family to all
geom_text
andgeom_label
elementsupdate_geom_defaults("text", list(family = "Public Sans")) update_geom_defaults("label", list(family = "Public Sans"))
geom_text
(and probablygeom_label
) is not impacted by theme settings, so setting this option keeps you from having to manually add the font family to each instance.- There’s also an
update_stat_defaults
Setting color defaults (source)
# Discrete with a list options(ggplot2.discrete.colour = list_of_colors) # Continuous with a function options( ggplot2.continuous.colour = \(...){ ::scale_color_scico( scicopalette = "batlow", ... ) } ) # Ordinal with a function options( ggplot2.ordinal.colour = \(...){ scale_color_viridis_d( option = "G", direction = -1, ... ) } )
- If you have a particular set of palettes you like to use for projects, these options can be put into a R file and loaded via
source
e.g.source(here::here("_defaults.R"))
- If you have a particular set of palettes you like to use for projects, these options can be put into a R file and loaded via
Basic Syntax
<- function() { theme_psc theme_void(base_family = "Open Sans") + theme() }
Use Utility FIles
utils/base-theme.R (source)
## R/themes/base_theme.R #' Get default theme colors #' @param palette Optional custom color palette #' @return List of theme colors <- function(palette = NULL) { get_theme_colors list( background = "#f5f5f2", title = "gray20", subtitle = "gray30", text = "gray30", caption = "gray40", palette = palette # This can be overridden weekly ) } #' Create base theme with consistent elements #' @param colors List of colors from get_theme_colors() #' @return ggplot theme object <- function(colors = get_theme_colors()) { create_base_theme # Only include the truly fixed elements that don't change weekly theme_minimal(base_size = 14, base_family = fonts$text) + theme( plot.title.position = "plot", plot.caption.position = "plot", legend.position = "plot", # Background elements plot.background = element_rect(fill = colors$background, color = colors$background), panel.background = element_rect(fill = colors$background, color = colors$background), # Common margins plot.margin = margin(t = 10, r = 20, b = 10, l = 20), # Text elements that stay consistent axis.text = element_text( family = fonts$text, size = rel(0.79), color = colors$text ),axis.title = element_text( family = fonts$text, size = rel(0.93), face = "bold", color = colors$text ) # Note: specific theme elements should be added in the scripts ) } #' Extend base theme with customizations #' @param base_theme Existing theme to extend #' @param new_theme_elements List of theme elements specific to this project #' @return Modified ggplot theme <- function(base_theme, new_theme_elements) { extend_base_theme + new_theme_elements base_theme }
script.R (source)
source(here::here("utils/base_theme.R")) # Get base colors with custom palette <- colors get_theme_colors(palette = c("#f7fbff", "#9ecae1", "#2171b5", "#084594")) <- create_base_theme(colors) base_theme <- extend_base_theme( project_theme base_theme,theme( # project-specific modifications axis.line.x = element_line(color = "#252525", linewidth = .2), panel.spacing.x = unit(2, 'lines'), panel.spacing.y = unit(1, 'lines'), panel.grid.major.x = element_blank(), panel.grid.major.y = element_line(color = alpha(colors[5], 0.2), linewidth = 0.2), panel.grid.minor = element_blank(), ) ) theme_set(project_theme)
Transparent Background
+ p theme( panel.background = element_rect(fill='transparent'), #transparent panel bg plot.background = element_rect(fill='transparent', color=NA), #transparent plot bg panel.grid.major = element_blank(), #remove major gridlines panel.grid.minor = element_blank(), #remove minor gridlines legend.background = element_rect(fill='transparent'), #transparent legend bg legend.box.background = element_rect(fill='transparent') #transparent legend panel )ggsave('myplot.png', p, bg='transparent')
theme_notebook
<- function(...) { theme_notebook theme_minimal() + theme( plot.title = element_text(family = "Anybody", face = "bold", size = 18), plot.subtitle = element_text(family = "Anybody", size = 14), axis.title = element_text(family = "Anybody", size = 12), axis.text = element_text(family = "Anybody", size = 10), panel.background = element_rect(fill='#FFFDF9FF'), panel.grid.minor = element_blank(), plot.background = element_rect(fill='#FFFDF9FF', color = '#FFFDF9FF'), legend.background = element_rect(color = '#FFFDF9FF', fill='#FFFDF9FF'), legend.box.background = element_rect(fill='#FFFDF9FF', color = '#FFFDF9FF'), ... ) }
Andrew Heiss (source)
<- function() { theme_public theme_minimal(base_family = "Public Sans") + theme( panel.grid.minor = element_blank(), plot.title = element_text(family = "Public Sans", face = "bold", size = rel(1.25)), plot.subtitle = element_text(family = "Public Sans Light", face = "plain"), plot.caption = element_text(family = "Public Sans Light", face = "plain"), axis.title = element_text(family = "Public Sans Semibold", size = rel(0.8)), axis.title.x = element_text(hjust = 0), axis.title.y = element_text(hjust = 1), strip.text = element_text( family = "Public Sans Semibold", face = "plain", size = rel(0.8), hjust = 0 ),strip.background = element_rect(fill = "grey90", color = NA), legend.title = element_text(family = "Public Sans Semibold", size = rel(0.8)), legend.text = element_text(size = rel(0.8)), legend.position = "bottom", legend.justification = "left", legend.title.position = "top", legend.margin = margin(l = 0, t = 0) ) }
- Don’t think he’s particular about this font. He was just working with government survey data and that’s the font they use.
Cedric Sherer (article)
theme_set(theme_minimal(base_size = 15, base_family = "Anybody")) theme_update( axis.title.x = element_text(margin = margin(12, 0, 0, 0), color = "grey30"), axis.title.y = element_text(margin = margin(0, 12, 0, 0), color = "grey30"), panel.grid.minor = element_blank(), panel.border = element_rect(color = "grey45", fill = NA, linewidth = 1.5), panel.spacing = unit(.9, "lines"), strip.text = element_text(size = rel(1)), plot.title = element_text(size = rel(1.4), face = "bold", hjust = .5), plot.title.position = "plot" )
Eric Bickel
::theme_set( ggplot2theme_bw(base_size = 12) + theme( plot.title = element_text(face = 'bold', hjust = 0), text = element_text(colour = '#4e5c65'), panel.background = element_rect('#ffffff'), strip.background = element_rect('#ffffff', colour = 'white'), plot.background = element_rect('#ffffff'), panel.border = element_rect(colour = '#ffffff'), panel.grid.major.x = element_blank(), panel.grid.major.y = element_blank(), panel.grid.minor.y = element_blank(), legend.background = element_rect('#ffffff'), legend.title = element_blank(), legend.position = 'right', legend.direction = 'vertical', legend.key = element_blank(), strip.text = element_text(face = 'bold', size = 10), axis.text = element_text(face = 'bold', size = 9), axis.title = element_blank(), axis.ticks = element_blank() )
Bespoke
Comparison Plot (source)
-
Code
library(tidyverse) library(scales) <- comparison_plot function( df, highlight_town, value_type, highlight_color ) { <- plot |> df ggplot( aes( x = value, y = 1 )+ ) # Light gray lines for all towns geom_point( shape = 124, color = "gray80" + ) # Line for town to highlight geom_point( data = df |> filter(location == highlight_town), shape = 124, color = highlight_color, size = 10 ) if (value_type == "percent") { <- plot + final_plot scale_x_continuous( labels = percent_format(accuracy = 1) ) } if (value_type == "number") { <- plot + final_plot scale_x_continuous( labels = comma_format(accuracy = 1) ) } } comparison_plot( df = single_family_homes, highlight_town = "Hartford", value_type = "percent", highlight_color = psc_blue + ) theme_whatever() <- big_number_plot function(value, text, value_color ) {ggplot() + # Add value geom_text( aes( x = 1, y = 1, label = value ),color = value_color, fontface = "bold", size = 20, hjust = 0 + ) # Add text geom_text( aes( x = 1, y = 2, label = str_to_upper(text) ),color = "gray70", size = 7, hjust = 0 ) } big_number_plot( value = "19%", text = "Single-Family Homes as\nPercent of All Homes", value_color = psc_blue + ) theme_whatever() # should start with theme_void
-
Smooth Line Chart (source)
Code
= data.frame(x = -1:10, y = c(-3,2,1,2,5,3,6,7,3,6,10,11)) df = \(formula, data) { smooth_spline_x = model.matrix(formula[-2], data) X # this is dumb but good enough for a hack %*% rep(1, ncol(X)) X } = \(formula, data, ..., weights = NULL) { smooth_spline = smooth_spline_x(formula, data) x = eval(formula[[2]], data, environment(formula)) y structure( splinefun(x, y, ...), class = c("smooth_spline", "function"), formula = formula ) } = \(object, newdata, ..., weights = NULL, se.fit = FALSE) { predict.smooth_spline = object(smooth_spline_x(attr(object, "formula"), newdata)) fit if (se.fit) { list(fit = data.frame(fit = fit, lwr = 0, upr = 0), se.fit = NA_real_) else { } fit } } |> df ggplot(aes(x, y)) + stat_smooth(formula = y ~ x, method = smooth_spline) + geom_point()
Comet (source)
Code
library(tidyverse) library(jsonlite) library(ggtext) library(ggforce) library(extrafont) # Custom ggplot2 theme <- function () { theme_f5 theme_minimal(base_size = 9, base_family="Roboto") %+replace% theme( plot.background = element_rect(fill = 'floralwhite', color = "floralwhite"), panel.grid.minor = element_blank(), plot.subtitle = element_text(color = 'gray65', hjust = 0, margin=margin(0,0,10,0)) ) } # function for getting team data from pbpstats.com <- function(season) { get_data <- paste0("https://api.pbpstats.com/get-totals/nba?Season=", season, "&SeasonType=Regular%2BSeason&StartType=All&GroupBy=Season&Type=Team") url <- fromJSON(url) x # select variables we care about <- x$multi_row_table_data %>% df select(TeamAbbreviation, OffPoss, PenaltyOffPoss) %>% mutate(bonus_pct = PenaltyOffPoss / OffPoss, season = season) return(df) } # get data from 2023-24 and 2024-25 <- map_df(c("2023-24", "2024-25"), get_data) dat <- dat %>% df # select relevant variables select(TeamAbbreviation, bonus_pct, season) %>% # pivot data wide pivot_wider(names_from = season, values_from = bonus_pct) %>% # rename columns rename(abbr = TeamAbbreviation, bonus_pct_24 = `2023-24`, bonus_pct_25 = `2024-25`) %>% # create an indicator variable that tells us if a team is spending more or less time in bonus mutate(more_less = ifelse(bonus_pct_25 - bonus_pct_24 >= 0, "More Often", "Less Often"), # convert abbr to a factor and order data by bonus time in 2024-25 abbr = as.factor(abbr), abbr = fct_reorder(abbr, bonus_pct_25)) <- df %>% p ggplot() + geom_link( aes(x = bonus_pct_24, xend = bonus_pct_25, y = abbr, yend = abbr, color = more_less, linewidth = after_stat(index)), n = 1000) + geom_point( aes(x = bonus_pct_25, y = abbr, color = more_less), shape = 21, fill = "white", size = 3.5 + ) scale_color_manual(values = c("#E64B35FF", "#00A087FF")) + scale_linewidth(range = c(.01, 4)) + scale_x_continuous(limits = c(.175, .325), breaks = seq(0, .35, .025), labels = scales::percent_format(.1)) + coord_cartesian(clip = 'off') + theme_f5() + theme(legend.position = 'none', plot.title = element_markdown(size = 11, face = 'bold', family = 'Roboto'), plot.title.position = 'plot') + labs( title = "Which Teams Are Spending <span style='color:#00A087FF'>**More**</span> or <span style='color:#E64B35FF'>**Less**</span> Time In the Bonus vs. Last Season?", subtitle = "Teams sorted by percentage of possessions spent in the bonus in 2024-25", x = "Percentage of Possessions in Bonus", y = "") ggsave(plot = p, "bonus_pct_change.png", h = 6, w = 6, dpi = 1000, device = grDevices::png)