Files
civilyticsR/R/theme.R
T
jared f134621df5
R-CMD-check / R CMD check (push) Successful in 3m33s
fix: bump base font_size to 16pt and drop fig.retina for readable README
The old fig.retina=2 at 150 DPI rendered PNGs at 2x resolution that
GitHub markdown can't use, making text appear half its intended size.
Switching to 96 DPI without retina produces images at their natural
display size. Combined with the base font bump (14 → 16pt), titles
and labels now render with proper editorial presence.
2026-05-19 14:40:48 -06:00

420 lines
14 KiB
R

#' 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 `16`.
#' @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 `TRUE` (default), fill the plot and panel
#' backgrounds with the warm `paper` color. Set to `FALSE` for a
#' transparent background (useful for slides or overlay on colored
#' surfaces).
#'
#' @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) | ~23 pt |
#' | Subtitle | 1.0x | 16 pt |
#' | Axis text (tick labels) | `rel_small` (0.86x) | ~14 pt |
#' | Axis titles | `rel_small` (0.86x) | ~14 pt |
#' | Legend text | `rel_small` (0.86x) | ~14 pt |
#' | Caption | `rel_tiny` (0.79x) | ~13 pt |
#' | Legend title | `rel_tiny` (0.79x) | ~13 pt |
#' | Strip text (facets) | `rel_small` (0.86x) | ~14 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)
#'
#' # Default editorial theme
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics()
#'
#' # With both gridlines and brand colors
#' ggplot(mpg, aes(displ, hwy, colour = class)) +
#' geom_point() +
#' scale_color_civilytics() +
#' theme_civilytics(grid = "both")
#'
#' # Transparent background for embedding
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics(paper_bg = FALSE)
#'
#' # Larger text for poster or display
#' ggplot(mpg, aes(displ, hwy)) +
#' geom_point() +
#' theme_civilytics(font_size = 18)
#' }
theme_civilytics <- function(
font_size = 16,
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 = TRUE) {
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 = 16,
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
)
}
#' 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)
)
}