#' Civilytics ggplot2 theme #' #' A complete ggplot2 theme built on [ggplot2::theme_grey()] using the #' Civilytics brand color palette and typography. Requires ggplot2 >= 4.0.0 #' for the `ink`, `paper`, and `accent` base-theme parameters. #' #' Produces an editorial, Pew-style layout: visible x-axis line to ground #' the data, light horizontal gridlines for reference, no panel border or #' y-axis line. Plot title and caption are left-aligned to the full plot #' region. #' #' Brand fonts (Inter for UI text, Libre Franklin for titles) are loaded #' automatically via [showtext] when the package is attached. Call #' [civilytics_load_fonts()] to reload them if needed. #' #' @param font_size Numeric. Base font size in points. Default `14`. #' @param font_family Character. Font family for body/axis text. Default #' `"Inter"` (loaded via showtext). #' @param title_family Character. Font family for plot titles and strip labels. #' Default `"Libre Franklin"` (loaded via showtext). #' @param line_size Numeric. Base line width. Default `0.5`. #' @param rel_small Numeric. Scale factor for small text relative to #' `font_size`. Default `12/14`. #' @param rel_tiny Numeric. Scale factor for tiny text relative to `font_size`. #' Default `11/14`. #' @param rel_large Numeric. Scale factor for large text (titles) relative to #' `font_size`. Default `20/14` (~1.43x), matching the Civilytics editorial #' design system. #' @param ink Character. Hex code for foreground/text color. Defaults to #' [civilytics_colors]`["ink"]` (`#0E1A2B`). #' @param paper Character. Hex code for background color. Defaults to #' [civilytics_colors]`["paper"]` (`#FAF7F2`). #' @param accent Character. Hex code for accent/highlight color. Defaults to #' [civilytics_colors]`["ember_600"]` (`#C25311`). #' @param strip_color Character. Hex code for facet strip background. Defaults #' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`). #' @param grid Character. Which major gridlines to draw: `"y"` (default, #' horizontal only), `"x"` (vertical only), `"both"`, or `"none"`. #' @param paper_bg Logical. If `FALSE` (default), the plot and panel #' backgrounds are transparent (`fill = NA`). Set to `TRUE` to fill them #' with the warm `paper` color (the Civilytics cream canvas). #' #' @section Font size hierarchy: #' All text sizes are derived from `font_size` using relative scale factors. #' At the default `font_size = 14`: #' #' | Element | Scale factor | Default size | #' |:--------|:-------------|:-------------| #' | Plot title | `rel_large` (1.43x) | ~20 pt | #' | Subtitle | 1.0x | 14 pt | #' | Axis text (tick labels) | `rel_small` (0.86x) | ~12 pt | #' | Axis titles | `rel_small` (0.86x) | ~12 pt | #' | Legend text | `rel_small` (0.86x) | ~12 pt | #' | Caption | `rel_tiny` (0.79x) | ~11 pt | #' | Legend title | `rel_tiny` (0.79x) | ~11 pt | #' | Strip text (facets) | `rel_small` (0.86x) | ~12 pt | #' #' To uniformly scale all text, change `font_size`. To adjust only the title #' prominence, change `rel_large`. When using [civilytics_logo()] to add a #' logo below the plot, pass `font_scale` to compensate for viewport #' shrinkage. #' #' @return A complete ggplot2 [ggplot2::theme()] object. #' @export #' #' @examples #' \dontrun{ #' library(ggplot2) #' #' # Transparent background (default) — composites cleanly onto any surface #' ggplot(mpg, aes(displ, hwy)) + #' geom_point() + #' theme_civilytics() #' #' # Warm paper canvas (opt-in) #' ggplot(mpg, aes(displ, hwy)) + #' geom_point() + #' theme_civilytics(paper_bg = TRUE) #' #' # With both gridlines and brand colors #' ggplot(mpg, aes(displ, hwy, colour = class)) + #' geom_point() + #' scale_color_civilytics() + #' theme_civilytics(grid = "both") #' #' # Larger text for poster or display #' ggplot(mpg, aes(displ, hwy)) + #' geom_point() + #' theme_civilytics(font_size = 18) #' } theme_civilytics <- function( font_size = 14, font_family = CV_FONT_SANS, title_family = CV_FONT_DISPLAY, line_size = 0.5, rel_small = 12 / 14, rel_tiny = 11 / 14, rel_large = 20 / 14, ink = unname(civilytics_colors["ink"]), paper = unname(civilytics_colors["paper"]), accent = unname(civilytics_colors["ember_600"]), strip_color = unname(civilytics_colors["paper_2"]), grid = c("y", "x", "both", "none"), paper_bg = FALSE) { grid <- match.arg(grid) half_line <- font_size / 2 small_size <- rel_small * font_size rule_color <- unname(civilytics_colors["rule"]) ink_2 <- unname(civilytics_colors["ink_2"]) ink_3 <- unname(civilytics_colors["ink_3"]) bg_color <- if (isTRUE(paper_bg)) paper else NA # Grid line elements grid_line <- ggplot2::element_line(color = rule_color, linewidth = 0.35) no_line <- ggplot2::element_blank() ggplot2::theme_grey( base_size = font_size, base_family = font_family, ink = ink, paper = paper, accent = accent ) %+replace% ggplot2::theme( line = ggplot2::element_line( color = ink, linewidth = line_size, linetype = 1, lineend = "butt" ), rect = ggplot2::element_rect( fill = NA, color = NA, linewidth = line_size, linetype = 1 ), text = ggplot2::element_text( family = font_family, face = "plain", color = ink, size = font_size, hjust = 0.5, vjust = 0.5, angle = 0, lineheight = 0.9, margin = ggplot2::margin(), debug = FALSE ), # -- Axes -- axis.line = ggplot2::element_blank(), axis.line.x = ggplot2::element_line( color = ink, linewidth = 0.6, lineend = "square" ), axis.line.y = ggplot2::element_blank(), axis.text = ggplot2::element_text( color = ink_2, size = ggplot2::rel(rel_small) ), axis.text.x = ggplot2::element_text( margin = ggplot2::margin(t = small_size / 4), vjust = 1 ), axis.text.x.top = ggplot2::element_text( margin = ggplot2::margin(b = small_size / 4), vjust = 0 ), axis.text.y = ggplot2::element_text( margin = ggplot2::margin(r = small_size / 4), hjust = 1 ), axis.text.y.right = ggplot2::element_text( margin = ggplot2::margin(l = small_size / 4), hjust = 0 ), axis.ticks = ggplot2::element_line( color = ink_3, linewidth = 0.4 ), axis.ticks.length = ggplot2::unit(4, "pt"), axis.title.x = ggplot2::element_text( size = ggplot2::rel(rel_small), color = ink_3, margin = ggplot2::margin(t = 10), vjust = 1 ), axis.title.x.top = ggplot2::element_text( size = ggplot2::rel(rel_small), color = ink_3, margin = ggplot2::margin(b = half_line / 2), vjust = 0 ), axis.title.y = ggplot2::element_text( size = ggplot2::rel(rel_small), color = ink_3, angle = 90, margin = ggplot2::margin(r = 10), vjust = 1 ), axis.title.y.right = ggplot2::element_text( size = ggplot2::rel(rel_small), color = ink_3, angle = -90, margin = ggplot2::margin(l = half_line / 2), vjust = 0 ), # -- Legend -- legend.background = ggplot2::element_blank(), legend.spacing = ggplot2::unit(font_size, "pt"), legend.spacing.x = NULL, legend.spacing.y = NULL, legend.margin = ggplot2::margin(0, 0, 4, 0), legend.key = ggplot2::element_blank(), legend.key.size = ggplot2::unit(12, "pt"), legend.key.height = NULL, legend.key.width = NULL, legend.text = ggplot2::element_text( size = ggplot2::rel(rel_small), color = ink_2 ), legend.title = ggplot2::element_text( hjust = 0, face = "bold", size = ggplot2::rel(rel_tiny), color = ink_3 ), legend.position = "top", legend.direction = NULL, legend.justification = c("left", "center"), legend.box = NULL, legend.box.margin = ggplot2::margin(0, 0, 0, 0), legend.box.background = ggplot2::element_blank(), legend.box.spacing = ggplot2::unit(font_size, "pt"), # -- Panel -- panel.background = ggplot2::element_rect(fill = bg_color, color = NA), panel.border = ggplot2::element_blank(), panel.grid.minor = ggplot2::element_blank(), panel.grid.major.x = if (grid %in% c("x", "both")) grid_line else no_line, panel.grid.major.y = if (grid %in% c("y", "both")) grid_line else no_line, panel.spacing = ggplot2::unit(16, "pt"), panel.spacing.x = NULL, panel.spacing.y = NULL, panel.ontop = FALSE, # -- Facet strips -- strip.background = ggplot2::element_rect(fill = strip_color, color = NA), strip.text = ggplot2::element_text( family = font_family, face = "bold", size = ggplot2::rel(rel_small), color = ink, margin = ggplot2::margin( half_line / 2, half_line / 2, half_line / 2, half_line / 2 ) ), strip.text.x = NULL, strip.text.y = ggplot2::element_text(angle = -90), strip.placement = "inside", strip.placement.x = NULL, strip.placement.y = NULL, strip.switch.pad.grid = ggplot2::unit(half_line / 2, "pt"), strip.switch.pad.wrap = ggplot2::unit(half_line / 2, "pt"), # -- Plot-level -- plot.background = ggplot2::element_rect(fill = bg_color, color = NA), plot.title = ggplot2::element_text( family = title_family, face = "bold", size = ggplot2::rel(rel_large), hjust = 0, vjust = 1, margin = ggplot2::margin(b = 4) ), plot.title.position = "plot", plot.subtitle = ggplot2::element_text( size = ggplot2::rel(1), color = ink_2, hjust = 0, vjust = 1, lineheight = 1.3, margin = ggplot2::margin(b = 14) ), plot.caption = ggplot2::element_text( size = ggplot2::rel(rel_tiny), color = ink_3, hjust = 0, vjust = 1, lineheight = 1.3, margin = ggplot2::margin(t = 14) ), plot.caption.position = "plot", plot.tag = ggplot2::element_text( face = "bold", color = accent, size = ggplot2::rel(rel_tiny), hjust = 0, vjust = 0.7 ), plot.tag.position = c(0, 1), plot.margin = ggplot2::margin(16, 18, 16, 16), complete = TRUE ) } #' Dark variant of the Civilytics ggplot2 theme #' #' Convenience wrapper around [theme_civilytics()] with dark-background #' defaults: navy (`#1A2E4A`) paper, warm off-white (`#FAF7F2`) ink, and #' a lighter ember accent (`#E07840`). Facet strips use primary navy. #' #' Pair with `make_logo_grob(variant = "dark")` for the white logo. #' #' @inheritParams theme_civilytics #' #' @return A complete ggplot2 [ggplot2::theme()] object. #' @export #' #' @examples #' \dontrun{ #' library(ggplot2) #' ggplot(mpg, aes(displ, hwy, colour = class)) + #' geom_point() + #' scale_color_civilytics() + #' theme_civilytics_dark() #' } theme_civilytics_dark <- function( font_size = 14, font_family = CV_FONT_SANS, title_family = CV_FONT_DISPLAY, line_size = 0.5, rel_small = 12 / 14, rel_tiny = 11 / 14, rel_large = 20 / 14, ink = unname(civilytics_colors["paper"]), paper = unname(civilytics_colors["navy_700"]), accent = unname(civilytics_colors["ember_400"]), strip_color = unname(civilytics_colors["navy_600"]), grid = c("y", "x", "both", "none"), paper_bg = TRUE) { theme_civilytics( font_size = font_size, font_family = font_family, title_family = title_family, line_size = line_size, rel_small = rel_small, rel_tiny = rel_tiny, rel_large = rel_large, ink = ink, paper = paper, accent = accent, strip_color = strip_color, grid = grid, paper_bg = paper_bg ) + # The base theme hardcodes ink_2/ink_3 for subtitle/caption, which are # dark colors meant for light backgrounds. Override with lighter values # so text remains readable on the navy background. ggplot2::theme( plot.subtitle = ggplot2::element_text( color = unname(civilytics_colors["navy_200"]) ), plot.caption = ggplot2::element_text( color = unname(civilytics_colors["navy_300"]) ), axis.text = ggplot2::element_text( color = unname(civilytics_colors["navy_200"]) ), axis.title.x = ggplot2::element_text( color = unname(civilytics_colors["navy_300"]) ), axis.title.y = ggplot2::element_text( color = unname(civilytics_colors["navy_300"]) ) ) } #' Slide-friendly Civilytics ggplot2 theme #' #' Variant of [theme_civilytics()] sized for Reveal.js slides or PowerPoint #' exports: larger base font (18pt), transparent background, wider margins, #' and a heavier x-axis line. Gridlines default to horizontal only. #' #' @inheritParams theme_civilytics #' #' @return A complete ggplot2 [ggplot2::theme()] object. #' @export #' #' @examples #' \dontrun{ #' library(ggplot2) #' ggplot(mpg, aes(displ, hwy, colour = class)) + #' geom_point() + #' scale_color_civilytics() + #' theme_civilytics_slide() #' } theme_civilytics_slide <- function( font_size = 18, font_family = CV_FONT_SANS, title_family = CV_FONT_DISPLAY, line_size = 0.5, rel_small = 12 / 14, rel_tiny = 11 / 14, rel_large = 20 / 14, ink = unname(civilytics_colors["ink"]), paper = unname(civilytics_colors["paper"]), accent = unname(civilytics_colors["ember_600"]), strip_color = unname(civilytics_colors["paper_2"]), grid = c("y", "x", "both", "none"), paper_bg = FALSE) { theme_civilytics( font_size = font_size, font_family = font_family, title_family = title_family, line_size = line_size, rel_small = rel_small, rel_tiny = rel_tiny, rel_large = rel_large, ink = ink, paper = paper, accent = accent, strip_color = strip_color, grid = grid, paper_bg = paper_bg ) + ggplot2::theme( axis.line.x = ggplot2::element_line( color = ink, linewidth = 0.8, lineend = "square" ), plot.margin = ggplot2::margin(24, 24, 24, 24) ) } # -- Map themes ---------------------------------------------------------------- #' Shared map-theme overrides #' #' Strips away axes, ticks, gridlines, and axis titles/labels — the elements #' that are meaningless on a choropleth or spatial plot. #' #' @return A partial ggplot2 [ggplot2::theme()] object. #' @keywords internal .map_theme_extras <- function() { ggplot2::theme( axis.line = ggplot2::element_blank(), axis.line.x = ggplot2::element_blank(), axis.line.y = ggplot2::element_blank(), axis.text = ggplot2::element_blank(), axis.text.x = ggplot2::element_blank(), axis.text.y = ggplot2::element_blank(), axis.ticks = ggplot2::element_blank(), axis.ticks.length = ggplot2::unit(0, "pt"), axis.title.x = ggplot2::element_blank(), axis.title.y = ggplot2::element_blank(), panel.grid.major.x = ggplot2::element_blank(), panel.grid.major.y = ggplot2::element_blank(), panel.grid.minor = ggplot2::element_blank() ) } #' Map-friendly Civilytics ggplot2 theme #' #' Variant of [theme_civilytics()] for choropleths and spatial plots. #' Suppresses axes, axis labels, ticks, and gridlines while keeping the #' Civilytics brand typography, colors, and plot-level elements (title, #' subtitle, caption, legend). #' #' @inheritParams theme_civilytics #' #' @return A complete ggplot2 [ggplot2::theme()] object. #' @export #' #' @examples #' \dontrun{ #' library(ggplot2) #' # With sf data: #' ggplot(map_data) + #' geom_sf(aes(fill = value)) + #' scale_fill_civilytics_c("seq_navy") + #' theme_civilytics_map() #' } theme_civilytics_map <- function( font_size = 14, font_family = CV_FONT_SANS, title_family = CV_FONT_DISPLAY, line_size = 0.5, rel_small = 12 / 14, rel_tiny = 11 / 14, rel_large = 20 / 14, ink = unname(civilytics_colors["ink"]), paper = unname(civilytics_colors["paper"]), accent = unname(civilytics_colors["ember_600"]), strip_color = unname(civilytics_colors["paper_2"]), paper_bg = TRUE) { theme_civilytics( font_size = font_size, font_family = font_family, title_family = title_family, line_size = line_size, rel_small = rel_small, rel_tiny = rel_tiny, rel_large = rel_large, ink = ink, paper = paper, accent = accent, strip_color = strip_color, grid = "none", paper_bg = paper_bg ) + .map_theme_extras() } #' Dark map-friendly Civilytics ggplot2 theme #' #' Variant of [theme_civilytics_dark()] for choropleths and spatial plots. #' Suppresses axes, axis labels, ticks, and gridlines on a dark navy #' background. Pair with `make_logo_grob(variant = "dark")`. #' #' @inheritParams theme_civilytics_dark #' #' @return A complete ggplot2 [ggplot2::theme()] object. #' @export #' #' @examples #' \dontrun{ #' library(ggplot2) #' ggplot(map_data) + #' geom_sf(aes(fill = value)) + #' scale_fill_civilytics_c("seq_ember") + #' theme_civilytics_dark_map() #' } theme_civilytics_dark_map <- function( font_size = 14, font_family = CV_FONT_SANS, title_family = CV_FONT_DISPLAY, line_size = 0.5, rel_small = 12 / 14, rel_tiny = 11 / 14, rel_large = 20 / 14, ink = unname(civilytics_colors["paper"]), paper = unname(civilytics_colors["navy_700"]), accent = unname(civilytics_colors["ember_400"]), strip_color = unname(civilytics_colors["navy_600"]), paper_bg = TRUE) { theme_civilytics_dark( font_size = font_size, font_family = font_family, title_family = title_family, line_size = line_size, rel_small = rel_small, rel_tiny = rel_tiny, rel_large = rel_large, ink = ink, paper = paper, accent = accent, strip_color = strip_color, grid = "none", paper_bg = paper_bg ) + .map_theme_extras() } #' Slide-friendly map Civilytics ggplot2 theme #' #' Variant of [theme_civilytics_slide()] for choropleths and spatial plots #' on slides. Combines the larger base font and transparent background of #' the slide theme with suppressed axes, ticks, and gridlines. #' #' @inheritParams theme_civilytics_slide #' #' @return A complete ggplot2 [ggplot2::theme()] object. #' @export #' #' @examples #' \dontrun{ #' library(ggplot2) #' ggplot(map_data) + #' geom_sf(aes(fill = value)) + #' scale_fill_civilytics_c("seq_navy") + #' theme_civilytics_slide_map() #' } theme_civilytics_slide_map <- function( font_size = 18, font_family = CV_FONT_SANS, title_family = CV_FONT_DISPLAY, line_size = 0.5, rel_small = 12 / 14, rel_tiny = 11 / 14, rel_large = 20 / 14, ink = unname(civilytics_colors["ink"]), paper = unname(civilytics_colors["paper"]), accent = unname(civilytics_colors["ember_600"]), strip_color = unname(civilytics_colors["paper_2"]), paper_bg = FALSE) { theme_civilytics_slide( font_size = font_size, font_family = font_family, title_family = title_family, line_size = line_size, rel_small = rel_small, rel_tiny = rel_tiny, rel_large = rel_large, ink = ink, paper = paper, accent = accent, strip_color = strip_color, grid = "none", paper_bg = paper_bg ) + .map_theme_extras() }