From e257180f158dcaaf4bf4664967e76ba25908601d Mon Sep 17 00:00:00 2001 From: Kodor Date: Sat, 1 Aug 2026 02:07:09 -0400 Subject: [PATCH 1/3] fix: resolve locked namespace binding in civilytics_load_fonts() and default theme to transparent background - Replace .cv_fonts_loaded <<- TRUE with environment-based state (.cv_state) to avoid 'locked namespace binding' error (#18) - Add force=FALSE argument for idempotent font loading with guard check - Flip theme_civilytics() paper_bg default to FALSE (transparent) (#7) - Update roxygen docs and examples for both changes --- R/fonts.R | 35 ++++++++++++++++++++++++----------- R/theme.R | 18 ++++++++++-------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/R/fonts.R b/R/fonts.R index e73ee80..b0bd14f 100644 --- a/R/fonts.R +++ b/R/fonts.R @@ -5,32 +5,45 @@ CV_FONT_SANS <- "Inter" # axis text, legends, UI elements CV_FONT_SERIF <- "Source Serif 4" # body prose / editorial long-form CV_FONT_MONO <- "JetBrains Mono" # code, data tables, numeric callouts -# Internal flag so civilytics_load_fonts() is idempotent within a session. -.cv_fonts_loaded <- FALSE +# Mutable package state held in an environment so the binding itself stays +# locked (R locks all namespace bindings at load time) while the contents +# remain writable. See https://adv-r.hadley.nz/environments.html#environments-as-containers +.cv_state <- new.env(parent = emptyenv()) +.cv_state$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). +#' Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono 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`. +#' Subsequent calls within the same session are no-ops unless `force = TRUE`. +#' +#' @param force Logical. If `TRUE`, reload fonts even if they were already +#' loaded in this session. Default `FALSE`. +#' +#' @return Invisibly returns `TRUE` if fonts were loaded, `FALSE` if skipped +#' (already loaded and `force = FALSE`). #' @export #' #' @examples #' \dontrun{ #' civilytics_load_fonts() #' } -civilytics_load_fonts <- function() { +civilytics_load_fonts <- function(force = FALSE) { + if (.cv_state$fonts_loaded && !force) return(invisible(FALSE)) + sysfonts::font_add_google("Inter", family = "Inter") sysfonts::font_add_google("Libre Franklin", family = "Libre Franklin") sysfonts::font_add_google("Source Serif 4", family = "Source Serif 4") sysfonts::font_add_google("JetBrains Mono", family = "JetBrains Mono") showtext::showtext_auto() - .cv_fonts_loaded <<- TRUE - invisible(NULL) + + .cv_state$fonts_loaded <- TRUE + invisible(TRUE) } .onLoad <- function(libname, pkgname) { diff --git a/R/theme.R b/R/theme.R index 503e5ac..75a9af3 100644 --- a/R/theme.R +++ b/R/theme.R @@ -36,10 +36,12 @@ #' 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). +#' @param paper_bg Logical. If `TRUE`, fill the plot and panel backgrounds +#' with the warm `paper` color (Civilytics cream). Default is `FALSE` +#' (transparent) so that figures composite cleanly onto any background. +#' Set to `TRUE` for the branded cream canvas. Note: a transparent device +#' background (e.g., `dev = "ragg_png"`, `dev.args = list(background = +#' "transparent")`) is also needed for fully-transparent PNG exports. #' #' @section Font size hierarchy: #' All text sizes are derived from `font_size` using relative scale factors. @@ -68,7 +70,7 @@ #' \dontrun{ #' library(ggplot2) #' -#' # Default editorial theme +#' # Default — transparent background for embedding #' ggplot(mpg, aes(displ, hwy)) + #' geom_point() + #' theme_civilytics() @@ -79,10 +81,10 @@ #' scale_color_civilytics() + #' theme_civilytics(grid = "both") #' -#' # Transparent background for embedding +#' # Branded cream background (opt-in) #' ggplot(mpg, aes(displ, hwy)) + #' geom_point() + -#' theme_civilytics(paper_bg = FALSE) +#' theme_civilytics(paper_bg = TRUE) #' #' # Larger text for poster or display #' ggplot(mpg, aes(displ, hwy)) + @@ -102,7 +104,7 @@ theme_civilytics <- function( accent = unname(civilytics_colors["ember_600"]), strip_color = unname(civilytics_colors["paper_2"]), grid = c("y", "x", "both", "none"), - paper_bg = TRUE) { + paper_bg = FALSE) { grid <- match.arg(grid) half_line <- font_size / 2 From f32093c662aea56f23ba8d2605552f8c08d33345 Mon Sep 17 00:00:00 2001 From: Kodor Date: Sat, 1 Aug 2026 21:29:44 -0400 Subject: [PATCH 2/3] fix(test): update theme tests for paper_bg=FALSE default (#19) --- tests/testthat/test_theme.R | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test_theme.R b/tests/testthat/test_theme.R index 720f8a7..76e3fd0 100644 --- a/tests/testthat/test_theme.R +++ b/tests/testthat/test_theme.R @@ -133,8 +133,13 @@ test_that("theme_civilytics uses brand ink color for text", { expect_equal(th$text$colour, unname(civilytics_colors["ink"])) }) -test_that("theme_civilytics uses brand paper color for plot background", { +test_that("theme_civilytics has transparent background by default", { th <- theme_civilytics() + expect_true(is.na(th$plot.background$fill)) +}) + +test_that("theme_civilytics uses brand paper color when paper_bg = TRUE", { + th <- theme_civilytics(paper_bg = TRUE) expect_equal(th$plot.background$fill, unname(civilytics_colors["paper"])) }) From 967e7e1111eb4bc1ef8730b23ae8a6061822024d Mon Sep 17 00:00:00 2001 From: Kodor Date: Sat, 1 Aug 2026 21:44:50 -0400 Subject: [PATCH 3/3] docs: regenerate roxygen documentation for paper_bg and force params (#19) --- man/civilytics_load_fonts.Rd | 22 +++++++++++++++------- man/theme_civilytics.Rd | 18 ++++++++++-------- man/theme_civilytics_dark.Rd | 8 ++++---- man/theme_civilytics_dark_map.Rd | 8 ++++---- man/theme_civilytics_map.Rd | 8 ++++---- man/theme_civilytics_slide.Rd | 8 ++++---- man/theme_civilytics_slide_map.Rd | 8 ++++---- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/man/civilytics_load_fonts.Rd b/man/civilytics_load_fonts.Rd index 6447688..fda32e0 100644 --- a/man/civilytics_load_fonts.Rd +++ b/man/civilytics_load_fonts.Rd @@ -4,17 +4,25 @@ \alias{civilytics_load_fonts} \title{Load Civilytics brand fonts} \usage{ -civilytics_load_fonts() +civilytics_load_fonts(force = FALSE) +} +\arguments{ +\item{force}{Logical. If \code{TRUE}, reload fonts even if they were already +loaded in this session. Default \code{FALSE}.} } \value{ -Invisibly returns `NULL`. +Invisibly returns \code{TRUE} if fonts were loaded, \code{FALSE} if skipped +(already loaded and \code{force = FALSE}). } \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). +Downloads Inter, Libre Franklin, Source Serif 4 and JetBrains Mono from +Google Fonts via \code{sysfonts::font_add_google()}, then calls +\code{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). + +Subsequent calls within the same session are no-ops unless \code{force = TRUE}. } \examples{ \dontrun{ diff --git a/man/theme_civilytics.Rd b/man/theme_civilytics.Rd index c7dfeb4..fc2c3e8 100644 --- a/man/theme_civilytics.Rd +++ b/man/theme_civilytics.Rd @@ -17,7 +17,7 @@ theme_civilytics( accent = unname(civilytics_colors["ember_600"]), strip_color = unname(civilytics_colors["paper_2"]), grid = c("y", "x", "both", "none"), - paper_bg = TRUE + paper_bg = FALSE ) } \arguments{ @@ -56,10 +56,12 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} \item{grid}{Character. Which major gridlines to draw: `"y"` (default, horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} -\item{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).} +\item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds +with the warm `paper` color (Civilytics cream). Default is `FALSE` +(transparent) so that figures composite cleanly onto any background. +Set to `TRUE` for the branded cream canvas. Note: a transparent device +background (e.g., `dev = "ragg_png"`, `dev.args = list(background = +"transparent")`) is also needed for fully-transparent PNG exports.} } \value{ A complete ggplot2 [ggplot2::theme()] object. @@ -105,7 +107,7 @@ shrinkage. \dontrun{ library(ggplot2) -# Default editorial theme +# Default — transparent background for embedding ggplot(mpg, aes(displ, hwy)) + geom_point() + theme_civilytics() @@ -116,10 +118,10 @@ ggplot(mpg, aes(displ, hwy, colour = class)) + scale_color_civilytics() + theme_civilytics(grid = "both") -# Transparent background for embedding +# Branded cream background (opt-in) ggplot(mpg, aes(displ, hwy)) + geom_point() + - theme_civilytics(paper_bg = FALSE) + theme_civilytics(paper_bg = TRUE) # Larger text for poster or display ggplot(mpg, aes(displ, hwy)) + diff --git a/man/theme_civilytics_dark.Rd b/man/theme_civilytics_dark.Rd index 97dd6b8..23ac2c8 100644 --- a/man/theme_civilytics_dark.Rd +++ b/man/theme_civilytics_dark.Rd @@ -56,10 +56,10 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} \item{grid}{Character. Which major gridlines to draw: `"y"` (default, horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} -\item{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).} +\item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds +with the warm `paper` color (Civilytics cream). Default is `FALSE` +(transparent) so that figures composite cleanly onto any background. +Set to `TRUE` for the branded cream canvas.} } \value{ A complete ggplot2 [ggplot2::theme()] object. diff --git a/man/theme_civilytics_dark_map.Rd b/man/theme_civilytics_dark_map.Rd index 4d5999b..377d17c 100644 --- a/man/theme_civilytics_dark_map.Rd +++ b/man/theme_civilytics_dark_map.Rd @@ -52,10 +52,10 @@ design system.} \item{strip_color}{Character. Hex code for facet strip background. Defaults to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} -\item{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).} +\item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds +with the warm `paper` color (Civilytics cream). Default is `FALSE` +(transparent) so that figures composite cleanly onto any background. +Set to `TRUE` for the branded cream canvas.} } \value{ A complete ggplot2 [ggplot2::theme()] object. diff --git a/man/theme_civilytics_map.Rd b/man/theme_civilytics_map.Rd index 83d9c01..5ab7c2c 100644 --- a/man/theme_civilytics_map.Rd +++ b/man/theme_civilytics_map.Rd @@ -52,10 +52,10 @@ design system.} \item{strip_color}{Character. Hex code for facet strip background. Defaults to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} -\item{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).} +\item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds +with the warm `paper` color (Civilytics cream). Default is `FALSE` +(transparent) so that figures composite cleanly onto any background. +Set to `TRUE` for the branded cream canvas.} } \value{ A complete ggplot2 [ggplot2::theme()] object. diff --git a/man/theme_civilytics_slide.Rd b/man/theme_civilytics_slide.Rd index 4cff8c9..f853ba2 100644 --- a/man/theme_civilytics_slide.Rd +++ b/man/theme_civilytics_slide.Rd @@ -56,10 +56,10 @@ to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} \item{grid}{Character. Which major gridlines to draw: `"y"` (default, horizontal only), `"x"` (vertical only), `"both"`, or `"none"`.} -\item{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).} +\item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds +with the warm `paper` color (Civilytics cream). Default is `FALSE` +(transparent) so that figures composite cleanly onto any background. +Set to `TRUE` for the branded cream canvas.} } \value{ A complete ggplot2 [ggplot2::theme()] object. diff --git a/man/theme_civilytics_slide_map.Rd b/man/theme_civilytics_slide_map.Rd index 9d8bc45..a68d8d1 100644 --- a/man/theme_civilytics_slide_map.Rd +++ b/man/theme_civilytics_slide_map.Rd @@ -52,10 +52,10 @@ design system.} \item{strip_color}{Character. Hex code for facet strip background. Defaults to [civilytics_colors]`["paper_2"]` (`#F2EDE4`).} -\item{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).} +\item{paper_bg}{Logical. If `TRUE`, fill the plot and panel backgrounds +with the warm `paper` color (Civilytics cream). Default is `FALSE` +(transparent) so that figures composite cleanly onto any background. +Set to `TRUE` for the branded cream canvas.} } \value{ A complete ggplot2 [ggplot2::theme()] object.