From c93c4692677577cd150ee0c68d59341254dba26d Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Sun, 17 May 2026 10:36:59 -0600 Subject: [PATCH] feat: add brand color system, showtext fonts, and dark theme - R/colors.R: civilytics_colors named vector (from --cv-* CSS vars on civilytics.com), three palettes (main/sequential/diverging), scale_color_civilytics() and scale_fill_civilytics() for ggplot2 - R/fonts.R: auto-loads Inter + Libre Franklin from Google Fonts via showtext in .onLoad(); exports civilytics_load_fonts() for manual retry - R/theme.R: wires ggplot2 4.0 ink/paper/accent params into theme_grey() base, adds title_family + strip_color params; new theme_civilytics_dark() using navy_dark background and warm off-white text - R/logo.R: make_logo_grob(variant) accepts "light"/"dark" to select the white logo for dark-background compositions - DESCRIPTION: adds showtext, sysfonts; pins ggplot2 >= 4.0.0 - tests/testthat/test_theme.R: full test suite for colors, palettes, scales, both theme variants, and logo grob variants --- .Rbuildignore | 3 + DESCRIPTION | 6 +- NAMESPACE | 9 + R/colors.R | 162 +++++++++++++ R/fonts.R | 51 ++++ R/logo.R | 33 ++- R/theme.R | 444 +++++++++++++++++++++------------- man/add_logo_ga.Rd | 2 + man/civilytics_colors.Rd | 19 ++ man/civilytics_load_fonts.Rd | 23 ++ man/civilytics_pal.Rd | 29 +++ man/make_logo_grob.Rd | 18 +- man/scale_color_civilytics.Rd | 36 +++ man/scale_fill_civilytics.Rd | 36 +++ man/theme_civilytics.Rd | 64 ++++- man/theme_civilytics_dark.Rd | 76 ++++++ tests/testthat/test_theme.R | 191 ++++++++++++++- 17 files changed, 996 insertions(+), 206 deletions(-) create mode 100644 R/colors.R create mode 100644 R/fonts.R create mode 100644 man/civilytics_colors.Rd create mode 100644 man/civilytics_load_fonts.Rd create mode 100644 man/civilytics_pal.Rd create mode 100644 man/scale_color_civilytics.Rd create mode 100644 man/scale_fill_civilytics.Rd create mode 100644 man/theme_civilytics_dark.Rd diff --git a/.Rbuildignore b/.Rbuildignore index c9073cc..de74a95 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -6,3 +6,6 @@ ^Makefile$ ^Dockerfile$ ^LICENSE\.md$ +^\.claude$ +^\.playwright-mcp$ +^civilytics-site\.png$ diff --git a/DESCRIPTION b/DESCRIPTION index bde8e42..de829b4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,13 +14,15 @@ BugReports: https://gitea.civilytics.org/Civilytics/civilyticsR/issues Depends: R (>= 4.1.0) Imports: - ggplot2, + ggplot2 (>= 4.0.0), jpeg, png, stringr, gridExtra, grid, - stringdist + stringdist, + showtext, + sysfonts Encoding: UTF-8 Suggests: testthat (>= 3.0.0), diff --git a/NAMESPACE b/NAMESPACE index 68b993b..b3e75f9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,9 @@ export(add_logo) export(add_logo_ga) export(agresti_coull_interval) +export(civilytics_colors) +export(civilytics_load_fonts) +export(civilytics_pal) export(clopper_pearson) export(countCleanr) export(countDots) @@ -32,15 +35,21 @@ export(rnh) export(round_to_nearest_half) export(safe_max) export(safe_ratio) +export(scale_color_civilytics) +export(scale_fill_civilytics) export(simpleCap) export(star_subs) export(theme_civilytics) +export(theme_civilytics_dark) export(trim_max) export(waldInterval) export(z_gap_test) export(z_univariate) import(ggplot2) +importFrom(ggplot2,annotation_custom) +importFrom(ggplot2,ggplot) importFrom(ggplot2,theme) +importFrom(ggplot2,theme_void) importFrom(graphics,rasterImage) importFrom(grid,grid.draw) importFrom(grid,rasterGrob) diff --git a/R/colors.R b/R/colors.R new file mode 100644 index 0000000..6a00222 --- /dev/null +++ b/R/colors.R @@ -0,0 +1,162 @@ +#' Civilytics brand colors +#' +#' A named character vector of all Civilytics brand colors, matching the CSS +#' custom properties defined on www.civilytics.com (`--cv-*` variables). +#' +#' @format A named character vector of hex color codes. +#' @export +#' +#' @examples +#' civilytics_colors["accent"] +#' civilytics_colors[c("ink", "paper")] +civilytics_colors <- c( + # Paper (backgrounds) — warm off-white scale + paper = "#FAF7F2", + paper_2 = "#F2EDE4", + paper_3 = "#E6DFD1", + # Rules (borders/dividers) + rule = "#D6CEBD", + rule_strong = "#B8AE97", + # Ink (text / foreground) — dark-to-light navy-grey scale + ink = "#0E1A2B", + ink_2 = "#2B3A52", + ink_3 = "#5A6A82", + ink_4 = "#8C97AB", + # Navy — primary brand blue + navy = "#22406A", + navy_dark = "#1A2E4A", + # Accent — burnt orange + accent = "#C25311", + accent_dark = "#E07840", + accent_50 = "#FDF1E4", + accent_100 = "#FBE0C6", + accent_200 = "#F8C8A3" +) + +# Internal named list of visualization palettes. +# Each entry is a character vector of hex codes ordered for visual distinction. +.cv_palettes <- list( + # Qualitative: distinct hues for categorical data (up to 6 categories) + main = unname(civilytics_colors[c( + "navy_dark", "accent", "ink_3", "rule_strong", "navy", "accent_200" + )]), + # Sequential: light-to-dark navy for ordered/continuous data + sequential = unname(civilytics_colors[c( + "paper", "ink_4", "ink_3", "navy", "navy_dark" + )]), + # Diverging: orange <-> neutral <-> navy for data with a meaningful midpoint + diverging = unname(civilytics_colors[c( + "accent", "accent_200", "paper", "ink_4", "navy_dark" + )]) +) + +# Internal palette generator: returns a function(n) -> character vector. +.cv_pal_fun <- function(name) { + pal <- .cv_palettes[[name]] + if (is.null(pal)) { + stop(sprintf( + "'%s' is not a valid Civilytics palette. Choose from: %s", + name, paste(names(.cv_palettes), collapse = ", ") + ), call. = FALSE) + } + function(n) { + if (n > length(pal)) grDevices::colorRampPalette(pal)(n) else pal[seq_len(n)] + } +} + +#' Civilytics brand color palette +#' +#' Returns a character vector of hex codes from a named Civilytics palette. +#' Available palettes: `"main"` (qualitative, up to 6), `"sequential"` (light +#' to dark navy), `"diverging"` (orange–neutral–navy). +#' +#' @param name Character. Palette name: `"main"`, `"sequential"`, or +#' `"diverging"`. Defaults to `"main"`. +#' @param n Integer or `NULL`. Number of colors to return. If `NULL`, returns +#' all colors in the palette. If `n` exceeds the number of defined stops, +#' colors are interpolated via [grDevices::colorRampPalette()]. +#' +#' @return A character vector of hex color codes. +#' @export +#' +#' @examples +#' civilytics_pal() # all 6 qualitative colors +#' civilytics_pal("sequential", n = 3) +#' civilytics_pal("diverging", n = 5) +civilytics_pal <- function(name = "main", n = NULL) { + f <- .cv_pal_fun(name) + len <- length(.cv_palettes[[name]]) + f(if (is.null(n)) len else n) +} + +#' Civilytics color scale for ggplot2 +#' +#' Applies a Civilytics brand palette to the `colour` aesthetic. Use +#' `discrete = TRUE` for categorical variables and `discrete = FALSE` for +#' continuous gradients. +#' +#' @param palette Character. Palette name passed to [civilytics_pal()]. +#' Defaults to `"main"`. +#' @param discrete Logical. `TRUE` (default) for a discrete scale; `FALSE` for +#' a continuous gradient via [ggplot2::scale_color_gradientn()]. +#' @param ... Additional arguments passed to the underlying ggplot2 scale +#' function. +#' +#' @return A ggplot2 scale object. +#' @export +#' +#' @examples +#' library(ggplot2) +#' ggplot(mpg, aes(displ, hwy, colour = class)) + +#' geom_point() + +#' scale_color_civilytics() +#' +#' ggplot(mpg, aes(displ, hwy, colour = cty)) + +#' geom_point() + +#' scale_color_civilytics("sequential", discrete = FALSE) +scale_color_civilytics <- function(palette = "main", discrete = TRUE, ...) { + if (discrete) { + ggplot2::discrete_scale("colour", palette = .cv_pal_fun(palette), ...) + } else { + ggplot2::scale_color_gradientn( + colours = grDevices::colorRampPalette(.cv_palettes[[palette]])(256), + ... + ) + } +} + +#' Civilytics fill scale for ggplot2 +#' +#' Applies a Civilytics brand palette to the `fill` aesthetic. Use +#' `discrete = TRUE` for categorical variables and `discrete = FALSE` for +#' continuous gradients. +#' +#' @param palette Character. Palette name passed to [civilytics_pal()]. +#' Defaults to `"main"`. +#' @param discrete Logical. `TRUE` (default) for a discrete scale; `FALSE` for +#' a continuous gradient via [ggplot2::scale_fill_gradientn()]. +#' @param ... Additional arguments passed to the underlying ggplot2 scale +#' function. +#' +#' @return A ggplot2 scale object. +#' @export +#' +#' @examples +#' library(ggplot2) +#' ggplot(mpg, aes(class, fill = class)) + +#' geom_bar() + +#' scale_fill_civilytics() +#' +#' ggplot(faithfuld, aes(waiting, eruptions, fill = density)) + +#' geom_tile() + +#' scale_fill_civilytics("sequential", discrete = FALSE) +scale_fill_civilytics <- function(palette = "main", discrete = TRUE, ...) { + if (discrete) { + ggplot2::discrete_scale("fill", palette = .cv_pal_fun(palette), ...) + } else { + ggplot2::scale_fill_gradientn( + colours = grDevices::colorRampPalette(.cv_palettes[[palette]])(256), + ... + ) + } +} diff --git a/R/fonts.R b/R/fonts.R new file mode 100644 index 0000000..8af992a --- /dev/null +++ b/R/fonts.R @@ -0,0 +1,51 @@ +# Font family name constants used as defaults in theme functions. +# These match the --cv-font-* CSS custom properties on www.civilytics.com. +CV_FONT_DISPLAY <- "Libre Franklin" # headings / display text +CV_FONT_SANS <- "Inter" # axis text, legends, UI elements +CV_FONT_SERIF <- "Source Serif 4" # body prose (not used in default theme) + +# Internal flag so civilytics_load_fonts() is idempotent within a session. +.cv_fonts_loaded <- FALSE + +#' Load Civilytics brand fonts +#' +#' Downloads Inter and Libre Franklin from Google Fonts via +#' [sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so +#' that all graphics devices render text with those fonts. This is called +#' automatically when the package loads; use this function to retry if the +#' initial load failed (e.g., the machine was offline at load time). +#' +#' @return Invisibly returns `NULL`. +#' @export +#' +#' @examples +#' \dontrun{ +#' civilytics_load_fonts() +#' } +civilytics_load_fonts <- function() { + sysfonts::font_add_google("Inter", family = "Inter") + sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin") + showtext::showtext_auto() + .cv_fonts_loaded <<- TRUE + invisible(NULL) +} + +.onLoad <- function(libname, pkgname) { + tryCatch( + civilytics_load_fonts(), + error = function(e) { + # Store the failure so .onAttach can surface a message to the user. + options(.civilytics_fonts_failed = TRUE) + } + ) +} + +.onAttach <- function(libname, pkgname) { + if (isTRUE(getOption(".civilytics_fonts_failed"))) { + packageStartupMessage( + "[civilytics] Brand fonts (Inter, Libre Franklin) could not be loaded ", + "from Google Fonts. Charts will fall back to system fonts. ", + "Call civilytics_load_fonts() once you have an internet connection." + ) + } +} diff --git a/R/logo.R b/R/logo.R index f28c711..e38931c 100644 --- a/R/logo.R +++ b/R/logo.R @@ -121,12 +121,14 @@ has_caption <- function(gg) { #' @export #' #' @examples +#' \dontrun{ #' library(ggplot2); library(grid) #' tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics() #' tmp_logo <- make_logo_grob() #' plot_and_logo <- add_logo(tmp_plot, tmp_logo) #' grid.draw(plot_and_logo) #' dev.off() +#' } add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL, margin_param = NULL) { # Change position of logo depending on if plot has a caption if (!is.null(margin_param)) { @@ -157,17 +159,28 @@ add_logo_ga <- function(plot_list, logo, nrow = 1, widths = NULL, margin_param = #' Get a Civilytics Logo grob #' -#' @return a gg object which contains the logo file stored as a Grob suitable for manipulating in -#' grid +#' Returns a ggplot object containing the Civilytics logo as a rasterGrob, +#' ready to compose with plots via [add_logo()] or [add_logo_ga()]. +#' +#' @param variant Character. `"light"` (default) uses the standard dark logo +#' suitable for light backgrounds. `"dark"` uses the white logo suitable for +#' dark backgrounds (pairs with [theme_civilytics_dark()]). +#' +#' @return A ggplot object (class `"gg"`) containing the logo grob. #' @export -#' @import ggplot2 +#' @importFrom ggplot2 ggplot theme_void annotation_custom #' @examples -#' logo <- make_logo_grob() -#' class(logo) # gg -make_logo_grob <- function() { - ggplot() + - theme_void() + - annotation_custom(get_png(system.file("img", "civilytics_logo.png", - package = "civilytics")), xmin = 0.7, xmax = 1) +#' logo <- make_logo_grob() # light background +#' logo_dark <- make_logo_grob("dark") # dark background +#' class(logo) # "gg" "ggplot" +make_logo_grob <- function(variant = c("light", "dark")) { + variant <- match.arg(variant) + img_file <- if (variant == "dark") "civilytics_logo_white.png" else "civilytics_logo.png" + ggplot2::ggplot() + + ggplot2::theme_void() + + ggplot2::annotation_custom( + get_png(system.file("img", img_file, package = "civilytics")), + xmin = 0.7, xmax = 1 + ) } diff --git a/R/theme.R b/R/theme.R index 36f0ab4..675bcd2 100644 --- a/R/theme.R +++ b/R/theme.R @@ -1,165 +1,279 @@ -#' Make the Civilytics plot theme -#' -#' @param font_size default 14, a number representing the base font for the theme -#' @param font_family default "", a character for the font family to use in the theme -#' @param line_size default 0.5, the line size to use for the theme -#' @param rel_small default 12/14, the scale factor to create a small font from the base font_size -#' @param rel_tiny default 11/14, the scale factor to create a tiny font from the base font_size -#' @param rel_large default 16/14, the scale factor to create a large font from the base font_size -#' @return a ggplot2 theme object suitable for combining with ggplot objects to theme them -#' @export -theme_civilytics <- - function (font_size = 14, - font_family = "", - line_size = 0.5, - rel_small = 12 / 14, - rel_tiny = 11 / 14, - rel_large = 16 / 14) { - half_line <- font_size / 2 - small_size <- rel_small * font_size - theme_grey(base_size = font_size, base_family = font_family) %+replace% - theme( - line = element_line( - color = "black", - linewidth = line_size, - linetype = 1, - lineend = "butt" - ), - rect = element_rect( - fill = NA, - color = NA, - linewidth = line_size, - linetype = 1 - ), - text = element_text( - family = font_family, - face = "plain", - color = "black", - size = font_size, - hjust = 0.5, - vjust = 0.5, - angle = 0, - lineheight = 0.9, - margin = margin(), - debug = FALSE - ), - axis.line = element_line( - color = "black", - linewidth = line_size, - lineend = "square" - ), - axis.line.x = NULL, - axis.line.y = NULL, - axis.text = element_text(color = "black", - size = small_size), - axis.text.x = element_text(margin = margin(t = small_size / 4), - vjust = 1), - axis.text.x.top = element_text(margin = margin(b = small_size / 4), - vjust = 0), - axis.text.y = element_text(margin = margin(r = small_size / 4), - hjust = 1), - axis.text.y.right = element_text(margin = margin(l = small_size / 4), - hjust = 0), - axis.ticks = element_line(color = "black", - linewidth = line_size), - axis.ticks.length = unit(half_line / 2, - "pt"), - axis.title.x = element_text(margin = margin(t = half_line / 2), - vjust = 1), - axis.title.x.top = element_text(margin = margin(b = half_line / 2), - vjust = 0), - axis.title.y = element_text( - angle = 90, - margin = margin(r = half_line / - 2), - vjust = 1 - ), - axis.title.y.right = element_text( - angle = -90, - margin = margin(l = half_line / 2), - vjust = 0 - ), - legend.background = element_blank(), - legend.spacing = unit(font_size, "pt"), - legend.spacing.x = NULL, - legend.spacing.y = NULL, - legend.margin = margin(0, - 0, 0, 0), - legend.key = element_blank(), - legend.key.size = unit(1.1 * - font_size, "pt"), - legend.key.height = NULL, - legend.key.width = NULL, - legend.text = element_text(size = rel(rel_small)), - legend.title = element_text(hjust = 0), - legend.position = "right", - legend.direction = NULL, - legend.justification = c("left", - "center"), - legend.box = NULL, - legend.box.margin = margin(0, - 0, 0, 0), - legend.box.background = element_blank(), - legend.box.spacing = unit(font_size, "pt"), - panel.background = element_blank(), - panel.border = element_blank(), - panel.grid = element_blank(), - panel.grid.major = NULL, - panel.grid.minor = NULL, - panel.grid.major.x = NULL, - panel.grid.major.y = NULL, - panel.grid.minor.x = NULL, - panel.grid.minor.y = NULL, - panel.spacing = unit(half_line, - "pt"), - panel.spacing.x = NULL, - panel.spacing.y = NULL, - panel.ontop = FALSE, - strip.background = element_rect(fill = "grey80"), - strip.text = element_text( - size = rel(rel_small), - margin = margin(half_line / 2, half_line / - 2, half_line / 2, - half_line / 2) - ), - strip.text.x = NULL, - strip.text.y = element_text(angle = -90), - strip.placement = "inside", - strip.placement.x = NULL, - strip.placement.y = NULL, - strip.switch.pad.grid = unit(half_line / 2, - "pt"), - strip.switch.pad.wrap = unit(half_line / 2, - "pt"), - plot.background = element_blank(), - plot.title = element_text( - face = "bold", - size = rel(rel_large), - hjust = 0, - vjust = 1, - margin = margin(b = half_line) - ), - plot.subtitle = element_text( - size = rel(rel_small), - hjust = 0, - vjust = 1, - margin = margin(b = half_line) - ), - plot.caption = element_text( - size = rel(rel_tiny), - hjust = 0, # set hjust to 0 - vjust = 1, - lineheight = 1, - margin = margin(t = half_line) - ), - plot.tag = element_text( - face = "bold", - hjust = 0, - vjust = 0.7 - ), - plot.tag.position = c(0, 1), - plot.margin = margin(half_line, - half_line, half_line, half_line), - complete = TRUE - ) - } +#' 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. +#' +#' 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 `16/14`. +#' @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]`["accent"]` (`#C25311`). +#' @param strip_color Character. Hex code for facet strip background. Defaults +#' to [civilytics_colors]`["paper_2"]` (`#F2EDE4`). +#' +#' @return A complete ggplot2 [ggplot2::theme()] object. +#' @export +#' +#' @examples +#' library(ggplot2) +#' ggplot(mpg, aes(displ, hwy)) + +#' geom_point() + +#' theme_civilytics() +#' +#' ggplot(mpg, aes(displ, hwy, colour = class)) + +#' geom_point() + +#' scale_color_civilytics() + +#' theme_civilytics() +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 = 16 / 14, + ink = unname(civilytics_colors["ink"]), + paper = unname(civilytics_colors["paper"]), + accent = unname(civilytics_colors["accent"]), + strip_color = unname(civilytics_colors["paper_2"])) { + + half_line <- font_size / 2 + small_size <- rel_small * font_size + + 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_line( + color = ink, + linewidth = line_size, + lineend = "square" + ), + axis.line.x = NULL, + axis.line.y = NULL, + axis.text = ggplot2::element_text( + color = ink, + size = small_size + ), + 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, + linewidth = line_size + ), + axis.ticks.length = ggplot2::unit(half_line / 2, "pt"), + axis.title.x = ggplot2::element_text( + margin = ggplot2::margin(t = half_line / 2), + vjust = 1 + ), + axis.title.x.top = ggplot2::element_text( + margin = ggplot2::margin(b = half_line / 2), + vjust = 0 + ), + axis.title.y = ggplot2::element_text( + angle = 90, + margin = ggplot2::margin(r = half_line / 2), + vjust = 1 + ), + axis.title.y.right = ggplot2::element_text( + 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, 0, 0), + legend.key = ggplot2::element_blank(), + legend.key.size = ggplot2::unit(1.1 * font_size, "pt"), + legend.key.height = NULL, + legend.key.width = NULL, + legend.text = ggplot2::element_text(size = ggplot2::rel(rel_small)), + legend.title = ggplot2::element_text(hjust = 0), + legend.position = "right", + 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_blank(), + panel.border = ggplot2::element_blank(), + panel.grid = ggplot2::element_blank(), + panel.grid.major = NULL, + panel.grid.minor = NULL, + panel.grid.major.x = NULL, + panel.grid.major.y = NULL, + panel.grid.minor.x = NULL, + panel.grid.minor.y = NULL, + panel.spacing = ggplot2::unit(half_line, "pt"), + panel.spacing.x = NULL, + panel.spacing.y = NULL, + panel.ontop = FALSE, + # Facet strips — use brand paper_2 tint (overridden for dark variant) + strip.background = ggplot2::element_rect(fill = strip_color), + strip.text = ggplot2::element_text( + family = title_family, + size = ggplot2::rel(rel_small), + 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 = paper, 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 = half_line) + ), + plot.subtitle = ggplot2::element_text( + size = ggplot2::rel(rel_small), + hjust = 0, + vjust = 1, + margin = ggplot2::margin(b = half_line) + ), + plot.caption = ggplot2::element_text( + size = ggplot2::rel(rel_tiny), + hjust = 0, + vjust = 1, + lineheight = 1, + margin = ggplot2::margin(t = half_line) + ), + plot.tag = ggplot2::element_text( + face = "bold", + hjust = 0, + vjust = 0.7 + ), + plot.tag.position = c(0, 1), + plot.margin = ggplot2::margin(half_line, half_line, half_line, half_line), + complete = TRUE + ) +} + +#' Dark variant of the Civilytics ggplot2 theme +#' +#' A convenience wrapper around [theme_civilytics()] with dark-background +#' defaults: navy (`#1A2E4A`) background, warm off-white (`#FAF7F2`) text, and +#' a lighter orange accent (`#E07840`) suitable for dark backgrounds. Pair with +#' `make_logo_grob(variant = "dark")` to use the white logo. +#' +#' All parameters from [theme_civilytics()] are accepted and override the dark +#' defaults. +#' +#' @inheritParams theme_civilytics +#' +#' @return A complete ggplot2 [ggplot2::theme()] object. +#' @export +#' +#' @examples +#' library(ggplot2) +#' ggplot(mpg, aes(displ, hwy)) + +#' geom_point() + +#' theme_civilytics_dark() +#' +#' 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 = 16 / 14, + ink = unname(civilytics_colors["paper"]), + paper = unname(civilytics_colors["navy_dark"]), + accent = unname(civilytics_colors["accent_dark"]), + strip_color = unname(civilytics_colors["navy"])) { + + 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 + ) +} diff --git a/man/add_logo_ga.Rd b/man/add_logo_ga.Rd index 29bb7de..4414486 100644 --- a/man/add_logo_ga.Rd +++ b/man/add_logo_ga.Rd @@ -27,6 +27,7 @@ Add a logo to a ggplot2 object The resulting object needs to be drawn to the screen using grid.draw() } \examples{ +\dontrun{ library(ggplot2); library(grid) tmp_plot <- ggplot(mtcars) + aes(x = hp, y = disp) + geom_point() + theme_civilytics() tmp_logo <- make_logo_grob() @@ -34,3 +35,4 @@ plot_and_logo <- add_logo(tmp_plot, tmp_logo) grid.draw(plot_and_logo) dev.off() } +} diff --git a/man/civilytics_colors.Rd b/man/civilytics_colors.Rd new file mode 100644 index 0000000..b1cea73 --- /dev/null +++ b/man/civilytics_colors.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/colors.R +\name{civilytics_colors} +\alias{civilytics_colors} +\title{Civilytics brand colors} +\format{ +A named character vector of hex color codes. +} +\usage{ +civilytics_colors +} +\description{ +A named character vector of all Civilytics brand colors, matching the CSS +custom properties defined on www.civilytics.com (`--cv-*` variables). +} +\examples{ +civilytics_colors["accent"] +civilytics_colors[c("ink", "paper")] +} diff --git a/man/civilytics_load_fonts.Rd b/man/civilytics_load_fonts.Rd new file mode 100644 index 0000000..6447688 --- /dev/null +++ b/man/civilytics_load_fonts.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/fonts.R +\name{civilytics_load_fonts} +\alias{civilytics_load_fonts} +\title{Load Civilytics brand fonts} +\usage{ +civilytics_load_fonts() +} +\value{ +Invisibly returns `NULL`. +} +\description{ +Downloads Inter and Libre Franklin from Google Fonts via +[sysfonts::font_add_google()], then calls [showtext::showtext_auto()] so +that all graphics devices render text with those fonts. This is called +automatically when the package loads; use this function to retry if the +initial load failed (e.g., the machine was offline at load time). +} +\examples{ +\dontrun{ +civilytics_load_fonts() +} +} diff --git a/man/civilytics_pal.Rd b/man/civilytics_pal.Rd new file mode 100644 index 0000000..38bdb98 --- /dev/null +++ b/man/civilytics_pal.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/colors.R +\name{civilytics_pal} +\alias{civilytics_pal} +\title{Civilytics brand color palette} +\usage{ +civilytics_pal(name = "main", n = NULL) +} +\arguments{ +\item{name}{Character. Palette name: `"main"`, `"sequential"`, or +`"diverging"`. Defaults to `"main"`.} + +\item{n}{Integer or `NULL`. Number of colors to return. If `NULL`, returns +all colors in the palette. If `n` exceeds the number of defined stops, +colors are interpolated via [grDevices::colorRampPalette()].} +} +\value{ +A character vector of hex color codes. +} +\description{ +Returns a character vector of hex codes from a named Civilytics palette. +Available palettes: `"main"` (qualitative, up to 6), `"sequential"` (light +to dark navy), `"diverging"` (orange–neutral–navy). +} +\examples{ +civilytics_pal() # all 6 qualitative colors +civilytics_pal("sequential", n = 3) +civilytics_pal("diverging", n = 5) +} diff --git a/man/make_logo_grob.Rd b/man/make_logo_grob.Rd index d5f26cf..8d57744 100644 --- a/man/make_logo_grob.Rd +++ b/man/make_logo_grob.Rd @@ -4,16 +4,22 @@ \alias{make_logo_grob} \title{Get a Civilytics Logo grob} \usage{ -make_logo_grob() +make_logo_grob(variant = c("light", "dark")) +} +\arguments{ +\item{variant}{Character. `"light"` (default) uses the standard dark logo +suitable for light backgrounds. `"dark"` uses the white logo suitable for +dark backgrounds (pairs with [theme_civilytics_dark()]).} } \value{ -a gg object which contains the logo file stored as a Grob suitable for manipulating in -grid +A ggplot object (class `"gg"`) containing the logo grob. } \description{ -Get a Civilytics Logo grob +Returns a ggplot object containing the Civilytics logo as a rasterGrob, +ready to compose with plots via [add_logo()] or [add_logo_ga()]. } \examples{ -logo <- make_logo_grob() -class(logo) # gg +logo <- make_logo_grob() # light background +logo_dark <- make_logo_grob("dark") # dark background +class(logo) # "gg" "ggplot" } diff --git a/man/scale_color_civilytics.Rd b/man/scale_color_civilytics.Rd new file mode 100644 index 0000000..f54ab8c --- /dev/null +++ b/man/scale_color_civilytics.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/colors.R +\name{scale_color_civilytics} +\alias{scale_color_civilytics} +\title{Civilytics color scale for ggplot2} +\usage{ +scale_color_civilytics(palette = "main", discrete = TRUE, ...) +} +\arguments{ +\item{palette}{Character. Palette name passed to [civilytics_pal()]. +Defaults to `"main"`.} + +\item{discrete}{Logical. `TRUE` (default) for a discrete scale; `FALSE` for +a continuous gradient via [ggplot2::scale_color_gradientn()].} + +\item{...}{Additional arguments passed to the underlying ggplot2 scale +function.} +} +\value{ +A ggplot2 scale object. +} +\description{ +Applies a Civilytics brand palette to the `colour` aesthetic. Use +`discrete = TRUE` for categorical variables and `discrete = FALSE` for +continuous gradients. +} +\examples{ +library(ggplot2) +ggplot(mpg, aes(displ, hwy, colour = class)) + + geom_point() + + scale_color_civilytics() + +ggplot(mpg, aes(displ, hwy, colour = cty)) + + geom_point() + + scale_color_civilytics("sequential", discrete = FALSE) +} diff --git a/man/scale_fill_civilytics.Rd b/man/scale_fill_civilytics.Rd new file mode 100644 index 0000000..78487f2 --- /dev/null +++ b/man/scale_fill_civilytics.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/colors.R +\name{scale_fill_civilytics} +\alias{scale_fill_civilytics} +\title{Civilytics fill scale for ggplot2} +\usage{ +scale_fill_civilytics(palette = "main", discrete = TRUE, ...) +} +\arguments{ +\item{palette}{Character. Palette name passed to [civilytics_pal()]. +Defaults to `"main"`.} + +\item{discrete}{Logical. `TRUE` (default) for a discrete scale; `FALSE` for +a continuous gradient via [ggplot2::scale_fill_gradientn()].} + +\item{...}{Additional arguments passed to the underlying ggplot2 scale +function.} +} +\value{ +A ggplot2 scale object. +} +\description{ +Applies a Civilytics brand palette to the `fill` aesthetic. Use +`discrete = TRUE` for categorical variables and `discrete = FALSE` for +continuous gradients. +} +\examples{ +library(ggplot2) +ggplot(mpg, aes(class, fill = class)) + + geom_bar() + + scale_fill_civilytics() + +ggplot(faithfuld, aes(waiting, eruptions, fill = density)) + + geom_tile() + + scale_fill_civilytics("sequential", discrete = FALSE) +} diff --git a/man/theme_civilytics.Rd b/man/theme_civilytics.Rd index 8848383..658cd97 100644 --- a/man/theme_civilytics.Rd +++ b/man/theme_civilytics.Rd @@ -2,33 +2,75 @@ % Please edit documentation in R/theme.R \name{theme_civilytics} \alias{theme_civilytics} -\title{Make the Civilytics plot theme} +\title{Civilytics ggplot2 theme} \usage{ theme_civilytics( font_size = 14, - font_family = "", + font_family = CV_FONT_SANS, + title_family = CV_FONT_DISPLAY, line_size = 0.5, rel_small = 12/14, rel_tiny = 11/14, - rel_large = 16/14 + rel_large = 16/14, + ink = unname(civilytics_colors["ink"]), + paper = unname(civilytics_colors["paper"]), + accent = unname(civilytics_colors["accent"]), + strip_color = unname(civilytics_colors["paper_2"]) ) } \arguments{ -\item{font_size}{default 14, a number representing the base font for the theme} +\item{font_size}{Numeric. Base font size in points. Default `14`.} -\item{font_family}{default "", a character for the font family to use in the theme} +\item{font_family}{Character. Font family for body/axis text. Default +`"Inter"` (loaded via showtext).} -\item{line_size}{default 0.5, the line size to use for the theme} +\item{title_family}{Character. Font family for plot titles and strip labels. +Default `"Libre Franklin"` (loaded via showtext).} -\item{rel_small}{default 12/14, the scale factor to create a small font from the base font_size} +\item{line_size}{Numeric. Base line width. Default `0.5`.} -\item{rel_tiny}{default 11/14, the scale factor to create a tiny font from the base font_size} +\item{rel_small}{Numeric. Scale factor for small text relative to +`font_size`. Default `12/14`.} -\item{rel_large}{default 16/14, the scale factor to create a large font from the base font_size} +\item{rel_tiny}{Numeric. Scale factor for tiny text relative to `font_size`. +Default `11/14`.} + +\item{rel_large}{Numeric. Scale factor for large text (titles) relative to +`font_size`. Default `16/14`.} + +\item{ink}{Character. Hex code for foreground/text color. Defaults to +[civilytics_colors]`["ink"]` (`#0E1A2B`).} + +\item{paper}{Character. Hex code for background color. Defaults to +[civilytics_colors]`["paper"]` (`#FAF7F2`).} + +\item{accent}{Character. Hex code for accent/highlight color. Defaults to +[civilytics_colors]`["accent"]` (`#C25311`).} + +\item{strip_color}{Character. Hex code for facet strip background. Defaults +to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} } \value{ -a ggplot2 theme object suitable for combining with ggplot objects to theme them +A complete ggplot2 [ggplot2::theme()] object. } \description{ -Make the Civilytics plot 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. +} +\details{ +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. +} +\examples{ +library(ggplot2) +ggplot(mpg, aes(displ, hwy)) + + geom_point() + + theme_civilytics() + +ggplot(mpg, aes(displ, hwy, colour = class)) + + geom_point() + + scale_color_civilytics() + + theme_civilytics() } diff --git a/man/theme_civilytics_dark.Rd b/man/theme_civilytics_dark.Rd new file mode 100644 index 0000000..0dd25b2 --- /dev/null +++ b/man/theme_civilytics_dark.Rd @@ -0,0 +1,76 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/theme.R +\name{theme_civilytics_dark} +\alias{theme_civilytics_dark} +\title{Dark variant of the Civilytics ggplot2 theme} +\usage{ +theme_civilytics_dark( + 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 = 16/14, + ink = unname(civilytics_colors["paper"]), + paper = unname(civilytics_colors["navy_dark"]), + accent = unname(civilytics_colors["accent_dark"]), + strip_color = unname(civilytics_colors["navy"]) +) +} +\arguments{ +\item{font_size}{Numeric. Base font size in points. Default `14`.} + +\item{font_family}{Character. Font family for body/axis text. Default +`"Inter"` (loaded via showtext).} + +\item{title_family}{Character. Font family for plot titles and strip labels. +Default `"Libre Franklin"` (loaded via showtext).} + +\item{line_size}{Numeric. Base line width. Default `0.5`.} + +\item{rel_small}{Numeric. Scale factor for small text relative to +`font_size`. Default `12/14`.} + +\item{rel_tiny}{Numeric. Scale factor for tiny text relative to `font_size`. +Default `11/14`.} + +\item{rel_large}{Numeric. Scale factor for large text (titles) relative to +`font_size`. Default `16/14`.} + +\item{ink}{Character. Hex code for foreground/text color. Defaults to +[civilytics_colors]`["ink"]` (`#0E1A2B`).} + +\item{paper}{Character. Hex code for background color. Defaults to +[civilytics_colors]`["paper"]` (`#FAF7F2`).} + +\item{accent}{Character. Hex code for accent/highlight color. Defaults to +[civilytics_colors]`["accent"]` (`#C25311`).} + +\item{strip_color}{Character. Hex code for facet strip background. Defaults +to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} +} +\value{ +A complete ggplot2 [ggplot2::theme()] object. +} +\description{ +A convenience wrapper around [theme_civilytics()] with dark-background +defaults: navy (`#1A2E4A`) background, warm off-white (`#FAF7F2`) text, and +a lighter orange accent (`#E07840`) suitable for dark backgrounds. Pair with +`make_logo_grob(variant = "dark")` to use the white logo. +} +\details{ +All parameters from [theme_civilytics()] are accepted and override the dark +defaults. +} +\examples{ +library(ggplot2) +ggplot(mpg, aes(displ, hwy)) + + geom_point() + + theme_civilytics_dark() + +ggplot(mpg, aes(displ, hwy, colour = class)) + + geom_point() + + scale_color_civilytics() + + theme_civilytics_dark() +} diff --git a/tests/testthat/test_theme.R b/tests/testthat/test_theme.R index 60f20bb..407d7f9 100644 --- a/tests/testthat/test_theme.R +++ b/tests/testthat/test_theme.R @@ -1,12 +1,179 @@ -# context("Histograms") -# -# library(ggplot2) -# disp_hist_base <- function() hist(mtcars$disp) -# disp_hist_ggplot <- ggplot(mtcars, aes(disp)) + geom_histogram() -# -# test_that({ -# skip("Not currently working") -# vdiffr::expect_doppelganger("Base graphics histogram", disp_hist_base) -# vdiffr::expect_doppelganger("ggplot2 histogram", disp_hist_ggplot) -# -# }) +library(ggplot2) + +# --- civilytics_colors ------------------------------------------------------- + +test_that("civilytics_colors is a named character vector of hex codes", { + expect_type(civilytics_colors, "character") + expect_named(civilytics_colors) + expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", civilytics_colors))) +}) + +test_that("civilytics_colors contains expected brand keys", { + expected <- c("paper", "ink", "accent", "navy", "navy_dark", + "accent_dark", "paper_2", "rule") + expect_true(all(expected %in% names(civilytics_colors))) +}) + +test_that("civilytics_colors values match brand specification", { + expect_equal(unname(civilytics_colors["paper"]), "#FAF7F2") + expect_equal(unname(civilytics_colors["ink"]), "#0E1A2B") + expect_equal(unname(civilytics_colors["accent"]), "#C25311") + expect_equal(unname(civilytics_colors["navy_dark"]), "#1A2E4A") + expect_equal(unname(civilytics_colors["accent_dark"]),"#E07840") +}) + +# --- civilytics_pal ---------------------------------------------------------- + +test_that("civilytics_pal returns character vector of hex codes", { + result <- civilytics_pal() + expect_type(result, "character") + expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", result))) +}) + +test_that("civilytics_pal respects n argument", { + expect_length(civilytics_pal("main", n = 3), 3) + expect_length(civilytics_pal("sequential", n = 5), 5) +}) + +test_that("civilytics_pal interpolates when n exceeds palette size", { + result <- civilytics_pal("main", n = 10) + expect_length(result, 10) + expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", result))) +}) + +test_that("civilytics_pal errors on unknown palette name", { + expect_error(civilytics_pal("nope"), "'nope' is not a valid Civilytics palette") +}) + +test_that("civilytics_pal supports all three named palettes", { + expect_no_error(civilytics_pal("main")) + expect_no_error(civilytics_pal("sequential")) + expect_no_error(civilytics_pal("diverging")) +}) + +# --- scale_color/fill_civilytics --------------------------------------------- + +test_that("scale_color_civilytics returns a Scale object (discrete)", { + sc <- scale_color_civilytics() + expect_s3_class(sc, "Scale") +}) + +test_that("scale_color_civilytics returns a Scale object (continuous)", { + sc <- scale_color_civilytics("sequential", discrete = FALSE) + expect_s3_class(sc, "Scale") +}) + +test_that("scale_fill_civilytics returns a Scale object (discrete)", { + sc <- scale_fill_civilytics() + expect_s3_class(sc, "Scale") +}) + +test_that("scale_fill_civilytics returns a Scale object (continuous)", { + sc <- scale_fill_civilytics("sequential", discrete = FALSE) + expect_s3_class(sc, "Scale") +}) + +test_that("scale_color_civilytics works in a ggplot", { + p <- ggplot(mpg, aes(displ, hwy, colour = class)) + + geom_point() + + scale_color_civilytics() + expect_no_error(ggplot_build(p)) +}) + +test_that("scale_fill_civilytics works in a ggplot", { + p <- ggplot(mpg, aes(class, fill = class)) + + geom_bar() + + scale_fill_civilytics() + expect_no_error(ggplot_build(p)) +}) + +# --- theme_civilytics -------------------------------------------------------- + +test_that("theme_civilytics returns a complete ggplot2 theme", { + th <- theme_civilytics() + expect_s3_class(th, "theme") + expect_true(attr(th, "complete")) +}) + +test_that("theme_civilytics applies to a ggplot without error", { + p <- ggplot(mpg, aes(displ, hwy)) + + geom_point() + + theme_civilytics() + expect_no_error(ggplot_build(p)) +}) + +test_that("theme_civilytics uses brand ink color for text", { + th <- theme_civilytics() + expect_equal(th$text$colour, unname(civilytics_colors["ink"])) +}) + +test_that("theme_civilytics uses brand paper color for plot background", { + th <- theme_civilytics() + expect_equal(th$plot.background$fill, unname(civilytics_colors["paper"])) +}) + +test_that("theme_civilytics uses paper_2 for strip background by default", { + th <- theme_civilytics() + expect_equal(th$strip.background$fill, unname(civilytics_colors["paper_2"])) +}) + +test_that("theme_civilytics strip_color parameter is respected", { + th <- theme_civilytics(strip_color = "#FF0000") + expect_equal(th$strip.background$fill, "#FF0000") +}) + +test_that("theme_civilytics font_size parameter scales text elements", { + th_big <- theme_civilytics(font_size = 20) + th_sml <- theme_civilytics(font_size = 10) + expect_gt(th_big$text$size, th_sml$text$size) +}) + +# --- theme_civilytics_dark --------------------------------------------------- + +test_that("theme_civilytics_dark returns a complete ggplot2 theme", { + th <- theme_civilytics_dark() + expect_s3_class(th, "theme") + expect_true(attr(th, "complete")) +}) + +test_that("theme_civilytics_dark applies to a ggplot without error", { + p <- ggplot(mpg, aes(displ, hwy)) + + geom_point() + + theme_civilytics_dark() + expect_no_error(ggplot_build(p)) +}) + +test_that("theme_civilytics_dark uses paper color as ink (light text on dark bg)", { + th <- theme_civilytics_dark() + expect_equal(th$text$colour, unname(civilytics_colors["paper"])) +}) + +test_that("theme_civilytics_dark uses navy_dark as background", { + th <- theme_civilytics_dark() + expect_equal(th$plot.background$fill, unname(civilytics_colors["navy_dark"])) +}) + +test_that("theme_civilytics_dark uses navy for strip background", { + th <- theme_civilytics_dark() + expect_equal(th$strip.background$fill, unname(civilytics_colors["navy"])) +}) + +# --- make_logo_grob ---------------------------------------------------------- + +test_that("make_logo_grob returns a gg object for light variant", { + logo <- make_logo_grob("light") + expect_s3_class(logo, "gg") +}) + +test_that("make_logo_grob returns a gg object for dark variant", { + logo <- make_logo_grob("dark") + expect_s3_class(logo, "gg") +}) + +test_that("make_logo_grob defaults to light variant", { + expect_no_error(make_logo_grob()) +}) + +test_that("make_logo_grob errors on invalid variant", { + expect_error(make_logo_grob("purple")) +})