From e257180f158dcaaf4bf4664967e76ba25908601d Mon Sep 17 00:00:00 2001 From: Kodor Date: Sat, 1 Aug 2026 02:07:09 -0400 Subject: [PATCH] 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